isorite 0.13.0 → 0.14.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 +305 -14
- package/docs/setup.md +5 -4
- package/docs/terminal.md +1 -1
- package/guide/your-own-base/starting-a-base.md +4 -4
- package/package.json +1 -1
- package/web/_app/immutable/assets/{0.BEjn1Bfl.css → 0.BPP604gH.css} +1 -1
- package/web/_app/immutable/chunks/{D9JwUycj.js → BGW1TKtR.js} +1 -1
- package/web/_app/immutable/chunks/{BCyLp7pS.js → BPrp4s2r.js} +1 -1
- package/web/_app/immutable/chunks/{C8nK3Jvr.js → CIYFNih-.js} +1 -1
- package/web/_app/immutable/chunks/{O98JkczU.js → CPSlCQsu.js} +2 -2
- package/web/_app/immutable/chunks/{Bt-fHApV.js → DBLoWLMb.js} +1 -1
- package/web/_app/immutable/chunks/{BOxykeIc.js → DFKYo3Wx.js} +1 -1
- package/web/_app/immutable/chunks/{Cd_SqIWq.js → DxVFSMVD.js} +3 -3
- package/web/_app/immutable/chunks/{3fCuxe18.js → DymLl-qY.js} +1 -1
- package/web/_app/immutable/chunks/{s_hW4oA1.js → Dzi94aDH.js} +1 -1
- package/web/_app/immutable/chunks/{B6uHgLdT.js → NyWzYSz1.js} +1 -1
- package/web/_app/immutable/chunks/X3aTZRe_.js +1 -0
- package/web/_app/immutable/entry/{app.DM_AWBgH.js → app.BzA5QYtw.js} +2 -2
- package/web/_app/immutable/entry/start.BOh0Gj31.js +1 -0
- package/web/_app/immutable/nodes/{0.xSbkmYPn.js → 0.BmaxMLzC.js} +2 -2
- package/web/_app/immutable/nodes/{1.BD2wJdoH.js → 1.IQDu8pD1.js} +1 -1
- package/web/_app/immutable/nodes/2.D2Oi-ADx.js +1 -0
- package/web/_app/immutable/nodes/{3.e_5kWaf_.js → 3.CF1W85jb.js} +1 -1
- package/web/_app/immutable/nodes/{4.BEMEU6Jz.js → 4.BMYBn904.js} +1 -1
- package/web/_app/immutable/nodes/5.1vm6WZdM.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +13 -13
- package/web/_app/immutable/chunks/WLe0MT1c.js +0 -1
- package/web/_app/immutable/entry/start.BLhIKDcR.js +0 -1
- package/web/_app/immutable/nodes/2.shPqwtS_.js +0 -1
- package/web/_app/immutable/nodes/5.DxaO-e4O.js +0 -1
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } 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";
|
|
@@ -18,6 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
import { appendFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
19
19
|
import { createInterface } from "node:readline";
|
|
20
20
|
import { PassThrough } from "node:stream";
|
|
21
|
+
import { createServer } from "node:net";
|
|
21
22
|
//#region \0rolldown/runtime.js
|
|
22
23
|
var __defProp = Object.defineProperty;
|
|
23
24
|
var __esmMin = (fn, res, err) => () => {
|
|
@@ -2276,7 +2277,8 @@ function knownBases(config = configPath()) {
|
|
|
2276
2277
|
return BASE_NAME.test(name) && typeof row.path === "string" && row.path ? [{
|
|
2277
2278
|
name,
|
|
2278
2279
|
path: expand(row.path),
|
|
2279
|
-
lend: row.lend === true
|
|
2280
|
+
lend: row.lend === true,
|
|
2281
|
+
...typeof row.used === "string" ? { used: row.used } : {}
|
|
2280
2282
|
}] : [];
|
|
2281
2283
|
});
|
|
2282
2284
|
}
|
|
@@ -2286,6 +2288,16 @@ function baseName(folder) {
|
|
|
2286
2288
|
}
|
|
2287
2289
|
/** The base at `folder` by its name in the configuration, listed first if it is not: under its folder's name, or
|
|
2288
2290
|
* that name with -2, -3… where another folder has it. `as` names it, and `lend` lends it. */
|
|
2291
|
+
/** A base opened now: when it was last used is kept (`used`), for `isorite` to list the bases last used first. */
|
|
2292
|
+
function touchBase(folder, { path = configPath(), now = /* @__PURE__ */ new Date() } = {}) {
|
|
2293
|
+
const at = resolve(folder), bases = readConfig(basesPath(path)), name = knownBases(path).find((b) => b.path === at)?.name;
|
|
2294
|
+
if (!name) return;
|
|
2295
|
+
bases[name] = {
|
|
2296
|
+
...table(bases[name]),
|
|
2297
|
+
used: now.toISOString()
|
|
2298
|
+
};
|
|
2299
|
+
writeConfig(bases, basesPath(path));
|
|
2300
|
+
}
|
|
2289
2301
|
function rememberBase(folder, { path = configPath(), as, lend } = {}) {
|
|
2290
2302
|
const at = resolve(folder), had = knownBases(path).find((b) => b.path === at);
|
|
2291
2303
|
if (had && lend === void 0) return had.name;
|
|
@@ -5102,7 +5114,7 @@ var Bases = class {
|
|
|
5102
5114
|
}
|
|
5103
5115
|
};
|
|
5104
5116
|
/** The revisions answered, newest first: a client asking for one of these gets it. */
|
|
5105
|
-
const KNOWN = [
|
|
5117
|
+
const KNOWN$1 = [
|
|
5106
5118
|
"2026-07-28",
|
|
5107
5119
|
"2025-11-25",
|
|
5108
5120
|
"2025-06-18",
|
|
@@ -5160,13 +5172,13 @@ var McpEndpoint = class {
|
|
|
5160
5172
|
const p = rpc.params ?? {};
|
|
5161
5173
|
switch (rpc.method) {
|
|
5162
5174
|
case "initialize": return {
|
|
5163
|
-
protocolVersion: typeof p.protocolVersion === "string" && KNOWN.includes(p.protocolVersion) ? p.protocolVersion : KNOWN[1],
|
|
5175
|
+
protocolVersion: typeof p.protocolVersion === "string" && KNOWN$1.includes(p.protocolVersion) ? p.protocolVersion : KNOWN$1[1],
|
|
5164
5176
|
capabilities: this.#capabilities(),
|
|
5165
5177
|
serverInfo: this.#info(),
|
|
5166
5178
|
instructions: INSTRUCTIONS
|
|
5167
5179
|
};
|
|
5168
5180
|
case "server/discover": return {
|
|
5169
|
-
supportedVersions: KNOWN,
|
|
5181
|
+
supportedVersions: KNOWN$1,
|
|
5170
5182
|
capabilities: this.#capabilities(),
|
|
5171
5183
|
serverInfo: this.#info(),
|
|
5172
5184
|
instructions: INSTRUCTIONS
|
|
@@ -6026,7 +6038,7 @@ var AcpFollower = class {
|
|
|
6026
6038
|
};
|
|
6027
6039
|
//#endregion
|
|
6028
6040
|
//#region packages/server/src/version.ts
|
|
6029
|
-
const VERSION = "0.
|
|
6041
|
+
const VERSION = "0.14.0";
|
|
6030
6042
|
//#endregion
|
|
6031
6043
|
//#region packages/server/src/connect.ts
|
|
6032
6044
|
const HARNESSES = [{
|
|
@@ -6224,6 +6236,87 @@ async function tokenOf(url) {
|
|
|
6224
6236
|
return (await (await fetch(`${url}/api/session`)).json()).token;
|
|
6225
6237
|
}
|
|
6226
6238
|
//#endregion
|
|
6239
|
+
//#region packages/server/src/detect.ts
|
|
6240
|
+
const KNOWN = [
|
|
6241
|
+
{
|
|
6242
|
+
id: "claude",
|
|
6243
|
+
name: "Claude Code",
|
|
6244
|
+
bin: "claude",
|
|
6245
|
+
command: "npx -y @agentclientprotocol/claude-agent-acp",
|
|
6246
|
+
needs: "npx",
|
|
6247
|
+
keys: ["ANTHROPIC_API_KEY"],
|
|
6248
|
+
signedIn: true
|
|
6249
|
+
},
|
|
6250
|
+
{
|
|
6251
|
+
id: "codex",
|
|
6252
|
+
name: "Codex",
|
|
6253
|
+
bin: "codex",
|
|
6254
|
+
command: "npx -y @agentclientprotocol/codex-acp",
|
|
6255
|
+
needs: "npx",
|
|
6256
|
+
keys: ["CODEX_API_KEY", "OPENAI_API_KEY"]
|
|
6257
|
+
},
|
|
6258
|
+
{
|
|
6259
|
+
id: "opencode",
|
|
6260
|
+
name: "OpenCode",
|
|
6261
|
+
bin: "opencode",
|
|
6262
|
+
command: "opencode acp",
|
|
6263
|
+
keys: []
|
|
6264
|
+
},
|
|
6265
|
+
{
|
|
6266
|
+
id: "omp",
|
|
6267
|
+
name: "omp",
|
|
6268
|
+
bin: "omp",
|
|
6269
|
+
command: "omp acp",
|
|
6270
|
+
keys: []
|
|
6271
|
+
},
|
|
6272
|
+
{
|
|
6273
|
+
id: "hermes",
|
|
6274
|
+
name: "Hermes",
|
|
6275
|
+
bin: "hermes",
|
|
6276
|
+
command: "hermes acp",
|
|
6277
|
+
keys: []
|
|
6278
|
+
}
|
|
6279
|
+
];
|
|
6280
|
+
/** The first file called `name` on PATH (with the extensions Windows tries), or null. */
|
|
6281
|
+
function onPath(name, env = process.env) {
|
|
6282
|
+
const exts = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
|
|
6283
|
+
for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) for (const ext of exts) {
|
|
6284
|
+
const file = join(dir, name + ext);
|
|
6285
|
+
try {
|
|
6286
|
+
if (existsSync(file) && statSync(file).isFile()) return file;
|
|
6287
|
+
} catch {}
|
|
6288
|
+
}
|
|
6289
|
+
return null;
|
|
6290
|
+
}
|
|
6291
|
+
function detectAgents(env = process.env) {
|
|
6292
|
+
return KNOWN.flatMap((k) => {
|
|
6293
|
+
const path = onPath(k.bin, env);
|
|
6294
|
+
if (!path) return [];
|
|
6295
|
+
const keySet = k.keys.some((v) => Boolean(env[v]));
|
|
6296
|
+
const ready = !k.needs || onPath(k.needs, env) !== null;
|
|
6297
|
+
const parts = [`${k.name} is installed`];
|
|
6298
|
+
if (!ready) parts.push(`but it needs ${k.needs}, which isn't on PATH`);
|
|
6299
|
+
else if (k.keys.length && keySet) parts.push(`and ${k.keys.find((v) => env[v])} is set here`);
|
|
6300
|
+
else if (k.keys.length) parts.push(k.signedIn ? `; ${k.keys[0]} isn't set here, which is fine if you've signed in with it` : `, but ${k.keys.join(" or ")} isn't set in the terminal isorite was started from`);
|
|
6301
|
+
return [{
|
|
6302
|
+
id: k.id,
|
|
6303
|
+
name: k.name,
|
|
6304
|
+
command: k.command,
|
|
6305
|
+
path,
|
|
6306
|
+
keys: k.keys,
|
|
6307
|
+
keySet,
|
|
6308
|
+
ready,
|
|
6309
|
+
note: parts.join(" ").replace(" ;", ";").replace(" ,", ",") + "."
|
|
6310
|
+
}];
|
|
6311
|
+
});
|
|
6312
|
+
}
|
|
6313
|
+
/** Why an agent that stopped may have: the key it reads isn't set where isorite was started, for the command run. */
|
|
6314
|
+
function missingKey(command, env = process.env) {
|
|
6315
|
+
const k = KNOWN.find((x) => x.command === command);
|
|
6316
|
+
if (!k || !k.keys.length || k.keys.some((v) => env[v])) return "";
|
|
6317
|
+
return `${k.keys.join(" or ")} is not set in the terminal isorite was started from. Set it there (export ${k.keys[0]}=…) and start isorite again${k.signedIn ? ", or sign in with `claude` first" : ""}.`;
|
|
6318
|
+
}
|
|
6319
|
+
//#endregion
|
|
6227
6320
|
//#region packages/server/src/agents.ts
|
|
6228
6321
|
init_self();
|
|
6229
6322
|
const FRESH = 36e5;
|
|
@@ -6394,12 +6487,14 @@ var AgentRunner = class {
|
|
|
6394
6487
|
process.once("exit", () => this.#child?.kill());
|
|
6395
6488
|
}
|
|
6396
6489
|
state() {
|
|
6490
|
+
const hint = this.#problem && this.#command ? missingKey(this.#command, this.#env) : "";
|
|
6397
6491
|
return {
|
|
6398
6492
|
choice: chosenAgent(this.#base, this.#config),
|
|
6399
6493
|
running: this.#child !== null,
|
|
6400
6494
|
command: this.#command,
|
|
6401
6495
|
problem: this.#problem,
|
|
6402
|
-
said: this.#said
|
|
6496
|
+
said: this.#said,
|
|
6497
|
+
hint
|
|
6403
6498
|
};
|
|
6404
6499
|
}
|
|
6405
6500
|
/** Kept, and started in place of the one running; with none, the one running is stopped. */
|
|
@@ -6765,6 +6860,10 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
|
|
|
6765
6860
|
}
|
|
6766
6861
|
if (agents) {
|
|
6767
6862
|
app.get("/api/acp/agent", (c) => answer(c, async () => agents.state()));
|
|
6863
|
+
app.get("/api/acp/detected", (c) => answer(c, async () => {
|
|
6864
|
+
if (!local(c)) throw new EngineError("agents are looked for only from this machine", 403);
|
|
6865
|
+
return detectAgents();
|
|
6866
|
+
}));
|
|
6768
6867
|
app.get("/api/acp/registry", (c) => answer(c, async () => registryAgents().catch((e) => {
|
|
6769
6868
|
throw new EngineError(e.message, 502);
|
|
6770
6869
|
})));
|
|
@@ -7321,6 +7420,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
|
|
|
7321
7420
|
const opened = locate(path);
|
|
7322
7421
|
const device = deviceId(config);
|
|
7323
7422
|
const first = rememberBase(opened.notes, { path: config });
|
|
7423
|
+
touchBase(opened.notes, { path: config });
|
|
7324
7424
|
const embedder = model ? nodeEmbedder(model) : null;
|
|
7325
7425
|
const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
|
|
7326
7426
|
const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
|
|
@@ -7389,7 +7489,7 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
|
|
|
7389
7489
|
process.exit(1);
|
|
7390
7490
|
});
|
|
7391
7491
|
console.log(`isorite: ${notes}`);
|
|
7392
|
-
console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model)");
|
|
7492
|
+
console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model, 53 MB)");
|
|
7393
7493
|
const lent = bases.lent().filter((l) => l.name !== bases.first).map((l) => l.name);
|
|
7394
7494
|
if (lent.length) console.log(` lent to every session: ${lent.join(", ")} (isorite bases)`);
|
|
7395
7495
|
console.log(existsSync(join(WEB_DIR, "index.html")) ? ` ${link(`http://localhost:${port}/`)}` : ` the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
|
|
@@ -7509,6 +7609,11 @@ async function doctor(folder, o = {}) {
|
|
|
7509
7609
|
const { notes } = locate(folder);
|
|
7510
7610
|
const own = join(notes, OWN_DIR), settings = join(own, SETTINGS_FILE);
|
|
7511
7611
|
say("notes", "ok", existsSync(own) ? `${notes}, with its ${OWN_DIR}/` : `${notes}: its ${OWN_DIR}/ is made the first time it is opened`);
|
|
7612
|
+
if (existsSync(notes) && statSync(notes).isDirectory()) {
|
|
7613
|
+
const bundle = loadBundle(notes), issues = bundle.lint(), cycles = bundle.requiresCycles();
|
|
7614
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
7615
|
+
say("links", errors.length ? "fail" : issues.length || cycles.length ? "warn" : "ok", `${bundle.concepts.size} notes; ${errors.length} errors, ${issues.length - errors.length} warnings, ${cycles.length} requires cycles${issues.length ? `: ${issues.slice(0, 3).map((i) => `${i.path}: ${i.message}`).join("; ")}${issues.length > 3 ? `; and ${issues.length - 3} more (isorite check ${notes})` : ""}` : ""}`);
|
|
7616
|
+
}
|
|
7512
7617
|
const repository = existsSync(settings) ? parseSettings(readFileSync(settings, "utf8")).repository : void 0;
|
|
7513
7618
|
const found = repoOf(notes);
|
|
7514
7619
|
if (!repository && found) say("repository", "ok", `${found}: code links point here`);
|
|
@@ -7749,7 +7854,7 @@ const OPENS = /* @__PURE__ */ new Set([
|
|
|
7749
7854
|
"session/resume",
|
|
7750
7855
|
"session/fork"
|
|
7751
7856
|
]);
|
|
7752
|
-
const SKIP = /* @__PURE__ */ new Set([
|
|
7857
|
+
const SKIP$1 = /* @__PURE__ */ new Set([
|
|
7753
7858
|
"node_modules",
|
|
7754
7859
|
".git",
|
|
7755
7860
|
OWN_DIR,
|
|
@@ -7760,7 +7865,7 @@ const SKIP = /* @__PURE__ */ new Set([
|
|
|
7760
7865
|
]);
|
|
7761
7866
|
/** The base for a session started in `cwd`: the folder itself or one above it holding .isorite/, else the first
|
|
7762
7867
|
* below it, three levels down at most (a base inside the repository, as notes/); or none. */
|
|
7763
|
-
function findBase(cwd) {
|
|
7868
|
+
function findBase$1(cwd) {
|
|
7764
7869
|
for (let at = cwd;; at = dirname(at)) {
|
|
7765
7870
|
if (existsSync(join(at, ".isorite"))) return at;
|
|
7766
7871
|
if (dirname(at) === at) break;
|
|
@@ -7771,7 +7876,7 @@ function findBase(cwd) {
|
|
|
7771
7876
|
for (const dir of level) {
|
|
7772
7877
|
let names = [];
|
|
7773
7878
|
try {
|
|
7774
|
-
names = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !SKIP.has(d.name) && !d.name.startsWith(".")).map((d) => d.name).sort();
|
|
7879
|
+
names = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !SKIP$1.has(d.name) && !d.name.startsWith(".")).map((d) => d.name).sort();
|
|
7775
7880
|
} catch {
|
|
7776
7881
|
continue;
|
|
7777
7882
|
}
|
|
@@ -7793,7 +7898,7 @@ async function tokenWhenUp(url, wait = 6e4) {
|
|
|
7793
7898
|
}
|
|
7794
7899
|
}
|
|
7795
7900
|
async function acp(path, { agent, port = 4767, input, output, env = process.env, pane = false, url: at }) {
|
|
7796
|
-
if (!at) path ??= findBase(process.cwd()) ?? void 0;
|
|
7901
|
+
if (!at) path ??= findBase$1(process.cwd()) ?? void 0;
|
|
7797
7902
|
const server = at ? {
|
|
7798
7903
|
url: at,
|
|
7799
7904
|
release: () => void 0
|
|
@@ -9141,10 +9246,123 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
|
|
|
9141
9246
|
return 0;
|
|
9142
9247
|
}
|
|
9143
9248
|
//#endregion
|
|
9249
|
+
//#region packages/server/src/start.ts
|
|
9250
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
9251
|
+
"node_modules",
|
|
9252
|
+
"dist",
|
|
9253
|
+
"build",
|
|
9254
|
+
"target",
|
|
9255
|
+
"vendor"
|
|
9256
|
+
]);
|
|
9257
|
+
const isBase = (dir) => existsSync(join(dir, ".isorite")) && statSync(join(dir, ".isorite")).isDirectory();
|
|
9258
|
+
/** The base `from` is in or beside: a folder holding .isorite/ on the way up to the repository's top (or `from`
|
|
9259
|
+
* alone, outside a repository), else the one folder just inside that top that holds one. */
|
|
9260
|
+
function findBase(from) {
|
|
9261
|
+
const start = resolve(from), repo = repoOf(start), top = repo ?? start;
|
|
9262
|
+
for (let at = start;; at = dirname(at)) {
|
|
9263
|
+
if (isBase(at)) return {
|
|
9264
|
+
kind: "base",
|
|
9265
|
+
notes: at
|
|
9266
|
+
};
|
|
9267
|
+
if (at === top || dirname(at) === at) break;
|
|
9268
|
+
}
|
|
9269
|
+
let inside = [];
|
|
9270
|
+
try {
|
|
9271
|
+
inside = readdirSync(top, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name) && isBase(join(top, e.name))).map((e) => join(top, e.name)).sort();
|
|
9272
|
+
} catch {}
|
|
9273
|
+
if (inside.length === 1) return {
|
|
9274
|
+
kind: "base",
|
|
9275
|
+
notes: inside[0]
|
|
9276
|
+
};
|
|
9277
|
+
if (inside.length > 1) return {
|
|
9278
|
+
kind: "several",
|
|
9279
|
+
notes: inside
|
|
9280
|
+
};
|
|
9281
|
+
return repo ? {
|
|
9282
|
+
kind: "repo",
|
|
9283
|
+
repo
|
|
9284
|
+
} : { kind: "none" };
|
|
9285
|
+
}
|
|
9286
|
+
/** The first port from `from` that nothing is listening on, so a second `isorite` starts beside the first. */
|
|
9287
|
+
async function freePort(from = 4767, host = "127.0.0.1") {
|
|
9288
|
+
for (let port = from; port < from + 20; port++) if (await new Promise((done) => {
|
|
9289
|
+
const probe = createServer().once("error", () => done(false)).once("listening", () => probe.close(() => done(true)));
|
|
9290
|
+
probe.listen(port, host);
|
|
9291
|
+
})) return port;
|
|
9292
|
+
return from;
|
|
9293
|
+
}
|
|
9294
|
+
/** The bases this machine knows that are still there, the last used first (those never used by name). */
|
|
9295
|
+
function recent(config = configPath()) {
|
|
9296
|
+
return knownBases(config).filter((b) => existsSync(b.path)).sort((a, b) => (b.used ?? "").localeCompare(a.used ?? "") || a.name.localeCompare(b.name));
|
|
9297
|
+
}
|
|
9298
|
+
/** A base as the list shows it: its name with its project's, where two share a name. */
|
|
9299
|
+
function label(b, all) {
|
|
9300
|
+
const same = all.filter((o) => o.name.replace(/-\d+$/, "") === b.name.replace(/-\d+$/, "")).length > 1;
|
|
9301
|
+
const project = basename(repoOf(b.path) ?? dirname(b.path));
|
|
9302
|
+
return `${basename(b.path)}${same || basename(b.path) !== project ? ` (${project})` : ""}`;
|
|
9303
|
+
}
|
|
9304
|
+
/** The choice `isorite` makes, and what it says. */
|
|
9305
|
+
async function start(io) {
|
|
9306
|
+
const found = findBase(io.cwd);
|
|
9307
|
+
if (found.kind === "base") return io.open(found.notes);
|
|
9308
|
+
if (found.kind === "several") {
|
|
9309
|
+
io.out(`isorite: more than one base here:`);
|
|
9310
|
+
found.notes.forEach((n, i) => io.out(` ${i + 1} ${n}`));
|
|
9311
|
+
const pick = await io.choose("Open which?", found.notes.length);
|
|
9312
|
+
return pick === null ? 2 : io.open(found.notes[pick]);
|
|
9313
|
+
}
|
|
9314
|
+
if (found.kind === "repo") {
|
|
9315
|
+
const notes = join(found.repo, "notes");
|
|
9316
|
+
io.out(`isorite: no base in ${found.repo} yet.`);
|
|
9317
|
+
if (!existsSync(notes) && await io.confirm(`Make one at ${notes}?`)) return io.open(io.make(notes));
|
|
9318
|
+
if (existsSync(notes)) io.out(` ${notes} is there already: open it as it is with isorite serve ${notes}`);
|
|
9319
|
+
}
|
|
9320
|
+
const known = recent(io.config);
|
|
9321
|
+
if (!known.length && found.kind === "none") {
|
|
9322
|
+
io.out("isorite: no bases yet on this machine.");
|
|
9323
|
+
return await io.confirm("Open the guide, a small base about isorite?") ? io.guide() : 0;
|
|
9324
|
+
}
|
|
9325
|
+
const rows = [
|
|
9326
|
+
...known.map((b) => label(b, known)),
|
|
9327
|
+
`a new base here (${join(io.cwd, "notes")})`,
|
|
9328
|
+
"the guide"
|
|
9329
|
+
];
|
|
9330
|
+
io.out(known.length ? "isorite: the bases on this machine, last used first:" : "isorite:");
|
|
9331
|
+
rows.forEach((r, i) => io.out(` ${i + 1} ${r}`));
|
|
9332
|
+
const pick = await io.choose("Open which?", rows.length);
|
|
9333
|
+
if (pick === null) return 2;
|
|
9334
|
+
if (pick < known.length) return io.open(known[pick].path);
|
|
9335
|
+
if (pick === known.length) return io.open(io.make(join(io.cwd, "notes")));
|
|
9336
|
+
return io.guide();
|
|
9337
|
+
}
|
|
9338
|
+
/** The model for search by meaning, offered on the first start without one (M26): asked once, fetched on a yes,
|
|
9339
|
+
* and a no remembered so it isn't asked again (`isorite model` fetches it whenever wanted). A failed fetch is said
|
|
9340
|
+
* and the start goes on: search by words works without it. */
|
|
9341
|
+
async function offerModel(o) {
|
|
9342
|
+
if (o.installed || o.off || o.declined) return;
|
|
9343
|
+
if (!await o.confirm("Fetch the 53 MB model for search by meaning, once? Without it search is by words.")) {
|
|
9344
|
+
o.decline();
|
|
9345
|
+
o.out(" not fetched; isorite model fetches it whenever you want it");
|
|
9346
|
+
return;
|
|
9347
|
+
}
|
|
9348
|
+
if (await o.fetch()) o.out(" search by words, for now: isorite doctor says what stopped the fetch");
|
|
9349
|
+
}
|
|
9350
|
+
//#endregion
|
|
9144
9351
|
//#region packages/server/src/main.ts
|
|
9145
9352
|
init_network();
|
|
9146
9353
|
init_self();
|
|
9354
|
+
init_embedder();
|
|
9147
9355
|
init_model();
|
|
9356
|
+
const SHORT = `isorite: an agentic harness for understanding
|
|
9357
|
+
|
|
9358
|
+
isorite open the base here, or choose one (a terminal; --no-open, --port <n>)
|
|
9359
|
+
isorite doctor whether isorite can work here, and what to change if not (--report to paste to
|
|
9360
|
+
someone helping; run in a project it checks that project's base)
|
|
9361
|
+
isorite help this; isorite help all lists every command
|
|
9362
|
+
isorite --version
|
|
9363
|
+
|
|
9364
|
+
Notes are plain Markdown in a folder of your project; your own agent reads and writes them with you.
|
|
9365
|
+
`;
|
|
9148
9366
|
const USAGE = `isorite: an agentic harness for understanding
|
|
9149
9367
|
|
|
9150
9368
|
isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
|
|
@@ -9224,9 +9442,80 @@ function check(notes, out = console.log, unused = false) {
|
|
|
9224
9442
|
}
|
|
9225
9443
|
return errors ? 1 : 0;
|
|
9226
9444
|
}
|
|
9445
|
+
/** `isorite` at a terminal: asks on stdin, starts a server on the first free port (4767 up) and opens the page. */
|
|
9446
|
+
async function terminalStart({ port, noOpen }) {
|
|
9447
|
+
const { createInterface } = await import("node:readline/promises");
|
|
9448
|
+
const ask = async (question) => {
|
|
9449
|
+
const rl = createInterface({
|
|
9450
|
+
input: process.stdin,
|
|
9451
|
+
output: process.stdout
|
|
9452
|
+
});
|
|
9453
|
+
try {
|
|
9454
|
+
return (await rl.question(question)).trim();
|
|
9455
|
+
} finally {
|
|
9456
|
+
rl.close();
|
|
9457
|
+
}
|
|
9458
|
+
};
|
|
9459
|
+
const browser = !noOpen && !process.env.SSH_CONNECTION && process.env.ISORITE_NO_BROWSER !== "1";
|
|
9460
|
+
const confirm = async (q) => /^y(es)?$/i.test(await ask(`${q} [y/N] `));
|
|
9461
|
+
const declined = join(cacheDir(), "model-declined");
|
|
9462
|
+
const model = () => offerModel({
|
|
9463
|
+
installed: modelDir() !== null,
|
|
9464
|
+
off: process.env.ISORITE_EMBED === "off",
|
|
9465
|
+
declined: existsSync(declined),
|
|
9466
|
+
confirm,
|
|
9467
|
+
fetch: () => modelCommand(void 0, console.log),
|
|
9468
|
+
out: console.log,
|
|
9469
|
+
decline: () => {
|
|
9470
|
+
try {
|
|
9471
|
+
mkdirSync(dirname(declined), { recursive: true });
|
|
9472
|
+
writeFileSync(declined, "");
|
|
9473
|
+
} catch {}
|
|
9474
|
+
}
|
|
9475
|
+
});
|
|
9476
|
+
const serving = async (notes, at = "") => {
|
|
9477
|
+
await model();
|
|
9478
|
+
const use = port ?? await freePort();
|
|
9479
|
+
await serve$1(notes, { port: use });
|
|
9480
|
+
if (browser) openBrowser(`http://localhost:${use}/${at}`);
|
|
9481
|
+
return new Promise(() => void 0);
|
|
9482
|
+
};
|
|
9483
|
+
return start({
|
|
9484
|
+
cwd: process.cwd(),
|
|
9485
|
+
config: configPath(),
|
|
9486
|
+
out: console.log,
|
|
9487
|
+
confirm,
|
|
9488
|
+
choose: async (q, count) => {
|
|
9489
|
+
const n = Number(await ask(`${q} [1-${count}] `));
|
|
9490
|
+
return Number.isInteger(n) && n >= 1 && n <= count ? n - 1 : null;
|
|
9491
|
+
},
|
|
9492
|
+
open: (notes) => serving(notes),
|
|
9493
|
+
make: (folder) => {
|
|
9494
|
+
const made = newBase(folder);
|
|
9495
|
+
console.log(` made ${made}`);
|
|
9496
|
+
return made;
|
|
9497
|
+
},
|
|
9498
|
+
guide: async () => {
|
|
9499
|
+
await model();
|
|
9500
|
+
await guide(void 0, { port: port ?? await freePort() });
|
|
9501
|
+
return new Promise(() => void 0);
|
|
9502
|
+
}
|
|
9503
|
+
});
|
|
9504
|
+
}
|
|
9227
9505
|
function main(argv) {
|
|
9228
9506
|
const [command, ...rest] = argv;
|
|
9229
9507
|
try {
|
|
9508
|
+
if ((!command || command === "--no-open" || command === "--port") && process.stdin.isTTY && process.stdout.isTTY) {
|
|
9509
|
+
const args = command ? [command, ...rest] : rest, at = args.indexOf("--port"), asked = at < 0 ? void 0 : Number(args[at + 1]);
|
|
9510
|
+
if (asked !== void 0 && !(Number.isInteger(asked) && asked > 0 && asked < 65536)) {
|
|
9511
|
+
console.error("--port takes a number");
|
|
9512
|
+
return 2;
|
|
9513
|
+
}
|
|
9514
|
+
return terminalStart({
|
|
9515
|
+
port: asked,
|
|
9516
|
+
noOpen: args.includes("--no-open")
|
|
9517
|
+
});
|
|
9518
|
+
}
|
|
9230
9519
|
if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
|
|
9231
9520
|
if (command === "new" && rest[0]) {
|
|
9232
9521
|
const made = newBase(rest[0]);
|
|
@@ -9358,7 +9647,9 @@ function main(argv) {
|
|
|
9358
9647
|
console.error("--port takes a number");
|
|
9359
9648
|
return 2;
|
|
9360
9649
|
}
|
|
9361
|
-
const
|
|
9650
|
+
const named = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--port");
|
|
9651
|
+
const here = named ? void 0 : findBase(process.cwd());
|
|
9652
|
+
const folder = named ?? (here?.kind === "base" ? here.notes : void 0);
|
|
9362
9653
|
const ask = process.stdin.isTTY && process.stdout.isTTY ? async (question) => {
|
|
9363
9654
|
const { createInterface } = await import("node:readline/promises");
|
|
9364
9655
|
const rl = createInterface({
|
|
@@ -9406,7 +9697,7 @@ function main(argv) {
|
|
|
9406
9697
|
}
|
|
9407
9698
|
throw e;
|
|
9408
9699
|
}
|
|
9409
|
-
process.stdout.write(USAGE);
|
|
9700
|
+
process.stdout.write(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT);
|
|
9410
9701
|
return command && command !== "help" && command !== "--help" ? 2 : 0;
|
|
9411
9702
|
}
|
|
9412
9703
|
/** The commands that start something, which say so at once (M21): a subprocess that stalls after this is then
|
package/docs/setup.md
CHANGED
|
@@ -35,14 +35,13 @@ If the company's authority isn't installed on the machine, name its certificate
|
|
|
35
35
|
|
|
36
36
|
## 2. Your notes
|
|
37
37
|
|
|
38
|
-
A folder of Markdown files.
|
|
38
|
+
A folder of Markdown files. In your project's folder:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
isorite
|
|
42
|
-
isorite serve ~/notes/my-project
|
|
41
|
+
isorite
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
It opens the base it finds there (a folder with `.isorite/` in it, the project's top folder or its `notes/`); with none it offers to make `notes/`; from any other folder it lists the bases this machine knows, last used first, with a new base here and the guide. It opens your browser, and prints the link too: `http://localhost:4767/` (the next free port if that one is taken; `--port <n>` to choose, `--no-open` to leave the browser alone). To name a folder yourself: `isorite new ~/notes/my-project`, then `isorite serve ~/notes/my-project`. A folder of notes you have already is opened as it is. isorite keeps what it needs in the folder's `.isorite/`.
|
|
46
45
|
|
|
47
46
|
How you want the base kept (your notation, the level, the language, how notes are named) goes in `AGENTS.md` at the top of the notes, which every agent you attach is given. Edit it in Iso's settings, the icon at the right of its pane's header, or in any editor. An agent may offer a line for it when you say how you want things done.
|
|
48
47
|
|
|
@@ -72,6 +71,8 @@ lend = true
|
|
|
72
71
|
|
|
73
72
|
## Search by meaning (optional)
|
|
74
73
|
|
|
74
|
+
The first start without the model offers it, once; a no is remembered. Later, or by hand:
|
|
75
|
+
|
|
75
76
|
```sh
|
|
76
77
|
isorite model
|
|
77
78
|
```
|
package/docs/terminal.md
CHANGED
|
@@ -9,7 +9,7 @@ Setting isorite up the first time is in [setup.md](setup.md).
|
|
|
9
9
|
Keep the project's notes in a folder of their own, often inside the project's repository (`notes/`), or beside it (`~/notes/<project>`). It can start empty: `isorite new <folder>` makes it. isorite keeps what it needs in the folder's `.isorite/` (the journals of every change, the sessions it carried, and its settings), made the first time the folder is opened. Commit `.isorite/` with the notes, if they're in git.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
isorite serve
|
|
12
|
+
isorite # in the project: finds its base, opens the browser (isorite serve <notes> names one yourself)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Three ways in
|
|
@@ -8,14 +8,14 @@ isorite runs on your own machine, with Node 24 or later:
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install -g isorite
|
|
11
|
-
|
|
12
|
-
isorite
|
|
11
|
+
cd ~/code/my-project
|
|
12
|
+
isorite
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`isorite
|
|
15
|
+
`isorite` opens the project's base in the browser. With none it offers to make a `notes/` folder, and from any other folder it lists the bases it knows and the guide. A folder of Markdown you have already is a base as it is.
|
|
16
16
|
|
|
17
17
|
isorite calls no model itself: it attaches to the agent you already use, set up as it is for its own terminal, with its own key and model. From Iso's pane, choose one, or give isorite's MCP to the agent in its own terminal: see [your own agent](/terminal/your-own-agent.md "see also"). `isorite doctor` checks what isorite needs, and offers the project's MCP config. The steps are in the guide on GitHub: [setting up](https://github.com/lachlanjs/isorite/blob/main/docs/setup.md).
|
|
18
18
|
|
|
19
|
-
`isorite guide` puts this guide in a folder of your own, where your own agent can be attached.
|
|
19
|
+
`isorite guide` (or choosing the guide from the list `isorite` shows) puts this guide in a folder of your own, where your own agent can be attached.
|
|
20
20
|
|
|
21
21
|
Then [keep it well](/your-own-base/keeping-a-base-well.md "see also").
|
package/package.json
CHANGED