isorite 0.8.1 → 0.10.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/core.js +84 -10
- package/dist/main.js +692 -99
- package/docs/setup-for-agents.md +8 -5
- package/docs/setup.md +31 -4
- package/docs/terminal.md +9 -3
- package/notes/algorithms/the-quadtree/subdivision.svg +15 -0
- package/notes/algorithms/the-quadtree.md +2 -0
- package/package.json +1 -1
- package/plugin/server.ts +1 -1
- package/plugin/tui.tsx +6 -0
- package/web/_app/immutable/assets/{0.CTbO0owi.css → 0.CPW9fjPu.css} +1 -1
- package/web/_app/immutable/assets/NoteEditor.Wt5vU9Tb.css +1 -0
- package/web/_app/immutable/chunks/{8wVGBFSs.js → -2Meesge.js} +3 -3
- package/web/_app/immutable/chunks/{C_ER1Any.js → 6RXH8WsD.js} +1 -1
- package/web/_app/immutable/chunks/{BL3mLym4.js → 9z49435t.js} +1 -1
- package/web/_app/immutable/chunks/B0A4Snxv.js +6 -0
- package/web/_app/immutable/chunks/B8Qvvm53.js +1 -0
- package/web/_app/immutable/chunks/{DMAYXnpq.js → BA8Hpxhg.js} +1 -1
- package/web/_app/immutable/chunks/{CKSRTWxH.js → BAclEN0O.js} +1 -1
- package/web/_app/immutable/chunks/BJo70SOc.js +1 -0
- package/web/_app/immutable/chunks/{DECNRCf2.js → BLmF1B2i.js} +1 -1
- package/web/_app/immutable/chunks/{CiVjR8gq.js → BTclWEfx.js} +1 -1
- package/web/_app/immutable/chunks/{Cp_JbuGh.js → BWVMfAMh.js} +1 -1
- package/web/_app/immutable/chunks/{D4fLEcOQ.js → BXAOBcPb.js} +1 -1
- package/web/_app/immutable/chunks/BYSDTLqw.js +1 -0
- package/web/_app/immutable/chunks/{Cgml88Pl.js → BcgO-fBX.js} +1 -1
- package/web/_app/immutable/chunks/BimZp6R0.js +523 -0
- package/web/_app/immutable/chunks/{CO2Y5y64.js → Bq2GBAJ0.js} +1 -1
- package/web/_app/immutable/chunks/BqrMZ9MX.js +1 -0
- package/web/_app/immutable/chunks/{BN6xhG8e.js → Bt7VOT5H.js} +1 -1
- package/web/_app/immutable/chunks/{BGbXxNFa.js → BuUzVkG2.js} +1 -1
- package/web/_app/immutable/chunks/{Brxvm_b1.js → By4S_gy5.js} +1 -1
- package/web/_app/immutable/chunks/{CRIvE37b.js → BymwHy7o.js} +1 -1
- package/web/_app/immutable/chunks/{B4sg4e8i.js → C5MPRibb.js} +1 -1
- package/web/_app/immutable/chunks/CBkjDkEN.js +151 -0
- package/web/_app/immutable/chunks/{BrqJKzQU.js → CD6xPodA.js} +1 -1
- package/web/_app/immutable/chunks/{CEufJyoj.js → CQd9D8Nc.js} +1 -1
- package/web/_app/immutable/chunks/{BrGmYbJT.js → CT_Ry4q8.js} +1 -1
- package/web/_app/immutable/chunks/{CQadubS9.js → C_amgxV1.js} +1 -1
- package/web/_app/immutable/chunks/CiU6tYJv.js +1 -0
- package/web/_app/immutable/chunks/{spNKdrPy.js → CjtKPNlv.js} +1 -1
- package/web/_app/immutable/chunks/{DxLkKbK2.js → Cl5zRP_n.js} +1 -1
- package/web/_app/immutable/chunks/{yXUzBjUq.js → CsRPJjQS.js} +1 -1
- package/web/_app/immutable/chunks/{CETSRtnV.js → CyFLqlvP.js} +1 -1
- package/web/_app/immutable/chunks/{Bq5hCPe1.js → Czz9qOh8.js} +1 -1
- package/web/_app/immutable/chunks/{BguXRgvc.js → D3-rqfsM.js} +1 -1
- package/web/_app/immutable/chunks/{bcQIrid8.js → D5XlJBy8.js} +1 -1
- package/web/_app/immutable/chunks/{TuFg0KqZ.js → D5vocsTl.js} +1 -1
- package/web/_app/immutable/chunks/{CTscJvp6.js → D95RKsvN.js} +1 -1
- package/web/_app/immutable/chunks/{BzI3yqBQ.js → DBC2xjOo.js} +1 -1
- package/web/_app/immutable/chunks/{C1hqerMA.js → DCD8_jrW.js} +1 -1
- package/web/_app/immutable/chunks/{BsLNwr5A.js → DF89zyH-.js} +1 -1
- package/web/_app/immutable/chunks/{DsIcKd8x.js → DQjIi5Jv.js} +1 -1
- package/web/_app/immutable/chunks/{C-dfcq9W.js → DRX-TQzj.js} +1 -1
- package/web/_app/immutable/chunks/{BunrC_8P.js → DX4bJJO0.js} +1 -1
- package/web/_app/immutable/chunks/{EQVRMnZU.js → DXK7PB4g.js} +1 -1
- package/web/_app/immutable/chunks/{mbLqADPr.js → DXTK-eyo.js} +1 -1
- package/web/_app/immutable/chunks/{wCTXGvXm.js → DbNETcPs.js} +1 -1
- package/web/_app/immutable/chunks/{CX-xckzu.js → DfCZPwp1.js} +1 -1
- package/web/_app/immutable/chunks/{DKNOMI4Y.js → Dh1lBxfx.js} +1 -1
- package/web/_app/immutable/chunks/{BgczuZKA.js → DkxkvOjx.js} +1 -1
- package/web/_app/immutable/chunks/DlKQFacy.js +1 -0
- package/web/_app/immutable/chunks/{0M1khDs2.js → DlXrOzlh.js} +1 -1
- package/web/_app/immutable/chunks/{B85D_PlV.js → Dpa4Syyy.js} +1 -1
- package/web/_app/immutable/chunks/DshHzxgH.js +1 -0
- package/web/_app/immutable/chunks/{CdY1YAit.js → EMo1IeTW.js} +1 -1
- package/web/_app/immutable/chunks/Iqr2CudW.js +3 -0
- package/web/_app/immutable/chunks/{CoyQYage.js → RFxekmX-.js} +1 -1
- package/web/_app/immutable/chunks/{vk5Wkr31.js → RlApGeew.js} +1 -1
- package/web/_app/immutable/chunks/{CLVUDRRk.js → Xm0YTnxV.js} +1 -1
- package/web/_app/immutable/chunks/c7PFFMl4.js +1 -0
- package/web/_app/immutable/chunks/{CZ8wTb_M.js → gPtCGHiy.js} +1 -1
- package/web/_app/immutable/chunks/{D4SKbj9K.js → oWM_kgYq.js} +1 -1
- package/web/_app/immutable/chunks/{wj7G6pU5.js → rghE0CVz.js} +1 -1
- package/web/_app/immutable/chunks/{B9YLdnR_.js → tUSDgbpy.js} +1 -1
- package/web/_app/immutable/chunks/{DTdlqvEb.js → vfDvz3qP.js} +1 -1
- package/web/_app/immutable/chunks/{BRKS-CCX.js → whhiiiga.js} +1 -1
- package/web/_app/immutable/entry/app.iw6ZIOh2.js +2 -0
- package/web/_app/immutable/entry/start.CFnKMSWs.js +1 -0
- package/web/_app/immutable/nodes/0.BzEgwgsa.js +26 -0
- package/web/_app/immutable/nodes/1.rzuq9VRh.js +1 -0
- package/web/_app/immutable/nodes/2.D_jWsQLW.js +1 -0
- package/web/_app/immutable/nodes/3.BzRx3SiD.js +1 -0
- package/web/_app/immutable/nodes/4.DP2MGav1.js +2 -0
- package/web/_app/immutable/nodes/5.BPS-CCQT.js +5 -0
- package/web/_app/immutable/nodes/6.DQMCyNJ3.js +1 -0
- package/web/_app/immutable/nodes/7.MWEn-qPC.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +17 -17
- package/web/_app/immutable/assets/NoteEditor.8wWtZK_A.css +0 -1
- package/web/_app/immutable/chunks/B2TCcw-P.js +0 -6
- package/web/_app/immutable/chunks/BAkGB2fM.js +0 -1
- package/web/_app/immutable/chunks/CHbKgHy3.js +0 -1
- package/web/_app/immutable/chunks/CMTjeXEe.js +0 -1
- package/web/_app/immutable/chunks/CurmVJyp.js +0 -1
- package/web/_app/immutable/chunks/D29fKEzc.js +0 -1
- package/web/_app/immutable/chunks/D3BEa5-_.js +0 -673
- package/web/_app/immutable/chunks/DPnfDiTn.js +0 -1
- package/web/_app/immutable/chunks/DZYaqCwi.js +0 -1
- package/web/_app/immutable/chunks/D_19S_Iu.js +0 -1
- package/web/_app/immutable/chunks/DtPULAe0.js +0 -1
- package/web/_app/immutable/chunks/nMarIA7J.js +0 -3
- package/web/_app/immutable/entry/app.idRgnoYp.js +0 -2
- package/web/_app/immutable/entry/start.CEx_UQO6.js +0 -1
- package/web/_app/immutable/nodes/0.BUd_gY_8.js +0 -26
- package/web/_app/immutable/nodes/1.DHK7VmG0.js +0 -1
- package/web/_app/immutable/nodes/2.CDAt-6SJ.js +0 -1
- package/web/_app/immutable/nodes/3.4Wv5nmaw.js +0 -1
- package/web/_app/immutable/nodes/4.Ceb5g4Ps.js +0 -2
- package/web/_app/immutable/nodes/5.DioYOANs.js +0 -5
- package/web/_app/immutable/nodes/6.CzYfNb6g.js +0 -1
- package/web/_app/immutable/nodes/7.BxQaF3lD.js +0 -1
package/dist/main.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { Document, Scalar, isMap, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
6
6
|
import MarkdownIt from "markdown-it";
|
|
7
7
|
import { parse, stringify as stringify$1 } from "smol-toml";
|
|
8
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
8
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
10
|
import { arch, constants, homedir, hostname, platform, release, tmpdir } from "node:os";
|
|
10
11
|
import { createSecureContext, getCACertificates, setDefaultCACertificates } from "node:tls";
|
|
11
|
-
import {
|
|
12
|
+
import { get } from "node:http";
|
|
12
13
|
import { Worker } from "node:worker_threads";
|
|
13
14
|
import { Agent, request } from "node:https";
|
|
14
15
|
import { Readable } from "node:stream";
|
|
@@ -17,9 +18,11 @@ import { Hono } from "hono";
|
|
|
17
18
|
import { streamSSE } from "hono/streaming";
|
|
18
19
|
import { z } from "zod";
|
|
19
20
|
import { OpenCode } from "@opencode/client";
|
|
20
|
-
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
21
|
+
import { appendFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
22
|
+
import { createInterface } from "node:readline/promises";
|
|
21
23
|
import { createServer } from "node:net";
|
|
22
|
-
import {
|
|
24
|
+
import { isSea } from "node:sea";
|
|
25
|
+
import { createInterface as createInterface$1 } from "node:readline";
|
|
23
26
|
//#region \0rolldown/runtime.js
|
|
24
27
|
var __defProp = Object.defineProperty;
|
|
25
28
|
var __esmMin = (fn, res, err) => () => {
|
|
@@ -139,7 +142,7 @@ function normpath(p) {
|
|
|
139
142
|
}
|
|
140
143
|
return "/".repeat(initial) + out.join("/") || ".";
|
|
141
144
|
}
|
|
142
|
-
/** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere. */
|
|
145
|
+
/** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere: of text (as UTF-8), or of bytes. */
|
|
143
146
|
function sha256(message) {
|
|
144
147
|
const K = new Uint32Array([
|
|
145
148
|
1116352408,
|
|
@@ -207,7 +210,7 @@ function sha256(message) {
|
|
|
207
210
|
3204031479,
|
|
208
211
|
3329325298
|
|
209
212
|
]);
|
|
210
|
-
const data = new TextEncoder().encode(message);
|
|
213
|
+
const data = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
211
214
|
const len = data.length;
|
|
212
215
|
const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
|
|
213
216
|
padded.set(data);
|
|
@@ -374,7 +377,7 @@ function linkTexts(body) {
|
|
|
374
377
|
return out;
|
|
375
378
|
}
|
|
376
379
|
/** Every link and image in a body, in order: where it points, its title, and whether it is an image
|
|
377
|
-
* (``, which for an artifact or a picture means "shown here", an embed). */
|
|
380
|
+
* (``, which for an artifact or a picture means "shown here", an embed), with an image's alt text. */
|
|
378
381
|
function citeRefs(body) {
|
|
379
382
|
const { tokens } = parse$1(body);
|
|
380
383
|
const out = [];
|
|
@@ -388,7 +391,8 @@ function citeRefs(body) {
|
|
|
388
391
|
else if (c.type === "image") out.push({
|
|
389
392
|
href: c.attrGet("src") ?? "",
|
|
390
393
|
title: c.attrGet("title"),
|
|
391
|
-
embed: true
|
|
394
|
+
embed: true,
|
|
395
|
+
alt: c.content
|
|
392
396
|
});
|
|
393
397
|
}
|
|
394
398
|
return out;
|
|
@@ -443,6 +447,64 @@ var init_markdown = __esmMin((() => {
|
|
|
443
447
|
SPACES = /[\s_]+/gu;
|
|
444
448
|
}));
|
|
445
449
|
//#endregion
|
|
450
|
+
//#region packages/core/src/picture.ts
|
|
451
|
+
/** Width and height in pixels, or null. */
|
|
452
|
+
function pictureSize(bytes, type) {
|
|
453
|
+
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), n = bytes.length;
|
|
454
|
+
const at = (i, s) => [...s].every((c, k) => bytes[i + k] === c.charCodeAt(0));
|
|
455
|
+
const ok = (w, h) => w > 0 && h > 0 ? {
|
|
456
|
+
width: w,
|
|
457
|
+
height: h
|
|
458
|
+
} : null;
|
|
459
|
+
try {
|
|
460
|
+
if (type === "image/png" && n >= 24 && at(12, "IHDR")) return ok(v.getUint32(16), v.getUint32(20));
|
|
461
|
+
if (type === "image/gif" && n >= 10 && at(0, "GIF")) return ok(v.getUint16(6, true), v.getUint16(8, true));
|
|
462
|
+
if (type === "image/jpeg") {
|
|
463
|
+
for (let i = 2; i + 9 < n;) {
|
|
464
|
+
if (bytes[i] !== 255) return null;
|
|
465
|
+
const m = bytes[i + 1];
|
|
466
|
+
if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) return ok(v.getUint16(i + 7), v.getUint16(i + 5));
|
|
467
|
+
i += 2 + v.getUint16(i + 2);
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
if (type === "image/webp" && n >= 30 && at(0, "RIFF") && at(8, "WEBP")) {
|
|
472
|
+
if (at(12, "VP8 ")) return ok(v.getUint16(26, true) & 16383, v.getUint16(28, true) & 16383);
|
|
473
|
+
if (at(12, "VP8L")) {
|
|
474
|
+
const b = v.getUint32(21, true);
|
|
475
|
+
return ok((b & 16383) + 1, (b >> 14 & 16383) + 1);
|
|
476
|
+
}
|
|
477
|
+
if (at(12, "VP8X")) return ok(1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16), 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16));
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
if (type === "image/svg+xml") {
|
|
481
|
+
const head = new TextDecoder().decode(bytes.subarray(0, 4096)), tag = /<svg\b[^>]*>/i.exec(head)?.[0] ?? "";
|
|
482
|
+
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']\\s*([\\d.]+)\\s*(px)?\\s*["']`, "i").exec(tag);
|
|
483
|
+
const w = attr("width"), h = attr("height");
|
|
484
|
+
if (w && h) return ok(Math.round(Number(w[1])), Math.round(Number(h[1])));
|
|
485
|
+
const box = /\sviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(tag);
|
|
486
|
+
return box ? ok(Math.round(Number(box[1])), Math.round(Number(box[2]))) : null;
|
|
487
|
+
}
|
|
488
|
+
} catch {}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
var TYPES$1, pictureType;
|
|
492
|
+
var init_picture = __esmMin((() => {
|
|
493
|
+
TYPES$1 = {
|
|
494
|
+
png: "image/png",
|
|
495
|
+
jpg: "image/jpeg",
|
|
496
|
+
jpeg: "image/jpeg",
|
|
497
|
+
gif: "image/gif",
|
|
498
|
+
svg: "image/svg+xml",
|
|
499
|
+
webp: "image/webp",
|
|
500
|
+
avif: "image/avif"
|
|
501
|
+
};
|
|
502
|
+
pictureType = (path) => {
|
|
503
|
+
const dot = path.lastIndexOf(".");
|
|
504
|
+
return dot < 0 || path.lastIndexOf("/") > dot ? null : TYPES$1[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
505
|
+
};
|
|
506
|
+
}));
|
|
507
|
+
//#endregion
|
|
446
508
|
//#region packages/core/src/kinds.ts
|
|
447
509
|
function kindOf(type) {
|
|
448
510
|
const t = type.trim().toLowerCase();
|
|
@@ -515,11 +577,25 @@ function outwardLinks(body) {
|
|
|
515
577
|
}
|
|
516
578
|
seen.add(href);
|
|
517
579
|
}
|
|
580
|
+
for (const { href, embed, alt } of citeRefs(body)) {
|
|
581
|
+
if (!embed || seen.has(href)) continue;
|
|
582
|
+
const c = codeTarget(href);
|
|
583
|
+
if (!c || !pictureType(c.path)) continue;
|
|
584
|
+
out.push({
|
|
585
|
+
kind: "code",
|
|
586
|
+
href,
|
|
587
|
+
text: alt || c.path.split("/").at(-1),
|
|
588
|
+
path: c.path,
|
|
589
|
+
picture: true
|
|
590
|
+
});
|
|
591
|
+
seen.add(href);
|
|
592
|
+
}
|
|
518
593
|
return out;
|
|
519
594
|
}
|
|
520
595
|
var ABOUT, DONE, EMBED;
|
|
521
596
|
var init_kinds = __esmMin((() => {
|
|
522
597
|
init_markdown();
|
|
598
|
+
init_picture();
|
|
523
599
|
init_text();
|
|
524
600
|
ABOUT = {
|
|
525
601
|
question: "about",
|
|
@@ -722,7 +798,7 @@ var init_bundle = __esmMin((() => {
|
|
|
722
798
|
concept.links = links;
|
|
723
799
|
concept.outward = outwardLinks(concept.body);
|
|
724
800
|
for (const link of links) if (link.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${link.target}`);
|
|
725
|
-
for (const { href, title, embed } of citeRefs(concept.body)) {
|
|
801
|
+
for (const { href, title, embed, alt } of citeRefs(concept.body)) {
|
|
726
802
|
const path = this.filePath(concept, href), kind = path === null ? null : citeKind(path);
|
|
727
803
|
if (path === null || kind === null) continue;
|
|
728
804
|
const t = strip((title ?? "").toLowerCase());
|
|
@@ -732,10 +808,11 @@ var init_bundle = __esmMin((() => {
|
|
|
732
808
|
embed,
|
|
733
809
|
broken: !this.files.has(path),
|
|
734
810
|
rel: rating(title),
|
|
735
|
-
align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null
|
|
811
|
+
align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null,
|
|
812
|
+
...embed && alt ? { alt } : {}
|
|
736
813
|
};
|
|
737
814
|
concept.cites.push(cite);
|
|
738
|
-
if (cite.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${path}`);
|
|
815
|
+
if (cite.broken) this.issue(concept.path, "warning", "broken-link", kind === "image" ? `picture not in the notes: ${path}` : `broken link to ${path}`);
|
|
739
816
|
}
|
|
740
817
|
}
|
|
741
818
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -1806,13 +1883,16 @@ function mapModel(base) {
|
|
|
1806
1883
|
s
|
|
1807
1884
|
]);
|
|
1808
1885
|
}
|
|
1886
|
+
const holdsFiles = /* @__PURE__ */ new Set();
|
|
1887
|
+
for (const f of base.files) holdsFiles.add(f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "");
|
|
1809
1888
|
const dir = (id) => {
|
|
1810
1889
|
const d = base.directories.get(id);
|
|
1811
|
-
|
|
1890
|
+
const children = [...[...d.children].sort().map(dir).filter((c) => c !== null), ...[...d.concepts].sort().map(note)];
|
|
1891
|
+
return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
|
|
1812
1892
|
kind: "dir",
|
|
1813
1893
|
id: "d:" + id,
|
|
1814
1894
|
ref: id,
|
|
1815
|
-
children
|
|
1895
|
+
children
|
|
1816
1896
|
};
|
|
1817
1897
|
};
|
|
1818
1898
|
return {
|
|
@@ -1827,6 +1907,7 @@ var init_map = __esmMin((() => {
|
|
|
1827
1907
|
//#region packages/core/src/index.ts
|
|
1828
1908
|
var init_src$1 = __esmMin((() => {
|
|
1829
1909
|
init_bundle();
|
|
1910
|
+
init_picture();
|
|
1830
1911
|
init_frontmatter();
|
|
1831
1912
|
init_source();
|
|
1832
1913
|
init_markdown();
|
|
@@ -1881,8 +1962,9 @@ function loadBundle(root) {
|
|
|
1881
1962
|
}
|
|
1882
1963
|
//#endregion
|
|
1883
1964
|
//#region packages/engine/src/api.ts
|
|
1884
|
-
var EngineError, Conflict;
|
|
1965
|
+
var MAX_FILE_BYTES, EngineError, Conflict;
|
|
1885
1966
|
var init_api = __esmMin((() => {
|
|
1967
|
+
MAX_FILE_BYTES = 2e7;
|
|
1886
1968
|
EngineError = class extends Error {
|
|
1887
1969
|
status;
|
|
1888
1970
|
constructor(message, status = 400) {
|
|
@@ -1944,19 +2026,50 @@ function unreachable(err, url) {
|
|
|
1944
2026
|
try {
|
|
1945
2027
|
host = new URL(url).host;
|
|
1946
2028
|
} catch {}
|
|
1947
|
-
const
|
|
2029
|
+
const proxy = /Proxy response \((\d{3})\)/.exec(said)?.[1];
|
|
2030
|
+
const advice = proxy === "407" ? "The proxy (HTTPS_PROXY), not the provider, refused with 407: it wants credentials of its own, as http://user:password@proxy:port in HTTPS_PROXY, or its own sign-in. isorite can't answer a proxy that signs in by Kerberos or NTLM." : proxy ? `The proxy (HTTPS_PROXY) answered ${proxy}, not the provider: check HTTPS_PROXY, and NO_PROXY for hosts reached directly.` : UNTRUSTED.has(code) ? `Its certificate is signed by an authority not trusted here, often a company's own. Add it to the system's authorities (isorite trusts those), or set NODE_EXTRA_CA_CERTS to its certificate (a .pem), or name that as ca in [provider].` : code === "CERT_HAS_EXPIRED" ? "Its certificate has expired: that is the gateway's to fix." : code === "ERR_TLS_CERT_ALTNAME_INVALID" ? `Its certificate is not for ${host}: check the url in [provider].` : /CERTIFICATE_REQUIRED|HANDSHAKE_FAILURE|BAD_CERTIFICATE|UNKNOWN_CA/.test(code) ? "It asks for a client certificate (mTLS) and was given none it accepts: name yours as pfx (with pfx_passphrase), or cert and cert_key, in [provider]." : code === "UND_ERR_SOCKET" && url.startsWith("https:") ? "It closed the connection as it was being set up. A gateway that asks for a client certificate (mTLS) does this: name yours as pfx, or cert and cert_key, in [provider]." : code === "ENOTFOUND" || code === "EAI_AGAIN" ? `The name ${host} could not be found: check the url in [provider], and whether it is only found on the company's network or VPN.` : code === "ECONNREFUSED" ? `Nothing answers at ${host}: check the url and its port in [provider].` : code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ENETUNREACH" || code === "EHOSTUNREACH" ? `${host} did not answer. If the company's network reaches it through a proxy, set HTTPS_PROXY.` : "";
|
|
1948
2031
|
return [
|
|
1949
2032
|
said || err?.message || "no reason given",
|
|
1950
2033
|
code && !said.includes(code) ? ` (${code})` : "",
|
|
1951
2034
|
advice ? `. ${advice}` : ""
|
|
1952
2035
|
].join("");
|
|
1953
2036
|
}
|
|
2037
|
+
/** The header the key goes in, and what goes before it: Authorization with "Bearer " unless said otherwise (M21).
|
|
2038
|
+
* `key_header = "authorization"` alone means Bearer too: a bare key in Authorization is rarely what a gateway
|
|
2039
|
+
* wants. `key_prefix = ""` sends it bare. */
|
|
2040
|
+
function keySent(p) {
|
|
2041
|
+
const header = p.keyHeader || "Authorization";
|
|
2042
|
+
return {
|
|
2043
|
+
header,
|
|
2044
|
+
prefix: p.keyPrefix ?? (header.toLowerCase() === "authorization" ? "Bearer " : "")
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
/** How the key is sent, as said to the person: `Authorization: Bearer <41 characters>`. */
|
|
2048
|
+
function keySentAs(p, key) {
|
|
2049
|
+
const { header, prefix } = keySent(p);
|
|
2050
|
+
return `${header}: ${prefix}<${key.length} characters>`;
|
|
2051
|
+
}
|
|
2052
|
+
/** What a provider said with a refusal: its `error.message` (or `message`, or `error`), else its text without
|
|
2053
|
+
* markup, cut to 300 characters, and never the key, should it say it back. */
|
|
2054
|
+
function refusal(body, key = null) {
|
|
2055
|
+
let said = "";
|
|
2056
|
+
try {
|
|
2057
|
+
const j = JSON.parse(body);
|
|
2058
|
+
said = (typeof j.error === "string" ? j.error : j.error?.message) ?? j.message ?? "";
|
|
2059
|
+
} catch {
|
|
2060
|
+
said = body.replace(/<[^>]+>/g, " ");
|
|
2061
|
+
}
|
|
2062
|
+
said = said.replace(/\s+/g, " ").trim();
|
|
2063
|
+
if (key && key.length >= 8) said = said.split(key).join("<the key>");
|
|
2064
|
+
return said.length > 300 ? said.slice(0, 299) + "…" : said;
|
|
2065
|
+
}
|
|
1954
2066
|
/** The address of `path` at the provider, and the headers that go with every request to it: its key, where it
|
|
1955
2067
|
* wants it, and its own. */
|
|
1956
2068
|
function endpoint(p, key, path) {
|
|
1957
2069
|
const url = new URL(p.url.replace(/\/+$/, "") + path);
|
|
1958
2070
|
for (const [name, value] of Object.entries(p.query ?? {})) url.searchParams.set(name, value);
|
|
1959
|
-
const
|
|
2071
|
+
const sent = keySent(p);
|
|
2072
|
+
const auth = !key ? {} : { [sent.header]: sent.prefix + key };
|
|
1960
2073
|
return {
|
|
1961
2074
|
url: url.toString(),
|
|
1962
2075
|
headers: {
|
|
@@ -2023,13 +2136,7 @@ async function completeOnce(http, p, key, call, heard) {
|
|
|
2023
2136
|
if (!res.ok || !res.body) {
|
|
2024
2137
|
let detail = "";
|
|
2025
2138
|
try {
|
|
2026
|
-
|
|
2027
|
-
try {
|
|
2028
|
-
const j = JSON.parse(said);
|
|
2029
|
-
detail = (typeof j.error === "string" ? j.error : j.error?.message) ?? j.message ?? "";
|
|
2030
|
-
} catch {
|
|
2031
|
-
detail = said.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
2032
|
-
}
|
|
2139
|
+
detail = refusal(await res.text(), key);
|
|
2033
2140
|
} catch {}
|
|
2034
2141
|
if (DOWN.has(res.status)) throw new ModelError(`${p.name} is unavailable (${res.status}${detail ? `: ${detail}` : ""})`, 502, {
|
|
2035
2142
|
kind: "unavailable",
|
|
@@ -2041,7 +2148,7 @@ async function completeOnce(http, p, key, call, heard) {
|
|
|
2041
2148
|
retry: true,
|
|
2042
2149
|
after: afterOf(res)
|
|
2043
2150
|
});
|
|
2044
|
-
const advice = res.status === 401 ?
|
|
2151
|
+
const advice = res.status === 401 ? ` The key was not accepted${key ? `, sent as ${keySentAs(p, key)}` : ""}: set it again in settings, or check key_header and key_prefix.` : res.status === 402 ? " The key's credit is spent." : res.status === 404 ? ` ${p.name} may have no model called ${call.model}${p.defaultTiers ? ", which is OpenRouter's name for it: name this provider's models under [tiers] in config.toml" : ""}.` : "";
|
|
2045
2152
|
throw new ModelError(`${p.name} said ${res.status}${detail ? `: ${detail}` : ""}.${advice}`, res.status === 401 || res.status === 403 ? 409 : 502);
|
|
2046
2153
|
}
|
|
2047
2154
|
let text = "", usage = null, finish = "";
|
|
@@ -2671,6 +2778,9 @@ something at one place in the note they have open.`;
|
|
|
2671
2778
|
is drawn as the diagram. Draw one where a picture of how things stand to
|
|
2672
2779
|
each other helps, or when asked; not by habit. Maths in a label goes
|
|
2673
2780
|
between $$ and $$.
|
|
2781
|
+
- A picture is shown as . You can't see it: what
|
|
2782
|
+
you know of it is the words in the brackets. Keep a picture's line as it is
|
|
2783
|
+
when you rewrite around it, and never make up a picture's path.
|
|
2674
2784
|
- Ordinary Markdown otherwise. No HTML.`;
|
|
2675
2785
|
FORMAT_RULES = FORMAT;
|
|
2676
2786
|
LOOKUP = (rounds, meaning) => `# Looking things up
|
|
@@ -2779,7 +2889,7 @@ var init_lookup = __esmMin((() => {
|
|
|
2779
2889
|
...str$1,
|
|
2780
2890
|
description: "The note's id or path: basics/entropy or /basics/entropy.md."
|
|
2781
2891
|
} }, ["id"]),
|
|
2782
|
-
fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section.", {
|
|
2892
|
+
fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section. A picture in it is : you have its words, not the picture.", {
|
|
2783
2893
|
id: str$1,
|
|
2784
2894
|
section: {
|
|
2785
2895
|
...str$1,
|
|
@@ -4002,18 +4112,41 @@ var init_engine = __esmMin((() => {
|
|
|
4002
4112
|
const notes = {}, broken = await this.brokenCode();
|
|
4003
4113
|
for (const c of this.bundle.concepts.values()) {
|
|
4004
4114
|
const bad = broken.get(c.id) ?? [];
|
|
4005
|
-
const links = c.outward.map((o) => ({
|
|
4115
|
+
const links = c.outward.filter((o) => !(o.kind === "code" && o.picture)).map((o) => ({
|
|
4006
4116
|
kind: o.kind,
|
|
4007
4117
|
href: o.href,
|
|
4008
4118
|
text: o.text,
|
|
4009
4119
|
...bad.includes(o.href) ? { broken: true } : {}
|
|
4010
4120
|
}));
|
|
4121
|
+
const pictures = [], shown = /* @__PURE__ */ new Set();
|
|
4122
|
+
for (const p of c.cites) {
|
|
4123
|
+
if (p.kind !== "image" || !p.embed || shown.has(p.target)) continue;
|
|
4124
|
+
shown.add(p.target);
|
|
4125
|
+
pictures.push({
|
|
4126
|
+
href: p.target,
|
|
4127
|
+
path: p.target,
|
|
4128
|
+
from: "notes",
|
|
4129
|
+
text: p.alt || p.target.split("/").at(-1),
|
|
4130
|
+
...p.broken ? { broken: true } : {}
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
for (const o of c.outward) {
|
|
4134
|
+
if (o.kind !== "code" || !o.picture) continue;
|
|
4135
|
+
pictures.push({
|
|
4136
|
+
href: o.href,
|
|
4137
|
+
path: o.path,
|
|
4138
|
+
from: "code",
|
|
4139
|
+
text: o.text,
|
|
4140
|
+
...bad.includes(o.href) ? { broken: true } : {}
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4011
4143
|
notes[c.id] = {
|
|
4012
4144
|
title: c.title,
|
|
4013
4145
|
tokens: estimate(c.body.trim()),
|
|
4014
4146
|
...c.kind !== "note" ? { kind: c.kind } : {},
|
|
4015
4147
|
...c.done ? { done: c.done } : {},
|
|
4016
|
-
...links.length ? { links } : {}
|
|
4148
|
+
...links.length ? { links } : {},
|
|
4149
|
+
...pictures.length ? { pictures } : {}
|
|
4017
4150
|
};
|
|
4018
4151
|
}
|
|
4019
4152
|
return {
|
|
@@ -4410,6 +4543,107 @@ var init_engine = __esmMin((() => {
|
|
|
4410
4543
|
this.queue = run.catch(() => void 0);
|
|
4411
4544
|
return run;
|
|
4412
4545
|
}
|
|
4546
|
+
/** A picture (M22), in the notes or in the base's repository. The path must be a picture's, within its folder,
|
|
4547
|
+
* and (in the notes) never in a hidden folder such as `.isorite/`: nothing but a picture is given this way. */
|
|
4548
|
+
async file(path, from = "notes") {
|
|
4549
|
+
const type = pictureType(path);
|
|
4550
|
+
if (!type) return null;
|
|
4551
|
+
let files, at;
|
|
4552
|
+
if (from === "code") {
|
|
4553
|
+
const target = codeTarget(`code:${path}`), where = (await this.settingsOfBase()).repository;
|
|
4554
|
+
files = target && where ? this.opts.repository?.(where) ?? null : null;
|
|
4555
|
+
at = target?.path ?? "";
|
|
4556
|
+
} else {
|
|
4557
|
+
at = path.replace(/^\/+/, "");
|
|
4558
|
+
files = at.split("/").some((p) => !p || p === "." || p === ".." || p.startsWith(".")) ? null : this.opts.notes;
|
|
4559
|
+
}
|
|
4560
|
+
const bytes = files ? await files.readBytes(at) : null;
|
|
4561
|
+
if (!bytes) return null;
|
|
4562
|
+
if (bytes.length > 2e7) throw new EngineError(`${path} is larger than isorite gives (${MAX_FILE_BYTES / 1e6} MB)`, 413);
|
|
4563
|
+
return {
|
|
4564
|
+
bytes,
|
|
4565
|
+
type,
|
|
4566
|
+
version: sha256(bytes).slice(0, 16)
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
/** What was read of each picture, kept while its size and time are the same. */
|
|
4570
|
+
pictureCache = /* @__PURE__ */ new Map();
|
|
4571
|
+
/** Every picture in the notes (M22), found afresh: one put in the folder by hand since the base was opened is
|
|
4572
|
+
* among them, and from then on in the bundle too, so a link to it isn't broken. A hidden folder is never looked in. */
|
|
4573
|
+
async pictures() {
|
|
4574
|
+
const found = [];
|
|
4575
|
+
const visit = async (dir) => {
|
|
4576
|
+
for (const f of await this.opts.notes.list(dir)) {
|
|
4577
|
+
if (f.name.startsWith(".")) continue;
|
|
4578
|
+
const path = dir ? `${dir}/${f.name}` : f.name;
|
|
4579
|
+
if (f.dir) await visit(path);
|
|
4580
|
+
else if (pictureType(f.name)) found.push({
|
|
4581
|
+
path,
|
|
4582
|
+
...f.size === void 0 ? {} : { size: f.size },
|
|
4583
|
+
...f.mtime === void 0 ? {} : { mtime: f.mtime }
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
};
|
|
4587
|
+
await visit("");
|
|
4588
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
4589
|
+
for (const f of found) {
|
|
4590
|
+
seen.add(f.path);
|
|
4591
|
+
const kept = this.pictureCache.get(f.path);
|
|
4592
|
+
if (kept && f.size !== void 0 && kept.size === f.size && kept.mtime === f.mtime) {
|
|
4593
|
+
out.push(kept.info);
|
|
4594
|
+
continue;
|
|
4595
|
+
}
|
|
4596
|
+
const bytes = f.size !== void 0 && f.size > 2e7 ? null : await this.opts.notes.readBytes(f.path);
|
|
4597
|
+
if (!bytes) continue;
|
|
4598
|
+
const px = pictureSize(bytes, pictureType(f.path));
|
|
4599
|
+
const info = {
|
|
4600
|
+
path: f.path,
|
|
4601
|
+
bytes: bytes.length,
|
|
4602
|
+
width: px?.width ?? null,
|
|
4603
|
+
height: px?.height ?? null,
|
|
4604
|
+
sha256: sha256(bytes)
|
|
4605
|
+
};
|
|
4606
|
+
this.pictureCache.set(f.path, {
|
|
4607
|
+
...f.size === void 0 ? {} : { size: f.size },
|
|
4608
|
+
...f.mtime === void 0 ? {} : { mtime: f.mtime },
|
|
4609
|
+
info
|
|
4610
|
+
});
|
|
4611
|
+
out.push(info);
|
|
4612
|
+
}
|
|
4613
|
+
for (const path of this.pictureCache.keys()) if (!seen.has(path)) this.pictureCache.delete(path);
|
|
4614
|
+
let changed = false;
|
|
4615
|
+
for (const path of seen) if (!this.files.has(path)) {
|
|
4616
|
+
this.files.set(path, { path });
|
|
4617
|
+
changed = true;
|
|
4618
|
+
}
|
|
4619
|
+
for (const path of [...this.files.keys()]) if (pictureType(path) && !seen.has(path)) {
|
|
4620
|
+
this.files.delete(path);
|
|
4621
|
+
changed = true;
|
|
4622
|
+
}
|
|
4623
|
+
if (changed) this.built = this.index = null;
|
|
4624
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
4625
|
+
}
|
|
4626
|
+
/** Put a picture in the notes (M22). The path must be a picture's, out of any hidden folder, and free: a file
|
|
4627
|
+
* that's there is never replaced. Not journaled: a picture has no history, and stays when its link goes. */
|
|
4628
|
+
savePicture(path, bytes) {
|
|
4629
|
+
return this.one(async () => {
|
|
4630
|
+
const type = pictureType(path), parts = path.split("/");
|
|
4631
|
+
if (!type || parts.some((p) => !p || p === "." || p === ".." || p.startsWith(".")) || /[\\\x00-\x1f]/.test(path)) throw new EngineError(`not a picture's path: ${path}`, 400);
|
|
4632
|
+
if (bytes.length > 2e7) throw new EngineError(`the picture is larger than isorite takes (${MAX_FILE_BYTES / 1e6} MB)`, 413);
|
|
4633
|
+
if (await this.opts.notes.readBytes(path) !== null) throw new Conflict(`${path} is there already`, null);
|
|
4634
|
+
await this.opts.notes.writeBytes(path, bytes);
|
|
4635
|
+
this.files.set(path, { path });
|
|
4636
|
+
this.built = this.index = null;
|
|
4637
|
+
const px = pictureSize(bytes, type);
|
|
4638
|
+
return {
|
|
4639
|
+
path,
|
|
4640
|
+
bytes: bytes.length,
|
|
4641
|
+
width: px?.width ?? null,
|
|
4642
|
+
height: px?.height ?? null,
|
|
4643
|
+
sha256: sha256(bytes)
|
|
4644
|
+
};
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4413
4647
|
/** Where a note's file is: drafts and Understanding notes are among isorite's own files. */
|
|
4414
4648
|
where(id) {
|
|
4415
4649
|
if (!inBase(id)) return {
|
|
@@ -6463,6 +6697,8 @@ var init_ports = __esmMin((() => {
|
|
|
6463
6697
|
systemClock = { now: () => Date.now() };
|
|
6464
6698
|
MemoryFiles = class {
|
|
6465
6699
|
files = /* @__PURE__ */ new Map();
|
|
6700
|
+
/** Files held as bytes (pictures), apart from text. */
|
|
6701
|
+
bytes = /* @__PURE__ */ new Map();
|
|
6466
6702
|
constructor(files = {}) {
|
|
6467
6703
|
for (const [path, text] of Object.entries(files)) this.files.set(path, text);
|
|
6468
6704
|
}
|
|
@@ -6475,22 +6711,42 @@ var init_ports = __esmMin((() => {
|
|
|
6475
6711
|
async append(path, text) {
|
|
6476
6712
|
this.files.set(path, (this.files.get(path) ?? "") + text);
|
|
6477
6713
|
}
|
|
6714
|
+
async readBytes(path) {
|
|
6715
|
+
const b = this.bytes.get(path), t = this.files.get(path);
|
|
6716
|
+
return b ?? (t === void 0 ? null : new TextEncoder().encode(t));
|
|
6717
|
+
}
|
|
6718
|
+
async writeBytes(path, bytes) {
|
|
6719
|
+
this.files.delete(path);
|
|
6720
|
+
this.bytes.set(path, bytes);
|
|
6721
|
+
}
|
|
6478
6722
|
async remove(path) {
|
|
6479
6723
|
this.files.delete(path);
|
|
6724
|
+
this.bytes.delete(path);
|
|
6480
6725
|
}
|
|
6481
6726
|
async list(dir) {
|
|
6482
6727
|
const prefix = dir ? dir + "/" : "";
|
|
6483
6728
|
const found = /* @__PURE__ */ new Map();
|
|
6484
|
-
for (const path of this.files.keys()) {
|
|
6729
|
+
for (const path of [...this.files.keys(), ...this.bytes.keys()]) {
|
|
6485
6730
|
if (!path.startsWith(prefix)) continue;
|
|
6486
6731
|
const rest = path.slice(prefix.length), cut = rest.indexOf("/");
|
|
6487
|
-
if (cut
|
|
6488
|
-
|
|
6732
|
+
if (cut >= 0) {
|
|
6733
|
+
found.set(rest.slice(0, cut), {
|
|
6734
|
+
name: rest.slice(0, cut),
|
|
6735
|
+
dir: true
|
|
6736
|
+
});
|
|
6737
|
+
continue;
|
|
6738
|
+
}
|
|
6739
|
+
const b = this.bytes.get(path);
|
|
6740
|
+
found.set(rest, b ? {
|
|
6741
|
+
name: rest,
|
|
6742
|
+
dir: false,
|
|
6743
|
+
size: b.length
|
|
6744
|
+
} : {
|
|
6745
|
+
name: rest,
|
|
6746
|
+
dir: false
|
|
6747
|
+
});
|
|
6489
6748
|
}
|
|
6490
|
-
return [...found
|
|
6491
|
-
name,
|
|
6492
|
-
dir: isDir
|
|
6493
|
-
}));
|
|
6749
|
+
return [...found.values()];
|
|
6494
6750
|
}
|
|
6495
6751
|
};
|
|
6496
6752
|
MemorySecrets = class {
|
|
@@ -6697,7 +6953,7 @@ function writeHome(dir, { gateway = null, plugin = pluginDir, instructions = []
|
|
|
6697
6953
|
recursive: true,
|
|
6698
6954
|
force: true
|
|
6699
6955
|
});
|
|
6700
|
-
symlinkSync(plugin, link, "dir");
|
|
6956
|
+
symlinkSync(plugin, link, process.platform === "win32" ? "junction" : "dir");
|
|
6701
6957
|
}
|
|
6702
6958
|
return dir;
|
|
6703
6959
|
}
|
|
@@ -6724,12 +6980,14 @@ function withLocal(config, local) {
|
|
|
6724
6980
|
};
|
|
6725
6981
|
}
|
|
6726
6982
|
function linksTo(path, target) {
|
|
6983
|
+
const plain = (p) => resolve(p).replace(/[\\/]+$/, "");
|
|
6727
6984
|
try {
|
|
6728
|
-
return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target;
|
|
6985
|
+
return lstatSync(path).isSymbolicLink() && plain(readlinkSync(path)) === plain(target);
|
|
6729
6986
|
} catch {
|
|
6730
6987
|
return false;
|
|
6731
6988
|
}
|
|
6732
6989
|
}
|
|
6990
|
+
const PACKAGE$1 = `@opencode/cli@2.0.32`;
|
|
6733
6991
|
/** The version a `--version` line names, as "2.0.32", or null. */
|
|
6734
6992
|
function parseVersion(output) {
|
|
6735
6993
|
return output.match(/(\d+)\.(\d+)\.(\d+)/)?.[0] ?? null;
|
|
@@ -6749,20 +7007,57 @@ const probe = (command) => {
|
|
|
6749
7007
|
return null;
|
|
6750
7008
|
}
|
|
6751
7009
|
};
|
|
6752
|
-
/**
|
|
6753
|
-
|
|
7010
|
+
/** isorite's own prefix for OpenCode 2. Not ~/.config/isorite/opencode, which is OpenCode's configuration as
|
|
7011
|
+
* `term` writes it (home.ts): an npm prefix there would put node_modules among it. */
|
|
7012
|
+
function ownPrefix(env = process.env) {
|
|
7013
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "isorite", "opencode-cli");
|
|
7014
|
+
}
|
|
7015
|
+
/** The OpenCode 2 in isorite's own prefix, as npm links it; on Windows, the package's own opencode.exe, as npm's
|
|
7016
|
+
* opencode2.cmd can't be run without a shell (Node refuses .cmd files since 2024). */
|
|
7017
|
+
const ownCommand = (env = process.env, platform = process.platform) => platform === "win32" ? join(ownPrefix(env), "node_modules", "@opencode", "cli", "bin", "opencode.exe") : join(ownPrefix(env), "node_modules", ".bin", "opencode2");
|
|
7018
|
+
/** A command on the PATH as Windows can run it without a shell: `<name>.exe`, or for npm's `<name>.cmd` the
|
|
7019
|
+
* package's opencode.exe beside it, in the global node_modules. Null when there is neither. */
|
|
7020
|
+
function onWindowsPath(name, env = process.env, exists = existsSync) {
|
|
7021
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(win32.delimiter).filter(Boolean)) {
|
|
7022
|
+
if (exists(win32.join(dir, `${name}.exe`))) return win32.join(dir, `${name}.exe`);
|
|
7023
|
+
const exe = win32.join(dir, "node_modules", "@opencode", "cli", "bin", "opencode.exe");
|
|
7024
|
+
if (exists(win32.join(dir, `${name}.cmd`)) && exists(exe)) return exe;
|
|
7025
|
+
}
|
|
7026
|
+
return null;
|
|
7027
|
+
}
|
|
7028
|
+
/** The line that installs OpenCode 2 for isorite alone, to copy. */
|
|
7029
|
+
const installLine = (env = process.env) => `isorite opencode (npm install --prefix ${ownPrefix(env)} ${PACKAGE$1})`;
|
|
7030
|
+
/** OpenCode 2 wasn't found. `old` is an OpenCode 1 that was, if one was. */
|
|
7031
|
+
var NoOpenCode = class extends Error {
|
|
7032
|
+
old;
|
|
7033
|
+
constructor(tried, old, env) {
|
|
7034
|
+
super((old ? `isorite term needs OpenCode 2; ${old} is OpenCode 1 (${tried.join("; ")}).\n` : `isorite term needs OpenCode 2 (${tried.join("; ")}).\n`) + ` Install it for isorite only, leaving the global one as it is: ${installLine(env)}\n or name one with: ISORITE_OPENCODE=/path/to/opencode isorite term`);
|
|
7035
|
+
this.old = old;
|
|
7036
|
+
}
|
|
7037
|
+
};
|
|
7038
|
+
/** ISORITE_OPENCODE if set, else isorite's own prefix's, else `opencode2`, else `opencode`: the first that is
|
|
7039
|
+
* OpenCode 2. */
|
|
7040
|
+
function findOpenCode(env = process.env, run = probe, platform = process.platform, where = onWindowsPath) {
|
|
6754
7041
|
const tried = [];
|
|
6755
|
-
|
|
6756
|
-
|
|
7042
|
+
let old = null;
|
|
7043
|
+
const candidates = env.ISORITE_OPENCODE ? [[env.ISORITE_OPENCODE, "ISORITE_OPENCODE"]] : [
|
|
7044
|
+
[ownCommand(env, platform), "isorite opencode"],
|
|
7045
|
+
["opencode2", "the PATH"],
|
|
7046
|
+
["opencode", "the PATH"]
|
|
7047
|
+
];
|
|
7048
|
+
for (const [named, from] of candidates) {
|
|
7049
|
+
const command = platform === "win32" && from === "the PATH" ? where(named, env) ?? named : named;
|
|
6757
7050
|
const output = run(command);
|
|
6758
7051
|
const version = output === null ? null : parseVersion(output);
|
|
6759
7052
|
if (version && Number(version.split(".")[0]) === 2) return {
|
|
6760
7053
|
command,
|
|
6761
|
-
version
|
|
7054
|
+
version,
|
|
7055
|
+
from
|
|
6762
7056
|
};
|
|
6763
|
-
|
|
7057
|
+
if (version && Number(version.split(".")[0]) < 2) old ??= `${command} ${version}`;
|
|
7058
|
+
if (output !== null || from !== "isorite opencode") tried.push(output === null ? `${command}: not found` : `${command}: ${version ?? "unknown version"}`);
|
|
6764
7059
|
}
|
|
6765
|
-
throw new
|
|
7060
|
+
throw new NoOpenCode(tried, old, env);
|
|
6766
7061
|
}
|
|
6767
7062
|
//#endregion
|
|
6768
7063
|
//#region packages/server/src/config.ts
|
|
@@ -6850,6 +7145,7 @@ function providerFrom(config, env = process.env) {
|
|
|
6850
7145
|
...headers ? { headers } : {},
|
|
6851
7146
|
...query ? { query } : {},
|
|
6852
7147
|
...typeof p.key_header === "string" && p.key_header ? { keyHeader: p.key_header } : {},
|
|
7148
|
+
...typeof p.key_prefix === "string" ? { keyPrefix: p.key_prefix } : {},
|
|
6853
7149
|
...p.stream_usage === false ? { streamUsage: false } : {},
|
|
6854
7150
|
...typeof p.max_tokens === "number" && p.max_tokens > 0 ? { maxTokens: Math.floor(p.max_tokens) } : {}
|
|
6855
7151
|
};
|
|
@@ -6953,6 +7249,23 @@ function lendBase(name, lend, path = configPath()) {
|
|
|
6953
7249
|
}
|
|
6954
7250
|
//#endregion
|
|
6955
7251
|
//#region packages/server/src/model.ts
|
|
7252
|
+
/** Where a mirror keeps the .tgz (M21): ISORITE_MODEL_BASE_URL names the folder it's in, tried before the rest. */
|
|
7253
|
+
function mirrorOf(env = process.env) {
|
|
7254
|
+
const base = env.ISORITE_MODEL_BASE_URL?.trim();
|
|
7255
|
+
return base ? `${base.replace(/\/+$/, "")}/${RELEASE_FILE}` : null;
|
|
7256
|
+
}
|
|
7257
|
+
/** How to fetch the model by hand, to copy: the file, its SHA-256, and the command that takes it. */
|
|
7258
|
+
function byHand(env = process.env) {
|
|
7259
|
+
const mirror = mirrorOf(env);
|
|
7260
|
+
return [
|
|
7261
|
+
"Fetch this file another way (a browser, or a machine that can reach it):",
|
|
7262
|
+
` ${RELEASE}`,
|
|
7263
|
+
...mirror ? [` or from your mirror: ${mirror}`] : [],
|
|
7264
|
+
` SHA-256 ${RELEASE_SHA256}`,
|
|
7265
|
+
"then run:",
|
|
7266
|
+
` isorite model --from ~/Downloads/${RELEASE_FILE}`
|
|
7267
|
+
];
|
|
7268
|
+
}
|
|
6956
7269
|
/** Where the server looks for the model, in order, and why each place is there. */
|
|
6957
7270
|
function modelPlaces(env = process.env) {
|
|
6958
7271
|
if (env.ISORITE_EMBED_DIR) return [{
|
|
@@ -7042,7 +7355,7 @@ function unpack(pkg) {
|
|
|
7042
7355
|
}
|
|
7043
7356
|
/** Fetch the model's files into `out`, keeping only files whose SHA-256 is the one expected: from a folder if one is
|
|
7044
7357
|
* named; else from npm, then the GitHub release, then Hugging Face, each tried in turn. */
|
|
7045
|
-
async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release = RELEASE, hub = HUB, files = FILES } = {}) {
|
|
7358
|
+
async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release = RELEASE, mirror = null, url, hub = HUB, files = FILES, tgzSha = files === FILES ? RELEASE_SHA256 : null } = {}) {
|
|
7046
7359
|
mkdirSync(out, { recursive: true });
|
|
7047
7360
|
const missing = () => files.filter(([, to, want]) => !(existsSync(join(out, to)) && sha(readFileSync(join(out, to))) === want));
|
|
7048
7361
|
for (const [, to] of files) if (!missing().some((f) => f[1] === to)) say(`${to}: already here`);
|
|
@@ -7071,31 +7384,42 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
|
|
|
7071
7384
|
}
|
|
7072
7385
|
};
|
|
7073
7386
|
const why = (e, url) => e.message === "fetch failed" || e.cause ? unreachable(e, url) : e.message;
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
} catch (e) {
|
|
7077
|
-
tried.push(`npm (${pkg}): ${e.message}`);
|
|
7078
|
-
say(`From npm (${pkg}): ${e.message}`);
|
|
7079
|
-
}
|
|
7080
|
-
if (missing().length && release) {
|
|
7387
|
+
/** The .tgz at an address, checked by its SHA-256 before it's unpacked; a failure is kept in `tried`. */
|
|
7388
|
+
const fromWeb = async (where, address) => {
|
|
7081
7389
|
const tmp = mkdtempSync(join(tmpdir(), "isorite-model-release-"));
|
|
7082
7390
|
try {
|
|
7083
|
-
say(`From
|
|
7084
|
-
const res = await fetch(
|
|
7391
|
+
say(`From ${where}: ${address}`);
|
|
7392
|
+
const res = await fetch(address);
|
|
7085
7393
|
if (!res.ok) throw new Error(`it said ${res.status}`);
|
|
7086
|
-
const
|
|
7087
|
-
|
|
7394
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
7395
|
+
if (tgzSha && sha(buf) !== tgzSha) throw new Error(`not the file expected: its SHA-256 is ${sha(buf)}, not ${tgzSha}, so it was not unpacked`);
|
|
7396
|
+
const tgz = join(tmp, basename(new URL(address).pathname) || "isorite-model-granite-1.0.0.tgz");
|
|
7397
|
+
writeFileSync(tgz, buf);
|
|
7088
7398
|
fromPackage(tgz);
|
|
7089
7399
|
} catch (e) {
|
|
7090
|
-
tried.push(
|
|
7091
|
-
say(`From
|
|
7400
|
+
tried.push(`${where} (${address}): ${why(e, address)}`);
|
|
7401
|
+
say(`From ${where}: ${why(e, address)}`);
|
|
7092
7402
|
} finally {
|
|
7093
7403
|
rmSync(tmp, {
|
|
7094
7404
|
recursive: true,
|
|
7095
7405
|
force: true
|
|
7096
7406
|
});
|
|
7097
7407
|
}
|
|
7408
|
+
};
|
|
7409
|
+
if (url) {
|
|
7410
|
+
await fromWeb("--url", url);
|
|
7411
|
+
if (missing().length) throw new ModelFetchError(tried);
|
|
7412
|
+
say(`The model is in ${out}`);
|
|
7413
|
+
return;
|
|
7414
|
+
}
|
|
7415
|
+
if (missing().length && mirror) await fromWeb("the mirror (ISORITE_MODEL_BASE_URL)", mirror);
|
|
7416
|
+
if (missing().length && pkg) try {
|
|
7417
|
+
fromPackage(pkg);
|
|
7418
|
+
} catch (e) {
|
|
7419
|
+
tried.push(`npm (${pkg}): ${e.message}`);
|
|
7420
|
+
say(`From npm (${pkg}): ${e.message}`);
|
|
7098
7421
|
}
|
|
7422
|
+
if (missing().length && release) await fromWeb("GitHub", release);
|
|
7099
7423
|
if (missing().length && hub) {
|
|
7100
7424
|
say("From Hugging Face");
|
|
7101
7425
|
try {
|
|
@@ -7115,6 +7439,19 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
|
|
|
7115
7439
|
* checked by using it: it must load, and give DIM numbers for a sentence. Kept in the user's models, and used from
|
|
7116
7440
|
* then on. */
|
|
7117
7441
|
async function installFrom(from, say = console.log, env = process.env) {
|
|
7442
|
+
if (/\.(tgz|tar\.gz)$/.test(from) && existsSync(from) && statSync(from).isFile()) {
|
|
7443
|
+
const got = sha(readFileSync(from));
|
|
7444
|
+
if (got !== "8d8ab528ae289dfc81fd2bfa80c867ca0c8342a2d98f7c86141df4b15fdf028c") throw new Error(`${from} is not ${RELEASE_FILE}: its SHA-256 is ${got}, not ${RELEASE_SHA256}`);
|
|
7445
|
+
const dir = unpack(resolve(from));
|
|
7446
|
+
try {
|
|
7447
|
+
return await installFrom(join(dir, "package"), say, env);
|
|
7448
|
+
} finally {
|
|
7449
|
+
rmSync(dir, {
|
|
7450
|
+
recursive: true,
|
|
7451
|
+
force: true
|
|
7452
|
+
});
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7118
7455
|
if (!hasModel(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
|
|
7119
7456
|
const granite = !existsSync(join(from, "isorite-model.json")) && FILES.every(([, to, want]) => existsSync(join(from, to)) && sha(readFileSync(join(from, to))) === want);
|
|
7120
7457
|
let out = userModelDir(env);
|
|
@@ -7142,7 +7479,7 @@ async function installFrom(from, say = console.log, env = process.env) {
|
|
|
7142
7479
|
}
|
|
7143
7480
|
/** `isorite model [--from <folder>]`: says which model is installed and where, or installs one, and on failure
|
|
7144
7481
|
* where it looked, what it tried, and what to do. */
|
|
7145
|
-
async function modelCommand(from, say = console.log, env = process.env, sources = {}) {
|
|
7482
|
+
async function modelCommand(from, say = console.log, env = process.env, sources = {}, url) {
|
|
7146
7483
|
const told = (i, why) => say(`search by meaning: ${i.name} at ${i.dir} (sha256 ${i.sha.slice(0, 12)}, ${i.pooling} pooling), found by ${why}`);
|
|
7147
7484
|
if (env.ISORITE_EMBED === "off") say("ISORITE_EMBED=off: search by meaning is switched off, whatever is installed");
|
|
7148
7485
|
if (from) try {
|
|
@@ -7159,27 +7496,35 @@ async function modelCommand(from, say = console.log, env = process.env, sources
|
|
|
7159
7496
|
return 0;
|
|
7160
7497
|
}
|
|
7161
7498
|
say(`No model in ${places.map((p) => p.dir).join(", ")}`);
|
|
7499
|
+
if (!url) say(`If it can't be fetched from here: ${RELEASE} (SHA-256 ${RELEASE_SHA256}), then isorite model --from <that file>`);
|
|
7162
7500
|
try {
|
|
7163
|
-
await fetchModel(userModelDir(env), say,
|
|
7501
|
+
await fetchModel(userModelDir(env), say, {
|
|
7502
|
+
mirror: mirrorOf(env),
|
|
7503
|
+
...sources,
|
|
7504
|
+
...url ? { url } : {}
|
|
7505
|
+
});
|
|
7164
7506
|
return 0;
|
|
7165
7507
|
} catch (e) {
|
|
7166
7508
|
say(e.message);
|
|
7167
7509
|
say([
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
` -
|
|
7171
|
-
` -
|
|
7510
|
+
...byHand(env),
|
|
7511
|
+
"Or:",
|
|
7512
|
+
` - a mirror: ISORITE_MODEL_BASE_URL=<the folder ${RELEASE_FILE} is in> isorite model, or isorite model --url <its address>`,
|
|
7513
|
+
` - the model's files from a machine that has them (${userModelDir()} there): isorite model --from <folder>`,
|
|
7514
|
+
` - any compatible model (ONNX, 384 numbers out, with its tokenizer.json): isorite model --from <folder>, or ISORITE_EMBED_DIR=<folder>`,
|
|
7172
7515
|
"Search by words works without it."
|
|
7173
7516
|
].join("\n"));
|
|
7174
7517
|
return 1;
|
|
7175
7518
|
}
|
|
7176
7519
|
}
|
|
7177
|
-
var NAME$2, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
|
|
7520
|
+
var NAME$2, MODEL, RELEASE_FILE, RELEASE, RELEASE_SHA256, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
|
|
7178
7521
|
var init_model = __esmMin((() => {
|
|
7179
7522
|
init_src();
|
|
7180
7523
|
NAME$2 = "granite-embedding-small-english-r2";
|
|
7181
7524
|
MODEL = `${NAME$2}-q8`;
|
|
7182
|
-
|
|
7525
|
+
RELEASE_FILE = "isorite-model-granite-1.0.0.tgz";
|
|
7526
|
+
RELEASE = `https://github.com/lachlanjs/isorite/releases/download/model-granite-1.0.0/${RELEASE_FILE}`;
|
|
7527
|
+
RELEASE_SHA256 = "8d8ab528ae289dfc81fd2bfa80c867ca0c8342a2d98f7c86141df4b15fdf028c";
|
|
7183
7528
|
PACKAGE = "isorite-model-granite@1.0.0";
|
|
7184
7529
|
HUB = `https://huggingface.co/onnx-community/granite-embedding-small-english-r2-ONNX/resolve/1dc7835ba0cb9c76a3618d0bf0c427c97671b3c8`;
|
|
7185
7530
|
FILES = [
|
|
@@ -7861,6 +8206,7 @@ var Terminal = class {
|
|
|
7861
8206
|
};
|
|
7862
8207
|
//#endregion
|
|
7863
8208
|
//#region packages/server/src/http.ts
|
|
8209
|
+
init_src$1();
|
|
7864
8210
|
init_src();
|
|
7865
8211
|
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
7866
8212
|
"localhost",
|
|
@@ -7882,6 +8228,8 @@ const TYPES = {
|
|
|
7882
8228
|
".map": "application/json",
|
|
7883
8229
|
".txt": "text/plain; charset=utf-8"
|
|
7884
8230
|
};
|
|
8231
|
+
/** What a picture may do when opened on its own: be drawn, and nothing else (no script, no fetch, no form). */
|
|
8232
|
+
const PICTURE_CSP = "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'";
|
|
7885
8233
|
const Edit = z.object({
|
|
7886
8234
|
base: z.string().max(64),
|
|
7887
8235
|
text: z.string()
|
|
@@ -7982,6 +8330,7 @@ const SetupDraft = z.object({
|
|
|
7982
8330
|
url: z.string().max(2e3),
|
|
7983
8331
|
keyHeld: z.boolean(),
|
|
7984
8332
|
keyHeader: z.string().max(200),
|
|
8333
|
+
keyPrefix: z.string().max(50).default(""),
|
|
7985
8334
|
headers: z.array(Pair).max(40),
|
|
7986
8335
|
query: z.array(Pair).max(40),
|
|
7987
8336
|
pfx: Path,
|
|
@@ -8103,6 +8452,42 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
|
|
|
8103
8452
|
app.get("/api/map", (c) => answer(c, () => api.map()));
|
|
8104
8453
|
app.get("/api/timeline", (c) => answer(c, () => api.timeline()));
|
|
8105
8454
|
app.get("/api/search", (c) => answer(c, () => api.search(c.req.query("q") ?? "")));
|
|
8455
|
+
app.get("/api/pictures", (c) => answer(c, () => api.pictures()));
|
|
8456
|
+
app.post("/api/file", async (c) => {
|
|
8457
|
+
let origin = "";
|
|
8458
|
+
try {
|
|
8459
|
+
origin = new URL(c.req.header("origin") ?? "").host;
|
|
8460
|
+
} catch {}
|
|
8461
|
+
if (!c.req.header("origin") || origin !== c.req.header("host")) return refuse(c, 403, "cross-origin request refused");
|
|
8462
|
+
if (c.req.header("x-isorite-token") !== token) return refuse(c, 403, "bad token");
|
|
8463
|
+
const path = c.req.query("path") ?? "", type = pictureType(path);
|
|
8464
|
+
if (!type || (c.req.header("content-type") ?? "").split(";")[0].trim().toLowerCase() !== type) return refuse(c, 415, "a picture, of the type its name says");
|
|
8465
|
+
if (Number(c.req.header("content-length") ?? 0) > 2e7) return refuse(c, 413, "picture too large");
|
|
8466
|
+
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
8467
|
+
if (bytes.length > 2e7) return refuse(c, 413, "picture too large");
|
|
8468
|
+
return answer(c, () => api.savePicture(path, bytes));
|
|
8469
|
+
});
|
|
8470
|
+
app.get("/api/file", async (c) => {
|
|
8471
|
+
const code = c.req.query("code"), path = code ?? c.req.query("path") ?? "";
|
|
8472
|
+
let found;
|
|
8473
|
+
try {
|
|
8474
|
+
found = await api.file(path, code === void 0 ? "notes" : "code");
|
|
8475
|
+
} catch (e) {
|
|
8476
|
+
if (e instanceof EngineError) return refuse(c, e.status, e.message);
|
|
8477
|
+
throw e;
|
|
8478
|
+
}
|
|
8479
|
+
if (!found) return refuse(c, 404, `no picture at ${path}`);
|
|
8480
|
+
const etag = `"${found.version}"`;
|
|
8481
|
+
const headers = {
|
|
8482
|
+
"Content-Type": found.type,
|
|
8483
|
+
"Cache-Control": "no-cache",
|
|
8484
|
+
ETag: etag,
|
|
8485
|
+
"X-Content-Type-Options": "nosniff",
|
|
8486
|
+
"Content-Security-Policy": PICTURE_CSP
|
|
8487
|
+
};
|
|
8488
|
+
if (c.req.header("if-none-match") === etag) return c.body(null, 304, headers);
|
|
8489
|
+
return c.body(found.bytes, 200, headers);
|
|
8490
|
+
});
|
|
8106
8491
|
app.put("/api/note", async (c) => {
|
|
8107
8492
|
const refused = writeRefused(c);
|
|
8108
8493
|
if (refused) return refused;
|
|
@@ -8437,12 +8822,26 @@ var NodeFiles = class {
|
|
|
8437
8822
|
throw e;
|
|
8438
8823
|
}
|
|
8439
8824
|
}
|
|
8440
|
-
|
|
8825
|
+
async readBytes(path) {
|
|
8826
|
+
try {
|
|
8827
|
+
return new Uint8Array(await readFile(this.at(path)));
|
|
8828
|
+
} catch (e) {
|
|
8829
|
+
if (gone(e)) return null;
|
|
8830
|
+
throw e;
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8441
8833
|
async write(path, text) {
|
|
8834
|
+
await this.replace(path, text);
|
|
8835
|
+
}
|
|
8836
|
+
async writeBytes(path, bytes) {
|
|
8837
|
+
await this.replace(path, bytes);
|
|
8838
|
+
}
|
|
8839
|
+
/** Written beside the file and renamed over it, so a reader never sees half a note. */
|
|
8840
|
+
async replace(path, data) {
|
|
8442
8841
|
const full = this.at(path), beside = `${full}.${process.pid}.${this.n++}.tmp`;
|
|
8443
8842
|
await mkdir(dirname(full), { recursive: true });
|
|
8444
8843
|
try {
|
|
8445
|
-
await writeFile(beside,
|
|
8844
|
+
await writeFile(beside, data, typeof data === "string" ? "utf8" : void 0);
|
|
8446
8845
|
await rename(beside, full);
|
|
8447
8846
|
} catch (e) {
|
|
8448
8847
|
await rm(beside, { force: true });
|
|
@@ -8456,10 +8855,27 @@ var NodeFiles = class {
|
|
|
8456
8855
|
}
|
|
8457
8856
|
async list(dir) {
|
|
8458
8857
|
try {
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8858
|
+
const at = this.at(dir), entries = await readdir(at, { withFileTypes: true });
|
|
8859
|
+
return (await Promise.all(entries.filter((e) => e.isDirectory() || e.isFile()).map(async (e) => {
|
|
8860
|
+
if (e.isDirectory()) return {
|
|
8861
|
+
name: e.name,
|
|
8862
|
+
dir: true
|
|
8863
|
+
};
|
|
8864
|
+
if (e.name.endsWith(".md")) return {
|
|
8865
|
+
name: e.name,
|
|
8866
|
+
dir: false
|
|
8867
|
+
};
|
|
8868
|
+
const s = await stat(resolve(at, e.name)).catch(() => null);
|
|
8869
|
+
return s ? {
|
|
8870
|
+
name: e.name,
|
|
8871
|
+
dir: false,
|
|
8872
|
+
size: s.size,
|
|
8873
|
+
mtime: s.mtimeMs
|
|
8874
|
+
} : {
|
|
8875
|
+
name: e.name,
|
|
8876
|
+
dir: false
|
|
8877
|
+
};
|
|
8878
|
+
}))).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
8463
8879
|
} catch (e) {
|
|
8464
8880
|
if (gone(e)) return [];
|
|
8465
8881
|
throw e;
|
|
@@ -8481,6 +8897,7 @@ const FORM = [
|
|
|
8481
8897
|
"auth",
|
|
8482
8898
|
"key",
|
|
8483
8899
|
"key_header",
|
|
8900
|
+
"key_prefix",
|
|
8484
8901
|
"headers",
|
|
8485
8902
|
"query",
|
|
8486
8903
|
"pfx",
|
|
@@ -8512,6 +8929,7 @@ function readSetup(config) {
|
|
|
8512
8929
|
url: gateway ? p.url : OPENROUTER.url,
|
|
8513
8930
|
keyHeld: text(p.key) !== "",
|
|
8514
8931
|
keyHeader: text(p.key_header),
|
|
8932
|
+
keyPrefix: text(p.key_prefix).trim(),
|
|
8515
8933
|
headers: pairs(p.headers),
|
|
8516
8934
|
query: pairs(p.query),
|
|
8517
8935
|
pfx: text(p.pfx),
|
|
@@ -8553,6 +8971,9 @@ function applySetup(config, draft) {
|
|
|
8553
8971
|
}
|
|
8554
8972
|
if (gateway) {
|
|
8555
8973
|
if (draft.keyHeader.trim()) p.key_header = draft.keyHeader.trim();
|
|
8974
|
+
const prefix = (draft.keyPrefix ?? "").trim();
|
|
8975
|
+
if (prefix) p.key_prefix = prefix + " ";
|
|
8976
|
+
else if (old.key_prefix === "" && draft.keyHeader.trim()) p.key_prefix = "";
|
|
8556
8977
|
const pairs = (field, given) => {
|
|
8557
8978
|
const before = table(old[field]), out = {};
|
|
8558
8979
|
for (const pair of given) {
|
|
@@ -8713,7 +9134,7 @@ var Setup = class {
|
|
|
8713
9134
|
};
|
|
8714
9135
|
//#endregion
|
|
8715
9136
|
//#region packages/server/src/version.ts
|
|
8716
|
-
const VERSION = "0.
|
|
9137
|
+
const VERSION = "0.10.0";
|
|
8717
9138
|
//#endregion
|
|
8718
9139
|
//#region packages/server/src/git.ts
|
|
8719
9140
|
/** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
|
|
@@ -8946,6 +9367,27 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
|
|
|
8946
9367
|
process.on("SIGTERM", stop);
|
|
8947
9368
|
}
|
|
8948
9369
|
//#endregion
|
|
9370
|
+
//#region packages/server/src/self.ts
|
|
9371
|
+
/** Whether this is the single executable. */
|
|
9372
|
+
const single = () => {
|
|
9373
|
+
try {
|
|
9374
|
+
return isSea();
|
|
9375
|
+
} catch {
|
|
9376
|
+
return false;
|
|
9377
|
+
}
|
|
9378
|
+
};
|
|
9379
|
+
/** What runs isorite again with `args`, given the script (npm's or a checkout's) to run when not single. */
|
|
9380
|
+
function self(script, args, execArgv = process.execArgv) {
|
|
9381
|
+
return {
|
|
9382
|
+
command: process.execPath,
|
|
9383
|
+
args: single() ? args : [
|
|
9384
|
+
...execArgv,
|
|
9385
|
+
script,
|
|
9386
|
+
...args
|
|
9387
|
+
]
|
|
9388
|
+
};
|
|
9389
|
+
}
|
|
9390
|
+
//#endregion
|
|
8949
9391
|
//#region packages/server/src/term.ts
|
|
8950
9392
|
init_src$1();
|
|
8951
9393
|
const MAIN = import.meta.url.endsWith(".ts") ? fileURLToPath(new URL("./main.ts", import.meta.url)) : fileURLToPath(import.meta.url);
|
|
@@ -8992,14 +9434,14 @@ async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4,
|
|
|
8992
9434
|
if (!opened) throw new StudyError(`nothing is serving on port ${port}: name a base`);
|
|
8993
9435
|
const log = join(tmpdir(), `isorite-term-${port}.log`);
|
|
8994
9436
|
const out = openSync(log, "w");
|
|
8995
|
-
|
|
8996
|
-
MAIN,
|
|
9437
|
+
const again = self(MAIN, [
|
|
8997
9438
|
"serve",
|
|
8998
9439
|
path,
|
|
8999
9440
|
"--port",
|
|
9000
9441
|
String(port),
|
|
9001
9442
|
"--held"
|
|
9002
|
-
],
|
|
9443
|
+
], []);
|
|
9444
|
+
started = spawn(again.command, again.args, {
|
|
9003
9445
|
stdio: [
|
|
9004
9446
|
"ignore",
|
|
9005
9447
|
out,
|
|
@@ -9068,6 +9510,25 @@ async function freePort() {
|
|
|
9068
9510
|
});
|
|
9069
9511
|
});
|
|
9070
9512
|
}
|
|
9513
|
+
/** Whether `address` asks a browser that opens it for a password (M21): a 401 with Basic, as OpenCode 2 answers
|
|
9514
|
+
* for /api (its page itself is served to anyone). Asked as a browser navigates, by node:http: OpenCode leaves the
|
|
9515
|
+
* challenge out for its own page's fetches (Sec-Fetch-Mode: cors), which is all Node's fetch can send. */
|
|
9516
|
+
function asksPassword(address) {
|
|
9517
|
+
return new Promise((done) => {
|
|
9518
|
+
const req = get(address, {
|
|
9519
|
+
headers: {
|
|
9520
|
+
Accept: "text/html",
|
|
9521
|
+
"Sec-Fetch-Mode": "navigate",
|
|
9522
|
+
"Sec-Fetch-Dest": "document"
|
|
9523
|
+
},
|
|
9524
|
+
timeout: 3e3
|
|
9525
|
+
}, (res) => {
|
|
9526
|
+
res.resume();
|
|
9527
|
+
done(res.statusCode === 401 && /^\s*basic\b/i.test(String(res.headers["www-authenticate"] ?? "")));
|
|
9528
|
+
});
|
|
9529
|
+
req.on("timeout", () => req.destroy()).on("error", () => done(false));
|
|
9530
|
+
});
|
|
9531
|
+
}
|
|
9071
9532
|
/** A POST to the isorite server, made as its page makes one: from its origin, with the token it hands out. */
|
|
9072
9533
|
async function tell(url, path, body) {
|
|
9073
9534
|
const token = await tokenOf(url);
|
|
@@ -9187,8 +9648,57 @@ function where(port, models, env, tty, page = "/") {
|
|
|
9187
9648
|
async function tokenOf(url) {
|
|
9188
9649
|
return (await (await fetch(`${url}/api/session`)).json()).token;
|
|
9189
9650
|
}
|
|
9651
|
+
/** `isorite opencode` (M21): OpenCode 2, at the version isorite is tested with, in isorite's own npm prefix, the
|
|
9652
|
+
* global one left as it is. npm's registry, proxy and mirror settings apply. Run again, it installs it again. */
|
|
9653
|
+
function installOpenCode(env = process.env, say = console.log, npm = "npm") {
|
|
9654
|
+
const prefix = ownPrefix(env);
|
|
9655
|
+
say(`isorite opencode: installing ${PACKAGE$1} in ${prefix}, for isorite alone; nothing global is changed`);
|
|
9656
|
+
const win = process.platform === "win32";
|
|
9657
|
+
const done = spawnSync(npm, [
|
|
9658
|
+
"install",
|
|
9659
|
+
"--prefix",
|
|
9660
|
+
win ? `"${prefix}"` : prefix,
|
|
9661
|
+
"--no-fund",
|
|
9662
|
+
"--no-audit",
|
|
9663
|
+
"--loglevel=error",
|
|
9664
|
+
PACKAGE$1
|
|
9665
|
+
], {
|
|
9666
|
+
stdio: "inherit",
|
|
9667
|
+
env,
|
|
9668
|
+
shell: win
|
|
9669
|
+
});
|
|
9670
|
+
if (done.status !== 0) {
|
|
9671
|
+
say(`npm could not install it${done.error ? `: ${done.error.message}` : ""}. If npm's registry or mirror is the trouble, fetch an OpenCode 2 another way and name it: ISORITE_OPENCODE=/path/to/opencode isorite term`);
|
|
9672
|
+
return 1;
|
|
9673
|
+
}
|
|
9674
|
+
try {
|
|
9675
|
+
const found = findOpenCode(env);
|
|
9676
|
+
say(`OpenCode ${found.version} is ${found.command}: isorite term uses it from now on`);
|
|
9677
|
+
return 0;
|
|
9678
|
+
} catch (e) {
|
|
9679
|
+
say(e.message);
|
|
9680
|
+
return 1;
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
/** OpenCode 2, or, when there's none and a person is at the terminal, the offer to install it for isorite alone. */
|
|
9684
|
+
async function openCodeFor(env) {
|
|
9685
|
+
try {
|
|
9686
|
+
return findOpenCode(env);
|
|
9687
|
+
} catch (e) {
|
|
9688
|
+
if (!(e instanceof NoOpenCode) || env.ISORITE_OPENCODE || !process.stdin.isTTY || !process.stdout.isTTY) throw e;
|
|
9689
|
+
console.error(e.message);
|
|
9690
|
+
const ask = createInterface({
|
|
9691
|
+
input: process.stdin,
|
|
9692
|
+
output: process.stdout
|
|
9693
|
+
});
|
|
9694
|
+
const yes = /^y(es)?$/i.test((await ask.question(`Install OpenCode 2 for isorite only, in ${ownPrefix(env)}? [y/N] `)).trim());
|
|
9695
|
+
ask.close();
|
|
9696
|
+
if (!yes || installOpenCode(env) !== 0) throw new StudyError("isorite term needs OpenCode 2");
|
|
9697
|
+
return findOpenCode(env);
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9190
9700
|
async function term(path, { port = 4767, cwd = process.cwd(), env: given = process.env, wait = 12e4, onServer } = {}) {
|
|
9191
|
-
const opencode =
|
|
9701
|
+
const opencode = await openCodeFor(given);
|
|
9192
9702
|
const server = await ensureServer(path, {
|
|
9193
9703
|
port,
|
|
9194
9704
|
env: given,
|
|
@@ -9207,7 +9717,6 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9207
9717
|
}
|
|
9208
9718
|
const { gateway, env } = models;
|
|
9209
9719
|
const said = where(port, models, given, void 0, new URL(server.url).pathname.replace(/\/?$/, "/"));
|
|
9210
|
-
for (const line of said) console.log(line);
|
|
9211
9720
|
const agents = path === void 0 ? null : join(locate(path).notes, AGENTS_FILE);
|
|
9212
9721
|
const home = writeHome(defaultHome(env), {
|
|
9213
9722
|
gateway,
|
|
@@ -9229,6 +9738,8 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9229
9738
|
server.release();
|
|
9230
9739
|
throw e;
|
|
9231
9740
|
}
|
|
9741
|
+
if (await asksPassword(`${backend.url}/api/session`)) said.push(` OpenCode's own web page (port ${new URL(backend.url).port}) asks for a password: don't open it. isorite's page is above, and /web in the terminal opens it`);
|
|
9742
|
+
for (const line of said) console.log(line);
|
|
9232
9743
|
await tell(server.url, "/api/terminal/attach", {
|
|
9233
9744
|
url: backend.url,
|
|
9234
9745
|
password: backend.password,
|
|
@@ -9295,6 +9806,7 @@ const READ = [
|
|
|
9295
9806
|
"SSH_CONNECTION",
|
|
9296
9807
|
"ISORITE_EMBED",
|
|
9297
9808
|
"ISORITE_EMBED_DIR",
|
|
9809
|
+
"ISORITE_MODEL_BASE_URL",
|
|
9298
9810
|
"XDG_DATA_HOME",
|
|
9299
9811
|
"XDG_CACHE_HOME"
|
|
9300
9812
|
];
|
|
@@ -9341,7 +9853,10 @@ async function checkProvider(p, h, key, say) {
|
|
|
9341
9853
|
const missing = models.filter((m) => !listed.includes(m));
|
|
9342
9854
|
say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
|
|
9343
9855
|
if (listed.length && missing.length) say("models", "fail", `${missing.join(", ")} not among them; it has ${listed.slice(0, 12).join(", ")}${listed.length > 12 ? ", …" : ""}. Name its models under [tiers]`);
|
|
9344
|
-
} else
|
|
9856
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
9857
|
+
const said = refusal(await res.text().catch(() => ""), key);
|
|
9858
|
+
say("reached", "fail", `it said ${res.status} to the list of models${said ? `: “${said}”` : ""}. The key${key ? `, sent as ${keySentAs(p, key)},` : ""} or a header it wants (headers), is not accepted: check key_header and key_prefix`);
|
|
9859
|
+
} else say("reached", "ok", `it answered (${res.status} for the list of models, which some gateways don't have)`);
|
|
9345
9860
|
} catch (e) {
|
|
9346
9861
|
reached = false;
|
|
9347
9862
|
say("reached", "fail", unreachable(e, p.url));
|
|
@@ -9401,7 +9916,7 @@ async function doctor(folder, o = {}) {
|
|
|
9401
9916
|
const tls = gatewayTls(path, env);
|
|
9402
9917
|
http ??= gatewayHttp(provider, tls, env);
|
|
9403
9918
|
if (existsSync(path)) say("config", "ok", `${path}: ${provider.name} at ${provider.url}`);
|
|
9404
|
-
say("key", key ? "ok" : tls ? "warn" : "fail", key ?
|
|
9919
|
+
say("key", key ? "ok" : tls ? "warn" : "fail", key ? `sent as ${keySentAs(provider, key)}${provider.keyHeader?.toLowerCase() === "authorization" && provider.keyPrefix === void 0 ? ` (key_header = "${provider.keyHeader}" alone means Bearer since 0.9.0; key_prefix = "" sends it bare)` : ""}` : tls ? "none; the client certificate alone is sent" : "none: set one in Iso's settings, or as key under [provider]");
|
|
9405
9920
|
if (tls) say("certificate", "ok", `${tls.files.join(", ")}${tls.pfx || tls.cert ? "" : " (an authority only, no client certificate)"}`);
|
|
9406
9921
|
const tiers = `low ${provider.tiers.low}, mid ${provider.tiers.mid}, max ${provider.tiers.max}`;
|
|
9407
9922
|
say("models", provider.defaultTiers ? "warn" : "ok", provider.defaultTiers ? `${tiers}: OpenRouter's names, kept because [tiers] names none for ${provider.name}; a gateway usually calls its models otherwise` : tiers);
|
|
@@ -9427,11 +9942,21 @@ async function doctor(folder, o = {}) {
|
|
|
9427
9942
|
if (!o.report) out(` asking ${p.name}: its models, then ${models.length + 1} replies of a few words (a few hundred tokens in all)`);
|
|
9428
9943
|
await checkProvider(p, h, key, say);
|
|
9429
9944
|
}
|
|
9945
|
+
const began = Date.now(), git = spawnSync(o.git ?? "git", ["--version"], {
|
|
9946
|
+
encoding: "utf8",
|
|
9947
|
+
env,
|
|
9948
|
+
timeout: 3e3,
|
|
9949
|
+
killSignal: "SIGKILL"
|
|
9950
|
+
});
|
|
9951
|
+
const took = Date.now() - began;
|
|
9952
|
+
if (git.error && git.error.code === "ENOENT") say("git", "skip", "not found: isorite doesn't need it; OpenCode works without it, as a folder that isn't a repository");
|
|
9953
|
+
else if (git.status !== 0 || took >= 3e3) say("git", "warn", `git --version ${took >= 3e3 ? "didn't answer in 3 s" : `failed after ${took} ms`}: likely a wrapper (a company's) that waits on the network. isorite no longer waits on git to start; OpenCode may, and isorite term then says it's waiting for OpenCode's server`);
|
|
9954
|
+
else say("git", took > 1e3 ? "warn" : "ok", `${git.stdout.trim()}, in ${took} ms${took > 1e3 ? ": slow, likely a wrapper; OpenCode may wait on it as it starts" : ""}`);
|
|
9430
9955
|
try {
|
|
9431
9956
|
const found = findOpenCode(env, o.probe);
|
|
9432
|
-
say("opencode", "ok", `${found.command} ${found.version}`);
|
|
9957
|
+
say("opencode", "ok", `${found.command} ${found.version}${found.from === "the PATH" ? "" : ` (${found.from})`}`);
|
|
9433
9958
|
} catch (e) {
|
|
9434
|
-
say("opencode", "warn", e.message.replace(/\n\s*/g, " "));
|
|
9959
|
+
say("opencode", e instanceof NoOpenCode && e.old ? "fail" : "warn", e.message.replace(/\n\s*/g, " "));
|
|
9435
9960
|
}
|
|
9436
9961
|
const port = o.port ?? 4767, url = `http://127.0.0.1:${port}`, served = await serving(url);
|
|
9437
9962
|
say("port", "ok", served === null ? `${port} is free: isorite term starts a server there` : `${port} is isorite's, serving ${served}`);
|
|
@@ -9449,6 +9974,12 @@ async function doctor(folder, o = {}) {
|
|
|
9449
9974
|
known = false;
|
|
9450
9975
|
}
|
|
9451
9976
|
say("terminal", known ? "ok" : "warn", terminal);
|
|
9977
|
+
const hint = "isorite term also runs OpenCode's own web server, on a port of its own, which asks for a password: open isorite's pages and panes (/web in the terminal), not OpenCode's";
|
|
9978
|
+
if (served === null) say("browser pages", "ok", hint);
|
|
9979
|
+
else {
|
|
9980
|
+
const asking = (await Promise.all([`${url}/`, `${url}/api/session`].map(async (at) => await asksPassword(at) ? at : null))).filter(Boolean);
|
|
9981
|
+
say("browser pages", asking.length ? "fail" : "ok", asking.length ? `${asking.join(" and ")} asked for a password (401, Basic): that is not isorite's server; stop it, or give isorite another port (--port <n>)` : `isorite's pages at ${url}/ ask for no password. ${hint}`);
|
|
9982
|
+
}
|
|
9452
9983
|
if (env.SSH_CONNECTION) say("browser", "ok", `over SSH: on your own machine, ssh -L ${port}:localhost:${port} <this host>, then http://localhost:${port}/`);
|
|
9453
9984
|
let model = null;
|
|
9454
9985
|
if (env.ISORITE_EMBED === "off") say("search by meaning", "skip", "switched off (ISORITE_EMBED=off)");
|
|
@@ -9460,7 +9991,23 @@ async function doctor(folder, o = {}) {
|
|
|
9460
9991
|
say("search by meaning", "fail", `${dir}: ${e.message}`);
|
|
9461
9992
|
}
|
|
9462
9993
|
if (model) say("search by meaning", "ok", `${model.name} at ${model.dir}`);
|
|
9463
|
-
else if (!dir)
|
|
9994
|
+
else if (!dir) {
|
|
9995
|
+
let reach = "";
|
|
9996
|
+
if (!o.offline) {
|
|
9997
|
+
const from = mirrorOf(env) ?? RELEASE;
|
|
9998
|
+
try {
|
|
9999
|
+
const res = await fetch(from, {
|
|
10000
|
+
method: "HEAD",
|
|
10001
|
+
redirect: "follow",
|
|
10002
|
+
signal: AbortSignal.timeout(1e4)
|
|
10003
|
+
});
|
|
10004
|
+
reach = res.ok ? ` ${from} answers from here.` : ` ${from} said ${res.status}.`;
|
|
10005
|
+
} catch (e) {
|
|
10006
|
+
reach = ` ${from} can't be reached from here: ${unreachable(e, from)}.`;
|
|
10007
|
+
}
|
|
10008
|
+
}
|
|
10009
|
+
say("search by meaning", "warn", `no model in ${modelPlaces(env).map((p) => p.dir).join(" or ")}: isorite model fetches it (53 MB).${reach} By hand: ${mirrorOf(env) ?? RELEASE} (SHA-256 ${RELEASE_SHA256}), then isorite model --from <that file>. Search by words works without it`);
|
|
10010
|
+
}
|
|
9464
10011
|
}
|
|
9465
10012
|
if (folder) try {
|
|
9466
10013
|
const { notes } = locate(folder);
|
|
@@ -9538,7 +10085,8 @@ function proxied(env, execArgv = process.execArgv) {
|
|
|
9538
10085
|
}
|
|
9539
10086
|
/** Run this command again with `env`, its input and output its own; its exit code. */
|
|
9540
10087
|
function rerun(env, argv = process.argv, execArgv = process.execArgv) {
|
|
9541
|
-
const
|
|
10088
|
+
const again = self(argv[1], argv.slice(2), execArgv);
|
|
10089
|
+
const child = spawn(again.command, again.args, {
|
|
9542
10090
|
stdio: "inherit",
|
|
9543
10091
|
env
|
|
9544
10092
|
});
|
|
@@ -9746,7 +10294,7 @@ async function mcp(path, { port = 4767, agent = "mcp", quiet = QUIET_MS, input =
|
|
|
9746
10294
|
}
|
|
9747
10295
|
throw Object.assign(/* @__PURE__ */ new Error(`no method ${rpc.method}`), { code: -32601 });
|
|
9748
10296
|
};
|
|
9749
|
-
const lines = createInterface({
|
|
10297
|
+
const lines = createInterface$1({
|
|
9750
10298
|
input,
|
|
9751
10299
|
crlfDelay: Infinity
|
|
9752
10300
|
});
|
|
@@ -10829,6 +11377,19 @@ const finderHttp = (pace = 0) => scriptedHttp(finderAnswer, pace);
|
|
|
10829
11377
|
//#region packages/guide/src/node.ts
|
|
10830
11378
|
/** In a checkout, the package's notes/; in the npm package, where this is bundled into dist/, the guide/ beside it. */
|
|
10831
11379
|
const GUIDE_DIR = [join(import.meta.dirname, "..", "guide"), join(import.meta.dirname, "..", "notes")].find((d) => existsSync(join(d, "start-here.md"))) ?? join(import.meta.dirname, "..", "notes");
|
|
11380
|
+
/** The guide's pictures (M22), by path, as bytes. */
|
|
11381
|
+
function loadGuidePictures(dir = GUIDE_DIR) {
|
|
11382
|
+
const out = {};
|
|
11383
|
+
const visit = (at) => {
|
|
11384
|
+
for (const name of readdirSync(at)) {
|
|
11385
|
+
const path = join(at, name);
|
|
11386
|
+
if (statSync(path).isDirectory()) visit(path);
|
|
11387
|
+
else if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(name)) out[relative(dir, path).split(sep).join("/")] = new Uint8Array(readFileSync(path));
|
|
11388
|
+
}
|
|
11389
|
+
};
|
|
11390
|
+
visit(dir);
|
|
11391
|
+
return out;
|
|
11392
|
+
}
|
|
10832
11393
|
/** The guide's files as they stand in the package: its notes and its AGENTS.md, by path. */
|
|
10833
11394
|
function loadGuide(dir = GUIDE_DIR) {
|
|
10834
11395
|
const out = {};
|
|
@@ -10856,7 +11417,7 @@ function writeGuide(folder) {
|
|
|
10856
11417
|
fresh: false
|
|
10857
11418
|
};
|
|
10858
11419
|
if (existsSync(at) && readdirSync(at).some((f) => f !== ".isorite")) throw new StudyError(`${at} has files in it that are not the guide: name another folder, isorite guide <folder>`);
|
|
10859
|
-
for (const [path, text] of Object.entries(loadGuide())) {
|
|
11420
|
+
for (const [path, text] of [...Object.entries(loadGuide()), ...Object.entries(loadGuidePictures())]) {
|
|
10860
11421
|
mkdirSync(dirname(join(at, path)), { recursive: true });
|
|
10861
11422
|
writeFileSync(join(at, path), text);
|
|
10862
11423
|
}
|
|
@@ -10905,8 +11466,21 @@ async function writeExample(folder, now = Date.now()) {
|
|
|
10905
11466
|
mkdirSync(dirname(at), { recursive: true });
|
|
10906
11467
|
writeFileSync(at, text);
|
|
10907
11468
|
}
|
|
11469
|
+
copyPictures(NOTES_DIR, notes);
|
|
10908
11470
|
return { notes };
|
|
10909
11471
|
}
|
|
11472
|
+
/** The pictures among a folder's notes (M22), copied as they are: they have no history. */
|
|
11473
|
+
function copyPictures(from, to, dir = from) {
|
|
11474
|
+
for (const name of readdirSync(dir)) {
|
|
11475
|
+
const path = join(dir, name);
|
|
11476
|
+
if (statSync(path).isDirectory()) copyPictures(from, to, path);
|
|
11477
|
+
else if (citeKind(name) === "image") {
|
|
11478
|
+
const at = join(to, relative(from, path));
|
|
11479
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
11480
|
+
copyFileSync(path, at);
|
|
11481
|
+
}
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
10910
11484
|
const GEOMETRY_DIR = join(import.meta.dirname, "..", "geometry");
|
|
10911
11485
|
/** The geometry example's notes, and the code beside them, as they stand today, by path. */
|
|
10912
11486
|
function loadGeometry(dir = GEOMETRY_DIR) {
|
|
@@ -11037,7 +11611,8 @@ init_model();
|
|
|
11037
11611
|
const USAGE = `isorite: an agentic harness for understanding
|
|
11038
11612
|
|
|
11039
11613
|
isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
|
|
11040
|
-
isorite check <notes>
|
|
11614
|
+
isorite check <notes> [--unused] check a folder of notes: frontmatter, links, cycles; --unused lists
|
|
11615
|
+
the pictures no note shows
|
|
11041
11616
|
isorite serve <notes> [--port <n>] [--allow-host <name>]
|
|
11042
11617
|
open it in the browser, on this machine only
|
|
11043
11618
|
isorite migrate <study base> [--merge] move a study base (format 1) into its notes' .isorite/; exercises
|
|
@@ -11051,9 +11626,13 @@ const USAGE = `isorite: an agentic harness for understanding
|
|
|
11051
11626
|
isorite mcp [<notes>] [--port <n>] [--agent <name>]
|
|
11052
11627
|
the knowledge base's tools over MCP, on stdio, for Claude Code or Pi:
|
|
11053
11628
|
claude mcp add isorite -- isorite mcp <base> --agent claude-code
|
|
11054
|
-
isorite model [--from <folder>]
|
|
11055
|
-
|
|
11629
|
+
isorite model [--from <folder|.tgz>] [--url <url>]
|
|
11630
|
+
the model for search by meaning: which is installed, and where; else
|
|
11631
|
+
fetched (53 MB, once) from ISORITE_MODEL_BASE_URL, npm, GitHub or
|
|
11632
|
+
Hugging Face, or --url alone; or from the .tgz fetched by hand, or a
|
|
11056
11633
|
folder: its files copied there, or any compatible model
|
|
11634
|
+
isorite opencode OpenCode 2 for isorite term, installed in isorite's own folder
|
|
11635
|
+
(~/.config/isorite/opencode-cli), the global one left as it is
|
|
11057
11636
|
isorite doctor [<notes>] [--report] [--offline]
|
|
11058
11637
|
whether isorite can work here: the configuration, the network, the
|
|
11059
11638
|
provider (asked a few words, a few hundred tokens), the terminal and
|
|
@@ -11070,7 +11649,7 @@ const USAGE = `isorite: an agentic harness for understanding
|
|
|
11070
11649
|
the browser; run again, as you left it. Iso is your model, or with
|
|
11071
11650
|
none set up, the guide's finder (also: isorite --tutorial)
|
|
11072
11651
|
`;
|
|
11073
|
-
function check(notes, out = console.log) {
|
|
11652
|
+
function check(notes, out = console.log, unused = false) {
|
|
11074
11653
|
const root = resolve(notes);
|
|
11075
11654
|
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
11076
11655
|
out(`no folder at ${root}`);
|
|
@@ -11081,12 +11660,18 @@ function check(notes, out = console.log) {
|
|
|
11081
11660
|
for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
|
|
11082
11661
|
const errors = issues.filter((i) => i.level === "error").length;
|
|
11083
11662
|
out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - errors} warnings, ${cycles.length} requires cycles`);
|
|
11663
|
+
if (unused) {
|
|
11664
|
+
const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
|
|
11665
|
+
const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();
|
|
11666
|
+
for (const f of idle) out(`unused ${f}`);
|
|
11667
|
+
out(idle.length ? `${idle.length} ${idle.length === 1 ? "picture" : "pictures"} no note shows` : "every picture is shown by a note");
|
|
11668
|
+
}
|
|
11084
11669
|
return errors ? 1 : 0;
|
|
11085
11670
|
}
|
|
11086
11671
|
function main(argv) {
|
|
11087
11672
|
const [command, ...rest] = argv;
|
|
11088
11673
|
try {
|
|
11089
|
-
if (command === "check" && rest
|
|
11674
|
+
if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
|
|
11090
11675
|
if (command === "new" && rest[0]) {
|
|
11091
11676
|
const made = newBase(rest[0]);
|
|
11092
11677
|
console.log(`${made}: a knowledge base, empty. Open it in the browser, or beside your code in the terminal:\n isorite serve ${made}\n cd <your code> && isorite term ${made}`);
|
|
@@ -11151,13 +11736,18 @@ function main(argv) {
|
|
|
11151
11736
|
});
|
|
11152
11737
|
}
|
|
11153
11738
|
if (command === "model") {
|
|
11154
|
-
const from = rest[rest.indexOf("--from") + 1];
|
|
11739
|
+
const from = rest[rest.indexOf("--from") + 1], url = rest[rest.indexOf("--url") + 1];
|
|
11155
11740
|
if (rest.includes("--from") && !from) {
|
|
11156
|
-
console.error("--from takes a folder");
|
|
11741
|
+
console.error("--from takes a folder, or the model's .tgz");
|
|
11742
|
+
return 2;
|
|
11743
|
+
}
|
|
11744
|
+
if (rest.includes("--url") && !/^https?:\/\//.test(url ?? "")) {
|
|
11745
|
+
console.error("--url takes the address of the model's .tgz");
|
|
11157
11746
|
return 2;
|
|
11158
11747
|
}
|
|
11159
|
-
return modelCommand(rest.includes("--from") ? from : void 0);
|
|
11748
|
+
return modelCommand(rest.includes("--from") ? from : void 0, console.log, process.env, {}, rest.includes("--url") ? url : void 0);
|
|
11160
11749
|
}
|
|
11750
|
+
if (command === "opencode") return installOpenCode();
|
|
11161
11751
|
if (command === "memory") return memory(rest.find((a) => !a.startsWith("--")));
|
|
11162
11752
|
if (command === "bases") return bases(rest);
|
|
11163
11753
|
if (command === "--version" || command === "-v") {
|
|
@@ -11232,16 +11822,19 @@ function starting(argv, env = process.env, write = (s) => process.stderr.write(s
|
|
|
11232
11822
|
write("isorite: starting…\n");
|
|
11233
11823
|
return true;
|
|
11234
11824
|
}
|
|
11235
|
-
|
|
11236
|
-
|
|
11825
|
+
/** The command as run: said to be starting, the proxy turned on (running itself again), the system's authorities
|
|
11826
|
+
* trusted; its exit code. The single executable calls this (M21). */
|
|
11827
|
+
async function run(argv) {
|
|
11828
|
+
starting(argv);
|
|
11237
11829
|
delete process.env.ISORITE_STARTING;
|
|
11238
11830
|
const env = proxied(process.env);
|
|
11239
|
-
if (env)
|
|
11831
|
+
if (env) return rerun({
|
|
11240
11832
|
...env,
|
|
11241
11833
|
ISORITE_STARTING: "1"
|
|
11242
|
-
})
|
|
11834
|
+
});
|
|
11243
11835
|
trustSystem();
|
|
11244
|
-
|
|
11836
|
+
return main(argv);
|
|
11245
11837
|
}
|
|
11838
|
+
if (!single() && process.argv[1] && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href) process.exit(await run(process.argv.slice(2)));
|
|
11246
11839
|
//#endregion
|
|
11247
|
-
export { check, main, starting };
|
|
11840
|
+
export { check, main, run, starting };
|