isorite 0.14.0 → 0.16.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.
Files changed (99) hide show
  1. package/dist/main.js +757 -98
  2. package/docs/terminal.md +3 -1
  3. package/guide/out-of-the-base/web-and-code-links.md +3 -1
  4. package/package.json +1 -1
  5. package/web/_app/immutable/assets/0.BEFtmzIh.css +1 -0
  6. package/web/_app/immutable/assets/4.LR47AJKA.css +1 -0
  7. package/web/_app/immutable/assets/NoteList.C2GYAaLW.css +1 -0
  8. package/web/_app/immutable/chunks/{BWE_HS0c.js → 0B47b24t.js} +1 -1
  9. package/web/_app/immutable/chunks/{CzYPMYd4.js → 1j--pDqt.js} +1 -1
  10. package/web/_app/immutable/chunks/{e2LGvGEb.js → AnnzTlE8.js} +1 -1
  11. package/web/_app/immutable/chunks/{DRcTlHZP.js → B7LnLClC.js} +1 -1
  12. package/web/_app/immutable/chunks/{ZwM5k8r0.js → BAVnZFPa.js} +1 -1
  13. package/web/_app/immutable/chunks/BHORylQC.js +1 -0
  14. package/web/_app/immutable/chunks/{BiATTH-R.js → BLe29cGh.js} +1 -1
  15. package/web/_app/immutable/chunks/{C8h1oeAZ.js → BLzZIf7l.js} +1 -1
  16. package/web/_app/immutable/chunks/BMijTvgG.js +1 -0
  17. package/web/_app/immutable/chunks/{DCZYeMlH.js → BPpUSQ2Y.js} +1 -1
  18. package/web/_app/immutable/chunks/{kG4HctVa.js → BQeONalw.js} +1 -1
  19. package/web/_app/immutable/chunks/{CtkUy_15.js → BZX8cNYL.js} +1 -1
  20. package/web/_app/immutable/chunks/{Bb_N98K9.js → BiHyHFlW.js} +1 -1
  21. package/web/_app/immutable/chunks/{BGW1TKtR.js → BtYMl4bD.js} +1 -1
  22. package/web/_app/immutable/chunks/{DKqMzyyE.js → BtcGx-KQ.js} +1 -1
  23. package/web/_app/immutable/chunks/{BK31dpt6.js → Bytzbt5l.js} +1 -1
  24. package/web/_app/immutable/chunks/{DDAG55ce.js → C1FDDux-.js} +1 -1
  25. package/web/_app/immutable/chunks/C7qBBO-i.js +1 -0
  26. package/web/_app/immutable/chunks/{NyWzYSz1.js → C8FxuPry.js} +1 -1
  27. package/web/_app/immutable/chunks/{CdY1G_wk.js → C9Qdslk3.js} +1 -1
  28. package/web/_app/immutable/chunks/CA-rzUq8.js +524 -0
  29. package/web/_app/immutable/chunks/{CMIt1Ma8.js → CSKABQj2.js} +1 -1
  30. package/web/_app/immutable/chunks/CSTv2fL0.js +1 -0
  31. package/web/_app/immutable/chunks/{Dzi94aDH.js → CTuuQ66F.js} +1 -1
  32. package/web/_app/immutable/chunks/{PsfIQCJ1.js → CUB9vnd0.js} +1 -1
  33. package/web/_app/immutable/chunks/{DrVtdHUW.js → CUNetmvQ.js} +1 -1
  34. package/web/_app/immutable/chunks/{Djr0h_v1.js → CVqwkR8N.js} +1 -1
  35. package/web/_app/immutable/chunks/{CTDYUZPz.js → CWc-mNzo.js} +1 -1
  36. package/web/_app/immutable/chunks/{CryuQgNh.js → CZSmkKvQ.js} +1 -1
  37. package/web/_app/immutable/chunks/{BA78_vo5.js → CcE14TDI.js} +1 -1
  38. package/web/_app/immutable/chunks/{DBLoWLMb.js → CglDFp51.js} +1 -1
  39. package/web/_app/immutable/chunks/{DjUnonHY.js → Ch5Z7cbP.js} +1 -1
  40. package/web/_app/immutable/chunks/{DFuPQaNa.js → Cpjqsnqr.js} +1 -1
  41. package/web/_app/immutable/chunks/{Bd-WxALw.js → CwHHpacD.js} +1 -1
  42. package/web/_app/immutable/chunks/{UDPiRmKc.js → D7jLdCNu.js} +1 -1
  43. package/web/_app/immutable/chunks/{CIYFNih-.js → D9uNsgxP.js} +1 -1
  44. package/web/_app/immutable/chunks/{CFf_dAzX.js → DAVMwq6b.js} +1 -1
  45. package/web/_app/immutable/chunks/{C2npZzkn.js → DEQsS4Ez.js} +1 -1
  46. package/web/_app/immutable/chunks/{B-DzK7Gb.js → DFo4P1vU.js} +1 -1
  47. package/web/_app/immutable/chunks/{CdmC_l9F.js → DH_19--P.js} +1 -1
  48. package/web/_app/immutable/chunks/{l1LsK1sB.js → DJQ6favH.js} +1 -1
  49. package/web/_app/immutable/chunks/{BvPxdlBc.js → DJT73ARj.js} +1 -1
  50. package/web/_app/immutable/chunks/{BCGaqnao.js → DW5uy3Jw.js} +1 -1
  51. package/web/_app/immutable/chunks/{BT_btfh0.js → DWwu8a1P.js} +1 -1
  52. package/web/_app/immutable/chunks/{BhEqJU15.js → DY0M2GMn.js} +1 -1
  53. package/web/_app/immutable/chunks/{TanDZqdf.js → DbtvzL7r.js} +1 -1
  54. package/web/_app/immutable/chunks/DtEVmiYo.js +1 -0
  55. package/web/_app/immutable/chunks/{CT4_O9X_.js → DxRGLDbV.js} +1 -1
  56. package/web/_app/immutable/chunks/{D6sVhEz3.js → Ih6r6QR2.js} +1 -1
  57. package/web/_app/immutable/chunks/{BXBhaEho.js → Kem7IXj7.js} +1 -1
  58. package/web/_app/immutable/chunks/{Ct_EK1xu.js → MrW4zd8l.js} +1 -1
  59. package/web/_app/immutable/chunks/{BqipEJXw.js → Mwr1r9aw.js} +1 -1
  60. package/web/_app/immutable/chunks/{D1LK3xRu.js → OG9GsKke.js} +1 -1
  61. package/web/_app/immutable/chunks/{DymLl-qY.js → Qej0SEux.js} +1 -1
  62. package/web/_app/immutable/chunks/TmxIGCN3.js +1 -0
  63. package/web/_app/immutable/chunks/{CPSlCQsu.js → Wv-ICQRy.js} +4 -4
  64. package/web/_app/immutable/chunks/{B35sQTcK.js → ZTEErEOd.js} +1 -1
  65. package/web/_app/immutable/chunks/{Cf-5I5_6.js → e7GRa0yc.js} +1 -1
  66. package/web/_app/immutable/chunks/{C5Z-Y7Lu.js → f7mFVWQS.js} +1 -1
  67. package/web/_app/immutable/chunks/{Bj-5n5Pb.js → hjSZ041d.js} +1 -1
  68. package/web/_app/immutable/chunks/{DHJ8FcFD.js → mhWk5y4o.js} +1 -1
  69. package/web/_app/immutable/chunks/{CMikI1NP.js → zasx25UV.js} +3 -3
  70. package/web/_app/immutable/entry/app.B9TPZNFw.js +2 -0
  71. package/web/_app/immutable/entry/start.WU9HKeyT.js +1 -0
  72. package/web/_app/immutable/nodes/0.C9NFEqJd.js +3 -0
  73. package/web/_app/immutable/nodes/1.B5PJwWOG.js +1 -0
  74. package/web/_app/immutable/nodes/2.DRtC7zjs.js +1 -0
  75. package/web/_app/immutable/nodes/3.EEMisvUL.js +1 -0
  76. package/web/_app/immutable/nodes/4.DthmsFQC.js +19 -0
  77. package/web/_app/immutable/nodes/5.CEu0DlpQ.js +1 -0
  78. package/web/_app/immutable/nodes/6.DzvF0ZPt.js +1 -0
  79. package/web/_app/version.json +1 -1
  80. package/web/index.html +16 -16
  81. package/web/_app/immutable/assets/0.BPP604gH.css +0 -1
  82. package/web/_app/immutable/assets/4.fOHsAKBF.css +0 -1
  83. package/web/_app/immutable/assets/NoteList.E6jC-AtZ.css +0 -1
  84. package/web/_app/immutable/chunks/BPrp4s2r.js +0 -1
  85. package/web/_app/immutable/chunks/CAlIuau0.js +0 -1
  86. package/web/_app/immutable/chunks/Cl5gQm2e.js +0 -1
  87. package/web/_app/immutable/chunks/DFKYo3Wx.js +0 -1
  88. package/web/_app/immutable/chunks/DxVFSMVD.js +0 -523
  89. package/web/_app/immutable/chunks/X3aTZRe_.js +0 -1
  90. package/web/_app/immutable/chunks/ej_0XxR7.js +0 -1
  91. package/web/_app/immutable/entry/app.BzA5QYtw.js +0 -2
  92. package/web/_app/immutable/entry/start.BOh0Gj31.js +0 -1
  93. package/web/_app/immutable/nodes/0.BmaxMLzC.js +0 -2
  94. package/web/_app/immutable/nodes/1.IQDu8pD1.js +0 -1
  95. package/web/_app/immutable/nodes/2.D2Oi-ADx.js +0 -1
  96. package/web/_app/immutable/nodes/3.CF1W85jb.js +0 -1
  97. package/web/_app/immutable/nodes/4.BMYBn904.js +0 -15
  98. package/web/_app/immutable/nodes/5.1vm6WZdM.js +0 -1
  99. package/web/_app/immutable/nodes/6.C9ia8cDm.js +0 -1
package/dist/main.js CHANGED
@@ -619,6 +619,39 @@ function splitFrontmatter(source) {
619
619
  if (typeof meta !== "object" || Array.isArray(meta)) throw new FrontmatterError("frontmatter is not a mapping");
620
620
  return [meta, body.replace(/^\n+/, "")];
621
621
  }
622
+ /** A frontmatter line `key: value` at the top level whose value the YAML parser refused, such as an unquoted
623
+ * colon (`title: Foo: bar`) or a value starting with a backtick, `%` or `@`: agents write these often. The value
624
+ * is put in double quotes, for those lines alone, and the text kept as it was otherwise. Null when the
625
+ * frontmatter reads already, or the quotes would not make it read (an unclosed list, say): the note is left as written. */
626
+ function repairFrontmatter(source) {
627
+ try {
628
+ if (frontmatterText(source)[0] === null) return null;
629
+ } catch {
630
+ return null;
631
+ }
632
+ const bom = source.startsWith(BOM$1) ? BOM$1 : "", lines = source.slice(bom.length).split("\n");
633
+ let close = 1;
634
+ while (rstrip(lines[close]) !== "---") close++;
635
+ const raw = lines.slice(1, close).join("\n") + "\n";
636
+ const bad = /* @__PURE__ */ new Set();
637
+ for (const e of parseYaml(raw).errors) {
638
+ const at = e.linePos?.[0]?.line;
639
+ if (at) bad.add(at);
640
+ }
641
+ const fixed = [];
642
+ for (const n of bad) {
643
+ const m = /^([A-Za-z_][\w .-]*):[ \t]+(\S.*?)[ \t]*(\r?)$/.exec(lines[n] ?? "");
644
+ if (!m || /^["'\[{|>&*!#]/.test(m[2])) continue;
645
+ lines[n] = `${m[1]}: ${JSON.stringify(m[2])}${m[3]}`;
646
+ fixed.push(m[1]);
647
+ }
648
+ if (!fixed.length) return null;
649
+ if (parseYaml(lines.slice(1, close).join("\n") + "\n").errors.length) return null;
650
+ return {
651
+ text: bom + lines.join("\n"),
652
+ fixed
653
+ };
654
+ }
622
655
  /** Serialise frontmatter, keeping key order. */
623
656
  function dumpFrontmatter(meta) {
624
657
  return stringify(meta, {
@@ -885,16 +918,19 @@ function embedFor(id, ids) {
885
918
  for (const other of ids) if (other.split("/").at(-1) === name) shared++;
886
919
  return `[[!${shared > 1 ? id : name}]]`;
887
920
  }
888
- /** A code link's file and lines, or null when it is not one, or names a path outside the repository. */
921
+ /** A code link's file and, after `#`, its lines (`L40-L52`) or the symbol it names (`leapfrog`, `Body.step`: a
922
+ * function, method or class, found by its name wherever it has moved to, M29). Null when it is not one, or names a
923
+ * path outside the repository. */
889
924
  function codeTarget(href) {
890
- const m = /^code:([^#?]+)(?:#L(\d+)(?:-L?(\d+))?)?$/.exec(href.trim());
925
+ const m = /^code:([^#?]+)(?:#(?:L(\d+)(?:-L?(\d+))?|([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)))?$/.exec(href.trim());
891
926
  if (!m) return null;
892
927
  const path = decodeURIComponent(m[1]).replace(/^\.?\/+/, "");
893
928
  if (!path || path.split("/").some((p) => p === ".." || !p)) return null;
894
929
  return {
895
930
  path,
896
931
  ...m[2] ? { line: Number(m[2]) } : {},
897
- ...m[3] ? { end: Number(m[3]) } : {}
932
+ ...m[3] ? { end: Number(m[3]) } : {},
933
+ ...m[4] ? { symbol: m[4] } : {}
898
934
  };
899
935
  }
900
936
  /** A note's links out of the base, in order, each once. */
@@ -922,14 +958,20 @@ function outwardLinks(body) {
922
958
  for (const { href, embed, alt } of citeRefs(body)) {
923
959
  if (!embed || seen.has(href)) continue;
924
960
  const c = codeTarget(href);
925
- if (!c || !pictureType(c.path)) continue;
926
- out.push({
961
+ if (!c) continue;
962
+ if (pictureType(c.path)) out.push({
927
963
  kind: "code",
928
964
  href,
929
965
  text: alt || c.path.split("/").at(-1),
930
966
  path: c.path,
931
967
  picture: true
932
968
  });
969
+ else out.push({
970
+ kind: "code",
971
+ href,
972
+ text: alt || c.symbol || c.path.split("/").at(-1),
973
+ ...c
974
+ });
933
975
  seen.add(href);
934
976
  }
935
977
  return out;
@@ -2197,6 +2239,176 @@ function mapModel(base) {
2197
2239
  };
2198
2240
  }
2199
2241
  //#endregion
2242
+ //#region packages/core/src/symbol.ts
2243
+ const PY = /* @__PURE__ */ new Set(["py", "pyi"]);
2244
+ const BRACE = /* @__PURE__ */ new Set([
2245
+ "js",
2246
+ "jsx",
2247
+ "mjs",
2248
+ "cjs",
2249
+ "ts",
2250
+ "tsx",
2251
+ "mts",
2252
+ "cts",
2253
+ "go",
2254
+ "rs",
2255
+ "java",
2256
+ "kt",
2257
+ "kts",
2258
+ "cs",
2259
+ "c",
2260
+ "h",
2261
+ "cc",
2262
+ "cpp",
2263
+ "cxx",
2264
+ "hpp",
2265
+ "swift",
2266
+ "php",
2267
+ "scala",
2268
+ "dart",
2269
+ "zig",
2270
+ "svelte",
2271
+ "vue"
2272
+ ]);
2273
+ /** Whether symbols can be found in a file of this name. */
2274
+ function hasSymbols(path) {
2275
+ const ext = path.split(".").at(-1).toLowerCase();
2276
+ return PY.has(ext) || BRACE.has(ext);
2277
+ }
2278
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2279
+ /** The lines of `path`'s symbol `name` (`Class.method` for one inside another) in `text`, or null. */
2280
+ function findSymbol(text, path, name) {
2281
+ const ext = path.split(".").at(-1).toLowerCase();
2282
+ const lines = text.split(/\r?\n/);
2283
+ const parts = name.split(".").filter(Boolean);
2284
+ if (!parts.length || !PY.has(ext) && !BRACE.has(ext)) return null;
2285
+ let from = 0, to = lines.length - 1, span = null;
2286
+ for (const part of parts) {
2287
+ const found = PY.has(ext) ? pyFind(lines, part, from, to) : braceFind(lines, part, from, to);
2288
+ if (!found) return null;
2289
+ span = found;
2290
+ from = found.start;
2291
+ to = found.end - 1;
2292
+ }
2293
+ return span;
2294
+ }
2295
+ function pyFind(lines, name, from, to) {
2296
+ const re = new RegExp(`^(\\s*)(?:async\\s+)?(?:def|class)\\s+${esc(name)}\\b`);
2297
+ const at = [];
2298
+ for (let i = from; i <= to; i++) {
2299
+ const m = re.exec(lines[i]);
2300
+ if (m) at.push({
2301
+ i,
2302
+ indent: m[1].length
2303
+ });
2304
+ }
2305
+ const shallow = Math.min(...at.map((a) => a.indent), Infinity);
2306
+ const top = from === 0 && to === lines.length - 1;
2307
+ const here = at.filter((a) => a.indent === shallow && (!top || shallow === 0));
2308
+ let first = null, count = 0;
2309
+ for (const { i, indent } of here) {
2310
+ count++;
2311
+ if (first) continue;
2312
+ let open = i;
2313
+ while (open < lines.length - 1 && !/:\s*(#.*)?$/.test(lines[open])) open++;
2314
+ let end = open;
2315
+ for (let j = open + 1; j < lines.length; j++) {
2316
+ const l = lines[j];
2317
+ if (!l.trim()) continue;
2318
+ if (l.length - l.trimStart().length <= indent) break;
2319
+ end = j;
2320
+ }
2321
+ let start = i;
2322
+ while (start > 0 && new RegExp(`^\\s{${indent}}@`).test(lines[start - 1])) start--;
2323
+ first = {
2324
+ start: start + 1,
2325
+ end: Math.min(end, to) + 1,
2326
+ count: 1
2327
+ };
2328
+ }
2329
+ return first ? {
2330
+ ...first,
2331
+ count
2332
+ } : null;
2333
+ }
2334
+ const DECL = (n) => [
2335
+ new RegExp(`\\b(?:function\\*?|class|interface|enum|struct|trait|union|impl|fn|func|def|type|record|namespace|object|module)\\s+(?:<[^>]*>\\s*)?(?:\\([^)]*\\)\\s*)?${n}\\b`),
2336
+ new RegExp(`\\b(?:const|let|var|val)\\s+${n}\\s*(?::[^=]+)?=\\s*(?:async\\s*)?(?:function\\b|\\([^)]*\\)\\s*(?::[^=]+)?=>|\\w+\\s*=>)`),
2337
+ new RegExp(`^\\s*(?:(?:public|private|protected|internal|static|async|override|final|abstract|export|default|get|set|pub|unsafe|virtual|readonly|suspend|inline|open)\\s+)*(?:[\\w<>\\[\\],.?*&:]+\\s+)?\\*?${n}\\s*(?:<[^>]*>)?\\(`)
2338
+ ];
2339
+ function braceFind(lines, name, from, to) {
2340
+ const res = DECL(esc(name));
2341
+ let first = null, count = 0;
2342
+ let i = from;
2343
+ while (i <= to) {
2344
+ const line = lines[i];
2345
+ if (/^\s*(\/\/|\*|\/\*|#)/.test(line) || !res.some((re) => re.test(line))) {
2346
+ i++;
2347
+ continue;
2348
+ }
2349
+ const body = openBrace(lines, i, to);
2350
+ if (!body) {
2351
+ i++;
2352
+ continue;
2353
+ }
2354
+ count++;
2355
+ const end = closeBrace(lines, body.line, body.col);
2356
+ if (!first) {
2357
+ let start = i;
2358
+ while (start > 0 && /^\s*(@|#\[|\/\/|\/\*|\*)/.test(lines[start - 1])) start--;
2359
+ first = {
2360
+ start: start + 1,
2361
+ end: Math.min(end, to) + 1,
2362
+ count: 1
2363
+ };
2364
+ }
2365
+ i = end + 1;
2366
+ }
2367
+ return first ? {
2368
+ ...first,
2369
+ count
2370
+ } : null;
2371
+ }
2372
+ /** The first `{` from line `i`, if it comes before a `;` (within a few lines, the signature). */
2373
+ function openBrace(lines, i, to) {
2374
+ for (let j = i; j <= Math.min(to, i + 8); j++) {
2375
+ const l = stripStrings(lines[j]);
2376
+ const b = l.indexOf("{"), s = l.indexOf(";");
2377
+ if (b >= 0 && (s < 0 || b < s)) return {
2378
+ line: j,
2379
+ col: b
2380
+ };
2381
+ if (s >= 0) return null;
2382
+ }
2383
+ return null;
2384
+ }
2385
+ /** Blank out what is inside quotes and after `//`, so braces there are not counted. */
2386
+ function stripStrings(l) {
2387
+ return l.replace(/\/\/.*$/, (m) => " ".repeat(m.length)).replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, (m) => " ".repeat(m.length));
2388
+ }
2389
+ /** The line holding the `}` that closes the `{` at (line, col). The end of the file when it does not. */
2390
+ function closeBrace(lines, line, col) {
2391
+ let depth = 0, block = false;
2392
+ for (let j = line; j < lines.length; j++) {
2393
+ let l = stripStrings(lines[j]);
2394
+ if (block) {
2395
+ const e = l.indexOf("*/");
2396
+ if (e < 0) continue;
2397
+ l = " ".repeat(e + 2) + l.slice(e + 2);
2398
+ block = false;
2399
+ }
2400
+ l = l.replace(/\/\*.*?\*\//g, (m) => " ".repeat(m.length));
2401
+ const o = l.indexOf("/*");
2402
+ if (o >= 0) {
2403
+ block = true;
2404
+ l = l.slice(0, o);
2405
+ }
2406
+ for (let k = j === line ? col : 0; k < l.length; k++) if (l[k] === "{") depth++;
2407
+ else if (l[k] === "}" && --depth === 0) return j;
2408
+ }
2409
+ return lines.length - 1;
2410
+ }
2411
+ //#endregion
2200
2412
  //#region packages/core/src/node.ts
2201
2413
  /** Every file and folder under `root`, Markdown with its text. */
2202
2414
  function readFolder(root) {
@@ -2231,6 +2443,32 @@ function loadBundle(root) {
2231
2443
  return Bundle.fromFiles(readFolder(root));
2232
2444
  }
2233
2445
  //#endregion
2446
+ //#region packages/server/src/term.ts
2447
+ const wrap = (open, close) => (text) => colour() ? `\x1b[${open}m${text}\x1b[${close}m` : text;
2448
+ /** Whether to paint: asked each time, so a test or a caller can change the environment. */
2449
+ function colour(env = process.env, tty = Boolean(process.stdout.isTTY)) {
2450
+ if (env.NO_COLOR) return false;
2451
+ if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
2452
+ return tty && env.TERM !== "dumb";
2453
+ }
2454
+ const bold = wrap(1, 22);
2455
+ const dim = wrap(2, 22);
2456
+ const red = wrap(31, 39);
2457
+ const green = wrap(32, 39);
2458
+ const yellow = wrap(33, 39);
2459
+ /** A help text with its commands in bold and what they say dimmed: a line ` isorite word … said` and its continuations. */
2460
+ function helpText(text) {
2461
+ if (!colour()) return text;
2462
+ return text.split("\n").map((line, i) => {
2463
+ if (i === 0) return bold(line);
2464
+ if (/^ {20,}\S/.test(line)) return dim(line);
2465
+ const m = /^( isorite(?: [a-z-]+)?)( .*?)(\s{2,})(\S.*)$/.exec(line);
2466
+ if (m) return `${bold(m[1])}${m[2]}${m[3]}${dim(m[4])}`;
2467
+ const c = /^( isorite(?: [a-z-]+)?)(.*)$/.exec(line);
2468
+ return c ? `${bold(c[1])}${c[2]}` : line;
2469
+ }).join("\n");
2470
+ }
2471
+ //#endregion
2234
2472
  //#region packages/server/src/config.ts
2235
2473
  const configPath = (env = process.env) => join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "isorite", "config.toml");
2236
2474
  const DEVICE$1 = /^[a-z0-9]{4,16}$/;
@@ -2865,7 +3103,7 @@ var Conflict = class extends EngineError {
2865
3103
  };
2866
3104
  //#endregion
2867
3105
  //#region packages/engine/src/lookup.ts
2868
- const str$1 = { type: "string" };
3106
+ const str$2 = { type: "string" };
2869
3107
  const fn$1 = (name, description, properties, required) => ({
2870
3108
  type: "function",
2871
3109
  function: {
@@ -2881,9 +3119,9 @@ const fn$1 = (name, description, properties, required) => ({
2881
3119
  });
2882
3120
  const TOOLS = [
2883
3121
  fn$1("search_notes", "Keyword search over the knowledge base. Returns note ids, titles, types, one-sentence descriptions and a one-line snippet. Search by the words a note would use.", {
2884
- query: str$1,
3122
+ query: str$2,
2885
3123
  under: {
2886
- ...str$1,
3124
+ ...str$2,
2887
3125
  description: "Only notes in this folder."
2888
3126
  },
2889
3127
  limit: {
@@ -2892,24 +3130,24 @@ const TOOLS = [
2892
3130
  }
2893
3131
  }, ["query"]),
2894
3132
  fn$1("outline_note", "A note's description, its headings, the notes it links to and the notes that link to it, without its body. Use it to choose a section to read.", { id: {
2895
- ...str$1,
3133
+ ...str$2,
2896
3134
  description: "The note's id or path: basics/entropy or /basics/entropy.md."
2897
3135
  } }, ["id"]),
2898
3136
  fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section. A picture in it is ![what it shows](path): you have its words, not the picture.", {
2899
- id: str$1,
3137
+ id: str$2,
2900
3138
  section: {
2901
- ...str$1,
3139
+ ...str$2,
2902
3140
  description: "A heading's text, as outline_note gives it."
2903
3141
  }
2904
3142
  }, ["id"])
2905
3143
  ];
2906
3144
  const FIND_SIMILAR = fn$1("find_similar", "Find notes by meaning: notes that say something like the text you give, even in quite other words. Slower and vaguer than search_notes: use it after search_notes and the links of the notes in hand have not found what you need, or when you cannot guess the words a note would use. Give a question or a sentence, not keywords. Returns each note's nearest section, with how alike it is (0 to 1).", {
2907
3145
  text: {
2908
- ...str$1,
3146
+ ...str$2,
2909
3147
  description: "A question, or a sentence saying what you are looking for."
2910
3148
  },
2911
3149
  under: {
2912
- ...str$1,
3150
+ ...str$2,
2913
3151
  description: "Only notes in this folder."
2914
3152
  },
2915
3153
  limit: {
@@ -3233,6 +3471,14 @@ const ISORITE = `# Their knowledge base
3233
3471
  together. A problem has no steps inside it.
3234
3472
  - Files beside the notes that are not notes (data, documents, pictures,
3235
3473
  artifacts) are linked as any note is: [runs](/simulation/runs.csv).
3474
+ - Code in their repository is linked as [the step](code:sim/step.py#leapfrog):
3475
+ the path from the repository's root, then a name (\`#leapfrog\`, or
3476
+ \`#Class.method\`) or lines (\`#L40-L52\`). Prefer a name: it still finds the
3477
+ code when it moves. Look in the file for the name before you write it; a name
3478
+ that is not there marks the link broken. \`![the step](code:sim/step.py#leapfrog)\`
3479
+ shows that code in the note, where it is read.
3480
+ - Frontmatter is YAML: put a value in quotes when it holds a colon, as
3481
+ \`title: "Gauss: Bonnet"\` and \`description: "Why: the proof"\`.
3236
3482
  - Every change you make is marked as yours, in the note's history, and they
3237
3483
  can undo it.
3238
3484
 
@@ -3279,7 +3525,7 @@ or try; not for what an answer or a note says as well.
3279
3525
  problem, fix that and write the whole file again.
3280
3526
  4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
3281
3527
  Link it from the note it belongs to if they want it there.`;
3282
- const str = { type: "string" };
3528
+ const str$1 = { type: "string" };
3283
3529
  const fn = (name, description, properties, required) => ({
3284
3530
  type: "function",
3285
3531
  function: {
@@ -3295,16 +3541,16 @@ const fn = (name, description, properties, required) => ({
3295
3541
  });
3296
3542
  const ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an open question in their base, about one or more of their notes.", {
3297
3543
  title: {
3298
- ...str,
3544
+ ...str$1,
3299
3545
  description: "The question, short, ending in a question mark."
3300
3546
  },
3301
3547
  text: {
3302
- ...str,
3548
+ ...str$1,
3303
3549
  description: "A line or two on what is asked, or empty."
3304
3550
  },
3305
3551
  about: {
3306
3552
  type: "array",
3307
- items: str,
3553
+ items: str$1,
3308
3554
  description: "The ids of the notes it is about."
3309
3555
  }
3310
3556
  }, [
@@ -3313,7 +3559,7 @@ const ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an op
3313
3559
  "about"
3314
3560
  ]);
3315
3561
  const READ_DATA = fn("read_data", "A data file's shape: its columns with their types and ranges, the row count and the first rows. Read it before writing anything that uses the file, so the column names are real.", { path: {
3316
- ...str,
3562
+ ...str$1,
3317
3563
  description: "The file's path in the base: folder/name.csv."
3318
3564
  } }, ["path"]);
3319
3565
  const ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a complete working one to start from, the kit's reference, and the rules write_artifact checks. dashboard: numbers, charts and a table of one data file. compare: two data files side by side. simulation: a model with parameters and play. explorable: inputs, derived numbers and linked views. viewer: a 3D model.", { name: {
@@ -3328,21 +3574,21 @@ const ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a compl
3328
3574
  } }, ["name"]);
3329
3575
  const WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML file on the kit. The title, description and uses given here are written into it. It is checked, and run where it can be; the result says what to fix. Nothing is written while a rule is broken.", {
3330
3576
  path: {
3331
- ...str,
3577
+ ...str$1,
3332
3578
  description: "Where: folder/name.html, beside the notes it belongs to."
3333
3579
  },
3334
- title: str,
3580
+ title: str$1,
3335
3581
  description: {
3336
- ...str,
3582
+ ...str$1,
3337
3583
  description: "One sentence: what it shows, and what can be done with it."
3338
3584
  },
3339
3585
  uses: {
3340
3586
  type: "array",
3341
- items: str,
3587
+ items: str$1,
3342
3588
  description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
3343
3589
  },
3344
3590
  html: {
3345
- ...str,
3591
+ ...str$1,
3346
3592
  description: "The whole file."
3347
3593
  }
3348
3594
  }, [
@@ -3353,16 +3599,16 @@ const WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML fil
3353
3599
  "html"
3354
3600
  ]);
3355
3601
  const READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
3356
- ...str,
3602
+ ...str$1,
3357
3603
  description: "folder/name.html"
3358
3604
  } }, ["path"]);
3359
3605
  const OFFER_TEXT = fn("offer_text", "Offer them their own answers, lightly joined, to insert at the end of a note as their writing. Only their words.", {
3360
3606
  id: {
3361
- ...str,
3607
+ ...str$1,
3362
3608
  description: "The note's id, or AGENTS.md for a line of their instructions for the base."
3363
3609
  },
3364
3610
  text: {
3365
- ...str,
3611
+ ...str$1,
3366
3612
  description: "The passage, in Markdown."
3367
3613
  }
3368
3614
  }, ["id", "text"]);
@@ -3931,7 +4177,9 @@ var Engine = class Engine {
3931
4177
  for (const c of b.concepts.values()) for (const o of c.outward) {
3932
4178
  if (o.kind !== "code") continue;
3933
4179
  const k = o.path.lastIndexOf("/");
3934
- if (!(await names(k < 0 ? "" : o.path.slice(0, k))).has(o.path.slice(k + 1))) broken.set(c.id, [...broken.get(c.id) ?? [], o.href]);
4180
+ let gone = !(await names(k < 0 ? "" : o.path.slice(0, k))).has(o.path.slice(k + 1));
4181
+ if (!gone && o.symbol) gone = (await this.code(o.href)).state === "no-symbol";
4182
+ if (gone) broken.set(c.id, [...broken.get(c.id) ?? [], o.href]);
3935
4183
  }
3936
4184
  }
3937
4185
  this.codeCheck = {
@@ -4232,6 +4480,10 @@ var Engine = class Engine {
4232
4480
  return null;
4233
4481
  }
4234
4482
  }
4483
+ async deleteAgentRecord(agent, session) {
4484
+ if (!AGENT_ID.test(agent) || !session || session.length > 200) throw new EngineError("an agent's session is named by the agent and its session's id");
4485
+ await this.opts.own.remove(agentRecordPath(agent, session));
4486
+ }
4235
4487
  async agentRecords() {
4236
4488
  const own = this.opts.own, all = [];
4237
4489
  for (const a of await own.list(AGENTS_DIR).catch(() => [])) {
@@ -4303,6 +4555,25 @@ var Engine = class Engine {
4303
4555
  createNote(id, text) {
4304
4556
  return this.one(() => this.put(id, text, null));
4305
4557
  }
4558
+ async makeFolder(path) {
4559
+ path = path.trim().replace(/^\/+|\/+$/g, "");
4560
+ if (!inBase(path)) throw new EngineError("only a folder of the base is made here");
4561
+ checkNoteId(path);
4562
+ if (!this.opts.notes.mkdir) throw new EngineError("an empty folder can't be made here");
4563
+ if (this.files.has(`${path}.md`)) throw new EngineError(`there is a note at ${path} already`, 409);
4564
+ await this.one(async () => {
4565
+ await this.opts.notes.mkdir(path);
4566
+ const parts = path.split("/");
4567
+ for (let i = 1; i <= parts.length; i++) {
4568
+ const p = parts.slice(0, i).join("/");
4569
+ if (!this.files.has(p)) this.files.set(p, {
4570
+ path: p,
4571
+ dir: true
4572
+ });
4573
+ }
4574
+ this.built = this.index = null;
4575
+ });
4576
+ }
4306
4577
  /** `who` is you unless Iso moves it, in Write mode. */
4307
4578
  async moveNote(id, to, base, who = "you") {
4308
4579
  to = to.trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/, "");
@@ -4447,6 +4718,63 @@ var Engine = class Engine {
4447
4718
  version: sha256(bytes).slice(0, 16)
4448
4719
  };
4449
4720
  }
4721
+ /** The code a code link names (M29): its symbol's lines, or the lines it gives, or the file's start. A symbol is
4722
+ * found by its name in the file as it is now, so the link holds where the code has moved. */
4723
+ async code(href) {
4724
+ const target = codeTarget(href);
4725
+ if (!target) throw new EngineError("not a code link", 400);
4726
+ const { path, symbol } = target, where = await this.repositoryOfBase();
4727
+ const files = where ? this.opts.repository?.(where) ?? null : null;
4728
+ if (!files) return {
4729
+ state: "no-repository",
4730
+ path,
4731
+ ...symbol ? { symbol } : {}
4732
+ };
4733
+ const bytes = await files.readBytes(path);
4734
+ if (!bytes) return {
4735
+ state: "no-file",
4736
+ path,
4737
+ ...symbol ? { symbol } : {}
4738
+ };
4739
+ if (bytes.length > 2e6) return {
4740
+ state: "too-large",
4741
+ path
4742
+ };
4743
+ const lines = new TextDecoder().decode(bytes).split(/\r?\n/);
4744
+ if (lines.at(-1) === "") lines.pop();
4745
+ let start = target.line ?? 1, end = target.end ?? target.line ?? lines.length, count;
4746
+ if (symbol) {
4747
+ if (!hasSymbols(path)) return {
4748
+ state: "unsupported",
4749
+ path,
4750
+ symbol
4751
+ };
4752
+ const found = findSymbol(lines.join("\n"), path, symbol);
4753
+ if (!found) return {
4754
+ state: "no-symbol",
4755
+ path,
4756
+ symbol
4757
+ };
4758
+ ({start, end, count} = found);
4759
+ }
4760
+ end = Math.min(Math.max(end, start), lines.length);
4761
+ if (start > lines.length) return {
4762
+ state: "no-symbol",
4763
+ path
4764
+ };
4765
+ const shown = Math.min(end, start + 120 - 1);
4766
+ return {
4767
+ state: "ok",
4768
+ path,
4769
+ ...symbol ? { symbol } : {},
4770
+ start,
4771
+ end,
4772
+ total: lines.length,
4773
+ text: lines.slice(start - 1, shown).join("\n"),
4774
+ ...shown < end ? { more: true } : {},
4775
+ ...count && count > 1 ? { count } : {}
4776
+ };
4777
+ }
4450
4778
  /** What was read of each picture, kept while its size and time are the same. */
4451
4779
  pictureCache = /* @__PURE__ */ new Map();
4452
4780
  /** Every picture in the notes (M22), found afresh: one put in the folder by hand since the base was opened is
@@ -4552,6 +4880,7 @@ var Engine = class Engine {
4552
4880
  async put(id, text, base, why = "edit", extra = {}, who = "you") {
4553
4881
  const { files, path } = this.where(id);
4554
4882
  if (text.length > 2e6) throw new EngineError("note too large", 413);
4883
+ if (who !== "you" && path.endsWith(".md")) text = repairFrontmatter(text)?.text ?? text;
4555
4884
  const now = await files.read(path);
4556
4885
  if (base === null && now !== null) throw new EngineError(`there is already a note at ${id}`, 409);
4557
4886
  if (base !== null && now === null) throw new Conflict(`${id} is gone`, null);
@@ -4648,6 +4977,10 @@ var Engine = class Engine {
4648
4977
  turns: readTurns(text)
4649
4978
  };
4650
4979
  }
4980
+ async deleteConversation(id) {
4981
+ if (!TURN_ID.test(id)) throw new EngineError("not a conversation's id");
4982
+ await this.opts.own.remove(conversationPath(id));
4983
+ }
4651
4984
  async conversations() {
4652
4985
  const study = this.opts.own;
4653
4986
  const names = await study.list(CONVERSATIONS_DIR).catch(() => []);
@@ -4939,6 +5272,8 @@ var MemoryFiles = class {
4939
5272
  files = /* @__PURE__ */ new Map();
4940
5273
  /** Files held as bytes (pictures), apart from text. */
4941
5274
  bytes = /* @__PURE__ */ new Map();
5275
+ /** Folders made empty (M31). */
5276
+ folders = /* @__PURE__ */ new Set();
4942
5277
  constructor(files = {}) {
4943
5278
  for (const [path, text] of Object.entries(files)) this.files.set(path, text);
4944
5279
  }
@@ -4963,10 +5298,17 @@ var MemoryFiles = class {
4963
5298
  this.files.delete(path);
4964
5299
  this.bytes.delete(path);
4965
5300
  }
5301
+ async mkdir(dir) {
5302
+ this.folders.add(dir);
5303
+ }
4966
5304
  async list(dir) {
4967
5305
  const prefix = dir ? dir + "/" : "";
4968
5306
  const found = /* @__PURE__ */ new Map();
4969
- for (const path of [...this.files.keys(), ...this.bytes.keys()]) {
5307
+ for (const path of [
5308
+ ...this.files.keys(),
5309
+ ...this.bytes.keys(),
5310
+ ...[...this.folders].map((f) => f + "/")
5311
+ ]) {
4970
5312
  if (!path.startsWith(prefix)) continue;
4971
5313
  const rest = path.slice(prefix.length), cut = rest.indexOf("/");
4972
5314
  if (cut >= 0) {
@@ -5320,6 +5662,47 @@ const EMPTY = {
5320
5662
  const textOf = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
5321
5663
  const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
5322
5664
  const keyOf = (agent, session) => `${agent}\u0000${session}`;
5665
+ const str = (v) => typeof v === "string" ? v : "";
5666
+ /** The agent's slash commands, from available_commands_update. */
5667
+ const commandsOf = (v) => (Array.isArray(v) ? v : []).filter((c) => str(c.name)).map((c) => {
5668
+ const hint = str(c.input?.hint);
5669
+ return {
5670
+ name: str(c.name).replace(/^\//, ""),
5671
+ description: str(c.description),
5672
+ ...hint ? { hint } : {}
5673
+ };
5674
+ });
5675
+ /** The agent's modes, from a session's answer or a current_mode_update. */
5676
+ const modesOf = (v) => {
5677
+ const m = v;
5678
+ const options = (m?.availableModes ?? []).filter((o) => str(o.id)).map((o) => ({
5679
+ value: str(o.id),
5680
+ name: str(o.name) || str(o.id),
5681
+ ...str(o.description) ? { description: str(o.description) } : {}
5682
+ }));
5683
+ return options.length ? {
5684
+ kind: "mode",
5685
+ id: "mode",
5686
+ name: "Mode",
5687
+ current: str(m?.currentModeId),
5688
+ options
5689
+ } : null;
5690
+ };
5691
+ /** The agent's options (the model, say), whose choices may be grouped. */
5692
+ const configsOf = (v) => (Array.isArray(v) ? v : []).filter((o) => str(o.id) && Array.isArray(o.options)).flatMap((o) => {
5693
+ const options = o.options.flatMap((x) => Array.isArray(x.options) ? x.options : [x]).filter((x) => str(x.value)).map((x) => ({
5694
+ value: str(x.value),
5695
+ name: str(x.name) || str(x.value),
5696
+ ...str(x.description) ? { description: str(x.description) } : {}
5697
+ }));
5698
+ return options.length ? [{
5699
+ kind: "config",
5700
+ id: str(o.id),
5701
+ name: str(o.name) || str(o.id),
5702
+ current: str(o.currentValue),
5703
+ options
5704
+ }] : [];
5705
+ });
5323
5706
  /** The sessions the wrappers carry, as the pane draws them. One is shown: the one you chose, else the last to start. */
5324
5707
  var AcpFollower = class {
5325
5708
  #links = /* @__PURE__ */ new Map();
@@ -5402,6 +5785,8 @@ var AcpFollower = class {
5402
5785
  size: live.size,
5403
5786
  ours: this.#ours,
5404
5787
  sessions: this.#sessions(),
5788
+ commands: live.commands,
5789
+ settings: [...live.modes ? [live.modes] : [], ...live.configs],
5405
5790
  ...problem
5406
5791
  };
5407
5792
  else if (read) this.#view = {
@@ -5635,6 +6020,42 @@ var AcpFollower = class {
5635
6020
  link.opening = "";
5636
6021
  link.send({ type: "open" });
5637
6022
  }
6023
+ /** Set a mode or option of the session shown (the agent's own: its model, its plan mode). Optimistic: the agent's
6024
+ * answer or update settles it. */
6025
+ set(kind, id, value) {
6026
+ const live = this.#need();
6027
+ const setting = kind === "mode" ? live.modes : live.configs.find((c) => c.id === id);
6028
+ if (!setting || !setting.options.some((o) => o.value === value)) throw new Error(`${live.link.name || "The agent"} offers no such choice`);
6029
+ if (kind === "mode") live.modes = {
6030
+ ...setting,
6031
+ current: value
6032
+ };
6033
+ else live.configs = live.configs.map((c) => c.id === id ? {
6034
+ ...c,
6035
+ current: value
6036
+ } : c);
6037
+ live.link.send({
6038
+ type: "set",
6039
+ session: live.session,
6040
+ kind,
6041
+ id,
6042
+ value
6043
+ });
6044
+ this.#tell();
6045
+ }
6046
+ /** Remove what isorite kept of a session that is not live. What the agent keeps of its own is not touched. */
6047
+ async forget(agent, session) {
6048
+ const key = keyOf(agent, session);
6049
+ if (this.#lives.has(key)) throw new Error("that session is live: stop it before removing what is kept of it");
6050
+ if (!this.#api?.deleteAgentRecord) throw new Error("nothing is kept here");
6051
+ await this.#api.deleteAgentRecord(agent, session);
6052
+ if (this.#shown === key) {
6053
+ this.#shown = null;
6054
+ this.#chosen = false;
6055
+ this.#reading = null;
6056
+ }
6057
+ await this.#readKept();
6058
+ }
5638
6059
  /** Ask the attached agents for their sessions again: the drawer is being looked at. */
5639
6060
  refresh() {
5640
6061
  for (const link of this.#links.values()) if (link.can.list) link.send({ type: "list" });
@@ -5713,7 +6134,11 @@ var AcpFollower = class {
5713
6134
  break;
5714
6135
  }
5715
6136
  case "session/new":
5716
- if (typeof m.result?.sessionId === "string") this.#open(link, m.result.sessionId, key.startsWith("isorite-") ? "ours" : "in", typeof params.cwd === "string" ? params.cwd : "");
6137
+ if (typeof m.result?.sessionId === "string") {
6138
+ const opened = this.#open(link, m.result.sessionId, key.startsWith("isorite-") ? "ours" : "in", typeof params.cwd === "string" ? params.cwd : "");
6139
+ opened.modes = modesOf(m.result.modes);
6140
+ opened.configs = configsOf(m.result.configOptions);
6141
+ }
5717
6142
  break;
5718
6143
  case "session/load":
5719
6144
  case "session/resume":
@@ -5722,9 +6147,20 @@ var AcpFollower = class {
5722
6147
  this.#lives.delete(keyOf(link.agent, live.session));
5723
6148
  break;
5724
6149
  }
6150
+ if (m.result?.modes) live.modes = modesOf(m.result.modes);
6151
+ if (m.result?.configOptions) live.configs = configsOf(m.result.configOptions);
5725
6152
  this.#loaded(live);
5726
6153
  }
5727
6154
  break;
6155
+ case "session/set_mode":
6156
+ if (live && !m.error && live.modes) live.modes = {
6157
+ ...live.modes,
6158
+ current: str(params.modeId)
6159
+ };
6160
+ break;
6161
+ case "session/set_config_option":
6162
+ if (live && !m.error && m.result?.configOptions) live.configs = configsOf(m.result.configOptions);
6163
+ break;
5728
6164
  case "session/prompt":
5729
6165
  if (live) {
5730
6166
  const stop = m.error ? "failed" : m.result?.stopReason === "cancelled" ? "stopped" : m.result?.stopReason === "end_turn" ? "done" : "failed";
@@ -5788,7 +6224,10 @@ var AcpFollower = class {
5788
6224
  loading: false,
5789
6225
  replaying: false,
5790
6226
  pending: null,
5791
- given: /* @__PURE__ */ new Set()
6227
+ given: /* @__PURE__ */ new Set(),
6228
+ commands: [],
6229
+ modes: null,
6230
+ configs: []
5792
6231
  };
5793
6232
  live.link = link;
5794
6233
  if (cwd) live.cwd = cwd;
@@ -5978,7 +6417,19 @@ var AcpFollower = class {
5978
6417
  if (typeof cost === "number") live.cost = cost;
5979
6418
  break;
5980
6419
  }
5981
- case "session_info_update": if (typeof u.title === "string") live.title = u.title;
6420
+ case "session_info_update":
6421
+ if (typeof u.title === "string") live.title = u.title;
6422
+ break;
6423
+ case "available_commands_update":
6424
+ live.commands = commandsOf(u.availableCommands);
6425
+ break;
6426
+ case "current_mode_update":
6427
+ if (live.modes && typeof u.currentModeId === "string") live.modes = {
6428
+ ...live.modes,
6429
+ current: u.currentModeId
6430
+ };
6431
+ break;
6432
+ case "config_option_update": live.configs = configsOf(u.configOptions);
5982
6433
  }
5983
6434
  }
5984
6435
  /** The note a path names, if it is a Markdown file of the base (relative paths read from the session's folder). */
@@ -6038,7 +6489,7 @@ var AcpFollower = class {
6038
6489
  };
6039
6490
  //#endregion
6040
6491
  //#region packages/server/src/version.ts
6041
- const VERSION = "0.14.0";
6492
+ const VERSION = "0.16.0";
6042
6493
  //#endregion
6043
6494
  //#region packages/server/src/connect.ts
6044
6495
  const HARNESSES = [{
@@ -6310,6 +6761,27 @@ function detectAgents(env = process.env) {
6310
6761
  }];
6311
6762
  });
6312
6763
  }
6764
+ function listAgents(env = process.env) {
6765
+ const found = detectAgents(env).map((d) => ({
6766
+ ...d,
6767
+ found: true
6768
+ }));
6769
+ const have = new Set(found.map((d) => d.id));
6770
+ const rest = KNOWN.filter((k) => !have.has(k.id)).map((k) => ({
6771
+ id: k.id,
6772
+ name: k.name,
6773
+ command: k.command,
6774
+ path: "",
6775
+ keys: k.keys,
6776
+ keySet: false,
6777
+ ready: false,
6778
+ found: false,
6779
+ note: `${k.name} isn't on this machine's PATH.`
6780
+ }));
6781
+ return [...found, ...rest];
6782
+ }
6783
+ /** The ACP command of an agent known by name (`isorite acp omp`), or null. */
6784
+ const knownCommand = (id) => KNOWN.find((k) => k.id === id)?.command ?? null;
6313
6785
  /** Why an agent that stopped may have: the key it reads isn't set where isorite was started, for the command run. */
6314
6786
  function missingKey(command, env = process.env) {
6315
6787
  const k = KNOWN.find((x) => x.command === command);
@@ -6563,6 +7035,108 @@ var AgentRunner = class {
6563
7035
  }
6564
7036
  };
6565
7037
  //#endregion
7038
+ //#region packages/server/src/own-repo.ts
7039
+ const q = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
7040
+ const slashes = (p) => p.split("\\").join("/");
7041
+ function git(cwd, args, env) {
7042
+ return execFileSync("git", args, {
7043
+ cwd,
7044
+ env: plainGitEnv(env),
7045
+ timeout: 1e4,
7046
+ killSignal: "SIGKILL",
7047
+ encoding: "utf8",
7048
+ stdio: [
7049
+ "ignore",
7050
+ "pipe",
7051
+ "pipe"
7052
+ ]
7053
+ });
7054
+ }
7055
+ /** What this machine chose for a base, from bases.toml. */
7056
+ function historyChoice(base, config) {
7057
+ const v = table(readConfig(basesPath(config))[base]).history;
7058
+ return v === "project" || v === "separate" ? v : null;
7059
+ }
7060
+ function setHistoryChoice(base, choice, config) {
7061
+ const bases = readConfig(basesPath(config));
7062
+ if (!bases[base]) throw new Error(`${base} is not among the bases`);
7063
+ bases[base] = {
7064
+ ...table(bases[base]),
7065
+ history: choice
7066
+ };
7067
+ writeConfig(bases, basesPath(config));
7068
+ }
7069
+ /** Where isorite's own folder is kept, and the commands for the person to run. Runs `git ls-files`, `remote` and
7070
+ * `rev-parse`, which only read. */
7071
+ function ownRepoInfo(own, choice = null, env = process.env) {
7072
+ const project = repoOf(dirname(own));
7073
+ const base = {
7074
+ state: "none",
7075
+ path: own,
7076
+ project,
7077
+ remotes: [],
7078
+ choice,
7079
+ ask: false,
7080
+ commands: [],
7081
+ problem: ""
7082
+ };
7083
+ try {
7084
+ if (existsSync(join(own, ".git"))) {
7085
+ const remotes = git(own, ["remote"], env).split("\n").filter(Boolean);
7086
+ return {
7087
+ ...base,
7088
+ state: "own",
7089
+ remotes,
7090
+ commands: remotes.length ? [`git -C ${q(own)} push`] : [`git -C ${q(own)} remote add origin <the address of your private repository>`, `git -C ${q(own)} push -u origin HEAD`]
7091
+ };
7092
+ }
7093
+ if (!project) return base;
7094
+ const rel = slashes(relative(project, own));
7095
+ const tracked = git(project, [
7096
+ "ls-files",
7097
+ "--",
7098
+ rel
7099
+ ], env).trim() !== "";
7100
+ const path = git(project, [
7101
+ "rev-parse",
7102
+ "--git-path",
7103
+ "info/exclude"
7104
+ ], env).trim();
7105
+ const exclude = isAbsolute(path) ? path : resolve(project, path);
7106
+ const apart = [
7107
+ `git -C ${q(own)} init`,
7108
+ `echo ${q(rel + "/")} >> ${q(exclude)}`,
7109
+ `git -C ${q(own)} add -A`,
7110
+ `git -C ${q(own)} commit -m "isorite: start"`
7111
+ ];
7112
+ return {
7113
+ ...base,
7114
+ state: tracked ? "tracked" : "shared",
7115
+ ask: choice === null,
7116
+ commands: tracked ? [
7117
+ `git -C ${q(project)} rm -r --cached ${q(rel)}`,
7118
+ ...apart,
7119
+ `git -C ${q(project)} commit -m "stop tracking isorite's own files"`
7120
+ ] : apart
7121
+ };
7122
+ } catch (e) {
7123
+ return {
7124
+ ...base,
7125
+ state: "unavailable",
7126
+ problem: e.message.split("\n")[0] ?? "git did not answer"
7127
+ };
7128
+ }
7129
+ }
7130
+ /** For a base just made in a git project: ask whether its own files stay with the project or apart, keep the answer
7131
+ * for this machine, and say the commands for apart. Asks nothing outside a project. isorite runs none of it. */
7132
+ async function askWhereFilesLive(notes, ask, out, config, env = process.env) {
7133
+ if (!repoOf(notes)) return null;
7134
+ const choice = (await ask(`${notes} is in a git project. isorite's own files (.isorite/: journals, kept sessions, settings) can be committed with the project, or kept apart from it, so the people sharing it don't have them.\nKeep them [p]roject / [a]part? (p) `)).trim().toLowerCase().startsWith("a") ? "separate" : "project";
7135
+ setHistoryChoice(rememberBase(notes, { path: config }), choice, config);
7136
+ if (choice === "separate") out(`\nTo keep them apart, run these (isorite runs no git itself):\n${ownRepoInfo(join(notes, ".isorite"), "separate", env).commands.map((c) => ` ${c}`).join("\n")}\n`);
7137
+ return choice;
7138
+ }
7139
+ //#endregion
6566
7140
  //#region packages/server/src/http.ts
6567
7141
  const LOOPBACK = /* @__PURE__ */ new Set([
6568
7142
  "localhost",
@@ -6639,7 +7213,7 @@ const AgentStatus = z.object({
6639
7213
  session: z.string().min(1).max(200),
6640
7214
  busy: z.boolean()
6641
7215
  }).strict();
6642
- function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents }) {
7216
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents, config }) {
6643
7217
  const app = new Hono();
6644
7218
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
6645
7219
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -6749,6 +7323,10 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6749
7323
  if (c.req.header("if-none-match") === etag) return c.body(null, 304, headers);
6750
7324
  return c.body(found.bytes, 200, headers);
6751
7325
  });
7326
+ app.get("/api/code", async (c) => {
7327
+ const href = c.req.query("href") ?? "";
7328
+ return answer(c, () => api.code(href));
7329
+ });
6752
7330
  app.put("/api/note", async (c) => {
6753
7331
  const refused = writeRefused(c);
6754
7332
  if (refused) return refused;
@@ -6773,6 +7351,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6773
7351
  app.get("/api/agents/notes", (c) => answer(c, () => api.agentNotes()));
6774
7352
  post("/api/agents/notes", z.object({ may: z.boolean() }).strict(), (_c, b) => api.setAgentNotes(b.may));
6775
7353
  app.get("/api/conversations", (c) => answer(c, () => api.conversations()));
7354
+ post("/api/conversation/delete", z.object({ id: z.string().max(64) }).strict(), (_c, b) => api.deleteConversation(b.id));
6776
7355
  app.get("/api/conversation", (c) => answer(c, async () => await api.conversation(c.req.query("id") || void 0) ?? {
6777
7356
  id: "",
6778
7357
  turns: []
@@ -6785,6 +7364,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6785
7364
  to: z.string().max(400),
6786
7365
  base: z.string().max(64)
6787
7366
  }).strict(), (c, b) => api.moveNote(id(c), b.to, b.base));
7367
+ post("/api/folder", z.object({ path: z.string().max(400) }).strict(), (_c, b) => api.makeFolder(b.path));
6788
7368
  post("/api/note/delete", z.object({ base: z.string().max(64) }).strict(), (c, b) => api.deleteNote(id(c), b.base));
6789
7369
  post("/api/note/done", z.object({
6790
7370
  done: z.boolean(),
@@ -6851,6 +7431,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6851
7431
  }).strict(), (_c, b) => api.agentReceived(b.id, b.agent));
6852
7432
  post("/api/agent/status", AgentStatus, (_c, b) => api.agentStatus(b.agent, b.session, b.busy));
6853
7433
  post("/api/agent/take", z.object({ id: z.string().max(64) }).strict(), (_c, b) => api.agentTake(b.id));
7434
+ if (notes) {
7435
+ const configFile = config ?? configPath();
7436
+ app.get("/api/own/repo", (c) => answer(c, async () => local(c) ? ownRepoInfo(join(notes, ".isorite"), base ? historyChoice(base, configFile) : null) : null));
7437
+ post("/api/own/choose", z.object({ choice: z.enum(["project", "separate"]) }).strict(), async (c, b) => {
7438
+ if (!local(c)) throw new EngineError("this is chosen only from this machine", 403);
7439
+ if (!base) throw new EngineError("this base has no name here", 409);
7440
+ setHistoryChoice(base, b.choice, configFile);
7441
+ return ownRepoInfo(join(notes, ".isorite"), b.choice);
7442
+ });
7443
+ }
6854
7444
  if (notes) {
6855
7445
  app.get("/api/connect", (c) => answer(c, async () => proposal(notes)));
6856
7446
  post("/api/connect", z.object({ files: z.array(z.string().max(200)).max(10).optional() }).strict(), async (c, b) => {
@@ -6862,7 +7452,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6862
7452
  app.get("/api/acp/agent", (c) => answer(c, async () => agents.state()));
6863
7453
  app.get("/api/acp/detected", (c) => answer(c, async () => {
6864
7454
  if (!local(c)) throw new EngineError("agents are looked for only from this machine", 403);
6865
- return detectAgents();
7455
+ return listAgents();
6866
7456
  }));
6867
7457
  app.get("/api/acp/registry", (c) => answer(c, async () => registryAgents().catch((e) => {
6868
7458
  throw new EngineError(e.message, 502);
@@ -6967,6 +7557,23 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6967
7557
  }).strict(), async (_c, b) => {
6968
7558
  await acp.follow(b.agent, b.session).catch(refused);
6969
7559
  });
7560
+ post("/api/acp/forget", z.object({
7561
+ agent: z.string().max(40),
7562
+ session: z.string().min(1).max(200)
7563
+ }).strict(), async (_c, b) => {
7564
+ await acp.forget(b.agent, b.session).catch(refused);
7565
+ });
7566
+ post("/api/acp/set", z.object({
7567
+ kind: z.enum(["mode", "config"]),
7568
+ id: z.string().max(100),
7569
+ value: z.string().max(200)
7570
+ }).strict(), async (_c, b) => {
7571
+ try {
7572
+ acp.set(b.kind, b.id, b.value);
7573
+ } catch (e) {
7574
+ throw new EngineError(e.message, 409);
7575
+ }
7576
+ });
6970
7577
  post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
6971
7578
  await acp.fresh(b.agent).catch(refused);
6972
7579
  });
@@ -7133,6 +7740,9 @@ var NodeFiles = class {
7133
7740
  throw e;
7134
7741
  }
7135
7742
  }
7743
+ async mkdir(dir) {
7744
+ await mkdir(this.at(dir), { recursive: true });
7745
+ }
7136
7746
  async append(path, text) {
7137
7747
  const full = this.at(path);
7138
7748
  await mkdir(dirname(full), { recursive: true });
@@ -7457,7 +8067,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
7457
8067
  acp,
7458
8068
  bases,
7459
8069
  base: known.name,
7460
- ...loopback ? { agents } : {}
8070
+ ...loopback ? { agents } : {},
8071
+ config
7461
8072
  }).fetch
7462
8073
  };
7463
8074
  }
@@ -7488,12 +8099,14 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
7488
8099
  console.error(e.code === "EADDRINUSE" ? `port ${port} is in use (isorite serve --port <n>)` : e.message);
7489
8100
  process.exit(1);
7490
8101
  });
7491
- console.log(`isorite: ${notes}`);
7492
- console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model, 53 MB)");
8102
+ const row = (label, text) => console.log(` ${dim(label.padEnd(8))}${text}`);
8103
+ console.log(`${bold("isorite")} ${notes}`);
8104
+ row("search", embedder ? "by meaning" : `by words ${dim("(isorite model fetches the model, 53 MB)")}`);
7493
8105
  const lent = bases.lent().filter((l) => l.name !== bases.first).map((l) => l.name);
7494
- if (lent.length) console.log(` lent to every session: ${lent.join(", ")} (isorite bases)`);
7495
- console.log(existsSync(join(WEB_DIR, "index.html")) ? ` ${link(`http://localhost:${port}/`)}` : ` the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
7496
- if (process.env.SSH_CONNECTION && LOOPBACK.has(host)) console.log(` over SSH, the link opens on this machine; from your own, first run: ssh -L ${port}:localhost:${port} ${hostname()}`);
8106
+ if (lent.length) row("lent", `${lent.join(", ")} ${dim("(isorite bases)")}`);
8107
+ if (existsSync(join(WEB_DIR, "index.html"))) row("open", link(`http://localhost:${port}/`));
8108
+ else row("open", `the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
8109
+ if (process.env.SSH_CONNECTION && LOOPBACK.has(host)) row("ssh", `opens on this machine; from your own, first: ssh -L ${port}:localhost:${port} ${hostname()}`);
7497
8110
  stop = () => {
7498
8111
  server.close();
7499
8112
  process.exit(0);
@@ -7512,6 +8125,12 @@ const MARK = {
7512
8125
  warn: "!",
7513
8126
  skip: "–"
7514
8127
  };
8128
+ const PAINT = {
8129
+ ok: green,
8130
+ fail: red,
8131
+ warn: yellow,
8132
+ skip: dim
8133
+ };
7515
8134
  /** The environment variables isorite and Node read for it; the secret ones are shown only by length. */
7516
8135
  const READ = [
7517
8136
  "XDG_CONFIG_HOME",
@@ -7554,7 +8173,7 @@ async function doctor(folder, o = {}) {
7554
8173
  outcome,
7555
8174
  said
7556
8175
  });
7557
- if (!o.report) out(`${MARK[outcome]} ${step}: ${said}`);
8176
+ if (!o.report) out(`${PAINT[outcome](MARK[outcome])} ${bold(step)}: ${outcome === "skip" ? dim(said) : said}`);
7558
8177
  };
7559
8178
  const path = configPath(env), execArgv = o.execArgv ?? process.execArgv;
7560
8179
  if (!existsSync(path)) say("config", "ok", `there is no ${path} yet: it is made when a base is first opened`);
@@ -7614,6 +8233,8 @@ async function doctor(folder, o = {}) {
7614
8233
  const errors = issues.filter((i) => i.level === "error");
7615
8234
  say("links", errors.length ? "fail" : issues.length || cycles.length ? "warn" : "ok", `${bundle.concepts.size} notes; ${errors.length} errors, ${issues.length - errors.length} warnings, ${cycles.length} requires cycles${issues.length ? `: ${issues.slice(0, 3).map((i) => `${i.path}: ${i.message}`).join("; ")}${issues.length > 3 ? `; and ${issues.length - 3} more (isorite check ${notes})` : ""}` : ""}`);
7616
8235
  }
8236
+ const kept = ownRepoInfo(own, null, env);
8237
+ if (existsSync(own)) say("history", kept.state === "own" ? "ok" : kept.state === "unavailable" ? "skip" : kept.state === "none" ? "skip" : "ok", kept.state === "own" ? `${OWN_DIR}/ is a repository of its own${kept.remotes.length ? `, remote ${kept.remotes.join(", ")}` : `; to keep it elsewhere: git -C ${own} remote add origin <address>`}` : kept.state === "tracked" ? `${OWN_DIR}/ is tracked by the project's repository, so everyone sharing it has isorite's files; to keep them apart, run:\n ${kept.commands.join("\n ")}` : kept.state === "shared" ? `${OWN_DIR}/ is in the project's folder, to be committed with it; to keep it apart, run:\n ${kept.commands.join("\n ")}` : kept.state === "unavailable" ? `${OWN_DIR}/: git did not answer (${kept.problem})` : `${OWN_DIR}/ is in no git repository`);
7617
8238
  const repository = existsSync(settings) ? parseSettings(readFileSync(settings, "utf8")).repository : void 0;
7618
8239
  const found = repoOf(notes);
7619
8240
  if (!repository && found) say("repository", "ok", `${found}: code links point here`);
@@ -7660,7 +8281,7 @@ async function doctor(folder, o = {}) {
7660
8281
  ...checks.map((c) => `${MARK[c.outcome]} ${c.step}: ${c.said}`),
7661
8282
  "```"
7662
8283
  ].join("\n"));
7663
- } else out(failed ? `${failed} failed. isorite doctor --report gives all this as one block, its secrets hidden, to paste to someone helping.` : "nothing failed");
8284
+ } else out(failed ? `${red(`${failed} failed.`)} isorite doctor --report gives all this as one block, its secrets hidden, to paste to someone helping.` : green("nothing failed"));
7664
8285
  return failed ? 1 : 0;
7665
8286
  }
7666
8287
  //#endregion
@@ -8244,6 +8865,14 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8244
8865
  toAgent(m);
8245
8866
  }
8246
8867
  if (c.type === "list" && can.list) ask("session/list", { cwd });
8868
+ if (c.type === "set") ask(c.kind === "mode" ? "session/set_mode" : "session/set_config_option", c.kind === "mode" ? {
8869
+ sessionId: c.session,
8870
+ modeId: c.value
8871
+ } : {
8872
+ sessionId: c.session,
8873
+ configId: c.id,
8874
+ value: c.value
8875
+ });
8247
8876
  if (c.type === "open") {
8248
8877
  const params = {
8249
8878
  cwd,
@@ -9242,7 +9871,7 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
9242
9871
  return 0;
9243
9872
  }
9244
9873
  const width = Math.max(...known.map((b) => b.name.length));
9245
- for (const b of known) out(`${b.name.padEnd(width)} ${b.lend ? "lent " : " "} ${b.path}${existsSync(b.path) ? "" : " (not there)"}`);
9874
+ for (const b of known) out(`${bold(b.name.padEnd(width))} ${b.lend ? "lent " : " "} ${dim(b.path)}${existsSync(b.path) ? "" : ` ${red("(not there)")}`}`);
9246
9875
  return 0;
9247
9876
  }
9248
9877
  //#endregion
@@ -9303,18 +9932,23 @@ function label(b, all) {
9303
9932
  }
9304
9933
  /** The choice `isorite` makes, and what it says. */
9305
9934
  async function start(io) {
9935
+ const make = async (folder) => {
9936
+ const notes = io.make(folder);
9937
+ await io.made?.(notes);
9938
+ return notes;
9939
+ };
9306
9940
  const found = findBase(io.cwd);
9307
9941
  if (found.kind === "base") return io.open(found.notes);
9308
9942
  if (found.kind === "several") {
9309
9943
  io.out(`isorite: more than one base here:`);
9310
- found.notes.forEach((n, i) => io.out(` ${i + 1} ${n}`));
9944
+ found.notes.forEach((n, i) => io.out(` ${dim(String(i + 1))} ${n}`));
9311
9945
  const pick = await io.choose("Open which?", found.notes.length);
9312
9946
  return pick === null ? 2 : io.open(found.notes[pick]);
9313
9947
  }
9314
9948
  if (found.kind === "repo") {
9315
9949
  const notes = join(found.repo, "notes");
9316
9950
  io.out(`isorite: no base in ${found.repo} yet.`);
9317
- if (!existsSync(notes) && await io.confirm(`Make one at ${notes}?`)) return io.open(io.make(notes));
9951
+ if (!existsSync(notes) && await io.confirm(`Make one at ${notes}?`)) return io.open(await make(notes));
9318
9952
  if (existsSync(notes)) io.out(` ${notes} is there already: open it as it is with isorite serve ${notes}`);
9319
9953
  }
9320
9954
  const known = recent(io.config);
@@ -9328,11 +9962,11 @@ async function start(io) {
9328
9962
  "the guide"
9329
9963
  ];
9330
9964
  io.out(known.length ? "isorite: the bases on this machine, last used first:" : "isorite:");
9331
- rows.forEach((r, i) => io.out(` ${i + 1} ${r}`));
9965
+ rows.forEach((r, i) => io.out(` ${dim(String(i + 1))} ${r}`));
9332
9966
  const pick = await io.choose("Open which?", rows.length);
9333
9967
  if (pick === null) return 2;
9334
9968
  if (pick < known.length) return io.open(known[pick].path);
9335
- if (pick === known.length) return io.open(io.make(join(io.cwd, "notes")));
9969
+ if (pick === known.length) return io.open(await make(join(io.cwd, "notes")));
9336
9970
  return io.guide();
9337
9971
  }
9338
9972
  /** The model for search by meaning, offered on the first start without one (M26): asked once, fetched on a yes,
@@ -9355,62 +9989,66 @@ init_embedder();
9355
9989
  init_model();
9356
9990
  const SHORT = `isorite: an agentic harness for understanding
9357
9991
 
9358
- isorite open the base here, or choose one (a terminal; --no-open, --port <n>)
9359
- isorite doctor whether isorite can work here, and what to change if not (--report to paste to
9360
- someone helping; run in a project it checks that project's base)
9361
- isorite help this; isorite help all lists every command
9992
+ isorite open the base here, or choose one
9993
+ isorite doctor can isorite work here, and what to change if not
9994
+ isorite help this; help all lists every command
9362
9995
  isorite --version
9363
9996
 
9364
- Notes are plain Markdown in a folder of your project; your own agent reads and writes them with you.
9997
+ Notes are plain Markdown in a folder of your project. Your agent reads and writes them with you.
9365
9998
  `;
9366
9999
  const USAGE = `isorite: an agentic harness for understanding
9367
10000
 
9368
- isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
9369
- isorite check <notes> [--unused] check a folder of notes: frontmatter, links, cycles; --unused lists
9370
- the pictures no note shows
10001
+ isorite new <folder> a new base: an empty folder with .isorite/
10002
+ isorite check <notes> [--unused] [--fix]
10003
+ lint: frontmatter, links, cycles; --unused: pictures no note
10004
+ shows; --fix: quote a colon in frontmatter
9371
10005
  isorite serve <notes> [--port <n>] [--allow-host <name>]
9372
10006
  open it in the browser, on this machine only
9373
- isorite migrate <study base> [--merge] move a study base (format 1) into its notes' .isorite/; exercises
9374
- become problem notes; the study base is left as it was
9375
- isorite example <folder> [--geometry] write the example base (notes on an N-body simulation, three weeks
9376
- into being built), into an empty folder; --geometry: notes on the
9377
- differential geometry of surfaces, with their questions and problems
10007
+ isorite migrate <study base> [--merge] a study base (format 1) into its notes' .isorite/
10008
+ isorite example <folder> [--geometry] an example base, partway built, in an empty folder
9378
10009
  isorite mcp [<notes>] [--port <n>] [--agent <name>]
9379
- the knowledge base's tools over MCP, on stdio, for your agent in its
9380
- own terminal: claude mcp add isorite -- isorite mcp <base>
10010
+ the base's tools over MCP, on stdio:
10011
+ claude mcp add isorite -- isorite mcp <base>
9381
10012
  isorite acp --agent "<command>" [<notes>] [--port <n>] [--pane]
9382
- an ACP agent (claude-agent-acp, codex-acp, opencode acp, hermes acp)
9383
- behind your ACP front end (Toad, Zed), with the base's MCP, and its
9384
- sessions in Iso's pane; --pane: no front end, the pane alone
10013
+ an ACP agent behind your ACP front end, its sessions in Iso's
10014
+ pane; --pane: the pane alone
9385
10015
  isorite model [--from <folder|.tgz>] [--url <url>]
9386
- the model for search by meaning: which is installed, and where; else
9387
- fetched (53 MB, once) from ISORITE_MODEL_BASE_URL, npm, GitHub or
9388
- Hugging Face, or --url alone; or from the .tgz fetched by hand, or a
9389
- folder: its files copied there, or any compatible model
10016
+ the model for search by meaning: which, and where; else fetch it
10017
+ (53 MB, once)
9390
10018
  isorite doctor [<notes>] [--report] [--offline]
9391
- whether isorite can work here: the configuration, the network, the
9392
- model for search by meaning, the server, the notes and the project's
9393
- MCP config; --report gives it as one block, to paste to someone (or
9394
- an AI) helping
9395
- isorite memory [<folder>] the agent's memory: a knowledge base (~/isorite-memory unless named),
9396
- a git repository of its own, lent to every agent's session
9397
- isorite bases [lend|keep <name>] the bases this device knows, each listed as it is opened; lend one to
9398
- every session, or keep it to itself
9399
- isorite help setup how isorite is set up, every setting and failure: written for an AI
9400
- helping you set it up (docs/setup.md is for people)
9401
- isorite guide [<folder>] [--port <n>] the guide: a knowledge base about isorite, with things to do in it,
9402
- written into a folder (~/isorite-guide unless named) and opened in
9403
- the browser; run again, as you left it (also: isorite --tutorial)
10019
+ can isorite work here, a step at a time; --report: one block to
10020
+ paste to someone helping
10021
+ isorite memory [<folder>] the agent's memory: a base of its own, lent to every session
10022
+ isorite bases [lend|keep <name>] the bases this device knows; lend one to every session, or keep it
10023
+ isorite help setup every setting and failure, written for an AI helping you set up
10024
+ isorite guide [<folder>] [--port <n>] the guide: a base about isorite, to do things in (--tutorial)
9404
10025
  `;
9405
- function check(notes, out = console.log, unused = false) {
10026
+ function check(notes, out = console.log, unused = false, fix = false) {
9406
10027
  const root = resolve(notes);
9407
10028
  if (!existsSync(root) || !statSync(root).isDirectory()) {
9408
10029
  out(`no folder at ${root}`);
9409
10030
  return 2;
9410
10031
  }
9411
- const bundle = loadBundle(root), issues = bundle.lint(), cycles = bundle.requiresCycles();
9412
- for (const i of issues) out(`${i.level.padEnd(7)} ${i.path}: ${i.message}`);
9413
- for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
10032
+ let bundle = loadBundle(root), issues = bundle.lint();
10033
+ const mendable = issues.filter((i) => i.code === "frontmatter-invalid").flatMap((i) => {
10034
+ const m = repairFrontmatter(readFileSync(join(root, i.path), "utf8"));
10035
+ return m ? [{
10036
+ path: i.path,
10037
+ ...m
10038
+ }] : [];
10039
+ });
10040
+ if (fix && mendable.length) {
10041
+ for (const m of mendable) {
10042
+ writeFileSync(join(root, m.path), m.text);
10043
+ out(`fixed ${m.path}: quoted ${m.fixed.join(", ")}`);
10044
+ }
10045
+ bundle = loadBundle(root);
10046
+ issues = bundle.lint();
10047
+ }
10048
+ const cycles = bundle.requiresCycles();
10049
+ const level = (l) => (l === "error" ? red(l) : yellow(l)) + " ".repeat(Math.max(0, 7 - l.length));
10050
+ for (const i of issues) out(`${level(i.level)} ${bold(i.path)}: ${i.message}${i.code === "frontmatter-invalid" && !fix && mendable.some((m) => m.path === i.path) ? " (isorite check --fix quotes it)" : ""}`);
10051
+ for (const c of cycles) out(`${level("warning")} requires cycle: ${c.join(" -> ")}`);
9414
10052
  let artifactErrors = 0, artifactWarnings = 0;
9415
10053
  for (const f of [...bundle.files].filter((x) => citeKind(x) === "artifact").sort()) {
9416
10054
  const html = readFileSync(join(root, f), "utf8");
@@ -9427,13 +10065,14 @@ function check(notes, out = console.log, unused = false) {
9427
10065
  files,
9428
10066
  columns
9429
10067
  })) {
9430
- out(`${b.level.padEnd(7)} ${f}: ${b.message}`);
10068
+ out(`${level(b.level)} ${bold(f)}: ${b.message}`);
9431
10069
  if (b.level === "error") artifactErrors++;
9432
10070
  else artifactWarnings++;
9433
10071
  }
9434
10072
  }
9435
10073
  const errors = issues.filter((i) => i.level === "error").length + artifactErrors;
9436
- out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - issues.filter((i) => i.level === "error").length + artifactWarnings} warnings, ${cycles.length} requires cycles`);
10074
+ const warnings = issues.length - issues.filter((i) => i.level === "error").length + artifactWarnings;
10075
+ out(`${dim(`${bundle.concepts.size} notes in ${bundle.directories.size} folders;`)} ${(errors ? red : green)(`${errors} errors`)}, ${(warnings ? yellow : green)(`${warnings} warnings`)}, ${cycles.length} requires cycles`);
9437
10076
  if (unused) {
9438
10077
  const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
9439
10078
  const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();
@@ -9495,6 +10134,9 @@ async function terminalStart({ port, noOpen }) {
9495
10134
  console.log(` made ${made}`);
9496
10135
  return made;
9497
10136
  },
10137
+ made: async (notes) => {
10138
+ await askWhereFilesLive(notes, ask, console.log, configPath());
10139
+ },
9498
10140
  guide: async () => {
9499
10141
  await model();
9500
10142
  await guide(void 0, { port: port ?? await freePort() });
@@ -9516,10 +10158,25 @@ function main(argv) {
9516
10158
  noOpen: args.includes("--no-open")
9517
10159
  });
9518
10160
  }
9519
- if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
10161
+ if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"), rest.includes("--fix"));
9520
10162
  if (command === "new" && rest[0]) {
9521
10163
  const made = newBase(rest[0]);
9522
- console.log(`${made}: a knowledge base, empty. Open it in the browser, and attach your agent from Iso's pane:\n isorite serve ${made}`);
10164
+ const next = `${made}: a knowledge base, empty. Open it in the browser, and attach your agent from Iso's pane:\n isorite serve ${made}`;
10165
+ if (process.stdin.isTTY && process.stdout.isTTY && repoOf(made)) return (async () => {
10166
+ const { createInterface } = await import("node:readline/promises");
10167
+ const rl = createInterface({
10168
+ input: process.stdin,
10169
+ output: process.stdout
10170
+ });
10171
+ try {
10172
+ await askWhereFilesLive(made, (q) => rl.question(q), console.log, configPath());
10173
+ } finally {
10174
+ rl.close();
10175
+ }
10176
+ console.log(next);
10177
+ return 0;
10178
+ })();
10179
+ console.log(next);
9523
10180
  return 0;
9524
10181
  }
9525
10182
  if (command === "serve" && rest[0]) {
@@ -9586,9 +10243,15 @@ function main(argv) {
9586
10243
  const at = rest.indexOf(name);
9587
10244
  return at < 0 ? void 0 : rest[at + 1];
9588
10245
  };
9589
- const agent = flag("--agent"), port = flag("--port") === void 0 ? void 0 : Number(flag("--port"));
10246
+ const positional = rest.filter((a, i) => !a.startsWith("--") && ![
10247
+ "--port",
10248
+ "--agent",
10249
+ "--url"
10250
+ ].includes(rest[i - 1] ?? ""));
10251
+ const named = !flag("--agent") && positional[0] && !existsSync(positional[0]) ? knownCommand(positional[0]) : null;
10252
+ const agent = flag("--agent") ?? named ?? void 0, port = flag("--port") === void 0 ? void 0 : Number(flag("--port"));
9590
10253
  if (!agent) {
9591
- console.error("isorite acp --agent \"<the agent's ACP command>\" [<notes>] [--port <n>] [--pane]");
10254
+ console.error("isorite acp <omp|opencode|claude|codex|hermes> [<notes>] [--port <n>]\nisorite acp --agent \"<the agent's ACP command>\" [<notes>] [--port <n>] [--pane]");
9592
10255
  return 2;
9593
10256
  }
9594
10257
  if (port !== void 0 && !(Number.isInteger(port) && port > 0 && port < 65536)) {
@@ -9600,11 +10263,7 @@ function main(argv) {
9600
10263
  console.error("--url takes a base's address on this machine");
9601
10264
  return 2;
9602
10265
  }
9603
- return acp(rest.find((a, i) => !a.startsWith("--") && ![
9604
- "--port",
9605
- "--agent",
9606
- "--url"
9607
- ].includes(rest[i - 1] ?? "")), {
10266
+ return acp(named ? positional[1] : positional[0], {
9608
10267
  agent,
9609
10268
  pane,
9610
10269
  ...port === void 0 ? {} : { port },
@@ -9697,7 +10356,7 @@ function main(argv) {
9697
10356
  }
9698
10357
  throw e;
9699
10358
  }
9700
- process.stdout.write(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT);
10359
+ process.stdout.write(helpText(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT));
9701
10360
  return command && command !== "help" && command !== "--help" ? 2 : 0;
9702
10361
  }
9703
10362
  /** The commands that start something, which say so at once (M21): a subprocess that stalls after this is then