@bondsmith/bondsmithd 0.1.12 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +103 -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 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
@@ -16,7 +16,15 @@ 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 = 14;
19
+ var PROTOCOL_VERSION = 16;
20
+
21
+ // ../../packages/local-agent-protocol/src/branch-name.ts
22
+ var MAX_BRANCH_NAME_LENGTH = 250;
23
+ var BRANCH_CHARACTERS = /^[\p{L}\p{N}._/+#@-]+$/u;
24
+ var REJECTED_SHAPES = /^[-/]|\/\/|\.\.|@\{|^@$|[/.]$|(^|\/)\.|\.lock(\/|$)/;
25
+ function isValidBranchName(name) {
26
+ return name.length > 0 && name.length <= MAX_BRANCH_NAME_LENGTH && BRANCH_CHARACTERS.test(name) && !REJECTED_SHAPES.test(name);
27
+ }
20
28
 
21
29
  // ../../packages/local-agent-protocol/src/worktree-setup.ts
22
30
  var MAX_COPY_GLOBS = 50;
@@ -271,6 +279,7 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
271
279
  worktreeBranch: z3.string().min(1).max(250).refine((value) => !value.startsWith("-"), {
272
280
  message: "branch name can't start with -"
273
281
  }).optional(),
282
+ baseBranch: z3.string().max(MAX_BRANCH_NAME_LENGTH).refine(isValidBranchName).optional(),
274
283
  worktreeSetup: worktreeSetupSchema.optional(),
275
284
  requestId: z3.string().min(1).max(200).optional()
276
285
  }),
@@ -456,7 +465,8 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
456
465
  expiresAt: z3.number().int().positive(),
457
466
  // .catch(undefined): a malformed informational field must never fail the whole handshake message.
458
467
  protocolVersion: z3.number().int().positive().optional().catch(void 0),
459
- daemonVersion: z3.string().optional().catch(void 0)
468
+ daemonVersion: z3.string().optional().catch(void 0),
469
+ installedAgents: z3.array(z3.string().max(40)).max(20).optional().catch(void 0)
460
470
  }),
461
471
  z3.object({
462
472
  type: z3.literal("connection.authenticated"),
@@ -1542,12 +1552,28 @@ async function commitExists(cwd, sha) {
1542
1552
  return false;
1543
1553
  }
1544
1554
  }
1555
+ async function isAncestor(cwd, ancestor, descendant) {
1556
+ try {
1557
+ await runGit(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
1558
+ return true;
1559
+ } catch (error2) {
1560
+ if (error2.code === 1) return false;
1561
+ throw error2;
1562
+ }
1563
+ }
1545
1564
  async function resolveBaseFor(cwd, baseline) {
1546
1565
  if (!baseline) return resolveComparisonBase(cwd);
1547
1566
  if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
1548
1567
  return resolveComparisonBase(cwd);
1549
1568
  }
1550
- return { sha: baseline.baseSha ?? EMPTY_TREE_SHA, ref: SESSION_START_REF };
1569
+ const start2 = {
1570
+ sha: baseline.baseSha ?? EMPTY_TREE_SHA,
1571
+ ref: SESSION_START_REF
1572
+ };
1573
+ if (baseline.baseSha === null) return start2;
1574
+ const meeting = await resolveComparisonBase(cwd);
1575
+ if (meeting.ref === null || await isAncestor(cwd, meeting.sha, baseline.baseSha)) return start2;
1576
+ return meeting;
1551
1577
  }
1552
1578
  async function dropPreexisting(cwd, entries, baseline) {
1553
1579
  if (baseline.preexisting.size === 0) return entries;
@@ -2183,8 +2209,27 @@ var ConnectionAuthState = class {
2183
2209
  }
2184
2210
  };
2185
2211
 
2212
+ // src/sessions/agents/installed-agents.ts
2213
+ import { accessSync, constants as constants3, statSync } from "fs";
2214
+ import { delimiter, join as join6 } from "path";
2215
+ var KNOWN_AGENTS = ["claude", "codex", "opencode"];
2216
+ function isExecutableFile(path) {
2217
+ try {
2218
+ accessSync(path, constants3.X_OK);
2219
+ return statSync(path).isFile();
2220
+ } catch {
2221
+ return false;
2222
+ }
2223
+ }
2224
+ function listInstalledAgents(env = process.env) {
2225
+ const directories = (env.PATH ?? "").split(delimiter).filter((directory) => directory !== "");
2226
+ return KNOWN_AGENTS.filter(
2227
+ (agent) => directories.some((directory) => isExecutableFile(join6(directory, agent)))
2228
+ );
2229
+ }
2230
+
2186
2231
  // src/version.ts
2187
- var DAEMON_VERSION = true ? "0.1.12" : "dev";
2232
+ var DAEMON_VERSION = true ? "0.1.13" : "dev";
2188
2233
  var VERSION_FLAGS = ["--version", "-v"];
2189
2234
  function versionLine() {
2190
2235
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -2555,13 +2600,14 @@ async function dispatch(conn, message, {
2555
2600
  return;
2556
2601
  }
2557
2602
  case "session.create": {
2558
- const { title, cwd, command, worktreeBranch, worktreeSetup, requestId } = message;
2603
+ const { title, cwd, command, worktreeBranch, baseBranch, worktreeSetup, requestId } = message;
2559
2604
  try {
2560
2605
  const session = await sessionManager.create({
2561
2606
  title,
2562
2607
  cwd,
2563
2608
  command,
2564
2609
  worktreeBranch,
2610
+ baseBranch,
2565
2611
  worktreeSetup
2566
2612
  });
2567
2613
  broadcast({ type: "session.created", session, requestId });
@@ -2997,7 +3043,8 @@ function startRelayClient(deps) {
2997
3043
  hostname: deps.hostname(),
2998
3044
  expiresAt: challenge.expiresAt,
2999
3045
  protocolVersion: PROTOCOL_VERSION,
3000
- daemonVersion: DAEMON_VERSION
3046
+ daemonVersion: DAEMON_VERSION,
3047
+ installedAgents: listInstalledAgents()
3001
3048
  });
3002
3049
  }
3003
3050
  function handleFrame(frame) {
@@ -3137,9 +3184,9 @@ function startRelayClient(deps) {
3137
3184
  import { generateKeyPairSync, sign as sign2, verify as verify4 } from "crypto";
3138
3185
  import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3139
3186
  import { homedir as homedir4 } from "os";
3140
- import { dirname as dirname2, join as join6 } from "path";
3187
+ import { dirname as dirname2, join as join7 } from "path";
3141
3188
  function defaultCredentialsPath() {
3142
- return join6(homedir4(), ".bondsmith", "credentials");
3189
+ return join7(homedir4(), ".bondsmith", "credentials");
3143
3190
  }
3144
3191
  async function loadOrCreateDaemonIdentity(credentialsPath = defaultCredentialsPath()) {
3145
3192
  const existing = await tryReadIdentity(credentialsPath);
@@ -3208,9 +3255,9 @@ function createDaemonProofSigner(privateKeyPem) {
3208
3255
  import { randomBytes as randomBytes2 } from "crypto";
3209
3256
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3210
3257
  import { homedir as homedir5 } from "os";
3211
- import { dirname as dirname3, join as join7 } from "path";
3258
+ import { dirname as dirname3, join as join8 } from "path";
3212
3259
  function defaultDevTunnelTokenPath() {
3213
- return join7(homedir5(), ".bondsmith", "dev-tunnel-token");
3260
+ return join8(homedir5(), ".bondsmith", "dev-tunnel-token");
3214
3261
  }
3215
3262
  async function loadOrCreateDevTunnelToken(tokenPath = defaultDevTunnelTokenPath()) {
3216
3263
  const existing = await tryReadToken(tokenPath);
@@ -3834,11 +3881,11 @@ function stderrOf(error2) {
3834
3881
  const stderr = error2.stderr;
3835
3882
  return typeof stderr === "string" ? stderr : "";
3836
3883
  }
3837
- async function createWorktree(sourceCwd, worktreePath, branch) {
3884
+ async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
3885
+ const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
3886
+ if (startPoint) args.push(startPoint);
3838
3887
  try {
3839
- await execFile3("git", [...SAFE_GIT_CONFIG, "worktree", "add", "-b", branch, worktreePath], {
3840
- cwd: sourceCwd
3841
- });
3888
+ await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
3842
3889
  } catch (error2) {
3843
3890
  const stderr = stderrOf(error2);
3844
3891
  if (stderr.includes("a branch named")) {
@@ -3955,14 +4002,14 @@ function resolveSessionAttention(report, lastActivityAt, { nowMs, acknowledgedAt
3955
4002
  // src/sessions/tmux/create.ts
3956
4003
  import { access } from "fs/promises";
3957
4004
  import { homedir as homedir7 } from "os";
3958
- import { join as join10 } from "path";
4005
+ import { join as join11 } from "path";
3959
4006
 
3960
4007
  // src/sessions/agents/claude-hooks.ts
3961
4008
  import { execFile as execFile4 } from "child_process";
3962
4009
  import { randomUUID as randomUUID2 } from "crypto";
3963
4010
  import { mkdir as mkdir4, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
3964
4011
  import { homedir as homedir6 } from "os";
3965
- import { join as join8 } from "path";
4012
+ import { join as join9 } from "path";
3966
4013
  import { promisify as promisify4 } from "util";
3967
4014
 
3968
4015
  // src/sessions/session-env.ts
@@ -4044,7 +4091,7 @@ function claudeSettings() {
4044
4091
 
4045
4092
  // src/sessions/agents/claude-hooks.ts
4046
4093
  var execFileAsync = promisify4(execFile4);
4047
- var CLAUDE_HOOKS_DIR = join8(homedir6(), ".bondsmith", "hooks");
4094
+ var CLAUDE_HOOKS_DIR = join9(homedir6(), ".bondsmith", "hooks");
4048
4095
  var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
4049
4096
  var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
4050
4097
  var CLAUDE_BINARY = "claude";
@@ -4096,7 +4143,7 @@ async function prepareAgentCommand(command, {
4096
4143
  if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
4097
4144
  if (!isPlainClaudeCommand(command)) return command;
4098
4145
  if (!await supportsSettings()) return command;
4099
- const file = join8(hooksDir, "claude-settings.json");
4146
+ const file = join9(hooksDir, "claude-settings.json");
4100
4147
  const staging = `${file}.${randomUUID2()}.tmp`;
4101
4148
  try {
4102
4149
  await mkdir4(hooksDir, { recursive: true });
@@ -4116,10 +4163,33 @@ async function prepareAgentCommand(command, {
4116
4163
  );
4117
4164
  }
4118
4165
 
4166
+ // src/sessions/git/start-point.ts
4167
+ var WorktreeBaseBranchNotFoundError = class extends Error {
4168
+ constructor(branch) {
4169
+ super(`base branch "${branch}" was not found (looked for origin/${branch} and ${branch})`);
4170
+ this.name = "WorktreeBaseBranchNotFoundError";
4171
+ }
4172
+ };
4173
+ async function commitExists2(ref, cwd) {
4174
+ try {
4175
+ await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
4176
+ return true;
4177
+ } catch {
4178
+ return false;
4179
+ }
4180
+ }
4181
+ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
4182
+ if (baseBranch === void 0) return await findDefaultBranchRef(sourceCwd) ?? void 0;
4183
+ for (const candidate of [`origin/${baseBranch}`, baseBranch]) {
4184
+ if (await commitExists2(candidate, sourceCwd)) return candidate;
4185
+ }
4186
+ throw new WorktreeBaseBranchNotFoundError(baseBranch);
4187
+ }
4188
+
4119
4189
  // src/sessions/worktree-setup/copy-files.ts
4120
- import { constants as constants3 } from "fs";
4190
+ import { constants as constants4 } from "fs";
4121
4191
  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";
4192
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative, sep as sep2 } from "path";
4123
4193
  function isInside(root, path) {
4124
4194
  const rel = relative(root, path);
4125
4195
  return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
@@ -4147,14 +4217,14 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4147
4217
  return result;
4148
4218
  }
4149
4219
  async function copyOne(source, worktree, match) {
4150
- const from = join9(source, match);
4220
+ const from = join10(source, match);
4151
4221
  if (!(await lstat3(from)).isFile()) return false;
4152
4222
  if (!isInside(source, await realpath2(from))) return false;
4153
- const to = join9(worktree, match);
4223
+ const to = join10(worktree, match);
4154
4224
  await mkdir5(dirname4(to), { recursive: true });
4155
4225
  if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
4156
4226
  try {
4157
- await copyFile(from, to, constants3.COPYFILE_EXCL);
4227
+ await copyFile(from, to, constants4.COPYFILE_EXCL);
4158
4228
  } catch (error2) {
4159
4229
  if (error2.code === "EEXIST") return false;
4160
4230
  throw error2;
@@ -4170,7 +4240,7 @@ var InvalidCwdError = class extends Error {
4170
4240
  }
4171
4241
  };
4172
4242
  function worktreePathFor(sessionId) {
4173
- return join10(homedir7(), ".bondsmith", "worktrees", sessionId);
4243
+ return join11(homedir7(), ".bondsmith", "worktrees", sessionId);
4174
4244
  }
4175
4245
  async function createTmuxSession(input) {
4176
4246
  const sourceCwd = expandHome(input.cwd);
@@ -4179,7 +4249,8 @@ async function createTmuxSession(input) {
4179
4249
  let setupFailed = false;
4180
4250
  if (input.worktreeBranch) {
4181
4251
  const worktreePath = worktreePathFor(input.id);
4182
- await createWorktree(sourceCwd, worktreePath, input.worktreeBranch);
4252
+ const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
4253
+ await createWorktree(sourceCwd, worktreePath, input.worktreeBranch, startPoint);
4183
4254
  cwd = worktreePath;
4184
4255
  worktreeOf = sourceCwd;
4185
4256
  if (input.worktreeSetup?.copyGlobs.length) {
@@ -4580,7 +4651,8 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
4580
4651
  hostname: hostname(),
4581
4652
  expiresAt: challenge.expiresAt,
4582
4653
  protocolVersion: PROTOCOL_VERSION,
4583
- daemonVersion: DAEMON_VERSION
4654
+ daemonVersion: DAEMON_VERSION,
4655
+ installedAgents: listInstalledAgents()
4584
4656
  });
4585
4657
  const unauthenticatedTimer = setTimeout(() => {
4586
4658
  if (!connectionAuth.isAuthenticated()) socket.close();
@@ -4645,10 +4717,10 @@ if (isVersionRequest(process.argv.slice(2))) {
4645
4717
  process.exit(0);
4646
4718
  }
4647
4719
  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");
4720
+ const repoRoot = join12(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
4721
+ if (!existsSync(join12(repoRoot, "pnpm-workspace.yaml"))) return;
4722
+ const envLocal = join12(repoRoot, ".env.local");
4723
+ const envDefault = join12(repoRoot, ".env");
4652
4724
  loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
4653
4725
  }
4654
4726
  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.13",
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",