@anhlp/alp-runtime 0.0.0-stage → 0.0.1-beta.4
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 +202 -0
- package/NOTICE +7 -0
- package/README.md +6 -2
- package/dist/acp-transport.d.ts +130 -0
- package/dist/acp-transport.js +511 -0
- package/dist/antigravity-transport.d.ts +82 -0
- package/dist/antigravity-transport.js +619 -0
- package/dist/assignment-results.d.ts +18 -0
- package/dist/assignment-results.js +77 -0
- package/dist/attachments.d.ts +84 -0
- package/dist/attachments.js +213 -0
- package/dist/board-domain.d.ts +56 -0
- package/dist/board-domain.js +157 -0
- package/dist/board.d.ts +21 -0
- package/dist/board.js +69 -0
- package/dist/catalog.d.ts +31 -0
- package/dist/catalog.js +38 -0
- package/dist/claude-transport.d.ts +202 -0
- package/dist/claude-transport.js +817 -0
- package/dist/configure.d.ts +44 -0
- package/dist/configure.js +64 -0
- package/dist/contracts.d.ts +327 -0
- package/dist/contracts.js +16 -0
- package/dist/conversation.d.ts +45 -0
- package/dist/conversation.js +62 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.js +0 -0
- package/dist/github.d.ts +10 -0
- package/dist/github.js +39 -0
- package/dist/handoff.d.ts +36 -0
- package/dist/handoff.js +73 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +87 -0
- package/dist/language.d.ts +41 -0
- package/dist/language.js +68 -0
- package/dist/live.d.ts +48 -0
- package/dist/live.js +45 -0
- package/dist/mailbox.d.ts +26 -0
- package/dist/mailbox.js +38 -0
- package/dist/mcp-tools.d.ts +115 -0
- package/dist/mcp-tools.js +71 -0
- package/dist/permission-path.d.ts +2 -0
- package/dist/permission-path.js +44 -0
- package/dist/process-info.d.ts +14 -0
- package/dist/process-info.js +32 -0
- package/dist/providers.d.ts +28 -0
- package/dist/providers.js +71 -0
- package/dist/recall.d.ts +42 -0
- package/dist/recall.js +60 -0
- package/dist/resolve.d.ts +121 -0
- package/dist/resolve.js +238 -0
- package/dist/runtime-context.d.ts +43 -0
- package/dist/runtime-context.js +61 -0
- package/dist/runtime.d.ts +11 -0
- package/dist/runtime.js +3976 -0
- package/dist/session-config.d.ts +2102 -0
- package/dist/session-config.js +205 -0
- package/dist/session-state.d.ts +166 -0
- package/dist/session-state.js +0 -0
- package/dist/tool-detail.d.ts +81 -0
- package/dist/tool-detail.js +332 -0
- package/dist/tool-results.d.ts +12 -0
- package/dist/tool-results.js +25 -0
- package/dist/tools.d.ts +719 -0
- package/dist/tools.js +396 -0
- package/dist/transport.d.ts +47 -0
- package/dist/transport.js +169 -0
- package/dist/usage.d.ts +25 -0
- package/dist/usage.js +60 -0
- package/dist/workspace.d.ts +101 -0
- package/dist/workspace.js +259 -0
- package/dist/worktree-domain.d.ts +89 -0
- package/dist/worktree-domain.js +199 -0
- package/package.json +262 -4
- package/src/acp-transport.ts +508 -0
- package/src/antigravity-transport.ts +667 -0
- package/src/assignment-results.ts +64 -0
- package/src/attachments.ts +254 -0
- package/src/board-domain.ts +185 -0
- package/src/board.ts +80 -0
- package/src/catalog.ts +32 -0
- package/src/claude-transport.ts +938 -0
- package/src/configure.ts +93 -0
- package/src/contracts.ts +318 -0
- package/src/conversation.ts +81 -0
- package/src/events.ts +1 -0
- package/src/github.ts +37 -0
- package/src/handoff.ts +86 -0
- package/src/index.ts +32 -0
- package/src/language.ts +102 -0
- package/src/live.ts +86 -0
- package/src/mailbox.ts +49 -0
- package/src/mcp-tools.ts +73 -0
- package/src/permission-path.ts +37 -0
- package/src/process-info.ts +40 -0
- package/src/providers.ts +84 -0
- package/src/recall.ts +85 -0
- package/src/resolve.ts +230 -0
- package/src/runtime-context.ts +49 -0
- package/src/runtime.ts +4691 -0
- package/src/session-config.ts +304 -0
- package/src/session-state.ts +164 -0
- package/src/tool-detail.ts +397 -0
- package/src/tool-results.ts +26 -0
- package/src/tools.ts +397 -0
- package/src/transport.ts +116 -0
- package/src/usage.ts +79 -0
- package/src/workspace.ts +318 -0
- package/src/worktree-domain.ts +220 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
// packages/runtime/src/claude-transport.ts
|
|
2
|
+
import { writablePath } from "./permission-path.js";
|
|
3
|
+
import { accessSync, constants, existsSync, mkdirSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { optionalRead, claudeUsage } from "./runtime-context.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { PIN_KINDS } from "./board.js";
|
|
10
|
+
import { claudeToolDetail, resultText } from "./tool-detail.js";
|
|
11
|
+
import { CLOSE_REASONS, GATE_KINDS, TASK_STATUSES, TASK_TYPES } from "@anhlp/alp-core/tasks";
|
|
12
|
+
import { commandDecision } from "@anhlp/alp-core/permissions";
|
|
13
|
+
var CLAUDE_SDK = ["@anthropic-ai", "claude-agent-sdk"].join("/");
|
|
14
|
+
var handoffList = z.array(z.string().min(1)).max(100).optional();
|
|
15
|
+
var taskIds = z.array(z.string().min(1)).max(50).optional();
|
|
16
|
+
var taskLinks = z.object({ blockedBy: taskIds, related: taskIds, parent: z.string().min(1).optional() }).strict().optional();
|
|
17
|
+
var toolShapes = {
|
|
18
|
+
alp_delegate: {
|
|
19
|
+
agent: z.string().min(1),
|
|
20
|
+
task: z.string().min(1).max(32e3),
|
|
21
|
+
model: z.string().min(1).optional(),
|
|
22
|
+
thinking: z.string().min(1).optional(),
|
|
23
|
+
modelReason: z.string().min(1).optional(),
|
|
24
|
+
mode: z.enum(["read-only", "workspace-write", "full-access"]).optional(),
|
|
25
|
+
isolation: z.enum(["shared", "worktree"]).optional(),
|
|
26
|
+
writeRoot: z.string().min(1).optional(),
|
|
27
|
+
wait: z.boolean().optional(),
|
|
28
|
+
etaMinutes: z.number().int().min(1).max(1440).optional(),
|
|
29
|
+
taskId: z.string().min(1).optional(),
|
|
30
|
+
continueFrom: z.string().min(1).optional()
|
|
31
|
+
},
|
|
32
|
+
alp_merge: {
|
|
33
|
+
assignmentId: z.string().min(1),
|
|
34
|
+
skipVerify: z.string().min(1).max(2e3).optional()
|
|
35
|
+
},
|
|
36
|
+
alp_verify: {
|
|
37
|
+
taskId: z.string().min(1).optional()
|
|
38
|
+
},
|
|
39
|
+
alp_recall: {
|
|
40
|
+
assignmentId: z.string().min(1).optional(),
|
|
41
|
+
taskId: z.string().min(1).optional(),
|
|
42
|
+
question: z.string().min(1).max(4e3)
|
|
43
|
+
},
|
|
44
|
+
alp_lesson: {
|
|
45
|
+
scope: z.enum(["project", "user"]),
|
|
46
|
+
lesson: z.string().min(1).max(600)
|
|
47
|
+
},
|
|
48
|
+
alp_skill: {
|
|
49
|
+
name: z.string().min(1),
|
|
50
|
+
description: z.string().min(1).max(300),
|
|
51
|
+
body: z.string().min(1).max(2e4),
|
|
52
|
+
roles: z.array(z.string().min(1)).min(1).max(10),
|
|
53
|
+
lessons: z.array(z.string().min(1).max(600)).max(50).optional(),
|
|
54
|
+
replace: z.boolean().optional(),
|
|
55
|
+
requestId: z.string().min(1).max(100).optional()
|
|
56
|
+
},
|
|
57
|
+
alp_issue: {
|
|
58
|
+
action: z.enum(["search", "create", "comment"]),
|
|
59
|
+
target: z.enum(["project", "alp"]),
|
|
60
|
+
query: z.string().min(1).max(200).optional(),
|
|
61
|
+
requestId: z.string().min(1).max(100).optional(),
|
|
62
|
+
title: z.string().min(1).max(200).optional(),
|
|
63
|
+
body: z.string().min(1).max(2e4).optional(),
|
|
64
|
+
issue: z.number().int().positive().optional(),
|
|
65
|
+
labels: z.array(z.string().min(1).max(50)).max(10).optional()
|
|
66
|
+
},
|
|
67
|
+
alp_schedule: {
|
|
68
|
+
action: z.enum(["create", "list", "get", "cancel"]),
|
|
69
|
+
dueAt: z.string().min(1).max(80).optional(),
|
|
70
|
+
prompt: z.string().min(1).max(8e3).optional(),
|
|
71
|
+
requestId: z.string().min(1).max(200).optional(),
|
|
72
|
+
id: z.string().min(1).optional(),
|
|
73
|
+
status: z.enum(["pending", "delivered", "failed", "cancelled"]).optional()
|
|
74
|
+
},
|
|
75
|
+
alp_task: {
|
|
76
|
+
action: z.enum(["create", "update", "link", "start", "close", "reopen", "gate", "clear", "pour", "formulas", "show", "list", "ready"]),
|
|
77
|
+
id: z.string().min(1).optional(),
|
|
78
|
+
title: z.string().min(1).max(200).optional(),
|
|
79
|
+
description: z.string().max(8e3).optional(),
|
|
80
|
+
type: z.enum(TASK_TYPES).optional(),
|
|
81
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
82
|
+
labels: z.array(z.string().min(1).max(50)).max(10).optional(),
|
|
83
|
+
paths: z.array(z.string().min(1)).max(50).optional(),
|
|
84
|
+
parent: z.string().min(1).optional(),
|
|
85
|
+
blockedBy: taskIds,
|
|
86
|
+
discoveredFrom: z.string().min(1).optional(),
|
|
87
|
+
add: taskLinks,
|
|
88
|
+
remove: taskLinks,
|
|
89
|
+
reason: z.enum(CLOSE_REASONS).optional(),
|
|
90
|
+
kind: z.enum(GATE_KINDS).optional(),
|
|
91
|
+
until: z.string().min(1).optional(),
|
|
92
|
+
ref: z.string().min(1).optional(),
|
|
93
|
+
gate: z.string().min(1).optional(),
|
|
94
|
+
formula: z.string().min(1).optional(),
|
|
95
|
+
vars: z.record(z.string(), z.string()).optional(),
|
|
96
|
+
summary: z.string().min(1).max(2e3).optional(),
|
|
97
|
+
unverified: z.string().min(1).max(2e3).optional(),
|
|
98
|
+
note: z.string().min(1).max(2e3).optional(),
|
|
99
|
+
status: z.enum(TASK_STATUSES).optional(),
|
|
100
|
+
label: z.string().min(1).optional(),
|
|
101
|
+
limit: z.number().int().positive().optional()
|
|
102
|
+
},
|
|
103
|
+
alp_discard: {
|
|
104
|
+
assignmentId: z.string().min(1)
|
|
105
|
+
},
|
|
106
|
+
alp_cancel: {
|
|
107
|
+
assignmentId: z.string().min(1),
|
|
108
|
+
reason: z.string().optional()
|
|
109
|
+
},
|
|
110
|
+
alp_pin: {
|
|
111
|
+
kind: z.enum(PIN_KINDS),
|
|
112
|
+
body: z.string().min(1),
|
|
113
|
+
paths: z.array(z.string().min(1)).optional()
|
|
114
|
+
},
|
|
115
|
+
alp_board: {
|
|
116
|
+
kinds: z.array(z.enum(PIN_KINDS)).optional(),
|
|
117
|
+
limit: z.number().int().positive().optional()
|
|
118
|
+
},
|
|
119
|
+
alp_unpin: {
|
|
120
|
+
pinId: z.string().min(1)
|
|
121
|
+
},
|
|
122
|
+
alp_wait: {
|
|
123
|
+
assignments: z.array(z.string().min(1)).max(32).optional(),
|
|
124
|
+
timeoutMs: z.number().int().positive().optional()
|
|
125
|
+
},
|
|
126
|
+
alp_send: {
|
|
127
|
+
to: z.string().min(1),
|
|
128
|
+
kind: z.enum(["answer", "note", "steer"]),
|
|
129
|
+
body: z.string().min(1).max(8e3),
|
|
130
|
+
replyTo: z.string().min(1).optional()
|
|
131
|
+
},
|
|
132
|
+
alp_ask: {
|
|
133
|
+
question: z.string().min(1).max(8e3),
|
|
134
|
+
to: z.enum(["parent", "user"]).optional(),
|
|
135
|
+
options: z.array(z.string().min(1).max(200)).max(10).optional()
|
|
136
|
+
},
|
|
137
|
+
alp_handoff: {
|
|
138
|
+
outcome: z.enum(["complete", "partial", "blocked", "reconsider"]),
|
|
139
|
+
summary: z.string().min(1),
|
|
140
|
+
candidate: handoffList,
|
|
141
|
+
scope: handoffList,
|
|
142
|
+
verification: handoffList,
|
|
143
|
+
risks: handoffList,
|
|
144
|
+
discovered: handoffList,
|
|
145
|
+
ownership: z.string().optional(),
|
|
146
|
+
// ALP checks that the result follows from the criteria and findings (ALPD §38).
|
|
147
|
+
verdict: z.object({
|
|
148
|
+
result: z.enum(["pass", "pass_with_findings", "fail", "blocked"]),
|
|
149
|
+
criteria: z.array(z.object({ criterion: z.string().min(1), result: z.enum(["pass", "fail", "not_checked"]), evidence: z.string().min(1) }).strict()).min(1).max(50),
|
|
150
|
+
findings: z.array(z.object({ severity: z.enum(["critical", "high", "medium", "low"]), where: z.string().min(1), problem: z.string().min(1), fix: z.string().min(1).optional() }).strict()).max(100).optional()
|
|
151
|
+
}).strict().optional()
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var SKILL_PLUGIN = "alp";
|
|
155
|
+
function skillPlugin(directory, skills) {
|
|
156
|
+
rmSync(directory, { recursive: true, force: true });
|
|
157
|
+
mkdirSync(path.join(directory, ".claude-plugin"), { recursive: true });
|
|
158
|
+
mkdirSync(path.join(directory, "skills"));
|
|
159
|
+
writeFileSync(path.join(directory, ".claude-plugin", "plugin.json"), JSON.stringify({ name: SKILL_PLUGIN, description: "The agent's ALP skills" }));
|
|
160
|
+
for (const skill of skills) symlinkSync(path.dirname(skill.path), path.join(directory, "skills", skill.name), "dir");
|
|
161
|
+
return directory;
|
|
162
|
+
}
|
|
163
|
+
function claudeSandboxAvailable() {
|
|
164
|
+
if (process.env.ALP_CLAUDE_SANDBOX === "0") return false;
|
|
165
|
+
if (process.env.ALP_CLAUDE_SANDBOX === "1") return true;
|
|
166
|
+
if (process.platform === "darwin") return existsSync("/usr/bin/sandbox-exec");
|
|
167
|
+
if (process.platform !== "linux") return false;
|
|
168
|
+
const onPath = (name) => (process.env.PATH ?? "").split(path.delimiter).some((dir) => dir && existsSync(path.join(dir, name)));
|
|
169
|
+
return onPath("bwrap") && onPath("socat");
|
|
170
|
+
}
|
|
171
|
+
var WRITERS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
172
|
+
function claudeContent(part) {
|
|
173
|
+
const image = part?.type === "image" && typeof part.url === "string" ? /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(part.url) : null;
|
|
174
|
+
if (part?.type === "image" && !image) throw new Error("Claude takes images as inline base64 PNG, JPEG, GIF or WebP only");
|
|
175
|
+
return image ? { type: "image", source: { type: "base64", media_type: image[1], data: image[2] } } : { type: "text", text: part.text };
|
|
176
|
+
}
|
|
177
|
+
function suggestedRule(tool, input, suggestions) {
|
|
178
|
+
const suggestion = (Array.isArray(suggestions) ? suggestions : []).find((entry) => entry?.type === "addRules" && entry.behavior === "allow" && entry.rules?.length);
|
|
179
|
+
const rule = suggestion?.rules[0];
|
|
180
|
+
if (rule?.toolName) return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
|
|
181
|
+
if (tool === "Bash" && typeof input.command === "string" && !input.command.includes(")")) return `Bash(${input.command})`;
|
|
182
|
+
if (typeof input.file_path === "string" && !input.file_path.includes(")")) return `${tool}(${input.file_path})`;
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
var InputQueue = class {
|
|
186
|
+
values = [];
|
|
187
|
+
waiters = [];
|
|
188
|
+
ended = false;
|
|
189
|
+
push(value) {
|
|
190
|
+
if (this.ended) throw new Error("Claude input is closed");
|
|
191
|
+
const waiter = this.waiters.shift();
|
|
192
|
+
if (waiter) waiter({ value, done: false });
|
|
193
|
+
else this.values.push(value);
|
|
194
|
+
}
|
|
195
|
+
close() {
|
|
196
|
+
this.ended = true;
|
|
197
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
198
|
+
waiter({ value: void 0, done: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
[Symbol.asyncIterator]() {
|
|
202
|
+
return {
|
|
203
|
+
next: () => {
|
|
204
|
+
const value = this.values.shift();
|
|
205
|
+
if (value) return Promise.resolve({ value, done: false });
|
|
206
|
+
if (this.ended) {
|
|
207
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
208
|
+
}
|
|
209
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var claudePermissionMode = (sandbox) => sandbox === "read-only" ? "default" : sandbox === "full-access" ? "bypassPermissions" : "acceptEdits";
|
|
215
|
+
function claudePermissions(sandbox, currentSandbox, rules, ask) {
|
|
216
|
+
const readOnly = sandbox === "read-only";
|
|
217
|
+
const readers = ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Skill"];
|
|
218
|
+
return {
|
|
219
|
+
// Plan mode permits writes to plan files and requires ExitPlanMode approval.
|
|
220
|
+
// Fixed read-only callers restrict the tool surface; live sessions keep
|
|
221
|
+
// tools available and consult the current sandbox at the permission gate.
|
|
222
|
+
permissionMode: claudePermissionMode(sandbox),
|
|
223
|
+
// Lets a live session switch to full-access later; it bypasses nothing by itself.
|
|
224
|
+
allowDangerouslySkipPermissions: true,
|
|
225
|
+
...readOnly && !currentSandbox ? { tools: [...readers, ...rules?.floor === "read-only" ? ["Bash"] : []] } : {},
|
|
226
|
+
// Allow rules run without asking, even in a read-only session; deny rules hold even with full access.
|
|
227
|
+
...rules?.allow.length ? { allowedTools: rules.allow } : {},
|
|
228
|
+
disallowedTools: ["Agent", "Task", "TeamCreate", "EnterPlanMode", "ExitPlanMode", ...rules?.deny ?? []],
|
|
229
|
+
// Ask rules make Claude consult canUseTool in every mode, which asks the user.
|
|
230
|
+
...rules?.ask?.length ? { settings: { permissions: { ask: rules.ask } } } : {},
|
|
231
|
+
// The floor: Bash runs in the OS sandbox, which writes only the workspace (or nothing, read-only) and temp files, offline.
|
|
232
|
+
// A command leaves it only with dangerouslyDisableSandbox, which Claude allows itself for allow rules and asks canUseTool otherwise.
|
|
233
|
+
...rules?.floor ? { sandbox: { enabled: true, autoAllowBashIfSandboxed: false, allowUnsandboxedCommands: true, ...rules.floor === "read-only" && rules.floorRoot ? { filesystem: { denyWrite: [rules.floorRoot] } } : {} } } : {},
|
|
234
|
+
canUseTool: async (name, input, context) => {
|
|
235
|
+
const mode = currentSandbox ? currentSandbox() : sandbox;
|
|
236
|
+
const leaving = Boolean(rules?.floor) && name === "Bash" && input.dangerouslyDisableSandbox === true;
|
|
237
|
+
const sandboxed = rules?.floor === "read-only" && name === "Bash" && !leaving;
|
|
238
|
+
const beyond = mode === "read-only" && !readers.includes(name) && !name.startsWith("mcp__alp__") && !sandboxed || // With a floor, file tools write only the workspace and temp files, like Bash.
|
|
239
|
+
Boolean(rules?.floor && rules.floorRoot) && WRITERS.includes(name) && !writablePath(name === "NotebookEdit" ? input.notebook_path : input.file_path, rules.floorRoot) || leaving && mode !== "full-access";
|
|
240
|
+
if (leaving && typeof input.command === "string" && rules) {
|
|
241
|
+
const decision = commandDecision(rules, input.command);
|
|
242
|
+
if (decision === "deny") return { behavior: "deny", message: "A deny rule of your permission profile covers it" };
|
|
243
|
+
if (decision === "allow") return { behavior: "allow", updatedInput: input };
|
|
244
|
+
if (decision === "ask" && ask) context = { ...context, decisionReasonType: "rule" };
|
|
245
|
+
}
|
|
246
|
+
const reason = context?.decisionReasonType === "rule" && (rules?.ask?.length || leaving) ? "rule" : beyond && rules?.beyondMode === "ask" ? "mode" : void 0;
|
|
247
|
+
if (reason && ask) {
|
|
248
|
+
const rule = reason === "mode" ? suggestedRule(name, input, context?.suggestions) : void 0;
|
|
249
|
+
const answer = await ask({ tool: name, input, reason, ...rule ? { rule } : {} });
|
|
250
|
+
if (!answer.allow) return { behavior: "deny", message: answer.message ?? "The user did not allow it" };
|
|
251
|
+
const [, toolName, ruleContent] = /^([^(]+)(?:\((.*)\))?$/s.exec(rule ?? "") ?? [];
|
|
252
|
+
const approved = sandboxed && reason === "rule" ? { ...input, dangerouslyDisableSandbox: true } : input;
|
|
253
|
+
return { behavior: "allow", updatedInput: approved, ...answer.always && toolName ? { updatedPermissions: [{ type: "addRules", rules: [{ toolName, ...ruleContent ? { ruleContent } : {} }], behavior: "allow", destination: "session" }] } : {} };
|
|
254
|
+
}
|
|
255
|
+
if (leaving && beyond) return { behavior: "deny", message: "Bash runs inside the sandbox; leaving it needs an allow rule in your permission profile" };
|
|
256
|
+
if (beyond) return { behavior: "deny", message: mode === "read-only" ? "ALP session is read-only" : "Outside your workspace" };
|
|
257
|
+
return { behavior: "allow", updatedInput: input };
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var claudeLimited = (info) => info?.status === "rejected" && info.overageStatus !== "allowed" && info.overageStatus !== "allowed_warning";
|
|
262
|
+
var BOOT_MS = 15e3;
|
|
263
|
+
async function claudeChain(sessionId, dir) {
|
|
264
|
+
const { getSessionMessages } = await import(CLAUDE_SDK);
|
|
265
|
+
let messages = dir ? await getSessionMessages(sessionId, { dir }) : [];
|
|
266
|
+
if (!messages.length) messages = await getSessionMessages(sessionId);
|
|
267
|
+
return messages.filter((message) => (message.type === "user" || message.type === "assistant") && typeof message.uuid === "string").map((message) => {
|
|
268
|
+
const content = message.message?.content;
|
|
269
|
+
const toolResult = message.type === "user" && Array.isArray(content) && content.length > 0 && content.every((block) => block?.type === "tool_result");
|
|
270
|
+
return { uuid: message.uuid, type: message.type, ...toolResult ? { toolResult: true } : {} };
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
var ClaudeTransport = class {
|
|
274
|
+
constructor(command, cwd, env) {
|
|
275
|
+
this.command = command;
|
|
276
|
+
this.cwd = cwd;
|
|
277
|
+
this.env = env;
|
|
278
|
+
if (!path.isAbsolute(command)) {
|
|
279
|
+
const names = process.platform === "win32" && !path.extname(command) ? [command, `${command}.exe`] : [command];
|
|
280
|
+
const candidates = (env.PATH ?? "").split(path.delimiter).filter(Boolean).flatMap((directory) => names.map((name) => path.resolve(directory, name)));
|
|
281
|
+
const executable = candidates.find((candidate) => {
|
|
282
|
+
try {
|
|
283
|
+
accessSync(candidate, constants.X_OK);
|
|
284
|
+
return statSync(candidate).isFile();
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
if (!executable) throw new Error("Claude executable not found on PATH; set ALP_CLAUDE_BIN to an absolute native executable");
|
|
290
|
+
this.command = realpathSync(executable);
|
|
291
|
+
}
|
|
292
|
+
if (/\.(cmd|bat|ps1)$/i.test(this.command)) {
|
|
293
|
+
throw new Error("Claude executable must be a native binary, not a shell launcher");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
listeners = /* @__PURE__ */ new Set();
|
|
297
|
+
failures = /* @__PURE__ */ new Set();
|
|
298
|
+
requestHandler;
|
|
299
|
+
/** Whether the workspace had a .claude directory before a sandboxed session started. */
|
|
300
|
+
claudeDirectory;
|
|
301
|
+
input = new InputQueue();
|
|
302
|
+
query;
|
|
303
|
+
pump;
|
|
304
|
+
config;
|
|
305
|
+
threadId = "";
|
|
306
|
+
activeTurn;
|
|
307
|
+
closed = false;
|
|
308
|
+
assistantText = /* @__PURE__ */ new Map();
|
|
309
|
+
streamingMessage;
|
|
310
|
+
toolCalls = /* @__PURE__ */ new Map();
|
|
311
|
+
/** The last rate limit Claude reported, and whether the running turn hit one. */
|
|
312
|
+
rateLimit;
|
|
313
|
+
limitedTurn = false;
|
|
314
|
+
/** The model's context window and the fill at which Claude compacts it, as Claude reports them (ALPD §57). */
|
|
315
|
+
contextWindow;
|
|
316
|
+
compactAt;
|
|
317
|
+
/** The compaction running now, from its start to its boundary. */
|
|
318
|
+
compaction;
|
|
319
|
+
/** The plugin directory that loads the agent's skills, removed on close. */
|
|
320
|
+
skillDirectory;
|
|
321
|
+
/** Whether a turn ran in this process; before one, an error result is a boot refusal. */
|
|
322
|
+
turnsStarted = false;
|
|
323
|
+
/** Rejects when the process fails, for a resume that waits for Claude to start. */
|
|
324
|
+
bootFailure;
|
|
325
|
+
rejectBoot;
|
|
326
|
+
bootError;
|
|
327
|
+
async orchestrationContext() {
|
|
328
|
+
const [catalog, usage] = await Promise.all([
|
|
329
|
+
optionalRead(async () => this.query ? this.query.supportedModels() : void 0),
|
|
330
|
+
// Without a session yet, a short-lived Claude process answers (ALPD §58); it takes a second or two.
|
|
331
|
+
this.query ? optionalRead(async () => this.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true })) : optionalRead(() => this.probeUsage(), 15e3)
|
|
332
|
+
]);
|
|
333
|
+
return {
|
|
334
|
+
runtime: "claude",
|
|
335
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
336
|
+
catalogAvailable: Array.isArray(catalog),
|
|
337
|
+
models: (catalog ?? []).map((model) => ({
|
|
338
|
+
id: `claude:${model.value}`,
|
|
339
|
+
label: model.displayName,
|
|
340
|
+
description: model.description,
|
|
341
|
+
thinking: model.supportedEffortLevels
|
|
342
|
+
})),
|
|
343
|
+
usage: claudeUsage(usage)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
async initialize() {
|
|
347
|
+
}
|
|
348
|
+
/** The plan's usage windows before a session starts, from a Claude process that sends no prompt and is closed after. */
|
|
349
|
+
async probeUsage() {
|
|
350
|
+
if (this.closed) return void 0;
|
|
351
|
+
const { query } = await import(CLAUDE_SDK);
|
|
352
|
+
const probe = query({ prompt: new InputQueue(), options: { cwd: this.cwd, env: this.env, settingSources: [], persistSession: false, strictMcpConfig: true, mcpServers: {}, pathToClaudeCodeExecutable: this.command } });
|
|
353
|
+
try {
|
|
354
|
+
return await probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true });
|
|
355
|
+
} finally {
|
|
356
|
+
probe.close();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
onNotification(listener) {
|
|
360
|
+
this.listeners.add(listener);
|
|
361
|
+
return () => this.listeners.delete(listener);
|
|
362
|
+
}
|
|
363
|
+
onFailure(listener) {
|
|
364
|
+
this.failures.add(listener);
|
|
365
|
+
return () => this.failures.delete(listener);
|
|
366
|
+
}
|
|
367
|
+
onRequest(handler) {
|
|
368
|
+
this.requestHandler = handler;
|
|
369
|
+
}
|
|
370
|
+
async request(method, params) {
|
|
371
|
+
if (this.closed) throw new Error("Runtime is closed");
|
|
372
|
+
if (method === "session/configure") {
|
|
373
|
+
if (!this.query || !this.config) throw new Error("Claude thread is not initialized");
|
|
374
|
+
await this.query.setPermissionMode(claudePermissionMode(params.sandbox));
|
|
375
|
+
this.config.sandbox = params.sandbox;
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
378
|
+
if (method === "thread/chain") return { entries: await claudeChain(params.threadId, params.cwd ?? this.cwd) };
|
|
379
|
+
if (method === "thread/delete") {
|
|
380
|
+
const { deleteSession } = await import(CLAUDE_SDK);
|
|
381
|
+
await deleteSession(params.threadId, params.cwd ? { dir: params.cwd } : {});
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
if (method === "thread/start" || method === "thread/resume" || method === "thread/fork") {
|
|
385
|
+
this.clearAssistantText();
|
|
386
|
+
const { base, requireHistory, ...config } = params;
|
|
387
|
+
this.config = config;
|
|
388
|
+
this.threadId = method === "thread/resume" ? params.threadId : randomUUID();
|
|
389
|
+
const from = method === "thread/start" ? void 0 : method === "thread/resume" ? await this.resumeFrom(params.threadId, base, requireHistory) : { resume: params.threadId, fork: true };
|
|
390
|
+
await this.start(from);
|
|
391
|
+
if (method === "thread/resume" && (base || requireHistory)) await this.booted();
|
|
392
|
+
return {
|
|
393
|
+
thread: { id: this.threadId, turns: [] },
|
|
394
|
+
cwd: this.cwd,
|
|
395
|
+
model: params.model,
|
|
396
|
+
reasoningEffort: params.thinking
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (method === "turn/start" || method === "turn/steer") {
|
|
400
|
+
if (!this.query) throw new Error("Claude thread is not initialized");
|
|
401
|
+
const turnId = method === "turn/steer" ? params.expectedTurnId : randomUUID();
|
|
402
|
+
if (method === "turn/start") {
|
|
403
|
+
this.clearAssistantText();
|
|
404
|
+
this.activeTurn = turnId;
|
|
405
|
+
this.turnsStarted = true;
|
|
406
|
+
}
|
|
407
|
+
this.input.push({
|
|
408
|
+
type: "user",
|
|
409
|
+
message: {
|
|
410
|
+
role: "user",
|
|
411
|
+
content: params.input.map(claudeContent)
|
|
412
|
+
},
|
|
413
|
+
parent_tool_use_id: null,
|
|
414
|
+
// A turn's prompt entry in the transcript carries the turn's id, so ALP can cut the conversation at it (§12.7).
|
|
415
|
+
uuid: method === "turn/start" ? turnId : randomUUID(),
|
|
416
|
+
session_id: this.threadId
|
|
417
|
+
});
|
|
418
|
+
return method === "turn/start" ? { turn: { id: turnId } } : {};
|
|
419
|
+
}
|
|
420
|
+
if (method === "turn/interrupt") {
|
|
421
|
+
this.streamingMessage = void 0;
|
|
422
|
+
await this.query?.interrupt();
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
throw new Error(`Unsupported Claude runtime operation '${method}'`);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* How a resume starts. With a base, a session whose transcript does not exist yet is made from the
|
|
429
|
+
* base's conversation cut at `at` (a fork or rewind, §12.7): Claude writes it, under this session's
|
|
430
|
+
* id, only when its first prompt runs. Otherwise, the session's own transcript.
|
|
431
|
+
*/
|
|
432
|
+
async resumeFrom(threadId, base, requireHistory) {
|
|
433
|
+
const own = base || requireHistory ? await claudeChain(threadId, this.config.cwd) : [];
|
|
434
|
+
if (!base || own.length) {
|
|
435
|
+
if (requireHistory && !own.length) throw new Error(`Claude session ${threadId} not found`);
|
|
436
|
+
return { resume: threadId, fork: false };
|
|
437
|
+
}
|
|
438
|
+
const source = await claudeChain(base.threadId, this.config.cwd);
|
|
439
|
+
if (!source.length) throw new Error(`Claude session ${base.threadId} not found`);
|
|
440
|
+
if (base.at && !source.some((entry) => entry.uuid === base.at)) throw new Error(`Claude session ${base.threadId} has no entry ${base.at}`);
|
|
441
|
+
return { resume: base.threadId, fork: true, at: base.at };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Waits until Claude has started: its answer to the SDK's initialize request, or the failure that
|
|
445
|
+
* ends it first. Past BOOT_MS ALP goes on, as any resume does, and a later refusal fails the session.
|
|
446
|
+
*/
|
|
447
|
+
async booted() {
|
|
448
|
+
let timer;
|
|
449
|
+
try {
|
|
450
|
+
await Promise.race([
|
|
451
|
+
this.query.initializationResult?.().then(() => void 0, () => void 0),
|
|
452
|
+
this.bootFailure,
|
|
453
|
+
new Promise((resolve) => {
|
|
454
|
+
timer = setTimeout(resolve, BOOT_MS);
|
|
455
|
+
})
|
|
456
|
+
]);
|
|
457
|
+
} finally {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
}
|
|
460
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
461
|
+
if (this.bootError) throw this.bootError;
|
|
462
|
+
}
|
|
463
|
+
async start(from) {
|
|
464
|
+
const { createSdkMcpServer, query, tool } = await import(CLAUDE_SDK);
|
|
465
|
+
const config = this.config;
|
|
466
|
+
const mcpServers = this.convertMcp(config.mcpServers);
|
|
467
|
+
if (config.dynamicTools.length) {
|
|
468
|
+
const tools = config.dynamicTools.map((definition) => {
|
|
469
|
+
const base = toolShapes[definition.name];
|
|
470
|
+
if (!base) throw new Error(`Unsupported ALP tool '${definition.name}'`);
|
|
471
|
+
const schema = definition.inputSchema?.properties;
|
|
472
|
+
const actions = schema?.action?.enum;
|
|
473
|
+
const shape = Object.fromEntries(Object.entries(base).filter(([key]) => !schema || key in schema).map(([key, value]) => [key, key === "action" && actions?.length ? z.enum(actions) : value]));
|
|
474
|
+
return tool(definition.name, definition.description, shape, async (args) => {
|
|
475
|
+
if (!this.requestHandler || !this.activeTurn) {
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: "text", text: "ALP tools are unavailable" }],
|
|
478
|
+
isError: true
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const callId = `claude-tool-${randomUUID()}`;
|
|
482
|
+
this.emit("item/started", {
|
|
483
|
+
threadId: this.threadId,
|
|
484
|
+
item: {
|
|
485
|
+
type: "dynamicToolCall",
|
|
486
|
+
id: callId,
|
|
487
|
+
callId,
|
|
488
|
+
tool: definition.name,
|
|
489
|
+
arguments: args,
|
|
490
|
+
status: "inProgress"
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
const result = await this.requestHandler("item/tool/call", {
|
|
494
|
+
threadId: this.threadId,
|
|
495
|
+
turnId: this.activeTurn,
|
|
496
|
+
callId,
|
|
497
|
+
namespace: null,
|
|
498
|
+
tool: definition.name,
|
|
499
|
+
arguments: args
|
|
500
|
+
});
|
|
501
|
+
this.emit("item/completed", {
|
|
502
|
+
threadId: this.threadId,
|
|
503
|
+
item: {
|
|
504
|
+
type: "dynamicToolCall",
|
|
505
|
+
id: callId,
|
|
506
|
+
callId,
|
|
507
|
+
tool: definition.name,
|
|
508
|
+
arguments: args,
|
|
509
|
+
status: result?.success === false ? "failed" : "completed",
|
|
510
|
+
success: result?.success !== false,
|
|
511
|
+
contentItems: result?.contentItems ?? []
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
content: (result?.contentItems ?? []).map((item) => ({
|
|
516
|
+
type: "text",
|
|
517
|
+
text: item.text ?? JSON.stringify(item)
|
|
518
|
+
})),
|
|
519
|
+
isError: result?.success === false
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
mcpServers.alp = createSdkMcpServer({
|
|
524
|
+
name: "alp",
|
|
525
|
+
version: "0.0.0",
|
|
526
|
+
tools
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (config.floor) this.claudeDirectory ??= existsSync(path.join(config.cwd, ".claude"));
|
|
530
|
+
const rules = config.permissions || config.floor ? { allow: [], deny: [], ...config.permissions, ...config.floor ? { floor: config.floor, floorRoot: config.cwd } : {} } : null;
|
|
531
|
+
const { settings: ruleSettings, ...permissions } = claudePermissions(
|
|
532
|
+
config.sandbox,
|
|
533
|
+
() => config.sandbox,
|
|
534
|
+
rules,
|
|
535
|
+
async (request) => this.requestHandler ? await this.requestHandler("item/permission/request", request) : { allow: false, message: "No one can approve it" }
|
|
536
|
+
);
|
|
537
|
+
const settings = { ...config.thinking === "ultracode" ? { ultracode: true } : {}, ...config.context ? { autoCompactWindow: config.context } : {}, ...ruleSettings };
|
|
538
|
+
const plugins = [];
|
|
539
|
+
if (config.skills?.length) {
|
|
540
|
+
try {
|
|
541
|
+
this.skillDirectory ??= path.join(os.tmpdir(), "alp-skills", randomUUID());
|
|
542
|
+
plugins.push({ type: "local", path: skillPlugin(this.skillDirectory, config.skills), skipMcpDiscovery: true });
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const options = {
|
|
547
|
+
cwd: config.cwd,
|
|
548
|
+
env: this.env,
|
|
549
|
+
model: config.model,
|
|
550
|
+
effort: ["none", "off", "ultracode"].includes(config.thinking) ? void 0 : config.thinking,
|
|
551
|
+
thinking: ["none", "off"].includes(config.thinking) ? { type: "disabled" } : { type: "adaptive" },
|
|
552
|
+
systemPrompt: config.developerInstructions,
|
|
553
|
+
...permissions,
|
|
554
|
+
...Object.keys(settings).length ? { settings } : {},
|
|
555
|
+
mcpServers,
|
|
556
|
+
strictMcpConfig: true,
|
|
557
|
+
settingSources: [],
|
|
558
|
+
...plugins.length ? { plugins } : {},
|
|
559
|
+
persistSession: !config.ephemeral,
|
|
560
|
+
promptSuggestions: false,
|
|
561
|
+
includePartialMessages: true,
|
|
562
|
+
...from ? { resume: from.resume, ...from.fork ? { forkSession: true, sessionId: this.threadId } : {}, ...from.at ? { resumeSessionAt: from.at } : {} } : { sessionId: this.threadId },
|
|
563
|
+
// Explicit native path avoids SDK optional binaries inside Electron app.asar.
|
|
564
|
+
pathToClaudeCodeExecutable: this.command
|
|
565
|
+
};
|
|
566
|
+
this.bootFailure = new Promise((_, reject) => {
|
|
567
|
+
this.rejectBoot = reject;
|
|
568
|
+
});
|
|
569
|
+
this.bootFailure.catch(() => {
|
|
570
|
+
});
|
|
571
|
+
this.query = query({ prompt: this.input, options });
|
|
572
|
+
this.pump = this.consume(this.query).catch((error) => this.fail(error));
|
|
573
|
+
void this.measureContext();
|
|
574
|
+
}
|
|
575
|
+
/** Reads the window and the compaction threshold Claude resolved for this session, before any turn. */
|
|
576
|
+
async measureContext() {
|
|
577
|
+
const usage = await optionalRead(async () => this.query?.getContextUsage({ detail: "summary" }), 6e4);
|
|
578
|
+
if (!usage) return;
|
|
579
|
+
const window = usage.rawMaxTokens ?? usage.maxTokens;
|
|
580
|
+
if (typeof window === "number" && window > 0) this.contextWindow = window;
|
|
581
|
+
this.compactAt = usage.isAutoCompactEnabled !== false && typeof usage.autoCompactThreshold === "number" && usage.autoCompactThreshold > 0 ? usage.autoCompactThreshold : void 0;
|
|
582
|
+
}
|
|
583
|
+
convertMcp(values) {
|
|
584
|
+
const result = {};
|
|
585
|
+
for (const [name, value] of Object.entries(values)) {
|
|
586
|
+
result[name] = value.url ? {
|
|
587
|
+
type: "http",
|
|
588
|
+
url: value.url,
|
|
589
|
+
headers: value.http_headers ?? {}
|
|
590
|
+
} : {
|
|
591
|
+
type: "stdio",
|
|
592
|
+
command: value.command,
|
|
593
|
+
args: value.args ?? [],
|
|
594
|
+
env: value.env ?? {}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
async consume(stream) {
|
|
600
|
+
for await (const message of stream) this.handle(message);
|
|
601
|
+
if (!this.closed) this.fail(new Error("Claude session exited"));
|
|
602
|
+
}
|
|
603
|
+
handle(message) {
|
|
604
|
+
if (this.closed) return;
|
|
605
|
+
if (message.type === "rate_limit_event") {
|
|
606
|
+
this.rateLimit = message.rate_limit_info;
|
|
607
|
+
this.emit("account/rateLimits/updated", { claude: message.rate_limit_info });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const streamStart = message.type === "stream_event" && message.event?.type === "message_start";
|
|
611
|
+
if ((message.type === "assistant" || streamStart) && !message.parent_tool_use_id && !this.activeTurn) {
|
|
612
|
+
this.activeTurn = randomUUID();
|
|
613
|
+
this.emit("turn/started", { threadId: this.threadId, turn: { id: this.activeTurn } });
|
|
614
|
+
}
|
|
615
|
+
if (message.type === "stream_event") {
|
|
616
|
+
if (message.parent_tool_use_id) return;
|
|
617
|
+
const event = message.event;
|
|
618
|
+
if (event.type === "message_start") {
|
|
619
|
+
this.streamingMessage = event.message.id;
|
|
620
|
+
} else if (event.type === "message_stop") {
|
|
621
|
+
this.streamingMessage = void 0;
|
|
622
|
+
} else if (this.streamingMessage) {
|
|
623
|
+
const id = this.streamingMessage;
|
|
624
|
+
const state = this.assistantState(id);
|
|
625
|
+
let delta;
|
|
626
|
+
if (event.type === "content_block_start" && event.content_block.type === "text") {
|
|
627
|
+
state.blocks.set(event.index, { text: event.content_block.text, final: false });
|
|
628
|
+
delta = event.content_block.text;
|
|
629
|
+
} else if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
630
|
+
const block = state.blocks.get(event.index);
|
|
631
|
+
if (block && !block.final) {
|
|
632
|
+
block.text += event.delta.text;
|
|
633
|
+
delta = event.delta.text;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (delta) this.emit("item/agentMessage/delta", { threadId: this.threadId, turnId: this.activeTurn, itemId: id, delta });
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (message.type === "assistant" && message.error === "rate_limit") this.limitedTurn = true;
|
|
641
|
+
if (message.type === "system" && message.subtype === "init" && typeof message.model === "string") this.emit("alp/nativeModel", { threadId: this.threadId, model: message.model });
|
|
642
|
+
if (message.type === "system" && (message.subtype === "status" || message.subtype === "compact_boundary")) {
|
|
643
|
+
const system = message;
|
|
644
|
+
if (system.subtype === "status" && system.status === "compacting" && !this.compaction) {
|
|
645
|
+
this.compaction = randomUUID();
|
|
646
|
+
this.emit("item/started", { threadId: this.threadId, turnId: this.activeTurn, item: { type: "contextCompaction", id: this.compaction } });
|
|
647
|
+
} else if (system.subtype === "status" && system.compact_result === "failed") {
|
|
648
|
+
this.emit("item/completed", { threadId: this.threadId, turnId: this.activeTurn, item: { type: "contextCompaction", id: this.compaction ?? randomUUID(), status: "failed", error: system.compact_error } });
|
|
649
|
+
this.compaction = void 0;
|
|
650
|
+
} else if (system.subtype === "compact_boundary") {
|
|
651
|
+
const meta = system.compact_metadata ?? {};
|
|
652
|
+
this.emit("item/completed", {
|
|
653
|
+
threadId: this.threadId,
|
|
654
|
+
turnId: this.activeTurn,
|
|
655
|
+
item: { type: "contextCompaction", id: this.compaction ?? randomUUID(), status: "completed", trigger: meta.trigger, preTokens: meta.pre_tokens, postTokens: meta.post_tokens }
|
|
656
|
+
});
|
|
657
|
+
this.compaction = void 0;
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const usage = message.type === "assistant" && !message.parent_tool_use_id ? message.message.usage : void 0;
|
|
662
|
+
if (usage) {
|
|
663
|
+
const totalTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
664
|
+
const last = { totalTokens, inputTokens: usage.input_tokens ?? 0, cachedInputTokens: usage.cache_read_input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0 };
|
|
665
|
+
this.emit("thread/tokenUsage/updated", {
|
|
666
|
+
threadId: this.threadId,
|
|
667
|
+
turnId: this.activeTurn,
|
|
668
|
+
tokenUsage: { last, total: last, modelContextWindow: this.contextWindow ?? null, ...this.compactAt ? { autoCompactTokens: this.compactAt } : {} }
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (message.type === "result") {
|
|
672
|
+
const cost = message.total_cost_usd;
|
|
673
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) this.emit("thread/tokenUsage/updated", { threadId: this.threadId, turnId: this.activeTurn, tokenUsage: { totalCostUsd: cost } });
|
|
674
|
+
const windows = Object.values(message.modelUsage ?? {}).map((entry) => entry?.contextWindow).filter((value) => typeof value === "number" && value > 0);
|
|
675
|
+
if (windows.length && !this.compactAt) this.contextWindow = Math.max(...windows);
|
|
676
|
+
}
|
|
677
|
+
if (message.type === "assistant" && !message.parent_tool_use_id) {
|
|
678
|
+
const id = message.message.id;
|
|
679
|
+
const state = this.assistantState(id);
|
|
680
|
+
if (message.uuid && state.seen.has(message.uuid)) return;
|
|
681
|
+
if (message.uuid) state.seen.add(message.uuid);
|
|
682
|
+
for (const block of message.message.content) {
|
|
683
|
+
if (block.type === "text") {
|
|
684
|
+
const pending = [...state.blocks.entries()].sort(([a], [b]) => a - b).find(([, value]) => !value.final);
|
|
685
|
+
const index = pending?.[0] ?? Math.max(-1, ...state.blocks.keys()) + 1;
|
|
686
|
+
state.blocks.set(index, { text: block.text, final: true });
|
|
687
|
+
this.emit("item/completed", {
|
|
688
|
+
threadId: this.threadId,
|
|
689
|
+
item: { type: "agentMessage", id, text: [...state.blocks.entries()].sort(([a], [b]) => a - b).map(([, block2]) => block2.text).join("") }
|
|
690
|
+
});
|
|
691
|
+
} else if (block.type === "tool_use") {
|
|
692
|
+
const command = block.name === "Bash" ? String(block.input?.command ?? "") : `${block.name} ${JSON.stringify(block.input ?? {})}`;
|
|
693
|
+
this.toolCalls.set(block.id, { name: block.name, command, input: block.input });
|
|
694
|
+
const detail = claudeToolDetail(block.name, block.input);
|
|
695
|
+
this.emit("item/started", {
|
|
696
|
+
threadId: this.threadId,
|
|
697
|
+
item: {
|
|
698
|
+
type: "commandExecution",
|
|
699
|
+
id: block.id,
|
|
700
|
+
command,
|
|
701
|
+
cwd: this.cwd,
|
|
702
|
+
status: "inProgress",
|
|
703
|
+
aggregatedOutput: "",
|
|
704
|
+
...detail ? { tool: block.name, detail } : {}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (message.type === "user") {
|
|
712
|
+
const content = Array.isArray(message.message.content) ? message.message.content : [];
|
|
713
|
+
const results = content.filter((block) => block.type === "tool_result");
|
|
714
|
+
const structured = results.length === 1 ? message.tool_use_result : void 0;
|
|
715
|
+
for (const block of results) {
|
|
716
|
+
const call = this.toolCalls.get(block.tool_use_id);
|
|
717
|
+
if (!call) continue;
|
|
718
|
+
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
719
|
+
const text = resultText(block.content);
|
|
720
|
+
const detail = claudeToolDetail(call.name, call.input, { text, structured, error: Boolean(block.is_error) });
|
|
721
|
+
this.emit("item/completed", {
|
|
722
|
+
threadId: this.threadId,
|
|
723
|
+
item: {
|
|
724
|
+
type: "commandExecution",
|
|
725
|
+
id: block.tool_use_id,
|
|
726
|
+
command: call.command,
|
|
727
|
+
cwd: this.cwd,
|
|
728
|
+
status: block.is_error ? "failed" : "completed",
|
|
729
|
+
exitCode: block.is_error ? 1 : 0,
|
|
730
|
+
aggregatedOutput: output,
|
|
731
|
+
...detail ? { tool: call.name, detail, ...block.is_error ? { error: text } : {} } : {}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
this.toolCalls.delete(block.tool_use_id);
|
|
735
|
+
}
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (message.type === "result" && message.is_error && !this.activeTurn && !this.turnsStarted) {
|
|
739
|
+
this.refuseBoot(new Error(message.errors?.length ? message.errors.join("; ") : "Claude refused to start the session"));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (message.type === "result") this.clearAssistantText();
|
|
743
|
+
if (message.type === "result" && this.activeTurn) {
|
|
744
|
+
const turnId = this.activeTurn;
|
|
745
|
+
this.activeTurn = void 0;
|
|
746
|
+
const limited = this.limitedTurn || message.is_error && claudeLimited(this.rateLimit);
|
|
747
|
+
this.limitedTurn = false;
|
|
748
|
+
const failed = message.is_error || limited;
|
|
749
|
+
this.emit("turn/completed", {
|
|
750
|
+
threadId: this.threadId,
|
|
751
|
+
turn: {
|
|
752
|
+
id: turnId,
|
|
753
|
+
status: failed ? "failed" : "completed",
|
|
754
|
+
...failed ? { error: {
|
|
755
|
+
message: limited ? `Claude usage limit reached${this.rateLimit?.resetsAt ? `; resets ${new Date(this.rateLimit.resetsAt * 1e3).toISOString()}` : ""}` : "errors" in message && message.errors?.length ? message.errors.join("; ") : "Claude turn failed",
|
|
756
|
+
...limited ? { codexErrorInfo: "usageLimitExceeded", ...this.rateLimit?.resetsAt ? { resetsAt: this.rateLimit.resetsAt } : {} } : {}
|
|
757
|
+
} } : {}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
refuseBoot(error) {
|
|
763
|
+
this.bootError ??= error;
|
|
764
|
+
this.rejectBoot?.(error);
|
|
765
|
+
}
|
|
766
|
+
emit(method, params) {
|
|
767
|
+
for (const listener of this.listeners) listener(method, params);
|
|
768
|
+
}
|
|
769
|
+
assistantState(id) {
|
|
770
|
+
let state = this.assistantText.get(id);
|
|
771
|
+
if (!state) this.assistantText.set(id, state = { blocks: /* @__PURE__ */ new Map(), seen: /* @__PURE__ */ new Set() });
|
|
772
|
+
return state;
|
|
773
|
+
}
|
|
774
|
+
clearAssistantText() {
|
|
775
|
+
this.streamingMessage = void 0;
|
|
776
|
+
this.assistantText.clear();
|
|
777
|
+
}
|
|
778
|
+
fail(value) {
|
|
779
|
+
if (this.closed) return;
|
|
780
|
+
this.clearAssistantText();
|
|
781
|
+
const error = value instanceof Error ? value : new Error(String(value));
|
|
782
|
+
this.refuseBoot(error);
|
|
783
|
+
for (const listener of this.failures) listener(error);
|
|
784
|
+
}
|
|
785
|
+
async close() {
|
|
786
|
+
if (this.closed) return;
|
|
787
|
+
this.closed = true;
|
|
788
|
+
this.clearAssistantText();
|
|
789
|
+
this.input.close();
|
|
790
|
+
this.query?.close();
|
|
791
|
+
await this.pump?.catch(() => {
|
|
792
|
+
});
|
|
793
|
+
this.listeners.clear();
|
|
794
|
+
this.failures.clear();
|
|
795
|
+
if (this.skillDirectory) rmSync(this.skillDirectory, { recursive: true, force: true });
|
|
796
|
+
if (this.claudeDirectory === false && this.config) {
|
|
797
|
+
for (const directory of [".claude/.cc-writes", ".claude"]) {
|
|
798
|
+
try {
|
|
799
|
+
rmdirSync(path.join(this.config.cwd, directory));
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
export {
|
|
807
|
+
ClaudeTransport,
|
|
808
|
+
SKILL_PLUGIN,
|
|
809
|
+
claudeChain,
|
|
810
|
+
claudeContent,
|
|
811
|
+
claudeLimited,
|
|
812
|
+
claudePermissionMode,
|
|
813
|
+
claudePermissions,
|
|
814
|
+
claudeSandboxAvailable,
|
|
815
|
+
skillPlugin,
|
|
816
|
+
toolShapes
|
|
817
|
+
};
|