@diologue/local-agent 0.7.0 → 0.9.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";
@@ -795,6 +782,14 @@ var getRemoteUrl = async (cwd) => {
795
782
  throw err;
796
783
  }
797
784
  };
785
+ var parseGithubRemote = (url) => {
786
+ const m = /github\.com[:/]+([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(url.trim());
787
+ if (!m) return null;
788
+ const owner = m[1];
789
+ const repo = m[2];
790
+ if (!owner || !repo) return null;
791
+ return { owner, repo };
792
+ };
798
793
  var getDefaultBranch = async (cwd) => {
799
794
  try {
800
795
  const ref = (await runGit(cwd, ["rev-parse", "--abbrev-ref", "origin/HEAD"])).trim();
@@ -821,10 +816,105 @@ var branchNameForTitle = (title) => {
821
816
  const rand = Math.random().toString(36).slice(2, 7);
822
817
  return `diologue/${slug || "change"}-${rand}`;
823
818
  };
819
+ var branchExists = async (repoPath, branch) => {
820
+ try {
821
+ await runGit(repoPath, [
822
+ "rev-parse",
823
+ "--verify",
824
+ "--quiet",
825
+ `refs/heads/${branch}`
826
+ ]);
827
+ return true;
828
+ } catch (err) {
829
+ if (err instanceof GitCommandError) return false;
830
+ throw err;
831
+ }
832
+ };
833
+ var addWorktree = async (repoPath, worktreePath, branch) => {
834
+ await runGit(repoPath, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
835
+ };
836
+ var addWorktreeForBranch = async (repoPath, worktreePath, branch) => {
837
+ await runGit(repoPath, ["worktree", "add", worktreePath, branch]);
838
+ };
839
+ var removeWorktree = async (repoPath, worktreePath) => {
840
+ await runGit(repoPath, ["worktree", "remove", "--force", worktreePath]);
841
+ };
842
+ var pruneWorktrees = async (repoPath) => {
843
+ await runGit(repoPath, ["worktree", "prune"]);
844
+ };
845
+
846
+ // src/worktree.ts
847
+ var sanitize = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 50) || "session";
848
+ var createWorktreeManager = (options = {}) => {
849
+ const baseDir = options.baseDir ?? path.join(homedir(), ".diologue", "worktrees");
850
+ const map = /* @__PURE__ */ new Map();
851
+ const ensure = async (sessionId, repoPath, baseBranch) => {
852
+ const existing = map.get(sessionId);
853
+ if (existing && existsSync(existing.path)) return existing;
854
+ const safe = sanitize(sessionId);
855
+ const branch = `diologue/${safe}`;
856
+ const worktreePath = path.join(baseDir, safe);
857
+ await mkdir(baseDir, { recursive: true });
858
+ await pruneWorktrees(repoPath).catch(() => void 0);
859
+ if (existsSync(worktreePath)) {
860
+ await removeWorktree(repoPath, worktreePath).catch(() => void 0);
861
+ await rm(worktreePath, { recursive: true, force: true }).catch(
862
+ () => void 0
863
+ );
864
+ await pruneWorktrees(repoPath).catch(() => void 0);
865
+ }
866
+ if (await branchExists(repoPath, branch)) {
867
+ await addWorktreeForBranch(repoPath, worktreePath, branch);
868
+ } else {
869
+ await addWorktree(repoPath, worktreePath, branch);
870
+ }
871
+ const info = {
872
+ path: worktreePath,
873
+ branch,
874
+ base: baseBranch,
875
+ repoPath
876
+ };
877
+ map.set(sessionId, info);
878
+ return info;
879
+ };
880
+ const get = (sessionId) => map.get(sessionId) ?? null;
881
+ const remove = async (sessionId) => {
882
+ const info = map.get(sessionId);
883
+ if (!info) return;
884
+ map.delete(sessionId);
885
+ await removeWorktree(info.repoPath, info.path).catch(() => void 0);
886
+ await rm(info.path, { recursive: true, force: true }).catch(
887
+ () => void 0
888
+ );
889
+ };
890
+ const cleanupAll = async () => {
891
+ for (const id of [...map.keys()]) await remove(id);
892
+ };
893
+ return { ensure, get, remove, cleanupAll };
894
+ };
895
+
896
+ // src/routes/health.ts
897
+ var createHealthHandler = (config) => {
898
+ return (_req, res) => {
899
+ const body = {
900
+ ok: true,
901
+ helperVersion: config.helperVersion,
902
+ boundHost: config.host,
903
+ port: config.port,
904
+ startedAt: config.startedAt
905
+ };
906
+ res.json(body);
907
+ };
908
+ };
909
+
910
+ // src/routes/repo.ts
911
+ import { Router as createRouter } from "express";
912
+ import path3 from "node:path";
913
+ import { z } from "zod";
824
914
 
825
915
  // src/lib/paths.ts
826
916
  import { access, lstat, realpath, stat } from "node:fs/promises";
827
- import path from "node:path";
917
+ import path2 from "node:path";
828
918
  var InvalidRepoPathError = class extends Error {
829
919
  code;
830
920
  constructor(code, message) {
@@ -849,7 +939,7 @@ var validateRepoPath = async (raw) => {
849
939
  if (trimmed.length === 0) {
850
940
  throw new InvalidRepoPathError("empty", "path must not be empty");
851
941
  }
852
- if (!path.isAbsolute(trimmed)) {
942
+ if (!path2.isAbsolute(trimmed)) {
853
943
  throw new InvalidRepoPathError(
854
944
  "not_absolute",
855
945
  `path must be absolute (got: ${trimmed})`
@@ -871,7 +961,7 @@ var validateRepoPath = async (raw) => {
871
961
  `path is not a directory: ${resolved}`
872
962
  );
873
963
  }
874
- const gitMarker = path.join(resolved, ".git");
964
+ const gitMarker = path2.join(resolved, ".git");
875
965
  if (!await exists(gitMarker)) {
876
966
  throw new InvalidRepoPathError(
877
967
  "not_a_git_repo",
@@ -884,7 +974,7 @@ var validateRepoPath = async (raw) => {
884
974
 
885
975
  // src/lib/pick-directory.ts
886
976
  import { execFile as execFile2 } from "node:child_process";
887
- import { homedir } from "node:os";
977
+ import { homedir as homedir2 } from "node:os";
888
978
  var PROMPT = "Select a git repository";
889
979
  var dialogSpec = (platform) => {
890
980
  switch (platform) {
@@ -944,7 +1034,7 @@ var pickDirectory = async () => {
944
1034
  };
945
1035
  const result = await attempt(spec.cmd, spec.args);
946
1036
  if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
947
- return attempt("kdialog", ["--getexistingdirectory", homedir()]);
1037
+ return attempt("kdialog", ["--getexistingdirectory", homedir2()]);
948
1038
  }
949
1039
  return result;
950
1040
  };
@@ -1020,14 +1110,22 @@ var selectRepoSchema = z.object({
1020
1110
  });
1021
1111
  var applyPatchSchema = z.object({
1022
1112
  unified: z.string().min(1).max(8 * 1024 * 1024),
1023
- baselineHash: z.string().optional()
1113
+ baselineHash: z.string().optional(),
1114
+ sessionId: z.string().min(1).optional()
1024
1115
  });
1025
1116
  var revertPatchSchema = z.object({
1026
- unified: z.string().min(1).max(8 * 1024 * 1024)
1117
+ unified: z.string().min(1).max(8 * 1024 * 1024),
1118
+ sessionId: z.string().min(1).optional()
1027
1119
  });
1028
1120
  var createPrSchema = z.object({
1029
1121
  title: z.string().min(1).max(200),
1030
- body: z.string().max(2e4).optional()
1122
+ body: z.string().max(2e4).optional(),
1123
+ /** When set, the PR is created from this session's worktree branch. */
1124
+ sessionId: z.string().min(1).optional()
1125
+ });
1126
+ var pushSchema = z.object({
1127
+ title: z.string().min(1).max(200),
1128
+ sessionId: z.string().min(1).optional()
1031
1129
  });
1032
1130
  var buildRepoStatus = async (resolvedPath) => {
1033
1131
  const [branch, head, dirty] = await Promise.all([
@@ -1037,7 +1135,7 @@ var buildRepoStatus = async (resolvedPath) => {
1037
1135
  ]);
1038
1136
  return {
1039
1137
  path: resolvedPath,
1040
- name: path2.basename(resolvedPath),
1138
+ name: path3.basename(resolvedPath),
1041
1139
  branch,
1042
1140
  head,
1043
1141
  isDirty: dirty
@@ -1061,7 +1159,8 @@ var requireSelectedRepo = (state, res) => {
1061
1159
  }
1062
1160
  return repo;
1063
1161
  };
1064
- var createRepoRouter = (state) => {
1162
+ var readSessionId = (value) => typeof value === "string" && value ? value : void 0;
1163
+ var createRepoRouter = (state, worktrees) => {
1065
1164
  const router = createRouter();
1066
1165
  router.post("/select", async (req, res) => {
1067
1166
  const parsed = selectRepoSchema.safeParse(req.body);
@@ -1114,7 +1213,8 @@ var createRepoRouter = (state) => {
1114
1213
  }
1115
1214
  const { title } = parsed.data;
1116
1215
  const body = parsed.data.body ?? "Opened from the Diologue coding agent.";
1117
- const cwd = repo.path;
1216
+ const worktree = parsed.data.sessionId ? worktrees.get(parsed.data.sessionId) : null;
1217
+ const cwd = worktree ? worktree.path : repo.path;
1118
1218
  try {
1119
1219
  if (!await ghReady()) {
1120
1220
  res.status(501).json({
@@ -1137,12 +1237,17 @@ var createRepoRouter = (state) => {
1137
1237
  });
1138
1238
  return;
1139
1239
  }
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);
1240
+ let branch;
1241
+ let base;
1242
+ if (worktree) {
1243
+ branch = worktree.branch;
1244
+ base = worktree.base;
1245
+ } else {
1246
+ const current = await getBranch(cwd) ?? "";
1247
+ const onAgentBranch = current.startsWith("diologue/");
1248
+ base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
1249
+ branch = onAgentBranch ? current : branchNameForTitle(title);
1250
+ if (!onAgentBranch) await createBranch(cwd, branch);
1146
1251
  }
1147
1252
  await commitAll(cwd, title);
1148
1253
  await pushBranch(cwd, branch);
@@ -1161,6 +1266,69 @@ var createRepoRouter = (state) => {
1161
1266
  throw err;
1162
1267
  }
1163
1268
  });
1269
+ router.post("/push", async (req, res) => {
1270
+ const repo = requireSelectedRepo(state, res);
1271
+ if (!repo) return;
1272
+ const parsed = pushSchema.safeParse(req.body);
1273
+ if (!parsed.success) {
1274
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1275
+ return;
1276
+ }
1277
+ const worktree = parsed.data.sessionId ? worktrees.get(parsed.data.sessionId) : null;
1278
+ const cwd = worktree ? worktree.path : repo.path;
1279
+ try {
1280
+ const remoteUrl = await getRemoteUrl(cwd);
1281
+ if (!remoteUrl) {
1282
+ res.status(400).json({
1283
+ error: "no_remote",
1284
+ message: "This repo has no `origin` remote."
1285
+ });
1286
+ return;
1287
+ }
1288
+ const gh = parseGithubRemote(remoteUrl);
1289
+ if (!gh) {
1290
+ res.status(400).json({
1291
+ error: "not_github",
1292
+ message: "The `origin` remote isn't a GitHub repository."
1293
+ });
1294
+ return;
1295
+ }
1296
+ if (!await isDirty(cwd)) {
1297
+ res.status(409).json({
1298
+ error: "nothing_to_commit",
1299
+ message: "There are no changes in the working tree to open a PR for."
1300
+ });
1301
+ return;
1302
+ }
1303
+ let head;
1304
+ let base;
1305
+ if (worktree) {
1306
+ head = worktree.branch;
1307
+ base = worktree.base;
1308
+ } else {
1309
+ const current = await getBranch(cwd) ?? "";
1310
+ const onAgentBranch = current.startsWith("diologue/");
1311
+ base = onAgentBranch ? await getDefaultBranch(cwd) : current || "main";
1312
+ head = onAgentBranch ? current : branchNameForTitle(parsed.data.title);
1313
+ if (!onAgentBranch) await createBranch(cwd, head);
1314
+ }
1315
+ await commitAll(cwd, parsed.data.title);
1316
+ await pushBranch(cwd, head);
1317
+ const payload = {
1318
+ owner: gh.owner,
1319
+ repo: gh.repo,
1320
+ head,
1321
+ base
1322
+ };
1323
+ res.json(payload);
1324
+ } catch (err) {
1325
+ if (err instanceof GitCommandError) {
1326
+ sendGitError(res, err);
1327
+ return;
1328
+ }
1329
+ throw err;
1330
+ }
1331
+ });
1164
1332
  router.get("/status", async (_req, res) => {
1165
1333
  const repo = state.getSelectedRepo();
1166
1334
  if (!repo) {
@@ -1181,13 +1349,23 @@ var createRepoRouter = (state) => {
1181
1349
  throw err;
1182
1350
  }
1183
1351
  });
1184
- router.get("/diff", async (_req, res) => {
1352
+ const resolveWorkdir = (repo, sessionId) => {
1353
+ if (!sessionId) return repo.path;
1354
+ const wt = worktrees.get(sessionId);
1355
+ return wt ? wt.path : null;
1356
+ };
1357
+ router.get("/diff", async (req, res) => {
1185
1358
  const repo = requireSelectedRepo(state, res);
1186
1359
  if (!repo) {
1187
1360
  return;
1188
1361
  }
1362
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1363
+ if (!workdir) {
1364
+ res.json({ unified: "", sizeBytes: 0 });
1365
+ return;
1366
+ }
1189
1367
  try {
1190
- const unified = await getDiff(repo.path);
1368
+ const unified = await getDiff(workdir);
1191
1369
  const body = {
1192
1370
  unified,
1193
1371
  sizeBytes: Buffer.byteLength(unified, "utf8")
@@ -1201,13 +1379,18 @@ var createRepoRouter = (state) => {
1201
1379
  throw err;
1202
1380
  }
1203
1381
  });
1204
- router.get("/changed-files", async (_req, res) => {
1382
+ router.get("/changed-files", async (req, res) => {
1205
1383
  const repo = requireSelectedRepo(state, res);
1206
1384
  if (!repo) {
1207
1385
  return;
1208
1386
  }
1387
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1388
+ if (!workdir) {
1389
+ res.json({ files: [] });
1390
+ return;
1391
+ }
1209
1392
  try {
1210
- const files = await getStatusShort(repo.path);
1393
+ const files = await getStatusShort(workdir);
1211
1394
  const body = { files };
1212
1395
  res.json(body);
1213
1396
  } catch (err) {
@@ -1226,8 +1409,9 @@ var createRepoRouter = (state) => {
1226
1409
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1227
1410
  return;
1228
1411
  }
1412
+ const applyWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1229
1413
  try {
1230
- const check = await canApplyDiff(repo.path, parsed.data.unified);
1414
+ const check = await canApplyDiff(applyWorkdir, parsed.data.unified);
1231
1415
  if (!check.ok) {
1232
1416
  res.status(409).json({
1233
1417
  error: "diff_does_not_apply",
@@ -1236,7 +1420,7 @@ var createRepoRouter = (state) => {
1236
1420
  });
1237
1421
  return;
1238
1422
  }
1239
- await applyDiff(repo.path, parsed.data.unified);
1423
+ await applyDiff(applyWorkdir, parsed.data.unified);
1240
1424
  const paths = parseDiffPaths(parsed.data.unified);
1241
1425
  const body = {
1242
1426
  ok: true,
@@ -1260,8 +1444,9 @@ var createRepoRouter = (state) => {
1260
1444
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1261
1445
  return;
1262
1446
  }
1447
+ const revertWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1263
1448
  try {
1264
- const check = await canRevertDiff(repo.path, parsed.data.unified);
1449
+ const check = await canRevertDiff(revertWorkdir, parsed.data.unified);
1265
1450
  if (!check.ok) {
1266
1451
  res.status(409).json({
1267
1452
  error: "diff_does_not_revert",
@@ -1270,7 +1455,7 @@ var createRepoRouter = (state) => {
1270
1455
  });
1271
1456
  return;
1272
1457
  }
1273
- await revertDiff(repo.path, parsed.data.unified);
1458
+ await revertDiff(revertWorkdir, parsed.data.unified);
1274
1459
  const paths = parseDiffPaths(parsed.data.unified);
1275
1460
  const body = {
1276
1461
  ok: true,
@@ -1539,8 +1724,21 @@ var createAgentRouter = (deps) => {
1539
1724
  res.status(409).json({ error: "no_repo_selected" });
1540
1725
  return;
1541
1726
  }
1727
+ let workdir = repo.path;
1728
+ try {
1729
+ const worktree = await deps.worktrees.ensure(
1730
+ parsed.data.sessionId,
1731
+ repo.path,
1732
+ repo.branch ?? "main"
1733
+ );
1734
+ workdir = worktree.path;
1735
+ } catch (err) {
1736
+ logAgentRoute(
1737
+ `worktree create failed (falling back to in-place) session=${parsed.data.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
1738
+ );
1739
+ }
1542
1740
  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)"}`
1741
+ `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
1742
  );
1545
1743
  res.setHeader("Content-Type", "text/event-stream");
1546
1744
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -1563,7 +1761,7 @@ var createAgentRouter = (deps) => {
1563
1761
  const history = parsed.data.history ? parsed.data.history.map((m) => ({ role: m.role, content: m.content })) : void 0;
1564
1762
  for await (const event of deps.adapter.streamMessage({
1565
1763
  sessionId: parsed.data.sessionId,
1566
- repoPath: repo.path,
1764
+ repoPath: workdir,
1567
1765
  prompt: parsed.data.prompt,
1568
1766
  history,
1569
1767
  signal: controller.signal,
@@ -2748,8 +2946,8 @@ var mapEvent = (item, state) => {
2748
2946
  {
2749
2947
  type: "diff_proposed",
2750
2948
  unified: "",
2751
- files: files.map((path6) => ({
2752
- path: path6,
2949
+ files: files.map((path7) => ({
2950
+ path: path7,
2753
2951
  additions: 0,
2754
2952
  deletions: 0,
2755
2953
  status: "modified"
@@ -3251,13 +3449,17 @@ var createApp = (options) => {
3251
3449
  const state = options.state ?? createState();
3252
3450
  const adapter = options.adapter ?? buildDefaultAdapter();
3253
3451
  const brokerRegistry = createBrokerRegistry();
3452
+ const worktrees = createWorktreeManager();
3254
3453
  const app = express();
3255
3454
  app.use(express.json({ limit: "1mb" }));
3256
3455
  app.use(createCorsMiddleware({ allowedOrigin: options.config.allowedOrigin }));
3257
3456
  app.use(createAuthMiddleware({ token: options.config.token }));
3258
3457
  app.get("/health", createHealthHandler(options.config));
3259
- app.use("/repo", createRepoRouter(state));
3260
- app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
3458
+ app.use("/repo", createRepoRouter(state, worktrees));
3459
+ app.use(
3460
+ "/agent",
3461
+ createAgentRouter({ state, adapter, brokerRegistry, worktrees })
3462
+ );
3261
3463
  app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
3262
3464
  app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
3263
3465
  app.use("/agent/permission", createPermissionRouter({ adapter }));
@@ -3265,7 +3467,7 @@ var createApp = (options) => {
3265
3467
  app.use((req, res) => {
3266
3468
  res.status(404).json({ error: "not_found", method: req.method, path: req.path });
3267
3469
  });
3268
- return { app, state, adapter, brokerRegistry };
3470
+ return { app, state, adapter, brokerRegistry, worktrees };
3269
3471
  };
3270
3472
 
3271
3473
  // src/modes/quickstart.ts
@@ -3360,7 +3562,7 @@ var runQuickstart = async (options) => {
3360
3562
 
3361
3563
  // src/lib/keychain.ts
3362
3564
  import { promises as fs } from "node:fs";
3363
- import path5 from "node:path";
3565
+ import path6 from "node:path";
3364
3566
  import os from "node:os";
3365
3567
  var KEYTAR_SERVICE = "diologue.local-agent";
3366
3568
  var KEYTAR_ACCOUNT = "device-credential";
@@ -3383,7 +3585,7 @@ var loadKeytar = async () => {
3383
3585
  };
3384
3586
  var fileFallbackPath = () => {
3385
3587
  const home = os.homedir();
3386
- return path5.join(home, ".diologue", "credentials");
3588
+ return path6.join(home, ".diologue", "credentials");
3387
3589
  };
3388
3590
  var createCredentialStore = (options = {}) => {
3389
3591
  const filePath = options.filePathOverride ?? fileFallbackPath();
@@ -3430,7 +3632,7 @@ var createCredentialStore = (options = {}) => {
3430
3632
  return { backend: "keychain" };
3431
3633
  }
3432
3634
  }
3433
- await fs.mkdir(path5.dirname(filePath), { recursive: true });
3635
+ await fs.mkdir(path6.dirname(filePath), { recursive: true });
3434
3636
  await fs.writeFile(filePath, JSON.stringify(credential, null, 2), {
3435
3637
  mode: 384
3436
3638
  });
@@ -3479,7 +3681,7 @@ var runStart = async (options) => {
3479
3681
  process.env.LOCAL_AGENT_ALLOWED_ORIGIN = options.allowedOrigin;
3480
3682
  if (options.adapter) process.env.LOCAL_AGENT_ADAPTER = options.adapter;
3481
3683
  const config = loadConfig();
3482
- const { app, adapter } = createApp({ config });
3684
+ const { app, adapter, worktrees } = createApp({ config });
3483
3685
  const server = app.listen(config.port, config.host, async () => {
3484
3686
  const url = `http://${config.host}:${config.port}`;
3485
3687
  console.log(`[local-agent] Listening on ${url}`);
@@ -3495,7 +3697,9 @@ var runStart = async (options) => {
3495
3697
  const shutdown = (signal) => {
3496
3698
  console.log(`
3497
3699
  [local-agent] Received ${signal}, shutting down...`);
3498
- server.close(() => process.exit(0));
3700
+ void worktrees.cleanupAll().finally(() => {
3701
+ server.close(() => process.exit(0));
3702
+ });
3499
3703
  setTimeout(() => process.exit(0), 3e3).unref();
3500
3704
  };
3501
3705
  process.on("SIGINT", shutdown);