@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.
Files changed (280) hide show
  1. package/docs/commands.md +61 -10
  2. package/docs/dependencies.md +16 -14
  3. package/docs/hooks.md +64 -29
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +13 -9
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +227 -175
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +311 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +68 -23
  13. package/docs/ui.md +116 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +14 -4
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +178 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +127 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +13 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +33 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +34 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +68 -0
  61. package/runtime/before.d.ts +32 -0
  62. package/runtime/before.d.ts.map +1 -0
  63. package/runtime/before.js +214 -0
  64. package/runtime/bound.d.ts +4 -0
  65. package/runtime/bound.d.ts.map +1 -0
  66. package/runtime/bound.js +9 -0
  67. package/runtime/claude.d.ts +4 -2
  68. package/runtime/claude.d.ts.map +1 -1
  69. package/runtime/conversations.d.ts +17 -0
  70. package/runtime/conversations.d.ts.map +1 -0
  71. package/runtime/conversations.js +563 -0
  72. package/runtime/deadline.d.ts +0 -1
  73. package/runtime/deadline.d.ts.map +1 -1
  74. package/runtime/deadline.js +1 -1
  75. package/runtime/dependencies.d.ts +2 -0
  76. package/runtime/dependencies.d.ts.map +1 -1
  77. package/runtime/dependencies.js +10 -1
  78. package/runtime/engine.d.ts +13 -0
  79. package/runtime/engine.d.ts.map +1 -0
  80. package/runtime/engine.js +10 -0
  81. package/runtime/grants.d.ts +4 -0
  82. package/runtime/grants.d.ts.map +1 -1
  83. package/runtime/grants.js +68 -31
  84. package/runtime/hooks.d.ts +5 -4
  85. package/runtime/hooks.d.ts.map +1 -1
  86. package/runtime/hooks.js +29 -9
  87. package/runtime/installer.d.ts +1 -1
  88. package/runtime/installer.d.ts.map +1 -1
  89. package/runtime/installer.js +17 -14
  90. package/runtime/lifecycle.d.ts +3 -1
  91. package/runtime/lifecycle.d.ts.map +1 -1
  92. package/runtime/lifecycle.js +414 -212
  93. package/runtime/metadata.d.ts +3 -1
  94. package/runtime/metadata.d.ts.map +1 -1
  95. package/runtime/metadata.js +25 -14
  96. package/runtime/options.d.ts +3 -1
  97. package/runtime/options.d.ts.map +1 -1
  98. package/runtime/options.js +4 -4
  99. package/runtime/programs.d.ts +8 -0
  100. package/runtime/programs.d.ts.map +1 -1
  101. package/runtime/programs.js +39 -1
  102. package/runtime/router.d.ts +2 -1
  103. package/runtime/router.d.ts.map +1 -1
  104. package/runtime/router.js +11 -3
  105. package/runtime/sessions.d.ts +58 -0
  106. package/runtime/sessions.d.ts.map +1 -0
  107. package/runtime/sessions.js +433 -0
  108. package/runtime/state.d.ts +6 -2
  109. package/runtime/state.d.ts.map +1 -1
  110. package/runtime/state.js +46 -13
  111. package/runtime/toaster.d.ts +24 -0
  112. package/runtime/toaster.d.ts.map +1 -0
  113. package/runtime/toaster.js +286 -0
  114. package/runtime/ui.d.ts +6 -17
  115. package/runtime/ui.d.ts.map +1 -1
  116. package/runtime/ui.js +62 -118
  117. package/session/context/context.d.ts +12 -0
  118. package/session/context/context.d.ts.map +1 -0
  119. package/session/context/context.js +2 -0
  120. package/session/context/turn/message/document.d.ts +7 -0
  121. package/session/context/turn/message/document.d.ts.map +1 -0
  122. package/session/context/turn/message/document.js +1 -0
  123. package/session/context/turn/message/message.d.ts +55 -0
  124. package/session/context/turn/message/message.d.ts.map +1 -0
  125. package/session/context/turn/message/message.js +178 -0
  126. package/session/context/turn/message/note.d.ts +8 -0
  127. package/session/context/turn/message/note.d.ts.map +1 -0
  128. package/session/context/turn/message/note.js +1 -0
  129. package/session/context/turn/message/notice.d.ts +6 -0
  130. package/session/context/turn/message/notice.d.ts.map +1 -0
  131. package/session/context/turn/message/notice.js +1 -0
  132. package/session/context/turn/message/prompt.d.ts +8 -0
  133. package/session/context/turn/message/prompt.d.ts.map +1 -0
  134. package/session/context/turn/message/prompt.js +1 -0
  135. package/session/context/turn/message/reply.d.ts +6 -0
  136. package/session/context/turn/message/reply.d.ts.map +1 -0
  137. package/session/context/turn/message/reply.js +1 -0
  138. package/session/context/turn/message/sender.d.ts +14 -0
  139. package/session/context/turn/message/sender.d.ts.map +1 -0
  140. package/session/context/turn/message/sender.js +30 -0
  141. package/session/context/turn/message/skill.d.ts +7 -0
  142. package/session/context/turn/message/skill.d.ts.map +1 -0
  143. package/session/context/turn/message/skill.js +1 -0
  144. package/session/context/turn/message/tool-call.d.ts +9 -0
  145. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  146. package/session/context/turn/message/tool-call.js +1 -0
  147. package/session/context/turn/message/tool-result.d.ts +8 -0
  148. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  149. package/session/context/turn/message/tool-result.js +1 -0
  150. package/session/context/turn/turn.d.ts +13 -0
  151. package/session/context/turn/turn.d.ts.map +1 -0
  152. package/session/context/turn/turn.js +2 -0
  153. package/session/conversation.d.ts +73 -0
  154. package/session/conversation.d.ts.map +1 -0
  155. package/session/conversation.js +146 -0
  156. package/session/goal/goal.d.ts +11 -0
  157. package/session/goal/goal.d.ts.map +1 -0
  158. package/session/goal/goal.js +1 -0
  159. package/session/goal/task.d.ts +16 -0
  160. package/session/goal/task.d.ts.map +1 -0
  161. package/session/goal/task.js +3 -0
  162. package/session/session.d.ts +158 -0
  163. package/session/session.d.ts.map +1 -0
  164. package/session/session.js +2 -0
  165. package/src/jobs/check.ts +7 -4
  166. package/src/jobs/context.ts +24 -82
  167. package/src/jobs/permissions/decide-permission.ts +10 -8
  168. package/src/jobs/permissions/find-project-scope.ts +8 -14
  169. package/src/jobs/permissions/match-target.ts +3 -3
  170. package/src/jobs/permissions.ts +8 -6
  171. package/src/jobs/program.ts +13 -4
  172. package/src/jobs/prompt.ts +21 -24
  173. package/src/jobs/schedule.ts +14 -14
  174. package/src/jobs/slash-command.ts +20 -14
  175. package/src/jobs/status-line.ts +9 -21
  176. package/src/jobs/tool.ts +17 -11
  177. package/src/mod.ts +252 -97
  178. package/src/options.ts +22 -22
  179. package/src/records.ts +40 -23
  180. package/src/register.ts +34 -5
  181. package/src/runtime/announce.ts +76 -0
  182. package/src/runtime/before.ts +200 -0
  183. package/src/runtime/bound.ts +12 -0
  184. package/src/runtime/claude.ts +7 -2
  185. package/src/runtime/conversations.ts +579 -0
  186. package/src/runtime/deadline.ts +2 -2
  187. package/src/runtime/dependencies.ts +13 -1
  188. package/src/runtime/engine.ts +23 -0
  189. package/src/runtime/grants.ts +69 -29
  190. package/src/runtime/hooks.ts +39 -14
  191. package/src/runtime/installer.ts +16 -14
  192. package/src/runtime/lifecycle.ts +413 -201
  193. package/src/runtime/metadata.ts +37 -14
  194. package/src/runtime/options.ts +19 -4
  195. package/src/runtime/programs.ts +40 -1
  196. package/src/runtime/router.ts +11 -4
  197. package/src/runtime/sessions.ts +447 -0
  198. package/src/runtime/state.ts +50 -14
  199. package/src/runtime/toaster.ts +334 -0
  200. package/src/runtime/ui.ts +65 -143
  201. package/src/session/context/context.ts +14 -0
  202. package/src/session/context/turn/message/document.ts +8 -0
  203. package/src/session/context/turn/message/message.ts +203 -0
  204. package/src/session/context/turn/message/note.ts +9 -0
  205. package/src/session/context/turn/message/notice.ts +7 -0
  206. package/src/session/context/turn/message/prompt.ts +9 -0
  207. package/src/session/context/turn/message/reply.ts +7 -0
  208. package/src/session/context/turn/message/sender.ts +38 -0
  209. package/src/session/context/turn/message/skill.ts +8 -0
  210. package/src/session/context/turn/message/tool-call.ts +10 -0
  211. package/src/session/context/turn/message/tool-result.ts +9 -0
  212. package/src/session/context/turn/turn.ts +15 -0
  213. package/src/session/conversation.ts +194 -0
  214. package/src/session/goal/goal.ts +10 -0
  215. package/src/session/goal/task.ts +19 -0
  216. package/src/session/session.ts +141 -0
  217. package/src/testing/fake-claude.ts +25 -17
  218. package/src/testing/fake-elements.ts +3 -2
  219. package/src/testing/fake-files.ts +1 -1
  220. package/src/testing/fake-sessions.ts +277 -0
  221. package/src/testing.ts +138 -35
  222. package/src/ui/components.ts +3 -3
  223. package/src/ui/elements.ts +22 -8
  224. package/src/ui/markdown.ts +22 -26
  225. package/src/ui/places.ts +49 -0
  226. package/src/utils/call-effects.ts +5 -5
  227. package/src/utils/changelog.ts +32 -0
  228. package/src/utils/disposable.ts +3 -0
  229. package/src/utils/matcher.ts +37 -0
  230. package/src/utils/metadata.ts +17 -15
  231. package/src/utils/parse-shell.ts +9 -8
  232. package/src/utils/paths.ts +21 -4
  233. package/src/utils/text.ts +4 -0
  234. package/testing/fake-claude.d.ts +5 -1
  235. package/testing/fake-claude.d.ts.map +1 -1
  236. package/testing/fake-claude.js +21 -12
  237. package/testing/fake-elements.js +4 -2
  238. package/testing/fake-files.js +1 -1
  239. package/testing/fake-sessions.d.ts +81 -0
  240. package/testing/fake-sessions.d.ts.map +1 -0
  241. package/testing/fake-sessions.js +249 -0
  242. package/testing.d.ts +34 -13
  243. package/testing.d.ts.map +1 -1
  244. package/testing.js +115 -23
  245. package/ui/components.d.ts +2 -3
  246. package/ui/components.d.ts.map +1 -1
  247. package/ui/components.js +3 -3
  248. package/ui/elements.d.ts.map +1 -1
  249. package/ui/elements.js +19 -9
  250. package/ui/markdown.d.ts +35 -33
  251. package/ui/markdown.d.ts.map +1 -1
  252. package/ui/markdown.js +14 -16
  253. package/ui/places.d.ts +43 -0
  254. package/ui/places.d.ts.map +1 -0
  255. package/ui/places.js +31 -0
  256. package/utils/call-effects.d.ts +2 -1
  257. package/utils/call-effects.d.ts.map +1 -1
  258. package/utils/call-effects.js +3 -3
  259. package/utils/changelog.d.ts +9 -0
  260. package/utils/changelog.d.ts.map +1 -0
  261. package/utils/changelog.js +29 -0
  262. package/utils/disposable.d.ts +4 -0
  263. package/utils/disposable.d.ts.map +1 -0
  264. package/utils/disposable.js +1 -0
  265. package/utils/matcher.d.ts +13 -0
  266. package/utils/matcher.d.ts.map +1 -0
  267. package/utils/matcher.js +32 -0
  268. package/utils/metadata.d.ts +3 -3
  269. package/utils/metadata.js +18 -14
  270. package/utils/parse-shell.js +9 -7
  271. package/utils/paths.d.ts +4 -1
  272. package/utils/paths.d.ts.map +1 -1
  273. package/utils/paths.js +19 -4
  274. package/utils/text.d.ts +1 -0
  275. package/utils/text.d.ts.map +1 -1
  276. package/utils/text.js +3 -0
  277. package/src/ui/slots.ts +0 -36
  278. package/ui/slots.d.ts +0 -208
  279. package/ui/slots.d.ts.map +0 -1
  280. package/ui/slots.js +0 -19
@@ -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 agents, 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.
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 | `mod.http.fetch` to that host. A path starting with `/` or `~/` names a socket, such as `"~/.docker/run/docker.sock"` |
20
- | `"run": ["gh"]`, or `"run": "*"` | Run gh on your computer | `mod.process.run`, `mod.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 | `mod.fs.write` and `mod.metadata.update` outside the project and the mod's data folder. A folder covers every file below it |
22
- | `"conversation": true` | Read this conversation | `mod.session.messages`, and reading Claude Code's transcript files |
23
- | `"prompt": true` | Add text Claude reads and start turns | `mod.session.append`, `mod.session.submit`, the `prompt` and `schedule` jobs, and the hook answers listed below |
24
- | `"model": true` | Ask a model, which uses your plan | `mod.model.complete` |
25
- | `"agents": true` | Start agents | `mod.agent.spawn` |
26
- | `"tools": true` | Use and change Claude's tool calls | `mod.claude.tool.call`, and a hook that changes a tool call's input |
27
- | `"config": true` | Change your Claude Code settings | writes to `.claude/settings.json`, `.claude/settings.local.json`, and `.mcp.json` |
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 slot renders, toasts, slash commands, tools of the mod's own, and hooks need no permission.
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
- mod.permissions.has(name: 'network' | 'run' | 'files', value: string): boolean
50
- mod.permissions.has(name: 'conversation' | 'prompt' | 'model' | 'agents' | 'tools' | 'config' | 'approve'): boolean
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
- `has` says whether the person has the permission turned on, so a mod hides a feature instead of failing it:
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.has('model')) {
57
- const verdict = await mod.model.complete({ prompt: 'Is this reply a waste of time?' })
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
@@ -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 slot renders when one changes, and lets a person change the starting values without touching the mod's code.
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 `/branch` copies it. | Yes |
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 for the 20 sessions that wrote it last, and each `project` value for the 20 projects that wrote it last. A session or project past those 20 starts that value over from its starting 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('UserPromptSubmit', () => {
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('PostToolUse', (input) => {
75
+ mod.on(PostToolUse, (input) => {
72
76
  mod.state.memory.readFiles = [...mod.state.memory.readFiles, ...input.files.read]
73
77
  })
74
78
  },