isorite 0.15.0 → 0.17.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 (109) hide show
  1. package/dist/main.js +507 -62
  2. package/guide/out-of-the-base/web-and-code-links.md +3 -1
  3. package/package.json +1 -1
  4. package/web/_app/immutable/assets/0.DVQOzs4l.css +1 -0
  5. package/web/_app/immutable/assets/4.Dn5sJ8uq.css +1 -0
  6. package/web/_app/immutable/assets/5.DJhOSn7C.css +1 -0
  7. package/web/_app/immutable/assets/NoteList.Wxp_AH0Y.css +1 -0
  8. package/web/_app/immutable/chunks/{BAVnZFPa.js → 0KrpTYPM.js} +1 -1
  9. package/web/_app/immutable/chunks/{f7mFVWQS.js → 1MewxdCy.js} +1 -1
  10. package/web/_app/immutable/chunks/{DFo4P1vU.js → 1W6iRjg5.js} +1 -1
  11. package/web/_app/immutable/chunks/{BQeONalw.js → 2BcU70Iq.js} +1 -1
  12. package/web/_app/immutable/chunks/{CVqwkR8N.js → B-04K_Pl.js} +1 -1
  13. package/web/_app/immutable/chunks/{B_x6_Y3m.js → BBU5FdEo.js} +1 -1
  14. package/web/_app/immutable/chunks/{Djo-ijeo.js → BBhr3bLI.js} +1 -1
  15. package/web/_app/immutable/chunks/{CSKABQj2.js → BCsFs14H.js} +1 -1
  16. package/web/_app/immutable/chunks/BJOSVfg-.js +1 -0
  17. package/web/_app/immutable/chunks/{zasx25UV.js → BNlziJ_1.js} +3 -3
  18. package/web/_app/immutable/chunks/{BPpUSQ2Y.js → BbvUCVY_.js} +1 -1
  19. package/web/_app/immutable/chunks/{e7GRa0yc.js → BjLgp18-.js} +1 -1
  20. package/web/_app/immutable/chunks/{OG9GsKke.js → Bx_Vz2qr.js} +1 -1
  21. package/web/_app/immutable/chunks/{D7jLdCNu.js → C2EzU0qX.js} +1 -1
  22. package/web/_app/immutable/chunks/{DH_19--P.js → C2QvvHIN.js} +1 -1
  23. package/web/_app/immutable/chunks/{DJQ6favH.js → CAuYgj5q.js} +1 -1
  24. package/web/_app/immutable/chunks/{hjSZ041d.js → CB6ztdCQ.js} +1 -1
  25. package/web/_app/immutable/chunks/CHjmZsHD.js +1 -0
  26. package/web/_app/immutable/chunks/CM4EA3Wo.js +1 -0
  27. package/web/_app/immutable/chunks/{BsY4iMci.js → CMN7rSZK.js} +1 -1
  28. package/web/_app/immutable/chunks/CQhfj5-f.js +9 -0
  29. package/web/_app/immutable/chunks/{CSTv2fL0.js → CRbUrapz.js} +1 -1
  30. package/web/_app/immutable/chunks/{BLzZIf7l.js → CabmfniL.js} +1 -1
  31. package/web/_app/immutable/chunks/{C9Qdslk3.js → CcRcjPTM.js} +1 -1
  32. package/web/_app/immutable/chunks/{BDJlNL-I.js → CccIbhmH.js} +85 -85
  33. package/web/_app/immutable/chunks/{C1FDDux-.js → CdD6waB-.js} +1 -1
  34. package/web/_app/immutable/chunks/{CwHHpacD.js → CfTuc06U.js} +1 -1
  35. package/web/_app/immutable/chunks/{Mwr1r9aw.js → Cl-Xmm9N.js} +1 -1
  36. package/web/_app/immutable/chunks/{DxRGLDbV.js → CmFzeyfe.js} +1 -1
  37. package/web/_app/immutable/chunks/{CUNetmvQ.js → Cn7j5NKi.js} +1 -1
  38. package/web/_app/immutable/chunks/{DWwu8a1P.js → CpJ4vfn-.js} +1 -1
  39. package/web/_app/immutable/chunks/{MrW4zd8l.js → CwcA6hab.js} +1 -1
  40. package/web/_app/immutable/chunks/{AnnzTlE8.js → CxEu-Di8.js} +1 -1
  41. package/web/_app/immutable/chunks/{CUB9vnd0.js → DEwfz-Il.js} +1 -1
  42. package/web/_app/immutable/chunks/DH2bqH7t.js +517 -0
  43. package/web/_app/immutable/chunks/{CZSmkKvQ.js → DJHLXLij.js} +1 -1
  44. package/web/_app/immutable/chunks/{Kem7IXj7.js → DK9wjWIk.js} +1 -1
  45. package/web/_app/immutable/chunks/{BLe29cGh.js → DMTkA0aK.js} +1 -1
  46. package/web/_app/immutable/chunks/{D1MhxXyn.js → DMpFmiFg.js} +1 -1
  47. package/web/_app/immutable/chunks/{DW5uy3Jw.js → DQJRaTNz.js} +1 -1
  48. package/web/_app/immutable/chunks/{DJT73ARj.js → DRKiKCov.js} +1 -1
  49. package/web/_app/immutable/chunks/{DEQsS4Ez.js → DS7XcdHs.js} +1 -1
  50. package/web/_app/immutable/chunks/{0B47b24t.js → D_xpsaTJ.js} +1 -1
  51. package/web/_app/immutable/chunks/{LbnMg2Kc.js → Ddw9FbDu.js} +1 -1
  52. package/web/_app/immutable/chunks/{D4EpFhYZ.js → DeazQJkV.js} +1 -1
  53. package/web/_app/immutable/chunks/{Ch5Z7cbP.js → Df3spPXn.js} +1 -1
  54. package/web/_app/immutable/chunks/{1j--pDqt.js → DfLI-CvW.js} +1 -1
  55. package/web/_app/immutable/chunks/{Bytzbt5l.js → Dns-yz2Q.js} +1 -1
  56. package/web/_app/immutable/chunks/{mhWk5y4o.js → DqZ4UzUH.js} +1 -1
  57. package/web/_app/immutable/chunks/{BtcGx-KQ.js → DrPF-vh8.js} +1 -1
  58. package/web/_app/immutable/chunks/{DAVMwq6b.js → Ds9L6FPB.js} +1 -1
  59. package/web/_app/immutable/chunks/{CWc-mNzo.js → Dzu8v7nB.js} +1 -1
  60. package/web/_app/immutable/chunks/IECP4ppk.js +1 -0
  61. package/web/_app/immutable/chunks/NgkOM4d0.js +1 -0
  62. package/web/_app/immutable/chunks/{ZTEErEOd.js → ZDjPMhAb.js} +1 -1
  63. package/web/_app/immutable/chunks/ZDkogIhD.js +1 -0
  64. package/web/_app/immutable/chunks/{CcE14TDI.js → _zY4mrA0.js} +1 -1
  65. package/web/_app/immutable/chunks/{B7LnLClC.js → fpNFwizw.js} +1 -1
  66. package/web/_app/immutable/chunks/{BZX8cNYL.js → qfBACIi9.js} +1 -1
  67. package/web/_app/immutable/chunks/{Cpjqsnqr.js → rv_cF9pp.js} +1 -1
  68. package/web/_app/immutable/chunks/{DY0M2GMn.js → sBy91tGs.js} +1 -1
  69. package/web/_app/immutable/chunks/{BiHyHFlW.js → vGOSIW7X.js} +1 -1
  70. package/web/_app/immutable/chunks/{Ih6r6QR2.js → yYKJ4mbq.js} +1 -1
  71. package/web/_app/immutable/chunks/{DbtvzL7r.js → yy6sD_us.js} +1 -1
  72. package/web/_app/immutable/entry/app.CX19wIuG.js +2 -0
  73. package/web/_app/immutable/entry/start.DaiosA4i.js +1 -0
  74. package/web/_app/immutable/nodes/0.B6_Ycc14.js +3 -0
  75. package/web/_app/immutable/nodes/1.JA1sPybf.js +1 -0
  76. package/web/_app/immutable/nodes/2.DN2phYmE.js +1 -0
  77. package/web/_app/immutable/nodes/3.CbnLNp2F.js +1 -0
  78. package/web/_app/immutable/nodes/4.CIsG7Kdu.js +3 -0
  79. package/web/_app/immutable/nodes/5.D__u9ji8.js +19 -0
  80. package/web/_app/immutable/nodes/6.DBP0fpLt.js +1 -0
  81. package/web/_app/immutable/nodes/7.DuaH9aUr.js +1 -0
  82. package/web/_app/immutable/workers/worker-C_nKscAW.js +1 -0
  83. package/web/_app/version.json +1 -1
  84. package/web/fonts/LythMono-LICENSE.md +112 -0
  85. package/web/fonts/lyth-mono-latin-bold.woff2 +0 -0
  86. package/web/fonts/lyth-mono-latin-bolditalic.woff2 +0 -0
  87. package/web/fonts/lyth-mono-latin-italic.woff2 +0 -0
  88. package/web/fonts/lyth-mono-latin-regular.woff2 +0 -0
  89. package/web/index.html +19 -17
  90. package/web/_app/immutable/assets/0.CtWz9d2A.css +0 -1
  91. package/web/_app/immutable/assets/4.fOHsAKBF.css +0 -1
  92. package/web/_app/immutable/assets/NoteList.E6jC-AtZ.css +0 -1
  93. package/web/_app/immutable/chunks/B7Bf3hc7.js +0 -1
  94. package/web/_app/immutable/chunks/B9TKPEJD.js +0 -1
  95. package/web/_app/immutable/chunks/C7qBBO-i.js +0 -1
  96. package/web/_app/immutable/chunks/DtEVmiYo.js +0 -1
  97. package/web/_app/immutable/chunks/Dx_7xlS4.js +0 -523
  98. package/web/_app/immutable/chunks/wD7RSG2d.js +0 -1
  99. package/web/_app/immutable/entry/app.Av5W3V8v.js +0 -2
  100. package/web/_app/immutable/entry/start.CbpxCvWi.js +0 -1
  101. package/web/_app/immutable/nodes/0.BTVYyqUm.js +0 -3
  102. package/web/_app/immutable/nodes/1.DR-b7PMP.js +0 -1
  103. package/web/_app/immutable/nodes/2.BePyTqoa.js +0 -1
  104. package/web/_app/immutable/nodes/3.D2rfIXQP.js +0 -1
  105. package/web/_app/immutable/nodes/4.7tuUcODS.js +0 -15
  106. package/web/_app/immutable/nodes/5.Cs9hcGU9.js +0 -1
  107. package/web/_app/immutable/nodes/6.DzvF0ZPt.js +0 -1
  108. package/web/_app/immutable/workers/worker-CWkpeSVG.js +0 -1
  109. /package/web/_app/immutable/assets/{6.C00vyVPo.css → 7.C00vyVPo.css} +0 -0
package/dist/main.js CHANGED
@@ -15,7 +15,7 @@ import { serve } from "@hono/node-server";
15
15
  import { Hono } from "hono";
16
16
  import { streamSSE } from "hono/streaming";
17
17
  import { z } from "zod";
18
- import { appendFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
18
+ import { appendFile, mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
19
19
  import { createInterface } from "node:readline";
20
20
  import { PassThrough } from "node:stream";
21
21
  import { createServer } from "node:net";
@@ -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}$/;
@@ -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
 
@@ -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 = {
@@ -4307,6 +4555,69 @@ var Engine = class Engine {
4307
4555
  createNote(id, text) {
4308
4556
  return this.one(() => this.put(id, text, null));
4309
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
+ }
4577
+ async deleteFolder(path) {
4578
+ path = path.trim().replace(/^\/+|\/+$/g, "");
4579
+ if (!path || !inBase(path)) throw new EngineError("only a folder of the base is deleted here");
4580
+ checkNoteId(path);
4581
+ await this.dropFolder(path, true);
4582
+ }
4583
+ /** Take away an empty folder, and the empty ones inside it. A folder made by putting a note in it is not among the
4584
+ * listing's entries, so `strict` (a folder asked for by name) is what says it was not there. */
4585
+ async dropFolder(path, strict) {
4586
+ if (!this.opts.notes.rmdir) throw new EngineError("a folder can't be deleted here");
4587
+ await this.one(async () => {
4588
+ const inside = [...this.files.values()].filter((f) => f.path.startsWith(`${path}/`));
4589
+ const held = inside.filter((f) => !("dir" in f && f.dir)).length;
4590
+ if (strict && !this.files.has(path) && !inside.length) throw new EngineError(`there is no folder ${path}`, 404);
4591
+ if (held) throw new EngineError(`${path} still holds ${held} ${held === 1 ? "file" : "files"}; move or delete them first`, 409);
4592
+ for (const d of [...inside.map((f) => f.path), path].sort((a, b) => b.length - a.length)) {
4593
+ await this.opts.notes.rmdir(d);
4594
+ this.files.delete(d);
4595
+ }
4596
+ this.built = this.index = null;
4597
+ });
4598
+ }
4599
+ async moveFolder(from, to) {
4600
+ const clean = (p) => p.trim().replace(/^\/+|\/+$/g, "");
4601
+ from = clean(from);
4602
+ to = clean(to);
4603
+ if (!from || !to || !inBase(from) || !inBase(to)) throw new EngineError("only a folder of the base is moved, and only within it");
4604
+ checkNoteId(from);
4605
+ checkNoteId(to);
4606
+ if (to === from) throw new EngineError("the folder is there already");
4607
+ if (to.startsWith(`${from}/`)) throw new EngineError("a folder can't be moved into itself");
4608
+ const inside = [...this.files.values()].filter((f) => f.path.startsWith(`${from}/`));
4609
+ const dirs = inside.filter((f) => "dir" in f && f.dir).map((f) => f.path);
4610
+ const notes = inside.filter((f) => !("dir" in f && f.dir) && f.path.endsWith(".md")).map((f) => f.path.slice(0, -3));
4611
+ const others = inside.filter((f) => !("dir" in f && f.dir) && !f.path.endsWith(".md")).length;
4612
+ if (!this.files.has(from) && !inside.length) throw new EngineError(`there is no folder ${from}`, 404);
4613
+ if (others) throw new EngineError(`${from} holds ${others} ${others === 1 ? "file" : "files"} that ${others === 1 ? "isn't a note" : "aren't notes"} (pictures, say); move those by hand first`, 409);
4614
+ const at = (p) => `${to}${p.slice(from.length)}`;
4615
+ if (this.files.has(`${to}.md`)) throw new EngineError(`there is a note at ${to} already`, 409);
4616
+ for (const id of notes) if (this.files.has(`${at(id)}.md`)) throw new EngineError(`there is already a note at ${at(id)}`, 409);
4617
+ for (const d of [from, ...dirs].sort()) await this.makeFolder(at(d));
4618
+ for (const id of notes) await this.moveNote(id, at(id), (await this.readNote(id)).version);
4619
+ await this.dropFolder(from, false);
4620
+ }
4310
4621
  /** `who` is you unless Iso moves it, in Write mode. */
4311
4622
  async moveNote(id, to, base, who = "you") {
4312
4623
  to = to.trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/, "");
@@ -4451,6 +4762,94 @@ var Engine = class Engine {
4451
4762
  version: sha256(bytes).slice(0, 16)
4452
4763
  };
4453
4764
  }
4765
+ /** The code a code link names (M29): its symbol's lines, or the lines it gives, or the file's start. A symbol is
4766
+ * found by its name in the file as it is now, so the link holds where the code has moved. */
4767
+ async code(href, full = false) {
4768
+ const target = codeTarget(href);
4769
+ if (!target) throw new EngineError("not a code link", 400);
4770
+ const { path, symbol } = target, where = await this.repositoryOfBase();
4771
+ const files = where ? this.opts.repository?.(where) ?? null : null;
4772
+ if (!files) return {
4773
+ state: "no-repository",
4774
+ path,
4775
+ ...symbol ? { symbol } : {}
4776
+ };
4777
+ const bytes = await files.readBytes(path);
4778
+ if (!bytes) return {
4779
+ state: "no-file",
4780
+ path,
4781
+ ...symbol ? { symbol } : {}
4782
+ };
4783
+ if (bytes.length > 2e6) return {
4784
+ state: "too-large",
4785
+ path
4786
+ };
4787
+ const lines = new TextDecoder().decode(bytes).split(/\r?\n/);
4788
+ if (lines.at(-1) === "") lines.pop();
4789
+ let start = target.line ?? 1, end = target.end ?? target.line ?? lines.length, count;
4790
+ if (full && symbol) {
4791
+ const found = hasSymbols(path) ? findSymbol(lines.join("\n"), path, symbol) : null;
4792
+ return {
4793
+ state: "ok",
4794
+ path,
4795
+ symbol,
4796
+ start: 1,
4797
+ end: lines.length,
4798
+ total: lines.length,
4799
+ text: lines.join("\n"),
4800
+ ...found ? { mark: {
4801
+ start: found.start,
4802
+ end: found.end
4803
+ } } : {},
4804
+ ...found?.count && found.count > 1 ? { count: found.count } : {}
4805
+ };
4806
+ }
4807
+ if (symbol) {
4808
+ if (!hasSymbols(path)) return {
4809
+ state: "unsupported",
4810
+ path,
4811
+ symbol
4812
+ };
4813
+ const found = findSymbol(lines.join("\n"), path, symbol);
4814
+ if (!found) return {
4815
+ state: "no-symbol",
4816
+ path,
4817
+ symbol
4818
+ };
4819
+ ({start, end, count} = found);
4820
+ }
4821
+ end = Math.min(Math.max(end, start), lines.length);
4822
+ if (start > lines.length) return {
4823
+ state: "no-symbol",
4824
+ path
4825
+ };
4826
+ if (full) return {
4827
+ state: "ok",
4828
+ path,
4829
+ ...symbol ? { symbol } : {},
4830
+ start: 1,
4831
+ end: lines.length,
4832
+ total: lines.length,
4833
+ text: lines.join("\n"),
4834
+ ...symbol || target.line ? { mark: {
4835
+ start,
4836
+ end
4837
+ } } : {},
4838
+ ...count && count > 1 ? { count } : {}
4839
+ };
4840
+ const shown = Math.min(end, start + 120 - 1);
4841
+ return {
4842
+ state: "ok",
4843
+ path,
4844
+ ...symbol ? { symbol } : {},
4845
+ start,
4846
+ end,
4847
+ total: lines.length,
4848
+ text: lines.slice(start - 1, shown).join("\n"),
4849
+ ...shown < end ? { more: true } : {},
4850
+ ...count && count > 1 ? { count } : {}
4851
+ };
4852
+ }
4454
4853
  /** What was read of each picture, kept while its size and time are the same. */
4455
4854
  pictureCache = /* @__PURE__ */ new Map();
4456
4855
  /** Every picture in the notes (M22), found afresh: one put in the folder by hand since the base was opened is
@@ -4556,6 +4955,7 @@ var Engine = class Engine {
4556
4955
  async put(id, text, base, why = "edit", extra = {}, who = "you") {
4557
4956
  const { files, path } = this.where(id);
4558
4957
  if (text.length > 2e6) throw new EngineError("note too large", 413);
4958
+ if (who !== "you" && path.endsWith(".md")) text = repairFrontmatter(text)?.text ?? text;
4559
4959
  const now = await files.read(path);
4560
4960
  if (base === null && now !== null) throw new EngineError(`there is already a note at ${id}`, 409);
4561
4961
  if (base !== null && now === null) throw new Conflict(`${id} is gone`, null);
@@ -4947,6 +5347,8 @@ var MemoryFiles = class {
4947
5347
  files = /* @__PURE__ */ new Map();
4948
5348
  /** Files held as bytes (pictures), apart from text. */
4949
5349
  bytes = /* @__PURE__ */ new Map();
5350
+ /** Folders made empty (M31). */
5351
+ folders = /* @__PURE__ */ new Set();
4950
5352
  constructor(files = {}) {
4951
5353
  for (const [path, text] of Object.entries(files)) this.files.set(path, text);
4952
5354
  }
@@ -4971,10 +5373,20 @@ var MemoryFiles = class {
4971
5373
  this.files.delete(path);
4972
5374
  this.bytes.delete(path);
4973
5375
  }
5376
+ async mkdir(dir) {
5377
+ this.folders.add(dir);
5378
+ }
5379
+ async rmdir(dir) {
5380
+ this.folders.delete(dir);
5381
+ }
4974
5382
  async list(dir) {
4975
5383
  const prefix = dir ? dir + "/" : "";
4976
5384
  const found = /* @__PURE__ */ new Map();
4977
- for (const path of [...this.files.keys(), ...this.bytes.keys()]) {
5385
+ for (const path of [
5386
+ ...this.files.keys(),
5387
+ ...this.bytes.keys(),
5388
+ ...[...this.folders].map((f) => f + "/")
5389
+ ]) {
4978
5390
  if (!path.startsWith(prefix)) continue;
4979
5391
  const rest = path.slice(prefix.length), cut = rest.indexOf("/");
4980
5392
  if (cut >= 0) {
@@ -6155,7 +6567,7 @@ var AcpFollower = class {
6155
6567
  };
6156
6568
  //#endregion
6157
6569
  //#region packages/server/src/version.ts
6158
- const VERSION = "0.15.0";
6570
+ const VERSION = "0.17.0";
6159
6571
  //#endregion
6160
6572
  //#region packages/server/src/connect.ts
6161
6573
  const HARNESSES = [{
@@ -6989,6 +7401,10 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6989
7401
  if (c.req.header("if-none-match") === etag) return c.body(null, 304, headers);
6990
7402
  return c.body(found.bytes, 200, headers);
6991
7403
  });
7404
+ app.get("/api/code", async (c) => {
7405
+ const href = c.req.query("href") ?? "";
7406
+ return answer(c, () => api.code(href, c.req.query("full") === "1"));
7407
+ });
6992
7408
  app.put("/api/note", async (c) => {
6993
7409
  const refused = writeRefused(c);
6994
7410
  if (refused) return refused;
@@ -7026,6 +7442,12 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
7026
7442
  to: z.string().max(400),
7027
7443
  base: z.string().max(64)
7028
7444
  }).strict(), (c, b) => api.moveNote(id(c), b.to, b.base));
7445
+ post("/api/folder", z.object({ path: z.string().max(400) }).strict(), (_c, b) => api.makeFolder(b.path));
7446
+ post("/api/folder/delete", z.object({ path: z.string().max(400) }).strict(), (_c, b) => api.deleteFolder(b.path));
7447
+ post("/api/folder/move", z.object({
7448
+ from: z.string().max(400),
7449
+ to: z.string().max(400)
7450
+ }).strict(), (_c, b) => api.moveFolder(b.from, b.to));
7029
7451
  post("/api/note/delete", z.object({ base: z.string().max(64) }).strict(), (c, b) => api.deleteNote(id(c), b.base));
7030
7452
  post("/api/note/done", z.object({
7031
7453
  done: z.boolean(),
@@ -7401,6 +7823,16 @@ var NodeFiles = class {
7401
7823
  throw e;
7402
7824
  }
7403
7825
  }
7826
+ async mkdir(dir) {
7827
+ await mkdir(this.at(dir), { recursive: true });
7828
+ }
7829
+ async rmdir(dir) {
7830
+ try {
7831
+ await rmdir(this.at(dir));
7832
+ } catch (e) {
7833
+ if (e.code !== "ENOENT") throw e;
7834
+ }
7835
+ }
7404
7836
  async append(path, text) {
7405
7837
  const full = this.at(path);
7406
7838
  await mkdir(dirname(full), { recursive: true });
@@ -7757,12 +8189,14 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
7757
8189
  console.error(e.code === "EADDRINUSE" ? `port ${port} is in use (isorite serve --port <n>)` : e.message);
7758
8190
  process.exit(1);
7759
8191
  });
7760
- console.log(`isorite: ${notes}`);
7761
- console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model, 53 MB)");
8192
+ const row = (label, text) => console.log(` ${dim(label.padEnd(8))}${text}`);
8193
+ console.log(`${bold("isorite")} ${notes}`);
8194
+ row("search", embedder ? "by meaning" : `by words ${dim("(isorite model fetches the model, 53 MB)")}`);
7762
8195
  const lent = bases.lent().filter((l) => l.name !== bases.first).map((l) => l.name);
7763
- if (lent.length) console.log(` lent to every session: ${lent.join(", ")} (isorite bases)`);
7764
- 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/`);
7765
- 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()}`);
8196
+ if (lent.length) row("lent", `${lent.join(", ")} ${dim("(isorite bases)")}`);
8197
+ if (existsSync(join(WEB_DIR, "index.html"))) row("open", link(`http://localhost:${port}/`));
8198
+ else row("open", `the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
8199
+ if (process.env.SSH_CONNECTION && LOOPBACK.has(host)) row("ssh", `opens on this machine; from your own, first: ssh -L ${port}:localhost:${port} ${hostname()}`);
7766
8200
  stop = () => {
7767
8201
  server.close();
7768
8202
  process.exit(0);
@@ -7781,6 +8215,12 @@ const MARK = {
7781
8215
  warn: "!",
7782
8216
  skip: "–"
7783
8217
  };
8218
+ const PAINT = {
8219
+ ok: green,
8220
+ fail: red,
8221
+ warn: yellow,
8222
+ skip: dim
8223
+ };
7784
8224
  /** The environment variables isorite and Node read for it; the secret ones are shown only by length. */
7785
8225
  const READ = [
7786
8226
  "XDG_CONFIG_HOME",
@@ -7823,7 +8263,7 @@ async function doctor(folder, o = {}) {
7823
8263
  outcome,
7824
8264
  said
7825
8265
  });
7826
- if (!o.report) out(`${MARK[outcome]} ${step}: ${said}`);
8266
+ if (!o.report) out(`${PAINT[outcome](MARK[outcome])} ${bold(step)}: ${outcome === "skip" ? dim(said) : said}`);
7827
8267
  };
7828
8268
  const path = configPath(env), execArgv = o.execArgv ?? process.execArgv;
7829
8269
  if (!existsSync(path)) say("config", "ok", `there is no ${path} yet: it is made when a base is first opened`);
@@ -7931,7 +8371,7 @@ async function doctor(folder, o = {}) {
7931
8371
  ...checks.map((c) => `${MARK[c.outcome]} ${c.step}: ${c.said}`),
7932
8372
  "```"
7933
8373
  ].join("\n"));
7934
- } else out(failed ? `${failed} failed. isorite doctor --report gives all this as one block, its secrets hidden, to paste to someone helping.` : "nothing failed");
8374
+ } 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"));
7935
8375
  return failed ? 1 : 0;
7936
8376
  }
7937
8377
  //#endregion
@@ -9521,7 +9961,7 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
9521
9961
  return 0;
9522
9962
  }
9523
9963
  const width = Math.max(...known.map((b) => b.name.length));
9524
- for (const b of known) out(`${b.name.padEnd(width)} ${b.lend ? "lent " : " "} ${b.path}${existsSync(b.path) ? "" : " (not there)"}`);
9964
+ for (const b of known) out(`${bold(b.name.padEnd(width))} ${b.lend ? "lent " : " "} ${dim(b.path)}${existsSync(b.path) ? "" : ` ${red("(not there)")}`}`);
9525
9965
  return 0;
9526
9966
  }
9527
9967
  //#endregion
@@ -9591,7 +10031,7 @@ async function start(io) {
9591
10031
  if (found.kind === "base") return io.open(found.notes);
9592
10032
  if (found.kind === "several") {
9593
10033
  io.out(`isorite: more than one base here:`);
9594
- found.notes.forEach((n, i) => io.out(` ${i + 1} ${n}`));
10034
+ found.notes.forEach((n, i) => io.out(` ${dim(String(i + 1))} ${n}`));
9595
10035
  const pick = await io.choose("Open which?", found.notes.length);
9596
10036
  return pick === null ? 2 : io.open(found.notes[pick]);
9597
10037
  }
@@ -9612,7 +10052,7 @@ async function start(io) {
9612
10052
  "the guide"
9613
10053
  ];
9614
10054
  io.out(known.length ? "isorite: the bases on this machine, last used first:" : "isorite:");
9615
- rows.forEach((r, i) => io.out(` ${i + 1} ${r}`));
10055
+ rows.forEach((r, i) => io.out(` ${dim(String(i + 1))} ${r}`));
9616
10056
  const pick = await io.choose("Open which?", rows.length);
9617
10057
  if (pick === null) return 2;
9618
10058
  if (pick < known.length) return io.open(known[pick].path);
@@ -9639,62 +10079,66 @@ init_embedder();
9639
10079
  init_model();
9640
10080
  const SHORT = `isorite: an agentic harness for understanding
9641
10081
 
9642
- isorite open the base here, or choose one (a terminal; --no-open, --port <n>)
9643
- isorite doctor whether isorite can work here, and what to change if not (--report to paste to
9644
- someone helping; run in a project it checks that project's base)
9645
- isorite help this; isorite help all lists every command
10082
+ isorite open the base here, or choose one
10083
+ isorite doctor can isorite work here, and what to change if not
10084
+ isorite help this; help all lists every command
9646
10085
  isorite --version
9647
10086
 
9648
- Notes are plain Markdown in a folder of your project; your own agent reads and writes them with you.
10087
+ Notes are plain Markdown in a folder of your project. Your agent reads and writes them with you.
9649
10088
  `;
9650
10089
  const USAGE = `isorite: an agentic harness for understanding
9651
10090
 
9652
- isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
9653
- isorite check <notes> [--unused] check a folder of notes: frontmatter, links, cycles; --unused lists
9654
- the pictures no note shows
10091
+ isorite new <folder> a new base: an empty folder with .isorite/
10092
+ isorite check <notes> [--unused] [--fix]
10093
+ lint: frontmatter, links, cycles; --unused: pictures no note
10094
+ shows; --fix: quote a colon in frontmatter
9655
10095
  isorite serve <notes> [--port <n>] [--allow-host <name>]
9656
10096
  open it in the browser, on this machine only
9657
- isorite migrate <study base> [--merge] move a study base (format 1) into its notes' .isorite/; exercises
9658
- become problem notes; the study base is left as it was
9659
- isorite example <folder> [--geometry] write the example base (notes on an N-body simulation, three weeks
9660
- into being built), into an empty folder; --geometry: notes on the
9661
- differential geometry of surfaces, with their questions and problems
10097
+ isorite migrate <study base> [--merge] a study base (format 1) into its notes' .isorite/
10098
+ isorite example <folder> [--geometry] an example base, partway built, in an empty folder
9662
10099
  isorite mcp [<notes>] [--port <n>] [--agent <name>]
9663
- the knowledge base's tools over MCP, on stdio, for your agent in its
9664
- own terminal: claude mcp add isorite -- isorite mcp <base>
10100
+ the base's tools over MCP, on stdio:
10101
+ claude mcp add isorite -- isorite mcp <base>
9665
10102
  isorite acp --agent "<command>" [<notes>] [--port <n>] [--pane]
9666
- an ACP agent (claude-agent-acp, codex-acp, opencode acp, hermes acp)
9667
- behind your ACP front end (Toad, Zed), with the base's MCP, and its
9668
- sessions in Iso's pane; --pane: no front end, the pane alone
10103
+ an ACP agent behind your ACP front end, its sessions in Iso's
10104
+ pane; --pane: the pane alone
9669
10105
  isorite model [--from <folder|.tgz>] [--url <url>]
9670
- the model for search by meaning: which is installed, and where; else
9671
- fetched (53 MB, once) from ISORITE_MODEL_BASE_URL, npm, GitHub or
9672
- Hugging Face, or --url alone; or from the .tgz fetched by hand, or a
9673
- folder: its files copied there, or any compatible model
10106
+ the model for search by meaning: which, and where; else fetch it
10107
+ (53 MB, once)
9674
10108
  isorite doctor [<notes>] [--report] [--offline]
9675
- whether isorite can work here: the configuration, the network, the
9676
- model for search by meaning, the server, the notes and the project's
9677
- MCP config; --report gives it as one block, to paste to someone (or
9678
- an AI) helping
9679
- isorite memory [<folder>] the agent's memory: a knowledge base (~/isorite-memory unless named),
9680
- a git repository of its own, lent to every agent's session
9681
- isorite bases [lend|keep <name>] the bases this device knows, each listed as it is opened; lend one to
9682
- every session, or keep it to itself
9683
- isorite help setup how isorite is set up, every setting and failure: written for an AI
9684
- helping you set it up (docs/setup.md is for people)
9685
- isorite guide [<folder>] [--port <n>] the guide: a knowledge base about isorite, with things to do in it,
9686
- written into a folder (~/isorite-guide unless named) and opened in
9687
- the browser; run again, as you left it (also: isorite --tutorial)
10109
+ can isorite work here, a step at a time; --report: one block to
10110
+ paste to someone helping
10111
+ isorite memory [<folder>] the agent's memory: a base of its own, lent to every session
10112
+ isorite bases [lend|keep <name>] the bases this device knows; lend one to every session, or keep it
10113
+ isorite help setup every setting and failure, written for an AI helping you set up
10114
+ isorite guide [<folder>] [--port <n>] the guide: a base about isorite, to do things in (--tutorial)
9688
10115
  `;
9689
- function check(notes, out = console.log, unused = false) {
10116
+ function check(notes, out = console.log, unused = false, fix = false) {
9690
10117
  const root = resolve(notes);
9691
10118
  if (!existsSync(root) || !statSync(root).isDirectory()) {
9692
10119
  out(`no folder at ${root}`);
9693
10120
  return 2;
9694
10121
  }
9695
- const bundle = loadBundle(root), issues = bundle.lint(), cycles = bundle.requiresCycles();
9696
- for (const i of issues) out(`${i.level.padEnd(7)} ${i.path}: ${i.message}`);
9697
- for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
10122
+ let bundle = loadBundle(root), issues = bundle.lint();
10123
+ const mendable = issues.filter((i) => i.code === "frontmatter-invalid").flatMap((i) => {
10124
+ const m = repairFrontmatter(readFileSync(join(root, i.path), "utf8"));
10125
+ return m ? [{
10126
+ path: i.path,
10127
+ ...m
10128
+ }] : [];
10129
+ });
10130
+ if (fix && mendable.length) {
10131
+ for (const m of mendable) {
10132
+ writeFileSync(join(root, m.path), m.text);
10133
+ out(`fixed ${m.path}: quoted ${m.fixed.join(", ")}`);
10134
+ }
10135
+ bundle = loadBundle(root);
10136
+ issues = bundle.lint();
10137
+ }
10138
+ const cycles = bundle.requiresCycles();
10139
+ const level = (l) => (l === "error" ? red(l) : yellow(l)) + " ".repeat(Math.max(0, 7 - l.length));
10140
+ 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)" : ""}`);
10141
+ for (const c of cycles) out(`${level("warning")} requires cycle: ${c.join(" -> ")}`);
9698
10142
  let artifactErrors = 0, artifactWarnings = 0;
9699
10143
  for (const f of [...bundle.files].filter((x) => citeKind(x) === "artifact").sort()) {
9700
10144
  const html = readFileSync(join(root, f), "utf8");
@@ -9711,13 +10155,14 @@ function check(notes, out = console.log, unused = false) {
9711
10155
  files,
9712
10156
  columns
9713
10157
  })) {
9714
- out(`${b.level.padEnd(7)} ${f}: ${b.message}`);
10158
+ out(`${level(b.level)} ${bold(f)}: ${b.message}`);
9715
10159
  if (b.level === "error") artifactErrors++;
9716
10160
  else artifactWarnings++;
9717
10161
  }
9718
10162
  }
9719
10163
  const errors = issues.filter((i) => i.level === "error").length + artifactErrors;
9720
- 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`);
10164
+ const warnings = issues.length - issues.filter((i) => i.level === "error").length + artifactWarnings;
10165
+ 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`);
9721
10166
  if (unused) {
9722
10167
  const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
9723
10168
  const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();
@@ -9803,7 +10248,7 @@ function main(argv) {
9803
10248
  noOpen: args.includes("--no-open")
9804
10249
  });
9805
10250
  }
9806
- if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
10251
+ if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"), rest.includes("--fix"));
9807
10252
  if (command === "new" && rest[0]) {
9808
10253
  const made = newBase(rest[0]);
9809
10254
  const next = `${made}: a knowledge base, empty. Open it in the browser, and attach your agent from Iso's pane:\n isorite serve ${made}`;
@@ -10001,7 +10446,7 @@ function main(argv) {
10001
10446
  }
10002
10447
  throw e;
10003
10448
  }
10004
- process.stdout.write(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT);
10449
+ process.stdout.write(helpText(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT));
10005
10450
  return command && command !== "help" && command !== "--help" ? 2 : 0;
10006
10451
  }
10007
10452
  /** The commands that start something, which say so at once (M21): a subprocess that stalls after this is then