isorite 0.5.0 → 0.6.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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +460 -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/web/_app/immutable/assets/{0.DNvf5eFW.css → 0.LEIHnkPh.css} +1 -1
  7. package/web/_app/immutable/chunks/{DG2llOXe.js → 2DHY0Dzh.js} +3 -3
  8. package/web/_app/immutable/chunks/{C1xA2mdF.js → BQRMn_tl.js} +1 -1
  9. package/web/_app/immutable/chunks/C-352d8F.js +1 -0
  10. package/web/_app/immutable/chunks/{B9vLu37F.js → CHxNLteG.js} +1 -1
  11. package/web/_app/immutable/chunks/Cf6h5DpS.js +20 -0
  12. package/web/_app/immutable/chunks/D4A7-5k0.js +1 -0
  13. package/web/_app/immutable/chunks/Dijhflxl.js +1 -0
  14. package/web/_app/immutable/chunks/Ps0IPbdL.js +1 -0
  15. package/web/_app/immutable/chunks/{D4Jg9Smi.js → ZRnI3PpK.js} +1 -1
  16. package/web/_app/immutable/chunks/nhmVyQdD.js +3 -0
  17. package/web/_app/immutable/chunks/rTqzqD6j.js +1 -0
  18. package/web/_app/immutable/chunks/y24aTX4O.js +1 -0
  19. package/web/_app/immutable/entry/app.BQ0r6jSu.js +2 -0
  20. package/web/_app/immutable/entry/start.CIQiYrSU.js +1 -0
  21. package/web/_app/immutable/nodes/0.zPrOkn79.js +26 -0
  22. package/web/_app/immutable/nodes/1.DfBkOAo2.js +1 -0
  23. package/web/_app/immutable/nodes/2.fp8Y5joG.js +1 -0
  24. package/web/_app/immutable/nodes/3.Do9cwxjS.js +1 -0
  25. package/web/_app/immutable/nodes/4.CGd6gzGJ.js +2 -0
  26. package/web/_app/immutable/nodes/5.Dtac-HK8.js +5 -0
  27. package/web/_app/immutable/nodes/6._-kZkWYe.js +1 -0
  28. package/web/_app/immutable/nodes/7.BdTE6U4H.js +1 -0
  29. package/web/_app/version.json +1 -1
  30. package/web/index.html +17 -17
  31. package/web/_app/immutable/chunks/-7PsQOY4.js +0 -3
  32. package/web/_app/immutable/chunks/BAVeNK5c.js +0 -20
  33. package/web/_app/immutable/chunks/CQqNkDRK.js +0 -1
  34. package/web/_app/immutable/chunks/Cs9sQ8Oa.js +0 -1
  35. package/web/_app/immutable/chunks/DqVFJWdL.js +0 -1
  36. package/web/_app/immutable/chunks/HCrXwzvD.js +0 -1
  37. package/web/_app/immutable/chunks/MjV3DsQW.js +0 -1
  38. package/web/_app/immutable/chunks/uID4v_1K.js +0 -1
  39. package/web/_app/immutable/entry/app.CR9ubCCo.js +0 -2
  40. package/web/_app/immutable/entry/start.D4DOZCP6.js +0 -1
  41. package/web/_app/immutable/nodes/0.BYXv43VP.js +0 -26
  42. package/web/_app/immutable/nodes/1.DGP71l35.js +0 -1
  43. package/web/_app/immutable/nodes/2.CxspTYRG.js +0 -1
  44. package/web/_app/immutable/nodes/3.ZTzs6oEI.js +0 -1
  45. package/web/_app/immutable/nodes/4.CeIGu2c2.js +0 -2
  46. package/web/_app/immutable/nodes/5.k1DmYkGm.js +0 -5
  47. package/web/_app/immutable/nodes/6.Q0yGno8p.js +0 -1
  48. 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
  }
@@ -4365,6 +4365,14 @@ var init_engine = __esmMin((() => {
4365
4365
  get provider() {
4366
4366
  return this.opts.provider ?? OPENROUTER;
4367
4367
  }
4368
+ /** Take another provider into use, from the next request on: a setup kept from the settings (M18). What was
4369
+ * asked of the last one (its key's limit, its model's window) is asked again. */
4370
+ setProvider(provider, http) {
4371
+ this.opts.provider = provider;
4372
+ this.opts.http = http;
4373
+ this.window = void 0;
4374
+ this.keyLimit = void 0;
4375
+ }
4368
4376
  async settings() {
4369
4377
  return {
4370
4378
  provider: this.provider.name,
@@ -4561,7 +4569,7 @@ var init_engine = __esmMin((() => {
4561
4569
  async begin(req, http, asked) {
4562
4570
  if (asked.length > 4e3) throw new EngineError("that is too long to ask at once", 413);
4563
4571
  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);
4572
+ 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
4573
  const { messages, olds } = await this.compose(req, asked);
4566
4574
  const maxTokens = req.form === "insert" ? REPLY_TOKENS : req.form === "note" ? NOTE_TOKENS : Math.max(REPLY_TOKENS, Math.ceil(olds.join("").length / 3) + 1e3);
4567
4575
  const most = (req.form === "insert" || req.form === "rewrite") && req.choice?.lookup === false ? 0 : void 0;
@@ -5506,7 +5514,7 @@ var init_engine = __esmMin((() => {
5506
5514
  async beginTurn(conversation, req, http) {
5507
5515
  const study = this.opts.own;
5508
5516
  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);
5517
+ 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
5518
  const lookupOn = req.lookup !== false, mode = req.mode;
5511
5519
  let w = await this.working({
5512
5520
  ...req,
@@ -6702,7 +6710,11 @@ const textTable = (field, v, env) => v === void 0 ? void 0 : Object.fromEntries(
6702
6710
  * `headers`, `key_header`, `query` and `stream_usage = false`; `max_tokens` for models that think at length; its
6703
6711
  * client certificate is `gatewayTls`'s. */
6704
6712
  function providerConfig(path = configPath(), env = process.env) {
6705
- const config = readConfig(path), p = table(config.provider), tiers = table(config.tiers), prices = table(config.prices);
6713
+ return providerFrom(readConfig(path), env);
6714
+ }
6715
+ /** The provider a configuration names, as `providerConfig` reads it from the file: for a setup not yet kept (M18). */
6716
+ function providerFrom(config, env = process.env) {
6717
+ const p = table(config.provider), tiers = table(config.tiers), prices = table(config.prices);
6706
6718
  const custom = typeof p.url === "string" && p.url !== "";
6707
6719
  const base = custom ? {
6708
6720
  name: typeof p.name === "string" && p.name ? p.name : "custom",
@@ -6733,10 +6745,14 @@ function providerConfig(path = configPath(), env = process.env) {
6733
6745
  return out;
6734
6746
  }
6735
6747
  function gatewayTls(path = configPath(), env = process.env) {
6736
- const p = table(readConfig(path).provider), files = [];
6748
+ return gatewayTlsFrom(readConfig(path), dirname(path), env);
6749
+ }
6750
+ /** The certificate a configuration names, its paths from `dir` (the configuration's folder). */
6751
+ function gatewayTlsFrom(config, dir, env = process.env) {
6752
+ const p = table(config.provider), files = [];
6737
6753
  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);
6754
+ if (p[field] === void 0 || p[field] === "") return void 0;
6755
+ const named = valueOf(field, p[field], env), at = named.startsWith("~/") ? join(homedir(), named.slice(2)) : resolve(dir, named);
6740
6756
  if (!existsSync(at)) throw new Error(`[provider] ${field}: there is no file at ${at}`);
6741
6757
  files.push(at);
6742
6758
  return readFileSync(at);
@@ -6754,6 +6770,10 @@ function gatewayTls(path = configPath(), env = process.env) {
6754
6770
  if (p.pfx_passphrase !== void 0) tls.passphrase = valueOf("pfx_passphrase", p.pfx_passphrase, env);
6755
6771
  return files.length ? tls : null;
6756
6772
  }
6773
+ /** Whether Iso has a model here: a key kept, or a provider of your own (a gateway may need no key). */
6774
+ async function hasModel$1(config = configPath()) {
6775
+ return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
6776
+ }
6757
6777
  //#endregion
6758
6778
  //#region packages/server/src/model.ts
6759
6779
  /** Where the server looks for the model, in order, and why each place is there. */
@@ -6784,7 +6804,7 @@ function modelPlaces(env = process.env) {
6784
6804
  /** Where the model is, or null when it is not installed or is switched off (ISORITE_EMBED=off). */
6785
6805
  function modelDir(env = process.env) {
6786
6806
  if (env.ISORITE_EMBED === "off") return null;
6787
- return modelPlaces(env).find((p) => hasModel$1(p.dir))?.dir ?? null;
6807
+ return modelPlaces(env).find((p) => hasModel(p.dir))?.dir ?? null;
6788
6808
  }
6789
6809
  /** What is in a model's folder. `isorite-model.json` beside the files may give its `name` and `pooling` ("cls",
6790
6810
  * the first token's state, as granite is used; or "mean", the average of them all, as all-MiniLM is). */
@@ -6918,7 +6938,7 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
6918
6938
  * checked by using it: it must load, and give DIM numbers for a sentence. Kept in the user's models, and used from
6919
6939
  * then on. */
6920
6940
  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`);
6941
+ if (!hasModel(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
6922
6942
  const granite = !existsSync(join(from, "isorite-model.json")) && FILES.every(([, to, want]) => existsSync(join(from, to)) && sha(readFileSync(join(from, to))) === want);
6923
6943
  let out = userModelDir(env);
6924
6944
  if (granite) await fetchModel(out, say, { from });
@@ -6955,7 +6975,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
6955
6975
  say(e.message);
6956
6976
  return 1;
6957
6977
  }
6958
- const places = modelPlaces(env), found = places.find((p) => hasModel$1(p.dir));
6978
+ const places = modelPlaces(env), found = places.find((p) => hasModel(p.dir));
6959
6979
  if (found) {
6960
6980
  told(modelInfo(found.dir), found.why);
6961
6981
  say("Nothing to fetch.");
@@ -6977,7 +6997,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
6977
6997
  return 1;
6978
6998
  }
6979
6999
  }
6980
- var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel$1, ModelFetchError, modelFiles;
7000
+ var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
6981
7001
  var init_model = __esmMin((() => {
6982
7002
  init_src();
6983
7003
  NAME$1 = "granite-embedding-small-english-r2";
@@ -7010,7 +7030,7 @@ var init_model = __esmMin((() => {
7010
7030
  sha = (buf) => createHash("sha256").update(buf).digest("hex");
7011
7031
  userModelsDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models");
7012
7032
  userModelDir = (env = process.env) => join(userModelsDir(env), NAME$1);
7013
- hasModel$1 = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
7033
+ hasModel = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
7014
7034
  ModelFetchError = class extends Error {
7015
7035
  tried;
7016
7036
  constructor(tried) {
@@ -7145,16 +7165,17 @@ function gatewayHttp(provider, tls, env = process.env) {
7145
7165
  }
7146
7166
  /** What OpenCode may ask of the provider through isorite. */
7147
7167
  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
7168
+ /** The provider at /api/gateway, as `live` has it: each request is sent on with what `endpoint` adds (the key, where the provider
7149
7169
  * wants it, its headers and query) by `http`, and the reply streamed back. A provider that can't be reached is
7150
7170
  * answered as an OpenAI-compatible one answers an error, so OpenCode shows why. */
7151
- function relay(provider, http, key) {
7171
+ function relay(live, key) {
7152
7172
  const json = (status, message) => new Response(JSON.stringify({ error: { message } }), {
7153
7173
  status,
7154
7174
  headers: { "Content-Type": "application/json" }
7155
7175
  });
7156
7176
  return async (path, req) => {
7157
7177
  if (!RELAYED.has(path)) return json(404, `isorite relays only ${[...RELAYED].join(" and ")}`);
7178
+ const { provider, http } = live();
7158
7179
  const to = endpoint(provider, await key(), path), type = req.headers.get("content-type");
7159
7180
  const body = req.method === "POST" ? await req.text() : void 0;
7160
7181
  let res;
@@ -7641,6 +7662,35 @@ const Revised = z.object({
7641
7662
  text: z.union([z.string(), z.array(z.string()).max(40)])
7642
7663
  }).strict();
7643
7664
  const Key = z.object({ key: z.string().nullable() }).strict();
7665
+ const Pair = z.object({
7666
+ name: z.string().max(200),
7667
+ value: z.string().max(4e3),
7668
+ held: z.boolean().optional()
7669
+ }).strict();
7670
+ const Path = z.string().max(1e3);
7671
+ const SetupDraft = z.object({
7672
+ kind: z.enum(["openrouter", "gateway"]),
7673
+ name: z.string().max(100),
7674
+ url: z.string().max(2e3),
7675
+ keyHeld: z.boolean(),
7676
+ keyHeader: z.string().max(200),
7677
+ headers: z.array(Pair).max(40),
7678
+ query: z.array(Pair).max(40),
7679
+ pfx: Path,
7680
+ passphraseHeld: z.boolean(),
7681
+ cert: Path,
7682
+ certKey: Path,
7683
+ ca: Path,
7684
+ streamUsage: z.boolean(),
7685
+ maxTokens: z.number().int().positive().max(1e6).nullable(),
7686
+ tiers: z.object({
7687
+ low: z.string().max(200),
7688
+ mid: z.string().max(200),
7689
+ max: z.string().max(200)
7690
+ }).strict(),
7691
+ key: z.string().max(400).nullable().optional(),
7692
+ passphrase: z.string().max(1e3).nullable().optional()
7693
+ }).strict();
7644
7694
  const AgentName = z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/);
7645
7695
  const AgentLook = z.object({
7646
7696
  agent: AgentName,
@@ -7666,7 +7716,7 @@ const AgentStatus = z.object({
7666
7716
  session: z.string().min(1).max(200),
7667
7717
  busy: z.boolean()
7668
7718
  }).strict();
7669
- function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway }) {
7719
+ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway, setup }) {
7670
7720
  const app = new Hono();
7671
7721
  const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
7672
7722
  const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
@@ -7681,6 +7731,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
7681
7731
  } catch {}
7682
7732
  return !loopback || LOOPBACK.has(host) || host.endsWith(".ts.net") || allowed.has(host);
7683
7733
  };
7734
+ /** Asked from this machine itself: its own name, and (where the connection says) from its own address. A
7735
+ * tailnet's name is not, even through tailscale serve, whose connections come from this machine. */
7736
+ const local = (c) => {
7737
+ let host = "";
7738
+ try {
7739
+ host = new URL("http://" + (c.req.header("host") ?? "")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
7740
+ } catch {}
7741
+ const from = c.env?.incoming?.socket?.remoteAddress;
7742
+ return LOOPBACK.has(host) && (from === void 0 || /^(127\.|::1$|::ffff:127\.)/.test(from));
7743
+ };
7684
7744
  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
7745
  /** Why a write is refused, or null. */
7686
7746
  const writeRefused = (c) => {
@@ -7829,14 +7889,36 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
7829
7889
  app.get("/api/context", (c) => answer(c, () => api.context(c.req.query("id") || void 0, choiceOf(c.req.query("choice")))));
7830
7890
  post("/api/context/clear", z.object({}).strict(), () => api.clearConversation());
7831
7891
  app.get("/api/settings", (c) => answer(c, () => api.settings()));
7832
- post("/api/settings/key", Key, (_c, b) => api.setKey(b.key), "put");
7892
+ post("/api/settings/key", Key, async (_c, b) => {
7893
+ const s = await api.setKey(b.key);
7894
+ await setup?.changed();
7895
+ return api.settings().catch(() => s);
7896
+ }, "put");
7833
7897
  app.get("/api/spend", (c) => answer(c, () => api.spend()));
7834
7898
  post("/api/settings/spend-cap", z.object({ usd: z.number().positive().max(1e6).nullable() }).strict(), (_c, b) => api.setSpendCap(b.usd), "put");
7835
7899
  post("/api/settings/connect", z.object({ callback: z.string().max(2e3) }).strict(), (_c, b) => api.connect(b.callback));
7836
7900
  post("/api/settings/connected", z.object({
7837
7901
  code: z.string().min(1).max(2e3),
7838
7902
  state: z.string().min(1).max(200)
7839
- }).strict(), (_c, b) => api.connected(b.code, b.state));
7903
+ }).strict(), async (_c, b) => {
7904
+ const s = await api.connected(b.code, b.state);
7905
+ await setup?.changed();
7906
+ return api.settings().catch(() => s);
7907
+ });
7908
+ if (setup) {
7909
+ const FROM_HERE = "The provider's setup is changed only from this machine (localhost): the key goes where it says.";
7910
+ const here = (c) => {
7911
+ if (!local(c)) throw new EngineError(FROM_HERE, 403);
7912
+ };
7913
+ app.get("/api/settings/provider", (c) => answer(c, async () => setup.view(local(c))));
7914
+ post("/api/settings/provider", SetupDraft, (c, b) => {
7915
+ here(c);
7916
+ return setup.keep(b, true);
7917
+ }, "put");
7918
+ post("/api/settings/provider/check", z.object({ draft: SetupDraft.nullable() }).strict(), (c, b) => setup.check(local(c) ? b.draft : null));
7919
+ post("/api/settings/provider/models", z.object({ draft: SetupDraft.nullable() }).strict(), async (c, b) => ({ models: await setup.models(local(c) ? b.draft : null) }));
7920
+ post("/api/settings/report", z.object({}).strict(), async () => ({ text: await setup.report() }));
7921
+ }
7840
7922
  app.get("/api/agent/tools", (c) => answer(c, () => api.agentTools()));
7841
7923
  post("/api/agent/look", AgentLook, (_c, b) => api.agentLook(b));
7842
7924
  app.get("/api/agent/base", (c) => answer(c, async () => ({ notes: notes ?? null })));
@@ -8037,6 +8119,251 @@ var NodeFiles = class {
8037
8119
  }
8038
8120
  };
8039
8121
  //#endregion
8122
+ //#region packages/server/src/setup.ts
8123
+ init_src();
8124
+ /** What the page sent that cannot be kept: said back to it as it is (400). */
8125
+ var SetupError = class extends EngineError {};
8126
+ /** The fields of [provider] the form holds. The rest (spend_cap, cache_marks, context, …) are left as they are. */
8127
+ const FORM = [
8128
+ "url",
8129
+ "name",
8130
+ "auth",
8131
+ "key",
8132
+ "key_header",
8133
+ "headers",
8134
+ "query",
8135
+ "pfx",
8136
+ "pfx_passphrase",
8137
+ "cert",
8138
+ "cert_key",
8139
+ "ca",
8140
+ "stream_usage",
8141
+ "max_tokens"
8142
+ ];
8143
+ const NAMED = /^\$[A-Za-z_][A-Za-z0-9_]*$/;
8144
+ const text = (v) => typeof v === "string" || typeof v === "number" ? String(v) : "";
8145
+ const secretPair = (name, value) => SECRET.test(name) && !NAMED.test(value);
8146
+ /** The setup a configuration holds, its secrets held back. */
8147
+ function readSetup(config) {
8148
+ const p = table(config.provider), tiers = table(config.tiers);
8149
+ const pairs = (v) => Object.entries(table(v)).map(([name, x]) => secretPair(name, text(x)) ? {
8150
+ name,
8151
+ value: "",
8152
+ held: true
8153
+ } : {
8154
+ name,
8155
+ value: text(x)
8156
+ });
8157
+ const gateway = typeof p.url === "string" && p.url !== "";
8158
+ return {
8159
+ kind: gateway ? "gateway" : "openrouter",
8160
+ name: text(p.name),
8161
+ url: gateway ? p.url : OPENROUTER.url,
8162
+ keyHeld: text(p.key) !== "",
8163
+ keyHeader: text(p.key_header),
8164
+ headers: pairs(p.headers),
8165
+ query: pairs(p.query),
8166
+ pfx: text(p.pfx),
8167
+ passphraseHeld: p.pfx_passphrase !== void 0 && text(p.pfx_passphrase) !== "",
8168
+ cert: text(p.cert),
8169
+ certKey: text(p.cert_key),
8170
+ ca: text(p.ca),
8171
+ streamUsage: p.stream_usage !== false,
8172
+ maxTokens: typeof p.max_tokens === "number" && p.max_tokens > 0 ? p.max_tokens : null,
8173
+ tiers: Object.fromEntries(TIERS.map((t) => [t, text(tiers[t]) || OPENROUTER.tiers[t]]))
8174
+ };
8175
+ }
8176
+ /** The configuration with `draft` in place of its setup; and whether the key was forgotten for a new address. */
8177
+ function applySetup(config, draft) {
8178
+ const old = table(config.provider), p = Object.fromEntries(Object.entries(old).filter(([k]) => !FORM.includes(k)));
8179
+ const gateway = draft.kind === "gateway", url = gateway ? draft.url.trim() : "";
8180
+ if (gateway) {
8181
+ let parsed;
8182
+ try {
8183
+ parsed = new URL(url);
8184
+ } catch {
8185
+ throw new SetupError("the gateway's address is not an address: it begins https:// (or http:// on this machine)");
8186
+ }
8187
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new SetupError("the gateway's address begins https:// (or http:// on this machine)");
8188
+ p.url = url.replace(/\/+$/, "");
8189
+ if (draft.name.trim()) p.name = draft.name.trim();
8190
+ }
8191
+ const at = (url) => (url || OPENROUTER.url).replace(/\/+$/, "");
8192
+ const moved = at(text(old.url)) !== at(text(p.url));
8193
+ if (!moved && old.auth !== void 0) p.auth = old.auth;
8194
+ let forgot = false;
8195
+ if (typeof draft.key === "string" && draft.key.trim()) {
8196
+ const key = draft.key.trim();
8197
+ if (key.length > 400 || /\s/.test(key)) throw new SetupError("that does not look like a key");
8198
+ p.key = key;
8199
+ } else if (draft.key === void 0 && old.key !== void 0) {
8200
+ if (moved) forgot = true;
8201
+ else p.key = old.key;
8202
+ }
8203
+ if (gateway) {
8204
+ if (draft.keyHeader.trim()) p.key_header = draft.keyHeader.trim();
8205
+ const pairs = (field, given) => {
8206
+ const before = table(old[field]), out = {};
8207
+ for (const pair of given) {
8208
+ const name = pair.name.trim();
8209
+ if (!name) continue;
8210
+ 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}`);
8211
+ if (pair.held && !pair.value) {
8212
+ if (before[name] !== void 0 && !moved) out[name] = before[name];
8213
+ continue;
8214
+ }
8215
+ out[name] = pair.value;
8216
+ }
8217
+ if (Object.keys(out).length) p[field] = out;
8218
+ };
8219
+ pairs("headers", draft.headers);
8220
+ pairs("query", draft.query);
8221
+ for (const [field, value] of [
8222
+ ["pfx", draft.pfx],
8223
+ ["cert", draft.cert],
8224
+ ["cert_key", draft.certKey],
8225
+ ["ca", draft.ca]
8226
+ ]) if (value.trim()) p[field] = value.trim();
8227
+ if (p.pfx !== void 0) {
8228
+ if (typeof draft.passphrase === "string" && draft.passphrase) p.pfx_passphrase = draft.passphrase;
8229
+ else if (draft.passphrase === void 0 && old.pfx_passphrase !== void 0) p.pfx_passphrase = old.pfx_passphrase;
8230
+ }
8231
+ if (!draft.streamUsage) p.stream_usage = false;
8232
+ if (draft.maxTokens !== null) {
8233
+ if (!(Number.isInteger(draft.maxTokens) && draft.maxTokens > 0 && draft.maxTokens <= 1e6)) throw new SetupError("max_tokens is a whole number of tokens");
8234
+ p.max_tokens = draft.maxTokens;
8235
+ }
8236
+ }
8237
+ const tiers = Object.fromEntries(Object.entries(table(config.tiers)).filter(([k]) => !TIERS.includes(k)));
8238
+ for (const t of TIERS) {
8239
+ const model = draft.tiers[t]?.trim() ?? "";
8240
+ if (model.length > 200 || /\s/.test(model)) throw new SetupError(`the ${t} tier's model is not a model's name`);
8241
+ if (model && (gateway || model !== OPENROUTER.tiers[t])) tiers[t] = model;
8242
+ }
8243
+ const out = {
8244
+ ...config,
8245
+ provider: p
8246
+ };
8247
+ if (Object.keys(tiers).length) out.tiers = tiers;
8248
+ else delete out.tiers;
8249
+ if (!Object.keys(p).length) delete out.provider;
8250
+ return {
8251
+ config: out,
8252
+ forgot
8253
+ };
8254
+ }
8255
+ function reach(config, dir, env = process.env) {
8256
+ const provider = providerFrom(config, env), tls = gatewayTlsFrom(config, dir, env);
8257
+ return {
8258
+ provider,
8259
+ http: gatewayHttp(provider, tls, env),
8260
+ key: text(table(config.provider).key) || null,
8261
+ tls
8262
+ };
8263
+ }
8264
+ /** The setup, for the server's routes. */
8265
+ var Setup = class {
8266
+ path;
8267
+ env;
8268
+ o;
8269
+ constructor(o = {}) {
8270
+ this.o = o;
8271
+ this.env = o.env ?? process.env;
8272
+ this.path = o.config ?? configPath(this.env);
8273
+ }
8274
+ view(local) {
8275
+ return {
8276
+ setup: readSetup(readConfig(this.path)),
8277
+ local,
8278
+ config: this.path
8279
+ };
8280
+ }
8281
+ /** The configuration a draft would make, or the one kept (null). */
8282
+ draft(draft) {
8283
+ const config = readConfig(this.path);
8284
+ return draft ? applySetup(config, draft).config : config;
8285
+ }
8286
+ /** Doctor's checks of the provider, on the draft as if it were kept. Nothing is written. */
8287
+ async check(draft) {
8288
+ const checks = [], say = (step, outcome, said) => void checks.push({
8289
+ step,
8290
+ outcome,
8291
+ said
8292
+ });
8293
+ let r;
8294
+ try {
8295
+ r = reach(this.draft(draft), dirname(this.path), this.env);
8296
+ } catch (e) {
8297
+ say("setup", "fail", e.message);
8298
+ return checks;
8299
+ }
8300
+ if (!r.key && !r.tls && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(r.provider.url)) {
8301
+ say("key", "fail", "none: paste one, or sign in for one");
8302
+ return checks;
8303
+ }
8304
+ await checkProvider(r.provider, r.http, r.key, say);
8305
+ return checks;
8306
+ }
8307
+ /** The models the provider lists, for the tiers to be chosen from. Empty where it has no list. */
8308
+ async models(draft) {
8309
+ let r;
8310
+ try {
8311
+ r = reach(this.draft(draft), dirname(this.path), this.env);
8312
+ } catch (e) {
8313
+ throw new SetupError(e.message);
8314
+ }
8315
+ const to = endpoint(r.provider, r.key, "/models");
8316
+ let res;
8317
+ try {
8318
+ res = await r.http.fetch(to.url, {
8319
+ headers: to.headers,
8320
+ signal: AbortSignal.timeout(2e4)
8321
+ });
8322
+ } catch (e) {
8323
+ throw new SetupError(unreachable(e, r.provider.url));
8324
+ }
8325
+ if (!res.ok) {
8326
+ await res.body?.cancel();
8327
+ return [];
8328
+ }
8329
+ const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id).filter((id) => typeof id === "string");
8330
+ return [...new Set(listed)].sort();
8331
+ }
8332
+ /** 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
8333
+ * first time, since writing it loses its comments. */
8334
+ async keep(draft, local) {
8335
+ const { config } = applySetup(readConfig(this.path), draft);
8336
+ try {
8337
+ reach(config, dirname(this.path), this.env);
8338
+ } catch (e) {
8339
+ throw new SetupError(e.message);
8340
+ }
8341
+ if (existsSync(this.path) && !existsSync(`${this.path}.bak`)) copyFileSync(this.path, `${this.path}.bak`);
8342
+ writeConfig(config, this.path);
8343
+ await this.o.kept?.();
8344
+ return this.view(local);
8345
+ }
8346
+ /** A key was kept or forgotten by the engine: taken into use as a setup is. */
8347
+ async changed() {
8348
+ await this.o.kept?.();
8349
+ }
8350
+ /** isorite doctor --report, as one block with its secrets hidden. */
8351
+ async report() {
8352
+ const lines = [];
8353
+ await doctor(this.o.notes, {
8354
+ report: true,
8355
+ env: this.env,
8356
+ out: (l) => lines.push(l),
8357
+ ...this.o.version ? { version: this.o.version } : {},
8358
+ ...this.o.port ? { port: this.o.port } : {}
8359
+ });
8360
+ return lines.join("\n");
8361
+ }
8362
+ };
8363
+ //#endregion
8364
+ //#region packages/server/src/version.ts
8365
+ const VERSION = "0.6.0";
8366
+ //#endregion
8040
8367
  //#region packages/server/src/studybase.ts
8041
8368
  init_src$1();
8042
8369
  function git(cwd, ...args) {
@@ -8131,20 +8458,49 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
8131
8458
  } : {}
8132
8459
  });
8133
8460
  }
8461
+ /** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
8462
+ const { auth: _auth, ...openrouter } = OPENROUTER;
8463
+ const FINDER = {
8464
+ ...openrouter,
8465
+ name: "finder",
8466
+ url: "http://finder.invalid/v1"
8467
+ };
8468
+ /** The provider as the configuration has it now: kept from the settings (M18), it is taken into use at once, by
8469
+ * Iso and the relay. With `stand`, that is asked while there is no model here. */
8470
+ async function liveProvider(config, stand) {
8471
+ if (stand && !await hasModel$1(config)) return {
8472
+ provider: FINDER,
8473
+ http: stand
8474
+ };
8475
+ const provider = providerConfig(config);
8476
+ return {
8477
+ provider,
8478
+ http: gatewayHttp(provider, gatewayTls(config))
8479
+ };
8480
+ }
8134
8481
  /** A link most terminals let you click (OSC 8), when the output is a terminal; the address as it is otherwise. */
8135
8482
  const link$1 = (url, tty = Boolean(process.stdout.isTTY)) => tty ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url;
8136
8483
  /** `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. */
8484
+ * (M16). It is asked while there is no model here (no key, no provider of your own), and needs no key. */
8138
8485
  async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand } = {}) {
8139
8486
  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;
8487
+ const config = configPath();
8488
+ const current = () => liveProvider(config, stand);
8489
+ let live = await current();
8490
+ const secrets = new ConfigSecrets(config);
8491
+ const api = await openEngine(opened, deviceId(config), config, live.http, modelDir(), secrets);
8492
+ api.setProvider(live.provider, live.http);
8493
+ const setup = new Setup({
8494
+ config,
8495
+ notes: opened.notes,
8496
+ version: VERSION,
8497
+ port,
8498
+ kept: async () => {
8499
+ live = await current();
8500
+ api.setProvider(live.provider, live.http);
8501
+ }
8502
+ });
8503
+ const gateway = LOOPBACK.has(host) ? relay(() => live, () => secrets.get(KEY)) : void 0;
8148
8504
  const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
8149
8505
  const terminal = new Terminal();
8150
8506
  const app = createApp({
@@ -8155,7 +8511,8 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
8155
8511
  allowHosts,
8156
8512
  notes: opened.notes,
8157
8513
  terminal,
8158
- gateway
8514
+ gateway,
8515
+ setup
8159
8516
  });
8160
8517
  const server = serve({
8161
8518
  fetch: app.fetch,
@@ -8502,6 +8859,65 @@ const TOOL = {
8502
8859
  }
8503
8860
  }
8504
8861
  };
8862
+ /** The provider asked, as doctor asks it and as the page's check does (M18): its list of models, a short reply from
8863
+ * each tier's model, and one tool call. A few hundred tokens in all. */
8864
+ async function checkProvider(p, h, key, say) {
8865
+ const models = [.../* @__PURE__ */ new Set([
8866
+ p.tiers.low,
8867
+ p.tiers.mid,
8868
+ p.tiers.max
8869
+ ])];
8870
+ let reached = true;
8871
+ try {
8872
+ const to = endpoint(p, key, "/models");
8873
+ const res = await h.fetch(to.url, {
8874
+ headers: to.headers,
8875
+ signal: AbortSignal.timeout(2e4)
8876
+ });
8877
+ if (res.ok) {
8878
+ const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id ?? "");
8879
+ const missing = models.filter((m) => !listed.includes(m));
8880
+ say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
8881
+ 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]`);
8882
+ } 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)`);
8883
+ } catch (e) {
8884
+ reached = false;
8885
+ say("reached", "fail", unreachable(e, p.url));
8886
+ }
8887
+ if (reached) {
8888
+ for (const model of models) try {
8889
+ const reply = await complete(h, p, key, {
8890
+ model,
8891
+ maxTokens: 16,
8892
+ messages: [{
8893
+ role: "user",
8894
+ content: "Reply with the one word: ready"
8895
+ }],
8896
+ signal: AbortSignal.timeout(6e4)
8897
+ });
8898
+ const usage = reply.usage.tokensIn + reply.usage.tokensOut > 0;
8899
+ 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"}`);
8900
+ } catch (e) {
8901
+ const said = e.message;
8902
+ say(`reply from ${model}`, "fail", /stream_options/i.test(said) ? `${said} Set stream_usage = false under [provider].` : said);
8903
+ }
8904
+ try {
8905
+ const reply = await complete(h, p, key, {
8906
+ model: p.tiers.mid,
8907
+ maxTokens: 64,
8908
+ tools: [TOOL],
8909
+ messages: [{
8910
+ role: "user",
8911
+ content: "Ping, using the ping tool."
8912
+ }],
8913
+ signal: AbortSignal.timeout(6e4)
8914
+ });
8915
+ 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`);
8916
+ } catch (e) {
8917
+ say("tools", "fail", e instanceof ModelError ? e.message : String(e));
8918
+ }
8919
+ }
8920
+ }
8505
8921
  /** Run the checks; 0 when none failed. */
8506
8922
  async function doctor(folder, o = {}) {
8507
8923
  const env = o.env ?? process.env, out = o.out ?? console.log, checks = [];
@@ -8515,7 +8931,7 @@ async function doctor(folder, o = {}) {
8515
8931
  };
8516
8932
  const path = configPath(env), execArgv = o.execArgv ?? process.execArgv;
8517
8933
  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)`);
8934
+ 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
8935
  try {
8520
8936
  readConfig(path);
8521
8937
  provider = providerConfig(path, env);
@@ -8523,7 +8939,7 @@ async function doctor(folder, o = {}) {
8523
8939
  const tls = gatewayTls(path, env);
8524
8940
  http ??= gatewayHttp(provider, tls, env);
8525
8941
  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]");
8942
+ 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
8943
  if (tls) say("certificate", "ok", `${tls.files.join(", ")}${tls.pfx || tls.cert ? "" : " (an authority only, no client certificate)"}`);
8528
8944
  const tiers = `low ${provider.tiers.low}, mid ${provider.tiers.mid}, max ${provider.tiers.max}`;
8529
8945
  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 +8963,7 @@ async function doctor(folder, o = {}) {
8547
8963
  p.tiers.max
8548
8964
  ])];
8549
8965
  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
- }
8966
+ await checkProvider(p, h, key, say);
8600
8967
  }
8601
8968
  try {
8602
8969
  const found = findOpenCode(env, o.probe);
@@ -10015,7 +10382,6 @@ function loadGuide(dir = GUIDE_DIR) {
10015
10382
  }
10016
10383
  //#endregion
10017
10384
  //#region packages/server/src/guide.ts
10018
- init_src();
10019
10385
  init_src$1();
10020
10386
  const GUIDE_FOLDER = join(homedir(), "isorite-guide");
10021
10387
  /** Write the guide into `folder`, unless it is there already. Returns whether it was written now. A folder that
@@ -10037,19 +10403,15 @@ function writeGuide(folder) {
10037
10403
  fresh: true
10038
10404
  };
10039
10405
  }
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
10406
  async function guide(folder = GUIDE_FOLDER, { port = 4767, out = console.log } = {}) {
10045
10407
  const { notes, fresh } = writeGuide(folder);
10046
- const model = await hasModel();
10408
+ const model = await hasModel$1();
10047
10409
  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
10410
  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
10411
  out(` start at http://localhost:${port}/#/n/${START}`);
10050
10412
  await serve$1(notes, {
10051
10413
  port,
10052
- ...model ? {} : { stand: finderHttp(16) }
10414
+ stand: finderHttp(16)
10053
10415
  });
10054
10416
  if (process.env.ISORITE_NO_BROWSER !== "1") openBrowser(`http://localhost:${port}/#/n/${START}`);
10055
10417
  }
@@ -10126,7 +10488,6 @@ async function writeGeometry(folder, now = Date.now()) {
10126
10488
  //#region packages/server/src/main.ts
10127
10489
  init_src$1();
10128
10490
  init_model();
10129
- const VERSION = "0.5.0";
10130
10491
  const USAGE = `isorite: an agentic harness for understanding
10131
10492
 
10132
10493
  isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/