leglas 0.3.0 → 0.4.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/index.js CHANGED
@@ -845,6 +845,360 @@ function normalizeConfig(raw, options = {}) {
845
845
  };
846
846
  }
847
847
 
848
+ // ../server/dist/agent-command.js
849
+ var WATCH_PATH = ".leglas/watch.json";
850
+ var PROMPT_TOKEN = "{prompt}";
851
+ var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
852
+ function tokenize(template) {
853
+ const tokens = [];
854
+ let current = "";
855
+ let started = false;
856
+ let quote = null;
857
+ for (const character of template) {
858
+ if (quote !== null) {
859
+ if (character === quote)
860
+ quote = null;
861
+ else
862
+ current += character;
863
+ continue;
864
+ }
865
+ if (character === '"' || character === "'") {
866
+ quote = character;
867
+ started = true;
868
+ continue;
869
+ }
870
+ if (/\s/.test(character)) {
871
+ if (started)
872
+ tokens.push(current);
873
+ current = "";
874
+ started = false;
875
+ continue;
876
+ }
877
+ current += character;
878
+ started = true;
879
+ }
880
+ if (quote !== null) {
881
+ return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
882
+ }
883
+ if (started)
884
+ tokens.push(current);
885
+ return { ok: true, tokens };
886
+ }
887
+ function parseTemplate(raw) {
888
+ const tokenized = tokenize(raw);
889
+ if (!tokenized.ok)
890
+ return tokenized;
891
+ const { tokens } = tokenized;
892
+ const [command, ...args] = tokens;
893
+ if (command === void 0) {
894
+ return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
895
+ }
896
+ if (tokens.some((token) => token !== PROMPT_TOKEN && token.includes(PROMPT_TOKEN))) {
897
+ return {
898
+ ok: false,
899
+ error: `${PROMPT_TOKEN} must stand as a word of its own, for example: ${EXAMPLE}`
900
+ };
901
+ }
902
+ const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
903
+ if (placeholders > 1) {
904
+ return {
905
+ ok: false,
906
+ error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
907
+ };
908
+ }
909
+ if (command === PROMPT_TOKEN) {
910
+ return {
911
+ ok: false,
912
+ error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
913
+ };
914
+ }
915
+ return { ok: true, template: { command, args } };
916
+ }
917
+ function commandFor(template, prompt) {
918
+ if (!template.args.includes(PROMPT_TOKEN)) {
919
+ return { command: template.command, args: [...template.args, prompt] };
920
+ }
921
+ return {
922
+ command: template.command,
923
+ args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
924
+ };
925
+ }
926
+ function nextRequest(requests, failed) {
927
+ return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
928
+ }
929
+
930
+ // ../server/dist/agents.js
931
+ import { spawn } from "child_process";
932
+ import { constants } from "fs";
933
+ import { access, mkdir, readFile, writeFile } from "fs/promises";
934
+ import { delimiter, dirname, isAbsolute, join, relative } from "path";
935
+ var KNOWN_AGENTS = {
936
+ claude: {
937
+ name: "Claude",
938
+ binary: "claude",
939
+ args: (prompt) => [
940
+ "-p",
941
+ prompt,
942
+ "--output-format",
943
+ "stream-json",
944
+ "--verbose",
945
+ "--permission-mode",
946
+ "acceptEdits"
947
+ ],
948
+ terminalArgs: (prompt) => [
949
+ "-p",
950
+ prompt,
951
+ "--permission-mode",
952
+ "acceptEdits"
953
+ ],
954
+ resumeArgs: (sessionId, prompt) => [
955
+ "-p",
956
+ "--resume",
957
+ sessionId,
958
+ prompt,
959
+ "--output-format",
960
+ "stream-json",
961
+ "--verbose",
962
+ "--permission-mode",
963
+ "acceptEdits"
964
+ ],
965
+ // Every stream-json event names its session.
966
+ sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
967
+ authArgs: ["auth", "status"],
968
+ // `claude auth status` prints JSON with a loggedIn boolean. Only that
969
+ // field decides; any other shape stays unknown.
970
+ authVerdict: (result) => {
971
+ try {
972
+ const parsed = record(JSON.parse(result.stdout));
973
+ if (parsed?.loggedIn === true)
974
+ return "ok";
975
+ if (parsed?.loggedIn === false)
976
+ return "signed-out";
977
+ } catch {
978
+ }
979
+ return "unknown";
980
+ }
981
+ },
982
+ codex: {
983
+ name: "Codex",
984
+ binary: "codex",
985
+ args: (prompt) => ["exec", "--json", "-s", "workspace-write", prompt],
986
+ terminalArgs: (prompt) => ["exec", "-s", "workspace-write", prompt],
987
+ // No sandbox flag here: `codex exec resume` refuses it and inherits the
988
+ // session's own sandbox, which the first turn set to workspace-write.
989
+ resumeArgs: (sessionId, prompt) => [
990
+ "exec",
991
+ "resume",
992
+ sessionId,
993
+ "--json",
994
+ prompt
995
+ ],
996
+ sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
997
+ authArgs: ["login", "status"],
998
+ // `codex login status` exits 0 when logged in and nonzero when not.
999
+ authVerdict: (result) => result.code === 0 ? "ok" : "signed-out"
1000
+ },
1001
+ cursor: {
1002
+ name: "Cursor",
1003
+ binary: "cursor-agent",
1004
+ args: (prompt) => ["-p", prompt, "--output-format", "stream-json"],
1005
+ terminalArgs: (prompt) => ["-p", prompt],
1006
+ authArgs: ["status"],
1007
+ // UNVERIFIED: cursor-agent was not available on the build machine. The
1008
+ // reading is deliberately loose, and anything ambiguous stays unknown.
1009
+ authVerdict: (result) => {
1010
+ if (/logged in|signed in/i.test(result.stdout))
1011
+ return "ok";
1012
+ if (result.code !== 0 || /not logged in|log in|sign in/i.test(result.stdout))
1013
+ return "signed-out";
1014
+ return "unknown";
1015
+ }
1016
+ }
1017
+ };
1018
+ var PROBE_TIMEOUT_MS = 3e3;
1019
+ function execProbe(binary, args) {
1020
+ return new Promise((resolve) => {
1021
+ let child;
1022
+ try {
1023
+ child = spawn(binary, [...args], { shell: false, stdio: ["ignore", "pipe", "ignore"] });
1024
+ } catch {
1025
+ return resolve(null);
1026
+ }
1027
+ let stdout = "";
1028
+ child.stdout?.on("data", (chunk) => {
1029
+ if (stdout.length < 4096)
1030
+ stdout += chunk.toString();
1031
+ });
1032
+ const deadline = setTimeout(() => child.kill("SIGKILL"), PROBE_TIMEOUT_MS);
1033
+ child.once("error", () => {
1034
+ clearTimeout(deadline);
1035
+ resolve(null);
1036
+ });
1037
+ child.once("close", (code, signal) => {
1038
+ clearTimeout(deadline);
1039
+ resolve(signal !== null ? null : { code: code ?? 0, stdout });
1040
+ });
1041
+ });
1042
+ }
1043
+ async function pathLookup(binary) {
1044
+ const entries = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
1045
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
1046
+ for (const entry of entries) {
1047
+ for (const extension of extensions) {
1048
+ try {
1049
+ await access(join(entry, `${binary}${extension}`), constants.X_OK);
1050
+ return true;
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ }
1055
+ return false;
1056
+ }
1057
+ async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
1058
+ const entries = Object.entries(KNOWN_AGENTS);
1059
+ return Promise.all(entries.map(async ([id, adapter]) => {
1060
+ const available = await lookup(adapter.binary).catch(() => false);
1061
+ if (!available)
1062
+ return { id, name: adapter.name, available, auth: "unknown" };
1063
+ const result = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
1064
+ return {
1065
+ id,
1066
+ name: adapter.name,
1067
+ available,
1068
+ auth: result === null ? "unknown" : adapter.authVerdict(result)
1069
+ };
1070
+ }));
1071
+ }
1072
+ function record(value) {
1073
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1074
+ }
1075
+ function shownPath(value, cwd) {
1076
+ if (typeof value !== "string" || value === "")
1077
+ return null;
1078
+ if (!isAbsolute(value))
1079
+ return value;
1080
+ return relative(cwd, value) || ".";
1081
+ }
1082
+ function shownCommand(value) {
1083
+ let command = Array.isArray(value) ? value.filter((part) => typeof part === "string").join(" ") : typeof value === "string" ? value : "";
1084
+ command = command.trim();
1085
+ const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
1086
+ if (wrapped?.[1] !== void 0) {
1087
+ command = wrapped[1].trim();
1088
+ const quote = command[0];
1089
+ if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
1090
+ command = command.slice(1, -1);
1091
+ }
1092
+ }
1093
+ command = (command.split("\n")[0] ?? "").replace(/\s+/g, " ").trim();
1094
+ if (command === "")
1095
+ return null;
1096
+ return command.length > 48 ? `${command.slice(0, 47)}\u2026` : command;
1097
+ }
1098
+ function claudeActivity(event, cwd) {
1099
+ if (event.type !== "assistant")
1100
+ return null;
1101
+ const message = record(event.message);
1102
+ if (message === null || !Array.isArray(message.content))
1103
+ return null;
1104
+ for (const rawBlock of message.content) {
1105
+ const block = record(rawBlock);
1106
+ if (block?.type !== "tool_use" || typeof block.name !== "string")
1107
+ continue;
1108
+ const input = record(block.input);
1109
+ if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
1110
+ const path = shownPath(input?.file_path ?? input?.notebook_path, cwd);
1111
+ return path === null ? `using ${block.name}` : `editing ${path}`;
1112
+ }
1113
+ if (block.name === "Read") {
1114
+ const path = shownPath(input?.file_path ?? input?.path, cwd);
1115
+ return path === null ? "using Read" : `reading ${path}`;
1116
+ }
1117
+ if (block.name === "Bash") {
1118
+ const command = shownCommand(input?.command);
1119
+ return command === null ? "running a command" : `running ${command}`;
1120
+ }
1121
+ if (block.name === "Grep" || block.name === "Glob")
1122
+ return "searching the project";
1123
+ return `using ${block.name}`;
1124
+ }
1125
+ return null;
1126
+ }
1127
+ function codexActivity(event, cwd) {
1128
+ if (event.type !== "item.started" && event.type !== "item.completed")
1129
+ return null;
1130
+ const item = record(event.item);
1131
+ if (item === null)
1132
+ return null;
1133
+ if (item.type === "command_execution") {
1134
+ const command = shownCommand(item.command);
1135
+ return command === null ? "running a command" : `running ${command}`;
1136
+ }
1137
+ if (item.type !== "file_change")
1138
+ return null;
1139
+ const first = Array.isArray(item.changes) ? record(item.changes[0]) : null;
1140
+ const path = shownPath(first?.path ?? item.path, cwd);
1141
+ return path === null ? null : `editing ${path}`;
1142
+ }
1143
+ function activityFrom(agent, line, cwd = process.cwd()) {
1144
+ let event;
1145
+ try {
1146
+ event = record(JSON.parse(line));
1147
+ } catch {
1148
+ return null;
1149
+ }
1150
+ if (event === null)
1151
+ return null;
1152
+ if (agent === "claude")
1153
+ return claudeActivity(event, cwd);
1154
+ if (agent === "codex")
1155
+ return codexActivity(event, cwd);
1156
+ if (agent === "cursor")
1157
+ return claudeActivity(event, cwd);
1158
+ return null;
1159
+ }
1160
+ function sessionFrom(agent, line) {
1161
+ if (agent !== "claude" && agent !== "codex")
1162
+ return null;
1163
+ let event;
1164
+ try {
1165
+ event = record(JSON.parse(line));
1166
+ } catch {
1167
+ return null;
1168
+ }
1169
+ if (event === null)
1170
+ return null;
1171
+ return KNOWN_AGENTS[agent].sessionFrom(event);
1172
+ }
1173
+ function isAgentChoice(value) {
1174
+ return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
1175
+ }
1176
+ async function readWatchConfig(cwd) {
1177
+ try {
1178
+ const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
1179
+ return record(parsed) ?? {};
1180
+ } catch {
1181
+ return {};
1182
+ }
1183
+ }
1184
+ async function readAgentChoice(cwd) {
1185
+ const config = await readWatchConfig(cwd);
1186
+ return {
1187
+ agent: isAgentChoice(config.agent) ? config.agent : null,
1188
+ run: typeof config.run === "string" && config.run !== "" ? config.run : null
1189
+ };
1190
+ }
1191
+ async function saveAgentChoice(cwd, choice) {
1192
+ const config = await readWatchConfig(cwd);
1193
+ config.agent = choice.agent;
1194
+ if (choice.run !== void 0)
1195
+ config.run = choice.run;
1196
+ const path = join(cwd, WATCH_PATH);
1197
+ await mkdir(dirname(path), { recursive: true });
1198
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
1199
+ `, "utf8");
1200
+ }
1201
+
848
1202
  // ../server/dist/classify.js
849
1203
  var MANIFESTS = /* @__PURE__ */ new Set([
850
1204
  "package.json",
@@ -912,7 +1266,7 @@ function classifyDirection(input) {
912
1266
 
913
1267
  // ../server/dist/find-config.js
914
1268
  import { existsSync } from "fs";
915
- import { dirname, join, parse } from "path";
1269
+ import { dirname as dirname2, join as join2, parse } from "path";
916
1270
  var CONFIG_BASENAMES = [
917
1271
  "leglas.config.ts",
918
1272
  "leglas.config.mjs",
@@ -924,13 +1278,13 @@ function findConfigFile(startDir) {
924
1278
  let dir = startDir;
925
1279
  for (; ; ) {
926
1280
  for (const basename4 of CONFIG_BASENAMES) {
927
- const candidate = join(dir, basename4);
1281
+ const candidate = join2(dir, basename4);
928
1282
  if (existsSync(candidate))
929
1283
  return candidate;
930
1284
  }
931
1285
  if (dir === root)
932
1286
  return null;
933
- const parent = dirname(dir);
1287
+ const parent = dirname2(dir);
934
1288
  if (parent === dir)
935
1289
  return null;
936
1290
  dir = parent;
@@ -938,19 +1292,19 @@ function findConfigFile(startDir) {
938
1292
  }
939
1293
 
940
1294
  // ../server/dist/load-config.js
941
- import { readFile } from "fs/promises";
942
- import { relative } from "path";
1295
+ import { readFile as readFile2 } from "fs/promises";
1296
+ import { relative as relative2 } from "path";
943
1297
  import { pathToFileURL } from "url";
944
1298
  async function loadConfig(cwd) {
945
1299
  const path = findConfigFile(cwd);
946
1300
  if (path === null) {
947
1301
  return { ...normalizeConfig(void 0), path: null };
948
1302
  }
949
- const label = relative(cwd, path) || path;
1303
+ const label = relative2(cwd, path) || path;
950
1304
  let exported;
951
1305
  try {
952
1306
  if (path.endsWith(".json")) {
953
- exported = JSON.parse(await readFile(path, "utf8"));
1307
+ exported = JSON.parse(await readFile2(path, "utf8"));
954
1308
  } else {
955
1309
  const module = await import(pathToFileURL(path).href);
956
1310
  if (!("default" in module)) {
@@ -971,14 +1325,14 @@ async function loadConfig(cwd) {
971
1325
  }
972
1326
 
973
1327
  // ../server/dist/local-previews.js
974
- import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
975
- import { dirname as dirname2, join as join2 } from "path";
1328
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1329
+ import { dirname as dirname3, join as join3 } from "path";
976
1330
  var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
977
1331
  async function readLocalPreviews(cwd) {
978
- const path = join2(cwd, LOCAL_PREVIEWS_PATH);
1332
+ const path = join3(cwd, LOCAL_PREVIEWS_PATH);
979
1333
  let raw;
980
1334
  try {
981
- raw = await readFile2(path, "utf8");
1335
+ raw = await readFile3(path, "utf8");
982
1336
  } catch {
983
1337
  return { previews: [], errors: [] };
984
1338
  }
@@ -1027,9 +1381,9 @@ async function addLocalPreview(cwd, input, shared) {
1027
1381
  if (check.config === null) {
1028
1382
  return { ok: false, error: check.errors.join(" ") };
1029
1383
  }
1030
- const path = join2(cwd, LOCAL_PREVIEWS_PATH);
1031
- await mkdir(dirname2(path), { recursive: true });
1032
- await writeFile(path, `${JSON.stringify({ previews: [...existing.previews.map(toStored), candidate] }, null, 2)}
1384
+ const path = join3(cwd, LOCAL_PREVIEWS_PATH);
1385
+ await mkdir2(dirname3(path), { recursive: true });
1386
+ await writeFile2(path, `${JSON.stringify({ previews: [...existing.previews.map(toStored), candidate] }, null, 2)}
1033
1387
  `, "utf8");
1034
1388
  return { ok: true };
1035
1389
  }
@@ -1042,9 +1396,9 @@ async function dropLocalPreviews(cwd, titles) {
1042
1396
  const keep = existing.previews.filter((preview) => !titles.includes(preview.title));
1043
1397
  if (keep.length === existing.previews.length)
1044
1398
  return 0;
1045
- const path = join2(cwd, LOCAL_PREVIEWS_PATH);
1046
- await mkdir(dirname2(path), { recursive: true });
1047
- await writeFile(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
1399
+ const path = join3(cwd, LOCAL_PREVIEWS_PATH);
1400
+ await mkdir2(dirname3(path), { recursive: true });
1401
+ await writeFile2(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
1048
1402
  `, "utf8");
1049
1403
  return existing.previews.length - keep.length;
1050
1404
  }
@@ -1113,10 +1467,10 @@ ${headers}\r
1113
1467
  }
1114
1468
 
1115
1469
  // ../server/dist/worktree.js
1116
- import { execFile, spawn } from "child_process";
1470
+ import { execFile, spawn as spawn2 } from "child_process";
1117
1471
  import { rm } from "fs/promises";
1118
1472
  import net2 from "net";
1119
- import { join as join3 } from "path";
1473
+ import { join as join4 } from "path";
1120
1474
  import { promisify } from "util";
1121
1475
  var run = promisify(execFile);
1122
1476
  var WORKTREES_DIR = ".leglas/worktrees";
@@ -1153,7 +1507,7 @@ function answers(port) {
1153
1507
  }
1154
1508
  async function startWorktree(options) {
1155
1509
  const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
1156
- const path = join3(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
1510
+ const path = join4(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
1157
1511
  const log = options.onLog ?? (() => {
1158
1512
  });
1159
1513
  await rm(path, { recursive: true, force: true });
@@ -1214,7 +1568,7 @@ async function startAppProcess(options) {
1214
1568
  const port = await freePort();
1215
1569
  let child;
1216
1570
  try {
1217
- child = spawn(substitutePort(options.devCommand, port), {
1571
+ child = spawn2(substitutePort(options.devCommand, port), {
1218
1572
  cwd: options.cwd,
1219
1573
  shell: true,
1220
1574
  // Own process group, so stopping kills the shell and whatever it spawned
@@ -1254,9 +1608,9 @@ async function startAppProcess(options) {
1254
1608
  }
1255
1609
 
1256
1610
  // ../server/dist/requests.js
1257
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1611
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1258
1612
  import { randomBytes } from "crypto";
1259
- import { dirname as dirname3, join as join4 } from "path";
1613
+ import { dirname as dirname4, join as join5 } from "path";
1260
1614
  var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
1261
1615
  function targetFor(url) {
1262
1616
  if (!url.startsWith("/"))
@@ -1282,17 +1636,20 @@ function composeRequest(preview, intent) {
1282
1636
  const target = preview.file ?? targetFor(preview.url);
1283
1637
  const cleaned = intent.trim();
1284
1638
  const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
1639
+ const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
1285
1640
  const prompt = `In this project, change only the "${preview.title}" design direction. ${where}
1286
1641
 
1287
1642
  What to change: ${cleaned}
1288
1643
 
1644
+ ${pace}This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
1645
+
1289
1646
  Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. The direction is already registered, so nothing needs re-registering. Keep the change additive: do not rewrite shared components that other directions rely on.`;
1290
1647
  return { prompt, target };
1291
1648
  }
1292
1649
  var REQUESTS_PATH = ".leglas/requests.json";
1293
1650
  async function readRequests(cwd) {
1294
1651
  try {
1295
- const raw = await readFile3(join4(cwd, REQUESTS_PATH), "utf8");
1652
+ const raw = await readFile4(join5(cwd, REQUESTS_PATH), "utf8");
1296
1653
  const parsed = JSON.parse(raw);
1297
1654
  if (!Array.isArray(parsed.requests))
1298
1655
  return [];
@@ -1309,9 +1666,9 @@ async function readRequests(cwd) {
1309
1666
  }
1310
1667
  }
1311
1668
  async function writeQueue(cwd, requests) {
1312
- const path = join4(cwd, REQUESTS_PATH);
1313
- await mkdir2(dirname3(path), { recursive: true });
1314
- await writeFile2(path, `${JSON.stringify({ requests }, null, 2)}
1669
+ const path = join5(cwd, REQUESTS_PATH);
1670
+ await mkdir3(dirname4(path), { recursive: true });
1671
+ await writeFile3(path, `${JSON.stringify({ requests }, null, 2)}
1315
1672
  `, "utf8");
1316
1673
  }
1317
1674
  async function appendRequest(cwd, request) {
@@ -1351,13 +1708,264 @@ async function clearRequests(cwd) {
1351
1708
  return { cleared, pending: pending.length };
1352
1709
  }
1353
1710
 
1711
+ // ../server/dist/runner.js
1712
+ import { spawn as nodeSpawn } from "child_process";
1713
+ var POLL_MS = 2e3;
1714
+ var OUTPUT_LINES = 20;
1715
+ var SESSION_TURNS_CAP = 8;
1716
+ function resolveCommand(choice, prompt, sessionId = null) {
1717
+ if (choice.agent === null)
1718
+ return null;
1719
+ if (choice.agent === "custom") {
1720
+ if (choice.run === null)
1721
+ return null;
1722
+ const parsed = parseTemplate(choice.run);
1723
+ if (!parsed.ok)
1724
+ return null;
1725
+ return { agent: "custom", name: "Custom", ...commandFor(parsed.template, prompt), resumed: false };
1726
+ }
1727
+ const adapter = KNOWN_AGENTS[choice.agent];
1728
+ if (sessionId !== null && "resumeArgs" in adapter) {
1729
+ return {
1730
+ agent: choice.agent,
1731
+ name: adapter.name,
1732
+ command: adapter.binary,
1733
+ args: adapter.resumeArgs(sessionId, prompt),
1734
+ resumed: true
1735
+ };
1736
+ }
1737
+ return {
1738
+ agent: choice.agent,
1739
+ name: adapter.name,
1740
+ command: adapter.binary,
1741
+ args: adapter.args(prompt),
1742
+ resumed: false
1743
+ };
1744
+ }
1745
+ function lineReader(stream, onLine) {
1746
+ let buffered = "";
1747
+ const flush = () => {
1748
+ if (buffered === "")
1749
+ return;
1750
+ onLine(buffered.replace(/\r$/, ""));
1751
+ buffered = "";
1752
+ };
1753
+ stream.on("data", (chunk) => {
1754
+ buffered += chunk.toString();
1755
+ const lines = buffered.split("\n");
1756
+ buffered = lines.pop() ?? "";
1757
+ for (const line of lines)
1758
+ onLine(line.replace(/\r$/, ""));
1759
+ });
1760
+ stream.on("end", flush);
1761
+ return flush;
1762
+ }
1763
+ function defaultSpawn(command, args, options) {
1764
+ return nodeSpawn(command, args, options);
1765
+ }
1766
+ function startRunner(options) {
1767
+ const spawn4 = options.spawn ?? defaultSpawn;
1768
+ const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
1769
+ const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
1770
+ const failed = /* @__PURE__ */ new Set();
1771
+ let state = {
1772
+ running: false,
1773
+ requestId: null,
1774
+ agent: null,
1775
+ activity: null,
1776
+ startedAt: null
1777
+ };
1778
+ let stopped = false;
1779
+ let ticking = null;
1780
+ let stopPromise = null;
1781
+ let active = null;
1782
+ const sessions = /* @__PURE__ */ new Map();
1783
+ const idle = () => {
1784
+ state = { running: false, requestId: null, agent: null, activity: null, startedAt: null };
1785
+ };
1786
+ const rememberLine = (lines, line) => {
1787
+ lines.push(line);
1788
+ if (lines.length > OUTPUT_LINES)
1789
+ lines.splice(0, lines.length - OUTPUT_LINES);
1790
+ };
1791
+ const reportFailure = (request, error, lines) => {
1792
+ console.error(`Leglas agent failed for ${request.title}: ${error}`);
1793
+ for (const line of lines)
1794
+ console.error(` ${line}`);
1795
+ };
1796
+ const runChild = (request, resolved, lines, observed) => {
1797
+ let child;
1798
+ try {
1799
+ child = spawn4(resolved.command, resolved.args, {
1800
+ cwd: options.cwd,
1801
+ shell: false,
1802
+ stdio: ["ignore", "pipe", "pipe"]
1803
+ });
1804
+ } catch (error) {
1805
+ return Promise.resolve({
1806
+ ok: false,
1807
+ error: error instanceof Error ? error.message : String(error)
1808
+ });
1809
+ }
1810
+ const current = { child, requestId: request.id, cancelled: false };
1811
+ active = current;
1812
+ const stdoutFlush = lineReader(child.stdout, (line) => {
1813
+ rememberLine(lines, line);
1814
+ const sessionId = sessionFrom(resolved.agent, line);
1815
+ if (sessionId !== null)
1816
+ observed.sessionId = sessionId;
1817
+ const activity = activityFrom(resolved.agent, line, options.cwd);
1818
+ if (activity !== null) {
1819
+ if (activity.startsWith("editing"))
1820
+ observed.edited = true;
1821
+ if (active === current)
1822
+ state = { ...state, activity };
1823
+ }
1824
+ });
1825
+ const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
1826
+ return new Promise((resolve) => {
1827
+ let settled = false;
1828
+ const settle = (outcome) => {
1829
+ if (settled)
1830
+ return;
1831
+ settled = true;
1832
+ stdoutFlush();
1833
+ stderrFlush();
1834
+ resolve(outcome);
1835
+ };
1836
+ child.once("error", (error) => settle({ ok: false, error: error.message }));
1837
+ child.once("close", (code, signal) => {
1838
+ if (current.cancelled)
1839
+ return settle({ ok: false, error: "cancelled" });
1840
+ if (signal !== null)
1841
+ return settle({ ok: false, error: `stopped by ${signal}` });
1842
+ settle({ ok: true, code: code ?? 0 });
1843
+ });
1844
+ }).finally(() => {
1845
+ if (active === current)
1846
+ active = null;
1847
+ });
1848
+ };
1849
+ const handle = async (request, choice) => {
1850
+ const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
1851
+ const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
1852
+ let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null);
1853
+ if (resolved === null)
1854
+ return;
1855
+ const lines = [];
1856
+ try {
1857
+ if (!await markPickedUp(options.cwd, request.id))
1858
+ return;
1859
+ if (stopped) {
1860
+ failed.add(request.id);
1861
+ return;
1862
+ }
1863
+ state = {
1864
+ running: true,
1865
+ requestId: request.id,
1866
+ agent: resolved.name,
1867
+ activity: null,
1868
+ startedAt: Date.now()
1869
+ };
1870
+ const observed = { sessionId: null, edited: false };
1871
+ let outcome = await runChild(request, resolved, lines, observed);
1872
+ const cancelled = !outcome.ok && outcome.error === "cancelled";
1873
+ if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && !cancelled && // Not redundant with the line above: a stop that lands between the
1874
+ // first child settling and the retry starting finds no child to
1875
+ // cancel, so nothing says "cancelled". Stopped still means stopped.
1876
+ !stopped) {
1877
+ sessions.delete(resolved.agent);
1878
+ const cold = resolveCommand(choice, request.prompt);
1879
+ if (cold !== null) {
1880
+ resolved = cold;
1881
+ observed.sessionId = null;
1882
+ state = { ...state, activity: null };
1883
+ outcome = await runChild(request, resolved, lines, observed);
1884
+ }
1885
+ }
1886
+ if (outcome.ok && outcome.code === 0) {
1887
+ if (observed.sessionId !== null) {
1888
+ const previous = sessions.get(resolved.agent);
1889
+ sessions.set(resolved.agent, {
1890
+ id: observed.sessionId,
1891
+ turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
1892
+ });
1893
+ }
1894
+ await removeRequest(options.cwd, request.id);
1895
+ return;
1896
+ }
1897
+ sessions.delete(resolved.agent);
1898
+ failed.add(request.id);
1899
+ reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
1900
+ } finally {
1901
+ idle();
1902
+ }
1903
+ };
1904
+ const tick = async () => {
1905
+ if (stopped)
1906
+ return;
1907
+ const choice = await readAgentChoice(options.cwd);
1908
+ if (choice.agent === null || stopped)
1909
+ return;
1910
+ if (options.externallyAttached())
1911
+ return;
1912
+ const request = nextRequest(await readRequests(options.cwd), failed);
1913
+ if (request !== null && !stopped)
1914
+ await handle(request, choice);
1915
+ };
1916
+ const schedule = () => {
1917
+ if (stopped || ticking !== null)
1918
+ return;
1919
+ const task = tick();
1920
+ ticking = task;
1921
+ void task.catch((error) => console.error(`Leglas runner: ${error instanceof Error ? error.message : String(error)}`)).finally(() => {
1922
+ if (ticking === task)
1923
+ ticking = null;
1924
+ });
1925
+ };
1926
+ const timer = setEvery(schedule, POLL_MS);
1927
+ schedule();
1928
+ const cancel = (id) => {
1929
+ if (active === null || active.cancelled)
1930
+ return false;
1931
+ if (id !== void 0 && active.requestId !== id)
1932
+ return false;
1933
+ active.cancelled = true;
1934
+ failed.add(active.requestId);
1935
+ try {
1936
+ active.child.kill("SIGTERM");
1937
+ } catch {
1938
+ }
1939
+ return true;
1940
+ };
1941
+ const stop = () => {
1942
+ if (stopPromise !== null)
1943
+ return stopPromise;
1944
+ stopped = true;
1945
+ clearEvery(timer);
1946
+ cancel();
1947
+ stopPromise = Promise.resolve(ticking).catch(() => {
1948
+ }).then(() => {
1949
+ });
1950
+ return stopPromise;
1951
+ };
1952
+ return {
1953
+ stop,
1954
+ snapshot: () => ({ ...state, failedIds: [...failed] }),
1955
+ cancel,
1956
+ // schedule already refuses to overlap a tick in flight, so a nudge during
1957
+ // a run costs nothing and a nudge between runs starts the next one now.
1958
+ nudge: schedule
1959
+ };
1960
+ }
1961
+
1354
1962
  // ../server/dist/renames.js
1355
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1356
- import { dirname as dirname4, join as join5 } from "path";
1963
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1964
+ import { dirname as dirname5, join as join6 } from "path";
1357
1965
  var RENAMES_PATH = ".leglas/renames.json";
1358
1966
  async function readRenames(cwd) {
1359
1967
  try {
1360
- const raw = await readFile4(join5(cwd, RENAMES_PATH), "utf8");
1968
+ const raw = await readFile5(join6(cwd, RENAMES_PATH), "utf8");
1361
1969
  const parsed = JSON.parse(raw);
1362
1970
  if (parsed.renames === null || typeof parsed.renames !== "object")
1363
1971
  return {};
@@ -1367,9 +1975,9 @@ async function readRenames(cwd) {
1367
1975
  }
1368
1976
  }
1369
1977
  async function writeRenames(cwd, renames) {
1370
- const path = join5(cwd, RENAMES_PATH);
1371
- await mkdir3(dirname4(path), { recursive: true });
1372
- await writeFile3(path, `${JSON.stringify({ renames }, null, 2)}
1978
+ const path = join6(cwd, RENAMES_PATH);
1979
+ await mkdir4(dirname5(path), { recursive: true });
1980
+ await writeFile4(path, `${JSON.stringify({ renames }, null, 2)}
1373
1981
  `, "utf8");
1374
1982
  }
1375
1983
  function resolveTitle(input, titles, renames) {
@@ -1387,7 +1995,7 @@ function resolveTitle(input, titles, renames) {
1387
1995
  import { createReadStream, existsSync as existsSync2, statSync } from "fs";
1388
1996
  import http2 from "http";
1389
1997
  import net3 from "net";
1390
- import { extname, join as join6, normalize, relative as relative2 } from "path";
1998
+ import { extname, join as join7, normalize, relative as relative3 } from "path";
1391
1999
  var LEGLAS_PREFIX = "/leglas";
1392
2000
  var DEFAULT_PORT = 4100;
1393
2001
  var PORT_ATTEMPTS = 20;
@@ -1418,6 +2026,52 @@ function sendJson(res, status, body) {
1418
2026
  });
1419
2027
  res.end(payload);
1420
2028
  }
2029
+ function isKnownAgent(value) {
2030
+ return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
2031
+ }
2032
+ function isAllowedMutationHost(hostname) {
2033
+ const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
2034
+ if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1")
2035
+ return true;
2036
+ if (bare.endsWith(".local"))
2037
+ return true;
2038
+ if (!net3.isIPv4(bare))
2039
+ return false;
2040
+ const [first, second] = bare.split(".").map(Number);
2041
+ return first === 10 || first === 172 && second !== void 0 && second >= 16 && second <= 31 || first === 192 && second === 168;
2042
+ }
2043
+ function isLoopbackAddress(address) {
2044
+ if (address === void 0)
2045
+ return false;
2046
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address.startsWith("127.");
2047
+ }
2048
+ function isTrustedMutation(req) {
2049
+ if (!isLoopbackAddress(req.socket.remoteAddress))
2050
+ return false;
2051
+ if (typeof req.headers.host !== "string")
2052
+ return false;
2053
+ let host;
2054
+ try {
2055
+ host = new URL(`http://${req.headers.host}`);
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ if (!isAllowedMutationHost(host.hostname))
2060
+ return false;
2061
+ const rawOrigin = req.headers.origin;
2062
+ if (rawOrigin === void 0)
2063
+ return true;
2064
+ try {
2065
+ const origin = new URL(rawOrigin);
2066
+ return origin.protocol === "http:" && origin.host === host.host;
2067
+ } catch {
2068
+ return false;
2069
+ }
2070
+ }
2071
+ function hasJsonBody(req) {
2072
+ const contentType = req.headers["content-type"];
2073
+ return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
2074
+ }
1421
2075
  function probe(target, timeoutMs = 1e3) {
1422
2076
  return new Promise((resolve) => {
1423
2077
  let url;
@@ -1439,8 +2093,8 @@ function probe(target, timeoutMs = 1e3) {
1439
2093
  });
1440
2094
  }
1441
2095
  function serveFrom(res, dir, relativePath) {
1442
- const relative4 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
1443
- const candidate = join6(dir, relative4);
2096
+ const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
2097
+ const candidate = join7(dir, relative5);
1444
2098
  if (!candidate.startsWith(dir))
1445
2099
  return false;
1446
2100
  if (!existsSync2(candidate) || !statSync(candidate).isFile())
@@ -1453,9 +2107,9 @@ function serveFrom(res, dir, relativePath) {
1453
2107
  return true;
1454
2108
  }
1455
2109
  function serveShellFile(res, shellDir, urlPath) {
1456
- const relative4 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
1457
- const isRoot = relative4 === "" || relative4 === "." || relative4 === "/";
1458
- return serveFrom(res, shellDir, isRoot ? "index.html" : relative4);
2110
+ const relative5 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
2111
+ const isRoot = relative5 === "" || relative5 === "." || relative5 === "/";
2112
+ return serveFrom(res, shellDir, isRoot ? "index.html" : relative5);
1459
2113
  }
1460
2114
  function snapshotConfig(cwd) {
1461
2115
  const path = findConfigFile(cwd);
@@ -1471,15 +2125,15 @@ function configStalenessNotice(cwd, boot, current) {
1471
2125
  if (boot === null && current === null)
1472
2126
  return null;
1473
2127
  if (boot === null && current !== null) {
1474
- const label = relative2(cwd, current.path) || current.path;
2128
+ const label = relative3(cwd, current.path) || current.path;
1475
2129
  return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
1476
2130
  }
1477
2131
  if (boot !== null && current === null) {
1478
- const label = relative2(cwd, boot.path) || boot.path;
2132
+ const label = relative3(cwd, boot.path) || boot.path;
1479
2133
  return `${label} was removed after Leglas started. Restart leglas to run without it.`;
1480
2134
  }
1481
2135
  if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
1482
- const label = relative2(cwd, current.path) || current.path;
2136
+ const label = relative3(cwd, current.path) || current.path;
1483
2137
  return `${label} changed after Leglas started. Restart leglas to pick it up.`;
1484
2138
  }
1485
2139
  return null;
@@ -1525,14 +2179,40 @@ async function bind(server, requested) {
1525
2179
  throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
1526
2180
  }
1527
2181
  async function startServer(options) {
1528
- const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
2182
+ const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
1529
2183
  const target = config?.devServer ?? "http://localhost:3000";
1530
2184
  const proxy = createProxyHandler({ target });
1531
2185
  const bootConfigSnapshot = snapshotConfig(cwd);
1532
2186
  let lastSeen = null;
2187
+ const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
2188
+ let runner = null;
2189
+ let agentsCache = null;
2190
+ let agentsInflight = null;
2191
+ const AGENTS_FRESH_MS = 3e4;
2192
+ const probeAgents = () => {
2193
+ agentsInflight ??= detect().then((agents) => {
2194
+ agentsCache = { at: Date.now(), agents };
2195
+ return agents;
2196
+ }).finally(() => {
2197
+ agentsInflight = null;
2198
+ });
2199
+ return agentsInflight;
2200
+ };
2201
+ const currentAgents = () => {
2202
+ if (agentsCache === null)
2203
+ return probeAgents();
2204
+ if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
2205
+ void probeAgents().catch(() => {
2206
+ });
2207
+ }
2208
+ return Promise.resolve(agentsCache.agents);
2209
+ };
1533
2210
  const server = http2.createServer((req, res) => {
1534
2211
  const url = req.url ?? "/";
1535
2212
  const path = url.split("?")[0] ?? "/";
2213
+ if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
2214
+ return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
2215
+ }
1536
2216
  if (path === `${LEGLAS_PREFIX}/api/config`) {
1537
2217
  const boot = config?.previews ?? [];
1538
2218
  const errors = [...configErrors];
@@ -1576,7 +2256,54 @@ async function startServer(options) {
1576
2256
  url: preview.url,
1577
2257
  intent: parsed.intent.trim(),
1578
2258
  ...composed
1579
- }).then(() => sendJson(res, 200, { ok: true, ...composed })).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
2259
+ }).then(() => {
2260
+ runner?.nudge();
2261
+ sendJson(res, 200, { ok: true, ...composed });
2262
+ }).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
2263
+ });
2264
+ }
2265
+ if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
2266
+ return void Promise.all([currentAgents(), readAgentChoice(cwd)]).then(([agents, choice]) => sendJson(res, 200, {
2267
+ agents,
2268
+ choice: choice.agent,
2269
+ customRun: choice.run
2270
+ }));
2271
+ }
2272
+ if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
2273
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
2274
+ return sendJson(res, 403, {
2275
+ ok: false,
2276
+ error: "The agent choice can only be made from the machine running Leglas."
2277
+ });
2278
+ }
2279
+ if (!hasJsonBody(req)) {
2280
+ return sendJson(res, 400, { ok: false, error: "Agent choice must be JSON." });
2281
+ }
2282
+ let body = "";
2283
+ req.on("data", (chunk) => body += chunk);
2284
+ return void req.on("end", () => {
2285
+ let parsed;
2286
+ try {
2287
+ parsed = JSON.parse(body || "{}");
2288
+ } catch {
2289
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
2290
+ }
2291
+ if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
2292
+ return sendJson(res, 400, { ok: false, error: "Body needs a known agent." });
2293
+ }
2294
+ if (parsed.run !== void 0 && typeof parsed.run !== "string") {
2295
+ return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
2296
+ }
2297
+ if (parsed.agent === "custom") {
2298
+ if (typeof parsed.run !== "string") {
2299
+ return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
2300
+ }
2301
+ const template = parseTemplate(parsed.run);
2302
+ if (!template.ok)
2303
+ return sendJson(res, 400, { ok: false, error: template.error });
2304
+ return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
2305
+ }
2306
+ return void saveAgentChoice(cwd, { agent: parsed.agent }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
1580
2307
  });
1581
2308
  }
1582
2309
  if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
@@ -1597,11 +2324,119 @@ async function startServer(options) {
1597
2324
  });
1598
2325
  }
1599
2326
  if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
2327
+ const snapshot = runner?.snapshot() ?? {
2328
+ running: false,
2329
+ requestId: null,
2330
+ agent: null,
2331
+ activity: null,
2332
+ startedAt: null,
2333
+ failedIds: []
2334
+ };
1600
2335
  return void readRequests(cwd).then((requests) => sendJson(res, 200, {
1601
- requests: requests.map(({ id, title, intent, status }) => ({ id, title, intent, status })),
1602
- agent: { attached: lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS }
2336
+ requests: requests.map(({ id, title, intent, status }) => ({
2337
+ id,
2338
+ title,
2339
+ intent,
2340
+ status: snapshot.running && snapshot.requestId === id ? "running" : snapshot.failedIds.includes(id) ? "failed" : status
2341
+ })),
2342
+ agent: {
2343
+ attached: externallyAttached(),
2344
+ running: snapshot.running,
2345
+ name: snapshot.running ? snapshot.agent : null,
2346
+ activity: snapshot.running ? snapshot.activity : null,
2347
+ startedAt: snapshot.running ? snapshot.startedAt : null
2348
+ }
1603
2349
  }));
1604
2350
  }
2351
+ if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
2352
+ if (!hasJsonBody(req)) {
2353
+ return sendJson(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
2354
+ }
2355
+ let body = "";
2356
+ req.on("data", (chunk) => body += chunk);
2357
+ return void req.on("end", () => {
2358
+ let parsed;
2359
+ try {
2360
+ parsed = JSON.parse(body || "{}");
2361
+ } catch {
2362
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
2363
+ }
2364
+ if (parsed.id !== void 0 && typeof parsed.id !== "string") {
2365
+ return sendJson(res, 400, { ok: false, error: "The request id must be a string." });
2366
+ }
2367
+ return sendJson(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
2368
+ });
2369
+ }
2370
+ if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
2371
+ if (!hasJsonBody(req)) {
2372
+ return sendJson(res, 400, { ok: false, error: "Retry must be JSON." });
2373
+ }
2374
+ let body = "";
2375
+ req.on("data", (chunk) => body += chunk);
2376
+ return void req.on("end", async () => {
2377
+ let parsed;
2378
+ try {
2379
+ parsed = JSON.parse(body || "{}");
2380
+ } catch {
2381
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
2382
+ }
2383
+ if (typeof parsed.id !== "string") {
2384
+ return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
2385
+ }
2386
+ const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
2387
+ if (request === void 0) {
2388
+ return sendJson(res, 404, { ok: false, error: "No such request." });
2389
+ }
2390
+ if (!(runner?.snapshot().failedIds.includes(request.id) ?? false)) {
2391
+ return sendJson(res, 400, { ok: false, error: "Only a failed request can be retried." });
2392
+ }
2393
+ try {
2394
+ if (!await removeRequest(cwd, request.id)) {
2395
+ return sendJson(res, 404, { ok: false, error: "No such request." });
2396
+ }
2397
+ await appendRequest(cwd, {
2398
+ title: request.title,
2399
+ url: request.url,
2400
+ intent: request.intent,
2401
+ target: request.target,
2402
+ prompt: request.prompt
2403
+ });
2404
+ runner?.nudge();
2405
+ return sendJson(res, 200, { ok: true });
2406
+ } catch {
2407
+ return sendJson(res, 500, { ok: false, error: "The request could not be retried." });
2408
+ }
2409
+ });
2410
+ }
2411
+ if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
2412
+ if (!hasJsonBody(req)) {
2413
+ return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
2414
+ }
2415
+ let body = "";
2416
+ req.on("data", (chunk) => body += chunk);
2417
+ return void req.on("end", async () => {
2418
+ let parsed;
2419
+ try {
2420
+ parsed = JSON.parse(body || "{}");
2421
+ } catch {
2422
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
2423
+ }
2424
+ if (typeof parsed.id !== "string") {
2425
+ return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
2426
+ }
2427
+ if (!(runner?.snapshot().failedIds.includes(parsed.id) ?? false)) {
2428
+ return sendJson(res, 400, { ok: false, error: "Only a failed request can be dismissed." });
2429
+ }
2430
+ try {
2431
+ if (!await removeRequest(cwd, parsed.id)) {
2432
+ return sendJson(res, 404, { ok: false, error: "No such request." });
2433
+ }
2434
+ return sendJson(res, 200, { ok: true });
2435
+ } catch {
2436
+ return sendJson(res, 500, { ok: false, error: "The request could not be dismissed." });
2437
+ }
2438
+ });
2439
+ }
1605
2440
  if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
1606
2441
  let body = "";
1607
2442
  req.on("data", (chunk) => body += chunk);
@@ -1626,14 +2461,14 @@ async function startServer(options) {
1626
2461
  const rest = path.slice(FILES_PREFIX.length + 1);
1627
2462
  const slash = rest.indexOf("/");
1628
2463
  const slug = slash === -1 ? rest : rest.slice(0, slash);
1629
- let relative4 = slash === -1 ? "" : rest.slice(slash + 1);
2464
+ let relative5 = slash === -1 ? "" : rest.slice(slash + 1);
1630
2465
  try {
1631
- relative4 = decodeURIComponent(relative4);
2466
+ relative5 = decodeURIComponent(relative5);
1632
2467
  } catch {
1633
- relative4 = "";
2468
+ relative5 = "";
1634
2469
  }
1635
2470
  const dir = fileMounts.get(slug);
1636
- if (dir !== void 0 && relative4 !== "" && serveFrom(res, dir, relative4))
2471
+ if (dir !== void 0 && relative5 !== "" && serveFrom(res, dir, relative5))
1637
2472
  return;
1638
2473
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
1639
2474
  return res.end("Leglas: no such preview file.");
@@ -1665,16 +2500,23 @@ async function startServer(options) {
1665
2500
  proxy.upgrade(req, socket, head);
1666
2501
  });
1667
2502
  const port = await bind(server, options.port ?? DEFAULT_PORT);
2503
+ runner = startRunner({ cwd, externallyAttached });
2504
+ let closePromise = null;
1668
2505
  return {
1669
2506
  port,
1670
2507
  url: `http://localhost:${port}`,
1671
- close: () => new Promise((done) => {
1672
- for (const socket of sockets)
1673
- socket.destroy();
1674
- sockets.clear();
1675
- server.closeAllConnections();
1676
- server.close(() => done());
1677
- })
2508
+ close: () => {
2509
+ if (closePromise !== null)
2510
+ return closePromise;
2511
+ closePromise = runner.stop().then(() => new Promise((done) => {
2512
+ for (const socket of sockets)
2513
+ socket.destroy();
2514
+ sockets.clear();
2515
+ server.closeAllConnections();
2516
+ server.close(() => done());
2517
+ }));
2518
+ return closePromise;
2519
+ }
1678
2520
  };
1679
2521
  }
1680
2522
 
@@ -1733,11 +2575,11 @@ function planKeep(options) {
1733
2575
  }
1734
2576
 
1735
2577
  // src/run-init.ts
1736
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1737
- import { join as join7 } from "path";
2578
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
2579
+ import { join as join8 } from "path";
1738
2580
  async function readIfPresent(path) {
1739
2581
  try {
1740
- return await readFile5(path, "utf8");
2582
+ return await readFile6(path, "utf8");
1741
2583
  } catch {
1742
2584
  return null;
1743
2585
  }
@@ -1745,18 +2587,18 @@ async function readIfPresent(path) {
1745
2587
  async function runInit(options, deps) {
1746
2588
  const existingConfig = findConfigFile(options.cwd);
1747
2589
  const plan = planInit({
1748
- agents: await readIfPresent(join7(options.cwd, "AGENTS.md")),
2590
+ agents: await readIfPresent(join8(options.cwd, "AGENTS.md")),
1749
2591
  config: existingConfig === null ? null : "present",
1750
- gitignore: await readIfPresent(join7(options.cwd, ".gitignore")),
2592
+ gitignore: await readIfPresent(join8(options.cwd, ".gitignore")),
1751
2593
  force: options.force
1752
2594
  });
1753
2595
  const touched = [];
1754
2596
  for (const write of plan.writes) {
1755
- await writeFile4(join7(options.cwd, write.path), write.contents, "utf8");
2597
+ await writeFile5(join8(options.cwd, write.path), write.contents, "utf8");
1756
2598
  touched.push(write.path);
1757
2599
  }
1758
2600
  if (plan.gitignore !== null) {
1759
- await writeFile4(join7(options.cwd, ".gitignore"), plan.gitignore, "utf8");
2601
+ await writeFile5(join8(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1760
2602
  touched.push(".gitignore");
1761
2603
  }
1762
2604
  if (options.json) {
@@ -1776,8 +2618,8 @@ async function runInit(options, deps) {
1776
2618
 
1777
2619
  // src/run-keep.ts
1778
2620
  import { existsSync as existsSync3 } from "fs";
1779
- import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
1780
- import { dirname as dirname5, join as join8 } from "path";
2621
+ import { mkdir as mkdir5, readFile as readFile7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
2622
+ import { dirname as dirname6, join as join9 } from "path";
1781
2623
 
1782
2624
  // src/resolve-title.ts
1783
2625
  function resolveOrExplain(input, titles, renames) {
@@ -1821,18 +2663,18 @@ async function runKeep(options, deps) {
1821
2663
  if (!resolved.ok) return fail(resolved.error);
1822
2664
  const plan = planKeep({ title: resolved.title, previews, to: options.to });
1823
2665
  if (!plan.ok) return fail(plan.error);
1824
- const from = join8(options.cwd, plan.move.from);
1825
- const to = join8(options.cwd, plan.move.to);
2666
+ const from = join9(options.cwd, plan.move.from);
2667
+ const to = join9(options.cwd, plan.move.to);
1826
2668
  if (!existsSync3(from)) {
1827
2669
  return fail(`${plan.move.from} does not exist. Nothing to keep.`);
1828
2670
  }
1829
2671
  if (existsSync3(to)) {
1830
2672
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
1831
2673
  }
1832
- const source = await readFile6(from, "utf8");
1833
- await mkdir4(dirname5(to), { recursive: true });
1834
- await writeFile5(to, renameExport(source, plan.exportName), "utf8");
1835
- await rm2(join8(options.cwd, plan.removeDir), { recursive: true, force: true });
2674
+ const source = await readFile7(from, "utf8");
2675
+ await mkdir5(dirname6(to), { recursive: true });
2676
+ await writeFile6(to, renameExport(source, plan.exportName), "utf8");
2677
+ await rm2(join9(options.cwd, plan.removeDir), { recursive: true, force: true });
1836
2678
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
1837
2679
  if (options.json) {
1838
2680
  deps.log(
@@ -1867,11 +2709,11 @@ async function runKeep(options, deps) {
1867
2709
 
1868
2710
  // src/run-new.ts
1869
2711
  import { existsSync as existsSync4 } from "fs";
1870
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
1871
- import { dirname as dirname6, join as join9 } from "path";
2712
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
2713
+ import { dirname as dirname7, join as join10 } from "path";
1872
2714
  async function readIfPresent2(path) {
1873
2715
  try {
1874
- return await readFile7(path, "utf8");
2716
+ return await readFile8(path, "utf8");
1875
2717
  } catch {
1876
2718
  return null;
1877
2719
  }
@@ -1879,7 +2721,7 @@ async function readIfPresent2(path) {
1879
2721
  async function runNew(options, deps) {
1880
2722
  let from;
1881
2723
  if (options.from !== void 0) {
1882
- const contents = await readIfPresent2(join9(options.cwd, options.from));
2724
+ const contents = await readIfPresent2(join10(options.cwd, options.from));
1883
2725
  if (contents === null) {
1884
2726
  const message = `${options.from} does not exist, so there is nothing to use as the baseline.`;
1885
2727
  if (options.json) deps.log(JSON.stringify({ ok: false, error: message }));
@@ -1890,8 +2732,8 @@ async function runNew(options, deps) {
1890
2732
  }
1891
2733
  const plan = planNew({
1892
2734
  surface: options.surface,
1893
- packageJson: await readIfPresent2(join9(options.cwd, "package.json")),
1894
- gitignore: await readIfPresent2(join9(options.cwd, ".gitignore")),
2735
+ packageJson: await readIfPresent2(join10(options.cwd, "package.json")),
2736
+ gitignore: await readIfPresent2(join10(options.cwd, ".gitignore")),
1895
2737
  from
1896
2738
  });
1897
2739
  const fail = (error) => {
@@ -1914,19 +2756,19 @@ async function runNew(options, deps) {
1914
2756
  deps.log(plan.instructions);
1915
2757
  return { exitCode: 0, written: [] };
1916
2758
  }
1917
- const existing = plan.writes.filter((write) => existsSync4(join9(options.cwd, write.path)));
2759
+ const existing = plan.writes.filter((write) => existsSync4(join10(options.cwd, write.path)));
1918
2760
  if (existing.length > 0) {
1919
2761
  return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
1920
2762
  }
1921
2763
  const written = [];
1922
2764
  for (const write of plan.writes) {
1923
- const target = join9(options.cwd, write.path);
1924
- await mkdir5(dirname6(target), { recursive: true });
1925
- await writeFile6(target, write.contents, "utf8");
2765
+ const target = join10(options.cwd, write.path);
2766
+ await mkdir6(dirname7(target), { recursive: true });
2767
+ await writeFile7(target, write.contents, "utf8");
1926
2768
  written.push(write.path);
1927
2769
  }
1928
2770
  if (plan.gitignore !== null) {
1929
- await writeFile6(join9(options.cwd, ".gitignore"), plan.gitignore, "utf8");
2771
+ await writeFile7(join10(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1930
2772
  written.push(".gitignore");
1931
2773
  }
1932
2774
  if (options.json) {
@@ -1947,21 +2789,21 @@ async function runNew(options, deps) {
1947
2789
  }
1948
2790
 
1949
2791
  // src/run-previews.ts
1950
- import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
1951
- import { join as join10 } from "path";
2792
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
2793
+ import { join as join11 } from "path";
1952
2794
  function envelope(deps, ok, body) {
1953
2795
  deps.log(JSON.stringify({ ok, ...body }));
1954
2796
  }
1955
2797
  async function ensureIgnored(cwd) {
1956
- const path = join10(cwd, ".gitignore");
2798
+ const path = join11(cwd, ".gitignore");
1957
2799
  let current = null;
1958
2800
  try {
1959
- current = await readFile8(path, "utf8");
2801
+ current = await readFile9(path, "utf8");
1960
2802
  } catch {
1961
2803
  current = null;
1962
2804
  }
1963
2805
  const next = ignoreEntry(current);
1964
- if (next !== null) await writeFile7(path, next, "utf8");
2806
+ if (next !== null) await writeFile8(path, next, "utf8");
1965
2807
  }
1966
2808
  async function runAdd(options, deps) {
1967
2809
  const loaded = await loadConfig(options.cwd);
@@ -2205,99 +3047,25 @@ async function runShow(options, deps) {
2205
3047
  return { exitCode: 0 };
2206
3048
  }
2207
3049
 
2208
- // src/watch.ts
2209
- var WATCH_PATH = ".leglas/watch.json";
2210
- var PROMPT_TOKEN = "{prompt}";
2211
- var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
2212
- function tokenize(template) {
2213
- const tokens = [];
2214
- let current = "";
2215
- let started = false;
2216
- let quote = null;
2217
- for (const character of template) {
2218
- if (quote !== null) {
2219
- if (character === quote) quote = null;
2220
- else current += character;
2221
- continue;
2222
- }
2223
- if (character === '"' || character === "'") {
2224
- quote = character;
2225
- started = true;
2226
- continue;
2227
- }
2228
- if (/\s/.test(character)) {
2229
- if (started) tokens.push(current);
2230
- current = "";
2231
- started = false;
2232
- continue;
2233
- }
2234
- current += character;
2235
- started = true;
2236
- }
2237
- if (quote !== null) {
2238
- return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
2239
- }
2240
- if (started) tokens.push(current);
2241
- return { ok: true, tokens };
2242
- }
2243
- function parseTemplate(raw) {
2244
- const tokenized = tokenize(raw);
2245
- if (!tokenized.ok) return tokenized;
2246
- const { tokens } = tokenized;
2247
- const [command, ...args] = tokens;
2248
- if (command === void 0) {
2249
- return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
2250
- }
2251
- const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
2252
- if (placeholders === 0) {
2253
- return {
2254
- ok: false,
2255
- error: `The agent command needs ${PROMPT_TOKEN} as a word of its own, for example: ${EXAMPLE}`
2256
- };
2257
- }
2258
- if (placeholders > 1) {
2259
- return {
2260
- ok: false,
2261
- error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
2262
- };
2263
- }
2264
- if (command === PROMPT_TOKEN) {
2265
- return {
2266
- ok: false,
2267
- error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
2268
- };
2269
- }
2270
- return { ok: true, template: { command, args } };
2271
- }
2272
- function commandFor(template, prompt) {
2273
- return {
2274
- command: template.command,
2275
- args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
2276
- };
2277
- }
2278
- function nextRequest(requests, failed) {
2279
- return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
2280
- }
2281
-
2282
3050
  // src/run-watch.ts
2283
- import { spawn as spawn2 } from "child_process";
2284
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
2285
- import { dirname as dirname7, join as join11 } from "path";
2286
- var POLL_MS = 2e3;
3051
+ import { spawn as spawn3 } from "child_process";
3052
+ import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
3053
+ import { dirname as dirname8, join as join12 } from "path";
3054
+ var POLL_MS2 = 2e3;
2287
3055
  var HEARTBEAT_TIMEOUT_MS = 1e3;
2288
- async function readSavedTemplate(cwd) {
3056
+ async function saveTemplate(cwd, run3) {
3057
+ const path = join12(cwd, WATCH_PATH);
3058
+ let config = {};
2289
3059
  try {
2290
- const raw = await readFile9(join11(cwd, WATCH_PATH), "utf8");
2291
- const parsed = JSON.parse(raw);
2292
- return typeof parsed.run === "string" && parsed.run !== "" ? parsed.run : null;
3060
+ const parsed = JSON.parse(await readFile10(path, "utf8"));
3061
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3062
+ config = parsed;
3063
+ }
2293
3064
  } catch {
2294
- return null;
2295
3065
  }
2296
- }
2297
- async function saveTemplate(cwd, run3) {
2298
- const path = join11(cwd, WATCH_PATH);
2299
- await mkdir6(dirname7(path), { recursive: true });
2300
- await writeFile8(path, `${JSON.stringify({ run: run3 }, null, 2)}
3066
+ config.run = run3;
3067
+ await mkdir7(dirname8(path), { recursive: true });
3068
+ await writeFile9(path, `${JSON.stringify(config, null, 2)}
2301
3069
  `, "utf8");
2302
3070
  }
2303
3071
  function spawnAgent(command, args, cwd) {
@@ -2308,7 +3076,7 @@ function spawnAgent(command, args, cwd) {
2308
3076
  settled = true;
2309
3077
  resolve(outcome);
2310
3078
  };
2311
- const child = spawn2(command, args, { cwd, stdio: "inherit" });
3079
+ const child = spawn3(command, args, { cwd, stdio: "inherit" });
2312
3080
  child.on("error", (error) => settle({ ok: false, error: error.message }));
2313
3081
  child.on(
2314
3082
  "close",
@@ -2319,21 +3087,34 @@ function spawnAgent(command, args, cwd) {
2319
3087
  });
2320
3088
  }
2321
3089
  async function runWatch(options, deps) {
2322
- const saved = options.run === void 0 ? await readSavedTemplate(options.cwd) : null;
2323
- const raw = options.run ?? saved;
2324
- if (raw === null) {
3090
+ const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
3091
+ const raw = options.run ?? saved.run;
3092
+ let template;
3093
+ let shownCommand2;
3094
+ let synthesizedAgent = null;
3095
+ if (raw !== null) {
3096
+ const parsed = parseTemplate(raw);
3097
+ if (!parsed.ok) {
3098
+ deps.error(parsed.error);
3099
+ return { exitCode: 1 };
3100
+ }
3101
+ template = parsed.template;
3102
+ shownCommand2 = raw;
3103
+ } else if (saved.agent !== null && saved.agent !== "custom") {
3104
+ const adapter = KNOWN_AGENTS[saved.agent];
3105
+ template = {
3106
+ command: adapter.binary,
3107
+ args: adapter.terminalArgs(PROMPT_TOKEN)
3108
+ };
3109
+ shownCommand2 = [template.command, ...template.args].join(" ");
3110
+ synthesizedAgent = adapter.name;
3111
+ } else {
2325
3112
  deps.error(
2326
- 'Watch needs an agent command the first time: npx leglas watch --run "claude -p {prompt}"'
3113
+ 'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
2327
3114
  );
2328
3115
  return { exitCode: 1 };
2329
3116
  }
2330
- const parsed = parseTemplate(raw);
2331
- if (!parsed.ok) {
2332
- deps.error(parsed.error);
2333
- return { exitCode: 1 };
2334
- }
2335
- const template = parsed.template;
2336
- if (options.run !== void 0) await saveTemplate(options.cwd, raw).catch(() => {
3117
+ if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
2337
3118
  });
2338
3119
  const base = `http://localhost:${options.port ?? DEFAULT_PORT}`;
2339
3120
  const heartbeat = async (watching) => {
@@ -2347,11 +3128,15 @@ async function runWatch(options, deps) {
2347
3128
  } catch {
2348
3129
  }
2349
3130
  };
2350
- deps.log(`Watching for change requests. Each one runs: ${raw}`);
3131
+ if (synthesizedAgent !== null) {
3132
+ deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
3133
+ }
3134
+ deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
2351
3135
  deps.log("Stop with Ctrl-C.");
2352
3136
  const failed = /* @__PURE__ */ new Set();
2353
3137
  let stopped = false;
2354
3138
  let busy = false;
3139
+ let announced = false;
2355
3140
  let inflight = null;
2356
3141
  const handle = async (request) => {
2357
3142
  deps.log("");
@@ -2373,7 +3158,12 @@ async function runWatch(options, deps) {
2373
3158
  };
2374
3159
  const tick = async () => {
2375
3160
  if (stopped) return;
2376
- void heartbeat(true);
3161
+ if (announced) {
3162
+ void heartbeat(true);
3163
+ } else {
3164
+ await heartbeat(true);
3165
+ announced = true;
3166
+ }
2377
3167
  if (busy) return;
2378
3168
  busy = true;
2379
3169
  try {
@@ -2390,7 +3180,7 @@ async function runWatch(options, deps) {
2390
3180
  }
2391
3181
  };
2392
3182
  return new Promise((resolve) => {
2393
- const timer = setInterval(() => void tick(), POLL_MS);
3183
+ const timer = setInterval(() => void tick(), POLL_MS2);
2394
3184
  const stop = () => {
2395
3185
  if (stopped) return;
2396
3186
  stopped = true;
@@ -2409,12 +3199,12 @@ async function runWatch(options, deps) {
2409
3199
 
2410
3200
  // src/run-classify.ts
2411
3201
  import { stat } from "fs/promises";
2412
- import { join as join12 } from "path";
3202
+ import { join as join13 } from "path";
2413
3203
  async function runClassify(options, deps) {
2414
3204
  const declared = await Promise.all(
2415
3205
  options.changes.map(async (change) => ({
2416
3206
  ...change,
2417
- exists: await stat(join12(options.cwd, change.path)).then(
3207
+ exists: await stat(join13(options.cwd, change.path)).then(
2418
3208
  () => true,
2419
3209
  () => false
2420
3210
  )
@@ -2442,14 +3232,14 @@ async function runClassify(options, deps) {
2442
3232
  // src/run.ts
2443
3233
  import { existsSync as existsSync5 } from "fs";
2444
3234
  import { createRequire } from "module";
2445
- import { basename as basename3, dirname as dirname8, join as join13, relative as relative3 } from "path";
3235
+ import { basename as basename3, dirname as dirname9, join as join14, relative as relative4 } from "path";
2446
3236
  import { fileURLToPath } from "url";
2447
3237
  function findShellDir() {
2448
- const bundled = join13(dirname8(fileURLToPath(import.meta.url)), "shell");
2449
- if (existsSync5(join13(bundled, "index.html"))) return bundled;
3238
+ const bundled = join14(dirname9(fileURLToPath(import.meta.url)), "shell");
3239
+ if (existsSync5(join14(bundled, "index.html"))) return bundled;
2450
3240
  try {
2451
3241
  const require2 = createRequire(import.meta.url);
2452
- return dirname8(require2.resolve("@leglas/shell/dist/index.html"));
3242
+ return dirname9(require2.resolve("@leglas/shell/dist/index.html"));
2453
3243
  } catch {
2454
3244
  return null;
2455
3245
  }
@@ -2483,7 +3273,7 @@ async function run2(options, deps) {
2483
3273
  const fileMounts = /* @__PURE__ */ new Map();
2484
3274
  for (const preview of merged?.previews ?? []) {
2485
3275
  if (preview.file !== void 0) {
2486
- const absolute = join13(options.cwd, preview.file);
3276
+ const absolute = join14(options.cwd, preview.file);
2487
3277
  if (!existsSync5(absolute)) {
2488
3278
  worktreeErrors.push(
2489
3279
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
@@ -2494,7 +3284,7 @@ async function run2(options, deps) {
2494
3284
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
2495
3285
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
2496
3286
  }
2497
- fileMounts.set(slug, dirname8(absolute));
3287
+ fileMounts.set(slug, dirname9(absolute));
2498
3288
  previews.push({
2499
3289
  ...preview,
2500
3290
  url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
@@ -2555,7 +3345,7 @@ async function run2(options, deps) {
2555
3345
  })
2556
3346
  );
2557
3347
  } else {
2558
- const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative3(options.cwd, loaded.path) || loaded.path;
3348
+ const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative4(options.cwd, loaded.path) || loaded.path;
2559
3349
  deps.log(`Leglas ${url}`);
2560
3350
  deps.log(
2561
3351
  `app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
@@ -2594,6 +3384,8 @@ async function run2(options, deps) {
2594
3384
  export {
2595
3385
  AGENTS_MARKER_END,
2596
3386
  AGENTS_MARKER_START,
3387
+ DEFAULT_PORT,
3388
+ LEGLAS_PREFIX,
2597
3389
  PROMPT_TOKEN,
2598
3390
  WATCH_PATH,
2599
3391
  baselineFrom,