viberoom 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +119 -96
  3. package/dist/hub.js +18 -0
  4. package/dist/launcher.js +22 -0
  5. package/dist/main.js +4 -3
  6. package/dist/persona.js +2 -2
  7. package/dist/recipes.js +37 -14
  8. package/dist/room.js +3 -2
  9. package/dist/server.js +16 -3
  10. package/package.json +8 -5
  11. package/scripts/vendor-acp.mjs +85 -0
  12. package/ui/app.css +7 -0
  13. package/ui/app.js +116 -26
  14. package/ui/index.html +2 -0
  15. package/vendor/acp/claude-agent-acp/LICENSE +191 -0
  16. package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
  17. package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
  18. package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
  19. package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
  20. package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
  21. package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
  22. package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
  23. package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
  24. package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
  25. package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
  26. package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
  27. package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
  28. package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
  29. package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
  30. package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
  31. package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
  32. package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
  33. package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
  34. package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
  35. package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
  36. package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
  37. package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
  38. package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
  39. package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
  40. package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
  41. package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
  42. package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
  43. package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
  44. package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
  45. package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
  46. package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
  47. package/vendor/acp/claude-agent-acp/package.json +7 -0
  48. package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
  49. package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  50. package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
  51. package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
  52. package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
  53. package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
  54. package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  55. package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
  56. package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
  57. package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
  58. package/vendor/acp/claude-agent-sdk/package.json +7 -0
  59. package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
  60. package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
  61. package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
  62. package/vendor/acp/codex-acp/LICENSE +190 -0
  63. package/vendor/acp/codex-acp/dist/index.js +34238 -0
  64. package/vendor/acp/codex-acp/package.json +7 -0
@@ -0,0 +1,350 @@
1
+ import { createSdkMcpServer, } from "../../claude-agent-sdk/sdk.mjs";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { z } from "zod";
5
+ import { airExtensionMeta, clientSupportsAirCapability, withAirMeta } from "./air-extension.js";
6
+ export const AGENT_FILE_CHANGE_REPORT_CAPABILITY = "agentFileChangeReport";
7
+ export const FILE_CHANGE_AUDIT_SERVER_NAME = "claude_agent_acp";
8
+ export const FILE_CHANGE_AUDIT_TOOL_NAME = "report_changed_files";
9
+ export const FILE_CHANGE_AUDIT_WIRE_TOOL_NAME = `mcp__${FILE_CHANGE_AUDIT_SERVER_NAME}__${FILE_CHANGE_AUDIT_TOOL_NAME}`;
10
+ const FILE_CHANGE_AUDIT_MARKER = "claude-agent-acp-file-change-audit";
11
+ const MAX_REPORTED_PATHS = 1024;
12
+ const MAX_REPORTED_PATH_LENGTH = 4096;
13
+ export const AGENT_FILE_CHANGE_REPORT_MAX_BYTES = 256 * 1024;
14
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
15
+ const reportInputSchema = {
16
+ paths: z
17
+ .array(z.string())
18
+ .describe("Workspace file paths changed during this turn. Use absolute paths or paths relative to cwd."),
19
+ complete: z
20
+ .boolean()
21
+ .describe("True only when paths contains every workspace file changed during this turn."),
22
+ uncertainty: z
23
+ .string()
24
+ .trim()
25
+ .min(1)
26
+ .max(2000)
27
+ .optional()
28
+ .describe("Why the report may be incomplete or uncertain."),
29
+ };
30
+ export function agentFileChangeReportRequestId(meta) {
31
+ const air = airExtensionMeta(meta);
32
+ const request = air?.agentFileChangeReportRequest;
33
+ if (!request || typeof request !== "object" || Array.isArray(request))
34
+ return undefined;
35
+ const requestRecord = request;
36
+ const requestKeys = Object.keys(requestRecord);
37
+ if (requestKeys.length !== 2 ||
38
+ !requestKeys.includes("version") ||
39
+ !requestKeys.includes("requestId") ||
40
+ requestRecord.version !== 1)
41
+ return undefined;
42
+ const requestId = requestRecord.requestId;
43
+ return typeof requestId === "string" && REQUEST_ID_PATTERN.test(requestId)
44
+ ? requestId
45
+ : undefined;
46
+ }
47
+ export function supportsAgentFileChangeReport(capabilities) {
48
+ return clientSupportsAirCapability(capabilities, AGENT_FILE_CHANGE_REPORT_CAPABILITY);
49
+ }
50
+ export function agentFileChangeReportMeta(result) {
51
+ return withAirMeta(undefined, AGENT_FILE_CHANGE_REPORT_CAPABILITY, result);
52
+ }
53
+ export function createFileChangeAuditTurnState(requestId) {
54
+ return {
55
+ requestId,
56
+ phase: "requested",
57
+ };
58
+ }
59
+ export function isFileChangeAuditReportPhase(state) {
60
+ return state?.phase === "collecting" || state?.phase === "finished";
61
+ }
62
+ export function isFileChangeAuditTool(toolName) {
63
+ return toolName === FILE_CHANGE_AUDIT_WIRE_TOOL_NAME;
64
+ }
65
+ export function containsFileChangeAuditMarker(text) {
66
+ return text.includes(`<${FILE_CHANGE_AUDIT_MARKER}>`);
67
+ }
68
+ export function createFileChangeAuditSupport(options) {
69
+ const workspace = normalizeWorkspace(options.cwd, options.additionalDirectories);
70
+ const finishUnavailable = async (state, reason) => {
71
+ // Claim the terminal outcome before the first await. The report tool, a
72
+ // Stop hook, cancellation, and stream teardown can race, but only the
73
+ // winner may publish for this request id. Publication remains fail-open:
74
+ // an audit transport failure must never hold the user prompt open.
75
+ if (state.phase === "finished")
76
+ return;
77
+ state.phase = "finished";
78
+ try {
79
+ await options.publish({
80
+ version: 1,
81
+ requestId: state.requestId,
82
+ status: "unavailable",
83
+ reason,
84
+ });
85
+ }
86
+ catch (error) {
87
+ options.logError(`Failed to publish unavailable report ${state.requestId}: ${error}`);
88
+ }
89
+ };
90
+ const tool = {
91
+ name: FILE_CHANGE_AUDIT_TOOL_NAME,
92
+ description: "Internal audit tool. Call it only when a stop-hook message explicitly requests the file-change audit.",
93
+ inputSchema: reportInputSchema,
94
+ _meta: {
95
+ "anthropic/alwaysLoad": true,
96
+ "claude/endTurn": true,
97
+ },
98
+ handler: async (report) => {
99
+ const state = options.getActiveState();
100
+ if (!state || state.phase !== "collecting") {
101
+ return {
102
+ isError: true,
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `<${FILE_CHANGE_AUDIT_MARKER}>No file-change audit is active.</${FILE_CHANGE_AUDIT_MARKER}>`,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ // Flip before publishing: a client transport failure must not make the
112
+ // model retry the report and produce duplicates. Delivery is audit-only
113
+ // and fail-open; the normal turn must still finish.
114
+ state.phase = "finished";
115
+ const normalized = normalizeReportedPaths(report.paths, workspace);
116
+ const result = fitReportedAgentFileChangeReport({
117
+ version: 1,
118
+ requestId: state.requestId,
119
+ status: "reported",
120
+ paths: normalized.paths,
121
+ declaredComplete: report.complete && !normalized.truncated,
122
+ truncated: normalized.truncated,
123
+ ...(report.uncertainty ? { uncertainty: report.uncertainty } : {}),
124
+ });
125
+ try {
126
+ await options.publish(result);
127
+ }
128
+ catch (error) {
129
+ options.logError(`Failed to publish file-change report ${state.requestId}: ${error}`);
130
+ }
131
+ return {
132
+ content: [
133
+ {
134
+ type: "text",
135
+ text: `<${FILE_CHANGE_AUDIT_MARKER}>File-change audit recorded.</${FILE_CHANGE_AUDIT_MARKER}>`,
136
+ },
137
+ ],
138
+ structuredContent: result,
139
+ };
140
+ },
141
+ };
142
+ // canUseTool is not guaranteed to run for tools that the current permission
143
+ // mode auto-allows (notably bypassPermissions). A PreToolUse hook is the
144
+ // enforcement boundary that keeps the hidden continuation read-only in every
145
+ // mode: it can submit the report, but cannot inspect or mutate more files.
146
+ const preToolUseHook = async (input) => {
147
+ if (input.hook_event_name !== "PreToolUse")
148
+ return {};
149
+ const state = options.getActiveState();
150
+ const isReportTool = isFileChangeAuditTool(input.tool_name);
151
+ if (!isReportTool && !isFileChangeAuditReportPhase(state))
152
+ return {};
153
+ const allowReport = state?.phase === "collecting" && isReportTool;
154
+ return {
155
+ hookSpecificOutput: {
156
+ hookEventName: "PreToolUse",
157
+ permissionDecision: allowReport ? "allow" : "deny",
158
+ ...(!allowReport
159
+ ? {
160
+ permissionDecisionReason: "Only the internal file-change report is allowed during the audit.",
161
+ }
162
+ : {}),
163
+ },
164
+ };
165
+ };
166
+ const stopHook = async (input) => {
167
+ if (input.hook_event_name !== "Stop")
168
+ return {};
169
+ const state = options.getActiveState();
170
+ if (!state || state.phase === "finished")
171
+ return {};
172
+ // A Stop can be only a pause while a background generator is still
173
+ // running. Keep the request pending so a later final Stop can include its
174
+ // writes instead of declaring a prematurely complete path set.
175
+ if (state.phase === "requested" && (input.background_tasks?.length ?? 0) > 0)
176
+ return {};
177
+ if (state.phase === "collecting" || input.stop_hook_active) {
178
+ await finishUnavailable(state, "notReported");
179
+ return {};
180
+ }
181
+ // Set the phase before returning the continuation so every following SDK
182
+ // message is hidden even if the next stream starts immediately.
183
+ state.phase = "collecting";
184
+ return {
185
+ hookSpecificOutput: {
186
+ hookEventName: "Stop",
187
+ additionalContext: `<${FILE_CHANGE_AUDIT_MARKER}>` +
188
+ `Before stopping, call ${FILE_CHANGE_AUDIT_WIRE_TOOL_NAME} exactly once. ` +
189
+ "Report every workspace file changed during this user turn, including files changed " +
190
+ "by commands, generators, version-control operations, and child processes. " +
191
+ "Do not call any other tool and do not emit user-facing prose. " +
192
+ "Set complete to false and explain uncertainty when you are not sure the list is complete." +
193
+ `</${FILE_CHANGE_AUDIT_MARKER}>`,
194
+ },
195
+ };
196
+ };
197
+ return {
198
+ mcpServer: createSdkMcpServer({
199
+ name: FILE_CHANGE_AUDIT_SERVER_NAME,
200
+ version: "1.0.0",
201
+ tools: [tool],
202
+ alwaysLoad: true,
203
+ }),
204
+ preToolUseHook,
205
+ stopHook,
206
+ finishUnavailable,
207
+ };
208
+ }
209
+ function normalizeWorkspace(cwd, additionalDirectories) {
210
+ const normalizedCwd = canonicalizeWorkspaceRoot(path.resolve(cwd));
211
+ const seen = new Set([pathKey(normalizedCwd)]);
212
+ const normalizedAdditionalDirectories = [];
213
+ for (const directory of additionalDirectories) {
214
+ const normalized = canonicalizeWorkspaceRoot(path.resolve(normalizedCwd, directory));
215
+ const key = pathKey(normalized);
216
+ if (seen.has(key))
217
+ continue;
218
+ seen.add(key);
219
+ normalizedAdditionalDirectories.push(normalized);
220
+ }
221
+ return {
222
+ cwd: normalizedCwd,
223
+ additionalDirectories: normalizedAdditionalDirectories,
224
+ };
225
+ }
226
+ function normalizeReportedPaths(reportedPaths, workspace) {
227
+ const roots = [workspace.cwd, ...workspace.additionalDirectories];
228
+ const seen = new Set();
229
+ const result = [];
230
+ let totalBytes = 0;
231
+ let truncated = false;
232
+ for (const reportedPath of reportedPaths) {
233
+ if (reportedPath.trim().length === 0 ||
234
+ reportedPath.length > MAX_REPORTED_PATH_LENGTH ||
235
+ hasControlCharacter(reportedPath)) {
236
+ truncated = true;
237
+ continue;
238
+ }
239
+ let normalized;
240
+ try {
241
+ normalized = canonicalizeReportedPath(path.resolve(workspace.cwd, reportedPath));
242
+ }
243
+ catch {
244
+ truncated = true;
245
+ continue;
246
+ }
247
+ if (normalized.length > MAX_REPORTED_PATH_LENGTH || hasControlCharacter(normalized)) {
248
+ truncated = true;
249
+ continue;
250
+ }
251
+ if (!roots.some((root) => isWithinRoot(normalized, root))) {
252
+ truncated = true;
253
+ continue;
254
+ }
255
+ const key = pathKey(normalized);
256
+ if (roots.some((root) => pathKey(root) === key)) {
257
+ truncated = true;
258
+ continue;
259
+ }
260
+ if (seen.has(key)) {
261
+ continue;
262
+ }
263
+ const pathBytes = Buffer.byteLength(normalized, "utf8");
264
+ if (result.length >= MAX_REPORTED_PATHS ||
265
+ totalBytes + pathBytes > AGENT_FILE_CHANGE_REPORT_MAX_BYTES) {
266
+ truncated = true;
267
+ continue;
268
+ }
269
+ seen.add(key);
270
+ result.push(normalized);
271
+ totalBytes += pathBytes;
272
+ }
273
+ return { paths: result, truncated };
274
+ }
275
+ /** Keep the complete serialized report within AIR's wire limit. */
276
+ function fitReportedAgentFileChangeReport(report) {
277
+ const paths = [...report.paths];
278
+ let fitted = report;
279
+ while (Buffer.byteLength(JSON.stringify(fitted), "utf8") > AGENT_FILE_CHANGE_REPORT_MAX_BYTES) {
280
+ if (paths.length === 0) {
281
+ // The fixed fields and bounded uncertainty are far below the cap. Keep
282
+ // this loud if the wire contract changes without updating this helper.
283
+ throw new Error("Agent file-change report exceeds the wire limit without paths");
284
+ }
285
+ paths.pop();
286
+ fitted = {
287
+ ...report,
288
+ paths: [...paths],
289
+ declaredComplete: false,
290
+ truncated: true,
291
+ };
292
+ }
293
+ return fitted;
294
+ }
295
+ function isWithinRoot(candidate, root) {
296
+ const relative = path.relative(root, candidate);
297
+ return !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..";
298
+ }
299
+ /**
300
+ * Resolve filesystem aliases in a workspace root, including macOS' /tmp ->
301
+ * /private/tmp alias. For a root that does not exist yet, resolve its nearest
302
+ * existing ancestor and retain the missing suffix.
303
+ */
304
+ function canonicalizeWorkspaceRoot(value) {
305
+ return canonicalizeFromExistingAncestor(value);
306
+ }
307
+ /**
308
+ * Canonicalize the parent but not the leaf itself. A changed path may be
309
+ * deleted, or it may be a symlink whose node (rather than target) changed.
310
+ */
311
+ function canonicalizeReportedPath(value) {
312
+ const parent = canonicalizeFromExistingAncestor(path.dirname(value));
313
+ return path.resolve(parent, path.basename(value));
314
+ }
315
+ function canonicalizeFromExistingAncestor(value) {
316
+ const original = path.resolve(value);
317
+ let current = original;
318
+ const missingSegments = [];
319
+ while (true) {
320
+ try {
321
+ const canonical = fs.realpathSync.native(current);
322
+ return path.resolve(canonical, ...missingSegments.reverse());
323
+ }
324
+ catch (error) {
325
+ if (!isMissingPathError(error))
326
+ return original;
327
+ }
328
+ const parent = path.dirname(current);
329
+ if (parent === current)
330
+ return original;
331
+ missingSegments.push(path.basename(current));
332
+ current = parent;
333
+ }
334
+ }
335
+ function isMissingPathError(error) {
336
+ if (typeof error !== "object" || error === null || !("code" in error))
337
+ return false;
338
+ return error.code === "ENOENT" || error.code === "ENOTDIR";
339
+ }
340
+ function pathKey(value) {
341
+ return process.platform === "win32" ? value.toLowerCase() : value;
342
+ }
343
+ function hasControlCharacter(value) {
344
+ for (let index = 0; index < value.length; index++) {
345
+ const code = value.charCodeAt(index);
346
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
347
+ return true;
348
+ }
349
+ return false;
350
+ }
@@ -0,0 +1,41 @@
1
+ import { RequestError } from "@agentclientprotocol/sdk";
2
+ import { forkSession as forkClaudeSession, getSessionMessages, } from "../../claude-agent-sdk/sdk.mjs";
3
+ function forkPointMessageId(meta) {
4
+ const fork = meta?.jetbrains?.air?.fork;
5
+ if (fork?.version !== 1)
6
+ return undefined;
7
+ const messageId = fork.messageId?.trim();
8
+ return messageId || undefined;
9
+ }
10
+ function forkPointMessageIdCandidates(messageId) {
11
+ // Older AIR builds sent their visible segment id. Prefer the exact id before its ACP source id.
12
+ const protocolMessageId = messageId.replace(/:segment:\d+$/, "");
13
+ return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId];
14
+ }
15
+ export async function forkSession(params, dependencies) {
16
+ const messageId = forkPointMessageId(params._meta);
17
+ if (!messageId) {
18
+ const forked = await forkClaudeSession(params.sessionId, { dir: params.cwd });
19
+ return { sessionId: forked.sessionId };
20
+ }
21
+ const candidateIds = forkPointMessageIdCandidates(messageId);
22
+ const liveUuid = candidateIds
23
+ .map((candidateId) => dependencies.liveMessageIdToUuid?.get(candidateId))
24
+ .find(Boolean);
25
+ const history = liveUuid
26
+ ? undefined
27
+ : await getSessionMessages(params.sessionId, { dir: params.cwd });
28
+ const messageUuid = liveUuid ??
29
+ candidateIds
30
+ .map((candidateId) => history?.find((message) => dependencies.messageIdForGrouping(message) === candidateId)
31
+ ?.uuid)
32
+ .find(Boolean);
33
+ if (!messageUuid) {
34
+ throw RequestError.invalidParams({ messageId }, `Fork point message ${messageId} was not found in session ${params.sessionId}`);
35
+ }
36
+ const forked = await forkClaudeSession(params.sessionId, {
37
+ dir: params.cwd,
38
+ upToMessageId: messageUuid,
39
+ });
40
+ return { sessionId: forked.sessionId };
41
+ }
@@ -0,0 +1,50 @@
1
+ import { RequestError } from "@agentclientprotocol/sdk";
2
+ export const GOAL_EXTENSION_VERSION = 1;
3
+ export const GOAL_CONTROL_METHOD = "_session/goal";
4
+ export const GOAL_ACTIONS = ["set", "clear"];
5
+ export function goalUpdateFromPrompt(prompt) {
6
+ const match = /^\/goal(?:\s+([\s\S]*))?$/.exec(prompt);
7
+ const argument = match?.[1]?.trim();
8
+ if (!argument) {
9
+ return undefined;
10
+ }
11
+ if (argument === "clear") {
12
+ return null;
13
+ }
14
+ return {
15
+ objective: argument,
16
+ status: "active",
17
+ controlMethod: GOAL_CONTROL_METHOD,
18
+ };
19
+ }
20
+ export function parseGoalRequest(params) {
21
+ if (!params || typeof params !== "object") {
22
+ throw RequestError.invalidParams(undefined, "goal params must be an object");
23
+ }
24
+ const { sessionId, action, objective } = params;
25
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
26
+ throw RequestError.invalidParams(undefined, "goal params require a non-empty sessionId");
27
+ }
28
+ if (!GOAL_ACTIONS.includes(action)) {
29
+ throw RequestError.invalidParams(undefined, 'goal action must be "set" or "clear"');
30
+ }
31
+ if (action === "set" && (typeof objective !== "string" || objective.trim().length === 0)) {
32
+ throw RequestError.invalidParams(undefined, 'goal action "set" requires a non-empty objective');
33
+ }
34
+ return action === "set"
35
+ ? { sessionId, action, objective: objective.trim() }
36
+ : { sessionId, action: "clear" };
37
+ }
38
+ export function toGoalSnapshot(message) {
39
+ if (message.value === null) {
40
+ return null;
41
+ }
42
+ return {
43
+ objective: message.value.condition.trim(),
44
+ status: "active",
45
+ iterations: message.value.iterations,
46
+ lastReason: message.value.last_reason ?? null,
47
+ createdAt: message.value.set_at,
48
+ controlMethod: GOAL_CONTROL_METHOD,
49
+ };
50
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { resolveSettings } from "../../claude-agent-sdk/sdk.mjs";
3
+ import { claudeCliPath, runAcp } from "./acp-agent.js";
4
+ import packageJson from "../package.json" with { type: "json" };
5
+ import { appendFileSync, mkdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ // `--cli` is checked first so that `--version`/`-v` (and any other flags) are
8
+ // forwarded to the wrapped native CLI rather than swallowed by our own version
9
+ // handler below. Our version flag only applies when not delegating.
10
+ if (process.argv.includes("--cli")) {
11
+ const { spawn } = await import("node:child_process");
12
+ const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
13
+ const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
14
+ const signals = process.platform === "win32"
15
+ ? ["SIGINT", "SIGTERM"]
16
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
17
+ for (const sig of signals) {
18
+ process.on(sig, () => {
19
+ if (!child.killed)
20
+ child.kill(sig);
21
+ });
22
+ }
23
+ child.on("exit", (code, signal) => {
24
+ if (signal && process.platform !== "win32") {
25
+ // Remove our listener so re-raising actually terminates instead of
26
+ // re-entering the no-op handler, which would let us exit with code 0
27
+ // instead of the signal's conventional 128+N.
28
+ process.removeAllListeners(signal);
29
+ process.kill(process.pid, signal);
30
+ }
31
+ else {
32
+ process.exit(code ?? 1);
33
+ }
34
+ });
35
+ child.on("error", (err) => {
36
+ console.error(err);
37
+ process.exit(1);
38
+ });
39
+ }
40
+ else if (process.argv.includes("--version") || process.argv.includes("-v")) {
41
+ console.log(packageJson.version);
42
+ process.exit(0);
43
+ }
44
+ else {
45
+ // Apply env vars from the managed-policy tier before any SDK call so the
46
+ // SDK subprocess inherits them. Going through resolveSettings (vs. a raw
47
+ // read of managed-settings.json) also picks up MDM sources on macOS and
48
+ // HKLM/HKCU on Windows.
49
+ const policy = await resolveSettings({ settingSources: [] });
50
+ for (const [key, value] of Object.entries(policy.effective.env ?? {})) {
51
+ process.env[key] = value;
52
+ }
53
+ // stdout is used to send messages to the client
54
+ // we redirect everything else to stderr to make sure it doesn't interfere with ACP
55
+ console.log = console.error;
56
+ console.info = console.error;
57
+ console.warn = console.error;
58
+ console.debug = console.error;
59
+ process.on("unhandledRejection", (reason, promise) => {
60
+ console.error("Unhandled Rejection at:", promise, "reason:", reason);
61
+ });
62
+ const logDirectory = process.env.CLAUDE_AGENT_LOGS;
63
+ const logger = logDirectory
64
+ ? (() => {
65
+ mkdirSync(logDirectory, { recursive: true });
66
+ const logFile = join(logDirectory, "agent.log");
67
+ const writeLog = (...args) => {
68
+ const rendered = args
69
+ .map((arg) => (arg instanceof Error ? (arg.stack ?? arg.message) : String(arg)))
70
+ .join(" ");
71
+ appendFileSync(logFile, `${new Date().toISOString()} pid=${process.pid} ${rendered}\n`);
72
+ };
73
+ return {
74
+ log: writeLog,
75
+ error: (...args) => {
76
+ console.error(...args);
77
+ writeLog(...args);
78
+ },
79
+ };
80
+ })()
81
+ : undefined;
82
+ logger?.log("Claude ACP started");
83
+ const { connection, agent } = runAcp(logger);
84
+ async function shutdown() {
85
+ await agent.dispose().catch((err) => {
86
+ console.error("Error during cleanup:", err);
87
+ });
88
+ process.exit(0);
89
+ }
90
+ // Exit cleanly when the ACP connection closes (e.g. stdin EOF, transport
91
+ // error). Without this, `process.stdin.resume()` keeps the event loop
92
+ // alive indefinitely, causing orphan process accumulation in oneshot mode.
93
+ connection.closed.then(shutdown);
94
+ process.on("SIGTERM", shutdown);
95
+ process.on("SIGINT", shutdown);
96
+ // Keep process alive while connection is open
97
+ process.stdin.resume();
98
+ }
@@ -0,0 +1,5 @@
1
+ // Export the main agent class and utilities for library usage
2
+ export { ClaudeAcpAgent, isLocalCommandMetadata, stripLocalCommandMetadata, runAcp, toAcpNotifications, streamEventToAcpNotifications, } from "./acp-agent.js";
3
+ export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
4
+ export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
5
+ export { SettingsManager } from "./settings.js";