viberoom 0.3.1 → 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.
- package/NOTICE +12 -0
- package/README.md +119 -96
- package/dist/hub.js +18 -0
- package/dist/launcher.js +22 -0
- package/dist/main.js +4 -3
- package/dist/persona.js +1 -1
- package/dist/recipes.js +37 -14
- package/dist/room.js +3 -2
- package/dist/server.js +5 -0
- package/package.json +8 -5
- package/scripts/vendor-acp.mjs +85 -0
- package/ui/app.css +7 -0
- package/ui/app.js +47 -2
- package/ui/index.html +2 -0
- package/vendor/acp/claude-agent-acp/LICENSE +191 -0
- package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
- package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
- package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
- package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
- package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
- package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
- package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
- package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
- package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
- package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
- package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
- package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
- package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
- package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
- package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
- package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
- package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
- package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
- package/vendor/acp/claude-agent-acp/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
- package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
- package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
- package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
- package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
- package/vendor/acp/claude-agent-sdk/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
- package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
- package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
- package/vendor/acp/codex-acp/LICENSE +190 -0
- package/vendor/acp/codex-acp/dist/index.js +34238 -0
- 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";
|