isorite 0.7.0 → 0.8.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 (41) hide show
  1. package/README.md +1 -0
  2. package/dist/main.js +609 -96
  3. package/docs/setup-for-agents.md +3 -0
  4. package/docs/setup.md +20 -0
  5. package/docs/terminal.md +15 -0
  6. package/package.json +1 -1
  7. package/plugin/server.ts +4 -2
  8. package/web/_app/immutable/assets/{0.BKnIhFWM.css → 0.CTbO0owi.css} +1 -1
  9. package/web/_app/immutable/chunks/B-65IYX-.js +6 -0
  10. package/web/_app/immutable/chunks/{62dhNtvj.js → B91efLFT.js} +3 -3
  11. package/web/_app/immutable/chunks/{Cn-vtDpV.js → BjPiibIz.js} +1 -1
  12. package/web/_app/immutable/chunks/{BNjH_DUx.js → BrXPCJaM.js} +1 -1
  13. package/web/_app/immutable/chunks/{B7GA1FFB.js → BtUtjfIh.js} +1 -1
  14. package/web/_app/immutable/chunks/{CYNZ4aV_.js → CCiHXe5d.js} +1 -1
  15. package/web/_app/immutable/chunks/{DHfKGggc.js → CGzfi9Nl.js} +1 -1
  16. package/web/_app/immutable/chunks/CnP_Si9g.js +1 -0
  17. package/web/_app/immutable/chunks/{CWmfjLLv.js → D_JZ0swv.js} +1 -1
  18. package/web/_app/immutable/chunks/{WBFggAXs.js → DgZW3JtG.js} +1 -1
  19. package/web/_app/immutable/chunks/{CVbgxz8Y.js → Di_ac7WN.js} +1 -1
  20. package/web/_app/immutable/chunks/{DsBJWNWe.js → NmqB5ax3.js} +1 -1
  21. package/web/_app/immutable/chunks/{BoNMG-YE.js → dvrIP-Hm.js} +1 -1
  22. package/web/_app/immutable/chunks/hOAYc4Tf.js +1 -0
  23. package/web/_app/immutable/entry/{app.Dt52eXt2.js → app.DO9lj7h3.js} +2 -2
  24. package/web/_app/immutable/entry/start.Bg5TZs6n.js +1 -0
  25. package/web/_app/immutable/nodes/0.N1e1uAFq.js +26 -0
  26. package/web/_app/immutable/nodes/{1.B5qkcrvB.js → 1.BRTPLKyU.js} +1 -1
  27. package/web/_app/immutable/nodes/2.BWh1AYEd.js +1 -0
  28. package/web/_app/immutable/nodes/3.Cgqgn7Zv.js +1 -0
  29. package/web/_app/immutable/nodes/{4.BhNJiqT-.js → 4.DYW7Fxdy.js} +1 -1
  30. package/web/_app/immutable/nodes/{5.DXTbGemS.js → 5.Dkvkif0E.js} +1 -1
  31. package/web/_app/immutable/nodes/6.BinB0fuq.js +1 -0
  32. package/web/_app/version.json +1 -1
  33. package/web/index.html +15 -15
  34. package/web/_app/immutable/chunks/B3dCo0od.js +0 -1
  35. package/web/_app/immutable/chunks/D6Vzg2HD.js +0 -6
  36. package/web/_app/immutable/chunks/DkvLWnSd.js +0 -1
  37. package/web/_app/immutable/entry/start.mjqIl2PI.js +0 -1
  38. package/web/_app/immutable/nodes/0.DIhIOwuo.js +0 -26
  39. package/web/_app/immutable/nodes/2.CK3q7brK.js +0 -1
  40. package/web/_app/immutable/nodes/3.CrUewa31.js +0 -1
  41. package/web/_app/immutable/nodes/6.BZWaFgFP.js +0 -1
package/dist/main.js CHANGED
@@ -1622,7 +1622,7 @@ function locate$1(note, p) {
1622
1622
  }
1623
1623
  /** The note with a proposal written in. For a rewrite, `region` writes that one alone; without it, every region
1624
1624
  * still open. Null when the place (or, for a rewrite, any of the places asked for) cannot be found. */
1625
- function written(note, p, region) {
1625
+ function written$1(note, p, region) {
1626
1626
  if (p.form === "rewrite") {
1627
1627
  const places = (region !== void 0 ? [region] : p.open ?? p.regions.map((_, k) => k)).map((k) => ({
1628
1628
  k,
@@ -2704,6 +2704,43 @@ say plainly what you would need to look up to say more.`;
2704
2704
  }));
2705
2705
  //#endregion
2706
2706
  //#region packages/engine/src/lookup.ts
2707
+ /** Which base a call names, and its arguments for that base: `base`, or a note's id written `memory:tools/x`. */
2708
+ function baseOf(args) {
2709
+ const { base, ...rest } = args;
2710
+ let named = typeof base === "string" ? base.trim() : "";
2711
+ for (const k of ["id", "note"]) {
2712
+ const m = typeof rest[k] === "string" ? /^([a-z0-9][a-z0-9-]{0,39}):(?!\/\/)(.+)$/.exec(rest[k]) : null;
2713
+ if (m) {
2714
+ named ||= m[1];
2715
+ if (m[1] === named) rest[k] = m[2];
2716
+ }
2717
+ }
2718
+ return {
2719
+ base: named,
2720
+ args: rest
2721
+ };
2722
+ }
2723
+ /** The tools' definitions with `base`, for a request that has other bases lent to it. */
2724
+ function withBase(defs, names, always = false) {
2725
+ if (!names.length && !always) return defs;
2726
+ const base = {
2727
+ type: "string",
2728
+ description: `Another knowledge base lent to you, by name: ${names.length ? names.join(", ") : "one brief_base names"}. This base when not given. A note there may also be named <base>:folder/name.`
2729
+ };
2730
+ return defs.map((d) => ({
2731
+ ...d,
2732
+ function: {
2733
+ ...d.function,
2734
+ parameters: {
2735
+ ...d.function.parameters,
2736
+ properties: {
2737
+ ...d.function.parameters.properties,
2738
+ base
2739
+ }
2740
+ }
2741
+ }
2742
+ }));
2743
+ }
2707
2744
  /** A note's id from what a model writes for one: /basics/entropy.md, basics/entropy, [x](/basics/entropy.md). */
2708
2745
  function noteId(b, raw) {
2709
2746
  const id = String(raw ?? "").trim().replace(/^.*\]\(/, "").replace(/\)$/, "").replace(/\s+"[^"]*"$/, "").split("#")[0].replace(/^\/+/, "").replace(/\.md$/i, "");
@@ -2779,17 +2816,19 @@ var init_lookup = __esmMin((() => {
2779
2816
  named = /* @__PURE__ */ new Map();
2780
2817
  b;
2781
2818
  similar;
2782
- constructor(b, note, similar = null) {
2819
+ away;
2820
+ constructor(b, note, similar = null, away = null) {
2783
2821
  this.b = b;
2784
2822
  this.held = [note];
2785
2823
  this.similar = similar;
2824
+ this.away = away;
2786
2825
  }
2787
2826
  defs() {
2788
- return this.similar ? [
2827
+ return withBase(this.similar ? [
2789
2828
  TOOLS[0],
2790
2829
  FIND_SIMILAR,
2791
2830
  ...TOOLS.slice(1)
2792
- ] : TOOLS;
2831
+ ] : TOOLS, this.away?.lent().map((l) => l.name) ?? []);
2793
2832
  }
2794
2833
  /** The notes opened, each once, in order. */
2795
2834
  read() {
@@ -2809,6 +2848,19 @@ var init_lookup = __esmMin((() => {
2809
2848
  notes: []
2810
2849
  };
2811
2850
  let text;
2851
+ const there = baseOf(args);
2852
+ if (there.base) {
2853
+ try {
2854
+ text = await this.elsewhere(name, there.base, there.args, step);
2855
+ } catch (err) {
2856
+ text = this.fail(step, `That could not be looked up: ${err.message}`);
2857
+ }
2858
+ this.steps.push(step);
2859
+ return {
2860
+ text,
2861
+ step
2862
+ };
2863
+ }
2812
2864
  try {
2813
2865
  text = name === "search_notes" ? this.search(args, step) : name === "find_similar" ? await this.meaning(args, step) : name === "outline_note" ? this.outline(args, step) : name === "read_note" ? this.section(args, step) : this.fail(step, `There is no tool ${name}.`);
2814
2866
  } catch (err) {
@@ -2820,6 +2872,22 @@ var init_lookup = __esmMin((() => {
2820
2872
  step
2821
2873
  };
2822
2874
  }
2875
+ /** A look in another base lent to the request: what that base gives, said as being from there. */
2876
+ async elsewhere(name, base, args, step) {
2877
+ const lent = this.away?.lent().map((l) => l.name) ?? [];
2878
+ if (!lent.includes(base)) return this.fail(step, lent.length ? `There is no base ${base} lent here: the bases are ${lent.join(", ")}. Leave base out for this one.` : `There is no base ${base} lent here: leave base out.`);
2879
+ const note = typeof args.id === "string" ? args.id.replace(/^\/+/, "").replace(/\.md$/i, "") : void 0;
2880
+ step.how = name === "search_notes" ? "keyword" : name === "find_similar" ? "meaning" : "read";
2881
+ step.away = {
2882
+ base,
2883
+ ...note ? { note } : {},
2884
+ ...typeof args.section === "string" && args.section ? { section: args.section } : {}
2885
+ };
2886
+ const query = String(args.query ?? args.text ?? "").trim();
2887
+ if (query) step.query = query;
2888
+ step.said = note ? `Read ${note} in ${base}` : query ? `Searched ${base} for "${query}"` : `Looked in ${base}`;
2889
+ return this.away.look(base, name, args);
2890
+ }
2823
2891
  fail(step, why) {
2824
2892
  step.failed = true;
2825
2893
  step.said = why;
@@ -3147,6 +3215,7 @@ function turnMessages(a) {
3147
3215
  const held = reads.length ? `# Notes you read earlier in this conversation\n\n${reads.map(sentText).join("\n\n")}` : "";
3148
3216
  const aside = a.aside?.length ? `# Read earlier, set aside\n\n${a.aside.map((n) => `- ${n.title} (/${n.id}.md)`).join("\n")}` : "";
3149
3217
  const agents = a.agents?.trim() ? `# Their instructions for this knowledge base (AGENTS.md)\n\n${a.agents.trim()}` : "";
3218
+ const lent = a.lookup && a.lent?.length ? `# Other knowledge bases lent to you\n\nYou may read these with the same look-up tools, giving their base; you change notes only in this one. Look in them where what is asked may be there: how the person works, their tools, what was settled before.\n\n${a.lent.map((l) => `- ${l.name}: ${l.about}`).join("\n")}` : "";
3150
3219
  const known = understood.length ? `# Their understanding, as you have recorded it\n\n${understood.map(sentText).join("\n\n")}` : "";
3151
3220
  const broken = a.broken?.length ? `# Code links that no longer resolve\n\nSince you were last told, these links in their notes point to files no longer in their repository. Mention it only where it bears on what they ask.\n\n${a.broken.map((b) => `- in ${b.title} (/${b.id}.md): ${b.href}`).join("\n")}` : "";
3152
3221
  const request = `# What they ask now\n\n${a.asked}`;
@@ -3161,6 +3230,7 @@ function turnMessages(a) {
3161
3230
  role: "user",
3162
3231
  content: [
3163
3232
  agents,
3233
+ lent,
3164
3234
  earlier,
3165
3235
  known,
3166
3236
  gave,
@@ -3174,7 +3244,7 @@ function turnMessages(a) {
3174
3244
  {
3175
3245
  name: "instructions and tools",
3176
3246
  detail: `${a.mode} mode${checking ? ", checking" : ""}${a.lookup ? "" : ", no look-ups"}`,
3177
- tokens: estimate(system) + (a.lookup ? 500 : 0) + turnTools(a.mode, a.sent).length * 120
3247
+ tokens: estimate(system) + estimate(lent) + (a.lookup ? 500 : 0) + turnTools(a.mode, a.sent).length * 120
3178
3248
  },
3179
3249
  {
3180
3250
  name: "earlier in this conversation",
@@ -3749,7 +3819,7 @@ function keepDone(before, next) {
3749
3819
  return next;
3750
3820
  }
3751
3821
  }
3752
- var SIGN_IN_MS, REPLY_TOKENS, NOTE_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
3822
+ var SIGN_IN_MS, fromBase, REPLY_TOKENS, NOTE_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
3753
3823
  var init_engine = __esmMin((() => {
3754
3824
  init_src$1();
3755
3825
  init_api();
@@ -3762,6 +3832,7 @@ var init_engine = __esmMin((() => {
3762
3832
  init_meaning();
3763
3833
  init_provider();
3764
3834
  SIGN_IN_MS = 6e5;
3835
+ fromBase = (base, text) => `(From the base ${base}: name its notes ${base}:<id>, or give base "${base}".)\n${text}`;
3765
3836
  REPLY_TOKENS = 2500;
3766
3837
  NOTE_TOKENS = 6e3;
3767
3838
  DRAFT_PREFIX = "@draft/";
@@ -4023,7 +4094,7 @@ var init_engine = __esmMin((() => {
4023
4094
  }));
4024
4095
  }
4025
4096
  async agentTools() {
4026
- return [
4097
+ const tools = [
4027
4098
  BRIEF,
4028
4099
  TOOLS[0],
4029
4100
  ...this.similar ? [FIND_SIMILAR] : [],
@@ -4031,9 +4102,48 @@ var init_engine = __esmMin((() => {
4031
4102
  EDIT_NOTE,
4032
4103
  WRITE_NOTE
4033
4104
  ];
4105
+ return this.opts.elsewhere ? withBase(tools, this.lent().map((l) => l.name), true) : tools;
4106
+ }
4107
+ /** The bases lent to this one's sessions, not this one. */
4108
+ lent() {
4109
+ return (this.opts.elsewhere?.lent() ?? []).filter((l) => l.name !== this.opts.name);
4110
+ }
4111
+ /** Another base lent here, by name, or why there is none of that name. */
4112
+ other(base) {
4113
+ const lent = this.lent();
4114
+ const engine = lent.some((l) => l.name === base) ? this.opts.elsewhere?.base(base) : null;
4115
+ if (!engine) throw new EngineError(lent.length ? `there is no base ${base} lent here: the bases are ${lent.map((l) => l.name).join(", ")}; leave base out for ${this.opts.name}` : `there is no base ${base} lent here: leave base out`, 404);
4116
+ return engine;
4117
+ }
4118
+ /** For Iso: a look in the other bases lent here, as their own look-ups give it, drawn on no Atlas. */
4119
+ get away() {
4120
+ if (!this.opts.elsewhere) return null;
4121
+ return {
4122
+ lent: () => this.lent(),
4123
+ look: async (base, tool, args) => fromBase(base, (await new Lookup(this.other(base).bundle, "", this.other(base).similar).call(tool, JSON.stringify(args))).text)
4124
+ };
4125
+ }
4126
+ /** What this base is, in a line: its AGENTS.md's first line of text, else its name and size. */
4127
+ async about() {
4128
+ return ((await this.agents()).text ?? "").split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l && !l.startsWith("---")) ?? `${this.bundle.concepts.size} notes`;
4034
4129
  }
4035
4130
  async agentLook({ agent, session, tool, args, asked = "" }) {
4036
- if (tool === BRIEF.function.name) return { text: this.brief() };
4131
+ const there = baseOf(args);
4132
+ if (there.base && there.base !== this.opts.name) {
4133
+ const done = await this.other(there.base).agentLook({
4134
+ agent,
4135
+ session,
4136
+ tool,
4137
+ args: there.args,
4138
+ asked
4139
+ });
4140
+ return { text: fromBase(there.base, done.text) };
4141
+ }
4142
+ args = there.args;
4143
+ if (tool === BRIEF.function.name) {
4144
+ const lent = this.lent();
4145
+ return { text: this.brief() + (lent.length ? `\n\nOther knowledge bases lent to you, each with its own practice (brief_base with its base, before you use it):\n${lent.map((l) => `- ${l.name}: ${l.about}`).join("\n")}\nPass base to any tool to look or write there.` : "") };
4146
+ }
4037
4147
  if (tool === EDIT_NOTE.function.name || tool === WRITE_NOTE.function.name) {
4038
4148
  const str = (v, name) => {
4039
4149
  if (typeof v !== "string") throw new EngineError(`${tool} needs ${name}, as text`, 400);
@@ -4075,6 +4185,7 @@ var init_engine = __esmMin((() => {
4075
4185
  session
4076
4186
  });
4077
4187
  }
4188
+ if (!busy) for (const l of this.lent()) await this.opts.elsewhere?.base(l.name)?.agentStatus(agent, session, false);
4078
4189
  }
4079
4190
  async agentWrite({ agent, session, note, asked, edit }) {
4080
4191
  const who = `agent:${agent}`;
@@ -4600,7 +4711,7 @@ var init_engine = __esmMin((() => {
4600
4711
  tier: req.tier,
4601
4712
  place
4602
4713
  });
4603
- const lookup = new Lookup(this.bundle, req.note, this.similar);
4714
+ const lookup = new Lookup(this.bundle, req.note, this.similar, this.away);
4604
4715
  (async () => {
4605
4716
  let ran = null, offered = false;
4606
4717
  try {
@@ -5203,6 +5314,7 @@ var init_engine = __esmMin((() => {
5203
5314
  sent: [...sent, ...reads],
5204
5315
  aside,
5205
5316
  ...agentsText ? { agents: agentsText } : {},
5317
+ ...this.lent().length ? { lent: this.lent() } : {},
5206
5318
  asked: req.asked ?? ""
5207
5319
  });
5208
5320
  const ratio = hand.ratio ?? 1, scaled = (n) => Math.round(n * ratio);
@@ -5551,6 +5663,7 @@ var init_engine = __esmMin((() => {
5551
5663
  sent,
5552
5664
  aside: w.aside,
5553
5665
  ...w.agentsText ? { agents: w.agentsText } : {},
5666
+ ...this.lent().length ? { lent: this.lent() } : {},
5554
5667
  asked: req.asked,
5555
5668
  broken: await this.brokenNews("iso")
5556
5669
  });
@@ -5600,7 +5713,7 @@ var init_engine = __esmMin((() => {
5600
5713
  });
5601
5714
  };
5602
5715
  const who = `iso:${this.provider.name}/${this.provider.tiers[req.tier]}`;
5603
- const lookup = new Lookup(this.bundle, req.given.find((g) => g.kind !== "folder")?.id ?? "", this.similar);
5716
+ const lookup = new Lookup(this.bundle, req.given.find((g) => g.kind !== "folder")?.id ?? "", this.similar, this.away);
5604
5717
  const looks = new Set(lookup.defs().map((d) => d.function.name));
5605
5718
  const tools = {
5606
5719
  defs: () => [...lookupOn ? lookup.defs() : [], ...own],
@@ -5611,7 +5724,7 @@ var init_engine = __esmMin((() => {
5611
5724
  try {
5612
5725
  args = JSON.parse(raw || "{}");
5613
5726
  } catch {}
5614
- const id = noteId(this.bundle, args.id), want = typeof args.section === "string" ? args.section.replace(/^#+\s*/, "").trim() : "";
5727
+ const id = args.base ? null : noteId(this.bundle, args.id), want = typeof args.section === "string" ? args.section.replace(/^#+\s*/, "").trim() : "";
5615
5728
  const held = id ? inHand.get(id) : void 0;
5616
5729
  if (id && held && (held === "whole" || want && held.includes(want))) {
5617
5730
  const step = {
@@ -5635,6 +5748,8 @@ var init_engine = __esmMin((() => {
5635
5748
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) args = parsed;
5636
5749
  } catch {}
5637
5750
  if (!own.some((d) => d.function.name === name)) return { text: `There is no tool ${name} in ${mode} mode.` };
5751
+ const there = baseOf(args);
5752
+ if (there.base && there.base !== this.opts.name) return { text: `Notes are changed only in ${this.opts.name}, the base you are in: ${there.base} is lent to you to read.` };
5638
5753
  return { text: await this.turnTool(name, args, {
5639
5754
  turn,
5640
5755
  conversation,
@@ -6224,10 +6339,10 @@ var init_engine = __esmMin((() => {
6224
6339
  let text;
6225
6340
  if (p.form === "rewrite") {
6226
6341
  if (region !== void 0 && !p.open?.includes(region)) throw new EngineError("that change is no longer waiting: it was kept or removed", 409);
6227
- text = written(now, p, region);
6342
+ text = written$1(now, p, region);
6228
6343
  if (text === null) throw new EngineError(region === void 0 ? "a passage this would rewrite is no longer in the note as it was. Keep the others one by one." : "the passage this would rewrite is no longer in the note as it was", 409);
6229
6344
  } else {
6230
- text = written(now, p) ?? written(now, {
6345
+ text = written$1(now, p) ?? written$1(now, {
6231
6346
  ...p,
6232
6347
  after: null,
6233
6348
  before: null
@@ -6780,6 +6895,62 @@ function gatewayTlsFrom(config, dir, env = process.env) {
6780
6895
  async function hasModel$1(config = configPath()) {
6781
6896
  return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
6782
6897
  }
6898
+ const BASE_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
6899
+ const expand = (p) => p === "~" ? homedir() : p.startsWith("~/") ? join(homedir(), p.slice(2)) : resolve(p);
6900
+ /** A path as written in the file: under the home folder, with ~. */
6901
+ const written = (p) => {
6902
+ const home = homedir();
6903
+ return p === home ? "~" : p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
6904
+ };
6905
+ /** Where the bases are listed, beside the configuration. */
6906
+ const basesPath = (config = configPath()) => join(dirname(config), "bases.toml");
6907
+ /** The bases listed, in the order written. A row without a path, or with a name isorite
6908
+ * would not give one, is passed over. */
6909
+ function knownBases(config = configPath()) {
6910
+ return Object.entries(readConfig(basesPath(config))).flatMap(([name, v]) => {
6911
+ const row = table(v);
6912
+ return BASE_NAME.test(name) && typeof row.path === "string" && row.path ? [{
6913
+ name,
6914
+ path: expand(row.path),
6915
+ lend: row.lend === true
6916
+ }] : [];
6917
+ });
6918
+ }
6919
+ /** A name for a base from its folder: small letters, digits and dashes. */
6920
+ function baseName(folder) {
6921
+ return basename(folder).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "base";
6922
+ }
6923
+ /** The base at `folder` by its name in the configuration, listed first if it is not: under its folder's name, or
6924
+ * that name with -2, -3… where another folder has it. `as` names it, and `lend` lends it. */
6925
+ function rememberBase(folder, { path = configPath(), as, lend } = {}) {
6926
+ const at = resolve(folder), had = knownBases(path).find((b) => b.path === at);
6927
+ if (had && lend === void 0) return had.name;
6928
+ const bases = readConfig(basesPath(path));
6929
+ let name = had?.name;
6930
+ if (!name) {
6931
+ const stem = as && BASE_NAME.test(as) ? as : baseName(at), taken = new Set(Object.keys(bases));
6932
+ name = stem;
6933
+ for (let n = 2; taken.has(name); n++) name = `${stem.slice(0, 36)}-${n}`;
6934
+ }
6935
+ bases[name] = {
6936
+ ...table(bases[name]),
6937
+ path: written(at),
6938
+ ...lend === void 0 ? {} : { lend }
6939
+ };
6940
+ writeConfig(bases, basesPath(path));
6941
+ return name;
6942
+ }
6943
+ /** Lend a known base to every session, or stop lending it. False when there is no base of that name. */
6944
+ function lendBase(name, lend, path = configPath()) {
6945
+ const bases = readConfig(basesPath(path));
6946
+ if (!BASE_NAME.test(name) || !bases[name]) return false;
6947
+ bases[name] = {
6948
+ ...table(bases[name]),
6949
+ lend
6950
+ };
6951
+ writeConfig(bases, basesPath(path));
6952
+ return true;
6953
+ }
6783
6954
  //#endregion
6784
6955
  //#region packages/server/src/model.ts
6785
6956
  /** Where the server looks for the model, in order, and why each place is there. */
@@ -6821,7 +6992,7 @@ function modelInfo(dir) {
6821
6992
  } catch {}
6822
6993
  const graph = sha(readFileSync(join(dir, "model.onnx"))), granite = graph === FILES[0][2];
6823
6994
  const pooling = own.pooling === "mean" ? "mean" : "cls";
6824
- const name = typeof own.name === "string" && own.name ? own.name : granite ? NAME$1 : basename(dir);
6995
+ const name = typeof own.name === "string" && own.name ? own.name : granite ? NAME$2 : basename(dir);
6825
6996
  return {
6826
6997
  dir,
6827
6998
  name,
@@ -7003,11 +7174,11 @@ async function modelCommand(from, say = console.log, env = process.env, sources
7003
7174
  return 1;
7004
7175
  }
7005
7176
  }
7006
- var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
7177
+ var NAME$2, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
7007
7178
  var init_model = __esmMin((() => {
7008
7179
  init_src();
7009
- NAME$1 = "granite-embedding-small-english-r2";
7010
- MODEL = `${NAME$1}-q8`;
7180
+ NAME$2 = "granite-embedding-small-english-r2";
7181
+ MODEL = `${NAME$2}-q8`;
7011
7182
  RELEASE = "https://github.com/lachlanjs/isorite/releases/download/model-granite-1.0.0/isorite-model-granite-1.0.0.tgz";
7012
7183
  PACKAGE = "isorite-model-granite@1.0.0";
7013
7184
  HUB = `https://huggingface.co/onnx-community/granite-embedding-small-english-r2-ONNX/resolve/1dc7835ba0cb9c76a3618d0bf0c427c97671b3c8`;
@@ -7035,7 +7206,7 @@ var init_model = __esmMin((() => {
7035
7206
  ];
7036
7207
  sha = (buf) => createHash("sha256").update(buf).digest("hex");
7037
7208
  userModelsDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models");
7038
- userModelDir = (env = process.env) => join(userModelsDir(env), NAME$1);
7209
+ userModelDir = (env = process.env) => join(userModelsDir(env), NAME$2);
7039
7210
  hasModel = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
7040
7211
  ModelFetchError = class extends Error {
7041
7212
  tried;
@@ -7223,7 +7394,132 @@ function relay(live, key) {
7223
7394
  };
7224
7395
  }
7225
7396
  //#endregion
7397
+ //#region packages/server/src/bases.ts
7398
+ const NAME$1 = /^[a-z0-9][a-z0-9-]{0,39}$/;
7399
+ var Bases = class {
7400
+ first;
7401
+ #config;
7402
+ #make;
7403
+ #open = /* @__PURE__ */ new Map();
7404
+ #ready = /* @__PURE__ */ new Map();
7405
+ #about = /* @__PURE__ */ new Map();
7406
+ #holds = 0;
7407
+ #idle;
7408
+ /** `make` opens a base; `idle` is called when the last hold is let go (a server isorite term started). */
7409
+ constructor({ config, first, make, idle }) {
7410
+ this.#config = config;
7411
+ this.first = first;
7412
+ this.#make = make;
7413
+ this.#idle = idle;
7414
+ }
7415
+ /** The first base, and every lent one. */
7416
+ async start() {
7417
+ await this.get(this.first);
7418
+ await Promise.all(knownBases(this.#config).filter((b) => b.lend).map((b) => this.get(b.name)?.catch((e) => console.error(`isorite: ${b.name} (${b.path}) could not be opened: ${e.message}`))));
7419
+ }
7420
+ /** A known base, opened if it is not yet; null where none has that name. */
7421
+ get(name) {
7422
+ const had = this.#open.get(name);
7423
+ if (had) return had;
7424
+ const known = NAME$1.test(name) ? knownBases(this.#config).find((b) => b.name === name) : void 0;
7425
+ if (!known) return null;
7426
+ const opening = this.#make(known).then(async (opened) => {
7427
+ this.#ready.set(name, opened);
7428
+ this.#about.set(name, await opened.engine.about());
7429
+ opened.engine.events((e) => {
7430
+ if (e.type === "note-changed") opened.engine.about().then((a) => this.#about.set(name, a));
7431
+ });
7432
+ return opened;
7433
+ });
7434
+ opening.catch(() => this.#open.delete(name));
7435
+ this.#open.set(name, opening);
7436
+ return opening;
7437
+ }
7438
+ /** Every open base, for what is kept into use in all of them (the provider). */
7439
+ each(fn) {
7440
+ for (const o of this.#ready.values()) fn(o.engine);
7441
+ }
7442
+ lent() {
7443
+ return knownBases(this.#config).filter((b) => b.lend && this.#ready.has(b.name)).map((b) => ({
7444
+ name: b.name,
7445
+ about: this.#about.get(b.name) ?? ""
7446
+ }));
7447
+ }
7448
+ base(name) {
7449
+ return this.#ready.get(name)?.engine ?? null;
7450
+ }
7451
+ list() {
7452
+ return knownBases(this.#config).map((b) => ({
7453
+ name: b.name,
7454
+ path: b.path,
7455
+ lend: b.lend,
7456
+ open: this.#ready.has(b.name)
7457
+ }));
7458
+ }
7459
+ async lend(name, on) {
7460
+ if (!lendBase(name, on, this.#config)) return false;
7461
+ if (on) await this.get(name);
7462
+ return true;
7463
+ }
7464
+ async open(path) {
7465
+ const name = rememberBase(resolve(path), { path: this.#config });
7466
+ await this.get(name);
7467
+ return {
7468
+ name,
7469
+ first: name === this.first
7470
+ };
7471
+ }
7472
+ hold() {
7473
+ this.#holds++;
7474
+ let let_go = false;
7475
+ return () => {
7476
+ if (let_go) return;
7477
+ let_go = true;
7478
+ if (--this.#holds === 0) setTimeout(() => {
7479
+ if (this.#holds === 0) this.#idle?.();
7480
+ }, 1e3).unref();
7481
+ };
7482
+ }
7483
+ /** A request to the server: to a base's API by its address, /b/<name>/api/…, or to the first. A base's page is
7484
+ * the app's own page with ?base=<name> (SvelteKit's hash router needs its page at the root it was built for):
7485
+ * /b/<name> is sent there. */
7486
+ async fetch(req, env) {
7487
+ const url = new URL(req.url);
7488
+ const m = /^\/b\/([^/]+)(\/.*)?$/.exec(url.pathname);
7489
+ if (!m || !NAME$1.test(m[1])) return (await this.get(this.first)).fetch(req, env);
7490
+ const name = m[1];
7491
+ if (!m[2]?.startsWith("/api/")) return new Response(null, {
7492
+ status: 302,
7493
+ headers: { Location: `/?base=${name}${m[2]?.startsWith("/#") ? m[2].slice(1) : ""}` }
7494
+ });
7495
+ const opening = this.get(name);
7496
+ if (!opening) return new Response(JSON.stringify({ error: `no base ${name} is known here: isorite bases lists them` }), {
7497
+ status: 404,
7498
+ headers: { "Content-Type": "application/json" }
7499
+ });
7500
+ let opened;
7501
+ try {
7502
+ opened = await opening;
7503
+ } catch (e) {
7504
+ return new Response(JSON.stringify({ error: `${name} could not be opened: ${e.message}` }), {
7505
+ status: 500,
7506
+ headers: { "Content-Type": "application/json" }
7507
+ });
7508
+ }
7509
+ url.pathname = m[2];
7510
+ const body = req.method === "GET" || req.method === "HEAD" ? null : req.body;
7511
+ return opened.fetch(new Request(url, {
7512
+ method: req.method,
7513
+ headers: req.headers,
7514
+ body,
7515
+ signal: req.signal,
7516
+ ...body ? { duplex: "half" } : {}
7517
+ }), env);
7518
+ }
7519
+ };
7520
+ //#endregion
7226
7521
  //#region packages/server/src/terminal.ts
7522
+ init_src();
7227
7523
  const OUTPUT = 4e3;
7228
7524
  const LOOKUPS = /* @__PURE__ */ new Set([
7229
7525
  "search_notes",
@@ -7234,6 +7530,7 @@ const LOOKUPS = /* @__PURE__ */ new Set([
7234
7530
  ]);
7235
7531
  /** isorite's tools that change a note: their line opens it, where the agent's words are marked. */
7236
7532
  const CHANGES = /* @__PURE__ */ new Set(["edit_note", "write_note"]);
7533
+ const isorite = (name) => LOOKUPS.has(name) || CHANGES.has(name);
7237
7534
  /** What a tool call acted on, in a line. */
7238
7535
  function titleOf(name, input = {}) {
7239
7536
  const s = (k) => typeof input[k] === "string" ? input[k] : "";
@@ -7281,16 +7578,21 @@ function toTurns(messages, { live = /* @__PURE__ */ new Map(), ours = /* @__PURE
7281
7578
  const input = c.state.input ?? {};
7282
7579
  const output = (c.state.content ?? []).map((x) => x.text ?? "").join("").slice(0, OUTPUT);
7283
7580
  const status = c.state.status === "completed" ? "done" : /error|fail/.test(c.state.status) ? "failed" : "running";
7581
+ const { base, args } = isorite(c.name) ? baseOf(input) : {
7582
+ base: "",
7583
+ args: input
7584
+ };
7284
7585
  const item = {
7285
7586
  kind: "tool",
7286
7587
  id: c.id,
7287
7588
  name: c.name,
7288
- title: titleOf(c.name, input),
7589
+ title: (base ? `${base}: ` : "") + titleOf(c.name, args),
7289
7590
  status,
7290
7591
  output
7291
7592
  };
7292
- const named = LOOKUPS.has(c.name) ? input.id : CHANGES.has(c.name) ? input.note : void 0;
7593
+ const named = LOOKUPS.has(c.name) ? args.id : CHANGES.has(c.name) ? args.note : void 0;
7293
7594
  if (typeof named === "string" && named) item.note = named.replace(/^\//, "").replace(/\.md$/, "");
7595
+ if (base) item.base = base;
7294
7596
  const diff = diffOf(c.name, input);
7295
7597
  if (diff) item.diff = diff;
7296
7598
  turn.items.push(item);
@@ -7722,7 +8024,7 @@ const AgentStatus = z.object({
7722
8024
  session: z.string().min(1).max(200),
7723
8025
  busy: z.boolean()
7724
8026
  }).strict();
7725
- function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway, setup }) {
8027
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway, setup, bases, base }) {
7726
8028
  const app = new Hono();
7727
8029
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
7728
8030
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -7925,9 +8227,52 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
7925
8227
  post("/api/settings/provider/models", z.object({ draft: SetupDraft.nullable() }).strict(), async (c, b) => ({ models: await setup.models(local(c) ? b.draft : null) }));
7926
8228
  post("/api/settings/report", z.object({}).strict(), async () => ({ text: await setup.report() }));
7927
8229
  }
8230
+ if (bases) {
8231
+ app.get("/api/bases", (c) => answer(c, async () => ({
8232
+ here: base ?? "",
8233
+ local: local(c),
8234
+ bases: bases.list()
8235
+ })));
8236
+ const mine = (c) => {
8237
+ if (!local(c)) throw new EngineError("the bases are changed only from this machine", 403);
8238
+ };
8239
+ post("/api/bases/lend", z.object({
8240
+ name: z.string().max(40),
8241
+ lend: z.boolean()
8242
+ }).strict(), async (c, b) => {
8243
+ mine(c);
8244
+ if (!await bases.lend(b.name, b.lend)) throw new EngineError(`no base ${b.name} is known here`, 404);
8245
+ return { bases: bases.list() };
8246
+ });
8247
+ post("/api/bases/open", z.object({ path: z.string().min(1).max(4e3) }).strict(), async (c, b) => {
8248
+ mine(c);
8249
+ if (!isAbsolute(b.path) || !existsSync(b.path) || !statSync(b.path).isDirectory()) throw new EngineError(`no folder at ${b.path}`, 404);
8250
+ return bases.open(b.path);
8251
+ });
8252
+ app.get("/api/server/hold", (c) => {
8253
+ if (!local(c)) return refuse(c, 403, "held only from this machine");
8254
+ return streamSSE(c, async (stream) => {
8255
+ const release = bases.hold();
8256
+ const beat = setInterval(() => {
8257
+ stream.write(": held\n\n");
8258
+ }, 25e3);
8259
+ await stream.writeSSE({
8260
+ event: "held",
8261
+ data: "{}"
8262
+ });
8263
+ await new Promise((done) => stream.onAbort(done));
8264
+ clearInterval(beat);
8265
+ release();
8266
+ });
8267
+ });
8268
+ }
7928
8269
  app.get("/api/agent/tools", (c) => answer(c, () => api.agentTools()));
7929
8270
  post("/api/agent/look", AgentLook, (_c, b) => api.agentLook(b));
7930
- app.get("/api/agent/base", (c) => answer(c, async () => ({ notes: notes ?? null })));
8271
+ app.get("/api/agent/base", (c) => answer(c, async () => ({
8272
+ notes: notes ?? null,
8273
+ name: base ?? null,
8274
+ lent: (bases?.list() ?? []).filter((b) => b.lend && b.name !== base).map((b) => b.name)
8275
+ })));
7931
8276
  post("/api/agent/write", AgentChange, (_c, b) => api.agentWrite(b));
7932
8277
  post("/api/agent/send", z.object({
7933
8278
  note: z.string().max(400),
@@ -8368,7 +8713,7 @@ var Setup = class {
8368
8713
  };
8369
8714
  //#endregion
8370
8715
  //#region packages/server/src/version.ts
8371
- const VERSION = "0.7.0";
8716
+ const VERSION = "0.8.0";
8372
8717
  //#endregion
8373
8718
  //#region packages/server/src/studybase.ts
8374
8719
  init_src$1();
@@ -8439,15 +8784,16 @@ function newBase(path) {
8439
8784
  makeOwn(at);
8440
8785
  return at;
8441
8786
  }
8442
- async function openEngine({ notes }, device = deviceId(), config = configPath(), http, model = modelDir(), secrets = new ConfigSecrets(config)) {
8787
+ async function openEngine({ notes }, device = deviceId(), config = configPath(), http, model = modelDir(), secrets = new ConfigSecrets(config), extras = {}) {
8443
8788
  const provider = providerConfig(config);
8444
8789
  const own = makeOwn(notes);
8790
+ const embedder = extras.embedder !== void 0 ? extras.embedder : model ? nodeEmbedder(model) : null;
8445
8791
  return Engine.open({
8446
8792
  notes: new NodeFiles(notes),
8447
8793
  own: new NodeFiles(own),
8448
8794
  clock: systemClock,
8449
8795
  device,
8450
- name: basename(notes),
8796
+ name: extras.name ?? basename(notes),
8451
8797
  http: http ?? gatewayHttp(provider, gatewayTls(config)),
8452
8798
  secrets,
8453
8799
  provider,
@@ -8455,13 +8801,14 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
8455
8801
  const dir = resolve(notes, path);
8456
8802
  return existsSync(dir) && statSync(dir).isDirectory() ? new NodeFiles(dir) : null;
8457
8803
  },
8458
- ...model ? {
8459
- embedder: nodeEmbedder(model),
8804
+ ...embedder ? {
8805
+ embedder,
8460
8806
  cache: {
8461
8807
  files: new NodeFiles(cacheDir()),
8462
8808
  name: `vectors-${createHash("sha256").update(notes).digest("hex").slice(0, 16)}.json`
8463
8809
  }
8464
- } : {}
8810
+ } : {},
8811
+ ...extras.elsewhere ? { elsewhere: extras.elsewhere } : {}
8465
8812
  });
8466
8813
  }
8467
8814
  /** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
@@ -8486,16 +8833,49 @@ async function liveProvider(config, stand) {
8486
8833
  }
8487
8834
  /** A link most terminals let you click (OSC 8), when the output is a terminal; the address as it is otherwise. */
8488
8835
  const link$1 = (url, tty = Boolean(process.stdout.isTTY)) => tty ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url;
8489
- /** `stand`: in place of the provider, an answerer of its own, as the guide's finder is when no model is set up
8490
- * (M16). It is asked while there is no model here (no key, no provider of your own), and needs no key. */
8491
- async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand } = {}) {
8836
+ /** The server's bases, opened: the one named (remembered in bases.toml, M20) and every lent one, each with its
8837
+ * engine and app, and the server's answer to a request for any of them. `stand`: in place of the provider, an
8838
+ * answerer of its own, as the guide's finder is when no model is set up (M16), asked while there is no model here.
8839
+ * `idle`: what to do once the last of those holding the server lets it go. */
8840
+ async function openServer(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand, idle, config = configPath(), model = modelDir() } = {}) {
8492
8841
  const opened = locate(path);
8493
- const config = configPath();
8494
8842
  const current = () => liveProvider(config, stand);
8495
8843
  let live = await current();
8496
- const secrets = new ConfigSecrets(config);
8497
- const api = await openEngine(opened, deviceId(config), config, live.http, modelDir(), secrets);
8498
- api.setProvider(live.provider, live.http);
8844
+ const secrets = new ConfigSecrets(config), device = deviceId(config);
8845
+ const first = rememberBase(opened.notes, { path: config });
8846
+ const embedder = model ? nodeEmbedder(model) : null;
8847
+ const token = randomBytes(24).toString("base64url"), loopback = LOOPBACK.has(host);
8848
+ const gateway = loopback ? relay(() => live, () => secrets.get(KEY)) : void 0;
8849
+ const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
8850
+ const bases = new Bases({
8851
+ config,
8852
+ first,
8853
+ ...idle ? { idle } : {},
8854
+ make: async (known) => {
8855
+ const api = await openEngine({ notes: known.path }, device, config, live.http, model, secrets, {
8856
+ name: known.name,
8857
+ elsewhere: bases,
8858
+ embedder
8859
+ });
8860
+ api.setProvider(live.provider, live.http);
8861
+ return {
8862
+ engine: api,
8863
+ fetch: createApp({
8864
+ api,
8865
+ token,
8866
+ loopback,
8867
+ web,
8868
+ allowHosts,
8869
+ notes: known.path,
8870
+ terminal: new Terminal(),
8871
+ gateway,
8872
+ setup,
8873
+ bases,
8874
+ base: known.name
8875
+ }).fetch
8876
+ };
8877
+ }
8878
+ });
8499
8879
  const setup = new Setup({
8500
8880
  config,
8501
8881
  notes: opened.notes,
@@ -8503,25 +8883,29 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
8503
8883
  port,
8504
8884
  kept: async () => {
8505
8885
  live = await current();
8506
- api.setProvider(live.provider, live.http);
8886
+ bases.each((api) => api.setProvider(live.provider, live.http));
8507
8887
  }
8508
8888
  });
8509
- const gateway = LOOPBACK.has(host) ? relay(() => live, () => secrets.get(KEY)) : void 0;
8510
- const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
8511
- const terminal = new Terminal();
8512
- const app = createApp({
8513
- api,
8514
- token: randomBytes(24).toString("base64url"),
8515
- loopback: LOOPBACK.has(host),
8516
- web,
8517
- allowHosts,
8889
+ await bases.start();
8890
+ return {
8518
8891
  notes: opened.notes,
8519
- terminal,
8520
- gateway,
8521
- setup
8892
+ bases,
8893
+ embedder: Boolean(embedder),
8894
+ fetch: (req, env) => bases.fetch(req, env)
8895
+ };
8896
+ }
8897
+ /** `held`: started by isorite term or mcp, it stops once the last of them lets it go. */
8898
+ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand, held = false } = {}) {
8899
+ let stop = () => void 0;
8900
+ const { notes, bases, embedder, fetch } = await openServer(path, {
8901
+ host,
8902
+ port,
8903
+ allowHosts,
8904
+ ...stand ? { stand } : {},
8905
+ ...held ? { idle: () => stop() } : {}
8522
8906
  });
8523
8907
  const server = serve({
8524
- fetch: app.fetch,
8908
+ fetch,
8525
8909
  hostname: host,
8526
8910
  port
8527
8911
  });
@@ -8529,11 +8913,13 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
8529
8913
  console.error(e.code === "EADDRINUSE" ? `port ${port} is in use (isorite serve --port <n>)` : e.message);
8530
8914
  process.exit(1);
8531
8915
  });
8532
- console.log(`isorite: ${opened.notes}`);
8533
- console.log(api.meaning ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model)");
8534
- console.log(web ? ` ${link$1(`http://localhost:${port}/`)}` : ` the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
8916
+ console.log(`isorite: ${notes}`);
8917
+ console.log(embedder ? " search by meaning: on" : " search by meaning: off (isorite model fetches its model)");
8918
+ const lent = bases.lent().filter((l) => l.name !== bases.first).map((l) => l.name);
8919
+ if (lent.length) console.log(` lent to every session: ${lent.join(", ")} (isorite bases)`);
8920
+ console.log(existsSync(join(WEB_DIR, "index.html")) ? ` ${link$1(`http://localhost:${port}/`)}` : ` the app is not built (npm run build); the engine alone is at http://localhost:${port}/api/`);
8535
8921
  if (process.env.SSH_CONNECTION && LOOPBACK.has(host)) console.log(` over SSH, the link opens on this machine; from your own, first run: ssh -L ${port}:localhost:${port} ${hostname()}`);
8536
- const stop = () => {
8922
+ stop = () => {
8537
8923
  server.close();
8538
8924
  process.exit(0);
8539
8925
  };
@@ -8569,49 +8955,77 @@ async function serving(url) {
8569
8955
  return null;
8570
8956
  }
8571
8957
  }
8572
- /** The isorite server on the port, and how to let it go: one already running for this base (or for any, when
8573
- * no base is named), or one started for the base, its output kept in a log, stopped when let go. */
8958
+ /** The isorite server on the port, and how to let it go: one already running (for this base, or holding it as
8959
+ * well from now on, M20; or for any, when no base is named), or one started for the base, its output kept in a
8960
+ * log. `url` is the base's own address there. A server this started stops once the last of those using it lets it
8961
+ * go; one you started yourself goes on. */
8574
8962
  async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4 } = {}) {
8575
- const url = `http://127.0.0.1:${port}`;
8963
+ const root = `http://127.0.0.1:${port}`;
8576
8964
  const opened = path === void 0 ? null : locate(path);
8577
- const name = await serving(url);
8578
- if (name !== null) {
8579
- if (opened && name !== basename(opened.notes)) throw new StudyError(`port ${port} is serving ${name}, not ${basename(opened.notes)}: stop it, or use another port (--port <n>)`);
8580
- return {
8581
- url,
8582
- release: () => void 0
8583
- };
8584
- }
8585
- if (!opened) throw new StudyError(`nothing is serving on port ${port}: name a base`);
8586
- const log = join(tmpdir(), `isorite-term-${port}.log`);
8587
- const out = openSync(log, "w");
8588
- const server = spawn(process.execPath, [
8589
- MAIN,
8590
- "serve",
8591
- path,
8592
- "--port",
8593
- String(port)
8594
- ], {
8595
- stdio: [
8596
- "ignore",
8597
- out,
8598
- out
8599
- ],
8600
- env
8601
- });
8602
- const exited = new Promise((done) => server.once("exit", () => done()));
8603
- const started = Date.now();
8604
- while (await serving(url) === null) {
8605
- if (server.exitCode !== null || Date.now() - started > wait) {
8606
- server.kill();
8607
- throw new StudyError(`the isorite server did not start; its output is in ${log}`);
8965
+ let name = await serving(root), started = null;
8966
+ if (name === null) {
8967
+ if (!opened) throw new StudyError(`nothing is serving on port ${port}: name a base`);
8968
+ const log = join(tmpdir(), `isorite-term-${port}.log`);
8969
+ const out = openSync(log, "w");
8970
+ started = spawn(process.execPath, [
8971
+ MAIN,
8972
+ "serve",
8973
+ path,
8974
+ "--port",
8975
+ String(port),
8976
+ "--held"
8977
+ ], {
8978
+ stdio: [
8979
+ "ignore",
8980
+ out,
8981
+ out
8982
+ ],
8983
+ env,
8984
+ detached: true
8985
+ });
8986
+ const exited = new Promise((done) => started.once("exit", () => done()));
8987
+ const begun = Date.now();
8988
+ while ((name = await serving(root)) === null) {
8989
+ if (started.exitCode !== null || Date.now() - begun > wait) {
8990
+ started.kill();
8991
+ throw new StudyError(`the isorite server did not start; its output is in ${log}`);
8992
+ }
8993
+ await Promise.race([exited, new Promise((r) => setTimeout(r, 250))]);
8608
8994
  }
8609
- await Promise.race([exited, new Promise((r) => setTimeout(r, 250))]);
8995
+ started.unref();
8610
8996
  }
8997
+ let url = root;
8998
+ if (opened) {
8999
+ const at = await fetch(`${root}/api/bases/open`, {
9000
+ method: "POST",
9001
+ headers: {
9002
+ "Content-Type": "application/json",
9003
+ "x-isorite-token": await tokenOf(root),
9004
+ Origin: root
9005
+ },
9006
+ body: JSON.stringify({ path: opened.notes })
9007
+ }).catch(() => null);
9008
+ if (at?.ok) {
9009
+ const base = await at.json();
9010
+ if (!base.first) url = `${root}/b/${base.name}`;
9011
+ } else if (name !== basename(opened.notes)) throw new StudyError(`port ${port} is serving ${name}, not ${basename(opened.notes)}: stop it, or use another port (--port <n>)`);
9012
+ }
9013
+ const holding = new AbortController();
9014
+ const held = fetch(`${root}/api/server/hold`, { signal: holding.signal }).then((r) => {
9015
+ if (r.ok && r.body) (async () => {
9016
+ try {
9017
+ for await (const _ of r.body);
9018
+ } catch {}
9019
+ })();
9020
+ return r.ok;
9021
+ }, () => false);
8611
9022
  return {
8612
9023
  url,
8613
9024
  release: () => {
8614
- server.kill();
9025
+ holding.abort();
9026
+ held.then((ok) => {
9027
+ if (!ok && started) started.kill();
9028
+ });
8615
9029
  }
8616
9030
  };
8617
9031
  }
@@ -8632,7 +9046,7 @@ async function tell(url, path, body) {
8632
9046
  headers: {
8633
9047
  "Content-Type": "application/json",
8634
9048
  "x-isorite-token": token,
8635
- Origin: url
9049
+ Origin: new URL(url).origin
8636
9050
  },
8637
9051
  body: JSON.stringify(body)
8638
9052
  });
@@ -8723,9 +9137,9 @@ async function modelsFor(env, server) {
8723
9137
  }
8724
9138
  /** What `isorite term` says before OpenCode takes the screen, and again when it gives it back: where isorite is in
8725
9139
  * the browser, which models the terminal uses and where they come from, and, over SSH, how to reach the browser. */
8726
- function where(port, models, env, tty) {
9140
+ function where(port, models, env, tty, page = "/") {
8727
9141
  const names = models.gateway?.models.join(", ");
8728
- const lines = [`isorite: ${link$1(`http://localhost:${port}/`, tty)}`, models.from === "environment" ? ` the terminal's models: ${names}, at ${models.gateway.url} (ISORITE_GATEWAY_URL)` : models.from === "config" ? ` the terminal's models: ${names}, as config.toml names them, through the isorite server` : " the terminal's models: OpenCode's own, as no key or certificate is named in config.toml"];
9142
+ const lines = [`isorite: ${link$1(`http://localhost:${port}${page}`, tty)}`, models.from === "environment" ? ` the terminal's models: ${names}, at ${models.gateway.url} (ISORITE_GATEWAY_URL)` : models.from === "config" ? ` the terminal's models: ${names}, as config.toml names them, through the isorite server` : " the terminal's models: OpenCode's own, as no key or certificate is named in config.toml"];
8729
9143
  if (env.SSH_CONNECTION) lines.push(` over SSH, the link opens on this machine; from your own, first run: ssh -L ${port}:localhost:${port} ${hostname()}`);
8730
9144
  return lines;
8731
9145
  }
@@ -8752,7 +9166,7 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
8752
9166
  throw e;
8753
9167
  }
8754
9168
  const { gateway, env } = models;
8755
- const said = where(port, models, given);
9169
+ const said = where(port, models, given, void 0, new URL(server.url).pathname.replace(/\/?$/, "/"));
8756
9170
  for (const line of said) console.log(line);
8757
9171
  const agents = path === void 0 ? null : join(locate(path).notes, AGENTS_FILE);
8758
9172
  const home = writeHome(defaultHome(env), {
@@ -9225,7 +9639,7 @@ async function mcp(path, { port = 4767, agent = "mcp", quiet = QUIET_MS, input =
9225
9639
  headers: {
9226
9640
  "Content-Type": "application/json",
9227
9641
  [TOKEN_HEADER]: await token,
9228
- Origin: url
9642
+ Origin: new URL(url).origin
9229
9643
  },
9230
9644
  body: JSON.stringify(body)
9231
9645
  });
@@ -9255,7 +9669,7 @@ async function mcp(path, { port = 4767, agent = "mcp", quiet = QUIET_MS, input =
9255
9669
  name: "isorite",
9256
9670
  version: "0.1.0"
9257
9671
  },
9258
- instructions: "A knowledge base of notes. Call brief_base first, then look things up with search_notes, outline_note and read_note, following a note's links before searching again. Cite a note by its title and section."
9672
+ instructions: "A knowledge base of notes. Call brief_base first, then look things up with search_notes, outline_note and read_note, following a note's links before searching again. Cite a note by its title and section. Other knowledge bases lent to you (the agent's memory among them) are named by brief_base: pass base to the same tools to look or write there, and follow the practice each one's brief gives."
9259
9673
  };
9260
9674
  if (rpc.method === "ping") return {};
9261
9675
  if (rpc.method === "tools/list") return { tools: (await get("/api/agent/tools")).map(({ function: t }) => ({
@@ -10491,6 +10905,98 @@ async function writeGeometry(folder, now = Date.now()) {
10491
10905
  return { notes };
10492
10906
  }
10493
10907
  //#endregion
10908
+ //#region packages/server/src/memory.ts
10909
+ init_src$1();
10910
+ const MEMORY_FOLDER = join(homedir(), "isorite-memory");
10911
+ /** The memory's practice, as the agent is to follow it: read with brief_base, whichever base a session is for. */
10912
+ const MEMORY_AGENTS = `# What you keep across projects: the person, their tools, and what you have learned working with them.
10913
+
10914
+ This knowledge base is your memory. It is lent to every session, whatever project the session is for. Look in it,
10915
+ and keep it, with the knowledge base tools, giving \`base: "memory"\`.
10916
+
10917
+ ## When to look
10918
+
10919
+ - At the start of a task: search for the person, the project's tools and the kind of work.
10920
+ - Before asking the person something they may have told you before.
10921
+
10922
+ ## What to keep
10923
+
10924
+ Keep what will be useful in another project, or in a later session of this one:
10925
+ - **you/**: how the person likes to work, what they have corrected you on or confirmed, and why.
10926
+ - **tools/**: tools, services and machines, and how they are set up and reached.
10927
+ - **practice/**: ways of working that went well or badly, and mistakes not to repeat.
10928
+ - **projects/**: what is going on in each project that its own files don't say: goals, decisions, constraints,
10929
+ with dates as dates.
10930
+
10931
+ Don't keep what a project's own files, notes or history already say, what matters only to this conversation, or
10932
+ anything secret: keys, passwords, tokens.
10933
+
10934
+ ## How to keep it
10935
+
10936
+ - One note for each thing worth keeping: its title says it, its first lines give the fact, then **Why:** and
10937
+ **When it applies:**.
10938
+ - Link it to the notes it touches.
10939
+ - Update the note that is there rather than writing a second one, and delete a note that turns out to be wrong.
10940
+ - Write as you learn, not at the end: the person sees each note you write marked as yours, and can undo it.
10941
+ `;
10942
+ /** Make the memory, or say where it is: the folder (as isorite new makes it), a git repository of its own, its
10943
+ * practice, and its row in bases.toml, lent. */
10944
+ function memory(folder = MEMORY_FOLDER, { config = configPath(), out = console.log } = {}) {
10945
+ const at = resolve(folder), known = knownBases(config);
10946
+ const had = known.find((b) => b.path === at) ?? (folder === MEMORY_FOLDER ? known.find((b) => b.name === "memory") : void 0);
10947
+ if (had && existsSync(join(had.path, "AGENTS.md"))) {
10948
+ out(`the memory is ${had.name}, at ${had.path}: ${had.lend ? "lent to every session" : `not lent (isorite bases lend ${had.name})`}`);
10949
+ return 0;
10950
+ }
10951
+ if (!existsSync(join(at, "AGENTS.md"))) {
10952
+ if (existsSync(at) && readdirSync(at).some((f) => f !== ".isorite" && f !== ".git")) throw new StudyError(`${at} has files in it already, and no ${AGENTS_FILE}: name another folder, isorite memory <folder>`);
10953
+ newBase(at);
10954
+ writeFileSync(join(at, AGENTS_FILE), MEMORY_AGENTS);
10955
+ try {
10956
+ if (!existsSync(join(at, ".git"))) execFileSync("git", ["init", "--quiet"], {
10957
+ cwd: at,
10958
+ stdio: "ignore"
10959
+ });
10960
+ } catch {
10961
+ out(" (git is not here: the memory is not a repository yet)");
10962
+ }
10963
+ out(`isorite memory: made at ${at}, a private repository of its own: push it where you like`);
10964
+ }
10965
+ const name = rememberBase(at, {
10966
+ path: config,
10967
+ as: "memory",
10968
+ lend: true
10969
+ });
10970
+ out(` ${name}: lent to every session's agent and Iso, as listed in ${basesPath(config)}`);
10971
+ out(` watch it learn: isorite serve ${at}, or switch to ${name} in the page of a running server`);
10972
+ return 0;
10973
+ }
10974
+ /** isorite bases: the bases this device knows; `lend <name>` and `keep <name>` (lend it no longer). */
10975
+ function bases(args, { config = configPath(), out = console.log } = {}) {
10976
+ const [verb, name] = args;
10977
+ if (verb === "lend" || verb === "keep") {
10978
+ const known = knownBases(config).find((b) => b.name === name);
10979
+ if (!known) {
10980
+ out(`no base ${name ?? ""} is known: isorite bases lists them`);
10981
+ return 2;
10982
+ }
10983
+ rememberBase(known.path, {
10984
+ path: config,
10985
+ lend: verb === "lend"
10986
+ });
10987
+ out(`${known.name}: ${verb === "lend" ? "lent to every session" : "not lent"}`);
10988
+ return 0;
10989
+ }
10990
+ const known = knownBases(config);
10991
+ if (!known.length) {
10992
+ out("no bases yet: each one you open is listed here (isorite serve, term, mcp); isorite memory makes the agent's memory");
10993
+ return 0;
10994
+ }
10995
+ const width = Math.max(...known.map((b) => b.name.length));
10996
+ for (const b of known) out(`${b.name.padEnd(width)} ${b.lend ? "lent " : " "} ${b.path}${existsSync(b.path) ? "" : " (not there)"}`);
10997
+ return 0;
10998
+ }
10999
+ //#endregion
10494
11000
  //#region packages/server/src/main.ts
10495
11001
  init_src$1();
10496
11002
  init_model();
@@ -10519,6 +11025,10 @@ const USAGE = `isorite: an agentic harness for understanding
10519
11025
  provider (asked a few words, a few hundred tokens), the terminal and
10520
11026
  the notes; --report gives it as one block, secrets hidden, to paste
10521
11027
  to someone (or an AI) helping
11028
+ isorite memory [<folder>] the agent's memory: a knowledge base (~/isorite-memory unless named),
11029
+ a git repository of its own, lent to every session's agent and Iso
11030
+ isorite bases [lend|keep <name>] the bases this device knows, each listed as it is opened; lend one to
11031
+ every session, or keep it to itself
10522
11032
  isorite help setup how isorite is set up, every setting and failure: written for an AI
10523
11033
  helping you set it up (docs/setup.md is for people)
10524
11034
  isorite guide [<folder>] [--port <n>] the guide: a knowledge base about isorite, with things to do in it,
@@ -10557,7 +11067,8 @@ function main(argv) {
10557
11067
  const allowHosts = rest.flatMap((a, i) => a === "--allow-host" && rest[i + 1] ? [rest[i + 1]] : []);
10558
11068
  return serve$1(rest[0], {
10559
11069
  ...port === void 0 ? {} : { port },
10560
- allowHosts
11070
+ allowHosts,
11071
+ held: rest.includes("--held")
10561
11072
  }).then(() => new Promise(() => void 0), (e) => {
10562
11073
  if (e instanceof StudyError) {
10563
11074
  console.error(e.message);
@@ -10613,6 +11124,8 @@ function main(argv) {
10613
11124
  }
10614
11125
  return modelCommand(rest.includes("--from") ? from : void 0);
10615
11126
  }
11127
+ if (command === "memory") return memory(rest.find((a) => !a.startsWith("--")));
11128
+ if (command === "bases") return bases(rest);
10616
11129
  if (command === "--version" || command === "-v") {
10617
11130
  console.log(VERSION);
10618
11131
  return 0;