isorite 0.13.0 → 0.15.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 (93) hide show
  1. package/dist/main.js +643 -48
  2. package/docs/setup.md +5 -4
  3. package/docs/terminal.md +4 -2
  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.CtWz9d2A.css} +1 -1
  7. package/web/_app/immutable/chunks/{BWE_HS0c.js → 0B47b24t.js} +1 -1
  8. package/web/_app/immutable/chunks/{CzYPMYd4.js → 1j--pDqt.js} +1 -1
  9. package/web/_app/immutable/chunks/{e2LGvGEb.js → AnnzTlE8.js} +1 -1
  10. package/web/_app/immutable/chunks/B7Bf3hc7.js +1 -0
  11. package/web/_app/immutable/chunks/{DRcTlHZP.js → B7LnLClC.js} +1 -1
  12. package/web/_app/immutable/chunks/B9TKPEJD.js +1 -0
  13. package/web/_app/immutable/chunks/{ZwM5k8r0.js → BAVnZFPa.js} +1 -1
  14. package/web/_app/immutable/chunks/{O98JkczU.js → BDJlNL-I.js} +3 -3
  15. package/web/_app/immutable/chunks/{BiATTH-R.js → BLe29cGh.js} +1 -1
  16. package/web/_app/immutable/chunks/{C8h1oeAZ.js → BLzZIf7l.js} +1 -1
  17. package/web/_app/immutable/chunks/{DCZYeMlH.js → BPpUSQ2Y.js} +1 -1
  18. package/web/_app/immutable/chunks/{kG4HctVa.js → BQeONalw.js} +1 -1
  19. package/web/_app/immutable/chunks/{CtkUy_15.js → BZX8cNYL.js} +1 -1
  20. package/web/_app/immutable/chunks/{C8nK3Jvr.js → B_x6_Y3m.js} +1 -1
  21. package/web/_app/immutable/chunks/{Bb_N98K9.js → BiHyHFlW.js} +1 -1
  22. package/web/_app/immutable/chunks/{B6uHgLdT.js → BsY4iMci.js} +1 -1
  23. package/web/_app/immutable/chunks/{DKqMzyyE.js → BtcGx-KQ.js} +1 -1
  24. package/web/_app/immutable/chunks/{BK31dpt6.js → Bytzbt5l.js} +1 -1
  25. package/web/_app/immutable/chunks/{DDAG55ce.js → C1FDDux-.js} +1 -1
  26. package/web/_app/immutable/chunks/C7qBBO-i.js +1 -0
  27. package/web/_app/immutable/chunks/{CdY1G_wk.js → C9Qdslk3.js} +1 -1
  28. package/web/_app/immutable/chunks/{CMIt1Ma8.js → CSKABQj2.js} +1 -1
  29. package/web/_app/immutable/chunks/CSTv2fL0.js +1 -0
  30. package/web/_app/immutable/chunks/{PsfIQCJ1.js → CUB9vnd0.js} +1 -1
  31. package/web/_app/immutable/chunks/{DrVtdHUW.js → CUNetmvQ.js} +1 -1
  32. package/web/_app/immutable/chunks/{Djr0h_v1.js → CVqwkR8N.js} +1 -1
  33. package/web/_app/immutable/chunks/{CTDYUZPz.js → CWc-mNzo.js} +1 -1
  34. package/web/_app/immutable/chunks/{CryuQgNh.js → CZSmkKvQ.js} +1 -1
  35. package/web/_app/immutable/chunks/{BA78_vo5.js → CcE14TDI.js} +1 -1
  36. package/web/_app/immutable/chunks/{DjUnonHY.js → Ch5Z7cbP.js} +1 -1
  37. package/web/_app/immutable/chunks/{DFuPQaNa.js → Cpjqsnqr.js} +1 -1
  38. package/web/_app/immutable/chunks/{Bd-WxALw.js → CwHHpacD.js} +1 -1
  39. package/web/_app/immutable/chunks/{s_hW4oA1.js → D1MhxXyn.js} +1 -1
  40. package/web/_app/immutable/chunks/{Bt-fHApV.js → D4EpFhYZ.js} +1 -1
  41. package/web/_app/immutable/chunks/{UDPiRmKc.js → D7jLdCNu.js} +1 -1
  42. package/web/_app/immutable/chunks/{CFf_dAzX.js → DAVMwq6b.js} +1 -1
  43. package/web/_app/immutable/chunks/{C2npZzkn.js → DEQsS4Ez.js} +1 -1
  44. package/web/_app/immutable/chunks/{B-DzK7Gb.js → DFo4P1vU.js} +1 -1
  45. package/web/_app/immutable/chunks/{CdmC_l9F.js → DH_19--P.js} +1 -1
  46. package/web/_app/immutable/chunks/{l1LsK1sB.js → DJQ6favH.js} +1 -1
  47. package/web/_app/immutable/chunks/{BvPxdlBc.js → DJT73ARj.js} +1 -1
  48. package/web/_app/immutable/chunks/{BCGaqnao.js → DW5uy3Jw.js} +1 -1
  49. package/web/_app/immutable/chunks/{BT_btfh0.js → DWwu8a1P.js} +1 -1
  50. package/web/_app/immutable/chunks/{BhEqJU15.js → DY0M2GMn.js} +1 -1
  51. package/web/_app/immutable/chunks/{TanDZqdf.js → DbtvzL7r.js} +1 -1
  52. package/web/_app/immutable/chunks/{D9JwUycj.js → Djo-ijeo.js} +1 -1
  53. package/web/_app/immutable/chunks/DtEVmiYo.js +1 -0
  54. package/web/_app/immutable/chunks/{CT4_O9X_.js → DxRGLDbV.js} +1 -1
  55. package/web/_app/immutable/chunks/{Cd_SqIWq.js → Dx_7xlS4.js} +36 -36
  56. package/web/_app/immutable/chunks/{D6sVhEz3.js → Ih6r6QR2.js} +1 -1
  57. package/web/_app/immutable/chunks/{BXBhaEho.js → Kem7IXj7.js} +1 -1
  58. package/web/_app/immutable/chunks/{WLe0MT1c.js → LbnMg2Kc.js} +1 -1
  59. package/web/_app/immutable/chunks/{Ct_EK1xu.js → MrW4zd8l.js} +1 -1
  60. package/web/_app/immutable/chunks/{BqipEJXw.js → Mwr1r9aw.js} +1 -1
  61. package/web/_app/immutable/chunks/{D1LK3xRu.js → OG9GsKke.js} +1 -1
  62. package/web/_app/immutable/chunks/{B35sQTcK.js → ZTEErEOd.js} +1 -1
  63. package/web/_app/immutable/chunks/{Cf-5I5_6.js → e7GRa0yc.js} +1 -1
  64. package/web/_app/immutable/chunks/{C5Z-Y7Lu.js → f7mFVWQS.js} +1 -1
  65. package/web/_app/immutable/chunks/{Bj-5n5Pb.js → hjSZ041d.js} +1 -1
  66. package/web/_app/immutable/chunks/{DHJ8FcFD.js → mhWk5y4o.js} +1 -1
  67. package/web/_app/immutable/chunks/{3fCuxe18.js → wD7RSG2d.js} +1 -1
  68. package/web/_app/immutable/chunks/{CMikI1NP.js → zasx25UV.js} +3 -3
  69. package/web/_app/immutable/entry/app.Av5W3V8v.js +2 -0
  70. package/web/_app/immutable/entry/start.CbpxCvWi.js +1 -0
  71. package/web/_app/immutable/nodes/0.BTVYyqUm.js +3 -0
  72. package/web/_app/immutable/nodes/1.DR-b7PMP.js +1 -0
  73. package/web/_app/immutable/nodes/2.BePyTqoa.js +1 -0
  74. package/web/_app/immutable/nodes/3.D2rfIXQP.js +1 -0
  75. package/web/_app/immutable/nodes/4.7tuUcODS.js +15 -0
  76. package/web/_app/immutable/nodes/5.Cs9hcGU9.js +1 -0
  77. package/web/_app/immutable/nodes/6.DzvF0ZPt.js +1 -0
  78. package/web/_app/version.json +1 -1
  79. package/web/index.html +15 -15
  80. package/web/_app/immutable/chunks/BCyLp7pS.js +0 -1
  81. package/web/_app/immutable/chunks/BOxykeIc.js +0 -1
  82. package/web/_app/immutable/chunks/CAlIuau0.js +0 -1
  83. package/web/_app/immutable/chunks/Cl5gQm2e.js +0 -1
  84. package/web/_app/immutable/chunks/ej_0XxR7.js +0 -1
  85. package/web/_app/immutable/entry/app.DM_AWBgH.js +0 -2
  86. package/web/_app/immutable/entry/start.BLhIKDcR.js +0 -1
  87. package/web/_app/immutable/nodes/0.xSbkmYPn.js +0 -2
  88. package/web/_app/immutable/nodes/1.BD2wJdoH.js +0 -1
  89. package/web/_app/immutable/nodes/2.shPqwtS_.js +0 -1
  90. package/web/_app/immutable/nodes/3.e_5kWaf_.js +0 -1
  91. package/web/_app/immutable/nodes/4.BEMEU6Jz.js +0 -15
  92. package/web/_app/immutable/nodes/5.DxaO-e4O.js +0 -1
  93. package/web/_app/immutable/nodes/6.C9ia8cDm.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;
@@ -2853,7 +2865,7 @@ var Conflict = class extends EngineError {
2853
2865
  };
2854
2866
  //#endregion
2855
2867
  //#region packages/engine/src/lookup.ts
2856
- const str$1 = { type: "string" };
2868
+ const str$2 = { type: "string" };
2857
2869
  const fn$1 = (name, description, properties, required) => ({
2858
2870
  type: "function",
2859
2871
  function: {
@@ -2869,9 +2881,9 @@ const fn$1 = (name, description, properties, required) => ({
2869
2881
  });
2870
2882
  const TOOLS = [
2871
2883
  fn$1("search_notes", "Keyword search over the knowledge base. Returns note ids, titles, types, one-sentence descriptions and a one-line snippet. Search by the words a note would use.", {
2872
- query: str$1,
2884
+ query: str$2,
2873
2885
  under: {
2874
- ...str$1,
2886
+ ...str$2,
2875
2887
  description: "Only notes in this folder."
2876
2888
  },
2877
2889
  limit: {
@@ -2880,24 +2892,24 @@ const TOOLS = [
2880
2892
  }
2881
2893
  }, ["query"]),
2882
2894
  fn$1("outline_note", "A note's description, its headings, the notes it links to and the notes that link to it, without its body. Use it to choose a section to read.", { id: {
2883
- ...str$1,
2895
+ ...str$2,
2884
2896
  description: "The note's id or path: basics/entropy or /basics/entropy.md."
2885
2897
  } }, ["id"]),
2886
2898
  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 ![what it shows](path): you have its words, not the picture.", {
2887
- id: str$1,
2899
+ id: str$2,
2888
2900
  section: {
2889
- ...str$1,
2901
+ ...str$2,
2890
2902
  description: "A heading's text, as outline_note gives it."
2891
2903
  }
2892
2904
  }, ["id"])
2893
2905
  ];
2894
2906
  const FIND_SIMILAR = fn$1("find_similar", "Find notes by meaning: notes that say something like the text you give, even in quite other words. Slower and vaguer than search_notes: use it after search_notes and the links of the notes in hand have not found what you need, or when you cannot guess the words a note would use. Give a question or a sentence, not keywords. Returns each note's nearest section, with how alike it is (0 to 1).", {
2895
2907
  text: {
2896
- ...str$1,
2908
+ ...str$2,
2897
2909
  description: "A question, or a sentence saying what you are looking for."
2898
2910
  },
2899
2911
  under: {
2900
- ...str$1,
2912
+ ...str$2,
2901
2913
  description: "Only notes in this folder."
2902
2914
  },
2903
2915
  limit: {
@@ -3267,7 +3279,7 @@ or try; not for what an answer or a note says as well.
3267
3279
  problem, fix that and write the whole file again.
3268
3280
  4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
3269
3281
  Link it from the note it belongs to if they want it there.`;
3270
- const str = { type: "string" };
3282
+ const str$1 = { type: "string" };
3271
3283
  const fn = (name, description, properties, required) => ({
3272
3284
  type: "function",
3273
3285
  function: {
@@ -3283,16 +3295,16 @@ const fn = (name, description, properties, required) => ({
3283
3295
  });
3284
3296
  const ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an open question in their base, about one or more of their notes.", {
3285
3297
  title: {
3286
- ...str,
3298
+ ...str$1,
3287
3299
  description: "The question, short, ending in a question mark."
3288
3300
  },
3289
3301
  text: {
3290
- ...str,
3302
+ ...str$1,
3291
3303
  description: "A line or two on what is asked, or empty."
3292
3304
  },
3293
3305
  about: {
3294
3306
  type: "array",
3295
- items: str,
3307
+ items: str$1,
3296
3308
  description: "The ids of the notes it is about."
3297
3309
  }
3298
3310
  }, [
@@ -3301,7 +3313,7 @@ const ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an op
3301
3313
  "about"
3302
3314
  ]);
3303
3315
  const READ_DATA = fn("read_data", "A data file's shape: its columns with their types and ranges, the row count and the first rows. Read it before writing anything that uses the file, so the column names are real.", { path: {
3304
- ...str,
3316
+ ...str$1,
3305
3317
  description: "The file's path in the base: folder/name.csv."
3306
3318
  } }, ["path"]);
3307
3319
  const ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a complete working one to start from, the kit's reference, and the rules write_artifact checks. dashboard: numbers, charts and a table of one data file. compare: two data files side by side. simulation: a model with parameters and play. explorable: inputs, derived numbers and linked views. viewer: a 3D model.", { name: {
@@ -3316,21 +3328,21 @@ const ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a compl
3316
3328
  } }, ["name"]);
3317
3329
  const WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML file on the kit. The title, description and uses given here are written into it. It is checked, and run where it can be; the result says what to fix. Nothing is written while a rule is broken.", {
3318
3330
  path: {
3319
- ...str,
3331
+ ...str$1,
3320
3332
  description: "Where: folder/name.html, beside the notes it belongs to."
3321
3333
  },
3322
- title: str,
3334
+ title: str$1,
3323
3335
  description: {
3324
- ...str,
3336
+ ...str$1,
3325
3337
  description: "One sentence: what it shows, and what can be done with it."
3326
3338
  },
3327
3339
  uses: {
3328
3340
  type: "array",
3329
- items: str,
3341
+ items: str$1,
3330
3342
  description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
3331
3343
  },
3332
3344
  html: {
3333
- ...str,
3345
+ ...str$1,
3334
3346
  description: "The whole file."
3335
3347
  }
3336
3348
  }, [
@@ -3341,16 +3353,16 @@ const WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML fil
3341
3353
  "html"
3342
3354
  ]);
3343
3355
  const READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
3344
- ...str,
3356
+ ...str$1,
3345
3357
  description: "folder/name.html"
3346
3358
  } }, ["path"]);
3347
3359
  const OFFER_TEXT = fn("offer_text", "Offer them their own answers, lightly joined, to insert at the end of a note as their writing. Only their words.", {
3348
3360
  id: {
3349
- ...str,
3361
+ ...str$1,
3350
3362
  description: "The note's id, or AGENTS.md for a line of their instructions for the base."
3351
3363
  },
3352
3364
  text: {
3353
- ...str,
3365
+ ...str$1,
3354
3366
  description: "The passage, in Markdown."
3355
3367
  }
3356
3368
  }, ["id", "text"]);
@@ -4220,6 +4232,10 @@ var Engine = class Engine {
4220
4232
  return null;
4221
4233
  }
4222
4234
  }
4235
+ async deleteAgentRecord(agent, session) {
4236
+ if (!AGENT_ID.test(agent) || !session || session.length > 200) throw new EngineError("an agent's session is named by the agent and its session's id");
4237
+ await this.opts.own.remove(agentRecordPath(agent, session));
4238
+ }
4223
4239
  async agentRecords() {
4224
4240
  const own = this.opts.own, all = [];
4225
4241
  for (const a of await own.list(AGENTS_DIR).catch(() => [])) {
@@ -4636,6 +4652,10 @@ var Engine = class Engine {
4636
4652
  turns: readTurns(text)
4637
4653
  };
4638
4654
  }
4655
+ async deleteConversation(id) {
4656
+ if (!TURN_ID.test(id)) throw new EngineError("not a conversation's id");
4657
+ await this.opts.own.remove(conversationPath(id));
4658
+ }
4639
4659
  async conversations() {
4640
4660
  const study = this.opts.own;
4641
4661
  const names = await study.list(CONVERSATIONS_DIR).catch(() => []);
@@ -5102,7 +5122,7 @@ var Bases = class {
5102
5122
  }
5103
5123
  };
5104
5124
  /** The revisions answered, newest first: a client asking for one of these gets it. */
5105
- const KNOWN = [
5125
+ const KNOWN$1 = [
5106
5126
  "2026-07-28",
5107
5127
  "2025-11-25",
5108
5128
  "2025-06-18",
@@ -5160,13 +5180,13 @@ var McpEndpoint = class {
5160
5180
  const p = rpc.params ?? {};
5161
5181
  switch (rpc.method) {
5162
5182
  case "initialize": return {
5163
- protocolVersion: typeof p.protocolVersion === "string" && KNOWN.includes(p.protocolVersion) ? p.protocolVersion : KNOWN[1],
5183
+ protocolVersion: typeof p.protocolVersion === "string" && KNOWN$1.includes(p.protocolVersion) ? p.protocolVersion : KNOWN$1[1],
5164
5184
  capabilities: this.#capabilities(),
5165
5185
  serverInfo: this.#info(),
5166
5186
  instructions: INSTRUCTIONS
5167
5187
  };
5168
5188
  case "server/discover": return {
5169
- supportedVersions: KNOWN,
5189
+ supportedVersions: KNOWN$1,
5170
5190
  capabilities: this.#capabilities(),
5171
5191
  serverInfo: this.#info(),
5172
5192
  instructions: INSTRUCTIONS
@@ -5308,6 +5328,47 @@ const EMPTY = {
5308
5328
  const textOf = (blocks) => (Array.isArray(blocks) ? blocks : [blocks]).map((b) => b?.type === "text" ? b.text ?? "" : b?.type === "resource_link" ? `@${b.name ?? b.uri ?? ""}` : "").join("");
5309
5329
  const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
5310
5330
  const keyOf = (agent, session) => `${agent}\u0000${session}`;
5331
+ const str = (v) => typeof v === "string" ? v : "";
5332
+ /** The agent's slash commands, from available_commands_update. */
5333
+ const commandsOf = (v) => (Array.isArray(v) ? v : []).filter((c) => str(c.name)).map((c) => {
5334
+ const hint = str(c.input?.hint);
5335
+ return {
5336
+ name: str(c.name).replace(/^\//, ""),
5337
+ description: str(c.description),
5338
+ ...hint ? { hint } : {}
5339
+ };
5340
+ });
5341
+ /** The agent's modes, from a session's answer or a current_mode_update. */
5342
+ const modesOf = (v) => {
5343
+ const m = v;
5344
+ const options = (m?.availableModes ?? []).filter((o) => str(o.id)).map((o) => ({
5345
+ value: str(o.id),
5346
+ name: str(o.name) || str(o.id),
5347
+ ...str(o.description) ? { description: str(o.description) } : {}
5348
+ }));
5349
+ return options.length ? {
5350
+ kind: "mode",
5351
+ id: "mode",
5352
+ name: "Mode",
5353
+ current: str(m?.currentModeId),
5354
+ options
5355
+ } : null;
5356
+ };
5357
+ /** The agent's options (the model, say), whose choices may be grouped. */
5358
+ const configsOf = (v) => (Array.isArray(v) ? v : []).filter((o) => str(o.id) && Array.isArray(o.options)).flatMap((o) => {
5359
+ const options = o.options.flatMap((x) => Array.isArray(x.options) ? x.options : [x]).filter((x) => str(x.value)).map((x) => ({
5360
+ value: str(x.value),
5361
+ name: str(x.name) || str(x.value),
5362
+ ...str(x.description) ? { description: str(x.description) } : {}
5363
+ }));
5364
+ return options.length ? [{
5365
+ kind: "config",
5366
+ id: str(o.id),
5367
+ name: str(o.name) || str(o.id),
5368
+ current: str(o.currentValue),
5369
+ options
5370
+ }] : [];
5371
+ });
5311
5372
  /** The sessions the wrappers carry, as the pane draws them. One is shown: the one you chose, else the last to start. */
5312
5373
  var AcpFollower = class {
5313
5374
  #links = /* @__PURE__ */ new Map();
@@ -5390,6 +5451,8 @@ var AcpFollower = class {
5390
5451
  size: live.size,
5391
5452
  ours: this.#ours,
5392
5453
  sessions: this.#sessions(),
5454
+ commands: live.commands,
5455
+ settings: [...live.modes ? [live.modes] : [], ...live.configs],
5393
5456
  ...problem
5394
5457
  };
5395
5458
  else if (read) this.#view = {
@@ -5623,6 +5686,42 @@ var AcpFollower = class {
5623
5686
  link.opening = "";
5624
5687
  link.send({ type: "open" });
5625
5688
  }
5689
+ /** Set a mode or option of the session shown (the agent's own: its model, its plan mode). Optimistic: the agent's
5690
+ * answer or update settles it. */
5691
+ set(kind, id, value) {
5692
+ const live = this.#need();
5693
+ const setting = kind === "mode" ? live.modes : live.configs.find((c) => c.id === id);
5694
+ if (!setting || !setting.options.some((o) => o.value === value)) throw new Error(`${live.link.name || "The agent"} offers no such choice`);
5695
+ if (kind === "mode") live.modes = {
5696
+ ...setting,
5697
+ current: value
5698
+ };
5699
+ else live.configs = live.configs.map((c) => c.id === id ? {
5700
+ ...c,
5701
+ current: value
5702
+ } : c);
5703
+ live.link.send({
5704
+ type: "set",
5705
+ session: live.session,
5706
+ kind,
5707
+ id,
5708
+ value
5709
+ });
5710
+ this.#tell();
5711
+ }
5712
+ /** Remove what isorite kept of a session that is not live. What the agent keeps of its own is not touched. */
5713
+ async forget(agent, session) {
5714
+ const key = keyOf(agent, session);
5715
+ if (this.#lives.has(key)) throw new Error("that session is live: stop it before removing what is kept of it");
5716
+ if (!this.#api?.deleteAgentRecord) throw new Error("nothing is kept here");
5717
+ await this.#api.deleteAgentRecord(agent, session);
5718
+ if (this.#shown === key) {
5719
+ this.#shown = null;
5720
+ this.#chosen = false;
5721
+ this.#reading = null;
5722
+ }
5723
+ await this.#readKept();
5724
+ }
5626
5725
  /** Ask the attached agents for their sessions again: the drawer is being looked at. */
5627
5726
  refresh() {
5628
5727
  for (const link of this.#links.values()) if (link.can.list) link.send({ type: "list" });
@@ -5701,7 +5800,11 @@ var AcpFollower = class {
5701
5800
  break;
5702
5801
  }
5703
5802
  case "session/new":
5704
- if (typeof m.result?.sessionId === "string") this.#open(link, m.result.sessionId, key.startsWith("isorite-") ? "ours" : "in", typeof params.cwd === "string" ? params.cwd : "");
5803
+ if (typeof m.result?.sessionId === "string") {
5804
+ const opened = this.#open(link, m.result.sessionId, key.startsWith("isorite-") ? "ours" : "in", typeof params.cwd === "string" ? params.cwd : "");
5805
+ opened.modes = modesOf(m.result.modes);
5806
+ opened.configs = configsOf(m.result.configOptions);
5807
+ }
5705
5808
  break;
5706
5809
  case "session/load":
5707
5810
  case "session/resume":
@@ -5710,9 +5813,20 @@ var AcpFollower = class {
5710
5813
  this.#lives.delete(keyOf(link.agent, live.session));
5711
5814
  break;
5712
5815
  }
5816
+ if (m.result?.modes) live.modes = modesOf(m.result.modes);
5817
+ if (m.result?.configOptions) live.configs = configsOf(m.result.configOptions);
5713
5818
  this.#loaded(live);
5714
5819
  }
5715
5820
  break;
5821
+ case "session/set_mode":
5822
+ if (live && !m.error && live.modes) live.modes = {
5823
+ ...live.modes,
5824
+ current: str(params.modeId)
5825
+ };
5826
+ break;
5827
+ case "session/set_config_option":
5828
+ if (live && !m.error && m.result?.configOptions) live.configs = configsOf(m.result.configOptions);
5829
+ break;
5716
5830
  case "session/prompt":
5717
5831
  if (live) {
5718
5832
  const stop = m.error ? "failed" : m.result?.stopReason === "cancelled" ? "stopped" : m.result?.stopReason === "end_turn" ? "done" : "failed";
@@ -5776,7 +5890,10 @@ var AcpFollower = class {
5776
5890
  loading: false,
5777
5891
  replaying: false,
5778
5892
  pending: null,
5779
- given: /* @__PURE__ */ new Set()
5893
+ given: /* @__PURE__ */ new Set(),
5894
+ commands: [],
5895
+ modes: null,
5896
+ configs: []
5780
5897
  };
5781
5898
  live.link = link;
5782
5899
  if (cwd) live.cwd = cwd;
@@ -5966,7 +6083,19 @@ var AcpFollower = class {
5966
6083
  if (typeof cost === "number") live.cost = cost;
5967
6084
  break;
5968
6085
  }
5969
- case "session_info_update": if (typeof u.title === "string") live.title = u.title;
6086
+ case "session_info_update":
6087
+ if (typeof u.title === "string") live.title = u.title;
6088
+ break;
6089
+ case "available_commands_update":
6090
+ live.commands = commandsOf(u.availableCommands);
6091
+ break;
6092
+ case "current_mode_update":
6093
+ if (live.modes && typeof u.currentModeId === "string") live.modes = {
6094
+ ...live.modes,
6095
+ current: u.currentModeId
6096
+ };
6097
+ break;
6098
+ case "config_option_update": live.configs = configsOf(u.configOptions);
5970
6099
  }
5971
6100
  }
5972
6101
  /** The note a path names, if it is a Markdown file of the base (relative paths read from the session's folder). */
@@ -6026,7 +6155,7 @@ var AcpFollower = class {
6026
6155
  };
6027
6156
  //#endregion
6028
6157
  //#region packages/server/src/version.ts
6029
- const VERSION = "0.13.0";
6158
+ const VERSION = "0.15.0";
6030
6159
  //#endregion
6031
6160
  //#region packages/server/src/connect.ts
6032
6161
  const HARNESSES = [{
@@ -6224,6 +6353,108 @@ async function tokenOf(url) {
6224
6353
  return (await (await fetch(`${url}/api/session`)).json()).token;
6225
6354
  }
6226
6355
  //#endregion
6356
+ //#region packages/server/src/detect.ts
6357
+ const KNOWN = [
6358
+ {
6359
+ id: "claude",
6360
+ name: "Claude Code",
6361
+ bin: "claude",
6362
+ command: "npx -y @agentclientprotocol/claude-agent-acp",
6363
+ needs: "npx",
6364
+ keys: ["ANTHROPIC_API_KEY"],
6365
+ signedIn: true
6366
+ },
6367
+ {
6368
+ id: "codex",
6369
+ name: "Codex",
6370
+ bin: "codex",
6371
+ command: "npx -y @agentclientprotocol/codex-acp",
6372
+ needs: "npx",
6373
+ keys: ["CODEX_API_KEY", "OPENAI_API_KEY"]
6374
+ },
6375
+ {
6376
+ id: "opencode",
6377
+ name: "OpenCode",
6378
+ bin: "opencode",
6379
+ command: "opencode acp",
6380
+ keys: []
6381
+ },
6382
+ {
6383
+ id: "omp",
6384
+ name: "omp",
6385
+ bin: "omp",
6386
+ command: "omp acp",
6387
+ keys: []
6388
+ },
6389
+ {
6390
+ id: "hermes",
6391
+ name: "Hermes",
6392
+ bin: "hermes",
6393
+ command: "hermes acp",
6394
+ keys: []
6395
+ }
6396
+ ];
6397
+ /** The first file called `name` on PATH (with the extensions Windows tries), or null. */
6398
+ function onPath(name, env = process.env) {
6399
+ const exts = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
6400
+ for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) for (const ext of exts) {
6401
+ const file = join(dir, name + ext);
6402
+ try {
6403
+ if (existsSync(file) && statSync(file).isFile()) return file;
6404
+ } catch {}
6405
+ }
6406
+ return null;
6407
+ }
6408
+ function detectAgents(env = process.env) {
6409
+ return KNOWN.flatMap((k) => {
6410
+ const path = onPath(k.bin, env);
6411
+ if (!path) return [];
6412
+ const keySet = k.keys.some((v) => Boolean(env[v]));
6413
+ const ready = !k.needs || onPath(k.needs, env) !== null;
6414
+ const parts = [`${k.name} is installed`];
6415
+ if (!ready) parts.push(`but it needs ${k.needs}, which isn't on PATH`);
6416
+ else if (k.keys.length && keySet) parts.push(`and ${k.keys.find((v) => env[v])} is set here`);
6417
+ 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`);
6418
+ return [{
6419
+ id: k.id,
6420
+ name: k.name,
6421
+ command: k.command,
6422
+ path,
6423
+ keys: k.keys,
6424
+ keySet,
6425
+ ready,
6426
+ note: parts.join(" ").replace(" ;", ";").replace(" ,", ",") + "."
6427
+ }];
6428
+ });
6429
+ }
6430
+ function listAgents(env = process.env) {
6431
+ const found = detectAgents(env).map((d) => ({
6432
+ ...d,
6433
+ found: true
6434
+ }));
6435
+ const have = new Set(found.map((d) => d.id));
6436
+ const rest = KNOWN.filter((k) => !have.has(k.id)).map((k) => ({
6437
+ id: k.id,
6438
+ name: k.name,
6439
+ command: k.command,
6440
+ path: "",
6441
+ keys: k.keys,
6442
+ keySet: false,
6443
+ ready: false,
6444
+ found: false,
6445
+ note: `${k.name} isn't on this machine's PATH.`
6446
+ }));
6447
+ return [...found, ...rest];
6448
+ }
6449
+ /** The ACP command of an agent known by name (`isorite acp omp`), or null. */
6450
+ const knownCommand = (id) => KNOWN.find((k) => k.id === id)?.command ?? null;
6451
+ /** Why an agent that stopped may have: the key it reads isn't set where isorite was started, for the command run. */
6452
+ function missingKey(command, env = process.env) {
6453
+ const k = KNOWN.find((x) => x.command === command);
6454
+ if (!k || !k.keys.length || k.keys.some((v) => env[v])) return "";
6455
+ 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" : ""}.`;
6456
+ }
6457
+ //#endregion
6227
6458
  //#region packages/server/src/agents.ts
6228
6459
  init_self();
6229
6460
  const FRESH = 36e5;
@@ -6394,12 +6625,14 @@ var AgentRunner = class {
6394
6625
  process.once("exit", () => this.#child?.kill());
6395
6626
  }
6396
6627
  state() {
6628
+ const hint = this.#problem && this.#command ? missingKey(this.#command, this.#env) : "";
6397
6629
  return {
6398
6630
  choice: chosenAgent(this.#base, this.#config),
6399
6631
  running: this.#child !== null,
6400
6632
  command: this.#command,
6401
6633
  problem: this.#problem,
6402
- said: this.#said
6634
+ said: this.#said,
6635
+ hint
6403
6636
  };
6404
6637
  }
6405
6638
  /** Kept, and started in place of the one running; with none, the one running is stopped. */
@@ -6468,6 +6701,108 @@ var AgentRunner = class {
6468
6701
  }
6469
6702
  };
6470
6703
  //#endregion
6704
+ //#region packages/server/src/own-repo.ts
6705
+ const q = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
6706
+ const slashes = (p) => p.split("\\").join("/");
6707
+ function git(cwd, args, env) {
6708
+ return execFileSync("git", args, {
6709
+ cwd,
6710
+ env: plainGitEnv(env),
6711
+ timeout: 1e4,
6712
+ killSignal: "SIGKILL",
6713
+ encoding: "utf8",
6714
+ stdio: [
6715
+ "ignore",
6716
+ "pipe",
6717
+ "pipe"
6718
+ ]
6719
+ });
6720
+ }
6721
+ /** What this machine chose for a base, from bases.toml. */
6722
+ function historyChoice(base, config) {
6723
+ const v = table(readConfig(basesPath(config))[base]).history;
6724
+ return v === "project" || v === "separate" ? v : null;
6725
+ }
6726
+ function setHistoryChoice(base, choice, config) {
6727
+ const bases = readConfig(basesPath(config));
6728
+ if (!bases[base]) throw new Error(`${base} is not among the bases`);
6729
+ bases[base] = {
6730
+ ...table(bases[base]),
6731
+ history: choice
6732
+ };
6733
+ writeConfig(bases, basesPath(config));
6734
+ }
6735
+ /** Where isorite's own folder is kept, and the commands for the person to run. Runs `git ls-files`, `remote` and
6736
+ * `rev-parse`, which only read. */
6737
+ function ownRepoInfo(own, choice = null, env = process.env) {
6738
+ const project = repoOf(dirname(own));
6739
+ const base = {
6740
+ state: "none",
6741
+ path: own,
6742
+ project,
6743
+ remotes: [],
6744
+ choice,
6745
+ ask: false,
6746
+ commands: [],
6747
+ problem: ""
6748
+ };
6749
+ try {
6750
+ if (existsSync(join(own, ".git"))) {
6751
+ const remotes = git(own, ["remote"], env).split("\n").filter(Boolean);
6752
+ return {
6753
+ ...base,
6754
+ state: "own",
6755
+ remotes,
6756
+ commands: remotes.length ? [`git -C ${q(own)} push`] : [`git -C ${q(own)} remote add origin <the address of your private repository>`, `git -C ${q(own)} push -u origin HEAD`]
6757
+ };
6758
+ }
6759
+ if (!project) return base;
6760
+ const rel = slashes(relative(project, own));
6761
+ const tracked = git(project, [
6762
+ "ls-files",
6763
+ "--",
6764
+ rel
6765
+ ], env).trim() !== "";
6766
+ const path = git(project, [
6767
+ "rev-parse",
6768
+ "--git-path",
6769
+ "info/exclude"
6770
+ ], env).trim();
6771
+ const exclude = isAbsolute(path) ? path : resolve(project, path);
6772
+ const apart = [
6773
+ `git -C ${q(own)} init`,
6774
+ `echo ${q(rel + "/")} >> ${q(exclude)}`,
6775
+ `git -C ${q(own)} add -A`,
6776
+ `git -C ${q(own)} commit -m "isorite: start"`
6777
+ ];
6778
+ return {
6779
+ ...base,
6780
+ state: tracked ? "tracked" : "shared",
6781
+ ask: choice === null,
6782
+ commands: tracked ? [
6783
+ `git -C ${q(project)} rm -r --cached ${q(rel)}`,
6784
+ ...apart,
6785
+ `git -C ${q(project)} commit -m "stop tracking isorite's own files"`
6786
+ ] : apart
6787
+ };
6788
+ } catch (e) {
6789
+ return {
6790
+ ...base,
6791
+ state: "unavailable",
6792
+ problem: e.message.split("\n")[0] ?? "git did not answer"
6793
+ };
6794
+ }
6795
+ }
6796
+ /** For a base just made in a git project: ask whether its own files stay with the project or apart, keep the answer
6797
+ * for this machine, and say the commands for apart. Asks nothing outside a project. isorite runs none of it. */
6798
+ async function askWhereFilesLive(notes, ask, out, config, env = process.env) {
6799
+ if (!repoOf(notes)) return null;
6800
+ const choice = (await ask(`${notes} is in a git project. isorite's own files (.isorite/: journals, kept sessions, settings) can be committed with the project, or kept apart from it, so the people sharing it don't have them.\nKeep them [p]roject / [a]part? (p) `)).trim().toLowerCase().startsWith("a") ? "separate" : "project";
6801
+ setHistoryChoice(rememberBase(notes, { path: config }), choice, config);
6802
+ if (choice === "separate") out(`\nTo keep them apart, run these (isorite runs no git itself):\n${ownRepoInfo(join(notes, ".isorite"), "separate", env).commands.map((c) => ` ${c}`).join("\n")}\n`);
6803
+ return choice;
6804
+ }
6805
+ //#endregion
6471
6806
  //#region packages/server/src/http.ts
6472
6807
  const LOOPBACK = /* @__PURE__ */ new Set([
6473
6808
  "localhost",
@@ -6544,7 +6879,7 @@ const AgentStatus = z.object({
6544
6879
  session: z.string().min(1).max(200),
6545
6880
  busy: z.boolean()
6546
6881
  }).strict();
6547
- function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents }) {
6882
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bases, base, agents, config }) {
6548
6883
  const app = new Hono();
6549
6884
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
6550
6885
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -6678,6 +7013,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6678
7013
  app.get("/api/agents/notes", (c) => answer(c, () => api.agentNotes()));
6679
7014
  post("/api/agents/notes", z.object({ may: z.boolean() }).strict(), (_c, b) => api.setAgentNotes(b.may));
6680
7015
  app.get("/api/conversations", (c) => answer(c, () => api.conversations()));
7016
+ post("/api/conversation/delete", z.object({ id: z.string().max(64) }).strict(), (_c, b) => api.deleteConversation(b.id));
6681
7017
  app.get("/api/conversation", (c) => answer(c, async () => await api.conversation(c.req.query("id") || void 0) ?? {
6682
7018
  id: "",
6683
7019
  turns: []
@@ -6756,6 +7092,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6756
7092
  }).strict(), (_c, b) => api.agentReceived(b.id, b.agent));
6757
7093
  post("/api/agent/status", AgentStatus, (_c, b) => api.agentStatus(b.agent, b.session, b.busy));
6758
7094
  post("/api/agent/take", z.object({ id: z.string().max(64) }).strict(), (_c, b) => api.agentTake(b.id));
7095
+ if (notes) {
7096
+ const configFile = config ?? configPath();
7097
+ app.get("/api/own/repo", (c) => answer(c, async () => local(c) ? ownRepoInfo(join(notes, ".isorite"), base ? historyChoice(base, configFile) : null) : null));
7098
+ post("/api/own/choose", z.object({ choice: z.enum(["project", "separate"]) }).strict(), async (c, b) => {
7099
+ if (!local(c)) throw new EngineError("this is chosen only from this machine", 403);
7100
+ if (!base) throw new EngineError("this base has no name here", 409);
7101
+ setHistoryChoice(base, b.choice, configFile);
7102
+ return ownRepoInfo(join(notes, ".isorite"), b.choice);
7103
+ });
7104
+ }
6759
7105
  if (notes) {
6760
7106
  app.get("/api/connect", (c) => answer(c, async () => proposal(notes)));
6761
7107
  post("/api/connect", z.object({ files: z.array(z.string().max(200)).max(10).optional() }).strict(), async (c, b) => {
@@ -6765,6 +7111,10 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6765
7111
  }
6766
7112
  if (agents) {
6767
7113
  app.get("/api/acp/agent", (c) => answer(c, async () => agents.state()));
7114
+ app.get("/api/acp/detected", (c) => answer(c, async () => {
7115
+ if (!local(c)) throw new EngineError("agents are looked for only from this machine", 403);
7116
+ return listAgents();
7117
+ }));
6768
7118
  app.get("/api/acp/registry", (c) => answer(c, async () => registryAgents().catch((e) => {
6769
7119
  throw new EngineError(e.message, 502);
6770
7120
  })));
@@ -6868,6 +7218,23 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6868
7218
  }).strict(), async (_c, b) => {
6869
7219
  await acp.follow(b.agent, b.session).catch(refused);
6870
7220
  });
7221
+ post("/api/acp/forget", z.object({
7222
+ agent: z.string().max(40),
7223
+ session: z.string().min(1).max(200)
7224
+ }).strict(), async (_c, b) => {
7225
+ await acp.forget(b.agent, b.session).catch(refused);
7226
+ });
7227
+ post("/api/acp/set", z.object({
7228
+ kind: z.enum(["mode", "config"]),
7229
+ id: z.string().max(100),
7230
+ value: z.string().max(200)
7231
+ }).strict(), async (_c, b) => {
7232
+ try {
7233
+ acp.set(b.kind, b.id, b.value);
7234
+ } catch (e) {
7235
+ throw new EngineError(e.message, 409);
7236
+ }
7237
+ });
6871
7238
  post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
6872
7239
  await acp.fresh(b.agent).catch(refused);
6873
7240
  });
@@ -7321,6 +7688,7 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
7321
7688
  const opened = locate(path);
7322
7689
  const device = deviceId(config);
7323
7690
  const first = rememberBase(opened.notes, { path: config });
7691
+ touchBase(opened.notes, { path: config });
7324
7692
  const embedder = model ? nodeEmbedder(model) : null;
7325
7693
  const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
7326
7694
  const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
@@ -7357,7 +7725,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
7357
7725
  acp,
7358
7726
  bases,
7359
7727
  base: known.name,
7360
- ...loopback ? { agents } : {}
7728
+ ...loopback ? { agents } : {},
7729
+ config
7361
7730
  }).fetch
7362
7731
  };
7363
7732
  }
@@ -7389,7 +7758,7 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
7389
7758
  process.exit(1);
7390
7759
  });
7391
7760
  console.log(`isorite: ${notes}`);
7392
- console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model)");
7761
+ console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model, 53 MB)");
7393
7762
  const lent = bases.lent().filter((l) => l.name !== bases.first).map((l) => l.name);
7394
7763
  if (lent.length) console.log(` lent to every session: ${lent.join(", ")} (isorite bases)`);
7395
7764
  console.log(existsSync(join(WEB_DIR, "index.html")) ? ` ${link(`http://localhost:${port}/`)}` : ` the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
@@ -7509,6 +7878,13 @@ async function doctor(folder, o = {}) {
7509
7878
  const { notes } = locate(folder);
7510
7879
  const own = join(notes, OWN_DIR), settings = join(own, SETTINGS_FILE);
7511
7880
  say("notes", "ok", existsSync(own) ? `${notes}, with its ${OWN_DIR}/` : `${notes}: its ${OWN_DIR}/ is made the first time it is opened`);
7881
+ if (existsSync(notes) && statSync(notes).isDirectory()) {
7882
+ const bundle = loadBundle(notes), issues = bundle.lint(), cycles = bundle.requiresCycles();
7883
+ const errors = issues.filter((i) => i.level === "error");
7884
+ 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})` : ""}` : ""}`);
7885
+ }
7886
+ const kept = ownRepoInfo(own, null, env);
7887
+ if (existsSync(own)) say("history", kept.state === "own" ? "ok" : kept.state === "unavailable" ? "skip" : kept.state === "none" ? "skip" : "ok", kept.state === "own" ? `${OWN_DIR}/ is a repository of its own${kept.remotes.length ? `, remote ${kept.remotes.join(", ")}` : `; to keep it elsewhere: git -C ${own} remote add origin <address>`}` : kept.state === "tracked" ? `${OWN_DIR}/ is tracked by the project's repository, so everyone sharing it has isorite's files; to keep them apart, run:\n ${kept.commands.join("\n ")}` : kept.state === "shared" ? `${OWN_DIR}/ is in the project's folder, to be committed with it; to keep it apart, run:\n ${kept.commands.join("\n ")}` : kept.state === "unavailable" ? `${OWN_DIR}/: git did not answer (${kept.problem})` : `${OWN_DIR}/ is in no git repository`);
7512
7888
  const repository = existsSync(settings) ? parseSettings(readFileSync(settings, "utf8")).repository : void 0;
7513
7889
  const found = repoOf(notes);
7514
7890
  if (!repository && found) say("repository", "ok", `${found}: code links point here`);
@@ -7749,7 +8125,7 @@ const OPENS = /* @__PURE__ */ new Set([
7749
8125
  "session/resume",
7750
8126
  "session/fork"
7751
8127
  ]);
7752
- const SKIP = /* @__PURE__ */ new Set([
8128
+ const SKIP$1 = /* @__PURE__ */ new Set([
7753
8129
  "node_modules",
7754
8130
  ".git",
7755
8131
  OWN_DIR,
@@ -7760,7 +8136,7 @@ const SKIP = /* @__PURE__ */ new Set([
7760
8136
  ]);
7761
8137
  /** The base for a session started in `cwd`: the folder itself or one above it holding .isorite/, else the first
7762
8138
  * below it, three levels down at most (a base inside the repository, as notes/); or none. */
7763
- function findBase(cwd) {
8139
+ function findBase$1(cwd) {
7764
8140
  for (let at = cwd;; at = dirname(at)) {
7765
8141
  if (existsSync(join(at, ".isorite"))) return at;
7766
8142
  if (dirname(at) === at) break;
@@ -7771,7 +8147,7 @@ function findBase(cwd) {
7771
8147
  for (const dir of level) {
7772
8148
  let names = [];
7773
8149
  try {
7774
- names = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !SKIP.has(d.name) && !d.name.startsWith(".")).map((d) => d.name).sort();
8150
+ names = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !SKIP$1.has(d.name) && !d.name.startsWith(".")).map((d) => d.name).sort();
7775
8151
  } catch {
7776
8152
  continue;
7777
8153
  }
@@ -7793,7 +8169,7 @@ async function tokenWhenUp(url, wait = 6e4) {
7793
8169
  }
7794
8170
  }
7795
8171
  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;
8172
+ if (!at) path ??= findBase$1(process.cwd()) ?? void 0;
7797
8173
  const server = at ? {
7798
8174
  url: at,
7799
8175
  release: () => void 0
@@ -8139,6 +8515,14 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8139
8515
  toAgent(m);
8140
8516
  }
8141
8517
  if (c.type === "list" && can.list) ask("session/list", { cwd });
8518
+ if (c.type === "set") ask(c.kind === "mode" ? "session/set_mode" : "session/set_config_option", c.kind === "mode" ? {
8519
+ sessionId: c.session,
8520
+ modeId: c.value
8521
+ } : {
8522
+ sessionId: c.session,
8523
+ configId: c.id,
8524
+ value: c.value
8525
+ });
8142
8526
  if (c.type === "open") {
8143
8527
  const params = {
8144
8528
  cwd,
@@ -9141,10 +9525,128 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
9141
9525
  return 0;
9142
9526
  }
9143
9527
  //#endregion
9528
+ //#region packages/server/src/start.ts
9529
+ const SKIP = /* @__PURE__ */ new Set([
9530
+ "node_modules",
9531
+ "dist",
9532
+ "build",
9533
+ "target",
9534
+ "vendor"
9535
+ ]);
9536
+ const isBase = (dir) => existsSync(join(dir, ".isorite")) && statSync(join(dir, ".isorite")).isDirectory();
9537
+ /** The base `from` is in or beside: a folder holding .isorite/ on the way up to the repository's top (or `from`
9538
+ * alone, outside a repository), else the one folder just inside that top that holds one. */
9539
+ function findBase(from) {
9540
+ const start = resolve(from), repo = repoOf(start), top = repo ?? start;
9541
+ for (let at = start;; at = dirname(at)) {
9542
+ if (isBase(at)) return {
9543
+ kind: "base",
9544
+ notes: at
9545
+ };
9546
+ if (at === top || dirname(at) === at) break;
9547
+ }
9548
+ let inside = [];
9549
+ try {
9550
+ 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();
9551
+ } catch {}
9552
+ if (inside.length === 1) return {
9553
+ kind: "base",
9554
+ notes: inside[0]
9555
+ };
9556
+ if (inside.length > 1) return {
9557
+ kind: "several",
9558
+ notes: inside
9559
+ };
9560
+ return repo ? {
9561
+ kind: "repo",
9562
+ repo
9563
+ } : { kind: "none" };
9564
+ }
9565
+ /** The first port from `from` that nothing is listening on, so a second `isorite` starts beside the first. */
9566
+ async function freePort(from = 4767, host = "127.0.0.1") {
9567
+ for (let port = from; port < from + 20; port++) if (await new Promise((done) => {
9568
+ const probe = createServer().once("error", () => done(false)).once("listening", () => probe.close(() => done(true)));
9569
+ probe.listen(port, host);
9570
+ })) return port;
9571
+ return from;
9572
+ }
9573
+ /** The bases this machine knows that are still there, the last used first (those never used by name). */
9574
+ function recent(config = configPath()) {
9575
+ return knownBases(config).filter((b) => existsSync(b.path)).sort((a, b) => (b.used ?? "").localeCompare(a.used ?? "") || a.name.localeCompare(b.name));
9576
+ }
9577
+ /** A base as the list shows it: its name with its project's, where two share a name. */
9578
+ function label(b, all) {
9579
+ const same = all.filter((o) => o.name.replace(/-\d+$/, "") === b.name.replace(/-\d+$/, "")).length > 1;
9580
+ const project = basename(repoOf(b.path) ?? dirname(b.path));
9581
+ return `${basename(b.path)}${same || basename(b.path) !== project ? ` (${project})` : ""}`;
9582
+ }
9583
+ /** The choice `isorite` makes, and what it says. */
9584
+ async function start(io) {
9585
+ const make = async (folder) => {
9586
+ const notes = io.make(folder);
9587
+ await io.made?.(notes);
9588
+ return notes;
9589
+ };
9590
+ const found = findBase(io.cwd);
9591
+ if (found.kind === "base") return io.open(found.notes);
9592
+ if (found.kind === "several") {
9593
+ io.out(`isorite: more than one base here:`);
9594
+ found.notes.forEach((n, i) => io.out(` ${i + 1} ${n}`));
9595
+ const pick = await io.choose("Open which?", found.notes.length);
9596
+ return pick === null ? 2 : io.open(found.notes[pick]);
9597
+ }
9598
+ if (found.kind === "repo") {
9599
+ const notes = join(found.repo, "notes");
9600
+ io.out(`isorite: no base in ${found.repo} yet.`);
9601
+ if (!existsSync(notes) && await io.confirm(`Make one at ${notes}?`)) return io.open(await make(notes));
9602
+ if (existsSync(notes)) io.out(` ${notes} is there already: open it as it is with isorite serve ${notes}`);
9603
+ }
9604
+ const known = recent(io.config);
9605
+ if (!known.length && found.kind === "none") {
9606
+ io.out("isorite: no bases yet on this machine.");
9607
+ return await io.confirm("Open the guide, a small base about isorite?") ? io.guide() : 0;
9608
+ }
9609
+ const rows = [
9610
+ ...known.map((b) => label(b, known)),
9611
+ `a new base here (${join(io.cwd, "notes")})`,
9612
+ "the guide"
9613
+ ];
9614
+ io.out(known.length ? "isorite: the bases on this machine, last used first:" : "isorite:");
9615
+ rows.forEach((r, i) => io.out(` ${i + 1} ${r}`));
9616
+ const pick = await io.choose("Open which?", rows.length);
9617
+ if (pick === null) return 2;
9618
+ if (pick < known.length) return io.open(known[pick].path);
9619
+ if (pick === known.length) return io.open(await make(join(io.cwd, "notes")));
9620
+ return io.guide();
9621
+ }
9622
+ /** The model for search by meaning, offered on the first start without one (M26): asked once, fetched on a yes,
9623
+ * and a no remembered so it isn't asked again (`isorite model` fetches it whenever wanted). A failed fetch is said
9624
+ * and the start goes on: search by words works without it. */
9625
+ async function offerModel(o) {
9626
+ if (o.installed || o.off || o.declined) return;
9627
+ if (!await o.confirm("Fetch the 53 MB model for search by meaning, once? Without it search is by words.")) {
9628
+ o.decline();
9629
+ o.out(" not fetched; isorite model fetches it whenever you want it");
9630
+ return;
9631
+ }
9632
+ if (await o.fetch()) o.out(" search by words, for now: isorite doctor says what stopped the fetch");
9633
+ }
9634
+ //#endregion
9144
9635
  //#region packages/server/src/main.ts
9145
9636
  init_network();
9146
9637
  init_self();
9638
+ init_embedder();
9147
9639
  init_model();
9640
+ const SHORT = `isorite: an agentic harness for understanding
9641
+
9642
+ isorite open the base here, or choose one (a terminal; --no-open, --port <n>)
9643
+ isorite doctor whether isorite can work here, and what to change if not (--report to paste to
9644
+ someone helping; run in a project it checks that project's base)
9645
+ isorite help this; isorite help all lists every command
9646
+ isorite --version
9647
+
9648
+ Notes are plain Markdown in a folder of your project; your own agent reads and writes them with you.
9649
+ `;
9148
9650
  const USAGE = `isorite: an agentic harness for understanding
9149
9651
 
9150
9652
  isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
@@ -9224,13 +9726,102 @@ function check(notes, out = console.log, unused = false) {
9224
9726
  }
9225
9727
  return errors ? 1 : 0;
9226
9728
  }
9729
+ /** `isorite` at a terminal: asks on stdin, starts a server on the first free port (4767 up) and opens the page. */
9730
+ async function terminalStart({ port, noOpen }) {
9731
+ const { createInterface } = await import("node:readline/promises");
9732
+ const ask = async (question) => {
9733
+ const rl = createInterface({
9734
+ input: process.stdin,
9735
+ output: process.stdout
9736
+ });
9737
+ try {
9738
+ return (await rl.question(question)).trim();
9739
+ } finally {
9740
+ rl.close();
9741
+ }
9742
+ };
9743
+ const browser = !noOpen && !process.env.SSH_CONNECTION && process.env.ISORITE_NO_BROWSER !== "1";
9744
+ const confirm = async (q) => /^y(es)?$/i.test(await ask(`${q} [y/N] `));
9745
+ const declined = join(cacheDir(), "model-declined");
9746
+ const model = () => offerModel({
9747
+ installed: modelDir() !== null,
9748
+ off: process.env.ISORITE_EMBED === "off",
9749
+ declined: existsSync(declined),
9750
+ confirm,
9751
+ fetch: () => modelCommand(void 0, console.log),
9752
+ out: console.log,
9753
+ decline: () => {
9754
+ try {
9755
+ mkdirSync(dirname(declined), { recursive: true });
9756
+ writeFileSync(declined, "");
9757
+ } catch {}
9758
+ }
9759
+ });
9760
+ const serving = async (notes, at = "") => {
9761
+ await model();
9762
+ const use = port ?? await freePort();
9763
+ await serve$1(notes, { port: use });
9764
+ if (browser) openBrowser(`http://localhost:${use}/${at}`);
9765
+ return new Promise(() => void 0);
9766
+ };
9767
+ return start({
9768
+ cwd: process.cwd(),
9769
+ config: configPath(),
9770
+ out: console.log,
9771
+ confirm,
9772
+ choose: async (q, count) => {
9773
+ const n = Number(await ask(`${q} [1-${count}] `));
9774
+ return Number.isInteger(n) && n >= 1 && n <= count ? n - 1 : null;
9775
+ },
9776
+ open: (notes) => serving(notes),
9777
+ make: (folder) => {
9778
+ const made = newBase(folder);
9779
+ console.log(` made ${made}`);
9780
+ return made;
9781
+ },
9782
+ made: async (notes) => {
9783
+ await askWhereFilesLive(notes, ask, console.log, configPath());
9784
+ },
9785
+ guide: async () => {
9786
+ await model();
9787
+ await guide(void 0, { port: port ?? await freePort() });
9788
+ return new Promise(() => void 0);
9789
+ }
9790
+ });
9791
+ }
9227
9792
  function main(argv) {
9228
9793
  const [command, ...rest] = argv;
9229
9794
  try {
9795
+ if ((!command || command === "--no-open" || command === "--port") && process.stdin.isTTY && process.stdout.isTTY) {
9796
+ const args = command ? [command, ...rest] : rest, at = args.indexOf("--port"), asked = at < 0 ? void 0 : Number(args[at + 1]);
9797
+ if (asked !== void 0 && !(Number.isInteger(asked) && asked > 0 && asked < 65536)) {
9798
+ console.error("--port takes a number");
9799
+ return 2;
9800
+ }
9801
+ return terminalStart({
9802
+ port: asked,
9803
+ noOpen: args.includes("--no-open")
9804
+ });
9805
+ }
9230
9806
  if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
9231
9807
  if (command === "new" && rest[0]) {
9232
9808
  const made = newBase(rest[0]);
9233
- console.log(`${made}: a knowledge base, empty. Open it in the browser, and attach your agent from Iso's pane:\n isorite serve ${made}`);
9809
+ const next = `${made}: a knowledge base, empty. Open it in the browser, and attach your agent from Iso's pane:\n isorite serve ${made}`;
9810
+ if (process.stdin.isTTY && process.stdout.isTTY && repoOf(made)) return (async () => {
9811
+ const { createInterface } = await import("node:readline/promises");
9812
+ const rl = createInterface({
9813
+ input: process.stdin,
9814
+ output: process.stdout
9815
+ });
9816
+ try {
9817
+ await askWhereFilesLive(made, (q) => rl.question(q), console.log, configPath());
9818
+ } finally {
9819
+ rl.close();
9820
+ }
9821
+ console.log(next);
9822
+ return 0;
9823
+ })();
9824
+ console.log(next);
9234
9825
  return 0;
9235
9826
  }
9236
9827
  if (command === "serve" && rest[0]) {
@@ -9297,9 +9888,15 @@ function main(argv) {
9297
9888
  const at = rest.indexOf(name);
9298
9889
  return at < 0 ? void 0 : rest[at + 1];
9299
9890
  };
9300
- const agent = flag("--agent"), port = flag("--port") === void 0 ? void 0 : Number(flag("--port"));
9891
+ const positional = rest.filter((a, i) => !a.startsWith("--") && ![
9892
+ "--port",
9893
+ "--agent",
9894
+ "--url"
9895
+ ].includes(rest[i - 1] ?? ""));
9896
+ const named = !flag("--agent") && positional[0] && !existsSync(positional[0]) ? knownCommand(positional[0]) : null;
9897
+ const agent = flag("--agent") ?? named ?? void 0, port = flag("--port") === void 0 ? void 0 : Number(flag("--port"));
9301
9898
  if (!agent) {
9302
- console.error("isorite acp --agent \"<the agent's ACP command>\" [<notes>] [--port <n>] [--pane]");
9899
+ console.error("isorite acp <omp|opencode|claude|codex|hermes> [<notes>] [--port <n>]\nisorite acp --agent \"<the agent's ACP command>\" [<notes>] [--port <n>] [--pane]");
9303
9900
  return 2;
9304
9901
  }
9305
9902
  if (port !== void 0 && !(Number.isInteger(port) && port > 0 && port < 65536)) {
@@ -9311,11 +9908,7 @@ function main(argv) {
9311
9908
  console.error("--url takes a base's address on this machine");
9312
9909
  return 2;
9313
9910
  }
9314
- return acp(rest.find((a, i) => !a.startsWith("--") && ![
9315
- "--port",
9316
- "--agent",
9317
- "--url"
9318
- ].includes(rest[i - 1] ?? "")), {
9911
+ return acp(named ? positional[1] : positional[0], {
9319
9912
  agent,
9320
9913
  pane,
9321
9914
  ...port === void 0 ? {} : { port },
@@ -9358,7 +9951,9 @@ function main(argv) {
9358
9951
  console.error("--port takes a number");
9359
9952
  return 2;
9360
9953
  }
9361
- const folder = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--port");
9954
+ const named = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--port");
9955
+ const here = named ? void 0 : findBase(process.cwd());
9956
+ const folder = named ?? (here?.kind === "base" ? here.notes : void 0);
9362
9957
  const ask = process.stdin.isTTY && process.stdout.isTTY ? async (question) => {
9363
9958
  const { createInterface } = await import("node:readline/promises");
9364
9959
  const rl = createInterface({
@@ -9406,7 +10001,7 @@ function main(argv) {
9406
10001
  }
9407
10002
  throw e;
9408
10003
  }
9409
- process.stdout.write(USAGE);
10004
+ process.stdout.write(command === "help" && rest[0] === "all" ? USAGE : command && command !== "help" && command !== "--help" ? USAGE : SHORT);
9410
10005
  return command && command !== "help" && command !== "--help" ? 2 : 0;
9411
10006
  }
9412
10007
  /** The commands that start something, which say so at once (M21): a subprocess that stalls after this is then