@limno/core 0.1.9 → 0.1.11
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.cjs +279 -71
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +271 -63
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -59,8 +59,8 @@ function jsxName(name) {
|
|
|
59
59
|
return `${jsxName(name.object)}.${name.property.name}`;
|
|
60
60
|
}
|
|
61
61
|
var isCapitalized = (s) => /^[A-Z]/.test(s);
|
|
62
|
-
function componentName(
|
|
63
|
-
let p =
|
|
62
|
+
function componentName(path6, fileBase) {
|
|
63
|
+
let p = path6.getFunctionParent();
|
|
64
64
|
while (p) {
|
|
65
65
|
const node = p.node;
|
|
66
66
|
let name;
|
|
@@ -102,8 +102,8 @@ function stampSource(code, id, relFile) {
|
|
|
102
102
|
const fileBase = relFile.split("/").pop().replace(/\.[jt]sx?$/, "").replace(/^./, (c) => c.toUpperCase());
|
|
103
103
|
let touched = false;
|
|
104
104
|
traverse(ast, {
|
|
105
|
-
JSXOpeningElement(
|
|
106
|
-
const node =
|
|
105
|
+
JSXOpeningElement(path6) {
|
|
106
|
+
const node = path6.node;
|
|
107
107
|
const loc = node.loc;
|
|
108
108
|
if (!loc || node.name.end == null) return;
|
|
109
109
|
const name = jsxName(node.name);
|
|
@@ -111,7 +111,7 @@ function stampSource(code, id, relFile) {
|
|
|
111
111
|
const host = node.name.type === "JSXIdentifier" && !isCapitalized(name);
|
|
112
112
|
if (host) {
|
|
113
113
|
if (hasAttr(node, "data-limno-src")) return;
|
|
114
|
-
const comp = componentName(
|
|
114
|
+
const comp = componentName(path6, fileBase);
|
|
115
115
|
s.appendLeft(node.name.end, ` data-limno-src="${escapeAttr(where)}"${comp ? ` data-limno-comp="${escapeAttr(comp)}"` : ""}`);
|
|
116
116
|
touched = true;
|
|
117
117
|
} else {
|
|
@@ -223,16 +223,101 @@ async function stampTemplate(code, id, relFile, root) {
|
|
|
223
223
|
|
|
224
224
|
// src/service.ts
|
|
225
225
|
var import_node_crypto = require("crypto");
|
|
226
|
-
var
|
|
226
|
+
var import_node_fs4 = require("fs");
|
|
227
227
|
var import_node_module3 = require("module");
|
|
228
|
-
var
|
|
229
|
-
var
|
|
228
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
229
|
+
var import_spec2 = require("@limno/spec");
|
|
230
230
|
|
|
231
231
|
// src/runner.ts
|
|
232
|
+
var import_node_child_process2 = require("child_process");
|
|
233
|
+
var import_node_fs2 = require("fs");
|
|
234
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
235
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
236
|
+
var import_spec = require("@limno/spec");
|
|
237
|
+
|
|
238
|
+
// src/history.ts
|
|
232
239
|
var import_node_child_process = require("child_process");
|
|
233
240
|
var import_node_fs = require("fs");
|
|
234
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
235
241
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
242
|
+
var KEEP = 20;
|
|
243
|
+
function runsFile(folder) {
|
|
244
|
+
return import_node_path2.default.join(folder, "runs.json");
|
|
245
|
+
}
|
|
246
|
+
function readRuns(folder) {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse((0, import_node_fs.readFileSync)(runsFile(folder), "utf8")).runs ?? [];
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function writeRuns(folder, runs) {
|
|
254
|
+
(0, import_node_fs.mkdirSync)(folder, { recursive: true });
|
|
255
|
+
const file = runsFile(folder);
|
|
256
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
257
|
+
(0, import_node_fs.writeFileSync)(tmp, `${JSON.stringify({ runs: runs.slice(-KEEP) }, null, 2)}
|
|
258
|
+
`);
|
|
259
|
+
(0, import_node_fs.renameSync)(tmp, file);
|
|
260
|
+
}
|
|
261
|
+
function saveRun(folder, run2) {
|
|
262
|
+
const runs = readRuns(folder).filter((r) => r.id !== run2.id);
|
|
263
|
+
writeRuns(folder, [...runs, run2]);
|
|
264
|
+
}
|
|
265
|
+
function git(root, args, timeoutMs = 15e3) {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
let out = "";
|
|
268
|
+
let child;
|
|
269
|
+
try {
|
|
270
|
+
child = (0, import_node_child_process.spawn)("git", args, { cwd: root, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, stdio: ["ignore", "pipe", "pipe"] });
|
|
271
|
+
} catch {
|
|
272
|
+
return resolve({ code: null, out: "" });
|
|
273
|
+
}
|
|
274
|
+
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
275
|
+
child.stdout?.on("data", (d) => out += d);
|
|
276
|
+
child.stderr?.on("data", (d) => out += d);
|
|
277
|
+
child.on("error", () => {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
resolve({ code: null, out });
|
|
280
|
+
});
|
|
281
|
+
child.on("close", (code) => {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
resolve({ code, out });
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async function isGitRepo(root) {
|
|
288
|
+
return (await git(root, ["rev-parse", "--is-inside-work-tree"])).out.trim() === "true";
|
|
289
|
+
}
|
|
290
|
+
async function uncommitted(root, files) {
|
|
291
|
+
if (!files.length || !await isGitRepo(root)) return [];
|
|
292
|
+
const r = await git(root, ["status", "--porcelain", "--untracked-files=all", "--", ...files]);
|
|
293
|
+
if (r.code !== 0) return [];
|
|
294
|
+
const dirty = new Set(
|
|
295
|
+
r.out.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop())
|
|
296
|
+
);
|
|
297
|
+
const top = (await git(root, ["rev-parse", "--show-toplevel"])).out.trim();
|
|
298
|
+
return files.filter((f) => dirty.has(top ? import_node_path2.default.relative(top, import_node_path2.default.join(root, f)) : f) || dirty.has(f));
|
|
299
|
+
}
|
|
300
|
+
async function commitFiles(root, files, message) {
|
|
301
|
+
if (!await isGitRepo(root)) return { ok: false, error: "This project isn\u2019t a git repository." };
|
|
302
|
+
for (const marker of ["MERGE_HEAD", "rebase-merge", "rebase-apply", "CHERRY_PICK_HEAD"]) {
|
|
303
|
+
const p = (await git(root, ["rev-parse", "--git-path", marker])).out.trim();
|
|
304
|
+
if (p && (0, import_node_fs.existsSync)(import_node_path2.default.resolve(root, p))) return { ok: false, error: "Git is in the middle of a merge or rebase." };
|
|
305
|
+
}
|
|
306
|
+
const dirty = await uncommitted(root, files);
|
|
307
|
+
if (!dirty.length) return { ok: false, error: "Nothing to commit: those files have no changes." };
|
|
308
|
+
const add = await git(root, ["add", "--", ...dirty]);
|
|
309
|
+
if (add.code !== 0) return { ok: false, error: add.out.trim().split("\n").pop() || "git add failed" };
|
|
310
|
+
const commit = await git(root, ["commit", "-m", message, "--", ...dirty], 6e4);
|
|
311
|
+
if (commit.code !== 0) return { ok: false, error: commit.out.trim().split("\n").slice(-2).join(" ") || "git commit failed" };
|
|
312
|
+
const sha = (await git(root, ["rev-parse", "--short", "HEAD"])).out.trim();
|
|
313
|
+
return { ok: true, sha };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/runner.ts
|
|
317
|
+
var fullMessage = (text) => {
|
|
318
|
+
const t = String(text).trim().replace(/\n{3,}/g, "\n\n");
|
|
319
|
+
return t.length > 4e3 ? `${t.slice(0, 3999).trimEnd()}\u2026` : t;
|
|
320
|
+
};
|
|
236
321
|
var NAMES = { claude: "Claude Code", codex: "Codex" };
|
|
237
322
|
var SIGN_IN = { claude: "claude auth login", codex: "codex login" };
|
|
238
323
|
var PROMPT = [
|
|
@@ -240,26 +325,28 @@ var PROMPT = [
|
|
|
240
325
|
"Call limno_get_changes to read the visual changes and comments waiting for you, then implement them in this project's source by following the rules it gives you.",
|
|
241
326
|
"Report progress with limno_update_status, and answer comments with limno_reply.",
|
|
242
327
|
"Only read and edit files inside this project. Don't run shell commands; Limno checks the result on the page itself.",
|
|
243
|
-
"Finish with
|
|
328
|
+
"Finish with a summary the designer reads in Limno, at most two short sentences and 45 words in total:",
|
|
329
|
+
"first what you changed (or why you changed nothing); then, only if needed, what they must know or decide, as a direct question when it is one.",
|
|
330
|
+
"No file paths, no lists, nothing they already know from the change itself."
|
|
244
331
|
].join(" ");
|
|
245
|
-
var trustFile = () =>
|
|
332
|
+
var trustFile = () => import_node_path3.default.join(import_node_os.default.homedir(), ".limno", "trust.json");
|
|
246
333
|
function projectKey(root) {
|
|
247
334
|
try {
|
|
248
|
-
return (0,
|
|
335
|
+
return (0, import_node_fs2.realpathSync)(root);
|
|
249
336
|
} catch {
|
|
250
337
|
return root;
|
|
251
338
|
}
|
|
252
339
|
}
|
|
253
340
|
function readTrust() {
|
|
254
341
|
try {
|
|
255
|
-
return JSON.parse((0,
|
|
342
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(trustFile(), "utf8")).projects ?? {};
|
|
256
343
|
} catch {
|
|
257
344
|
return {};
|
|
258
345
|
}
|
|
259
346
|
}
|
|
260
347
|
function writeTrust(projects) {
|
|
261
|
-
(0,
|
|
262
|
-
(0,
|
|
348
|
+
(0, import_node_fs2.mkdirSync)(import_node_path3.default.dirname(trustFile()), { recursive: true });
|
|
349
|
+
(0, import_node_fs2.writeFileSync)(trustFile(), `${JSON.stringify({ projects }, null, 2)}
|
|
263
350
|
`);
|
|
264
351
|
}
|
|
265
352
|
function run(cmd, args, timeoutMs = 8e3) {
|
|
@@ -267,7 +354,7 @@ function run(cmd, args, timeoutMs = 8e3) {
|
|
|
267
354
|
let out = "";
|
|
268
355
|
let child;
|
|
269
356
|
try {
|
|
270
|
-
child = (0,
|
|
357
|
+
child = (0, import_node_child_process2.spawn)(cmd, args, { env: agentEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
271
358
|
} catch {
|
|
272
359
|
return resolve({ code: null, out: "" });
|
|
273
360
|
}
|
|
@@ -320,20 +407,20 @@ function isLocalRequest(req) {
|
|
|
320
407
|
return true;
|
|
321
408
|
}
|
|
322
409
|
function createRunner(root, folder, onState) {
|
|
323
|
-
const sessionsFile =
|
|
410
|
+
const sessionsFile = import_node_path3.default.join(folder, "runner.json");
|
|
324
411
|
let child = null;
|
|
325
412
|
let again = false;
|
|
326
413
|
let state = { status: "idle", agent: null, startedAt: null, endedAt: null, log: [], files: [] };
|
|
327
414
|
const sessions = () => {
|
|
328
415
|
try {
|
|
329
|
-
return JSON.parse((0,
|
|
416
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(sessionsFile, "utf8"));
|
|
330
417
|
} catch {
|
|
331
418
|
return {};
|
|
332
419
|
}
|
|
333
420
|
};
|
|
334
421
|
const saveSession = (agent, id) => {
|
|
335
|
-
(0,
|
|
336
|
-
(0,
|
|
422
|
+
(0, import_node_fs2.mkdirSync)(folder, { recursive: true });
|
|
423
|
+
(0, import_node_fs2.writeFileSync)(sessionsFile, `${JSON.stringify({ ...sessions(), [agent]: id }, null, 2)}
|
|
337
424
|
`);
|
|
338
425
|
};
|
|
339
426
|
let emitTimer;
|
|
@@ -351,15 +438,108 @@ function createRunner(root, folder, onState) {
|
|
|
351
438
|
};
|
|
352
439
|
const rel = (p) => {
|
|
353
440
|
if (!p) return "";
|
|
354
|
-
const abs =
|
|
355
|
-
return
|
|
441
|
+
const abs = import_node_path3.default.isAbsolute(p) ? p : import_node_path3.default.join(root, p);
|
|
442
|
+
return import_node_path3.default.relative(root, abs) || p;
|
|
356
443
|
};
|
|
357
444
|
const key = projectKey(root);
|
|
358
445
|
const allowed = () => readTrust()[key]?.agent ?? null;
|
|
446
|
+
const autoCommitOn = () => !!readTrust()[key]?.autoCommit;
|
|
447
|
+
const changeSetFile = import_node_path3.default.join(folder, "changeset.json");
|
|
448
|
+
const readSet = () => {
|
|
449
|
+
try {
|
|
450
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(changeSetFile, "utf8"));
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
let run2 = null;
|
|
456
|
+
let committing = false;
|
|
457
|
+
const say = (line) => console.log(`\x1B[36m[limno]\x1B[0m ${line}`);
|
|
458
|
+
const list = (files) => files.length <= 3 ? files.join(", ") : `${files.slice(0, 3).join(", ")} and ${files.length - 3} more`;
|
|
459
|
+
async function finishRun(agent) {
|
|
460
|
+
const startedAt = state.startedAt;
|
|
461
|
+
const files = state.files;
|
|
462
|
+
const set = readSet();
|
|
463
|
+
const ids = run2?.changeIds ?? [];
|
|
464
|
+
const changes = (set?.changes ?? []).filter((c) => ids.includes(c.id));
|
|
465
|
+
const rec = {
|
|
466
|
+
id: run2?.id ?? String(startedAt),
|
|
467
|
+
agent: NAMES[agent],
|
|
468
|
+
startedAt: startedAt ?? Date.now(),
|
|
469
|
+
endedAt: state.endedAt ?? Date.now(),
|
|
470
|
+
status: state.status === "done" ? "done" : state.status === "stopped" ? "stopped" : "failed",
|
|
471
|
+
files,
|
|
472
|
+
changeIds: ids,
|
|
473
|
+
summary: [...state.log].reverse().find((l) => l.kind === "message")?.text,
|
|
474
|
+
message: (0, import_spec.commitMessage)(changes)
|
|
475
|
+
};
|
|
476
|
+
run2 = rec;
|
|
477
|
+
try {
|
|
478
|
+
saveRun(folder, rec);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
if (!files.length) {
|
|
482
|
+
if (rec.status === "done") say(`${NAMES[agent]} finished without editing any files.`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const dirty = await uncommitted(root, files);
|
|
486
|
+
const git2 = dirty.length > 0 || await isGitRepo(root);
|
|
487
|
+
if (state.startedAt === startedAt) {
|
|
488
|
+
state.commit = { message: rec.message, uncommitted: dirty, git: git2 };
|
|
489
|
+
emit(true);
|
|
490
|
+
}
|
|
491
|
+
const n = files.length;
|
|
492
|
+
say(`${NAMES[agent]} edited ${n} ${n === 1 ? "file" : "files"}: ${list(files)}${dirty.length ? " (not committed yet)" : ""}`);
|
|
493
|
+
if (dirty.length) {
|
|
494
|
+
say(`Commit message: ${rec.message.split("\n")[0]}`);
|
|
495
|
+
if (autoCommitOn()) say("Auto-commit is on: Limno commits these files once every change is verified on the page.");
|
|
496
|
+
}
|
|
497
|
+
if (state.startedAt === startedAt) void changeSetChanged(set);
|
|
498
|
+
}
|
|
499
|
+
async function changeSetChanged(set) {
|
|
500
|
+
const commit = state.commit;
|
|
501
|
+
if (!set || !run2 || committing || !commit || commit.committed || !commit.uncommitted.length || !autoCommitOn()) return;
|
|
502
|
+
if (state.status !== "done") return;
|
|
503
|
+
const sent = set.changes.filter((c) => run2.changeIds.includes(c.id) && c.kind !== "note");
|
|
504
|
+
if (!sent.length) return;
|
|
505
|
+
if (sent.some((c) => c.status === "differs" || c.status === "failed")) {
|
|
506
|
+
if (!commit.note) {
|
|
507
|
+
commit.note = "A change doesn\u2019t match the preview yet.";
|
|
508
|
+
const off = (0, import_spec.describeChanges)(sent.filter((c) => c.status === "differs" || c.status === "failed")).length || 1;
|
|
509
|
+
say(`Not auto-committing: ${off === 1 ? "1 change doesn\u2019t" : `${off} changes don\u2019t`} match the preview yet. Review in Limno.`);
|
|
510
|
+
emit(true);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!sent.every((c) => c.status === "verified")) return;
|
|
515
|
+
committing = true;
|
|
516
|
+
try {
|
|
517
|
+
const r = await commitFiles(root, state.files, run2.message);
|
|
518
|
+
if (r.ok) {
|
|
519
|
+
commit.committed = { sha: r.sha, auto: true };
|
|
520
|
+
commit.uncommitted = [];
|
|
521
|
+
commit.note = void 0;
|
|
522
|
+
run2.commit = { sha: r.sha, at: Date.now(), auto: true };
|
|
523
|
+
try {
|
|
524
|
+
saveRun(folder, run2);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
say(`Committed ${state.files.length === 1 ? "1 file" : `${state.files.length} files`} as ${r.sha}: ${run2.message.split("\n")[0]} (not pushed)`);
|
|
528
|
+
} else {
|
|
529
|
+
commit.note = `Not committed: ${r.error}`;
|
|
530
|
+
say(`Didn\u2019t auto-commit: ${r.error}`);
|
|
531
|
+
}
|
|
532
|
+
emit(true);
|
|
533
|
+
} finally {
|
|
534
|
+
committing = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
359
537
|
function limnoLine(tool, input = {}) {
|
|
360
538
|
if (tool.endsWith("get_changes")) return "Reading your changes";
|
|
361
539
|
if (tool.endsWith("update_status")) {
|
|
362
|
-
const
|
|
540
|
+
const ids = Array.isArray(input.ids) ? input.ids : [];
|
|
541
|
+
const sent = (readSet()?.changes ?? []).filter((c) => ids.includes(c.id));
|
|
542
|
+
const n = sent.length ? (0, import_spec.describeChanges)(sent).length : ids.length;
|
|
363
543
|
const status = String(input.status ?? "");
|
|
364
544
|
return status === "implemented" ? `Marked ${n || ""} ${n === 1 ? "change" : "changes"} done`.replace(" ", " ") : status === "failed" ? "Reported a change it couldn\u2019t make" : "Working on your changes";
|
|
365
545
|
}
|
|
@@ -383,7 +563,7 @@ function createRunner(root, folder, onState) {
|
|
|
383
563
|
else if (name === "Edit" || name === "MultiEdit" || name === "Write") log({ kind: "edit", text: `Editing ${rel(input.file_path)}`, file: rel(input.file_path) });
|
|
384
564
|
else if (name === "Glob" || name === "Grep") log({ kind: "search", text: "Searching the project" });
|
|
385
565
|
} else if (block.type === "text" && String(block.text).trim()) {
|
|
386
|
-
log({ kind: "message", text:
|
|
566
|
+
log({ kind: "message", text: fullMessage(block.text) });
|
|
387
567
|
}
|
|
388
568
|
}
|
|
389
569
|
return;
|
|
@@ -404,7 +584,7 @@ function createRunner(root, folder, onState) {
|
|
|
404
584
|
else if (item.type === "file_change" && e.type === "item.completed") {
|
|
405
585
|
for (const c of item.changes ?? []) log({ kind: "edit", text: `Editing ${rel(c.path)}`, file: rel(c.path) });
|
|
406
586
|
} else if (item.type === "agent_message" && e.type === "item.completed" && item.text) {
|
|
407
|
-
log({ kind: "message", text:
|
|
587
|
+
log({ kind: "message", text: fullMessage(item.text) });
|
|
408
588
|
} else if (item.type === "command_execution" && e.type === "item.started") {
|
|
409
589
|
log({ kind: "search", text: "Looking through the project" });
|
|
410
590
|
}
|
|
@@ -451,7 +631,7 @@ function createRunner(root, folder, onState) {
|
|
|
451
631
|
return resume ? ["exec", ...opts, "resume", resume, PROMPT] : ["exec", ...opts, PROMPT];
|
|
452
632
|
}
|
|
453
633
|
function launch(agent, resume) {
|
|
454
|
-
const proc = (0,
|
|
634
|
+
const proc = (0, import_node_child_process2.spawn)(agent, args(agent, resume), { cwd: root, env: agentEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
455
635
|
child = proc;
|
|
456
636
|
let buf = "";
|
|
457
637
|
let stderr = "";
|
|
@@ -486,7 +666,11 @@ function createRunner(root, folder, onState) {
|
|
|
486
666
|
launch(agent);
|
|
487
667
|
return;
|
|
488
668
|
}
|
|
489
|
-
if (state.status === "stopped")
|
|
669
|
+
if (state.status === "stopped") {
|
|
670
|
+
emit(true);
|
|
671
|
+
void finishRun(agent);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
490
674
|
const failed = code !== 0 || !!state.error;
|
|
491
675
|
const tail = stderr.trim().split("\n").filter(Boolean).slice(-2).join(" ").slice(0, 300);
|
|
492
676
|
state = {
|
|
@@ -496,6 +680,7 @@ function createRunner(root, folder, onState) {
|
|
|
496
680
|
error: failed ? state.error ?? (tail || `${NAMES[agent]} exited${signal ? ` (${signal})` : ` with code ${code}`}`) : void 0
|
|
497
681
|
};
|
|
498
682
|
emit(true);
|
|
683
|
+
void finishRun(agent);
|
|
499
684
|
if (again && !failed) {
|
|
500
685
|
again = false;
|
|
501
686
|
void start();
|
|
@@ -514,16 +699,32 @@ function createRunner(root, folder, onState) {
|
|
|
514
699
|
if (!info?.installed) return { ok: false, error: `${NAMES[agent]} isn\u2019t installed on this computer.` };
|
|
515
700
|
if (!info.signedIn) return { ok: false, error: `${NAMES[agent]} isn\u2019t signed in. Run \u201C${SIGN_IN[agent]}\u201D in a terminal, then send again.` };
|
|
516
701
|
state = { status: "starting", agent, startedAt: Date.now(), endedAt: null, log: [], files: [] };
|
|
702
|
+
const queued = (readSet()?.changes ?? []).filter((c) => c.enabled && ["queued", "in_progress", "differs", "failed"].includes(c.status)).map((c) => c.id);
|
|
703
|
+
run2 = { id: String(state.startedAt), agent: NAMES[agent], startedAt: state.startedAt, endedAt: 0, status: "done", files: [], changeIds: queued, message: "" };
|
|
517
704
|
emit(true);
|
|
518
705
|
launch(agent, sessions()[agent]);
|
|
519
706
|
return { ok: true };
|
|
520
707
|
}
|
|
521
708
|
return {
|
|
522
709
|
async info(force) {
|
|
523
|
-
|
|
710
|
+
if (state.commit && !state.commit.committed && state.commit.uncommitted.length && !child) {
|
|
711
|
+
state.commit = { ...state.commit, uncommitted: await uncommitted(root, state.files) };
|
|
712
|
+
}
|
|
713
|
+
return { agents: await detectAgents(force), allowed: allowed(), autoCommit: autoCommitOn(), state };
|
|
714
|
+
},
|
|
715
|
+
setAutoCommit(on) {
|
|
716
|
+
const t = readTrust();
|
|
717
|
+
if (!t[key]) return;
|
|
718
|
+
t[key] = { ...t[key], autoCommit: on };
|
|
719
|
+
writeTrust(t);
|
|
720
|
+
if (on) void changeSetChanged(readSet());
|
|
721
|
+
},
|
|
722
|
+
changeSetChanged(set) {
|
|
723
|
+
void changeSetChanged(set);
|
|
524
724
|
},
|
|
525
725
|
allow(agent) {
|
|
526
|
-
|
|
726
|
+
const prev = readTrust()[key];
|
|
727
|
+
writeTrust({ ...readTrust(), [key]: { agent, at: Date.now(), autoCommit: prev?.autoCommit } });
|
|
527
728
|
},
|
|
528
729
|
forget() {
|
|
529
730
|
const t = readTrust();
|
|
@@ -547,11 +748,11 @@ function createRunner(root, folder, onState) {
|
|
|
547
748
|
}
|
|
548
749
|
|
|
549
750
|
// src/update.ts
|
|
550
|
-
var
|
|
551
|
-
var
|
|
751
|
+
var import_node_child_process3 = require("child_process");
|
|
752
|
+
var import_node_fs3 = require("fs");
|
|
552
753
|
var import_node_module2 = require("module");
|
|
553
754
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
554
|
-
var
|
|
755
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
555
756
|
var require2 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
556
757
|
var TTL = 60 * 60 * 1e3;
|
|
557
758
|
function currentVersion() {
|
|
@@ -563,7 +764,7 @@ function currentVersion() {
|
|
|
563
764
|
}
|
|
564
765
|
function installedVersion(fallback) {
|
|
565
766
|
try {
|
|
566
|
-
return JSON.parse((0,
|
|
767
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(require2.resolve("../package.json"), "utf8")).version;
|
|
567
768
|
} catch {
|
|
568
769
|
return fallback;
|
|
569
770
|
}
|
|
@@ -577,18 +778,18 @@ var newer = (a, b) => {
|
|
|
577
778
|
function packageManager(root) {
|
|
578
779
|
let dir = root;
|
|
579
780
|
for (; ; ) {
|
|
580
|
-
if ((0,
|
|
581
|
-
if ((0,
|
|
582
|
-
if ((0,
|
|
583
|
-
if ((0,
|
|
584
|
-
const up =
|
|
781
|
+
if ((0, import_node_fs3.existsSync)(import_node_path4.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
782
|
+
if ((0, import_node_fs3.existsSync)(import_node_path4.default.join(dir, "yarn.lock"))) return "yarn";
|
|
783
|
+
if ((0, import_node_fs3.existsSync)(import_node_path4.default.join(dir, "bun.lockb")) || (0, import_node_fs3.existsSync)(import_node_path4.default.join(dir, "bun.lock"))) return "bun";
|
|
784
|
+
if ((0, import_node_fs3.existsSync)(import_node_path4.default.join(dir, "package-lock.json"))) return "npm";
|
|
785
|
+
const up = import_node_path4.default.dirname(dir);
|
|
585
786
|
if (up === dir) return "npm";
|
|
586
787
|
dir = up;
|
|
587
788
|
}
|
|
588
789
|
}
|
|
589
790
|
function listedPackages(root) {
|
|
590
791
|
try {
|
|
591
|
-
const pkg = JSON.parse((0,
|
|
792
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(import_node_path4.default.join(root, "package.json"), "utf8"));
|
|
592
793
|
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).filter((n) => n.startsWith("@limno/") && n !== "@limno/cli");
|
|
593
794
|
} catch {
|
|
594
795
|
return [];
|
|
@@ -601,10 +802,10 @@ function installArgs(pm, pkgs) {
|
|
|
601
802
|
if (pm === "bun") return ["bun", ["add", "-d", ...at]];
|
|
602
803
|
return ["npm", ["install", "-D", ...at]];
|
|
603
804
|
}
|
|
604
|
-
var cacheFile = () =>
|
|
805
|
+
var cacheFile = () => import_node_path4.default.join(import_node_os2.default.homedir(), ".limno", "update-check.json");
|
|
605
806
|
async function latestVersion() {
|
|
606
807
|
try {
|
|
607
|
-
const cached = JSON.parse((0,
|
|
808
|
+
const cached = JSON.parse((0, import_node_fs3.readFileSync)(cacheFile(), "utf8"));
|
|
608
809
|
if (Date.now() - cached.at < TTL) return cached.latest;
|
|
609
810
|
} catch {
|
|
610
811
|
}
|
|
@@ -613,8 +814,8 @@ async function latestVersion() {
|
|
|
613
814
|
if (!res.ok) return null;
|
|
614
815
|
const latest = (await res.json()).latest ?? null;
|
|
615
816
|
if (latest) {
|
|
616
|
-
(0,
|
|
617
|
-
(0,
|
|
817
|
+
(0, import_node_fs3.mkdirSync)(import_node_path4.default.dirname(cacheFile()), { recursive: true });
|
|
818
|
+
(0, import_node_fs3.writeFileSync)(cacheFile(), JSON.stringify({ at: Date.now(), latest }));
|
|
618
819
|
}
|
|
619
820
|
return latest;
|
|
620
821
|
} catch {
|
|
@@ -641,14 +842,14 @@ function createUpdater(root) {
|
|
|
641
842
|
const [cmd, args] = installArgs(packageManager(root), pkgs);
|
|
642
843
|
installing = new Promise((resolve) => {
|
|
643
844
|
let output = "";
|
|
644
|
-
const child = (0,
|
|
845
|
+
const child = (0, import_node_child_process3.spawn)(cmd, args, { cwd: root, env: process.env, shell: process.platform === "win32" });
|
|
645
846
|
child.stdout?.on("data", (d) => output += d);
|
|
646
847
|
child.stderr?.on("data", (d) => output += d);
|
|
647
848
|
child.on("error", (e) => resolve({ ok: false, output: String(e) }));
|
|
648
849
|
child.on("close", (code) => {
|
|
649
850
|
if (code === 0) {
|
|
650
851
|
try {
|
|
651
|
-
(0,
|
|
852
|
+
(0, import_node_fs3.writeFileSync)(cacheFile(), JSON.stringify({ at: 0, latest: "" }));
|
|
652
853
|
} catch {
|
|
653
854
|
}
|
|
654
855
|
}
|
|
@@ -668,16 +869,16 @@ var require3 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
|
668
869
|
var STALE_MS = 3e4;
|
|
669
870
|
function readJson(file) {
|
|
670
871
|
try {
|
|
671
|
-
return JSON.parse((0,
|
|
872
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
|
|
672
873
|
} catch {
|
|
673
874
|
return null;
|
|
674
875
|
}
|
|
675
876
|
}
|
|
676
877
|
function writeAtomic(file, data) {
|
|
677
|
-
(0,
|
|
878
|
+
(0, import_node_fs4.mkdirSync)(import_node_path5.default.dirname(file), { recursive: true });
|
|
678
879
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
679
|
-
(0,
|
|
680
|
-
(0,
|
|
880
|
+
(0, import_node_fs4.writeFileSync)(tmp, data);
|
|
881
|
+
(0, import_node_fs4.renameSync)(tmp, file);
|
|
681
882
|
}
|
|
682
883
|
function body(req) {
|
|
683
884
|
return new Promise((resolve, reject) => {
|
|
@@ -698,9 +899,9 @@ function overlayFile() {
|
|
|
698
899
|
return require3.resolve("@limno/overlay/dist/overlay.js");
|
|
699
900
|
}
|
|
700
901
|
function createLimnoService({ root, dir = ".limno" }) {
|
|
701
|
-
const folder =
|
|
702
|
-
const file =
|
|
703
|
-
const presenceFile =
|
|
902
|
+
const folder = import_node_path5.default.join(root, dir);
|
|
903
|
+
const file = import_node_path5.default.join(folder, "changeset.json");
|
|
904
|
+
const presenceFile = import_node_path5.default.join(folder, "agent.json");
|
|
704
905
|
let lastWrittenHash = "";
|
|
705
906
|
const clients = /* @__PURE__ */ new Set();
|
|
706
907
|
function broadcast(event, data) {
|
|
@@ -729,23 +930,23 @@ data: ${JSON.stringify(data)}
|
|
|
729
930
|
return { connected, client: connected ? presence?.client ?? null : null, lastSeen: presence?.lastSeen ?? null, setup: setupInfo() };
|
|
730
931
|
}
|
|
731
932
|
function installConfig(target) {
|
|
732
|
-
const dest = target === "claude" ?
|
|
933
|
+
const dest = target === "claude" ? import_node_path5.default.join(root, ".mcp.json") : import_node_path5.default.join(root, ".cursor", "mcp.json");
|
|
733
934
|
let config = {};
|
|
734
|
-
if ((0,
|
|
935
|
+
if ((0, import_node_fs4.existsSync)(dest)) {
|
|
735
936
|
try {
|
|
736
|
-
config = JSON.parse((0,
|
|
937
|
+
config = JSON.parse((0, import_node_fs4.readFileSync)(dest, "utf8"));
|
|
737
938
|
} catch {
|
|
738
|
-
throw new Error(`${
|
|
939
|
+
throw new Error(`${import_node_path5.default.relative(root, dest)} isn't valid JSON; fix or remove it first.`);
|
|
739
940
|
}
|
|
740
941
|
}
|
|
741
942
|
const { command, args } = setupInfo();
|
|
742
943
|
const portable = args.map((a, i) => {
|
|
743
944
|
if (args[i - 1] === "--root") return ".";
|
|
744
|
-
return
|
|
945
|
+
return import_node_path5.default.isAbsolute(a) ? import_node_path5.default.relative(root, a).split(import_node_path5.default.sep).join("/") : a;
|
|
745
946
|
});
|
|
746
947
|
config.mcpServers = { ...config.mcpServers ?? {}, limno: { command, args: portable } };
|
|
747
948
|
writeAtomic(dest, JSON.stringify(config, null, 2) + "\n");
|
|
748
|
-
return
|
|
949
|
+
return import_node_path5.default.relative(root, dest);
|
|
749
950
|
}
|
|
750
951
|
let lastAgent = "";
|
|
751
952
|
function pushAgent() {
|
|
@@ -756,12 +957,13 @@ data: ${JSON.stringify(data)}
|
|
|
756
957
|
broadcast("agent", status);
|
|
757
958
|
}
|
|
758
959
|
function onChangeSetFile() {
|
|
759
|
-
const raw = (0,
|
|
960
|
+
const raw = (0, import_node_fs4.existsSync)(file) ? (0, import_node_fs4.readFileSync)(file, "utf8") : "";
|
|
760
961
|
if (!raw || hash(raw) === lastWrittenHash) return;
|
|
761
962
|
const set = readJson(file);
|
|
762
963
|
if (set) broadcast("changeset", set);
|
|
964
|
+
runner.changeSetChanged(set);
|
|
763
965
|
}
|
|
764
|
-
(0,
|
|
966
|
+
(0, import_node_fs4.mkdirSync)(folder, { recursive: true });
|
|
765
967
|
let watcher = null;
|
|
766
968
|
let watchedIno = -1;
|
|
767
969
|
let debounce;
|
|
@@ -769,8 +971,8 @@ data: ${JSON.stringify(data)}
|
|
|
769
971
|
watcher?.close();
|
|
770
972
|
watcher = null;
|
|
771
973
|
try {
|
|
772
|
-
watchedIno = (0,
|
|
773
|
-
watcher = (0,
|
|
974
|
+
watchedIno = (0, import_node_fs4.statSync)(folder).ino;
|
|
975
|
+
watcher = (0, import_node_fs4.watch)(folder, (_event, name) => {
|
|
774
976
|
if (!name || name.endsWith(".tmp")) return;
|
|
775
977
|
clearTimeout(debounce);
|
|
776
978
|
debounce = setTimeout(() => {
|
|
@@ -786,7 +988,7 @@ data: ${JSON.stringify(data)}
|
|
|
786
988
|
const presenceTimer = setInterval(() => {
|
|
787
989
|
let ino = -1;
|
|
788
990
|
try {
|
|
789
|
-
ino = (0,
|
|
991
|
+
ino = (0, import_node_fs4.statSync)(folder).ino;
|
|
790
992
|
} catch {
|
|
791
993
|
}
|
|
792
994
|
if (ino !== -1 && ino !== watchedIno) {
|
|
@@ -803,8 +1005,8 @@ data: ${JSON.stringify(data)}
|
|
|
803
1005
|
async function openInEditor(spec) {
|
|
804
1006
|
const m = /^(.*?)(?::(\d+))?(?::(\d+))?$/.exec(spec);
|
|
805
1007
|
if (!m) return false;
|
|
806
|
-
const target =
|
|
807
|
-
if (!target.startsWith(root +
|
|
1008
|
+
const target = import_node_path5.default.resolve(root, m[1]);
|
|
1009
|
+
if (!target.startsWith(root + import_node_path5.default.sep) || !(0, import_node_fs4.existsSync)(target)) return false;
|
|
808
1010
|
const { default: launch } = await import("launch-editor");
|
|
809
1011
|
launch(`${target}${m[2] ? `:${m[2]}` : ""}${m[3] ? `:${m[3]}` : ""}`);
|
|
810
1012
|
return true;
|
|
@@ -814,7 +1016,7 @@ data: ${JSON.stringify(data)}
|
|
|
814
1016
|
if (!url.startsWith(ROUTE)) return next ? next() : send(res, 404, { error: "not found" });
|
|
815
1017
|
try {
|
|
816
1018
|
if (url === `${ROUTE}/overlay.js`) {
|
|
817
|
-
return send(res, 200, (0,
|
|
1019
|
+
return send(res, 200, (0, import_node_fs4.readFileSync)(overlayFile(), "utf8"), "text/javascript");
|
|
818
1020
|
}
|
|
819
1021
|
if (url === `${ROUTE}/events`) {
|
|
820
1022
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" });
|
|
@@ -824,20 +1026,21 @@ data: ${JSON.stringify(data)}
|
|
|
824
1026
|
return;
|
|
825
1027
|
}
|
|
826
1028
|
if (url === `${ROUTE}/changeset` && req.method === "GET") {
|
|
827
|
-
return send(res, 200, readJson(file) ?? (0,
|
|
1029
|
+
return send(res, 200, readJson(file) ?? (0, import_spec2.emptyChangeSet)());
|
|
828
1030
|
}
|
|
829
1031
|
if (url === `${ROUTE}/changeset` && req.method === "PUT") {
|
|
830
1032
|
const incoming = JSON.parse(await body(req));
|
|
831
|
-
const merged = (0,
|
|
1033
|
+
const merged = (0, import_spec2.mergeStatuses)(incoming, readJson(file));
|
|
832
1034
|
merged.updatedAt = Date.now();
|
|
833
1035
|
const raw = JSON.stringify(merged, null, 2);
|
|
834
1036
|
lastWrittenHash = hash(raw);
|
|
835
1037
|
writeAtomic(file, raw);
|
|
1038
|
+
runner.changeSetChanged(merged);
|
|
836
1039
|
return send(res, 200, merged);
|
|
837
1040
|
}
|
|
838
1041
|
if (url === `${ROUTE}/prompt` && req.method === "GET") {
|
|
839
|
-
const set = readJson(file) ?? (0,
|
|
840
|
-
return send(res, 200, (0,
|
|
1042
|
+
const set = readJson(file) ?? (0, import_spec2.emptyChangeSet)();
|
|
1043
|
+
return send(res, 200, (0, import_spec2.renderPrompt)(set, { mcp: false, statuses: ["draft", "queued", "in_progress", "differs", "failed"] }), "text/markdown; charset=utf-8");
|
|
841
1044
|
}
|
|
842
1045
|
if (url === `${ROUTE}/agent` && req.method === "GET") {
|
|
843
1046
|
return send(res, 200, agentStatus());
|
|
@@ -874,6 +1077,11 @@ data: ${JSON.stringify(data)}
|
|
|
874
1077
|
runner.forget();
|
|
875
1078
|
return send(res, 200, await runner.info());
|
|
876
1079
|
}
|
|
1080
|
+
if (action === "autocommit") {
|
|
1081
|
+
const { on } = JSON.parse(await body(req));
|
|
1082
|
+
runner.setAutoCommit(!!on);
|
|
1083
|
+
return send(res, 200, await runner.info());
|
|
1084
|
+
}
|
|
877
1085
|
if (action === "run") return send(res, 200, await runner.start());
|
|
878
1086
|
if (action === "stop") {
|
|
879
1087
|
runner.stop();
|
|
@@ -885,7 +1093,7 @@ data: ${JSON.stringify(data)}
|
|
|
885
1093
|
return send(res, await openInEditor(spec) ? 200 : 404, { ok: true });
|
|
886
1094
|
}
|
|
887
1095
|
if (url === `${ROUTE}/info`) {
|
|
888
|
-
return send(res, 200, { name: "limno", root, file:
|
|
1096
|
+
return send(res, 200, { name: "limno", root, file: import_node_path5.default.relative(root, file) });
|
|
889
1097
|
}
|
|
890
1098
|
send(res, 404, { error: "not found" });
|
|
891
1099
|
} catch (err) {
|