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.
Files changed (31) hide show
  1. package/dist/main.js +305 -14
  2. package/docs/setup.md +5 -4
  3. package/docs/terminal.md +1 -1
  4. package/guide/your-own-base/starting-a-base.md +4 -4
  5. package/package.json +1 -1
  6. package/web/_app/immutable/assets/{0.BEjn1Bfl.css → 0.BPP604gH.css} +1 -1
  7. package/web/_app/immutable/chunks/{D9JwUycj.js → BGW1TKtR.js} +1 -1
  8. package/web/_app/immutable/chunks/{BCyLp7pS.js → BPrp4s2r.js} +1 -1
  9. package/web/_app/immutable/chunks/{C8nK3Jvr.js → CIYFNih-.js} +1 -1
  10. package/web/_app/immutable/chunks/{O98JkczU.js → CPSlCQsu.js} +2 -2
  11. package/web/_app/immutable/chunks/{Bt-fHApV.js → DBLoWLMb.js} +1 -1
  12. package/web/_app/immutable/chunks/{BOxykeIc.js → DFKYo3Wx.js} +1 -1
  13. package/web/_app/immutable/chunks/{Cd_SqIWq.js → DxVFSMVD.js} +3 -3
  14. package/web/_app/immutable/chunks/{3fCuxe18.js → DymLl-qY.js} +1 -1
  15. package/web/_app/immutable/chunks/{s_hW4oA1.js → Dzi94aDH.js} +1 -1
  16. package/web/_app/immutable/chunks/{B6uHgLdT.js → NyWzYSz1.js} +1 -1
  17. package/web/_app/immutable/chunks/X3aTZRe_.js +1 -0
  18. package/web/_app/immutable/entry/{app.DM_AWBgH.js → app.BzA5QYtw.js} +2 -2
  19. package/web/_app/immutable/entry/start.BOh0Gj31.js +1 -0
  20. package/web/_app/immutable/nodes/{0.xSbkmYPn.js → 0.BmaxMLzC.js} +2 -2
  21. package/web/_app/immutable/nodes/{1.BD2wJdoH.js → 1.IQDu8pD1.js} +1 -1
  22. package/web/_app/immutable/nodes/2.D2Oi-ADx.js +1 -0
  23. package/web/_app/immutable/nodes/{3.e_5kWaf_.js → 3.CF1W85jb.js} +1 -1
  24. package/web/_app/immutable/nodes/{4.BEMEU6Jz.js → 4.BMYBn904.js} +1 -1
  25. package/web/_app/immutable/nodes/5.1vm6WZdM.js +1 -0
  26. package/web/_app/version.json +1 -1
  27. package/web/index.html +13 -13
  28. package/web/_app/immutable/chunks/WLe0MT1c.js +0 -1
  29. package/web/_app/immutable/entry/start.BLhIKDcR.js +0 -1
  30. package/web/_app/immutable/nodes/2.shPqwtS_.js +0 -1
  31. 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.13.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 folder = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--port");
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. To start from nothing:
38
+ A folder of Markdown files. In your project's folder:
39
39
 
40
40
  ```sh
41
- isorite new ~/notes/my-project
42
- isorite serve ~/notes/my-project
41
+ isorite
43
42
  ```
44
43
 
45
- `isorite new` makes the folder, empty but for isorite's own `.isorite/`. A folder of notes you have already is opened as it is, with `isorite serve`. It prints a link to click: `http://localhost:4767/`. isorite keeps what it needs in the folder's `.isorite/`.
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 ~/code/my-project/notes
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
- isorite new ~/notes/physics
12
- isorite serve ~/notes/physics
11
+ cd ~/code/my-project
12
+ isorite
13
13
  ```
14
14
 
15
- `isorite new` makes an empty base, and `isorite serve` opens it in the browser. A folder of Markdown you have already is a base as it is.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isorite",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "An agentic harness for understanding: your notes as a knowledge base, an Atlas of them, an agent that reads them, and study.",
5
5
  "license": "MIT",
6
6
  "type": "module",