@cmodjs/core 0.3.0 → 1.5.0
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 +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/docs/permissions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Permissions
|
|
2
2
|
|
|
3
|
-
A mod declares what it does beyond drawing and answering hooks: 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
|
|
3
|
+
A mod declares what it does beyond drawing and answering hooks: 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 `/mods`, 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
|
|
|
@@ -16,18 +16,18 @@ They sit in the `package.json` `cmod` key, beside `install` and `keys`, because
|
|
|
16
16
|
|
|
17
17
|
| Permission | The person reads | cmod checks |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| `"network": ["api.github.com"]` | Connect to api.github.com | `
|
|
20
|
-
| `"run": ["gh"]`, or `"run": "*"` | Run gh on your computer | `
|
|
21
|
-
| `"files": ["~/.zshrc"]` | Change ~/.zshrc | `
|
|
22
|
-
| `"conversation": true` | Read
|
|
23
|
-
| `"prompt": true` | Add text Claude reads and start turns | `
|
|
24
|
-
| `"model": true` | Ask a model, which uses your plan | `
|
|
25
|
-
| `"
|
|
26
|
-
| `"tools": true` | Use and change Claude's tool calls | `
|
|
27
|
-
| `"config":
|
|
19
|
+
| `"network": ["api.github.com"]` | Connect to api.github.com | `http.fetch` to that host. A path starting with `/` or `~/` names a socket, such as `"~/.docker/run/docker.sock"`. The address of the mod's own running `program` job needs none |
|
|
20
|
+
| `"run": ["gh"]`, or `"run": "*"` | Run gh on your computer | `process.run`, `process.spawn`, and the `program` and `check` jobs. A program matches by its file name, so `"mermaid-ascii"` covers `${mod.dataFolder}/bin/mermaid-ascii` and the copy in cmod's programs folder |
|
|
21
|
+
| `"files": ["~/.zshrc"]` | Change ~/.zshrc | `fs.write` and `metadata.update` outside the project and the mod's data folder. A folder covers every file below it |
|
|
22
|
+
| `"conversation": true` | Read your Claude Code conversations | Reading `sessions`: their messages, turns, contexts, goals, and tasks, `sessions.list`, `search`, `on`, `before`, `usage`, and `clients`. Also reading Claude Code's transcript files |
|
|
23
|
+
| `"prompt": true` | Add text Claude reads and start turns | A session's `send`, a `before` answer that changes or refuses text, the mod's `rules/`, the `prompt` and `schedule` jobs, and the hook answers listed below |
|
|
24
|
+
| `"model": true` | Ask a model, which uses your plan | `model.complete` |
|
|
25
|
+
| `"tasks": true` | Start tasks that work beside Claude | A session's `startTask`, and a `before(TaskStart)` or `before(TaskEnd)` answer |
|
|
26
|
+
| `"tools": true` | Use and change Claude's tool calls | A session's `callTool`, a `before(ToolCall)` answer, and a listener or a job that changes a tool call's input |
|
|
27
|
+
| `"config": ["cleanupPeriodDays"]` | Change cleanupPeriodDays in your Claude Code settings | `settings.user.update`, `settings.project.update`, and `settings.local.update` of that setting. `mcpServers` covers the project's `.mcp.json` |
|
|
28
28
|
| `"approve": true` | Approve Claude's tool calls for you | an allow or an ask from the mod's permission rules or hooks, and a `retry`. A deny needs no permission |
|
|
29
29
|
|
|
30
|
-
Reading files, drawing panes and
|
|
30
|
+
Reading files, drawing panes and renders, toasts, slash commands, tools of the mod's own, and listeners need no permission. Writing a Claude Code settings file with `fs.write` is refused with every permission, and names the `settings` call to use instead ([mod.md](mod.md#settings-claude-code-s-settings-files)).
|
|
31
31
|
|
|
32
32
|
`cmod check` adds `"approve": true` to `package.json` when `hooks/register.ts` calls `registerPermissionCheck` and the line is missing, and says so.
|
|
33
33
|
|
|
@@ -46,16 +46,27 @@ The install opens the installer pane in Claude Code and lists every permission i
|
|
|
46
46
|
## Check a permission before a feature uses it
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
type Permissions = {
|
|
50
|
+
readonly conversation: boolean
|
|
51
|
+
readonly prompt: boolean
|
|
52
|
+
readonly model: boolean
|
|
53
|
+
readonly tasks: boolean
|
|
54
|
+
readonly tools: boolean
|
|
55
|
+
readonly approve: boolean
|
|
56
|
+
readonly network: { allows(host: string): boolean }
|
|
57
|
+
readonly run: { allows(program: string): boolean }
|
|
58
|
+
readonly files: { allows(path: string): boolean }
|
|
59
|
+
readonly config: { allows(key: string): boolean }
|
|
60
|
+
}
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
`
|
|
63
|
+
`mod.permissions` says whether the person has each permission turned on, so a mod hides a feature instead of failing it. A flag is `true` while it is granted, and `allows` answers for one host, program, path, or settings key:
|
|
54
64
|
|
|
55
65
|
```ts
|
|
56
|
-
if (mod.permissions.
|
|
57
|
-
const verdict = await
|
|
66
|
+
if (mod.permissions.model) {
|
|
67
|
+
const verdict = await model.complete({ model: 'haiku', prompt: 'Is this reply a waste of time?' })
|
|
58
68
|
}
|
|
69
|
+
if (mod.permissions.run.allows('gh')) mod.every(60_000, refreshRuns)
|
|
59
70
|
```
|
|
60
71
|
|
|
61
72
|
## When a call has no grant
|
package/docs/session.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Sessions
|
|
2
|
+
|
|
3
|
+
`sessions` reads and follows Claude Code's conversations: the one the mod runs in, every other one Claude Code saved, and a search across them. A mod never parses a transcript.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Prompt, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
7
|
+
|
|
8
|
+
const prompts = (await sessions.current.messages()).filter((message) => message.type === Prompt)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Reading a conversation needs the `conversation` permission. Sending to one and changing its text need `prompt`, calling a tool and changing a tool call need `tools`, and starting or changing a task needs `tasks` ([permissions.md](permissions.md)).
|
|
12
|
+
|
|
13
|
+
## The shape of a conversation
|
|
14
|
+
|
|
15
|
+
A session is one agent and its record. It runs as a series of contexts: a compaction or `/compact` ends one context and starts the next with a summary of the last. Each context holds turns: a turn starts at a prompt and ends when the agent stops. Each turn holds the messages the agent and the people and programs around it exchanged. The session's goal is what it is for, and its tasks are the work it handed to other sessions. Each task's `owner` is the session that does that work, such as an `Explore` subagent, with a record of its own.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Session one agent and its record
|
|
19
|
+
├── Goal what the session is for
|
|
20
|
+
│ └── Task work handed to another session
|
|
21
|
+
│ ├── owner the Session doing it, with its own goal, turns, and messages
|
|
22
|
+
│ └── Task a task its owner handed on
|
|
23
|
+
└── Context from the start, or a compaction, to the next compaction
|
|
24
|
+
└── Turn from a prompt to the end of the agent's answer
|
|
25
|
+
└── Message a prompt, a reply, a tool call, and the rest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every part of the record answers the same three calls: `search` finds its messages, `on` hears what happens in it, and `before` changes or refuses what is about to happen in it.
|
|
29
|
+
|
|
30
|
+
## sessions
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
type Sessions = {
|
|
34
|
+
readonly current: Session
|
|
35
|
+
list(options?: { running?: boolean; since?: string; everyProject?: boolean }): Promise<Session[]>
|
|
36
|
+
search(text: string, options?: { running?: boolean; since?: string; everyProject?: boolean; limit?: number; before?: Message }): Promise<Message[]>
|
|
37
|
+
on(event, listener): Disposable
|
|
38
|
+
on(event, matcher, listener): Disposable
|
|
39
|
+
before(event, listener): Disposable
|
|
40
|
+
before(event, matcher, listener): Disposable
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `current` is the session the mod runs in. It is there from `setup` on, and follows `/clear`, `/resume`, a fork, `/cd`, and a `cd` in a Bash or PowerShell call.
|
|
45
|
+
- `list` lists the project's sessions, the most recent first. `everyProject` lists every project's, `running` keeps the sessions still open in a Claude Code window, and `since` keeps those active since a time, written as `30m`, `6h`, `2d`, `1w`, or a date such as `2026-10-01`.
|
|
46
|
+
- `search` answers the newest 50 messages whose text contains `text`, any case, in the project's sessions or, with `everyProject`, every project's. `text` is at least 3 characters. `limit` changes the 50, and `before` takes the last message of a page to answer the page after it. A tool call is searched by its name and the first 1,024 characters of each input value, a document by its path and text, and a tool result is not searched.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const earlier = await sessions.search('deploy job', { since: '1w' })
|
|
50
|
+
const last = earlier.at(-1)
|
|
51
|
+
const older = last === undefined ? [] : await sessions.search('deploy job', { since: '1w', before: last })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `on` and `before` hear every running session of the project, this one included ([Listen](#listen-to-a-record), [Change what happens](#change-what-happens)).
|
|
55
|
+
|
|
56
|
+
While a mod holds the `conversation` permission, the cmod plugin indexes every conversation in the background at each session's start, so `search` waits only for what changed since. The `cmod history` command answers the same questions in a terminal ([commands.md](commands.md#cmod-history)).
|
|
57
|
+
|
|
58
|
+
## Session
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
type Session = {
|
|
62
|
+
readonly id: string
|
|
63
|
+
readonly title: string
|
|
64
|
+
readonly project: string
|
|
65
|
+
readonly agentType?: string
|
|
66
|
+
readonly model: string
|
|
67
|
+
readonly cwd: string
|
|
68
|
+
readonly isRunning: boolean
|
|
69
|
+
readonly startedAt: string
|
|
70
|
+
readonly lastAt: string
|
|
71
|
+
readonly forkedFrom?: string
|
|
72
|
+
goal(): Promise<Goal>
|
|
73
|
+
context(): Promise<Context>
|
|
74
|
+
contexts(): Promise<Context[]>
|
|
75
|
+
turn(): Promise<Turn | undefined>
|
|
76
|
+
turns(options?: { last?: number }): Promise<Turn[]>
|
|
77
|
+
messages(options?: { after?: string; last?: number }): Promise<Message[]>
|
|
78
|
+
search(text: string, options?: { limit?: number; before?: Message }): Promise<Message[]>
|
|
79
|
+
on(event, listener): Disposable
|
|
80
|
+
on(event, matcher, listener): Disposable
|
|
81
|
+
before(event, listener): Disposable
|
|
82
|
+
before(event, matcher, listener): Disposable
|
|
83
|
+
send(message: { type: Prompt | Note; text: string; as?: Person | Session }): Promise<void>
|
|
84
|
+
startTask(task: { prompt: string; agentType?: string; model?: string; name?: string; cwd?: string }): Promise<Task>
|
|
85
|
+
callTool(call: { name: string; input: Record<string, unknown> }): Promise<ToolResult>
|
|
86
|
+
usage(): Promise<Usage>
|
|
87
|
+
clients(): Promise<Client[]>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `id` is Claude Code's session id, and a task owner's `id` is its task's id. `title` is the name the person gave it with `/rename`, or the first line of its first prompt. A resumed or forked session with no name, and a session the mod started in after its first prompt, has an empty `sessions.current.title` until `sessions.list()` reads it.
|
|
92
|
+
- `project` is the project the session works in, and `cwd` is its working folder. For the current session both follow `/cd`, and `cwd` follows a `cd` in a Bash or PowerShell call.
|
|
93
|
+
- `agentType` is the agent definition the session runs, such as `Explore` for a task's owner, or the agent a session started with `claude --agent` runs. A plain session has none.
|
|
94
|
+
- `model` is the model the session runs on. `startedAt` and `lastAt` are ISO times.
|
|
95
|
+
- `forkedFrom` names the session a `/branch` or a fork copied it from.
|
|
96
|
+
- `isRunning` is true while the session is open in a Claude Code window, and for a task's owner while its task runs.
|
|
97
|
+
|
|
98
|
+
### Read a record
|
|
99
|
+
|
|
100
|
+
- `goal` answers its goal. The goal's `tasks()` answers the tasks the session handed on, and each task's `owner` reads like any session.
|
|
101
|
+
- `context` answers the context it is in now, and `contexts` all of them in order. A context's `turns()` answers its own.
|
|
102
|
+
- `turn` answers the turn running now, and nothing between turns. `turns` answers its turns in order, `last` keeping that many from the end. A turn's `messages()` answers its own.
|
|
103
|
+
- `messages` answers its messages in order, those of its tasks left out. `last` keeps that many from the end, and `after` starts after a message's `id`.
|
|
104
|
+
- `search` finds its messages whose text contains `text`, newest first, the way `sessions.search` does. A goal's, a task's, a context's, and a turn's `search` find only their own.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const [last] = await sessions.current.turns({ last: 1 })
|
|
108
|
+
const calls = (await last?.messages() ?? []).filter((message) => message.type === ToolCall)
|
|
109
|
+
const explorers = (await (await sessions.current.goal()).tasks()).filter((task) => task.owner.agentType === 'Explore')
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A session that has ended keeps its record, so every read works on it.
|
|
113
|
+
|
|
114
|
+
### Listen to a record
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
part.on(event: SessionEvent, listener: (payload) => unknown): Disposable
|
|
118
|
+
part.on(event: SessionEvent, matcher: Matcher<payload>, listener: (payload) => unknown): Disposable
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`on` runs `listener` for each new message, turn, context, task, goal change, or usage change, as it happens. `sessions`, a session, its goal, a task, a context, and a turn each have `on`, and each hears what happens inside it: an event reaches the part it happens in and every part above it.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Message ──> Turn ──> Context ──> Session ──> sessions
|
|
125
|
+
TaskStart, TaskEnd, and the owner's record ──> Task ──> its parent Task ──> Goal ──> Session
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
So `turn.on(Reply, …)` hears the replies of that turn only, `task.on(Message, …)` hears every message of the task's owner and of the tasks it handed on, and `sessions.on(Prompt, …)` hears every prompt of every running session in the project.
|
|
129
|
+
|
|
130
|
+
| Event | The listener gets | It happens in |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `Message` | Every new message. | Its turn |
|
|
133
|
+
| `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `Document`, `Notice` | Each new message of that type. | Its turn |
|
|
134
|
+
| `TurnStart`, `TurnEnd` | The `Turn` that started or ended. | Its context |
|
|
135
|
+
| `ContextStart`, `ContextEnd` | The `Context` that started or ended. | The session |
|
|
136
|
+
| `TaskStart`, `TaskEnd` | The `Task` that started or ended. | Its parent task, or the goal |
|
|
137
|
+
| `GoalChange` | The `Goal` with its new text. | The goal |
|
|
138
|
+
| `UsageChange` | The session's new `Usage`. | The session |
|
|
139
|
+
|
|
140
|
+
The current session's events arrive as Claude Code writes them, and another running session's within a moment.
|
|
141
|
+
|
|
142
|
+
A matcher keeps the payloads it matches. It is a partial copy of the payload: a key it names must match, and a key it leaves out matches anything. A string matches the same string, a `RegExp` matches the text it tests true on, and a list matches any one of its items. A key the payload lacks matches nothing.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { Reply, sessions, TaskStart, ToolCall } from '../node_modules/@cmodjs/core/mod.js'
|
|
146
|
+
|
|
147
|
+
sessions.current.on(ToolCall, { name: /^(Edit|Write)$/ }, (call) => void (mod.state.session.edits += 1))
|
|
148
|
+
sessions.current.on(TaskStart, { owner: { agentType: 'Explore' } }, (task) => mod.log(`Exploring: ${task.title}`))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A listener that throws shows the error toast `The <event> listener failed`. `dispose()` on the `Disposable` stops the listener.
|
|
152
|
+
|
|
153
|
+
### Change what happens
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
part.before(event: BeforeEvent, listener: (payload) => Answer | void): Disposable
|
|
157
|
+
part.before(event: BeforeEvent, matcher: Matcher<payload>, listener: (payload) => Answer | void): Disposable
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`before` runs `listener` just before something happens, and its answer changes or refuses it. Returning nothing lets it happen as it was. Each part has `before`, and hears what is about to happen inside it, the way `on` does.
|
|
161
|
+
|
|
162
|
+
| Event | The listener can answer | Covers |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `Prompt` | `{ text }` or `{ refuse }` | A typed prompt, a mod's prompt, a slash command, and a message from another session |
|
|
165
|
+
| `Note` | `{ text }` | A note any mod or Claude Code adds |
|
|
166
|
+
| `Skill` | `{ text }` | A Skill as Claude Code loads it |
|
|
167
|
+
| `Document` | `{ text }` or `{ refuse }` | A `CLAUDE.md` file as Claude Code reads it. `refuse` leaves it out |
|
|
168
|
+
| `Reply`, `ToolResult`, `Notice` | `{ text }` | The text the transcript keeps |
|
|
169
|
+
| `ToolCall` | `{ input }`, `{ result }`, or `{ refuse }` | A tool call. `result` answers it without running the tool |
|
|
170
|
+
| `TaskStart` | `{ owner: { model } }` or `{ refuse }` | A task about to start, and the model its owner runs |
|
|
171
|
+
| `TaskEnd` | `{ refuse }` | A task about to end. `refuse` sends its owner on with the reason |
|
|
172
|
+
| `TurnEnd` | `{ refuse }` | The agent about to stop. `refuse` sends it on with the reason |
|
|
173
|
+
| `ContextEnd` | `{ refuse }` | A compaction about to start. `refuse` keeps the context |
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
sessions.current.before(ToolCall, { name: 'Bash', input: { command: /\brm -rf\b/ } }, () => ({ refuse: 'Delete files one at a time.' }))
|
|
177
|
+
sessions.current.before(TaskStart, { owner: { agentType: 'Explore' } }, () => ({ owner: { model: 'haiku' } }))
|
|
178
|
+
sessions.current.before(TurnEnd, async () => ((await testsPass()) ? undefined : { refuse: 'The tests fail. Fix them before you stop.' }))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
An answer that changes text needs the `prompt` permission, a `ToolCall` answer needs `tools`, and a `TaskStart` or `TaskEnd` answer needs `tasks`. Without it the answer changes nothing, and the toast `The before(<event>) listener failed` names the permission. When several listeners answer, each sees what the one before it changed, and the first `refuse` or `result` decides.
|
|
182
|
+
|
|
183
|
+
`before` on another session, and `sessions.before`, ask the mod over cmod from that session. That session waits up to 5 seconds for the answer, then lets the event happen as it was and logs why. `sessions.before` answers once for each event, whichever session it happens in.
|
|
184
|
+
|
|
185
|
+
### Act on a session
|
|
186
|
+
|
|
187
|
+
Every action works on the current session and on every other running session. cmod carries an action to another session, and that session's cmod does it there.
|
|
188
|
+
|
|
189
|
+
- `send` adds a message to the session, and needs the `prompt` permission.
|
|
190
|
+
- A `Note` is text the agent reads on its next call to the model and the person does not see. It starts no turn. It rejects with `<mod>: Claude Code refused the note: <reason>` when a plugin refuses it.
|
|
191
|
+
- A `Prompt` is the next prompt, so the agent answers it as a turn of its own. Claude Code holds it until the session is idle, so it never cuts into a turn, and frames it as `The <mod> plugin sent a message:`. With `as: Person`, the agent reads it bare, as the person's own words, while every hook still sees that the mod sent it. It rejects with `<mod>: Claude Code dropped the prompt: <reason>` when a plugin drops it.
|
|
192
|
+
- `as: Session` sends it to another session as a message from this one, which Claude Code shows as one session writing to another.
|
|
193
|
+
- To a task's owner, `send` delivers the text to that running subagent.
|
|
194
|
+
- `startTask` starts a task in the session, the way the Agent tool starts one, and resolves the `Task` once it started. It does not wait for the task to finish. `agentType` names the owner's agent type, such as `Explore`, `name` titles the task, and `model` and `cwd` change what it runs on and where. It needs the `tasks` permission, and rejects with `<mod>: Claude Code refused the task: <reason>` when a hook refuses it.
|
|
195
|
+
- `callTool` runs a tool in the session, such as `{ name: 'Bash', input: { command: 'bun test' } }`, and resolves its `ToolResult`. It needs the `tools` permission.
|
|
196
|
+
- `usage` answers the session's model, its context window's fill, and its cost so far, as the status line shows them: `{ model, context: { tokens?, window?, percent? }, cost?: { usd } }`. For an ended session and a task's owner it answers the model and the tokens of its last reply, from its transcript.
|
|
197
|
+
- `clients` answers the Claude apps showing the session now: `'terminal'`, `'desktop'`, `'mobile'`, and `'vscode'`. An ended session shows in none.
|
|
198
|
+
|
|
199
|
+
A task's owner takes `send`, `usage`, and `clients`. Claude Code gives a subagent no way to start a task or run a tool from outside, so its `startTask` and `callTool` reject. An action on a session that has ended rejects with `session <id> has ended`, and `send` with `<mod>: session <id> has ended, so it takes no messages.`
|
|
200
|
+
|
|
201
|
+
A prompt that comes on a clock, such as every 10 minutes, belongs in a `schedule` job instead, which Claude Code shows as one `Running scheduled task` row ([jobs.md](jobs.md#schedule)).
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
mod.every(60_000, async () => {
|
|
205
|
+
const { stdout } = await process.run(['gh', 'run', 'list', '--limit', '1', '--json', 'conclusion'])
|
|
206
|
+
if (JSON.parse(stdout)[0]?.conclusion !== 'failure') return
|
|
207
|
+
await sessions.current.send({ type: Note, text: 'CI failed on the last push.' })
|
|
208
|
+
await sessions.current.send({ type: Prompt, text: 'Find the cause of the CI failure and fix it.' })
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The task's answer arrives once it finishes, on a `TaskEnd` listener:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
mod.on(UserPromptSubmit, { prompt: /^\/summarize/ }, async () => {
|
|
216
|
+
const task = await sessions.current.startTask({ prompt: 'Summarize README.md in one line.', agentType: 'Explore', name: 'Summarize the README' })
|
|
217
|
+
const ended = await new Promise<Task>((resolve) => task.on(TaskEnd, resolve))
|
|
218
|
+
const [answer] = await ended.owner.messages({ last: 1 })
|
|
219
|
+
return { hookSpecificOutput: { hookEventName: UserPromptSubmit, additionalContext: answer?.type === Reply ? answer.text : '' } }
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Message
|
|
224
|
+
|
|
225
|
+
Every message has these fields, and the fields of its type:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
type MessageOf<Type> = {
|
|
229
|
+
readonly id: string
|
|
230
|
+
readonly type: Type
|
|
231
|
+
readonly at: string
|
|
232
|
+
readonly session: string
|
|
233
|
+
readonly context: number
|
|
234
|
+
readonly turn?: string
|
|
235
|
+
readonly task?: string
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `id` names the transcript row the message is in. When one row holds several messages, such as a reply with its tool calls, each `id` ends in `:<n>`. `mod.ui.scroll({ to: { requestId: message.id.split(':')[0] } })` scrolls to the message's row ([ui.md](ui.md#scrolling)).
|
|
240
|
+
- `at` is its ISO time. `session` is the session's id, and `context` the number of the context it is in, from 1.
|
|
241
|
+
- `turn` is the `id` of the prompt that opened its turn. A task's message names its owner's turn. A message before the first prompt has none.
|
|
242
|
+
- `task` is the id of the task whose message it is. A message of the session itself has none.
|
|
243
|
+
|
|
244
|
+
| Type | Constant | Its own fields |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `'prompt'` | `Prompt` | `text`, `from`: the prompt that opens a turn. A slash command reads as the command typed, such as `/review 12`. |
|
|
247
|
+
| `'note'` | `Note` | `text`, `from`: text Claude reads that is not a prompt, such as a mod's note or a task's notification. |
|
|
248
|
+
| `'reply'` | `Reply` | `text`: what Claude said. |
|
|
249
|
+
| `'toolCall'` | `ToolCall` | `callId`, `name`, `input`, `cwd`: a tool Claude called, and the folder it called it in. |
|
|
250
|
+
| `'toolResult'` | `ToolResult` | `callId`, `text`, `isError`: what the tool answered. |
|
|
251
|
+
| `'skill'` | `Skill` | `name`, `text`: a Skill Claude Code loaded. |
|
|
252
|
+
| `'document'` | `Document` | `path`, `text`: a `CLAUDE.md` file Claude Code read for a folder. |
|
|
253
|
+
| `'notice'` | `Notice` | `text`: a line Claude Code added, such as a model switch. |
|
|
254
|
+
|
|
255
|
+
`Message` is any of these, and `message.type` tells them apart:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
for (const message of await sessions.current.messages()) {
|
|
259
|
+
if (message.type === ToolCall) console.log(message.name, message.input)
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Sender
|
|
264
|
+
|
|
265
|
+
A prompt's and a note's `from` says who sent it:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
type Sender = { readonly type: SenderType; readonly name?: string; readonly as?: 'person' }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| `type` | Who sent it |
|
|
272
|
+
| --- | --- |
|
|
273
|
+
| `Person` (`'person'`) | The person, at the keyboard or from a phone. |
|
|
274
|
+
| `'mod'` | A mod, named by `name`. `as: Person` when it sent the prompt as the person's. |
|
|
275
|
+
| `'session'` | Another session. |
|
|
276
|
+
| `'task'` | A task, reporting back. |
|
|
277
|
+
| `Schedule` (`'schedule'`) | A scheduled prompt. |
|
|
278
|
+
| `Channel` (`'channel'`) | A channel, such as Slack, named by `name`. |
|
|
279
|
+
| `Sdk` (`'sdk'`) | A program driving Claude Code through the SDK. |
|
|
280
|
+
|
|
281
|
+
## Turn, Context, Goal, and Task
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
type Turn = { readonly prompt: Prompt; readonly context: number; readonly openedAt: string; readonly closedAt?: string; messages(): Promise<Message[]>; search; on; before }
|
|
285
|
+
|
|
286
|
+
type Context = { readonly number: number; readonly openedAt: string; readonly closedAt?: string; turns(): Promise<Turn[]>; search; on; before }
|
|
287
|
+
|
|
288
|
+
type Goal = { readonly text: string; tasks(): Promise<Task[]>; search; on; before; update(change: { text: string }): Promise<void> }
|
|
289
|
+
|
|
290
|
+
type Task = {
|
|
291
|
+
readonly id: string
|
|
292
|
+
readonly title: string
|
|
293
|
+
readonly owner: Session
|
|
294
|
+
readonly openedBy: string
|
|
295
|
+
readonly openedAt: string
|
|
296
|
+
readonly closedAt?: string
|
|
297
|
+
tasks(): Promise<Task[]>
|
|
298
|
+
messages(): Promise<Message[]>
|
|
299
|
+
search; on; before
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- A turn's `closedAt` is set once the agent stops. A turn still running has none.
|
|
304
|
+
- A context's `number` counts from 1. Its `closedAt` is set at the compaction that ended it.
|
|
305
|
+
- A goal's `text` starts as the session's first prompt. `update` replaces it, and a `GoalChange` listener hears the new text.
|
|
306
|
+
- A task's `owner` is the session doing the work. Its goal is the task's prompt, its `agentType` and `model` are what the task runs, and its turns and messages are the task's own. Claude Code's checklist tools, `TaskCreate`, `TaskUpdate`, and `TaskList`, are not tasks: they are its todo list, which the `TodoCreated` and `TodoCompleted` hooks hear ([hooks.md](hooks.md)).
|
|
307
|
+
- A task's `openedBy` is the `callId` of the Agent call that started it, and empty for a task a mod started. Its `closedAt` is set once it answered.
|
|
308
|
+
|
|
309
|
+
## Test it
|
|
310
|
+
|
|
311
|
+
`testMod` fakes the conversation: `tested.add`, `tested.endTurn`, `tested.compact`, and `tested.startTask` build the current session, and the `sessions` option gives the earlier ones ([testing.md](testing.md#build-a-conversation)).
|
package/docs/state.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# State
|
|
2
2
|
|
|
3
|
-
`mod.state` holds the mod's values. Claude Mod Manager (cmod) saves them, redraws the mod's panes and
|
|
3
|
+
`mod.state` holds the mod's values. Claude Mod Manager (cmod) saves them, redraws the mod's panes and renders when one changes, and lets a person change the starting values without touching the mod's code.
|
|
4
4
|
|
|
5
5
|
## Declare the state
|
|
6
6
|
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
| Group | How long a value lasts | Saved |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| `memory` | Until `/clear`, `--resume`, `/branch`, or a reload of Claude Code or its plugins. A compaction keeps it. | No |
|
|
12
|
-
| `session` | This conversation. It comes back on `--resume`, starts over on `/clear`, and
|
|
12
|
+
| `session` | This conversation. It comes back on `--resume`, starts over on `/clear`, and a fork of the conversation starts with it. | Yes |
|
|
13
13
|
| `project` | This project, in every conversation. | Yes |
|
|
14
14
|
| `global` | Every project. | Yes |
|
|
15
15
|
|
|
16
|
-
cmod keeps each `session` value
|
|
16
|
+
cmod keeps each `session` value as long as Claude Code keeps the session's transcript, so a session comes back with its values for as long as `--resume` can find it.
|
|
17
|
+
|
|
18
|
+
A fork of the conversation, by `/branch`, `claude --resume <id> --fork-session`, or a move to the agents view, starts with the `session` values of the conversation it came from. Outside `/branch`, Claude Code does not say which conversation a fork came from, so the cmod plugin matches the fork's copied conversation to the one it came from, which needs its "Read your Claude Code conversations" permission. A fork starts with the starting values when that permission is off, when it was taken while the conversation it came from was still answering, or in a `-p` run.
|
|
19
|
+
|
|
20
|
+
cmod keeps each `project` value for the 20 projects that wrote it last, and a project past those 20 starts that value over from its starting value.
|
|
17
21
|
|
|
18
22
|
Put values the mod works out again on every prompt, such as a git status, in `memory`.
|
|
19
23
|
|
|
@@ -38,14 +42,14 @@ export const initialState: TasksState = {
|
|
|
38
42
|
Then pass it to `defineMod`:
|
|
39
43
|
|
|
40
44
|
```ts
|
|
41
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
45
|
+
import { defineMod, UserPromptSubmit } from '../node_modules/@cmodjs/core/mod.js'
|
|
42
46
|
import { initialState } from './state.js'
|
|
43
47
|
|
|
44
48
|
export const tasks = defineMod({
|
|
45
49
|
name: 'tasks',
|
|
46
50
|
state: initialState,
|
|
47
51
|
setup(mod) {
|
|
48
|
-
mod.on(
|
|
52
|
+
mod.on(UserPromptSubmit, () => {
|
|
49
53
|
mod.state.session.prompts += 1
|
|
50
54
|
})
|
|
51
55
|
},
|
|
@@ -62,13 +66,13 @@ Assign to a key. cmod saves the new value and redraws the mod.
|
|
|
62
66
|
- An object or an array in the state is frozen. Changing it in place throws `<mod>: mod.state.<path> cannot change in place.` Assign a new value instead:
|
|
63
67
|
|
|
64
68
|
```ts
|
|
65
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
69
|
+
import { defineMod, PostToolUse } from '../node_modules/@cmodjs/core/mod.js'
|
|
66
70
|
|
|
67
71
|
export const reads = defineMod({
|
|
68
72
|
name: 'reads',
|
|
69
73
|
state: { memory: { readFiles: [] as readonly string[] } },
|
|
70
74
|
setup(mod) {
|
|
71
|
-
mod.on(
|
|
75
|
+
mod.on(PostToolUse, (input) => {
|
|
72
76
|
mod.state.memory.readFiles = [...mod.state.memory.readFiles, ...input.files.read]
|
|
73
77
|
})
|
|
74
78
|
},
|