@lucascouts/claude-agent-acp-plus 0.8.0 → 0.9.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 (62) hide show
  1. package/dist/acp-agent.d.ts +107 -95
  2. package/dist/acp-agent.d.ts.map +1 -1
  3. package/dist/acp-agent.js +1050 -818
  4. package/dist/clear-context-coordinator.d.ts +58 -0
  5. package/dist/clear-context-coordinator.d.ts.map +1 -0
  6. package/dist/clear-context-coordinator.js +79 -0
  7. package/dist/exit-plan.d.ts +42 -0
  8. package/dist/exit-plan.d.ts.map +1 -0
  9. package/dist/exit-plan.js +154 -0
  10. package/dist/file-change-audit.d.ts +56 -0
  11. package/dist/file-change-audit.d.ts.map +1 -0
  12. package/dist/file-change-audit.js +379 -0
  13. package/dist/index.js +24 -1
  14. package/dist/permissions/effects.d.ts +19 -0
  15. package/dist/permissions/effects.d.ts.map +1 -0
  16. package/dist/permissions/effects.js +166 -0
  17. package/dist/permissions/modes.d.ts +8 -0
  18. package/dist/permissions/modes.d.ts.map +1 -0
  19. package/dist/permissions/modes.js +41 -0
  20. package/dist/permissions/normalization.d.ts +6 -0
  21. package/dist/permissions/normalization.d.ts.map +1 -0
  22. package/dist/permissions/normalization.js +100 -0
  23. package/dist/permissions/options/filesystem.d.ts +9 -0
  24. package/dist/permissions/options/filesystem.d.ts.map +1 -0
  25. package/dist/permissions/options/filesystem.js +124 -0
  26. package/dist/permissions/options/shared.d.ts +39 -0
  27. package/dist/permissions/options/shared.d.ts.map +1 -0
  28. package/dist/permissions/options/shared.js +64 -0
  29. package/dist/permissions/options/shell.d.ts +5 -0
  30. package/dist/permissions/options/shell.d.ts.map +1 -0
  31. package/dist/permissions/options/shell.js +100 -0
  32. package/dist/permissions/options/tools.d.ts +11 -0
  33. package/dist/permissions/options/tools.d.ts.map +1 -0
  34. package/dist/permissions/options/tools.js +135 -0
  35. package/dist/permissions/options.d.ts +5 -0
  36. package/dist/permissions/options.d.ts.map +1 -0
  37. package/dist/permissions/options.js +60 -0
  38. package/dist/permissions/presentation.d.ts +15 -0
  39. package/dist/permissions/presentation.d.ts.map +1 -0
  40. package/dist/permissions/presentation.js +83 -0
  41. package/dist/permissions/response.d.ts +11 -0
  42. package/dist/permissions/response.d.ts.map +1 -0
  43. package/dist/permissions/response.js +19 -0
  44. package/dist/session-config-ids.d.ts +5 -0
  45. package/dist/session-config-ids.d.ts.map +1 -0
  46. package/dist/session-config-ids.js +4 -0
  47. package/dist/session-failure-extension.d.ts +104 -0
  48. package/dist/session-failure-extension.d.ts.map +1 -0
  49. package/dist/session-failure-extension.js +352 -0
  50. package/dist/session-mode.d.ts +66 -0
  51. package/dist/session-mode.d.ts.map +1 -0
  52. package/dist/session-mode.js +246 -0
  53. package/dist/session-titles.d.ts +77 -0
  54. package/dist/session-titles.d.ts.map +1 -0
  55. package/dist/session-titles.js +199 -0
  56. package/dist/tool-result-meta.d.ts +8 -0
  57. package/dist/tool-result-meta.d.ts.map +1 -0
  58. package/dist/tool-result-meta.js +19 -0
  59. package/dist/tools.d.ts +4 -6
  60. package/dist/tools.d.ts.map +1 -1
  61. package/dist/tools.js +129 -24
  62. package/package.json +9 -9
@@ -0,0 +1,352 @@
1
+ import { USAGE_LIMIT_ERROR_PREFIXES, } from "@anthropic-ai/claude-agent-sdk";
2
+ import { randomUUID } from "node:crypto";
3
+ const JETBRAINS_META_KEY = "jetbrains";
4
+ const AIR_META_KEY = "air";
5
+ const AIR_EXTENSION_VERSION_KEY = "version";
6
+ const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
7
+ const AIR_SESSION_FAILURE_KEY = "sessionFailure";
8
+ const AIR_EXTENSION_VERSION = 1;
9
+ export function airSessionFailureCapabilityMeta(...additionalCapabilities) {
10
+ return {
11
+ [JETBRAINS_META_KEY]: {
12
+ [AIR_META_KEY]: {
13
+ [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
14
+ [AIR_EXTENSION_CAPABILITIES_KEY]: [AIR_SESSION_FAILURE_KEY, ...additionalCapabilities],
15
+ },
16
+ },
17
+ };
18
+ }
19
+ export function createSessionFailureState() {
20
+ return {
21
+ epoch: randomUUID(),
22
+ revisions: new Map(),
23
+ active: new Map(),
24
+ };
25
+ }
26
+ const AIR_FAILURE_POLICY = {
27
+ advisory: {
28
+ category: "unknown",
29
+ actions: [],
30
+ fallbackTitle: "Claude reported a notice.",
31
+ },
32
+ auth_required: {
33
+ category: "access",
34
+ actions: ["login"],
35
+ fallbackTitle: "Sign in to continue using Claude.",
36
+ },
37
+ bad_request: {
38
+ category: "request",
39
+ actions: [],
40
+ fallbackTitle: "Claude could not process this request.",
41
+ },
42
+ budget_exhausted: {
43
+ category: "limit",
44
+ actions: ["new_session"],
45
+ fallbackTitle: "This Claude session reached its configured budget.",
46
+ },
47
+ context_exhausted: {
48
+ category: "limit",
49
+ actions: ["new_session"],
50
+ fallbackTitle: "This Claude turn reached its configured limit.",
51
+ },
52
+ internal_error: {
53
+ category: "service",
54
+ actions: ["retry", "new_session"],
55
+ fallbackTitle: "Claude Agent encountered an internal error.",
56
+ },
57
+ overloaded: {
58
+ category: "service",
59
+ actions: ["retry"],
60
+ fallbackTitle: "Claude is temporarily overloaded.",
61
+ },
62
+ provider_error: {
63
+ category: "service",
64
+ actions: ["retry"],
65
+ fallbackTitle: "The model provider reported an error.",
66
+ },
67
+ quota_exhausted: {
68
+ category: "limit",
69
+ actions: [],
70
+ fallbackTitle: "The Claude account has no available quota.",
71
+ },
72
+ rate_limited: {
73
+ category: "limit",
74
+ actions: ["retry"],
75
+ fallbackTitle: "Claude is temporarily rate limited.",
76
+ },
77
+ transport_lost: {
78
+ category: "connection",
79
+ actions: ["new_session"],
80
+ fallbackTitle: "The connection to Claude was lost.",
81
+ },
82
+ worker_shutdown: {
83
+ category: "connection",
84
+ actions: ["new_session"],
85
+ fallbackTitle: "The Claude worker is shutting down.",
86
+ },
87
+ };
88
+ export function sessionFailureMeta(failure) {
89
+ return {
90
+ [JETBRAINS_META_KEY]: {
91
+ [AIR_META_KEY]: {
92
+ [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
93
+ [AIR_SESSION_FAILURE_KEY]: {
94
+ id: failure.id,
95
+ revision: failure.revision,
96
+ category: failure.category,
97
+ severity: failure.severity,
98
+ title: failure.title,
99
+ ...(failure.details ? { details: failure.details } : {}),
100
+ actions: failure.actions,
101
+ },
102
+ },
103
+ },
104
+ };
105
+ }
106
+ /** `getSessionMessages` deliberately exposes only the API message and strips
107
+ * transcript-level `error` / `isApiErrorMessage` fields. The SDK exports the
108
+ * exact stable prefixes used by its synthetic usage-limit errors, so replay
109
+ * can recover this one typed failure without matching arbitrary model prose. */
110
+ export function assistantMessageText(apiMessage) {
111
+ if (!apiMessage || typeof apiMessage !== "object")
112
+ return undefined;
113
+ const { content } = apiMessage;
114
+ if (typeof content === "string")
115
+ return content || undefined;
116
+ if (!Array.isArray(content))
117
+ return undefined;
118
+ const text = content
119
+ .map((block) => block &&
120
+ typeof block === "object" &&
121
+ "type" in block &&
122
+ block.type === "text" &&
123
+ "text" in block &&
124
+ typeof block.text === "string"
125
+ ? block.text
126
+ : "")
127
+ .join("");
128
+ return text || undefined;
129
+ }
130
+ export function isSyntheticUsageLimitMessage(apiMessage) {
131
+ if (!apiMessage || typeof apiMessage !== "object")
132
+ return false;
133
+ const { model } = apiMessage;
134
+ if (model !== "<synthetic>")
135
+ return false;
136
+ const text = assistantMessageText(apiMessage);
137
+ if (!text)
138
+ return false;
139
+ return USAGE_LIMIT_ERROR_PREFIXES.some((prefix) => text.startsWith(prefix));
140
+ }
141
+ export function activeUsageLimitMessage(messages) {
142
+ let active;
143
+ for (const message of messages) {
144
+ if (message.type !== "assistant" || message.parent_tool_use_id !== null)
145
+ continue;
146
+ if (isSyntheticUsageLimitMessage(message.message)) {
147
+ const title = assistantMessageText(message.message);
148
+ if (title)
149
+ active = { uuid: message.uuid, title };
150
+ continue;
151
+ }
152
+ const model = message.message && typeof message.message === "object" && "model" in message.message
153
+ ? message.message.model
154
+ : undefined;
155
+ // A later real model answer proves the account recovered. Synthetic
156
+ // local-command/interruption messages do not: they can be appended while
157
+ // the provider remains unavailable.
158
+ if (model !== undefined && model !== "<synthetic>")
159
+ active = undefined;
160
+ }
161
+ return active;
162
+ }
163
+ export function supportsAirSessionFailures(capabilities) {
164
+ const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY];
165
+ const air = jetbrains?.[AIR_META_KEY];
166
+ const version = air?.[AIR_EXTENSION_VERSION_KEY];
167
+ const advertised = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
168
+ return (typeof version === "number" &&
169
+ Number.isFinite(version) &&
170
+ Number.isInteger(version) &&
171
+ version >= AIR_EXTENSION_VERSION &&
172
+ Array.isArray(advertised) &&
173
+ advertised.some((capability) => capability === AIR_SESSION_FAILURE_KEY));
174
+ }
175
+ function sessionFailureRecoveryPolicy(kind, turnId) {
176
+ switch (kind) {
177
+ case "advisory":
178
+ return "never";
179
+ case "auth_required":
180
+ return "auth_status";
181
+ case "transport_lost":
182
+ case "worker_shutdown":
183
+ return "runtime_rebind";
184
+ case "quota_exhausted":
185
+ return "real_model_success";
186
+ default:
187
+ return turnId ? "next_attempt" : "real_model_success";
188
+ }
189
+ }
190
+ /** Owns the AIR failure lifecycle for both the live consumer and history replay.
191
+ * Published revisions become active only after delivery succeeds; recovery only
192
+ * removes internal active state because transcript records are historical. */
193
+ export class SessionFailureController {
194
+ sessionId;
195
+ state;
196
+ capabilities;
197
+ isCurrent;
198
+ sendUpdate;
199
+ logger;
200
+ constructor(options) {
201
+ this.sessionId = options.sessionId;
202
+ this.state = options.state;
203
+ this.capabilities = options.capabilities;
204
+ this.isCurrent = options.isCurrent;
205
+ this.sendUpdate = options.sendUpdate;
206
+ this.logger = options.logger;
207
+ }
208
+ isSupported() {
209
+ return supportsAirSessionFailures(this.capabilities);
210
+ }
211
+ async emit(failure) {
212
+ if (!this.isCurrent()) {
213
+ this.logger.error(`Session ${this.sessionId}: ignored AIR session failure from a stale consumer`);
214
+ return false;
215
+ }
216
+ try {
217
+ await this.sendUpdate({
218
+ sessionId: this.sessionId,
219
+ update: {
220
+ sessionUpdate: "session_info_update",
221
+ _meta: sessionFailureMeta(failure),
222
+ },
223
+ });
224
+ return true;
225
+ }
226
+ catch (error) {
227
+ this.logger.error(`Session ${this.sessionId}: failed to publish AIR session failure: ${error}`);
228
+ return false;
229
+ }
230
+ }
231
+ recordActive(failure) {
232
+ this.state.revisions.set(failure.id, failure.revision);
233
+ this.state.active.set(failure.id, failure);
234
+ this.state.lastNotice =
235
+ failure.kind === "advisory" ? { title: failure.title, id: failure.id } : undefined;
236
+ }
237
+ async clear(shouldClear = () => true) {
238
+ if (!this.isSupported())
239
+ return true;
240
+ this.state.lastNotice = undefined;
241
+ for (const failure of [...this.state.active.values()]) {
242
+ if (!shouldClear(failure))
243
+ continue;
244
+ this.state.active.delete(failure.id);
245
+ }
246
+ return true;
247
+ }
248
+ async prepare(kind, failureOptions = {}) {
249
+ if (!this.isSupported() || !this.isCurrent())
250
+ return undefined;
251
+ const policy = AIR_FAILURE_POLICY[kind];
252
+ const title = failureOptions.title || policy.fallbackTitle;
253
+ if (kind === "advisory") {
254
+ const id = this.state.lastNotice?.title === title
255
+ ? this.state.lastNotice.id
256
+ : `${this.sessionId}:notice:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
257
+ if (this.state.lastNotice?.title !== title) {
258
+ this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
259
+ }
260
+ return {
261
+ id,
262
+ revision: (this.state.revisions.get(id) ?? 0) + 1,
263
+ kind,
264
+ category: policy.category,
265
+ severity: "warning",
266
+ title,
267
+ ...(failureOptions.details ? { details: failureOptions.details } : {}),
268
+ actions: policy.actions,
269
+ recoveryPolicy: sessionFailureRecoveryPolicy(kind),
270
+ };
271
+ }
272
+ const id = failureOptions.turnId && !failureOptions.sessionScoped
273
+ ? `${failureOptions.turnId}:error`
274
+ : `${this.sessionId}:session-error:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
275
+ if (!failureOptions.turnId || failureOptions.sessionScoped) {
276
+ this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
277
+ }
278
+ return {
279
+ id,
280
+ revision: (this.state.revisions.get(id) ?? 0) + 1,
281
+ kind,
282
+ category: policy.category,
283
+ severity: failureOptions.severity ?? "error",
284
+ title,
285
+ ...(failureOptions.details ? { details: failureOptions.details } : {}),
286
+ actions: failureOptions.severity === "warning" ? [] : policy.actions,
287
+ recoveryPolicy: sessionFailureRecoveryPolicy(kind, failureOptions.turnId && !failureOptions.sessionScoped ? failureOptions.turnId : undefined),
288
+ ...(failureOptions.turnId && !failureOptions.sessionScoped
289
+ ? { turnId: failureOptions.turnId }
290
+ : {}),
291
+ };
292
+ }
293
+ async publish(kind, failureOptions = {}) {
294
+ const failure = await this.prepare(kind, failureOptions);
295
+ if (!failure)
296
+ return;
297
+ if (await this.emit(failure))
298
+ this.recordActive(failure);
299
+ }
300
+ async restore(id, kind, title, active = true) {
301
+ if (!this.isSupported() || !this.isCurrent())
302
+ return false;
303
+ const policy = AIR_FAILURE_POLICY[kind];
304
+ const failure = {
305
+ id,
306
+ revision: (this.state.revisions.get(id) ?? 0) + 1,
307
+ kind,
308
+ category: policy.category,
309
+ severity: "error",
310
+ title,
311
+ actions: policy.actions,
312
+ recoveryPolicy: sessionFailureRecoveryPolicy(kind),
313
+ };
314
+ if (!(await this.emit(failure)))
315
+ return false;
316
+ this.state.revisions.set(failure.id, failure.revision);
317
+ if (active) {
318
+ this.state.active.set(failure.id, failure);
319
+ }
320
+ else {
321
+ this.state.active.delete(failure.id);
322
+ }
323
+ return true;
324
+ }
325
+ }
326
+ export function providerFailureCategory(errorKind, isUsageLimit = false) {
327
+ if (isUsageLimit)
328
+ return "quota_exhausted";
329
+ switch (errorKind) {
330
+ case "authentication_failed":
331
+ case "oauth_org_not_allowed":
332
+ return "auth_required";
333
+ case "billing_error":
334
+ case "account_on_hold":
335
+ return "quota_exhausted";
336
+ case "rate_limit":
337
+ return "rate_limited";
338
+ case "overloaded":
339
+ return "overloaded";
340
+ case "invalid_request":
341
+ case "model_not_found":
342
+ return "bad_request";
343
+ case "max_output_tokens":
344
+ return "context_exhausted";
345
+ case "server_error":
346
+ case "unknown":
347
+ case undefined:
348
+ return "provider_error";
349
+ default:
350
+ return "provider_error";
351
+ }
352
+ }
@@ -0,0 +1,66 @@
1
+ import { type SessionConfigOption, type SessionModeState, type SessionNotification, type SetSessionModeRequest, type SetSessionModeResponse } from "@agentclientprotocol/sdk";
2
+ import type { ModelInfo, PermissionMode, PermissionResult, Query } from "@anthropic-ai/claude-agent-sdk";
3
+ export declare const MODE_CONFIG_ID = "mode";
4
+ export declare const AUTO_MODE_FALLBACK: PermissionMode;
5
+ export type SessionMode = {
6
+ query: Pick<Query, "setPermissionMode">;
7
+ queryClosed?: boolean;
8
+ modes: SessionModeState;
9
+ models: {
10
+ currentModelId: string;
11
+ };
12
+ modelInfos: ModelInfo[];
13
+ /** Prevents the model-specific Auto fallback from spamming the transcript. */
14
+ autoModeFallbackWarningShown?: boolean;
15
+ /** Initial mode fallback is reported after session/new, on the first prompt. */
16
+ autoModeFallbackWarningPending?: boolean;
17
+ };
18
+ export type SessionModeManagerOptions<S extends SessionMode> = {
19
+ getSession(sessionId: string): S | undefined;
20
+ sessionEndedMessage: string;
21
+ updateConfigOption(sessionId: string, configId: string, value: string): Promise<void>;
22
+ sessionUpdate(params: SessionNotification): Promise<void>;
23
+ logError(...args: unknown[]): void;
24
+ };
25
+ type ModeConfigSession = SessionMode & {
26
+ configOptions: SessionConfigOption[];
27
+ };
28
+ type InitializeSessionModeParams = {
29
+ query: Pick<Query, "setPermissionMode">;
30
+ requestedMode: PermissionMode;
31
+ currentModelInfo?: ModelInfo;
32
+ currentModelId: string;
33
+ };
34
+ /** Owns session-mode policy and the ACP/SDK synchronization it requires. */
35
+ export declare class SessionModeManager<S extends SessionMode> {
36
+ private readonly options;
37
+ constructor(options: SessionModeManagerOptions<S>);
38
+ initialize({ query, requestedMode, currentModelInfo, currentModelId, }: InitializeSessionModeParams): Promise<{
39
+ modes: SessionModeState;
40
+ autoModeFallbackWarningPending: boolean;
41
+ }>;
42
+ static configOption(modes: SessionModeState): SessionConfigOption;
43
+ syncConfig(session: ModeConfigSession, mode: string): void;
44
+ availableModeIds(modes: SessionModeState): string[];
45
+ effectiveMode(session: SessionMode, requestedMode: PermissionMode): PermissionMode;
46
+ applyPermissionFallback(session: SessionMode, permissionResult: PermissionResult): {
47
+ permissionResult: PermissionResult;
48
+ fallbackApplied: boolean;
49
+ };
50
+ /** Reconcile mode after a model switch. The caller rebuilds config options
51
+ * before publishing the returned state change. */
52
+ reconcileForModel(session: SessionMode, model: ModelInfo | undefined): Promise<boolean>;
53
+ selectMode(sessionId: string, modeId: string): Promise<PermissionMode>;
54
+ setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse>;
55
+ publishCurrent(sessionId: string, mode: string): Promise<void>;
56
+ publishFallbackWarning(sessionId: string, session: SessionMode): Promise<void>;
57
+ publishFallbackState(sessionId: string, session: SessionMode): Promise<void>;
58
+ private requireOpenSession;
59
+ private isAutoUnavailable;
60
+ private isAvailable;
61
+ private parseMode;
62
+ private buildAvailableModes;
63
+ private trySyncMode;
64
+ }
65
+ export {};
66
+ //# sourceMappingURL=session-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-mode.d.ts","sourceRoot":"","sources":["../src/session-mode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,246 @@
1
+ import { RequestError, } from "@agentclientprotocol/sdk";
2
+ import { ALLOW_BYPASS } from "./permissions/modes.js";
3
+ export const MODE_CONFIG_ID = "mode";
4
+ export const AUTO_MODE_FALLBACK = "acceptEdits";
5
+ const AUTO_MODE_FALLBACK_NOTICE = "**Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.";
6
+ /** Owns session-mode policy and the ACP/SDK synchronization it requires. */
7
+ export class SessionModeManager {
8
+ options;
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ async initialize({ query, requestedMode, currentModelInfo, currentModelId, }) {
13
+ const availableModes = this.buildAvailableModes();
14
+ let effectiveMode = requestedMode;
15
+ let autoModeFallbackWarningPending = false;
16
+ if (effectiveMode === "auto" &&
17
+ currentModelInfo !== undefined &&
18
+ currentModelInfo.supportsAutoMode !== true) {
19
+ this.options.logError(`permissions.defaultMode "auto" is not available for model ` +
20
+ `"${currentModelId}"; falling back to "${AUTO_MODE_FALLBACK}".`);
21
+ effectiveMode = AUTO_MODE_FALLBACK;
22
+ autoModeFallbackWarningPending = true;
23
+ await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
24
+ }
25
+ else if (!this.isAvailable(availableModes, effectiveMode)) {
26
+ this.options.logError(`permissions.defaultMode "${effectiveMode}" is not available in ` +
27
+ `this session; falling back to "default".`);
28
+ effectiveMode = "default";
29
+ await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
30
+ }
31
+ return {
32
+ modes: { currentModeId: effectiveMode, availableModes },
33
+ autoModeFallbackWarningPending,
34
+ };
35
+ }
36
+ static configOption(modes) {
37
+ return {
38
+ id: MODE_CONFIG_ID,
39
+ name: "Mode",
40
+ description: "Session permission mode",
41
+ category: "mode",
42
+ type: "select",
43
+ currentValue: modes.currentModeId,
44
+ options: modes.availableModes.map((mode) => ({
45
+ value: mode.id,
46
+ name: mode.name,
47
+ description: mode.description,
48
+ _meta: mode._meta,
49
+ })),
50
+ };
51
+ }
52
+ syncConfig(session, mode) {
53
+ session.modes = { ...session.modes, currentModeId: mode };
54
+ session.configOptions = session.configOptions.map((option) => option.id === MODE_CONFIG_ID && typeof option.currentValue === "string"
55
+ ? { ...option, currentValue: mode }
56
+ : option);
57
+ }
58
+ availableModeIds(modes) {
59
+ return modes.availableModes.map((mode) => mode.id);
60
+ }
61
+ effectiveMode(session, requestedMode) {
62
+ return requestedMode === "auto" && this.isAutoUnavailable(session)
63
+ ? AUTO_MODE_FALLBACK
64
+ : requestedMode;
65
+ }
66
+ applyPermissionFallback(session, permissionResult) {
67
+ const fallbackApplied = this.isAutoUnavailable(session) &&
68
+ permissionResult.behavior === "allow" &&
69
+ permissionResult.updatedPermissions?.some((update) => update.type === "setMode" && update.mode === "auto") === true;
70
+ if (!fallbackApplied || permissionResult.behavior !== "allow") {
71
+ return { permissionResult, fallbackApplied: false };
72
+ }
73
+ return {
74
+ permissionResult: {
75
+ ...permissionResult,
76
+ updatedPermissions: permissionResult.updatedPermissions?.map((update) => update.type === "setMode" && update.mode === "auto"
77
+ ? { ...update, mode: AUTO_MODE_FALLBACK }
78
+ : update),
79
+ },
80
+ fallbackApplied: true,
81
+ };
82
+ }
83
+ /** Reconcile mode after a model switch. The caller rebuilds config options
84
+ * before publishing the returned state change. */
85
+ async reconcileForModel(session, model) {
86
+ if (session.modes.currentModeId !== "auto" ||
87
+ model === undefined ||
88
+ model.supportsAutoMode === true) {
89
+ return false;
90
+ }
91
+ session.modes = {
92
+ availableModes: session.modes.availableModes,
93
+ currentModeId: AUTO_MODE_FALLBACK,
94
+ };
95
+ await this.trySyncMode(session.query, AUTO_MODE_FALLBACK, `Failed to sync permissionMode to "${AUTO_MODE_FALLBACK}" after model switch invalidated "auto":`);
96
+ return true;
97
+ }
98
+ async selectMode(sessionId, modeId) {
99
+ const session = this.requireOpenSession(sessionId);
100
+ const requestedMode = this.parseMode(modeId);
101
+ if (!this.isAvailable(session.modes.availableModes, requestedMode)) {
102
+ throw new Error(`Mode ${modeId} is not available in this session`);
103
+ }
104
+ const effectiveMode = this.effectiveMode(session, requestedMode);
105
+ try {
106
+ await session.query.setPermissionMode(effectiveMode);
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error) {
110
+ if (!error.message)
111
+ error.message = "Invalid Mode";
112
+ throw error;
113
+ }
114
+ // eslint-disable-next-line preserve-caught-error
115
+ throw new Error("Invalid Mode");
116
+ }
117
+ if (effectiveMode !== requestedMode) {
118
+ await this.publishFallbackWarning(sessionId, session);
119
+ }
120
+ return effectiveMode;
121
+ }
122
+ async setSessionMode(params) {
123
+ const effectiveMode = await this.selectMode(params.sessionId, params.modeId);
124
+ if (effectiveMode !== params.modeId) {
125
+ await this.publishCurrent(params.sessionId, effectiveMode);
126
+ }
127
+ await this.options.updateConfigOption(params.sessionId, MODE_CONFIG_ID, effectiveMode);
128
+ return {};
129
+ }
130
+ async publishCurrent(sessionId, mode) {
131
+ await this.options.sessionUpdate({
132
+ sessionId,
133
+ update: { sessionUpdate: "current_mode_update", currentModeId: mode },
134
+ });
135
+ }
136
+ async publishFallbackWarning(sessionId, session) {
137
+ session.autoModeFallbackWarningPending = false;
138
+ if (session.autoModeFallbackWarningShown)
139
+ return;
140
+ session.autoModeFallbackWarningShown = true;
141
+ try {
142
+ await this.options.sessionUpdate({
143
+ sessionId,
144
+ update: {
145
+ sessionUpdate: "agent_message_chunk",
146
+ content: { type: "text", text: AUTO_MODE_FALLBACK_NOTICE },
147
+ },
148
+ });
149
+ }
150
+ catch (error) {
151
+ // The fallback has already been applied; a failed advisory must not turn
152
+ // the successful mode change into a failed request.
153
+ this.options.logError(`Failed to publish Auto mode fallback warning: ${error}`);
154
+ }
155
+ }
156
+ async publishFallbackState(sessionId, session) {
157
+ await this.publishCurrent(sessionId, AUTO_MODE_FALLBACK);
158
+ await this.publishFallbackWarning(sessionId, session);
159
+ }
160
+ requireOpenSession(sessionId) {
161
+ const session = this.options.getSession(sessionId);
162
+ if (!session)
163
+ throw new Error("Session not found");
164
+ if (session.queryClosed) {
165
+ throw RequestError.internalError(undefined, this.options.sessionEndedMessage);
166
+ }
167
+ return session;
168
+ }
169
+ isAutoUnavailable(session, modelId = session.models.currentModelId) {
170
+ const modelInfo = session.modelInfos.find((model) => model.value === modelId);
171
+ return modelInfo !== undefined && modelInfo.supportsAutoMode !== true;
172
+ }
173
+ isAvailable(availableModes, modeId) {
174
+ return availableModes.some((mode) => mode.id === modeId);
175
+ }
176
+ parseMode(modeId) {
177
+ switch (modeId) {
178
+ case "auto":
179
+ case "default":
180
+ case "acceptEdits":
181
+ case "bypassPermissions":
182
+ case "dontAsk":
183
+ case "plan":
184
+ return modeId;
185
+ default:
186
+ throw new Error("Invalid Mode");
187
+ }
188
+ }
189
+ buildAvailableModes() {
190
+ const modes = [
191
+ {
192
+ id: "default",
193
+ name: "Manual",
194
+ description: "Always ask before making changes",
195
+ _meta: { kind: "standard" },
196
+ },
197
+ {
198
+ id: "acceptEdits",
199
+ name: "Accept edits",
200
+ description: "Automatically accept all file edits",
201
+ _meta: { kind: "standard" },
202
+ },
203
+ {
204
+ id: "plan",
205
+ name: "Plan",
206
+ description: "Create a plan before making changes",
207
+ _meta: { kind: "plan" },
208
+ },
209
+ {
210
+ id: "auto",
211
+ name: "Auto",
212
+ description: "Claude handles permission decisions",
213
+ _meta: { kind: "auto_review" },
214
+ },
215
+ // Fork override, re-implanted here from the pre-merge `buildAvailableModes`
216
+ // in `acp-agent.ts`: upstream accepts `dontAsk` in `parseMode` but never
217
+ // advertises it, so adopting upstream's catalog wholesale would silently
218
+ // drop a mode the fork has offered since story 006 (R2.5 names it).
219
+ // No `_meta.kind` is set: upstream's four kinds all describe a mode that
220
+ // asks or auto-approves, and `dontAsk` denies instead — claiming any of
221
+ // them would assert a semantic upstream has not defined for this mode.
222
+ {
223
+ id: "dontAsk",
224
+ name: "Don't Ask",
225
+ description: "Don't prompt for permissions, deny if not pre-approved",
226
+ },
227
+ ];
228
+ if (ALLOW_BYPASS) {
229
+ modes.push({
230
+ id: "bypassPermissions",
231
+ name: "Bypass permissions",
232
+ description: "Accepts all permissions",
233
+ _meta: { kind: "full_access" },
234
+ });
235
+ }
236
+ return modes;
237
+ }
238
+ async trySyncMode(query, mode, errorMessage) {
239
+ try {
240
+ await query.setPermissionMode(mode);
241
+ }
242
+ catch (error) {
243
+ this.options.logError(errorMessage, error);
244
+ }
245
+ }
246
+ }