@anhlp/alp-runtime 0.0.0-beta.1

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