@bondsmith/bondsmithd 0.1.13 → 0.1.14

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 +156 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,13 +10,14 @@ import { fileURLToPath } from "url";
10
10
  // ../../packages/local-agent-protocol/src/session.ts
11
11
  var SESSION_STATUSES = ["running", "exited"];
12
12
  var AGENT_STATES = ["working", "waiting", "done"];
13
+ var MAX_SKIPPED_FILES_REPORTED = 20;
13
14
 
14
15
  // ../../packages/local-agent-protocol/src/messages.ts
15
16
  var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
16
17
  var INVALID_MESSAGE_ERROR = "invalid message";
17
18
 
18
19
  // ../../packages/local-agent-protocol/src/protocol-version.ts
19
- var PROTOCOL_VERSION = 16;
20
+ var PROTOCOL_VERSION = 17;
20
21
 
21
22
  // ../../packages/local-agent-protocol/src/branch-name.ts
22
23
  var MAX_BRANCH_NAME_LENGTH = 250;
@@ -204,6 +205,7 @@ var localSessionSchema = z3.object({
204
205
  // .catch(undefined): a state this version doesn't know must never drop the whole session message.
205
206
  agentState: z3.enum(AGENT_STATES).nullable().optional().catch(void 0),
206
207
  worktreeSetupFailed: z3.boolean().optional().catch(void 0),
208
+ worktreeSetupSkipped: z3.array(z3.string().max(300)).max(MAX_SKIPPED_FILES_REPORTED).optional().catch(void 0),
207
209
  headline: z3.string().nullable().optional().catch(void 0)
208
210
  });
209
211
  var workspaceFileEntrySchema = z3.object({
@@ -1184,7 +1186,7 @@ function expandHome(cwd) {
1184
1186
  }
1185
1187
 
1186
1188
  // src/sessions/git/client.ts
1187
- import { execFile as execFileCb } from "child_process";
1189
+ import { execFile as execFileCb, spawn as spawn2 } from "child_process";
1188
1190
  import { promisify } from "util";
1189
1191
  var execFile = promisify(execFileCb);
1190
1192
  var MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
@@ -1239,6 +1241,43 @@ function runGitDiff(args, cwd) {
1239
1241
  function runGitDiffAllowingExitCodes(args, cwd, allowedExitCodes) {
1240
1242
  return runGitAllowingExitCodes(["diff", ...SAFE_DIFF_FLAGS, ...args], cwd, allowedExitCodes);
1241
1243
  }
1244
+ var GIT_INPUT_TIMEOUT_MS = 3e4;
1245
+ async function runGitWithInput(args, cwd, input, options = {}) {
1246
+ const { allowedExitCodes = [], timeoutMs = GIT_INPUT_TIMEOUT_MS } = options;
1247
+ return new Promise((resolve2, reject) => {
1248
+ const child = spawn2("git", [...SAFE_GIT_CONFIG, ...args], { cwd });
1249
+ const stdout = [];
1250
+ const stderr = [];
1251
+ let settled = false;
1252
+ const settle = (finish) => {
1253
+ if (settled) return;
1254
+ settled = true;
1255
+ clearTimeout(timer);
1256
+ finish();
1257
+ };
1258
+ const timer = setTimeout(() => {
1259
+ child.kill("SIGKILL");
1260
+ settle(() => reject(new Error(`git ${args[0]} timed out after ${timeoutMs} ms`)));
1261
+ }, timeoutMs);
1262
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
1263
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
1264
+ child.stdin.on("error", () => {
1265
+ });
1266
+ child.on("error", (error2) => settle(() => reject(error2)));
1267
+ child.on(
1268
+ "close",
1269
+ (code) => settle(() => {
1270
+ if (code === 0 || code !== null && allowedExitCodes.includes(code)) {
1271
+ resolve2(Buffer.concat(stdout).toString("utf8"));
1272
+ return;
1273
+ }
1274
+ const message = Buffer.concat(stderr).toString("utf8").trim();
1275
+ reject(new Error(`git ${args[0]} exited with ${code}${message ? `: ${message}` : ""}`));
1276
+ })
1277
+ );
1278
+ child.stdin.end(input);
1279
+ });
1280
+ }
1242
1281
 
1243
1282
  // src/sessions/files/plan-file.ts
1244
1283
  import { constants as constants2 } from "fs";
@@ -2229,7 +2268,7 @@ function listInstalledAgents(env = process.env) {
2229
2268
  }
2230
2269
 
2231
2270
  // src/version.ts
2232
- var DAEMON_VERSION = true ? "0.1.13" : "dev";
2271
+ var DAEMON_VERSION = true ? "0.1.14" : "dev";
2233
2272
  var VERSION_FLAGS = ["--version", "-v"];
2234
2273
  function versionLine() {
2235
2274
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -3934,11 +3973,25 @@ function wrapCommandWithSetup(input) {
3934
3973
  ].join(" ");
3935
3974
  }
3936
3975
 
3976
+ // src/sessions/worktree-setup/skipped-files.ts
3977
+ var SETUP_SKIPPED_OPTION = "@bondsmith-setup-skipped";
3978
+ function encodeSkippedFiles(names) {
3979
+ return names.slice(0, MAX_SKIPPED_FILES_REPORTED).map(encodeURIComponent).join(",");
3980
+ }
3981
+ function decodeSkippedFiles(raw) {
3982
+ if (raw === "") return [];
3983
+ try {
3984
+ return raw.split(",").map(decodeURIComponent);
3985
+ } catch {
3986
+ return [];
3987
+ }
3988
+ }
3989
+
3937
3990
  // src/sessions/tmux/agent-state.ts
3938
3991
  var AGENT_STATE_OPTION = "@bondsmith-agent-state";
3939
3992
  var HEADLINE_OPTION = "@bondsmith-headline";
3940
3993
  var MAX_HEADLINE_CHARS = 200;
3941
- var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${HEADLINE_OPTION}}`;
3994
+ var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${SETUP_SKIPPED_OPTION}} #{${HEADLINE_OPTION}}`;
3942
3995
  var AGENT_STATE_OUTPUT_GRACE_MS = 2e3;
3943
3996
  var FUTURE_REPORT_ALLOWANCE_MS = 2e3;
3944
3997
  var WORKING_SILENCE_MS = 12e4;
@@ -3956,12 +4009,20 @@ function isAgentState(value) {
3956
4009
  function parseLiveSessionReports(output) {
3957
4010
  const reports = /* @__PURE__ */ new Map();
3958
4011
  for (const line of output.split("\n")) {
3959
- const [sessionName, agentState = "", bell = "", setupFailed = "", ...headline] = line.split(" ");
4012
+ const [
4013
+ sessionName,
4014
+ agentState = "",
4015
+ bell = "",
4016
+ setupFailed = "",
4017
+ setupSkipped = "",
4018
+ ...headline
4019
+ ] = line.split(" ");
3960
4020
  if (sessionName) {
3961
4021
  reports.set(sessionName, {
3962
4022
  agentState,
3963
4023
  bell: bell === "1",
3964
4024
  setupFailed: setupFailed === "1",
4025
+ setupSkipped: decodeSkippedFiles(setupSkipped),
3965
4026
  headline: cleanHeadline(headline.join(" "))
3966
4027
  });
3967
4028
  }
@@ -4190,19 +4251,74 @@ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
4190
4251
  import { constants as constants4 } from "fs";
4191
4252
  import { copyFile, glob, lstat as lstat3, mkdir as mkdir5, realpath as realpath2 } from "fs/promises";
4192
4253
  import { dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative, sep as sep2 } from "path";
4254
+ var GIT_EXIT_NONE_IGNORED = 1;
4255
+ var TRACKED_CHECK_BATCH = 200;
4193
4256
  function isInside(root, path) {
4194
4257
  const rel = relative(root, path);
4195
4258
  return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
4196
4259
  }
4260
+ async function ignoredPaths(worktree, paths) {
4261
+ if (paths.length === 0) return /* @__PURE__ */ new Set();
4262
+ const output = await runGitWithInput(
4263
+ ["check-ignore", "--stdin", "-z"],
4264
+ worktree,
4265
+ paths.map((path) => `${path}\0`).join(""),
4266
+ { allowedExitCodes: [GIT_EXIT_NONE_IGNORED] }
4267
+ );
4268
+ return new Set(output.split("\0").filter((path) => path !== ""));
4269
+ }
4270
+ async function trackedPaths(worktree, paths) {
4271
+ const tracked = /* @__PURE__ */ new Set();
4272
+ for (let start2 = 0; start2 < paths.length; start2 += TRACKED_CHECK_BATCH) {
4273
+ const output = await runGit(
4274
+ [
4275
+ "--literal-pathspecs",
4276
+ "ls-files",
4277
+ "-z",
4278
+ "--",
4279
+ ...paths.slice(start2, start2 + TRACKED_CHECK_BATCH)
4280
+ ],
4281
+ worktree
4282
+ );
4283
+ for (const path of output.split("\0")) if (path !== "") tracked.add(path);
4284
+ }
4285
+ return tracked;
4286
+ }
4197
4287
  async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4198
- const result = { copied: [], failed: 0 };
4288
+ const result = { copied: [], failed: 0, skipped: [] };
4199
4289
  const patterns = globs.filter(isSafeCopyGlob);
4200
4290
  if (patterns.length === 0) return result;
4201
4291
  try {
4202
4292
  const source = await realpath2(sourceRoot);
4203
4293
  const worktree = await realpath2(worktreeRoot);
4294
+ const candidates = [];
4204
4295
  for await (const match of glob(patterns, { cwd: source })) {
4205
4296
  if (match.split(sep2).includes(".git")) continue;
4297
+ try {
4298
+ if (await isCopyCandidate(source, worktree, match)) candidates.push(match);
4299
+ } catch (error2) {
4300
+ result.failed += 1;
4301
+ console.error("Failed to inspect a file to copy into the worktree", { match, error: error2 });
4302
+ }
4303
+ }
4304
+ let tracked;
4305
+ let ignored;
4306
+ try {
4307
+ tracked = await trackedPaths(worktree, candidates);
4308
+ ignored = await ignoredPaths(worktree, candidates);
4309
+ } catch (error2) {
4310
+ result.failed += candidates.length;
4311
+ console.error("Could not ask git what the worktree tracks and ignores; copying nothing", {
4312
+ error: error2
4313
+ });
4314
+ return result;
4315
+ }
4316
+ for (const match of candidates) {
4317
+ if (tracked.has(match)) continue;
4318
+ if (!ignored.has(match)) {
4319
+ result.skipped.push(match);
4320
+ continue;
4321
+ }
4206
4322
  try {
4207
4323
  if (await copyOne(source, worktree, match)) result.copied.push(match);
4208
4324
  } catch (error2) {
@@ -4216,15 +4332,24 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4216
4332
  }
4217
4333
  return result;
4218
4334
  }
4219
- async function copyOne(source, worktree, match) {
4335
+ async function isCopyCandidate(source, worktree, match) {
4220
4336
  const from = join10(source, match);
4221
4337
  if (!(await lstat3(from)).isFile()) return false;
4222
4338
  if (!isInside(source, await realpath2(from))) return false;
4339
+ try {
4340
+ await lstat3(join10(worktree, match));
4341
+ return false;
4342
+ } catch (error2) {
4343
+ if (error2.code === "ENOENT") return true;
4344
+ throw error2;
4345
+ }
4346
+ }
4347
+ async function copyOne(source, worktree, match) {
4223
4348
  const to = join10(worktree, match);
4224
4349
  await mkdir5(dirname4(to), { recursive: true });
4225
4350
  if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
4226
4351
  try {
4227
- await copyFile(from, to, constants4.COPYFILE_EXCL);
4352
+ await copyFile(join10(source, match), to, constants4.COPYFILE_EXCL);
4228
4353
  } catch (error2) {
4229
4354
  if (error2.code === "EEXIST") return false;
4230
4355
  throw error2;
@@ -4247,6 +4372,7 @@ async function createTmuxSession(input) {
4247
4372
  let cwd = sourceCwd;
4248
4373
  let worktreeOf = null;
4249
4374
  let setupFailed = false;
4375
+ let skippedFiles = [];
4250
4376
  if (input.worktreeBranch) {
4251
4377
  const worktreePath = worktreePathFor(input.id);
4252
4378
  const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
@@ -4260,6 +4386,7 @@ async function createTmuxSession(input) {
4260
4386
  input.worktreeSetup.copyGlobs
4261
4387
  );
4262
4388
  setupFailed = copy.failed > 0;
4389
+ skippedFiles = copy.skipped;
4263
4390
  }
4264
4391
  }
4265
4392
  try {
@@ -4297,6 +4424,15 @@ async function createTmuxSession(input) {
4297
4424
  await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
4298
4425
  }
4299
4426
  if (setupFailed) await runTmux(["set-option", "-t", tmuxSessionName, SETUP_FAILED_OPTION, "1"]);
4427
+ if (skippedFiles.length > 0) {
4428
+ await runTmux([
4429
+ "set-option",
4430
+ "-t",
4431
+ tmuxSessionName,
4432
+ SETUP_SKIPPED_OPTION,
4433
+ encodeSkippedFiles(skippedFiles)
4434
+ ]);
4435
+ }
4300
4436
  if (baseline) {
4301
4437
  await runTmux([
4302
4438
  "set-option",
@@ -4319,7 +4455,10 @@ async function createTmuxSession(input) {
4319
4455
  lastActivityAt: null,
4320
4456
  changedFileCount: null,
4321
4457
  worktreeOf,
4322
- ...setupFailed && { worktreeSetupFailed: true }
4458
+ ...setupFailed && { worktreeSetupFailed: true },
4459
+ ...skippedFiles.length > 0 && {
4460
+ worktreeSetupSkipped: skippedFiles.slice(0, MAX_SKIPPED_FILES_REPORTED)
4461
+ }
4323
4462
  };
4324
4463
  return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
4325
4464
  }
@@ -4515,17 +4654,15 @@ ${error2.message}
4515
4654
  session,
4516
4655
  this.baselines.get(session.id)
4517
4656
  );
4657
+ const report = agentReports.get(session.tmuxSessionName);
4518
4658
  const updated = {
4519
4659
  ...enriched,
4520
- agentState: resolveSessionAttention(
4521
- agentReports.get(session.tmuxSessionName),
4522
- enriched.lastActivityAt,
4523
- { acknowledgedAtMs: this.acknowledgements.get(session.id) ?? null }
4524
- ),
4525
- headline: agentReports.get(session.tmuxSessionName)?.headline ?? null,
4526
- ...agentReports.get(session.tmuxSessionName)?.setupFailed && {
4527
- worktreeSetupFailed: true
4528
- }
4660
+ agentState: resolveSessionAttention(report, enriched.lastActivityAt, {
4661
+ acknowledgedAtMs: this.acknowledgements.get(session.id) ?? null
4662
+ }),
4663
+ headline: report?.headline ?? null,
4664
+ ...report?.setupFailed && { worktreeSetupFailed: true },
4665
+ ...report?.setupSkipped?.length && { worktreeSetupSkipped: report.setupSkipped }
4529
4666
  };
4530
4667
  if (!snapshotEquals(session, updated)) {
4531
4668
  this.sessions.set(session.id, updated);
@@ -4550,7 +4687,7 @@ ${error2.message}
4550
4687
  }
4551
4688
  };
4552
4689
  function snapshotEquals(a, b) {
4553
- return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && (a.headline ?? null) === (b.headline ?? null) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false);
4690
+ return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && (a.headline ?? null) === (b.headline ?? null) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false) && JSON.stringify(a.worktreeSetupSkipped ?? []) === JSON.stringify(b.worktreeSetupSkipped ?? []);
4554
4691
  }
4555
4692
 
4556
4693
  // src/ws/server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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",