isorite 0.14.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 (92) hide show
  1. package/dist/main.js +342 -38
  2. package/docs/terminal.md +3 -1
  3. package/package.json +1 -1
  4. package/web/_app/immutable/assets/{0.BPP604gH.css → 0.CtWz9d2A.css} +1 -1
  5. package/web/_app/immutable/chunks/{BWE_HS0c.js → 0B47b24t.js} +1 -1
  6. package/web/_app/immutable/chunks/{CzYPMYd4.js → 1j--pDqt.js} +1 -1
  7. package/web/_app/immutable/chunks/{e2LGvGEb.js → AnnzTlE8.js} +1 -1
  8. package/web/_app/immutable/chunks/B7Bf3hc7.js +1 -0
  9. package/web/_app/immutable/chunks/{DRcTlHZP.js → B7LnLClC.js} +1 -1
  10. package/web/_app/immutable/chunks/B9TKPEJD.js +1 -0
  11. package/web/_app/immutable/chunks/{ZwM5k8r0.js → BAVnZFPa.js} +1 -1
  12. package/web/_app/immutable/chunks/{CPSlCQsu.js → BDJlNL-I.js} +3 -3
  13. package/web/_app/immutable/chunks/{BiATTH-R.js → BLe29cGh.js} +1 -1
  14. package/web/_app/immutable/chunks/{C8h1oeAZ.js → BLzZIf7l.js} +1 -1
  15. package/web/_app/immutable/chunks/{DCZYeMlH.js → BPpUSQ2Y.js} +1 -1
  16. package/web/_app/immutable/chunks/{kG4HctVa.js → BQeONalw.js} +1 -1
  17. package/web/_app/immutable/chunks/{CtkUy_15.js → BZX8cNYL.js} +1 -1
  18. package/web/_app/immutable/chunks/{CIYFNih-.js → B_x6_Y3m.js} +1 -1
  19. package/web/_app/immutable/chunks/{Bb_N98K9.js → BiHyHFlW.js} +1 -1
  20. package/web/_app/immutable/chunks/{NyWzYSz1.js → BsY4iMci.js} +1 -1
  21. package/web/_app/immutable/chunks/{DKqMzyyE.js → BtcGx-KQ.js} +1 -1
  22. package/web/_app/immutable/chunks/{BK31dpt6.js → Bytzbt5l.js} +1 -1
  23. package/web/_app/immutable/chunks/{DDAG55ce.js → C1FDDux-.js} +1 -1
  24. package/web/_app/immutable/chunks/C7qBBO-i.js +1 -0
  25. package/web/_app/immutable/chunks/{CdY1G_wk.js → C9Qdslk3.js} +1 -1
  26. package/web/_app/immutable/chunks/{CMIt1Ma8.js → CSKABQj2.js} +1 -1
  27. package/web/_app/immutable/chunks/CSTv2fL0.js +1 -0
  28. package/web/_app/immutable/chunks/{PsfIQCJ1.js → CUB9vnd0.js} +1 -1
  29. package/web/_app/immutable/chunks/{DrVtdHUW.js → CUNetmvQ.js} +1 -1
  30. package/web/_app/immutable/chunks/{Djr0h_v1.js → CVqwkR8N.js} +1 -1
  31. package/web/_app/immutable/chunks/{CTDYUZPz.js → CWc-mNzo.js} +1 -1
  32. package/web/_app/immutable/chunks/{CryuQgNh.js → CZSmkKvQ.js} +1 -1
  33. package/web/_app/immutable/chunks/{BA78_vo5.js → CcE14TDI.js} +1 -1
  34. package/web/_app/immutable/chunks/{DjUnonHY.js → Ch5Z7cbP.js} +1 -1
  35. package/web/_app/immutable/chunks/{DFuPQaNa.js → Cpjqsnqr.js} +1 -1
  36. package/web/_app/immutable/chunks/{Bd-WxALw.js → CwHHpacD.js} +1 -1
  37. package/web/_app/immutable/chunks/{Dzi94aDH.js → D1MhxXyn.js} +1 -1
  38. package/web/_app/immutable/chunks/{DBLoWLMb.js → D4EpFhYZ.js} +1 -1
  39. package/web/_app/immutable/chunks/{UDPiRmKc.js → D7jLdCNu.js} +1 -1
  40. package/web/_app/immutable/chunks/{CFf_dAzX.js → DAVMwq6b.js} +1 -1
  41. package/web/_app/immutable/chunks/{C2npZzkn.js → DEQsS4Ez.js} +1 -1
  42. package/web/_app/immutable/chunks/{B-DzK7Gb.js → DFo4P1vU.js} +1 -1
  43. package/web/_app/immutable/chunks/{CdmC_l9F.js → DH_19--P.js} +1 -1
  44. package/web/_app/immutable/chunks/{l1LsK1sB.js → DJQ6favH.js} +1 -1
  45. package/web/_app/immutable/chunks/{BvPxdlBc.js → DJT73ARj.js} +1 -1
  46. package/web/_app/immutable/chunks/{BCGaqnao.js → DW5uy3Jw.js} +1 -1
  47. package/web/_app/immutable/chunks/{BT_btfh0.js → DWwu8a1P.js} +1 -1
  48. package/web/_app/immutable/chunks/{BhEqJU15.js → DY0M2GMn.js} +1 -1
  49. package/web/_app/immutable/chunks/{TanDZqdf.js → DbtvzL7r.js} +1 -1
  50. package/web/_app/immutable/chunks/{BGW1TKtR.js → Djo-ijeo.js} +1 -1
  51. package/web/_app/immutable/chunks/DtEVmiYo.js +1 -0
  52. package/web/_app/immutable/chunks/{CT4_O9X_.js → DxRGLDbV.js} +1 -1
  53. package/web/_app/immutable/chunks/{DxVFSMVD.js → Dx_7xlS4.js} +36 -36
  54. package/web/_app/immutable/chunks/{D6sVhEz3.js → Ih6r6QR2.js} +1 -1
  55. package/web/_app/immutable/chunks/{BXBhaEho.js → Kem7IXj7.js} +1 -1
  56. package/web/_app/immutable/chunks/LbnMg2Kc.js +1 -0
  57. package/web/_app/immutable/chunks/{Ct_EK1xu.js → MrW4zd8l.js} +1 -1
  58. package/web/_app/immutable/chunks/{BqipEJXw.js → Mwr1r9aw.js} +1 -1
  59. package/web/_app/immutable/chunks/{D1LK3xRu.js → OG9GsKke.js} +1 -1
  60. package/web/_app/immutable/chunks/{B35sQTcK.js → ZTEErEOd.js} +1 -1
  61. package/web/_app/immutable/chunks/{Cf-5I5_6.js → e7GRa0yc.js} +1 -1
  62. package/web/_app/immutable/chunks/{C5Z-Y7Lu.js → f7mFVWQS.js} +1 -1
  63. package/web/_app/immutable/chunks/{Bj-5n5Pb.js → hjSZ041d.js} +1 -1
  64. package/web/_app/immutable/chunks/{DHJ8FcFD.js → mhWk5y4o.js} +1 -1
  65. package/web/_app/immutable/chunks/{DymLl-qY.js → wD7RSG2d.js} +1 -1
  66. package/web/_app/immutable/chunks/{CMikI1NP.js → zasx25UV.js} +3 -3
  67. package/web/_app/immutable/entry/app.Av5W3V8v.js +2 -0
  68. package/web/_app/immutable/entry/start.CbpxCvWi.js +1 -0
  69. package/web/_app/immutable/nodes/0.BTVYyqUm.js +3 -0
  70. package/web/_app/immutable/nodes/1.DR-b7PMP.js +1 -0
  71. package/web/_app/immutable/nodes/2.BePyTqoa.js +1 -0
  72. package/web/_app/immutable/nodes/3.D2rfIXQP.js +1 -0
  73. package/web/_app/immutable/nodes/4.7tuUcODS.js +15 -0
  74. package/web/_app/immutable/nodes/5.Cs9hcGU9.js +1 -0
  75. package/web/_app/immutable/nodes/6.DzvF0ZPt.js +1 -0
  76. package/web/_app/version.json +1 -1
  77. package/web/index.html +15 -15
  78. package/web/_app/immutable/chunks/BPrp4s2r.js +0 -1
  79. package/web/_app/immutable/chunks/CAlIuau0.js +0 -1
  80. package/web/_app/immutable/chunks/Cl5gQm2e.js +0 -1
  81. package/web/_app/immutable/chunks/DFKYo3Wx.js +0 -1
  82. package/web/_app/immutable/chunks/X3aTZRe_.js +0 -1
  83. package/web/_app/immutable/chunks/ej_0XxR7.js +0 -1
  84. package/web/_app/immutable/entry/app.BzA5QYtw.js +0 -2
  85. package/web/_app/immutable/entry/start.BOh0Gj31.js +0 -1
  86. package/web/_app/immutable/nodes/0.BmaxMLzC.js +0 -2
  87. package/web/_app/immutable/nodes/1.IQDu8pD1.js +0 -1
  88. package/web/_app/immutable/nodes/2.D2Oi-ADx.js +0 -1
  89. package/web/_app/immutable/nodes/3.CF1W85jb.js +0 -1
  90. package/web/_app/immutable/nodes/4.BMYBn904.js +0 -15
  91. package/web/_app/immutable/nodes/5.1vm6WZdM.js +0 -1
  92. package/web/_app/immutable/nodes/6.C9ia8cDm.js +0 -1
package/dist/main.js CHANGED
@@ -2865,7 +2865,7 @@ var Conflict = class extends EngineError {
2865
2865
  };
2866
2866
  //#endregion
2867
2867
  //#region packages/engine/src/lookup.ts
2868
- const str$1 = { type: "string" };
2868
+ const str$2 = { type: "string" };
2869
2869
  const fn$1 = (name, description, properties, required) => ({
2870
2870
  type: "function",
2871
2871
  function: {
@@ -2881,9 +2881,9 @@ const fn$1 = (name, description, properties, required) => ({
2881
2881
  });
2882
2882
  const TOOLS = [
2883
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.", {
2884
- query: str$1,
2884
+ query: str$2,
2885
2885
  under: {
2886
- ...str$1,
2886
+ ...str$2,
2887
2887
  description: "Only notes in this folder."
2888
2888
  },
2889
2889
  limit: {
@@ -2892,24 +2892,24 @@ const TOOLS = [
2892
2892
  }
2893
2893
  }, ["query"]),
2894
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: {
2895
- ...str$1,
2895
+ ...str$2,
2896
2896
  description: "The note's id or path: basics/entropy or /basics/entropy.md."
2897
2897
  } }, ["id"]),
2898
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.", {
2899
- id: str$1,
2899
+ id: str$2,
2900
2900
  section: {
2901
- ...str$1,
2901
+ ...str$2,
2902
2902
  description: "A heading's text, as outline_note gives it."
2903
2903
  }
2904
2904
  }, ["id"])
2905
2905
  ];
2906
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).", {
2907
2907
  text: {
2908
- ...str$1,
2908
+ ...str$2,
2909
2909
  description: "A question, or a sentence saying what you are looking for."
2910
2910
  },
2911
2911
  under: {
2912
- ...str$1,
2912
+ ...str$2,
2913
2913
  description: "Only notes in this folder."
2914
2914
  },
2915
2915
  limit: {
@@ -3279,7 +3279,7 @@ or try; not for what an answer or a note says as well.
3279
3279
  problem, fix that and write the whole file again.
3280
3280
  4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
3281
3281
  Link it from the note it belongs to if they want it there.`;
3282
- const str = { type: "string" };
3282
+ const str$1 = { type: "string" };
3283
3283
  const fn = (name, description, properties, required) => ({
3284
3284
  type: "function",
3285
3285
  function: {
@@ -3295,16 +3295,16 @@ const fn = (name, description, properties, required) => ({
3295
3295
  });
3296
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.", {
3297
3297
  title: {
3298
- ...str,
3298
+ ...str$1,
3299
3299
  description: "The question, short, ending in a question mark."
3300
3300
  },
3301
3301
  text: {
3302
- ...str,
3302
+ ...str$1,
3303
3303
  description: "A line or two on what is asked, or empty."
3304
3304
  },
3305
3305
  about: {
3306
3306
  type: "array",
3307
- items: str,
3307
+ items: str$1,
3308
3308
  description: "The ids of the notes it is about."
3309
3309
  }
3310
3310
  }, [
@@ -3313,7 +3313,7 @@ const ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an op
3313
3313
  "about"
3314
3314
  ]);
3315
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: {
3316
- ...str,
3316
+ ...str$1,
3317
3317
  description: "The file's path in the base: folder/name.csv."
3318
3318
  } }, ["path"]);
3319
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: {
@@ -3328,21 +3328,21 @@ const ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a compl
3328
3328
  } }, ["name"]);
3329
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.", {
3330
3330
  path: {
3331
- ...str,
3331
+ ...str$1,
3332
3332
  description: "Where: folder/name.html, beside the notes it belongs to."
3333
3333
  },
3334
- title: str,
3334
+ title: str$1,
3335
3335
  description: {
3336
- ...str,
3336
+ ...str$1,
3337
3337
  description: "One sentence: what it shows, and what can be done with it."
3338
3338
  },
3339
3339
  uses: {
3340
3340
  type: "array",
3341
- items: str,
3341
+ items: str$1,
3342
3342
  description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
3343
3343
  },
3344
3344
  html: {
3345
- ...str,
3345
+ ...str$1,
3346
3346
  description: "The whole file."
3347
3347
  }
3348
3348
  }, [
@@ -3353,16 +3353,16 @@ const WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML fil
3353
3353
  "html"
3354
3354
  ]);
3355
3355
  const READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
3356
- ...str,
3356
+ ...str$1,
3357
3357
  description: "folder/name.html"
3358
3358
  } }, ["path"]);
3359
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.", {
3360
3360
  id: {
3361
- ...str,
3361
+ ...str$1,
3362
3362
  description: "The note's id, or AGENTS.md for a line of their instructions for the base."
3363
3363
  },
3364
3364
  text: {
3365
- ...str,
3365
+ ...str$1,
3366
3366
  description: "The passage, in Markdown."
3367
3367
  }
3368
3368
  }, ["id", "text"]);
@@ -4232,6 +4232,10 @@ var Engine = class Engine {
4232
4232
  return null;
4233
4233
  }
4234
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
+ }
4235
4239
  async agentRecords() {
4236
4240
  const own = this.opts.own, all = [];
4237
4241
  for (const a of await own.list(AGENTS_DIR).catch(() => [])) {
@@ -4648,6 +4652,10 @@ var Engine = class Engine {
4648
4652
  turns: readTurns(text)
4649
4653
  };
4650
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
+ }
4651
4659
  async conversations() {
4652
4660
  const study = this.opts.own;
4653
4661
  const names = await study.list(CONVERSATIONS_DIR).catch(() => []);
@@ -5320,6 +5328,47 @@ const EMPTY = {
5320
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("");
5321
5329
  const lines = (v) => v ? v.replace(/\n$/, "").split("\n") : [];
5322
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
+ });
5323
5372
  /** The sessions the wrappers carry, as the pane draws them. One is shown: the one you chose, else the last to start. */
5324
5373
  var AcpFollower = class {
5325
5374
  #links = /* @__PURE__ */ new Map();
@@ -5402,6 +5451,8 @@ var AcpFollower = class {
5402
5451
  size: live.size,
5403
5452
  ours: this.#ours,
5404
5453
  sessions: this.#sessions(),
5454
+ commands: live.commands,
5455
+ settings: [...live.modes ? [live.modes] : [], ...live.configs],
5405
5456
  ...problem
5406
5457
  };
5407
5458
  else if (read) this.#view = {
@@ -5635,6 +5686,42 @@ var AcpFollower = class {
5635
5686
  link.opening = "";
5636
5687
  link.send({ type: "open" });
5637
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
+ }
5638
5725
  /** Ask the attached agents for their sessions again: the drawer is being looked at. */
5639
5726
  refresh() {
5640
5727
  for (const link of this.#links.values()) if (link.can.list) link.send({ type: "list" });
@@ -5713,7 +5800,11 @@ var AcpFollower = class {
5713
5800
  break;
5714
5801
  }
5715
5802
  case "session/new":
5716
- 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
+ }
5717
5808
  break;
5718
5809
  case "session/load":
5719
5810
  case "session/resume":
@@ -5722,9 +5813,20 @@ var AcpFollower = class {
5722
5813
  this.#lives.delete(keyOf(link.agent, live.session));
5723
5814
  break;
5724
5815
  }
5816
+ if (m.result?.modes) live.modes = modesOf(m.result.modes);
5817
+ if (m.result?.configOptions) live.configs = configsOf(m.result.configOptions);
5725
5818
  this.#loaded(live);
5726
5819
  }
5727
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;
5728
5830
  case "session/prompt":
5729
5831
  if (live) {
5730
5832
  const stop = m.error ? "failed" : m.result?.stopReason === "cancelled" ? "stopped" : m.result?.stopReason === "end_turn" ? "done" : "failed";
@@ -5788,7 +5890,10 @@ var AcpFollower = class {
5788
5890
  loading: false,
5789
5891
  replaying: false,
5790
5892
  pending: null,
5791
- given: /* @__PURE__ */ new Set()
5893
+ given: /* @__PURE__ */ new Set(),
5894
+ commands: [],
5895
+ modes: null,
5896
+ configs: []
5792
5897
  };
5793
5898
  live.link = link;
5794
5899
  if (cwd) live.cwd = cwd;
@@ -5978,7 +6083,19 @@ var AcpFollower = class {
5978
6083
  if (typeof cost === "number") live.cost = cost;
5979
6084
  break;
5980
6085
  }
5981
- 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);
5982
6099
  }
5983
6100
  }
5984
6101
  /** The note a path names, if it is a Markdown file of the base (relative paths read from the session's folder). */
@@ -6038,7 +6155,7 @@ var AcpFollower = class {
6038
6155
  };
6039
6156
  //#endregion
6040
6157
  //#region packages/server/src/version.ts
6041
- const VERSION = "0.14.0";
6158
+ const VERSION = "0.15.0";
6042
6159
  //#endregion
6043
6160
  //#region packages/server/src/connect.ts
6044
6161
  const HARNESSES = [{
@@ -6310,6 +6427,27 @@ function detectAgents(env = process.env) {
6310
6427
  }];
6311
6428
  });
6312
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;
6313
6451
  /** Why an agent that stopped may have: the key it reads isn't set where isorite was started, for the command run. */
6314
6452
  function missingKey(command, env = process.env) {
6315
6453
  const k = KNOWN.find((x) => x.command === command);
@@ -6563,6 +6701,108 @@ var AgentRunner = class {
6563
6701
  }
6564
6702
  };
6565
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
6566
6806
  //#region packages/server/src/http.ts
6567
6807
  const LOOPBACK = /* @__PURE__ */ new Set([
6568
6808
  "localhost",
@@ -6639,7 +6879,7 @@ const AgentStatus = z.object({
6639
6879
  session: z.string().min(1).max(200),
6640
6880
  busy: z.boolean()
6641
6881
  }).strict();
6642
- 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 }) {
6643
6883
  const app = new Hono();
6644
6884
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
6645
6885
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -6773,6 +7013,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6773
7013
  app.get("/api/agents/notes", (c) => answer(c, () => api.agentNotes()));
6774
7014
  post("/api/agents/notes", z.object({ may: z.boolean() }).strict(), (_c, b) => api.setAgentNotes(b.may));
6775
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));
6776
7017
  app.get("/api/conversation", (c) => answer(c, async () => await api.conversation(c.req.query("id") || void 0) ?? {
6777
7018
  id: "",
6778
7019
  turns: []
@@ -6851,6 +7092,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6851
7092
  }).strict(), (_c, b) => api.agentReceived(b.id, b.agent));
6852
7093
  post("/api/agent/status", AgentStatus, (_c, b) => api.agentStatus(b.agent, b.session, b.busy));
6853
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
+ }
6854
7105
  if (notes) {
6855
7106
  app.get("/api/connect", (c) => answer(c, async () => proposal(notes)));
6856
7107
  post("/api/connect", z.object({ files: z.array(z.string().max(200)).max(10).optional() }).strict(), async (c, b) => {
@@ -6862,7 +7113,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6862
7113
  app.get("/api/acp/agent", (c) => answer(c, async () => agents.state()));
6863
7114
  app.get("/api/acp/detected", (c) => answer(c, async () => {
6864
7115
  if (!local(c)) throw new EngineError("agents are looked for only from this machine", 403);
6865
- return detectAgents();
7116
+ return listAgents();
6866
7117
  }));
6867
7118
  app.get("/api/acp/registry", (c) => answer(c, async () => registryAgents().catch((e) => {
6868
7119
  throw new EngineError(e.message, 502);
@@ -6967,6 +7218,23 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, acp, bas
6967
7218
  }).strict(), async (_c, b) => {
6968
7219
  await acp.follow(b.agent, b.session).catch(refused);
6969
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
+ });
6970
7238
  post("/api/acp/new", z.object({ agent: z.string().max(40) }).strict(), async (_c, b) => {
6971
7239
  await acp.fresh(b.agent).catch(refused);
6972
7240
  });
@@ -7457,7 +7725,8 @@ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts =
7457
7725
  acp,
7458
7726
  bases,
7459
7727
  base: known.name,
7460
- ...loopback ? { agents } : {}
7728
+ ...loopback ? { agents } : {},
7729
+ config
7461
7730
  }).fetch
7462
7731
  };
7463
7732
  }
@@ -7614,6 +7883,8 @@ async function doctor(folder, o = {}) {
7614
7883
  const errors = issues.filter((i) => i.level === "error");
7615
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})` : ""}` : ""}`);
7616
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`);
7617
7888
  const repository = existsSync(settings) ? parseSettings(readFileSync(settings, "utf8")).repository : void 0;
7618
7889
  const found = repoOf(notes);
7619
7890
  if (!repository && found) say("repository", "ok", `${found}: code links point here`);
@@ -8244,6 +8515,14 @@ async function acp(path, { agent, port = 4767, input, output, env = process.env,
8244
8515
  toAgent(m);
8245
8516
  }
8246
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
+ });
8247
8526
  if (c.type === "open") {
8248
8527
  const params = {
8249
8528
  cwd,
@@ -9303,6 +9582,11 @@ function label(b, all) {
9303
9582
  }
9304
9583
  /** The choice `isorite` makes, and what it says. */
9305
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
+ };
9306
9590
  const found = findBase(io.cwd);
9307
9591
  if (found.kind === "base") return io.open(found.notes);
9308
9592
  if (found.kind === "several") {
@@ -9314,7 +9598,7 @@ async function start(io) {
9314
9598
  if (found.kind === "repo") {
9315
9599
  const notes = join(found.repo, "notes");
9316
9600
  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));
9601
+ if (!existsSync(notes) && await io.confirm(`Make one at ${notes}?`)) return io.open(await make(notes));
9318
9602
  if (existsSync(notes)) io.out(` ${notes} is there already: open it as it is with isorite serve ${notes}`);
9319
9603
  }
9320
9604
  const known = recent(io.config);
@@ -9332,7 +9616,7 @@ async function start(io) {
9332
9616
  const pick = await io.choose("Open which?", rows.length);
9333
9617
  if (pick === null) return 2;
9334
9618
  if (pick < known.length) return io.open(known[pick].path);
9335
- if (pick === known.length) return io.open(io.make(join(io.cwd, "notes")));
9619
+ if (pick === known.length) return io.open(await make(join(io.cwd, "notes")));
9336
9620
  return io.guide();
9337
9621
  }
9338
9622
  /** The model for search by meaning, offered on the first start without one (M26): asked once, fetched on a yes,
@@ -9495,6 +9779,9 @@ async function terminalStart({ port, noOpen }) {
9495
9779
  console.log(` made ${made}`);
9496
9780
  return made;
9497
9781
  },
9782
+ made: async (notes) => {
9783
+ await askWhereFilesLive(notes, ask, console.log, configPath());
9784
+ },
9498
9785
  guide: async () => {
9499
9786
  await model();
9500
9787
  await guide(void 0, { port: port ?? await freePort() });
@@ -9519,7 +9806,22 @@ function main(argv) {
9519
9806
  if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
9520
9807
  if (command === "new" && rest[0]) {
9521
9808
  const made = newBase(rest[0]);
9522
- 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);
9523
9825
  return 0;
9524
9826
  }
9525
9827
  if (command === "serve" && rest[0]) {
@@ -9586,9 +9888,15 @@ function main(argv) {
9586
9888
  const at = rest.indexOf(name);
9587
9889
  return at < 0 ? void 0 : rest[at + 1];
9588
9890
  };
9589
- 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"));
9590
9898
  if (!agent) {
9591
- 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]");
9592
9900
  return 2;
9593
9901
  }
9594
9902
  if (port !== void 0 && !(Number.isInteger(port) && port > 0 && port < 65536)) {
@@ -9600,11 +9908,7 @@ function main(argv) {
9600
9908
  console.error("--url takes a base's address on this machine");
9601
9909
  return 2;
9602
9910
  }
9603
- return acp(rest.find((a, i) => !a.startsWith("--") && ![
9604
- "--port",
9605
- "--agent",
9606
- "--url"
9607
- ].includes(rest[i - 1] ?? "")), {
9911
+ return acp(named ? positional[1] : positional[0], {
9608
9912
  agent,
9609
9913
  pane,
9610
9914
  ...port === void 0 ? {} : { port },
package/docs/terminal.md CHANGED
@@ -16,7 +16,7 @@ isorite # in the project: finds its base, opens the browser (isorite se
16
16
 
17
17
  ### In Iso's pane
18
18
 
19
- With no agent attached, Iso's pane says how to attach one. *Here, in this pane*: choose an agent of the [ACP registry](https://agentclientprotocol.com/get-started/registry), or give the command of one it doesn't list (`hermes acp`). isorite starts it for the base, behind `isorite acp --pane`, in the repository the base is in, with the server's environment, and starts it again when the base is next opened. It runs a program, so it is chosen from this machine only; the choice is kept in `~/.config/isorite/bases.toml` (`agent`, or `agent_command`). The drawer's *stop it* stops it.
19
+ With no agent attached, Iso's pane offers a card for each agent isorite knows (Claude Code, OpenCode, omp, Codex, Hermes), saying whether it is found on this machine's PATH and, for those that read a key, whether it is set where isorite was started (by name; no key is read or kept). *Use in this pane* starts it for the base; *in my terminal* gives isorite's MCP address and, to share the session live with the pane, the `isorite acp <agent>` line to run in an ACP front end such as Toad. *Another agent* takes one of the [ACP registry](https://agentclientprotocol.com/get-started/registry), or the command of one it doesn't list (`hermes acp`). The drawer's *change agent* is the same choice later. isorite starts it for the base, behind `isorite acp --pane`, in the repository the base is in, with the server's environment, and starts it again when the base is next opened. It runs a program, so it is chosen from this machine only; the choice is kept in `~/.config/isorite/bases.toml` (`agent`, or `agent_command`). The drawer's *stop it* stops it. A session isorite kept can be removed from the drawer (*remove*): that deletes isorite's copy only, never the agent's own history, and a live session is not removed.
20
20
 
21
21
  A registry agent is run as the registry says: its program for this platform, fetched once into `~/.local/share/isorite/agents/<id>/<version>/` and checked against its SHA-256; else with `npx -y` or `uvx`. `ISORITE_ACP_REGISTRY` names another registry, an address or a file.
22
22
 
@@ -28,6 +28,8 @@ Toad in the terminal, Zed, or JetBrains: give it isorite as its agent, with the
28
28
  isorite acp --agent "npx -y @agentclientprotocol/claude-agent-acp" ~/code/my-project/notes
29
29
  ```
30
30
 
31
+ For an agent isorite knows by name there is a shorter form: `isorite acp omp`, `opencode`, `claude`, `codex` or `hermes` (the pane's cards show the line for each). A folder of that name here is taken as the notes instead.
32
+
31
33
  Run in a folder with a base in it or under it, the notes can be left out. This starts the isorite server for the notes, unless one is running on port 4767 (`--port <n>` for another), and the agent behind it. Every message passes through: the session is live in the front end and in the pane at once, asked from either. isorite's MCP is added to each session the agent opens, so the agent needs no config of its own for it.
32
34
 
33
35
  ### In the agent's own terminal