Modalità registrazione: nasconde a schermo chiavi, dati personali, importi e clienti mentre lavori in Claude Code, e tiene chiusi i file privati. /rec la…

Un marketplace di mod per Claude Code, pensato per capire e controllare una sessione: barra della cache e dei consumi, registro di ciò che viene scritto fuori dalla repo, conferma delle proposte, raccolta degli inciampi, modalità rec per le registrazioni e inbox con bozze, etichette e task. Ogni mod è un plugin indipendente, installabile singolarmente con un comando.
Ogni mod si accende e si spegne con un comando /..., che funziona in terminale, nell'app Desktop e da iPad. Lo stato si ricorda tra una sessione e l'altra.
⏳ barra-cache (/barra) — tre righe sopra il prompt. La prima è la memoria della chat: una barra con i token del contesto occupato e quanto è costata la sessione a listino (verde fino al 60%, gialla fino all'80%, rossa oltre, con il suggerimento Nuova chat). La seconda mostra l'*uso delle 5 ore* e l'*uso della settimana* dell'abbonamento, solo dove il motore riporta le cifre. La terza dice da quanto è aperta la sessione e quanto resta di cache calda: la prompt cache dura un'ora dall'ultima richiesta della conversazione principale (le richieste dei subagent non la rinnovano), poi la richiesta successiva riscrive tutto il contesto a prezzo pieno. Con la cache scaduta e almeno 20mila token di contesto compare un avviso rosso. Sotto, una riga con branch, file modificati e commit da pubblicare (letta da git, solo dove $.process esiste: nel cloud non compare) e i bottoni: Commit e push (tasto g), Push (tasto p, compare solo quando ci sono commit da pubblicare e nessuna modifica da committare, senza token) e Handoff (tasto n), che fa scrivere al modello un riassunto di ripartenza in ~/.claude/handoffs/, svuota la chat con /clear e riparte da quel file. Dove la banda non viene disegnata (ad esempio l'app su iPad) restano /cache, che mostra minuti rimasti, contesto, costo dell'ultimo turno e quota di cache letta, /push e /handoff (alias /nuova). I comandi sono anche file statici in barra-cache/commands/, perché l'elenco dello slash dell'app non mostra quelli registrati a runtime; l'hook risponde prima del modello, quindi non costano token.
📒 scritture-esterne (/scritture) — a fine turno elenca tutto ciò che il turno ha scritto fuori dalla repo: Notion, Postpickr, Spreaker, Gmail, Calendar, Drive, git push, gh, curl -X POST e gli script con --applica o --elimina. Ogni riga ha servizio, azione, bersaglio, un link apri se la risposta ne contiene uno, e una croce rossa se la chiamata è fallita o negata. Le letture non compaiono. Il registro sparisce all'inizio del turno successivo. Riconosce dal nome i connettori più comuni; per quelli con id opaco serve l'opzione servizi del plugin.
✅ conferma-proposte (/conferma) — quando l'ultima risposta di Claude chiude con una proposta o una domanda di conferma (una domanda, oppure formule come "procedo", "vuoi che", "confermi"), mostra sopra il prompt il passaggio che propone e due bottoni: Sì, procedi (tasto s) e No, fermati (tasto x). Il bottone invia la risposta come se l'avessi scritta tu. Non usa il modello, quindi non consuma token. Funziona anche nel cloud e da iPad.
🛠 correggi (/correggi) — durante la sessione annota gli inciampi: tool che falliscono, azioni negate dal sistema di permessi e le tue correzioni ("non vedo", "hai sbagliato", "riprova"). Con almeno due inciampi compare una riga con il conteggio, le skill usate e il bottone Proponi correzione (tasto l), che chiede a Claude di individuare la causa e proporre la modifica esatta alla skill o al file di istruzioni, senza applicarla. /correggi elenco mostra gli inciampi raccolti. Consuma token solo quando premi il bottone.
🔴 rec (/rec) — la modalità per registrare un video o lavorare in una sessione live con ospiti. Maschera a schermo chiavi e valori dei .env, email, nomi, telefoni, indirizzi, codice fiscale, partita IVA, IBAN, importi in euro e cifre vicino a parole come fatturato, margine, compenso, preventivo. I risultati di posta, chat, task, file, calendario, Notion e strumenti di pagamento si disegnano nascosti. Claude continua a lavorare sui dati reali: cambia solo ciò che si vede (se modifica un file da 29 a 39 euro, il file cambia davvero, lo schermo no). Tiene chiusi i file privati (.env, credenziali, fatture, contratti, preventivi, buste paga) e gli strumenti di pagamento, e ogni prompt porta una nota nascosta che chiede a Claude di usare segnaposto al posto di nomi e cifre. Un ● REC rosso sopra il prompt e nel piè di pagina ricorda che è acceso. /rec alterna, /rec rigoroso maschera anche ogni cifra grande, /rec off spegne, /rec config crea ~/.claude/mods-data/rec/config.json per il tuo nome (nomiVisibili), le persone da nascondere (nomiNascosti), le cartelle private (percorsiPrivati) e gli strumenti extra (strumentiAffari, strumentiChiusi). Limiti: cambia ciò che è disegnato, non ciò che è memorizzato; il riconoscimento per pattern non prende tutto quello che è scritto a parole, quindi riguarda il girato prima di pubblicarlo; i titoli delle chat nella barra laterale dell'app Desktop non si possono mascherare. Adattata da recording-mode di Nate Herk (MIT, vedi rec/NOTICE.md).
📬 posta (posta o /posta) — apre un pannello con la inbox Gmail: l'elenco delle ultime 12 mail per account (una finestra di 5 righe che scorre con j e k), i bottoni e, sotto, il testo intero della mail selezionata (tutti i messaggi del thread, dal più recente, con allegati), letto da Gmail senza modello. I bottoni sono Bozza (b), Label (l), Bozza+Label (m), Task (t), più Aggiorna (r) e Chiudi (x). Le quattro azioni girano dentro la mod, senza subagent: il modello riceve il thread senza alcun tool e sceglie solo i contenuti, mentre le scritture su Gmail e Notion le fa il codice dopo averle validate (la chat resta pulita: l'esito compare nel pannello e in un avviso). Bozza scrive con le regole di ~/.claude/mods-data/posta/sistematore.md e crea una bozza di risposta (la mail non viene mai inviata). Label fa scegliere al modello economico un'etichetta tra quelle esistenti e la applica solo se il nome coincide (non ne crea). Task fa proporre al modello economico titolo, scadenza, urgenza, importanza, impegno e contesto, li riporta ai valori ammessi (scadenza non valida o passata: domani alle 09:00) e crea la pagina nel database Tasks di Notion con il link alla mail. Solo Label e Bozza+Label tolgono la mail dall'inbox (finisce nella cartella dell'etichetta); Bozza e Task la lasciano in inbox. I lavori in corso mostrano uno spinner, quelli riusciti spariscono dopo 8 secondi e quelli falliti si rilanciano con Riprova (y). Nel dettaglio compare l'ultimo messaggio del thread; i precedenti sono righe ripiegate e si aprono con Mostra precedenti (p). Scrivendo posta senza barra il pannello si apre senza avviso dell'app e senza token; posta off lo chiude. Gli account sono in DEFAULT_ACCOUNTS e si sostituiscono con la chiave accounts dello store. In modalità Auto anche le chiamate della mod passano dal classificatore, che sulle scritture non dà verdetto: vanno autorizzate in permissions.allow le letture Gmail (search_threads, get_thread, list_labels) e le quattro scritture (create_draft, label_thread, unlabel_thread di Gmail, notion-create-pages di Notion), con il prefisso mcp__<id connettore>__. Un errore resta visibile solo sulla mail a cui appartiene e sparisce quando la stessa azione riesce.
claude --version.Una mod è codice eseguito con i tuoi permessi: può leggere e scrivere file, avviare processi e vedere prompt e tool call della sessione. Il sorgente di ogni mod è in hooks/: leggilo prima dell'installazione.
claude plugin marketplace add andreabrugnoli/mods
claude plugin install barra-cache@andrea-mods
claude plugin install scritture-esterne@andrea-mods
claude plugin install conferma-proposte@andrea-mods
claude plugin install correggi@andrea-mods
claude plugin install rec@andrea-mods
claude plugin install posta@andrea-mods
claude.Verifica: apri /plugin e controlla che le mod risultino attive. Per provare una mod in una sola sessione senza installarla, clona la repo e avvia claude --plugin-dir ./mods/barra-cache. Per disattivarla, usa il tab Installed di /plugin.
Le sessioni cloud (claude.ai/code, app per iPad) non leggono le tue impostazioni locali: partono da un contenitore pulito e caricano i plugin dichiarati nella repo su cui lavori. Per attivare una mod in una repo, aggiungi a .claude/settings.json di quella repo:
{
"extraKnownMarketplaces": {
"andrea-mods": { "source": { "source": "github", "repo": "andreabrugnoli/mods" } }
},
"enabledPlugins": { "barra-cache@andrea-mods": true, "rec@andrea-mods": true }
}
Lo fa per te scripts/abilita-cloud.sh <percorso-repo> [mod ...], che unisce la voce alle impostazioni esistenti. Se il tuo gitignore (anche globale) esclude .claude/settings.json, aggiungilo con git add -f .claude/settings.json, poi commit e push. La sessione cloud legge il file dal branch.
Su iPad la banda sopra il prompt può non essere disegnata: restano /cache, /push e /nuova, e tutti i comandi on/off (/barra, /scritture, /conferma, /correggi, /rec) funzionano uguale. Lo stato ($.store) e i riassunti in ~/.claude/handoffs/ vivono nel contenitore della sessione e si perdono alla sua chiusura.
Accendere e spegnere (senza argomento alternano; accettano anche on e off):
/barra: la barra sopra il prompt./scritture: il registro delle scritture esterne./conferma: i bottoni Sì e No sulle proposte./correggi: la raccolta degli inciampi (/correggi elenco li mostra)./rec: la modalità registrazione (/rec rigoroso, /rec off, /rec config).posta o /posta: la inbox con i bottoni Bozza, Label e Task (posta aggiorna, posta off).Altri comandi di barra-cache:
/cache: minuti di cache rimasti, contesto occupato, costo dell'ultimo turno e quota di cache letta./push: pubblica i commit del branch corrente./nuova: riassume la sessione in un file e riparte da una chat pulita.Durata della cache in barra-cache: parte da un'ora (TTL_MS) e si corregge da sola. Dopo una pausa di almeno 6 minuti guarda la quota di cache letta: se la cache è stata riscritta, passa a 5 minuti e lo ricorda tra le sessioni; se è stata letta, conferma l'ora.
Soglie dei colori in barra-cache: CONTEXT_WARN, CONTEXT_BAD, COST_WARN, CACHE_OK e CACHE_BAD, in cima allo stesso file.
Nomi dei connettori in scritture-esterne: opzione servizi del plugin.
Per modificare una mod, clona la repo e caricala con claude --plugin-dir: ogni salvataggio ricarica il modulo nella sessione aperta.
mods/
├── .claude-plugin/marketplace.json # elenco delle mod installabili
├── barra-cache/
├── scritture-esterne/
├── conferma-proposte/
├── correggi/
├── rec/
├── posta/
└── scripts/abilita-cloud.sh
Ogni cartella di mod è un plugin completo: .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.js e tests/ (eseguibili con claude plugin test ./<mod>). claude plugin validate ./<mod> elenca gli eventi intercettati e le chiamate al motore.
Licenza: MIT.
hooks/register.js 283 lines1// rec: /rec prima di registrare un video o lavorare in una sessione live con ospiti.
2// Adattato da recording-mode di Nate Herk (MIT, vedi NOTICE.md).
3// Finché è acceso, in ogni sessione di Claude Code su questa macchina:
4// - chiavi, valori dei .env, email, nomi, telefoni, indirizzi, codice fiscale, partita IVA, IBAN,
5// importi e cifre d'affari vengono mascherati dove la chat disegna testo
6// - i risultati di posta, chat, task, file, calendario e strumenti di pagamento si disegnano nascosti
7// - Claude non può aprire i file privati (.env, credenziali, memoria di Claude, fatture, contratti,
8// preventivi e quanto elenchi nel file di configurazione) né gli strumenti di pagamento
9// - ogni prompt porta una nota nascosta che chiede a Claude di tenere nomi e cifre fuori dalle risposte
10// - un ● REC rosso sopra il prompt e nel piè di pagina ricorda che è acceso
11// Claude lavora sempre sui dati reali: cambia solo ciò che si vede a schermo.
12// /rec rigoroso maschera anche ogni cifra grande. /rec off spegne tutto.
13// /rec config crea ~/.claude/mods-data/rec/config.json per il tuo nome, le persone da nascondere,
14// le cartelle private e gli strumenti extra.
15
16import { makeMasker, deepMask, hideText, privatePathHit, toolCallTargets, secretValuesFromEnv, businessSource, financeTool } from './privacy.js'
17
18const FLAG_PATH = '/.claude/mods-data/rec/stato.json'
19const CONFIG_PATH = '/.claude/mods-data/rec/config.json'
20
21const CONFIG_TEMPLATE = {
22 nomiVisibili: ['Andrea Brugnoli', 'abcomunica'],
23 nomiNascosti: ['Un cliente', 'Un collaboratore'],
24 percorsiPrivati: ['clienti/', 'trattative/', 'appunti-privati.md'],
25 strumentiAffari: [],
26 strumentiChiusi: [],
27}
28
29const CONFIG_HELP = [
30 'nomiVisibili: il tuo nome e il tuo marchio, mostrati anche durante la registrazione.',
31 'nomiNascosti: persone sempre mascherate (clienti, collaboratori). I nomi nelle email e nei campi contatto si imparano da soli.',
32 'percorsiPrivati: cartelle o file che Claude non può aprire durante la registrazione. Vale qualsiasi parte del percorso, maiuscole comprese.',
33 'strumentiAffari: nomi di tool o comandi (anche solo una parte) i cui risultati si disegnano nascosti, oltre all\'elenco già previsto.',
34 'strumentiChiusi: nomi di tool che Claude non può chiamare durante la registrazione, oltre agli strumenti di pagamento già previsti.',
35]
36
37// Un punto rosso che pulsa e la scritta REC. Si disegna come immagine semplice: un Svg interattivo
38// finisce in un iframe che nel tema scuro dipinge un riquadro bianco e si ricarica a ogni ridisegno.
39const REC_RED = '#e5484d'
40const REC_SVG =
41 '<svg xmlns="http://www.w3.org/2000/svg" width="54" height="18" viewBox="0 0 54 18">' +
42 `<circle cx="8" cy="9" r="5" fill="${REC_RED}"><animate attributeName="opacity" values="1;0.35;1" dur="1.6s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1"/></circle>` +
43 `<text x="19" y="13.2" font-family="-apple-system,'Segoe UI',system-ui,sans-serif" font-size="12" font-weight="700" letter-spacing="0.8" fill="${REC_RED}">REC</text>` +
44 '</svg>'
45
46let home = ''
47let cwd = ''
48let rec = { on: false, strict: false, since: 0 }
49// I valori dei .env: restano solo in memoria, non vengono scritti da nessuna parte
50let values = []
51let config = { nomiVisibili: [], nomiNascosti: [], percorsiPrivati: [], strumentiAffari: [], strumentiChiusi: [] }
52let mask = (s) => s
53let blocked = 0
54let noteSent = false
55// Gli id delle chiamate i cui risultati sono dati d'affari
56const businessCalls = new Set()
57
58function strings(v) {
59 return Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x.trim()).map((x) => x.trim()).slice(0, 500) : []
60}
61
62// La nota che Claude legge accanto a ogni prompt mentre si registra
63export function noteOn(keepNames = []) {
64 const keep = keepNames.length ? ` (a parte ${keepNames.join(', ')})` : ''
65 return (
66 `Modalità rec attiva: lo schermo è in registrazione per un video pubblico o una sessione live con ospiti. Nelle risposte e nei comandi lascia fuori nomi di persone${keep}, email, telefoni, indirizzi, numeri di conto, importi, fatturato, utili, prezzi, compensi e altre cifre d'affari, e piani interni, strategie, trattative e questioni di clienti. ` +
67 'Scrivi segnaposto come [nome], [importo] o [piano interno] e tieni i riassunti generici. I file privati e gli strumenti di pagamento restano chiusi finché la registrazione non finisce.'
68 )
69}
70const NOTE_OFF = 'La modalità rec è finita. La nota precedente sulla registrazione non vale più.'
71
72function apply() {
73 mask = rec.on ? makeMasker({ strict: rec.strict, values, names: config.nomiNascosti, keepNames: config.nomiVisibili }) : (s) => s
74}
75
76async function load($) {
77 await loadEnvValues($)
78 await loadConfig($)
79}
80
81function unload() {
82 values = []
83 businessCalls.clear()
84}
85
86async function loadConfig($) {
87 try {
88 const path = home + CONFIG_PATH
89 if (!(await $.fs.exists(path))) return
90 const raw = JSON.parse(await $.fs.read(path))
91 config = {
92 nomiVisibili: strings(raw.nomiVisibili),
93 nomiNascosti: strings(raw.nomiNascosti),
94 percorsiPrivati: strings(raw.percorsiPrivati),
95 strumentiAffari: strings(raw.strumentiAffari),
96 strumentiChiusi: strings(raw.strumentiChiusi),
97 }
98 } catch {
99 // file di configurazione rotto: valgono le regole già previste
100 }
101}
102
103function parentDirs(path, levels) {
104 const parts = String(path || '').replace(/\\/g, '/').split('/')
105 const out = []
106 for (let i = parts.length; i > 0 && out.length <= levels; i--) out.push(parts.slice(0, i).join('/'))
107 return out.filter(Boolean)
108}
109
110async function loadEnvValues($) {
111 const found = []
112 for (const dir of parentDirs(cwd, 3)) {
113 for (const name of ['.env', '.env.local']) {
114 const p = dir + '/' + name
115 try {
116 if (await $.fs.exists(p)) found.push(...secretValuesFromEnv(await $.fs.read(p)))
117 } catch {
118 // illeggibile: i pattern coprono comunque i formati di chiave più comuni
119 }
120 }
121 }
122 values = [...new Set(found)].sort((a, b) => b.length - a.length)
123}
124
125async function readFlag($) {
126 try {
127 const path = home + FLAG_PATH
128 if (!(await $.fs.exists(path))) return { on: false, strict: false, since: 0 }
129 const flag = JSON.parse(await $.fs.read(path))
130 return { on: !!flag.on, strict: !!flag.strict, since: flag.since || 0 }
131 } catch {
132 return { on: false, strict: false, since: 0 }
133 }
134}
135
136// Un'altra sessione può aver acceso o spento la registrazione
137async function syncFlag($) {
138 const flag = await readFlag($)
139 if (flag.on === rec.on && flag.strict === rec.strict) return
140 rec = flag
141 if (rec.on) await load($)
142 else unload()
143 apply()
144 $.ui.invalidate('ui.render')
145}
146
147async function setFlag($, on, strict) {
148 rec = { on, strict: on && strict, since: on ? await $.clock.now() : 0 }
149 try {
150 await $.fs.write(home + FLAG_PATH, JSON.stringify(rec))
151 } catch {
152 // dove il file non si scrive la modalità vale solo per questa sessione
153 }
154 if (rec.on) await load($)
155 else unload()
156 apply()
157 $.ui.invalidate('ui.render')
158}
159
160// /rec config: crea il file la prima volta, poi dice dov'è e cosa contiene
161async function configText($) {
162 const path = home + CONFIG_PATH
163 let made = false
164 try {
165 if (!(await $.fs.exists(path))) {
166 await $.fs.write(path, JSON.stringify(CONFIG_TEMPLATE, null, 2) + '\n')
167 made = true
168 }
169 } catch {
170 return `Non riesco a scrivere ${path}. Crealo a mano con questa forma:\n${JSON.stringify(CONFIG_TEMPLATE, null, 2)}`
171 }
172 await loadConfig($)
173 apply()
174 return [
175 `${made ? 'Creato' : 'Il tuo'} file di configurazione di rec: ${path}`,
176 made
177 ? 'Contiene valori d\'esempio. Sostituiscili con i tuoi, salva e lancia di nuovo /rec.'
178 : `Caricati: ${config.nomiVisibili.length} nomi visibili, ${config.nomiNascosti.length} nomi nascosti, ${config.percorsiPrivati.length} percorsi privati.`,
179 ...CONFIG_HELP,
180 ].join('\n')
181}
182
183// /rec alterna, /rec rigoroso accende anche la maschera sulle cifre grandi, /rec off spegne, /rec config crea la configurazione
184async function runRec($, e) {
185 const arg = String(e.args || '').trim().toLowerCase()
186 if (arg === 'config') return { text: await configText($) }
187 if (arg === 'off' || arg === 'spento' || (arg === '' && rec.on)) {
188 await setFlag($, false, false)
189 $.ui.toast('rec spento.' + (blocked ? ` Ha tenuto chiusi ${blocked} file privati.` : ''))
190 blocked = 0
191 return { text: 'rec · spento' }
192 }
193 const strict = arg === 'rigoroso' || arg === 'strict'
194 await setFlag($, true, strict)
195 $.ui.toast(`rec acceso${strict ? ' (rigoroso)' : ''}.`, { timeoutMs: 3000 })
196 return { text: 'rec · acceso' + (strict ? ' (rigoroso)' : '') }
197}
198
199export function register(on) {
200 on('session.start', async ($, e, next) => {
201 home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE')) || ''
202 cwd = await $.session.cwd()
203 await $.command.register({
204 name: 'rec',
205 description: 'Accende o spegne la modalità rec: nasconde chiavi, dati personali, importi e file privati a schermo',
206 argumentHint: '[rigoroso|off|config]',
207 immediate: true,
208 })
209 await loadConfig($)
210 await syncFlag($)
211 $.clock.every(3000, () => syncFlag($).catch(() => {}))
212 return next(e)
213 })
214
215 on('command.run', { command: 'rec' }, runRec)
216 // Il comando statico del plugin si chiama anche rec:registra
217 on('command.run', { command: 'rec:registra' }, runRec)
218
219 // Claude legge una nota accanto a ogni prompt mentre si registra, e una in più quando finisce
220 on('prompt.submit', async ($, e, next) => {
221 const note = rec.on ? noteOn(config.nomiVisibili) : noteSent ? NOTE_OFF : null
222 if (!note) return next(e)
223 noteSent = rec.on
224 return next({ ...e, context: [...(e.context ?? []), note] })
225 })
226
227 // File privati e strumenti di pagamento restano chiusi mentre si registra
228 on('tool.call', async ($, e, next) => {
229 if (!rec.on) return next(e)
230 const hit = financeTool(e.tool, config.strumentiChiusi) ? 'strumenti di pagamento' : privatePathHit(toolCallTargets(e).join('\n'), config.percorsiPrivati)
231 if (!hit) {
232 if (businessSource(e.tool, e, config.strumentiAffari)) businessCalls.add(e.tool_use_id)
233 return next(e)
234 }
235 blocked += 1
236 return {
237 deny: `La modalità rec è attiva, quindi "${hit}" resta chiuso finché lo schermo è in registrazione. Continua senza, oppure chiedi all'utente di lanciare prima /rec off.`,
238 }
239 })
240
241 // Righe di testo: prompt, risposte, output dei comandi
242 on('ui.render', { component: ['UserMessage', 'AssistantMessage', 'CommandOutput'] }, async ($, e, next) => {
243 if (!rec.on || typeof e.props.text !== 'string') return next(e)
244 return next({ ...e, props: { ...e.props, text: mask(e.props.text) } })
245 })
246
247 // Righe dei tool: l'input si maschera, il risultato di un tool d'affari si nasconde per intero
248 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
249 if (!rec.on) return next(e)
250 const business = businessSource(e.props.tool, e.props.input, config.strumentiAffari) || businessCalls.has(e.props.tool_use_id)
251 if (business) businessCalls.add(e.props.tool_use_id)
252 const props = { ...e.props, input: deepMask(e.props.input, mask) }
253 if (e.props.output !== undefined) props.output = deepMask(e.props.output, business ? hideText : mask)
254 return next({ ...e, props })
255 })
256
257 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
258 if (!rec.on) return next(e)
259 const business = businessSource(e.props.tool, null, config.strumentiAffari) || businessCalls.has(e.props.tool_use_id)
260 return next({ ...e, props: { ...e.props, output: deepMask(e.props.output, business ? hideText : mask) } })
261 })
262
263 // Solo l'indicatore: un punto rosso che pulsa e REC
264 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
265 const below = await next(e)
266 if (e.props && e.props.hasSurvey) return below
267 if (!rec.on) return below
268 const el = $.ui.resolve(e)
269 const line =
270 e.surface === 'terminal' || !el.Svg
271 ? el.Text({ color: 'red', bold: true, children: ['● REC'] })
272 : el.Svg({ source: REC_SVG, alt: 'REC', width: 54, height: 18 })
273 return el.Box({ flexDirection: 'column', children: below ? [line, below] : [line] })
274 })
275
276 // L'etichetta nel piè di pagina si vede anche con la banda chiusa
277 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
278 if (!rec.on) return next(e)
279 const modes = Array.isArray(e.props && e.props.modes) ? e.props.modes : []
280 return next({ ...e, props: { ...e.props, modes: ['● REC', ...modes] } })
281 })
282}
283hooks/privacy.js 392 lines1// Maschera chiavi, dati personali e cifre d'affari per le registrazioni a schermo.
2// Funzioni pure, nessuna chiamata all'API delle mod. Adattato da recording-mode di Nate Herk (MIT):
3// pattern italiani per importi, codice fiscale, partita IVA, telefoni, indirizzi e parole del fatturato.
4
5const DOTS = '••••••••'
6const HIDE = '•••'
7const HIDDEN_OUTPUT = '••• nascosto durante la registrazione'
8
9const SECRET_PATTERNS = [
10 /sk-ant-[A-Za-z0-9_-]{16,}/g,
11 /\bsk-(?:proj-|live-|test-|svcacct-)?[A-Za-z0-9_-]{20,}/g,
12 /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
13 /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
14 /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
15 /\bAIza[0-9A-Za-z_-]{30,}/g,
16 /\bAKIA[0-9A-Z]{16}\b/g,
17 /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
18 /\b(?:hf|r8|pk_live|sk_live|rk_live|whsec|pat|key)_[A-Za-z0-9]{20,}/g,
19 /\bBearer\s+[A-Za-z0-9._~+/-]{20,}=*/gi,
20]
21
22// NOME=valore oppure "nome": "valore" dove il nome sembra una credenziale
23const ASSIGNMENT =
24 /\b([A-Za-z0-9_.-]*(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|CLIENT[_-]?SECRET|AUTH)[A-Za-z0-9_.-]*)(["']?\s*[=:]\s*["']?)([^\s"'`,;}{]{6,})/gi
25
26const EMAIL_SRC = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}'
27const EMAIL = new RegExp(`\\b${EMAIL_SRC}\\b`, 'g')
28
29// Persone: "Mario Rossi <mario@x.it>", "displayName": "Mario Rossi", righe Da:/A:/Cliente:
30const NAME_WORD = "[A-Z][\\p{L}'’.-]*"
31const DISPLAY_NAME = new RegExp(`(["']?)(${NAME_WORD}(?:[ \\t]+${NAME_WORD}){0,3})\\1([ \\t]*<[ \\t]*${EMAIL_SRC}[ \\t]*>)`, 'gu')
32const PERSON_KEY =
33 /("(?:displayName|display_name|fullName|full_name|firstName|first_name|lastName|last_name|givenName|given_name|familyName|family_name|real_name|realName|senderName|sender_name|authorName|author_name|username|user_name|cognome|referente|cliente)"\s*:\s*")([^"]{1,80})(")/g
34const PERSON_LINE =
35 /^([ \t>*-]*(?:From|To|Cc|Bcc|Reply-To|Attendees?|Organizer|Invitees?|Guests?|Assignees?|Sender|Owner|Da|A|Ccn|Rispondi a|Partecipanti|Organizzatore|Invitati|Assegnatari?|Mittente|Destinatario|Cliente|Referente|Contatto)[ \t]*:[ \t]*)(\S.*)$/gm
36
37// Dati personali
38const PHONE = /(?<![\w.])(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}(?![\w.])/g
39const PHONE_INTL = /(?<![\w.])\+\d{1,3}[\s.-]?\(?\d{1,4}\)?(?:[\s.-]?\d{2,4}){2,4}(?![\w.])/g
40// Cellulari italiani senza prefisso: 347 123 4567, 347.1234567 (solo con separatore, per non toccare numeri qualunque)
41const PHONE_IT_MOBILE = /(?<![\w.])3\d{2}[\s.-]\d{3}[\s.-]?\d{3,4}(?![\w.])/g
42// Fissi italiani: 045 1234567, 02-123456
43const PHONE_IT_LAND = /(?<![\w.])0\d{1,3}[\s.-]\d{5,8}(?![\w.])/g
44const SSN = /(?<![\w-])\d{3}-\d{2}-\d{4}(?![\w-])/g
45const EIN = /(?<![\w-])\d{2}-\d{7}(?![\w-])/g
46const CARD = /(?<![\w-])(?:4\d{3}|5[1-5]\d{2}|2[2-7]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))(?:[ -]?\d{2,4}){3,4}(?![\w-])/g
47const IBAN = /\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]{4}){3,7}(?: ?[A-Z0-9]{1,3})?\b/g
48const ACCOUNT =
49 /\b((?:routing|account|acct|a\/c|aba|swift|bic|iban|passport|driver'?s license|conto(?: corrente)?|c\/c|passaporto|patente|carta d'identit[àa])(?:\s*(?:number|no\.?|num|n\.?|numero|#))?\s*[:#]?\s*)(\d[\d -]{3,}\d)/gi
50const CARD_ENDING = /\b((?:ending(?:\s+in)?|last\s+(?:4|four)(?:\s+digits)?|termina(?:nte)?\s+(?:con|in)|ultime\s+4(?:\s+cifre)?)\s*[:#]?\s*)(\d{4})\b/gi
51// Codice fiscale e partita IVA
52const CODICE_FISCALE = /\b[A-Za-z]{6}\d{2}[A-EHLMPR-Ta-ehlmpr-t]\d{2}[A-Za-z]\d{3}[A-Za-z]\b/g
53const PARTITA_IVA = /\b((?:P\.?\s?IVA|partita\s+iva|VAT)\s*[:.]?\s*(?:IT)?\s*)(\d{11})\b/gi
54const IT_VAT = /\bIT\d{11}\b/g
55// Indirizzi italiani: Via Roma 12, Piazza dei Signori, 3, e CAP con città e provincia
56const VIA = /\b(?:Via|Viale|Piazza|Piazzale|Corso|Largo|Vicolo|Lungotevere|Lungarno|Strada|Contrada|Salita|Borgo)\s+(?:(?:d[eia]l?l?[ao]?|dei|degli|delle)\s+|[dD]'|S\.\s|San\s|Santa\s)?[\p{L}'.]+(?:\s+[\p{L}'.]+){0,2},?\s*(?:n\.?\s*)?\d{1,4}[A-Za-z]?\b/gu
57const CAP_CITTA = /\b\d{5}\s+[A-Z][\p{L}'-]+(?:\s+[A-Z][\p{L}'-]+)?\s*\([A-Z]{2}\)/gu
58const CAP = /\b(CAP\s*[:.]?\s*)(\d{5})\b/gi
59const STREET =
60 /\b\d{1,6}\s+(?:[NSEW]\.?\s+)?(?:[A-Z][\p{L}'.-]*\s+){1,3}(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Place|Pl|Parkway|Pkwy|Highway|Hwy|Circle|Cir|Terrace|Ter|Trail|Trl|Square|Sq)\b\.?(?:,?\s+(?:Apt|Apartment|Suite|Ste|Unit|#)\.?\s*[\w-]+)?/gu
61const PO_BOX = /\bP\.?\s?O\.?\s+Box\s+\d+/gi
62const STATE_ZIP =
63 /(,\s*(?:A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY]))\s+\d{5}(?:-\d{4})?\b/g
64const BIRTH = /\b((?:DOB|D\.O\.B\.|date of birth|birth\s?date|birthday|born(?: on)?|nato il|nata il|data di nascita|nascita)\s*[:-]?\s*)([A-Za-z0-9 ,/.-]{4,20}\d)/gi
65const IPV4 = /(?<![\w.])((?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3})(?![\w.])/g
66
67// Importi: 1.250,00 €, € 1.250, 1250 euro, 3,4 milioni di euro, $29
68const SCALE = '(?:\\s?(?:[kKmMbB]|bn|million|thousand|billion|mila|milioni|miliardi|mln|mld)\\b)?'
69const MONEY = new RegExp(`(?:US|CA|AU)?[$€£¥]\\s?\\d[\\d.,]*${SCALE}`, 'g')
70const MONEY_AFTER = new RegExp(`(?<![\\w.,])\\d[\\d.,]*${SCALE}(?:\\s?(?:di\\s)?(?:€|\\$|£|euro\\b|eur\\b|usd\\b|dollari\\b|dollars\\b|bucks\\b))`, 'gi')
71const MONEY_CODE = new RegExp(`(?<![\\w.])(?:USD|EUR|GBP|CAD|AUD)\\s?\\d[\\d.,]*${SCALE}`, 'g')
72
73// Cifre vicino a parole del fatturato: "il fatturato è 84mila", "margine al 40%", "compenso 500"
74const FIN_WORD = new RegExp(
75 '\\b(?:' +
76 [
77 // italiano
78 'fatturato', 'ricav[oi]', 'util[ei]', 'margin[ei]', 'redditi?o', 'guadagn[oi]', 'stipendi?o?', 'compens[oi]', 'parcell[ae]', 'onorari?o?', 'tariff[ae]',
79 'prezz[oi]', 'preventiv[oi]', 'fattur[ae]', 'bonific[oi]', 'iva', 'imposte', 'tasse', 'sald[oi]', 'budget', 'spes[ae]', 'cost[oi]', 'incass[oi]',
80 'pagat[oi]', 'pagament[oi]', 'rat[ae]', 'canone', 'abbonament[oi]', 'debit[oi]', 'prestit[oi]', 'finanziament[oi]', 'investiment[oi]',
81 'commission[ei]', 'rimbors[oi]', 'acconto', 'caparra', 'sconto', 'nett[oi]', 'lord[oi]', 'dividend[oi]', 'trattativ[ae]', 'liquidit[àa]', 'ordin[ei]',
82 // inglese
83 'revenues?', 'mrr', 'arr', 'gmv', 'profits?', 'profit share', 'margins?', 'income', 'earnings', 'payroll', 'salar(?:y|ies)', 'wages?', 'compensation', 'bonus(?:es)?',
84 'equity', 'stakes?', 'valuation', 'runway', 'burn(?: rate)?', 'cash(?:flow)?', 'balances?', 'budgets?', 'spend(?:ing)?', 'pric(?:e|es|ing)', 'fees?', 'invoices?',
85 'payouts?', 'dividends?', 'tax(?:es)?', 'sales', 'sponsor(?:s|ships?)?', 'retainers?', 'royalt(?:y|ies)', 'refunds?', 'expenses?', 'debts?', 'loans?', 'funding',
86 'investments?', 'deals?', 'net', 'gross', 'paid', 'pays?', 'owed?', 'costs?',
87 ].join('|') +
88 ')\\b',
89 'gi',
90)
91const FIGURE = /(?<![\w.])\d+(?:[.,]\d+)*(?:\s?(?:%|per\s?cento\b|percent\b|[kKmMbB]\b|bn\b|mila\b|milioni\b|miliardi\b|mln\b|mld\b|million\b|thousand\b|billion\b|x\b))?/g
92const BIG_FIGURE = /(?<![\w.])(?:\d{1,3}(?:[.,]\d{3})+(?:[.,]\d+)?|\d+(?:[.,]\d+)?\s?(?:%|per\s?cento\b|[kKmMbB]\b|mila\b|milioni\b|miliardi\b|mln\b|mld\b|percent\b|million\b|thousand\b|billion\b))/g
93
94// Nomi singoli che sono anche parole comuni, e nomi che restano visibili (i tuoi)
95const COMMON_WORDS = new Set(
96 (
97 'Will Mark Grant Bill Rich Art Hope Faith Joy May June April August Summer Rose Page Chase Hunter Max Ray Dawn Sky Brook Lane Dean Gene Guy Frank Sterling Major Price Young King Long Little Wood Hill Stone Field Ford Hall Bell Rice Banks Case Cook Fox Gray Green Brown White Black Day Lee West North South Love Church Park Street Bishop Mason Miller Baker Carter Cole Wells Hart Moon Star Van Von De Del La Le Da Di St Mr Mrs Ms Dr Jr Sr ' +
98 'Claude Code Team Support Admin Info Hello The And Ai Slack Gmail Google Calendar Meeting Sync Notes Update Review Weekly Daily ' +
99 'Monday Tuesday Wednesday Thursday Friday Saturday Sunday Lunedì Martedì Mercoledì Giovedì Venerdì Sabato Domenica ' +
100 'Gennaio Febbraio Marzo Aprile Maggio Giugno Luglio Agosto Settembre Ottobre Novembre Dicembre ' +
101 'Il Lo Gli Un Una Ciao Notion Calendario Riunione Note Aggiornamento Settimanale Giornaliero Via Viale Piazza Corso San Santa Don Dott Sig Prof Avv Ing Geom Rag Italia Rosa Fiore Gioia Speranza Pace Felice Bianco Neri Rossi'
102 ).split(' '),
103)
104const ROLE_LOCAL_PARTS =
105 /^(?:info|hello|hi|ciao|team|support|supporto|assistenza|help|admin|noreply|no-reply|donotreply|contact|contatti|sales|vendite|billing|fatture|fatturazione|amministrazione|segreteria|ufficio|ordini|accounts?|contabilita|notifications?|news|newsletter|marketing|office|mail|posta|security|privacy|legal|press|jobs|careers|hr|ops|dev|bot|alerts?|updates?|calendar|invites?)$/i
106
107// File che restano chiusi durante la registrazione. Aggiungi cartelle e nomi tuoi in
108// ~/.claude/mods-data/rec/config.json ("percorsiPrivati"); /rec config crea il file.
109const PRIVATE_PATHS = [
110 /(^|[\\/\s"'`])\.env(\.[A-Za-z0-9_-]+)?(?=$|[\s"'`;|&)])/i,
111 /\.credentials\.json/i,
112 /[\\/]\.claude\.json\b/i,
113 /\.claude[\\/]projects[\\/][^\\/\s"'`]+[\\/]memory/i,
114 /(^|[\\/\s"'`])\.(?:ssh|aws|gnupg)(?=$|[\\/])/i,
115 /\bid_(?:rsa|ed25519|ecdsa)\b/i,
116 /(^|[\\/\s"'`])\.(?:netrc|npmrc|pypirc)\b/i,
117 /\.(?:pem|p12|pfx)\b/i,
118 /taxes?[-_ ]?20\d\d/i,
119 /[\\/_-](?:payroll|invoices?|contracts?|financials?|finances?|budgets?|forecasts?|cap[-_ ]?table|term[-_ ]?sheets?|offer[-_ ]?letters?|business[-_ ]?plans?)(?=$|[\\/._\s"'`;|&)-])/i,
120 /(^|[\s"'`])(?:payroll|invoices?|contracts?|financials?|finances?|budgets?|forecasts?|business[-_ ]?plans?)(?=[\\/.])/i,
121 // italiano
122 /[\\/_-](?:fatture?|contratti?|preventivi?|bilanci?|buste[-_ ]?paga|cedolini|stipendi|f24|modello[-_ ]?unico|dichiarazion[ei]|piano[-_ ]?industriale|offert[ae]|lettere?[-_ ]?d[-_ ]?incarico)(?=$|[\\/._\s"'`;|&)-])/i,
123 /(^|[\s"'`])(?:fatture?|contratti?|preventivi?|bilanci?|buste[-_ ]?paga|cedolini|stipendi)(?=[\\/.])/i,
124 /\b730[-_ ]?20\d\d/i,
125]
126
127// Tool i cui risultati sono dati d'affari: disegnati nascosti durante la registrazione
128const BUSINESS_TOOL = new RegExp(
129 '(?:^|__|_)(?:clickup|slack|gmail|outlook|notion|asana|linear|jira|clay|qbo|quickbooks|xero|fireflies|hubspot|salesforce|stripe|calendar|gcal|google_drive|drive|sheets|spreaker|postpickr)' +
130 '|search_threads|get_thread|get_message|list_drafts|get_draft|read_file_content|download_file_content|search_files|list_recent_files|get_file_metadata' +
131 '|profit_loss|cash_flow|balance_sheet|payroll|session_transcripts|export_transcript' +
132 '|list_events|get_event|search_events|list_calendars|get_values|get_spreadsheet|get_meeting|search_meetings|list_upcoming_meetings|get_scratchpad_note|search_scratchpad_notes|get_posts|get_scheduled_posts|get_drafts',
133 'i',
134)
135const BUSINESS_COMMAND = /\bgws(?:\.cmd|\.exe)?\b|fireflies|quickbooks|\bnotion-[a-z-]+/i
136// I tool di pagamento restano chiusi durante la registrazione
137const FINANCE_TOOL = /__(?:qbo_|quickbooks|profit_loss|cash_flow|balance_sheet|benchmarking_quickbooks|money_onboarding|company_info|stripe_)/i
138
139function escapeRegExp(s) {
140 return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
141}
142
143// I valori dei file .env: abbastanza lunghi da essere segreti, e non parole o numeri semplici
144export function secretValuesFromEnv(text) {
145 const values = []
146 for (const line of String(text || '').split(/\r?\n/)) {
147 const m = line.match(/^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.*)\s*$/)
148 if (!m) continue
149 let v = m[1].trim()
150 if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1)
151 if (v.length < 8) continue
152 if (/^(true|false|null|none|yes|no|si|sì)$/i.test(v)) continue
153 if (/^\d+(\.\d+)?$/.test(v)) continue
154 if (/your[-_ ]?(key|token|secret)|here$|^<.*>$|^xxx/i.test(v)) continue
155 values.push(v)
156 }
157 return [...new Set(values)].sort((a, b) => b.length - a.length)
158}
159
160// Le forme del nome di una persona che vengono mascherate: nome completo, slug e ogni parte che non sia una parola comune
161function nameForms(full, keep) {
162 const clean = String(full || '').replace(/\s+/g, ' ').trim()
163 if (!clean || clean.length > 60 || keep.has(clean)) return []
164 const forms = new Set()
165 const parts = clean.split(/[ -]/).filter((p) => /^[A-Z][\p{L}'’.]*$/u.test(p) && p.length >= 2)
166 if (parts.length >= 2) {
167 forms.add(clean)
168 forms.add(clean.toLowerCase())
169 forms.add(clean.toLowerCase().replace(/ /g, '-'))
170 forms.add(clean.toLowerCase().replace(/ /g, '_'))
171 }
172 for (const p of parts) if (!COMMON_WORDS.has(p) && !keep.has(p)) forms.add(p)
173 return [...forms]
174}
175
176function namesFromEmail(email) {
177 const local = email.split('@')[0]
178 if (ROLE_LOCAL_PARTS.test(local)) return null
179 const parts = local.split(/[._+-]/).filter((p) => /^[a-z]{2,}$/i.test(p))
180 if (parts.length < 2 || parts.length > 4) return null
181 return parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(' ')
182}
183
184// Maschera i numeri nella stessa frase di una parola del fatturato
185function maskFigures(text, strict) {
186 if (strict) text = text.replace(BIG_FIGURE, HIDE)
187 const ranges = []
188 for (const m of text.matchAll(FIN_WORD)) {
189 const s = m.index
190 const e = s + m[0].length
191 let a = Math.max(0, s - 30)
192 let b = Math.min(text.length, e + 45)
193 const before = text.slice(a, s)
194 const stop = Math.max(before.lastIndexOf('\n'), before.lastIndexOf(';'), before.search(/[.!?]\s[^.!?]*$/))
195 if (stop >= 0) a += stop + 1
196 const after = text.slice(e, b)
197 const end = after.search(/\n|;|[.!?](?:\s|$)/)
198 if (end >= 0) b = e + end
199 ranges.push([a, b])
200 }
201 if (!ranges.length) return text
202 return text.replace(FIGURE, (fig, offset) => {
203 if (/^(?:19|20)\d\d$/.test(fig)) return fig // un anno
204 return ranges.some(([a, b]) => offset >= a && offset < b) ? HIDE : fig
205 })
206}
207
208function luhn(digits) {
209 let sum = 0
210 for (let i = 0; i < digits.length; i++) {
211 let d = Number(digits[digits.length - 1 - i])
212 if (i % 2 === 1) {
213 d *= 2
214 if (d > 9) d -= 9
215 }
216 sum += d
217 }
218 return sum % 10 === 0
219}
220
221function publicIp(ip) {
222 const [a, b] = ip.split('.').map(Number)
223 if (a === 10 || a === 127 || a === 0 || ip === '255.255.255.255') return false
224 if (a === 192 && b === 168) return false
225 if (a === 172 && b >= 16 && b <= 31) return false
226 if (a === 169 && b === 254) return false
227 return true
228}
229
230// Il tuo nome e le sue parti restano visibili
231function keepSet(keepNames) {
232 const keep = new Set()
233 for (const n of keepNames || []) {
234 const clean = String(n || '').replace(/\s+/g, ' ').trim()
235 if (!clean) continue
236 keep.add(clean)
237 keep.add(clean.toLowerCase())
238 keep.add(clean.toLowerCase().replace(/ /g, '-'))
239 for (const part of clean.split(/[ -]/)) if (part) keep.add(part)
240 }
241 return keep
242}
243
244// strict: maschera anche ogni cifra grande (1.250 / 18% / 40k) ovunque compaia
245// names: persone da mascherare; keepNames: nomi che restano visibili (i tuoi)
246export function makeMasker({ strict = false, values = [], names = [], keepNames = [] } = {}) {
247 const KEEP_NAMES = keepSet(keepNames)
248 const valueRe = values.length ? new RegExp(values.map(escapeRegExp).join('|'), 'g') : null
249 // Nomi: l'elenco passato più quelli appresi da email e campi contatto. Solo in memoria.
250 const people = new Set()
251 let nameRe = null
252 let dirty = false
253 const learn = (full) => {
254 if (people.size > 2000) return
255 for (const f of nameForms(full, KEEP_NAMES)) {
256 if (!people.has(f)) {
257 people.add(f)
258 dirty = true
259 }
260 }
261 }
262 for (const n of names) learn(n)
263 const nameRegex = () => {
264 if (dirty) {
265 const list = [...people].sort((a, b) => b.length - a.length).map(escapeRegExp)
266 nameRe = list.length ? new RegExp(`(?<![\\p{L}\\p{N}_])(?:${list.join('|')})(?:['’]s)?(?![\\p{L}\\p{N}_])`, 'gu') : null
267 dirty = false
268 }
269 return nameRe
270 }
271
272 return function mask(text) {
273 if (typeof text !== 'string' || text.length === 0) return text
274 let out = text
275 if (valueRe) out = out.replace(valueRe, DOTS)
276 for (const re of SECRET_PATTERNS) out = out.replace(re, DOTS)
277 out = out.replace(ASSIGNMENT, (_, name, sep) => name + sep + DOTS)
278
279 // persone: si imparano prima delle email che le nominano
280 for (const m of out.matchAll(EMAIL)) {
281 const n = namesFromEmail(m[0])
282 if (n) learn(n)
283 }
284 out = out.replace(DISPLAY_NAME, (_, q, name, addr) => {
285 learn(name)
286 return KEEP_NAMES.has(name) ? q + name + q + addr : HIDE + addr
287 })
288 out = out.replace(PERSON_KEY, (_, head, value, tail) => {
289 learn(value)
290 return head + (KEEP_NAMES.has(value) ? value : HIDE) + tail
291 })
292 out = out.replace(PERSON_LINE, (line, label, value) => (KEEP_NAMES.has(value.trim()) ? line : label + HIDE))
293 out = out.replace(EMAIL, '•••@•••')
294 const re = nameRegex()
295 if (re) out = out.replace(re, HIDE)
296
297 // dati personali
298 out = out.replace(CARD, (m) => {
299 const digits = m.replace(/\D/g, '')
300 return digits.length >= 13 && digits.length <= 19 && luhn(digits) ? '•••• •••• •••• ••••' : m
301 })
302 out = out.replace(SSN, '•••-••-••••')
303 out = out.replace(EIN, '••-•••••••')
304 out = out.replace(CODICE_FISCALE, '••••••••••••••••')
305 out = out.replace(PARTITA_IVA, (_, head) => head + HIDE)
306 out = out.replace(IT_VAT, HIDE)
307 out = out.replace(IBAN, HIDE)
308 out = out.replace(ACCOUNT, (_, head) => head + HIDE)
309 out = out.replace(CARD_ENDING, (_, head) => head + '••••')
310 out = out.replace(BIRTH, (_, head) => head + HIDE)
311 out = out.replace(VIA, HIDE)
312 out = out.replace(CAP_CITTA, HIDE)
313 out = out.replace(CAP, (_, head) => head + '•••••')
314 out = out.replace(STREET, HIDE)
315 out = out.replace(PO_BOX, HIDE)
316 out = out.replace(STATE_ZIP, (_, head) => head + ' •••••')
317 out = out.replace(PHONE_INTL, '+•• •••')
318 out = out.replace(PHONE, '•••-•••-••••')
319 out = out.replace(PHONE_IT_MOBILE, '••• •••••••')
320 out = out.replace(PHONE_IT_LAND, '••• •••••')
321 out = out.replace(IPV4, (ip) => (publicIp(ip) ? '•••.•••.•••.•••' : ip))
322
323 // importi e cifre d'affari
324 out = out.replace(MONEY, HIDE + ' €')
325 out = out.replace(MONEY_AFTER, HIDE + ' €')
326 out = out.replace(MONEY_CODE, HIDE)
327 out = maskFigures(out, strict)
328 return out
329 }
330}
331
332// Maschera ogni stringa dentro dati semplici, mantenendo la forma.
333export function deepMask(value, mask, depth = 0) {
334 if (depth > 12) return value
335 if (typeof value === 'string') return mask(value)
336 if (Array.isArray(value)) return value.map((v) => deepMask(v, mask, depth + 1))
337 if (value && typeof value === 'object') {
338 const proto = Object.getPrototypeOf(value)
339 if (proto !== Object.prototype && proto !== null) return value
340 const out = {}
341 for (const [k, v] of Object.entries(value)) out[k] = deepMask(v, mask, depth + 1)
342 return out
343 }
344 return value
345}
346
347// Nasconde il testo leggibile dentro il risultato di un tool d'affari, tenendo le etichette brevi (type: 'text') perché la riga si disegni.
348export function hideText(s) {
349 if (typeof s !== 'string' || s.length === 0) return s
350 return s.length > 24 || /\s/.test(s) ? HIDDEN_OUTPUT : s
351}
352
353// extra: cartelle o nomi di file dal file di configurazione, cercati in qualsiasi punto del percorso, senza badare alle maiuscole
354export function privatePathHit(text, extra = []) {
355 const s = String(text || '')
356 for (const re of PRIVATE_PATHS) {
357 const m = s.match(re)
358 if (m) return m[0].trim().replace(/^["'`\\/_-]/, '')
359 }
360 const flat = s.replace(/\\/g, '/').toLowerCase()
361 for (const item of extra) {
362 const needle = String(item || '').replace(/\\/g, '/').toLowerCase().trim()
363 if (needle && flat.includes(needle)) return String(item)
364 }
365 return null
366}
367
368// Le stringhe negli argomenti di una chiamata che possono nominare un file o contenere un comando.
369export function toolCallTargets(e) {
370 const keys = ['file_path', 'path', 'pattern', 'glob', 'command', 'notebook_path', 'url']
371 const out = []
372 for (const k of keys) if (typeof e?.[k] === 'string') out.push(e[k])
373 return out
374}
375
376// Se il risultato di un tool è dato d'affari (posta, chat, task, file, calendario, finanza)
377// extra: altri nomi di tool o comandi (anche solo una parte) i cui risultati si disegnano nascosti
378export function businessSource(tool, input, extra = []) {
379 const name = String(tool || '')
380 if (BUSINESS_TOOL.test(name)) return true
381 const cmd = input && typeof input.command === 'string' ? input.command : ''
382 const hay = (name + ' ' + cmd).toLowerCase()
383 if (extra.some((x) => x && hay.includes(String(x).toLowerCase()))) return true
384 return cmd ? BUSINESS_COMMAND.test(cmd) : false
385}
386
387// extra: altri nomi di tool (anche solo una parte) che restano chiusi durante la registrazione
388export function financeTool(tool, extra = []) {
389 const name = String(tool || '')
390 return FINANCE_TOOL.test(name) || extra.some((x) => x && name.toLowerCase().includes(String(x).toLowerCase()))
391}
392