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.
- 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 +2 -2
- package/dist/recipes.js +37 -14
- package/dist/room.js +3 -2
- package/dist/server.js +16 -3
- package/package.json +8 -5
- package/scripts/vendor-acp.mjs +85 -0
- package/ui/app.css +7 -0
- package/ui/app.js +116 -26
- 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,19 @@
|
|
|
1
|
+
import { applyClaudePermissionSelection, parseClaudePermissionSelection } from "./effects.js";
|
|
2
|
+
/** Decode, validate, and interpret an ACP response exactly once. */
|
|
3
|
+
export function decodeClaudePermissionResponse(response, toolName, input, toolUseID, offeredOptions, durableChangeSet) {
|
|
4
|
+
const selection = parseClaudePermissionSelection(response, toolName);
|
|
5
|
+
const offeredOption = offeredOptions.find((option) => option.optionId === selection.optionId);
|
|
6
|
+
if (!offeredOption) {
|
|
7
|
+
throw new Error(`Permission option was not offered: ${selection.optionId}`);
|
|
8
|
+
}
|
|
9
|
+
const permissionResult = applyClaudePermissionSelection(selection, {
|
|
10
|
+
toolName,
|
|
11
|
+
input,
|
|
12
|
+
toolUseID,
|
|
13
|
+
durableChangeSet,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
permissionResult,
|
|
17
|
+
...(selection.contextResetMode ? { contextResetMode: selection.contextResetMode } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { USAGE_LIMIT_ERROR_PREFIXES, } from "../../claude-agent-sdk/sdk.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { AIR_SESSION_FAILURE_CAPABILITY, airCapabilityMeta, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
4
|
+
export function airSessionFailureCapabilityMeta(...additionalCapabilities) {
|
|
5
|
+
return airCapabilityMeta(AIR_SESSION_FAILURE_CAPABILITY, ...additionalCapabilities);
|
|
6
|
+
}
|
|
7
|
+
export function createSessionFailureState() {
|
|
8
|
+
return {
|
|
9
|
+
epoch: randomUUID(),
|
|
10
|
+
revisions: new Map(),
|
|
11
|
+
active: new Map(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const AIR_FAILURE_POLICY = {
|
|
15
|
+
advisory: {
|
|
16
|
+
category: "unknown",
|
|
17
|
+
actions: [],
|
|
18
|
+
fallbackTitle: "Claude reported a notice.",
|
|
19
|
+
},
|
|
20
|
+
auth_required: {
|
|
21
|
+
category: "access",
|
|
22
|
+
actions: ["login"],
|
|
23
|
+
fallbackTitle: "Sign in to continue using Claude.",
|
|
24
|
+
},
|
|
25
|
+
bad_request: {
|
|
26
|
+
category: "request",
|
|
27
|
+
actions: [],
|
|
28
|
+
fallbackTitle: "Claude could not process this request.",
|
|
29
|
+
},
|
|
30
|
+
budget_exhausted: {
|
|
31
|
+
category: "limit",
|
|
32
|
+
actions: ["new_session"],
|
|
33
|
+
fallbackTitle: "This Claude session reached its configured budget.",
|
|
34
|
+
},
|
|
35
|
+
context_exhausted: {
|
|
36
|
+
category: "limit",
|
|
37
|
+
actions: ["new_session"],
|
|
38
|
+
fallbackTitle: "This Claude turn reached its configured limit.",
|
|
39
|
+
},
|
|
40
|
+
internal_error: {
|
|
41
|
+
category: "service",
|
|
42
|
+
actions: ["retry", "new_session"],
|
|
43
|
+
fallbackTitle: "Claude Agent encountered an internal error.",
|
|
44
|
+
},
|
|
45
|
+
overloaded: {
|
|
46
|
+
category: "service",
|
|
47
|
+
actions: ["retry"],
|
|
48
|
+
fallbackTitle: "Claude is temporarily overloaded.",
|
|
49
|
+
},
|
|
50
|
+
provider_error: {
|
|
51
|
+
category: "service",
|
|
52
|
+
actions: ["retry"],
|
|
53
|
+
fallbackTitle: "The model provider reported an error.",
|
|
54
|
+
},
|
|
55
|
+
quota_exhausted: {
|
|
56
|
+
category: "limit",
|
|
57
|
+
actions: [],
|
|
58
|
+
fallbackTitle: "The Claude account has no available quota.",
|
|
59
|
+
},
|
|
60
|
+
rate_limited: {
|
|
61
|
+
category: "limit",
|
|
62
|
+
actions: ["retry"],
|
|
63
|
+
fallbackTitle: "Claude is temporarily rate limited.",
|
|
64
|
+
},
|
|
65
|
+
transport_lost: {
|
|
66
|
+
category: "connection",
|
|
67
|
+
actions: ["new_session"],
|
|
68
|
+
fallbackTitle: "The connection to Claude was lost.",
|
|
69
|
+
},
|
|
70
|
+
worker_shutdown: {
|
|
71
|
+
category: "connection",
|
|
72
|
+
actions: ["new_session"],
|
|
73
|
+
fallbackTitle: "The Claude worker is shutting down.",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export function sessionFailureMeta(failure) {
|
|
77
|
+
return withAirMeta(undefined, AIR_SESSION_FAILURE_CAPABILITY, {
|
|
78
|
+
id: failure.id,
|
|
79
|
+
revision: failure.revision,
|
|
80
|
+
category: failure.category,
|
|
81
|
+
severity: failure.severity,
|
|
82
|
+
title: failure.title,
|
|
83
|
+
...(failure.details ? { details: failure.details } : {}),
|
|
84
|
+
actions: failure.actions,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** `getSessionMessages` deliberately exposes only the API message and strips
|
|
88
|
+
* transcript-level `error` / `isApiErrorMessage` fields. The SDK exports the
|
|
89
|
+
* exact stable prefixes used by its synthetic usage-limit errors, so replay
|
|
90
|
+
* can recover this one typed failure without matching arbitrary model prose. */
|
|
91
|
+
export function assistantMessageText(apiMessage) {
|
|
92
|
+
if (!apiMessage || typeof apiMessage !== "object")
|
|
93
|
+
return undefined;
|
|
94
|
+
const { content } = apiMessage;
|
|
95
|
+
if (typeof content === "string")
|
|
96
|
+
return content || undefined;
|
|
97
|
+
if (!Array.isArray(content))
|
|
98
|
+
return undefined;
|
|
99
|
+
const text = content
|
|
100
|
+
.map((block) => block &&
|
|
101
|
+
typeof block === "object" &&
|
|
102
|
+
"type" in block &&
|
|
103
|
+
block.type === "text" &&
|
|
104
|
+
"text" in block &&
|
|
105
|
+
typeof block.text === "string"
|
|
106
|
+
? block.text
|
|
107
|
+
: "")
|
|
108
|
+
.join("");
|
|
109
|
+
return text || undefined;
|
|
110
|
+
}
|
|
111
|
+
export function isSyntheticUsageLimitMessage(apiMessage) {
|
|
112
|
+
if (!apiMessage || typeof apiMessage !== "object")
|
|
113
|
+
return false;
|
|
114
|
+
const { model } = apiMessage;
|
|
115
|
+
if (model !== "<synthetic>")
|
|
116
|
+
return false;
|
|
117
|
+
const text = assistantMessageText(apiMessage);
|
|
118
|
+
if (!text)
|
|
119
|
+
return false;
|
|
120
|
+
return USAGE_LIMIT_ERROR_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
121
|
+
}
|
|
122
|
+
export function activeUsageLimitMessage(messages) {
|
|
123
|
+
let active;
|
|
124
|
+
for (const message of messages) {
|
|
125
|
+
if (message.type !== "assistant" || message.parent_tool_use_id !== null)
|
|
126
|
+
continue;
|
|
127
|
+
if (isSyntheticUsageLimitMessage(message.message)) {
|
|
128
|
+
const title = assistantMessageText(message.message);
|
|
129
|
+
if (title)
|
|
130
|
+
active = { uuid: message.uuid, title };
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const model = message.message && typeof message.message === "object" && "model" in message.message
|
|
134
|
+
? message.message.model
|
|
135
|
+
: undefined;
|
|
136
|
+
// A later real model answer proves the account recovered. Synthetic
|
|
137
|
+
// local-command/interruption messages do not: they can be appended while
|
|
138
|
+
// the provider remains unavailable.
|
|
139
|
+
if (model !== undefined && model !== "<synthetic>")
|
|
140
|
+
active = undefined;
|
|
141
|
+
}
|
|
142
|
+
return active;
|
|
143
|
+
}
|
|
144
|
+
export function supportsAirSessionFailures(capabilities) {
|
|
145
|
+
return clientSupportsAirCapability(capabilities, AIR_SESSION_FAILURE_CAPABILITY);
|
|
146
|
+
}
|
|
147
|
+
function sessionFailureRecoveryPolicy(kind, turnId) {
|
|
148
|
+
switch (kind) {
|
|
149
|
+
case "advisory":
|
|
150
|
+
return "never";
|
|
151
|
+
case "auth_required":
|
|
152
|
+
return "auth_status";
|
|
153
|
+
case "transport_lost":
|
|
154
|
+
case "worker_shutdown":
|
|
155
|
+
return "runtime_rebind";
|
|
156
|
+
case "quota_exhausted":
|
|
157
|
+
return "real_model_success";
|
|
158
|
+
default:
|
|
159
|
+
return turnId ? "next_attempt" : "real_model_success";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Owns the AIR failure lifecycle for both the live consumer and history replay.
|
|
163
|
+
* Published revisions become active only after delivery succeeds; recovery only
|
|
164
|
+
* removes internal active state because transcript records are historical. */
|
|
165
|
+
export class SessionFailureController {
|
|
166
|
+
sessionId;
|
|
167
|
+
state;
|
|
168
|
+
capabilities;
|
|
169
|
+
isCurrent;
|
|
170
|
+
sendUpdate;
|
|
171
|
+
logger;
|
|
172
|
+
constructor(options) {
|
|
173
|
+
this.sessionId = options.sessionId;
|
|
174
|
+
this.state = options.state;
|
|
175
|
+
this.capabilities = options.capabilities;
|
|
176
|
+
this.isCurrent = options.isCurrent;
|
|
177
|
+
this.sendUpdate = options.sendUpdate;
|
|
178
|
+
this.logger = options.logger;
|
|
179
|
+
}
|
|
180
|
+
isSupported() {
|
|
181
|
+
return supportsAirSessionFailures(this.capabilities);
|
|
182
|
+
}
|
|
183
|
+
async emit(failure) {
|
|
184
|
+
if (!this.isCurrent()) {
|
|
185
|
+
this.logger.error(`Session ${this.sessionId}: ignored AIR session failure from a stale consumer`);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await this.sendUpdate({
|
|
190
|
+
sessionId: this.sessionId,
|
|
191
|
+
update: {
|
|
192
|
+
sessionUpdate: "session_info_update",
|
|
193
|
+
_meta: sessionFailureMeta(failure),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.logger.error(`Session ${this.sessionId}: failed to publish AIR session failure: ${error}`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
recordActive(failure) {
|
|
204
|
+
this.state.revisions.set(failure.id, failure.revision);
|
|
205
|
+
this.state.active.set(failure.id, failure);
|
|
206
|
+
this.state.lastNotice =
|
|
207
|
+
failure.kind === "advisory" ? { title: failure.title, id: failure.id } : undefined;
|
|
208
|
+
}
|
|
209
|
+
async clear(shouldClear = () => true) {
|
|
210
|
+
if (!this.isSupported())
|
|
211
|
+
return true;
|
|
212
|
+
this.state.lastNotice = undefined;
|
|
213
|
+
for (const failure of [...this.state.active.values()]) {
|
|
214
|
+
if (!shouldClear(failure))
|
|
215
|
+
continue;
|
|
216
|
+
this.state.active.delete(failure.id);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
async prepare(kind, failureOptions = {}) {
|
|
221
|
+
if (!this.isSupported() || !this.isCurrent())
|
|
222
|
+
return undefined;
|
|
223
|
+
const policy = AIR_FAILURE_POLICY[kind];
|
|
224
|
+
const title = failureOptions.title || policy.fallbackTitle;
|
|
225
|
+
if (kind === "advisory") {
|
|
226
|
+
const id = this.state.lastNotice?.title === title
|
|
227
|
+
? this.state.lastNotice.id
|
|
228
|
+
: `${this.sessionId}:notice:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
|
|
229
|
+
if (this.state.lastNotice?.title !== title) {
|
|
230
|
+
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
235
|
+
kind,
|
|
236
|
+
category: policy.category,
|
|
237
|
+
severity: "warning",
|
|
238
|
+
title,
|
|
239
|
+
...(failureOptions.details ? { details: failureOptions.details } : {}),
|
|
240
|
+
actions: policy.actions,
|
|
241
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const id = failureOptions.turnId && !failureOptions.sessionScoped
|
|
245
|
+
? `${failureOptions.turnId}:error`
|
|
246
|
+
: `${this.sessionId}:session-error:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
|
|
247
|
+
if (!failureOptions.turnId || failureOptions.sessionScoped) {
|
|
248
|
+
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
id,
|
|
252
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
253
|
+
kind,
|
|
254
|
+
category: policy.category,
|
|
255
|
+
severity: failureOptions.severity ?? "error",
|
|
256
|
+
title,
|
|
257
|
+
...(failureOptions.details ? { details: failureOptions.details } : {}),
|
|
258
|
+
actions: failureOptions.severity === "warning" ? [] : policy.actions,
|
|
259
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind, failureOptions.turnId && !failureOptions.sessionScoped ? failureOptions.turnId : undefined),
|
|
260
|
+
...(failureOptions.turnId && !failureOptions.sessionScoped
|
|
261
|
+
? { turnId: failureOptions.turnId }
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async publish(kind, failureOptions = {}) {
|
|
266
|
+
const failure = await this.prepare(kind, failureOptions);
|
|
267
|
+
if (!failure)
|
|
268
|
+
return;
|
|
269
|
+
if (await this.emit(failure))
|
|
270
|
+
this.recordActive(failure);
|
|
271
|
+
}
|
|
272
|
+
async restore(id, kind, title, active = true) {
|
|
273
|
+
if (!this.isSupported() || !this.isCurrent())
|
|
274
|
+
return false;
|
|
275
|
+
const policy = AIR_FAILURE_POLICY[kind];
|
|
276
|
+
const failure = {
|
|
277
|
+
id,
|
|
278
|
+
revision: (this.state.revisions.get(id) ?? 0) + 1,
|
|
279
|
+
kind,
|
|
280
|
+
category: policy.category,
|
|
281
|
+
severity: "error",
|
|
282
|
+
title,
|
|
283
|
+
actions: policy.actions,
|
|
284
|
+
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
|
|
285
|
+
};
|
|
286
|
+
if (!(await this.emit(failure)))
|
|
287
|
+
return false;
|
|
288
|
+
this.state.revisions.set(failure.id, failure.revision);
|
|
289
|
+
if (active) {
|
|
290
|
+
this.state.active.set(failure.id, failure);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
this.state.active.delete(failure.id);
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export function providerFailureCategory(errorKind, isUsageLimit = false) {
|
|
299
|
+
if (isUsageLimit)
|
|
300
|
+
return "quota_exhausted";
|
|
301
|
+
switch (errorKind) {
|
|
302
|
+
case "authentication_failed":
|
|
303
|
+
case "oauth_org_not_allowed":
|
|
304
|
+
return "auth_required";
|
|
305
|
+
case "billing_error":
|
|
306
|
+
case "account_on_hold":
|
|
307
|
+
return "quota_exhausted";
|
|
308
|
+
case "rate_limit":
|
|
309
|
+
return "rate_limited";
|
|
310
|
+
case "overloaded":
|
|
311
|
+
return "overloaded";
|
|
312
|
+
case "invalid_request":
|
|
313
|
+
case "model_not_found":
|
|
314
|
+
return "bad_request";
|
|
315
|
+
case "max_output_tokens":
|
|
316
|
+
return "context_exhausted";
|
|
317
|
+
case "server_error":
|
|
318
|
+
case "unknown":
|
|
319
|
+
case undefined:
|
|
320
|
+
return "provider_error";
|
|
321
|
+
default:
|
|
322
|
+
return "provider_error";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
];
|
|
216
|
+
if (ALLOW_BYPASS) {
|
|
217
|
+
modes.push({
|
|
218
|
+
id: "bypassPermissions",
|
|
219
|
+
name: "Bypass permissions",
|
|
220
|
+
description: "Accepts all permissions",
|
|
221
|
+
_meta: { kind: "full_access" },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return modes;
|
|
225
|
+
}
|
|
226
|
+
async trySyncMode(query, mode, errorMessage) {
|
|
227
|
+
try {
|
|
228
|
+
await query.setPermissionMode(mode);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.options.logError(errorMessage, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|