@borgee/agents-host 0.2.84 → 0.2.94
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/README.md +1 -1
- package/dist/agents-host.js +15 -5
- package/dist/background-runs.d.ts +2 -1
- package/dist/background-runs.js +48 -17
- package/dist/chat/chat-control-plane.d.ts +1 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +3 -0
- package/dist/chat/sdk-chat-control-plane.js +15 -0
- package/dist/plugin-sdk.js +136 -20
- package/dist/plugin-sdk.js.map +3 -3
- package/dist/providers/claude/adapter.d.ts +1 -0
- package/dist/providers/claude/adapter.js +31 -3
- package/dist/providers/claude/background-run-observer.d.ts +32 -0
- package/dist/providers/claude/background-run-observer.js +381 -0
- package/dist/providers/claude/cli-client.d.ts +12 -0
- package/dist/providers/claude/cli-client.js +449 -69
- package/dist/providers/claude/foreground-handoff.d.ts +4 -0
- package/dist/providers/claude/foreground-handoff.js +45 -0
- package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
- package/dist/providers/claude/task-cancellation-protocol.js +21 -0
- package/dist/providers/copilot/activity-metadata.d.ts +3 -0
- package/dist/providers/copilot/activity-metadata.js +19 -0
- package/dist/providers/copilot/cli-client.js +5 -0
- package/dist/providers/provider-adapter.d.ts +8 -0
- package/dist/providers/provider-adapter.js +4 -0
- package/dist/types.d.ts +8 -0
- package/dist/vendor/claude-agent-acp/LICENSE +191 -0
- package/dist/vendor/claude-agent-acp/NOTICE +8 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
- package/dist/vendor/claude-agent-acp/package.json +85 -0
- package/package.json +14 -10
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CreateElicitationResponse } from "@agentclientprotocol/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* Convert an MCP elicitation request (from the SDK's `onElicitation` callback)
|
|
5
|
+
* into an ACP `CreateElicitationRequest`. Returns `null` when the request can't
|
|
6
|
+
* be represented (e.g. a url-mode request with no url).
|
|
7
|
+
*/
|
|
8
|
+
export function mcpElicitationToCreateRequest(request, sessionId) {
|
|
9
|
+
if (request.mode === "url") {
|
|
10
|
+
if (!request.url) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
mode: "url",
|
|
15
|
+
sessionId,
|
|
16
|
+
message: request.message,
|
|
17
|
+
url: request.url,
|
|
18
|
+
// URL elicitations need a stable id so the client can correlate the
|
|
19
|
+
// later `session/complete_elicitation` notification. MCP servers usually
|
|
20
|
+
// provide one; fall back to a generated id if not.
|
|
21
|
+
elicitationId: request.elicitationId ?? randomUUID(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Form mode (the default). The MCP `requestedSchema` is already a JSON Schema
|
|
25
|
+
// with primitive-typed properties, which is structurally what ACP expects.
|
|
26
|
+
return {
|
|
27
|
+
mode: "form",
|
|
28
|
+
sessionId,
|
|
29
|
+
message: request.message,
|
|
30
|
+
requestedSchema: normalizeElicitationSchema(request.requestedSchema),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Content of an accepted elicitation response.
|
|
35
|
+
*
|
|
36
|
+
* Uses the SDK's validating guard rather than an `action === "accept"` check:
|
|
37
|
+
* the guard both narrows past the union's custom/future variant and validates
|
|
38
|
+
* the payload, so a malformed accept (right tag, ill-typed content) yields
|
|
39
|
+
* empty content — the same classification the SDK's wire validators apply.
|
|
40
|
+
*/
|
|
41
|
+
function acceptedElicitationContent(response) {
|
|
42
|
+
return CreateElicitationResponse.isAccept(response) ? (response.content ?? {}) : {};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Map an ACP elicitation response back to the MCP `ElicitResult` the SDK expects
|
|
46
|
+
* to hand back to the requesting server.
|
|
47
|
+
*/
|
|
48
|
+
export function createElicitationResponseToElicitResult(response) {
|
|
49
|
+
switch (response.action) {
|
|
50
|
+
case "accept":
|
|
51
|
+
return { action: "accept", content: acceptedElicitationContent(response) };
|
|
52
|
+
case "decline":
|
|
53
|
+
return { action: "decline" };
|
|
54
|
+
case "cancel":
|
|
55
|
+
default:
|
|
56
|
+
return { action: "cancel" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Pull the well-formed questions out of an AskUserQuestion tool input. Returns
|
|
61
|
+
* `null` when there are no usable questions — including the case where every
|
|
62
|
+
* entry is malformed and filtering leaves an empty list — so callers can treat
|
|
63
|
+
* "nothing to ask" uniformly.
|
|
64
|
+
*/
|
|
65
|
+
export function extractAskUserQuestions(input) {
|
|
66
|
+
const questions = input.questions;
|
|
67
|
+
if (!Array.isArray(questions)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const valid = questions.filter((q) => !!q && typeof q.question === "string" && Array.isArray(q.options) && q.options.length > 0);
|
|
71
|
+
return valid.length > 0 ? valid : null;
|
|
72
|
+
}
|
|
73
|
+
/** Stable form-field key for the question at the given index. */
|
|
74
|
+
function questionFieldKey(index) {
|
|
75
|
+
return `question_${index}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Form-field key for the per-question free-text "custom answer" field that sits
|
|
79
|
+
* alongside `question_<n>`. Mirrors the first-party clients, where every
|
|
80
|
+
* question carries its own "Other" box rather than one form-level field.
|
|
81
|
+
*/
|
|
82
|
+
function questionCustomFieldKey(index) {
|
|
83
|
+
return `question_${index}_custom`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `_meta` key under which a bridged enum option carries its `preview`, the one
|
|
87
|
+
* option field ACP's `EnumOption` still has no slot for (descriptions are
|
|
88
|
+
* first-class as of schema 1.19). Namespaced like the agent's other `_meta`
|
|
89
|
+
* extensions (`_claude/...`).
|
|
90
|
+
*/
|
|
91
|
+
const OPTION_META_KEY = "_claude/askUserQuestionOption";
|
|
92
|
+
/**
|
|
93
|
+
* Shared `_meta` key for marking a per-question free-text field as the custom
|
|
94
|
+
* answer companion for a select question. This intentionally has no
|
|
95
|
+
* agent-specific namespace so ACP clients can recognize the same marker across
|
|
96
|
+
* Codex, Claude, and other AskUserQuestion bridges.
|
|
97
|
+
*/
|
|
98
|
+
const CUSTOM_ANSWER_META_KEY = "_askUserQuestionCustomAnswer";
|
|
99
|
+
/**
|
|
100
|
+
* Render the AskUserQuestion tool's questions as an ACP form elicitation.
|
|
101
|
+
*
|
|
102
|
+
* Fields are keyed by a short stable id (`question_<n>`) rather than the full
|
|
103
|
+
* question text, so the question text appears in exactly one place per field.
|
|
104
|
+
* Single-select questions use a titled `oneOf` enum; multi-select questions use
|
|
105
|
+
* an array with a titled `anyOf` item enum. The enum `const` is always the
|
|
106
|
+
* option label, since that is what the tool records as the answer; an option's
|
|
107
|
+
* secondary text travels in the enum option's own `description` field.
|
|
108
|
+
*
|
|
109
|
+
* Each question is followed by its own optional free-text "custom answer" field
|
|
110
|
+
* (`question_<n>_custom`), mirroring the CLI's per-question "Other" box: the
|
|
111
|
+
* user can type their own answer instead of picking an option, scoped to that
|
|
112
|
+
* specific question. Nothing is marked required, so the user can also just skip
|
|
113
|
+
* — matching the built-in tool, which always offers Skip + a free-text box.
|
|
114
|
+
*/
|
|
115
|
+
export function askUserQuestionsToCreateRequest(questions, sessionId, toolCallId) {
|
|
116
|
+
const single = questions.length === 1;
|
|
117
|
+
const properties = {};
|
|
118
|
+
questions.forEach((question, index) => {
|
|
119
|
+
const options = question.options.map((option) => {
|
|
120
|
+
const enumOption = {
|
|
121
|
+
const: option.label,
|
|
122
|
+
title: option.label,
|
|
123
|
+
};
|
|
124
|
+
if (option.description) {
|
|
125
|
+
enumOption.description = option.description;
|
|
126
|
+
}
|
|
127
|
+
// The SDK option's `preview` (mockups, code snippets, comparisons shown
|
|
128
|
+
// on focus) still has no structural slot in `EnumOption`, so forward it
|
|
129
|
+
// under ACP's reserved `_meta` extension point for clients that render it.
|
|
130
|
+
if (option.preview) {
|
|
131
|
+
enumOption._meta = { [OPTION_META_KEY]: { preview: option.preview } };
|
|
132
|
+
}
|
|
133
|
+
return enumOption;
|
|
134
|
+
});
|
|
135
|
+
// For a single question the prompt is carried by `message`, so we don't
|
|
136
|
+
// repeat it in the field description. With multiple questions each field
|
|
137
|
+
// needs its own question text.
|
|
138
|
+
const description = single ? undefined : question.question;
|
|
139
|
+
const title = question.header || undefined;
|
|
140
|
+
properties[questionFieldKey(index)] = question.multiSelect
|
|
141
|
+
? { type: "array", title, description, items: { anyOf: options } }
|
|
142
|
+
: { type: "string", title, description, oneOf: options };
|
|
143
|
+
properties[questionCustomFieldKey(index)] = {
|
|
144
|
+
type: "string",
|
|
145
|
+
title: "Other",
|
|
146
|
+
description: "Type your own answer instead of choosing an option above (optional).",
|
|
147
|
+
_meta: {
|
|
148
|
+
[CUSTOM_ANSWER_META_KEY]: {
|
|
149
|
+
questionId: questionFieldKey(index),
|
|
150
|
+
isCustomAnswer: true,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
const requestedSchema = {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties,
|
|
158
|
+
};
|
|
159
|
+
const message = single ? questions[0].question : "Please answer the following questions.";
|
|
160
|
+
return {
|
|
161
|
+
mode: "form",
|
|
162
|
+
sessionId,
|
|
163
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
164
|
+
message,
|
|
165
|
+
requestedSchema,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Fold an ACP elicitation response into the AskUserQuestion tool's input.
|
|
170
|
+
*
|
|
171
|
+
* Selected labels are read back from the indexed form fields and written into
|
|
172
|
+
* `answers` as a `{ [questionText]: label }` map (comma-joining multi-selects)
|
|
173
|
+
* — the key shape the tool's own `call()` reads. A non-empty per-question
|
|
174
|
+
* custom-answer field (`question_<n>_custom`) takes precedence over that
|
|
175
|
+
* question's selection, since the user typed their own answer instead of
|
|
176
|
+
* picking one. Decline yields empty answers (the model is told the user skipped
|
|
177
|
+
* rather than the turn aborting); cancel — and any custom/future action we
|
|
178
|
+
* don't understand — aborts the tool call.
|
|
179
|
+
*/
|
|
180
|
+
export function applyAskElicitationResponse(response, toolInput, questions) {
|
|
181
|
+
if (response.action === "decline") {
|
|
182
|
+
return { action: "answered", updatedInput: { ...toolInput, answers: {} } };
|
|
183
|
+
}
|
|
184
|
+
if (response.action !== "accept") {
|
|
185
|
+
return { action: "cancel" };
|
|
186
|
+
}
|
|
187
|
+
const content = acceptedElicitationContent(response);
|
|
188
|
+
// Typed against the tool's own output schema so the answer/response shapes
|
|
189
|
+
// stay in sync with what the built-in tool's call() expects to read back.
|
|
190
|
+
const answers = {};
|
|
191
|
+
questions.forEach((question, index) => {
|
|
192
|
+
// A typed custom answer wins over the selection: the user chose to write
|
|
193
|
+
// their own answer for this question instead of picking an option.
|
|
194
|
+
const custom = content[questionCustomFieldKey(index)];
|
|
195
|
+
if (typeof custom === "string" && custom.trim() !== "") {
|
|
196
|
+
answers[question.question] = custom.trim();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const value = content[questionFieldKey(index)];
|
|
200
|
+
if (value === undefined || value === null) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const text = Array.isArray(value) ? value.join(", ") : String(value);
|
|
204
|
+
if (text === "") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
answers[question.question] = text;
|
|
208
|
+
});
|
|
209
|
+
return { action: "answered", updatedInput: { ...toolInput, answers } };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Coerce an arbitrary MCP `requestedSchema` into an ACP `ElicitationSchema`.
|
|
213
|
+
* The two are structurally compatible JSON Schemas; we just guarantee the
|
|
214
|
+
* `type: "object"` discriminator is present.
|
|
215
|
+
*/
|
|
216
|
+
function normalizeElicitationSchema(schema) {
|
|
217
|
+
if (!schema || typeof schema !== "object") {
|
|
218
|
+
return { type: "object", properties: {} };
|
|
219
|
+
}
|
|
220
|
+
return { ...schema, type: "object" };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The `request_user_dialog` kind the CLI emits when a model refusal has a
|
|
224
|
+
* fallback available but needs user consent before retrying (e.g. Claude Fable
|
|
225
|
+
* declining a request with Opus available as the fallback). Declaring this
|
|
226
|
+
* kind in `supportedDialogKinds` is the opt-in: the CLI fails closed and never
|
|
227
|
+
* emits an undeclared kind — the flow degrades to the classic refusal error
|
|
228
|
+
* ending the turn.
|
|
229
|
+
*/
|
|
230
|
+
export const REFUSAL_FALLBACK_DIALOG_KIND = "refusal_fallback_prompt";
|
|
231
|
+
/**
|
|
232
|
+
* Validate the opaque dialog payload into a {@link RefusalFallbackPrompt}.
|
|
233
|
+
* Returns `null` when the required fields are missing or mistyped (a newer CLI
|
|
234
|
+
* may reshape the payload), so the caller can cancel the dialog and let the
|
|
235
|
+
* CLI apply its default behavior instead of rendering something misleading.
|
|
236
|
+
*/
|
|
237
|
+
export function extractRefusalFallbackPrompt(payload) {
|
|
238
|
+
const { originalModel, fallbackModel, apiRefusalCategory, guidanceText } = payload;
|
|
239
|
+
if (typeof originalModel !== "string" || typeof fallbackModel !== "string") {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
originalModel,
|
|
244
|
+
fallbackModel,
|
|
245
|
+
apiRefusalCategory: typeof apiRefusalCategory === "string" ? apiRefusalCategory : null,
|
|
246
|
+
...(typeof guidanceText === "string" && guidanceText ? { guidanceText } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Form-field key carrying the user's choice in the refusal-fallback form. */
|
|
250
|
+
const REFUSAL_FALLBACK_CHOICE_KEY = "choice";
|
|
251
|
+
/** Wire values of the dialog's result enum (CLI schema). `edit_prompt` is
|
|
252
|
+
* deliberately not offered: in the CLI it prefills the composer with the
|
|
253
|
+
* refused prompt for edit-and-retry, and ACP has no composer-prefill surface
|
|
254
|
+
* — the user can simply edit and resend on their own. */
|
|
255
|
+
const RETRY_FALLBACK_RESULT = "retry_fallback";
|
|
256
|
+
const KEEP_REFUSAL_RESULT = "cancelled";
|
|
257
|
+
/**
|
|
258
|
+
* Render the refusal-fallback consent prompt as an ACP form elicitation: a
|
|
259
|
+
* single-select between retrying on the fallback model and keeping the
|
|
260
|
+
* refusal. The enum `const`s are the dialog's wire result values, so the
|
|
261
|
+
* response maps back without a translation table.
|
|
262
|
+
*/
|
|
263
|
+
export function refusalFallbackToCreateRequest(prompt, sessionId) {
|
|
264
|
+
const category = prompt.apiRefusalCategory ? ` (${prompt.apiRefusalCategory})` : "";
|
|
265
|
+
const guidance = prompt.guidanceText ? `\n\n${prompt.guidanceText}` : "";
|
|
266
|
+
return {
|
|
267
|
+
mode: "form",
|
|
268
|
+
sessionId,
|
|
269
|
+
message: `${prompt.originalModel} declined this request${category}. ` +
|
|
270
|
+
`Retry with ${prompt.fallbackModel}?` +
|
|
271
|
+
guidance,
|
|
272
|
+
requestedSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
[REFUSAL_FALLBACK_CHOICE_KEY]: {
|
|
276
|
+
type: "string",
|
|
277
|
+
oneOf: [
|
|
278
|
+
{
|
|
279
|
+
const: RETRY_FALLBACK_RESULT,
|
|
280
|
+
title: `Retry with ${prompt.fallbackModel}`,
|
|
281
|
+
description: `The session continues on ${prompt.fallbackModel}.`,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
const: KEEP_REFUSAL_RESULT,
|
|
285
|
+
title: "Keep the refusal",
|
|
286
|
+
description: "You can send a new message.",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Map the elicitation response back to the dialog's result enum. Only an
|
|
296
|
+
* explicit accept-with-retry resolves to `retry_fallback`; decline, cancel, a
|
|
297
|
+
* skipped field, or an unrecognized value all keep the refusal — the dialog's
|
|
298
|
+
* own default — so a dismissed or half-filled form can never trigger a model
|
|
299
|
+
* switch the user didn't ask for.
|
|
300
|
+
*/
|
|
301
|
+
export function refusalFallbackResultFromResponse(response) {
|
|
302
|
+
const choice = acceptedElicitationContent(response)[REFUSAL_FALLBACK_CHOICE_KEY];
|
|
303
|
+
return choice === RETRY_FALLBACK_RESULT ? RETRY_FALLBACK_RESULT : KEEP_REFUSAL_RESULT;
|
|
304
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import { claudeCliPath, runAcp } from "./acp-agent.js";
|
|
4
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
5
|
+
// `--cli` is checked first so that `--version`/`-v` (and any other flags) are
|
|
6
|
+
// forwarded to the wrapped native CLI rather than swallowed by our own version
|
|
7
|
+
// handler below. Our version flag only applies when not delegating.
|
|
8
|
+
if (process.argv.includes("--cli")) {
|
|
9
|
+
const { spawn } = await import("node:child_process");
|
|
10
|
+
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
|
|
11
|
+
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
|
|
12
|
+
const signals = process.platform === "win32"
|
|
13
|
+
? ["SIGINT", "SIGTERM"]
|
|
14
|
+
: ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
15
|
+
for (const sig of signals) {
|
|
16
|
+
process.on(sig, () => {
|
|
17
|
+
if (!child.killed)
|
|
18
|
+
child.kill(sig);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
child.on("exit", (code, signal) => {
|
|
22
|
+
if (signal && process.platform !== "win32") {
|
|
23
|
+
// Remove our listener so re-raising actually terminates instead of
|
|
24
|
+
// re-entering the no-op handler, which would let us exit with code 0
|
|
25
|
+
// instead of the signal's conventional 128+N.
|
|
26
|
+
process.removeAllListeners(signal);
|
|
27
|
+
process.kill(process.pid, signal);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.exit(code ?? 1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
child.on("error", (err) => {
|
|
34
|
+
console.error(err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
39
|
+
console.log(packageJson.version);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Apply env vars from the managed-policy tier before any SDK call so the
|
|
44
|
+
// SDK subprocess inherits them. Going through resolveSettings (vs. a raw
|
|
45
|
+
// read of managed-settings.json) also picks up MDM sources on macOS and
|
|
46
|
+
// HKLM/HKCU on Windows.
|
|
47
|
+
const policy = await resolveSettings({ settingSources: [] });
|
|
48
|
+
for (const [key, value] of Object.entries(policy.effective.env ?? {})) {
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
// stdout is used to send messages to the client
|
|
52
|
+
// we redirect everything else to stderr to make sure it doesn't interfere with ACP
|
|
53
|
+
console.log = console.error;
|
|
54
|
+
console.info = console.error;
|
|
55
|
+
console.warn = console.error;
|
|
56
|
+
console.debug = console.error;
|
|
57
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
58
|
+
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
|
59
|
+
});
|
|
60
|
+
const { connection, agent } = runAcp();
|
|
61
|
+
async function shutdown() {
|
|
62
|
+
await agent.dispose().catch((err) => {
|
|
63
|
+
console.error("Error during cleanup:", err);
|
|
64
|
+
});
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
// Exit cleanly when the ACP connection closes (e.g. stdin EOF, transport
|
|
68
|
+
// error). Without this, `process.stdin.resume()` keeps the event loop
|
|
69
|
+
// alive indefinitely, causing orphan process accumulation in oneshot mode.
|
|
70
|
+
connection.closed.then(shutdown);
|
|
71
|
+
process.on("SIGTERM", shutdown);
|
|
72
|
+
process.on("SIGINT", shutdown);
|
|
73
|
+
// Keep process alive while connection is open
|
|
74
|
+
process.stdin.resume();
|
|
75
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ClaudeAcpAgent, isLocalCommandMetadata, stripLocalCommandMetadata, runAcp, toAcpNotifications, streamEventToAcpNotifications, type ToolUpdateMeta, type NewSessionMeta, type SDKMessageFilter, } from "./acp-agent.js";
|
|
2
|
+
export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
3
|
+
export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
|
|
4
|
+
export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
|
|
5
|
+
export type { ClaudePlanEntry } from "./tools.js";
|
|
6
|
+
//# sourceMappingURL=lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,MAAM,EACN,kBAAkB,EAClB,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG7E,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -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";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Settings } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
export interface SettingsManagerOptions {
|
|
3
|
+
onChange?: () => void;
|
|
4
|
+
logger?: {
|
|
5
|
+
log: (...args: any[]) => void;
|
|
6
|
+
error: (...args: any[]) => void;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manages Claude Code settings using the SDK's `resolveSettings` merge engine
|
|
11
|
+
* so the values we see match what `query()` would observe.
|
|
12
|
+
*
|
|
13
|
+
* Watches the user/project/local/managed settings files for changes and
|
|
14
|
+
* re-resolves through the SDK on update. Escalating `permissions.defaultMode`
|
|
15
|
+
* values from repo-committed sources are filtered out via
|
|
16
|
+
* `filterEscalatingDefaultMode`, matching the CLI's trust policy.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SettingsManager {
|
|
19
|
+
private cwd;
|
|
20
|
+
private effective;
|
|
21
|
+
private watchers;
|
|
22
|
+
private onChange?;
|
|
23
|
+
private logger;
|
|
24
|
+
private initialized;
|
|
25
|
+
private disposed;
|
|
26
|
+
private debounceTimer;
|
|
27
|
+
private initPromise;
|
|
28
|
+
constructor(cwd: string, options?: SettingsManagerOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the settings manager by loading all settings and setting up file watchers
|
|
31
|
+
*/
|
|
32
|
+
initialize(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Paths the SDK reads when resolving settings for this cwd. Watching the
|
|
35
|
+
* containing directories means we pick up file creation as well as edits.
|
|
36
|
+
*/
|
|
37
|
+
private getWatchedPaths;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the effective settings via the SDK and applies the CLI's trust
|
|
40
|
+
* filter for escalating `permissions.defaultMode` values.
|
|
41
|
+
*/
|
|
42
|
+
private loadAllSettings;
|
|
43
|
+
/**
|
|
44
|
+
* Sets up file watchers for all settings files
|
|
45
|
+
*/
|
|
46
|
+
private setupWatchers;
|
|
47
|
+
/**
|
|
48
|
+
* Handles settings file changes with debouncing to avoid rapid reloads
|
|
49
|
+
*/
|
|
50
|
+
private handleSettingsChange;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the current merged settings
|
|
53
|
+
*/
|
|
54
|
+
getSettings(): Settings;
|
|
55
|
+
/**
|
|
56
|
+
* Returns the current working directory
|
|
57
|
+
*/
|
|
58
|
+
getCwd(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Updates the working directory and reloads project-specific settings
|
|
61
|
+
*/
|
|
62
|
+
setCwd(cwd: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Disposes of file watchers and cleans up resources
|
|
65
|
+
*/
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,gCAAgC,CAAC;AA0BxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAC7E;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAa;IAC9B,OAAO,CAAC,MAAM,CAAqE;IACnF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,WAAW,CAA8B;gBAErC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAMzD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBjC;;;OAGG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;YACW,eAAe;IAU7B;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;OAEG;IACH,WAAW,IAAI,QAAQ;IAIvB;;OAEG;IACH,MAAM,IAAI,MAAM;IAIhB;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxC;;OAEG;IACH,OAAO,IAAI,IAAI;CAehB"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
// resolveSettings and filterEscalatingDefaultMode are marked @alpha in the
|
|
4
|
+
// SDK; API may shift in a future release.
|
|
5
|
+
import { filterEscalatingDefaultMode, resolveSettings, } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
import { CLAUDE_CONFIG_DIR } from "./acp-agent.js";
|
|
7
|
+
/**
|
|
8
|
+
* Permission rule format examples:
|
|
9
|
+
* - "Read" - matches all Read tool calls
|
|
10
|
+
* - "Read(./.env)" - matches specific path
|
|
11
|
+
* - "Read(./.env.*)" - glob pattern
|
|
12
|
+
* - "Read(./secrets/**)" - recursive glob
|
|
13
|
+
* - "Bash(npm run lint)" - exact command prefix
|
|
14
|
+
* - "Bash(npm run:*)" - command prefix with wildcard
|
|
15
|
+
*
|
|
16
|
+
* Docs: https://code.claude.com/docs/en/iam#tool-specific-permission-rules
|
|
17
|
+
*/
|
|
18
|
+
function getManagedSettingsPath() {
|
|
19
|
+
switch (process.platform) {
|
|
20
|
+
case "darwin":
|
|
21
|
+
return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
|
22
|
+
case "win32":
|
|
23
|
+
return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
|
|
24
|
+
default:
|
|
25
|
+
return "/etc/claude-code/managed-settings.json";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Manages Claude Code settings using the SDK's `resolveSettings` merge engine
|
|
30
|
+
* so the values we see match what `query()` would observe.
|
|
31
|
+
*
|
|
32
|
+
* Watches the user/project/local/managed settings files for changes and
|
|
33
|
+
* re-resolves through the SDK on update. Escalating `permissions.defaultMode`
|
|
34
|
+
* values from repo-committed sources are filtered out via
|
|
35
|
+
* `filterEscalatingDefaultMode`, matching the CLI's trust policy.
|
|
36
|
+
*/
|
|
37
|
+
export class SettingsManager {
|
|
38
|
+
cwd;
|
|
39
|
+
effective = {};
|
|
40
|
+
watchers = [];
|
|
41
|
+
onChange;
|
|
42
|
+
logger;
|
|
43
|
+
initialized = false;
|
|
44
|
+
disposed = false;
|
|
45
|
+
debounceTimer = null;
|
|
46
|
+
initPromise = null;
|
|
47
|
+
constructor(cwd, options) {
|
|
48
|
+
this.cwd = cwd;
|
|
49
|
+
this.onChange = options?.onChange;
|
|
50
|
+
this.logger = options?.logger ?? console;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the settings manager by loading all settings and setting up file watchers
|
|
54
|
+
*/
|
|
55
|
+
async initialize() {
|
|
56
|
+
if (this.initialized) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (this.initPromise) {
|
|
60
|
+
return this.initPromise;
|
|
61
|
+
}
|
|
62
|
+
this.disposed = false;
|
|
63
|
+
this.initPromise = this.loadAllSettings().then(() => {
|
|
64
|
+
if (!this.disposed) {
|
|
65
|
+
this.setupWatchers();
|
|
66
|
+
this.initialized = true;
|
|
67
|
+
}
|
|
68
|
+
this.initPromise = null;
|
|
69
|
+
});
|
|
70
|
+
return this.initPromise;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Paths the SDK reads when resolving settings for this cwd. Watching the
|
|
74
|
+
* containing directories means we pick up file creation as well as edits.
|
|
75
|
+
*/
|
|
76
|
+
getWatchedPaths() {
|
|
77
|
+
return [
|
|
78
|
+
path.join(CLAUDE_CONFIG_DIR, "settings.json"),
|
|
79
|
+
path.join(this.cwd, ".claude", "settings.json"),
|
|
80
|
+
path.join(this.cwd, ".claude", "settings.local.json"),
|
|
81
|
+
getManagedSettingsPath(),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the effective settings via the SDK and applies the CLI's trust
|
|
86
|
+
* filter for escalating `permissions.defaultMode` values.
|
|
87
|
+
*/
|
|
88
|
+
async loadAllSettings() {
|
|
89
|
+
try {
|
|
90
|
+
const resolved = await resolveSettings({ cwd: this.cwd });
|
|
91
|
+
this.effective = filterEscalatingDefaultMode(resolved);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error("Failed to resolve settings:", error);
|
|
95
|
+
this.effective = {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sets up file watchers for all settings files
|
|
100
|
+
*/
|
|
101
|
+
setupWatchers() {
|
|
102
|
+
for (const filePath of this.getWatchedPaths()) {
|
|
103
|
+
try {
|
|
104
|
+
const dir = path.dirname(filePath);
|
|
105
|
+
const filename = path.basename(filePath);
|
|
106
|
+
if (fs.existsSync(dir)) {
|
|
107
|
+
const watcher = fs.watch(dir, (eventType, changedFilename) => {
|
|
108
|
+
if (changedFilename === filename) {
|
|
109
|
+
this.handleSettingsChange();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
watcher.on("error", (error) => {
|
|
113
|
+
this.logger.error(`Settings watcher error for ${filePath}:`, error);
|
|
114
|
+
});
|
|
115
|
+
this.watchers.push(watcher);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.logger.error(`Failed to set up watcher for ${filePath}:`, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Handles settings file changes with debouncing to avoid rapid reloads
|
|
125
|
+
*/
|
|
126
|
+
handleSettingsChange() {
|
|
127
|
+
if (this.debounceTimer) {
|
|
128
|
+
clearTimeout(this.debounceTimer);
|
|
129
|
+
}
|
|
130
|
+
this.debounceTimer = setTimeout(async () => {
|
|
131
|
+
this.debounceTimer = null;
|
|
132
|
+
if (this.disposed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await this.loadAllSettings();
|
|
137
|
+
if (!this.disposed) {
|
|
138
|
+
this.onChange?.();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logger.error("Failed to reload settings:", error);
|
|
143
|
+
}
|
|
144
|
+
}, 100);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns the current merged settings
|
|
148
|
+
*/
|
|
149
|
+
getSettings() {
|
|
150
|
+
return this.effective;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Returns the current working directory
|
|
154
|
+
*/
|
|
155
|
+
getCwd() {
|
|
156
|
+
return this.cwd;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Updates the working directory and reloads project-specific settings
|
|
160
|
+
*/
|
|
161
|
+
async setCwd(cwd) {
|
|
162
|
+
if (this.cwd === cwd) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.dispose();
|
|
166
|
+
this.cwd = cwd;
|
|
167
|
+
await this.initialize();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Disposes of file watchers and cleans up resources
|
|
171
|
+
*/
|
|
172
|
+
dispose() {
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.initialized = false;
|
|
175
|
+
this.initPromise = null;
|
|
176
|
+
if (this.debounceTimer) {
|
|
177
|
+
clearTimeout(this.debounceTimer);
|
|
178
|
+
this.debounceTimer = null;
|
|
179
|
+
}
|
|
180
|
+
for (const watcher of this.watchers) {
|
|
181
|
+
watcher.close();
|
|
182
|
+
}
|
|
183
|
+
this.watchers = [];
|
|
184
|
+
}
|
|
185
|
+
}
|