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.
- package/dist/main.js +507 -62
- package/guide/out-of-the-base/web-and-code-links.md +3 -1
- package/package.json +1 -1
- package/web/_app/immutable/assets/0.DVQOzs4l.css +1 -0
- package/web/_app/immutable/assets/4.Dn5sJ8uq.css +1 -0
- package/web/_app/immutable/assets/5.DJhOSn7C.css +1 -0
- package/web/_app/immutable/assets/NoteList.Wxp_AH0Y.css +1 -0
- package/web/_app/immutable/chunks/{BAVnZFPa.js → 0KrpTYPM.js} +1 -1
- package/web/_app/immutable/chunks/{f7mFVWQS.js → 1MewxdCy.js} +1 -1
- package/web/_app/immutable/chunks/{DFo4P1vU.js → 1W6iRjg5.js} +1 -1
- package/web/_app/immutable/chunks/{BQeONalw.js → 2BcU70Iq.js} +1 -1
- package/web/_app/immutable/chunks/{CVqwkR8N.js → B-04K_Pl.js} +1 -1
- package/web/_app/immutable/chunks/{B_x6_Y3m.js → BBU5FdEo.js} +1 -1
- package/web/_app/immutable/chunks/{Djo-ijeo.js → BBhr3bLI.js} +1 -1
- package/web/_app/immutable/chunks/{CSKABQj2.js → BCsFs14H.js} +1 -1
- package/web/_app/immutable/chunks/BJOSVfg-.js +1 -0
- package/web/_app/immutable/chunks/{zasx25UV.js → BNlziJ_1.js} +3 -3
- package/web/_app/immutable/chunks/{BPpUSQ2Y.js → BbvUCVY_.js} +1 -1
- package/web/_app/immutable/chunks/{e7GRa0yc.js → BjLgp18-.js} +1 -1
- package/web/_app/immutable/chunks/{OG9GsKke.js → Bx_Vz2qr.js} +1 -1
- package/web/_app/immutable/chunks/{D7jLdCNu.js → C2EzU0qX.js} +1 -1
- package/web/_app/immutable/chunks/{DH_19--P.js → C2QvvHIN.js} +1 -1
- package/web/_app/immutable/chunks/{DJQ6favH.js → CAuYgj5q.js} +1 -1
- package/web/_app/immutable/chunks/{hjSZ041d.js → CB6ztdCQ.js} +1 -1
- package/web/_app/immutable/chunks/CHjmZsHD.js +1 -0
- package/web/_app/immutable/chunks/CM4EA3Wo.js +1 -0
- package/web/_app/immutable/chunks/{BsY4iMci.js → CMN7rSZK.js} +1 -1
- package/web/_app/immutable/chunks/CQhfj5-f.js +9 -0
- package/web/_app/immutable/chunks/{CSTv2fL0.js → CRbUrapz.js} +1 -1
- package/web/_app/immutable/chunks/{BLzZIf7l.js → CabmfniL.js} +1 -1
- package/web/_app/immutable/chunks/{C9Qdslk3.js → CcRcjPTM.js} +1 -1
- package/web/_app/immutable/chunks/{BDJlNL-I.js → CccIbhmH.js} +85 -85
- package/web/_app/immutable/chunks/{C1FDDux-.js → CdD6waB-.js} +1 -1
- package/web/_app/immutable/chunks/{CwHHpacD.js → CfTuc06U.js} +1 -1
- package/web/_app/immutable/chunks/{Mwr1r9aw.js → Cl-Xmm9N.js} +1 -1
- package/web/_app/immutable/chunks/{DxRGLDbV.js → CmFzeyfe.js} +1 -1
- package/web/_app/immutable/chunks/{CUNetmvQ.js → Cn7j5NKi.js} +1 -1
- package/web/_app/immutable/chunks/{DWwu8a1P.js → CpJ4vfn-.js} +1 -1
- package/web/_app/immutable/chunks/{MrW4zd8l.js → CwcA6hab.js} +1 -1
- package/web/_app/immutable/chunks/{AnnzTlE8.js → CxEu-Di8.js} +1 -1
- package/web/_app/immutable/chunks/{CUB9vnd0.js → DEwfz-Il.js} +1 -1
- package/web/_app/immutable/chunks/DH2bqH7t.js +517 -0
- package/web/_app/immutable/chunks/{CZSmkKvQ.js → DJHLXLij.js} +1 -1
- package/web/_app/immutable/chunks/{Kem7IXj7.js → DK9wjWIk.js} +1 -1
- package/web/_app/immutable/chunks/{BLe29cGh.js → DMTkA0aK.js} +1 -1
- package/web/_app/immutable/chunks/{D1MhxXyn.js → DMpFmiFg.js} +1 -1
- package/web/_app/immutable/chunks/{DW5uy3Jw.js → DQJRaTNz.js} +1 -1
- package/web/_app/immutable/chunks/{DJT73ARj.js → DRKiKCov.js} +1 -1
- package/web/_app/immutable/chunks/{DEQsS4Ez.js → DS7XcdHs.js} +1 -1
- package/web/_app/immutable/chunks/{0B47b24t.js → D_xpsaTJ.js} +1 -1
- package/web/_app/immutable/chunks/{LbnMg2Kc.js → Ddw9FbDu.js} +1 -1
- package/web/_app/immutable/chunks/{D4EpFhYZ.js → DeazQJkV.js} +1 -1
- package/web/_app/immutable/chunks/{Ch5Z7cbP.js → Df3spPXn.js} +1 -1
- package/web/_app/immutable/chunks/{1j--pDqt.js → DfLI-CvW.js} +1 -1
- package/web/_app/immutable/chunks/{Bytzbt5l.js → Dns-yz2Q.js} +1 -1
- package/web/_app/immutable/chunks/{mhWk5y4o.js → DqZ4UzUH.js} +1 -1
- package/web/_app/immutable/chunks/{BtcGx-KQ.js → DrPF-vh8.js} +1 -1
- package/web/_app/immutable/chunks/{DAVMwq6b.js → Ds9L6FPB.js} +1 -1
- package/web/_app/immutable/chunks/{CWc-mNzo.js → Dzu8v7nB.js} +1 -1
- package/web/_app/immutable/chunks/IECP4ppk.js +1 -0
- package/web/_app/immutable/chunks/NgkOM4d0.js +1 -0
- package/web/_app/immutable/chunks/{ZTEErEOd.js → ZDjPMhAb.js} +1 -1
- package/web/_app/immutable/chunks/ZDkogIhD.js +1 -0
- package/web/_app/immutable/chunks/{CcE14TDI.js → _zY4mrA0.js} +1 -1
- package/web/_app/immutable/chunks/{B7LnLClC.js → fpNFwizw.js} +1 -1
- package/web/_app/immutable/chunks/{BZX8cNYL.js → qfBACIi9.js} +1 -1
- package/web/_app/immutable/chunks/{Cpjqsnqr.js → rv_cF9pp.js} +1 -1
- package/web/_app/immutable/chunks/{DY0M2GMn.js → sBy91tGs.js} +1 -1
- package/web/_app/immutable/chunks/{BiHyHFlW.js → vGOSIW7X.js} +1 -1
- package/web/_app/immutable/chunks/{Ih6r6QR2.js → yYKJ4mbq.js} +1 -1
- package/web/_app/immutable/chunks/{DbtvzL7r.js → yy6sD_us.js} +1 -1
- package/web/_app/immutable/entry/app.CX19wIuG.js +2 -0
- package/web/_app/immutable/entry/start.DaiosA4i.js +1 -0
- package/web/_app/immutable/nodes/0.B6_Ycc14.js +3 -0
- package/web/_app/immutable/nodes/1.JA1sPybf.js +1 -0
- package/web/_app/immutable/nodes/2.DN2phYmE.js +1 -0
- package/web/_app/immutable/nodes/3.CbnLNp2F.js +1 -0
- package/web/_app/immutable/nodes/4.CIsG7Kdu.js +3 -0
- package/web/_app/immutable/nodes/5.D__u9ji8.js +19 -0
- package/web/_app/immutable/nodes/6.DBP0fpLt.js +1 -0
- package/web/_app/immutable/nodes/7.DuaH9aUr.js +1 -0
- package/web/_app/immutable/workers/worker-C_nKscAW.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/fonts/LythMono-LICENSE.md +112 -0
- package/web/fonts/lyth-mono-latin-bold.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-bolditalic.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-italic.woff2 +0 -0
- package/web/fonts/lyth-mono-latin-regular.woff2 +0 -0
- package/web/index.html +19 -17
- package/web/_app/immutable/assets/0.CtWz9d2A.css +0 -1
- package/web/_app/immutable/assets/4.fOHsAKBF.css +0 -1
- package/web/_app/immutable/assets/NoteList.E6jC-AtZ.css +0 -1
- package/web/_app/immutable/chunks/B7Bf3hc7.js +0 -1
- package/web/_app/immutable/chunks/B9TKPEJD.js +0 -1
- package/web/_app/immutable/chunks/C7qBBO-i.js +0 -1
- package/web/_app/immutable/chunks/DtEVmiYo.js +0 -1
- package/web/_app/immutable/chunks/Dx_7xlS4.js +0 -523
- package/web/_app/immutable/chunks/wD7RSG2d.js +0 -1
- package/web/_app/immutable/entry/app.Av5W3V8v.js +0 -2
- package/web/_app/immutable/entry/start.CbpxCvWi.js +0 -1
- package/web/_app/immutable/nodes/0.BTVYyqUm.js +0 -3
- package/web/_app/immutable/nodes/1.DR-b7PMP.js +0 -1
- package/web/_app/immutable/nodes/2.BePyTqoa.js +0 -1
- package/web/_app/immutable/nodes/3.D2rfIXQP.js +0 -1
- package/web/_app/immutable/nodes/4.7tuUcODS.js +0 -15
- package/web/_app/immutable/nodes/5.Cs9hcGU9.js +0 -1
- package/web/_app/immutable/nodes/6.DzvF0ZPt.js +0 -1
- package/web/_app/immutable/workers/worker-CWkpeSVG.js +0 -1
- /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
|
|
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+))
|
|
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
|
|
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. \`\`
|
|
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
|
-
|
|
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 [
|
|
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.
|
|
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(`
|
|
7761
|
-
console.log(
|
|
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)
|
|
7764
|
-
|
|
7765
|
-
|
|
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
|
|
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) ? "" :
|
|
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
|
|
9643
|
-
isorite doctor
|
|
9644
|
-
|
|
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
|
|
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>
|
|
9653
|
-
isorite check <notes> [--unused]
|
|
9654
|
-
|
|
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]
|
|
9658
|
-
|
|
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
|
|
9664
|
-
|
|
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
|
|
9667
|
-
|
|
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
|
|
9671
|
-
|
|
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
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
isorite
|
|
9680
|
-
|
|
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
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
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
|
|
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
|
-
|
|
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
|