allaboard 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/agent/claude/backend.js +124 -0
- package/dist/agent/claude/capabilities.js +30 -0
- package/dist/agent/claude/chrome.js +42 -0
- package/dist/agent/claude/hooks.js +37 -0
- package/dist/agent/claude/info.js +61 -0
- package/dist/agent/claude/listing.js +230 -0
- package/dist/agent/claude/module.js +82 -0
- package/dist/agent/claude/prompt.js +24 -0
- package/dist/agent/claude/record.js +9 -0
- package/dist/agent/claude/requests.js +122 -0
- package/dist/agent/claude/session.js +441 -0
- package/dist/agent/claude/translate.js +581 -0
- package/dist/agent/claude/trust.js +576 -0
- package/dist/agent/claude/usage-probe.js +103 -0
- package/dist/agent/claude/usage.js +110 -0
- package/dist/agent/claude/wire.js +22 -0
- package/dist/agent/seam.js +21 -0
- package/dist/chat/seam.js +10 -0
- package/dist/chat/slack/app/answers.js +209 -0
- package/dist/chat/slack/app/app.js +262 -0
- package/dist/chat/slack/app/clip.js +191 -0
- package/dist/chat/slack/app/guards.js +162 -0
- package/dist/chat/slack/app/home-controls.js +91 -0
- package/dist/chat/slack/app/messages.js +181 -0
- package/dist/chat/slack/app/open.js +392 -0
- package/dist/chat/slack/app/order.js +37 -0
- package/dist/chat/slack/app/requests.js +482 -0
- package/dist/chat/slack/app/resume-bind.js +71 -0
- package/dist/chat/slack/app/wire.js +83 -0
- package/dist/chat/slack/attachments.js +348 -0
- package/dist/chat/slack/bind.js +67 -0
- package/dist/chat/slack/channel.js +67 -0
- package/dist/chat/slack/channels.js +43 -0
- package/dist/chat/slack/delete.js +278 -0
- package/dist/chat/slack/footer.js +43 -0
- package/dist/chat/slack/hold.js +37 -0
- package/dist/chat/slack/home.js +1069 -0
- package/dist/chat/slack/module.js +174 -0
- package/dist/chat/slack/notice.js +64 -0
- package/dist/chat/slack/openfile/modal.js +362 -0
- package/dist/chat/slack/openfile/read.js +29 -0
- package/dist/chat/slack/quiet-logger.js +52 -0
- package/dist/chat/slack/repair.js +123 -0
- package/dist/chat/slack/reply/blocks.js +372 -0
- package/dist/chat/slack/reply/chars.js +59 -0
- package/dist/chat/slack/reply/clients.js +171 -0
- package/dist/chat/slack/reply/errors.js +125 -0
- package/dist/chat/slack/reply/escape.js +25 -0
- package/dist/chat/slack/reply/limiter.js +86 -0
- package/dist/chat/slack/reply/markdown.js +210 -0
- package/dist/chat/slack/reply/places.js +262 -0
- package/dist/chat/slack/reply/sinks.js +2403 -0
- package/dist/chat/slack/reply/status.js +433 -0
- package/dist/chat/slack/reply/tasks.js +11 -0
- package/dist/chat/slack/requests.js +552 -0
- package/dist/chat/slack/resume.js +127 -0
- package/dist/chat/slack/setup.js +114 -0
- package/dist/chat/slack/texts.js +185 -0
- package/dist/chat/slack/thread.js +537 -0
- package/dist/chat/slack/update.js +71 -0
- package/dist/chat/slack/voice.js +84 -0
- package/dist/cli.js +326 -0
- package/dist/clock.js +21 -0
- package/dist/core/cleanup.js +145 -0
- package/dist/core/commands.js +172 -0
- package/dist/core/config.js +174 -0
- package/dist/core/files/changed.js +125 -0
- package/dist/core/files/listing.js +382 -0
- package/dist/core/files/paths.js +9 -0
- package/dist/core/files/read.js +191 -0
- package/dist/core/files/waiting.js +34 -0
- package/dist/core/flows/answers.js +133 -0
- package/dist/core/flows/ask-owner.js +95 -0
- package/dist/core/flows/asking.js +14 -0
- package/dist/core/flows/bind.js +95 -0
- package/dist/core/flows/contract.js +1 -0
- package/dist/core/flows/flows.js +35 -0
- package/dist/core/flows/hold.js +22 -0
- package/dist/core/flows/inbound.js +24 -0
- package/dist/core/flows/lists.js +21 -0
- package/dist/core/flows/messages.js +293 -0
- package/dist/core/flows/notices.js +51 -0
- package/dist/core/flows/repair.js +106 -0
- package/dist/core/flows/resume.js +163 -0
- package/dist/core/flows/setup.js +130 -0
- package/dist/core/flows/status.js +98 -0
- package/dist/core/flows/talk.js +207 -0
- package/dist/core/flows/update.js +370 -0
- package/dist/core/flows/words.js +171 -0
- package/dist/core/folders.js +93 -0
- package/dist/core/footer.js +313 -0
- package/dist/core/guide.js +50 -0
- package/dist/core/hold.js +126 -0
- package/dist/core/instance.js +46 -0
- package/dist/core/lock.js +412 -0
- package/dist/core/models.js +10 -0
- package/dist/core/paths.js +75 -0
- package/dist/core/port/file-flows.js +1 -0
- package/dist/core/port/home.js +1 -0
- package/dist/core/port/requests.js +1 -0
- package/dist/core/port/threads.js +1 -0
- package/dist/core/port/updates.js +1 -0
- package/dist/core/port.js +17 -0
- package/dist/core/reply/fold.js +175 -0
- package/dist/core/reply/previews.js +258 -0
- package/dist/core/reply/renderer.js +846 -0
- package/dist/core/reply/tail.js +24 -0
- package/dist/core/reply/words.js +32 -0
- package/dist/core/requests.js +158 -0
- package/dist/core/sessions/constants.js +61 -0
- package/dist/core/sessions/deps.js +1 -0
- package/dist/core/sessions/directory.js +86 -0
- package/dist/core/sessions/errors.js +79 -0
- package/dist/core/sessions/manager.js +378 -0
- package/dist/core/sessions/session.js +3297 -0
- package/dist/core/sessions/tasks.js +266 -0
- package/dist/core/sessions/turn.js +45 -0
- package/dist/core/setup.js +28 -0
- package/dist/core/state.js +724 -0
- package/dist/core/texts.js +451 -0
- package/dist/core/updates.js +332 -0
- package/dist/daemon.js +566 -0
- package/dist/host/npm.js +134 -0
- package/dist/host/program.js +49 -0
- package/dist/host/run.js +52 -0
- package/dist/host/service/launchd.js +385 -0
- package/dist/host/service/listing.js +42 -0
- package/dist/host/service/seam.js +1 -0
- package/dist/log.js +37 -0
- package/dist/main.js +29 -0
- package/dist/node-version.js +40 -0
- package/dist/registry.js +12 -0
- package/dist/semver.js +53 -0
- package/dist/text.js +89 -0
- package/dist/updates-host.js +113 -0
- package/dist/values.js +30 -0
- package/dist/version.js +54 -0
- package/package.json +32 -4
- package/slack-app-manifest.json +50 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's wire records into the daemon's session events.
|
|
3
|
+
*
|
|
4
|
+
* A record is one object as the CLI wrote it, which the Agent SDK's `query()` yields unchanged.
|
|
5
|
+
* Each is read as `unknown` and narrowed field by field. What is read here is what the renderer
|
|
6
|
+
* and the session need off the SDK's messages, so the core decides from events and never from a
|
|
7
|
+
* wire record.
|
|
8
|
+
*
|
|
9
|
+
* Three things need memory across records, and `Translator` keeps them for one session: which
|
|
10
|
+
* messages a stream announced, since Claude Code sends a streamed text again whole; which
|
|
11
|
+
* prompts the daemon sent, since a prompt comes back as a user record; and whether a compaction
|
|
12
|
+
* is under way, since the status record that ends one says so only to who knows it started.
|
|
13
|
+
*/
|
|
14
|
+
import { strip } from "../../text.js";
|
|
15
|
+
import { isRecord, records } from "../../values.js";
|
|
16
|
+
import { integer, number, string, words } from "./wire.js";
|
|
17
|
+
// Each task_type (measured on Claude Code 2.1.280: `local_bash` for a background command,
|
|
18
|
+
// `local_agent` for a subagent). A type not listed is a task of no kind the daemon names.
|
|
19
|
+
const TASK_KINDS = new Map([
|
|
20
|
+
["local_agent", "subagent"],
|
|
21
|
+
["local_bash", "command"],
|
|
22
|
+
]);
|
|
23
|
+
// The Python package's `TERMINAL_TASK_STATUSES` (claude-agent-sdk 0.2.165), which the TypeScript
|
|
24
|
+
// package does not export: a task can end with a `task_updated` of one of these and no
|
|
25
|
+
// notification.
|
|
26
|
+
const TERMINAL_TASK_STATUSES = new Set([
|
|
27
|
+
"completed",
|
|
28
|
+
"failed",
|
|
29
|
+
"stopped",
|
|
30
|
+
"killed",
|
|
31
|
+
]);
|
|
32
|
+
const INTERRUPTED = new Set(["aborted_streaming", "aborted_tools"]);
|
|
33
|
+
const AUTHENTICATION_FAILED = "authentication_failed";
|
|
34
|
+
const SYNTHETIC_MODEL = "<synthetic>";
|
|
35
|
+
// The signs a line of a `structuredPatch` hunk opens with; the backslash is
|
|
36
|
+
// ``, about the line above.
|
|
37
|
+
const SIGNS = new Set(["-", "+", " ", "\\"]);
|
|
38
|
+
// Runs remembered for a notification that may never be folded (one a turn of its own reports).
|
|
39
|
+
const RUNS_KEPT = 200;
|
|
40
|
+
/**
|
|
41
|
+
* A record's kind: its `type`, with the `subtype` of a `system` record and the event type of a
|
|
42
|
+
* `stream_event`. Null for what is no record.
|
|
43
|
+
*/
|
|
44
|
+
export function kindOf(record) {
|
|
45
|
+
if (!isRecord(record))
|
|
46
|
+
return null;
|
|
47
|
+
const type = string(record.type);
|
|
48
|
+
if (type === null)
|
|
49
|
+
return null;
|
|
50
|
+
if (type === "system")
|
|
51
|
+
return `system:${string(record.subtype) ?? ""}`;
|
|
52
|
+
if (type === "stream_event") {
|
|
53
|
+
return `stream_event:${isRecord(record.event) ? (string(record.event.type) ?? "") : ""}`;
|
|
54
|
+
}
|
|
55
|
+
return type;
|
|
56
|
+
}
|
|
57
|
+
function parentOf(record) {
|
|
58
|
+
return words(record.parent_tool_use_id);
|
|
59
|
+
}
|
|
60
|
+
// Stream events: the Claude API's streaming events, passed through unparsed.
|
|
61
|
+
function messageStart(memory, record) {
|
|
62
|
+
const event = isRecord(record.event) ? record.event : {};
|
|
63
|
+
const messageId = isRecord(event.message) ? string(event.message.id) : null;
|
|
64
|
+
const parentCallId = parentOf(record);
|
|
65
|
+
if (parentCallId === null)
|
|
66
|
+
memory.open = true;
|
|
67
|
+
if (messageId !== null)
|
|
68
|
+
memory.streamed.add(messageId);
|
|
69
|
+
else if (parentCallId === null)
|
|
70
|
+
memory.unnamed = true;
|
|
71
|
+
return [{ type: "message_started", parentCallId }];
|
|
72
|
+
}
|
|
73
|
+
function messageStop(memory, record) {
|
|
74
|
+
const parentCallId = parentOf(record);
|
|
75
|
+
if (parentCallId === null)
|
|
76
|
+
memory.open = false;
|
|
77
|
+
return [{ type: "message_ended", parentCallId }];
|
|
78
|
+
}
|
|
79
|
+
// A subagent's text never reaches a reply, so no text event carries a parent call.
|
|
80
|
+
function contentBlockStart(_memory, record) {
|
|
81
|
+
const event = isRecord(record.event) ? record.event : {};
|
|
82
|
+
if (!isRecord(event.content_block) || event.content_block.type !== "text")
|
|
83
|
+
return [];
|
|
84
|
+
if (parentOf(record) !== null)
|
|
85
|
+
return [];
|
|
86
|
+
return [{ type: "text_started" }];
|
|
87
|
+
}
|
|
88
|
+
function contentBlockDelta(_memory, record) {
|
|
89
|
+
const event = isRecord(record.event) ? record.event : {};
|
|
90
|
+
if (!isRecord(event.delta) || event.delta.type !== "text_delta")
|
|
91
|
+
return [];
|
|
92
|
+
const text = words(event.delta.text);
|
|
93
|
+
if (text === null || parentOf(record) !== null)
|
|
94
|
+
return [];
|
|
95
|
+
return [{ type: "text_delta", text }];
|
|
96
|
+
}
|
|
97
|
+
// Messages.
|
|
98
|
+
/** The text of a message's text blocks, joined and trimmed. */
|
|
99
|
+
function wordsOf(blocks) {
|
|
100
|
+
return strip(blocks
|
|
101
|
+
.filter((block) => block.type === "text")
|
|
102
|
+
.map((block) => string(block.text) ?? "")
|
|
103
|
+
.join(""));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a message's text reached the core through no stream event. At the top level, never
|
|
107
|
+
* while a streamed message is unfinished (its text may be this one's, sent again whole after a
|
|
108
|
+
* failed stream, which no recording shows) or once a stream named no id.
|
|
109
|
+
*/
|
|
110
|
+
function unannounced(memory, messageId) {
|
|
111
|
+
return !memory.streamed.has(messageId) && !memory.open && !memory.unnamed;
|
|
112
|
+
}
|
|
113
|
+
function callStarted(block, parentCallId) {
|
|
114
|
+
const callId = string(block.id);
|
|
115
|
+
const toolName = string(block.name);
|
|
116
|
+
if (callId === null || toolName === null)
|
|
117
|
+
return [];
|
|
118
|
+
const input = isRecord(block.input) ? block.input : {};
|
|
119
|
+
return [{ type: "call_started", callId, toolName, input, parentCallId }];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The text of a tool result, whatever its form: a string, the text parts of a list a line each,
|
|
123
|
+
* or the type of a single object (a server tool's result). Null for anything else.
|
|
124
|
+
*/
|
|
125
|
+
function resultText(content) {
|
|
126
|
+
if (typeof content === "string")
|
|
127
|
+
return content;
|
|
128
|
+
if (Array.isArray(content)) {
|
|
129
|
+
return records(content)
|
|
130
|
+
.map((part) => string(part.text) ?? "")
|
|
131
|
+
.join("\n");
|
|
132
|
+
}
|
|
133
|
+
if (isRecord(content))
|
|
134
|
+
return string(content.type) ?? "";
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function callEnded(block, parentCallId, isError, change, model = null) {
|
|
138
|
+
const callId = string(block.tool_use_id);
|
|
139
|
+
if (callId === null)
|
|
140
|
+
return [];
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
type: "call_ended",
|
|
144
|
+
callId,
|
|
145
|
+
isError,
|
|
146
|
+
output: resultText(block.content),
|
|
147
|
+
fileChange: change,
|
|
148
|
+
model,
|
|
149
|
+
parentCallId,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
function assistant(memory, record) {
|
|
154
|
+
const message = isRecord(record.message) ? record.message : {};
|
|
155
|
+
if (!Array.isArray(message.content))
|
|
156
|
+
return [];
|
|
157
|
+
const blocks = records(message.content);
|
|
158
|
+
const parentCallId = parentOf(record);
|
|
159
|
+
const text = wordsOf(blocks);
|
|
160
|
+
if (record.error !== undefined && record.error !== null) {
|
|
161
|
+
// Claude Code wrote this message itself, about a request of its own that failed: its words
|
|
162
|
+
// name the failure and what to do next, and nothing else in it is a call or an answer. In
|
|
163
|
+
// the recorded 529 they come in this message with no stream event of their own.
|
|
164
|
+
const category = String(record.error);
|
|
165
|
+
const kind = category === AUTHENTICATION_FAILED ? "authentication" : "other";
|
|
166
|
+
return [{ type: "agent_error", kind, category, text, parentCallId }];
|
|
167
|
+
}
|
|
168
|
+
const events = [];
|
|
169
|
+
// A message Claude Code wrote itself (a local command's answer, recorded: `usage.jsonl`) names
|
|
170
|
+
// `<synthetic>`, no model the session runs on.
|
|
171
|
+
const model = words(message.model);
|
|
172
|
+
if (parentCallId === null && model !== null && model !== SYNTHETIC_MODEL) {
|
|
173
|
+
memory.mainModel = model;
|
|
174
|
+
}
|
|
175
|
+
const messageId = string(message.id);
|
|
176
|
+
if (text !== "" &&
|
|
177
|
+
parentCallId === null &&
|
|
178
|
+
messageId !== null &&
|
|
179
|
+
unannounced(memory, messageId)) {
|
|
180
|
+
// Text no stream event announced is a command's own output (`Goal set: …` before a goal's
|
|
181
|
+
// first inner turn, recorded: `goal.jsonl`). A subagent's is left out: no reply shows it.
|
|
182
|
+
events.push({ type: "text", text });
|
|
183
|
+
}
|
|
184
|
+
for (const block of blocks) {
|
|
185
|
+
if (block.type === "tool_use" || block.type === "server_tool_use") {
|
|
186
|
+
events.push(...callStarted(block, parentCallId));
|
|
187
|
+
}
|
|
188
|
+
else if (block.type === "tool_result") {
|
|
189
|
+
events.push(...callEnded(block, parentCallId, block.is_error === true, null));
|
|
190
|
+
}
|
|
191
|
+
else if (block.type === "advisor_tool_result") {
|
|
192
|
+
// The one server tool result read, as the Python SDK's `parse_message` keeps this one
|
|
193
|
+
// alone (claude-agent-sdk 0.2.165). It carries no error flag.
|
|
194
|
+
events.push(...callEnded(block, parentCallId, false, null));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return events;
|
|
198
|
+
}
|
|
199
|
+
function hunkOf(value) {
|
|
200
|
+
if (!isRecord(value))
|
|
201
|
+
return null;
|
|
202
|
+
const oldStart = integer(value.oldStart);
|
|
203
|
+
const newStart = integer(value.newStart);
|
|
204
|
+
if (oldStart === null || newStart === null || !Array.isArray(value.lines))
|
|
205
|
+
return null;
|
|
206
|
+
const lines = [];
|
|
207
|
+
for (const line of value.lines) {
|
|
208
|
+
if (typeof line !== "string" || !SIGNS.has(line.charAt(0)))
|
|
209
|
+
return null;
|
|
210
|
+
lines.push(line);
|
|
211
|
+
}
|
|
212
|
+
return { oldStart, newStart, lines };
|
|
213
|
+
}
|
|
214
|
+
function hunksOf(patch) {
|
|
215
|
+
if (!Array.isArray(patch))
|
|
216
|
+
return null;
|
|
217
|
+
const hunks = [];
|
|
218
|
+
for (const entry of patch) {
|
|
219
|
+
const hunk = hunkOf(entry);
|
|
220
|
+
if (hunk === null)
|
|
221
|
+
return null;
|
|
222
|
+
hunks.push(hunk);
|
|
223
|
+
}
|
|
224
|
+
return hunks;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* What an `Edit` or a `Write` did to a file, from `tool_use_result`: a shape the SDK types as
|
|
228
|
+
* `unknown` and does not document, measured on Claude Code 2.1.283 (2026-09-27,
|
|
229
|
+
* `test/fixtures/sdk/edit-write.jsonl`). Any other shape gives null, and the core shows the
|
|
230
|
+
* call's generic line.
|
|
231
|
+
*/
|
|
232
|
+
export function fileChange(result) {
|
|
233
|
+
if (!isRecord(result))
|
|
234
|
+
return null;
|
|
235
|
+
const path = string(result.filePath);
|
|
236
|
+
if (path === null)
|
|
237
|
+
return null;
|
|
238
|
+
const hunks = hunksOf(result.structuredPatch);
|
|
239
|
+
if (result.type === "create") {
|
|
240
|
+
const content = string(result.content);
|
|
241
|
+
if (content === null)
|
|
242
|
+
return null;
|
|
243
|
+
return { path, kind: "created", hunks: hunks ?? [], content };
|
|
244
|
+
}
|
|
245
|
+
if (hunks === null || hunks.length === 0)
|
|
246
|
+
return null;
|
|
247
|
+
return { path, kind: "edited", hunks, content: null };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The model an Agent call's agent ran on, from `resolvedModel` of the call's `tool_use_result`
|
|
251
|
+
* (the SDK's `AgentOutput`, SDK 0.3.300; measured on `subagent`, `subagent-foreground` and
|
|
252
|
+
* `background-forms`), when it is not the model of the main thread's last message, which made
|
|
253
|
+
* the call: the terminal names a subagent's model only then (captured on Claude Code 2.1.300,
|
|
254
|
+
* 2026-10-15). Null for any other result and when either model is unknown.
|
|
255
|
+
*/
|
|
256
|
+
function agentModel(memory, result) {
|
|
257
|
+
if (!isRecord(result) || memory.mainModel === null)
|
|
258
|
+
return null;
|
|
259
|
+
const ran = words(result.resolvedModel);
|
|
260
|
+
return ran === memory.mainModel ? null : ran;
|
|
261
|
+
}
|
|
262
|
+
function user(memory, record) {
|
|
263
|
+
const promptId = string(record.uuid);
|
|
264
|
+
if (promptId !== null && memory.sent.has(promptId)) {
|
|
265
|
+
// The replay of a prompt this session sent (`replay-user-messages`): it shows nothing. A
|
|
266
|
+
// uuid that names no prompt of ours (a resumed session's, a command's output) is none.
|
|
267
|
+
return [{ type: "prompt_taken", promptId }];
|
|
268
|
+
}
|
|
269
|
+
// A background subagent hands its report back as a message from a peer that names its task
|
|
270
|
+
// (`SDKMessageOrigin`, kind `peer`, `senderTaskId`), and the agent answers it in a turn of
|
|
271
|
+
// its own: measured 2026-10-10 on Claude Code 2.1.294 and 2.1.296 with the owner's setting
|
|
272
|
+
// sources loaded (`test/fixtures/sdk/subagent-handback.jsonl`). A peer with no task of this
|
|
273
|
+
// session (another session's message) says nothing about a task's report.
|
|
274
|
+
const origin = isRecord(record.origin) ? record.origin : {};
|
|
275
|
+
const sender = origin.kind === "peer" ? string(origin.senderTaskId) : null;
|
|
276
|
+
if (sender !== null)
|
|
277
|
+
return [{ type: "report_started", taskId: sender }];
|
|
278
|
+
// A task's notification folded into the running turn is replayed with the run it reports
|
|
279
|
+
// (recorded 2026-10-15, `task-end-in-turn-folded`); one a turn of its own reports is not
|
|
280
|
+
// (`task-end-in-turn-unfolded`), nor is a Monitor's line, which names no run.
|
|
281
|
+
const runId = origin.kind === "task-notification" ? string(origin.runId) : null;
|
|
282
|
+
const folded = runId === null ? undefined : memory.runs.get(runId);
|
|
283
|
+
if (runId !== null && folded !== undefined) {
|
|
284
|
+
memory.runs.delete(runId);
|
|
285
|
+
return [{ type: "notification_folded", taskId: folded }];
|
|
286
|
+
}
|
|
287
|
+
const message = isRecord(record.message) ? record.message : {};
|
|
288
|
+
if (!Array.isArray(message.content))
|
|
289
|
+
return [];
|
|
290
|
+
const blocks = records(message.content);
|
|
291
|
+
const parentCallId = parentOf(record);
|
|
292
|
+
// One `tool_use_result` per message: it belongs to a result only when it is alone.
|
|
293
|
+
const results = blocks.filter((block) => block.type === "tool_result");
|
|
294
|
+
const change = results.length === 1 ? fileChange(record.tool_use_result) : null;
|
|
295
|
+
const model = results.length === 1 ? agentModel(memory, record.tool_use_result) : null;
|
|
296
|
+
const events = [];
|
|
297
|
+
for (const block of blocks) {
|
|
298
|
+
if (block.type === "tool_use")
|
|
299
|
+
events.push(...callStarted(block, parentCallId));
|
|
300
|
+
else if (block.type === "tool_result") {
|
|
301
|
+
events.push(...callEnded(block, parentCallId, block.is_error === true, change, model));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return events;
|
|
305
|
+
}
|
|
306
|
+
// System records.
|
|
307
|
+
function init(_memory, record) {
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
type: "session_started",
|
|
311
|
+
sessionId: string(record.session_id),
|
|
312
|
+
agentVersion: string(record.claude_code_version),
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
function status(memory, record) {
|
|
317
|
+
const events = [];
|
|
318
|
+
// Claude Code reports the permission mode after a change (measured after each
|
|
319
|
+
// `set_permission_mode`).
|
|
320
|
+
const mode = string(record.permissionMode);
|
|
321
|
+
if (mode !== null)
|
|
322
|
+
events.push({ type: "mode_changed", mode });
|
|
323
|
+
// A compaction opens with `status` `compacting` and ends 14 to 37 seconds later in the
|
|
324
|
+
// recordings (2026-10-08, CLI 2.1.292) with a `status` record that carries `compact_result`.
|
|
325
|
+
// The record that reports a permission mode carries neither, so one that arrives meanwhile
|
|
326
|
+
// ends nothing.
|
|
327
|
+
const reported = record.status ?? null;
|
|
328
|
+
const compacting = reported === "compacting" ||
|
|
329
|
+
(memory.compacting && reported === null && !Object.hasOwn(record, "compact_result"));
|
|
330
|
+
if (compacting !== memory.compacting) {
|
|
331
|
+
memory.compacting = compacting;
|
|
332
|
+
events.push(compacting ? { type: "compaction_started" } : { type: "compaction_ended" });
|
|
333
|
+
}
|
|
334
|
+
return events;
|
|
335
|
+
}
|
|
336
|
+
// `running` comes 0.5 seconds after a prompt and 0.07 seconds after the notification of a
|
|
337
|
+
// background command, about a second before the turn's `message_start`; `idle` right after a
|
|
338
|
+
// turn's result, an interrupted one too, and only after the report of a background subagent,
|
|
339
|
+
// with none in between (recorded 2026-10-14, CLI 2.1.296: `session-state-shell`,
|
|
340
|
+
// `session-state-agent`, `session-state-approval`).
|
|
341
|
+
const AGENT_STATES = new Map([
|
|
342
|
+
["running", "working"],
|
|
343
|
+
["requires_action", "waiting"],
|
|
344
|
+
["idle", "idle"],
|
|
345
|
+
]);
|
|
346
|
+
function sessionState(_memory, record) {
|
|
347
|
+
const state = AGENT_STATES.get(string(record.state) ?? "");
|
|
348
|
+
return state === undefined ? [] : [{ type: "state_changed", state }];
|
|
349
|
+
}
|
|
350
|
+
function compactBoundary(_memory, record) {
|
|
351
|
+
const metadata = isRecord(record.compact_metadata) ? record.compact_metadata : {};
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
type: "compacted",
|
|
355
|
+
tokensBefore: integer(metadata.pre_tokens),
|
|
356
|
+
tokensAfter: integer(metadata.post_tokens),
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
function taskStarted(memory, record) {
|
|
361
|
+
const taskId = string(record.task_id);
|
|
362
|
+
const description = string(record.description);
|
|
363
|
+
if (taskId === null || description === null)
|
|
364
|
+
return [];
|
|
365
|
+
// Absent for a task this process did not register (`SDKTaskStartedMessage.run_id`).
|
|
366
|
+
const runId = string(record.run_id);
|
|
367
|
+
if (runId !== null) {
|
|
368
|
+
memory.runs.set(runId, taskId);
|
|
369
|
+
for (const kept of memory.runs.keys()) {
|
|
370
|
+
if (memory.runs.size <= RUNS_KEPT)
|
|
371
|
+
break;
|
|
372
|
+
memory.runs.delete(kept);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const taskType = words(record.task_type);
|
|
376
|
+
return [
|
|
377
|
+
{
|
|
378
|
+
type: "task_started",
|
|
379
|
+
taskId,
|
|
380
|
+
kind: TASK_KINDS.get(taskType ?? "") ?? "other",
|
|
381
|
+
taskType,
|
|
382
|
+
description,
|
|
383
|
+
callId: words(record.tool_use_id),
|
|
384
|
+
agentType: words(record.subagent_type),
|
|
385
|
+
background: typeof record.is_backgrounded === "boolean" ? record.is_backgrounded : null,
|
|
386
|
+
parentTaskId: words(record.parent_task_id),
|
|
387
|
+
},
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
function taskProgress(_memory, record) {
|
|
391
|
+
const taskId = string(record.task_id);
|
|
392
|
+
const description = string(record.description);
|
|
393
|
+
if (taskId === null || description === null)
|
|
394
|
+
return [];
|
|
395
|
+
return [{ type: "task_progress", taskId, description, callId: words(record.tool_use_id) }];
|
|
396
|
+
}
|
|
397
|
+
function taskUpdated(_memory, record) {
|
|
398
|
+
// Terminal task completion sometimes arrives only as a `task_updated` patch, with no
|
|
399
|
+
// notification (Python SDK, `parse_message`): the status is the patch's, when it has one.
|
|
400
|
+
const patch = isRecord(record.patch) ? record.patch : {};
|
|
401
|
+
const reported = string(patch.status);
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
type: "task_updated",
|
|
405
|
+
taskId: string(record.task_id) ?? "",
|
|
406
|
+
status: reported,
|
|
407
|
+
terminal: reported !== null && TERMINAL_TASK_STATUSES.has(reported),
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
function taskNotification(memory, record) {
|
|
412
|
+
const taskId = string(record.task_id);
|
|
413
|
+
const reported = string(record.status);
|
|
414
|
+
const summary = string(record.summary);
|
|
415
|
+
if (taskId === null || reported === null || summary === null)
|
|
416
|
+
return [];
|
|
417
|
+
const usage = isRecord(record.usage) ? record.usage : {};
|
|
418
|
+
// The replay of a folded notification names the run, which the task's start recorded: a task
|
|
419
|
+
// whose run this process never saw cannot be named (`SDKTaskStartedMessage.run_id`).
|
|
420
|
+
const runId = string(record.run_id);
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
type: "task_ended",
|
|
424
|
+
taskId,
|
|
425
|
+
status: reported,
|
|
426
|
+
summary,
|
|
427
|
+
durationMs: number(usage.duration_ms),
|
|
428
|
+
callId: words(record.tool_use_id),
|
|
429
|
+
foldNamed: runId !== null && memory.runs.has(runId),
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
// The rest.
|
|
434
|
+
function rateLimit() {
|
|
435
|
+
return [{ type: "limits_changed" }];
|
|
436
|
+
}
|
|
437
|
+
function tokensOf(usage) {
|
|
438
|
+
const tokens = {};
|
|
439
|
+
if (!isRecord(usage))
|
|
440
|
+
return tokens;
|
|
441
|
+
for (const [model, counts] of Object.entries(usage)) {
|
|
442
|
+
if (!isRecord(counts))
|
|
443
|
+
continue;
|
|
444
|
+
// A count a model does not report (no cache, say) counts as none.
|
|
445
|
+
tokens[model] = {
|
|
446
|
+
input: number(counts.inputTokens) ?? 0,
|
|
447
|
+
output: number(counts.outputTokens) ?? 0,
|
|
448
|
+
cacheRead: number(counts.cacheReadInputTokens) ?? 0,
|
|
449
|
+
cacheCreation: number(counts.cacheCreationInputTokens) ?? 0,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return tokens;
|
|
453
|
+
}
|
|
454
|
+
function endingOf(record) {
|
|
455
|
+
const reason = string(record.terminal_reason);
|
|
456
|
+
if (reason !== null && INTERRUPTED.has(reason))
|
|
457
|
+
return "interrupted";
|
|
458
|
+
return record.is_error === true ? "error" : "done";
|
|
459
|
+
}
|
|
460
|
+
// The outputs of `/effort` and `/model`, measured on Claude Code 2.1.280 (2026-09-23) and 2.1.301
|
|
461
|
+
// (2026-10-16); the "with X effort" form is the one ccstatusline reads from transcripts.
|
|
462
|
+
const EFFORT_OUTPUT = /^(?:Set effort level to|Effort level set to) ([a-z0-9-]+)/i;
|
|
463
|
+
const MODEL_OUTPUT = /^Set model to\b(?:.*? with ([a-z0-9-]+) effort)?/is;
|
|
464
|
+
/**
|
|
465
|
+
* The effort a result's command set. `/effort` and `/model` run no Stop hook (2.1.280) and no
|
|
466
|
+
* message carries the level, so it is read from the output, and only from the result that names
|
|
467
|
+
* the command in `local_command`: the text of any other turn is the model's. The result of a
|
|
468
|
+
* command that changed nothing (a failure, a read) gives no event; a model change that names no
|
|
469
|
+
* effort gives a null level.
|
|
470
|
+
*/
|
|
471
|
+
export function effortChanged(record) {
|
|
472
|
+
const command = string(record.local_command);
|
|
473
|
+
const output = strip(string(record.result) ?? "");
|
|
474
|
+
if (command === "effort") {
|
|
475
|
+
const set = EFFORT_OUTPUT.exec(output);
|
|
476
|
+
return set ? [{ type: "effort_changed", level: set[1].toLowerCase() }] : [];
|
|
477
|
+
}
|
|
478
|
+
if (command === "model") {
|
|
479
|
+
const set = MODEL_OUTPUT.exec(output);
|
|
480
|
+
return set ? [{ type: "effort_changed", level: set[1]?.toLowerCase() ?? null }] : [];
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
function result(memory, record) {
|
|
485
|
+
// The stream's memory is a turn's: an interrupted stream has no `message_stop`, and the next
|
|
486
|
+
// turn must not read as still inside it.
|
|
487
|
+
memory.streamed.clear();
|
|
488
|
+
memory.open = false;
|
|
489
|
+
memory.unnamed = false;
|
|
490
|
+
memory.compacting = false;
|
|
491
|
+
// Claude Code started the turn itself (a task notification, say) when the origin names
|
|
492
|
+
// anything but the owner: none or `human` is the owner's own prompt.
|
|
493
|
+
const origin = isRecord(record.origin) ? string(record.origin.kind) : null;
|
|
494
|
+
return [
|
|
495
|
+
...effortChanged(record),
|
|
496
|
+
{
|
|
497
|
+
type: "turn_ended",
|
|
498
|
+
sessionId: words(record.session_id),
|
|
499
|
+
startedBy: origin === null || origin === "human" ? "owner" : "agent",
|
|
500
|
+
finalText: string(record.result),
|
|
501
|
+
ending: endingOf(record),
|
|
502
|
+
// 0 for the turn a task's notification gets once its report was already handed back.
|
|
503
|
+
steps: typeof record.num_turns === "number" ? record.num_turns : null,
|
|
504
|
+
tokens: tokensOf(record.modelUsage),
|
|
505
|
+
},
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
const HANDLERS = {
|
|
509
|
+
assistant,
|
|
510
|
+
rate_limit_event: rateLimit,
|
|
511
|
+
result,
|
|
512
|
+
"stream_event:content_block_delta": contentBlockDelta,
|
|
513
|
+
"stream_event:content_block_start": contentBlockStart,
|
|
514
|
+
"stream_event:message_start": messageStart,
|
|
515
|
+
"stream_event:message_stop": messageStop,
|
|
516
|
+
"system:compact_boundary": compactBoundary,
|
|
517
|
+
"system:init": init,
|
|
518
|
+
"system:session_state_changed": sessionState,
|
|
519
|
+
"system:status": status,
|
|
520
|
+
"system:task_notification": taskNotification,
|
|
521
|
+
"system:task_progress": taskProgress,
|
|
522
|
+
"system:task_started": taskStarted,
|
|
523
|
+
"system:task_updated": taskUpdated,
|
|
524
|
+
user,
|
|
525
|
+
};
|
|
526
|
+
/** The kinds of record that can give an event. */
|
|
527
|
+
export const TRANSLATED_KINDS = Object.keys(HANDLERS);
|
|
528
|
+
/**
|
|
529
|
+
* The recorded kinds the daemon does not read: nothing in the reply or in the session depends
|
|
530
|
+
* on them. A kind in neither list is new, and `test/agent/claude/translate.test.ts` fails on the
|
|
531
|
+
* recording that carries it.
|
|
532
|
+
*/
|
|
533
|
+
export const UNREAD_KINDS = [
|
|
534
|
+
// The queue of a prompt sent while a turn runs; absent from the SDK's union of messages.
|
|
535
|
+
"command_lifecycle",
|
|
536
|
+
// `/clear`: the new session id comes with the `init` that follows.
|
|
537
|
+
"conversation_reset",
|
|
538
|
+
// The end of a content block and a message's stop reason: neither shows anything.
|
|
539
|
+
"stream_event:content_block_stop",
|
|
540
|
+
"stream_event:message_delta",
|
|
541
|
+
// A request being retried: the message about the failure follows when the retries run out.
|
|
542
|
+
"system:api_retry",
|
|
543
|
+
// The list of background tasks: the task records say the same, one task at a time.
|
|
544
|
+
"system:background_tasks_changed",
|
|
545
|
+
"system:thinking_tokens",
|
|
546
|
+
// A subagent's heartbeat while its call runs.
|
|
547
|
+
"tool_progress",
|
|
548
|
+
];
|
|
549
|
+
function handlerOf(kind) {
|
|
550
|
+
return kind !== null && Object.hasOwn(HANDLERS, kind)
|
|
551
|
+
? HANDLERS[kind]
|
|
552
|
+
: null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* One session's translator, as a class: it has two entry points over one memory (`translate`
|
|
556
|
+
* for what arrives, `promptSent` for what leaves), and the session wrapper holds exactly one
|
|
557
|
+
* for as long as its process lives.
|
|
558
|
+
*/
|
|
559
|
+
export class Translator {
|
|
560
|
+
#memory = {
|
|
561
|
+
sent: new Set(),
|
|
562
|
+
mainModel: null,
|
|
563
|
+
streamed: new Set(),
|
|
564
|
+
open: false,
|
|
565
|
+
unnamed: false,
|
|
566
|
+
compacting: false,
|
|
567
|
+
runs: new Map(),
|
|
568
|
+
};
|
|
569
|
+
/** The daemon is sending a prompt under `promptId`: its replay will be `prompt_taken`. */
|
|
570
|
+
promptSent(promptId) {
|
|
571
|
+
this.#memory.sent.add(promptId);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The events one record gives, in the order the reply acts on them. A record of a kind the
|
|
575
|
+
* daemon does not read, or of none it knows, gives none.
|
|
576
|
+
*/
|
|
577
|
+
translate(record) {
|
|
578
|
+
const handler = handlerOf(kindOf(record));
|
|
579
|
+
return handler !== null && isRecord(record) ? handler(this.#memory, record) : [];
|
|
580
|
+
}
|
|
581
|
+
}
|