seshmux 0.1.5 → 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 (23) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +6 -6
  3. package/.next/standalone/.next/build-manifest.json +5 -5
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/page.js +3 -3
  7. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  9. package/.next/standalone/.next/server/pages/500.html +1 -1
  10. package/.next/standalone/.next/server/pages-manifest.json +1 -1
  11. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  12. package/.next/standalone/.next/static/chunks/app/page-6e6c6148c4d1c925.js +1 -0
  13. package/.next/standalone/.next/static/chunks/{webpack-f60e08036f4baea4.js → webpack-452b158709859445.js} +1 -1
  14. package/.next/standalone/.next/static/css/{87ea7bb93e0b1326.css → a41d278d539911dd.css} +1 -1
  15. package/.next/standalone/.next/static/css/{bc53104caace13e7.css → e4d6e5dc25191e2a.css} +1 -1
  16. package/.next/standalone/package.json +1 -1
  17. package/.next/standalone/seshmux-server.js +378 -99
  18. package/README.md +16 -0
  19. package/bin/seshmux.js +89 -14
  20. package/package.json +1 -1
  21. package/.next/standalone/.next/static/chunks/app/page-b9c3810608a2c3d4.js +0 -1
  22. /package/.next/standalone/.next/static/{INANHMG7kWM6Af6v_Vj9d → z5Dh_tm8C81dOurk7OuAw}/_buildManifest.js +0 -0
  23. /package/.next/standalone/.next/static/{INANHMG7kWM6Af6v_Vj9d → z5Dh_tm8C81dOurk7OuAw}/_ssgManifest.js +0 -0
@@ -372,7 +372,9 @@ var init_needs_input = __esm({
372
372
  var workspaces_exports = {};
373
373
  __export(workspaces_exports, {
374
374
  create: () => create,
375
+ defaultBranch: () => defaultBranch,
375
376
  dirtyCount: () => dirtyCount,
377
+ git: () => git,
376
378
  list: () => list,
377
379
  listAll: () => listAll,
378
380
  reconcile: () => reconcile,
@@ -479,13 +481,24 @@ async function isGitRepo(repoPath) {
479
481
  }
480
482
  }
481
483
  async function defaultBranch(repoPath) {
484
+ const localExists = async (name) => {
485
+ try {
486
+ await git(repoPath, ["show-ref", "--verify", "--quiet", `refs/heads/${name}`]);
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ }
491
+ };
482
492
  try {
483
493
  const out2 = await git(repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
484
494
  const ref = out2.trim().replace(/^origin\//, "");
485
- if (ref) return ref;
495
+ if (ref) return await localExists(ref) ? ref : `origin/${ref}`;
486
496
  } catch {
487
497
  }
488
- const out = await git(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
498
+ for (const name of ["main", "master"]) {
499
+ if (await localExists(name)) return name;
500
+ }
501
+ const out = await git(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "");
489
502
  return out.trim() || "main";
490
503
  }
491
504
  function create(repoPath) {
@@ -753,10 +766,10 @@ function encodeProjectId(cwd) {
753
766
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
754
767
  }
755
768
  function decodeProjectDir(dir) {
756
- const path9 = dir.replace(/-/g, "/");
757
- const segments = path9.split("/").filter(Boolean);
769
+ const path10 = dir.replace(/-/g, "/");
770
+ const segments = path10.split("/").filter(Boolean);
758
771
  const name = segments[segments.length - 1] ?? dir;
759
- return { path: path9, name };
772
+ return { path: path10, name };
760
773
  }
761
774
  function firstUserText(content) {
762
775
  if (typeof content === "string") return content;
@@ -908,28 +921,28 @@ async function computeRootScan(root, provider) {
908
921
  if (!newestFile || s.st.mtimeMs > newestFile.mtime) newestFile = { path: s.fp, mtime: s.st.mtimeMs };
909
922
  }
910
923
  if (createdAt === Number.MAX_SAFE_INTEGER) createdAt = updatedAt;
911
- let path9 = decoded.path;
924
+ let path10 = decoded.path;
912
925
  let name = decoded.name;
913
926
  if (newestFile) {
914
927
  const head = await readHead(newestFile.path, Math.floor(newestFile.mtime));
915
928
  if (head.cwd) {
916
- path9 = head.cwd;
929
+ path10 = head.cwd;
917
930
  name = head.cwd.split("/").filter(Boolean).pop() || decoded.name;
918
931
  dirCwds.push({ dirPath, cwd: head.cwd });
919
932
  }
920
933
  }
921
934
  let missing = false;
922
935
  try {
923
- missing = !(await (0, import_promises2.stat)(path9)).isDirectory();
936
+ missing = !(await (0, import_promises2.stat)(path10)).isDirectory();
924
937
  } catch {
925
938
  missing = true;
926
939
  }
927
- const parentPath = parentOf.get(path9);
940
+ const parentPath = parentOf.get(path10);
928
941
  return {
929
942
  id: d.name,
930
943
  provider,
931
944
  name,
932
- path: parentPath ?? path9,
945
+ path: parentPath ?? path10,
933
946
  sessionCount: files.length,
934
947
  createdAt,
935
948
  updatedAt,
@@ -1128,8 +1141,8 @@ function rgSearch(root, q, limit) {
1128
1141
  continue;
1129
1142
  }
1130
1143
  if (ev.type !== "match") continue;
1131
- const path9 = ev.data?.path?.text ?? "";
1132
- const m = path9.match(/([^/]+)\.jsonl$/);
1144
+ const path10 = ev.data?.path?.text ?? "";
1145
+ const m = path10.match(/([^/]+)\.jsonl$/);
1133
1146
  if (!m) continue;
1134
1147
  out.push({ sessionId: m[1], line: ev.data?.lines?.text ?? "" });
1135
1148
  if (out.length >= limit) break;
@@ -1600,9 +1613,9 @@ function nodeFromSources(agentId, s) {
1600
1613
  jsonlPath
1601
1614
  };
1602
1615
  }
1603
- async function readJsonSafe(path9) {
1616
+ async function readJsonSafe(path10) {
1604
1617
  try {
1605
- return JSON.parse(await (0, import_promises6.readFile)(path9, "utf8"));
1618
+ return JSON.parse(await (0, import_promises6.readFile)(path10, "utf8"));
1606
1619
  } catch {
1607
1620
  return null;
1608
1621
  }
@@ -1985,20 +1998,20 @@ function ourVersion(command) {
1985
1998
  const m = command.match(new RegExp(`${MARKER}_V(\\d+)=1`));
1986
1999
  return m ? Number(m[1]) : null;
1987
2000
  }
1988
- async function readJson2(path9) {
2001
+ async function readJson2(path10) {
1989
2002
  try {
1990
- const raw = await (0, import_promises8.readFile)(path9, "utf8");
2003
+ const raw = await (0, import_promises8.readFile)(path10, "utf8");
1991
2004
  return JSON.parse(raw);
1992
2005
  } catch (err) {
1993
2006
  if (err instanceof Error && err.code === "ENOENT") return {};
1994
2007
  throw new Error(`settings.json is not valid JSON: ${err.message}`);
1995
2008
  }
1996
2009
  }
1997
- async function atomicWrite(path9, content) {
1998
- await (0, import_promises8.mkdir)((0, import_node_path10.dirname)(path9), { recursive: true });
1999
- const tmp = (0, import_node_path10.join)((0, import_node_path10.dirname)(path9), `.${(0, import_node_crypto3.randomBytes)(6).toString("hex")}.tmp`);
2010
+ async function atomicWrite(path10, content) {
2011
+ await (0, import_promises8.mkdir)((0, import_node_path10.dirname)(path10), { recursive: true });
2012
+ const tmp = (0, import_node_path10.join)((0, import_node_path10.dirname)(path10), `.${(0, import_node_crypto3.randomBytes)(6).toString("hex")}.tmp`);
2000
2013
  await (0, import_promises8.writeFile)(tmp, content, "utf8");
2001
- await (0, import_promises8.rename)(tmp, path9);
2014
+ await (0, import_promises8.rename)(tmp, path10);
2002
2015
  }
2003
2016
  function asHooksObject(cfg) {
2004
2017
  const h = cfg.hooks;
@@ -2687,15 +2700,15 @@ var init_codex2 = __esm({
2687
2700
  const projects = [];
2688
2701
  for (const [id, { cwd, count, createdAt, updatedAt }] of byProject) {
2689
2702
  const { name } = decodeProjectDir(id);
2690
- const path9 = cwd || id;
2703
+ const path10 = cwd || id;
2691
2704
  const displayName = cwd ? cwd.split("/").filter(Boolean).pop() || name : name;
2692
2705
  let missing = false;
2693
2706
  try {
2694
- missing = !(await (0, import_promises11.stat)(path9)).isDirectory();
2707
+ missing = !(await (0, import_promises11.stat)(path10)).isDirectory();
2695
2708
  } catch {
2696
2709
  missing = true;
2697
2710
  }
2698
- projects.push({ id, provider: this.id, name: displayName, path: path9, sessionCount: count, createdAt, updatedAt, missing });
2711
+ projects.push({ id, provider: this.id, name: displayName, path: path10, sessionCount: count, createdAt, updatedAt, missing });
2699
2712
  }
2700
2713
  projects.sort((a, b) => a.name.localeCompare(b.name));
2701
2714
  return projects;
@@ -3032,8 +3045,8 @@ function startWatching(deps) {
3032
3045
  awaitWriteFinish: { stabilityThreshold: 250 },
3033
3046
  ignored: (p, stats) => !!stats?.isFile() && !p.endsWith(".jsonl")
3034
3047
  });
3035
- w.on("add", (path9) => schedule(path9, provider, config, "add"));
3036
- w.on("change", (path9) => schedule(path9, provider, config, "change"));
3048
+ w.on("add", (path10) => schedule(path10, provider, config, "add"));
3049
+ w.on("change", (path10) => schedule(path10, provider, config, "change"));
3037
3050
  w.on("error", (err) => console.error(`[watch] ${provider} watcher error:`, err));
3038
3051
  return w;
3039
3052
  });
@@ -3486,8 +3499,8 @@ function defaultRun(cmd, args) {
3486
3499
  async function which(run, bin) {
3487
3500
  try {
3488
3501
  const { stdout } = await run("which", [bin]);
3489
- const path9 = stdout.trim().split("\n")[0];
3490
- return path9 || void 0;
3502
+ const path10 = stdout.trim().split("\n")[0];
3503
+ return path10 || void 0;
3491
3504
  } catch {
3492
3505
  return void 0;
3493
3506
  }
@@ -3501,10 +3514,10 @@ async function version(run, bin, flag = "--version") {
3501
3514
  }
3502
3515
  }
3503
3516
  async function detectTool(run, bin, versionFlag = "--version") {
3504
- const path9 = await which(run, bin);
3505
- if (!path9) return { found: false };
3517
+ const path10 = await which(run, bin);
3518
+ if (!path10) return { found: false };
3506
3519
  const v = await version(run, bin, versionFlag);
3507
- return { found: true, path: path9, ...v ? { version: v } : {} };
3520
+ return { found: true, path: path10, ...v ? { version: v } : {} };
3508
3521
  }
3509
3522
  async function detectEnv(deps) {
3510
3523
  const run = deps?.run ?? defaultRun;
@@ -3517,8 +3530,8 @@ async function detectEnv(deps) {
3517
3530
  async function agentEnv(id) {
3518
3531
  const provider = byId.get(id);
3519
3532
  const bin = provider.commands.fresh("")[0];
3520
- const path9 = await which(run, bin);
3521
- const v = path9 ? await version(run, bin) : void 0;
3533
+ const path10 = await which(run, bin);
3534
+ const v = path10 ? await version(run, bin) : void 0;
3522
3535
  let store = { found: false, projects: 0, bytes: 0 };
3523
3536
  try {
3524
3537
  const d = await provider.detect();
@@ -3526,7 +3539,7 @@ async function detectEnv(deps) {
3526
3539
  store = { found: projects > 0, projects, bytes: d.store?.bytes ?? 0 };
3527
3540
  } catch {
3528
3541
  }
3529
- return { found: !!path9, ...path9 ? { path: path9 } : {}, ...v ? { version: v } : {}, store };
3542
+ return { found: !!path10, ...path10 ? { path: path10 } : {}, ...v ? { version: v } : {}, store };
3530
3543
  }
3531
3544
  const [claude, codex, tmux, rg] = await Promise.all([
3532
3545
  agentEnv("claude"),
@@ -4125,24 +4138,24 @@ function bridgeServerConfig() {
4125
4138
  if (process.env.SESHMUX_CONFIG_DIR) cfg.env = { SESHMUX_CONFIG_DIR: process.env.SESHMUX_CONFIG_DIR };
4126
4139
  return cfg;
4127
4140
  }
4128
- async function atomicWrite2(path9, content) {
4129
- await (0, import_promises12.mkdir)((0, import_node_path16.dirname)(path9), { recursive: true });
4130
- const tmp = (0, import_node_path16.join)((0, import_node_path16.dirname)(path9), `.${(0, import_node_crypto5.randomBytes)(6).toString("hex")}.tmp`);
4141
+ async function atomicWrite2(path10, content) {
4142
+ await (0, import_promises12.mkdir)((0, import_node_path16.dirname)(path10), { recursive: true });
4143
+ const tmp = (0, import_node_path16.join)((0, import_node_path16.dirname)(path10), `.${(0, import_node_crypto5.randomBytes)(6).toString("hex")}.tmp`);
4131
4144
  await (0, import_promises12.writeFile)(tmp, content, "utf8");
4132
- await (0, import_promises12.rename)(tmp, path9);
4145
+ await (0, import_promises12.rename)(tmp, path10);
4133
4146
  }
4134
- async function readJson3(path9) {
4147
+ async function readJson3(path10) {
4135
4148
  try {
4136
- const raw = await (0, import_promises12.readFile)(path9, "utf8");
4149
+ const raw = await (0, import_promises12.readFile)(path10, "utf8");
4137
4150
  return JSON.parse(raw);
4138
4151
  } catch {
4139
4152
  return {};
4140
4153
  }
4141
4154
  }
4142
- async function registerClaude(path9) {
4155
+ async function registerClaude(path10) {
4143
4156
  let cfg = {};
4144
4157
  try {
4145
- const parsed = JSON.parse(await (0, import_promises12.readFile)(path9, "utf8"));
4158
+ const parsed = JSON.parse(await (0, import_promises12.readFile)(path10, "utf8"));
4146
4159
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4147
4160
  throw new Error("top-level JSON is not an object");
4148
4161
  }
@@ -4150,17 +4163,17 @@ async function registerClaude(path9) {
4150
4163
  } catch (e) {
4151
4164
  if (e.code !== "ENOENT") {
4152
4165
  throw new Error(
4153
- `refusing to modify ${path9}: it exists but is not valid JSON (${e.message}) \u2014 fix or move it, then re-register (nothing was written)`
4166
+ `refusing to modify ${path10}: it exists but is not valid JSON (${e.message}) \u2014 fix or move it, then re-register (nothing was written)`
4154
4167
  );
4155
4168
  }
4156
4169
  }
4157
4170
  const mcpServers = cfg.mcpServers && typeof cfg.mcpServers === "object" ? cfg.mcpServers : {};
4158
4171
  mcpServers["seshmux-bridge"] = bridgeServerConfig();
4159
4172
  cfg.mcpServers = mcpServers;
4160
- await atomicWrite2(path9, JSON.stringify(cfg, null, 2) + "\n");
4173
+ await atomicWrite2(path10, JSON.stringify(cfg, null, 2) + "\n");
4161
4174
  }
4162
- async function isClaudeRegistered(path9) {
4163
- const cfg = await readJson3(path9);
4175
+ async function isClaudeRegistered(path10) {
4176
+ const cfg = await readJson3(path10);
4164
4177
  const mcpServers = cfg.mcpServers;
4165
4178
  return !!(mcpServers && typeof mcpServers === "object" && "seshmux-bridge" in mcpServers);
4166
4179
  }
@@ -4180,10 +4193,10 @@ function codexTomlBlock() {
4180
4193
  function codexHasBridgeBlock(raw) {
4181
4194
  return /^\[mcp_servers\.seshmux-bridge\]/m.test(raw);
4182
4195
  }
4183
- async function registerCodex(path9) {
4196
+ async function registerCodex(path10) {
4184
4197
  let raw = "";
4185
4198
  try {
4186
- raw = await (0, import_promises12.readFile)(path9, "utf8");
4199
+ raw = await (0, import_promises12.readFile)(path10, "utf8");
4187
4200
  } catch {
4188
4201
  raw = "";
4189
4202
  }
@@ -4192,17 +4205,17 @@ async function registerCodex(path9) {
4192
4205
  /\[mcp_servers\.seshmux-bridge\][\s\S]*?(?=\n\[|$)/,
4193
4206
  codexTomlBlock().trimEnd() + "\n"
4194
4207
  );
4195
- if (replaced !== raw) await atomicWrite2(path9, replaced);
4208
+ if (replaced !== raw) await atomicWrite2(path10, replaced);
4196
4209
  return;
4197
4210
  }
4198
4211
  const needsSep = raw.length > 0 && !raw.endsWith("\n\n");
4199
4212
  const sep = raw.length === 0 ? "" : needsSep ? raw.endsWith("\n") ? "\n" : "\n\n" : "";
4200
4213
  const next2 = raw + sep + codexTomlBlock();
4201
- await atomicWrite2(path9, next2);
4214
+ await atomicWrite2(path10, next2);
4202
4215
  }
4203
- async function isCodexRegistered(path9) {
4216
+ async function isCodexRegistered(path10) {
4204
4217
  try {
4205
- const raw = await (0, import_promises12.readFile)(path9, "utf8");
4218
+ const raw = await (0, import_promises12.readFile)(path10, "utf8");
4206
4219
  return codexHasBridgeBlock(raw);
4207
4220
  } catch {
4208
4221
  return false;
@@ -4273,28 +4286,28 @@ __export(bridge_exports, {
4273
4286
  function providerName2(p) {
4274
4287
  return p === "codex" ? "Codex" : "Claude";
4275
4288
  }
4276
- async function isDir(path9) {
4289
+ async function isDir(path10) {
4277
4290
  try {
4278
- return (await (0, import_promises13.stat)(path9)).isDirectory();
4291
+ return (await (0, import_promises13.stat)(path10)).isDirectory();
4279
4292
  } catch {
4280
4293
  return false;
4281
4294
  }
4282
4295
  }
4283
- async function atomicWrite3(path9, content) {
4284
- const dir = (0, import_node_path17.join)(path9, "..");
4296
+ async function atomicWrite3(path10, content) {
4297
+ const dir = (0, import_node_path17.join)(path10, "..");
4285
4298
  await (0, import_promises13.mkdir)(dir, { recursive: true });
4286
4299
  const tmp = (0, import_node_path17.join)(dir, `.${(0, import_node_crypto6.randomBytes)(6).toString("hex")}.tmp`);
4287
4300
  await (0, import_promises13.writeFile)(tmp, content, "utf8");
4288
- await (0, import_promises13.rename)(tmp, path9);
4301
+ await (0, import_promises13.rename)(tmp, path10);
4289
4302
  }
4290
4303
  async function appendScratchpad(repo, text) {
4291
- const path9 = (0, import_node_path17.join)(repo, ".seshmux", "handoff.md");
4304
+ const path10 = (0, import_node_path17.join)(repo, ".seshmux", "handoff.md");
4292
4305
  let existing = "";
4293
4306
  try {
4294
- existing = await (0, import_promises13.readFile)(path9, "utf8");
4307
+ existing = await (0, import_promises13.readFile)(path10, "utf8");
4295
4308
  } catch {
4296
4309
  }
4297
- await atomicWrite3(path9, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + text + "\n");
4310
+ await atomicWrite3(path10, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + text + "\n");
4298
4311
  }
4299
4312
  async function defaultResolveProvider(projectId, sessionId) {
4300
4313
  const providers = await getProviders();
@@ -4341,7 +4354,7 @@ async function bridgeRoutes(f, deps) {
4341
4354
  const resolveProvider = deps.resolveSessionProvider ?? defaultResolveProvider;
4342
4355
  const brief = deps.composeBrief ? (p, s) => deps.composeBrief(p, s) : (p, s) => composeBrief(p, s);
4343
4356
  const review = deps.composeDiffReview ? (p, s) => deps.composeDiffReview(p, s) : (p, s, repo) => composeDiffReview(p, s, {}, repo);
4344
- const planoff = deps.runPlanoff ?? ((path9, task) => runPlanoff(path9, task));
4357
+ const planoff = deps.runPlanoff ?? ((path10, task) => runPlanoff(path10, task));
4345
4358
  const registerBridge2 = deps.registerBridge ?? (() => registerBridge());
4346
4359
  const bridgeStatus2 = deps.bridgeStatus ?? (() => bridgeStatus());
4347
4360
  const listLivePtys = deps.listLivePtys ?? defaultListLivePtys;
@@ -4535,8 +4548,8 @@ var projects_exports = {};
4535
4548
  __export(projects_exports, {
4536
4549
  default: () => projectsRoutes
4537
4550
  });
4538
- function isTmpProject(path9) {
4539
- const p = path9.endsWith("/") ? path9 : path9 + "/";
4551
+ function isTmpProject(path10) {
4552
+ const p = path10.endsWith("/") ? path10 : path10 + "/";
4540
4553
  return TMP_ROOTS.some((root) => p.startsWith(root));
4541
4554
  }
4542
4555
  async function projectsRoutes(f) {
@@ -5256,10 +5269,10 @@ var workspaces_exports2 = {};
5256
5269
  __export(workspaces_exports2, {
5257
5270
  default: () => workspaceRoutes
5258
5271
  });
5259
- async function isDir2(path9) {
5272
+ async function isDir2(path10) {
5260
5273
  try {
5261
- const { stat: stat10 } = await import("node:fs/promises");
5262
- return (await stat10(path9)).isDirectory();
5274
+ const { stat: stat11 } = await import("node:fs/promises");
5275
+ return (await stat11(path10)).isDirectory();
5263
5276
  } catch {
5264
5277
  return false;
5265
5278
  }
@@ -5373,6 +5386,271 @@ var init_workspaces2 = __esm({
5373
5386
  }
5374
5387
  });
5375
5388
 
5389
+ // server/lib/git-stats.ts
5390
+ function parseNumstat(out) {
5391
+ if (out.includes("\0")) return parseNumstatZ(out);
5392
+ const rows = [];
5393
+ for (const line of out.split("\n")) {
5394
+ if (!line.trim()) continue;
5395
+ const [a, r, ...rest] = line.split(" ");
5396
+ if (rest.length === 0) continue;
5397
+ rows.push({ path: rest.join(" "), added: num(a), removed: num(r) });
5398
+ }
5399
+ return rows;
5400
+ }
5401
+ function parseNumstatZ(out) {
5402
+ const tokens = out.split("\0");
5403
+ const rows = [];
5404
+ for (let i = 0; i < tokens.length; i++) {
5405
+ const t = tokens[i];
5406
+ if (!t) continue;
5407
+ const tab1 = t.indexOf(" ");
5408
+ const tab2 = t.indexOf(" ", tab1 + 1);
5409
+ if (tab1 === -1 || tab2 === -1) continue;
5410
+ const added = num(t.slice(0, tab1));
5411
+ const removed = num(t.slice(tab1 + 1, tab2));
5412
+ const rest = t.slice(tab2 + 1);
5413
+ if (rest === "") {
5414
+ const oldPath = tokens[++i];
5415
+ const newPath = tokens[++i];
5416
+ if (oldPath && newPath) rows.push({ path: newPath, added, removed, oldPath });
5417
+ } else {
5418
+ rows.push({ path: rest, added, removed });
5419
+ }
5420
+ }
5421
+ return rows;
5422
+ }
5423
+ async function resolveMergeBase(dir, baseRef) {
5424
+ if (!baseRef) return "HEAD";
5425
+ try {
5426
+ return (await git(dir, ["merge-base", baseRef, "HEAD"])).trim() || "HEAD";
5427
+ } catch {
5428
+ return "HEAD";
5429
+ }
5430
+ }
5431
+ async function untrackedLines(abs) {
5432
+ try {
5433
+ const st = await (0, import_promises16.stat)(abs);
5434
+ const hit = lineCache.get(abs);
5435
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit;
5436
+ let lines = 0;
5437
+ let read = 0;
5438
+ let lastByte = 10;
5439
+ let binary = false;
5440
+ if (st.size > 0) {
5441
+ const fh = await (0, import_promises16.open)(abs, "r");
5442
+ try {
5443
+ const buf = Buffer.alloc(64 * 1024);
5444
+ const limit = Math.min(st.size, LINE_COUNT_CAP);
5445
+ while (read < limit) {
5446
+ const { bytesRead } = await fh.read(buf, 0, buf.length, read);
5447
+ if (bytesRead <= 0) break;
5448
+ if (read === 0 && buf.subarray(0, Math.min(bytesRead, 8192)).includes(0)) {
5449
+ binary = true;
5450
+ break;
5451
+ }
5452
+ for (let i = 0; i < bytesRead; i++) if (buf[i] === 10) lines++;
5453
+ lastByte = buf[bytesRead - 1];
5454
+ read += bytesRead;
5455
+ }
5456
+ } finally {
5457
+ await fh.close();
5458
+ }
5459
+ }
5460
+ const result = {
5461
+ mtimeMs: st.mtimeMs,
5462
+ size: st.size,
5463
+ lines: binary || st.size === 0 ? 0 : lines + (lastByte !== 10 && read >= st.size ? 1 : 0),
5464
+ approx: !binary && st.size > LINE_COUNT_CAP
5465
+ };
5466
+ if (lineCache.size > 1e4) lineCache.clear();
5467
+ lineCache.set(abs, result);
5468
+ return result;
5469
+ } catch {
5470
+ return { lines: 0, approx: false };
5471
+ }
5472
+ }
5473
+ async function fileDiff(dir, baseRef, relPath) {
5474
+ const empty = { diff: "", truncated: false };
5475
+ try {
5476
+ const abs = import_node_path20.default.resolve(dir, relPath);
5477
+ if (!abs.startsWith(import_node_path20.default.resolve(dir) + import_node_path20.default.sep)) return empty;
5478
+ const base = await resolveMergeBase(dir, baseRef);
5479
+ const tracked = await git(dir, ["diff", "--no-renames", base, "--", relPath]);
5480
+ if (tracked) return truncateDiff(tracked);
5481
+ const untracked = splitZ(await git(dir, ["ls-files", "-o", "--exclude-standard", "-z", "--", relPath]));
5482
+ if (untracked.length === 0) return empty;
5483
+ try {
5484
+ return truncateDiff(await git(dir, ["diff", "--no-index", "--", "/dev/null", abs]));
5485
+ } catch (e) {
5486
+ const out = e.stdout;
5487
+ return typeof out === "string" ? truncateDiff(out) : empty;
5488
+ }
5489
+ } catch {
5490
+ return empty;
5491
+ }
5492
+ }
5493
+ function truncateDiff(diff) {
5494
+ const lines = diff.split("\n");
5495
+ if (lines.length <= MAX_DIFF_LINES) return { diff, truncated: false };
5496
+ return { diff: lines.slice(0, MAX_DIFF_LINES).join("\n"), truncated: true };
5497
+ }
5498
+ function changes(dir, baseRef, wantTree) {
5499
+ const key = `${dir}\0${baseRef ?? ""}\0${wantTree}`;
5500
+ const hit = changesMemo.get(key);
5501
+ if (hit && Date.now() - hit.at < CHANGES_TTL_MS) return hit.promise;
5502
+ const promise = computeChanges(dir, baseRef, wantTree);
5503
+ if (changesMemo.size > 200) changesMemo.clear();
5504
+ changesMemo.set(key, { at: Date.now(), promise });
5505
+ void promise.then((res) => {
5506
+ if (res.degraded && changesMemo.get(key)?.promise === promise) changesMemo.delete(key);
5507
+ });
5508
+ return promise;
5509
+ }
5510
+ async function computeChanges(dir, baseRef, wantTree) {
5511
+ try {
5512
+ const base = await resolveMergeBase(dir, baseRef);
5513
+ const [numstatOut, nameStatusOut, untrackedOut] = await Promise.all([
5514
+ git(dir, ["diff", "--numstat", "-z", base]),
5515
+ git(dir, ["diff", "--name-status", "-z", base]),
5516
+ git(dir, ["ls-files", "-o", "--exclude-standard", "-z"])
5517
+ ]);
5518
+ const statusTokens = splitZ(nameStatusOut);
5519
+ const statusByPath = /* @__PURE__ */ new Map();
5520
+ for (let i = 0; i < statusTokens.length; ) {
5521
+ const code = statusTokens[i++]?.charAt(0);
5522
+ if (!code) continue;
5523
+ if (code === "R" || code === "C") {
5524
+ i++;
5525
+ const newPath = statusTokens[i++];
5526
+ if (newPath) statusByPath.set(newPath, code);
5527
+ } else {
5528
+ const p = statusTokens[i++];
5529
+ if (p) statusByPath.set(p, code);
5530
+ }
5531
+ }
5532
+ const files = [];
5533
+ for (const f of parseNumstat(numstatOut)) {
5534
+ if (f.oldPath) {
5535
+ const code = statusByPath.get(f.path) ?? "R";
5536
+ files.push({ path: f.path, added: f.added, removed: f.removed, status: code });
5537
+ if (code !== "C") files.push({ path: f.oldPath, added: 0, removed: 0, status: "D" });
5538
+ } else {
5539
+ files.push({ path: f.path, added: f.added, removed: f.removed, status: statusByPath.get(f.path) ?? "M" });
5540
+ }
5541
+ }
5542
+ const untracked = splitZ(untrackedOut);
5543
+ for (let i = 0; i < untracked.length; i += 16) {
5544
+ const batch = await Promise.all(
5545
+ untracked.slice(i, i + 16).map(async (rel) => {
5546
+ const { lines, approx } = await untrackedLines(import_node_path20.default.join(dir, rel));
5547
+ return { path: rel, added: lines, removed: 0, status: "A", ...approx ? { approx: true } : {} };
5548
+ })
5549
+ );
5550
+ files.push(...batch);
5551
+ }
5552
+ const result = {
5553
+ added: files.reduce((n, f) => n + f.added, 0),
5554
+ removed: files.reduce((n, f) => n + f.removed, 0),
5555
+ files
5556
+ };
5557
+ if (wantTree) {
5558
+ const tracked = splitZ(await git(dir, ["ls-files", "-z"]));
5559
+ result.tree = [.../* @__PURE__ */ new Set([...tracked, ...untracked])].sort();
5560
+ }
5561
+ return result;
5562
+ } catch {
5563
+ return { added: 0, removed: 0, files: [], degraded: true };
5564
+ }
5565
+ }
5566
+ var import_promises16, import_node_path20, num, LINE_COUNT_CAP, lineCache, splitZ, MAX_DIFF_LINES, changesMemo, CHANGES_TTL_MS;
5567
+ var init_git_stats = __esm({
5568
+ "server/lib/git-stats.ts"() {
5569
+ "use strict";
5570
+ import_promises16 = require("node:fs/promises");
5571
+ import_node_path20 = __toESM(require("node:path"));
5572
+ init_workspaces();
5573
+ num = (s) => s === "-" ? 0 : Number(s) || 0;
5574
+ LINE_COUNT_CAP = 10 * 1024 * 1024;
5575
+ lineCache = /* @__PURE__ */ new Map();
5576
+ splitZ = (out) => out.split("\0").filter(Boolean);
5577
+ MAX_DIFF_LINES = 5e3;
5578
+ changesMemo = /* @__PURE__ */ new Map();
5579
+ CHANGES_TTL_MS = 5e3;
5580
+ }
5581
+ });
5582
+
5583
+ // server/routes/git.ts
5584
+ var git_exports = {};
5585
+ __export(git_exports, {
5586
+ default: () => gitRoutes
5587
+ });
5588
+ async function gitRoutes(f, deps = {}) {
5589
+ const resolveRepo = deps.resolveRepo ?? defaultResolveRepo;
5590
+ const listWorkspaces = deps.listWorkspaces ?? list;
5591
+ const repoMemo = /* @__PURE__ */ new Map();
5592
+ const REPO_TTL_MS = 6e4;
5593
+ async function repoFor(projectId) {
5594
+ const hit = repoMemo.get(projectId);
5595
+ if (hit && Date.now() - hit.at < REPO_TTL_MS) return hit.path;
5596
+ const path10 = await resolveRepo(projectId);
5597
+ if (repoMemo.size > 500) repoMemo.clear();
5598
+ if (path10) repoMemo.set(projectId, { at: Date.now(), path: path10 });
5599
+ return path10;
5600
+ }
5601
+ async function resolveTarget(projectId, branch) {
5602
+ const repo = await repoFor(projectId);
5603
+ if (!repo) return null;
5604
+ let dir = repo;
5605
+ if (branch?.startsWith("agent/")) {
5606
+ const rec = (await listWorkspaces(repo).catch(() => [])).find((r) => r.branch === branch);
5607
+ if (rec) dir = rec.dir;
5608
+ }
5609
+ const base = await defaultBranch(repo).catch(() => null);
5610
+ return { dir, base };
5611
+ }
5612
+ f.get(
5613
+ "/api/git/changes",
5614
+ async (req, reply) => {
5615
+ const { project, branch, tree } = req.query;
5616
+ if (!project) {
5617
+ reply.code(400);
5618
+ return { error: "project is required" };
5619
+ }
5620
+ const target = await resolveTarget(project, branch);
5621
+ if (!target) {
5622
+ reply.code(404);
5623
+ return { error: "project not found" };
5624
+ }
5625
+ return changes(target.dir, target.base, tree === "1");
5626
+ }
5627
+ );
5628
+ f.get(
5629
+ "/api/git/changes/file",
5630
+ async (req, reply) => {
5631
+ const { project, branch, path: relPath } = req.query;
5632
+ if (!project || !relPath) {
5633
+ reply.code(400);
5634
+ return { error: "project and path are required" };
5635
+ }
5636
+ const target = await resolveTarget(project, branch);
5637
+ if (!target) {
5638
+ reply.code(404);
5639
+ return { error: "project not found" };
5640
+ }
5641
+ return fileDiff(target.dir, target.base, relPath);
5642
+ }
5643
+ );
5644
+ }
5645
+ var init_git = __esm({
5646
+ "server/routes/git.ts"() {
5647
+ "use strict";
5648
+ init_git_stats();
5649
+ init_workspaces();
5650
+ init_bridge();
5651
+ }
5652
+ });
5653
+
5376
5654
  // server/lib/teams.ts
5377
5655
  function composeTeamPrompt(t, task) {
5378
5656
  const roster = t.members.map((m) => ` - ${oneLine(m.name)} \u2014 ${oneLine(m.role)}${m.model ? ` (${m.model})` : ""}`).join("\n");
@@ -5386,16 +5664,16 @@ function composeTeamPrompt(t, task) {
5386
5664
  }
5387
5665
  async function readAll2() {
5388
5666
  try {
5389
- return JSON.parse(await (0, import_promises16.readFile)(file(), "utf8"));
5667
+ return JSON.parse(await (0, import_promises17.readFile)(file(), "utf8"));
5390
5668
  } catch {
5391
5669
  return [];
5392
5670
  }
5393
5671
  }
5394
5672
  async function writeAll2(list2) {
5395
- await (0, import_promises16.mkdir)(configDir(), { recursive: true });
5396
- const tmp = import_node_path20.default.join(configDir(), `.${(0, import_node_crypto8.randomBytes)(6).toString("hex")}.tmp`);
5397
- await (0, import_promises16.writeFile)(tmp, JSON.stringify(list2, null, 2), "utf8");
5398
- await (0, import_promises16.rename)(tmp, file());
5673
+ await (0, import_promises17.mkdir)(configDir(), { recursive: true });
5674
+ const tmp = import_node_path21.default.join(configDir(), `.${(0, import_node_crypto8.randomBytes)(6).toString("hex")}.tmp`);
5675
+ await (0, import_promises17.writeFile)(tmp, JSON.stringify(list2, null, 2), "utf8");
5676
+ await (0, import_promises17.rename)(tmp, file());
5399
5677
  }
5400
5678
  function update2(mutate) {
5401
5679
  const run = writeChain2.then(async () => {
@@ -5413,15 +5691,15 @@ async function saveTemplate(t) {
5413
5691
  async function deleteTemplate(name) {
5414
5692
  await update2((list2) => list2.filter((x) => x.name !== name));
5415
5693
  }
5416
- var import_promises16, import_node_crypto8, import_node_path20, file, oneLine, writeChain2, listTemplates;
5694
+ var import_promises17, import_node_crypto8, import_node_path21, file, oneLine, writeChain2, listTemplates;
5417
5695
  var init_teams = __esm({
5418
5696
  "server/lib/teams.ts"() {
5419
5697
  "use strict";
5420
- import_promises16 = require("node:fs/promises");
5698
+ import_promises17 = require("node:fs/promises");
5421
5699
  import_node_crypto8 = require("node:crypto");
5422
- import_node_path20 = __toESM(require("node:path"));
5700
+ import_node_path21 = __toESM(require("node:path"));
5423
5701
  init_daemon_client();
5424
- file = () => import_node_path20.default.join(configDir(), "teams.json");
5702
+ file = () => import_node_path21.default.join(configDir(), "teams.json");
5425
5703
  oneLine = (s) => s.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
5426
5704
  writeChain2 = Promise.resolve();
5427
5705
  listTemplates = readAll2;
@@ -5433,10 +5711,10 @@ var teams_exports = {};
5433
5711
  __export(teams_exports, {
5434
5712
  default: () => teamRoutes
5435
5713
  });
5436
- async function isDir3(path9) {
5714
+ async function isDir3(path10) {
5437
5715
  try {
5438
- const { stat: stat10 } = await import("node:fs/promises");
5439
- return (await stat10(path9)).isDirectory();
5716
+ const { stat: stat11 } = await import("node:fs/promises");
5717
+ return (await stat11(path10)).isDirectory();
5440
5718
  } catch {
5441
5719
  return false;
5442
5720
  }
@@ -5593,22 +5871,22 @@ var scratchpad_exports = {};
5593
5871
  __export(scratchpad_exports, {
5594
5872
  default: () => scratchpadRoutes
5595
5873
  });
5596
- async function isDir4(path9) {
5874
+ async function isDir4(path10) {
5597
5875
  try {
5598
- return (await (0, import_promises17.stat)(path9)).isDirectory();
5876
+ return (await (0, import_promises18.stat)(path10)).isDirectory();
5599
5877
  } catch {
5600
5878
  return false;
5601
5879
  }
5602
5880
  }
5603
5881
  function scratchpadPath(repo) {
5604
- return (0, import_node_path21.join)(repo, ".seshmux", "handoff.md");
5882
+ return (0, import_node_path22.join)(repo, ".seshmux", "handoff.md");
5605
5883
  }
5606
- async function atomicWrite4(path9, content) {
5607
- const dir = (0, import_node_path21.join)(path9, "..");
5608
- await (0, import_promises17.mkdir)(dir, { recursive: true });
5609
- const tmp = (0, import_node_path21.join)(dir, `.${(0, import_node_crypto9.randomBytes)(6).toString("hex")}.tmp`);
5610
- await (0, import_promises17.writeFile)(tmp, content, "utf8");
5611
- await (0, import_promises17.rename)(tmp, path9);
5884
+ async function atomicWrite4(path10, content) {
5885
+ const dir = (0, import_node_path22.join)(path10, "..");
5886
+ await (0, import_promises18.mkdir)(dir, { recursive: true });
5887
+ const tmp = (0, import_node_path22.join)(dir, `.${(0, import_node_crypto9.randomBytes)(6).toString("hex")}.tmp`);
5888
+ await (0, import_promises18.writeFile)(tmp, content, "utf8");
5889
+ await (0, import_promises18.rename)(tmp, path10);
5612
5890
  }
5613
5891
  async function scratchpadRoutes(f, deps = {}) {
5614
5892
  const defaultResolveRepo2 = async (id) => {
@@ -5634,7 +5912,7 @@ async function scratchpadRoutes(f, deps = {}) {
5634
5912
  }
5635
5913
  deps.onOpen?.(req.params.projectId, repo);
5636
5914
  try {
5637
- const content = await (0, import_promises17.readFile)(scratchpadPath(repo), "utf8");
5915
+ const content = await (0, import_promises18.readFile)(scratchpadPath(repo), "utf8");
5638
5916
  return { content };
5639
5917
  } catch {
5640
5918
  return { content: "" };
@@ -5648,25 +5926,25 @@ async function scratchpadRoutes(f, deps = {}) {
5648
5926
  reply.code(404);
5649
5927
  return { error: "project not found" };
5650
5928
  }
5651
- const path9 = scratchpadPath(repo);
5929
+ const path10 = scratchpadPath(repo);
5652
5930
  let content = typeof req.body?.content === "string" ? req.body.content : "";
5653
5931
  try {
5654
- await (0, import_promises17.stat)(path9);
5932
+ await (0, import_promises18.stat)(path10);
5655
5933
  } catch {
5656
5934
  if (!content.startsWith("# ")) content = TEMPLATE + content;
5657
5935
  }
5658
- await atomicWrite4(path9, content);
5936
+ await atomicWrite4(path10, content);
5659
5937
  return { ok: true, content };
5660
5938
  }
5661
5939
  );
5662
5940
  }
5663
- var import_node_crypto9, import_promises17, import_node_path21, TEMPLATE;
5941
+ var import_node_crypto9, import_promises18, import_node_path22, TEMPLATE;
5664
5942
  var init_scratchpad = __esm({
5665
5943
  "server/routes/scratchpad.ts"() {
5666
5944
  "use strict";
5667
5945
  import_node_crypto9 = require("node:crypto");
5668
- import_promises17 = require("node:fs/promises");
5669
- import_node_path21 = require("node:path");
5946
+ import_promises18 = require("node:fs/promises");
5947
+ import_node_path22 = require("node:path");
5670
5948
  init_scan();
5671
5949
  init_types();
5672
5950
  TEMPLATE = `# Shared scratchpad
@@ -5731,12 +6009,12 @@ __export(notify_exports, {
5731
6009
  default: () => notifyRoutes
5732
6010
  });
5733
6011
  function configPath2() {
5734
- const dir = process.env.SESHMUX_CONFIG_DIR || import_node_path22.default.join(import_node_os10.default.homedir(), ".config", "seshmux");
5735
- return import_node_path22.default.join(dir, "config.json");
6012
+ const dir = process.env.SESHMUX_CONFIG_DIR || import_node_path23.default.join(import_node_os10.default.homedir(), ".config", "seshmux");
6013
+ return import_node_path23.default.join(dir, "config.json");
5736
6014
  }
5737
6015
  async function macNotificationsEnabled() {
5738
6016
  try {
5739
- const raw = await (0, import_promises18.readFile)(configPath2(), "utf8");
6017
+ const raw = await (0, import_promises19.readFile)(configPath2(), "utf8");
5740
6018
  const cfg = JSON.parse(raw);
5741
6019
  return cfg?.settings?.macNotifications !== false;
5742
6020
  } catch {
@@ -5772,13 +6050,13 @@ async function notifyRoutes(f) {
5772
6050
  return { ok: true, delivered: true };
5773
6051
  });
5774
6052
  }
5775
- var import_node_child_process8, import_promises18, import_node_path22, import_node_os10;
6053
+ var import_node_child_process8, import_promises19, import_node_path23, import_node_os10;
5776
6054
  var init_notify = __esm({
5777
6055
  "server/routes/notify.ts"() {
5778
6056
  "use strict";
5779
6057
  import_node_child_process8 = require("node:child_process");
5780
- import_promises18 = require("node:fs/promises");
5781
- import_node_path22 = __toESM(require("node:path"));
6058
+ import_promises19 = require("node:fs/promises");
6059
+ import_node_path23 = __toESM(require("node:path"));
5782
6060
  import_node_os10 = __toESM(require("node:os"));
5783
6061
  }
5784
6062
  });
@@ -5992,6 +6270,7 @@ async function startServer({ port: port2 = 4700, dev = false } = {}) {
5992
6270
  await (await Promise.resolve().then(() => (init_workspaces(), workspaces_exports))).reconcile().catch(() => {
5993
6271
  });
5994
6272
  await f.register((await Promise.resolve().then(() => (init_workspaces2(), workspaces_exports2))).default);
6273
+ await f.register((await Promise.resolve().then(() => (init_git(), git_exports))).default);
5995
6274
  await f.register((await Promise.resolve().then(() => (init_teams2(), teams_exports))).default, {
5996
6275
  onTeamWatch: (teamName, leadSessionId, configPath3) => hub.watchTeam(teamName, leadSessionId, configPath3)
5997
6276
  });