@cmodjs/core 3.1.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +38 -21
- package/docs/dependencies.md +27 -20
- package/docs/index.md +21 -17
- package/docs/install-steps.md +37 -29
- package/docs/jobs.md +25 -52
- package/docs/mod.md +93 -65
- package/docs/options.md +2 -2
- package/docs/permissions.md +4 -4
- package/docs/session.md +14 -12
- package/docs/testing.md +55 -32
- package/docs/ui.md +255 -263
- package/hotkeys.d.ts +22 -0
- package/hotkeys.d.ts.map +1 -0
- package/hotkeys.js +26 -0
- package/installer.d.ts +1 -1
- package/installer.d.ts.map +1 -1
- package/installer.js +11 -6
- package/jobs/check.d.ts +2 -1
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts +2 -1
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +1 -1
- package/jobs/permissions.d.ts +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +2 -2
- package/jobs/program.js +1 -1
- package/jobs/prompt.d.ts +2 -1
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +20 -13
- package/jobs/schedule.d.ts +2 -1
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +15 -1
- package/jobs/slash-command.d.ts +6 -2
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +6 -5
- package/jobs/tool.d.ts +6 -4
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -3
- package/mod.d.ts +63 -42
- package/mod.d.ts.map +1 -1
- package/mod.js +11 -1
- package/package.json +1 -1
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +41 -31
- package/register.d.ts.map +1 -1
- package/register.js +1 -2
- package/runtime/announce.d.ts +1 -1
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +6 -7
- package/runtime/before.d.ts +2 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +1 -1
- package/runtime/claude.d.ts +0 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +4 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +42 -14
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +2 -1
- package/runtime/engine.d.ts +5 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.js +2 -2
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hotkeys.d.ts +15 -0
- package/runtime/hotkeys.d.ts.map +1 -0
- package/runtime/hotkeys.js +58 -0
- package/runtime/installer.d.ts +5 -2
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +23 -12
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +190 -141
- package/runtime/options.js +2 -2
- package/runtime/pages.d.ts +28 -0
- package/runtime/pages.d.ts.map +1 -0
- package/runtime/pages.js +98 -0
- package/runtime/parts.d.ts +14 -0
- package/runtime/parts.d.ts.map +1 -0
- package/runtime/parts.js +59 -0
- package/runtime/permission-check.d.ts +2 -2
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/programs.js +1 -1
- package/runtime/router.d.ts +2 -2
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +1 -1
- package/runtime/sessions.d.ts +10 -9
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +14 -5
- package/runtime/state.d.ts +2 -1
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +1 -0
- package/runtime/toaster.d.ts +1 -1
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +10 -4
- package/runtime/ui.d.ts +87 -5
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +374 -109
- package/runtime/watches.d.ts +9 -0
- package/runtime/watches.d.ts.map +1 -0
- package/runtime/watches.js +73 -0
- package/runtime/widgets.d.ts +48 -0
- package/runtime/widgets.d.ts.map +1 -0
- package/runtime/widgets.js +219 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -2
- package/session/session.d.ts +11 -5
- package/session/session.d.ts.map +1 -1
- package/src/hotkeys.ts +51 -0
- package/src/installer.ts +8 -4
- package/src/jobs/check.ts +4 -3
- package/src/jobs/context.ts +3 -2
- package/src/jobs/permissions.ts +4 -3
- package/src/jobs/program.ts +1 -1
- package/src/jobs/prompt.ts +23 -14
- package/src/jobs/schedule.ts +16 -2
- package/src/jobs/slash-command.ts +10 -7
- package/src/jobs/tool.ts +7 -5
- package/src/mod.ts +59 -47
- package/src/records.ts +46 -31
- package/src/register.ts +2 -3
- package/src/runtime/announce.ts +6 -7
- package/src/runtime/before.ts +4 -4
- package/src/runtime/claude.ts +0 -1
- package/src/runtime/conversations.ts +66 -20
- package/src/runtime/dependencies.ts +2 -1
- package/src/runtime/engine.ts +5 -0
- package/src/runtime/grants.ts +2 -2
- package/src/runtime/hooks.ts +1 -0
- package/src/runtime/hotkeys.ts +80 -0
- package/src/runtime/installer.ts +24 -12
- package/src/runtime/lifecycle.ts +189 -136
- package/src/runtime/options.ts +2 -2
- package/src/runtime/pages.ts +127 -0
- package/src/runtime/parts.ts +70 -0
- package/src/runtime/permission-check.ts +2 -2
- package/src/runtime/programs.ts +1 -1
- package/src/runtime/router.ts +3 -3
- package/src/runtime/sessions.ts +24 -15
- package/src/runtime/state.ts +3 -1
- package/src/runtime/toaster.ts +10 -4
- package/src/runtime/ui.ts +407 -115
- package/src/runtime/watches.ts +75 -0
- package/src/runtime/widgets.ts +287 -0
- package/src/session/context/turn/message/message.ts +7 -2
- package/src/session/session.ts +10 -6
- package/src/testing/fake-claude.ts +9 -6
- package/src/testing/fake-elements.ts +7 -4
- package/src/testing/fake-sessions.ts +6 -4
- package/src/testing.ts +203 -46
- package/src/ui/areas.ts +27 -0
- package/src/ui/elements.ts +35 -0
- package/src/ui/markdown.ts +18 -18
- package/src/ui/panes.ts +73 -0
- package/src/ui/styles.ts +53 -0
- package/src/utils/subscription.ts +1 -0
- package/testing/fake-claude.d.ts +0 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +9 -5
- package/testing/fake-elements.js +7 -4
- package/testing/fake-sessions.d.ts +2 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +1 -0
- package/testing.d.ts +21 -6
- package/testing.d.ts.map +1 -1
- package/testing.js +206 -45
- package/ui/areas.d.ts +29 -0
- package/ui/areas.d.ts.map +1 -0
- package/ui/areas.js +6 -0
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +36 -0
- package/ui/markdown.d.ts +14 -14
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +11 -11
- package/ui/panes.d.ts +71 -0
- package/ui/panes.d.ts.map +1 -0
- package/ui/panes.js +16 -0
- package/ui/styles.d.ts +44 -0
- package/ui/styles.d.ts.map +1 -0
- package/ui/styles.js +24 -0
- package/utils/subscription.d.ts +4 -0
- package/utils/subscription.d.ts.map +1 -0
- package/jobs/status-line.d.ts +0 -6
- package/jobs/status-line.d.ts.map +0 -1
- package/jobs/status-line.js +0 -39
- package/src/jobs/status-line.ts +0 -44
- package/src/ui/define-pane.ts +0 -27
- package/src/ui/places.ts +0 -49
- package/src/utils/disposable.ts +0 -3
- package/ui/define-pane.d.ts +0 -15
- package/ui/define-pane.d.ts.map +0 -1
- package/ui/define-pane.js +0 -12
- package/ui/places.d.ts +0 -43
- package/ui/places.d.ts.map +0 -1
- package/ui/places.js +0 -31
- package/utils/disposable.d.ts +0 -4
- package/utils/disposable.d.ts.map +0 -1
- /package/utils/{disposable.js → subscription.js} +0 -0
package/docs/jobs.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Jobs
|
|
2
2
|
|
|
3
|
-
A job is a ready-made feature of a mod: a slash command, a tool, permission rules, a check, a prompt, a
|
|
3
|
+
A job is a ready-made feature of a mod: a slash command, a tool, permission rules, a check, a prompt, a schedule, or a background program. Add each in `setup` with `mod.use`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
mod.use<Handle>(job: Job<Handle, State>): Handle
|
|
@@ -10,7 +10,7 @@ type Job<Handle, State> = (context: JobContext<State>) => Handle
|
|
|
10
10
|
|
|
11
11
|
Each job function, such as `slashCommand(options)`, returns a `Job`. `mod.use` runs the job and returns its handle. A job that has nothing to hand back returns `void`.
|
|
12
12
|
|
|
13
|
-
A job's bad options throw at one of two times. `check`, `permissions`, `prompt`, and `schedule` check their options when the job function is called. `slashCommand`, `tool`,
|
|
13
|
+
A job's bad options throw at one of two times. `check`, `permissions`, `prompt`, and `schedule` check their options when the job function is called. `slashCommand`, `tool`, and `program` check theirs when the job runs, at `mod.use`, so the error surfaces as `its setup function threw: …`.
|
|
14
14
|
|
|
15
15
|
| Job | Import | Adds |
|
|
16
16
|
| --- | --- | --- |
|
|
@@ -20,7 +20,6 @@ A job's bad options throw at one of two times. `check`, `permissions`, `prompt`,
|
|
|
20
20
|
| `check` | `jobs/check.js` | A command that runs after matching calls, and reports a failure to Claude |
|
|
21
21
|
| `prompt` | `jobs/prompt.js` | Text Claude reads: kept current with the mod's state, before matching prompts, or after matching calls |
|
|
22
22
|
| `schedule` | `jobs/schedule.js` | A prompt Claude Code sends Claude on a cron schedule, kept current with the mod's state |
|
|
23
|
-
| `statusLine` | `jobs/status-line.js` | A status line |
|
|
24
23
|
| `program` | `jobs/program.js` | A background program the mod talks to over HTTP |
|
|
25
24
|
|
|
26
25
|
Each job announces itself. The first time a version starts, Claude Mod Manager (cmod) logs what the mod added, such as `tasks added /tasks, the open_ticket tool and 2 permission rules.`
|
|
@@ -34,7 +33,6 @@ A job gives each function it calls a deadline. The whole function must answer wi
|
|
|
34
33
|
| `slashCommand`'s `reply` | 30 s | The command shows `/<name> failed: /<name> passed the 30 s deadline of slashCommand`. |
|
|
35
34
|
| `tool`'s `execute` | 30 s | The call is denied with `The <name> tool failed: the <name> tool passed the 30 s deadline of tool`. |
|
|
36
35
|
| `prompt`'s `prompt` | 5 s | The prompt adds nothing, and the reason goes to Claude Code's debug log. |
|
|
37
|
-
| `statusLine`'s `text` | 2 s | The status line keeps its last text, and the reason goes to Claude Code's debug log. |
|
|
38
36
|
| `permissions`' `when` | 2 s | The rule matches, and its reason adds `Its when check failed: it passed the 2 s deadline of permissions`. |
|
|
39
37
|
| `check`'s `run` | `timeoutMs`, 60 s by default, 10 minutes at most | The call's result says the command passed the deadline. |
|
|
40
38
|
|
|
@@ -51,21 +49,19 @@ slashCommand<State>(options: {
|
|
|
51
49
|
readonly argumentHint?: string
|
|
52
50
|
readonly immediate?: true
|
|
53
51
|
readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => CommandReply | void | Promise<CommandReply | void>
|
|
54
|
-
}): Job<
|
|
52
|
+
}): Job<SlashCommand, State>
|
|
55
53
|
|
|
56
54
|
type CommandReply = string | { text?: string; context?: string } | undefined
|
|
55
|
+
type SlashCommand = { readonly name: string; remove(): void }
|
|
57
56
|
```
|
|
58
57
|
|
|
59
58
|
- `name` is the command without the slash: 1 to 64 letters, digits, `_`, or `-`. The person types `/<name>`. Another name throws `<mod>: "<name>" is not a slash command name.`
|
|
60
59
|
- `description` is the line the typeahead and `/help` show.
|
|
61
60
|
- `argumentHint` is drawn dim after the name, such as `[path]`.
|
|
62
61
|
- `immediate: true` runs the command at once while Claude is still working. Without it, the command waits for the turn to end.
|
|
63
|
-
- A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies. `
|
|
64
|
-
- A
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
mod.use(slashCommand({ name: 'mode', description: 'Switch to the next mode', immediate: true, reply: (_input, mod) => void (mod.state.session.mode = next(mod.state.session.mode)) }))
|
|
68
|
-
```
|
|
62
|
+
- A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies. `remove()` on the handle stops the mod answering the command.
|
|
63
|
+
- A command's `reply` runs inside the person's own input, so a page or pane it opens shows at any width ([ui.md](ui.md#the-sidebar-and-its-pages)).
|
|
64
|
+
- A key combo is a hotkey, not a command ([mod.md](mod.md#hotkeys)).
|
|
69
65
|
- `reply` gets `args` and `positionals`. `args` is everything after the name as Claude Code passes it, and `''` for a bare `/<name>`. cmod hands it on unchanged. `positionals` is that text split into words the way a shell splits them, so extra spaces never reach it. `tested.type('/todo milk')` drops the spaces after the name.
|
|
70
66
|
- A `string` reply, or `text`, shows as the command's output. `context` is text Claude reads after the output, and the person never sees. `undefined` shows nothing.
|
|
71
67
|
- A `reply` that throws shows `/<name> failed: <error>`.
|
|
@@ -103,10 +99,12 @@ tool<Schema, State>(options: {
|
|
|
103
99
|
readonly description: string
|
|
104
100
|
readonly inputSchema?: Schema
|
|
105
101
|
readonly execute: (input: FromSchema<Schema>, mod: Mod<State>) => unknown
|
|
106
|
-
}): Job<
|
|
102
|
+
}): Job<Tool, State>
|
|
103
|
+
|
|
104
|
+
type Tool = { readonly name: string; remove(): void }
|
|
107
105
|
```
|
|
108
106
|
|
|
109
|
-
- `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name. `
|
|
107
|
+
- `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name. `remove()` on the handle stops the mod answering the tool.
|
|
110
108
|
- `description` tells Claude what the tool does.
|
|
111
109
|
- `inputSchema` is a JSON Schema with `type: 'object'`. It types `execute`'s `input`, so write it inline. cmod checks each call's input against it, draft 2020-12, and denies an input that does not fit with the list of its errors. Its properties cannot be named `tool`, `tool_use_id`, `consent`, or `agentId`, which Claude Code keeps for itself.
|
|
112
110
|
- `execute` returns the result Claude reads. A `string` and an array of content blocks go to Claude as they are. Any other value goes as JSON. `undefined` returns nothing.
|
|
@@ -321,35 +319,6 @@ export const loop = defineMod({
|
|
|
321
319
|
})
|
|
322
320
|
```
|
|
323
321
|
|
|
324
|
-
## statusLine
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
statusLine<State>(options: {
|
|
328
|
-
readonly text: (usage: Usage, mod: Mod<State>) => string | undefined | Promise<string | undefined>
|
|
329
|
-
readonly interval?: number
|
|
330
|
-
}): Job<void, State>
|
|
331
|
-
|
|
332
|
-
type Usage = { model: string; context: { tokens?: number; window: number; percent?: number }; cost?: { usd: number } }
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
- `text` returns the status line's text, or `undefined` to clear it.
|
|
336
|
-
- cmod calls `text` when the mod starts, every `interval` milliseconds (10000 by default), and whenever Claude Code measures the session. It sends the text only when it changed.
|
|
337
|
-
- A `text` that throws keeps the last text, and the error goes to Claude Code's debug log.
|
|
338
|
-
- A mod has one status line. A second `statusLine` throws: join the texts in one.
|
|
339
|
-
- An `interval` below 1 throws.
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
343
|
-
import { statusLine } from '../node_modules/@cmodjs/core/jobs/status-line.js'
|
|
344
|
-
|
|
345
|
-
export const usage = defineMod({
|
|
346
|
-
name: 'usage',
|
|
347
|
-
setup(mod) {
|
|
348
|
-
mod.use(statusLine({ text: ({ model, context }) => `${model} ${context.percent ?? 0}%` }))
|
|
349
|
-
},
|
|
350
|
-
})
|
|
351
|
-
```
|
|
352
|
-
|
|
353
322
|
## program
|
|
354
323
|
|
|
355
324
|
It needs `"run"` with the program's name. Without the grant the program stops at once and names the line to add ([permissions.md](permissions.md)).
|
|
@@ -403,29 +372,33 @@ A `Job` is a function. `mod.use` calls it with a `JobContext` and returns what i
|
|
|
403
372
|
type JobContext<State> = {
|
|
404
373
|
readonly mod: Mod<State>
|
|
405
374
|
announce(feature: string): void
|
|
406
|
-
reserveName(type: string, name: string, taken: string):
|
|
375
|
+
reserveName(type: string, name: string, taken: string): Reservation
|
|
407
376
|
}
|
|
377
|
+
|
|
378
|
+
type Reservation = { remove(): void }
|
|
408
379
|
```
|
|
409
380
|
|
|
410
|
-
- `mod` is the mod that uses the job. A job builds on `mod.every`, `mod
|
|
381
|
+
- `mod` is the mod that uses the job. A job builds on `mod.every`, the `add` calls of `mod`, and the imports from `mod.js`, such as the methods of `sessions`, as `setup` does ([mod.md](mod.md)).
|
|
411
382
|
- `announce` adds a phrase to the list cmod logs when the mod first starts.
|
|
412
|
-
- `reserveName` throws `taken` when the mod already reserved that `type` and `name`, so a job refuses a duplicate. `
|
|
383
|
+
- `reserveName` throws `taken` when the mod already reserved that `type` and `name`, so a job refuses a duplicate. `remove()` on what it returns frees the name again.
|
|
413
384
|
|
|
414
|
-
A job that hands back a feature the mod turns off returns a `
|
|
385
|
+
A job that hands back a feature the mod turns off returns a handle with `remove()`, as `slashCommand` and `tool` do:
|
|
415
386
|
|
|
416
387
|
```ts
|
|
417
|
-
import { defineMod, sessions, type
|
|
388
|
+
import { defineMod, sessions, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
389
|
+
|
|
390
|
+
type EditCounter = { count(): number; remove(): void }
|
|
418
391
|
|
|
419
|
-
const editCounter = (tool: string): Job<
|
|
392
|
+
const editCounter = (tool: string): Job<EditCounter> => ({ mod, announce, reserveName }) => {
|
|
420
393
|
const reserved = reserveName('editCounter', tool, `${mod.name}: edits of ${tool} are already counted.`)
|
|
421
394
|
let edits = 0
|
|
422
395
|
const listening = sessions.current.onToolCall({ name: tool }, () => void (edits += 1))
|
|
423
396
|
announce(`a count of ${tool} calls`)
|
|
424
397
|
return {
|
|
425
398
|
count: () => edits,
|
|
426
|
-
|
|
427
|
-
listening.
|
|
428
|
-
reserved.
|
|
399
|
+
remove() {
|
|
400
|
+
listening.remove()
|
|
401
|
+
reserved.remove()
|
|
429
402
|
},
|
|
430
403
|
}
|
|
431
404
|
}
|
|
@@ -434,7 +407,7 @@ export const counter = defineMod({
|
|
|
434
407
|
name: 'counter',
|
|
435
408
|
setup(mod) {
|
|
436
409
|
const writes = mod.use(editCounter('Write'))
|
|
437
|
-
sessions.current.onTurnEnd(() => mod.
|
|
410
|
+
sessions.current.onTurnEnd(() => mod.toast(`${writes.count()} files written so far`))
|
|
438
411
|
},
|
|
439
412
|
})
|
|
440
413
|
```
|
package/docs/mod.md
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
# Define a mod
|
|
2
2
|
|
|
3
|
-
`defineMod` describes a mod: its name, its state,
|
|
3
|
+
`defineMod` describes a mod: its name, its state, its options, its hotkeys, and the `setup` function that adds everything to Claude Code and returns the methods it offers other mods. `registerMod` registers the definition with Claude Code.
|
|
4
4
|
|
|
5
5
|
## defineMod
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
defineMod<State, Name, Options>(definition: ModDefinition<State, Name, Options>): ModDefinition<State, Name, Options>
|
|
8
|
+
defineMod<State, Name, Options, Hotkeys>(definition: ModDefinition<State, Name, Options, Hotkeys>): ModDefinition<State, Name, Options, Hotkeys>
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
type ModDefinition<State, Name, Options> = {
|
|
12
|
+
type ModDefinition<State, Name, Options, Hotkeys> = {
|
|
13
13
|
readonly name: Name
|
|
14
14
|
readonly state?: State
|
|
15
15
|
readonly options?: Options
|
|
16
|
-
readonly
|
|
16
|
+
readonly hotkeys?: Hotkeys
|
|
17
17
|
readonly installer?: readonly Step<State>[]
|
|
18
|
-
setup(mod: Mod<State, Options>):
|
|
18
|
+
setup(mod: Mod<State, Options, Hotkeys>): Api | void | Promise<Api | void>
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- `name` is the mod's name. Use the `name` in `.claude-plugin/plugin.json`. Claude Mod Manager (cmod) keys the saved state and the options by this name, and the data folder and the Skill overrides by the plugin's name, so the two must match. `defineMod` throws `defineMod: the mod needs a name, such as the name in .claude-plugin/plugin.json.` for a blank name.
|
|
23
23
|
- `state` holds the starting values, grouped by how long they last. [state.md](state.md) explains the groups.
|
|
24
24
|
- `options` declares what a person sets, such as a token, and types `mod.options`. [options.md](options.md) explains them.
|
|
25
|
-
- `
|
|
25
|
+
- `hotkeys` declares the key combos the mod listens for, each with a title and a default combo the person can change ([Hotkeys](#hotkeys)).
|
|
26
26
|
- `installer` lists the steps the person finishes in Claude Code after the permissions and the options, such as signing in. [install-steps.md](install-steps.md#steps-in-claude-code) explains them.
|
|
27
|
-
- `setup` runs once when the mod starts. It adds listeners, panes,
|
|
27
|
+
- `setup` runs once when the mod starts. It adds listeners, pages, panes, widgets, styles, and jobs. It may be `async`. What it returns is the mod's API: an object of methods other mods call. Return nothing for a mod that offers none. [dependencies.md](dependencies.md) explains it.
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
30
|
import { defineMod, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -37,7 +37,7 @@ export const greeter = defineMod({
|
|
|
37
37
|
sessions.current.onPrompt(() => {
|
|
38
38
|
if (mod.state.session.isGreeted) return
|
|
39
39
|
mod.state.session.isGreeted = true
|
|
40
|
-
mod.
|
|
40
|
+
mod.toast(`Hello from ${mod.name}`)
|
|
41
41
|
})
|
|
42
42
|
},
|
|
43
43
|
})
|
|
@@ -47,15 +47,17 @@ export const greeter = defineMod({
|
|
|
47
47
|
|
|
48
48
|
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod the person has not accepted yet opens the installer pane, `Install <name>`, which lists its permissions and the changes it makes to the computer, and waits for Accept ([install-steps.md](install-steps.md)). An option with no default and no value is asked on the next page of the same pane. Then cmod loads the saved state, runs `setup`, marks as open each of the mod's panes that Claude Code already shows, and opens the pane again for each `installer` step the mod declares that is not done.
|
|
49
49
|
|
|
50
|
-
The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane,
|
|
50
|
+
The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane, the Branch widget and /deploy.`, what its install step set up, and each permission the person allowed, with a Settings button that opens Settings at the mod. The first time a new version starts, the toast is `Updated to <version>`: the first three changes of that version's `CHANGELOG.md` section, the version the machine ran before, and a Changelog button ([Release notes](#release-notes)). A mod with no changelog lists what it adds now and no longer adds instead. A new session, `/reload-plugins`, and a version the machine already ran show no toast.
|
|
51
51
|
|
|
52
52
|
When a step fails, or the person declines the install, the mod stays stopped. Once another session sets it up, the mod starts at the next prompt of every session it stopped in, with no `/reload-plugins`.
|
|
53
53
|
|
|
54
|
+
A mod that loads while a session runs, such as after `/reload-plugins` or `/plugin install`, hears no SessionStart from Claude Code. cmod runs its `onSessionStart` and `beforeSessionStart` listeners before the next prompt instead, once, with the source `resume`.
|
|
55
|
+
|
|
54
56
|
When a step fails, the mod does not start. An error toast above the prompt names the failure, with the fix under it:
|
|
55
57
|
|
|
56
58
|
- `<mod>: state.<group> is not a lifetime. …` or `<mod>: state.<group> is not an object of values. …` when the declared state is not valid ([state.md](state.md)).
|
|
57
59
|
- `its state did not load: <error>` when reading the saved values fails.
|
|
58
|
-
- `it needs <titles>` when the person chose Not now on an option with no default, or no one can answer, as in `claude -p` ([options.md](options.md)). This one says `
|
|
60
|
+
- `it needs <titles>` when the person chose Not now on an option with no default, or no one can answer, as in `claude -p` ([options.md](options.md)). This one says `Retry asks for it.`, and `Retry` opens the question again.
|
|
59
61
|
- `its setup function threw: <error>` when `setup` throws or rejects.
|
|
60
62
|
- `its open panes did not load: <error>` when Claude Code cannot list the open panes.
|
|
61
63
|
- `the details of its session did not load: <error>` when Claude Code cannot name the session's id, model, or start time.
|
|
@@ -98,7 +100,7 @@ A mod's `CHANGELOG.md`, at its root, holds what changed in each version, written
|
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
- `cmod publish` releases the version's section as the GitHub release's notes, and refuses a version with no section ([commands.md](commands.md#cmod-publish)). Every release ships the file.
|
|
101
|
-
- The Updated toast shows the first three changes of the new version's section, and its Changelog button opens
|
|
103
|
+
- The Updated toast shows the first three changes of the new version's section, and its Changelog button opens the mod's Changelog in Settings, the whole file with the installed version at the top of the view.
|
|
102
104
|
- A linked checkout whose version has no section yet shows its `[Unreleased]` section, so the notes show as soon as the version moves.
|
|
103
105
|
- `cmod new` writes the file with an `[Unreleased]` and a `[0.1.0]` section.
|
|
104
106
|
|
|
@@ -141,82 +143,87 @@ Keep `register.ts` this small. Claude Code checks a hooks module before it loads
|
|
|
141
143
|
|
|
142
144
|
## mod: this mod
|
|
143
145
|
|
|
144
|
-
`setup` gets `mod`, of type `Mod<State, Options>`. Every listener, render, and job gets the same `mod`. `mod` holds what belongs to this mod: its name, its folders, its values, its grants, and what it adds to Claude Code.
|
|
146
|
+
`setup` gets `mod`, of type `Mod<State, Options, Hotkeys>`. Every listener, render, and job gets the same `mod`. `mod` holds what belongs to this mod: its name, its folders, its values, its grants, its hotkeys, and what it adds to Claude Code.
|
|
145
147
|
|
|
146
148
|
```ts
|
|
147
|
-
type Mod<State, Options> = {
|
|
149
|
+
type Mod<State, Options, Hotkeys> = {
|
|
148
150
|
readonly name: string
|
|
149
151
|
readonly folder: string
|
|
150
152
|
readonly dataFolder: string
|
|
151
153
|
readonly state: Readonly<State>
|
|
152
154
|
readonly options: OptionValues<Options>
|
|
153
155
|
readonly permissions: Permissions
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
readonly hotkeys: { readonly [name in keyof Hotkeys]: Hotkey }
|
|
157
|
+
every(ms, listener): Timer
|
|
158
|
+
after(ms, listener): Timer
|
|
156
159
|
use(job): Handle
|
|
157
|
-
log(text): void
|
|
158
|
-
readonly
|
|
159
|
-
readonly settings: { page
|
|
160
|
+
log(text, options?): void
|
|
161
|
+
readonly sidebar: { add(page): Page }
|
|
162
|
+
readonly settings: { add(page): Page; open(): Promise<void> }
|
|
163
|
+
readonly panes: { add(pane): Pane }
|
|
164
|
+
readonly widgets: { add(widget): Widget }
|
|
165
|
+
readonly styles: { add(style): Style }
|
|
166
|
+
readonly toast: Toast
|
|
167
|
+
ask(question, options?): Promise<string>
|
|
160
168
|
readonly dependencies: CmodDependencies
|
|
161
169
|
}
|
|
162
170
|
```
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
| `settings` | Adds the mod's own pages to `/mods`, and opens them. | below |
|
|
177
|
-
| `dependencies` | Calls the methods of other mods. | [dependencies.md](dependencies.md) |
|
|
172
|
+
- `name` is the mod's name from `defineMod`.
|
|
173
|
+
- `folder` is the plugin's own folder, where the files the mod ships live.
|
|
174
|
+
- `dataFolder` is the mod's own folder in the cmod store: `$XDG_DATA_HOME/cmod/data/<plugin name>`, or `~/.local/share/cmod/data/<plugin name>` when `XDG_DATA_HOME` is not set. The install step gets the same folder as `CMOD_DATA`. cmod deletes it when the mod is removed ([install-steps.md](install-steps.md)).
|
|
175
|
+
- `state` holds the mod's values. Assign to a key to change it ([state.md](state.md)).
|
|
176
|
+
- `options` holds the values a person set for the mod's options, typed from their declaration. Read-only ([options.md](options.md)).
|
|
177
|
+
- `permissions` says whether the person granted each permission ([permissions.md](permissions.md)).
|
|
178
|
+
- `hotkeys` holds each hotkey `defineMod` declared ([Hotkeys](#hotkeys)).
|
|
179
|
+
- `every` and `after` run a listener on a timer while the session runs.
|
|
180
|
+
- `use` adds a job, such as a slash command or a tool, and returns its handle ([jobs.md](jobs.md)).
|
|
181
|
+
- `log` writes a line, under the mod's name, to Claude Code's debug log.
|
|
182
|
+
- `sidebar`, `settings`, `panes`, `widgets`, and `styles` add the mod's pages, panes, widgets, and styles. `toast` shows a short message and `ask` asks a question ([ui.md](ui.md)).
|
|
183
|
+
- `dependencies` calls the methods of other mods ([dependencies.md](dependencies.md)).
|
|
178
184
|
|
|
179
185
|
Everything that is Claude Code's or the computer's rather than the mod's is an import from `mod.js`, so a function in any file calls it without being handed `mod`:
|
|
180
186
|
|
|
181
187
|
```ts
|
|
182
|
-
import { commands, config, fs, http, metadata, model, process, sessions, settings } from '../node_modules/@cmodjs/core/mod.js'
|
|
188
|
+
import { clipboard, commands, config, fs, http, metadata, model, process, sessions, settings } from '../node_modules/@cmodjs/core/mod.js'
|
|
183
189
|
```
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
| `http` | Fetches a URL. | below |
|
|
191
|
+
- `sessions` is the conversations: the one the mod runs in, every other one, and search across them. Everything that happens in a session, from a prompt to a permission dialog, is a method on it ([session.md](session.md)).
|
|
192
|
+
- `commands` is every slash command: it lists them, runs one, and hears or changes each run.
|
|
193
|
+
- `settings` is Claude Code's `settings.json` files: the person's, the project's, and the project's local one.
|
|
194
|
+
- `config` is the rows of Claude Code's `/config` menu.
|
|
195
|
+
- `model` asks a model one question, outside the conversation.
|
|
196
|
+
- `fs` reads, writes, lists, finds, and watches files.
|
|
197
|
+
- `metadata` reads and changes the mod's keys in one file's metadata.
|
|
198
|
+
- `process` runs a program.
|
|
199
|
+
- `http` fetches a URL.
|
|
200
|
+
- `clipboard` puts text on the person's clipboard ([ui.md](ui.md#toasts)).
|
|
196
201
|
|
|
197
202
|
Each import works once the mod has started, in `setup` and anything it adds. Each one checks the permission it needs, and rejects with the line to add or the grant to turn on when the person has not granted it ([permissions.md](permissions.md)).
|
|
198
203
|
|
|
199
|
-
###
|
|
204
|
+
### Take back what the mod added
|
|
200
205
|
|
|
201
|
-
|
|
206
|
+
Every `add` call, every `on` and `before` method, `fs.onChange`, `settings.beforeFileChange`, and `hotkey.onPress` return a part or a `Subscription`, and each has `remove()`, which takes back what the call added. So a mod turns a feature off and on while it runs. Everything a mod adds is taken back when the mod stops, so a mod that keeps a feature for the whole session ignores what the call returns.
|
|
202
207
|
|
|
203
208
|
```ts
|
|
204
|
-
type
|
|
209
|
+
type Subscription = { remove(): void }
|
|
205
210
|
```
|
|
206
211
|
|
|
207
212
|
```ts
|
|
208
213
|
const watching = sessions.current.onToolCall({ name: 'Edit' }, () => void (mod.state.session.edits += 1))
|
|
209
|
-
mod.use(slashCommand({ name: 'stop-counting', description: 'Stop counting edits', reply: () => watching.
|
|
214
|
+
mod.use(slashCommand({ name: 'stop-counting', description: 'Stop counting edits', reply: () => watching.remove() }))
|
|
210
215
|
```
|
|
211
216
|
|
|
212
217
|
### mod.every and mod.after
|
|
213
218
|
|
|
214
219
|
```ts
|
|
215
|
-
mod.every(ms: number, listener: () => unknown):
|
|
216
|
-
mod.after(ms: number, listener: () => unknown):
|
|
220
|
+
mod.every(ms: number, listener: () => unknown): Timer
|
|
221
|
+
mod.after(ms: number, listener: () => unknown): Timer
|
|
222
|
+
|
|
223
|
+
type Timer = { readonly isRunning: boolean; stop(): void }
|
|
217
224
|
```
|
|
218
225
|
|
|
219
|
-
`every` runs `listener` every `ms` milliseconds until the session ends or the mod
|
|
226
|
+
`every` runs `listener` every `ms` milliseconds until the session ends or the mod calls `stop()`. A tick that comes while the last run is still going is skipped, so a slow run never stacks up. `after` runs `listener` once, `ms` milliseconds from now, unless the mod calls `stop()` first. `isRunning` is true until the timer stops, and an `after` timer stops once it ran. A run that throws shows the error toast `The every <ms> ms timer failed`, or `The after <ms> ms timer failed`, and the next tick of an `every` runs again. `ms` is a whole number above 0 and at most `2147483647`, and any other `ms` stops `setup` with `<mod>: mod.every is <ms>. Give a whole number of milliseconds above 0 and at most 2147483647.`
|
|
220
227
|
|
|
221
228
|
A watch that reacts on its own is `every` plus `mod.state` plus a `prompt` with no trigger: `every` checks the outside world, writes what it found to `mod.state`, and the panes and the prompt follow the state ([jobs.md](jobs.md#prompt)).
|
|
222
229
|
|
|
@@ -237,18 +244,39 @@ mod.log(text: string, options?: { to?: 'debug' | 'transcript' }): void
|
|
|
237
244
|
|
|
238
245
|
With `{ to: 'transcript' }`, the line shows dim in the conversation, where the person reads it and Claude never does, such as "Formatted 3 files" after a turn.
|
|
239
246
|
|
|
240
|
-
###
|
|
247
|
+
### Hotkeys
|
|
248
|
+
|
|
249
|
+
A hotkey is a key combo the mod listens for, such as `alt+shift+p` to show a page. The mod declares each one with a title and a default combo, and the person changes the combo in Settings, under the mod, Hotkeys, the way VS Code handles an extension's keybindings.
|
|
241
250
|
|
|
242
251
|
```ts
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
hotkeys?: { readonly [name: string]: { readonly title: string; readonly default: Combo } }
|
|
253
|
+
|
|
254
|
+
type Hotkey = {
|
|
255
|
+
readonly title: string
|
|
256
|
+
readonly combo: Combo | undefined
|
|
257
|
+
onPress(listener: () => unknown): Subscription
|
|
258
|
+
}
|
|
245
259
|
```
|
|
246
260
|
|
|
247
|
-
|
|
261
|
+
- A name is letters, digits, and `_`, starting with a letter. `title` is the label Settings shows beside the combo.
|
|
262
|
+
- A combo is one or two of `ctrl`, `alt`, `shift`, and `meta`, in that order, then a key: a letter, a digit, `f1` to `f12`, `tab`, `enter`, `escape`, `space`, `backspace`, `delete`, an arrow, `home`, `end`, `pageup`, `pagedown`, or a mark such as `/`. `ctrl+q`, `alt+shift+p`, and `ctrl+shift+k` are combos.
|
|
263
|
+
- `defineMod` throws for a name, a title, or a combo that is not one, and for two hotkeys with one default.
|
|
264
|
+
- `cmod check` writes the hotkeys into `package.json` `"cmod".hotkeys`, and `cmod publish` refuses a mod whose `package.json` differs from `defineMod`. The install question lists each combo, and `cmod setup` binds it in Claude Code's `keybindings.json`. A combo the person already bound to something else keeps their binding, and the hotkey has no combo until they pick one in Settings.
|
|
265
|
+
- `combo` is the combo bound when the mod started, or `undefined` when `keybindings.json` binds none to the hotkey, such as when another binding kept the default.
|
|
266
|
+
- `onPress` runs `listener` each time the person presses the combo. A page or pane the listener opens shows at any width, because the press is the person's own ([ui.md](ui.md#the-sidebar-and-its-pages)).
|
|
267
|
+
- `tested.hotkey(name)` presses a hotkey in a test ([testing.md](testing.md)).
|
|
248
268
|
|
|
249
269
|
```tsx
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
export const notes = defineMod({
|
|
271
|
+
name: 'notes',
|
|
272
|
+
hotkeys: {
|
|
273
|
+
toggle: { title: 'Show or hide the notes', default: 'alt+shift+n' },
|
|
274
|
+
},
|
|
275
|
+
setup(mod) {
|
|
276
|
+
const page = mod.sidebar.add(notesPage)
|
|
277
|
+
mod.hotkeys.toggle.onPress(() => page.toggle({ focus: true }))
|
|
278
|
+
},
|
|
279
|
+
})
|
|
252
280
|
```
|
|
253
281
|
|
|
254
282
|
## commands: slash commands
|
|
@@ -256,8 +284,8 @@ mod.use(slashCommand({ name: 'ci-runs', description: 'Show the CI runs', reply:
|
|
|
256
284
|
```ts
|
|
257
285
|
commands.list(): Promise<CommandInfo[]>
|
|
258
286
|
commands.run(name: string, args?: string): Promise<{ text?: string; note?: string }>
|
|
259
|
-
commands.beforeRun(matcher?, listener: (run: CommandRun) => Answer | void, options?):
|
|
260
|
-
commands.onRun(matcher?, listener: (run: CommandRun) => unknown, options?):
|
|
287
|
+
commands.beforeRun(matcher?, listener: (run: CommandRun) => Answer | void, options?): Subscription
|
|
288
|
+
commands.onRun(matcher?, listener: (run: CommandRun) => unknown, options?): Subscription
|
|
261
289
|
|
|
262
290
|
type CommandInfo = { name: string; description: string; source: 'builtin' | 'plugin' | 'user' | 'mcp'; plugin?: string }
|
|
263
291
|
type CommandRun = { name: string; args: string; source?: 'builtin' | 'plugin' | 'user' | 'mcp'; from: Sender; session: string; cwd: string }
|
|
@@ -285,7 +313,7 @@ settings.project.read(): Promise<Settings>
|
|
|
285
313
|
settings.project.update(values: Readonly<Record<string, unknown>>): Promise<void>
|
|
286
314
|
settings.local.read(): Promise<Settings>
|
|
287
315
|
settings.local.update(values: Readonly<Record<string, unknown>>): Promise<void>
|
|
288
|
-
settings.beforeFileChange(matcher?, listener: (change: SettingsFileChange) => { refuse: string } | void):
|
|
316
|
+
settings.beforeFileChange(matcher?, listener: (change: SettingsFileChange) => { refuse: string } | void): Subscription
|
|
289
317
|
```
|
|
290
318
|
|
|
291
319
|
`settings.user` is the person's own `~/.claude/settings.json`, `settings.project` is the project's `.claude/settings.json`, and `settings.local` is the project's `.claude/settings.local.json`.
|
|
@@ -385,7 +413,7 @@ fs.list(path?: string): Promise<Entry[]>
|
|
|
385
413
|
fs.exists(path: string): Promise<boolean>
|
|
386
414
|
fs.stat(path: string, options?: { resolve: boolean }): Promise<Stat>
|
|
387
415
|
fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
|
|
388
|
-
fs.onChange({ path: string | readonly string[] }, listener: (change: FileChange) => unknown):
|
|
416
|
+
fs.onChange({ path: string | readonly string[] }, listener: (change: FileChange) => unknown): Subscription
|
|
389
417
|
```
|
|
390
418
|
|
|
391
419
|
```ts
|
|
@@ -400,10 +428,10 @@ type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number;
|
|
|
400
428
|
- `list` lists a folder, the working folder without a path. A symbolic link is neither a file nor a folder, with `isLink` true. `modifiedAt` is the time it last changed, in milliseconds since 1970.
|
|
401
429
|
- `exists` answers whether the path leads to a file or a folder.
|
|
402
430
|
- `stat` answers what the path leads to, following a symbolic link, and rejects a missing path. With `{ resolve: true }` it also answers `realPath`: the absolute path, every symbolic link followed.
|
|
403
|
-
- `onChange` hears each file
|
|
431
|
+
- `onChange` hears each file that changes, appears, or goes at a path or under a folder, the mod's own saves included. cmod checks the paths twice a second, and at most 2000 files under one call's folders. A call over more stops, with an error toast that names its paths. It works from `setup` or any time later, and for a path that does not exist yet. A relative path is under the session's working folder. `path` takes strings only, unlike other matchers:
|
|
404
432
|
|
|
405
433
|
```ts
|
|
406
|
-
fs.onChange({ path: `${mod.dataFolder}/history` }, ({ path, type }) => mod.
|
|
434
|
+
fs.onChange({ path: `${mod.dataFolder}/history` }, ({ path, type }) => mod.toast(`${path}: ${type}`))
|
|
407
435
|
```
|
|
408
436
|
|
|
409
437
|
A file the mod ships is under `mod.folder`. A file the mod keeps for itself belongs in `mod.dataFolder`.
|
|
@@ -494,7 +522,7 @@ export const notes = defineMod({
|
|
|
494
522
|
setup(mod) {
|
|
495
523
|
sessions.current.onTurnEnd(async () => {
|
|
496
524
|
await fs.write(`${mod.dataFolder}/last-stop.txt`, new Date().toISOString()).catch((error: unknown) => {
|
|
497
|
-
mod.
|
|
525
|
+
mod.toast.error('Could not save the last stop', { description: messageOf(error) })
|
|
498
526
|
})
|
|
499
527
|
})
|
|
500
528
|
},
|
|
@@ -528,7 +556,7 @@ import { sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
|
528
556
|
|
|
529
557
|
sessions.current.onToolCall({ name: 'Bash' }, (call) => {
|
|
530
558
|
for (const { argv, folder } of parseShell(String(call.input['command'])).commands) {
|
|
531
|
-
if (basename(argv[0]) === 'trace') mod.
|
|
559
|
+
if (basename(argv[0]) === 'trace') mod.toast(`trace runs in ${resolve(call.cwd, folder)}`)
|
|
532
560
|
}
|
|
533
561
|
})
|
|
534
562
|
```
|
package/docs/options.md
CHANGED
|
@@ -18,7 +18,7 @@ export const ciWatch = defineMod({
|
|
|
18
18
|
branch: option.text({ title: 'Branch', description: 'The branch to watch', default: 'main' }),
|
|
19
19
|
},
|
|
20
20
|
setup(mod) {
|
|
21
|
-
mod.
|
|
21
|
+
mod.toast(`Watching ${mod.options.branch} every ${mod.options.every} minutes`)
|
|
22
22
|
},
|
|
23
23
|
})
|
|
24
24
|
```
|
|
@@ -49,7 +49,7 @@ Each value is, in order of precedence:
|
|
|
49
49
|
3. The value the person set in `/config`.
|
|
50
50
|
4. The `default` in the declaration.
|
|
51
51
|
|
|
52
|
-
An option with no default and no value is asked before `setup` runs, on a page of the installer pane: one field per option, each saved to `/config` through `cmod option` as the person submits it. A secret is set in `/config` itself, where Claude Code keeps it in the keychain, and the page says so. When the person chooses Not now, or no one can answer, as in `claude -p`, the mod does not start: its error toast says `it needs <title>` and `
|
|
52
|
+
An option with no default and no value is asked before `setup` runs, on a page of the installer pane: one field per option, each saved to `/config` through `cmod option` as the person submits it. A secret is set in `/config` itself, where Claude Code keeps it in the keychain, and the page says so. When the person chooses Not now, or no one can answer, as in `claude -p`, the mod does not start: its error toast says `it needs <title>` and `Retry asks for it.`, and `Retry` opens the question again. A secret has an `Open /config` button, because Claude Code keeps secrets in /config and the keychain.
|
|
53
53
|
|
|
54
54
|
`cmod install`, `cmod link`, and `cmod try` set options from the terminal with `--option key=value`, once per option ([commands.md](commands.md)).
|
|
55
55
|
|
package/docs/permissions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Permissions
|
|
2
2
|
|
|
3
|
-
A mod declares what it does beyond drawing and listening: the hosts it connects to, the programs it runs, the files it changes outside its project, and whether it reads the conversation, adds text Claude reads, asks a model, starts tasks, uses Claude's tools, changes Claude Code's settings, or approves tool calls. The person sees each permission as one sentence when they install the mod, turns each one on or off in
|
|
3
|
+
A mod declares what it does beyond drawing and listening: the hosts it connects to, the programs it runs, the files it changes outside its project, and whether it reads the conversation, adds text Claude reads, asks a model, starts tasks, uses Claude's tools, changes Claude Code's settings, or approves tool calls. The person sees each permission as one sentence when they install the mod, turns each one on or off in Settings, and cmod checks every call and every answer against what they granted.
|
|
4
4
|
|
|
5
5
|
Permissions bind every mod built on `@cmodjs/core`, and the plugin directory reads them from `package.json`. They are a contract, not a sandbox: a Claude Code plugin can skip cmod and call Claude Code directly.
|
|
6
6
|
|
|
@@ -35,9 +35,9 @@ Anthropic's plugin directory does not list a mod that can approve Claude's tool
|
|
|
35
35
|
|
|
36
36
|
## What the person sees
|
|
37
37
|
|
|
38
|
-
The install opens the installer pane in Claude Code and lists every permission in `permissions`, with the install step, the program, and the
|
|
38
|
+
The install opens the installer pane in Claude Code and lists every permission in `permissions`, with the install step, the program, and the hotkeys. An update asks again only when it adds a permission, and lists only the new ones. Removing the mod revokes everything it was granted.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Settings lists the same permissions as toggles, under the mod, Permissions. A toggle runs `cmod permission <mod> <name> [value] on|off`, and the mod sees the change before its next call, with no reload.
|
|
41
41
|
|
|
42
42
|
`cmod check` prints the sentences, so you read what the person will see:
|
|
43
43
|
|
|
@@ -76,7 +76,7 @@ if (mod.permissions.run.allows('gh')) mod.every(60_000, refreshRuns)
|
|
|
76
76
|
The call rejects, and the error names the fix:
|
|
77
77
|
|
|
78
78
|
- An undeclared call names the line to add: `ci-watch calls http.fetch(https://api.github.com/repos), which needs "permissions": { "network": ["api.github.com"] } in package.json "cmod".`
|
|
79
|
-
- A declared call the person turned off names where to turn it on: `ci-watch calls model.complete without your grant to "Ask a model, which uses your plan". Turn it on in
|
|
79
|
+
- A declared call the person turned off names where to turn it on: `ci-watch calls model.complete without your grant to "Ask a model, which uses your plan". Turn it on in Settings.` The error toast that shows it has a `Settings` button that opens the mod's Permissions.
|
|
80
80
|
- A `program` job without its grant stops at once with the same line, with no restarts.
|
|
81
81
|
|
|
82
82
|
## When a before answer has no grant
|