@bivy/bivy 0.18.9-staging.12 → 0.18.9-staging.13
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.
|
@@ -12,36 +12,71 @@ import path from "node:path";
|
|
|
12
12
|
* account-wide copy rides in the E2E model-auth vault envelope (see server.ts),
|
|
13
13
|
* and a synced import stamps the file with the writer's timestamp so every
|
|
14
14
|
* machine compares like with like. Clearing is an empty file, not a delete, so
|
|
15
|
-
* the empty state carries a timestamp and propagates too.
|
|
15
|
+
* the empty state carries a timestamp and propagates too. The one switch beside
|
|
16
|
+
* the text — whether sessions also get the Bivy note — lives in a small sidecar
|
|
17
|
+
* file and shares that clock: flipping it rewrites AGENTS.md's timestamp.
|
|
16
18
|
*
|
|
17
19
|
* Delivery is the caller's business; this module only produces the composed
|
|
18
20
|
* text (and a file holding it, for agents that take a path).
|
|
19
21
|
*/
|
|
20
22
|
/** Cap on the editable text. It rides on every turn of every session. */
|
|
21
23
|
export const MAX_AGENT_INSTRUCTIONS_BYTES = 16 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* What every agent session is told about running under Bivy, whether or not the
|
|
26
|
+
* user wrote instructions: that the user is in a chat, and which `bivy` commands
|
|
27
|
+
* reach it. The commands read $BIVY_SESSION_ID (src/runtime/session-env.ts), so
|
|
28
|
+
* this works for any agent with a shell. Keep it short: it rides on every turn.
|
|
29
|
+
*/
|
|
30
|
+
export const BIVY_AGENT_NOTE = [
|
|
31
|
+
"You are running inside Bivy. The user follows this session in a chat app (web or phone): they cannot see your " +
|
|
32
|
+
"terminal or files you only write to disk. $BIVY_SESSION_ID identifies this session; the `bivy` commands below use it.",
|
|
33
|
+
"- Show a LOCAL file or image (report, screenshot, chart, a file they asked for): run " +
|
|
34
|
+
'`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it. Images render ' +
|
|
35
|
+
"inline, other files as downloads; the path must be inside the session workspace. Markdown image syntax like " +
|
|
36
|
+
" does not render a local path. A REMOTE `https://` image in markdown does render.",
|
|
37
|
+
"- Something with a UI: `bivy app publish <manifest.json>` (a web server's port, a static build, a terminal) or " +
|
|
38
|
+
"`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
|
|
39
|
+
"can check your work; `bivy app present` tells the user a visible change is ready to look at. " +
|
|
40
|
+
"`bivy app --help` has the details.",
|
|
41
|
+
].join("\n");
|
|
22
42
|
const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
|
|
23
43
|
"instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
|
|
24
44
|
export function agentInstructionsPath(appDir) {
|
|
25
45
|
return path.join(appDir, "AGENTS.md");
|
|
26
46
|
}
|
|
47
|
+
function optionsPath(appDir) {
|
|
48
|
+
return path.join(appDir, "agent-instructions.json");
|
|
49
|
+
}
|
|
50
|
+
function readBivyNote(appDir) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(optionsPath(appDir), "utf8")).bivyNote !== false;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
27
58
|
function composedPath(appDir) {
|
|
28
59
|
return path.join(appDir, "agent-instructions.composed.md");
|
|
29
60
|
}
|
|
30
61
|
export function readAgentInstructions(appDir) {
|
|
62
|
+
const bivyNote = readBivyNote(appDir);
|
|
31
63
|
try {
|
|
32
64
|
const file = agentInstructionsPath(appDir);
|
|
33
|
-
return { text: fs.readFileSync(file, "utf8"), updatedAt: Math.trunc(fs.statSync(file).mtimeMs) };
|
|
65
|
+
return { text: fs.readFileSync(file, "utf8"), updatedAt: Math.trunc(fs.statSync(file).mtimeMs), bivyNote };
|
|
34
66
|
}
|
|
35
67
|
catch {
|
|
36
|
-
return { text: "", updatedAt: 0 };
|
|
68
|
+
return { text: "", updatedAt: 0, bivyNote };
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
|
-
|
|
71
|
+
/** Write the text (and optionally the Bivy-note switch), stamped `updatedAt`. */
|
|
72
|
+
export function writeAgentInstructions(appDir, text, updatedAt = Date.now(), bivyNote) {
|
|
40
73
|
if (Buffer.byteLength(text, "utf8") > MAX_AGENT_INSTRUCTIONS_BYTES) {
|
|
41
74
|
throw new Error(`Agent instructions are limited to ${MAX_AGENT_INSTRUCTIONS_BYTES / 1024} KB.`);
|
|
42
75
|
}
|
|
43
76
|
const file = agentInstructionsPath(appDir);
|
|
44
77
|
fs.mkdirSync(appDir, { recursive: true });
|
|
78
|
+
if (bivyNote !== undefined)
|
|
79
|
+
fs.writeFileSync(optionsPath(appDir), `${JSON.stringify({ bivyNote })}\n`);
|
|
45
80
|
fs.writeFileSync(file, text);
|
|
46
81
|
fs.utimesSync(file, new Date(updatedAt), new Date(updatedAt));
|
|
47
82
|
return readAgentInstructions(appDir);
|
|
@@ -51,6 +86,8 @@ export function writeAgentInstructions(appDir, text, updatedAt = Date.now()) {
|
|
|
51
86
|
* - "imported": the synced copy was newer and is now the local file.
|
|
52
87
|
* - "local-newer": this machine holds a newer copy the account should get.
|
|
53
88
|
* - "unchanged": nothing to do.
|
|
89
|
+
* A synced copy without `bivyNote` comes from a peer that predates the switch,
|
|
90
|
+
* where the note was always on.
|
|
54
91
|
*/
|
|
55
92
|
export function mergeSyncedAgentInstructions(appDir, incoming) {
|
|
56
93
|
const local = readAgentInstructions(appDir);
|
|
@@ -58,26 +95,29 @@ export function mergeSyncedAgentInstructions(appDir, incoming) {
|
|
|
58
95
|
if (typeof synced?.text !== "string" || typeof synced.updatedAt !== "number") {
|
|
59
96
|
return local.updatedAt > 0 ? "local-newer" : "unchanged";
|
|
60
97
|
}
|
|
61
|
-
|
|
98
|
+
const bivyNote = synced.bivyNote !== false;
|
|
99
|
+
if (synced.text === local.text && bivyNote === local.bivyNote)
|
|
62
100
|
return "unchanged";
|
|
63
101
|
if (synced.updatedAt > local.updatedAt) {
|
|
64
|
-
writeAgentInstructions(appDir, synced.text, synced.updatedAt);
|
|
102
|
+
writeAgentInstructions(appDir, synced.text, synced.updatedAt, bivyNote);
|
|
65
103
|
return "imported";
|
|
66
104
|
}
|
|
67
105
|
return "local-newer";
|
|
68
106
|
}
|
|
69
|
-
/**
|
|
70
|
-
export function composeAgentInstructions(text) {
|
|
107
|
+
/** The Bivy note (unless switched off), then the user's text (if any) under the precedence preamble. */
|
|
108
|
+
export function composeAgentInstructions(text, bivyNote = true) {
|
|
71
109
|
const body = text.trim();
|
|
72
|
-
return body ? `${PREAMBLE}\n\n${body}
|
|
110
|
+
return [bivyNote ? BIVY_AGENT_NOTE : "", body ? `${PREAMBLE}\n\n${body}` : ""].filter(Boolean).map((part) => `${part}\n`).join("\n");
|
|
73
111
|
}
|
|
74
112
|
/**
|
|
75
|
-
* The instructions for a session starting now, or undefined when
|
|
76
|
-
*
|
|
113
|
+
* The instructions for a session starting now, or undefined when there is
|
|
114
|
+
* nothing to send (no text and the Bivy note switched off) or the file can't be
|
|
115
|
+
* written. Keeps a composed copy on disk (rewritten only when it changes) for
|
|
77
116
|
* agents that take a file path rather than text.
|
|
78
117
|
*/
|
|
79
118
|
export function sessionInstructions(appDir) {
|
|
80
|
-
const text =
|
|
119
|
+
const { text: userText, bivyNote } = readAgentInstructions(appDir);
|
|
120
|
+
const text = composeAgentInstructions(userText, bivyNote);
|
|
81
121
|
const file = composedPath(appDir);
|
|
82
122
|
try {
|
|
83
123
|
if (!text) {
|
|
@@ -86,6 +126,7 @@ export function sessionInstructions(appDir) {
|
|
|
86
126
|
fs.rmSync(file, { force: true });
|
|
87
127
|
return undefined;
|
|
88
128
|
}
|
|
129
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
89
130
|
let current;
|
|
90
131
|
try {
|
|
91
132
|
current = fs.readFileSync(file, "utf8");
|
|
@@ -82,30 +82,6 @@ function brandClaudeModel(model) {
|
|
|
82
82
|
const family = CLAUDE_MODEL_FAMILIES.find((f) => model.name === f || model.name.startsWith(`${f} `));
|
|
83
83
|
return family ? { ...model, name: `Claude ${model.name}` } : model;
|
|
84
84
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Appended to the Claude Code system prompt so the agent DISCOVERS the outbound
|
|
87
|
-
* attachment capability. `bivy attach` is just a shell command — without this the
|
|
88
|
-
* agent has no way to know it exists and, when asked to "send a file", concludes
|
|
89
|
-
* it can't (it looks for a tool, finds none). BIVY_SESSION_ID is injected into the
|
|
90
|
-
* subprocess env (see spawnQuery), so the bare command resolves the session. Keep
|
|
91
|
-
* this short: it rides on every turn's system prompt.
|
|
92
|
-
*
|
|
93
|
-
* The chat still has no route to a LOCAL/workspace file path, so that half of the
|
|
94
|
-
* guidance (use `bivy attach`, not markdown, for those) stands. A REMOTE
|
|
95
|
-
* `https://` image URL is different: the node now fetches it server-side and
|
|
96
|
-
* serves it back to the chat (see src/session/inline-image-fetch.ts, issue #293),
|
|
97
|
-
* so plain markdown is the right tool there — `bivy attach` only works on files
|
|
98
|
-
* already inside the workspace, which a URL by definition isn't.
|
|
99
|
-
*/
|
|
100
|
-
export const BIVY_ATTACH_SYSTEM_PROMPT = "Sending files and images to the user: the person you're talking to is in a chat UI. They cannot see files you only " +
|
|
101
|
-
"write to disk, and the chat has no route to a workspace file path. " +
|
|
102
|
-
"To show them a LOCAL file or image — a report, screenshot, chart, or a file they asked for — run " +
|
|
103
|
-
'`bivy attach <path> [--caption "short note"]` in your shell. ' +
|
|
104
|
-
"An image renders inline in the chat; any other file shows as a downloadable chip. The path must be inside the session " +
|
|
105
|
-
"workspace. Do NOT use markdown image syntax like  for a local file or workspace path — it will not render; " +
|
|
106
|
-
"always use `bivy attach` for those. A REMOTE image you already have a URL for is different: plain markdown " +
|
|
107
|
-
"`` renders it inline, no attach needed. Prefer `bivy attach` / markdown links over pasting large " +
|
|
108
|
-
"file contents or describing where a file lives on disk.";
|
|
109
85
|
/** Name of the in-process MCP server the native attach tool is registered
|
|
110
86
|
* under (see buildAttachMcpServer) — the SDK namespaces the tool the agent
|
|
111
87
|
* sees as `mcp__<server>__<tool>`. */
|
|
@@ -114,8 +90,8 @@ export const BIVY_ATTACH_MCP_SERVER_NAME = "bivy";
|
|
|
114
90
|
export const BIVY_ATTACH_TOOL_NAME = "attach_to_chat";
|
|
115
91
|
/**
|
|
116
92
|
* Build the in-process MCP server that exposes `attach_to_chat` as a native
|
|
117
|
-
* tool call (issue #291) — the stronger sibling of
|
|
118
|
-
*
|
|
93
|
+
* tool call (issue #291) — the stronger sibling of the Bivy note's
|
|
94
|
+
* `bivy attach` hint: the agent sees this in its actual tool list instead of having
|
|
119
95
|
* to discover a shell command from prose. Bound to one session's id so the
|
|
120
96
|
* handler always attaches into the conversation that called it, regardless of
|
|
121
97
|
* how many Claude sessions this node is running concurrently.
|
|
@@ -877,15 +853,11 @@ class ClaudeSession {
|
|
|
877
853
|
permissionMode,
|
|
878
854
|
canUseTool,
|
|
879
855
|
env,
|
|
880
|
-
// Keep the default Claude Code prompt, appending the
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
// reach the in-process MCP tool directly.
|
|
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") },
|
|
856
|
+
// Keep the default Claude Code prompt, appending the session instructions:
|
|
857
|
+
// the Bivy note (how to reach the user's chat — `bivy attach`, previews)
|
|
858
|
+
// and the user's account-wide instructions. The repo's CLAUDE.md still
|
|
859
|
+
// loads through the preset as usual.
|
|
860
|
+
systemPrompt: { type: "preset", preset: "claude_code", ...(this.runtimeOptions.instructions ? { append: this.runtimeOptions.instructions } : {}) },
|
|
889
861
|
...(this.runtimeOptions.executablePath ? { pathToClaudeCodeExecutable: this.runtimeOptions.executablePath } : {}),
|
|
890
862
|
};
|
|
891
863
|
if (resumeId)
|
|
@@ -895,7 +867,7 @@ class ClaudeSession {
|
|
|
895
867
|
if (this.desiredModel)
|
|
896
868
|
options.model = this.desiredModel;
|
|
897
869
|
// Native attach_to_chat tool (issue #291) — the stronger, tool-based sibling
|
|
898
|
-
// of the
|
|
870
|
+
// of the Bivy note's `bivy attach` line. Wired only when the daemon handed us a
|
|
899
871
|
// callback (see ClaudeCodeRuntimeOptions.attachToChat); absent in a few
|
|
900
872
|
// deliberately minimal test harnesses, and gracefully degrades to the prompt
|
|
901
873
|
// hint alone if this SDK build lacks the MCP builder helpers.
|
package/dist/apps/styles.css
CHANGED
|
@@ -2863,6 +2863,8 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
|
|
|
2863
2863
|
.node-menu-glyph { color: var(--accent); flex: none; }
|
|
2864
2864
|
.eph-steps { margin: 4px 0; padding-left: 18px; color: var(--muted); font-size: 13px; display: grid; gap: 4px; }
|
|
2865
2865
|
.code-snippet { margin: 6px 0 0; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px; background: var(--surface); font-size: 12px; }
|
|
2866
|
+
/* Settings → Agent instructions: Bivy's note, wrapped to the sheet's width. */
|
|
2867
|
+
.agent-note-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
2866
2868
|
select.picker-search { appearance: auto; }
|
|
2867
2869
|
.node-menu-sep { height: 1px; background: var(--line); margin: 4px 0; }
|
|
2868
2870
|
/* PrToast, UpdatePrompt (both `.update-toast`), and ErrorToast used to each
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// own shell can resolve its chat session without being told the id.
|
|
5
5
|
// `bivy attach <path>` (bin/bivy.mjs's cmdAttach) reads $BIVY_SESSION_ID to know
|
|
6
6
|
// which session to post an outbound attachment to — see
|
|
7
|
-
//
|
|
7
|
+
// BIVY_AGENT_NOTE (src/agent-instructions.ts) for the discoverability half of this
|
|
8
8
|
// feature (issue #288 shipped attach; issue #290 is making it universal). One
|
|
9
9
|
// helper, one env-var name, so a new adapter can't independently invent — or
|
|
10
10
|
// simply forget — its own convention.
|
package/dist/server.js
CHANGED
|
@@ -72,7 +72,7 @@ import { createSessionEngine } from "./session/engine.js";
|
|
|
72
72
|
import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportSyncableProviderAuth, exportProviderAuthTombstones, importProviderAuth, removeProvider, setProviderApiKey, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, setCredentialUnattended, exportUnattendedRecords, unattendedCredentialRevision, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, exportSyncableRecords, exportRecordTombstones, importCredentialRecords, reconcileHostedCredentialRecords } from "./credentials/api.js";
|
|
73
73
|
import { listProviders } from "./runtime/provider-catalog.js";
|
|
74
74
|
import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
|
|
75
|
-
import { mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
|
|
75
|
+
import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
|
|
76
76
|
import { execEphemeralRequest } from "./ephemeral-exec.js";
|
|
77
77
|
import { ApprovalManager } from "./approval.js";
|
|
78
78
|
import { QuestionManager, validQuestions, isAskUserQuestionTool, formatQuestionResult } from "./question.js";
|
|
@@ -1374,7 +1374,7 @@ function nodeSettingsSnapshot() {
|
|
|
1374
1374
|
autoAttachToolImages: readSettings().autoAttachToolImages === true,
|
|
1375
1375
|
appScreenshots: appScreenshotsEnabled(),
|
|
1376
1376
|
forkWorkspaceMaxBytes: Number.isInteger(readSettings().forkWorkspaceMaxBytes) ? Number(readSettings().forkWorkspaceMaxBytes) : 50 * 1024 * 1024,
|
|
1377
|
-
agentInstructions: { ...readAgentInstructions(appDir), maxBytes: MAX_AGENT_INSTRUCTIONS_BYTES },
|
|
1377
|
+
agentInstructions: { ...readAgentInstructions(appDir), maxBytes: MAX_AGENT_INSTRUCTIONS_BYTES, bivyNoteText: BIVY_AGENT_NOTE },
|
|
1378
1378
|
};
|
|
1379
1379
|
}
|
|
1380
1380
|
async function applyNodeSettings(patch) {
|
|
@@ -1389,6 +1389,11 @@ async function applyNodeSettings(patch) {
|
|
|
1389
1389
|
writeAgentInstructions(appDir, patch.agentInstructions);
|
|
1390
1390
|
void pushModelAuthToControlPlane();
|
|
1391
1391
|
}
|
|
1392
|
+
// The Bivy-note switch syncs with the text: keep the text, stamp a new time.
|
|
1393
|
+
if (typeof patch.agentInstructionsBivyNote === "boolean") {
|
|
1394
|
+
writeAgentInstructions(appDir, readAgentInstructions(appDir).text, Date.now(), patch.agentInstructionsBivyNote);
|
|
1395
|
+
void pushModelAuthToControlPlane();
|
|
1396
|
+
}
|
|
1392
1397
|
const settings = readSettings();
|
|
1393
1398
|
if (typeof patch.name === "string" && patch.name.trim()) {
|
|
1394
1399
|
const prev = identity.name;
|
package/package.json
CHANGED