@bondsmith/bondsmithd 0.1.7 → 0.1.8

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.
Files changed (2) hide show
  1. package/dist/index.js +234 -31
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { createServer } from "http";
5
5
  import { existsSync } from "fs";
6
6
  import { hostname as hostname2 } from "os";
7
- import { dirname as dirname4, join as join7 } from "path";
7
+ import { dirname as dirname4, join as join8 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // ../../packages/local-agent-protocol/src/session.ts
@@ -16,7 +16,7 @@ var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
16
16
  var INVALID_MESSAGE_ERROR = "invalid message";
17
17
 
18
18
  // ../../packages/local-agent-protocol/src/protocol-version.ts
19
- var PROTOCOL_VERSION = 4;
19
+ var PROTOCOL_VERSION = 7;
20
20
 
21
21
  // ../../packages/local-agent-protocol/src/project-path.ts
22
22
  var PROJECT_PATH_INVALID_REASONS = [
@@ -60,6 +60,8 @@ var signedConnectionChallengeSchema = z.object({
60
60
  var MAX_INSERT_TEXT_BYTES = 32 * 1024;
61
61
  var MAX_INPUT_CHARS = 16 * 1024;
62
62
  var MAX_WIRE_MESSAGE_BYTES = 256 * 1024;
63
+ var MAX_DAEMON_REPLY_BYTES = 200 * 1024;
64
+ var MAX_RANGE_LINES = 500;
63
65
  var MAX_TERMINAL_DIMENSION = 1e3;
64
66
  var terminalSizeSchema = z2.object({
65
67
  cols: z2.number().int().min(1).max(MAX_TERMINAL_DIMENSION),
@@ -95,14 +97,32 @@ var workspaceStatusSchema = z2.object({
95
97
  branch: z2.string().nullable(),
96
98
  // .catch(undefined): a malformed informational field must never drop the whole status message.
97
99
  baseRef: z2.string().nullable().optional().catch(void 0),
98
- files: z2.array(workspaceFileEntrySchema)
100
+ files: z2.array(workspaceFileEntrySchema),
101
+ totalFiles: z2.number().int().nonnegative().optional().catch(void 0)
99
102
  });
100
103
  var workspaceFileDiffSchema = z2.object({
101
104
  path: z2.string(),
102
105
  staged: z2.boolean(),
103
106
  patch: z2.string().nullable(),
104
- binary: z2.boolean()
107
+ binary: z2.boolean(),
108
+ tooLarge: z2.boolean().optional(),
109
+ version: z2.string().optional()
105
110
  });
111
+ var workspaceFileRangeSchema = z2.discriminatedUnion("ok", [
112
+ z2.object({
113
+ ok: z2.literal(true),
114
+ path: z2.string(),
115
+ startLine: z2.number().int().min(1),
116
+ lines: z2.array(z2.string()).max(MAX_RANGE_LINES),
117
+ totalLines: z2.number().int().nonnegative(),
118
+ version: z2.string()
119
+ }),
120
+ z2.object({
121
+ ok: z2.literal(false),
122
+ path: z2.string(),
123
+ reason: z2.enum(["changed", "missing", "binary", "too_large", "not_allowed"])
124
+ })
125
+ ]);
106
126
  var projectPathValidationSchema = z2.discriminatedUnion("valid", [
107
127
  z2.object({ valid: z2.literal(true), path: z2.string() }),
108
128
  z2.object({ valid: z2.literal(false), reason: z2.enum(PROJECT_PATH_INVALID_REASONS) })
@@ -166,6 +186,15 @@ var browserMessageSchema = z2.discriminatedUnion("type", [
166
186
  sessionId: z2.string(),
167
187
  requestId: z2.string().min(1).max(200)
168
188
  }),
189
+ z2.object({
190
+ type: z2.literal("workspace.readFileRange"),
191
+ sessionId: z2.string(),
192
+ path: z2.string().min(1).max(4096),
193
+ startLine: z2.number().int().min(1),
194
+ lineCount: z2.number().int().min(1).max(MAX_RANGE_LINES),
195
+ version: z2.string().min(1).max(100),
196
+ requestId: z2.string().min(1).max(200)
197
+ }),
169
198
  z2.object({
170
199
  type: z2.literal("workspace.getDiff"),
171
200
  sessionId: z2.string(),
@@ -250,6 +279,12 @@ var daemonMessageSchema = z2.discriminatedUnion("type", [
250
279
  requestId: z2.string(),
251
280
  status: workspaceStatusSchema
252
281
  }),
282
+ z2.object({
283
+ type: z2.literal("workspace.fileRange"),
284
+ sessionId: z2.string(),
285
+ requestId: z2.string(),
286
+ range: workspaceFileRangeSchema
287
+ }),
253
288
  z2.object({
254
289
  type: z2.literal("workspace.diff"),
255
290
  sessionId: z2.string(),
@@ -370,6 +405,7 @@ var SIGNABLE_ACTIONS = [
370
405
  "session.list",
371
406
  "workspace.getStatus",
372
407
  "workspace.getDiff",
408
+ "workspace.readFileRange",
373
409
  "project.validatePath"
374
410
  ];
375
411
  var envelopeSchema = z3.object({
@@ -954,7 +990,7 @@ import { sign } from "crypto";
954
990
  import WebSocket from "ws";
955
991
 
956
992
  // src/version.ts
957
- var DAEMON_VERSION = true ? "0.1.7" : "dev";
993
+ var DAEMON_VERSION = true ? "0.1.8" : "dev";
958
994
  var VERSION_FLAGS = ["--version", "-v"];
959
995
  function versionLine() {
960
996
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -1378,12 +1414,42 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
1378
1414
  function error(message, requestId) {
1379
1415
  return { type: "error", message, requestId };
1380
1416
  }
1417
+ function fitDiffReply(reply) {
1418
+ const { patch, tooLarge } = reply.diff;
1419
+ const fits = (patch === null || patch.length <= MAX_DAEMON_REPLY_BYTES) && Buffer.byteLength(JSON.stringify(reply)) <= MAX_DAEMON_REPLY_BYTES;
1420
+ if (tooLarge || fits) return reply;
1421
+ return { ...reply, diff: { ...reply.diff, patch: null, tooLarge: true } };
1422
+ }
1423
+ function fitStatusReply(reply) {
1424
+ if (Buffer.byteLength(JSON.stringify(reply)) <= MAX_DAEMON_REPLY_BYTES) return reply;
1425
+ const { files } = reply.status;
1426
+ const bare = { ...reply, status: { ...reply.status, files: [], totalFiles: files.length } };
1427
+ let budget = MAX_DAEMON_REPLY_BYTES - Buffer.byteLength(JSON.stringify(bare));
1428
+ const kept = [];
1429
+ for (const file of files) {
1430
+ const cost = Buffer.byteLength(JSON.stringify(file)) + 1;
1431
+ if (cost > budget) break;
1432
+ budget -= cost;
1433
+ kept.push(file);
1434
+ }
1435
+ return { ...bare, status: { ...bare.status, files: kept } };
1436
+ }
1381
1437
  function errorMessageOf(err, fallback) {
1382
1438
  return err instanceof Error ? err.message : fallback;
1383
1439
  }
1384
1440
  function requestIdOf(message) {
1385
1441
  return "requestId" in message ? message.requestId : void 0;
1386
1442
  }
1443
+ function stringRequestId(value) {
1444
+ if (typeof value !== "object" || value === null || !("requestId" in value)) return void 0;
1445
+ const { requestId } = value;
1446
+ return typeof requestId === "string" && requestId !== "" ? requestId.slice(0, 200) : void 0;
1447
+ }
1448
+ function requestIdOfUnparsed(raw) {
1449
+ const envelope = typeof raw === "object" && raw !== null && "envelope" in raw ? raw.envelope : void 0;
1450
+ const payload = typeof envelope === "object" && envelope !== null && "payload" in envelope ? envelope.payload : void 0;
1451
+ return stringRequestId(payload) ?? stringRequestId(raw);
1452
+ }
1387
1453
  function getSessionOrError(sessionManager, conn, sessionId, requestId) {
1388
1454
  const session = sessionManager.get(sessionId);
1389
1455
  if (!session) conn.send(error("unknown session", requestId));
@@ -1395,7 +1461,7 @@ async function handleBrowserMessage(conn, raw, deps) {
1395
1461
  const { envelope } = signedCommand.data;
1396
1462
  const innerMessage = browserMessageSchema.safeParse(envelope.payload);
1397
1463
  if (!innerMessage.success || innerMessage.data.type !== envelope.action) {
1398
- conn.send(error(INVALID_MESSAGE_ERROR));
1464
+ conn.send(error(INVALID_MESSAGE_ERROR, requestIdOfUnparsed(raw)));
1399
1465
  return;
1400
1466
  }
1401
1467
  const admitted = await conn.admitInOrder(async () => {
@@ -1421,7 +1487,7 @@ async function handleBrowserMessage(conn, raw, deps) {
1421
1487
  }
1422
1488
  const parsed = browserMessageSchema.safeParse(raw);
1423
1489
  if (!parsed.success) {
1424
- conn.send(error(INVALID_MESSAGE_ERROR));
1490
+ conn.send(error(INVALID_MESSAGE_ERROR, requestIdOfUnparsed(raw)));
1425
1491
  return;
1426
1492
  }
1427
1493
  if (parsed.data.type === "connection.authenticate") {
@@ -1449,6 +1515,7 @@ async function dispatch(conn, message, {
1449
1515
  broadcast,
1450
1516
  getWorkspaceStatus: getWorkspaceStatus2,
1451
1517
  getWorkspaceDiff: getWorkspaceDiff2,
1518
+ readFileRange: readFileRange2,
1452
1519
  validateProjectPath: validateProjectPath2,
1453
1520
  hostname: hostname3,
1454
1521
  daemonFingerprint,
@@ -1584,12 +1651,14 @@ async function dispatch(conn, message, {
1584
1651
  session.cwd,
1585
1652
  sessionManager.getBaseline(session.id)
1586
1653
  );
1587
- conn.send({
1588
- type: "workspace.status",
1589
- sessionId: message.sessionId,
1590
- requestId: message.requestId,
1591
- status
1592
- });
1654
+ conn.send(
1655
+ fitStatusReply({
1656
+ type: "workspace.status",
1657
+ sessionId: message.sessionId,
1658
+ requestId: message.requestId,
1659
+ status
1660
+ })
1661
+ );
1593
1662
  } catch (err) {
1594
1663
  conn.send(error(errorMessageOf(err, "failed to read workspace status"), message.requestId));
1595
1664
  }
@@ -1605,14 +1674,38 @@ async function dispatch(conn, message, {
1605
1674
  message.staged,
1606
1675
  sessionManager.getBaseline(session.id)
1607
1676
  );
1677
+ conn.send(
1678
+ fitDiffReply({
1679
+ type: "workspace.diff",
1680
+ sessionId: message.sessionId,
1681
+ requestId: message.requestId,
1682
+ diff
1683
+ })
1684
+ );
1685
+ } catch (err) {
1686
+ conn.send(error(errorMessageOf(err, "failed to read workspace diff"), message.requestId));
1687
+ }
1688
+ return;
1689
+ }
1690
+ case "workspace.readFileRange": {
1691
+ const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
1692
+ if (!session) return;
1693
+ try {
1694
+ const range = await readFileRange2(
1695
+ session.cwd,
1696
+ message.path,
1697
+ message.startLine,
1698
+ message.lineCount,
1699
+ message.version
1700
+ );
1608
1701
  conn.send({
1609
- type: "workspace.diff",
1702
+ type: "workspace.fileRange",
1610
1703
  sessionId: message.sessionId,
1611
1704
  requestId: message.requestId,
1612
- diff
1705
+ range
1613
1706
  });
1614
1707
  } catch (err) {
1615
- conn.send(error(errorMessageOf(err, "failed to read workspace diff"), message.requestId));
1708
+ conn.send(error(errorMessageOf(err, "failed to read file range"), message.requestId));
1616
1709
  }
1617
1710
  return;
1618
1711
  }
@@ -2552,9 +2645,99 @@ function presentPendingAuthorizationOnTerminal(store) {
2552
2645
  };
2553
2646
  }
2554
2647
 
2648
+ // src/sessions/files/read-range.ts
2649
+ import { constants } from "fs";
2650
+ import { lstat, open, realpath, stat as stat2 } from "fs/promises";
2651
+ import { isAbsolute as isAbsolute2, join as join5, sep } from "path";
2652
+ var MAX_RANGE_FILE_BYTES = 2 * 1024 * 1024;
2653
+ var REPLY_ENVELOPE_BYTES = 8 * 1024;
2654
+ var BINARY_SNIFF_BYTES = 8e3;
2655
+ function fileVersion(stats) {
2656
+ return `${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.ino}`;
2657
+ }
2658
+ async function locateInside(cwd, path) {
2659
+ if (isAbsolute2(path)) return { refused: "not_allowed" };
2660
+ try {
2661
+ const root = await realpath(cwd);
2662
+ const lexical = join5(root, path);
2663
+ if ((await lstat(lexical)).isSymbolicLink()) return { refused: "not_allowed" };
2664
+ const real = await realpath(lexical);
2665
+ return real === root || real.startsWith(root + sep) ? { found: real } : { refused: "missing" };
2666
+ } catch {
2667
+ return { refused: "missing" };
2668
+ }
2669
+ }
2670
+ async function readExactly(handle, size) {
2671
+ const buffer = Buffer.allocUnsafe(size + 1);
2672
+ let filled = 0;
2673
+ while (filled < buffer.length) {
2674
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
2675
+ if (bytesRead === 0) break;
2676
+ filled += bytesRead;
2677
+ }
2678
+ return filled === size ? buffer.subarray(0, size) : void 0;
2679
+ }
2680
+ async function currentFileVersion(cwd, path) {
2681
+ const located = await locateInside(cwd, path);
2682
+ if (!("found" in located)) return void 0;
2683
+ try {
2684
+ return fileVersion(await stat2(located.found));
2685
+ } catch {
2686
+ return void 0;
2687
+ }
2688
+ }
2689
+ function splitLines(text) {
2690
+ if (text === "") return [];
2691
+ const lines = text.split("\n");
2692
+ if (lines[lines.length - 1] === "") lines.pop();
2693
+ return lines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
2694
+ }
2695
+ async function readFileRange(cwd, path, startLine, lineCount, version) {
2696
+ const refuse = (reason) => ({ ok: false, path, reason });
2697
+ const located = await locateInside(cwd, path);
2698
+ if (!("found" in located)) return refuse(located.refused);
2699
+ let content;
2700
+ let handle;
2701
+ try {
2702
+ handle = await open(
2703
+ located.found,
2704
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
2705
+ );
2706
+ const before = await handle.stat();
2707
+ if (!before.isFile()) return refuse("not_allowed");
2708
+ if (fileVersion(before) !== version) return refuse("changed");
2709
+ if (before.size > MAX_RANGE_FILE_BYTES) return refuse("too_large");
2710
+ const bytes = await readExactly(handle, before.size);
2711
+ if (bytes === void 0 || fileVersion(await handle.stat()) !== version) {
2712
+ return refuse("changed");
2713
+ }
2714
+ content = bytes;
2715
+ } catch {
2716
+ return refuse("missing");
2717
+ } finally {
2718
+ await handle?.close();
2719
+ }
2720
+ if (content.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return refuse("binary");
2721
+ const lines = splitLines(content.toString("utf8"));
2722
+ const page = [];
2723
+ let budget = MAX_DAEMON_REPLY_BYTES - REPLY_ENVELOPE_BYTES;
2724
+ const wanted = Math.min(lineCount, MAX_RANGE_LINES);
2725
+ for (let index = startLine - 1; index < lines.length && page.length < wanted; index++) {
2726
+ const line = lines[index];
2727
+ const cost = Buffer.byteLength(JSON.stringify(line)) + 1;
2728
+ if (cost > budget) {
2729
+ if (page.length === 0) return refuse("too_large");
2730
+ break;
2731
+ }
2732
+ budget -= cost;
2733
+ page.push(line);
2734
+ }
2735
+ return { ok: true, path, startLine, lines: page, totalLines: lines.length, version };
2736
+ }
2737
+
2555
2738
  // src/sessions/git/baseline.ts
2556
- import { lstat } from "fs/promises";
2557
- import { join as join5 } from "path";
2739
+ import { lstat as lstat2 } from "fs/promises";
2740
+ import { join as join6 } from "path";
2558
2741
 
2559
2742
  // src/sessions/git/base.ts
2560
2743
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -2595,7 +2778,7 @@ var MAX_PREEXISTING_FILES = 200;
2595
2778
  var DELETED = "deleted";
2596
2779
  async function fingerprintFile(cwd, path) {
2597
2780
  try {
2598
- const stats = await lstat(join5(cwd, path));
2781
+ const stats = await lstat2(join6(cwd, path));
2599
2782
  return `${stats.size}:${Math.trunc(stats.mtimeMs)}`;
2600
2783
  } catch {
2601
2784
  return DELETED;
@@ -2852,8 +3035,12 @@ async function isUntracked(cwd, path) {
2852
3035
  const out = await runGit(["ls-files", "--others", "--exclude-standard", "--", path], cwd);
2853
3036
  return out.trim() !== "";
2854
3037
  }
3038
+ function isOutputOverflow(err) {
3039
+ return err?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
3040
+ }
2855
3041
  async function getWorkspaceDiff(cwd, path, staged, baseline) {
2856
3042
  const base = await resolveBaseFor(cwd, baseline);
3043
+ const version = await currentFileVersion(cwd, path);
2857
3044
  const pathspec = await withRenameSource(cwd, base.sha, path);
2858
3045
  const numstat = await runGitDiff(
2859
3046
  ["--relative", "--numstat", "-M", base.sha, "--", ...pathspec],
@@ -2863,13 +3050,27 @@ async function getWorkspaceDiff(cwd, path, staged, baseline) {
2863
3050
  if (binary) {
2864
3051
  return { path, staged, patch: null, binary: true };
2865
3052
  }
2866
- let patch = await runGitDiff(["--relative", "-M", base.sha, "--", ...pathspec], cwd);
2867
- if (patch.trim() === "" && await isUntracked(cwd, path)) {
2868
- patch = await runGitDiffAllowingExitCodes(["--no-index", "--", "/dev/null", path], cwd, [
2869
- 1
2870
- ]).catch(() => "");
3053
+ try {
3054
+ let patch = await runGitDiff(["--relative", "-M", base.sha, "--", ...pathspec], cwd);
3055
+ if (patch.trim() === "" && await isUntracked(cwd, path)) {
3056
+ patch = await runGitDiffAllowingExitCodes(["--no-index", "--", "/dev/null", path], cwd, [
3057
+ 1
3058
+ ]).catch((err) => {
3059
+ if (isOutputOverflow(err)) throw err;
3060
+ return "";
3061
+ });
3062
+ }
3063
+ return {
3064
+ path,
3065
+ staged,
3066
+ patch: patch || null,
3067
+ binary: false,
3068
+ ...version === void 0 ? {} : { version }
3069
+ };
3070
+ } catch (err) {
3071
+ if (isOutputOverflow(err)) return { path, staged, patch: null, binary: false, tooLarge: true };
3072
+ throw err;
2871
3073
  }
2872
- return { path, staged, patch: patch || null, binary: false };
2873
3074
  }
2874
3075
 
2875
3076
  // src/ws/server.ts
@@ -3092,7 +3293,7 @@ function resolveSessionAttention(report, lastActivityAt, nowMs) {
3092
3293
  // src/sessions/tmux/create.ts
3093
3294
  import { access } from "fs/promises";
3094
3295
  import { homedir as homedir5 } from "os";
3095
- import { join as join6 } from "path";
3296
+ import { join as join7 } from "path";
3096
3297
  var InvalidCwdError = class extends Error {
3097
3298
  constructor(cwd) {
3098
3299
  super(`working directory does not exist: ${cwd}`);
@@ -3100,7 +3301,7 @@ var InvalidCwdError = class extends Error {
3100
3301
  }
3101
3302
  };
3102
3303
  function worktreePathFor(sessionId) {
3103
- return join6(homedir5(), ".bondsmith", "worktrees", sessionId);
3304
+ return join7(homedir5(), ".bondsmith", "worktrees", sessionId);
3104
3305
  }
3105
3306
  async function createTmuxSession(input) {
3106
3307
  const sourceCwd = expandHome(input.cwd);
@@ -3484,6 +3685,7 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
3484
3685
  broadcast,
3485
3686
  getWorkspaceStatus,
3486
3687
  getWorkspaceDiff,
3688
+ readFileRange,
3487
3689
  validateProjectPath,
3488
3690
  hostname: () => hostname(),
3489
3691
  daemonFingerprint,
@@ -3524,10 +3726,10 @@ if (isVersionRequest(process.argv.slice(2))) {
3524
3726
  process.exit(0);
3525
3727
  }
3526
3728
  function loadRepoRootEnv() {
3527
- const repoRoot = join7(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
3528
- if (!existsSync(join7(repoRoot, "pnpm-workspace.yaml"))) return;
3529
- const envLocal = join7(repoRoot, ".env.local");
3530
- const envDefault = join7(repoRoot, ".env");
3729
+ const repoRoot = join8(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
3730
+ if (!existsSync(join8(repoRoot, "pnpm-workspace.yaml"))) return;
3731
+ const envLocal = join8(repoRoot, ".env.local");
3732
+ const envDefault = join8(repoRoot, ".env");
3531
3733
  loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
3532
3734
  }
3533
3735
  loadRepoRootEnv();
@@ -3581,6 +3783,7 @@ async function start() {
3581
3783
  ptyAttachments,
3582
3784
  getWorkspaceStatus,
3583
3785
  getWorkspaceDiff,
3786
+ readFileRange,
3584
3787
  validateProjectPath,
3585
3788
  hostname: hostname2,
3586
3789
  pendingAuthorizations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "description": "Local daemon for Bondsmith agent sessions — runs on the user's own machine, pairs with a Bondsmith account, and exposes tmux-backed terminal sessions to the Bondsmith web app over a signed WebSocket protocol.",
6
6
  "homepage": "https://bondsmith.dev",