@cmodjs/core 1.5.1 → 2.0.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 +5 -5
- package/docs/index.md +12 -11
- package/docs/jobs.md +12 -16
- package/docs/mod.md +117 -39
- package/docs/permissions.md +17 -21
- package/docs/session.md +171 -82
- package/docs/state.md +5 -5
- package/docs/testing.md +57 -41
- package/docs/ui.md +9 -9
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +1 -1
- package/jobs/prompt.d.ts +1 -3
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -21
- package/jobs/slash-command.d.ts +1 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +1 -1
- package/jobs/status-line.d.ts +0 -1
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -4
- package/mod.d.ts +53 -105
- package/mod.d.ts.map +1 -1
- package/mod.js +18 -80
- package/package.json +1 -1
- package/records.d.ts +3 -1
- package/records.d.ts.map +1 -1
- package/records.js +14 -5
- package/register.d.ts.map +1 -1
- package/register.js +3 -1
- package/runtime/before.d.ts +5 -1
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +81 -39
- package/runtime/claude.d.ts +6 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +45 -26
- package/runtime/deadline.d.ts +17 -0
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +34 -0
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +12 -0
- package/runtime/grants.d.ts +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +19 -12
- package/runtime/hooks.d.ts +28 -11
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +20 -123
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +67 -50
- package/runtime/sessions.d.ts +31 -5
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +342 -81
- package/runtime/tool-calls.d.ts +0 -1
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +0 -19
- package/session/context/turn/message/instruction-file.d.ts +9 -0
- package/session/context/turn/message/instruction-file.d.ts.map +1 -0
- package/session/context/turn/message/instruction-file.js +4 -0
- package/session/context/turn/message/message.d.ts +5 -4
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -6
- package/session/context/turn/message/sender.d.ts +2 -1
- package/session/context/turn/message/sender.d.ts.map +1 -1
- package/session/context/turn/message/sender.js +4 -1
- package/session/context/turn/message/tool-call.d.ts +5 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -1
- package/session/context/turn/message/tool-result.d.ts +6 -1
- package/session/context/turn/message/tool-result.d.ts.map +1 -1
- package/session/context/turn/turn.d.ts +7 -0
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +10 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +14 -5
- package/session/goal/goal.d.ts +1 -1
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +2 -2
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +361 -26
- package/session/session.d.ts.map +1 -1
- package/session/session.js +40 -0
- package/src/jobs/permissions/decide-permission.ts +2 -1
- package/src/jobs/prompt.ts +3 -20
- package/src/jobs/slash-command.ts +2 -2
- package/src/jobs/status-line.ts +4 -7
- package/src/mod.ts +52 -147
- package/src/records.ts +15 -5
- package/src/register.ts +3 -1
- package/src/runtime/before.ts +90 -37
- package/src/runtime/claude.ts +5 -1
- package/src/runtime/conversations.ts +41 -25
- package/src/runtime/deadline.ts +38 -0
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/grants.ts +15 -10
- package/src/runtime/hooks.ts +48 -116
- package/src/runtime/lifecycle.ts +66 -51
- package/src/runtime/sessions.ts +366 -90
- package/src/runtime/tool-calls.ts +0 -19
- package/src/session/context/turn/message/instruction-file.ts +13 -0
- package/src/session/context/turn/message/message.ts +11 -9
- package/src/session/context/turn/message/sender.ts +5 -2
- package/src/session/context/turn/message/tool-call.ts +3 -0
- package/src/session/context/turn/message/tool-result.ts +6 -1
- package/src/session/context/turn/turn.ts +4 -0
- package/src/session/conversation.ts +21 -8
- package/src/session/goal/goal.ts +1 -1
- package/src/session/goal/task.ts +2 -2
- package/src/session/session.ts +227 -24
- package/src/testing/claude-code.ts +23 -0
- package/src/testing/fake-claude.ts +5 -1
- package/src/testing/fake-sessions.ts +38 -24
- package/src/testing.ts +84 -16
- package/testing/claude-code.d.ts +12 -0
- package/testing/claude-code.d.ts.map +1 -0
- package/testing/claude-code.js +10 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +4 -1
- package/testing/fake-sessions.d.ts +8 -6
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +35 -20
- package/testing.d.ts +38 -9
- package/testing.d.ts.map +1 -1
- package/testing.js +54 -5
- package/docs/hooks.md +0 -259
- package/session/context/turn/message/document.d.ts +0 -7
- package/session/context/turn/message/document.d.ts.map +0 -1
- package/session/context/turn/message/document.js +0 -1
- package/src/session/context/turn/message/document.ts +0 -8
package/docs/permissions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Permissions
|
|
2
2
|
|
|
3
|
-
A mod declares what it does beyond drawing and
|
|
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 `/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
|
|
|
@@ -19,15 +19,15 @@ They sit in the `package.json` `cmod` key, beside `install` and `keys`, because
|
|
|
19
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
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
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 |
|
|
23
|
-
| `"prompt": true` | Add text Claude reads and start turns | A session's `send`, a `
|
|
22
|
+
| `"conversation": true` | Read your other Claude Code conversations | Every session but the one the mod runs in: its messages, turns, contexts, goals, tasks, and events, `sessions.list`, `sessions.search`, the `on` methods of `sessions`, and a `before` method on another session. Also reading Claude Code's transcript files. Hearing and reading the mod's own session needs no permission |
|
|
23
|
+
| `"prompt": true` | Add text Claude reads and start turns | A session's `send`, `commands.run`, a `Turn`'s `abort`, the mod's `rules/`, the `prompt` and `schedule` jobs, and the `before` answers listed below |
|
|
24
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 `
|
|
26
|
-
| `"tools": true` | Use and change Claude's tool calls | A session's `callTool`,
|
|
27
|
-
| `"config": ["
|
|
28
|
-
| `"approve": true` | Approve Claude's tool calls for you | an allow or an ask from the mod's permission rules or
|
|
25
|
+
| `"tasks": true` | Start tasks that work beside Claude | A session's `startTask`, and a `beforeTaskStart` answer |
|
|
26
|
+
| `"tools": true` | Use and change Claude's tool calls | A session's `callTool`, and the `before` answers listed below |
|
|
27
|
+
| `"config": ["env.ANTHROPIC_BASE_URL"]` | Change env.ANTHROPIC_BASE_URL in your Claude Code settings | Each value `settings.user.update`, `settings.project.update`, and `settings.local.update` change. A key path covers its own value and every key below it, so `"env"` covers every key in `env`, and `await settings.user.update({ env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317' } })` needs `"env.ANTHROPIC_BASE_URL"` or `"env"`. `mcpServers` covers the project's `.mcp.json` |
|
|
28
|
+
| `"approve": true` | Approve Claude's tool calls for you | an allow or an ask from the mod's permission rules or listeners, and a `retry`. A refusal needs no permission |
|
|
29
29
|
|
|
30
|
-
Reading files, drawing panes and renders, toasts, slash commands, tools of the mod's own, and
|
|
30
|
+
Reading files, drawing panes and renders, toasts, slash commands, tools of the mod's own, listeners on the mod's own session, and a `refuse` answer 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
|
|
|
@@ -56,11 +56,11 @@ type Permissions = {
|
|
|
56
56
|
readonly network: { allows(host: string): boolean }
|
|
57
57
|
readonly run: { allows(program: string): boolean }
|
|
58
58
|
readonly files: { allows(path: string): boolean }
|
|
59
|
-
readonly config: { allows(
|
|
59
|
+
readonly config: { allows(keyPath: string): boolean }
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
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
|
|
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 path, such as `env.ANTHROPIC_BASE_URL`:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
if (mod.permissions.model) {
|
|
@@ -77,22 +77,18 @@ The call rejects, and the error names the fix:
|
|
|
77
77
|
- 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 /mods ci-watch.`
|
|
78
78
|
- A `program` job without its grant stops at once with the same line, with no restarts.
|
|
79
79
|
|
|
80
|
-
## When a
|
|
80
|
+
## When a before answer has no grant
|
|
81
81
|
|
|
82
|
-
cmod checks the answer each
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
ci-watch answered additionalContext without your grant to "Add text Claude reads and start turns", so cmod dropped it. Turn it on in /mods ci-watch.
|
|
86
|
-
```
|
|
82
|
+
cmod checks the answer each `before` listener returns, part by part. A part without its grant changes nothing, and cmod shows the error toast `The <method> listener failed` or logs once which part it dropped and the permission to turn on.
|
|
87
83
|
|
|
88
84
|
| Answer part | Permission |
|
|
89
85
|
| --- | --- |
|
|
90
|
-
| `
|
|
91
|
-
| a
|
|
92
|
-
|
|
|
93
|
-
| an allow
|
|
86
|
+
| `text`, `note`, `title`, and `firstPrompt`, a `prompt` job's text, a `beforeToolResult` answer, a `beforeTurnEnd` refusal, which keeps Claude going, and every `commands.beforeRun` answer but a refusal | `prompt` |
|
|
87
|
+
| a `beforeToolCall` answer | `tools` |
|
|
88
|
+
| a `beforeTaskStart` answer | `tasks` |
|
|
89
|
+
| an `allow`, with the `input` and `updates` a `beforePermissionRequest` allow carries, an `ask`, and `retry` | `approve` |
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
Any other `refuse` needs no permission. A tool of the mod's own, `mcp__<mod>__*`, answers freely.
|
|
96
92
|
|
|
97
93
|
## Test permissions
|
|
98
94
|
|
package/docs/session.md
CHANGED
|
@@ -8,7 +8,7 @@ import { Prompt, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
|
8
8
|
const prompts = (await sessions.current.messages()).filter((message) => message.type === Prompt)
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A mod hears and reads the session it runs in with no permission. Every other session needs the `conversation` permission. Sending to a session and changing its text need `prompt`, calling a tool and changing a tool call need `tools`, and starting or changing a task needs `tasks`. A `refuse` answer needs no permission ([permissions.md](permissions.md)).
|
|
12
12
|
|
|
13
13
|
## The shape of a conversation
|
|
14
14
|
|
|
@@ -31,27 +31,27 @@ Every part of the record answers the same three calls: `search` finds its messag
|
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
33
|
type Sessions = {
|
|
34
|
-
readonly current:
|
|
34
|
+
readonly current: CurrentSession
|
|
35
35
|
list(options?: { running?: boolean; since?: string; everyProject?: boolean }): Promise<Session[]>
|
|
36
|
-
search(text: string, options?: { running?: boolean; since?: string; everyProject?: boolean;
|
|
37
|
-
|
|
38
|
-
on(event, matcher, listener): Disposable
|
|
39
|
-
before(event, listener): Disposable
|
|
40
|
-
before(event, matcher, listener): Disposable
|
|
36
|
+
search(text: string, matcher?: Matcher<Message>, options?: { running?: boolean; since?: string; everyProject?: boolean; before?: Message }): AsyncIterable<Message>
|
|
37
|
+
onMessage(matcher?, listener, options?): Disposable // and every on method below
|
|
41
38
|
}
|
|
42
39
|
```
|
|
43
40
|
|
|
44
41
|
- `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
42
|
- `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`
|
|
43
|
+
- `search` yields the messages whose text contains `text`, any case, newest first, in the project's sessions or, with `everyProject`, every project's. `text` is at least 3 characters. It reads the index a page at a time as the loop asks for more, and `break` stops it. A matcher keeps the messages it matches, and `before` starts after a message, so a "load more" picks up where a loop broke. A tool call is searched by its name and the first 1,024 characters of each input value, an instruction file by its path and text, and a tool result is not searched.
|
|
47
44
|
|
|
48
45
|
```ts
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
46
|
+
const found: Message[] = []
|
|
47
|
+
for await (const message of sessions.search('deploy job', { type: Prompt }, { since: '1w' })) {
|
|
48
|
+
found.push(message)
|
|
49
|
+
if (found.length === 20) break
|
|
50
|
+
}
|
|
51
|
+
const more = sessions.search('deploy job', { type: Prompt }, { since: '1w', before: found.at(-1) })
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
- `on`
|
|
54
|
+
- Its `on` methods hear every running session of the project, this one included, and need the `conversation` permission ([Listen](#listen-to-a-record)). `sessions.current`'s methods hear this session with none. `sessions` has no `before` methods. To change what happens in another session, call a `before` method on that session ([Change what happens](#change-what-happens)).
|
|
55
55
|
|
|
56
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
57
|
|
|
@@ -69,22 +69,20 @@ type Session = {
|
|
|
69
69
|
readonly startedAt: string
|
|
70
70
|
readonly lastAt: string
|
|
71
71
|
readonly forkedFrom?: string
|
|
72
|
-
goal
|
|
72
|
+
readonly goal: Goal
|
|
73
73
|
context(): Promise<Context>
|
|
74
74
|
contexts(): Promise<Context[]>
|
|
75
75
|
turn(): Promise<Turn | undefined>
|
|
76
76
|
turns(options?: { last?: number }): Promise<Turn[]>
|
|
77
77
|
messages(options?: { after?: string; last?: number }): Promise<Message[]>
|
|
78
|
-
search(text: string,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
before(event, listener): Disposable
|
|
82
|
-
before(event, matcher, listener): Disposable
|
|
78
|
+
search(text: string, matcher?: Matcher<Message>): AsyncIterable<Message>
|
|
79
|
+
onMessage(matcher?, listener, options?): Disposable // and every on method below
|
|
80
|
+
beforePrompt(matcher?, listener, options?): Disposable // and every before method below
|
|
83
81
|
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>
|
|
82
|
+
startTask(task: { prompt: string; title: string; agentType?: string; model?: string; name?: string; cwd?: string }): Promise<Task>
|
|
85
83
|
callTool(call: { name: string; input: Record<string, unknown> }): Promise<ToolResult>
|
|
86
84
|
usage(): Promise<Usage>
|
|
87
|
-
|
|
85
|
+
surfaces(): Promise<Surface[]>
|
|
88
86
|
}
|
|
89
87
|
```
|
|
90
88
|
|
|
@@ -97,16 +95,16 @@ type Session = {
|
|
|
97
95
|
|
|
98
96
|
### Read a record
|
|
99
97
|
|
|
100
|
-
- `goal`
|
|
98
|
+
- `goal` is its goal. `sessions.current.goal` follows the session the mod runs in, as `sessions.current` does, so a listener added on it in `setup` keeps hearing across `/clear`, `/resume`, and a fork. The goal's `text()` reads its text now, and its `tasks()` answers the tasks the session handed on. Each task's `owner` reads like any session.
|
|
101
99
|
- `context` answers the context it is in now, and `contexts` all of them in order. A context's `turns()` answers its own.
|
|
102
100
|
- `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
101
|
- `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`
|
|
102
|
+
- `search` yields 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
103
|
|
|
106
104
|
```ts
|
|
107
105
|
const [last] = await sessions.current.turns({ last: 1 })
|
|
108
106
|
const calls = (await last?.messages() ?? []).filter((message) => message.type === ToolCall)
|
|
109
|
-
const explorers = (await
|
|
107
|
+
const explorers = (await sessions.current.goal.tasks()).filter((task) => task.owner.agentType === 'Explore')
|
|
110
108
|
```
|
|
111
109
|
|
|
112
110
|
A session that has ended keeps its record, so every read works on it.
|
|
@@ -114,73 +112,123 @@ A session that has ended keeps its record, so every read works on it.
|
|
|
114
112
|
### Listen to a record
|
|
115
113
|
|
|
116
114
|
```ts
|
|
117
|
-
part.on(
|
|
118
|
-
part.on(
|
|
115
|
+
part.on<Event>(listener: (payload, time: { deadline: number; signal: AbortSignal }) => unknown, options?: { timeoutMs?: number }): Disposable
|
|
116
|
+
part.on<Event>(matcher: Matcher<payload>, listener, options?): Disposable
|
|
119
117
|
```
|
|
120
118
|
|
|
121
|
-
`
|
|
119
|
+
Each event has its own method, such as `onReply` or `onTaskStart`, which 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 every `on` method, and each hears what happens inside it: an event reaches the part it happens in and every part above it.
|
|
120
|
+
|
|
121
|
+
A session is one agent: the conversation's main agent, or the agent doing a task. An event runs up one chain:
|
|
122
122
|
|
|
123
123
|
```
|
|
124
|
-
|
|
125
|
-
TaskStart, TaskEnd, and the owner's record ──> Task ──> its parent Task ──> Goal ──> Session
|
|
124
|
+
Turn ──> Context ──> Session ──> that session's Task ──> each Task above ──> Goal ──> sessions
|
|
126
125
|
```
|
|
127
126
|
|
|
128
|
-
|
|
127
|
+
The goal belongs to the main agent, so the main agent's events reach it too. So:
|
|
128
|
+
- `sessions.current.onToolCall(…)` hears the main agent's tool calls only.
|
|
129
|
+
- `sessions.current.goal.onToolCall(…)` hears every agent's in the session.
|
|
130
|
+
- `task.onMessage(…)` hears every message of the task's owner and of the tasks it handed on, and `task.owner.onMessage(…)` hears the owner's alone.
|
|
131
|
+
- `turn.onReply(…)` hears the replies of that turn only, and `sessions.onPrompt(…)` hears every prompt of every running session in the project.
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
A turn or a context that has ended takes no listener: its methods throw `<mod>: turn <id> has ended, so its onReply never fires. Listen on its Session.`
|
|
134
|
+
|
|
135
|
+
| Method | The listener gets | It happens in |
|
|
131
136
|
| --- | --- | --- |
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
137
|
+
| `onMessage` | Every new message. | Its turn |
|
|
138
|
+
| `onPrompt`, `onNote`, `onReply`, `onToolCall`, `onToolResult`, `onSkill`, `onInstructionFile`, `onNotice` | Each new message of that type. | Its turn |
|
|
139
|
+
| `onTurnStart`, `onTurnEnd` | The `Turn` that started or ended. | Its context |
|
|
140
|
+
| `onContextStart`, `onContextEnd` | The `Context` that started or ended. | The session |
|
|
141
|
+
| `onTaskStart`, `onTaskEnd` | The `Task` that started or ended. | Its parent task, or the goal |
|
|
142
|
+
| `onGoalChange` | The `Goal` with its new text. | The goal |
|
|
143
|
+
| `onUsageChange` | The session's new `Usage`. | The session |
|
|
139
144
|
|
|
140
145
|
The current session's events arrive as Claude Code writes them, and another running session's within a moment.
|
|
141
146
|
|
|
142
147
|
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
148
|
|
|
144
149
|
```ts
|
|
145
|
-
import {
|
|
150
|
+
import { sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
151
|
+
|
|
152
|
+
sessions.current.onToolCall({ name: /^(Edit|Write)$/ }, (call) => void (mod.state.session.edits += 1))
|
|
153
|
+
sessions.current.onTaskStart({ owner: { agentType: 'Explore' } }, (task) => mod.log(`Exploring: ${task.title}`))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A listener that throws shows the error toast `The <method> listener failed`, such as `The onReply listener failed`. `dispose()` on the `Disposable` stops the listener.
|
|
157
|
+
|
|
158
|
+
### Give a listener time
|
|
146
159
|
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
A listener gets 10 seconds. `timeoutMs` changes that, a whole number above 0 and at most `600000`. The second argument a listener gets is its `deadline`, the time it ends as `Date.now()` counts, and a `signal` that aborts then, so a slow call such as `http.fetch` can stop with it:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
sessions.current.onTurnEnd(async (turn, { signal }) => {
|
|
164
|
+
await http.fetch('https://hooks.example.com/turn', { method: 'POST', body: turn.answer ?? '', signal })
|
|
165
|
+
}, { timeoutMs: 30_000 })
|
|
149
166
|
```
|
|
150
167
|
|
|
151
|
-
A listener that
|
|
168
|
+
An `on` listener that passes its time stops quietly. A `before` listener that passes its time shows the error toast, and what was about to happen goes on as it was. With `onFailure: 'refuse'`, a `before` listener that throws or passes its time refuses instead, with the error as the reason, for a gate that must hold even when it breaks.
|
|
152
169
|
|
|
153
170
|
### Change what happens
|
|
154
171
|
|
|
155
172
|
```ts
|
|
156
|
-
part.before(
|
|
157
|
-
part.before(
|
|
173
|
+
part.before<Event>(listener: (payload, time) => Answer | void, options?: { timeoutMs?: number; onFailure?: 'pass' | 'refuse' }): Disposable
|
|
174
|
+
part.before<Event>(matcher: Matcher<payload>, listener, options?): Disposable
|
|
158
175
|
```
|
|
159
176
|
|
|
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
|
|
177
|
+
Each `before` method, such as `beforeToolCall`, runs `listener` just before something happens, and its answer changes or refuses it. Returning nothing lets it happen as it was. Each part except `sessions` has every `before` method, and hears what is about to happen inside it, the way `on` methods do.
|
|
161
178
|
|
|
162
|
-
|
|
|
179
|
+
| Method | The listener can answer | Covers |
|
|
163
180
|
| --- | --- | --- |
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
181
|
+
| `beforePrompt` | `{ text?, note?, title? }` or `{ refuse }` | A typed prompt, a mod's prompt, a command Claude Code sends as a prompt, such as a project's `/review`, and a message from another session. `note` reaches Claude beside a typed or a mod's prompt, and `title` names the session. A task's first prompt reaches `beforePrompt` as the subagent starts, where `note` is the only answer and the subagent reads it before its first step. To keep a task from starting, refuse it in `beforeTaskStart`. A built-in command such as `/model` is never a prompt: `commands.beforeRun` hears every command ([mod.md](mod.md#commands-slash-commands)). |
|
|
182
|
+
| `beforeNote` | `{ text }` | A note any mod or Claude Code adds |
|
|
183
|
+
| `beforeSkill` | `{ text }` | A Skill as Claude Code loads it |
|
|
184
|
+
| `beforeInstructionFile` | `{ text }` or `{ refuse }` | A `CLAUDE.md` file as Claude Code reads it. `refuse` leaves it out |
|
|
185
|
+
| `beforeReply`, `beforeNotice` | `{ text }` | The text the transcript keeps |
|
|
186
|
+
| `beforeToolResult` | `{ output?, isError?, note? }` | The result, after the tool ran. Here `output` is what the tool answered Claude Code, such as `{ stdout, stderr }` for Bash, and an `output` answer replaces it in the same shape, `note` adds text beside it, and `isError: true` makes Claude read it as a failure, with the note as the reason. A failed call takes only a note |
|
|
187
|
+
| `beforeToolCall` | `{ input }`, `{ result }`, or `{ refuse }` | A tool call. `result` answers it without running the tool |
|
|
188
|
+
| `beforeTaskStart` | `{ owner: { model } }` or `{ refuse }` | A task about to start, and the model its owner runs |
|
|
189
|
+
| `beforeTurnEnd` | `{ refuse }` | The agent about to stop. `refuse` sends it on with the reason. On `task.owner`, it is the task about to hand back its answer |
|
|
190
|
+
| `beforeContextEnd` | `{ refuse }` | A compaction about to start. `refuse` keeps the context |
|
|
174
191
|
|
|
175
192
|
```ts
|
|
176
|
-
sessions.current.
|
|
177
|
-
sessions.current.
|
|
178
|
-
sessions.current.
|
|
193
|
+
sessions.current.beforeToolCall({ name: 'Bash', input: { command: /\brm -rf\b/ } }, () => ({ refuse: 'Delete files one at a time.' }))
|
|
194
|
+
sessions.current.beforeTaskStart({ owner: { agentType: 'Explore' } }, () => ({ owner: { model: 'haiku' } }))
|
|
195
|
+
sessions.current.beforeTurnEnd(async () => ((await testsPass()) ? undefined : { refuse: 'The tests fail. Fix them before you stop.' }))
|
|
179
196
|
```
|
|
180
197
|
|
|
181
|
-
An answer that changes text needs the `prompt` permission, a `
|
|
198
|
+
An answer that changes text needs the `prompt` permission, a `beforeToolCall` answer needs `tools`, and a `beforeTaskStart` answer needs `tasks`. Without it the answer changes nothing, and the toast `The <method> listener failed` names the permission. When several listeners answer, each sees what the one before it changed, and the first `refuse` or `result` decides.
|
|
199
|
+
|
|
200
|
+
### The session's own events
|
|
201
|
+
|
|
202
|
+
`sessions.current` is a `CurrentSession`, which also hears what happens to the session itself. Each event has an `on` method, and one a mod can change also has a `before` method. A `before` method's answer reaches Claude Code, and an `on` listener never makes Claude Code wait. These methods hear the main agent, never a subagent.
|
|
203
|
+
|
|
204
|
+
| Method | Claude Code's moment | The payload | The listener can answer |
|
|
205
|
+
| --- | --- | --- | --- |
|
|
206
|
+
| `beforeProjectSetup`, `onProjectSetup` | `claude --init-only` sets up the project | `ProjectSetup`: `trigger` | nothing: Claude Code waits for it |
|
|
207
|
+
| `beforeSessionStart`, `onSessionStart` | a startup, resume, `/clear`, or fork, and for `beforeSessionStart` a compaction | `SessionStartAsked`, `SessionStarted`: `source`, `model?`, `title?`, `idleSeconds?`, `contextTokenCount?`, `isCacheWarm?`, `cacheWriteUsd?` | `{ note?, title?, reloadSkills?, firstPrompt? }` |
|
|
208
|
+
| `onSessionEnd` | the session ends | `SessionEnded`: `reason` | |
|
|
209
|
+
| `beforeModelChange`, `onModelChange` | `/model` switches the model | `ModelChange`: `model`, `previous`, `requested?`, `source`, `contextTokenCount`, `isCacheWarm`, `cacheTtl`, `cacheWriteUsd`, `pricing?` | `{ ask }` or `{ refuse }` |
|
|
210
|
+
| `onCwdChange` | the working folder moves | `CwdChange`: `previous`, and `cwd` is the new folder | |
|
|
211
|
+
| `onDirectoryAdd` | `/add-dir` adds a folder | `DirectoryAdd`: `directory`, `source` | |
|
|
212
|
+
| `beforeWorktreeCreate`, `onWorktreeCreate`, `onWorktreeRemove` | a worktree is made or removed | `Worktree`: `name?`, `path?` | `{ result }`, the worktree's path |
|
|
213
|
+
| `onNotification` | Claude Code notifies the person | `Notification`: `text`, `title?`, `type` | |
|
|
214
|
+
| `beforeElicitation`, `onElicitation` | an MCP server asks the person | `Elicitation`: `id?`, `server`, `text`, `mode?`, `url?`, `schema?` | `{ refuse }` |
|
|
215
|
+
| `beforeElicitationResponse`, `onElicitationResponse` | the person answers an MCP server | `ElicitationResponse`: `id?`, `server`, `mode?`, `action`, `content?` | `{ refuse }` |
|
|
216
|
+
| `beforePermissionCheck`, `onPermissionCheck` | Claude Code decides whether a tool call runs, asks, or is refused, before any dialog, and when a mod asks `tools.check` | `PermissionCheck`: `name`, `input`, `callId?`, `decision`, `reason?`, `rule?`, `ceiling?` | `{ allow: true }`, `{ ask }`, or `{ refuse }`. `allow` approves a call in every mode, auto mode included, and needs `approve`. A mod never overrides a refusal from the person's settings |
|
|
217
|
+
| `beforePermissionRequest`, `onPermissionRequest` | a permission dialog is about to show | `PermissionRequest`: `name`, `input`, `suggestions`, `mcpServer?` | `{ allow: true, input?, updates? }` or `{ refuse, interrupt? }` |
|
|
218
|
+
| `beforePermissionDenial`, `onPermissionDenial` | a tool call was denied | `PermissionDenial`: `name`, `input`, `callId`, `reason`, `mcpServer?` | `{ retry: true }` |
|
|
219
|
+
| `beforeToolBatch`, `onToolBatch` | a batch of tool calls finished | `ToolBatch`: `calls`, each `{ callId, name, input, output? }` | `{ note }` |
|
|
220
|
+
| `beforeTodoCreate`, `onTodoCreate`, `beforeTodoComplete`, `onTodoComplete` | a todo is added to or ticked on the checklist | `Todo`: `id`, `subject`, `description?`, `teammate?` | `{ refuse }` |
|
|
221
|
+
| `beforeReplyDisplay`, `onReplyDisplay` | a reply is about to show | `ReplyDisplay`: `replyId`, `index`, `isLast`, `text` | `{ text }`, what the person sees |
|
|
222
|
+
|
|
223
|
+
Every payload also carries where it happened: `session`, `cwd`, and `agentType?` and `permissionMode?` when Claude Code reports them. A matcher keeps the payloads it matches:
|
|
182
224
|
|
|
183
|
-
|
|
225
|
+
```ts
|
|
226
|
+
sessions.current.beforeSessionStart({ source: 'compact' }, () => ({ note: 'Re-read DESIGN.md before you continue.' }))
|
|
227
|
+
sessions.current.beforeModelChange({ model: /haiku/ }, () => ({ refuse: 'This project stays on opus.' }))
|
|
228
|
+
sessions.current.onCwdChange(({ cwd, previous }) => mod.log(`moved from ${previous} to ${cwd}`))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A `before` method on another session asks 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.
|
|
184
232
|
|
|
185
233
|
### Act on a session
|
|
186
234
|
|
|
@@ -191,12 +239,12 @@ Every action works on the current session and on every other running session. cm
|
|
|
191
239
|
- 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
240
|
- `as: Session` sends it to another session as a message from this one, which Claude Code shows as one session writing to another.
|
|
193
241
|
- 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`
|
|
242
|
+
- `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. `title` is the task's title, `agentType` names the owner's agent type, such as `Explore`, `name` is the address a teammate's `SendMessage` reaches it by, 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
243
|
- `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,
|
|
197
|
-
- `
|
|
244
|
+
- `usage` answers the session's model, its context window's fill, and its cost so far, as the status line shows them: `{ model, contextWindow: { tokenCount?, size?, 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.
|
|
245
|
+
- `surfaces` answers the Claude apps showing the session now: `'terminal'`, `'desktop'`, `'mobile'`, and `'vscode'`. An ended session shows in none.
|
|
198
246
|
|
|
199
|
-
A task's owner
|
|
247
|
+
A task's owner is a `TaskOwner`, a session without `startTask` and `callTool`, because Claude Code gives a subagent no way to start a task or run a tool from outside. It takes `send`, `usage`, and `surfaces`. 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
248
|
|
|
201
249
|
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
250
|
|
|
@@ -209,15 +257,21 @@ mod.every(60_000, async () => {
|
|
|
209
257
|
})
|
|
210
258
|
```
|
|
211
259
|
|
|
212
|
-
The task's answer arrives once it finishes, on
|
|
260
|
+
The task's answer arrives once it finishes, on an `onTaskEnd` listener:
|
|
213
261
|
|
|
214
262
|
```ts
|
|
215
|
-
mod.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
mod.use(slashCommand({
|
|
264
|
+
name: 'summarize',
|
|
265
|
+
description: 'Summarize the README in one line',
|
|
266
|
+
async reply() {
|
|
267
|
+
const task = await sessions.current.startTask({ prompt: 'Summarize README.md in one line.', title: 'Summarize the README', agentType: 'Explore' })
|
|
268
|
+
task.onTaskEnd(async (ended) => {
|
|
269
|
+
const [answer] = await ended.owner.messages({ last: 1 })
|
|
270
|
+
if (answer?.type === Reply) mod.ui.toast(answer.text)
|
|
271
|
+
})
|
|
272
|
+
return 'Summarizing the README.'
|
|
273
|
+
},
|
|
274
|
+
}))
|
|
221
275
|
```
|
|
222
276
|
|
|
223
277
|
## Message
|
|
@@ -243,15 +297,30 @@ type MessageOf<Type> = {
|
|
|
243
297
|
|
|
244
298
|
| Type | Constant | Its own fields |
|
|
245
299
|
| --- | --- | --- |
|
|
246
|
-
| `'prompt'` | `Prompt` | `text`, `from`: the prompt that opens a turn. A
|
|
300
|
+
| `'prompt'` | `Prompt` | `text`, `from`: the prompt that opens a turn. A command Claude Code sends as a prompt reads as typed, such as `/review 12`. |
|
|
247
301
|
| `'note'` | `Note` | `text`, `from`: text Claude reads that is not a prompt, such as a mod's note or a task's notification. |
|
|
248
302
|
| `'reply'` | `Reply` | `text`: what Claude said. |
|
|
249
|
-
| `'toolCall'` | `ToolCall` | `callId`, `name`, `input`, `cwd`: a tool Claude called,
|
|
250
|
-
| `'toolResult'` | `ToolResult` | `callId`, `
|
|
303
|
+
| `'toolCall'` | `ToolCall` | `callId`, `name`, `input`, `cwd`, `files`: a tool Claude called, the folder it called it in, and the files it reads and changes. |
|
|
304
|
+
| `'toolResult'` | `ToolResult` | `callId`, `name`, `input`, `cwd`, `files`, `output`, `isError`: the call's tool, input, folder, and files, and what the tool answered. `output` is the text Claude read, or the tool's own result for `callTool`. |
|
|
251
305
|
| `'skill'` | `Skill` | `name`, `text`: a Skill Claude Code loaded. |
|
|
252
|
-
| `'
|
|
306
|
+
| `'instructionFile'` | `InstructionFile` | `path`, `text`, `isRule`: a `CLAUDE.md` file or a rule file Claude Code read. `isRule` is true for a file in a `.claude/rules/` folder. |
|
|
253
307
|
| `'notice'` | `Notice` | `text`: a line Claude Code added, such as a model switch. |
|
|
254
308
|
|
|
309
|
+
### files: what a tool call reads and changes
|
|
310
|
+
|
|
311
|
+
A `ToolCall`'s and a `ToolResult`'s `files` is `{ read: string[]; changed: string[] }`, absolute paths that cmod works out from the call:
|
|
312
|
+
|
|
313
|
+
- `Read` reads its `file_path`. `Edit`, `Write`, and `NotebookEdit` change their path.
|
|
314
|
+
- `Grep` counts its `path`, or the working folder when it names none, and `Glob` counts its `pattern` joined to its `path`. Since `read` lists only files, a `Grep` of a folder and every `Glob` usually leave `read` empty.
|
|
315
|
+
- A `Bash` command reads and changes the files its commands name, such as `cat a.ts > b.ts`. A path that holds a shell expansion, such as `$HOME`, is left out.
|
|
316
|
+
- `read` lists only paths that are files now. `changed` lists every path the call writes, whether it exists or not.
|
|
317
|
+
- A path cmod cannot check, such as one it has no permission to read, is left out of `read`, and cmod writes the path and the reason to Claude Code's debug log.
|
|
318
|
+
- When cmod cannot work the files out, both lists are empty, and the reason goes to Claude Code's debug log.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
sessions.current.onToolResult(({ files }) => void (mod.state.session.edited += files.changed.length))
|
|
322
|
+
```
|
|
323
|
+
|
|
255
324
|
`Message` is any of these, and `message.type` tells them apart:
|
|
256
325
|
|
|
257
326
|
```ts
|
|
@@ -277,33 +346,53 @@ type Sender = { readonly type: SenderType; readonly name?: string; readonly as?:
|
|
|
277
346
|
| `Schedule` (`'schedule'`) | A scheduled prompt. |
|
|
278
347
|
| `Channel` (`'channel'`) | A channel, such as Slack, named by `name`. |
|
|
279
348
|
| `Sdk` (`'sdk'`) | A program driving Claude Code through the SDK. |
|
|
349
|
+
| `Engine` (`'engine'`) | Claude Code itself, such as the prompt that keeps a turn going after it hit its output limit. |
|
|
280
350
|
|
|
281
351
|
## Turn, Context, Goal, and Task
|
|
282
352
|
|
|
283
353
|
```ts
|
|
284
|
-
type Turn = {
|
|
354
|
+
type Turn = {
|
|
355
|
+
readonly prompt: Prompt
|
|
356
|
+
readonly context: number
|
|
357
|
+
readonly openedAt: string
|
|
358
|
+
readonly closedAt?: string
|
|
359
|
+
readonly answer?: string
|
|
360
|
+
readonly reason?: 'answer' | 'error'
|
|
361
|
+
readonly failure?: { readonly type: string; readonly text: string }
|
|
362
|
+
messages(): Promise<Message[]>
|
|
363
|
+
abort(): Promise<void>
|
|
364
|
+
search; on…; before…
|
|
365
|
+
}
|
|
285
366
|
|
|
286
|
-
type Context = { readonly number: number; readonly openedAt: string; readonly closedAt?: string; turns(): Promise<Turn[]>; search; on
|
|
367
|
+
type Context = { readonly number: number; readonly openedAt: string; readonly closedAt?: string; turns(): Promise<Turn[]>; search; on…; before… }
|
|
287
368
|
|
|
288
|
-
type Goal = {
|
|
369
|
+
type Goal = { text(): Promise<string>; tasks(): Promise<Task[]>; search; on…; before…; update(change: { text: string }): Promise<void> }
|
|
289
370
|
|
|
290
371
|
type Task = {
|
|
291
372
|
readonly id: string
|
|
292
373
|
readonly title: string
|
|
293
|
-
readonly owner:
|
|
374
|
+
readonly owner: TaskOwner
|
|
294
375
|
readonly openedBy: string
|
|
295
376
|
readonly openedAt: string
|
|
296
377
|
readonly closedAt?: string
|
|
297
378
|
tasks(): Promise<Task[]>
|
|
298
379
|
messages(): Promise<Message[]>
|
|
299
|
-
search; on
|
|
380
|
+
search; on…; before…
|
|
300
381
|
}
|
|
301
382
|
```
|
|
302
383
|
|
|
303
384
|
- A turn's `closedAt` is set once the agent stops. A turn still running has none.
|
|
385
|
+
- The turn `onTurnEnd` hears carries why it ended. `reason` is `'answer'` when Claude finished, with its last words in `answer`, and `'error'` when a request failed, with `failure` naming the error's `type`, such as `'rate_limit'`, and its `text`. A turn a mod kept going with `beforeTurnEnd` ends only at the stop that nothing refuses. A turn read later through `turns()` has `closedAt` and none of these.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
sessions.current.onTurnEnd((turn) => {
|
|
389
|
+
if (turn.failure !== undefined) mod.ui.toast.error(`Claude stopped: ${turn.failure.text}`)
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
- `abort()` stops a turn while it runs, a task's turn included, and needs the `prompt` permission. It throws on a turn that has ended. Claude Code shows no marker, so a mod that aborts tells the person with a toast.
|
|
304
393
|
- A context's `number` counts from 1. Its `closedAt` is set at the compaction that ended it.
|
|
305
|
-
- A goal's
|
|
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
|
|
394
|
+
- A goal's text starts as the session's first prompt, and `text()` reads it. `update` replaces it, and `onGoalChange` hears the goal, whose `text()` reads the new text.
|
|
395
|
+
- A task's `owner` is a `TaskOwner`: the session doing the work, which can't start tasks or call tools. 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 `onTodoCreate` and `onTodoComplete` hear.
|
|
307
396
|
- 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
397
|
|
|
309
398
|
## Test it
|
package/docs/state.md
CHANGED
|
@@ -42,14 +42,14 @@ export const initialState: TasksState = {
|
|
|
42
42
|
Then pass it to `defineMod`:
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
import { defineMod,
|
|
45
|
+
import { defineMod, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
46
46
|
import { initialState } from './state.js'
|
|
47
47
|
|
|
48
48
|
export const tasks = defineMod({
|
|
49
49
|
name: 'tasks',
|
|
50
50
|
state: initialState,
|
|
51
51
|
setup(mod) {
|
|
52
|
-
|
|
52
|
+
sessions.current.onPrompt(() => {
|
|
53
53
|
mod.state.session.prompts += 1
|
|
54
54
|
})
|
|
55
55
|
},
|
|
@@ -66,14 +66,14 @@ Assign to a key. cmod saves the new value and redraws the mod.
|
|
|
66
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:
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
|
-
import { defineMod,
|
|
69
|
+
import { defineMod, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
70
70
|
|
|
71
71
|
export const reads = defineMod({
|
|
72
72
|
name: 'reads',
|
|
73
73
|
state: { memory: { readFiles: [] as readonly string[] } },
|
|
74
74
|
setup(mod) {
|
|
75
|
-
|
|
76
|
-
mod.state.memory.readFiles = [...mod.state.memory.readFiles, ...
|
|
75
|
+
sessions.current.onToolResult(({ files }) => {
|
|
76
|
+
mod.state.memory.readFiles = [...mod.state.memory.readFiles, ...files.read]
|
|
77
77
|
})
|
|
78
78
|
},
|
|
79
79
|
})
|