SLOPSHOPPER

save-point

Salvataggi automatici del progetto prima che Claude modifichi i file: /salvataggi per tornare a quando funzionava, /salva per un salvataggio tuo.

newpaneguardcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · save-point
│ ┃ salvataggi ✕ › fix the failing auth test and add an audit log call │ ┃ Prima che Claude modifichi i file salvo │ ┃ com'era il progetto. Scegli un punto per ⏺ Read(src/auth.ts) │ ┃ tornarci. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ [ ⭐ Salva adesso ] ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Ancora nessun salvataggio: il primo arriva ⎿ 3 pass, 1 fail │ ┃ quando Claude inizia a modificare i file. │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /salva │ ⎿ save-point: Salvato (nessun file cambiato dall'ultimo salvataggi │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ save-point: 💾 salvato oggi 08:53

Draws

Pane · salvataggi
Prima che Claude modifichi i file salvo com'era il progetto. Scegli un punto per tornarci. [ ⭐ Salva adesso ] Ancora nessun salvataggio: il primo arriva quando Claude inizia a modificare i file.
README

5 mod per Claude Code (per chi non è sviluppatore)

Cinque mod gratuite per chi usa Claude Code per costruire i propri progetti (un sito, un'app, un'automazione) anche senza saper programmare.

ModIn una riga
💾save-pointClaude ha rotto qualcosa? Torni a quando funzionava con un clic.
💡cosa-hai-fattoDopo ogni modifica ti spiega in parole semplici cosa ha fatto Claude e come provarlo.
⛽benzinaVedi quanto ti resta del piano Claude prima che si fermi, e quando si ricarica.
⚡quick-repliesPulsanti per le risposte che scrivi sempre: "Continua", "Procedi", "Non funziona, controlla tu".
🌐localhostVedi le app che hai acceso sul computer, aprile nel browser o spegnile.

Puoi installarne una sola, alcune o tutte e cinque: sono indipendenti.


Indice


Cos'è una mod

Una mod è un piccolo plugin che cambia come Claude Code si comporta e cosa ti mostra: una riga sopra il punto dove scrivi, un pannello laterale, un pulsante, un avviso. Non cambia il modello e non legge niente fuori dal tuo computer: gira dentro Claude Code, sul tuo Mac o PC.

Dove le vedi:

  • sopra il prompt (la "banda"): benzina, cosa-hai-fatto e quick-replies scrivono lì;
  • in un pannello che si apre con un comando: /salvataggi (save-point) e /localhost.

Cosa ti serve

  1. Claude Code 2.1.287 o più recente. Per controllare, nel terminale: `` claude --version ` Se è più vecchio, aggiornalo con: ` claude update ``
  2. Il terminale. Le mod si installano dal terminale; una volta installate funzionano anche nella scheda Code dell'app desktop di Claude.
  3. Solo per save-point: git. Sul Mac di solito c'è già. Se manca, nel terminale: xcode-select --install. Su Linux: installa il pacchetto git.

Installazione passo passo

  1. Apri il terminale ed entra nella cartella di un tuo progetto, poi avvia Claude Code: `` cd percorso/del/tuo/progetto claude ``
  2. Nel punto dove scrivi a Claude, incolla una riga per ogni mod che vuoi e premi Invio: `` /plugin install save-point --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install cosa-hai-fatto --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install benzina --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install quick-replies --marketplace cavallinilorenzo/claude-code-mods ` ` /plugin install localhost --marketplace cavallinilorenzo/claude-code-mods ``
  3. La prima volta Claude Code chiede Add marketplace?: rispondi y.
  4. Poi ti chiede dove installarla: lascia la prima opzione (il tuo utente) e premi Invio. Così la mod funziona in tutti i tuoi progetti.
  5. Per quick-replies e cosa-hai-fatto compare anche una schermata con le impostazioni: puoi lasciare quelle predefinite e premere Invio.
  6. Vedrai Installed <nome>. Plugin is now active. La mod è già attiva, non serve riavviare.

In alternativa, dal terminale normale (fuori da Claude Code):

claude plugin marketplace add cavallinilorenzo/claude-code-mods
claude plugin install save-point@lorenzo-mods
claude plugin install cosa-hai-fatto@lorenzo-mods
claude plugin install benzina@lorenzo-mods
claude plugin install quick-replies@lorenzo-mods
claude plugin install localhost@lorenzo-mods

Le mod una per una

💾 save-point

Il problema: chiedi una modifica, Claude cambia dieci file e l'app non parte più. Non sai cosa ha toccato e non sai come tornare indietro.

Cosa fa: ogni volta che Claude sta per modificare i file, save-point salva com'era il progetto un attimo prima. Lo fa da solo, una volta per ogni tua richiesta, e il salvataggio prende il nome della richiesta.

Come si usa:

ComandoCosa succede
/salvataggiApre l'elenco dei salvataggi. Accanto a ognuno c'è Torna qui: lo premi, confermi con Sì, torna qui e tutti i file tornano com'erano in quel momento.
/salvaSalva adesso, a mano.
/salva funziona il loginSalva adesso con un nome tuo: comparirà come ⭐ funziona il login.

Cosa vedi in /salvataggi:

oggi 15:42  Prima di: «aggiungi il carrello»     [ Torna qui ]
oggi 15:10  ⭐ funziona il login                  [ Torna qui ]
oggi 14:31  Prima di: «cambia i colori del sito»  [ Torna qui ]

Da sapere:

  • Puoi sempre annullare. Prima di tornare indietro, save-point salva com'era il progetto in quel momento ("Com'era prima di tornare a…"). Se cambi idea, torni a quel salvataggio.
  • Claude viene avvisato. Dopo un ritorno indietro, save-point dice a Claude che i file sono cambiati, così non lavora su una versione che non c'è più.
  • Non tocca il tuo git. Se usi già git o GitHub, i tuoi commit e branch restano come sono: i salvataggi stanno in una cartella separata, ~/.claude/save-points/.
  • Cosa non salva: cartelle pesanti che si rigenerano da sole (node_modules, .venv, venv, __pycache__, .next, dist, build, target, .cache), i file .log e i file che il tuo progetto ignora già con .gitignore.
  • Non salva mai la tua home intera né la radice del disco: se avvii Claude lì, save-point non fa niente e te lo dice. Avvia Claude dentro la cartella del progetto.
  • Spazio su disco: i salvataggi sono compressi e i file uguali non vengono duplicati. Se un giorno vuoi liberare spazio, puoi cancellare la cartella ~/.claude/save-points/.

💡 cosa-hai-fatto

Il problema: Claude risponde con nomi di file, funzioni e comandi. Tu vuoi solo sapere cosa è cambiato e se funziona.

Cosa fa: quando una risposta di Claude ha modificato dei file, sopra il prompt compare un riassunto in parole semplici, senza gergo tecnico, con un passo concreto per provarlo.

💡 In breve: Ora i clienti possono pagare con la carta e il carrello mostra le spese di spedizione.
👉 Prova: aggiungi un prodotto al carrello e premi «Paga».
[ Spiegamelo meglio ]  [ Nascondi ]
  • Spiegamelo meglio chiede a Claude di spiegarti con calma cosa ha cambiato, perché e come verificarlo.
  • Nascondi toglie il riassunto fino alla prossima modifica.
  • Se Claude ha solo risposto a una domanda senza toccare file, non compare niente.

Da sapere:

  • Il riassunto lo scrive Claude Haiku, il modello più piccolo e veloce: dopo ogni risposta che modifica file fa una chiamata breve. Consuma poco, ma conta sul tuo piano.
  • Lingua: italiano di default. Puoi cambiarla dalle impostazioni (es. english, español).

⛽ benzina

Il problema: stai lavorando e all'improvviso Claude si ferma perché hai finito il limite del tuo piano. Oppure la chat è così lunga che Claude inizia a dimenticare le cose.

Cosa fa: mostra sopra il prompt quanto ti resta, come l'indicatore della benzina:

⛽  5 ore ▰▰▰▱▱▱▱▱ 38% · ricarica alle 16:40   Settimana ▰▰▰▰▰▰▱▱ 80%   Chat ▰▰▰▰▰▱▱▱ 64%
  • 5 ore: quanto ti resta del limite che si ricarica ogni cinque ore, e a che ora si ricarica.
  • Settimana: quanto ti resta del limite settimanale.
  • Chat: quanto spazio resta nella conversazione attuale.

Quando serve ti dà un consiglio in chiaro:

  • "Quasi a secco: tra poco Claude si ferma fino alle 16:40."
  • "La chat è lunga e Claude inizia a dimenticare: se passi a un'altra cosa, scrivi /clear."

Ti avvisa anche con una notifica quando superi l'80% e il 95% di un limite.

ComandoCosa succede
/benzinaNasconde l'indicatore, o lo fa ricomparire. La scelta resta salvata.

Da sapere: i limiti delle 5 ore e della settimana compaiono solo con un abbonamento Claude (Pro o Max) e dopo la prima risposta di Claude. Se usi una chiave API vedi solo la Chat.


⚡ quick-replies

Il problema: scrivi cento volte al giorno "continua", "procedi", "ok va bene".

Cosa fa: dopo ogni risposta di Claude, sopra il prompt compaiono dei pulsanti con le risposte che usi sempre. Un clic e la risposta parte.

[ Continua ]  [ Procedi ]  [ Ok a tutte le tue raccomandazioni ]  [ Non funziona, controlla tu e sistemalo ]

Le risposte si cambiano dalle impostazioni: scrivile separate da |, fino a 9. Ad esempio:

Continua|Procedi|Spiegamelo in parole semplici|Avviamelo così lo provo

🌐 localhost

Il problema: Claude ti dice "l'app è su localhost:5173". Ma quale? Quante ne hai accese? Come le spegni?

Cosa fa: /localhost apre un pannello con tutte le app che hai acceso sul computer (vite, next, django, flask, uvicorn…), la porta, la cartella da cui sono partite e due pulsanti:

:5173  node    ~/progetti/negozio          apri  [ ferma ]
:8000  Python  ~/progetti/negozio/backend  apri  [ ferma ]
:3000  node    ~/progetti/vecchio-test     apri  [ ferma ]

[ Ferma quelli di questo progetto (2) ]  [ Ferma tutti ]  [ Aggiorna ]
  • apri apre l'app nel browser.
  • ferma la spegne.
  • In verde quelle del progetto in cui sei.
  • L'elenco si aggiorna da solo quando Claude avvia o ferma qualcosa.

Da sapere: funziona su Mac e Linux.


Impostazioni

Due mod hanno impostazioni:

ModImpostazioneDefault
quick-repliesRisposte rapide (separate da `\`)`Continua\Procedi\Ok a tutte le tue raccomandazioni\Non funziona, controlla tu e sistemalo`
cosa-hai-fattoLingua delle spiegazioniitaliano

Le scegli la prima volta, nella schermata che compare durante l'installazione. Per cambiarle dopo, dal terminale:

echo '{"replies": "Continua|Procedi|Spiegamelo in parole semplici"}' | claude plugin configure quick-replies@lorenzo-mods --values-stdin
echo '{"language": "english"}' | claude plugin configure cosa-hai-fatto@lorenzo-mods --values-stdin

Per vedere i valori attuali: claude plugin configure quick-replies@lorenzo-mods.


Aggiornare, disattivare, disinstallare

Dal terminale:

claude plugin update save-point@lorenzo-mods      # prende l'ultima versione
claude plugin disable benzina@lorenzo-mods        # la spegne senza toglierla
claude plugin enable benzina@lorenzo-mods         # la riaccende
claude plugin uninstall localhost@lorenzo-mods    # la toglie

Dentro Claude Code, /plugin apre lo stesso menu con i pulsanti.


Domande frequenti

Devo saper programmare? No. Si installano incollando una riga e si usano con pulsanti e comandi in italiano.

Costano qualcosa? Le mod sono gratis. Solo cosa-hai-fatto fa una piccola chiamata a Claude Haiku dopo le risposte che modificano file, che conta sul tuo piano Claude come qualsiasi altro messaggio.

Mandano i miei dati da qualche parte? No. Lavorano sul tuo computer. save-point salva i file in ~/.claude/save-points/. cosa-hai-fatto manda a Claude (come fa già Claude Code) la tua richiesta, i nomi dei file modificati e la risposta di Claude, per scrivere il riassunto. Il codice è tutto in questa pagina: puoi leggerlo.

save-point sostituisce git o GitHub? No. È una rete di sicurezza per tornare indietro mentre lavori con Claude. Se vuoi pubblicare il progetto o lavorarci con altre persone, git e GitHub restano lo strumento giusto.

E il tasto Esc due volte di Claude Code ("rewind")? Il rewind di Claude Code annulla le modifiche fatte con i suoi strumenti di modifica dei file. save-point salva tutta la cartella, quindi copre anche quello che cambia per altre vie (comandi, installazioni, script), e i salvataggi non dipendono dalla conversazione: li ritrovi anche in una chat nuova.

Funzionano nell'app desktop? Sì, nella scheda Code. Si installano dal terminale una volta sola e poi le trovi anche lì.

Funzionano su Windows? benzina, cosa-hai-fatto e quick-replies sì. save-point e localhost sono pensate per Mac e Linux: su Windows non le ho provate.


Problemi comuni

Cosa vediCosa fare
Marketplace file not foundControlla di aver scritto bene cavallinilorenzo/claude-code-mods.
Plugin "…" not found in marketplaceControlla il nome della mod: save-point, cosa-hai-fatto, benzina, quick-replies, localhost.
/plugin install non esiste nell'app desktopÈ normale: installale dal terminale, poi le trovi anche nell'app.
save-point dice che non salva la homeHai avviato Claude nella tua home. Chiudi, entra nella cartella del progetto (cd …) e riavvia claude.
save-point dice che serve gitMac: xcode-select --install. Linux: installa il pacchetto git.
benzina mostra solo la ChatUsi una chiave API invece di un abbonamento, oppure Claude non ha ancora risposto in questa sessione.
La banda sopra il prompt non si vedeSi può ridurre con [-] o ctrl+x ctrl+a: premi di nuovo per riaprirla.
Una mod non fa nienteAggiorna Claude Code (claude update) e controlla con claude plugin list che sia attiva.

Per chi vuole modificarle

Ogni mod è una cartella in mods/ con:

  • .claude-plugin/plugin.json, il nome, la descrizione e le impostazioni;
  • hooks/register.tsx, il codice della mod;
  • hooks/*.test.ts(x), i test;
  • types/index.d.ts, i valori che la mod tiene in memoria.

Per provarne una modificata senza installarla:

claude --plugin-dir ./mods/save-point

Per controllarla e lanciare i test:

claude plugin validate ./mods/save-point
claude plugin test ./mods/save-point

Idee e segnalazioni: apri una issue.


Licenza

MIT: puoi usarle, modificarle e ricondividerle, mantenendo la nota di copyright.

Fatto da Lorenzo Cavallini.

Source 3 files
hooks/register.tsx 252 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SavePoint } from '../types'
5import {
6  EXCLUDES,
7  LOG_FORMAT,
8  commitMessage,
9  formatWhen,
10  gitArgv,
11  isUnsafeFolder,
12  parseLog,
13  promptLabel,
14  storeDir,
15} from './savepoints'
16
17const PANE = 'salvataggi'
18const SHOWN = 30
19// The tools that change files: the first of them in a turn takes a save point first.
20const CHANGING_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'Bash', 'PowerShell'])
21
22const points = atom({ plugin: 'save-point', key: 'points' } as const, [])
23const confirming = atom({ plugin: 'save-point', key: 'confirming' } as const, null)
24const isBusy = atom({ plugin: 'save-point', key: 'isBusy' } as const, false)
25
26type Place = { cwd: string; store: string }
27
28async function place($: EngineInterface): Promise<Place | null> {
29  const cwd = await $.session.cwd()
30  const home = await $.env.get('HOME')
31  if (home === undefined || isUnsafeFolder(cwd, home)) {
32    return null
33  }
34  return { cwd, store: storeDir(home, cwd) }
35}
36
37async function git($: EngineInterface, where: Place, args: string[], timeoutMs = 120_000) {
38  return $.process.run(gitArgv(where.store, where.cwd, args), { cwd: where.cwd, timeoutMs })
39}
40
41async function ensureStore($: EngineInterface, where: Place): Promise<void> {
42  if (await $.fs.exists(`${where.store}/HEAD`)) {
43    return
44  }
45  await $.process.run(['git', 'init', '--bare', '--quiet', where.store])
46  await $.process.run(['git', `--git-dir=${where.store}`, 'config', 'core.bare', 'false'])
47  await $.fs.write(`${where.store}/info/exclude`, `${EXCLUDES.join('\n')}\n`)
48}
49
50async function refresh($: EngineInterface): Promise<SavePoint[]> {
51  const where = await place($)
52  if (where === null || !(await $.fs.exists(`${where.store}/HEAD`))) {
53    await update($, points, () => [])
54    return []
55  }
56  const log = await git($, where, ['log', `-n${SHOWN}`, `--format=${LOG_FORMAT}`]).catch(() => null)
57  const found = log !== null && log.exitCode === 0 ? parseLog(log.stdout) : []
58  await update($, points, () => found)
59  return found
60}
61
62type Saved = 'saved' | 'unchanged' | 'unsafe' | 'failed'
63
64async function save($: EngineInterface, kind: SavePoint['kind'], label: string): Promise<Saved> {
65  const where = await place($)
66  if (where === null) {
67    return 'unsafe'
68  }
69  try {
70    await ensureStore($, where)
71    const added = await git($, where, ['add', '--all', '--', ':/'])
72    if (added.exitCode !== 0) {
73      return 'failed'
74    }
75    const hasHead = (await git($, where, ['rev-parse', '--verify', '--quiet', 'HEAD'])).exitCode === 0
76    const isSame = hasHead && (await git($, where, ['diff', '--cached', '--quiet'])).exitCode === 0
77    // An automatic save point of nothing new would only crowd the list; one the person asked for is kept.
78    if (isSame && kind !== 'manual') {
79      return 'unchanged'
80    }
81    const committed = await git($, where, ['commit', '--quiet', '--no-verify', '--allow-empty', '-m', commitMessage(kind, label)])
82    if (committed.exitCode !== 0) {
83      return 'failed'
84    }
85    await refresh($)
86    const now = await $.clock.now()
87    $.ui.status(`💾 salvato ${formatWhen(now, now)}`)
88    return isSame ? 'unchanged' : 'saved'
89  } catch {
90    return 'failed'
91  }
92}
93
94async function restore($: EngineInterface, point: SavePoint): Promise<void> {
95  const where = await place($)
96  if (where === null) {
97    return
98  }
99  await update($, isBusy, () => true)
100  try {
101    const now = await $.clock.now()
102    const when = formatWhen(point.savedAt, now)
103    // What is there now is saved first, so going back can itself be undone.
104    await save($, 'restore', `Com'era prima di tornare a ${when}`)
105    const restored = await git($, where, ['restore', `--source=${point.sha}`, '--staged', '--worktree', '--', ':/'])
106    if (restored.exitCode !== 0) {
107      $.ui.toast(`Non sono riuscito a tornare al salvataggio: ${restored.stderr.trim().slice(0, 200)}`)
108      return
109    }
110    await save($, 'restore', `Tornato a ${when}: ${point.label}`)
111    await $.session
112      .append({
113        message: {
114          type: 'user',
115          content: [
116            {
117              type: 'text',
118              text:
119                `[save-point] L'utente ha riportato i file del progetto a com'erano ${when} (${point.label}). ` +
120                'Le modifiche fatte dopo non ci sono più: rileggi i file prima di modificarli.',
121            },
122          ],
123        },
124      })
125      .catch(() => undefined)
126    $.ui.toast(`Fatto: i file sono tornati a ${when}. Se cambi idea, il primo salvataggio della lista è com'erano prima.`)
127  } finally {
128    await update($, confirming, () => null)
129    await update($, isBusy, () => false)
130  }
131}
132
133function describe(result: Saved): string {
134  switch (result) {
135    case 'saved':
136      return 'Salvato. Lo trovi in /salvataggi.'
137    case 'unchanged':
138      return 'Salvato (nessun file cambiato dall\'ultimo salvataggio).'
139    case 'unsafe':
140      return 'Non salvo questa cartella: è la tua home o la radice del disco. Apri Claude dentro la cartella del progetto.'
141    case 'failed':
142      return 'Non sono riuscito a salvare: serve git installato (su Mac: xcode-select --install).'
143  }
144}
145
146export const register: Register = on => {
147  let prompt = ''
148  let isSavedThisTurn = false
149
150  on('session.start', async ($, e, next) => {
151    await $.command.register({
152      name: 'salva',
153      description: 'Salva adesso com\'è il progetto (es. /salva funziona il login)',
154      argumentHint: '[nome]',
155    })
156    await $.command.register({
157      name: 'salvataggi',
158      description: 'I salvataggi del progetto: torna a com\'era prima di una richiesta',
159    })
160    void refresh($)
161
162    return next(e)
163  })
164
165  on('turn.start', ($, e, next) => {
166    prompt = e.text
167    isSavedThisTurn = false
168
169    return next(e)
170  })
171
172  on('tool.call', async ($, e, next) => {
173    if (!isSavedThisTurn && CHANGING_TOOLS.has(String(e.tool))) {
174      isSavedThisTurn = true
175      await save($, 'auto', promptLabel(prompt))
176    }
177
178    return next(e)
179  })
180
181  on('command.run', { command: 'salva' }, async ($, e) => {
182    const name = e.args.trim()
183    const result = await save($, 'manual', name.length > 0 ? `⭐ ${name}` : '⭐ Salvato a mano')
184
185    return { text: describe(result) }
186  })
187
188  on('command.run', { command: 'salvataggi' }, async $ => {
189    const found = await refresh($)
190    await update($, confirming, () => null)
191    await $.ui.open({ id: PANE, title: 'salvataggi', focus: true, closeOnEscape: true })
192
193    return { text: `${found.length} salvataggi per questo progetto.` }
194  })
195
196  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
197    const { Box, Button, Text } = $.ui.resolve(e)
198    const list = await read($, points)
199    const pending = await read($, confirming)
200    const busy = await read($, isBusy)
201    const now = await $.clock.now()
202
203    return (
204      <Box flexDirection="column">
205        <Text dimColor>
206          Prima che Claude modifichi i file salvo com'era il progetto. Scegli un punto per tornarci.
207        </Text>
208        <Box flexDirection="row" columnGap={1} marginY={1}>
209          <Button
210            key="save-now"
211            label="⭐ Salva adesso"
212            variant="primary"
213            onPress={async () => {
214              $.ui.toast(describe(await save($, 'manual', '⭐ Salvato a mano')))
215            }}
216          />
217        </Box>
218        {busy && <Text color="warning">Sto riportando i file indietro…</Text>}
219        {list.length === 0 && (
220          <Text dimColor>Ancora nessun salvataggio: il primo arriva quando Claude inizia a modificare i file.</Text>
221        )}
222        {list.map((point, index) => (
223          <Box key={`row-${point.sha}`} flexDirection="column">
224            <Box flexDirection="row" columnGap={1}>
225              <Text dimColor>{formatWhen(point.savedAt, now).padEnd(11)}</Text>
226              <Box flexGrow={1}>
227                <Text wrap="truncate-end" bold={point.kind === 'manual'} dimColor={point.kind === 'restore'}>
228                  {point.label}
229                </Text>
230              </Box>
231              {pending !== point.sha && !busy && (
232                <Button
233                  key={`back-${index}`}
234                  label="Torna qui"
235                  onPress={() => update($, confirming, () => point.sha)}
236                />
237              )}
238            </Box>
239            {pending === point.sha && !busy && (
240              <Box flexDirection="row" columnGap={1} marginLeft={2}>
241                <Text color="warning">Riporto tutti i file a {formatWhen(point.savedAt, now)}?</Text>
242                <Button key="confirm" label="Sì, torna qui" variant="primary" onPress={() => restore($, point)} />
243                <Button key="cancel" label="Annulla" onPress={() => update($, confirming, () => null)} />
244              </Box>
245            )}
246          </Box>
247        ))}
248      </Box>
249    )
250  })
251}
252
hooks/savepoints.ts 101 lines
1import type { SavePoint } from '../types'
2
3// Folders that are rebuilt from scratch or too heavy to copy: never part of a save point.
4export const EXCLUDES = [
5  'node_modules/',
6  '.venv/',
7  'venv/',
8  '__pycache__/',
9  '.next/',
10  '.nuxt/',
11  '.svelte-kit/',
12  '.cache/',
13  'dist/',
14  'build/',
15  'target/',
16  '.DS_Store',
17  '*.log',
18]
19
20const KINDS = ['auto', 'manual', 'restore'] as const
21
22// The save points live outside the project, one git store per folder, so the
23// project's own git (if any) never sees them.
24export function storeDir(home: string, cwd: string): string {
25  const slug = cwd.replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
26  return `${home}/.claude/save-points/${slug || 'root'}`
27}
28
29// Saving the home folder or the disk root would copy everything the person owns.
30export function isUnsafeFolder(cwd: string, home: string | undefined): boolean {
31  const folder = cwd.replace(/\/+$/, '')
32  if (folder === '' || folder === '/') {
33    return true
34  }
35  if (home === undefined) {
36    return false
37  }
38  const homeFolder = home.replace(/\/+$/, '')
39  return folder === homeFolder || homeFolder.startsWith(`${folder}/`)
40}
41
42export function gitArgv(store: string, cwd: string, args: string[]): string[] {
43  return [
44    'git',
45    `--git-dir=${store}`,
46    `--work-tree=${cwd}`,
47    '-c', 'user.name=save-point',
48    '-c', 'user.email=save-point@localhost',
49    '-c', 'commit.gpgsign=false',
50    '-c', 'core.hooksPath=/dev/null',
51    '-c', 'core.quotepath=false',
52    ...args,
53  ]
54}
55
56export function commitMessage(kind: SavePoint['kind'], label: string): string {
57  return `[${kind}] ${label.replace(/\s+/g, ' ').trim()}`
58}
59
60export const LOG_FORMAT = '%H%x09%ct%x09%s'
61
62export function parseLog(stdout: string): SavePoint[] {
63  return stdout
64    .split('\n')
65    .filter(line => line.trim().length > 0)
66    .flatMap(line => {
67      const [sha, seconds, ...rest] = line.split('\t')
68      const subject = rest.join('\t')
69      const match = /^\[(auto|manual|restore)\] (.*)$/.exec(subject)
70      if (sha === undefined || seconds === undefined || match === null) {
71        return []
72      }
73      const kind = KINDS.find(k => k === match[1]) ?? 'auto'
74      return [{ sha, savedAt: Number(seconds) * 1000, kind, label: match[2] ?? '' }]
75    })
76}
77
78// "Prima di: «aggiungi la pagina login»", cut so a long prompt stays one line.
79export function promptLabel(prompt: string, max = 60): string {
80  const oneLine = prompt.replace(/\[(Image|Pasted text) #\d+[^\]]*\]/g, '').replace(/\s+/g, ' ').trim()
81  if (oneLine.length === 0) {
82    return 'Prima che Claude modificasse i file'
83  }
84  const cut = oneLine.length > max ? `${oneLine.slice(0, max - 1).trimEnd()}…` : oneLine
85  return `Prima di: «${cut}»`
86}
87
88export function formatWhen(savedAt: number, now: number): string {
89  const when = new Date(savedAt)
90  const time = `${String(when.getHours()).padStart(2, '0')}:${String(when.getMinutes()).padStart(2, '0')}`
91  const today = new Date(now)
92  const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()
93  if (savedAt >= startOfToday) {
94    return `oggi ${time}`
95  }
96  if (savedAt >= startOfToday - 86_400_000) {
97    return `ieri ${time}`
98  }
99  return `${when.getDate()}/${when.getMonth() + 1} ${time}`
100}
101
types/index.d.ts 19 lines
1export type SavePoint = {
2  sha: string
3  /** When it was saved, in milliseconds. */
4  savedAt: number
5  kind: 'auto' | 'manual' | 'restore'
6  label: string
7}
8
9declare module 'claude-code' {
10  interface PluginState {
11    'save-point': {
12      points: SavePoint[]
13      /** The save point waiting for "Sì, torna qui", if any. */
14      confirming: string | null
15      isBusy: boolean
16    }
17  }
18}
19