@diologue/local-agent 0.6.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";
@@ -726,9 +713,9 @@ var getDiff = async (cwd) => {
726
713
  return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
727
714
  };
728
715
  var runGitWithStdin = async (cwd, args, input) => {
729
- const { execFile: execFile3 } = await import("node:child_process");
716
+ const { execFile: execFile4 } = await import("node:child_process");
730
717
  return new Promise((resolve, reject) => {
731
- const child = execFile3(
718
+ const child = execFile4(
732
719
  "git",
733
720
  args,
734
721
  {
@@ -787,10 +774,139 @@ var parseDiffPaths = (unified) => {
787
774
  }
788
775
  return paths;
789
776
  };
777
+ var getRemoteUrl = async (cwd) => {
778
+ try {
779
+ return (await runGit(cwd, ["remote", "get-url", "origin"])).trim() || null;
780
+ } catch (err) {
781
+ if (err instanceof GitCommandError) return null;
782
+ throw err;
783
+ }
784
+ };
785
+ var getDefaultBranch = async (cwd) => {
786
+ try {
787
+ const ref = (await runGit(cwd, ["rev-parse", "--abbrev-ref", "origin/HEAD"])).trim();
788
+ const name = ref.replace(/^origin\//, "");
789
+ return name || "main";
790
+ } catch (err) {
791
+ if (err instanceof GitCommandError) return "main";
792
+ throw err;
793
+ }
794
+ };
795
+ var createBranch = async (cwd, name) => {
796
+ await runGit(cwd, ["checkout", "-b", name]);
797
+ };
798
+ var commitAll = async (cwd, message) => {
799
+ await runGit(cwd, ["add", "-A"]);
800
+ await runGit(cwd, ["commit", "-m", message]);
801
+ return (await runGit(cwd, ["rev-parse", "--short", "HEAD"])).trim();
802
+ };
803
+ var pushBranch = async (cwd, branch) => {
804
+ await runGit(cwd, ["push", "-u", "origin", branch]);
805
+ };
806
+ var branchNameForTitle = (title) => {
807
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
808
+ const rand = Math.random().toString(36).slice(2, 7);
809
+ return `diologue/${slug || "change"}-${rand}`;
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";
790
906
 
791
907
  // src/lib/paths.ts
792
908
  import { access, lstat, realpath, stat } from "node:fs/promises";
793
- import path from "node:path";
909
+ import path2 from "node:path";
794
910
  var InvalidRepoPathError = class extends Error {
795
911
  code;
796
912
  constructor(code, message) {
@@ -815,7 +931,7 @@ var validateRepoPath = async (raw) => {
815
931
  if (trimmed.length === 0) {
816
932
  throw new InvalidRepoPathError("empty", "path must not be empty");
817
933
  }
818
- if (!path.isAbsolute(trimmed)) {
934
+ if (!path2.isAbsolute(trimmed)) {
819
935
  throw new InvalidRepoPathError(
820
936
  "not_absolute",
821
937
  `path must be absolute (got: ${trimmed})`
@@ -837,7 +953,7 @@ var validateRepoPath = async (raw) => {
837
953
  `path is not a directory: ${resolved}`
838
954
  );
839
955
  }
840
- const gitMarker = path.join(resolved, ".git");
956
+ const gitMarker = path2.join(resolved, ".git");
841
957
  if (!await exists(gitMarker)) {
842
958
  throw new InvalidRepoPathError(
843
959
  "not_a_git_repo",
@@ -850,7 +966,7 @@ var validateRepoPath = async (raw) => {
850
966
 
851
967
  // src/lib/pick-directory.ts
852
968
  import { execFile as execFile2 } from "node:child_process";
853
- import { homedir } from "node:os";
969
+ import { homedir as homedir2 } from "node:os";
854
970
  var PROMPT = "Select a git repository";
855
971
  var dialogSpec = (platform) => {
856
972
  switch (platform) {
@@ -910,21 +1026,94 @@ var pickDirectory = async () => {
910
1026
  };
911
1027
  const result = await attempt(spec.cmd, spec.args);
912
1028
  if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
913
- return attempt("kdialog", ["--getexistingdirectory", homedir()]);
1029
+ return attempt("kdialog", ["--getexistingdirectory", homedir2()]);
914
1030
  }
915
1031
  return result;
916
1032
  };
917
1033
 
1034
+ // src/lib/github.ts
1035
+ import { execFile as execFile3 } from "node:child_process";
1036
+ var GhError = class extends Error {
1037
+ constructor(message, code, stderr) {
1038
+ super(message);
1039
+ this.code = code;
1040
+ this.stderr = stderr;
1041
+ this.name = "GhError";
1042
+ }
1043
+ };
1044
+ var run2 = (args, cwd) => new Promise((resolve, reject) => {
1045
+ execFile3(
1046
+ "gh",
1047
+ args,
1048
+ { cwd, timeout: 6e4, windowsHide: true },
1049
+ (err, stdout, stderr) => {
1050
+ if (err) {
1051
+ const code = err.code;
1052
+ if (code === "ENOENT") {
1053
+ reject(
1054
+ new GhError(
1055
+ "The GitHub CLI (gh) isn't installed on this machine.",
1056
+ "gh_unavailable"
1057
+ )
1058
+ );
1059
+ return;
1060
+ }
1061
+ reject(new GhError(stderr || err.message, "gh_failed", stderr));
1062
+ return;
1063
+ }
1064
+ resolve({ stdout, stderr });
1065
+ }
1066
+ );
1067
+ });
1068
+ var ghReady = async () => {
1069
+ try {
1070
+ await run2(["auth", "status"]);
1071
+ return true;
1072
+ } catch {
1073
+ return false;
1074
+ }
1075
+ };
1076
+ var createPullRequest = async (opts) => {
1077
+ const { stdout } = await run2(
1078
+ [
1079
+ "pr",
1080
+ "create",
1081
+ "--base",
1082
+ opts.base,
1083
+ "--head",
1084
+ opts.head,
1085
+ "--title",
1086
+ opts.title,
1087
+ "--body",
1088
+ opts.body
1089
+ ],
1090
+ opts.cwd
1091
+ );
1092
+ const url = stdout.split("\n").map((l) => l.trim()).filter(Boolean).reverse().find((l) => l.startsWith("http"));
1093
+ if (!url) {
1094
+ throw new GhError("gh pr create didn't return a PR URL.", "gh_failed", stdout);
1095
+ }
1096
+ return url;
1097
+ };
1098
+
918
1099
  // src/routes/repo.ts
919
1100
  var selectRepoSchema = z.object({
920
1101
  path: z.string().min(1)
921
1102
  });
922
1103
  var applyPatchSchema = z.object({
923
1104
  unified: z.string().min(1).max(8 * 1024 * 1024),
924
- baselineHash: z.string().optional()
1105
+ baselineHash: z.string().optional(),
1106
+ sessionId: z.string().min(1).optional()
925
1107
  });
926
1108
  var revertPatchSchema = z.object({
927
- 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()
1111
+ });
1112
+ var createPrSchema = z.object({
1113
+ title: z.string().min(1).max(200),
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()
928
1117
  });
929
1118
  var buildRepoStatus = async (resolvedPath) => {
930
1119
  const [branch, head, dirty] = await Promise.all([
@@ -934,7 +1123,7 @@ var buildRepoStatus = async (resolvedPath) => {
934
1123
  ]);
935
1124
  return {
936
1125
  path: resolvedPath,
937
- name: path2.basename(resolvedPath),
1126
+ name: path3.basename(resolvedPath),
938
1127
  branch,
939
1128
  head,
940
1129
  isDirty: dirty
@@ -958,7 +1147,8 @@ var requireSelectedRepo = (state, res) => {
958
1147
  }
959
1148
  return repo;
960
1149
  };
961
- var createRepoRouter = (state) => {
1150
+ var readSessionId = (value) => typeof value === "string" && value ? value : void 0;
1151
+ var createRepoRouter = (state, worktrees) => {
962
1152
  const router = createRouter();
963
1153
  router.post("/select", async (req, res) => {
964
1154
  const parsed = selectRepoSchema.safeParse(req.body);
@@ -1001,6 +1191,69 @@ var createRepoRouter = (state) => {
1001
1191
  message: "No desktop folder dialog is available on this machine. Type the repo path instead."
1002
1192
  });
1003
1193
  });
1194
+ router.post("/create-pr", async (req, res) => {
1195
+ const repo = requireSelectedRepo(state, res);
1196
+ if (!repo) return;
1197
+ const parsed = createPrSchema.safeParse(req.body);
1198
+ if (!parsed.success) {
1199
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1200
+ return;
1201
+ }
1202
+ const { title } = parsed.data;
1203
+ const body = parsed.data.body ?? "Opened from the Diologue coding agent.";
1204
+ const worktree = parsed.data.sessionId ? worktrees.get(parsed.data.sessionId) : null;
1205
+ const cwd = worktree ? worktree.path : repo.path;
1206
+ try {
1207
+ if (!await ghReady()) {
1208
+ res.status(501).json({
1209
+ error: "gh_unavailable",
1210
+ message: "GitHub CLI (gh) isn't installed or authenticated on this machine. Run `gh auth login`, or push the branch and open the PR manually."
1211
+ });
1212
+ return;
1213
+ }
1214
+ if (!await getRemoteUrl(cwd)) {
1215
+ res.status(400).json({
1216
+ error: "no_remote",
1217
+ message: "This repo has no `origin` remote. Add one (git remote add origin \u2026) before creating a PR."
1218
+ });
1219
+ return;
1220
+ }
1221
+ if (!await isDirty(cwd)) {
1222
+ res.status(409).json({
1223
+ error: "nothing_to_commit",
1224
+ message: "There are no changes in the working tree to open a PR for."
1225
+ });
1226
+ return;
1227
+ }
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);
1239
+ }
1240
+ await commitAll(cwd, title);
1241
+ await pushBranch(cwd, branch);
1242
+ const url = await createPullRequest({ cwd, base, head: branch, title, body });
1243
+ const payload = { url, branch, base };
1244
+ res.json(payload);
1245
+ } catch (err) {
1246
+ if (err instanceof GhError) {
1247
+ res.status(502).json({ error: err.code, message: err.message });
1248
+ return;
1249
+ }
1250
+ if (err instanceof GitCommandError) {
1251
+ sendGitError(res, err);
1252
+ return;
1253
+ }
1254
+ throw err;
1255
+ }
1256
+ });
1004
1257
  router.get("/status", async (_req, res) => {
1005
1258
  const repo = state.getSelectedRepo();
1006
1259
  if (!repo) {
@@ -1021,13 +1274,23 @@ var createRepoRouter = (state) => {
1021
1274
  throw err;
1022
1275
  }
1023
1276
  });
1024
- 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) => {
1025
1283
  const repo = requireSelectedRepo(state, res);
1026
1284
  if (!repo) {
1027
1285
  return;
1028
1286
  }
1287
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1288
+ if (!workdir) {
1289
+ res.json({ unified: "", sizeBytes: 0 });
1290
+ return;
1291
+ }
1029
1292
  try {
1030
- const unified = await getDiff(repo.path);
1293
+ const unified = await getDiff(workdir);
1031
1294
  const body = {
1032
1295
  unified,
1033
1296
  sizeBytes: Buffer.byteLength(unified, "utf8")
@@ -1041,13 +1304,18 @@ var createRepoRouter = (state) => {
1041
1304
  throw err;
1042
1305
  }
1043
1306
  });
1044
- router.get("/changed-files", async (_req, res) => {
1307
+ router.get("/changed-files", async (req, res) => {
1045
1308
  const repo = requireSelectedRepo(state, res);
1046
1309
  if (!repo) {
1047
1310
  return;
1048
1311
  }
1312
+ const workdir = resolveWorkdir(repo, readSessionId(req.query.sessionId));
1313
+ if (!workdir) {
1314
+ res.json({ files: [] });
1315
+ return;
1316
+ }
1049
1317
  try {
1050
- const files = await getStatusShort(repo.path);
1318
+ const files = await getStatusShort(workdir);
1051
1319
  const body = { files };
1052
1320
  res.json(body);
1053
1321
  } catch (err) {
@@ -1066,8 +1334,9 @@ var createRepoRouter = (state) => {
1066
1334
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1067
1335
  return;
1068
1336
  }
1337
+ const applyWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1069
1338
  try {
1070
- const check = await canApplyDiff(repo.path, parsed.data.unified);
1339
+ const check = await canApplyDiff(applyWorkdir, parsed.data.unified);
1071
1340
  if (!check.ok) {
1072
1341
  res.status(409).json({
1073
1342
  error: "diff_does_not_apply",
@@ -1076,7 +1345,7 @@ var createRepoRouter = (state) => {
1076
1345
  });
1077
1346
  return;
1078
1347
  }
1079
- await applyDiff(repo.path, parsed.data.unified);
1348
+ await applyDiff(applyWorkdir, parsed.data.unified);
1080
1349
  const paths = parseDiffPaths(parsed.data.unified);
1081
1350
  const body = {
1082
1351
  ok: true,
@@ -1100,8 +1369,9 @@ var createRepoRouter = (state) => {
1100
1369
  res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1101
1370
  return;
1102
1371
  }
1372
+ const revertWorkdir = parsed.data.sessionId && worktrees.get(parsed.data.sessionId)?.path || repo.path;
1103
1373
  try {
1104
- const check = await canRevertDiff(repo.path, parsed.data.unified);
1374
+ const check = await canRevertDiff(revertWorkdir, parsed.data.unified);
1105
1375
  if (!check.ok) {
1106
1376
  res.status(409).json({
1107
1377
  error: "diff_does_not_revert",
@@ -1110,7 +1380,7 @@ var createRepoRouter = (state) => {
1110
1380
  });
1111
1381
  return;
1112
1382
  }
1113
- await revertDiff(repo.path, parsed.data.unified);
1383
+ await revertDiff(revertWorkdir, parsed.data.unified);
1114
1384
  const paths = parseDiffPaths(parsed.data.unified);
1115
1385
  const body = {
1116
1386
  ok: true,
@@ -1379,8 +1649,21 @@ var createAgentRouter = (deps) => {
1379
1649
  res.status(409).json({ error: "no_repo_selected" });
1380
1650
  return;
1381
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
+ }
1382
1665
  logAgentRoute(
1383
- `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)"}`
1384
1667
  );
1385
1668
  res.setHeader("Content-Type", "text/event-stream");
1386
1669
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -1403,7 +1686,7 @@ var createAgentRouter = (deps) => {
1403
1686
  const history = parsed.data.history ? parsed.data.history.map((m) => ({ role: m.role, content: m.content })) : void 0;
1404
1687
  for await (const event of deps.adapter.streamMessage({
1405
1688
  sessionId: parsed.data.sessionId,
1406
- repoPath: repo.path,
1689
+ repoPath: workdir,
1407
1690
  prompt: parsed.data.prompt,
1408
1691
  history,
1409
1692
  signal: controller.signal,
@@ -2588,8 +2871,8 @@ var mapEvent = (item, state) => {
2588
2871
  {
2589
2872
  type: "diff_proposed",
2590
2873
  unified: "",
2591
- files: files.map((path6) => ({
2592
- path: path6,
2874
+ files: files.map((path7) => ({
2875
+ path: path7,
2593
2876
  additions: 0,
2594
2877
  deletions: 0,
2595
2878
  status: "modified"
@@ -3091,13 +3374,17 @@ var createApp = (options) => {
3091
3374
  const state = options.state ?? createState();
3092
3375
  const adapter = options.adapter ?? buildDefaultAdapter();
3093
3376
  const brokerRegistry = createBrokerRegistry();
3377
+ const worktrees = createWorktreeManager();
3094
3378
  const app = express();
3095
3379
  app.use(express.json({ limit: "1mb" }));
3096
3380
  app.use(createCorsMiddleware({ allowedOrigin: options.config.allowedOrigin }));
3097
3381
  app.use(createAuthMiddleware({ token: options.config.token }));
3098
3382
  app.get("/health", createHealthHandler(options.config));
3099
- app.use("/repo", createRepoRouter(state));
3100
- 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
+ );
3101
3388
  app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
3102
3389
  app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
3103
3390
  app.use("/agent/permission", createPermissionRouter({ adapter }));
@@ -3105,7 +3392,7 @@ var createApp = (options) => {
3105
3392
  app.use((req, res) => {
3106
3393
  res.status(404).json({ error: "not_found", method: req.method, path: req.path });
3107
3394
  });
3108
- return { app, state, adapter, brokerRegistry };
3395
+ return { app, state, adapter, brokerRegistry, worktrees };
3109
3396
  };
3110
3397
 
3111
3398
  // src/modes/quickstart.ts
@@ -3200,7 +3487,7 @@ var runQuickstart = async (options) => {
3200
3487
 
3201
3488
  // src/lib/keychain.ts
3202
3489
  import { promises as fs } from "node:fs";
3203
- import path5 from "node:path";
3490
+ import path6 from "node:path";
3204
3491
  import os from "node:os";
3205
3492
  var KEYTAR_SERVICE = "diologue.local-agent";
3206
3493
  var KEYTAR_ACCOUNT = "device-credential";
@@ -3223,7 +3510,7 @@ var loadKeytar = async () => {
3223
3510
  };
3224
3511
  var fileFallbackPath = () => {
3225
3512
  const home = os.homedir();
3226
- return path5.join(home, ".diologue", "credentials");
3513
+ return path6.join(home, ".diologue", "credentials");
3227
3514
  };
3228
3515
  var createCredentialStore = (options = {}) => {
3229
3516
  const filePath = options.filePathOverride ?? fileFallbackPath();
@@ -3270,7 +3557,7 @@ var createCredentialStore = (options = {}) => {
3270
3557
  return { backend: "keychain" };
3271
3558
  }
3272
3559
  }
3273
- await fs.mkdir(path5.dirname(filePath), { recursive: true });
3560
+ await fs.mkdir(path6.dirname(filePath), { recursive: true });
3274
3561
  await fs.writeFile(filePath, JSON.stringify(credential, null, 2), {
3275
3562
  mode: 384
3276
3563
  });
@@ -3319,7 +3606,7 @@ var runStart = async (options) => {
3319
3606
  process.env.LOCAL_AGENT_ALLOWED_ORIGIN = options.allowedOrigin;
3320
3607
  if (options.adapter) process.env.LOCAL_AGENT_ADAPTER = options.adapter;
3321
3608
  const config = loadConfig();
3322
- const { app, adapter } = createApp({ config });
3609
+ const { app, adapter, worktrees } = createApp({ config });
3323
3610
  const server = app.listen(config.port, config.host, async () => {
3324
3611
  const url = `http://${config.host}:${config.port}`;
3325
3612
  console.log(`[local-agent] Listening on ${url}`);
@@ -3335,7 +3622,9 @@ var runStart = async (options) => {
3335
3622
  const shutdown = (signal) => {
3336
3623
  console.log(`
3337
3624
  [local-agent] Received ${signal}, shutting down...`);
3338
- server.close(() => process.exit(0));
3625
+ void worktrees.cleanupAll().finally(() => {
3626
+ server.close(() => process.exit(0));
3627
+ });
3339
3628
  setTimeout(() => process.exit(0), 3e3).unref();
3340
3629
  };
3341
3630
  process.on("SIGINT", shutdown);