@limno/core 0.1.14 → 0.1.15

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 CHANGED
@@ -458,6 +458,31 @@ function createRunner(root, folder, onState) {
458
458
  return null;
459
459
  }
460
460
  };
461
+ const settle = (ids, edited, summary) => {
462
+ const set = readSet();
463
+ if (!set) return;
464
+ const now = Date.now();
465
+ const why = state.status === "stopped" ? "Stopped before it finished." : state.status === "failed" ? state.error ?? `${state.agent ? NAMES[state.agent] : "The agent"} stopped with an error.` : summary ?? "Finished without editing any files.";
466
+ let touched = false;
467
+ for (const c of set.changes) {
468
+ if (!ids.includes(c.id) || c.status !== "in_progress" && c.status !== "queued") continue;
469
+ touched = true;
470
+ c.statusAt = now;
471
+ if (state.status === "done" && edited) c.status = "implemented";
472
+ else {
473
+ c.status = "failed";
474
+ c.agentNote = why;
475
+ }
476
+ }
477
+ if (!touched) return;
478
+ set.updatedAt = now;
479
+ try {
480
+ const tmp = `${changeSetFile}.${process.pid}.tmp`;
481
+ (0, import_node_fs2.writeFileSync)(tmp, JSON.stringify(set, null, 2));
482
+ (0, import_node_fs2.renameSync)(tmp, changeSetFile);
483
+ } catch {
484
+ }
485
+ };
461
486
  let run2 = null;
462
487
  let committing = false;
463
488
  const say = (line) => console.log(`\x1B[36m[limno]\x1B[0m ${line}`);
@@ -480,6 +505,7 @@ function createRunner(root, folder, onState) {
480
505
  message: (0, import_spec.commitMessage)(changes)
481
506
  };
482
507
  run2 = rec;
508
+ settle(ids, files.length > 0, rec.summary);
483
509
  try {
484
510
  saveRun(folder, rec);
485
511
  } catch {
@@ -596,7 +622,7 @@ function createRunner(root, folder, onState) {
596
622
  }
597
623
  }
598
624
  function args(agent, resume) {
599
- const server = { command: "npx", args: ["-y", "@limno/mcp", "--root", root] };
625
+ const server = { command: "npx", args: ["-y", "@limno/mcp", "--root", root, "--background"] };
600
626
  if (agent === "claude") {
601
627
  return [
602
628
  "-p",
@@ -875,6 +901,7 @@ function createUpdater(root) {
875
901
  var ROUTE = "/__limno";
876
902
  var require3 = (0, import_node_module3.createRequire)(importMetaUrl);
877
903
  var STALE_MS = 3e4;
904
+ var isPresenceFile = (name) => /^agent(-\d+)?\.json$/.test(name);
878
905
  function readJson(file) {
879
906
  try {
880
907
  return JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
@@ -909,7 +936,6 @@ function overlayFile() {
909
936
  function createLimnoService({ root, dir = ".limno" }) {
910
937
  const folder = import_node_path5.default.join(root, dir);
911
938
  const file = import_node_path5.default.join(folder, "changeset.json");
912
- const presenceFile = import_node_path5.default.join(folder, "agent.json");
913
939
  let lastWrittenHash = "";
914
940
  const clients = /* @__PURE__ */ new Set();
915
941
  function broadcast(event, data) {
@@ -925,17 +951,29 @@ data: ${JSON.stringify(data)}
925
951
  return { command: "npx", args: ["-y", "@limno/mcp", "--root", root], root };
926
952
  }
927
953
  function agentStatus() {
928
- const presence = readJson(presenceFile);
929
- let alive = !!presence?.pid;
930
- if (alive) {
954
+ let names = [];
955
+ try {
956
+ names = (0, import_node_fs4.readdirSync)(folder).filter(isPresenceFile);
957
+ } catch {
958
+ }
959
+ let best = null;
960
+ for (const name of names) {
961
+ const presence = readJson(import_node_path5.default.join(folder, name));
962
+ if (!presence?.pid) continue;
963
+ let alive = true;
931
964
  try {
932
965
  process.kill(presence.pid, 0);
933
966
  } catch {
934
967
  alive = false;
935
968
  }
969
+ if (!alive) {
970
+ if (name !== "agent.json") (0, import_node_fs4.rmSync)(import_node_path5.default.join(folder, name), { force: true });
971
+ continue;
972
+ }
973
+ if (Date.now() - (presence.lastSeen ?? 0) >= STALE_MS) continue;
974
+ if (!best || (presence.lastSeen ?? 0) > (best.lastSeen ?? 0)) best = presence;
936
975
  }
937
- const connected = alive && Date.now() - (presence?.lastSeen ?? 0) < STALE_MS;
938
- return { connected, client: connected ? presence?.client ?? null : null, lastSeen: presence?.lastSeen ?? null, setup: setupInfo() };
976
+ return { connected: !!best, client: best?.client ?? null, lastSeen: best?.lastSeen ?? null, setup: setupInfo() };
939
977
  }
940
978
  function installConfig(target) {
941
979
  const dest = target === "claude" ? import_node_path5.default.join(root, ".mcp.json") : import_node_path5.default.join(root, ".cursor", "mcp.json");
@@ -989,7 +1027,7 @@ data: ${JSON.stringify(data)}
989
1027
  const names = new Set(pending);
990
1028
  pending.clear();
991
1029
  if (names.has("changeset.json")) onChangeSetFile();
992
- if (names.has("agent.json")) pushAgent();
1030
+ if ([...names].some(isPresenceFile)) pushAgent();
993
1031
  }, 40);
994
1032
  });
995
1033
  watcher.unref?.();
package/dist/index.js CHANGED
@@ -174,14 +174,14 @@ async function stampTemplate(code, id, relFile, root) {
174
174
 
175
175
  // src/service.ts
176
176
  import { createHash } from "crypto";
177
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, statSync, watch, writeFileSync as writeFileSync4 } from "fs";
177
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, renameSync as renameSync3, rmSync, statSync, watch, writeFileSync as writeFileSync4 } from "fs";
178
178
  import { createRequire as createRequire3 } from "module";
179
179
  import path5 from "path";
180
180
  import { emptyChangeSet, mergeStatuses, renderPrompt } from "@limno/spec";
181
181
 
182
182
  // src/runner.ts
183
183
  import { spawn as spawn2 } from "child_process";
184
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
184
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
185
185
  import os from "os";
186
186
  import path3 from "path";
187
187
  import { commitMessage, describeChanges } from "@limno/spec";
@@ -408,6 +408,31 @@ function createRunner(root, folder, onState) {
408
408
  return null;
409
409
  }
410
410
  };
411
+ const settle = (ids, edited, summary) => {
412
+ const set = readSet();
413
+ if (!set) return;
414
+ const now = Date.now();
415
+ const why = state.status === "stopped" ? "Stopped before it finished." : state.status === "failed" ? state.error ?? `${state.agent ? NAMES[state.agent] : "The agent"} stopped with an error.` : summary ?? "Finished without editing any files.";
416
+ let touched = false;
417
+ for (const c of set.changes) {
418
+ if (!ids.includes(c.id) || c.status !== "in_progress" && c.status !== "queued") continue;
419
+ touched = true;
420
+ c.statusAt = now;
421
+ if (state.status === "done" && edited) c.status = "implemented";
422
+ else {
423
+ c.status = "failed";
424
+ c.agentNote = why;
425
+ }
426
+ }
427
+ if (!touched) return;
428
+ set.updatedAt = now;
429
+ try {
430
+ const tmp = `${changeSetFile}.${process.pid}.tmp`;
431
+ writeFileSync2(tmp, JSON.stringify(set, null, 2));
432
+ renameSync2(tmp, changeSetFile);
433
+ } catch {
434
+ }
435
+ };
411
436
  let run2 = null;
412
437
  let committing = false;
413
438
  const say = (line) => console.log(`\x1B[36m[limno]\x1B[0m ${line}`);
@@ -430,6 +455,7 @@ function createRunner(root, folder, onState) {
430
455
  message: commitMessage(changes)
431
456
  };
432
457
  run2 = rec;
458
+ settle(ids, files.length > 0, rec.summary);
433
459
  try {
434
460
  saveRun(folder, rec);
435
461
  } catch {
@@ -546,7 +572,7 @@ function createRunner(root, folder, onState) {
546
572
  }
547
573
  }
548
574
  function args(agent, resume) {
549
- const server = { command: "npx", args: ["-y", "@limno/mcp", "--root", root] };
575
+ const server = { command: "npx", args: ["-y", "@limno/mcp", "--root", root, "--background"] };
550
576
  if (agent === "claude") {
551
577
  return [
552
578
  "-p",
@@ -825,6 +851,7 @@ function createUpdater(root) {
825
851
  var ROUTE = "/__limno";
826
852
  var require3 = createRequire3(import.meta.url);
827
853
  var STALE_MS = 3e4;
854
+ var isPresenceFile = (name) => /^agent(-\d+)?\.json$/.test(name);
828
855
  function readJson(file) {
829
856
  try {
830
857
  return JSON.parse(readFileSync4(file, "utf8"));
@@ -836,7 +863,7 @@ function writeAtomic(file, data) {
836
863
  mkdirSync4(path5.dirname(file), { recursive: true });
837
864
  const tmp = `${file}.${process.pid}.tmp`;
838
865
  writeFileSync4(tmp, data);
839
- renameSync2(tmp, file);
866
+ renameSync3(tmp, file);
840
867
  }
841
868
  function body(req) {
842
869
  return new Promise((resolve, reject) => {
@@ -859,7 +886,6 @@ function overlayFile() {
859
886
  function createLimnoService({ root, dir = ".limno" }) {
860
887
  const folder = path5.join(root, dir);
861
888
  const file = path5.join(folder, "changeset.json");
862
- const presenceFile = path5.join(folder, "agent.json");
863
889
  let lastWrittenHash = "";
864
890
  const clients = /* @__PURE__ */ new Set();
865
891
  function broadcast(event, data) {
@@ -875,17 +901,29 @@ data: ${JSON.stringify(data)}
875
901
  return { command: "npx", args: ["-y", "@limno/mcp", "--root", root], root };
876
902
  }
877
903
  function agentStatus() {
878
- const presence = readJson(presenceFile);
879
- let alive = !!presence?.pid;
880
- if (alive) {
904
+ let names = [];
905
+ try {
906
+ names = readdirSync(folder).filter(isPresenceFile);
907
+ } catch {
908
+ }
909
+ let best = null;
910
+ for (const name of names) {
911
+ const presence = readJson(path5.join(folder, name));
912
+ if (!presence?.pid) continue;
913
+ let alive = true;
881
914
  try {
882
915
  process.kill(presence.pid, 0);
883
916
  } catch {
884
917
  alive = false;
885
918
  }
919
+ if (!alive) {
920
+ if (name !== "agent.json") rmSync(path5.join(folder, name), { force: true });
921
+ continue;
922
+ }
923
+ if (Date.now() - (presence.lastSeen ?? 0) >= STALE_MS) continue;
924
+ if (!best || (presence.lastSeen ?? 0) > (best.lastSeen ?? 0)) best = presence;
886
925
  }
887
- const connected = alive && Date.now() - (presence?.lastSeen ?? 0) < STALE_MS;
888
- return { connected, client: connected ? presence?.client ?? null : null, lastSeen: presence?.lastSeen ?? null, setup: setupInfo() };
926
+ return { connected: !!best, client: best?.client ?? null, lastSeen: best?.lastSeen ?? null, setup: setupInfo() };
889
927
  }
890
928
  function installConfig(target) {
891
929
  const dest = target === "claude" ? path5.join(root, ".mcp.json") : path5.join(root, ".cursor", "mcp.json");
@@ -939,7 +977,7 @@ data: ${JSON.stringify(data)}
939
977
  const names = new Set(pending);
940
978
  pending.clear();
941
979
  if (names.has("changeset.json")) onChangeSetFile();
942
- if (names.has("agent.json")) pushAgent();
980
+ if ([...names].some(isPresenceFile)) pushAgent();
943
981
  }, 40);
944
982
  });
945
983
  watcher.unref?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limno/core",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Limno's framework-neutral dev tooling: JSX source stamping, the change-set store and the endpoints the editor talks to.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -37,9 +37,9 @@
37
37
  "dependencies": {
38
38
  "@babel/parser": "^7.29.0",
39
39
  "@babel/traverse": "^7.29.0",
40
- "@limno/mcp": "0.1.8",
41
- "@limno/overlay": "0.1.14",
42
- "@limno/spec": "0.1.8",
40
+ "@limno/mcp": "0.1.9",
41
+ "@limno/overlay": "0.1.15",
42
+ "@limno/spec": "0.1.9",
43
43
  "launch-editor": "^2.14.1",
44
44
  "magic-string": "^0.30.17"
45
45
  },