Sceglie Haiku, Sonnet o Opus per ogni richiesta con regole fisse, per risparmiare token

I mods di Ale per Claude Code (terminale, scheda Code del desktop, VS Code).
Una volta sola, nel terminale:
claude plugin marketplace add StraordinaryAS/claude-mods
claude plugin install guardia-regole@ale-mods
claude plugin install scelta-modello@ale-mods
claude plugin install tipografia-it@ale-mods
Dentro una sessione Claude Code: /plugin install guardia-regole --marketplace StraordinaryAS/claude-mods
App desktop senza terminale: + accanto al prompt › Plugins › Add marketplace › StraordinaryAS/claude-mods › Add plugin.
Aggiornare dopo nuove versioni: claude plugin marketplace update ale-mods, poi /reload-plugins nella sessione.
| Mod | Cosa fa |
|---|---|
guardia-regole | Blocca commit e push su main/master e i segreti in chiaro (token, chiavi, password) nei file scritti da Claude. I file .env sono ammessi. |
scelta-modello | Per ogni messaggio sceglie Haiku (traduzioni, riassunti, formattazione), Opus (piani, architettura, bug, revisioni) o Sonnet (tutto il resto). Le conferme ("vai", "ok", "2") tengono il modello di prima. @haiku, @sonnet, @opus a inizio messaggio forzano la scelta. A conversazione lunga non scende di livello, per non perdere la cache. /modello-auto off lo spegne. Regole in mods/scelta-modello/hooks/rules.ts. |
tipografia-it | Skill con le regole di tipografia italiana per documenti, PDF e web: righe isolate, spazi, trattini, caporali, apostrofo ’, accenti, font e interlinea. |
I plugin non arrivano nella chat di claude.ai. Per avere tipografia-it anche lì, comprimi la cartella mods/tipografia-it/skills/tipografia-it in uno zip e caricalo da Impostazioni › Funzionalità › Skill › Carica skill.
Nuova cartella mods/<nome>/ con .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.ts e test in tests/. Poi una voce in .claude-plugin/marketplace.json. Controllo: claude plugin validate mods/<nome> e claude plugin test mods/<nome>.
hooks/register.ts 46 lines1import type { Register } from 'claude-code'
2import { choose, keepForCache, MODEL, type Tier } from './rules'
3
4export const register: Register = on => {
5 let enabled = true
6 let wanted: Tier | undefined // scelto dall'ultimo messaggio
7 let running: Tier | undefined // usato davvero nell'ultimo turno
8
9 on('session.start', async ($, e, next) => {
10 await $.command.register({
11 name: 'modello-auto',
12 description: 'Accende o spegne la scelta automatica del modello (on, off, stato)',
13 })
14 return next(e)
15 })
16
17 on('command.run', { command: 'modello-auto' }, ($, e) => {
18 const arg = e.args.trim().toLowerCase()
19 if (arg === 'off') enabled = false
20 if (arg === 'on') enabled = true
21 if (!enabled) $.ui.status(undefined)
22 return { text: `Scelta automatica del modello: ${enabled ? 'accesa' : 'spenta'}${running ? ` (ultimo: ${running})` : ''}.` }
23 })
24
25 on('prompt.submit', ($, e, next) => {
26 if (!enabled || e.command) return next(e)
27 const choice = choose(e.text, wanted)
28 wanted = choice.tier
29 $.ui.status(`modello: ${choice.tier} · ${choice.reason}`)
30 return next(choice.text === e.text ? e : { ...e, text: choice.text })
31 }).catch(($, e, next) => next(e))
32
33 on('turn.step', async function* ($, e, next) {
34 // Solo la conversazione principale; i sotto-agenti restano come sono.
35 if (!enabled || !wanted || e.agentId !== undefined) return yield* next(e)
36
37 // Il modello si decide al primo passo del turno e resta fisso fino alla fine.
38 if (e.index === 0) {
39 running = keepForCache(wanted, running, e.messageCount) ? running! : wanted
40 if (running !== wanted) $.ui.status(`modello: ${running} · resto qui, conversazione lunga`)
41 }
42 const tier = running ?? wanted
43 return yield* next({ ...e, model: MODEL[tier] })
44 })
45}
46hooks/rules.ts 47 lines1// Regole fisse: nessun modello viene chiamato per decidere.
2// Per cambiarle, modifica le liste qui sotto.
3
4export type Tier = 'haiku' | 'sonnet' | 'opus'
5
6/** Nome passato al motore per ogni livello (alias o id completo). */
7export const MODEL: Record<Tier, string> = { haiku: 'haiku', sonnet: 'sonnet', opus: 'opus' }
8
9const RANK: Record<Tier, number> = { haiku: 0, sonnet: 1, opus: 2 }
10
11// Lavoro che richiede ragionamento: piani, architettura, bug, revisioni.
12const OPUS = /\b(piano|pianifica|architettura|progett[ai]|strategi[ae]|analizza a fondo|ragiona|perch[eé] non funziona|debug|bug|non capisco perch[eé]|refactor\w*|rivedi il codice|code review|sicurezza|vulnerabilit)/i
13
14// Compiti meccanici e brevi: traduzioni, riassunti, formattazione.
15const HAIKU = /\b(tradu[ci]\w*|riassum\w*|riassunto|correggi (l'ortografia|i refusi|gli errori di battitura)|rinomina|formatta|elenca|converti in|estrai (i |le )?(dati|date|nomi|email)|che ore)/i
16
17// Conferme brevi: proseguono il lavoro in corso, quindi tengono il modello di prima.
18const CONFIRM = /^(vai|ok|okay|s[iì]|no|fatto|procedi|continua|perfetto|grazie|va bene|\d{1,2}[.)]?|[a-c])\W*$/i
19
20const OVERRIDE = /^\s*@(haiku|sonnet|opus)\b\s*/i
21
22export type Choice = { tier: Tier; reason: string; text: string }
23
24/**
25 * Sceglie il livello per un nuovo messaggio.
26 * `previous` è il livello del turno prima, se c'è.
27 */
28export const choose = (raw: string, previous?: Tier): Choice => {
29 const forced = raw.match(OVERRIDE)
30 if (forced) return { tier: forced[1]!.toLowerCase() as Tier, reason: 'scelto da te', text: raw.replace(OVERRIDE, '') }
31
32 const text = raw.trim()
33 if (previous && (text === '' || CONFIRM.test(text))) return { tier: previous, reason: 'conferma, prosegue', text: raw }
34 if (OPUS.test(text)) return { tier: 'opus', reason: 'ragionamento', text: raw }
35 if (HAIKU.test(text) && text.length < 600 && !text.includes('```')) return { tier: 'haiku', reason: 'compito semplice', text: raw }
36 return { tier: 'sonnet', reason: 'predefinito', text: raw }
37}
38
39/**
40 * A conversazione lunga, scendere di livello costa più di quanto fa risparmiare:
41 * il nuovo modello deve rileggere tutto il contesto senza cache.
42 */
43export const LONG_CONVERSATION = 40
44
45export const keepForCache = (wanted: Tier, current: Tier | undefined, messageCount: number) =>
46 current !== undefined && RANK[wanted] < RANK[current] && messageCount > LONG_CONVERSATION
47