@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
+ "![](path) 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
- export function writeAgentInstructions(appDir, text, updatedAt = Date.now()) {
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
- if (synced.text === local.text)
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
- /** Wrap the user's text in the precedence preamble; "" when there is nothing to send. */
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}\n` : "";
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 the user has
76
- * none. Keeps a composed copy on disk (rewritten only when it changes) for
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 = composeAgentInstructions(readAgentInstructions(appDir).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 ![](path) 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
- "`![alt](https://...)` 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 BIVY_ATTACH_SYSTEM_PROMPT's
118
- * shell-out hint: the agent sees this in its actual tool list instead of having
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 note that teaches the
881
- // agent how to send a file to the user (`bivy attach`) — otherwise the
882
- // capability is undiscoverable and "send me X as an attachment" fails.
883
- // Kept even when the native tool below is also registered: it's a cheap,
884
- // harmless fallback for a shell/subprocess the agent spawns that can't
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 system-prompt hint above. Wired only when the daemon handed us a
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.
@@ -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
- // claude-code.ts's BIVY_ATTACH_SYSTEM_PROMPT for the discoverability half of this
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.9-staging.12",
3
+ "version": "0.18.9-staging.13",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",