@bondsmith/bondsmithd 0.1.12 → 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 +256 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,19 +4,28 @@
4
4
  import { createServer } from "http";
5
5
  import { existsSync } from "fs";
6
6
  import { hostname as hostname2 } from "os";
7
- import { dirname as dirname5, join as join11 } from "path";
7
+ import { dirname as dirname5, join as join12 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
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 = 14;
20
+ var PROTOCOL_VERSION = 17;
21
+
22
+ // ../../packages/local-agent-protocol/src/branch-name.ts
23
+ var MAX_BRANCH_NAME_LENGTH = 250;
24
+ var BRANCH_CHARACTERS = /^[\p{L}\p{N}._/+#@-]+$/u;
25
+ var REJECTED_SHAPES = /^[-/]|\/\/|\.\.|@\{|^@$|[/.]$|(^|\/)\.|\.lock(\/|$)/;
26
+ function isValidBranchName(name) {
27
+ return name.length > 0 && name.length <= MAX_BRANCH_NAME_LENGTH && BRANCH_CHARACTERS.test(name) && !REJECTED_SHAPES.test(name);
28
+ }
20
29
 
21
30
  // ../../packages/local-agent-protocol/src/worktree-setup.ts
22
31
  var MAX_COPY_GLOBS = 50;
@@ -196,6 +205,7 @@ var localSessionSchema = z3.object({
196
205
  // .catch(undefined): a state this version doesn't know must never drop the whole session message.
197
206
  agentState: z3.enum(AGENT_STATES).nullable().optional().catch(void 0),
198
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),
199
209
  headline: z3.string().nullable().optional().catch(void 0)
200
210
  });
201
211
  var workspaceFileEntrySchema = z3.object({
@@ -271,6 +281,7 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
271
281
  worktreeBranch: z3.string().min(1).max(250).refine((value) => !value.startsWith("-"), {
272
282
  message: "branch name can't start with -"
273
283
  }).optional(),
284
+ baseBranch: z3.string().max(MAX_BRANCH_NAME_LENGTH).refine(isValidBranchName).optional(),
274
285
  worktreeSetup: worktreeSetupSchema.optional(),
275
286
  requestId: z3.string().min(1).max(200).optional()
276
287
  }),
@@ -456,7 +467,8 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
456
467
  expiresAt: z3.number().int().positive(),
457
468
  // .catch(undefined): a malformed informational field must never fail the whole handshake message.
458
469
  protocolVersion: z3.number().int().positive().optional().catch(void 0),
459
- daemonVersion: z3.string().optional().catch(void 0)
470
+ daemonVersion: z3.string().optional().catch(void 0),
471
+ installedAgents: z3.array(z3.string().max(40)).max(20).optional().catch(void 0)
460
472
  }),
461
473
  z3.object({
462
474
  type: z3.literal("connection.authenticated"),
@@ -1174,7 +1186,7 @@ function expandHome(cwd) {
1174
1186
  }
1175
1187
 
1176
1188
  // src/sessions/git/client.ts
1177
- import { execFile as execFileCb } from "child_process";
1189
+ import { execFile as execFileCb, spawn as spawn2 } from "child_process";
1178
1190
  import { promisify } from "util";
1179
1191
  var execFile = promisify(execFileCb);
1180
1192
  var MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
@@ -1229,6 +1241,43 @@ function runGitDiff(args, cwd) {
1229
1241
  function runGitDiffAllowingExitCodes(args, cwd, allowedExitCodes) {
1230
1242
  return runGitAllowingExitCodes(["diff", ...SAFE_DIFF_FLAGS, ...args], cwd, allowedExitCodes);
1231
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
+ }
1232
1281
 
1233
1282
  // src/sessions/files/plan-file.ts
1234
1283
  import { constants as constants2 } from "fs";
@@ -1542,12 +1591,28 @@ async function commitExists(cwd, sha) {
1542
1591
  return false;
1543
1592
  }
1544
1593
  }
1594
+ async function isAncestor(cwd, ancestor, descendant) {
1595
+ try {
1596
+ await runGit(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
1597
+ return true;
1598
+ } catch (error2) {
1599
+ if (error2.code === 1) return false;
1600
+ throw error2;
1601
+ }
1602
+ }
1545
1603
  async function resolveBaseFor(cwd, baseline) {
1546
1604
  if (!baseline) return resolveComparisonBase(cwd);
1547
1605
  if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
1548
1606
  return resolveComparisonBase(cwd);
1549
1607
  }
1550
- return { sha: baseline.baseSha ?? EMPTY_TREE_SHA, ref: SESSION_START_REF };
1608
+ const start2 = {
1609
+ sha: baseline.baseSha ?? EMPTY_TREE_SHA,
1610
+ ref: SESSION_START_REF
1611
+ };
1612
+ if (baseline.baseSha === null) return start2;
1613
+ const meeting = await resolveComparisonBase(cwd);
1614
+ if (meeting.ref === null || await isAncestor(cwd, meeting.sha, baseline.baseSha)) return start2;
1615
+ return meeting;
1551
1616
  }
1552
1617
  async function dropPreexisting(cwd, entries, baseline) {
1553
1618
  if (baseline.preexisting.size === 0) return entries;
@@ -2183,8 +2248,27 @@ var ConnectionAuthState = class {
2183
2248
  }
2184
2249
  };
2185
2250
 
2251
+ // src/sessions/agents/installed-agents.ts
2252
+ import { accessSync, constants as constants3, statSync } from "fs";
2253
+ import { delimiter, join as join6 } from "path";
2254
+ var KNOWN_AGENTS = ["claude", "codex", "opencode"];
2255
+ function isExecutableFile(path) {
2256
+ try {
2257
+ accessSync(path, constants3.X_OK);
2258
+ return statSync(path).isFile();
2259
+ } catch {
2260
+ return false;
2261
+ }
2262
+ }
2263
+ function listInstalledAgents(env = process.env) {
2264
+ const directories = (env.PATH ?? "").split(delimiter).filter((directory) => directory !== "");
2265
+ return KNOWN_AGENTS.filter(
2266
+ (agent) => directories.some((directory) => isExecutableFile(join6(directory, agent)))
2267
+ );
2268
+ }
2269
+
2186
2270
  // src/version.ts
2187
- var DAEMON_VERSION = true ? "0.1.12" : "dev";
2271
+ var DAEMON_VERSION = true ? "0.1.14" : "dev";
2188
2272
  var VERSION_FLAGS = ["--version", "-v"];
2189
2273
  function versionLine() {
2190
2274
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -2555,13 +2639,14 @@ async function dispatch(conn, message, {
2555
2639
  return;
2556
2640
  }
2557
2641
  case "session.create": {
2558
- const { title, cwd, command, worktreeBranch, worktreeSetup, requestId } = message;
2642
+ const { title, cwd, command, worktreeBranch, baseBranch, worktreeSetup, requestId } = message;
2559
2643
  try {
2560
2644
  const session = await sessionManager.create({
2561
2645
  title,
2562
2646
  cwd,
2563
2647
  command,
2564
2648
  worktreeBranch,
2649
+ baseBranch,
2565
2650
  worktreeSetup
2566
2651
  });
2567
2652
  broadcast({ type: "session.created", session, requestId });
@@ -2997,7 +3082,8 @@ function startRelayClient(deps) {
2997
3082
  hostname: deps.hostname(),
2998
3083
  expiresAt: challenge.expiresAt,
2999
3084
  protocolVersion: PROTOCOL_VERSION,
3000
- daemonVersion: DAEMON_VERSION
3085
+ daemonVersion: DAEMON_VERSION,
3086
+ installedAgents: listInstalledAgents()
3001
3087
  });
3002
3088
  }
3003
3089
  function handleFrame(frame) {
@@ -3137,9 +3223,9 @@ function startRelayClient(deps) {
3137
3223
  import { generateKeyPairSync, sign as sign2, verify as verify4 } from "crypto";
3138
3224
  import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3139
3225
  import { homedir as homedir4 } from "os";
3140
- import { dirname as dirname2, join as join6 } from "path";
3226
+ import { dirname as dirname2, join as join7 } from "path";
3141
3227
  function defaultCredentialsPath() {
3142
- return join6(homedir4(), ".bondsmith", "credentials");
3228
+ return join7(homedir4(), ".bondsmith", "credentials");
3143
3229
  }
3144
3230
  async function loadOrCreateDaemonIdentity(credentialsPath = defaultCredentialsPath()) {
3145
3231
  const existing = await tryReadIdentity(credentialsPath);
@@ -3208,9 +3294,9 @@ function createDaemonProofSigner(privateKeyPem) {
3208
3294
  import { randomBytes as randomBytes2 } from "crypto";
3209
3295
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3210
3296
  import { homedir as homedir5 } from "os";
3211
- import { dirname as dirname3, join as join7 } from "path";
3297
+ import { dirname as dirname3, join as join8 } from "path";
3212
3298
  function defaultDevTunnelTokenPath() {
3213
- return join7(homedir5(), ".bondsmith", "dev-tunnel-token");
3299
+ return join8(homedir5(), ".bondsmith", "dev-tunnel-token");
3214
3300
  }
3215
3301
  async function loadOrCreateDevTunnelToken(tokenPath = defaultDevTunnelTokenPath()) {
3216
3302
  const existing = await tryReadToken(tokenPath);
@@ -3834,11 +3920,11 @@ function stderrOf(error2) {
3834
3920
  const stderr = error2.stderr;
3835
3921
  return typeof stderr === "string" ? stderr : "";
3836
3922
  }
3837
- async function createWorktree(sourceCwd, worktreePath, branch) {
3923
+ async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
3924
+ const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
3925
+ if (startPoint) args.push(startPoint);
3838
3926
  try {
3839
- await execFile3("git", [...SAFE_GIT_CONFIG, "worktree", "add", "-b", branch, worktreePath], {
3840
- cwd: sourceCwd
3841
- });
3927
+ await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
3842
3928
  } catch (error2) {
3843
3929
  const stderr = stderrOf(error2);
3844
3930
  if (stderr.includes("a branch named")) {
@@ -3887,11 +3973,25 @@ function wrapCommandWithSetup(input) {
3887
3973
  ].join(" ");
3888
3974
  }
3889
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
+
3890
3990
  // src/sessions/tmux/agent-state.ts
3891
3991
  var AGENT_STATE_OPTION = "@bondsmith-agent-state";
3892
3992
  var HEADLINE_OPTION = "@bondsmith-headline";
3893
3993
  var MAX_HEADLINE_CHARS = 200;
3894
- 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}}`;
3895
3995
  var AGENT_STATE_OUTPUT_GRACE_MS = 2e3;
3896
3996
  var FUTURE_REPORT_ALLOWANCE_MS = 2e3;
3897
3997
  var WORKING_SILENCE_MS = 12e4;
@@ -3909,12 +4009,20 @@ function isAgentState(value) {
3909
4009
  function parseLiveSessionReports(output) {
3910
4010
  const reports = /* @__PURE__ */ new Map();
3911
4011
  for (const line of output.split("\n")) {
3912
- 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(" ");
3913
4020
  if (sessionName) {
3914
4021
  reports.set(sessionName, {
3915
4022
  agentState,
3916
4023
  bell: bell === "1",
3917
4024
  setupFailed: setupFailed === "1",
4025
+ setupSkipped: decodeSkippedFiles(setupSkipped),
3918
4026
  headline: cleanHeadline(headline.join(" "))
3919
4027
  });
3920
4028
  }
@@ -3955,14 +4063,14 @@ function resolveSessionAttention(report, lastActivityAt, { nowMs, acknowledgedAt
3955
4063
  // src/sessions/tmux/create.ts
3956
4064
  import { access } from "fs/promises";
3957
4065
  import { homedir as homedir7 } from "os";
3958
- import { join as join10 } from "path";
4066
+ import { join as join11 } from "path";
3959
4067
 
3960
4068
  // src/sessions/agents/claude-hooks.ts
3961
4069
  import { execFile as execFile4 } from "child_process";
3962
4070
  import { randomUUID as randomUUID2 } from "crypto";
3963
4071
  import { mkdir as mkdir4, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
3964
4072
  import { homedir as homedir6 } from "os";
3965
- import { join as join8 } from "path";
4073
+ import { join as join9 } from "path";
3966
4074
  import { promisify as promisify4 } from "util";
3967
4075
 
3968
4076
  // src/sessions/session-env.ts
@@ -4044,7 +4152,7 @@ function claudeSettings() {
4044
4152
 
4045
4153
  // src/sessions/agents/claude-hooks.ts
4046
4154
  var execFileAsync = promisify4(execFile4);
4047
- var CLAUDE_HOOKS_DIR = join8(homedir6(), ".bondsmith", "hooks");
4155
+ var CLAUDE_HOOKS_DIR = join9(homedir6(), ".bondsmith", "hooks");
4048
4156
  var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
4049
4157
  var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
4050
4158
  var CLAUDE_BINARY = "claude";
@@ -4096,7 +4204,7 @@ async function prepareAgentCommand(command, {
4096
4204
  if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
4097
4205
  if (!isPlainClaudeCommand(command)) return command;
4098
4206
  if (!await supportsSettings()) return command;
4099
- const file = join8(hooksDir, "claude-settings.json");
4207
+ const file = join9(hooksDir, "claude-settings.json");
4100
4208
  const staging = `${file}.${randomUUID2()}.tmp`;
4101
4209
  try {
4102
4210
  await mkdir4(hooksDir, { recursive: true });
@@ -4116,23 +4224,101 @@ async function prepareAgentCommand(command, {
4116
4224
  );
4117
4225
  }
4118
4226
 
4227
+ // src/sessions/git/start-point.ts
4228
+ var WorktreeBaseBranchNotFoundError = class extends Error {
4229
+ constructor(branch) {
4230
+ super(`base branch "${branch}" was not found (looked for origin/${branch} and ${branch})`);
4231
+ this.name = "WorktreeBaseBranchNotFoundError";
4232
+ }
4233
+ };
4234
+ async function commitExists2(ref, cwd) {
4235
+ try {
4236
+ await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
4237
+ return true;
4238
+ } catch {
4239
+ return false;
4240
+ }
4241
+ }
4242
+ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
4243
+ if (baseBranch === void 0) return await findDefaultBranchRef(sourceCwd) ?? void 0;
4244
+ for (const candidate of [`origin/${baseBranch}`, baseBranch]) {
4245
+ if (await commitExists2(candidate, sourceCwd)) return candidate;
4246
+ }
4247
+ throw new WorktreeBaseBranchNotFoundError(baseBranch);
4248
+ }
4249
+
4119
4250
  // src/sessions/worktree-setup/copy-files.ts
4120
- import { constants as constants3 } from "fs";
4251
+ import { constants as constants4 } from "fs";
4121
4252
  import { copyFile, glob, lstat as lstat3, mkdir as mkdir5, realpath as realpath2 } from "fs/promises";
4122
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative, sep as sep2 } from "path";
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;
4123
4256
  function isInside(root, path) {
4124
4257
  const rel = relative(root, path);
4125
4258
  return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
4126
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
+ }
4127
4287
  async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4128
- const result = { copied: [], failed: 0 };
4288
+ const result = { copied: [], failed: 0, skipped: [] };
4129
4289
  const patterns = globs.filter(isSafeCopyGlob);
4130
4290
  if (patterns.length === 0) return result;
4131
4291
  try {
4132
4292
  const source = await realpath2(sourceRoot);
4133
4293
  const worktree = await realpath2(worktreeRoot);
4294
+ const candidates = [];
4134
4295
  for await (const match of glob(patterns, { cwd: source })) {
4135
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
+ }
4136
4322
  try {
4137
4323
  if (await copyOne(source, worktree, match)) result.copied.push(match);
4138
4324
  } catch (error2) {
@@ -4146,15 +4332,24 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4146
4332
  }
4147
4333
  return result;
4148
4334
  }
4149
- async function copyOne(source, worktree, match) {
4150
- const from = join9(source, match);
4335
+ async function isCopyCandidate(source, worktree, match) {
4336
+ const from = join10(source, match);
4151
4337
  if (!(await lstat3(from)).isFile()) return false;
4152
4338
  if (!isInside(source, await realpath2(from))) return false;
4153
- const to = join9(worktree, match);
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) {
4348
+ const to = join10(worktree, match);
4154
4349
  await mkdir5(dirname4(to), { recursive: true });
4155
4350
  if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
4156
4351
  try {
4157
- await copyFile(from, to, constants3.COPYFILE_EXCL);
4352
+ await copyFile(join10(source, match), to, constants4.COPYFILE_EXCL);
4158
4353
  } catch (error2) {
4159
4354
  if (error2.code === "EEXIST") return false;
4160
4355
  throw error2;
@@ -4170,16 +4365,18 @@ var InvalidCwdError = class extends Error {
4170
4365
  }
4171
4366
  };
4172
4367
  function worktreePathFor(sessionId) {
4173
- return join10(homedir7(), ".bondsmith", "worktrees", sessionId);
4368
+ return join11(homedir7(), ".bondsmith", "worktrees", sessionId);
4174
4369
  }
4175
4370
  async function createTmuxSession(input) {
4176
4371
  const sourceCwd = expandHome(input.cwd);
4177
4372
  let cwd = sourceCwd;
4178
4373
  let worktreeOf = null;
4179
4374
  let setupFailed = false;
4375
+ let skippedFiles = [];
4180
4376
  if (input.worktreeBranch) {
4181
4377
  const worktreePath = worktreePathFor(input.id);
4182
- await createWorktree(sourceCwd, worktreePath, input.worktreeBranch);
4378
+ const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
4379
+ await createWorktree(sourceCwd, worktreePath, input.worktreeBranch, startPoint);
4183
4380
  cwd = worktreePath;
4184
4381
  worktreeOf = sourceCwd;
4185
4382
  if (input.worktreeSetup?.copyGlobs.length) {
@@ -4189,6 +4386,7 @@ async function createTmuxSession(input) {
4189
4386
  input.worktreeSetup.copyGlobs
4190
4387
  );
4191
4388
  setupFailed = copy.failed > 0;
4389
+ skippedFiles = copy.skipped;
4192
4390
  }
4193
4391
  }
4194
4392
  try {
@@ -4226,6 +4424,15 @@ async function createTmuxSession(input) {
4226
4424
  await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
4227
4425
  }
4228
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
+ }
4229
4436
  if (baseline) {
4230
4437
  await runTmux([
4231
4438
  "set-option",
@@ -4248,7 +4455,10 @@ async function createTmuxSession(input) {
4248
4455
  lastActivityAt: null,
4249
4456
  changedFileCount: null,
4250
4457
  worktreeOf,
4251
- ...setupFailed && { worktreeSetupFailed: true }
4458
+ ...setupFailed && { worktreeSetupFailed: true },
4459
+ ...skippedFiles.length > 0 && {
4460
+ worktreeSetupSkipped: skippedFiles.slice(0, MAX_SKIPPED_FILES_REPORTED)
4461
+ }
4252
4462
  };
4253
4463
  return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
4254
4464
  }
@@ -4444,17 +4654,15 @@ ${error2.message}
4444
4654
  session,
4445
4655
  this.baselines.get(session.id)
4446
4656
  );
4657
+ const report = agentReports.get(session.tmuxSessionName);
4447
4658
  const updated = {
4448
4659
  ...enriched,
4449
- agentState: resolveSessionAttention(
4450
- agentReports.get(session.tmuxSessionName),
4451
- enriched.lastActivityAt,
4452
- { acknowledgedAtMs: this.acknowledgements.get(session.id) ?? null }
4453
- ),
4454
- headline: agentReports.get(session.tmuxSessionName)?.headline ?? null,
4455
- ...agentReports.get(session.tmuxSessionName)?.setupFailed && {
4456
- worktreeSetupFailed: true
4457
- }
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 }
4458
4666
  };
4459
4667
  if (!snapshotEquals(session, updated)) {
4460
4668
  this.sessions.set(session.id, updated);
@@ -4479,7 +4687,7 @@ ${error2.message}
4479
4687
  }
4480
4688
  };
4481
4689
  function snapshotEquals(a, b) {
4482
- 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 ?? []);
4483
4691
  }
4484
4692
 
4485
4693
  // src/ws/server.ts
@@ -4580,7 +4788,8 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
4580
4788
  hostname: hostname(),
4581
4789
  expiresAt: challenge.expiresAt,
4582
4790
  protocolVersion: PROTOCOL_VERSION,
4583
- daemonVersion: DAEMON_VERSION
4791
+ daemonVersion: DAEMON_VERSION,
4792
+ installedAgents: listInstalledAgents()
4584
4793
  });
4585
4794
  const unauthenticatedTimer = setTimeout(() => {
4586
4795
  if (!connectionAuth.isAuthenticated()) socket.close();
@@ -4645,10 +4854,10 @@ if (isVersionRequest(process.argv.slice(2))) {
4645
4854
  process.exit(0);
4646
4855
  }
4647
4856
  function loadRepoRootEnv() {
4648
- const repoRoot = join11(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
4649
- if (!existsSync(join11(repoRoot, "pnpm-workspace.yaml"))) return;
4650
- const envLocal = join11(repoRoot, ".env.local");
4651
- const envDefault = join11(repoRoot, ".env");
4857
+ const repoRoot = join12(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
4858
+ if (!existsSync(join12(repoRoot, "pnpm-workspace.yaml"))) return;
4859
+ const envLocal = join12(repoRoot, ".env.local");
4860
+ const envDefault = join12(repoRoot, ".env");
4652
4861
  loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
4653
4862
  }
4654
4863
  loadRepoRootEnv();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.12",
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",