@bondsmith/bondsmithd 0.1.4 → 0.1.6

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 +211 -45
  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 join6 } from "path";
7
+ import { dirname as dirname4, join as join7 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // ../../packages/local-agent-protocol/src/session.ts
@@ -951,7 +951,7 @@ import { sign } from "crypto";
951
951
  import WebSocket from "ws";
952
952
 
953
953
  // src/version.ts
954
- var DAEMON_VERSION = true ? "0.1.4" : "dev";
954
+ var DAEMON_VERSION = true ? "0.1.6" : "dev";
955
955
  var VERSION_FLAGS = ["--version", "-v"];
956
956
  function versionLine() {
957
957
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -1378,9 +1378,12 @@ function error(message, requestId) {
1378
1378
  function errorMessageOf(err, fallback) {
1379
1379
  return err instanceof Error ? err.message : fallback;
1380
1380
  }
1381
- function getSessionOrError(sessionManager, conn, sessionId) {
1381
+ function requestIdOf(message) {
1382
+ return "requestId" in message ? message.requestId : void 0;
1383
+ }
1384
+ function getSessionOrError(sessionManager, conn, sessionId, requestId) {
1382
1385
  const session = sessionManager.get(sessionId);
1383
- if (!session) conn.send(error("unknown session"));
1386
+ if (!session) conn.send(error("unknown session", requestId));
1384
1387
  return session;
1385
1388
  }
1386
1389
  async function handleBrowserMessage(conn, raw, deps) {
@@ -1400,11 +1403,11 @@ async function handleBrowserMessage(conn, raw, deps) {
1400
1403
  deps.trustedControllersPath
1401
1404
  );
1402
1405
  if (!verdict.ok) {
1403
- conn.send(error(verifyRejectionMessage(verdict.reason)));
1406
+ conn.send(error(verifyRejectionMessage(verdict.reason), requestIdOf(innerMessage.data)));
1404
1407
  return false;
1405
1408
  }
1406
1409
  if (conn.connectionAuth.isRevoked()) {
1407
- conn.send(error(REVOKED_REJECTION_MESSAGE));
1410
+ conn.send(error(REVOKED_REJECTION_MESSAGE, requestIdOf(innerMessage.data)));
1408
1411
  return false;
1409
1412
  }
1410
1413
  return true;
@@ -1432,7 +1435,7 @@ async function handleBrowserMessage(conn, raw, deps) {
1432
1435
  return;
1433
1436
  }
1434
1437
  if (isSignableAction(parsed.data.type)) {
1435
- conn.send(error("signature required for this command"));
1438
+ conn.send(error("signature required for this command", requestIdOf(parsed.data)));
1436
1439
  return;
1437
1440
  }
1438
1441
  await dispatch(conn, parsed.data, deps);
@@ -1571,10 +1574,13 @@ async function dispatch(conn, message, {
1571
1574
  return;
1572
1575
  }
1573
1576
  case "workspace.getStatus": {
1574
- const session = getSessionOrError(sessionManager, conn, message.sessionId);
1577
+ const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
1575
1578
  if (!session) return;
1576
1579
  try {
1577
- const status = await getWorkspaceStatus2(session.cwd);
1580
+ const status = await getWorkspaceStatus2(
1581
+ session.cwd,
1582
+ sessionManager.getBaseline(session.id)
1583
+ );
1578
1584
  conn.send({
1579
1585
  type: "workspace.status",
1580
1586
  sessionId: message.sessionId,
@@ -1582,15 +1588,20 @@ async function dispatch(conn, message, {
1582
1588
  status
1583
1589
  });
1584
1590
  } catch (err) {
1585
- conn.send(error(errorMessageOf(err, "failed to read workspace status")));
1591
+ conn.send(error(errorMessageOf(err, "failed to read workspace status"), message.requestId));
1586
1592
  }
1587
1593
  return;
1588
1594
  }
1589
1595
  case "workspace.getDiff": {
1590
- const session = getSessionOrError(sessionManager, conn, message.sessionId);
1596
+ const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
1591
1597
  if (!session) return;
1592
1598
  try {
1593
- const diff = await getWorkspaceDiff2(session.cwd, message.path, message.staged);
1599
+ const diff = await getWorkspaceDiff2(
1600
+ session.cwd,
1601
+ message.path,
1602
+ message.staged,
1603
+ sessionManager.getBaseline(session.id)
1604
+ );
1594
1605
  conn.send({
1595
1606
  type: "workspace.diff",
1596
1607
  sessionId: message.sessionId,
@@ -1598,7 +1609,7 @@ async function dispatch(conn, message, {
1598
1609
  diff
1599
1610
  });
1600
1611
  } catch (err) {
1601
- conn.send(error(errorMessageOf(err, "failed to read workspace diff")));
1612
+ conn.send(error(errorMessageOf(err, "failed to read workspace diff"), message.requestId));
1602
1613
  }
1603
1614
  return;
1604
1615
  }
@@ -2538,6 +2549,10 @@ function presentPendingAuthorizationOnTerminal(store) {
2538
2549
  };
2539
2550
  }
2540
2551
 
2552
+ // src/sessions/git/baseline.ts
2553
+ import { lstat } from "fs/promises";
2554
+ import { join as join5 } from "path";
2555
+
2541
2556
  // src/sessions/git/base.ts
2542
2557
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
2543
2558
  var DEFAULT_BRANCH_CANDIDATES = ["origin/main", "origin/master", "main", "master"];
@@ -2571,6 +2586,107 @@ async function resolveComparisonBase(cwd) {
2571
2586
  return mergeBase ? { sha: mergeBase, ref: defaultRef } : { sha: "HEAD", ref: null };
2572
2587
  }
2573
2588
 
2589
+ // src/sessions/git/baseline.ts
2590
+ var SESSION_START_REF = "session start";
2591
+ var MAX_PREEXISTING_FILES = 200;
2592
+ var DELETED = "deleted";
2593
+ async function fingerprintFile(cwd, path) {
2594
+ try {
2595
+ const stats = await lstat(join5(cwd, path));
2596
+ return `${stats.size}:${Math.trunc(stats.mtimeMs)}`;
2597
+ } catch {
2598
+ return DELETED;
2599
+ }
2600
+ }
2601
+ async function headSha(cwd) {
2602
+ try {
2603
+ return (await runGit(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], cwd)).trim() || null;
2604
+ } catch {
2605
+ return null;
2606
+ }
2607
+ }
2608
+ async function listChangedPaths(cwd, baseSha) {
2609
+ const [tracked, untracked] = await Promise.all([
2610
+ runGitDiff(["--relative", "--name-only", "-z", baseSha], cwd),
2611
+ runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
2612
+ ]);
2613
+ const paths = [...tracked.split("\0"), ...untracked.split("\0")].filter((p) => p.length > 0);
2614
+ return [...new Set(paths)];
2615
+ }
2616
+ async function captureBaseline(cwd, options) {
2617
+ try {
2618
+ await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
2619
+ } catch {
2620
+ return null;
2621
+ }
2622
+ try {
2623
+ const baseSha = await headSha(cwd);
2624
+ const preexisting = /* @__PURE__ */ new Map();
2625
+ if (!options.freshCheckout) {
2626
+ const changed = await listChangedPaths(cwd, baseSha ?? EMPTY_TREE_SHA);
2627
+ for (const path of changed.slice(0, MAX_PREEXISTING_FILES)) {
2628
+ preexisting.set(path, await fingerprintFile(cwd, path));
2629
+ }
2630
+ }
2631
+ return { baseSha, preexisting };
2632
+ } catch (error2) {
2633
+ console.error("Failed to take a session baseline; comparing against the default branch", {
2634
+ cwd,
2635
+ error: error2
2636
+ });
2637
+ return null;
2638
+ }
2639
+ }
2640
+ async function commitExists(cwd, sha) {
2641
+ try {
2642
+ await runGit(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], cwd);
2643
+ return true;
2644
+ } catch {
2645
+ return false;
2646
+ }
2647
+ }
2648
+ async function resolveBaseFor(cwd, baseline) {
2649
+ if (!baseline) return resolveComparisonBase(cwd);
2650
+ if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
2651
+ return resolveComparisonBase(cwd);
2652
+ }
2653
+ return { sha: baseline.baseSha ?? EMPTY_TREE_SHA, ref: SESSION_START_REF };
2654
+ }
2655
+ async function dropPreexisting(cwd, entries, baseline) {
2656
+ if (baseline.preexisting.size === 0) return entries;
2657
+ const untouched = await Promise.all(
2658
+ entries.map(async (entry) => {
2659
+ const before = baseline.preexisting.get(entry.path);
2660
+ return before !== void 0 && before === await fingerprintFile(cwd, entry.path);
2661
+ })
2662
+ );
2663
+ return entries.filter((_, index) => !untouched[index]);
2664
+ }
2665
+ function encodeBaseline(baseline) {
2666
+ const serialized = {
2667
+ v: 1,
2668
+ base: baseline.baseSha,
2669
+ files: [...baseline.preexisting]
2670
+ };
2671
+ return Buffer.from(JSON.stringify(serialized), "utf8").toString("base64url");
2672
+ }
2673
+ function isSerializedBaseline(value) {
2674
+ if (typeof value !== "object" || value === null) return false;
2675
+ const candidate = value;
2676
+ return candidate.v === 1 && (candidate.base === null || typeof candidate.base === "string") && Array.isArray(candidate.files) && candidate.files.every(
2677
+ (file) => Array.isArray(file) && file.length === 2 && typeof file[0] === "string" && typeof file[1] === "string"
2678
+ );
2679
+ }
2680
+ function decodeBaseline(encoded) {
2681
+ try {
2682
+ const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2683
+ if (!isSerializedBaseline(parsed)) return null;
2684
+ return { baseSha: parsed.base, preexisting: new Map(parsed.files) };
2685
+ } catch {
2686
+ return null;
2687
+ }
2688
+ }
2689
+
2574
2690
  // src/sessions/git/status.ts
2575
2691
  async function getCurrentBranch(cwd) {
2576
2692
  try {
@@ -2666,15 +2782,15 @@ function parseNameStatus(output) {
2666
2782
  }
2667
2783
  return changed;
2668
2784
  }
2669
- async function getWorkspaceStatus(cwd) {
2785
+ async function getWorkspaceStatus(cwd, baseline) {
2670
2786
  const branch = await getCurrentBranch(cwd);
2671
2787
  if (branch === null && !await isGitRepo(cwd)) {
2672
2788
  return { isGitRepo: false, branch: null, files: [] };
2673
2789
  }
2674
- const base = await resolveComparisonBase(cwd);
2790
+ const base = await resolveBaseFor(cwd, baseline);
2675
2791
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
2676
- runGitDiff(["--name-status", "-z", "-M", base.sha], cwd),
2677
- runGitDiff(["--numstat", "-z", "-M", base.sha], cwd),
2792
+ runGitDiff(["--relative", "--name-status", "-z", "-M", base.sha], cwd),
2793
+ runGitDiff(["--relative", "--numstat", "-z", "-M", base.sha], cwd),
2678
2794
  runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
2679
2795
  ]);
2680
2796
  const numstat = parseNumstat(numstatRaw);
@@ -2698,13 +2814,19 @@ async function getWorkspaceStatus(cwd) {
2698
2814
  deletions: null,
2699
2815
  binary: false
2700
2816
  }));
2701
- return { isGitRepo: true, branch, baseRef: base.ref, files: [...tracked, ...untracked] };
2817
+ const files = [...tracked, ...untracked];
2818
+ return {
2819
+ isGitRepo: true,
2820
+ branch,
2821
+ baseRef: base.ref,
2822
+ files: baseline ? await dropPreexisting(cwd, files, baseline) : files
2823
+ };
2702
2824
  }
2703
- async function enrichSessionWithGitMetadata(session) {
2825
+ async function enrichSessionWithGitMetadata(session, baseline) {
2704
2826
  const [branch, repositoryFullName, status, lastActivityAt] = await Promise.all([
2705
2827
  getCurrentBranch(session.cwd),
2706
2828
  getRepositoryFullName(session.cwd),
2707
- getWorkspaceStatus(session.cwd),
2829
+ getWorkspaceStatus(session.cwd, baseline),
2708
2830
  getPaneActivity(session.tmuxSessionName)
2709
2831
  ]);
2710
2832
  const changedFileCount = status.isGitRepo ? new Set(status.files.map((f) => f.path)).size : null;
@@ -2719,7 +2841,7 @@ async function enrichSessionWithGitMetadata(session) {
2719
2841
 
2720
2842
  // src/sessions/git/diff.ts
2721
2843
  async function withRenameSource(cwd, baseSha, path) {
2722
- const nameStatus = await runGitDiff(["--name-status", "-z", "-M", baseSha], cwd);
2844
+ const nameStatus = await runGitDiff(["--relative", "--name-status", "-z", "-M", baseSha], cwd);
2723
2845
  const previousPath = parseNameStatus(nameStatus).find((c) => c.path === path)?.previousPath;
2724
2846
  return previousPath === void 0 ? [path] : [previousPath, path];
2725
2847
  }
@@ -2727,15 +2849,18 @@ async function isUntracked(cwd, path) {
2727
2849
  const out = await runGit(["ls-files", "--others", "--exclude-standard", "--", path], cwd);
2728
2850
  return out.trim() !== "";
2729
2851
  }
2730
- async function getWorkspaceDiff(cwd, path, staged) {
2731
- const base = await resolveComparisonBase(cwd);
2852
+ async function getWorkspaceDiff(cwd, path, staged, baseline) {
2853
+ const base = await resolveBaseFor(cwd, baseline);
2732
2854
  const pathspec = await withRenameSource(cwd, base.sha, path);
2733
- const numstat = await runGitDiff(["--numstat", "-M", base.sha, "--", ...pathspec], cwd);
2855
+ const numstat = await runGitDiff(
2856
+ ["--relative", "--numstat", "-M", base.sha, "--", ...pathspec],
2857
+ cwd
2858
+ );
2734
2859
  const binary = /^(-\t-\t)/.test(numstat.trim());
2735
2860
  if (binary) {
2736
2861
  return { path, staged, patch: null, binary: true };
2737
2862
  }
2738
- let patch = await runGitDiff(["-M", base.sha, "--", ...pathspec], cwd);
2863
+ let patch = await runGitDiff(["--relative", "-M", base.sha, "--", ...pathspec], cwd);
2739
2864
  if (patch.trim() === "" && await isUntracked(cwd, path)) {
2740
2865
  patch = await runGitDiffAllowingExitCodes(["--no-index", "--", "/dev/null", path], cwd, [
2741
2866
  1
@@ -2814,7 +2939,7 @@ ${new TmuxNotFoundError().message}
2814
2939
  }
2815
2940
  return false;
2816
2941
  }
2817
- const created = { process: ptyProcess, refCount: 0, dataListeners: /* @__PURE__ */ new Set() };
2942
+ const created = { process: ptyProcess, dataListeners: /* @__PURE__ */ new Set([onData]) };
2818
2943
  this.handles.set(sessionId, created);
2819
2944
  ptyProcess.onData((data) => {
2820
2945
  for (const listener of created.dataListeners) listener(data);
@@ -2822,12 +2947,9 @@ ${new TmuxNotFoundError().message}
2822
2947
  ptyProcess.onExit(() => {
2823
2948
  if (this.handles.get(sessionId) === created) this.handles.delete(sessionId);
2824
2949
  });
2825
- created.refCount += 1;
2826
- created.dataListeners.add(onData);
2827
2950
  return true;
2828
2951
  }
2829
2952
  await this.sendPaneSnapshot(tmuxSessionName, onData);
2830
- handle.refCount += 1;
2831
2953
  handle.dataListeners.add(onData);
2832
2954
  return true;
2833
2955
  }
@@ -2854,8 +2976,7 @@ ${new TmuxNotFoundError().message}
2854
2976
  release(sessionId, onData) {
2855
2977
  const handle = this.handles.get(sessionId);
2856
2978
  if (!handle?.dataListeners.delete(onData)) return;
2857
- handle.refCount -= 1;
2858
- if (handle.refCount <= 0) {
2979
+ if (handle.dataListeners.size === 0) {
2859
2980
  handle.process.kill();
2860
2981
  this.handles.delete(sessionId);
2861
2982
  }
@@ -2936,7 +3057,7 @@ async function removeWorktree(sourceCwd, worktreePath) {
2936
3057
  // src/sessions/tmux/create.ts
2937
3058
  import { access } from "fs/promises";
2938
3059
  import { homedir as homedir5 } from "os";
2939
- import { join as join5 } from "path";
3060
+ import { join as join6 } from "path";
2940
3061
  var InvalidCwdError = class extends Error {
2941
3062
  constructor(cwd) {
2942
3063
  super(`working directory does not exist: ${cwd}`);
@@ -2944,7 +3065,7 @@ var InvalidCwdError = class extends Error {
2944
3065
  }
2945
3066
  };
2946
3067
  function worktreePathFor(sessionId) {
2947
- return join5(homedir5(), ".bondsmith", "worktrees", sessionId);
3068
+ return join6(homedir5(), ".bondsmith", "worktrees", sessionId);
2948
3069
  }
2949
3070
  async function createTmuxSession(input) {
2950
3071
  const sourceCwd = expandHome(input.cwd);
@@ -2963,6 +3084,7 @@ async function createTmuxSession(input) {
2963
3084
  }
2964
3085
  const tmuxSessionName = tmuxSessionNameFor(input.id);
2965
3086
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3087
+ const baseline = await captureBaseline(cwd, { freshCheckout: worktreeOf !== null });
2966
3088
  await runTmux(["new-session", "-d", "-s", tmuxSessionName, "-c", cwd, input.command], {
2967
3089
  env: filterSpawnEnv(process.env)
2968
3090
  });
@@ -2974,6 +3096,15 @@ async function createTmuxSession(input) {
2974
3096
  if (worktreeOf) {
2975
3097
  await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
2976
3098
  }
3099
+ if (baseline) {
3100
+ await runTmux([
3101
+ "set-option",
3102
+ "-t",
3103
+ tmuxSessionName,
3104
+ "@bondsmith-baseline",
3105
+ encodeBaseline(baseline)
3106
+ ]);
3107
+ }
2977
3108
  const session = {
2978
3109
  id: input.id,
2979
3110
  title: input.title,
@@ -2988,7 +3119,7 @@ async function createTmuxSession(input) {
2988
3119
  changedFileCount: null,
2989
3120
  worktreeOf
2990
3121
  };
2991
- return enrichSessionWithGitMetadata(session);
3122
+ return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
2992
3123
  }
2993
3124
  async function stopTmuxSession(tmuxSessionName) {
2994
3125
  await runTmux(["kill-session", "-t", tmuxSessionName]);
@@ -2996,6 +3127,7 @@ async function stopTmuxSession(tmuxSessionName) {
2996
3127
 
2997
3128
  // src/sessions/tmux/parse.ts
2998
3129
  var FIELD_COUNT = 7;
3130
+ var BASELINE_FIELD_INDEX = 7;
2999
3131
  function parseTmuxSessions(output) {
3000
3132
  const sessions = [];
3001
3133
  for (const line of output.split("\n")) {
@@ -3025,6 +3157,19 @@ function parseTmuxSessions(output) {
3025
3157
  }
3026
3158
  return sessions;
3027
3159
  }
3160
+ function parseSessionBaselines(output) {
3161
+ const baselines = /* @__PURE__ */ new Map();
3162
+ for (const line of output.split("\n")) {
3163
+ const fields = line.split(" ");
3164
+ const sessionName = fields[0];
3165
+ const id = fields[1];
3166
+ const encoded = fields[BASELINE_FIELD_INDEX];
3167
+ if (!sessionName || !isBondsmithTmuxSessionName(sessionName) || !id || !encoded) continue;
3168
+ const baseline = decodeBaseline(encoded);
3169
+ if (baseline) baselines.set(id, baseline);
3170
+ }
3171
+ return baselines;
3172
+ }
3028
3173
 
3029
3174
  // src/sessions/tmux/discover.ts
3030
3175
  var LIST_FORMAT = [
@@ -3034,15 +3179,21 @@ var LIST_FORMAT = [
3034
3179
  "#{@bondsmith-command}",
3035
3180
  "#{@bondsmith-cwd}",
3036
3181
  "#{@bondsmith-started-at}",
3037
- "#{@bondsmith-worktree-of}"
3182
+ "#{@bondsmith-worktree-of}",
3183
+ "#{@bondsmith-baseline}"
3038
3184
  ].join(" ");
3039
3185
  async function discoverSessions() {
3040
3186
  try {
3041
3187
  const output = await listTmuxSessions(LIST_FORMAT);
3042
- const sessions = parseTmuxSessions(output);
3043
- return Promise.all(sessions.map((session) => enrichSessionWithGitMetadata(session)));
3188
+ const baselines = parseSessionBaselines(output);
3189
+ const sessions = await Promise.all(
3190
+ parseTmuxSessions(output).map(
3191
+ (session) => enrichSessionWithGitMetadata(session, baselines.get(session.id))
3192
+ )
3193
+ );
3194
+ return { sessions, baselines };
3044
3195
  } catch (error2) {
3045
- if (error2 instanceof TmuxNoServerRunningError) return [];
3196
+ if (error2 instanceof TmuxNoServerRunningError) return { sessions: [], baselines: /* @__PURE__ */ new Map() };
3046
3197
  if (error2 instanceof TmuxNotFoundError) {
3047
3198
  console.error(`
3048
3199
  ${error2.message}
@@ -3056,13 +3207,19 @@ ${error2.message}
3056
3207
  // src/sessions/session-manager.ts
3057
3208
  var SessionManager = class {
3058
3209
  sessions = /* @__PURE__ */ new Map();
3210
+ // What each session's folder looked like when it started, so its file list shows only its own changes.
3211
+ baselines = /* @__PURE__ */ new Map();
3059
3212
  listeners = /* @__PURE__ */ new Set();
3060
3213
  async initialize() {
3061
3214
  const discovered = await discoverSessions();
3062
3215
  this.sessions.clear();
3063
- for (const session of discovered) {
3216
+ this.baselines.clear();
3217
+ for (const session of discovered.sessions) {
3064
3218
  this.sessions.set(session.id, session);
3065
3219
  }
3220
+ for (const [id, baseline] of discovered.baselines) {
3221
+ this.baselines.set(id, baseline);
3222
+ }
3066
3223
  }
3067
3224
  list() {
3068
3225
  return Array.from(this.sessions.values());
@@ -3070,6 +3227,10 @@ var SessionManager = class {
3070
3227
  get(sessionId) {
3071
3228
  return this.sessions.get(sessionId);
3072
3229
  }
3230
+ /** Undefined for a session started before baselines existed, which compares against the default branch. */
3231
+ getBaseline(sessionId) {
3232
+ return this.baselines.get(sessionId);
3233
+ }
3073
3234
  onSessionUpdated(listener) {
3074
3235
  this.listeners.add(listener);
3075
3236
  return () => this.listeners.delete(listener);
@@ -3079,8 +3240,9 @@ var SessionManager = class {
3079
3240
  }
3080
3241
  async create(input) {
3081
3242
  const id = uuidv4();
3082
- const session = await createTmuxSession({ id, ...input });
3243
+ const { session, baseline } = await createTmuxSession({ id, ...input });
3083
3244
  this.sessions.set(id, session);
3245
+ if (baseline) this.baselines.set(id, baseline);
3084
3246
  return session;
3085
3247
  }
3086
3248
  async stop(sessionId) {
@@ -3140,7 +3302,10 @@ ${error2.message}
3140
3302
  return;
3141
3303
  }
3142
3304
  try {
3143
- const updated = await enrichSessionWithGitMetadata(session);
3305
+ const updated = await enrichSessionWithGitMetadata(
3306
+ session,
3307
+ this.baselines.get(session.id)
3308
+ );
3144
3309
  if (!gitSnapshotEquals(session, updated)) {
3145
3310
  this.sessions.set(session.id, updated);
3146
3311
  this.emitUpdated(updated);
@@ -3157,6 +3322,7 @@ ${error2.message}
3157
3322
  removeAsExited(session) {
3158
3323
  const updated = { ...session, status: "exited" };
3159
3324
  this.sessions.delete(session.id);
3325
+ this.baselines.delete(session.id);
3160
3326
  this.emitUpdated(updated);
3161
3327
  return updated;
3162
3328
  }
@@ -3317,10 +3483,10 @@ if (isVersionRequest(process.argv.slice(2))) {
3317
3483
  process.exit(0);
3318
3484
  }
3319
3485
  function loadRepoRootEnv() {
3320
- const repoRoot = join6(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
3321
- if (!existsSync(join6(repoRoot, "pnpm-workspace.yaml"))) return;
3322
- const envLocal = join6(repoRoot, ".env.local");
3323
- const envDefault = join6(repoRoot, ".env");
3486
+ const repoRoot = join7(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
3487
+ if (!existsSync(join7(repoRoot, "pnpm-workspace.yaml"))) return;
3488
+ const envLocal = join7(repoRoot, ".env.local");
3489
+ const envDefault = join7(repoRoot, ".env");
3324
3490
  loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
3325
3491
  }
3326
3492
  loadRepoRootEnv();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",