isorite 0.5.0 → 0.7.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 (226) hide show
  1. package/README.md +4 -3
  2. package/dist/main.js +466 -99
  3. package/docs/setup-for-agents.md +5 -1
  4. package/docs/setup.md +18 -4
  5. package/package.json +1 -1
  6. package/plugin/server.ts +1 -1
  7. package/web/_app/immutable/assets/0.BKnIhFWM.css +1 -0
  8. package/web/_app/immutable/assets/NoteEditor.8wWtZK_A.css +1 -0
  9. package/web/_app/immutable/chunks/0M1khDs2.js +2 -0
  10. package/web/_app/immutable/chunks/2b4Gb5sE.js +1 -0
  11. package/web/_app/immutable/chunks/62dhNtvj.js +673 -0
  12. package/web/_app/immutable/chunks/84nbDH0I.js +1 -0
  13. package/web/_app/immutable/chunks/8H7HVH4w.js +1 -0
  14. package/web/_app/immutable/chunks/8wVGBFSs.js +45 -0
  15. package/web/_app/immutable/chunks/AF4Hwhju.js +1 -0
  16. package/web/_app/immutable/chunks/{C1xA2mdF.js → B3dCo0od.js} +1 -1
  17. package/web/_app/immutable/chunks/B45xgp2S.js +1 -0
  18. package/web/_app/immutable/chunks/B78wSQJQ.js +1 -0
  19. package/web/_app/immutable/chunks/B7GA1FFB.js +1 -0
  20. package/web/_app/immutable/chunks/B85D_PlV.js +5 -0
  21. package/web/_app/immutable/chunks/B9NhS7L_.js +1 -0
  22. package/web/_app/immutable/chunks/B9YLdnR_.js +1 -0
  23. package/web/_app/immutable/chunks/BEHa2uNW.js +1 -0
  24. package/web/_app/immutable/chunks/BEMVcqjs.js +1 -0
  25. package/web/_app/immutable/chunks/BEjRio4Q.js +1 -0
  26. package/web/_app/immutable/chunks/BEn0Cwpx.js +1 -0
  27. package/web/_app/immutable/chunks/BGbXxNFa.js +2 -0
  28. package/web/_app/immutable/chunks/BGfruO1u.js +1 -0
  29. package/web/_app/immutable/chunks/BHEdbvp9.js +1 -0
  30. package/web/_app/immutable/chunks/BKJx4mf9.js +1 -0
  31. package/web/_app/immutable/chunks/BL3mLym4.js +5 -0
  32. package/web/_app/immutable/chunks/BLp5-jwC.js +1 -0
  33. package/web/_app/immutable/chunks/BN6xhG8e.js +1 -0
  34. package/web/_app/immutable/chunks/BNjH_DUx.js +1 -0
  35. package/web/_app/immutable/chunks/BOVgKnng.js +1 -0
  36. package/web/_app/immutable/chunks/BPTmHmyx.js +1 -0
  37. package/web/_app/immutable/chunks/BPW39P52.js +1 -0
  38. package/web/_app/immutable/chunks/BRKS-CCX.js +5 -0
  39. package/web/_app/immutable/chunks/BRsoo5mP.js +1 -0
  40. package/web/_app/immutable/chunks/BSzj6Xyz.js +1 -0
  41. package/web/_app/immutable/chunks/BUZw3wgd.js +1 -0
  42. package/web/_app/immutable/chunks/BV0c4Hdn.js +1 -0
  43. package/web/_app/immutable/chunks/BVIeWmwY.js +1 -0
  44. package/web/_app/immutable/chunks/BWonG38x.js +1 -0
  45. package/web/_app/immutable/chunks/BXxG0Yva.js +1 -0
  46. package/web/_app/immutable/chunks/BZmnMkFw.js +1 -0
  47. package/web/_app/immutable/chunks/BZyAS7I4.js +1 -0
  48. package/web/_app/immutable/chunks/B_1Jd8HM.js +1 -0
  49. package/web/_app/immutable/chunks/Bbt0DTmZ.js +1 -0
  50. package/web/_app/immutable/chunks/Bffqt7tx.js +9 -0
  51. package/web/_app/immutable/chunks/BflJGmDX.js +1 -0
  52. package/web/_app/immutable/chunks/BgczuZKA.js +4 -0
  53. package/web/_app/immutable/chunks/BguXRgvc.js +2 -0
  54. package/web/_app/immutable/chunks/BkSkiI9m.js +1 -0
  55. package/web/_app/immutable/chunks/BoNMG-YE.js +1 -0
  56. package/web/_app/immutable/chunks/Bq5hCPe1.js +2 -0
  57. package/web/_app/immutable/chunks/BrGmYbJT.js +2 -0
  58. package/web/_app/immutable/chunks/BrqJKzQU.js +2 -0
  59. package/web/_app/immutable/chunks/Brxvm_b1.js +4 -0
  60. package/web/_app/immutable/chunks/BsLNwr5A.js +2 -0
  61. package/web/_app/immutable/chunks/Bsdphhtm.js +1 -0
  62. package/web/_app/immutable/chunks/Bt6PBEDR.js +1 -0
  63. package/web/_app/immutable/chunks/BthdM_4N.js +1 -0
  64. package/web/_app/immutable/chunks/Bu8mWwtz.js +1 -0
  65. package/web/_app/immutable/chunks/BunrC_8P.js +4 -0
  66. package/web/_app/immutable/chunks/BwTct_5T.js +1 -0
  67. package/web/_app/immutable/chunks/BxTFtVqu.js +13 -0
  68. package/web/_app/immutable/chunks/BzI3yqBQ.js +3 -0
  69. package/web/_app/immutable/chunks/C-dfcq9W.js +7 -0
  70. package/web/_app/immutable/chunks/C2OyD2Y6.js +1 -0
  71. package/web/_app/immutable/chunks/C2dAgh7P.js +1 -0
  72. package/web/_app/immutable/chunks/C357ku5U.js +1 -0
  73. package/web/_app/immutable/chunks/C4jVoHG1.js +1 -0
  74. package/web/_app/immutable/chunks/C6Btt2Xs.js +1 -0
  75. package/web/_app/immutable/chunks/C9zpzAZU.js +1 -0
  76. package/web/_app/immutable/chunks/CDAjAYjS.js +1 -0
  77. package/web/_app/immutable/chunks/CETSRtnV.js +2 -0
  78. package/web/_app/immutable/chunks/CEufJyoj.js +1 -0
  79. package/web/_app/immutable/chunks/CHbKgHy3.js +1 -0
  80. package/web/_app/immutable/chunks/CKSRTWxH.js +2 -0
  81. package/web/_app/immutable/chunks/CLVUDRRk.js +2 -0
  82. package/web/_app/immutable/chunks/CLVgA2GD.js +1 -0
  83. package/web/_app/immutable/chunks/CO2Y5y64.js +2 -0
  84. package/web/_app/immutable/chunks/CPF5uNy5.js +1 -0
  85. package/web/_app/immutable/chunks/CQadubS9.js +2 -0
  86. package/web/_app/immutable/chunks/CQfgDRfP.js +1 -0
  87. package/web/_app/immutable/chunks/CRIvE37b.js +2 -0
  88. package/web/_app/immutable/chunks/CRzLMJPS.js +1 -0
  89. package/web/_app/immutable/chunks/CTIncYxj.js +1 -0
  90. package/web/_app/immutable/chunks/CTscJvp6.js +2 -0
  91. package/web/_app/immutable/chunks/CUpZ7dEl.js +1 -0
  92. package/web/_app/immutable/chunks/CVbgxz8Y.js +1 -0
  93. package/web/_app/immutable/chunks/CWmfjLLv.js +1 -0
  94. package/web/_app/immutable/chunks/CX-xckzu.js +4 -0
  95. package/web/_app/immutable/chunks/CXXgtw0p.js +1 -0
  96. package/web/_app/immutable/chunks/CYNZ4aV_.js +1 -0
  97. package/web/_app/immutable/chunks/CZ8wTb_M.js +8 -0
  98. package/web/_app/immutable/chunks/C_eisQCp.js +1 -0
  99. package/web/_app/immutable/chunks/CdY1YAit.js +2 -0
  100. package/web/_app/immutable/chunks/Ce_hS451.js +1 -0
  101. package/web/_app/immutable/chunks/CgA1WoRv.js +1 -0
  102. package/web/_app/immutable/chunks/CggtIYAM.js +1 -0
  103. package/web/_app/immutable/chunks/ChfNwlCY.js +1 -0
  104. package/web/_app/immutable/chunks/ChtP43wD.js +1 -0
  105. package/web/_app/immutable/chunks/CiVjR8gq.js +3 -0
  106. package/web/_app/immutable/chunks/CjI9Y6RI.js +1 -0
  107. package/web/_app/immutable/chunks/ClOhbFR7.js +1 -0
  108. package/web/_app/immutable/chunks/Cn-vtDpV.js +1 -0
  109. package/web/_app/immutable/chunks/CnaYoQib.js +1 -0
  110. package/web/_app/immutable/chunks/Co3PbHdE.js +1 -0
  111. package/web/_app/immutable/chunks/CoyQYage.js +1 -0
  112. package/web/_app/immutable/chunks/Cp5ooBfH.js +1 -0
  113. package/web/_app/immutable/chunks/Cp_JbuGh.js +2 -0
  114. package/web/_app/immutable/chunks/Cq5C893B.js +1 -0
  115. package/web/_app/immutable/chunks/CqHI_q-D.js +1 -0
  116. package/web/_app/immutable/chunks/CqftRsjg.js +1 -0
  117. package/web/_app/immutable/chunks/Cshzl8qu.js +1 -0
  118. package/web/_app/immutable/chunks/Cx3rYX2g.js +1 -0
  119. package/web/_app/immutable/chunks/CxDmU1I4.js +1 -0
  120. package/web/_app/immutable/chunks/CygcOYNo.js +1 -0
  121. package/web/_app/immutable/chunks/CzMBulkp.js +1 -0
  122. package/web/_app/immutable/chunks/D1ij_kNL.js +1 -0
  123. package/web/_app/immutable/chunks/D2Q8cUp1.js +1 -0
  124. package/web/_app/immutable/chunks/D4SKbj9K.js +6 -0
  125. package/web/_app/immutable/chunks/D4c0_ac3.js +1 -0
  126. package/web/_app/immutable/chunks/D4fLEcOQ.js +8 -0
  127. package/web/_app/immutable/chunks/D5EjA2JK.js +1 -0
  128. package/web/_app/immutable/chunks/D5jiJvOU.js +1 -0
  129. package/web/_app/immutable/chunks/D6QVYUu9.js +1 -0
  130. package/web/_app/immutable/chunks/D6RzAl64.js +1 -0
  131. package/web/_app/immutable/chunks/D6Vzg2HD.js +6 -0
  132. package/web/_app/immutable/chunks/D8WpQPaX.js +1 -0
  133. package/web/_app/immutable/chunks/D9f6rSsU.js +1 -0
  134. package/web/_app/immutable/chunks/D9qYN0V5.js +1 -0
  135. package/web/_app/immutable/chunks/DCl_8MCW.js +1 -0
  136. package/web/_app/immutable/chunks/DECNRCf2.js +1 -0
  137. package/web/_app/immutable/chunks/DHLQUbN7.js +1 -0
  138. package/web/_app/immutable/chunks/DHfKGggc.js +1 -0
  139. package/web/_app/immutable/chunks/DIGvaVgJ.js +2 -0
  140. package/web/_app/immutable/chunks/DKB6_1j6.js +1 -0
  141. package/web/_app/immutable/chunks/DKNOMI4Y.js +1 -0
  142. package/web/_app/immutable/chunks/DLa7NjNO.js +1 -0
  143. package/web/_app/immutable/chunks/DMAYXnpq.js +13 -0
  144. package/web/_app/immutable/chunks/DNzd62Q_.js +1 -0
  145. package/web/_app/immutable/chunks/DPCEkfD_.js +1 -0
  146. package/web/_app/immutable/chunks/DTF72YNO.js +1 -0
  147. package/web/_app/immutable/chunks/DTdlqvEb.js +12 -0
  148. package/web/_app/immutable/chunks/DTszW0OQ.js +1 -0
  149. package/web/_app/immutable/chunks/DWkQyNnU.js +1 -0
  150. package/web/_app/immutable/chunks/DXiLM1-G.js +1 -0
  151. package/web/_app/immutable/chunks/DZYaqCwi.js +1 -0
  152. package/web/_app/immutable/chunks/D_19S_Iu.js +1 -0
  153. package/web/_app/immutable/chunks/D_aBVMvK.js +11 -0
  154. package/web/_app/immutable/chunks/DciMwFJ1.js +1 -0
  155. package/web/_app/immutable/chunks/De_zI4J0.js +1 -0
  156. package/web/_app/immutable/chunks/DgAKBe0r.js +1 -0
  157. package/web/_app/immutable/chunks/DjVOVvNP.js +1 -0
  158. package/web/_app/immutable/chunks/DkvLWnSd.js +1 -0
  159. package/web/_app/immutable/chunks/Dp_KdFgX.js +1 -0
  160. package/web/_app/immutable/chunks/Ds8Pkiic.js +1 -0
  161. package/web/_app/immutable/chunks/{D4Jg9Smi.js → DsBJWNWe.js} +1 -1
  162. package/web/_app/immutable/chunks/DtbnBenW.js +1 -0
  163. package/web/_app/immutable/chunks/DuQMFDvy.js +1 -0
  164. package/web/_app/immutable/chunks/DwuoZtxw.js +1 -0
  165. package/web/_app/immutable/chunks/DxLkKbK2.js +5 -0
  166. package/web/_app/immutable/chunks/Dz-mlCAC.js +1 -0
  167. package/web/_app/immutable/chunks/EQVRMnZU.js +2 -0
  168. package/web/_app/immutable/chunks/GwMd3KmG.js +1 -0
  169. package/web/_app/immutable/chunks/K_3X9lyC.js +23 -0
  170. package/web/_app/immutable/chunks/PPSTk1ow.js +1 -0
  171. package/web/_app/immutable/chunks/S7ucR4pR.js +1 -0
  172. package/web/_app/immutable/chunks/TuFg0KqZ.js +4 -0
  173. package/web/_app/immutable/chunks/VlUiK86J.js +1 -0
  174. package/web/_app/immutable/chunks/WBFggAXs.js +1 -0
  175. package/web/_app/immutable/chunks/_rHYGvON.js +1 -0
  176. package/web/_app/immutable/chunks/bcQIrid8.js +1 -0
  177. package/web/_app/immutable/chunks/gfMMiTNf.js +1 -0
  178. package/web/_app/immutable/chunks/gpDK5sbt.js +1 -0
  179. package/web/_app/immutable/chunks/kS471soi.js +1 -0
  180. package/web/_app/immutable/chunks/mbLqADPr.js +1 -0
  181. package/web/_app/immutable/chunks/nMarIA7J.js +3 -0
  182. package/web/_app/immutable/chunks/oBAE_ms0.js +1 -0
  183. package/web/_app/immutable/chunks/p9eYesGn.js +1 -0
  184. package/web/_app/immutable/chunks/qgSLujhx.js +1 -0
  185. package/web/_app/immutable/chunks/spNKdrPy.js +3 -0
  186. package/web/_app/immutable/chunks/tLWDfEkZ.js +1 -0
  187. package/web/_app/immutable/chunks/u8YMmbU0.js +1 -0
  188. package/web/_app/immutable/chunks/vk5Wkr31.js +1 -0
  189. package/web/_app/immutable/chunks/wCTXGvXm.js +2 -0
  190. package/web/_app/immutable/chunks/wj7G6pU5.js +5 -0
  191. package/web/_app/immutable/chunks/xvONpD0Z.js +1 -0
  192. package/web/_app/immutable/chunks/yxd8jSz3.js +1 -0
  193. package/web/_app/immutable/entry/app.Dt52eXt2.js +2 -0
  194. package/web/_app/immutable/entry/start.mjqIl2PI.js +1 -0
  195. package/web/_app/immutable/nodes/0.DIhIOwuo.js +26 -0
  196. package/web/_app/immutable/nodes/1.B5qkcrvB.js +1 -0
  197. package/web/_app/immutable/nodes/2.CK3q7brK.js +1 -0
  198. package/web/_app/immutable/nodes/3.CrUewa31.js +1 -0
  199. package/web/_app/immutable/nodes/4.BhNJiqT-.js +2 -0
  200. package/web/_app/immutable/nodes/5.DXTbGemS.js +5 -0
  201. package/web/_app/immutable/nodes/6.BZWaFgFP.js +1 -0
  202. package/web/_app/immutable/nodes/7.BxQaF3lD.js +1 -0
  203. package/web/_app/version.json +1 -1
  204. package/web/index.html +22 -18
  205. package/web/_app/immutable/assets/0.DNvf5eFW.css +0 -1
  206. package/web/_app/immutable/assets/NoteEditor.BCjOff8L.css +0 -1
  207. package/web/_app/immutable/chunks/-7PsQOY4.js +0 -3
  208. package/web/_app/immutable/chunks/B9vLu37F.js +0 -1
  209. package/web/_app/immutable/chunks/BAVeNK5c.js +0 -20
  210. package/web/_app/immutable/chunks/CQqNkDRK.js +0 -1
  211. package/web/_app/immutable/chunks/Cs9sQ8Oa.js +0 -1
  212. package/web/_app/immutable/chunks/DG2llOXe.js +0 -666
  213. package/web/_app/immutable/chunks/DqVFJWdL.js +0 -1
  214. package/web/_app/immutable/chunks/HCrXwzvD.js +0 -1
  215. package/web/_app/immutable/chunks/MjV3DsQW.js +0 -1
  216. package/web/_app/immutable/chunks/uID4v_1K.js +0 -1
  217. package/web/_app/immutable/entry/app.CR9ubCCo.js +0 -2
  218. package/web/_app/immutable/entry/start.D4DOZCP6.js +0 -1
  219. package/web/_app/immutable/nodes/0.BYXv43VP.js +0 -26
  220. package/web/_app/immutable/nodes/1.DGP71l35.js +0 -1
  221. package/web/_app/immutable/nodes/2.CxspTYRG.js +0 -1
  222. package/web/_app/immutable/nodes/3.ZTzs6oEI.js +0 -1
  223. package/web/_app/immutable/nodes/4.CeIGu2c2.js +0 -2
  224. package/web/_app/immutable/nodes/5.k1DmYkGm.js +0 -5
  225. package/web/_app/immutable/nodes/6.Q0yGno8p.js +0 -1
  226. package/web/_app/immutable/nodes/7.BJRu312o.js +0 -1
package/dist/main.js CHANGED
@@ -62,7 +62,7 @@ function cmpTuple(a, b) {
62
62
  }
63
63
  /** A frontmatter value as text: true/false, whole-number floats without ".0",
64
64
  * nothing for null. */
65
- function text(value) {
65
+ function text$1(value) {
66
66
  if (value === null || value === void 0) return "";
67
67
  if (typeof value === "boolean") return value ? "true" : "false";
68
68
  return String(value);
@@ -450,14 +450,14 @@ function kindOf(type) {
450
450
  }
451
451
  /** Note ids from frontmatter: a list, or one; `/a/b.md` and `a/b` alike. */
452
452
  function idsOf(raw) {
453
- return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
453
+ return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text$1(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
454
454
  }
455
455
  /** When it was resolved or solved, as written (a date), or null while it is open. */
456
456
  function doneOf(meta, kind) {
457
457
  if (kind === "note") return null;
458
458
  const v = meta[DONE[kind]];
459
459
  if (v === null || v === void 0 || v === false || v === "") return null;
460
- return v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
460
+ return v instanceof Date ? v.toISOString().slice(0, 10) : text$1(v);
461
461
  }
462
462
  /** The embeds in a note's body, in order, as written (a name, or a path). */
463
463
  function embedRefs(body) {
@@ -575,17 +575,17 @@ var init_bundle = __esmMin((() => {
575
575
  }
576
576
  get type() {
577
577
  const t = this.meta.type;
578
- return t === null || t === void 0 || t === "" ? "" : text(t);
578
+ return t === null || t === void 0 || t === "" ? "" : text$1(t);
579
579
  }
580
580
  get title() {
581
581
  const t = this.meta.title;
582
- if (t) return text(t);
582
+ if (t) return text$1(t);
583
583
  const stem = basename$1(this.id);
584
584
  const words = strip(stem.replaceAll("-", " ").replaceAll("_", " "));
585
585
  return words ? words[0].toUpperCase() + words.slice(1).toLowerCase() : stem;
586
586
  }
587
587
  get description() {
588
- return text(this.meta.description || "");
588
+ return text$1(this.meta.description || "");
589
589
  }
590
590
  /** A note, an open question, or a problem (M12). */
591
591
  get kind() {
@@ -598,10 +598,10 @@ var init_bundle = __esmMin((() => {
598
598
  get tags() {
599
599
  let tags = this.meta.tags || [];
600
600
  if (typeof tags === "string") tags = [tags];
601
- return Array.isArray(tags) ? tags.filter((t) => t !== null && t !== void 0).map(text) : [];
601
+ return Array.isArray(tags) ? tags.filter((t) => t !== null && t !== void 0).map(text$1) : [];
602
602
  }
603
603
  get status() {
604
- return text(this.meta.status || "stable");
604
+ return text$1(this.meta.status || "stable");
605
605
  }
606
606
  get generated() {
607
607
  return isEntry(this.meta.generated) ? this.meta.generated : {};
@@ -618,10 +618,10 @@ var init_bundle = __esmMin((() => {
618
618
  get trust() {
619
619
  const events = this.verified;
620
620
  if (!events.length) return "unverified";
621
- return events.some((e) => text(e.by).startsWith("human:")) ? "human-reviewed" : "machine-confirmed";
621
+ return events.some((e) => text$1(e.by).startsWith("human:")) ? "human-reviewed" : "machine-confirmed";
622
622
  }
623
623
  get lastHumanVerification() {
624
- const times = this.verified.filter((e) => text(e.by).startsWith("human:")).map((e) => toTime(e.at)).filter((t) => t !== null);
624
+ const times = this.verified.filter((e) => text$1(e.by).startsWith("human:")).map((e) => toTime(e.at)).filter((t) => t !== null);
625
625
  return times.length ? Math.max(...times) : null;
626
626
  }
627
627
  /** Human-reviewed, but meaningfully changed since the latest human check. */
@@ -1757,14 +1757,14 @@ function parseStudyConfig(source) {
1757
1757
  }
1758
1758
  const base = raw.base ?? {};
1759
1759
  const format = raw.format === void 0 ? 1 : Number(raw.format);
1760
- if (format !== 1) throw new StudyError(`${CONFIG_FILE} is format ${text(raw.format)}; this isorite reads format 1`);
1760
+ if (format !== 1) throw new StudyError(`${CONFIG_FILE} is format ${text$1(raw.format)}; this isorite reads format 1`);
1761
1761
  if (typeof base.path !== "string" || !base.path) throw new StudyError(`${CONFIG_FILE} needs [base] path, the knowledge base's folder`);
1762
1762
  return {
1763
1763
  format,
1764
1764
  base: {
1765
1765
  path: base.path,
1766
- remote: text(base.remote ?? ""),
1767
- root: text(base.root ?? "")
1766
+ remote: text$1(base.remote ?? ""),
1767
+ root: text$1(base.root ?? "")
1768
1768
  }
1769
1769
  };
1770
1770
  }
@@ -2665,6 +2665,12 @@ something at one place in the note they have open.`;
2665
2665
  - Maths is LaTeX between dollar signs: $p(x)$ in a line, and a displayed
2666
2666
  equation as a paragraph of its own, with $$ alone on the line before it
2667
2667
  and $$ alone on the line after.
2668
+ - Code is a fenced block named with its language (\`\`\`python), so it is
2669
+ coloured by it.
2670
+ - A diagram is a fenced block in the language mermaid (Mermaid's syntax), and
2671
+ is drawn as the diagram. Draw one where a picture of how things stand to
2672
+ each other helps, or when asked; not by habit. Maths in a label goes
2673
+ between $$ and $$.
2668
2674
  - Ordinary Markdown otherwise. No HTML.`;
2669
2675
  FORMAT_RULES = FORMAT;
2670
2676
  LOOKUP = (rounds, meaning) => `# Looking things up
@@ -4365,6 +4371,14 @@ var init_engine = __esmMin((() => {
4365
4371
  get provider() {
4366
4372
  return this.opts.provider ?? OPENROUTER;
4367
4373
  }
4374
+ /** Take another provider into use, from the next request on: a setup kept from the settings (M18). What was
4375
+ * asked of the last one (its key's limit, its model's window) is asked again. */
4376
+ setProvider(provider, http) {
4377
+ this.opts.provider = provider;
4378
+ this.opts.http = http;
4379
+ this.window = void 0;
4380
+ this.keyLimit = void 0;
4381
+ }
4368
4382
  async settings() {
4369
4383
  return {
4370
4384
  provider: this.provider.name,
@@ -4561,7 +4575,7 @@ var init_engine = __esmMin((() => {
4561
4575
  async begin(req, http, asked) {
4562
4576
  if (asked.length > 4e3) throw new EngineError("that is too long to ask at once", 413);
4563
4577
  const key = await this.opts.secrets?.get("provider.key") ?? null;
4564
- if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings, behind the logo.", 409);
4578
+ if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in Iso's settings.", 409);
4565
4579
  const { messages, olds } = await this.compose(req, asked);
4566
4580
  const maxTokens = req.form === "insert" ? REPLY_TOKENS : req.form === "note" ? NOTE_TOKENS : Math.max(REPLY_TOKENS, Math.ceil(olds.join("").length / 3) + 1e3);
4567
4581
  const most = (req.form === "insert" || req.form === "rewrite") && req.choice?.lookup === false ? 0 : void 0;
@@ -5506,7 +5520,7 @@ var init_engine = __esmMin((() => {
5506
5520
  async beginTurn(conversation, req, http) {
5507
5521
  const study = this.opts.own;
5508
5522
  const key = await this.opts.secrets?.get("provider.key") ?? null;
5509
- if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings, behind the logo.", 409);
5523
+ if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in Iso's settings.", 409);
5510
5524
  const lookupOn = req.lookup !== false, mode = req.mode;
5511
5525
  let w = await this.working({
5512
5526
  ...req,
@@ -6702,7 +6716,11 @@ const textTable = (field, v, env) => v === void 0 ? void 0 : Object.fromEntries(
6702
6716
  * `headers`, `key_header`, `query` and `stream_usage = false`; `max_tokens` for models that think at length; its
6703
6717
  * client certificate is `gatewayTls`'s. */
6704
6718
  function providerConfig(path = configPath(), env = process.env) {
6705
- const config = readConfig(path), p = table(config.provider), tiers = table(config.tiers), prices = table(config.prices);
6719
+ return providerFrom(readConfig(path), env);
6720
+ }
6721
+ /** The provider a configuration names, as `providerConfig` reads it from the file: for a setup not yet kept (M18). */
6722
+ function providerFrom(config, env = process.env) {
6723
+ const p = table(config.provider), tiers = table(config.tiers), prices = table(config.prices);
6706
6724
  const custom = typeof p.url === "string" && p.url !== "";
6707
6725
  const base = custom ? {
6708
6726
  name: typeof p.name === "string" && p.name ? p.name : "custom",
@@ -6733,10 +6751,14 @@ function providerConfig(path = configPath(), env = process.env) {
6733
6751
  return out;
6734
6752
  }
6735
6753
  function gatewayTls(path = configPath(), env = process.env) {
6736
- const p = table(readConfig(path).provider), files = [];
6754
+ return gatewayTlsFrom(readConfig(path), dirname(path), env);
6755
+ }
6756
+ /** The certificate a configuration names, its paths from `dir` (the configuration's folder). */
6757
+ function gatewayTlsFrom(config, dir, env = process.env) {
6758
+ const p = table(config.provider), files = [];
6737
6759
  const file = (field) => {
6738
- if (p[field] === void 0) return void 0;
6739
- const named = valueOf(field, p[field], env), at = named.startsWith("~/") ? join(homedir(), named.slice(2)) : resolve(dirname(path), named);
6760
+ if (p[field] === void 0 || p[field] === "") return void 0;
6761
+ const named = valueOf(field, p[field], env), at = named.startsWith("~/") ? join(homedir(), named.slice(2)) : resolve(dir, named);
6740
6762
  if (!existsSync(at)) throw new Error(`[provider] ${field}: there is no file at ${at}`);
6741
6763
  files.push(at);
6742
6764
  return readFileSync(at);
@@ -6754,6 +6776,10 @@ function gatewayTls(path = configPath(), env = process.env) {
6754
6776
  if (p.pfx_passphrase !== void 0) tls.passphrase = valueOf("pfx_passphrase", p.pfx_passphrase, env);
6755
6777
  return files.length ? tls : null;
6756
6778
  }
6779
+ /** Whether Iso has a model here: a key kept, or a provider of your own (a gateway may need no key). */
6780
+ async function hasModel$1(config = configPath()) {
6781
+ return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
6782
+ }
6757
6783
  //#endregion
6758
6784
  //#region packages/server/src/model.ts
6759
6785
  /** Where the server looks for the model, in order, and why each place is there. */
@@ -6784,7 +6810,7 @@ function modelPlaces(env = process.env) {
6784
6810
  /** Where the model is, or null when it is not installed or is switched off (ISORITE_EMBED=off). */
6785
6811
  function modelDir(env = process.env) {
6786
6812
  if (env.ISORITE_EMBED === "off") return null;
6787
- return modelPlaces(env).find((p) => hasModel$1(p.dir))?.dir ?? null;
6813
+ return modelPlaces(env).find((p) => hasModel(p.dir))?.dir ?? null;
6788
6814
  }
6789
6815
  /** What is in a model's folder. `isorite-model.json` beside the files may give its `name` and `pooling` ("cls",
6790
6816
  * the first token's state, as granite is used; or "mean", the average of them all, as all-MiniLM is). */
@@ -6918,7 +6944,7 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
6918
6944
  * checked by using it: it must load, and give DIM numbers for a sentence. Kept in the user's models, and used from
6919
6945
  * then on. */
6920
6946
  async function installFrom(from, say = console.log, env = process.env) {
6921
- if (!hasModel$1(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
6947
+ if (!hasModel(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
6922
6948
  const granite = !existsSync(join(from, "isorite-model.json")) && FILES.every(([, to, want]) => existsSync(join(from, to)) && sha(readFileSync(join(from, to))) === want);
6923
6949
  let out = userModelDir(env);
6924
6950
  if (granite) await fetchModel(out, say, { from });
@@ -6955,7 +6981,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
6955
6981
  say(e.message);
6956
6982
  return 1;
6957
6983
  }
6958
- const places = modelPlaces(env), found = places.find((p) => hasModel$1(p.dir));
6984
+ const places = modelPlaces(env), found = places.find((p) => hasModel(p.dir));
6959
6985
  if (found) {
6960
6986
  told(modelInfo(found.dir), found.why);
6961
6987
  say("Nothing to fetch.");
@@ -6977,7 +7003,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
6977
7003
  return 1;
6978
7004
  }
6979
7005
  }
6980
- var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel$1, ModelFetchError, modelFiles;
7006
+ var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
6981
7007
  var init_model = __esmMin((() => {
6982
7008
  init_src();
6983
7009
  NAME$1 = "granite-embedding-small-english-r2";
@@ -7010,7 +7036,7 @@ var init_model = __esmMin((() => {
7010
7036
  sha = (buf) => createHash("sha256").update(buf).digest("hex");
7011
7037
  userModelsDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models");
7012
7038
  userModelDir = (env = process.env) => join(userModelsDir(env), NAME$1);
7013
- hasModel$1 = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
7039
+ hasModel = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
7014
7040
  ModelFetchError = class extends Error {
7015
7041
  tried;
7016
7042
  constructor(tried) {
@@ -7145,16 +7171,17 @@ function gatewayHttp(provider, tls, env = process.env) {
7145
7171
  }
7146
7172
  /** What OpenCode may ask of the provider through isorite. */
7147
7173
  const RELAYED = /* @__PURE__ */ new Set(["/chat/completions", "/models"]);
7148
- /** The provider at /api/gateway: each request is sent on with what `endpoint` adds (the key, where the provider
7174
+ /** The provider at /api/gateway, as `live` has it: each request is sent on with what `endpoint` adds (the key, where the provider
7149
7175
  * wants it, its headers and query) by `http`, and the reply streamed back. A provider that can't be reached is
7150
7176
  * answered as an OpenAI-compatible one answers an error, so OpenCode shows why. */
7151
- function relay(provider, http, key) {
7177
+ function relay(live, key) {
7152
7178
  const json = (status, message) => new Response(JSON.stringify({ error: { message } }), {
7153
7179
  status,
7154
7180
  headers: { "Content-Type": "application/json" }
7155
7181
  });
7156
7182
  return async (path, req) => {
7157
7183
  if (!RELAYED.has(path)) return json(404, `isorite relays only ${[...RELAYED].join(" and ")}`);
7184
+ const { provider, http } = live();
7158
7185
  const to = endpoint(provider, await key(), path), type = req.headers.get("content-type");
7159
7186
  const body = req.method === "POST" ? await req.text() : void 0;
7160
7187
  let res;
@@ -7641,6 +7668,35 @@ const Revised = z.object({
7641
7668
  text: z.union([z.string(), z.array(z.string()).max(40)])
7642
7669
  }).strict();
7643
7670
  const Key = z.object({ key: z.string().nullable() }).strict();
7671
+ const Pair = z.object({
7672
+ name: z.string().max(200),
7673
+ value: z.string().max(4e3),
7674
+ held: z.boolean().optional()
7675
+ }).strict();
7676
+ const Path = z.string().max(1e3);
7677
+ const SetupDraft = z.object({
7678
+ kind: z.enum(["openrouter", "gateway"]),
7679
+ name: z.string().max(100),
7680
+ url: z.string().max(2e3),
7681
+ keyHeld: z.boolean(),
7682
+ keyHeader: z.string().max(200),
7683
+ headers: z.array(Pair).max(40),
7684
+ query: z.array(Pair).max(40),
7685
+ pfx: Path,
7686
+ passphraseHeld: z.boolean(),
7687
+ cert: Path,
7688
+ certKey: Path,
7689
+ ca: Path,
7690
+ streamUsage: z.boolean(),
7691
+ maxTokens: z.number().int().positive().max(1e6).nullable(),
7692
+ tiers: z.object({
7693
+ low: z.string().max(200),
7694
+ mid: z.string().max(200),
7695
+ max: z.string().max(200)
7696
+ }).strict(),
7697
+ key: z.string().max(400).nullable().optional(),
7698
+ passphrase: z.string().max(1e3).nullable().optional()
7699
+ }).strict();
7644
7700
  const AgentName = z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/);
7645
7701
  const AgentLook = z.object({
7646
7702
  agent: AgentName,
@@ -7666,7 +7722,7 @@ const AgentStatus = z.object({
7666
7722
  session: z.string().min(1).max(200),
7667
7723
  busy: z.boolean()
7668
7724
  }).strict();
7669
- function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway }) {
7725
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway, setup }) {
7670
7726
  const app = new Hono();
7671
7727
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
7672
7728
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -7681,6 +7737,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
7681
7737
  } catch {}
7682
7738
  return !loopback || LOOPBACK.has(host) || host.endsWith(".ts.net") || allowed.has(host);
7683
7739
  };
7740
+ /** Asked from this machine itself: its own name, and (where the connection says) from its own address. A
7741
+ * tailnet's name is not, even through tailscale serve, whose connections come from this machine. */
7742
+ const local = (c) => {
7743
+ let host = "";
7744
+ try {
7745
+ host = new URL("http://" + (c.req.header("host") ?? "")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
7746
+ } catch {}
7747
+ const from = c.env?.incoming?.socket?.remoteAddress;
7748
+ return LOOPBACK.has(host) && (from === void 0 || /^(127\.|::1$|::ffff:127\.)/.test(from));
7749
+ };
7684
7750
  const NOT_HERE = "isorite answers only to this machine's own names (localhost) and to a tailnet's (….ts.net). To be reached by another name, start it with: isorite serve <base> --allow-host <that name>";
7685
7751
  /** Why a write is refused, or null. */
7686
7752
  const writeRefused = (c) => {
@@ -7829,14 +7895,36 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
7829
7895
  app.get("/api/context", (c) => answer(c, () => api.context(c.req.query("id") || void 0, choiceOf(c.req.query("choice")))));
7830
7896
  post("/api/context/clear", z.object({}).strict(), () => api.clearConversation());
7831
7897
  app.get("/api/settings", (c) => answer(c, () => api.settings()));
7832
- post("/api/settings/key", Key, (_c, b) => api.setKey(b.key), "put");
7898
+ post("/api/settings/key", Key, async (_c, b) => {
7899
+ const s = await api.setKey(b.key);
7900
+ await setup?.changed();
7901
+ return api.settings().catch(() => s);
7902
+ }, "put");
7833
7903
  app.get("/api/spend", (c) => answer(c, () => api.spend()));
7834
7904
  post("/api/settings/spend-cap", z.object({ usd: z.number().positive().max(1e6).nullable() }).strict(), (_c, b) => api.setSpendCap(b.usd), "put");
7835
7905
  post("/api/settings/connect", z.object({ callback: z.string().max(2e3) }).strict(), (_c, b) => api.connect(b.callback));
7836
7906
  post("/api/settings/connected", z.object({
7837
7907
  code: z.string().min(1).max(2e3),
7838
7908
  state: z.string().min(1).max(200)
7839
- }).strict(), (_c, b) => api.connected(b.code, b.state));
7909
+ }).strict(), async (_c, b) => {
7910
+ const s = await api.connected(b.code, b.state);
7911
+ await setup?.changed();
7912
+ return api.settings().catch(() => s);
7913
+ });
7914
+ if (setup) {
7915
+ const FROM_HERE = "The provider's setup is changed only from this machine (localhost): the key goes where it says.";
7916
+ const here = (c) => {
7917
+ if (!local(c)) throw new EngineError(FROM_HERE, 403);
7918
+ };
7919
+ app.get("/api/settings/provider", (c) => answer(c, async () => setup.view(local(c))));
7920
+ post("/api/settings/provider", SetupDraft, (c, b) => {
7921
+ here(c);
7922
+ return setup.keep(b, true);
7923
+ }, "put");
7924
+ post("/api/settings/provider/check", z.object({ draft: SetupDraft.nullable() }).strict(), (c, b) => setup.check(local(c) ? b.draft : null));
7925
+ post("/api/settings/provider/models", z.object({ draft: SetupDraft.nullable() }).strict(), async (c, b) => ({ models: await setup.models(local(c) ? b.draft : null) }));
7926
+ post("/api/settings/report", z.object({}).strict(), async () => ({ text: await setup.report() }));
7927
+ }
7840
7928
  app.get("/api/agent/tools", (c) => answer(c, () => api.agentTools()));
7841
7929
  post("/api/agent/look", AgentLook, (_c, b) => api.agentLook(b));
7842
7930
  app.get("/api/agent/base", (c) => answer(c, async () => ({ notes: notes ?? null })));
@@ -8037,6 +8125,251 @@ var NodeFiles = class {
8037
8125
  }
8038
8126
  };
8039
8127
  //#endregion
8128
+ //#region packages/server/src/setup.ts
8129
+ init_src();
8130
+ /** What the page sent that cannot be kept: said back to it as it is (400). */
8131
+ var SetupError = class extends EngineError {};
8132
+ /** The fields of [provider] the form holds. The rest (spend_cap, cache_marks, context, …) are left as they are. */
8133
+ const FORM = [
8134
+ "url",
8135
+ "name",
8136
+ "auth",
8137
+ "key",
8138
+ "key_header",
8139
+ "headers",
8140
+ "query",
8141
+ "pfx",
8142
+ "pfx_passphrase",
8143
+ "cert",
8144
+ "cert_key",
8145
+ "ca",
8146
+ "stream_usage",
8147
+ "max_tokens"
8148
+ ];
8149
+ const NAMED = /^\$[A-Za-z_][A-Za-z0-9_]*$/;
8150
+ const text = (v) => typeof v === "string" || typeof v === "number" ? String(v) : "";
8151
+ const secretPair = (name, value) => SECRET.test(name) && !NAMED.test(value);
8152
+ /** The setup a configuration holds, its secrets held back. */
8153
+ function readSetup(config) {
8154
+ const p = table(config.provider), tiers = table(config.tiers);
8155
+ const pairs = (v) => Object.entries(table(v)).map(([name, x]) => secretPair(name, text(x)) ? {
8156
+ name,
8157
+ value: "",
8158
+ held: true
8159
+ } : {
8160
+ name,
8161
+ value: text(x)
8162
+ });
8163
+ const gateway = typeof p.url === "string" && p.url !== "";
8164
+ return {
8165
+ kind: gateway ? "gateway" : "openrouter",
8166
+ name: text(p.name),
8167
+ url: gateway ? p.url : OPENROUTER.url,
8168
+ keyHeld: text(p.key) !== "",
8169
+ keyHeader: text(p.key_header),
8170
+ headers: pairs(p.headers),
8171
+ query: pairs(p.query),
8172
+ pfx: text(p.pfx),
8173
+ passphraseHeld: p.pfx_passphrase !== void 0 && text(p.pfx_passphrase) !== "",
8174
+ cert: text(p.cert),
8175
+ certKey: text(p.cert_key),
8176
+ ca: text(p.ca),
8177
+ streamUsage: p.stream_usage !== false,
8178
+ maxTokens: typeof p.max_tokens === "number" && p.max_tokens > 0 ? p.max_tokens : null,
8179
+ tiers: Object.fromEntries(TIERS.map((t) => [t, text(tiers[t]) || OPENROUTER.tiers[t]]))
8180
+ };
8181
+ }
8182
+ /** The configuration with `draft` in place of its setup; and whether the key was forgotten for a new address. */
8183
+ function applySetup(config, draft) {
8184
+ const old = table(config.provider), p = Object.fromEntries(Object.entries(old).filter(([k]) => !FORM.includes(k)));
8185
+ const gateway = draft.kind === "gateway", url = gateway ? draft.url.trim() : "";
8186
+ if (gateway) {
8187
+ let parsed;
8188
+ try {
8189
+ parsed = new URL(url);
8190
+ } catch {
8191
+ throw new SetupError("the gateway's address is not an address: it begins https:// (or http:// on this machine)");
8192
+ }
8193
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new SetupError("the gateway's address begins https:// (or http:// on this machine)");
8194
+ p.url = url.replace(/\/+$/, "");
8195
+ if (draft.name.trim()) p.name = draft.name.trim();
8196
+ }
8197
+ const at = (url) => (url || OPENROUTER.url).replace(/\/+$/, "");
8198
+ const moved = at(text(old.url)) !== at(text(p.url));
8199
+ if (!moved && old.auth !== void 0) p.auth = old.auth;
8200
+ let forgot = false;
8201
+ if (typeof draft.key === "string" && draft.key.trim()) {
8202
+ const key = draft.key.trim();
8203
+ if (key.length > 400 || /\s/.test(key)) throw new SetupError("that does not look like a key");
8204
+ p.key = key;
8205
+ } else if (draft.key === void 0 && old.key !== void 0) {
8206
+ if (moved) forgot = true;
8207
+ else p.key = old.key;
8208
+ }
8209
+ if (gateway) {
8210
+ if (draft.keyHeader.trim()) p.key_header = draft.keyHeader.trim();
8211
+ const pairs = (field, given) => {
8212
+ const before = table(old[field]), out = {};
8213
+ for (const pair of given) {
8214
+ const name = pair.name.trim();
8215
+ if (!name) continue;
8216
+ if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) throw new SetupError(`${field === "headers" ? "a header's" : "a query value's"} name has a character it cannot have: ${name}`);
8217
+ if (pair.held && !pair.value) {
8218
+ if (before[name] !== void 0 && !moved) out[name] = before[name];
8219
+ continue;
8220
+ }
8221
+ out[name] = pair.value;
8222
+ }
8223
+ if (Object.keys(out).length) p[field] = out;
8224
+ };
8225
+ pairs("headers", draft.headers);
8226
+ pairs("query", draft.query);
8227
+ for (const [field, value] of [
8228
+ ["pfx", draft.pfx],
8229
+ ["cert", draft.cert],
8230
+ ["cert_key", draft.certKey],
8231
+ ["ca", draft.ca]
8232
+ ]) if (value.trim()) p[field] = value.trim();
8233
+ if (p.pfx !== void 0) {
8234
+ if (typeof draft.passphrase === "string" && draft.passphrase) p.pfx_passphrase = draft.passphrase;
8235
+ else if (draft.passphrase === void 0 && old.pfx_passphrase !== void 0) p.pfx_passphrase = old.pfx_passphrase;
8236
+ }
8237
+ if (!draft.streamUsage) p.stream_usage = false;
8238
+ if (draft.maxTokens !== null) {
8239
+ if (!(Number.isInteger(draft.maxTokens) && draft.maxTokens > 0 && draft.maxTokens <= 1e6)) throw new SetupError("max_tokens is a whole number of tokens");
8240
+ p.max_tokens = draft.maxTokens;
8241
+ }
8242
+ }
8243
+ const tiers = Object.fromEntries(Object.entries(table(config.tiers)).filter(([k]) => !TIERS.includes(k)));
8244
+ for (const t of TIERS) {
8245
+ const model = draft.tiers[t]?.trim() ?? "";
8246
+ if (model.length > 200 || /\s/.test(model)) throw new SetupError(`the ${t} tier's model is not a model's name`);
8247
+ if (model && (gateway || model !== OPENROUTER.tiers[t])) tiers[t] = model;
8248
+ }
8249
+ const out = {
8250
+ ...config,
8251
+ provider: p
8252
+ };
8253
+ if (Object.keys(tiers).length) out.tiers = tiers;
8254
+ else delete out.tiers;
8255
+ if (!Object.keys(p).length) delete out.provider;
8256
+ return {
8257
+ config: out,
8258
+ forgot
8259
+ };
8260
+ }
8261
+ function reach(config, dir, env = process.env) {
8262
+ const provider = providerFrom(config, env), tls = gatewayTlsFrom(config, dir, env);
8263
+ return {
8264
+ provider,
8265
+ http: gatewayHttp(provider, tls, env),
8266
+ key: text(table(config.provider).key) || null,
8267
+ tls
8268
+ };
8269
+ }
8270
+ /** The setup, for the server's routes. */
8271
+ var Setup = class {
8272
+ path;
8273
+ env;
8274
+ o;
8275
+ constructor(o = {}) {
8276
+ this.o = o;
8277
+ this.env = o.env ?? process.env;
8278
+ this.path = o.config ?? configPath(this.env);
8279
+ }
8280
+ view(local) {
8281
+ return {
8282
+ setup: readSetup(readConfig(this.path)),
8283
+ local,
8284
+ config: this.path
8285
+ };
8286
+ }
8287
+ /** The configuration a draft would make, or the one kept (null). */
8288
+ draft(draft) {
8289
+ const config = readConfig(this.path);
8290
+ return draft ? applySetup(config, draft).config : config;
8291
+ }
8292
+ /** Doctor's checks of the provider, on the draft as if it were kept. Nothing is written. */
8293
+ async check(draft) {
8294
+ const checks = [], say = (step, outcome, said) => void checks.push({
8295
+ step,
8296
+ outcome,
8297
+ said
8298
+ });
8299
+ let r;
8300
+ try {
8301
+ r = reach(this.draft(draft), dirname(this.path), this.env);
8302
+ } catch (e) {
8303
+ say("setup", "fail", e.message);
8304
+ return checks;
8305
+ }
8306
+ if (!r.key && !r.tls && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(r.provider.url)) {
8307
+ say("key", "fail", "none: paste one, or sign in for one");
8308
+ return checks;
8309
+ }
8310
+ await checkProvider(r.provider, r.http, r.key, say);
8311
+ return checks;
8312
+ }
8313
+ /** The models the provider lists, for the tiers to be chosen from. Empty where it has no list. */
8314
+ async models(draft) {
8315
+ let r;
8316
+ try {
8317
+ r = reach(this.draft(draft), dirname(this.path), this.env);
8318
+ } catch (e) {
8319
+ throw new SetupError(e.message);
8320
+ }
8321
+ const to = endpoint(r.provider, r.key, "/models");
8322
+ let res;
8323
+ try {
8324
+ res = await r.http.fetch(to.url, {
8325
+ headers: to.headers,
8326
+ signal: AbortSignal.timeout(2e4)
8327
+ });
8328
+ } catch (e) {
8329
+ throw new SetupError(unreachable(e, r.provider.url));
8330
+ }
8331
+ if (!res.ok) {
8332
+ await res.body?.cancel();
8333
+ return [];
8334
+ }
8335
+ const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id).filter((id) => typeof id === "string");
8336
+ return [...new Set(listed)].sort();
8337
+ }
8338
+ /** Keep the draft, once it is known to be usable, and take it into use. The file as it was is kept beside it the
8339
+ * first time, since writing it loses its comments. */
8340
+ async keep(draft, local) {
8341
+ const { config } = applySetup(readConfig(this.path), draft);
8342
+ try {
8343
+ reach(config, dirname(this.path), this.env);
8344
+ } catch (e) {
8345
+ throw new SetupError(e.message);
8346
+ }
8347
+ if (existsSync(this.path) && !existsSync(`${this.path}.bak`)) copyFileSync(this.path, `${this.path}.bak`);
8348
+ writeConfig(config, this.path);
8349
+ await this.o.kept?.();
8350
+ return this.view(local);
8351
+ }
8352
+ /** A key was kept or forgotten by the engine: taken into use as a setup is. */
8353
+ async changed() {
8354
+ await this.o.kept?.();
8355
+ }
8356
+ /** isorite doctor --report, as one block with its secrets hidden. */
8357
+ async report() {
8358
+ const lines = [];
8359
+ await doctor(this.o.notes, {
8360
+ report: true,
8361
+ env: this.env,
8362
+ out: (l) => lines.push(l),
8363
+ ...this.o.version ? { version: this.o.version } : {},
8364
+ ...this.o.port ? { port: this.o.port } : {}
8365
+ });
8366
+ return lines.join("\n");
8367
+ }
8368
+ };
8369
+ //#endregion
8370
+ //#region packages/server/src/version.ts
8371
+ const VERSION = "0.7.0";
8372
+ //#endregion
8040
8373
  //#region packages/server/src/studybase.ts
8041
8374
  init_src$1();
8042
8375
  function git(cwd, ...args) {
@@ -8131,20 +8464,49 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
8131
8464
  } : {}
8132
8465
  });
8133
8466
  }
8467
+ /** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
8468
+ const { auth: _auth, ...openrouter } = OPENROUTER;
8469
+ const FINDER = {
8470
+ ...openrouter,
8471
+ name: "finder",
8472
+ url: "http://finder.invalid/v1"
8473
+ };
8474
+ /** The provider as the configuration has it now: kept from the settings (M18), it is taken into use at once, by
8475
+ * Iso and the relay. With `stand`, that is asked while there is no model here. */
8476
+ async function liveProvider(config, stand) {
8477
+ if (stand && !await hasModel$1(config)) return {
8478
+ provider: FINDER,
8479
+ http: stand
8480
+ };
8481
+ const provider = providerConfig(config);
8482
+ return {
8483
+ provider,
8484
+ http: gatewayHttp(provider, gatewayTls(config))
8485
+ };
8486
+ }
8134
8487
  /** A link most terminals let you click (OSC 8), when the output is a terminal; the address as it is otherwise. */
8135
8488
  const link$1 = (url, tty = Boolean(process.stdout.isTTY)) => tty ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url;
8136
8489
  /** `stand`: in place of the provider, an answerer of its own, as the guide's finder is when no model is set up
8137
- * (M16). It needs no key, and the key kept, if any, is not sent to it. */
8490
+ * (M16). It is asked while there is no model here (no key, no provider of your own), and needs no key. */
8138
8491
  async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand } = {}) {
8139
8492
  const opened = locate(path);
8140
- const config = configPath(), provider = providerConfig(config), http = stand ?? gatewayHttp(provider, gatewayTls(config));
8141
- const kept = new ConfigSecrets(config);
8142
- const secrets = stand ? {
8143
- get: (name) => name === "provider.key" ? Promise.resolve("none") : kept.get(name),
8144
- set: (name, value) => kept.set(name, value)
8145
- } : kept;
8146
- const api = await openEngine(opened, deviceId(config), config, http, modelDir(), secrets);
8147
- const gateway = LOOPBACK.has(host) ? relay(provider, http, () => secrets.get(KEY)) : void 0;
8493
+ const config = configPath();
8494
+ const current = () => liveProvider(config, stand);
8495
+ 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);
8499
+ const setup = new Setup({
8500
+ config,
8501
+ notes: opened.notes,
8502
+ version: VERSION,
8503
+ port,
8504
+ kept: async () => {
8505
+ live = await current();
8506
+ api.setProvider(live.provider, live.http);
8507
+ }
8508
+ });
8509
+ const gateway = LOOPBACK.has(host) ? relay(() => live, () => secrets.get(KEY)) : void 0;
8148
8510
  const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
8149
8511
  const terminal = new Terminal();
8150
8512
  const app = createApp({
@@ -8155,7 +8517,8 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
8155
8517
  allowHosts,
8156
8518
  notes: opened.notes,
8157
8519
  terminal,
8158
- gateway
8520
+ gateway,
8521
+ setup
8159
8522
  });
8160
8523
  const server = serve({
8161
8524
  fetch: app.fetch,
@@ -8502,6 +8865,65 @@ const TOOL = {
8502
8865
  }
8503
8866
  }
8504
8867
  };
8868
+ /** The provider asked, as doctor asks it and as the page's check does (M18): its list of models, a short reply from
8869
+ * each tier's model, and one tool call. A few hundred tokens in all. */
8870
+ async function checkProvider(p, h, key, say) {
8871
+ const models = [.../* @__PURE__ */ new Set([
8872
+ p.tiers.low,
8873
+ p.tiers.mid,
8874
+ p.tiers.max
8875
+ ])];
8876
+ let reached = true;
8877
+ try {
8878
+ const to = endpoint(p, key, "/models");
8879
+ const res = await h.fetch(to.url, {
8880
+ headers: to.headers,
8881
+ signal: AbortSignal.timeout(2e4)
8882
+ });
8883
+ if (res.ok) {
8884
+ const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id ?? "");
8885
+ const missing = models.filter((m) => !listed.includes(m));
8886
+ say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
8887
+ if (listed.length && missing.length) say("models", "fail", `${missing.join(", ")} not among them; it has ${listed.slice(0, 12).join(", ")}${listed.length > 12 ? ", …" : ""}. Name its models under [tiers]`);
8888
+ } else say("reached", res.status === 401 || res.status === 403 ? "fail" : "ok", res.status === 401 || res.status === 403 ? `it said ${res.status} to the list of models: the key${p.keyHeader ? "" : ", or the header it goes in (key_header)"}, or a header it wants (headers), is not accepted` : `it answered (${res.status} for the list of models, which some gateways don't have)`);
8889
+ } catch (e) {
8890
+ reached = false;
8891
+ say("reached", "fail", unreachable(e, p.url));
8892
+ }
8893
+ if (reached) {
8894
+ for (const model of models) try {
8895
+ const reply = await complete(h, p, key, {
8896
+ model,
8897
+ maxTokens: 16,
8898
+ messages: [{
8899
+ role: "user",
8900
+ content: "Reply with the one word: ready"
8901
+ }],
8902
+ signal: AbortSignal.timeout(6e4)
8903
+ });
8904
+ const usage = reply.usage.tokensIn + reply.usage.tokensOut > 0;
8905
+ say(`reply from ${model}`, usage || p.streamUsage === false ? "ok" : "warn", `"${reply.text.trim().slice(0, 40)}"${usage ? `, ${reply.usage.tokensIn} tokens in and ${reply.usage.tokensOut} out` : p.streamUsage === false ? "" : ": no usage reported, so the gauge can't count it"}`);
8906
+ } catch (e) {
8907
+ const said = e.message;
8908
+ say(`reply from ${model}`, "fail", /stream_options/i.test(said) ? `${said} Set stream_usage = false under [provider].` : said);
8909
+ }
8910
+ try {
8911
+ const reply = await complete(h, p, key, {
8912
+ model: p.tiers.mid,
8913
+ maxTokens: 64,
8914
+ tools: [TOOL],
8915
+ messages: [{
8916
+ role: "user",
8917
+ content: "Ping, using the ping tool."
8918
+ }],
8919
+ signal: AbortSignal.timeout(6e4)
8920
+ });
8921
+ say("tools", reply.calls.length ? "ok" : "fail", reply.calls.length ? `${p.tiers.mid} called ${reply.calls[0].name}` : `${p.tiers.mid} answered without calling the tool: Iso and the terminal need a model, and a gateway, that pass tools on`);
8922
+ } catch (e) {
8923
+ say("tools", "fail", e instanceof ModelError ? e.message : String(e));
8924
+ }
8925
+ }
8926
+ }
8505
8927
  /** Run the checks; 0 when none failed. */
8506
8928
  async function doctor(folder, o = {}) {
8507
8929
  const env = o.env ?? process.env, out = o.out ?? console.log, checks = [];
@@ -8515,7 +8937,7 @@ async function doctor(folder, o = {}) {
8515
8937
  };
8516
8938
  const path = configPath(env), execArgv = o.execArgv ?? process.execArgv;
8517
8939
  let provider = null, http = o.http ?? null, key = null;
8518
- if (!existsSync(path)) say("config", "warn", `there is no ${path}: Iso uses OpenRouter, and needs a key (settings, behind the logo, or key under [provider] there)`);
8940
+ if (!existsSync(path)) say("config", "warn", `there is no ${path}: Iso uses OpenRouter, and needs a key (in Iso's settings, or key under [provider] there)`);
8519
8941
  try {
8520
8942
  readConfig(path);
8521
8943
  provider = providerConfig(path, env);
@@ -8523,7 +8945,7 @@ async function doctor(folder, o = {}) {
8523
8945
  const tls = gatewayTls(path, env);
8524
8946
  http ??= gatewayHttp(provider, tls, env);
8525
8947
  if (existsSync(path)) say("config", "ok", `${path}: ${provider.name} at ${provider.url}`);
8526
- say("key", key ? "ok" : tls ? "warn" : "fail", key ? `${hidden(key)}, sent ${provider.keyHeader ? `in ${provider.keyHeader}` : "as Authorization: Bearer"}` : tls ? "none; the client certificate alone is sent" : "none: set one in settings (behind the logo), or as key under [provider]");
8948
+ say("key", key ? "ok" : tls ? "warn" : "fail", key ? `${hidden(key)}, sent ${provider.keyHeader ? `in ${provider.keyHeader}` : "as Authorization: Bearer"}` : tls ? "none; the client certificate alone is sent" : "none: set one in Iso's settings, or as key under [provider]");
8527
8949
  if (tls) say("certificate", "ok", `${tls.files.join(", ")}${tls.pfx || tls.cert ? "" : " (an authority only, no client certificate)"}`);
8528
8950
  const tiers = `low ${provider.tiers.low}, mid ${provider.tiers.mid}, max ${provider.tiers.max}`;
8529
8951
  say("models", provider.defaultTiers ? "warn" : "ok", provider.defaultTiers ? `${tiers}: OpenRouter's names, kept because [tiers] names none for ${provider.name}; a gateway usually calls its models otherwise` : tiers);
@@ -8547,56 +8969,7 @@ async function doctor(folder, o = {}) {
8547
8969
  p.tiers.max
8548
8970
  ])];
8549
8971
  if (!o.report) out(` asking ${p.name}: its models, then ${models.length + 1} replies of a few words (a few hundred tokens in all)`);
8550
- let reached = true;
8551
- try {
8552
- const to = endpoint(p, key, "/models");
8553
- const res = await h.fetch(to.url, {
8554
- headers: to.headers,
8555
- signal: AbortSignal.timeout(2e4)
8556
- });
8557
- if (res.ok) {
8558
- const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id ?? "");
8559
- const missing = models.filter((m) => !listed.includes(m));
8560
- say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
8561
- if (listed.length && missing.length) say("models", "fail", `${missing.join(", ")} not among them; it has ${listed.slice(0, 12).join(", ")}${listed.length > 12 ? ", …" : ""}. Name its models under [tiers]`);
8562
- } else say("reached", res.status === 401 || res.status === 403 ? "fail" : "ok", res.status === 401 || res.status === 403 ? `it said ${res.status} to the list of models: the key${p.keyHeader ? "" : ", or the header it goes in (key_header)"}, or a header it wants (headers), is not accepted` : `it answered (${res.status} for the list of models, which some gateways don't have)`);
8563
- } catch (e) {
8564
- reached = false;
8565
- say("reached", "fail", unreachable(e, p.url));
8566
- }
8567
- if (reached) {
8568
- for (const model of models) try {
8569
- const reply = await complete(h, p, key, {
8570
- model,
8571
- maxTokens: 16,
8572
- messages: [{
8573
- role: "user",
8574
- content: "Reply with the one word: ready"
8575
- }],
8576
- signal: AbortSignal.timeout(6e4)
8577
- });
8578
- const usage = reply.usage.tokensIn + reply.usage.tokensOut > 0;
8579
- say(`reply from ${model}`, usage || p.streamUsage === false ? "ok" : "warn", `"${reply.text.trim().slice(0, 40)}"${usage ? `, ${reply.usage.tokensIn} tokens in and ${reply.usage.tokensOut} out` : p.streamUsage === false ? "" : ": no usage reported, so the gauge can't count it"}`);
8580
- } catch (e) {
8581
- const said = e.message;
8582
- say(`reply from ${model}`, "fail", /stream_options/i.test(said) ? `${said} Set stream_usage = false under [provider].` : said);
8583
- }
8584
- try {
8585
- const reply = await complete(h, p, key, {
8586
- model: p.tiers.mid,
8587
- maxTokens: 64,
8588
- tools: [TOOL],
8589
- messages: [{
8590
- role: "user",
8591
- content: "Ping, using the ping tool."
8592
- }],
8593
- signal: AbortSignal.timeout(6e4)
8594
- });
8595
- say("tools", reply.calls.length ? "ok" : "fail", reply.calls.length ? `${p.tiers.mid} called ${reply.calls[0].name}` : `${p.tiers.mid} answered without calling the tool: Iso and the terminal need a model, and a gateway, that pass tools on`);
8596
- } catch (e) {
8597
- say("tools", "fail", e instanceof ModelError ? e.message : String(e));
8598
- }
8599
- }
8972
+ await checkProvider(p, h, key, say);
8600
8973
  }
8601
8974
  try {
8602
8975
  const found = findOpenCode(env, o.probe);
@@ -10015,7 +10388,6 @@ function loadGuide(dir = GUIDE_DIR) {
10015
10388
  }
10016
10389
  //#endregion
10017
10390
  //#region packages/server/src/guide.ts
10018
- init_src();
10019
10391
  init_src$1();
10020
10392
  const GUIDE_FOLDER = join(homedir(), "isorite-guide");
10021
10393
  /** Write the guide into `folder`, unless it is there already. Returns whether it was written now. A folder that
@@ -10037,19 +10409,15 @@ function writeGuide(folder) {
10037
10409
  fresh: true
10038
10410
  };
10039
10411
  }
10040
- /** Whether Iso has a model here: a key kept, or a provider of your own (a gateway may need no key). */
10041
- async function hasModel(config = configPath()) {
10042
- return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
10043
- }
10044
10412
  async function guide(folder = GUIDE_FOLDER, { port = 4767, out = console.log } = {}) {
10045
10413
  const { notes, fresh } = writeGuide(folder);
10046
- const model = await hasModel();
10414
+ const model = await hasModel$1();
10047
10415
  out(fresh ? `isorite guide: written to ${notes}; it is yours to change, and run again it opens as you left it` : `isorite guide: ${notes}, as you left it`);
10048
10416
  out(model ? " Iso: your provider's model" : " Iso: the guide's finder, which answers with the notes that match (no model is set up)");
10049
10417
  out(` start at http://localhost:${port}/#/n/${START}`);
10050
10418
  await serve$1(notes, {
10051
10419
  port,
10052
- ...model ? {} : { stand: finderHttp(16) }
10420
+ stand: finderHttp(16)
10053
10421
  });
10054
10422
  if (process.env.ISORITE_NO_BROWSER !== "1") openBrowser(`http://localhost:${port}/#/n/${START}`);
10055
10423
  }
@@ -10126,7 +10494,6 @@ async function writeGeometry(folder, now = Date.now()) {
10126
10494
  //#region packages/server/src/main.ts
10127
10495
  init_src$1();
10128
10496
  init_model();
10129
- const VERSION = "0.5.0";
10130
10497
  const USAGE = `isorite: an agentic harness for understanding
10131
10498
 
10132
10499
  isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/