@diologue/local-agent 0.7.0 → 0.8.0

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.
package/dist/cli.mjs CHANGED
@@ -83,7 +83,7 @@ var init_engine_release = __esm({
83
83
  import { access as access2, constants } from "node:fs/promises";
84
84
  import { readFileSync } from "node:fs";
85
85
  import { createRequire } from "node:module";
86
- import path3 from "node:path";
86
+ import path4 from "node:path";
87
87
  import { fileURLToPath } from "node:url";
88
88
  var engineRelease, __filename, __dirname, LOCAL_AGENT_ROOT, REPO_ROOT, BUNDLE_DIR_INSTALLED, BUNDLE_DIR_LOCAL_BUILD, ENGINE_BUNDLE_ENV, exists2, resolveOpencodeAiBinary, bundleFilename, findEngineBundle;
89
89
  var init_engine_bundle = __esm({
@@ -92,15 +92,15 @@ var init_engine_bundle = __esm({
92
92
  init_engine_release();
93
93
  engineRelease = engine_release_default;
94
94
  __filename = fileURLToPath(import.meta.url);
95
- __dirname = path3.dirname(__filename);
96
- LOCAL_AGENT_ROOT = path3.resolve(__dirname, "../..");
97
- REPO_ROOT = path3.resolve(LOCAL_AGENT_ROOT, "..");
98
- BUNDLE_DIR_INSTALLED = path3.join(
95
+ __dirname = path4.dirname(__filename);
96
+ LOCAL_AGENT_ROOT = path4.resolve(__dirname, "../..");
97
+ REPO_ROOT = path4.resolve(LOCAL_AGENT_ROOT, "..");
98
+ BUNDLE_DIR_INSTALLED = path4.join(
99
99
  LOCAL_AGENT_ROOT,
100
100
  "dist/engine",
101
101
  engineRelease.releaseTag
102
102
  );
103
- BUNDLE_DIR_LOCAL_BUILD = path3.join(
103
+ BUNDLE_DIR_LOCAL_BUILD = path4.join(
104
104
  REPO_ROOT,
105
105
  "build/diologue-engine-bundles"
106
106
  );
@@ -117,11 +117,11 @@ var init_engine_bundle = __esm({
117
117
  try {
118
118
  const require2 = createRequire(import.meta.url);
119
119
  const pkgJsonPath = require2.resolve("opencode-ai/package.json");
120
- const pkgDir = path3.dirname(pkgJsonPath);
120
+ const pkgDir = path4.dirname(pkgJsonPath);
121
121
  const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
122
122
  const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.opencode;
123
123
  if (!binRel) return null;
124
- return path3.join(pkgDir, binRel);
124
+ return path4.join(pkgDir, binRel);
125
125
  } catch {
126
126
  return null;
127
127
  }
@@ -145,11 +145,11 @@ var init_engine_bundle = __esm({
145
145
  return { path: opencodeAi, source: "opencode-ai" };
146
146
  }
147
147
  const filename = bundleFilename();
148
- const installed = path3.join(BUNDLE_DIR_INSTALLED, filename);
148
+ const installed = path4.join(BUNDLE_DIR_INSTALLED, filename);
149
149
  if (await exists2(installed)) {
150
150
  return { path: installed, source: "installed" };
151
151
  }
152
- const localBuild = path3.join(BUNDLE_DIR_LOCAL_BUILD, filename);
152
+ const localBuild = path4.join(BUNDLE_DIR_LOCAL_BUILD, filename);
153
153
  if (await exists2(localBuild)) {
154
154
  return { path: localBuild, source: "local-build" };
155
155
  }
@@ -165,7 +165,7 @@ __export(engine_locator_local_exports, {
165
165
  });
166
166
  import { access as access3, constants as constants2 } from "node:fs/promises";
167
167
  import { spawn as spawn2 } from "node:child_process";
168
- import path4 from "node:path";
168
+ import path5 from "node:path";
169
169
  import { fileURLToPath as fileURLToPath2 } from "node:url";
170
170
  var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, isRecord, parseExistingInlineConfig, mergeInlineConfig, buildEngineEnv, defaultClientFactory, LocalEngineLocator, truncate;
171
171
  var init_engine_locator_local = __esm({
@@ -173,9 +173,9 @@ var init_engine_locator_local = __esm({
173
173
  "use strict";
174
174
  init_engine_bundle();
175
175
  __filename2 = fileURLToPath2(import.meta.url);
176
- __dirname2 = path4.dirname(__filename2);
177
- REPO_ROOT2 = path4.resolve(__dirname2, "../../..");
178
- DEFAULT_ENGINE_DIR = path4.join(REPO_ROOT2, "build/diologue-engine");
176
+ __dirname2 = path5.dirname(__filename2);
177
+ REPO_ROOT2 = path5.resolve(__dirname2, "../../..");
178
+ DEFAULT_ENGINE_DIR = path5.join(REPO_ROOT2, "build/diologue-engine");
179
179
  ENTRY_REL = "packages/opencode/src/index.ts";
180
180
  DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
181
181
  LISTENING_REGEX = /listening on (https?:\/\/[^\s]+)/i;
@@ -260,7 +260,7 @@ var init_engine_locator_local = __esm({
260
260
  - Reinstall the package so postinstall fetches a bundle, or
261
261
  - Run \`npm run rebrand-engine\` to populate the source tree.`;
262
262
  }
263
- const entry = path4.join(this.enginePath, ENTRY_REL);
263
+ const entry = path5.join(this.enginePath, ENTRY_REL);
264
264
  if (!await exists3(entry)) {
265
265
  return `[engine-locator/local] Engine entry not found at ${entry}. The rebrand may have produced a partial tree \u2014 re-run \`npm run rebrand-engine\`.`;
266
266
  }
@@ -293,7 +293,7 @@ var init_engine_locator_local = __esm({
293
293
  command: this.runtime,
294
294
  args: [
295
295
  "run",
296
- path4.join(this.enginePath, ENTRY_REL),
296
+ path5.join(this.enginePath, ENTRY_REL),
297
297
  "serve",
298
298
  "--port",
299
299
  "0",
@@ -356,7 +356,7 @@ var init_engine_locator_local = __esm({
356
356
  return new Promise((resolve, reject) => {
357
357
  let settled = false;
358
358
  let buffered = "";
359
- const tag = `${this.runtime}/${path4.basename(this.enginePath)}`;
359
+ const tag = `${this.runtime}/${path5.basename(this.enginePath)}`;
360
360
  const settle = (fn) => {
361
361
  if (settled) return;
362
362
  settled = true;
@@ -560,24 +560,11 @@ var createState = () => {
560
560
  };
561
561
  };
562
562
 
563
- // src/routes/health.ts
564
- var createHealthHandler = (config) => {
565
- return (_req, res) => {
566
- const body = {
567
- ok: true,
568
- helperVersion: config.helperVersion,
569
- boundHost: config.host,
570
- port: config.port,
571
- startedAt: config.startedAt
572
- };
573
- res.json(body);
574
- };
575
- };
576
-
577
- // src/routes/repo.ts
578
- import { Router as createRouter } from "express";
579
- import path2 from "node:path";
580
- import { z } from "zod";
563
+ // src/worktree.ts
564
+ import { existsSync } from "node:fs";
565
+ import { mkdir, rm } from "node:fs/promises";
566
+ import { homedir } from "node:os";
567
+ import path from "node:path";
581
568
 
582
569
  // src/lib/git.ts
583
570
  import { execFile } from "node:child_process";
@@ -821,10 +808,105 @@ var branchNameForTitle = (title) => {
821
808
  const rand = Math.random().toString(36).slice(2, 7);
822
809
  return `diologue/${slug || "change"}-${rand}`;
823
810
  };
811
+ var branchExists = async (repoPath, branch) => {
812
+ try {
813
+ await runGit(repoPath, [
814
+ "rev-parse",
815
+ "--verify",
816
+ "--quiet",
817
+ `refs/heads/${branch}`
818
+ ]);
819
+ return true;
820
+ } catch (err) {
821
+ if (err instanceof GitCommandError) return false;
822
+ throw err;
823
+ }
824
+ };
825
+ var addWorktree = async (repoPath, worktreePath, branch) => {
826
+ await runGit(repoPath, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
827
+ };
828
+ var addWorktreeForBranch = async (repoPath, worktreePath, branch) => {
829
+ await runGit(repoPath, ["worktree", "add", worktreePath, branch]);
830
+ };
831
+ var removeWorktree = async (repoPath, worktreePath) => {
832
+ await runGit(repoPath, ["worktree", "remove", "--force", worktreePath]);
833
+ };
834
+ var pruneWorktrees = async (repoPath) => {
835
+ await runGit(repoPath, ["worktree", "prune"]);
836
+ };
837
+
838
+ // src/worktree.ts
839
+ var sanitize = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 50) || "session";
840
+ var createWorktreeManager = (options = {}) => {
841
+ const baseDir = options.baseDir ?? path.join(homedir(), ".diologue", "worktrees");
842
+ const map = /* @__PURE__ */ new Map();
843
+ const ensure = async (sessionId, repoPath, baseBranch) => {
844
+ const existing = map.get(sessionId);
845
+ if (existing && existsSync(existing.path)) return existing;
846
+ const safe = sanitize(sessionId);
847
+ const branch = `diologue/${safe}`;
848
+ const worktreePath = path.join(baseDir, safe);
849
+ await mkdir(baseDir, { recursive: true });
850
+ await pruneWorktrees(repoPath).catch(() => void 0);
851
+ if (existsSync(worktreePath)) {
852
+ await removeWorktree(repoPath, worktreePath).catch(() => void 0);
853
+ await rm(worktreePath, { recursive: true, force: true }).catch(
854
+ () => void 0
855
+ );
856
+ await pruneWorktrees(repoPath).catch(() => void 0);
857
+ }
858
+ if (await branchExists(repoPath, branch)) {
859
+ await addWorktreeForBranch(repoPath, worktreePath, branch);
860
+ } else {
861
+ await addWorktree(repoPath, worktreePath, branch);
862
+ }
863
+ const info = {
864
+ path: worktreePath,
865
+ branch,
866
+ base: baseBranch,
867
+ repoPath
868
+ };
869
+ map.set(sessionId, info);
870
+ return info;
871
+ };
872
+ const get = (sessionId) => map.get(sessionId) ?? null;
873
+ const remove = async (sessionId) => {
874
+ const info = map.get(sessionId);
875
+ if (!info) return;
876
+ map.delete(sessionId);
877
+ await removeWorktree(info.repoPath, info.path).catch(() => void 0);
878
+ await rm(info.path, { recursive: true, force: true }).catch(
879
+ () => void 0
880
+ );
881
+ };
882
+ const cleanupAll = async () => {
883
+ for (const id of [...map.keys()]) await remove(id);
884
+ };
885
+ return { ensure, get, remove, cleanupAll };
886
+ };
887
+
888
+ // src/routes/health.ts
889
+ var createHealthHandler = (config) => {
890
+ return (_req, res) => {
891
+ const body = {
892
+ ok: true,
893
+ helperVersion: config.helperVersion,
894
+ boundHost: config.host,
895
+ port: config.port,
896
+ startedAt: config.startedAt
897
+ };
898
+ res.json(body);
899
+ };
900
+ };
901
+
902
+ // src/routes/repo.ts
903
+ import { Router as createRouter } from "express";
904
+ import path3 from "node:path";
905
+ import { z } from "zod";
824
906
 
825
907
  // src/lib/paths.ts
826
908
  import { access, lstat, realpath, stat } from "node:fs/promises";
827
- import path from "node:path";
909
+ import path2 from "node:path";
828
910
  var InvalidRepoPathError = class extends Error {
829
911
  code;
830
912
  constructor(code, message) {
@@ -849,7 +931,7 @@ var validateRepoPath = async (raw) => {
849
931
  if (trimmed.length === 0) {
850
932
  throw new InvalidRepoPathError("empty", "path must not be empty");
851
933
  }
852
- if (!path.isAbsolute(trimmed)) {
934
+ if (!path2.isAbsolute(trimmed)) {
853
935
  throw new InvalidRepoPathError(
854
936
  "not_absolute",
855
937
  `path must be absolute (got: ${trimmed})`
@@ -871,7 +953,7 @@ var validateRepoPath = async (raw) => {
871
953
  `path is not a directory: ${resolved}`
872
954
  );
873
955
  }
874
- const gitMarker = path.join(resolved, ".git");
956
+ const gitMarker = path2.join(resolved, ".git");
875
957
  if (!await exists(gitMarker)) {
876
958
  throw new InvalidRepoPathError(
877
959
  "not_a_git_repo",
@@ -884,7 +966,7 @@ var validateRepoPath = async (raw) => {
884
966
 
885
967
  // src/lib/pick-directory.ts
886
968
  import { execFile as execFile2 } from "node:child_process";
887
- import { homedir } from "node:os";
969
+ import { homedir as homedir2 } from "node:os";
888
970
  var PROMPT = "Select a git repository";
889
971
  var dialogSpec = (platform) => {
890
972
  switch (platform) {
@@ -944,7 +1026,7 @@ var pickDirectory = async () => {
944
1026
  };
945
1027
  const result = await attempt(spec.cmd, spec.args);
946
1028
  if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
947
- return attempt("kdialog", ["--getexistingdirectory", homedir()]);
1029
+ return attempt("kdialog", ["--getexistingdirectory", homedir2()]);
948
1030
  }
949
1031
  return result;
950
1032
  };
@@ -1020,14 +1102,18 @@ var selectRepoSchema = z.object({
1020
1102
  });
1021
1103
  var applyPatchSchema = z.object({
1022
1104
  unified: z.string().min(1).max(8 * 1024 * 1024),
1023
- baselineHash: z.string().optional()
1105
+ baselineHash: z.string().optional(),
1106
+ sessionId: z.string().min(1).optional()
1024
1107
  });
1025
1108
  var revertPatchSchema = z.object({
1026
- unified: z.string().min(1).max(8 * 1024 * 1024)
1109
+ unified: z.string().min(1).max(8 * 1024 * 1024),
1110
+ sessionId: z.string().min(1).optional()
1027
1111
  });
1028
1112
  var createPrSchema = z.object({
1029
1113
  title: z.string().min(1).max(200),
1030
- body: z.string().max(2e4).optional()
1114
+ body: z.string().max(2e4).optional(),
1115
+ /** When set, the PR is created from this session's worktree branch. */
1116
+ sessionId: z.string().min(1).optional()
1031
1117
  });
1032
1118
  var buildRepoStatus = async (resolvedPath) => {
1033
1119
  const [branch, head, dirty] = await Promise.all([
@@ -1037,7 +1123,7 @@ var buildRepoStatus = async (resolvedPath) => {
1037
1123
  ]);
1038
1124
  return {
1039
1125
  path: resolvedPath,
1040
- name: path2.basename(resolvedPath),
1126
+ name: path3.basename(resolvedPath),
1041
1127
  branch,
1042
1128
  head,
1043
1129
  isDirty: dirty
@@ -1061,7 +1147,8 @@ var requireSelectedRepo = (state, res) => {
1061
1147
  }
1062
1148
  return repo;
1063
1149
  };
1064
- var createRepoRouter = (state) => {
1150
+ var readSessionId = (value) => typeof value === "string" && value ? value : void 0;
1151
+ var createRepoRouter = (state, worktrees) => {
1065
1152
  const router = createRouter();
1066
1153
  router.post("/select", async (req, res) => {
1067
1154
  const parsed = selectRepoSchema.safeParse(req.body);
@@ -1114,7 +1201,8 @@ var createRepoRouter = (state) => {
1114
1201
  }
1115
1202
  const { title } = parsed.data;
1116
1203
  const body = parsed.data.body ?? "Opened from the Diologue coding agent.";
1117
- const cwd = repo.path;
1204
+ const worktree = parsed.data.sessionId ? worktrees.get(parsed.data.sessionId) : null;
1205
+ const cwd = worktree ? worktree.path : repo.path;
1118
1206
  try {
1119
1207
  if (!await ghReady()) {
1120
1208
  res.status(501).json({
@@ -1137,12 +1225,17 @@ var createRepoRouter = (state) => {
1137
1225
  });
1138
1226
  return;
1139
1227
  }
1140
- const current = await getBranch(cwd) ?? "";
1141
- const onAgentBranch = current.startsWith("diologue/");
1142
- const base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
1143
- const branch = onAgentBranch ? current : branchNameForTitle(title);
1144
- if (!onAgentBranch) {
1145
- await createBranch(cwd, branch);
1228
+ let branch;
1229
+ let base;
1230
+ if (worktree) {
1231
+ branch = worktree.branch;
1232
+ base = worktree.base;
1233
+ } else {
1234
+ const current = await getBranch(cwd) ?? "";
1235
+ const onAgentBranch = current.startsWith("diologue/");
1236
+ base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
1237
+ branch = onAgentBranch ? current : branchNameForTitle(title);
1238
+ if (!onAgentBranch) await createBranch(cwd, branch);
1146
1239
  }
1147
1240
  await commitAll(cwd, title);
1148
1241
  await pushBranch(cwd, branch);
@@ -1181,13 +1274,23 @@ var createRepoRouter = (state) => {
1181
1274
  throw err;
1182
1275
  }
1183
1276
  });
1184
- router.get("/diff", async (_req, res) => {
1277
+ const resolveWorkdir = (repo, sessionId) => {
1278
+ if (!sessionId) return repo.path;
1279
+ const wt = worktrees.get(sessionId);
1280
+ return wt ? wt.path : null;
1281
+ };
1282
+ router.get("/diff", async (req, res) => {
1185
1283
  const repo = requireSelectedRepo(state, res);
1186
1284
  if (!repo) {
1187
1285
  return;
1188
1286
  }
1287
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1288
+ if (!workdir) {
1289
+ res.json({ unified: "", sizeBytes: 0 });
1290
+ return;
1291
+ }
1189
1292
  try {
1190
- const unified = await getDiff(repo.path);
1293
+ const unified = await getDiff(workdir);
1191
1294
  const body = {
1192
1295
  unified,
1193
1296
  sizeBytes: Buffer.byteLength(unified, "utf8")
@@ -1201,13 +1304,18 @@ var createRepoRouter = (state) => {
1201
1304
  throw err;
1202
1305
  }
1203
1306
  });
1204
- router.get("/changed-files", async (_req, res) => {
1307
+ router.get("/changed-files", async (req, res) => {
1205
1308
  const repo = requireSelectedRepo(state, res);
1206
1309
  if (!repo) {
1207
1310
  return;
1208
1311
  }
1312
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1313
+ if (!workdir) {
1314
+ res.json({ files: [] });
1315
+ return;
1316
+ }
1209
1317
  try {
1210
- const files = await getStatusShort(repo.path);
1318
+ const files = await getStatusShort(workdir);
1211
1319
  const body = { files };
1212
1320
  res.json(body);
1213
1321
  } catch (err) {
@@ -1226,8 +1334,9 @@ var createRepoRouter = (state) => {
1226
1334
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1227
1335
  return;
1228
1336
  }
1337
+ const applyWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1229
1338
  try {
1230
- const check = await canApplyDiff(repo.path, parsed.data.unified);
1339
+ const check = await canApplyDiff(applyWorkdir, parsed.data.unified);
1231
1340
  if (!check.ok) {
1232
1341
  res.status(409).json({
1233
1342
  error: "diff_does_not_apply",
@@ -1236,7 +1345,7 @@ var createRepoRouter = (state) => {
1236
1345
  });
1237
1346
  return;
1238
1347
  }
1239
- await applyDiff(repo.path, parsed.data.unified);
1348
+ await applyDiff(applyWorkdir, parsed.data.unified);
1240
1349
  const paths = parseDiffPaths(parsed.data.unified);
1241
1350
  const body = {
1242
1351
  ok: true,
@@ -1260,8 +1369,9 @@ var createRepoRouter = (state) => {
1260
1369
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1261
1370
  return;
1262
1371
  }
1372
+ const revertWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1263
1373
  try {
1264
- const check = await canRevertDiff(repo.path, parsed.data.unified);
1374
+ const check = await canRevertDiff(revertWorkdir, parsed.data.unified);
1265
1375
  if (!check.ok) {
1266
1376
  res.status(409).json({
1267
1377
  error: "diff_does_not_revert",
@@ -1270,7 +1380,7 @@ var createRepoRouter = (state) => {
1270
1380
  });
1271
1381
  return;
1272
1382
  }
1273
- await revertDiff(repo.path, parsed.data.unified);
1383
+ await revertDiff(revertWorkdir, parsed.data.unified);
1274
1384
  const paths = parseDiffPaths(parsed.data.unified);
1275
1385
  const body = {
1276
1386
  ok: true,
@@ -1539,8 +1649,21 @@ var createAgentRouter = (deps) => {
1539
1649
  res.status(409).json({ error: "no_repo_selected" });
1540
1650
  return;
1541
1651
  }
1652
+ let workdir = repo.path;
1653
+ try {
1654
+ const worktree = await deps.worktrees.ensure(
1655
+ parsed.data.sessionId,
1656
+ repo.path,
1657
+ repo.branch ?? "main"
1658
+ );
1659
+ workdir = worktree.path;
1660
+ } catch (err) {
1661
+ logAgentRoute(
1662
+ `worktree create failed (falling back to in-place) session=${parsed.data.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
1663
+ );
1664
+ }
1542
1665
  logAgentRoute(
1543
- `message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
1666
+ `message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} workdir=${workdir} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
1544
1667
  );
1545
1668
  res.setHeader("Content-Type", "text/event-stream");
1546
1669
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -1563,7 +1686,7 @@ var createAgentRouter = (deps) => {
1563
1686
  const history = parsed.data.history ? parsed.data.history.map((m) => ({ role: m.role, content: m.content })) : void 0;
1564
1687
  for await (const event of deps.adapter.streamMessage({
1565
1688
  sessionId: parsed.data.sessionId,
1566
- repoPath: repo.path,
1689
+ repoPath: workdir,
1567
1690
  prompt: parsed.data.prompt,
1568
1691
  history,
1569
1692
  signal: controller.signal,
@@ -2748,8 +2871,8 @@ var mapEvent = (item, state) => {
2748
2871
  {
2749
2872
  type: "diff_proposed",
2750
2873
  unified: "",
2751
- files: files.map((path6) => ({
2752
- path: path6,
2874
+ files: files.map((path7) => ({
2875
+ path: path7,
2753
2876
  additions: 0,
2754
2877
  deletions: 0,
2755
2878
  status: "modified"
@@ -3251,13 +3374,17 @@ var createApp = (options) => {
3251
3374
  const state = options.state ?? createState();
3252
3375
  const adapter = options.adapter ?? buildDefaultAdapter();
3253
3376
  const brokerRegistry = createBrokerRegistry();
3377
+ const worktrees = createWorktreeManager();
3254
3378
  const app = express();
3255
3379
  app.use(express.json({ limit: "1mb" }));
3256
3380
  app.use(createCorsMiddleware({ allowedOrigin: options.config.allowedOrigin }));
3257
3381
  app.use(createAuthMiddleware({ token: options.config.token }));
3258
3382
  app.get("/health", createHealthHandler(options.config));
3259
- app.use("/repo", createRepoRouter(state));
3260
- app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
3383
+ app.use("/repo", createRepoRouter(state, worktrees));
3384
+ app.use(
3385
+ "/agent",
3386
+ createAgentRouter({ state, adapter, brokerRegistry, worktrees })
3387
+ );
3261
3388
  app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
3262
3389
  app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
3263
3390
  app.use("/agent/permission", createPermissionRouter({ adapter }));
@@ -3265,7 +3392,7 @@ var createApp = (options) => {
3265
3392
  app.use((req, res) => {
3266
3393
  res.status(404).json({ error: "not_found", method: req.method, path: req.path });
3267
3394
  });
3268
- return { app, state, adapter, brokerRegistry };
3395
+ return { app, state, adapter, brokerRegistry, worktrees };
3269
3396
  };
3270
3397
 
3271
3398
  // src/modes/quickstart.ts
@@ -3360,7 +3487,7 @@ var runQuickstart = async (options) => {
3360
3487
 
3361
3488
  // src/lib/keychain.ts
3362
3489
  import { promises as fs } from "node:fs";
3363
- import path5 from "node:path";
3490
+ import path6 from "node:path";
3364
3491
  import os from "node:os";
3365
3492
  var KEYTAR_SERVICE = "diologue.local-agent";
3366
3493
  var KEYTAR_ACCOUNT = "device-credential";
@@ -3383,7 +3510,7 @@ var loadKeytar = async () => {
3383
3510
  };
3384
3511
  var fileFallbackPath = () => {
3385
3512
  const home = os.homedir();
3386
- return path5.join(home, ".diologue", "credentials");
3513
+ return path6.join(home, ".diologue", "credentials");
3387
3514
  };
3388
3515
  var createCredentialStore = (options = {}) => {
3389
3516
  const filePath = options.filePathOverride ?? fileFallbackPath();
@@ -3430,7 +3557,7 @@ var createCredentialStore = (options = {}) => {
3430
3557
  return { backend: "keychain" };
3431
3558
  }
3432
3559
  }
3433
- await fs.mkdir(path5.dirname(filePath), { recursive: true });
3560
+ await fs.mkdir(path6.dirname(filePath), { recursive: true });
3434
3561
  await fs.writeFile(filePath, JSON.stringify(credential, null, 2), {
3435
3562
  mode: 384
3436
3563
  });
@@ -3479,7 +3606,7 @@ var runStart = async (options) => {
3479
3606
  process.env.LOCAL_AGENT_ALLOWED_ORIGIN = options.allowedOrigin;
3480
3607
  if (options.adapter) process.env.LOCAL_AGENT_ADAPTER = options.adapter;
3481
3608
  const config = loadConfig();
3482
- const { app, adapter } = createApp({ config });
3609
+ const { app, adapter, worktrees } = createApp({ config });
3483
3610
  const server = app.listen(config.port, config.host, async () => {
3484
3611
  const url = `http://${config.host}:${config.port}`;
3485
3612
  console.log(`[local-agent] Listening on ${url}`);
@@ -3495,7 +3622,9 @@ var runStart = async (options) => {
3495
3622
  const shutdown = (signal) => {
3496
3623
  console.log(`
3497
3624
  [local-agent] Received ${signal}, shutting down...`);
3498
- server.close(() => process.exit(0));
3625
+ void worktrees.cleanupAll().finally(() => {
3626
+ server.close(() => process.exit(0));
3627
+ });
3499
3628
  setTimeout(() => process.exit(0), 3e3).unref();
3500
3629
  };
3501
3630
  process.on("SIGINT", shutdown);