@cmodjs/core 1.0.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 (122) hide show
  1. package/docs/commands.md +22 -11
  2. package/docs/dependencies.md +6 -4
  3. package/docs/hooks.md +26 -3
  4. package/docs/index.md +1 -1
  5. package/docs/install-steps.md +5 -3
  6. package/docs/mod.md +29 -3
  7. package/docs/permissions.md +4 -4
  8. package/docs/session.md +126 -63
  9. package/docs/testing.md +7 -5
  10. package/docs/ui.md +9 -1
  11. package/jobs/program.d.ts.map +1 -1
  12. package/jobs/program.js +7 -2
  13. package/mod.d.ts +30 -6
  14. package/mod.d.ts.map +1 -1
  15. package/mod.js +34 -1
  16. package/package.json +1 -1
  17. package/records.d.ts +4 -1
  18. package/records.d.ts.map +1 -1
  19. package/records.js +6 -1
  20. package/register.d.ts.map +1 -1
  21. package/register.js +17 -0
  22. package/runtime/announce.d.ts.map +1 -1
  23. package/runtime/announce.js +2 -0
  24. package/runtime/before.d.ts +32 -0
  25. package/runtime/before.d.ts.map +1 -0
  26. package/runtime/before.js +214 -0
  27. package/runtime/claude.d.ts +2 -1
  28. package/runtime/claude.d.ts.map +1 -1
  29. package/runtime/conversations.d.ts +1 -0
  30. package/runtime/conversations.d.ts.map +1 -1
  31. package/runtime/conversations.js +333 -61
  32. package/runtime/dependencies.js +1 -1
  33. package/runtime/engine.d.ts +2 -6
  34. package/runtime/engine.d.ts.map +1 -1
  35. package/runtime/grants.d.ts.map +1 -1
  36. package/runtime/grants.js +8 -8
  37. package/runtime/hooks.d.ts +1 -1
  38. package/runtime/hooks.d.ts.map +1 -1
  39. package/runtime/hooks.js +19 -4
  40. package/runtime/installer.d.ts +1 -1
  41. package/runtime/installer.d.ts.map +1 -1
  42. package/runtime/installer.js +6 -5
  43. package/runtime/lifecycle.d.ts.map +1 -1
  44. package/runtime/lifecycle.js +129 -58
  45. package/runtime/metadata.d.ts.map +1 -1
  46. package/runtime/metadata.js +5 -4
  47. package/runtime/sessions.d.ts +26 -4
  48. package/runtime/sessions.d.ts.map +1 -1
  49. package/runtime/sessions.js +315 -117
  50. package/runtime/toaster.d.ts +5 -12
  51. package/runtime/toaster.d.ts.map +1 -1
  52. package/runtime/toaster.js +133 -23
  53. package/runtime/ui.d.ts.map +1 -1
  54. package/runtime/ui.js +26 -6
  55. package/session/context/context.d.ts +3 -1
  56. package/session/context/context.d.ts.map +1 -1
  57. package/session/context/turn/message/message.d.ts +1 -0
  58. package/session/context/turn/message/message.d.ts.map +1 -1
  59. package/session/context/turn/message/message.js +42 -1
  60. package/session/context/turn/turn.d.ts +2 -1
  61. package/session/context/turn/turn.d.ts.map +1 -1
  62. package/session/conversation.d.ts +20 -11
  63. package/session/conversation.d.ts.map +1 -1
  64. package/session/conversation.js +50 -27
  65. package/session/goal/goal.d.ts +3 -2
  66. package/session/goal/goal.d.ts.map +1 -1
  67. package/session/goal/task.d.ts +4 -4
  68. package/session/goal/task.d.ts.map +1 -1
  69. package/session/session.d.ts +80 -21
  70. package/session/session.d.ts.map +1 -1
  71. package/session/session.js +1 -0
  72. package/src/jobs/program.ts +7 -2
  73. package/src/mod.ts +48 -5
  74. package/src/records.ts +6 -1
  75. package/src/register.ts +17 -0
  76. package/src/runtime/announce.ts +2 -0
  77. package/src/runtime/before.ts +200 -0
  78. package/src/runtime/claude.ts +3 -0
  79. package/src/runtime/conversations.ts +337 -60
  80. package/src/runtime/dependencies.ts +1 -1
  81. package/src/runtime/engine.ts +2 -1
  82. package/src/runtime/grants.ts +8 -10
  83. package/src/runtime/hooks.ts +20 -4
  84. package/src/runtime/installer.ts +6 -5
  85. package/src/runtime/lifecycle.ts +135 -55
  86. package/src/runtime/metadata.ts +5 -4
  87. package/src/runtime/sessions.ts +316 -111
  88. package/src/runtime/toaster.ts +160 -44
  89. package/src/runtime/ui.ts +23 -4
  90. package/src/session/context/context.ts +3 -1
  91. package/src/session/context/turn/message/message.ts +39 -1
  92. package/src/session/context/turn/turn.ts +2 -1
  93. package/src/session/conversation.ts +56 -31
  94. package/src/session/goal/goal.ts +3 -2
  95. package/src/session/goal/task.ts +4 -4
  96. package/src/session/session.ts +62 -15
  97. package/src/testing/fake-claude.ts +4 -2
  98. package/src/testing/fake-sessions.ts +94 -20
  99. package/src/testing.ts +27 -3
  100. package/src/utils/call-effects.ts +2 -2
  101. package/src/utils/disposable.ts +3 -0
  102. package/src/utils/matcher.ts +1 -1
  103. package/src/utils/paths.ts +6 -3
  104. package/testing/fake-claude.d.ts +1 -0
  105. package/testing/fake-claude.d.ts.map +1 -1
  106. package/testing/fake-claude.js +3 -1
  107. package/testing/fake-sessions.d.ts +8 -1
  108. package/testing/fake-sessions.d.ts.map +1 -1
  109. package/testing/fake-sessions.js +97 -18
  110. package/testing.d.ts +1 -0
  111. package/testing.d.ts.map +1 -1
  112. package/testing.js +23 -2
  113. package/utils/call-effects.d.ts +1 -0
  114. package/utils/call-effects.d.ts.map +1 -1
  115. package/utils/call-effects.js +1 -1
  116. package/utils/disposable.d.ts +4 -0
  117. package/utils/disposable.d.ts.map +1 -0
  118. package/utils/disposable.js +1 -0
  119. package/utils/matcher.js +1 -1
  120. package/utils/paths.d.ts +2 -1
  121. package/utils/paths.d.ts.map +1 -1
  122. package/utils/paths.js +6 -3
@@ -0,0 +1,200 @@
1
+ import type { Args, Frozen } from 'claude-code'
2
+ import type { Disposable } from '../mod.js'
3
+ import { ContextEnd } from '../session/context/context.js'
4
+ import { Document } from '../session/context/turn/message/document.js'
5
+ import { Message, messagesOf, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
6
+ import { Note } from '../session/context/turn/message/note.js'
7
+ import { Notice } from '../session/context/turn/message/notice.js'
8
+ import { Prompt } from '../session/context/turn/message/prompt.js'
9
+ import { Reply } from '../session/context/turn/message/reply.js'
10
+ import { senderOf } from '../session/context/turn/message/sender.js'
11
+ import { Skill } from '../session/context/turn/message/skill.js'
12
+ import { ToolCall } from '../session/context/turn/message/tool-call.js'
13
+ import { ToolResult } from '../session/context/turn/message/tool-result.js'
14
+ import { TurnEnd } from '../session/context/turn/turn.js'
15
+ import type { ConversationState, SessionChange, TaskData } from '../session/conversation.js'
16
+ import { TaskEnd, TaskStart } from '../session/goal/task.js'
17
+ import type { BeforeEvent } from '../session/session.js'
18
+ import type { Claude } from './claude.js'
19
+ import { cmodPluginName } from './dependencies.js'
20
+ import type { Router } from './router.js'
21
+ import { toolInputOf } from './tool-calls.js'
22
+
23
+ export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns'>
24
+
25
+ export type Decide = (event: BeforeEvent, change: SessionChange) => Promise<Partial<Answers> | undefined>
26
+
27
+ export type Answers = { readonly text: string; readonly refuse: string; readonly input: Readonly<Record<string, unknown>>; readonly result: string; readonly owner: { readonly model: string } }
28
+
29
+ export type BeforeHooks = { readonly place: () => Promise<Place>; readonly wants: (event: BeforeEvent) => Promise<boolean>; readonly decide: Decide }
30
+
31
+ const appended: readonly BeforeEvent[] = [Note, Reply, ToolResult, Notice]
32
+
33
+ const relayTag = /^(?:<cross-session-message[^>]*>\s*)?<cmod-relay>([\s\S]*)<\/cmod-relay>(?:\s*<\/cross-session-message>[\s\S]*)?$/
34
+
35
+ export function relayOf(e: Frozen<Args<'session.receive'>>): string | undefined {
36
+ const isFromCmod = e.origin.kind === 'peer' && !('teammate' in e.origin) && e.origin.plugin === cmodPluginName
37
+ return isFromCmod ? relayTag.exec(e.text)?.[1] : undefined
38
+ }
39
+
40
+ export function installBefore({ claude, router, hooks }: { readonly claude: Claude; readonly router: Router; readonly hooks: BeforeHooks }): Disposable {
41
+ const { place, wants, decide } = hooks
42
+ const now = () => new Date().toISOString()
43
+ const asked = async (event: BeforeEvent, change: SessionChange) => (await decide(event, change)) ?? {}
44
+ const message = (where: Place, unplaced: Unplaced, task: string | undefined, isInTurn = true): SessionChange => {
45
+ const turn = isInTurn ? (task === undefined ? where.turn : where.taskTurns[task]) : undefined
46
+ const context = task === undefined ? where.context : (where.tasks[task]?.context ?? where.context)
47
+ return { type: Message, message: { ...unplaced, session: where.session, context, ...(turn === undefined ? {} : { turn: turn.prompt.id }), ...(task === undefined ? {} : { task }) } as Message }
48
+ }
49
+ const added: Disposable[] = [
50
+ router.add('prompt.submit', async (e, next) => {
51
+ if (!(await wants(Prompt)) || e.text.startsWith('/')) return next(e)
52
+ const where = await place()
53
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: senderOf(e.origin, false) }, undefined, false))
54
+ if (answer.refuse !== undefined) return { drop: answer.refuse }
55
+ return next(answer.text === undefined ? e : { ...e, text: answer.text })
56
+ }),
57
+ router.add('command.run', async (e, next) => {
58
+ if (!(await wants(Prompt))) return next(e)
59
+ const typed = e.args === '' ? `/${e.command}` : `/${e.command} ${e.args}`
60
+ const where = await place()
61
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: typed, from: senderOf(e.origin, false) }, undefined, false))
62
+ if (answer.refuse !== undefined) return { text: answer.refuse }
63
+ if (answer.text === undefined || answer.text === typed) return next(e)
64
+ const same = new RegExp(`^/${e.command}(?:\\s+|$)`).exec(answer.text)
65
+ if (same !== null) return next({ ...e, args: answer.text.slice(same[0].length) })
66
+ void claude.prompt.submit({ text: answer.text })
67
+ return {}
68
+ }),
69
+ router.add('session.receive', async (e, next) => {
70
+ if (!(await wants(Prompt)) || relayOf(e) !== undefined) return next(e)
71
+ const where = await place()
72
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: { type: 'session' } }, e.agentId, false))
73
+ if (answer.refuse !== undefined) return { consumed: answer.refuse }
74
+ return next(answer.text === undefined ? e : { ...e, text: answer.text })
75
+ }),
76
+ router.add('skill.prompt', async (e, next) => {
77
+ if (!(await wants(Skill))) return next(e)
78
+ const where = await place()
79
+ const answer = await asked(Skill, message(where, { id: '', at: now(), type: Skill, name: e.skill, text: e.text }, undefined))
80
+ return answer.text === undefined ? next(e) : { text: answer.text }
81
+ }),
82
+ router.add('prompt.attachment', async (e, next) => {
83
+ if (!(await wants(Document)) || e.type !== 'nested_memory') return next(e)
84
+ const where = await place()
85
+ const path = /^Contents of (.+?) \(/.exec(e.text)?.[1] ?? ''
86
+ const answer = await asked(Document, message(where, { id: '', at: now(), type: Document, path, text: e.text }, e.agentId))
87
+ if (answer.refuse !== undefined) return { text: null }
88
+ return answer.text === undefined ? next(e) : { text: answer.text }
89
+ }),
90
+ router.add('session.append', async (e, next) => {
91
+ const wanted = new Set((await Promise.all(appended.map(async (event) => ((await wants(event)) ? [event] : [])))).flat())
92
+ if (wanted.size === 0) return next(e)
93
+ const row = rowOfAppend({ uuid: e.uuid, door: e.door, origin: e.origin, message: e.message }, now(), '')
94
+ const found = messagesOf(row, e.agentId !== undefined)
95
+ if (!found.some((each) => wanted.has(each.type))) return next(e)
96
+ const where = await place()
97
+ const texts = new Map<number, string>()
98
+ for (const [part, unplaced] of found.entries()) {
99
+ if (!wanted.has(unplaced.type)) continue
100
+ const answer = await asked(unplaced.type, message(where, unplaced, e.agentId))
101
+ if (answer.text !== undefined) texts.set(part, answer.text)
102
+ }
103
+ return next(texts.size === 0 ? e : { ...e, message: { ...e.message, content: withTexts(row, texts) as typeof e.message.content } })
104
+ }),
105
+ router.add('tool.call', async (e, next) => {
106
+ if (!(await wants(ToolCall))) return next(e)
107
+ const where = await place()
108
+ const input = toolInputOf(e)
109
+ const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd: await claude.session.cwd() }, e.agentId))
110
+ if (answer.refuse !== undefined) return { deny: answer.refuse }
111
+ if (answer.result !== undefined) return { result: answer.result }
112
+ if (answer.input === undefined) return next(e)
113
+ const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => !(key in input)))
114
+ return next({ ...reserved, ...answer.input } as Frozen<Args<'tool.call'>>)
115
+ }),
116
+ router.add('agent.spawn', async (e, next) => {
117
+ if (!(await wants(TaskStart))) return next(e)
118
+ const where = await place()
119
+ const task: TaskData = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) }
120
+ const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } })
121
+ if (answer.refuse !== undefined) return { deny: answer.refuse }
122
+ return next(answer.owner === undefined ? e : { ...e, model: answer.owner.model })
123
+ }),
124
+ router.add('classic.SubagentStop', async (e, next) => {
125
+ if (!(await wants(TaskEnd)) && !(await wants(TurnEnd))) return next(e)
126
+ const where = await place()
127
+ const task = where.tasks[e.agent_id]
128
+ if (task === undefined) return next(e)
129
+ const turn = where.taskTurns[e.agent_id]
130
+ const ending = (await wants(TaskEnd)) ? await asked(TaskEnd, { type: TaskEnd, task: { ...task, closedAt: now() } }) : {}
131
+ const refuse = ending.refuse ?? ((await wants(TurnEnd)) && turn !== undefined ? (await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } })).refuse : undefined)
132
+ const below = await next(e)
133
+ return refuse === undefined ? below : { ...below, block: refuse }
134
+ }),
135
+ router.add('classic.Stop', async (e, next) => {
136
+ if (!(await wants(TurnEnd))) return next(e)
137
+ const where = await place()
138
+ if (where.turn === undefined) return next(e)
139
+ const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...where.turn, closedAt: now() } })
140
+ const below = await next(e)
141
+ return answer.refuse === undefined ? below : { ...below, block: answer.refuse }
142
+ }),
143
+ router.add('session.compact', async (e, next) => {
144
+ if (!(await wants(ContextEnd)) || e.trigger === 'precompute') return next(e)
145
+ const where = await place()
146
+ const task = e.agentId === undefined ? undefined : where.tasks[e.agentId]
147
+ if (e.agentId !== undefined && task === undefined) return next(e)
148
+ const context = task === undefined ? { number: where.context, openedAt: where.contextOpenedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, task: task.id }
149
+ const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } })
150
+ return answer.refuse === undefined ? next(e) : { skip: answer.refuse }
151
+ }),
152
+ ]
153
+ return { dispose: () => added.forEach((each) => each.dispose()) }
154
+ }
155
+
156
+ export function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange {
157
+ if (change.type === Message && answer.text !== undefined) return { ...change, message: { ...change.message, text: answer.text } as Message }
158
+ if (change.type === Message && answer.input !== undefined && change.message.type === ToolCall) return { ...change, message: { ...change.message, input: answer.input } }
159
+ if (change.type === TaskStart && answer.owner !== undefined) return { ...change, task: { ...change.task, model: answer.owner.model } }
160
+ return change
161
+ }
162
+
163
+ export async function foldAnswers(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined> {
164
+ let latest = change
165
+ let result: Partial<Answers> | undefined
166
+ for (const ask of askers) {
167
+ try {
168
+ const answer = answerOf(event, await ask(latest))
169
+ if (answer === undefined) continue
170
+ result = { ...result, ...answer }
171
+ if (answer.refuse !== undefined || answer.result !== undefined) return result
172
+ latest = withAnswer(latest, answer)
173
+ } catch (error) {
174
+ report(error)
175
+ }
176
+ }
177
+ return result
178
+ }
179
+
180
+ export function answerOf(event: BeforeEvent, answer: unknown): Partial<Answers> | undefined {
181
+ if (typeof answer !== 'object' || answer === null) return undefined
182
+ const given = answer as Partial<Answers>
183
+ const allowed: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>> = {
184
+ prompt: ['text', 'refuse'],
185
+ note: ['text'],
186
+ skill: ['text'],
187
+ document: ['text', 'refuse'],
188
+ reply: ['text'],
189
+ toolCall: ['input', 'result', 'refuse'],
190
+ toolResult: ['text'],
191
+ notice: ['text'],
192
+ taskStart: ['owner', 'refuse'],
193
+ taskEnd: ['refuse'],
194
+ turnEnd: ['refuse'],
195
+ contextEnd: ['refuse'],
196
+ }
197
+ const unread = Object.keys(given).filter((key) => !allowed[event].includes(key as keyof Answers))
198
+ if (unread.length > 0) throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`)
199
+ return given
200
+ }
@@ -30,6 +30,8 @@ import type {
30
30
  Timer,
31
31
  ToolSpec,
32
32
  UiCopyArgs,
33
+ UiFocusArgs,
34
+ UiFocusResult,
33
35
  UiCopyResult,
34
36
  UiLogOptions,
35
37
  UiOpenResult,
@@ -51,6 +53,7 @@ export type Claude = {
51
53
  panes(): Promise<readonly UiPane[]>
52
54
  scroll(args: UiScrollArgs): Promise<UiScrollResult>
53
55
  copy(args: UiCopyArgs): Promise<UiCopyResult>
56
+ focus(args: UiFocusArgs): Promise<UiFocusResult>
54
57
  resolve(e: ResolveInput): ElementTable<RenderSurface>
55
58
  }
56
59
  readonly process: {