@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 +276 -72
- package/dist/cli.mjs.map +4 -4
- package/package.json +1 -1
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
|
|
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 =
|
|
96
|
-
LOCAL_AGENT_ROOT =
|
|
97
|
-
REPO_ROOT =
|
|
98
|
-
BUNDLE_DIR_INSTALLED =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
177
|
-
REPO_ROOT2 =
|
|
178
|
-
DEFAULT_ENGINE_DIR =
|
|
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 =
|
|
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
|
-
|
|
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}/${
|
|
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/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
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",
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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((
|
|
2752
|
-
path:
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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);
|