@bivy/bivy 0.18.8 → 0.18.9-staging.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/bivy.mjs +43 -39
- package/bin/codex-app-server-shim.mjs +3 -0
- package/dist/agent-bridges.d.mts +26 -0
- package/dist/agent-bridges.mjs +271 -0
- package/dist/agent-instructions.js +103 -0
- package/dist/agents/claude-code/integration.js +1 -1
- package/dist/agents/claude-code/runtime.js +24 -15
- package/dist/agents/pi/integration.js +4 -7
- package/dist/agents/pi/runtime.js +3 -0
- package/dist/agents/profiles.js +3 -0
- package/dist/apps/gateway.js +2 -8
- package/dist/apps/mime.js +16 -0
- package/dist/apps/screenshot.js +4 -1
- package/dist/apps/styles.css +45 -5
- package/dist/harness/mcp-config.js +4 -0
- package/dist/harness/mcp-inject.js +1 -1
- package/dist/harness/mcp-serve-cli.js +14 -1
- package/dist/runtime/agent-service.js +2 -2
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/model-namer.js +2 -0
- package/dist/runtime/pi-oauth.js +2 -0
- package/dist/runtime/process.js +3 -3
- package/dist/runtime/protocol.js +14 -6
- package/dist/runtime/remote.js +2 -2
- package/dist/runtime/session-env.js +18 -2
- package/dist/server.js +72 -6
- package/dist/terminal.js +3 -60
- package/package.json +11 -12
- package/bin/patch-pi-dependencies.mjs +0 -56
- package/bin/repair-pty-permissions.mjs +0 -35
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// * The SDK is loaded with a dynamic import so it stays an *optional*
|
|
14
14
|
// dependency: a Bivy install only needs it when this runtime is selected.
|
|
15
15
|
import { withSessionCredentials, credentialEnvFallback } from "../../credentials/session.js";
|
|
16
|
-
import { createRequire } from "node:module";
|
|
17
16
|
import { randomUUID } from "node:crypto";
|
|
18
17
|
import { EventEmitter } from "node:events";
|
|
19
18
|
import { withExactCapabilitySurface } from "../../runtime/types.js";
|
|
@@ -30,6 +29,7 @@ import { anthropicCredentialPreflight, describeAnthropicError, isAnthropicAuthEr
|
|
|
30
29
|
import { toModelInfo as sharedToModelInfo } from "../../runtime/normalize.js";
|
|
31
30
|
import { hasLiveProcessForCwd } from "../../runtime/native-process-scan.js";
|
|
32
31
|
import { bivySessionEnv } from "../../runtime/session-env.js";
|
|
32
|
+
import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
|
|
33
33
|
/** Binary names a live Claude Code process could be running under (see
|
|
34
34
|
* native-process-scan.ts's best-effort cwd match). */
|
|
35
35
|
const CLAUDE_BIN_NAMES = ["claude"];
|
|
@@ -190,20 +190,15 @@ export function claudeRuntimeFromEnv() {
|
|
|
190
190
|
}
|
|
191
191
|
/** True when `@anthropic-ai/claude-agent-sdk` is resolvable in this install. */
|
|
192
192
|
export function claudeSdkInstalled() {
|
|
193
|
-
|
|
194
|
-
createRequire(import.meta.url).resolve(SDK_PACKAGE);
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
193
|
+
return bridgeInstalled(SDK_PACKAGE);
|
|
200
194
|
}
|
|
201
195
|
async function loadSdk() {
|
|
196
|
+
enableBridgeResolution();
|
|
202
197
|
try {
|
|
203
198
|
return await import(SDK_PACKAGE);
|
|
204
199
|
}
|
|
205
200
|
catch {
|
|
206
|
-
throw new Error(`The claude-code-sdk runtime requires the "${SDK_PACKAGE}"
|
|
201
|
+
throw new Error(`The claude-code-sdk runtime requires the "${SDK_PACKAGE}" bridge. Install it with: bivy agents:install --bridges`);
|
|
207
202
|
}
|
|
208
203
|
}
|
|
209
204
|
/**
|
|
@@ -303,6 +298,9 @@ function extractText(message) {
|
|
|
303
298
|
* handled separately (see INTERRUPT_MARKER) because — unlike these — they carry
|
|
304
299
|
* meaning a human wants to see. */
|
|
305
300
|
const DROP_META_TEXT = /^\s*<(?:task-notification|system-reminder)[\s>/]/;
|
|
301
|
+
/** Re-drives a self-woken turn after a credential reload (see recoverFromAuthError).
|
|
302
|
+
* Wrapped as a system-reminder so DROP_META_TEXT keeps it out of the chat. */
|
|
303
|
+
const AUTH_RECOVERY_CONTINUE = "<system-reminder>Your last request failed because the sign-in expired; it has been refreshed. Continue exactly where you left off.</system-reminder>";
|
|
306
304
|
/** The CLI writes this synthetic user-role marker whenever a turn is aborted —
|
|
307
305
|
* by a real user Stop OR by any teardown of the streaming query (our shutdown,
|
|
308
306
|
* a mid-flight credential reload, a TUI refresh). We don't drop it: we surface
|
|
@@ -885,7 +883,9 @@ class ClaudeSession {
|
|
|
885
883
|
// Kept even when the native tool below is also registered: it's a cheap,
|
|
886
884
|
// harmless fallback for a shell/subprocess the agent spawns that can't
|
|
887
885
|
// reach the in-process MCP tool directly.
|
|
888
|
-
|
|
886
|
+
// The user's account-wide instructions follow it; the repo's CLAUDE.md
|
|
887
|
+
// still loads through the preset as usual.
|
|
888
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: [BIVY_ATTACH_SYSTEM_PROMPT, this.runtimeOptions.instructions].filter(Boolean).join("\n\n") },
|
|
889
889
|
...(this.runtimeOptions.executablePath ? { pathToClaudeCodeExecutable: this.runtimeOptions.executablePath } : {}),
|
|
890
890
|
};
|
|
891
891
|
if (resumeId)
|
|
@@ -1076,15 +1076,19 @@ class ClaudeSession {
|
|
|
1076
1076
|
/** Recover a provider 401 regardless of whether the SDK throws it, emits it as
|
|
1077
1077
|
* assistant text, or puts it in an error result. The latter is how revoked
|
|
1078
1078
|
* OAuth tokens commonly arrive, so only handling consume()'s catch would leave
|
|
1079
|
-
* the turn stopped until the user sent another message.
|
|
1079
|
+
* the turn stopped until the user sent another message.
|
|
1080
|
+
*
|
|
1081
|
+
* A turn the agent woke into on its own (a background task's notification,
|
|
1082
|
+
* with no user prompt in flight) recovers too: its trigger lived in the torn-down
|
|
1083
|
+
* query, so the resumed one is nudged with a hidden continuation instead. */
|
|
1080
1084
|
async recoverFromAuthError(raw) {
|
|
1081
|
-
if (!isAnthropicAuthError(raw) || this.reloadedThisTurn
|
|
1085
|
+
if (!isAnthropicAuthError(raw) || this.reloadedThisTurn)
|
|
1082
1086
|
return false;
|
|
1083
1087
|
this.reloadedThisTurn = true;
|
|
1084
1088
|
if (!(await this.restartWithFreshCredential(this.spawnedToken)))
|
|
1085
1089
|
return false;
|
|
1086
1090
|
this.streaming = true;
|
|
1087
|
-
this.input.push({ type: "user", message: { role: "user", content: this.inFlightPrompt }, parent_tool_use_id: null });
|
|
1091
|
+
this.input.push({ type: "user", message: { role: "user", content: this.inFlightPrompt ?? AUTH_RECOVERY_CONTINUE }, parent_tool_use_id: null });
|
|
1088
1092
|
return true;
|
|
1089
1093
|
}
|
|
1090
1094
|
beginMessage() {
|
|
@@ -1273,6 +1277,11 @@ class ClaudeSession {
|
|
|
1273
1277
|
// Turn completed — the prompt is on disk now, so drop the copy kept for a
|
|
1274
1278
|
// mid-flight reload re-drive (see inFlightPrompt / consume's catch).
|
|
1275
1279
|
this.inFlightPrompt = undefined;
|
|
1280
|
+
// Re-arm the one-reload-per-turn guard only after a clean turn, so the
|
|
1281
|
+
// next (possibly self-woken) turn can recover, while a failed turn that
|
|
1282
|
+
// already reloaded surfaces its error instead of looping.
|
|
1283
|
+
if (!resultError)
|
|
1284
|
+
this.reloadedThisTurn = false;
|
|
1276
1285
|
if (message.subtype && message.subtype !== "success") {
|
|
1277
1286
|
this.emit({ type: "tool_result", error: message.subtype, message: message.result });
|
|
1278
1287
|
}
|
|
@@ -1516,12 +1525,12 @@ export class ClaudeCodeRuntime {
|
|
|
1516
1525
|
return [{ id: "anthropic", name: "Anthropic", oauth: true, models: FALLBACK_MODELS }];
|
|
1517
1526
|
}
|
|
1518
1527
|
async createSession(options) {
|
|
1519
|
-
const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider);
|
|
1528
|
+
const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider);
|
|
1520
1529
|
this.sessions.push(session);
|
|
1521
1530
|
return { session };
|
|
1522
1531
|
}
|
|
1523
1532
|
async openSession(options) {
|
|
1524
|
-
const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
|
|
1533
|
+
const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
|
|
1525
1534
|
this.sessions.push(session);
|
|
1526
1535
|
return {
|
|
1527
1536
|
session,
|
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { defineAgentIntegration } from "../definition.js";
|
|
7
7
|
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
8
|
+
import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
|
|
8
9
|
export const PI_TESTED_VERSION = "0.87.1";
|
|
9
10
|
export function piAgentDir() {
|
|
10
11
|
return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
@@ -18,6 +19,7 @@ export function piCommand() {
|
|
|
18
19
|
* PI_CODING_AGENT_DIR (normally ~/.pi/agent). Takeover discovery must inspect
|
|
19
20
|
* that native store rather than only the governed one. */
|
|
20
21
|
export async function listNativePiSessions() {
|
|
22
|
+
enableBridgeResolution();
|
|
21
23
|
const { SessionManager } = await import("@earendil-works/pi-coding-agent");
|
|
22
24
|
// Do not pass the sessions root here. Pi's default store is split into one
|
|
23
25
|
// encoded subdirectory per cwd, and SessionManager.listAll(customDir) scans
|
|
@@ -40,13 +42,7 @@ export function piCommandAvailable() {
|
|
|
40
42
|
return resolveExecutable(piCommand()) !== null;
|
|
41
43
|
}
|
|
42
44
|
export function piBridgeInstalled() {
|
|
43
|
-
|
|
44
|
-
import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
45
|
+
return bridgeInstalled("@earendil-works/pi-coding-agent");
|
|
50
46
|
}
|
|
51
47
|
function unsupportedNodeMessage() {
|
|
52
48
|
return `Pi requires Node.js 22.19+ (found ${process.version}). Upgrade Node, or select another agent such as Claude Code/Codex/OpenCode.`;
|
|
@@ -67,6 +63,7 @@ export class LazyPiRuntime {
|
|
|
67
63
|
async runtime() {
|
|
68
64
|
if (!nodeSupportsPi())
|
|
69
65
|
throw new Error(unsupportedNodeMessage());
|
|
66
|
+
enableBridgeResolution();
|
|
70
67
|
this.inner ??= import("./runtime.js").then(({ PiRuntime }) => new PiRuntime(this.options));
|
|
71
68
|
return this.inner;
|
|
72
69
|
}
|
|
@@ -459,6 +459,9 @@ export class PiRuntime {
|
|
|
459
459
|
modelRuntime,
|
|
460
460
|
resourceLoaderOptions: {
|
|
461
461
|
extensionFactories: [guardianFactory(sessionId, options.toolInterceptor), ...providedTools],
|
|
462
|
+
// Additive: keep whatever Pi discovered itself, then the user's
|
|
463
|
+
// account-wide instructions (see OpenSessionOptions.instructions).
|
|
464
|
+
...(options.instructions ? { appendSystemPromptOverride: (base) => [...base, options.instructions.text] } : {}),
|
|
462
465
|
},
|
|
463
466
|
});
|
|
464
467
|
// Override only the built-in bash definition. Pi still owns rendering,
|
package/dist/agents/profiles.js
CHANGED
|
@@ -48,6 +48,9 @@ export const AGENT_PROFILES = {
|
|
|
48
48
|
// `opencode run -s <id> "<prompt>"` continues a prior session by its own id
|
|
49
49
|
// (`-s, --session session id to continue`, per `opencode run --help`).
|
|
50
50
|
resume: { template: ["run", "-s", "{id}"] },
|
|
51
|
+
// OPENCODE_CONFIG_CONTENT is merged over OpenCode's own config; its
|
|
52
|
+
// `instructions` entries are loaded alongside the repo's AGENTS.md.
|
|
53
|
+
instructions: { env: { OPENCODE_CONFIG_CONTENT: '{"instructions":["{fileJson}"]}' } },
|
|
51
54
|
// `opencode run --model <provider/model> "<prompt>"` — the flag follows the
|
|
52
55
|
// `run` subcommand (insertAt: 1). Models are `provider/model` ids.
|
|
53
56
|
model: {
|
package/dist/apps/gateway.js
CHANGED
|
@@ -13,13 +13,7 @@ import { displayViewer, DISPLAY_MENU_PATH, DISPLAY_SOCKET_PATH, DISPLAY_STATS_PA
|
|
|
13
13
|
import { pressMenu, readMenuPath, readMenus } from "./menu.js";
|
|
14
14
|
import { inspectorScript } from "./inspector.js";
|
|
15
15
|
import { readStrokes, readElementScrolls } from "./annotate.js";
|
|
16
|
-
|
|
17
|
-
".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8",
|
|
18
|
-
".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
|
|
19
|
-
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon",
|
|
20
|
-
".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".wasm": "application/wasm",
|
|
21
|
-
".txt": "text/plain; charset=utf-8", ".pdf": "application/pdf", ".mp4": "video/mp4", ".webm": "video/webm",
|
|
22
|
-
};
|
|
16
|
+
import { contentType } from "./mime.js";
|
|
23
17
|
const COOKIE = "__Host-bivy-preview";
|
|
24
18
|
const OPEN_PATH = "/__bivy/open";
|
|
25
19
|
const REDEEM_PATH = "/__bivy/redeem";
|
|
@@ -534,7 +528,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
534
528
|
return;
|
|
535
529
|
}
|
|
536
530
|
const headers = responseHeaders({}, this.ancestors(id));
|
|
537
|
-
headers["content-type"] =
|
|
531
|
+
headers["content-type"] = contentType(file);
|
|
538
532
|
if (inspect && path.extname(file).toLowerCase() === ".html" && data.length <= MAX_INJECT_BYTES)
|
|
539
533
|
data = withInspector(data);
|
|
540
534
|
sendBytes(req, res, status, headers, data);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Content types for files a static app view serves. Browsers refuse module
|
|
5
|
+
* scripts (and some stylesheets) without the right one, so every server of a
|
|
6
|
+
* static snapshot — the preview gateway and the screenshot loopback — uses this. */
|
|
7
|
+
const MIME = {
|
|
8
|
+
".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8",
|
|
9
|
+
".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
|
|
10
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon",
|
|
11
|
+
".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".wasm": "application/wasm",
|
|
12
|
+
".txt": "text/plain; charset=utf-8", ".pdf": "application/pdf", ".mp4": "video/mp4", ".webm": "video/webm",
|
|
13
|
+
};
|
|
14
|
+
export function contentType(file) {
|
|
15
|
+
return MIME[path.extname(file).toLowerCase()] || "application/octet-stream";
|
|
16
|
+
}
|
package/dist/apps/screenshot.js
CHANGED
|
@@ -7,6 +7,7 @@ import http from "node:http";
|
|
|
7
7
|
import { once } from "node:events";
|
|
8
8
|
import { spawn, execFileSync } from "node:child_process";
|
|
9
9
|
import { WebSocket } from "ws";
|
|
10
|
+
import { contentType } from "./mime.js";
|
|
10
11
|
import { captureFrame, encodePng } from "./rfb.js";
|
|
11
12
|
/** Browsers tried in order; BIVY_CHROME overrides. Any Chromium works. */
|
|
12
13
|
const CANDIDATES = [
|
|
@@ -45,12 +46,14 @@ async function serveSnapshot(files) {
|
|
|
45
46
|
let file = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
46
47
|
if (file.endsWith("/"))
|
|
47
48
|
file += "index.html";
|
|
48
|
-
const
|
|
49
|
+
const name = files.has(file) || path.extname(file) ? file : "/index.html";
|
|
50
|
+
const data = files.get(name);
|
|
49
51
|
if (!data) {
|
|
50
52
|
res.writeHead(404);
|
|
51
53
|
res.end();
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
56
|
+
res.writeHead(200, { "content-type": contentType(name) });
|
|
54
57
|
res.end(data);
|
|
55
58
|
});
|
|
56
59
|
server.listen(0, "127.0.0.1");
|
package/dist/apps/styles.css
CHANGED
|
@@ -120,7 +120,7 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
|
|
|
120
120
|
.menu-item:hover, .menu-item[aria-checked="true"], .menu-item.active { background: var(--surface-2); }
|
|
121
121
|
.menu-item.danger { color: var(--danger); }
|
|
122
122
|
.menu-item-label { flex: 1; min-width: 0; }
|
|
123
|
-
.menu-item.separated { margin-top: var(--space-1); border-top: 1px solid var(--line);
|
|
123
|
+
.menu-item.separated { margin-top: var(--space-1); border-top: 1px solid var(--line); }
|
|
124
124
|
/* A section title inside a menu (e.g. "Preview cards" above its choices). */
|
|
125
125
|
.menu-heading { padding: var(--space-2) var(--space-3) var(--space-1); color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; text-transform: uppercase; }
|
|
126
126
|
|
|
@@ -2214,6 +2214,42 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2214
2214
|
.settings-nav-item.active .settings-nav-icon { color: var(--accent); }
|
|
2215
2215
|
.settings-nav-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2216
2216
|
.settings-nav-chevron { display: none; flex: none; color: var(--muted); font-size: 18px; }
|
|
2217
|
+
/* Account identity card pinned to the top of the nav (Claude/ChatGPT pattern):
|
|
2218
|
+
avatar + email on one raised surface. */
|
|
2219
|
+
.settings-account-card {
|
|
2220
|
+
display: flex; flex-direction: column; gap: 1px; padding: var(--space-1);
|
|
2221
|
+
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
|
|
2222
|
+
}
|
|
2223
|
+
.settings-account-avatar {
|
|
2224
|
+
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
2225
|
+
width: 36px; height: 36px; border-radius: var(--radius-full);
|
|
2226
|
+
background: var(--surface-3); color: var(--ink); font-size: var(--text-sm); font-weight: var(--weight-semibold);
|
|
2227
|
+
}
|
|
2228
|
+
.settings-account-avatar svg { width: 20px; height: 20px; }
|
|
2229
|
+
.settings-account-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
2230
|
+
.settings-account-sub { font-size: var(--text-xs); color: var(--muted); font-weight: normal; }
|
|
2231
|
+
.settings-account-usage { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-1) 10px var(--space-2); font-size: var(--text-xs); color: var(--muted); }
|
|
2232
|
+
.settings-account-usage-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); min-height: 20px; }
|
|
2233
|
+
.settings-account-usage-count { color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
2234
|
+
.settings-account-usage-note { font-weight: var(--weight-semibold); }
|
|
2235
|
+
.settings-account-usage[data-state="near"] .settings-account-usage-note { color: var(--warn); }
|
|
2236
|
+
.settings-account-usage[data-state="reached"] .settings-account-usage-note { color: var(--danger); }
|
|
2237
|
+
.settings-account-usage .banner { margin: 0; }
|
|
2238
|
+
.settings-account-usage-status { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
2239
|
+
.settings-account-usage > .btn.block { margin-top: 0; }
|
|
2240
|
+
/* The same meter on the Account page, aligned with the plan rows. */
|
|
2241
|
+
.settings-section > .settings-account-usage { padding: var(--space-1) 0 0; font-size: var(--text-sm); }
|
|
2242
|
+
|
|
2243
|
+
/* Account page offer (e.g. upgrade): a .card, layout only. */
|
|
2244
|
+
.account-offer { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-4); margin-top: var(--space-4); }
|
|
2245
|
+
.account-offer .card-head { margin-bottom: 0; align-items: baseline; }
|
|
2246
|
+
.account-offer .card-sub { margin: calc(-1 * var(--space-2)) 0 0; }
|
|
2247
|
+
.account-offer-price { font-size: var(--text-base); font-weight: var(--weight-semibold); color: var(--ink); white-space: nowrap; }
|
|
2248
|
+
.account-offer-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); font-size: var(--text-sm); }
|
|
2249
|
+
.account-offer-points li { display: flex; align-items: flex-start; gap: var(--space-2); }
|
|
2250
|
+
.account-offer-check { flex: none; display: inline-flex; color: var(--accent); margin-top: 1px; }
|
|
2251
|
+
.account-offer > .btn.block { margin-top: var(--space-1); }
|
|
2252
|
+
.account-offer + .settings-section { margin-top: var(--space-5); }
|
|
2217
2253
|
.settings-nav-version { flex: none; display: flex; flex-direction: column; gap: 1px; padding: 10px 10px 2px; color: var(--muted); font-size: 12px; letter-spacing: 0.02em; }
|
|
2218
2254
|
.settings-nav-version-updated { font-size: 11px; opacity: 0.8; }
|
|
2219
2255
|
|
|
@@ -2228,6 +2264,13 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2228
2264
|
.settings-body { flex: 1; overflow-y: auto; min-height: 0; padding: 14px 18px 28px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; animation: settings-panel-in var(--dur) var(--ease-standard); }
|
|
2229
2265
|
.settings-body .settings-form { padding: 0; }
|
|
2230
2266
|
|
|
2267
|
+
/* Canonical meter — a thin allowance/usage bar (role="meter"). data-state
|
|
2268
|
+
near/reached tints it warn/danger. Reflects the styleguide (.meter). */
|
|
2269
|
+
.meter { display: block; height: 6px; border-radius: var(--radius-full); background: var(--surface-3); overflow: hidden; }
|
|
2270
|
+
.meter-fill { display: block; height: 100%; border-radius: inherit; background: var(--accent); transition: width var(--dur) var(--ease-standard); }
|
|
2271
|
+
.meter[data-state="near"] .meter-fill { background: var(--warn); }
|
|
2272
|
+
.meter[data-state="reached"] .meter-fill { background: var(--danger); }
|
|
2273
|
+
|
|
2231
2274
|
/* Canonical selectable — the shared "active-accent-tint" choice control. Its
|
|
2232
2275
|
selected state (accent border + soft accent fill) is the one that used to be
|
|
2233
2276
|
copy-pasted across the theme picker, segmented buttons and ruleset chips; new
|
|
@@ -2353,6 +2396,7 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2353
2396
|
.settings-nav-group-label { font-size: 12px; padding: 10px 6px 4px; }
|
|
2354
2397
|
.settings-nav-item { padding: 14px 12px; font-size: 16px; }
|
|
2355
2398
|
.settings-nav-chevron { display: inline-flex; }
|
|
2399
|
+
.settings-account-card { background: var(--surface-2); margin-bottom: var(--space-2); }
|
|
2356
2400
|
.settings-content { padding-top: max(0px, env(safe-area-inset-top)); }
|
|
2357
2401
|
.settings-back { display: inline-flex; }
|
|
2358
2402
|
.settings-body { padding: 14px 16px max(28px, env(safe-area-inset-bottom)); }
|
|
@@ -2489,10 +2533,6 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2489
2533
|
.fork-progress-dialog { color: var(--ink); margin: auto; max-width: calc(100% - var(--space-8)); }
|
|
2490
2534
|
.fork-progress-dialog::backdrop { background: var(--overlay); }
|
|
2491
2535
|
.app-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
2492
|
-
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
2493
|
-
.stat { border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px 12px; background: var(--surface-2); }
|
|
2494
|
-
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
|
|
2495
|
-
.stat-value { font-size: 16px; font-weight: 700; margin-top: 2px; }
|
|
2496
2536
|
textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace; }
|
|
2497
2537
|
|
|
2498
2538
|
/* ---- Terminal ---- */
|
|
@@ -97,6 +97,10 @@ export function bivyToolsServerSpec(opts) {
|
|
|
97
97
|
const env = { BIVY_SESSION_ID: opts.sessionId };
|
|
98
98
|
if (opts.endpoint)
|
|
99
99
|
env.BIVY_MCP_ENDPOINT = opts.endpoint;
|
|
100
|
+
// The user's account-wide instructions, advertised as the server's MCP
|
|
101
|
+
// `instructions` — the best-effort channel for agents with no native one.
|
|
102
|
+
if (opts.instructionsFile)
|
|
103
|
+
env.BIVY_AGENT_INSTRUCTIONS_FILE = opts.instructionsFile;
|
|
100
104
|
return { command: opts.bivyCommand ?? "bivy", args: ["mcp-serve"], env };
|
|
101
105
|
}
|
|
102
106
|
/**
|
|
@@ -136,7 +136,7 @@ export function injectBivyToolsForSession(agentId, ctx, bivyCommand = "bivy") {
|
|
|
136
136
|
});
|
|
137
137
|
if (!target)
|
|
138
138
|
return { injected: [], restore: () => { } };
|
|
139
|
-
const spec = bivyToolsServerSpec({ sessionId: ctx.sessionId, endpoint: ctx.endpoint, bivyCommand });
|
|
139
|
+
const spec = bivyToolsServerSpec({ sessionId: ctx.sessionId, endpoint: ctx.endpoint, bivyCommand, instructionsFile: ctx.instructionsFile });
|
|
140
140
|
const ext = path.extname(target).toLowerCase();
|
|
141
141
|
const openCode = agentId === "opencode" || isOpenCodeConfigFile(target);
|
|
142
142
|
const existed = fs.existsSync(target);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// with an injected fetch; the process/transport wiring is thin. Deliberately do
|
|
21
21
|
// not expose governed Runs here: agents should use their native sub-agent tools,
|
|
22
22
|
// which stay inside the parent Session instead of cluttering the Session list.
|
|
23
|
+
import fs from "node:fs";
|
|
23
24
|
import path from "node:path";
|
|
24
25
|
import { fileURLToPath } from "node:url";
|
|
25
26
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -88,13 +89,25 @@ export async function runAttachToChat(endpoint, sessionId, args, fetchImpl, toke
|
|
|
88
89
|
const name = body?.name || filePath;
|
|
89
90
|
return { isError: false, text: `Attached ${name} to the chat as ${body?.kind === "image" ? "an inline image" : "a downloadable file"}. The user can see it now.` };
|
|
90
91
|
}
|
|
92
|
+
/** The user's instructions to advertise at initialize, or undefined when there are none. */
|
|
93
|
+
function readInstructions(file) {
|
|
94
|
+
if (!file)
|
|
95
|
+
return undefined;
|
|
96
|
+
try {
|
|
97
|
+
return fs.readFileSync(file, "utf8").trim() || undefined;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
91
103
|
/** Build the Bivy MCP `Server` with tools/list + tools/call handlers wired. */
|
|
92
104
|
export function createBivyMcpServer(deps = {}) {
|
|
93
105
|
const endpoint = deps.endpoint ?? process.env.BIVY_MCP_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
94
106
|
const sessionId = deps.sessionId ?? process.env.BIVY_SESSION_ID ?? process.env.BIVY_MCP_SESSION ?? "";
|
|
95
107
|
const token = deps.token ?? process.env.BIVY_MCP_TOKEN ?? undefined;
|
|
96
108
|
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
97
|
-
const
|
|
109
|
+
const instructions = readInstructions(deps.instructionsFile ?? process.env.BIVY_AGENT_INSTRUCTIONS_FILE);
|
|
110
|
+
const server = new Server({ name: "bivy", version: "1.0.0" }, { capabilities: { tools: {} }, ...(instructions ? { instructions } : {}) });
|
|
98
111
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: BIVY_MCP_TOOLS }));
|
|
99
112
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
100
113
|
const args = (req.params.arguments ?? {});
|
|
@@ -135,8 +135,8 @@ export class AgentService {
|
|
|
135
135
|
const toolProvider = message.options.toolSpecs?.length ? this.makeToolProvider(svc, message.options.toolSpecs) : undefined;
|
|
136
136
|
const workspace = message.options.workspace ?? process.cwd();
|
|
137
137
|
const result = message.op === "open" && message.options.sessionFile
|
|
138
|
-
? await runtime.openSession({ workspace, credentialLabels: message.options.credentialLabels, sessionFile: message.options.sessionFile, toolInterceptor, toolProvider })
|
|
139
|
-
: await runtime.createSession({ workspace, credentialLabels: message.options.credentialLabels, toolInterceptor, toolProvider });
|
|
138
|
+
? await runtime.openSession({ workspace, credentialLabels: message.options.credentialLabels, sessionFile: message.options.sessionFile, toolInterceptor, toolProvider, instructions: message.options.instructions })
|
|
139
|
+
: await runtime.createSession({ workspace, credentialLabels: message.options.credentialLabels, toolInterceptor, toolProvider, instructions: message.options.instructions });
|
|
140
140
|
svc.session = result.session;
|
|
141
141
|
svc.id = result.session.id;
|
|
142
142
|
svc.lastSent = this.mirror(result.session);
|
package/dist/runtime/index.js
CHANGED
|
@@ -643,6 +643,7 @@ function acpRuntimeOptions(opts) {
|
|
|
643
643
|
// An ACP-promoted opencode still surfaces/expands its on-disk commands (the
|
|
644
644
|
// ACP handshake doesn't carry them); a bare ACP agent has none.
|
|
645
645
|
...(slashCommands ? { slashCommands } : {}),
|
|
646
|
+
...(opts.instructionsEnv ? { instructionsEnv: opts.instructionsEnv } : {}),
|
|
646
647
|
...(opts.credsDir ? { credentials: createCredentialStore(opts.credsDir) } : {}),
|
|
647
648
|
// Declared credential behaviors apply to the governed path exactly as they do
|
|
648
649
|
// to the pipe path: the preflight surfaces a clear no-credential error before
|
|
@@ -1147,6 +1148,7 @@ function makeCliRuntime(id, options, spec) {
|
|
|
1147
1148
|
...(spec.authOwner && spec.authOwner !== "agent" ? { credsDir: options.credsDir } : {}),
|
|
1148
1149
|
sandbox: options.sandbox,
|
|
1149
1150
|
mcpConfig: options.mcpConfig,
|
|
1151
|
+
instructionsEnv: spec.instructions?.env,
|
|
1150
1152
|
}));
|
|
1151
1153
|
}
|
|
1152
1154
|
const structured = executionMode === "structured-pipe";
|
|
@@ -1264,6 +1266,7 @@ function makeCliRuntime(id, options, spec) {
|
|
|
1264
1266
|
thinking: cliThinkingConfig(id, spec),
|
|
1265
1267
|
usageReporting: cliUsageReporting(spec),
|
|
1266
1268
|
slashCommands: cliSlashCommands(spec),
|
|
1269
|
+
...(spec.instructions ? { instructionsEnv: spec.instructions.env } : {}),
|
|
1267
1270
|
...resumeOpts,
|
|
1268
1271
|
...nativeSessionOpts,
|
|
1269
1272
|
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// inference SDK, kept under src/runtime/ so the core daemon (src/server.ts) stays
|
|
14
14
|
// free of any @earendil-works import.
|
|
15
15
|
import { createPiModelRuntime } from "./pi-oauth.js";
|
|
16
|
+
import { enableBridgeResolution } from "../agent-bridges.mjs";
|
|
16
17
|
function cleanSessionName(value) {
|
|
17
18
|
return value
|
|
18
19
|
.replace(/[\r\n"'`]/g, " ")
|
|
@@ -54,6 +55,7 @@ export async function suggestNameFromSelectedModel(opts) {
|
|
|
54
55
|
if (!prompt)
|
|
55
56
|
return undefined;
|
|
56
57
|
try {
|
|
58
|
+
enableBridgeResolution();
|
|
57
59
|
const [{ completeSimple }, { ModelRegistry }] = await Promise.all([
|
|
58
60
|
import("@earendil-works/pi-ai/compat"),
|
|
59
61
|
import("@earendil-works/pi-coding-agent"),
|
package/dist/runtime/pi-oauth.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from "node:path";
|
|
|
12
12
|
import { createCredentialVault } from "./credential-store.js";
|
|
13
13
|
import { isNativeOAuthProvider } from "./oauth/model-oauth-providers.js";
|
|
14
14
|
import { selectedCredentialStore } from "../credentials/selected-store.js";
|
|
15
|
+
import { enableBridgeResolution } from "../agent-bridges.mjs";
|
|
15
16
|
/** Adapt Bivy's store to pi-ai's structurally-identical CredentialStore for injection. */
|
|
16
17
|
export function piCredentialStore(store) {
|
|
17
18
|
return store;
|
|
@@ -25,6 +26,7 @@ export function piCredentialStore(store) {
|
|
|
25
26
|
*/
|
|
26
27
|
export async function createPiModelRuntime(opts) {
|
|
27
28
|
const store = opts.store ?? createCredentialVault(opts.credsDir);
|
|
29
|
+
enableBridgeResolution();
|
|
28
30
|
const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
|
|
29
31
|
const selected = selectedCredentialStore(store, opts.credsDir, { workspace: opts.workspace, credentialLabels: opts.credentialLabels });
|
|
30
32
|
for (const provider of Object.keys(opts.credentialLabels ?? {}))
|
package/dist/runtime/process.js
CHANGED
|
@@ -8,7 +8,7 @@ import { buildAgentCredentialEnv } from "./credentials.js";
|
|
|
8
8
|
import { withSessionCredentials, credentialEnvFallback } from "../credentials/session.js";
|
|
9
9
|
import { egressEnv, sessionEgressEnv } from "../harness/egress.js";
|
|
10
10
|
import { depCacheEnv } from "../harness/dep-cache.js";
|
|
11
|
-
import { bivySessionEnv } from "./session-env.js";
|
|
11
|
+
import { bivySessionEnv, withSessionInstructions } from "./session-env.js";
|
|
12
12
|
import { withExactCapabilitySurface } from "./types.js";
|
|
13
13
|
/**
|
|
14
14
|
* Send `signal` to `child`'s whole process group when possible, so a forking CLI
|
|
@@ -539,7 +539,7 @@ export class ProcessRuntime {
|
|
|
539
539
|
return [...byProvider.values()];
|
|
540
540
|
}
|
|
541
541
|
async createSession(options) {
|
|
542
|
-
const session = new ProcessSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace);
|
|
542
|
+
const session = new ProcessSession(withSessionInstructions(await withSessionCredentials(this.options, options.credentialLabels), options.instructions), options.workspace);
|
|
543
543
|
this.sessions.push(session);
|
|
544
544
|
return { session, warning: "Generic CLI runtime streams stdout/stderr only; approvals, model picker, and resume depend on the underlying agent protocol." };
|
|
545
545
|
}
|
|
@@ -547,7 +547,7 @@ export class ProcessRuntime {
|
|
|
547
547
|
// Resumable runtimes bind the agent's session id so each prompt continues it
|
|
548
548
|
// (see resumeArgs); non-resumable ones ignore the ref and start fresh.
|
|
549
549
|
if (this.options.resumable) {
|
|
550
|
-
const session = new ProcessSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.sessionFile);
|
|
550
|
+
const session = new ProcessSession(withSessionInstructions(await withSessionCredentials(this.options, options.credentialLabels), options.instructions), options.workspace, options.sessionFile);
|
|
551
551
|
this.sessions.push(session);
|
|
552
552
|
return { session };
|
|
553
553
|
}
|
package/dist/runtime/protocol.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { EventEmitter } from "node:events";
|
|
6
6
|
import { buildAgentCredentialEnv } from "./credentials.js";
|
|
7
7
|
import { withSessionCredentials, credentialEnvFallback } from "../credentials/session.js";
|
|
8
|
-
import { bivySessionEnv } from "./session-env.js";
|
|
8
|
+
import { bivySessionEnv, withSessionInstructions } from "./session-env.js";
|
|
9
9
|
import { mergeAgentCommands } from "./slash-commands.js";
|
|
10
10
|
import { withExactCapabilitySurface } from "./types.js";
|
|
11
11
|
import { extractTokenUsage } from "./cli-parsers.js";
|
|
@@ -157,6 +157,7 @@ class ProtocolSession {
|
|
|
157
157
|
cwd;
|
|
158
158
|
capabilitiesRef;
|
|
159
159
|
toolInterceptor;
|
|
160
|
+
instructions;
|
|
160
161
|
id;
|
|
161
162
|
child;
|
|
162
163
|
buffer = "";
|
|
@@ -225,11 +226,15 @@ class ProtocolSession {
|
|
|
225
226
|
// taking the ref as its id — otherwise the daemon persists a SECOND metadata
|
|
226
227
|
// row keyed by the ref, duplicating the conversation. Falls back to the ref,
|
|
227
228
|
// then a fresh UUID, preserving the prior behaviour when no id is supplied.
|
|
228
|
-
canonicalId
|
|
229
|
+
canonicalId,
|
|
230
|
+
// The user's account-wide instructions (OpenSessionOptions.instructions),
|
|
231
|
+
// sent on session.create/session.resume for the shim to hand its agent.
|
|
232
|
+
instructions) {
|
|
229
233
|
this.runtimeOptions = runtimeOptions;
|
|
230
234
|
this.cwd = cwd;
|
|
231
235
|
this.capabilitiesRef = capabilitiesRef;
|
|
232
236
|
this.toolInterceptor = toolInterceptor;
|
|
237
|
+
this.instructions = instructions;
|
|
233
238
|
this.id = canonicalId || resumeRef || randomUUID();
|
|
234
239
|
this.resumeRef = resumeRef;
|
|
235
240
|
if (resumeRef) {
|
|
@@ -891,12 +896,15 @@ class ProtocolSession {
|
|
|
891
896
|
// resume.
|
|
892
897
|
const resumeRef = this.resumeRef ?? this.runtimeSessionRef;
|
|
893
898
|
const created = resumeRef && this.capabilitiesRef.resume
|
|
894
|
-
? await this.command("session.resume", { workspace: this.cwd, sessionId: this.id, runtimeSessionRef: resumeRef, resumeRef })
|
|
895
|
-
: await this.command("session.create", { workspace: this.cwd, sessionId: this.id });
|
|
899
|
+
? await this.command("session.resume", { workspace: this.cwd, sessionId: this.id, runtimeSessionRef: resumeRef, resumeRef, ...this.instructionsField() })
|
|
900
|
+
: await this.command("session.create", { workspace: this.cwd, sessionId: this.id, ...this.instructionsField() });
|
|
896
901
|
if (typeof created.runtimeSessionRef === "string")
|
|
897
902
|
this.runtimeSessionRef = created.runtimeSessionRef;
|
|
898
903
|
this.started = true;
|
|
899
904
|
}
|
|
905
|
+
instructionsField() {
|
|
906
|
+
return this.instructions ? { instructions: this.instructions } : {};
|
|
907
|
+
}
|
|
900
908
|
async prompt(text, options) {
|
|
901
909
|
const wasStarted = this.started;
|
|
902
910
|
await this.open();
|
|
@@ -1039,7 +1047,7 @@ export class ProtocolRuntime {
|
|
|
1039
1047
|
return this.options.catalog ?? [];
|
|
1040
1048
|
}
|
|
1041
1049
|
async createSession(options) {
|
|
1042
|
-
const session = new ProtocolSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, this.capabilities, options.toolInterceptor);
|
|
1050
|
+
const session = new ProtocolSession(withSessionInstructions(await withSessionCredentials(this.options, options.credentialLabels), options.instructions), options.workspace, this.capabilities, options.toolInterceptor, undefined, undefined, options.instructions?.text);
|
|
1043
1051
|
try {
|
|
1044
1052
|
await session.start();
|
|
1045
1053
|
this.sessions.push(session);
|
|
@@ -1056,7 +1064,7 @@ export class ProtocolRuntime {
|
|
|
1056
1064
|
// Adopt the caller's canonical id (a reopen of a known session) so the
|
|
1057
1065
|
// resumed session keeps its original id instead of taking `sessionFile` (the
|
|
1058
1066
|
// agent's own ref) as its id — see OpenSessionOptions.canonicalId.
|
|
1059
|
-
const session = new ProtocolSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, this.capabilities, options.toolInterceptor, options.sessionFile, options.canonicalId);
|
|
1067
|
+
const session = new ProtocolSession(withSessionInstructions(await withSessionCredentials(this.options, options.credentialLabels), options.instructions), options.workspace, this.capabilities, options.toolInterceptor, options.sessionFile, options.canonicalId, options.instructions?.text);
|
|
1060
1068
|
try {
|
|
1061
1069
|
await session.start();
|
|
1062
1070
|
if (!this.options.resumable && !this.capabilities.resume) {
|
package/dist/runtime/remote.js
CHANGED
|
@@ -543,12 +543,12 @@ export class RemoteRuntime {
|
|
|
543
543
|
async createSession(options) {
|
|
544
544
|
const transport = await this.config.connect();
|
|
545
545
|
const toolSpecs = options.toolProvider?.list();
|
|
546
|
-
return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "create", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
|
|
546
|
+
return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "create", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, ...(options.instructions ? { instructions: options.instructions } : {}), hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
|
|
547
547
|
}
|
|
548
548
|
async openSession(options) {
|
|
549
549
|
const transport = await this.config.connect();
|
|
550
550
|
const toolSpecs = options.toolProvider?.list();
|
|
551
|
-
return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "open", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, sessionFile: options.sessionFile, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
|
|
551
|
+
return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "open", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, sessionFile: options.sessionFile, ...(options.instructions ? { instructions: options.instructions } : {}), hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
|
|
552
552
|
}
|
|
553
553
|
/**
|
|
554
554
|
* Re-attach to a session already live on the agent service (Stage 2 routing).
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
1
|
//
|
|
4
2
|
// Shared across every runtime adapter that spawns (or configures the spawn of) a
|
|
5
3
|
// subprocess for its agent: fold this into that subprocess's env so the agent's
|
|
@@ -20,3 +18,21 @@
|
|
|
20
18
|
export function bivySessionEnv(sessionId) {
|
|
21
19
|
return { BIVY_SESSION_ID: sessionId };
|
|
22
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Apply a profile's instructions env template (AgentProfile.instructions) for one
|
|
23
|
+
* session: `{file}` → the composed instructions file, `{fileJson}` → the same
|
|
24
|
+
* path escaped for use inside a JSON string. A variable the operator already set
|
|
25
|
+
* is left alone rather than clobbered. No template or no instructions = options
|
|
26
|
+
* returned unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export function withSessionInstructions(options, instructions) {
|
|
29
|
+
if (!options.instructionsEnv || !instructions)
|
|
30
|
+
return options;
|
|
31
|
+
const patch = {};
|
|
32
|
+
for (const [name, template] of Object.entries(options.instructionsEnv)) {
|
|
33
|
+
if (process.env[name] !== undefined || options.env?.[name] !== undefined)
|
|
34
|
+
continue;
|
|
35
|
+
patch[name] = template.replace(/\{fileJson\}/g, JSON.stringify(instructions.file).slice(1, -1)).replace(/\{file\}/g, instructions.file);
|
|
36
|
+
}
|
|
37
|
+
return { ...options, env: { ...options.env, ...patch } };
|
|
38
|
+
}
|