@bivy/bivy 0.18.11-staging.16 → 0.18.11-staging.3
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/bin/acp-shim.mjs +16 -142
- package/bin/agent-manifest.json +2 -2
- package/bin/bivy.mjs +22 -172
- package/bin/prune-sessions.mjs +0 -38
- package/dist/agent-instructions.js +0 -4
- package/dist/agents/claude-code/integration.js +1 -1
- package/dist/agents/claude-code/runtime.js +0 -15
- package/dist/agents/codex/integration.js +1 -1
- package/dist/agents/profiles.js +3 -3
- package/dist/apps/styles.css +1 -26
- package/dist/certification/generated.js +4 -4
- package/dist/control-plane-tasks.js +1 -6
- package/dist/controllers/fork-commands.js +20 -20
- package/dist/exec.js +4 -41
- package/dist/run-tools.js +2 -15
- package/dist/runtime/protocol.js +4 -60
- package/dist/runtime/tool-call-map.js +0 -24
- package/dist/server.js +25 -289
- package/dist/session/event-log.js +3 -61
- package/dist/session/fork-standup.js +0 -5
- package/dist/session/fork.js +1 -5
- package/dist/session/sibling-client.js +3 -40
- package/dist/session/transcript-merge.js +1 -29
- package/dist/session/transcript-persistence.js +0 -4
- package/package.json +2 -2
- package/dist/runtime/model-ref.js +0 -33
- package/dist/session/delegation-cards.js +0 -28
- package/dist/session/delegations.js +0 -21
package/bin/acp-shim.mjs
CHANGED
|
@@ -136,31 +136,9 @@ let initialized = false;
|
|
|
136
136
|
let modelConfigId = "model";
|
|
137
137
|
// A model chosen before the ACP session existed, applied once it does.
|
|
138
138
|
let pendingModel = null;
|
|
139
|
-
// True while session/load replays history (see session.resume).
|
|
140
|
-
let replayingHistory = false;
|
|
141
|
-
// The config-option id carrying reasoning effort (ACP category "thought_level"),
|
|
142
|
-
// when the agent advertises one.
|
|
143
|
-
let thoughtConfigId = null;
|
|
144
139
|
// toolCallId -> { requestId, options } so a later bivy tool.decision answers the
|
|
145
140
|
// right ACP permission request with a concrete optionId.
|
|
146
141
|
const permissionRequests = new Map();
|
|
147
|
-
// Calls the human (or policy) already approved. An agent that performs an
|
|
148
|
-
// approved edit through fs/write_text_file (Grok does) must not be gated — and
|
|
149
|
-
// carded — a second time for the same file.
|
|
150
|
-
const approvedCalls = new Set();
|
|
151
|
-
|
|
152
|
-
/** The approved, still-running call whose inputs name `file`, if any. */
|
|
153
|
-
function approvedCallWriting(file) {
|
|
154
|
-
for (const id of approvedCalls) {
|
|
155
|
-
const state = toolCallState.get(id);
|
|
156
|
-
if (!state) { approvedCalls.delete(id); continue; }
|
|
157
|
-
const input = toolCallInput(state);
|
|
158
|
-
const paths = [input.path, input.file_path, input.filePath, ...(state.locations || []).map((l) => l?.path)].filter(Boolean);
|
|
159
|
-
const root = fs.realpathSync(cwd);
|
|
160
|
-
if (paths.some((p) => path.resolve(root, String(p)) === file || path.resolve(cwd, String(p)) === file)) return id;
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
142
|
const sandboxTier = process.env.BIVY_ACP_SANDBOX || "workspace-write";
|
|
165
143
|
|
|
166
144
|
function isWithin(root, candidate) {
|
|
@@ -212,9 +190,6 @@ function finishTurnDone() {
|
|
|
212
190
|
turnDrainTimer = null;
|
|
213
191
|
if (!turnDraining) return;
|
|
214
192
|
turnDraining = false;
|
|
215
|
-
ownCalls.clear();
|
|
216
|
-
childParent.clear();
|
|
217
|
-
approvedCalls.clear();
|
|
218
193
|
bivy({ type: "session.status", status: "idle" });
|
|
219
194
|
bivy({ type: "session.done" });
|
|
220
195
|
}
|
|
@@ -247,11 +222,6 @@ function mergeToolCallState(toolCallId, u) {
|
|
|
247
222
|
const next = {
|
|
248
223
|
kind: u.kind ?? prev.kind,
|
|
249
224
|
title: u.title ?? prev.title,
|
|
250
|
-
// Agents open a call titled with the tool's own name ("task",
|
|
251
|
-
// "spawn_subagent", "bash") and later retitle it with prose ("Count README
|
|
252
|
-
// lines"). Keep the first identifier-like title as the stable name, so a
|
|
253
|
-
// delegation still classifies as one after the retitle.
|
|
254
|
-
name: prev.name ?? (typeof u.title === "string" && /^[A-Za-z_][\w.:-]*$/.test(u.title) ? u.title : undefined),
|
|
255
225
|
rawInput: u.rawInput && typeof u.rawInput === "object" && Object.keys(u.rawInput).length ? u.rawInput : prev.rawInput,
|
|
256
226
|
locations: Array.isArray(u.locations) && u.locations.length ? u.locations : prev.locations,
|
|
257
227
|
content: content && content.length ? content : prev.content,
|
|
@@ -289,60 +259,7 @@ function toolCallInput(state) {
|
|
|
289
259
|
* both the node's bucket classifier and the client's own name-based heuristic,
|
|
290
260
|
* which both expect short tool-name-like tokens, not prose. */
|
|
291
261
|
function toolCallName(state) {
|
|
292
|
-
return (state.kind && KIND_TOOL_NAME[state.kind]) || state.
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// --- sub-agent sessions ------------------------------------------------------
|
|
296
|
-
// Agents that run sub-agents as ACP sessions of their own (Grok's
|
|
297
|
-
// spawn_subagent) stream the child's updates over this same connection, tagged
|
|
298
|
-
// with the CHILD's sessionId. Folding those into the parent turn glued the
|
|
299
|
-
// sub-agent's prose onto the parent's answer and listed its tools as the
|
|
300
|
-
// parent's. Route them instead: child tool calls carry `parentToolCallId` (the
|
|
301
|
-
// delegating call, so the transcript nests them) and child prose stays out of
|
|
302
|
-
// the parent reply — the delegation's own tool result carries its answer.
|
|
303
|
-
const childParent = new Map(); // child sessionId -> delegating toolCallId ("" if unknown)
|
|
304
|
-
// This turn's own (non-child) calls, kept after they complete: a background
|
|
305
|
-
// sub-agent's spawn call often finishes before the child's first update lands.
|
|
306
|
-
const ownCalls = new Map(); // toolCallId -> accumulated state
|
|
307
|
-
const DELEGATION_NAME = /agent|task|delegat/i;
|
|
308
|
-
|
|
309
|
-
function isDelegation(state) {
|
|
310
|
-
const input = state?.rawInput || {};
|
|
311
|
-
return DELEGATION_NAME.test(String(state?.name || "")) || typeof input.subagent_type === "string" || typeof input.prompt === "string";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** The delegating call for a child session: an explicit link when the agent
|
|
315
|
-
* sent one (see linkChildSession), else the newest delegation-shaped call of
|
|
316
|
-
* this turn that no other child has claimed, else the newest open call. */
|
|
317
|
-
function parentCallFor(childSessionId) {
|
|
318
|
-
if (childParent.has(childSessionId)) return childParent.get(childSessionId);
|
|
319
|
-
const claimed = new Set(childParent.values());
|
|
320
|
-
const calls = [...ownCalls].reverse().filter(([id]) => !claimed.has(id));
|
|
321
|
-
const pick = calls.find(([, st]) => isDelegation(st)) ?? calls.find(([id]) => toolCallState.has(id));
|
|
322
|
-
const parent = pick?.[0] ?? "";
|
|
323
|
-
childParent.set(childSessionId, parent);
|
|
324
|
-
return parent;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** An agent-specific notification that names a child session and its parent
|
|
328
|
-
* (`child_session_id` / `parent_session_id`, plus the tool call id or the
|
|
329
|
-
* delegation's description) pins the link instead of the heuristic above. */
|
|
330
|
-
function linkChildSession(update) {
|
|
331
|
-
const child = update?.child_session_id ?? update?.childSessionId;
|
|
332
|
-
const parentSession = update?.parent_session_id ?? update?.parentSessionId;
|
|
333
|
-
if (!child || (parentSession && parentSession !== sessionId) || childParent.get(child)) return;
|
|
334
|
-
const byId = update.tool_call_id ?? update.toolCallId;
|
|
335
|
-
const match = byId && ownCalls.has(byId)
|
|
336
|
-
? byId
|
|
337
|
-
: [...ownCalls].reverse().find(([, st]) => update.description && (st.title === update.description || st.rawInput?.description === update.description))?.[0];
|
|
338
|
-
if (match) childParent.set(child, match);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Routing for an update from `sid`: null for this session, else the parent
|
|
342
|
-
* call id ("" when it can't be placed). */
|
|
343
|
-
function childRoute(sid) {
|
|
344
|
-
if (!sid || !sessionId || sid === sessionId) return null;
|
|
345
|
-
return parentCallFor(sid);
|
|
262
|
+
return (state.kind && KIND_TOOL_NAME[state.kind]) || state.title || state.kind || "tool";
|
|
346
263
|
}
|
|
347
264
|
|
|
348
265
|
async function ensureInitialized() {
|
|
@@ -374,33 +291,24 @@ function publishModels(result) {
|
|
|
374
291
|
// ACP model ids are `provider/model`; split the provider so Bivy can group and
|
|
375
292
|
// scope provider-specific settings the same way it does for other runtimes.
|
|
376
293
|
.map((m) => ({ ...m, provider: m.id.includes("/") ? m.id.split("/")[0] : "agent" }));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const thoughtOption = options.find((o) => String(o?.category ?? "") === "thought_level");
|
|
380
|
-
const levels = (Array.isArray(thoughtOption?.options) ? thoughtOption.options : []).map((o) => String(o?.value ?? "")).filter(Boolean);
|
|
381
|
-
if (!models.length && !levels.length) return;
|
|
382
|
-
if (models.length) modelConfigId = String(modelOption?.id ?? "model");
|
|
383
|
-
if (levels.length) thoughtConfigId = String(thoughtOption.id);
|
|
294
|
+
if (!models.length) return;
|
|
295
|
+
modelConfigId = String(modelOption?.id ?? "model");
|
|
384
296
|
bivy({
|
|
385
297
|
type: "runtime.models",
|
|
386
298
|
models,
|
|
387
299
|
...(modelOption?.currentValue ? { currentModel: String(modelOption.currentValue) } : {}),
|
|
388
|
-
...(levels.length ? { thinking: { levels, ...(thoughtOption?.currentValue ? { current: String(thoughtOption.currentValue) } : {}) } } : {}),
|
|
389
300
|
});
|
|
390
301
|
}
|
|
391
302
|
|
|
392
303
|
// --- ACP → bivy: streamed session/update notifications ----------------------
|
|
393
304
|
function onSessionUpdate(params) {
|
|
394
305
|
const u = params?.update;
|
|
395
|
-
if (!u || typeof u !== "object"
|
|
306
|
+
if (!u || typeof u !== "object") return;
|
|
396
307
|
// Any update arriving while the turn is draining means the agent is still
|
|
397
308
|
// emitting the tail of this turn (see the drain note above) — reset the quiet
|
|
398
309
|
// window so session.done waits for it instead of sealing history short.
|
|
399
310
|
if (turnDraining) scheduleTurnDone();
|
|
400
311
|
const kind = String(u.sessionUpdate || "");
|
|
401
|
-
const parentCall = childRoute(params?.sessionId);
|
|
402
|
-
const child = parentCall !== null;
|
|
403
|
-
const nest = parentCall ? { parentToolCallId: parentCall } : {};
|
|
404
312
|
const textOf = (content) => {
|
|
405
313
|
if (!content) return "";
|
|
406
314
|
if (typeof content === "string") return content;
|
|
@@ -419,12 +327,12 @@ function onSessionUpdate(params) {
|
|
|
419
327
|
switch (kind) {
|
|
420
328
|
case "agent_message_chunk": {
|
|
421
329
|
const t = textOf(u.content);
|
|
422
|
-
if (t
|
|
330
|
+
if (t) bivy({ type: "message.delta", text: t });
|
|
423
331
|
break;
|
|
424
332
|
}
|
|
425
333
|
case "agent_thought_chunk": {
|
|
426
334
|
const t = textOf(u.content);
|
|
427
|
-
if (t
|
|
335
|
+
if (t) bivy({ type: "message.reasoning", text: t });
|
|
428
336
|
break;
|
|
429
337
|
}
|
|
430
338
|
case "tool_call": {
|
|
@@ -432,14 +340,12 @@ function onSessionUpdate(params) {
|
|
|
432
340
|
// shows the action; the result arrives via tool_call_update.
|
|
433
341
|
const toolCallId = String(u.toolCallId ?? u.id ?? "");
|
|
434
342
|
const state = mergeToolCallState(toolCallId, u);
|
|
435
|
-
|
|
436
|
-
bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...nest });
|
|
343
|
+
bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
437
344
|
break;
|
|
438
345
|
}
|
|
439
346
|
case "tool_call_update": {
|
|
440
347
|
const toolCallId = String(u.toolCallId ?? u.id ?? "");
|
|
441
348
|
const state = mergeToolCallState(toolCallId, u);
|
|
442
|
-
if (!child && ownCalls.has(toolCallId)) ownCalls.set(toolCallId, state);
|
|
443
349
|
const status = String(u.status || "");
|
|
444
350
|
if (status === "completed" || status === "failed") {
|
|
445
351
|
toolCallState.delete(toolCallId);
|
|
@@ -456,18 +362,17 @@ function onSessionUpdate(params) {
|
|
|
456
362
|
// the terminal frame's content when it has some, else the accumulated.
|
|
457
363
|
result: textOf(u.content) || textOf(state.content) || status,
|
|
458
364
|
isError: status === "failed",
|
|
459
|
-
...nest,
|
|
460
365
|
});
|
|
461
366
|
} else {
|
|
462
367
|
// Still running: forward the fuller name/input as it fills in so a live
|
|
463
368
|
// tool card isn't stuck with the sparse initial notification.
|
|
464
|
-
bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state)
|
|
369
|
+
bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
465
370
|
}
|
|
466
371
|
break;
|
|
467
372
|
}
|
|
468
373
|
case "plan":
|
|
469
374
|
// Optional planning stream — fold into reasoning so nothing is lost.
|
|
470
|
-
if (
|
|
375
|
+
if (Array.isArray(u.entries)) bivy({ type: "message.reasoning", text: u.entries.map((e) => `• ${e.content ?? ""}`).join("\n") });
|
|
471
376
|
break;
|
|
472
377
|
default:
|
|
473
378
|
break;
|
|
@@ -486,9 +391,7 @@ async function onAgentRequest(id, method, params) {
|
|
|
486
391
|
const options = Array.isArray(params?.options) ? params.options : [];
|
|
487
392
|
permissionRequests.set(toolCallId, { requestId: id, options });
|
|
488
393
|
const state = mergeToolCallState(toolCallId, tc);
|
|
489
|
-
|
|
490
|
-
if (parentCall === null) ownCalls.set(toolCallId, state);
|
|
491
|
-
bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...(parentCall ? { parentToolCallId: parentCall } : {}) });
|
|
394
|
+
bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
492
395
|
return;
|
|
493
396
|
}
|
|
494
397
|
case "fs/read_text_file": {
|
|
@@ -510,15 +413,9 @@ async function onAgentRequest(id, method, params) {
|
|
|
510
413
|
try {
|
|
511
414
|
if (sandboxTier === "read-only") throw new Error("writes are disabled by the read-only sandbox");
|
|
512
415
|
const file = workspacePath(params?.path, { write: true });
|
|
513
|
-
if (approvedCallWriting(file)) {
|
|
514
|
-
fs.writeFileSync(file, String(params?.content ?? ""));
|
|
515
|
-
agentReply(id, {});
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
416
|
const toolCallId = `acp-fs-write-${id}`;
|
|
519
417
|
permissionRequests.set(toolCallId, { kind: "fs-write", requestId: id, file, content: String(params?.content ?? "") });
|
|
520
|
-
|
|
521
|
-
bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file }, ...(parentCall ? { parentToolCallId: parentCall } : {}) });
|
|
418
|
+
bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file } });
|
|
522
419
|
} catch (e) {
|
|
523
420
|
agentReplyError(id, -32000, `write failed: ${e.message}`);
|
|
524
421
|
}
|
|
@@ -552,7 +449,6 @@ createInterface({ input: agent.stdout }).on("line", (line) => {
|
|
|
552
449
|
if (msg.id !== undefined && msg.method) { void onAgentRequest(msg.id, msg.method, msg.params); return; }
|
|
553
450
|
// Notification (method, no id).
|
|
554
451
|
if (msg.method === "session/update") onSessionUpdate(msg.params);
|
|
555
|
-
else if (msg.params?.update) linkChildSession(msg.params.update);
|
|
556
452
|
});
|
|
557
453
|
|
|
558
454
|
/**
|
|
@@ -611,14 +507,7 @@ async function onBivyCommand(msg) {
|
|
|
611
507
|
// hanging the reopen with no watchdog to recover it (resume isn't a
|
|
612
508
|
// "working" turn). On timeout/failure we fall back to a fresh session so
|
|
613
509
|
// the chat still opens instead of spinning on "Fetching transcript…".
|
|
614
|
-
|
|
615
|
-
// session/load. Bivy already has that history; forwarding the replay
|
|
616
|
-
// re-recorded past thinking (and could fold old text onto the last
|
|
617
|
-
// reply) as if it were live.
|
|
618
|
-
replayingHistory = true;
|
|
619
|
-
let res;
|
|
620
|
-
try { res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 }); }
|
|
621
|
-
finally { replayingHistory = false; }
|
|
510
|
+
const res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 });
|
|
622
511
|
sessionId = res?.sessionId ?? ref;
|
|
623
512
|
publishModels(res);
|
|
624
513
|
} catch (error) {
|
|
@@ -665,22 +554,16 @@ async function onBivyCommand(msg) {
|
|
|
665
554
|
permissionRequests.delete(msg.toolCallId);
|
|
666
555
|
const allow = msg.decision !== "deny";
|
|
667
556
|
if (entry.kind === "fs-write") {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
try { fs.writeFileSync(entry.file, entry.content); }
|
|
673
|
-
catch (e) { error = `write failed: ${e.message}`; }
|
|
557
|
+
if (!allow) agentReplyError(entry.requestId, -32001, String(msg.reason || "write denied by Bivy policy"));
|
|
558
|
+
else {
|
|
559
|
+
try { fs.writeFileSync(entry.file, entry.content); agentReply(entry.requestId, {}); }
|
|
560
|
+
catch (e) { agentReplyError(entry.requestId, -32000, `write failed: ${e.message}`); }
|
|
674
561
|
}
|
|
675
|
-
if (error) agentReplyError(entry.requestId, allow ? -32000 : -32001, error);
|
|
676
|
-
else agentReply(entry.requestId, {});
|
|
677
|
-
bivy({ type: "tool.result", toolCallId: msg.toolCallId, name: "write", result: error || `Wrote ${entry.file}`, isError: Boolean(error) });
|
|
678
562
|
return;
|
|
679
563
|
}
|
|
680
564
|
// Pick an ACP option matching the human's choice by its `kind`
|
|
681
565
|
// (allow_once/allow_always vs reject_once/reject_always); fall back to the
|
|
682
566
|
// first option, or a cancelled outcome when nothing fits.
|
|
683
|
-
if (allow) approvedCalls.add(msg.toolCallId);
|
|
684
567
|
const want = allow ? /^allow/ : /^reject/;
|
|
685
568
|
const opt = entry.options.find((o) => want.test(String(o.kind || ""))) ?? entry.options[0];
|
|
686
569
|
if (opt && opt.optionId !== undefined) agentReply(entry.requestId, { outcome: { outcome: "selected", optionId: opt.optionId } });
|
|
@@ -701,15 +584,6 @@ async function onBivyCommand(msg) {
|
|
|
701
584
|
bivy({ replyTo: id, ok: true });
|
|
702
585
|
return;
|
|
703
586
|
}
|
|
704
|
-
case "thinking.set": {
|
|
705
|
-
const level = String(msg.level ?? "").trim();
|
|
706
|
-
if (sessionId && thoughtConfigId && level) {
|
|
707
|
-
try { await agentRequest("session/set_config_option", { sessionId, configId: thoughtConfigId, value: level }, { timeoutMs: 15_000 }); }
|
|
708
|
-
catch (e) { bivy({ type: "runtime.debug", message: `acp set thought level failed: ${e instanceof Error ? e.message : String(e)}` }); }
|
|
709
|
-
}
|
|
710
|
-
if (id !== undefined) bivy({ replyTo: id, ok: true });
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
587
|
case "session.abort": {
|
|
714
588
|
// A pending drain must not fire session.done after the user cancelled —
|
|
715
589
|
// the turn is being torn down, not finishing on its own.
|
package/bin/agent-manifest.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"hidden": false,
|
|
23
23
|
"supportTier": "supported",
|
|
24
24
|
"certification": "release-tested",
|
|
25
|
-
"testedVersion": "1.18.
|
|
25
|
+
"testedVersion": "1.18.32",
|
|
26
26
|
"headlessFlags": [
|
|
27
27
|
"run",
|
|
28
28
|
"-s"
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"hidden": false,
|
|
192
192
|
"supportTier": "supported",
|
|
193
193
|
"certification": "release-tested",
|
|
194
|
-
"testedVersion": "1.0.
|
|
194
|
+
"testedVersion": "1.0.41",
|
|
195
195
|
"headlessFlags": [
|
|
196
196
|
"--always-approve",
|
|
197
197
|
"-p",
|
package/bin/bivy.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
35
35
|
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash, verify as cryptoVerify } from "node:crypto";
|
|
36
36
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
37
|
import vm from "node:vm";
|
|
38
|
-
import {
|
|
38
|
+
import { selectStaleSessions, sessionActivityMs } from "./prune-sessions.mjs";
|
|
39
39
|
import { nativeResumeRef, resolveSessionsLimit, truncateSavedSessions } from "./sessions-list.mjs";
|
|
40
40
|
import { renderManagedBlock, upsertManagedBlock, removeManagedBlock, rcFileForShell } from "./shim-path.mjs";
|
|
41
41
|
import { removeInstallAndState } from "./uninstall-paths.mjs";
|
|
@@ -1827,7 +1827,7 @@ function cmdCompletions(args = []) {
|
|
|
1827
1827
|
const shell = (args[0] || "").toLowerCase();
|
|
1828
1828
|
const commands = [
|
|
1829
1829
|
"run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
|
|
1830
|
-
"send", "attach", "suggest", "
|
|
1830
|
+
"send", "attach", "suggest", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
|
|
1831
1831
|
"update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
|
|
1832
1832
|
"github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
|
|
1833
1833
|
];
|
|
@@ -2462,131 +2462,6 @@ async function cmdSuggest(args = []) {
|
|
|
2462
2462
|
console.log(c.green("Suggested in the chat. The user can start it in one tap."));
|
|
2463
2463
|
}
|
|
2464
2464
|
|
|
2465
|
-
// Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
|
|
2466
|
-
// self-contained task from this session to another agent, optionally on another
|
|
2467
|
-
// account machine, and get its answer back. Agent-agnostic — any agent with a
|
|
2468
|
-
// shell can call it — and meant for when the user asks for another agent/machine.
|
|
2469
|
-
const DELEGATE_USAGE = `Usage: bivy delegate "<task>" [--agent <id>] [--machine <name>] [--model <model>] [--repo owner/repo] [--approval never|risky|always|autonomous] [--sandbox <tier>] [--wait [seconds]] [--json]
|
|
2470
|
-
bivy delegate "<task>" --to <agent>[@<machine>],<agent>[@<machine>],… [--wait [seconds]]
|
|
2471
|
-
bivy delegate machines [--json]
|
|
2472
|
-
bivy delegate status <run-id> [--json]
|
|
2473
|
-
bivy delegate wait <run-id> [--timeout <seconds>] [--json]
|
|
2474
|
-
|
|
2475
|
-
Hands a self-contained task to another agent (and/or another machine on your account) as a
|
|
2476
|
-
delegated Run tied to this session ($BIVY_SESSION_ID). With --wait (or 'wait'), prints the
|
|
2477
|
-
child's final answer and any branch/PR it produced once it finishes. --to sends the same task
|
|
2478
|
-
to several agents/machines at once (up to 3) so the results can be compared.
|
|
2479
|
-
'machines' lists your machines and the agents installed on each.`;
|
|
2480
|
-
|
|
2481
|
-
async function cmdDelegate(args = []) {
|
|
2482
|
-
if (!args.length || args.some((a) => a === "-h" || a === "--help")) { console.log(DELEGATE_USAGE); return; }
|
|
2483
|
-
const flagsWithValue = new Set(["--session", "--agent", "--machine", "--model", "--repo", "--timeout", "--to", "--approval", "--sandbox"]);
|
|
2484
|
-
// Children inherit this session's approval/sandbox unless set here.
|
|
2485
|
-
const safetyFlags = () => {
|
|
2486
|
-
const safety = { approval: flag("--approval"), sandbox: flag("--sandbox") };
|
|
2487
|
-
return safety.approval || safety.sandbox ? { safety } : {};
|
|
2488
|
-
};
|
|
2489
|
-
const flag = (name) => {
|
|
2490
|
-
const i = args.indexOf(name);
|
|
2491
|
-
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
2492
|
-
};
|
|
2493
|
-
const json = args.includes("--json");
|
|
2494
|
-
const waitIndex = args.indexOf("--wait");
|
|
2495
|
-
const waitArg = waitIndex >= 0 && /^\d+$/.test(args[waitIndex + 1] ?? "") ? args[waitIndex + 1] : undefined;
|
|
2496
|
-
const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithValue.has(args[i - 1])) && !(waitArg && i === waitIndex + 1));
|
|
2497
|
-
const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
|
|
2498
|
-
if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
|
|
2499
|
-
const config = loadConfig();
|
|
2500
|
-
if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
|
|
2501
|
-
let token;
|
|
2502
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2503
|
-
const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
|
|
2504
|
-
const call = async (path, body) => {
|
|
2505
|
-
const res = await fetch(`${base}${path}`, {
|
|
2506
|
-
method: body ? "POST" : "GET",
|
|
2507
|
-
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
2508
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
2509
|
-
});
|
|
2510
|
-
const data = await res.json().catch(() => ({}));
|
|
2511
|
-
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2512
|
-
return data;
|
|
2513
|
-
};
|
|
2514
|
-
// needs_attention = parked by the machine's policy; it won't move on its own.
|
|
2515
|
-
const terminal = (run) => ["succeeded", "failed", "cancelled", "needs_attention"].includes(run?.status);
|
|
2516
|
-
// The node caps one wait at 300s; loop until the child ends or our budget does.
|
|
2517
|
-
const waitFor = async (runId, seconds) => {
|
|
2518
|
-
const deadline = Date.now() + seconds * 1000;
|
|
2519
|
-
let run = await call(`/${encodeURIComponent(runId)}`);
|
|
2520
|
-
while (!terminal(run) && Date.now() < deadline) {
|
|
2521
|
-
const slice = Math.max(1, Math.min(300, Math.ceil((deadline - Date.now()) / 1000)));
|
|
2522
|
-
run = await call(`/${encodeURIComponent(runId)}/wait`, { timeoutSeconds: slice });
|
|
2523
|
-
}
|
|
2524
|
-
return run;
|
|
2525
|
-
};
|
|
2526
|
-
const print = (run) => {
|
|
2527
|
-
if (json) { console.log(JSON.stringify(run, null, 2)); return; }
|
|
2528
|
-
console.log(`Run ${run.runId}: ${run.status}${terminal(run) ? "" : " (still running — 'bivy delegate wait " + run.runId + "')"}`);
|
|
2529
|
-
const refs = run.references || {};
|
|
2530
|
-
if (refs.failure) console.log(`Failure: ${refs.failure}`);
|
|
2531
|
-
if (refs.branch) console.log(`Branch: ${refs.branch}`);
|
|
2532
|
-
if (refs.prUrl) console.log(`PR: ${refs.prUrl}`);
|
|
2533
|
-
if (refs.sessionId) console.log(`Child session: ${refs.sessionId}`);
|
|
2534
|
-
if (run.answer) console.log(`\nAnswer:\n${run.answer}`);
|
|
2535
|
-
};
|
|
2536
|
-
try {
|
|
2537
|
-
const [action, runId] = positional;
|
|
2538
|
-
if (action === "machines" && positional.length === 1) {
|
|
2539
|
-
const res = await fetch(`${url(config)}/api/machines`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
2540
|
-
const data = await res.json().catch(() => ({}));
|
|
2541
|
-
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2542
|
-
if (json) { console.log(JSON.stringify(data.machines, null, 2)); return; }
|
|
2543
|
-
for (const m of data.machines || []) {
|
|
2544
|
-
const agents = m.agents ? m.agents.map((a) => a.id).join(", ") || "none" : m.error ? `unknown (${m.error})` : "unknown while offline";
|
|
2545
|
-
console.log(`${m.name}${m.self ? " (this machine)" : ""} — ${m.online ? "online" : "offline"} — agents: ${agents}`);
|
|
2546
|
-
}
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
if (action === "status" && runId) return print(await call(`/${encodeURIComponent(runId)}`));
|
|
2550
|
-
if (action === "wait" && runId) return print(await waitFor(runId, Number(flag("--timeout") ?? 600)));
|
|
2551
|
-
const instructions = positional.join(" ").trim();
|
|
2552
|
-
if (!instructions) { console.error(c.red(DELEGATE_USAGE)); process.exit(1); return; }
|
|
2553
|
-
// Fan-out: the same task to several agent@machine targets, one group, so the
|
|
2554
|
-
// parent's transcript shows them side by side to compare.
|
|
2555
|
-
const to = flag("--to");
|
|
2556
|
-
if (to) {
|
|
2557
|
-
const targets = to.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
|
|
2558
|
-
const [agent, machine] = t.split("@");
|
|
2559
|
-
return { agent: agent ? governedChatAgentId(agent) : undefined, machine: machine || undefined, label: t };
|
|
2560
|
-
});
|
|
2561
|
-
const group = `fanout-${Date.now().toString(36)}`;
|
|
2562
|
-
const runs = [];
|
|
2563
|
-
for (const target of targets) {
|
|
2564
|
-
runs.push({ target, run: await call("", { instructions, agent: target.agent, machine: target.machine, model: flag("--model"), repo: flag("--repo"), ...safetyFlags(), group }) });
|
|
2565
|
-
}
|
|
2566
|
-
if (waitIndex < 0) {
|
|
2567
|
-
if (json) console.log(JSON.stringify(runs.map((r) => r.run), null, 2));
|
|
2568
|
-
else for (const { target, run } of runs) console.log(`${target.label}: run ${run.runId} (${run.status})`);
|
|
2569
|
-
return;
|
|
2570
|
-
}
|
|
2571
|
-
const finished = await Promise.all(runs.map(async ({ target, run }) => ({ target, run: await waitFor(run.runId, Number(waitArg ?? 600)) })));
|
|
2572
|
-
if (json) { console.log(JSON.stringify(finished.map((f) => f.run), null, 2)); return; }
|
|
2573
|
-
for (const { target, run } of finished) { console.log(`\n── ${target.label}`); print(run); }
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
const agent = flag("--agent");
|
|
2577
|
-
const started = await call("", { instructions, agent: agent ? governedChatAgentId(agent) : undefined, machine: flag("--machine"), model: flag("--model"), repo: flag("--repo"), ...safetyFlags() });
|
|
2578
|
-
if (waitIndex < 0) {
|
|
2579
|
-
if (!json) console.log(`Delegated as run ${started.runId} (${started.status}). Follow it with 'bivy delegate wait ${started.runId}'.`);
|
|
2580
|
-
else print(started);
|
|
2581
|
-
return;
|
|
2582
|
-
}
|
|
2583
|
-
print(await waitFor(started.runId, Number(waitArg ?? 600)));
|
|
2584
|
-
} catch (error) {
|
|
2585
|
-
console.error(c.red(`Delegate failed: ${error?.message || String(error)}`));
|
|
2586
|
-
process.exit(1);
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
2465
|
// Universal app publishing: every agent that can write JSON and run a command
|
|
2591
2466
|
// uses the same contract. Relative static directories are manifest-relative.
|
|
2592
2467
|
async function cmdApp(args = []) {
|
|
@@ -3174,11 +3049,6 @@ ${c.bold("bivy prune")} — remove old sessions, --clone workspaces, and git wor
|
|
|
3174
3049
|
is never pruned while the node is running, regardless of --keep/--older-than —
|
|
3175
3050
|
only idle worktrees are removed. If the node is down, nothing is live to guard.
|
|
3176
3051
|
|
|
3177
|
-
A session and its worktree are removed together: pruning a worktree also
|
|
3178
|
-
removes the sessions that ran in it, pruning a session also removes its
|
|
3179
|
-
worktree (once no remaining session uses it), and sessions whose worktree is
|
|
3180
|
-
already gone are removed — so no session is left pointing at a deleted checkout.
|
|
3181
|
-
|
|
3182
3052
|
Paths (default to the installed node)
|
|
3183
3053
|
--data-dir <dir> node data dir (default: this install's .bivy, or $BIVY_DATA_DIR)
|
|
3184
3054
|
--workspace <dir> also scan this workspace for worktrees (default: configured workspace)
|
|
@@ -3246,41 +3116,12 @@ async function cmdPrune(args = []) {
|
|
|
3246
3116
|
const now = Date.now();
|
|
3247
3117
|
const plan = [];
|
|
3248
3118
|
let worktreeGuard = { reachable: false, protected: 0 };
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
// Worktrees: keep the newest N overall (across all roots), matching the node
|
|
3256
|
-
// prune script. Scan roots: the data dir plus the configured workspace repo.
|
|
3257
|
-
const worktreeRoots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
|
|
3258
|
-
const staleWorktrees = doWorktrees ? selectStale(worktreeRoots.flatMap((r) => pruneListEntries(r, "dir")), keepFor("worktrees"), ageMs, now) : [];
|
|
3259
|
-
// Guard: never delete a worktree a live agent is using. Ask the daemon what's
|
|
3260
|
-
// live (it owns every runtime and PTY); protected worktrees drop out of the
|
|
3261
|
-
// removal set entirely, so an aggressive --older-than can't nuke a running
|
|
3262
|
-
// agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
|
|
3263
|
-
const live = await liveWorktreePaths(config);
|
|
3264
|
-
const isLive = (p) => isLiveWorktree(p, live.paths);
|
|
3265
|
-
const idleWorktrees = staleWorktrees.filter((e) => !isLive(e.path));
|
|
3266
|
-
worktreeGuard = { reachable: live.reachable, protected: staleWorktrees.length - idleWorktrees.length };
|
|
3267
|
-
// A session and its worktree go together: removing one without the other left
|
|
3268
|
-
// sessions pointing at a deleted checkout (or checkouts nobody can reach).
|
|
3269
|
-
const linked = linkSessionWorktrees({
|
|
3270
|
-
sessions: allSessions,
|
|
3271
|
-
staleSessions: selectedSessions,
|
|
3272
|
-
staleWorktrees: idleWorktrees.map((e) => e.path),
|
|
3273
|
-
isLive,
|
|
3274
|
-
exists: (p) => fs.existsSync(p),
|
|
3275
|
-
});
|
|
3276
|
-
const staleSessions = linked.sessions;
|
|
3277
|
-
const worktreeMtime = new Map(idleWorktrees.map((e) => [path.resolve(e.path), e.mtimeMs]));
|
|
3278
|
-
const worktreesToRemove = linked.worktrees.map((p) => {
|
|
3279
|
-
let mtimeMs = worktreeMtime.get(p);
|
|
3280
|
-
if (mtimeMs === undefined) { try { mtimeMs = fs.statSync(p).mtimeMs; } catch { mtimeMs = now; } }
|
|
3281
|
-
return { path: p, mtimeMs };
|
|
3282
|
-
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3283
|
-
if (doSessions || staleSessions.length) {
|
|
3119
|
+
if (doSessions) {
|
|
3120
|
+
// Sessions live in the metadata index (all agents) plus each agent's own
|
|
3121
|
+
// transcript store — not just .bivy/pi/sessions. Select from metadata so
|
|
3122
|
+
// shim/native (Claude Code, Codex, …) sessions are actually covered; the old
|
|
3123
|
+
// pi-sessions-only scan silently no-oped for every terminal-started session.
|
|
3124
|
+
const staleSessions = selectStaleSessions(loadMetadataSessions(dataDir).sessions, keepFor("sessions"), ageMs, now);
|
|
3284
3125
|
plan.push({
|
|
3285
3126
|
kind: "sessions",
|
|
3286
3127
|
root: metadataFilePath(dataDir),
|
|
@@ -3303,8 +3144,20 @@ async function cmdPrune(args = []) {
|
|
|
3303
3144
|
const dir = path.join(dataDir, "workspaces");
|
|
3304
3145
|
plan.push({ kind: "workspaces", root: dir, remove: selectStale(pruneListEntries(dir, "dir"), keepFor("workspaces"), ageMs, now) });
|
|
3305
3146
|
}
|
|
3306
|
-
if (doWorktrees
|
|
3307
|
-
|
|
3147
|
+
if (doWorktrees) {
|
|
3148
|
+
// Keep the newest N worktrees overall (across all roots), matching the node
|
|
3149
|
+
// prune script. Scan roots: the data dir plus the configured workspace repo.
|
|
3150
|
+
const roots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
|
|
3151
|
+
const all = roots.flatMap((r) => pruneListEntries(r, "dir"));
|
|
3152
|
+
const stale = selectStale(all, keepFor("worktrees"), ageMs, now);
|
|
3153
|
+
// Guard: never delete a worktree a live agent is using. Ask the daemon what's
|
|
3154
|
+
// live (it owns every runtime and PTY); protected worktrees drop out of the
|
|
3155
|
+
// removal set entirely, so an aggressive --older-than can't nuke a running
|
|
3156
|
+
// agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
|
|
3157
|
+
const live = await liveWorktreePaths(config);
|
|
3158
|
+
const remove = stale.filter((e) => !isLiveWorktree(e.path, live.paths));
|
|
3159
|
+
worktreeGuard = { reachable: live.reachable, protected: stale.length - remove.length };
|
|
3160
|
+
plan.push({ kind: "worktrees", root: roots.join(", ") || "(none found)", remove });
|
|
3308
3161
|
}
|
|
3309
3162
|
const total = plan.reduce((n, p) => n + p.remove.length, 0);
|
|
3310
3163
|
|
|
@@ -5615,9 +5468,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
5615
5468
|
case "suggest":
|
|
5616
5469
|
await cmdSuggest(args);
|
|
5617
5470
|
break;
|
|
5618
|
-
case "delegate":
|
|
5619
|
-
await cmdDelegate(args);
|
|
5620
|
-
break;
|
|
5621
5471
|
case "completions":
|
|
5622
5472
|
case "completion":
|
|
5623
5473
|
cmdCompletions(args);
|
package/bin/prune-sessions.mjs
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
// not just Pi's `.bivy/pi/sessions`. Prune selects from that index; these helpers
|
|
10
10
|
// decide which entries are stale. No I/O here on purpose.
|
|
11
11
|
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
|
|
14
12
|
// Mirrors the server's isEmptyUntitledSummary: a session with no real title, no
|
|
15
13
|
// first message and no messages is the hidden "Untitled" noise the sidebar filters
|
|
16
14
|
// out. Keep-N protects only *real* sessions; empties are always eligible.
|
|
@@ -32,42 +30,6 @@ export function sessionActivityMs(s) {
|
|
|
32
30
|
return 0;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
// A Bivy-managed worktree lives at <repo>/.bivy/worktrees/<slug>. Only those are
|
|
36
|
-
// ever removed on a session's behalf — never an arbitrary path a record names.
|
|
37
|
-
export function isManagedWorktreePath(p) {
|
|
38
|
-
const parts = path.resolve(String(p ?? "")).split(path.sep);
|
|
39
|
-
return parts.length >= 3 && parts[parts.length - 3] === ".bivy" && parts[parts.length - 2] === "worktrees";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Sessions and their worktrees are pruned as one unit, so neither outlives the
|
|
43
|
-
// other. Selection runs per kind (sessions and worktrees have different keep
|
|
44
|
-
// counts), then this links the two removal sets:
|
|
45
|
-
// - a session goes when its worktree goes, or when its worktree is already
|
|
46
|
-
// gone from disk (a session that can no longer open its checkout);
|
|
47
|
-
// - a worktree goes when every session using it goes (and no live agent holds it).
|
|
48
|
-
// Live ("working") sessions are never added, and their worktree is never removed. `isLive(path)` / `exists(path)` are
|
|
49
|
-
// the caller's I/O; paths are compared resolved. Returns the widened sets.
|
|
50
|
-
export function linkSessionWorktrees({ sessions, staleSessions, staleWorktrees, isLive = () => false, exists = () => true }) {
|
|
51
|
-
const removeSessions = new Map(staleSessions.map((s) => [s.id, s]));
|
|
52
|
-
const removeWorktrees = new Set(staleWorktrees.map((p) => path.resolve(p)));
|
|
53
|
-
const byWorktree = new Map();
|
|
54
|
-
for (const s of sessions) {
|
|
55
|
-
if (!s.worktree || !isManagedWorktreePath(s.worktree)) continue;
|
|
56
|
-
const wt = path.resolve(s.worktree);
|
|
57
|
-
byWorktree.set(wt, [...(byWorktree.get(wt) ?? []), s]);
|
|
58
|
-
}
|
|
59
|
-
for (const [wt, owners] of byWorktree) {
|
|
60
|
-
if (owners.some((s) => s.status === "working")) removeWorktrees.delete(wt);
|
|
61
|
-
else if (exists(wt) && owners.every((s) => removeSessions.has(s.id)) && !isLive(wt)) removeWorktrees.add(wt);
|
|
62
|
-
}
|
|
63
|
-
for (const [wt, owners] of byWorktree) {
|
|
64
|
-
if (!removeWorktrees.has(wt) && exists(wt)) continue;
|
|
65
|
-
for (const s of owners) if (s.status !== "working") removeSessions.set(s.id, s);
|
|
66
|
-
}
|
|
67
|
-
const sorted = [...removeSessions.values()].sort((a, b) => sessionActivityMs(b) - sessionActivityMs(a));
|
|
68
|
-
return { sessions: sorted, worktrees: [...removeWorktrees] };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
33
|
// Removal set for sessions. Live ("working") sessions are never touched. The
|
|
72
34
|
// newest `keep` NON-EMPTY sessions (across every agent) survive; empty/untitled
|
|
73
35
|
// sessions are always beyond the keep window. An item is removed only when it is
|
|
@@ -42,10 +42,6 @@ export const BIVY_AGENT_NOTE = [
|
|
|
42
42
|
'`bivy suggest "<complete instruction>" [--title "short label"]` instead of only listing them. The user can ' +
|
|
43
43
|
"start each in one tap, in this session or in a parallel one that works in its own copy of the project, so " +
|
|
44
44
|
"write it to stand on its own, with paths relative to the project root.",
|
|
45
|
-
"- Only when the user asks for another agent or another of their machines to take part: " +
|
|
46
|
-
'`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
|
|
47
|
-
"its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
|
|
48
|
-
"lists machines and their agents. Use your own sub-agents for everything else.",
|
|
49
45
|
].join("\n");
|
|
50
46
|
const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
|
|
51
47
|
"instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
|
|
@@ -4,7 +4,7 @@ import { defineAgentIntegration } from "../definition.js";
|
|
|
4
4
|
import { withExactCapabilitySurface } from "../../runtime/types.js";
|
|
5
5
|
import { createCredentialStore } from "../../runtime/credentials.js";
|
|
6
6
|
import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled, } from "./runtime.js";
|
|
7
|
-
export const CLAUDE_TESTED_VERSION = "0.3.
|
|
7
|
+
export const CLAUDE_TESTED_VERSION = "0.3.281";
|
|
8
8
|
const CLAUDE_CAPABILITIES = withExactCapabilitySurface({
|
|
9
9
|
toolInterception: true,
|
|
10
10
|
modelSelection: true,
|