@bivy/bivy 0.5.1-staging.59 → 0.5.1-staging.61
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/bivy.mjs +8 -0
- package/dist/harness/mcp-config-formats.js +27 -0
- package/dist/harness/mcp-config.js +24 -0
- package/dist/harness/mcp-inject.js +78 -2
- package/dist/harness/mcp-serve-cli.js +123 -0
- package/dist/integrations/index.js +1 -1
- package/dist/integrations/manager.js +51 -13
- package/dist/integrations/registry.js +23 -0
- package/dist/runtime/claude-code.js +54 -0
- package/dist/runtime/index.js +1 -1
- package/dist/server.js +62 -5
- package/package.json +3 -2
package/bin/bivy.mjs
CHANGED
|
@@ -145,6 +145,7 @@ const attachEntry = path.join(repoRoot, packaged ? "dist/attach.js" : "src/attac
|
|
|
145
145
|
const relayAttachEntry = path.join(repoRoot, packaged ? "dist/relay-attach.js" : "src/relay-attach.ts");
|
|
146
146
|
const execEntry = path.join(repoRoot, packaged ? "dist/exec.js" : "src/exec.ts");
|
|
147
147
|
const mcpProxyEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-proxy-cli.js" : "src/harness/mcp-proxy-cli.ts");
|
|
148
|
+
const mcpServeEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-serve-cli.js" : "src/harness/mcp-serve-cli.ts");
|
|
148
149
|
const qrEntry = path.join(repoRoot, "public", "qr.js");
|
|
149
150
|
const tsxCli = packaged ? "" : path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
150
151
|
const nodeBin = process.execPath;
|
|
@@ -4360,6 +4361,13 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
4360
4361
|
// agent's cwd so relative server commands resolve.
|
|
4361
4362
|
await run(nodeBin, [...nodeScriptArgs(mcpProxyEntry), ...args], { cwd: process.cwd(), env: process.env });
|
|
4362
4363
|
break;
|
|
4364
|
+
case "mcp-serve":
|
|
4365
|
+
// Bivy-owned MCP server (exposes attach_to_chat and future chat tools).
|
|
4366
|
+
// Injected into a non-SDK agent's MCP config so the agent discovers the
|
|
4367
|
+
// tool; like mcp-proxy its stdin/stdout ARE the JSON-RPC stream, so emit
|
|
4368
|
+
// nothing else here and inherit stdio verbatim.
|
|
4369
|
+
await run(nodeBin, [...nodeScriptArgs(mcpServeEntry), ...args], { cwd: process.cwd(), env: process.env });
|
|
4370
|
+
break;
|
|
4363
4371
|
case "help":
|
|
4364
4372
|
case "-h":
|
|
4365
4373
|
case "--help":
|
|
@@ -15,6 +15,33 @@
|
|
|
15
15
|
// exact original bytes on session end regardless. Pure string→string, unit-tested
|
|
16
16
|
// in test/harness-mcp-formats.test.ts.
|
|
17
17
|
import { PROXY_MARKER } from "./mcp-config.js";
|
|
18
|
+
function escapeRegExp(s) {
|
|
19
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
}
|
|
21
|
+
/** True when a TOML config already declares `[mcp_servers.<name>]`. */
|
|
22
|
+
export function hasTomlServer(content, name) {
|
|
23
|
+
return new RegExp(`^\\s*\\[mcp_servers\\.${escapeRegExp(name)}\\]\\s*$`, "m").test(content);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Append a `[mcp_servers.<name>]` table to a Codex TOML config (the reverse of
|
|
27
|
+
* routeThroughProxy's rewrite — this ADDS Bivy's own server rather than wrapping
|
|
28
|
+
* the agent's). Idempotent: returns `inserted: false` untouched when a table of
|
|
29
|
+
* that name already exists. Env is written as an inline table; string values are
|
|
30
|
+
* TOML-escaped via JSON.stringify (safe for the ASCII paths/ids we stamp).
|
|
31
|
+
*/
|
|
32
|
+
export function insertTomlServer(content, name, spec) {
|
|
33
|
+
if (hasTomlServer(content, name))
|
|
34
|
+
return { content, inserted: false };
|
|
35
|
+
const block = [`[mcp_servers.${name}]`, `command = ${JSON.stringify(spec.command)}`, `args = ${tomlStringArray(spec.args ?? [])}`];
|
|
36
|
+
if (spec.env && Object.keys(spec.env).length) {
|
|
37
|
+
const pairs = Object.entries(spec.env).map(([k, v]) => `${k} = ${JSON.stringify(v)}`);
|
|
38
|
+
block.push(`env = { ${pairs.join(", ")} }`);
|
|
39
|
+
}
|
|
40
|
+
// Separate from prior content with a blank line; keep a trailing newline.
|
|
41
|
+
const base = content.length === 0 || content.endsWith("\n") ? content : `${content}\n`;
|
|
42
|
+
const sep = base.length > 0 && !base.endsWith("\n\n") ? "\n" : "";
|
|
43
|
+
return { content: `${base}${sep}${block.join("\n")}\n`, inserted: true };
|
|
44
|
+
}
|
|
18
45
|
function proxyArgs(launcher, server, command, origArgs) {
|
|
19
46
|
return [...(launcher.argsPrefix ?? []), PROXY_MARKER, "--server", server, "--", command, ...origArgs];
|
|
20
47
|
}
|
|
@@ -85,6 +85,30 @@ export function parseProxiedArgs(args) {
|
|
|
85
85
|
const tail = args.slice(sep + 1);
|
|
86
86
|
return { server, command: tail[0], args: tail.slice(1) };
|
|
87
87
|
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Bivy-owned tools server — the mirror of the proxy above. routeThroughProxy
|
|
90
|
+
// wraps the agent's OWN servers; this ADDS a `bivy` server (run via `bivy
|
|
91
|
+
// mcp-serve`) so the agent discovers Bivy's chat tools (attach_to_chat, …) in
|
|
92
|
+
// its own tool list. Session id + node URL ride in its env so the tool can post
|
|
93
|
+
// back to the right session.
|
|
94
|
+
/** The server spec that launches `bivy mcp-serve` for a session. */
|
|
95
|
+
export function bivyToolsServerSpec(opts) {
|
|
96
|
+
const env = { BIVY_SESSION_ID: opts.sessionId };
|
|
97
|
+
if (opts.endpoint)
|
|
98
|
+
env.BIVY_MCP_ENDPOINT = opts.endpoint;
|
|
99
|
+
return { command: opts.bivyCommand ?? "bivy", args: ["mcp-serve"], env };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Insert the Bivy tools server under `mcpServers.<name>` (default "bivy").
|
|
103
|
+
* Idempotent: an existing entry of that name is left untouched (returns
|
|
104
|
+
* `added: false`), so a re-inject or a user's own `bivy` server never doubles up.
|
|
105
|
+
*/
|
|
106
|
+
export function withBivyToolsServer(config, spec, name = "bivy") {
|
|
107
|
+
const servers = config.mcpServers ?? {};
|
|
108
|
+
if (servers[name])
|
|
109
|
+
return { config, added: false };
|
|
110
|
+
return { config: { ...config, mcpServers: { ...servers, [name]: spec } }, added: true };
|
|
111
|
+
}
|
|
88
112
|
/** JSON MCP-config file candidates for an agent, most-specific (safest) first. */
|
|
89
113
|
export function agentMcpConfigTargets(agentId, ctx) {
|
|
90
114
|
const ws = (...parts) => nodePath.join(ctx.workspace, ...parts);
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// FS + network channels. Unit-tested in test/harness-mcp-inject.test.ts.
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import { agentMcpConfigTargets, routeThroughProxy, } from "./mcp-config.js";
|
|
18
|
-
import { injectTomlMcp, injectYamlMcp } from "./mcp-config-formats.js";
|
|
17
|
+
import { agentMcpConfigTargets, bivyToolsServerSpec, routeThroughProxy, withBivyToolsServer, } from "./mcp-config.js";
|
|
18
|
+
import { injectTomlMcp, injectYamlMcp, insertTomlServer } from "./mcp-config-formats.js";
|
|
19
19
|
/** The proxy launcher Bivy injects — `bivy mcp-proxy …`. */
|
|
20
20
|
export function bivyProxyLauncher(bivyCommand = "bivy") {
|
|
21
21
|
return { command: bivyCommand, argsPrefix: ["mcp-proxy"] };
|
|
@@ -110,6 +110,82 @@ export function injectMcpConfigFile(filePath, launcher) {
|
|
|
110
110
|
},
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Add the `bivy` tools server (attach_to_chat …) to an agent's MCP config so the
|
|
115
|
+
* agent DISCOVERS it as a tool. Unlike the proxy inject (which only rewrites
|
|
116
|
+
* servers a file already has), this CREATES the config when absent so an agent
|
|
117
|
+
* that ships no MCP config still gets the tool. Handles the most-specific JSON
|
|
118
|
+
* config (session-local for claude/gemini/opencode/generic) and Codex's TOML
|
|
119
|
+
* (`~/.codex/config.toml` — Codex has no project-local option). restore() deletes
|
|
120
|
+
* a file it created and rewrites the exact original bytes of one it modified.
|
|
121
|
+
* Idempotent (a `bivy` server already present is a no-op, so concurrent sessions
|
|
122
|
+
* sharing a global config don't double up). Best-effort; never throws. Goose YAML
|
|
123
|
+
* is a follow-up.
|
|
124
|
+
*/
|
|
125
|
+
export function injectBivyToolsForSession(agentId, ctx, bivyCommand = "bivy") {
|
|
126
|
+
const target = agentMcpConfigTargets(agentId, ctx).find((t) => {
|
|
127
|
+
const ext = path.extname(t).toLowerCase();
|
|
128
|
+
return ext === ".json" || ext === ".toml";
|
|
129
|
+
});
|
|
130
|
+
if (!target)
|
|
131
|
+
return { injected: [], restore: () => { } };
|
|
132
|
+
const spec = bivyToolsServerSpec({ sessionId: ctx.sessionId, endpoint: ctx.endpoint, bivyCommand });
|
|
133
|
+
const ext = path.extname(target).toLowerCase();
|
|
134
|
+
const existed = fs.existsSync(target);
|
|
135
|
+
let original;
|
|
136
|
+
if (existed) {
|
|
137
|
+
try {
|
|
138
|
+
original = fs.readFileSync(target, "utf8");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { injected: [], restore: () => { } };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let nextContent;
|
|
145
|
+
if (ext === ".json") {
|
|
146
|
+
let parsed = {};
|
|
147
|
+
if (original !== undefined) {
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(original);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// A config we can't parse is not ours to rewrite — leave it be.
|
|
153
|
+
return { injected: [], restore: () => { } };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const { config, added } = withBivyToolsServer(parsed, spec);
|
|
157
|
+
if (!added)
|
|
158
|
+
return { injected: [], restore: () => { } };
|
|
159
|
+
nextContent = `${JSON.stringify(config, null, 2)}\n`;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const res = insertTomlServer(original ?? "", "bivy", { command: spec.command ?? bivyCommand, args: spec.args, env: spec.env });
|
|
163
|
+
if (!res.inserted)
|
|
164
|
+
return { injected: [], restore: () => { } };
|
|
165
|
+
nextContent = res.content;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
169
|
+
fs.writeFileSync(target, nextContent);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return { injected: [], restore: () => { } };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
injected: [target],
|
|
176
|
+
restore: () => {
|
|
177
|
+
try {
|
|
178
|
+
if (existed && original !== undefined)
|
|
179
|
+
fs.writeFileSync(target, original);
|
|
180
|
+
else
|
|
181
|
+
fs.rmSync(target, { force: true });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Restore is best-effort.
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
113
189
|
/**
|
|
114
190
|
* Inject the proxy into every MCP config an agent reads for this session.
|
|
115
191
|
* Returns the injected files and a single restore() covering all of them.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Universal Agent Harness — `bivy mcp-serve` entry point.
|
|
4
|
+
//
|
|
5
|
+
// A Bivy-OWNED stdio MCP server (the mirror of `bivy mcp-proxy`, which wraps the
|
|
6
|
+
// agent's OWN servers). Injected into every non-SDK agent's MCP config at session
|
|
7
|
+
// start (see mcp-inject.ts's injectBivyToolsForSession), it exposes Bivy's chat
|
|
8
|
+
// affordances as first-class tools so ANY agent — codex, gemini, aider, opencode,
|
|
9
|
+
// … — discovers them in its tool list instead of having to be told about a shell
|
|
10
|
+
// command (issue #290). Today it serves one tool, `attach_to_chat`.
|
|
11
|
+
//
|
|
12
|
+
// Claude and Pi already get `attach_to_chat` natively (in-process SDK MCP server /
|
|
13
|
+
// integration ToolProvider); this covers everyone else. The tool just POSTs to the
|
|
14
|
+
// node's existing `POST /api/session/:id/attach` endpoint — the exact plumbing
|
|
15
|
+
// `bivy attach` uses — so it reuses the same workspace confinement, storage, and
|
|
16
|
+
// live broadcast. The session id and node URL arrive via env (BIVY_SESSION_ID /
|
|
17
|
+
// BIVY_MCP_ENDPOINT), stamped into the injected server spec.
|
|
18
|
+
//
|
|
19
|
+
// The attach client is factored out and unit-tested (test/harness-mcp-serve.test.ts)
|
|
20
|
+
// with an injected fetch; the process/transport wiring is thin.
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
24
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4317";
|
|
27
|
+
/** The tools this server advertises, in MCP `tools/list` shape. */
|
|
28
|
+
export const BIVY_MCP_TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: "attach_to_chat",
|
|
31
|
+
description: "Send a file or image from the session workspace into the chat the user is reading. The person you're " +
|
|
32
|
+
"talking to is in a chat UI: they cannot see files you only write to disk, and the chat cannot load workspace " +
|
|
33
|
+
"paths or remote image URLs. Use this to show them a report, screenshot, chart, or any file they asked for. An " +
|
|
34
|
+
"image renders inline; any other file shows as a downloadable chip. The path must be inside the session " +
|
|
35
|
+
"workspace. Prefer this over pasting large file contents, describing where a file lives, or markdown image " +
|
|
36
|
+
"syntax (which will not render).",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: "string", description: "Path to the file to send, inside the session workspace (absolute, or relative to it)." },
|
|
41
|
+
caption: { type: "string", description: "Optional short note shown with the attachment." },
|
|
42
|
+
},
|
|
43
|
+
required: ["path"],
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Perform an `attach_to_chat` call by POSTing to the node's attach endpoint,
|
|
50
|
+
* which confines the path to the workspace, stores the bytes, and broadcasts the
|
|
51
|
+
* chip. Never throws — every failure is returned as `{ isError: true, text }` so
|
|
52
|
+
* the agent gets an actionable message instead of a broken tool.
|
|
53
|
+
*/
|
|
54
|
+
export async function runAttachToChat(endpoint, sessionId, args, fetchImpl, token) {
|
|
55
|
+
if (!sessionId)
|
|
56
|
+
return { isError: true, text: "No active Bivy session to attach to (BIVY_SESSION_ID is not set)." };
|
|
57
|
+
const filePath = typeof args.path === "string" ? args.path.trim() : "";
|
|
58
|
+
if (!filePath)
|
|
59
|
+
return { isError: true, text: "Provide a `path` to a file inside the session workspace." };
|
|
60
|
+
const caption = typeof args.caption === "string" ? args.caption : undefined;
|
|
61
|
+
const base = endpoint.replace(/\/+$/, "");
|
|
62
|
+
const headers = { "content-type": "application/json" };
|
|
63
|
+
if (token)
|
|
64
|
+
headers.authorization = `Bearer ${token}`;
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await fetchImpl(`${base}/api/session/${encodeURIComponent(sessionId)}/attach`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body: JSON.stringify({ path: filePath, caption }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return { isError: true, text: `Could not reach the Bivy node to attach the file: ${error instanceof Error ? error.message : String(error)}` };
|
|
75
|
+
}
|
|
76
|
+
const body = (await res.json().catch(() => ({})));
|
|
77
|
+
if (!res.ok || body?.ok === false) {
|
|
78
|
+
return { isError: true, text: `Attach failed: ${body?.error || `node returned ${res.status}`}` };
|
|
79
|
+
}
|
|
80
|
+
const name = body?.name || filePath;
|
|
81
|
+
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.` };
|
|
82
|
+
}
|
|
83
|
+
/** Build the Bivy MCP `Server` with tools/list + tools/call handlers wired. */
|
|
84
|
+
export function createBivyMcpServer(deps = {}) {
|
|
85
|
+
const endpoint = deps.endpoint ?? process.env.BIVY_MCP_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
86
|
+
const sessionId = deps.sessionId ?? process.env.BIVY_SESSION_ID ?? process.env.BIVY_MCP_SESSION ?? "";
|
|
87
|
+
const token = deps.token ?? process.env.BIVY_MCP_TOKEN ?? undefined;
|
|
88
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
89
|
+
const server = new Server({ name: "bivy", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
90
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: BIVY_MCP_TOOLS }));
|
|
91
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
92
|
+
if (req.params.name !== "attach_to_chat") {
|
|
93
|
+
return { isError: true, content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }] };
|
|
94
|
+
}
|
|
95
|
+
const result = await runAttachToChat(endpoint, sessionId, (req.params.arguments ?? {}), fetchImpl, token);
|
|
96
|
+
return { isError: result.isError, content: [{ type: "text", text: result.text }] };
|
|
97
|
+
});
|
|
98
|
+
return server;
|
|
99
|
+
}
|
|
100
|
+
/** Connect the Bivy MCP server to stdio and serve until the stream closes. */
|
|
101
|
+
export async function runMcpServeCli(deps = {}) {
|
|
102
|
+
const server = createBivyMcpServer(deps);
|
|
103
|
+
const transport = new StdioServerTransport();
|
|
104
|
+
await server.connect(transport);
|
|
105
|
+
// Resolves when the transport closes (agent disconnects / process is killed).
|
|
106
|
+
await new Promise((resolve) => transport.onclose = resolve);
|
|
107
|
+
}
|
|
108
|
+
// Run when invoked directly (via `bivy mcp-serve` → tsx this file). Emit nothing
|
|
109
|
+
// to stdout except JSON-RPC — the transport owns stdin/stdout.
|
|
110
|
+
const invokedDirectly = (() => {
|
|
111
|
+
try {
|
|
112
|
+
return Boolean(process.argv[1]) && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
if (invokedDirectly) {
|
|
119
|
+
void runMcpServeCli().catch((error) => {
|
|
120
|
+
process.stderr.write(`bivy mcp-serve: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
3
|
export { IntegrationManager } from "./manager.js";
|
|
4
|
-
export { BUILT_IN_INTEGRATIONS } from "./registry.js";
|
|
4
|
+
export { BUILT_IN_INTEGRATIONS, ATTACH_TO_CHAT_TOOL } from "./registry.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { IntegrationStore } from "./store.js";
|
|
5
|
-
import { BUILT_IN_INTEGRATIONS } from "./registry.js";
|
|
5
|
+
import { ATTACH_TO_CHAT_TOOL, BUILT_IN_INTEGRATIONS } from "./registry.js";
|
|
6
6
|
import { buildAuthorizeUrl, createPkce, exchangeCode, refreshToken, } from "./oauth.js";
|
|
7
7
|
import { SecretVault } from "../secrets.js";
|
|
8
8
|
/**
|
|
@@ -10,9 +10,11 @@ import { SecretVault } from "../secrets.js";
|
|
|
10
10
|
*
|
|
11
11
|
* - REST handlers in server.ts call `list / connectApiKey / startOAuth /
|
|
12
12
|
* completeOAuth / disconnect`.
|
|
13
|
-
* - `toolProvider()` exposes the connected integrations' tools
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* - `toolProvider()` exposes the connected integrations' tools — plus the
|
|
14
|
+
* always-on `attach_to_chat` tool (issue #291), when the daemon wired an
|
|
15
|
+
* `attachToChat` callback — as a runtime-agnostic ToolProvider handed to each
|
|
16
|
+
* session; the tools execute here on the daemon (where credentials live) for
|
|
17
|
+
* in-process AND remote agents alike.
|
|
16
18
|
*/
|
|
17
19
|
export class IntegrationManager {
|
|
18
20
|
store;
|
|
@@ -21,11 +23,15 @@ export class IntegrationManager {
|
|
|
21
23
|
pending = new Map();
|
|
22
24
|
/** Names of tools flagged risky (approval-gated), regardless of connection. */
|
|
23
25
|
riskyTools;
|
|
24
|
-
|
|
26
|
+
/** Backs the native `attach_to_chat` tool (see toolProvider); undefined = the
|
|
27
|
+
* tool isn't offered (e.g. a test harness that never wired one). */
|
|
28
|
+
attachToChat;
|
|
29
|
+
constructor(appDir, registry = BUILT_IN_INTEGRATIONS, attachToChat) {
|
|
25
30
|
this.store = new IntegrationStore(appDir);
|
|
26
31
|
this.secrets = new SecretVault(appDir);
|
|
27
32
|
this.registry = registry;
|
|
28
33
|
this.riskyTools = new Set(registry.flatMap((d) => d.tools.filter((t) => t.risky).map((t) => t.name)));
|
|
34
|
+
this.attachToChat = attachToChat;
|
|
29
35
|
}
|
|
30
36
|
// --- helpers ------------------------------------------------------------
|
|
31
37
|
def(id) {
|
|
@@ -236,15 +242,23 @@ export class IntegrationManager {
|
|
|
236
242
|
}
|
|
237
243
|
// --- agent-agnostic tool provider ---------------------------------------
|
|
238
244
|
/**
|
|
239
|
-
* A runtime-agnostic ToolProvider exposing every connected integration's tools
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
245
|
+
* A runtime-agnostic ToolProvider exposing every connected integration's tools,
|
|
246
|
+
* plus the always-on `attach_to_chat` tool (issue #291) when this manager was
|
|
247
|
+
* built with an `attachToChat` callback. This is the seam the daemon hands to a
|
|
248
|
+
* session (in-process OR remote) so any agent can use the tools without the
|
|
249
|
+
* IntegrationManager knowing which agent it is — the tools execute HERE, on the
|
|
250
|
+
* daemon, where the credentials/HTTP clients live. A snapshot of the connected
|
|
251
|
+
* set is taken per call (at session start), mirroring the previous per-session
|
|
252
|
+
* behavior; disconnected integrations contribute nothing, so the tool surface
|
|
253
|
+
* and system prompt stay clean.
|
|
254
|
+
*
|
|
255
|
+
* `sessionIdRef` resolves the calling session for `attach_to_chat`: the
|
|
256
|
+
* provider is built before the session it will serve exists (see
|
|
257
|
+
* AttachToChatFn's doc), so the caller passes a box and fills `.current` in
|
|
258
|
+
* once the id is known rather than a plain string. Ignored (and the tool
|
|
259
|
+
* omitted) when either it or the attachToChat callback is absent.
|
|
246
260
|
*/
|
|
247
|
-
toolProvider() {
|
|
261
|
+
toolProvider(sessionIdRef) {
|
|
248
262
|
const specs = [];
|
|
249
263
|
const executors = new Map();
|
|
250
264
|
for (const def of this.registry) {
|
|
@@ -266,6 +280,30 @@ export class IntegrationManager {
|
|
|
266
280
|
});
|
|
267
281
|
}
|
|
268
282
|
}
|
|
283
|
+
if (this.attachToChat && sessionIdRef) {
|
|
284
|
+
const attachToChat = this.attachToChat;
|
|
285
|
+
specs.push({
|
|
286
|
+
name: ATTACH_TO_CHAT_TOOL.name,
|
|
287
|
+
label: ATTACH_TO_CHAT_TOOL.label,
|
|
288
|
+
description: ATTACH_TO_CHAT_TOOL.description,
|
|
289
|
+
promptSnippet: ATTACH_TO_CHAT_TOOL.description,
|
|
290
|
+
parameters: ATTACH_TO_CHAT_TOOL.parameters,
|
|
291
|
+
});
|
|
292
|
+
executors.set(ATTACH_TO_CHAT_TOOL.name, async (params) => {
|
|
293
|
+
const sessionId = sessionIdRef.current;
|
|
294
|
+
if (!sessionId)
|
|
295
|
+
return { content: [{ type: "text", text: "Session is not ready yet — try again in a moment." }], details: {}, isError: true };
|
|
296
|
+
const p = (params ?? {});
|
|
297
|
+
const filePath = typeof p.filePath === "string" ? p.filePath.trim() : "";
|
|
298
|
+
if (!filePath)
|
|
299
|
+
return { content: [{ type: "text", text: "filePath is required" }], details: {}, isError: true };
|
|
300
|
+
const caption = typeof p.caption === "string" ? p.caption : undefined;
|
|
301
|
+
const result = attachToChat(sessionId, { filePath, caption });
|
|
302
|
+
if ("error" in result)
|
|
303
|
+
return { content: [{ type: "text", text: result.error }], details: {}, isError: true };
|
|
304
|
+
return { content: [{ type: "text", text: `Attached ${result.ref.name} (${result.ref.kind}, ${result.ref.mimeType}) to the chat.` }], details: { ref: result.ref } };
|
|
305
|
+
});
|
|
306
|
+
}
|
|
269
307
|
return {
|
|
270
308
|
list: () => specs,
|
|
271
309
|
invoke: async (toolName, _toolCallId, params, signal) => {
|
|
@@ -16,6 +16,29 @@ function toRfc822({ to, subject, body }) {
|
|
|
16
16
|
const lines = [`To: ${to}`, `Subject: ${subject}`, "Content-Type: text/plain; charset=UTF-8", "", body];
|
|
17
17
|
return Buffer.from(lines.join("\r\n")).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
18
18
|
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// attach_to_chat (issue #291): the native, tool-based sibling of the CLI
|
|
21
|
+
// `bivy attach <path>` / POST /api/session/:id/attach path #288 added. Unlike
|
|
22
|
+
// the integrations below it needs no auth and isn't gated on a "connected"
|
|
23
|
+
// account — every agent should always have it — so it's declared here as data
|
|
24
|
+
// (name/description/schema, matching every other tool) but wired up by
|
|
25
|
+
// IntegrationManager.toolProvider instead of BUILT_IN_INTEGRATIONS: its
|
|
26
|
+
// executor needs the calling session's id (to push the attachment into the
|
|
27
|
+
// right chat), which the `http`-only IntegrationToolDef.execute shape below
|
|
28
|
+
// has no room for.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export const ATTACH_TO_CHAT_TOOL = {
|
|
31
|
+
name: "attach_to_chat",
|
|
32
|
+
label: "Attach to chat",
|
|
33
|
+
description: "Push a file or image from the session workspace into the chat as an attachment, exactly like the CLI `bivy attach` " +
|
|
34
|
+
"or the composer's paperclip upload but as a direct tool call. Use this — not markdown image syntax, not describing " +
|
|
35
|
+
"where a file lives — whenever the user should see a report, screenshot, chart, or a file they asked for; they " +
|
|
36
|
+
"cannot see files you only write to disk. The path must be inside the session workspace.",
|
|
37
|
+
parameters: Type.Object({
|
|
38
|
+
filePath: Type.String({ description: "Path to the file, absolute or relative to the session workspace." }),
|
|
39
|
+
caption: Type.Optional(Type.String({ description: "Short caption shown next to the attachment in the chat." })),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
19
42
|
export const BUILT_IN_INTEGRATIONS = [
|
|
20
43
|
{
|
|
21
44
|
id: "notion",
|
|
@@ -22,6 +22,7 @@ import fs from "node:fs";
|
|
|
22
22
|
import { depCacheEnv } from "../harness/dep-cache.js";
|
|
23
23
|
import os from "node:os";
|
|
24
24
|
import path from "node:path";
|
|
25
|
+
import { z } from "zod";
|
|
25
26
|
import { sandboxTier, claudePermissionModeFor } from "../harness/sandbox.js";
|
|
26
27
|
import { autoAttachToolImagesEnabled, PassiveImageBudget } from "../harness/tool-image-attachments.js";
|
|
27
28
|
import { anthropicCredentialPreflight, describeAnthropicError, isAnthropicAuthError } from "./anthropic-preflight.js";
|
|
@@ -73,6 +74,46 @@ export const BIVY_ATTACH_SYSTEM_PROMPT = "Sending files and images to the user:
|
|
|
73
74
|
"An image renders inline in the chat; any other file shows as a downloadable chip. The path must be inside the session " +
|
|
74
75
|
"workspace. Do NOT use markdown image syntax like  to show a local file or a URL — it will not render; always " +
|
|
75
76
|
"use `bivy attach`. Prefer this over pasting large file contents or describing where a file lives on disk.";
|
|
77
|
+
/** Name of the in-process MCP server the native attach tool is registered
|
|
78
|
+
* under (see buildAttachMcpServer) — the SDK namespaces the tool the agent
|
|
79
|
+
* sees as `mcp__<server>__<tool>`. */
|
|
80
|
+
export const BIVY_ATTACH_MCP_SERVER_NAME = "bivy";
|
|
81
|
+
/** The tool's own name, unnamespaced (see BIVY_ATTACH_MCP_SERVER_NAME). */
|
|
82
|
+
export const BIVY_ATTACH_TOOL_NAME = "attach_to_chat";
|
|
83
|
+
/**
|
|
84
|
+
* Build the in-process MCP server that exposes `attach_to_chat` as a native
|
|
85
|
+
* tool call (issue #291) — the stronger sibling of BIVY_ATTACH_SYSTEM_PROMPT's
|
|
86
|
+
* shell-out hint: the agent sees this in its actual tool list instead of having
|
|
87
|
+
* to discover a shell command from prose. Bound to one session's id so the
|
|
88
|
+
* handler always attaches into the conversation that called it, regardless of
|
|
89
|
+
* how many Claude sessions this node is running concurrently.
|
|
90
|
+
*
|
|
91
|
+
* `sdk` is the already-loaded SDK module (see loadSdk) — `tool`/
|
|
92
|
+
* createSdkMcpServer are read off it dynamically for the same reason the rest
|
|
93
|
+
* of this adapter never imports SDK values statically: the package is
|
|
94
|
+
* optional, and a static import would force every Bivy install to have it.
|
|
95
|
+
* Returns undefined if this SDK build doesn't export the MCP builder helpers
|
|
96
|
+
* (older/trimmed installs) — the caller degrades to prompt-only discoverability.
|
|
97
|
+
*/
|
|
98
|
+
function buildAttachMcpServer(sdk, sessionId, attachToChat) {
|
|
99
|
+
if (typeof sdk?.tool !== "function" || typeof sdk?.createSdkMcpServer !== "function")
|
|
100
|
+
return undefined;
|
|
101
|
+
const attachTool = sdk.tool(BIVY_ATTACH_TOOL_NAME, "Push a file or image from the session workspace into the chat as an attachment, exactly like the CLI `bivy attach` " +
|
|
102
|
+
"or the composer's paperclip upload but as a direct tool call. Use this — not markdown image syntax, not describing " +
|
|
103
|
+
"where a file lives — whenever the user should see a report, screenshot, chart, or a file they asked for; they " +
|
|
104
|
+
"cannot see files you only write to disk. The path must be inside the session workspace.", {
|
|
105
|
+
filePath: z.string().describe("Path to the file, absolute or relative to the session workspace."),
|
|
106
|
+
caption: z.string().optional().describe("Short caption shown next to the attachment in the chat."),
|
|
107
|
+
}, async (args) => {
|
|
108
|
+
const result = attachToChat(sessionId, { filePath: args.filePath, caption: args.caption });
|
|
109
|
+
if ("error" in result)
|
|
110
|
+
return { content: [{ type: "text", text: result.error }], isError: true };
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: `Attached ${result.ref.name} (${result.ref.kind}, ${result.ref.mimeType}) to the chat.` }],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return sdk.createSdkMcpServer({ name: BIVY_ATTACH_MCP_SERVER_NAME, tools: [attachTool] });
|
|
116
|
+
}
|
|
76
117
|
export function claudeRuntimeFromEnv() {
|
|
77
118
|
return {
|
|
78
119
|
defaultModel: process.env.BIVY_CLAUDE_MODEL?.trim() || undefined,
|
|
@@ -737,6 +778,9 @@ class ClaudeSession {
|
|
|
737
778
|
// Keep the default Claude Code prompt, appending the note that teaches the
|
|
738
779
|
// agent how to send a file to the user (`bivy attach`) — otherwise the
|
|
739
780
|
// capability is undiscoverable and "send me X as an attachment" fails.
|
|
781
|
+
// Kept even when the native tool below is also registered: it's a cheap,
|
|
782
|
+
// harmless fallback for a shell/subprocess the agent spawns that can't
|
|
783
|
+
// reach the in-process MCP tool directly.
|
|
740
784
|
systemPrompt: { type: "preset", preset: "claude_code", append: BIVY_ATTACH_SYSTEM_PROMPT },
|
|
741
785
|
};
|
|
742
786
|
if (resumeId)
|
|
@@ -745,6 +789,16 @@ class ClaudeSession {
|
|
|
745
789
|
options.sessionId = this.id;
|
|
746
790
|
if (this.desiredModel)
|
|
747
791
|
options.model = this.desiredModel;
|
|
792
|
+
// Native attach_to_chat tool (issue #291) — the stronger, tool-based sibling
|
|
793
|
+
// of the system-prompt hint above. Wired only when the daemon handed us a
|
|
794
|
+
// callback (see ClaudeCodeRuntimeOptions.attachToChat); absent in a few
|
|
795
|
+
// deliberately minimal test harnesses, and gracefully degrades to the prompt
|
|
796
|
+
// hint alone if this SDK build lacks the MCP builder helpers.
|
|
797
|
+
if (this.runtimeOptions.attachToChat) {
|
|
798
|
+
const attachServer = buildAttachMcpServer(sdk, this.id, this.runtimeOptions.attachToChat);
|
|
799
|
+
if (attachServer)
|
|
800
|
+
options.mcpServers = { [BIVY_ATTACH_MCP_SERVER_NAME]: attachServer };
|
|
801
|
+
}
|
|
748
802
|
const q = sdk.query({ prompt: this.input, options });
|
|
749
803
|
this.query = q;
|
|
750
804
|
void this.consume(q);
|
package/dist/runtime/index.js
CHANGED
|
@@ -1457,7 +1457,7 @@ export function makeRuntime(options) {
|
|
|
1457
1457
|
case "claude-code-sdk":
|
|
1458
1458
|
// Share the node's provider logins (the shared vault) so the user doesn't
|
|
1459
1459
|
// re-auth Anthropic for this agent.
|
|
1460
|
-
return new ClaudeCodeRuntime({ ...claudeRuntimeFromEnv(), credentials: createCredentialStore(options.credsDir), sandbox: options.sandbox });
|
|
1460
|
+
return new ClaudeCodeRuntime({ ...claudeRuntimeFromEnv(), credentials: createCredentialStore(options.credsDir), sandbox: options.sandbox, attachToChat: options.attachToChat });
|
|
1461
1461
|
default:
|
|
1462
1462
|
// Every CLI agent in CLI_AGENT_SPECS is dispatched here as data — no per-id
|
|
1463
1463
|
// case to maintain. Anything that isn't a known CLI agent throws below.
|
package/dist/server.js
CHANGED
|
@@ -55,7 +55,7 @@ import { evictToCap, dirSizeBytes } from "./harness/cache-evict.js";
|
|
|
55
55
|
import { checkDiskAdmission } from "./harness/disk-admission.js";
|
|
56
56
|
import { sandboxTier, setConfiguredSandboxTier, normalizeSandboxTier } from "./harness/sandbox.js";
|
|
57
57
|
import { setConfiguredAutoAttachToolImages } from "./harness/tool-image-attachments.js";
|
|
58
|
-
import { injectMcpProxyForSession } from "./harness/mcp-inject.js";
|
|
58
|
+
import { injectMcpProxyForSession, injectBivyToolsForSession } from "./harness/mcp-inject.js";
|
|
59
59
|
import { parseRepo, inferGitHubRepoFromWorkspace, isSharedCloneRoot, resolveGitHubToken, cloneOrUpdateRepo, resolveDefaultBaseRef, resolveBranchBaseRef, fetchOrigin } from "./repo-workspace.js";
|
|
60
60
|
import { configureGitAuth, writeGitCredentialEndpoint } from "./git-auth.js";
|
|
61
61
|
import { GitHubTaskPoller, resolveGitHubTaskConfig, buildTaskPrompt, buildResumePrompt, buildInteractiveResumePrompt, DEFAULT_ISSUE_INSTRUCTIONS, parseBivyDirectives, commitAll, pushBranch, mergeBaseIntoBranch, completeMerge, abortMerge, findOpenPullRequestForBranch, findPullRequestsForBranch, findMergedPullRequestForBranch, issueBranchName, getPullRequest, commentIssue, listOpenLabelledIssues, selectActionableIssues, getIssue, getIssueCommentBody, addLabel, removeLabel, announcePickup, } from "./github-tasks.js";
|
|
@@ -484,7 +484,7 @@ function resolveApproval(id, approved) {
|
|
|
484
484
|
}
|
|
485
485
|
return ok;
|
|
486
486
|
}
|
|
487
|
-
const integrations = new IntegrationManager(appDir);
|
|
487
|
+
const integrations = new IntegrationManager(appDir, undefined, attachToChatForSession);
|
|
488
488
|
const terminals = new TerminalManager();
|
|
489
489
|
// Per-session agents: a node holds one AgentRuntime instance *per agent id*,
|
|
490
490
|
// built lazily and cached, instead of a single global runtime. `defaultRuntimeId`
|
|
@@ -492,7 +492,7 @@ const terminals = new TerminalManager();
|
|
|
492
492
|
// be swapped under a live conversation, so the agent is chosen at session creation
|
|
493
493
|
// and fixed for that session's life; switching agents in the UI starts a new one.
|
|
494
494
|
let defaultRuntimeId = (process.env.BIVY_RUNTIME ?? "pi").toLowerCase();
|
|
495
|
-
const runtimeHost = new RuntimeHost({ credsDir, piDir, sessionsDir });
|
|
495
|
+
const runtimeHost = new RuntimeHost({ credsDir, piDir, sessionsDir, attachToChat: attachToChatForSession });
|
|
496
496
|
// In-session model reroute (docs/rulesets.md). Opt-in: set
|
|
497
497
|
// BIVY_SESSION_MODEL_FALLBACK to a comma-separated model list and a session that
|
|
498
498
|
// hits an exhausted-credits / rate-limit turn error swaps down the list (via the
|
|
@@ -1129,6 +1129,22 @@ function handlePassiveToolImage(record, event) {
|
|
|
1129
1129
|
console.warn("[attachments] failed to store a passively-surfaced tool image:", result.error);
|
|
1130
1130
|
}
|
|
1131
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Session-id-keyed wrapper around attachToChat, handed to runtime adapters as
|
|
1134
|
+
* the `attachToChat` callback that backs each agent's native "attach to chat"
|
|
1135
|
+
* tool surface (Claude's SDK tool, Pi's ToolProvider tool — issue #291). Those
|
|
1136
|
+
* tools are wired at runtime/tool-provider construction time, before the
|
|
1137
|
+
* specific session that will run them exists — a per-session circular
|
|
1138
|
+
* dependency (build the tools -> need the session -> need the tools) — so the
|
|
1139
|
+
* callback takes a session id and resolves the live record from openSessions
|
|
1140
|
+
* when it actually fires, exactly like the HTTP endpoint below does by path.
|
|
1141
|
+
*/
|
|
1142
|
+
function attachToChatForSession(sessionId, opts) {
|
|
1143
|
+
const record = openSessions.get(sessionId);
|
|
1144
|
+
if (!record)
|
|
1145
|
+
return { error: "Session not found" };
|
|
1146
|
+
return attachToChat(record, opts);
|
|
1147
|
+
}
|
|
1132
1148
|
function approvalModeFrom(value) {
|
|
1133
1149
|
return value === "never" || value === "risky" || value === "always" || value === "autonomous" ? value : undefined;
|
|
1134
1150
|
}
|
|
@@ -6825,7 +6841,9 @@ async function refreshRecordAfterTui(record) {
|
|
|
6825
6841
|
record.unsubscribe = undefined;
|
|
6826
6842
|
const rt = await ensureRuntimeAvailable(record.runtimeId);
|
|
6827
6843
|
const workspace = record.worktree?.path || oldSession.cwd || record.workspace;
|
|
6828
|
-
|
|
6844
|
+
// Refreshing an EXISTING record: its id is already known, so attach_to_chat
|
|
6845
|
+
// (see toolProvider's SessionIdRef doc) can be wired live, not deferred.
|
|
6846
|
+
const runtimeSessionOptions = { workspace, toolProvider: integrations.toolProvider({ current: record.id }), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
|
|
6829
6847
|
const { session, warning } = await runtimeHost.openSession(rt, { ...runtimeSessionOptions, sessionFile: record.sessionFile });
|
|
6830
6848
|
record.session = session;
|
|
6831
6849
|
record.sessionFile = session.sessionFile ?? record.sessionFile;
|
|
@@ -7340,7 +7358,12 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
7340
7358
|
throw new Error(`Refusing to start a session in the shared clone root ${runtimeWorkspace} without an isolated worktree — ` +
|
|
7341
7359
|
`this would collide with concurrent sessions on the same repo. Retry; if it persists the checkout may be busy.`);
|
|
7342
7360
|
}
|
|
7343
|
-
|
|
7361
|
+
// A brand-new session's id isn't known until runtimeHost.{create,open}Session
|
|
7362
|
+
// resolves below, but the ToolProvider (and its attach_to_chat tool) must be
|
|
7363
|
+
// built now, up front — so hand it this box instead of a session id and fill
|
|
7364
|
+
// `.current` in the moment `sessionId` is (see toolProvider's SessionIdRef doc).
|
|
7365
|
+
const attachSessionIdRef = {};
|
|
7366
|
+
const runtimeSessionOptions = { workspace: runtimeWorkspace, toolProvider: integrations.toolProvider(attachSessionIdRef), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
|
|
7344
7367
|
// Stage 2/3: prefer re-attaching to a still-live remote session — routed to its
|
|
7345
7368
|
// OWN agent service — over re-opening a fresh copy from disk. Falls back to
|
|
7346
7369
|
// open/create when nothing live is there.
|
|
@@ -7358,6 +7381,10 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
7358
7381
|
? await runtimeHost.openSession(rt, { ...runtimeSessionOptions, sessionFile: requestedSessionFile })
|
|
7359
7382
|
: await runtimeHost.createSession(rt, runtimeSessionOptions));
|
|
7360
7383
|
const sessionId = session.id;
|
|
7384
|
+
// Now that it's known, unblock any attach_to_chat call this session's agent
|
|
7385
|
+
// makes (see attachSessionIdRef above) — set synchronously, well before any
|
|
7386
|
+
// prompt (and so any tool call) can reach this session.
|
|
7387
|
+
attachSessionIdRef.current = sessionId;
|
|
7361
7388
|
// Resuming an existing session: restore Bivy's canonical name onto the runtime
|
|
7362
7389
|
// session when the runtime didn't itself (the Claude Code adapter resumes by id
|
|
7363
7390
|
// and starts nameless). Without this getName() is undefined, so opening a
|
|
@@ -7411,6 +7438,36 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
7411
7438
|
// Injection is best-effort; never block session creation.
|
|
7412
7439
|
}
|
|
7413
7440
|
}
|
|
7441
|
+
// Bivy-owned tools (attach_to_chat, …): make them discoverable to non-SDK
|
|
7442
|
+
// agents by adding a `bivy` server (run via `bivy mcp-serve`) to the agent's
|
|
7443
|
+
// config. Default-on (not gated on BIVY_MCP_PROXY) — it only ADDS a safe,
|
|
7444
|
+
// session-scoped, restored capability the agent already has via env, so the
|
|
7445
|
+
// chat can receive files. Claude/Pi expose these natively (in-process SDK MCP
|
|
7446
|
+
// server / integration ToolProvider), so the tool-interception runtimes are
|
|
7447
|
+
// skipped to avoid a duplicate registration.
|
|
7448
|
+
if (!rt.capabilities.toolInterception) {
|
|
7449
|
+
try {
|
|
7450
|
+
const res = injectBivyToolsForSession(rt.id, {
|
|
7451
|
+
workspace: sessionWorkspace,
|
|
7452
|
+
home: os.homedir(),
|
|
7453
|
+
sessionId,
|
|
7454
|
+
endpoint: process.env.BIVY_MCP_ENDPOINT,
|
|
7455
|
+
});
|
|
7456
|
+
if (res.injected.length) {
|
|
7457
|
+
const prev = record.mcpRestore;
|
|
7458
|
+
record.mcpRestore = () => { try {
|
|
7459
|
+
res.restore();
|
|
7460
|
+
}
|
|
7461
|
+
finally {
|
|
7462
|
+
prev?.();
|
|
7463
|
+
} };
|
|
7464
|
+
console.log(`MCP tools: added bivy server to ${res.injected.length} config(s) for session ${sessionId}`);
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
catch {
|
|
7468
|
+
// Best-effort; never block session creation.
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7414
7471
|
rememberSession(record);
|
|
7415
7472
|
rememberWorkspace(sessionWorkspace);
|
|
7416
7473
|
attachSessionListeners(record);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivy/bivy",
|
|
3
|
-
"version": "0.5.1-staging.
|
|
3
|
+
"version": "0.5.1-staging.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"express": "^5.2.1",
|
|
38
38
|
"node-pty": "^1.1.0",
|
|
39
39
|
"typebox": "^1.3.6",
|
|
40
|
-
"ws": "^8.21.1"
|
|
40
|
+
"ws": "^8.21.1",
|
|
41
|
+
"zod": "^4.0.0"
|
|
41
42
|
},
|
|
42
43
|
"overrides": {
|
|
43
44
|
"@hono/node-server": "2.0.12",
|