Mode d'emploi des mods, en français : ce qu'est un mod, événements, panneaux, commandes, userConfig, installer, activer, tester, écrire un mod pas à pas, et…

Mods for Claude Code: small panes that show what your agents do. They never block or rewrite a tool call; at most they warn.
Needs Claude Code 2.1.287 or later (function-hook mods, panes, Svg element).
| Mod | Command | What it shows | ||||
|---|---|---|---|---|---|---|
factory | /factory, /factory <title or id> | Dashboard of every conversation (model, effort, context %, cost today, running subagents, PR/MR and CI, waiting on you). Click one for its agent tree: main thread, each subagent and Workflow node with model, effort, tokens and estimated cost, the bill per model and per turn, refused tool calls, the session log and warnings (subagent without an explicit model, Opus doing read-only search, big context without handoff/RESUME.md, timer tools). | ||||
control | `/control [setup\ | usage\ | practices\ | history] [scan]` | SETUP: everything that shapes a session (settings, env, permissions, global and repo CLAUDE.md, agents, skills, hook scripts, mods, scheduled tasks, MCP servers, plugins, memory), with hidden rules and any blocker (hook, deny rule, mod deny path) at the top. USAGE: 1/7/30-day model, effort and delegation mix from the transcripts, each metric against a target. PRACTICES and HISTORY: your rules file, followed or not, and its version history. | |
ci | /ci [refresh], `/ci route <repo\ | session\ | here> <backend-id>, /ci clear <target>` | CI routing: the CI backends from your config (GitHub Actions, GitLab.com shared runners, a self-hosted runner, VM runners still coming), whether each is usable now (token present yes/no, runners online, running/pending jobs, Actions per routed repo), and which repo or conversation uses which. A lead conversation or you set the route; each conversation sees its route in the status line and as a short note in its system prompt. | ||
scenarios | `/scenarios [S1..] [prompt\ | agent\ | workflow\ | frontmatter\ | env]` | Scénarios d'agents: presets of roles -> model, effort, unit (session, sub-conversation, Workflow agent(), Agent subagent) and max parallel (S1 Standard V6, S2 Économe, S3 Qualité max, S4 Nuit, plus yours from a JSON file). For each role, the exact settings (Agent call, agent() opts, agent-file frontmatter, env vars) and the documented limits, each with its doc URL and "non vérifié" where the docs say nothing; a Copy button for a ready-to-paste snippet. Warn-only toast when an Agent call names no model. |
guide | /mods-guide [onglet ou section] | Mode d'emploi des mods, in French: tabs (Sommaire, Bases, Événements, Interface, Configuration, Installer et activer, Tester, Écrire un mod pas à pas, Référence API, Nos mods, FAQ), foldable categories, and one card per mod of this repo built live from its plugin.json and hooks module. Same content in MODE-EMPLOI.md. |
factory and control read local files only (transcripts under ~/.claude/projects, the desktop app's session files, your settings) through a Python scanner (standard library) and draw the result. Dollar figures are estimates from token counts and the rate table in each mod's config.
What was kept from the earlier mods when this repo was set up (V6, Oct 2026). Only the keep rows are in this repo.
| Mod | Verdict | Why |
|---|---|---|
| factory | keep | The only view of the agent tree with models, efforts and cost. Next: feed it from hook events (agent.spawn, turn.start) instead of scanning files. |
| control | keep | The "nothing hidden" proof: every rule source, env variable and blocker in one place. Its metrics and practices still follow the V5 rules and are being rewritten for V6. |
| coordinator | merge into factory | Linked sub-conversations remove most lane collisions; only the "green PR not merged" and "waiting on you" rows are worth keeping, as a strip in factory. |
| context band (cc-control band, context-meter) | merge into a V6 band | Small and correct: context size against the hand-off threshold, read-only. Not written yet. |
| cc-control guards | drop | They refused and rewrote tool calls (send caps, spawn caps, rewrites). Blocking hooks are out. |
| ci | new (Oct 2026) | Which CI backend a repo or conversation should use, and whether it works right now, in one place instead of in rules text. |
| scenarios | new (Oct 2026) | Which model, effort and unit each role uses, with the doc that says so, instead of rules text that drifts. |
| guide | new (Oct 2026) | How mods work and how to write one, plus a card per mod that reads the mod itself. |
| cost-tracker, activity-pane, launchpad, night | drop | Wrong numbers for new model ids, superseded by factory, or built on timer wake-ups. |
| V4 pages and watcher | drop | Dead since early Oct, replaced by factory and the app's own agent view. |
Copy or symlink a mod folder to ~/.claude/mods/<name>:
git clone https://github.com/julian-passebecq/claude-mod.git
# copy
cp -r claude-mod/mods/factory ~/.claude/mods/factory
# or symlink (Windows: mklink /D, as admin or with developer mode)
ln -s "$PWD/claude-mod/mods/factory" ~/.claude/mods/factory
Then load it, either for every session with the environment (in ~/.claude/settings.json under env, folders separated by ; on Windows, : elsewhere):
"CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/factory;~/.claude/mods/control;~/.claude/mods/ci;~/.claude/mods/scenarios;~/.claude/mods/guide"
or as a dev-mod in the desktop app for one session. Python 3 must be on PATH (the scanners); gh / glab are used, when present, for PR/MR and CI state.
Each mod has a config file next to it (factory.config.json, config.json): rates, thresholds, colors, labels and paths are set there, nothing is hard-coded. factory also deep-merges ~/.claude/effort-board/factory.config.json if you have one. In control/config.json, set paths.projects_root (the folder that holds your repos) and the optional doc paths.
cp claude-mod/mods/ci/ci.config.example.json ~/.claude/ci.config.json # then fill the <PLACEHOLDERS>
python -I ~/.claude/mods/ci/scanner/ci.py show --check # same data as /ci, from any shell
python -I ~/.claude/mods/ci/scanner/ci.py route ~/code/my-repo gitlab-local --by lead
--config, then $CI_MOD_CONFIG, then ~/.claude/ci.config.json, then ci.config.json next to the mod, then the shipped example. Each backend has id, label, kind (github | gitlab), host, runner_tag, status (active | coming-soon | disabled) and notes; optional group, concurrent, minutes_month, public_only, check_runners, pending_projects. A value still in <angle brackets> counts as unset. Labels and colors are in labels.gh auth status, glab auth status, or the env variable names listed under hosts); a token value is never printed or stored. With a GitLab token: runners with the backend's tag online/offline and their running jobs (pending jobs for the projects in pending_projects). With gh: Actions availability for the repos routed to a GitHub backend. Missing tools or tokens show as unknown. Results are cached (status_cache, cache_minutes); /ci refresh checks now.routing_file (default ~/.claude/ci-routing.json), one entry per repo path or session id with backend, who set it and when. A session route wins over a repo route; a worktree inside a routed repo follows the repo. status_line and prompt_note in the config turn the status-line entry and the system-prompt note off.PATH; gh and glab are optional.deny, no rewriting of tool calls or prompts.ci also writes its routing file, on /ci route and /ci clear).gh / glab for PR state and the GitLab API for runner status (ci).python -I -m unittest discover -s mods/control/tests -p "test_*.py"
python -I -m unittest discover -s mods/ci/tests -p "test_*.py"
node --test mods/factory/tests/logic.test.mjs mods/scenarios/tests/logic.test.mjs mods/guide/tests/guide.test.mjs
node mods/guide/scripts/build.mjs # regenerate MODE-EMPLOI.md (table of contents, Nos mods cards)
# hook and pane tests, with the Claude Code test kit:
claude plugin test mods/factory
claude plugin test mods/control
claude plugin test mods/ci
claude plugin test mods/scenarios
claude plugin test mods/guide
# before every push (public repo): no private paths, tokens, e-mails or transcripts
python -I tools/confidentiality_scan.py .
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is ignored since Claude Code 2.1.287 (mods are on by default, overview). GitHub Actions (.github/workflows/tests.yml) runs the Python and node tests and the confidentiality scan; the test-kit suites need the claude CLI and run locally.
control's reference-window test replays the author's own transcripts and only runs with CONTROL_REFERENCE=1.
MIT
hooks/register.tsx 183 lines1// guide mod ("Mode d'emploi des mods"): MODE-EMPLOI.md as a pane with tabs. The first tab, Sommaire, is the whole tree
2// with a link to every tab, category and sub-section; each other tab folds its categories. The "Nos mods" cards are
3// rebuilt at each draw from the neighbouring mods' plugin.json and hooks module. Read-only: no hook on tools or agents.
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import type { Doc, ModsCatalog } from '../types'
8import { card, modsMarkdown, modulePath } from './cards'
9import { between, locate, parse, slug } from './toc'
10
11const PANE = { id: 'mods-guide', title: "Mode d'emploi des mods" }
12const SOMMAIRE = 'sommaire'
13const tabAtom = atom({ plugin: 'guide', key: 'tab' } as const, SOMMAIRE)
14const catAtom = atom({ plugin: 'guide', key: 'cat' } as const, '')
15const subAtom = atom({ plugin: 'guide', key: 'sub' } as const, '')
16
17const text = async ($: EngineInterface, path: string): Promise<string | undefined> => {
18 try {
19 return (await $.fs.read(path, { as: 'text' })) as string
20 } catch {
21 return undefined
22 }
23}
24
25/** The guide with live cards for "Nos mods". */
26const loadDoc = async ($: EngineInterface): Promise<{ doc: Doc; problem: string }> => {
27 const root = $.plugin.root.replace(/\\/g, '/')
28 const md = await text($, `${root}/MODE-EMPLOI.md`)
29 if (!md) return { doc: { title: PANE.title, intro: '', tabs: [] }, problem: `cannot read ${root}/MODE-EMPLOI.md` }
30 let live = md
31 const raw = await text($, `${root}/mods.json`)
32 if (raw) {
33 try {
34 const catalog = JSON.parse(raw) as ModsCatalog
35 const mods = root.replace(/\/[^/]+$/, '')
36 const cards = []
37 for (const entry of catalog.mods) {
38 const dir = `${mods}/${entry.dir}`
39 const manifest = await text($, `${dir}/.claude-plugin/plugin.json`)
40 const rel = modulePath(await text($, `${dir}/hooks/hooks.json`))
41 cards.push(card(entry, catalog, manifest, rel ? await text($, `${dir}/${rel}`) : undefined))
42 }
43 live = between(md, 'mods', modsMarkdown(cards, catalog))
44 } catch {}
45 }
46 return { doc: parse(live), problem: '' }
47}
48
49/** `/mods-guide [onglet ou section]`: an anchor, or the start of a title. */
50const find = (doc: Doc, args: string) => {
51 const want = slug(args)
52 if (!want) return undefined
53 const exact = locate(doc, want)
54 if (exact) return exact
55 for (const t of doc.tabs) {
56 if (t.id.startsWith(want)) return { tab: t.id, cat: '', sub: '' }
57 for (const c of t.cats) {
58 if (c.id.startsWith(want)) return { tab: t.id, cat: c.id, sub: '' }
59 for (const s of c.subs) if (s.id.startsWith(want)) return { tab: t.id, cat: c.id, sub: s.id }
60 }
61 }
62 return undefined
63}
64
65const go = async ($: EngineInterface, to: { tab: string; cat: string; sub: string }) => {
66 await update($, tabAtom, () => to.tab)
67 await update($, catAtom, () => to.cat)
68 await update($, subAtom, () => to.sub)
69}
70
71export const register: Register = on => {
72 on('session.start', async ($, e, next) => {
73 await $.command.register({
74 name: 'mods-guide',
75 description: "Mode d'emploi des mods : onglets Bases, Événements, Interface, Configuration, Installer, Tester, Écrire un mod, Référence, Nos mods, FAQ",
76 argumentHint: '[onglet ou section]',
77 })
78 return next(e)
79 })
80
81 on('command.run', { command: 'mods-guide' }, async ($, e) => {
82 const { doc, problem } = await loadDoc($)
83 if (problem) return { text: `mods-guide: ${problem}` }
84 const args = (e.args ?? '').trim()
85 const to = args ? find(doc, args) : { tab: SOMMAIRE, cat: '', sub: '' }
86 if (!to) return { text: `mods-guide: pas de section « ${args} ». Onglets : ${doc.tabs.map(t => t.title).join(', ')}` }
87 await go($, to)
88 await $.ui.open({ id: PANE.id, title: PANE.title })
89 const where = to.tab === SOMMAIRE ? 'Sommaire' : doc.tabs.find(t => t.id === to.tab)?.title
90 return { text: `mods-guide: ouvert sur ${where}${to.cat ? ` › ${to.cat}` : ''}${to.sub ? ` › ${to.sub}` : ''}` }
91 })
92
93 on('ui.render', { component: 'Pane', requestId: 'mods-guide' }, async ($, e) => {
94 const { Box, Text, Button, Markdown } = $.ui.resolve(e)
95 const { doc, problem } = await loadDoc($)
96 const tab = await read($, tabAtom)
97 const cat = await read($, catAtom)
98 const sub = await read($, subAtom)
99 const current = doc.tabs.find(t => t.id === tab)
100 const on_ = (yes: boolean) => (yes ? {} : { plain: true as const })
101
102 const tabs = (
103 <Box flexDirection="row" gap={1} flexWrap="wrap">
104 <Button key="tab-sommaire" {...on_(!current)} onPress={() => go($, { tab: SOMMAIRE, cat: '', sub: '' })}>
105 Sommaire
106 </Button>
107 {doc.tabs.map(t => (
108 <Button key={`tab-${t.id}`} {...on_(t.id === current?.id)} onPress={() => go($, { tab: t.id, cat: '', sub: '' })}>
109 {t.title}
110 </Button>
111 ))}
112 </Box>
113 )
114
115 if (problem) return <Text color="error">{problem}</Text>
116
117 if (!current) {
118 return (
119 <Box flexDirection="column" gap={1}>
120 {tabs}
121 <Text bold>{`Sommaire général · ${doc.title}`}</Text>
122 {doc.intro ? <Markdown text={doc.intro.replace(/^Doc officielle.*$/m, '').trim()} /> : null}
123 <Box flexDirection="column">
124 {doc.tabs.map(t => (
125 <Box flexDirection="column">
126 <Button key={`go-${t.id}`} plain bold onPress={() => go($, { tab: t.id, cat: '', sub: '' })}>
127 {t.title}
128 </Button>
129 {t.cats.map(c => (
130 <Box flexDirection="column" paddingLeft={2}>
131 <Button key={`go-${c.id}`} plain onPress={() => go($, { tab: t.id, cat: c.id, sub: '' })}>
132 {c.title}
133 </Button>
134 {c.subs.map(s => (
135 <Box paddingLeft={2}>
136 <Button key={`go-${s.id}`} plain dimColor onPress={() => go($, { tab: t.id, cat: c.id, sub: s.id })}>
137 {s.title}
138 </Button>
139 </Box>
140 ))}
141 </Box>
142 ))}
143 </Box>
144 ))}
145 </Box>
146 </Box>
147 )
148 }
149
150 return (
151 <Box flexDirection="column" gap={1}>
152 {tabs}
153 <Text bold>{current.title}</Text>
154 {current.intro ? <Markdown text={current.intro} /> : null}
155 {current.cats.map(c => {
156 const open = c.id === cat
157 return (
158 <Box flexDirection="column">
159 <Button key={`cat-${c.id}`} plain bold onPress={() => go($, { tab: current.id, cat: open ? '' : c.id, sub: '' })}>
160 {`${open ? '▾' : '▸'} ${c.title}${open ? '' : ` (${c.subs.length})`}`}
161 </Button>
162 {open && (
163 <Box flexDirection="column" paddingLeft={2} gap={1}>
164 {c.body ? <Markdown text={c.body} /> : null}
165 {c.subs.map(s => (
166 <Box flexDirection="column">
167 <Text bold {...(s.id === sub ? { color: 'success' as const } : {})}>{`${s.id === sub ? '→ ' : ''}${s.title}`}</Text>
168 {s.body ? <Markdown text={s.body} /> : null}
169 </Box>
170 ))}
171 </Box>
172 )}
173 </Box>
174 )
175 })}
176 <Button key="back" plain dimColor onPress={() => go($, { tab: SOMMAIRE, cat: '', sub: '' })}>
177 ← Sommaire
178 </Button>
179 </Box>
180 )
181 })
182}
183hooks/cards.ts 106 lines1// "Nos mods": one card per mod, built from what the mod itself ships (plugin.json, hooks/hooks.json, its hooks
2// module's source) plus the few facts only mods.json knows (state, data read, tests). Pure: no `$`.
3import type { Card, CatalogEntry, ModsCatalog } from '../types'
4
5type Manifest = {
6 name?: string
7 version?: string
8 description?: string
9 userConfig?: Record<string, { title?: string; default?: unknown; options?: unknown[] }>
10}
11
12const uniq = (xs: string[]) => [...new Set(xs)]
13
14/** Events the module hooks: `on('event'` / `on("event"`. */
15export const eventsIn = (src: string) => uniq([...src.matchAll(/\bon\(\s*['"]([\w.*-]+)['"]/g)].map(m => m[1]!))
16
17/** Slash commands the module registers: `$.command.register({ name: 'x'`. */
18export const commandsIn = (src: string) =>
19 uniq([...src.matchAll(/\$\.command\.register\(\s*\{[^}]*?\bname:\s*['"]([\w-]+)['"]/gs)].map(m => `/${m[1]}`))
20
21/** Whether the source can refuse anything: a `deny` answer or a `drop` of a prompt. */
22export const canBlock = (src: string) => /\{\s*deny\s*:|\bdeny:\s*['"`]|\{\s*drop\s*:/.test(src)
23
24/** Whether it draws: a Pane, a band above the prompt, a status line, toasts. */
25export function drawsIn(src: string): string[] {
26 const out: string[] = []
27 if (/component:\s*['"]Pane['"]/.test(src)) out.push('panneau')
28 if (/component:\s*['"]AbovePrompt['"]/.test(src)) out.push('bandeau')
29 if (/\$\.ui\.status\(/.test(src)) out.push('ligne de statut')
30 if (/\$\.ui\.toast\(/.test(src)) out.push('toasts')
31 if (/['"]prompt\.compose['"]/.test(src)) out.push('note dans le prompt système')
32 return out
33}
34
35/** The hooks module path a hooks.json names, relative to the mod folder. */
36export function modulePath(hooksJson: string | undefined): string | undefined {
37 try {
38 const first = (JSON.parse(hooksJson ?? '') as { modules?: string[] }).modules?.[0]
39 return first ? `hooks/${first.replace(/^\.\//, '')}` : undefined
40 } catch {
41 return undefined
42 }
43}
44
45/** One card. `manifest` and `source` are undefined for a mod that is not in this folder (planned, or not installed here). */
46export function card(entry: CatalogEntry, catalog: ModsCatalog, manifest?: string, source?: string): Card {
47 let m: Manifest = {}
48 try {
49 m = manifest ? (JSON.parse(manifest) as Manifest) : {}
50 } catch {}
51 const src = source ?? ''
52 const options = Object.entries(m.userConfig ?? {}).map(([key, f]) => {
53 const choices = Array.isArray(f.options) ? ` (${f.options.join(' | ')})` : ''
54 return `${key}${f.title ? ` « ${f.title} »` : ''}${choices}, défaut ${JSON.stringify(f.default ?? '')}`
55 })
56 const found = Boolean(manifest)
57 return {
58 dir: entry.dir,
59 name: m.name ?? entry.name ?? entry.dir,
60 version: m.version ?? '',
61 description: m.description ?? entry.description ?? '',
62 state: catalog.states[entry.state] ?? entry.state,
63 found,
64 events: eventsIn(src),
65 commands: commandsIn(src),
66 draws: drawsIn(src),
67 blocks: canBlock(src),
68 options,
69 data: entry.data ?? '',
70 note: entry.note ?? '',
71 tests: entry.tests ?? [],
72 enable: found ? `ajoute <dépôt>/mods/${entry.dir} à CLAUDE_CODE_PLUGIN_DIRS, ou lance claude --plugin-dir mods/${entry.dir}` : 'pas encore dans le dépôt',
73 }
74}
75
76/** The same card as Markdown, for MODE-EMPLOI.md. */
77export function cardMarkdown(c: Card): string {
78 const rows = [
79 `#### ${c.name}`,
80 '',
81 `${c.description || '(pas de description)'}`,
82 '',
83 `- **État** : ${c.state}${c.version ? ` · version ${c.version}` : ''}${c.note ? ` · ${c.note}` : ''}`,
84 `- **Fonctionnement** : ${c.found ? `événements ${c.events.map(e => `\`${e}\``).join(', ') || 'aucun'}` : 'pas encore écrit'} · lit : ${c.data || '—'} · ${c.found ? (c.blocks ? '⚠ peut refuser un appel' : 'aucun blocage') : 'sans blocage prévu'}`,
85 `- **Fonctionnalités** : commandes ${c.commands.map(x => `\`${x}\``).join(', ') || 'aucune'} · affiche : ${c.draws.join(', ') || 'rien'} · options userConfig : ${c.options.length ? c.options.join(' ; ') : 'aucune'}`,
86 `- **Activer** : ${c.found ? `\`<dépôt>/mods/${c.dir}\` dans \`CLAUDE_CODE_PLUGIN_DIRS\`, ou \`claude --plugin-dir mods/${c.dir}\`` : c.enable}`,
87 `- **Tests** : ${c.tests.length ? c.tests.map(t => `\`${t}\``).join(' · ') : 'aucun pour l’instant'}`,
88 ]
89 return rows.join('\n')
90}
91
92/** The whole "Nos mods" block: one card per mod, then the archived / rejected table. */
93export function modsMarkdown(cards: Card[], catalog: ModsCatalog): string {
94 const parts = ['### Fiches', ...cards.map(cardMarkdown)]
95 parts.push(
96 [
97 '### Archivés / rejetés',
98 '',
99 '| Mod | Verdict | Pourquoi |',
100 '|---|---|---|',
101 ...catalog.archived.map(a => `| ${a.name} | ${a.verdict} | ${a.why} |`),
102 ].join('\n'),
103 )
104 return parts.join('\n\n')
105}
106hooks/toc.ts 111 lines1// MODE-EMPLOI.md as a tree: `##` = a tab, `###` = a category, `####` = a sub-section.
2// Pure: the pane, the build script (scripts/build.mjs) and node --test all use it.
3import type { Doc, Section, Tab } from '../types'
4
5/** GitHub's heading anchor: lower case, punctuation dropped (letters of any script kept), spaces to dashes. */
6export function slug(title: string): string {
7 return title
8 .trim()
9 .toLowerCase()
10 .replace(/[^\p{L}\p{N}\s_-]/gu, '')
11 .replace(/\s/g, '-')
12}
13
14const TOC = /<!-- toc:start -->[\s\S]*?<!-- toc:end -->/
15
16/** Splits the guide into tabs, categories and sub-sections. The table of contents block is left out of every body. */
17export function parse(md: string): Doc {
18 const text = md.replace(/\r\n/g, '\n').replace(TOC, '')
19 const doc: Doc = { title: '', intro: '', tabs: [] }
20 let tab: Tab | undefined
21 let cat: Section | undefined
22 let sub: Section | undefined
23 const body: string[] = []
24 let fence = false
25 const flush = () => {
26 const t = body.join('\n').trim()
27 body.length = 0
28 if (sub) sub.body = t
29 else if (cat) cat.body = t
30 else if (tab) tab.intro = t
31 else doc.intro = t
32 }
33 for (const line of text.split('\n')) {
34 if (/^\s*```/.test(line)) fence = !fence
35 const m = fence ? null : /^(#{1,4})\s+(.*?)\s*$/.exec(line)
36 if (!m) {
37 body.push(line)
38 continue
39 }
40 const level = m[1]!.length
41 const title = m[2]!
42 flush()
43 if (level === 1) doc.title = title
44 else if (level === 2) {
45 tab = { id: slug(title), title, intro: '', cats: [] }
46 doc.tabs.push(tab)
47 cat = undefined
48 sub = undefined
49 } else if (level === 3 && tab) {
50 cat = { id: slug(title), title, body: '', subs: [] }
51 tab.cats.push(cat)
52 sub = undefined
53 } else if (level === 4 && cat) {
54 sub = { id: slug(title), title, body: '', subs: [] }
55 cat.subs.push(sub)
56 } else body.push(line)
57 }
58 flush()
59 return doc
60}
61
62/** Every anchor of the tree, in order; the build script refuses a duplicate. */
63export function anchors(doc: Doc): string[] {
64 const out: string[] = []
65 for (const t of doc.tabs) {
66 out.push(t.id)
67 for (const c of t.cats) {
68 out.push(c.id)
69 for (const s of c.subs) out.push(s.id)
70 }
71 }
72 return out
73}
74
75/** The table of contents as nested Markdown links to the same anchors. */
76export function tocMarkdown(doc: Doc): string {
77 const lines: string[] = []
78 for (const t of doc.tabs) {
79 lines.push(`- [${t.title}](#${t.id})`)
80 for (const c of t.cats) {
81 lines.push(` - [${c.title}](#${c.id})`)
82 for (const s of c.subs) lines.push(` - [${s.title}](#${s.id})`)
83 }
84 }
85 return lines.join('\n')
86}
87
88/** Puts a fresh table of contents between the markers (the markers must be there). */
89export function withToc(md: string): string {
90 const toc = tocMarkdown(parse(md))
91 return md.replace(TOC, `<!-- toc:start -->\n${toc}\n<!-- toc:end -->`)
92}
93
94/** Replaces what lies between `<!-- name:start -->` and `<!-- name:end -->`. */
95export function between(md: string, name: string, content: string): string {
96 const re = new RegExp(`<!-- ${name}:start -->[\\s\\S]*?<!-- ${name}:end -->`)
97 return md.replace(re, `<!-- ${name}:start -->\n${content}\n<!-- ${name}:end -->`)
98}
99
100/** Where a press on a Sommaire link leads: the tab, the category to unfold and the sub-section to mark. */
101export function locate(doc: Doc, anchor: string): { tab: string; cat: string; sub: string } | undefined {
102 for (const t of doc.tabs) {
103 if (t.id === anchor) return { tab: t.id, cat: '', sub: '' }
104 for (const c of t.cats) {
105 if (c.id === anchor) return { tab: t.id, cat: c.id, sub: '' }
106 for (const s of c.subs) if (s.id === anchor) return { tab: t.id, cat: c.id, sub: s.id }
107 }
108 }
109 return undefined
110}
111types/index.d.ts 52 lines1// Shapes of the guide mod: the parsed MODE-EMPLOI.md, mods.json, a mod card, and its `$.state` values.
2
3export type Section = { id: string; title: string; body: string; subs: Section[] }
4export type Tab = { id: string; title: string; intro: string; cats: Section[] }
5export type Doc = { title: string; intro: string; tabs: Tab[] }
6
7export type CatalogEntry = {
8 dir: string
9 name?: string
10 state: string
11 description?: string
12 data?: string
13 note?: string
14 tests?: string[]
15}
16export type ModsCatalog = {
17 states: Record<string, string>
18 mods: CatalogEntry[]
19 archived: { name: string; verdict: string; why: string }[]
20}
21export type Card = {
22 dir: string
23 name: string
24 version: string
25 description: string
26 state: string
27 /** the mod's plugin.json was found beside this one */
28 found: boolean
29 events: string[]
30 commands: string[]
31 draws: string[]
32 blocks: boolean
33 options: string[]
34 data: string
35 note: string
36 tests: string[]
37 enable: string
38}
39
40declare module 'claude-code' {
41 interface PluginState {
42 guide: {
43 /** tab shown: 'sommaire' or a `##` heading's anchor */
44 tab: string
45 /** category unfolded in that tab ('' = none) */
46 cat: string
47 /** sub-section marked after a Sommaire link ('' = none) */
48 sub: string
49 }
50 }
51}
52