isorite 0.8.0 → 0.9.1
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.
- package/dist/main.js +411 -94
- package/docs/setup-for-agents.md +9 -5
- package/docs/setup.md +31 -4
- package/docs/terminal.md +9 -3
- package/package.json +1 -1
- package/plugin/tui.tsx +6 -0
- package/web/_app/immutable/chunks/{BtUtjfIh.js → B3aPQAYq.js} +1 -1
- package/web/_app/immutable/chunks/{D_JZ0swv.js → BXF2XoRC.js} +1 -1
- package/web/_app/immutable/chunks/{hOAYc4Tf.js → CBWiKnLz.js} +1 -1
- package/web/_app/immutable/chunks/{Di_ac7WN.js → CJ1dhP0p.js} +1 -1
- package/web/_app/immutable/chunks/{BrXPCJaM.js → CLkY0zVi.js} +1 -1
- package/web/_app/immutable/chunks/{CnP_Si9g.js → D-aFnd2T.js} +1 -1
- package/web/_app/immutable/chunks/{DgZW3JtG.js → DDuWClBR.js} +1 -1
- package/web/_app/immutable/chunks/{BjPiibIz.js → DII7jNnq.js} +1 -1
- package/web/_app/immutable/chunks/{CGzfi9Nl.js → DRQEUdad.js} +1 -1
- package/web/_app/immutable/chunks/{NmqB5ax3.js → DacMdKjW.js} +1 -1
- package/web/_app/immutable/chunks/{B91efLFT.js → DcgxJ8bF.js} +3 -3
- package/web/_app/immutable/chunks/{CCiHXe5d.js → Ddgejj-u.js} +1 -1
- package/web/_app/immutable/chunks/{dvrIP-Hm.js → GRrQoDGJ.js} +1 -1
- package/web/_app/immutable/chunks/{B-65IYX-.js → vUsuwDSf.js} +1 -1
- package/web/_app/immutable/entry/{app.DO9lj7h3.js → app.CswuY3sh.js} +2 -2
- package/web/_app/immutable/entry/start.D8ZvSyBD.js +1 -0
- package/web/_app/immutable/nodes/0.DBoa3NAQ.js +26 -0
- package/web/_app/immutable/nodes/{1.BRTPLKyU.js → 1.DrwFerb8.js} +1 -1
- package/web/_app/immutable/nodes/2.DPmbppXV.js +1 -0
- package/web/_app/immutable/nodes/3.BW7AbIrS.js +1 -0
- package/web/_app/immutable/nodes/{4.DYW7Fxdy.js → 4.B5HAyMKs.js} +1 -1
- package/web/_app/immutable/nodes/{5.Dkvkif0E.js → 5.BvScSoCM.js} +1 -1
- package/web/_app/immutable/nodes/6.Bna6qY2G.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +14 -14
- package/web/_app/immutable/entry/start.Bg5TZs6n.js +0 -1
- package/web/_app/immutable/nodes/0.N1e1uAFq.js +0 -26
- package/web/_app/immutable/nodes/2.BWh1AYEd.js +0 -1
- package/web/_app/immutable/nodes/3.Cgqgn7Zv.js +0 -1
- package/web/_app/immutable/nodes/6.BinB0fuq.js +0 -1
package/dist/main.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { Document, Scalar, isMap, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
6
6
|
import MarkdownIt from "markdown-it";
|
|
7
7
|
import { parse, stringify as stringify$1 } from "smol-toml";
|
|
8
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
8
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
10
|
import { arch, constants, homedir, hostname, platform, release, tmpdir } from "node:os";
|
|
10
11
|
import { createSecureContext, getCACertificates, setDefaultCACertificates } from "node:tls";
|
|
11
|
-
import {
|
|
12
|
+
import { get } from "node:http";
|
|
12
13
|
import { Worker } from "node:worker_threads";
|
|
13
14
|
import { Agent, request } from "node:https";
|
|
14
15
|
import { Readable } from "node:stream";
|
|
@@ -18,8 +19,10 @@ import { streamSSE } from "hono/streaming";
|
|
|
18
19
|
import { z } from "zod";
|
|
19
20
|
import { OpenCode } from "@opencode/client";
|
|
20
21
|
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
22
|
+
import { createInterface } from "node:readline/promises";
|
|
21
23
|
import { createServer } from "node:net";
|
|
22
|
-
import {
|
|
24
|
+
import { isSea } from "node:sea";
|
|
25
|
+
import { createInterface as createInterface$1 } from "node:readline";
|
|
23
26
|
//#region \0rolldown/runtime.js
|
|
24
27
|
var __defProp = Object.defineProperty;
|
|
25
28
|
var __esmMin = (fn, res, err) => () => {
|
|
@@ -1944,19 +1947,50 @@ function unreachable(err, url) {
|
|
|
1944
1947
|
try {
|
|
1945
1948
|
host = new URL(url).host;
|
|
1946
1949
|
} catch {}
|
|
1947
|
-
const
|
|
1950
|
+
const proxy = /Proxy response \((\d{3})\)/.exec(said)?.[1];
|
|
1951
|
+
const advice = proxy === "407" ? "The proxy (HTTPS_PROXY), not the provider, refused with 407: it wants credentials of its own, as http://user:password@proxy:port in HTTPS_PROXY, or its own sign-in. isorite can't answer a proxy that signs in by Kerberos or NTLM." : proxy ? `The proxy (HTTPS_PROXY) answered ${proxy}, not the provider: check HTTPS_PROXY, and NO_PROXY for hosts reached directly.` : UNTRUSTED.has(code) ? `Its certificate is signed by an authority not trusted here, often a company's own. Add it to the system's authorities (isorite trusts those), or set NODE_EXTRA_CA_CERTS to its certificate (a .pem), or name that as ca in [provider].` : code === "CERT_HAS_EXPIRED" ? "Its certificate has expired: that is the gateway's to fix." : code === "ERR_TLS_CERT_ALTNAME_INVALID" ? `Its certificate is not for ${host}: check the url in [provider].` : /CERTIFICATE_REQUIRED|HANDSHAKE_FAILURE|BAD_CERTIFICATE|UNKNOWN_CA/.test(code) ? "It asks for a client certificate (mTLS) and was given none it accepts: name yours as pfx (with pfx_passphrase), or cert and cert_key, in [provider]." : code === "UND_ERR_SOCKET" && url.startsWith("https:") ? "It closed the connection as it was being set up. A gateway that asks for a client certificate (mTLS) does this: name yours as pfx, or cert and cert_key, in [provider]." : code === "ENOTFOUND" || code === "EAI_AGAIN" ? `The name ${host} could not be found: check the url in [provider], and whether it is only found on the company's network or VPN.` : code === "ECONNREFUSED" ? `Nothing answers at ${host}: check the url and its port in [provider].` : code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ENETUNREACH" || code === "EHOSTUNREACH" ? `${host} did not answer. If the company's network reaches it through a proxy, set HTTPS_PROXY.` : "";
|
|
1948
1952
|
return [
|
|
1949
1953
|
said || err?.message || "no reason given",
|
|
1950
1954
|
code && !said.includes(code) ? ` (${code})` : "",
|
|
1951
1955
|
advice ? `. ${advice}` : ""
|
|
1952
1956
|
].join("");
|
|
1953
1957
|
}
|
|
1958
|
+
/** The header the key goes in, and what goes before it: Authorization with "Bearer " unless said otherwise (M21).
|
|
1959
|
+
* `key_header = "authorization"` alone means Bearer too: a bare key in Authorization is rarely what a gateway
|
|
1960
|
+
* wants. `key_prefix = ""` sends it bare. */
|
|
1961
|
+
function keySent(p) {
|
|
1962
|
+
const header = p.keyHeader || "Authorization";
|
|
1963
|
+
return {
|
|
1964
|
+
header,
|
|
1965
|
+
prefix: p.keyPrefix ?? (header.toLowerCase() === "authorization" ? "Bearer " : "")
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
/** How the key is sent, as said to the person: `Authorization: Bearer <41 characters>`. */
|
|
1969
|
+
function keySentAs(p, key) {
|
|
1970
|
+
const { header, prefix } = keySent(p);
|
|
1971
|
+
return `${header}: ${prefix}<${key.length} characters>`;
|
|
1972
|
+
}
|
|
1973
|
+
/** What a provider said with a refusal: its `error.message` (or `message`, or `error`), else its text without
|
|
1974
|
+
* markup, cut to 300 characters, and never the key, should it say it back. */
|
|
1975
|
+
function refusal(body, key = null) {
|
|
1976
|
+
let said = "";
|
|
1977
|
+
try {
|
|
1978
|
+
const j = JSON.parse(body);
|
|
1979
|
+
said = (typeof j.error === "string" ? j.error : j.error?.message) ?? j.message ?? "";
|
|
1980
|
+
} catch {
|
|
1981
|
+
said = body.replace(/<[^>]+>/g, " ");
|
|
1982
|
+
}
|
|
1983
|
+
said = said.replace(/\s+/g, " ").trim();
|
|
1984
|
+
if (key && key.length >= 8) said = said.split(key).join("<the key>");
|
|
1985
|
+
return said.length > 300 ? said.slice(0, 299) + "…" : said;
|
|
1986
|
+
}
|
|
1954
1987
|
/** The address of `path` at the provider, and the headers that go with every request to it: its key, where it
|
|
1955
1988
|
* wants it, and its own. */
|
|
1956
1989
|
function endpoint(p, key, path) {
|
|
1957
1990
|
const url = new URL(p.url.replace(/\/+$/, "") + path);
|
|
1958
1991
|
for (const [name, value] of Object.entries(p.query ?? {})) url.searchParams.set(name, value);
|
|
1959
|
-
const
|
|
1992
|
+
const sent = keySent(p);
|
|
1993
|
+
const auth = !key ? {} : { [sent.header]: sent.prefix + key };
|
|
1960
1994
|
return {
|
|
1961
1995
|
url: url.toString(),
|
|
1962
1996
|
headers: {
|
|
@@ -2023,13 +2057,7 @@ async function completeOnce(http, p, key, call, heard) {
|
|
|
2023
2057
|
if (!res.ok || !res.body) {
|
|
2024
2058
|
let detail = "";
|
|
2025
2059
|
try {
|
|
2026
|
-
|
|
2027
|
-
try {
|
|
2028
|
-
const j = JSON.parse(said);
|
|
2029
|
-
detail = (typeof j.error === "string" ? j.error : j.error?.message) ?? j.message ?? "";
|
|
2030
|
-
} catch {
|
|
2031
|
-
detail = said.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
2032
|
-
}
|
|
2060
|
+
detail = refusal(await res.text(), key);
|
|
2033
2061
|
} catch {}
|
|
2034
2062
|
if (DOWN.has(res.status)) throw new ModelError(`${p.name} is unavailable (${res.status}${detail ? `: ${detail}` : ""})`, 502, {
|
|
2035
2063
|
kind: "unavailable",
|
|
@@ -2041,7 +2069,7 @@ async function completeOnce(http, p, key, call, heard) {
|
|
|
2041
2069
|
retry: true,
|
|
2042
2070
|
after: afterOf(res)
|
|
2043
2071
|
});
|
|
2044
|
-
const advice = res.status === 401 ?
|
|
2072
|
+
const advice = res.status === 401 ? ` The key was not accepted${key ? `, sent as ${keySentAs(p, key)}` : ""}: set it again in settings, or check key_header and key_prefix.` : res.status === 402 ? " The key's credit is spent." : res.status === 404 ? ` ${p.name} may have no model called ${call.model}${p.defaultTiers ? ", which is OpenRouter's name for it: name this provider's models under [tiers] in config.toml" : ""}.` : "";
|
|
2045
2073
|
throw new ModelError(`${p.name} said ${res.status}${detail ? `: ${detail}` : ""}.${advice}`, res.status === 401 || res.status === 403 ? 409 : 502);
|
|
2046
2074
|
}
|
|
2047
2075
|
let text = "", usage = null, finish = "";
|
|
@@ -6697,7 +6725,7 @@ function writeHome(dir, { gateway = null, plugin = pluginDir, instructions = []
|
|
|
6697
6725
|
recursive: true,
|
|
6698
6726
|
force: true
|
|
6699
6727
|
});
|
|
6700
|
-
symlinkSync(plugin, link, "dir");
|
|
6728
|
+
symlinkSync(plugin, link, process.platform === "win32" ? "junction" : "dir");
|
|
6701
6729
|
}
|
|
6702
6730
|
return dir;
|
|
6703
6731
|
}
|
|
@@ -6724,12 +6752,14 @@ function withLocal(config, local) {
|
|
|
6724
6752
|
};
|
|
6725
6753
|
}
|
|
6726
6754
|
function linksTo(path, target) {
|
|
6755
|
+
const plain = (p) => resolve(p).replace(/[\\/]+$/, "");
|
|
6727
6756
|
try {
|
|
6728
|
-
return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target;
|
|
6757
|
+
return lstatSync(path).isSymbolicLink() && plain(readlinkSync(path)) === plain(target);
|
|
6729
6758
|
} catch {
|
|
6730
6759
|
return false;
|
|
6731
6760
|
}
|
|
6732
6761
|
}
|
|
6762
|
+
const PACKAGE$1 = `@opencode/cli@2.0.32`;
|
|
6733
6763
|
/** The version a `--version` line names, as "2.0.32", or null. */
|
|
6734
6764
|
function parseVersion(output) {
|
|
6735
6765
|
return output.match(/(\d+)\.(\d+)\.(\d+)/)?.[0] ?? null;
|
|
@@ -6749,20 +6779,57 @@ const probe = (command) => {
|
|
|
6749
6779
|
return null;
|
|
6750
6780
|
}
|
|
6751
6781
|
};
|
|
6752
|
-
/**
|
|
6753
|
-
|
|
6782
|
+
/** isorite's own prefix for OpenCode 2. Not ~/.config/isorite/opencode, which is OpenCode's configuration as
|
|
6783
|
+
* `term` writes it (home.ts): an npm prefix there would put node_modules among it. */
|
|
6784
|
+
function ownPrefix(env = process.env) {
|
|
6785
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "isorite", "opencode-cli");
|
|
6786
|
+
}
|
|
6787
|
+
/** The OpenCode 2 in isorite's own prefix, as npm links it; on Windows, the package's own opencode.exe, as npm's
|
|
6788
|
+
* opencode2.cmd can't be run without a shell (Node refuses .cmd files since 2024). */
|
|
6789
|
+
const ownCommand = (env = process.env, platform = process.platform) => platform === "win32" ? join(ownPrefix(env), "node_modules", "@opencode", "cli", "bin", "opencode.exe") : join(ownPrefix(env), "node_modules", ".bin", "opencode2");
|
|
6790
|
+
/** A command on the PATH as Windows can run it without a shell: `<name>.exe`, or for npm's `<name>.cmd` the
|
|
6791
|
+
* package's opencode.exe beside it, in the global node_modules. Null when there is neither. */
|
|
6792
|
+
function onWindowsPath(name, env = process.env, exists = existsSync) {
|
|
6793
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(win32.delimiter).filter(Boolean)) {
|
|
6794
|
+
if (exists(win32.join(dir, `${name}.exe`))) return win32.join(dir, `${name}.exe`);
|
|
6795
|
+
const exe = win32.join(dir, "node_modules", "@opencode", "cli", "bin", "opencode.exe");
|
|
6796
|
+
if (exists(win32.join(dir, `${name}.cmd`)) && exists(exe)) return exe;
|
|
6797
|
+
}
|
|
6798
|
+
return null;
|
|
6799
|
+
}
|
|
6800
|
+
/** The line that installs OpenCode 2 for isorite alone, to copy. */
|
|
6801
|
+
const installLine = (env = process.env) => `isorite opencode (npm install --prefix ${ownPrefix(env)} ${PACKAGE$1})`;
|
|
6802
|
+
/** OpenCode 2 wasn't found. `old` is an OpenCode 1 that was, if one was. */
|
|
6803
|
+
var NoOpenCode = class extends Error {
|
|
6804
|
+
old;
|
|
6805
|
+
constructor(tried, old, env) {
|
|
6806
|
+
super((old ? `isorite term needs OpenCode 2; ${old} is OpenCode 1 (${tried.join("; ")}).\n` : `isorite term needs OpenCode 2 (${tried.join("; ")}).\n`) + ` Install it for isorite only, leaving the global one as it is: ${installLine(env)}\n or name one with: ISORITE_OPENCODE=/path/to/opencode isorite term`);
|
|
6807
|
+
this.old = old;
|
|
6808
|
+
}
|
|
6809
|
+
};
|
|
6810
|
+
/** ISORITE_OPENCODE if set, else isorite's own prefix's, else `opencode2`, else `opencode`: the first that is
|
|
6811
|
+
* OpenCode 2. */
|
|
6812
|
+
function findOpenCode(env = process.env, run = probe, platform = process.platform, where = onWindowsPath) {
|
|
6754
6813
|
const tried = [];
|
|
6755
|
-
|
|
6756
|
-
|
|
6814
|
+
let old = null;
|
|
6815
|
+
const candidates = env.ISORITE_OPENCODE ? [[env.ISORITE_OPENCODE, "ISORITE_OPENCODE"]] : [
|
|
6816
|
+
[ownCommand(env, platform), "isorite opencode"],
|
|
6817
|
+
["opencode2", "the PATH"],
|
|
6818
|
+
["opencode", "the PATH"]
|
|
6819
|
+
];
|
|
6820
|
+
for (const [named, from] of candidates) {
|
|
6821
|
+
const command = platform === "win32" && from === "the PATH" ? where(named, env) ?? named : named;
|
|
6757
6822
|
const output = run(command);
|
|
6758
6823
|
const version = output === null ? null : parseVersion(output);
|
|
6759
6824
|
if (version && Number(version.split(".")[0]) === 2) return {
|
|
6760
6825
|
command,
|
|
6761
|
-
version
|
|
6826
|
+
version,
|
|
6827
|
+
from
|
|
6762
6828
|
};
|
|
6763
|
-
|
|
6829
|
+
if (version && Number(version.split(".")[0]) < 2) old ??= `${command} ${version}`;
|
|
6830
|
+
if (output !== null || from !== "isorite opencode") tried.push(output === null ? `${command}: not found` : `${command}: ${version ?? "unknown version"}`);
|
|
6764
6831
|
}
|
|
6765
|
-
throw new
|
|
6832
|
+
throw new NoOpenCode(tried, old, env);
|
|
6766
6833
|
}
|
|
6767
6834
|
//#endregion
|
|
6768
6835
|
//#region packages/server/src/config.ts
|
|
@@ -6850,6 +6917,7 @@ function providerFrom(config, env = process.env) {
|
|
|
6850
6917
|
...headers ? { headers } : {},
|
|
6851
6918
|
...query ? { query } : {},
|
|
6852
6919
|
...typeof p.key_header === "string" && p.key_header ? { keyHeader: p.key_header } : {},
|
|
6920
|
+
...typeof p.key_prefix === "string" ? { keyPrefix: p.key_prefix } : {},
|
|
6853
6921
|
...p.stream_usage === false ? { streamUsage: false } : {},
|
|
6854
6922
|
...typeof p.max_tokens === "number" && p.max_tokens > 0 ? { maxTokens: Math.floor(p.max_tokens) } : {}
|
|
6855
6923
|
};
|
|
@@ -6953,6 +7021,23 @@ function lendBase(name, lend, path = configPath()) {
|
|
|
6953
7021
|
}
|
|
6954
7022
|
//#endregion
|
|
6955
7023
|
//#region packages/server/src/model.ts
|
|
7024
|
+
/** Where a mirror keeps the .tgz (M21): ISORITE_MODEL_BASE_URL names the folder it's in, tried before the rest. */
|
|
7025
|
+
function mirrorOf(env = process.env) {
|
|
7026
|
+
const base = env.ISORITE_MODEL_BASE_URL?.trim();
|
|
7027
|
+
return base ? `${base.replace(/\/+$/, "")}/${RELEASE_FILE}` : null;
|
|
7028
|
+
}
|
|
7029
|
+
/** How to fetch the model by hand, to copy: the file, its SHA-256, and the command that takes it. */
|
|
7030
|
+
function byHand(env = process.env) {
|
|
7031
|
+
const mirror = mirrorOf(env);
|
|
7032
|
+
return [
|
|
7033
|
+
"Fetch this file another way (a browser, or a machine that can reach it):",
|
|
7034
|
+
` ${RELEASE}`,
|
|
7035
|
+
...mirror ? [` or from your mirror: ${mirror}`] : [],
|
|
7036
|
+
` SHA-256 ${RELEASE_SHA256}`,
|
|
7037
|
+
"then run:",
|
|
7038
|
+
` isorite model --from ~/Downloads/${RELEASE_FILE}`
|
|
7039
|
+
];
|
|
7040
|
+
}
|
|
6956
7041
|
/** Where the server looks for the model, in order, and why each place is there. */
|
|
6957
7042
|
function modelPlaces(env = process.env) {
|
|
6958
7043
|
if (env.ISORITE_EMBED_DIR) return [{
|
|
@@ -7042,7 +7127,7 @@ function unpack(pkg) {
|
|
|
7042
7127
|
}
|
|
7043
7128
|
/** Fetch the model's files into `out`, keeping only files whose SHA-256 is the one expected: from a folder if one is
|
|
7044
7129
|
* named; else from npm, then the GitHub release, then Hugging Face, each tried in turn. */
|
|
7045
|
-
async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release = RELEASE, hub = HUB, files = FILES } = {}) {
|
|
7130
|
+
async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release = RELEASE, mirror = null, url, hub = HUB, files = FILES, tgzSha = files === FILES ? RELEASE_SHA256 : null } = {}) {
|
|
7046
7131
|
mkdirSync(out, { recursive: true });
|
|
7047
7132
|
const missing = () => files.filter(([, to, want]) => !(existsSync(join(out, to)) && sha(readFileSync(join(out, to))) === want));
|
|
7048
7133
|
for (const [, to] of files) if (!missing().some((f) => f[1] === to)) say(`${to}: already here`);
|
|
@@ -7071,31 +7156,42 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
|
|
|
7071
7156
|
}
|
|
7072
7157
|
};
|
|
7073
7158
|
const why = (e, url) => e.message === "fetch failed" || e.cause ? unreachable(e, url) : e.message;
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
} catch (e) {
|
|
7077
|
-
tried.push(`npm (${pkg}): ${e.message}`);
|
|
7078
|
-
say(`From npm (${pkg}): ${e.message}`);
|
|
7079
|
-
}
|
|
7080
|
-
if (missing().length && release) {
|
|
7159
|
+
/** The .tgz at an address, checked by its SHA-256 before it's unpacked; a failure is kept in `tried`. */
|
|
7160
|
+
const fromWeb = async (where, address) => {
|
|
7081
7161
|
const tmp = mkdtempSync(join(tmpdir(), "isorite-model-release-"));
|
|
7082
7162
|
try {
|
|
7083
|
-
say(`From
|
|
7084
|
-
const res = await fetch(
|
|
7163
|
+
say(`From ${where}: ${address}`);
|
|
7164
|
+
const res = await fetch(address);
|
|
7085
7165
|
if (!res.ok) throw new Error(`it said ${res.status}`);
|
|
7086
|
-
const
|
|
7087
|
-
|
|
7166
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
7167
|
+
if (tgzSha && sha(buf) !== tgzSha) throw new Error(`not the file expected: its SHA-256 is ${sha(buf)}, not ${tgzSha}, so it was not unpacked`);
|
|
7168
|
+
const tgz = join(tmp, basename(new URL(address).pathname) || "isorite-model-granite-1.0.0.tgz");
|
|
7169
|
+
writeFileSync(tgz, buf);
|
|
7088
7170
|
fromPackage(tgz);
|
|
7089
7171
|
} catch (e) {
|
|
7090
|
-
tried.push(
|
|
7091
|
-
say(`From
|
|
7172
|
+
tried.push(`${where} (${address}): ${why(e, address)}`);
|
|
7173
|
+
say(`From ${where}: ${why(e, address)}`);
|
|
7092
7174
|
} finally {
|
|
7093
7175
|
rmSync(tmp, {
|
|
7094
7176
|
recursive: true,
|
|
7095
7177
|
force: true
|
|
7096
7178
|
});
|
|
7097
7179
|
}
|
|
7180
|
+
};
|
|
7181
|
+
if (url) {
|
|
7182
|
+
await fromWeb("--url", url);
|
|
7183
|
+
if (missing().length) throw new ModelFetchError(tried);
|
|
7184
|
+
say(`The model is in ${out}`);
|
|
7185
|
+
return;
|
|
7098
7186
|
}
|
|
7187
|
+
if (missing().length && mirror) await fromWeb("the mirror (ISORITE_MODEL_BASE_URL)", mirror);
|
|
7188
|
+
if (missing().length && pkg) try {
|
|
7189
|
+
fromPackage(pkg);
|
|
7190
|
+
} catch (e) {
|
|
7191
|
+
tried.push(`npm (${pkg}): ${e.message}`);
|
|
7192
|
+
say(`From npm (${pkg}): ${e.message}`);
|
|
7193
|
+
}
|
|
7194
|
+
if (missing().length && release) await fromWeb("GitHub", release);
|
|
7099
7195
|
if (missing().length && hub) {
|
|
7100
7196
|
say("From Hugging Face");
|
|
7101
7197
|
try {
|
|
@@ -7115,6 +7211,19 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
|
|
|
7115
7211
|
* checked by using it: it must load, and give DIM numbers for a sentence. Kept in the user's models, and used from
|
|
7116
7212
|
* then on. */
|
|
7117
7213
|
async function installFrom(from, say = console.log, env = process.env) {
|
|
7214
|
+
if (/\.(tgz|tar\.gz)$/.test(from) && existsSync(from) && statSync(from).isFile()) {
|
|
7215
|
+
const got = sha(readFileSync(from));
|
|
7216
|
+
if (got !== "8d8ab528ae289dfc81fd2bfa80c867ca0c8342a2d98f7c86141df4b15fdf028c") throw new Error(`${from} is not ${RELEASE_FILE}: its SHA-256 is ${got}, not ${RELEASE_SHA256}`);
|
|
7217
|
+
const dir = unpack(resolve(from));
|
|
7218
|
+
try {
|
|
7219
|
+
return await installFrom(join(dir, "package"), say, env);
|
|
7220
|
+
} finally {
|
|
7221
|
+
rmSync(dir, {
|
|
7222
|
+
recursive: true,
|
|
7223
|
+
force: true
|
|
7224
|
+
});
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7118
7227
|
if (!hasModel(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
|
|
7119
7228
|
const granite = !existsSync(join(from, "isorite-model.json")) && FILES.every(([, to, want]) => existsSync(join(from, to)) && sha(readFileSync(join(from, to))) === want);
|
|
7120
7229
|
let out = userModelDir(env);
|
|
@@ -7142,7 +7251,7 @@ async function installFrom(from, say = console.log, env = process.env) {
|
|
|
7142
7251
|
}
|
|
7143
7252
|
/** `isorite model [--from <folder>]`: says which model is installed and where, or installs one, and on failure
|
|
7144
7253
|
* where it looked, what it tried, and what to do. */
|
|
7145
|
-
async function modelCommand(from, say = console.log, env = process.env, sources = {}) {
|
|
7254
|
+
async function modelCommand(from, say = console.log, env = process.env, sources = {}, url) {
|
|
7146
7255
|
const told = (i, why) => say(`search by meaning: ${i.name} at ${i.dir} (sha256 ${i.sha.slice(0, 12)}, ${i.pooling} pooling), found by ${why}`);
|
|
7147
7256
|
if (env.ISORITE_EMBED === "off") say("ISORITE_EMBED=off: search by meaning is switched off, whatever is installed");
|
|
7148
7257
|
if (from) try {
|
|
@@ -7159,27 +7268,35 @@ async function modelCommand(from, say = console.log, env = process.env, sources
|
|
|
7159
7268
|
return 0;
|
|
7160
7269
|
}
|
|
7161
7270
|
say(`No model in ${places.map((p) => p.dir).join(", ")}`);
|
|
7271
|
+
if (!url) say(`If it can't be fetched from here: ${RELEASE} (SHA-256 ${RELEASE_SHA256}), then isorite model --from <that file>`);
|
|
7162
7272
|
try {
|
|
7163
|
-
await fetchModel(userModelDir(env), say,
|
|
7273
|
+
await fetchModel(userModelDir(env), say, {
|
|
7274
|
+
mirror: mirrorOf(env),
|
|
7275
|
+
...sources,
|
|
7276
|
+
...url ? { url } : {}
|
|
7277
|
+
});
|
|
7164
7278
|
return 0;
|
|
7165
7279
|
} catch (e) {
|
|
7166
7280
|
say(e.message);
|
|
7167
7281
|
say([
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
` -
|
|
7171
|
-
` -
|
|
7282
|
+
...byHand(env),
|
|
7283
|
+
"Or:",
|
|
7284
|
+
` - a mirror: ISORITE_MODEL_BASE_URL=<the folder ${RELEASE_FILE} is in> isorite model, or isorite model --url <its address>`,
|
|
7285
|
+
` - the model's files from a machine that has them (${userModelDir()} there): isorite model --from <folder>`,
|
|
7286
|
+
` - any compatible model (ONNX, 384 numbers out, with its tokenizer.json): isorite model --from <folder>, or ISORITE_EMBED_DIR=<folder>`,
|
|
7172
7287
|
"Search by words works without it."
|
|
7173
7288
|
].join("\n"));
|
|
7174
7289
|
return 1;
|
|
7175
7290
|
}
|
|
7176
7291
|
}
|
|
7177
|
-
var NAME$2, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
|
|
7292
|
+
var NAME$2, MODEL, RELEASE_FILE, RELEASE, RELEASE_SHA256, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
|
|
7178
7293
|
var init_model = __esmMin((() => {
|
|
7179
7294
|
init_src();
|
|
7180
7295
|
NAME$2 = "granite-embedding-small-english-r2";
|
|
7181
7296
|
MODEL = `${NAME$2}-q8`;
|
|
7182
|
-
|
|
7297
|
+
RELEASE_FILE = "isorite-model-granite-1.0.0.tgz";
|
|
7298
|
+
RELEASE = `https://github.com/lachlanjs/isorite/releases/download/model-granite-1.0.0/${RELEASE_FILE}`;
|
|
7299
|
+
RELEASE_SHA256 = "8d8ab528ae289dfc81fd2bfa80c867ca0c8342a2d98f7c86141df4b15fdf028c";
|
|
7183
7300
|
PACKAGE = "isorite-model-granite@1.0.0";
|
|
7184
7301
|
HUB = `https://huggingface.co/onnx-community/granite-embedding-small-english-r2-ONNX/resolve/1dc7835ba0cb9c76a3618d0bf0c427c97671b3c8`;
|
|
7185
7302
|
FILES = [
|
|
@@ -7982,6 +8099,7 @@ const SetupDraft = z.object({
|
|
|
7982
8099
|
url: z.string().max(2e3),
|
|
7983
8100
|
keyHeld: z.boolean(),
|
|
7984
8101
|
keyHeader: z.string().max(200),
|
|
8102
|
+
keyPrefix: z.string().max(50).default(""),
|
|
7985
8103
|
headers: z.array(Pair).max(40),
|
|
7986
8104
|
query: z.array(Pair).max(40),
|
|
7987
8105
|
pfx: Path,
|
|
@@ -8481,6 +8599,7 @@ const FORM = [
|
|
|
8481
8599
|
"auth",
|
|
8482
8600
|
"key",
|
|
8483
8601
|
"key_header",
|
|
8602
|
+
"key_prefix",
|
|
8484
8603
|
"headers",
|
|
8485
8604
|
"query",
|
|
8486
8605
|
"pfx",
|
|
@@ -8512,6 +8631,7 @@ function readSetup(config) {
|
|
|
8512
8631
|
url: gateway ? p.url : OPENROUTER.url,
|
|
8513
8632
|
keyHeld: text(p.key) !== "",
|
|
8514
8633
|
keyHeader: text(p.key_header),
|
|
8634
|
+
keyPrefix: text(p.key_prefix).trim(),
|
|
8515
8635
|
headers: pairs(p.headers),
|
|
8516
8636
|
query: pairs(p.query),
|
|
8517
8637
|
pfx: text(p.pfx),
|
|
@@ -8553,6 +8673,9 @@ function applySetup(config, draft) {
|
|
|
8553
8673
|
}
|
|
8554
8674
|
if (gateway) {
|
|
8555
8675
|
if (draft.keyHeader.trim()) p.key_header = draft.keyHeader.trim();
|
|
8676
|
+
const prefix = (draft.keyPrefix ?? "").trim();
|
|
8677
|
+
if (prefix) p.key_prefix = prefix + " ";
|
|
8678
|
+
else if (old.key_prefix === "" && draft.keyHeader.trim()) p.key_prefix = "";
|
|
8556
8679
|
const pairs = (field, given) => {
|
|
8557
8680
|
const before = table(old[field]), out = {};
|
|
8558
8681
|
for (const pair of given) {
|
|
@@ -8713,28 +8836,47 @@ var Setup = class {
|
|
|
8713
8836
|
};
|
|
8714
8837
|
//#endregion
|
|
8715
8838
|
//#region packages/server/src/version.ts
|
|
8716
|
-
const VERSION = "0.
|
|
8839
|
+
const VERSION = "0.9.1";
|
|
8717
8840
|
//#endregion
|
|
8718
|
-
//#region packages/server/src/
|
|
8719
|
-
|
|
8720
|
-
|
|
8841
|
+
//#region packages/server/src/git.ts
|
|
8842
|
+
/** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
|
|
8843
|
+
* worktree or submodule, a file. No `git` is run. */
|
|
8844
|
+
function repoOf(folder) {
|
|
8845
|
+
for (let at = resolve(folder);; at = dirname(at)) {
|
|
8846
|
+
if (existsSync(join(at, ".git"))) return at;
|
|
8847
|
+
if (dirname(at) === at) return null;
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
/** The environment for a `git` isorite runs: none of the shell's GIT_* (a GIT_DIR would point it elsewhere), no
|
|
8851
|
+
* system configuration (where a company's hooks and helpers are usually set), and no prompt for a password. */
|
|
8852
|
+
function plainGitEnv(env = process.env) {
|
|
8853
|
+
return {
|
|
8854
|
+
...Object.fromEntries(Object.entries(env).filter(([k]) => !k.startsWith("GIT_"))),
|
|
8855
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
8856
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
8857
|
+
};
|
|
8858
|
+
}
|
|
8859
|
+
/** `git init` in a folder, given `timeout` ms; whether it was made a repository. */
|
|
8860
|
+
function gitInit(folder, { env = process.env, timeout = 1e4 } = {}) {
|
|
8721
8861
|
try {
|
|
8722
|
-
|
|
8723
|
-
cwd,
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
}).trim();
|
|
8862
|
+
execFileSync("git", ["init", "--quiet"], {
|
|
8863
|
+
cwd: folder,
|
|
8864
|
+
stdio: "ignore",
|
|
8865
|
+
env: plainGitEnv(env),
|
|
8866
|
+
timeout,
|
|
8867
|
+
killSignal: "SIGKILL"
|
|
8868
|
+
});
|
|
8869
|
+
return existsSync(join(folder, ".git"));
|
|
8731
8870
|
} catch {
|
|
8732
|
-
return
|
|
8871
|
+
return false;
|
|
8733
8872
|
}
|
|
8734
8873
|
}
|
|
8874
|
+
//#endregion
|
|
8875
|
+
//#region packages/server/src/studybase.ts
|
|
8876
|
+
init_src$1();
|
|
8735
8877
|
/** Where format 1 put a study base when not told: beside the knowledge base's repository, named after it. */
|
|
8736
8878
|
function defaultStudyPath(notes) {
|
|
8737
|
-
const repo =
|
|
8879
|
+
const repo = repoOf(notes) ?? resolve(notes);
|
|
8738
8880
|
return join(dirname(repo), basename(repo) + "-study");
|
|
8739
8881
|
}
|
|
8740
8882
|
/** A study base's configuration, and its knowledge base's folder as an absolute path. */
|
|
@@ -8927,6 +9069,27 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
|
|
|
8927
9069
|
process.on("SIGTERM", stop);
|
|
8928
9070
|
}
|
|
8929
9071
|
//#endregion
|
|
9072
|
+
//#region packages/server/src/self.ts
|
|
9073
|
+
/** Whether this is the single executable. */
|
|
9074
|
+
const single = () => {
|
|
9075
|
+
try {
|
|
9076
|
+
return isSea();
|
|
9077
|
+
} catch {
|
|
9078
|
+
return false;
|
|
9079
|
+
}
|
|
9080
|
+
};
|
|
9081
|
+
/** What runs isorite again with `args`, given the script (npm's or a checkout's) to run when not single. */
|
|
9082
|
+
function self(script, args, execArgv = process.execArgv) {
|
|
9083
|
+
return {
|
|
9084
|
+
command: process.execPath,
|
|
9085
|
+
args: single() ? args : [
|
|
9086
|
+
...execArgv,
|
|
9087
|
+
script,
|
|
9088
|
+
...args
|
|
9089
|
+
]
|
|
9090
|
+
};
|
|
9091
|
+
}
|
|
9092
|
+
//#endregion
|
|
8930
9093
|
//#region packages/server/src/term.ts
|
|
8931
9094
|
init_src$1();
|
|
8932
9095
|
const MAIN = import.meta.url.endsWith(".ts") ? fileURLToPath(new URL("./main.ts", import.meta.url)) : fileURLToPath(import.meta.url);
|
|
@@ -8946,6 +9109,12 @@ function openBrowser(url) {
|
|
|
8946
9109
|
}).on("error", () => void 0).unref();
|
|
8947
9110
|
} catch {}
|
|
8948
9111
|
}
|
|
9112
|
+
/** How long a step may take before it's said what is being waited for (M21), and where it's said: stderr, as
|
|
9113
|
+
* `mcp` speaks on stdout. */
|
|
9114
|
+
const SLOW = 2e3;
|
|
9115
|
+
const waiting = (line) => {
|
|
9116
|
+
process.stderr.write(line + "\n");
|
|
9117
|
+
};
|
|
8949
9118
|
/** The name the server on this URL serves, or null when nothing answers. */
|
|
8950
9119
|
async function serving(url) {
|
|
8951
9120
|
try {
|
|
@@ -8959,7 +9128,7 @@ async function serving(url) {
|
|
|
8959
9128
|
* well from now on, M20; or for any, when no base is named), or one started for the base, its output kept in a
|
|
8960
9129
|
* log. `url` is the base's own address there. A server this started stops once the last of those using it lets it
|
|
8961
9130
|
* go; one you started yourself goes on. */
|
|
8962
|
-
async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4 } = {}) {
|
|
9131
|
+
async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4, say = waiting } = {}) {
|
|
8963
9132
|
const root = `http://127.0.0.1:${port}`;
|
|
8964
9133
|
const opened = path === void 0 ? null : locate(path);
|
|
8965
9134
|
let name = await serving(root), started = null;
|
|
@@ -8967,14 +9136,14 @@ async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4
|
|
|
8967
9136
|
if (!opened) throw new StudyError(`nothing is serving on port ${port}: name a base`);
|
|
8968
9137
|
const log = join(tmpdir(), `isorite-term-${port}.log`);
|
|
8969
9138
|
const out = openSync(log, "w");
|
|
8970
|
-
|
|
8971
|
-
MAIN,
|
|
9139
|
+
const again = self(MAIN, [
|
|
8972
9140
|
"serve",
|
|
8973
9141
|
path,
|
|
8974
9142
|
"--port",
|
|
8975
9143
|
String(port),
|
|
8976
9144
|
"--held"
|
|
8977
|
-
],
|
|
9145
|
+
], []);
|
|
9146
|
+
started = spawn(again.command, again.args, {
|
|
8978
9147
|
stdio: [
|
|
8979
9148
|
"ignore",
|
|
8980
9149
|
out,
|
|
@@ -8985,7 +9154,12 @@ async function ensureServer(path, { port = 4767, env = process.env, wait = 12e4
|
|
|
8985
9154
|
});
|
|
8986
9155
|
const exited = new Promise((done) => started.once("exit", () => done()));
|
|
8987
9156
|
const begun = Date.now();
|
|
9157
|
+
let told = false;
|
|
8988
9158
|
while ((name = await serving(root)) === null) {
|
|
9159
|
+
if (!told && Date.now() - begun > SLOW) {
|
|
9160
|
+
say(`isorite: waiting for the isorite server; its output is in ${log}`);
|
|
9161
|
+
told = true;
|
|
9162
|
+
}
|
|
8989
9163
|
if (started.exitCode !== null || Date.now() - begun > wait) {
|
|
8990
9164
|
started.kill();
|
|
8991
9165
|
throw new StudyError(`the isorite server did not start; its output is in ${log}`);
|
|
@@ -9038,6 +9212,25 @@ async function freePort() {
|
|
|
9038
9212
|
});
|
|
9039
9213
|
});
|
|
9040
9214
|
}
|
|
9215
|
+
/** Whether `address` asks a browser that opens it for a password (M21): a 401 with Basic, as OpenCode 2 answers
|
|
9216
|
+
* for /api (its page itself is served to anyone). Asked as a browser navigates, by node:http: OpenCode leaves the
|
|
9217
|
+
* challenge out for its own page's fetches (Sec-Fetch-Mode: cors), which is all Node's fetch can send. */
|
|
9218
|
+
function asksPassword(address) {
|
|
9219
|
+
return new Promise((done) => {
|
|
9220
|
+
const req = get(address, {
|
|
9221
|
+
headers: {
|
|
9222
|
+
Accept: "text/html",
|
|
9223
|
+
"Sec-Fetch-Mode": "navigate",
|
|
9224
|
+
"Sec-Fetch-Dest": "document"
|
|
9225
|
+
},
|
|
9226
|
+
timeout: 3e3
|
|
9227
|
+
}, (res) => {
|
|
9228
|
+
res.resume();
|
|
9229
|
+
done(res.statusCode === 401 && /^\s*basic\b/i.test(String(res.headers["www-authenticate"] ?? "")));
|
|
9230
|
+
});
|
|
9231
|
+
req.on("timeout", () => req.destroy()).on("error", () => done(false));
|
|
9232
|
+
});
|
|
9233
|
+
}
|
|
9041
9234
|
/** A POST to the isorite server, made as its page makes one: from its origin, with the token it hands out. */
|
|
9042
9235
|
async function tell(url, path, body) {
|
|
9043
9236
|
const token = await tokenOf(url);
|
|
@@ -9054,7 +9247,7 @@ async function tell(url, path, body) {
|
|
|
9054
9247
|
}
|
|
9055
9248
|
/** OpenCode's own server, on this machine only, with a password of its own; its output kept in a log. The terminal
|
|
9056
9249
|
* attaches to it, and so does the isorite server, which shows its session in the Iso pane (M11). */
|
|
9057
|
-
async function startOpenCode(command, { cwd, env, wait = 6e4 }) {
|
|
9250
|
+
async function startOpenCode(command, { cwd, env, wait = 6e4, say = waiting }) {
|
|
9058
9251
|
const port = await freePort(), password = randomBytes(24).toString("base64url"), url = `http://127.0.0.1:${port}`;
|
|
9059
9252
|
const log = join(tmpdir(), `isorite-opencode-${port}.log`);
|
|
9060
9253
|
const out = openSync(log, "w");
|
|
@@ -9078,7 +9271,17 @@ async function startOpenCode(command, { cwd, env, wait = 6e4 }) {
|
|
|
9078
9271
|
});
|
|
9079
9272
|
const auth = { authorization: "Basic " + Buffer.from(`opencode:${password}`).toString("base64") };
|
|
9080
9273
|
const started = Date.now();
|
|
9274
|
+
let told = 0;
|
|
9081
9275
|
for (;;) {
|
|
9276
|
+
const waited = Date.now() - started;
|
|
9277
|
+
if (told === 0 && waited > SLOW) {
|
|
9278
|
+
say("isorite: waiting for OpenCode's server");
|
|
9279
|
+
told = 1;
|
|
9280
|
+
}
|
|
9281
|
+
if (told === 1 && waited > 1e4) {
|
|
9282
|
+
say(`isorite: OpenCode's server has not answered in 10 s (a git it runs may be waiting); its output is in ${log}`);
|
|
9283
|
+
told = 2;
|
|
9284
|
+
}
|
|
9082
9285
|
if (server.exitCode !== null || Date.now() - started > wait) {
|
|
9083
9286
|
server.kill();
|
|
9084
9287
|
throw new StudyError(`OpenCode's server did not start; its output is in ${log}`);
|
|
@@ -9147,8 +9350,57 @@ function where(port, models, env, tty, page = "/") {
|
|
|
9147
9350
|
async function tokenOf(url) {
|
|
9148
9351
|
return (await (await fetch(`${url}/api/session`)).json()).token;
|
|
9149
9352
|
}
|
|
9353
|
+
/** `isorite opencode` (M21): OpenCode 2, at the version isorite is tested with, in isorite's own npm prefix, the
|
|
9354
|
+
* global one left as it is. npm's registry, proxy and mirror settings apply. Run again, it installs it again. */
|
|
9355
|
+
function installOpenCode(env = process.env, say = console.log, npm = "npm") {
|
|
9356
|
+
const prefix = ownPrefix(env);
|
|
9357
|
+
say(`isorite opencode: installing ${PACKAGE$1} in ${prefix}, for isorite alone; nothing global is changed`);
|
|
9358
|
+
const win = process.platform === "win32";
|
|
9359
|
+
const done = spawnSync(npm, [
|
|
9360
|
+
"install",
|
|
9361
|
+
"--prefix",
|
|
9362
|
+
win ? `"${prefix}"` : prefix,
|
|
9363
|
+
"--no-fund",
|
|
9364
|
+
"--no-audit",
|
|
9365
|
+
"--loglevel=error",
|
|
9366
|
+
PACKAGE$1
|
|
9367
|
+
], {
|
|
9368
|
+
stdio: "inherit",
|
|
9369
|
+
env,
|
|
9370
|
+
shell: win
|
|
9371
|
+
});
|
|
9372
|
+
if (done.status !== 0) {
|
|
9373
|
+
say(`npm could not install it${done.error ? `: ${done.error.message}` : ""}. If npm's registry or mirror is the trouble, fetch an OpenCode 2 another way and name it: ISORITE_OPENCODE=/path/to/opencode isorite term`);
|
|
9374
|
+
return 1;
|
|
9375
|
+
}
|
|
9376
|
+
try {
|
|
9377
|
+
const found = findOpenCode(env);
|
|
9378
|
+
say(`OpenCode ${found.version} is ${found.command}: isorite term uses it from now on`);
|
|
9379
|
+
return 0;
|
|
9380
|
+
} catch (e) {
|
|
9381
|
+
say(e.message);
|
|
9382
|
+
return 1;
|
|
9383
|
+
}
|
|
9384
|
+
}
|
|
9385
|
+
/** OpenCode 2, or, when there's none and a person is at the terminal, the offer to install it for isorite alone. */
|
|
9386
|
+
async function openCodeFor(env) {
|
|
9387
|
+
try {
|
|
9388
|
+
return findOpenCode(env);
|
|
9389
|
+
} catch (e) {
|
|
9390
|
+
if (!(e instanceof NoOpenCode) || env.ISORITE_OPENCODE || !process.stdin.isTTY || !process.stdout.isTTY) throw e;
|
|
9391
|
+
console.error(e.message);
|
|
9392
|
+
const ask = createInterface({
|
|
9393
|
+
input: process.stdin,
|
|
9394
|
+
output: process.stdout
|
|
9395
|
+
});
|
|
9396
|
+
const yes = /^y(es)?$/i.test((await ask.question(`Install OpenCode 2 for isorite only, in ${ownPrefix(env)}? [y/N] `)).trim());
|
|
9397
|
+
ask.close();
|
|
9398
|
+
if (!yes || installOpenCode(env) !== 0) throw new StudyError("isorite term needs OpenCode 2");
|
|
9399
|
+
return findOpenCode(env);
|
|
9400
|
+
}
|
|
9401
|
+
}
|
|
9150
9402
|
async function term(path, { port = 4767, cwd = process.cwd(), env: given = process.env, wait = 12e4, onServer } = {}) {
|
|
9151
|
-
const opencode =
|
|
9403
|
+
const opencode = await openCodeFor(given);
|
|
9152
9404
|
const server = await ensureServer(path, {
|
|
9153
9405
|
port,
|
|
9154
9406
|
env: given,
|
|
@@ -9167,7 +9419,6 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9167
9419
|
}
|
|
9168
9420
|
const { gateway, env } = models;
|
|
9169
9421
|
const said = where(port, models, given, void 0, new URL(server.url).pathname.replace(/\/?$/, "/"));
|
|
9170
|
-
for (const line of said) console.log(line);
|
|
9171
9422
|
const agents = path === void 0 ? null : join(locate(path).notes, AGENTS_FILE);
|
|
9172
9423
|
const home = writeHome(defaultHome(env), {
|
|
9173
9424
|
gateway,
|
|
@@ -9176,7 +9427,8 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9176
9427
|
const shared = {
|
|
9177
9428
|
...env,
|
|
9178
9429
|
OPENCODE_CONFIG_DIR: home,
|
|
9179
|
-
ISORITE_URL: server.url
|
|
9430
|
+
ISORITE_URL: server.url,
|
|
9431
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
9180
9432
|
};
|
|
9181
9433
|
let backend;
|
|
9182
9434
|
try {
|
|
@@ -9188,6 +9440,8 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9188
9440
|
server.release();
|
|
9189
9441
|
throw e;
|
|
9190
9442
|
}
|
|
9443
|
+
if (await asksPassword(`${backend.url}/api/session`)) said.push(` OpenCode's own web page (port ${new URL(backend.url).port}) asks for a password: don't open it. isorite's page is above, and /web in the terminal opens it`);
|
|
9444
|
+
for (const line of said) console.log(line);
|
|
9191
9445
|
await tell(server.url, "/api/terminal/attach", {
|
|
9192
9446
|
url: backend.url,
|
|
9193
9447
|
password: backend.password,
|
|
@@ -9200,7 +9454,8 @@ async function term(path, { port = 4767, cwd = process.cwd(), env: given = proce
|
|
|
9200
9454
|
...given,
|
|
9201
9455
|
OPENCODE_CONFIG_DIR: home,
|
|
9202
9456
|
ISORITE_URL: server.url,
|
|
9203
|
-
OPENCODE_PASSWORD: backend.password
|
|
9457
|
+
OPENCODE_PASSWORD: backend.password,
|
|
9458
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
9204
9459
|
}
|
|
9205
9460
|
});
|
|
9206
9461
|
const forward = (signal) => () => child.kill(signal);
|
|
@@ -9253,6 +9508,7 @@ const READ = [
|
|
|
9253
9508
|
"SSH_CONNECTION",
|
|
9254
9509
|
"ISORITE_EMBED",
|
|
9255
9510
|
"ISORITE_EMBED_DIR",
|
|
9511
|
+
"ISORITE_MODEL_BASE_URL",
|
|
9256
9512
|
"XDG_DATA_HOME",
|
|
9257
9513
|
"XDG_CACHE_HOME"
|
|
9258
9514
|
];
|
|
@@ -9299,7 +9555,10 @@ async function checkProvider(p, h, key, say) {
|
|
|
9299
9555
|
const missing = models.filter((m) => !listed.includes(m));
|
|
9300
9556
|
say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
|
|
9301
9557
|
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]`);
|
|
9302
|
-
} else
|
|
9558
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
9559
|
+
const said = refusal(await res.text().catch(() => ""), key);
|
|
9560
|
+
say("reached", "fail", `it said ${res.status} to the list of models${said ? `: “${said}”` : ""}. The key${key ? `, sent as ${keySentAs(p, key)},` : ""} or a header it wants (headers), is not accepted: check key_header and key_prefix`);
|
|
9561
|
+
} else say("reached", "ok", `it answered (${res.status} for the list of models, which some gateways don't have)`);
|
|
9303
9562
|
} catch (e) {
|
|
9304
9563
|
reached = false;
|
|
9305
9564
|
say("reached", "fail", unreachable(e, p.url));
|
|
@@ -9359,7 +9618,7 @@ async function doctor(folder, o = {}) {
|
|
|
9359
9618
|
const tls = gatewayTls(path, env);
|
|
9360
9619
|
http ??= gatewayHttp(provider, tls, env);
|
|
9361
9620
|
if (existsSync(path)) say("config", "ok", `${path}: ${provider.name} at ${provider.url}`);
|
|
9362
|
-
say("key", key ? "ok" : tls ? "warn" : "fail", key ?
|
|
9621
|
+
say("key", key ? "ok" : tls ? "warn" : "fail", key ? `sent as ${keySentAs(provider, key)}${provider.keyHeader?.toLowerCase() === "authorization" && provider.keyPrefix === void 0 ? ` (key_header = "${provider.keyHeader}" alone means Bearer since 0.9.0; key_prefix = "" sends it bare)` : ""}` : tls ? "none; the client certificate alone is sent" : "none: set one in Iso's settings, or as key under [provider]");
|
|
9363
9622
|
if (tls) say("certificate", "ok", `${tls.files.join(", ")}${tls.pfx || tls.cert ? "" : " (an authority only, no client certificate)"}`);
|
|
9364
9623
|
const tiers = `low ${provider.tiers.low}, mid ${provider.tiers.mid}, max ${provider.tiers.max}`;
|
|
9365
9624
|
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);
|
|
@@ -9385,11 +9644,21 @@ async function doctor(folder, o = {}) {
|
|
|
9385
9644
|
if (!o.report) out(` asking ${p.name}: its models, then ${models.length + 1} replies of a few words (a few hundred tokens in all)`);
|
|
9386
9645
|
await checkProvider(p, h, key, say);
|
|
9387
9646
|
}
|
|
9647
|
+
const began = Date.now(), git = spawnSync(o.git ?? "git", ["--version"], {
|
|
9648
|
+
encoding: "utf8",
|
|
9649
|
+
env,
|
|
9650
|
+
timeout: 3e3,
|
|
9651
|
+
killSignal: "SIGKILL"
|
|
9652
|
+
});
|
|
9653
|
+
const took = Date.now() - began;
|
|
9654
|
+
if (git.error && git.error.code === "ENOENT") say("git", "skip", "not found: isorite doesn't need it; OpenCode works without it, as a folder that isn't a repository");
|
|
9655
|
+
else if (git.status !== 0 || took >= 3e3) say("git", "warn", `git --version ${took >= 3e3 ? "didn't answer in 3 s" : `failed after ${took} ms`}: likely a wrapper (a company's) that waits on the network. isorite no longer waits on git to start; OpenCode may, and isorite term then says it's waiting for OpenCode's server`);
|
|
9656
|
+
else say("git", took > 1e3 ? "warn" : "ok", `${git.stdout.trim()}, in ${took} ms${took > 1e3 ? ": slow, likely a wrapper; OpenCode may wait on it as it starts" : ""}`);
|
|
9388
9657
|
try {
|
|
9389
9658
|
const found = findOpenCode(env, o.probe);
|
|
9390
|
-
say("opencode", "ok", `${found.command} ${found.version}`);
|
|
9659
|
+
say("opencode", "ok", `${found.command} ${found.version}${found.from === "the PATH" ? "" : ` (${found.from})`}`);
|
|
9391
9660
|
} catch (e) {
|
|
9392
|
-
say("opencode", "warn", e.message.replace(/\n\s*/g, " "));
|
|
9661
|
+
say("opencode", e instanceof NoOpenCode && e.old ? "fail" : "warn", e.message.replace(/\n\s*/g, " "));
|
|
9393
9662
|
}
|
|
9394
9663
|
const port = o.port ?? 4767, url = `http://127.0.0.1:${port}`, served = await serving(url);
|
|
9395
9664
|
say("port", "ok", served === null ? `${port} is free: isorite term starts a server there` : `${port} is isorite's, serving ${served}`);
|
|
@@ -9407,6 +9676,12 @@ async function doctor(folder, o = {}) {
|
|
|
9407
9676
|
known = false;
|
|
9408
9677
|
}
|
|
9409
9678
|
say("terminal", known ? "ok" : "warn", terminal);
|
|
9679
|
+
const hint = "isorite term also runs OpenCode's own web server, on a port of its own, which asks for a password: open isorite's pages and panes (/web in the terminal), not OpenCode's";
|
|
9680
|
+
if (served === null) say("browser pages", "ok", hint);
|
|
9681
|
+
else {
|
|
9682
|
+
const asking = (await Promise.all([`${url}/`, `${url}/api/session`].map(async (at) => await asksPassword(at) ? at : null))).filter(Boolean);
|
|
9683
|
+
say("browser pages", asking.length ? "fail" : "ok", asking.length ? `${asking.join(" and ")} asked for a password (401, Basic): that is not isorite's server; stop it, or give isorite another port (--port <n>)` : `isorite's pages at ${url}/ ask for no password. ${hint}`);
|
|
9684
|
+
}
|
|
9410
9685
|
if (env.SSH_CONNECTION) say("browser", "ok", `over SSH: on your own machine, ssh -L ${port}:localhost:${port} <this host>, then http://localhost:${port}/`);
|
|
9411
9686
|
let model = null;
|
|
9412
9687
|
if (env.ISORITE_EMBED === "off") say("search by meaning", "skip", "switched off (ISORITE_EMBED=off)");
|
|
@@ -9418,7 +9693,23 @@ async function doctor(folder, o = {}) {
|
|
|
9418
9693
|
say("search by meaning", "fail", `${dir}: ${e.message}`);
|
|
9419
9694
|
}
|
|
9420
9695
|
if (model) say("search by meaning", "ok", `${model.name} at ${model.dir}`);
|
|
9421
|
-
else if (!dir)
|
|
9696
|
+
else if (!dir) {
|
|
9697
|
+
let reach = "";
|
|
9698
|
+
if (!o.offline) {
|
|
9699
|
+
const from = mirrorOf(env) ?? RELEASE;
|
|
9700
|
+
try {
|
|
9701
|
+
const res = await fetch(from, {
|
|
9702
|
+
method: "HEAD",
|
|
9703
|
+
redirect: "follow",
|
|
9704
|
+
signal: AbortSignal.timeout(1e4)
|
|
9705
|
+
});
|
|
9706
|
+
reach = res.ok ? ` ${from} answers from here.` : ` ${from} said ${res.status}.`;
|
|
9707
|
+
} catch (e) {
|
|
9708
|
+
reach = ` ${from} can't be reached from here: ${unreachable(e, from)}.`;
|
|
9709
|
+
}
|
|
9710
|
+
}
|
|
9711
|
+
say("search by meaning", "warn", `no model in ${modelPlaces(env).map((p) => p.dir).join(" or ")}: isorite model fetches it (53 MB).${reach} By hand: ${mirrorOf(env) ?? RELEASE} (SHA-256 ${RELEASE_SHA256}), then isorite model --from <that file>. Search by words works without it`);
|
|
9712
|
+
}
|
|
9422
9713
|
}
|
|
9423
9714
|
if (folder) try {
|
|
9424
9715
|
const { notes } = locate(folder);
|
|
@@ -9496,7 +9787,8 @@ function proxied(env, execArgv = process.execArgv) {
|
|
|
9496
9787
|
}
|
|
9497
9788
|
/** Run this command again with `env`, its input and output its own; its exit code. */
|
|
9498
9789
|
function rerun(env, argv = process.argv, execArgv = process.execArgv) {
|
|
9499
|
-
const
|
|
9790
|
+
const again = self(argv[1], argv.slice(2), execArgv);
|
|
9791
|
+
const child = spawn(again.command, again.args, {
|
|
9500
9792
|
stdio: "inherit",
|
|
9501
9793
|
env
|
|
9502
9794
|
});
|
|
@@ -9704,7 +9996,7 @@ async function mcp(path, { port = 4767, agent = "mcp", quiet = QUIET_MS, input =
|
|
|
9704
9996
|
}
|
|
9705
9997
|
throw Object.assign(/* @__PURE__ */ new Error(`no method ${rpc.method}`), { code: -32601 });
|
|
9706
9998
|
};
|
|
9707
|
-
const lines = createInterface({
|
|
9999
|
+
const lines = createInterface$1({
|
|
9708
10000
|
input,
|
|
9709
10001
|
crlfDelay: Infinity
|
|
9710
10002
|
});
|
|
@@ -10941,7 +11233,7 @@ anything secret: keys, passwords, tokens.
|
|
|
10941
11233
|
`;
|
|
10942
11234
|
/** Make the memory, or say where it is: the folder (as isorite new makes it), a git repository of its own, its
|
|
10943
11235
|
* practice, and its row in bases.toml, lent. */
|
|
10944
|
-
function memory(folder = MEMORY_FOLDER, { config = configPath(), out = console.log } = {}) {
|
|
11236
|
+
function memory(folder = MEMORY_FOLDER, { config = configPath(), out = console.log, env = process.env } = {}) {
|
|
10945
11237
|
const at = resolve(folder), known = knownBases(config);
|
|
10946
11238
|
const had = known.find((b) => b.path === at) ?? (folder === MEMORY_FOLDER ? known.find((b) => b.name === "memory") : void 0);
|
|
10947
11239
|
if (had && existsSync(join(had.path, "AGENTS.md"))) {
|
|
@@ -10952,15 +11244,7 @@ function memory(folder = MEMORY_FOLDER, { config = configPath(), out = console.l
|
|
|
10952
11244
|
if (existsSync(at) && readdirSync(at).some((f) => f !== ".isorite" && f !== ".git")) throw new StudyError(`${at} has files in it already, and no ${AGENTS_FILE}: name another folder, isorite memory <folder>`);
|
|
10953
11245
|
newBase(at);
|
|
10954
11246
|
writeFileSync(join(at, AGENTS_FILE), MEMORY_AGENTS);
|
|
10955
|
-
|
|
10956
|
-
if (!existsSync(join(at, ".git"))) execFileSync("git", ["init", "--quiet"], {
|
|
10957
|
-
cwd: at,
|
|
10958
|
-
stdio: "ignore"
|
|
10959
|
-
});
|
|
10960
|
-
} catch {
|
|
10961
|
-
out(" (git is not here: the memory is not a repository yet)");
|
|
10962
|
-
}
|
|
10963
|
-
out(`isorite memory: made at ${at}, a private repository of its own: push it where you like`);
|
|
11247
|
+
out(existsSync(join(at, ".git")) || gitInit(at, { env }) ? `isorite memory: made at ${at}, a private repository of its own: push it where you like` : `isorite memory: made at ${at}; not a git repository (git did not answer, or is not here): git init it yourself`);
|
|
10964
11248
|
}
|
|
10965
11249
|
const name = rememberBase(at, {
|
|
10966
11250
|
path: config,
|
|
@@ -11017,9 +11301,13 @@ const USAGE = `isorite: an agentic harness for understanding
|
|
|
11017
11301
|
isorite mcp [<notes>] [--port <n>] [--agent <name>]
|
|
11018
11302
|
the knowledge base's tools over MCP, on stdio, for Claude Code or Pi:
|
|
11019
11303
|
claude mcp add isorite -- isorite mcp <base> --agent claude-code
|
|
11020
|
-
isorite model [--from <folder>]
|
|
11021
|
-
|
|
11304
|
+
isorite model [--from <folder|.tgz>] [--url <url>]
|
|
11305
|
+
the model for search by meaning: which is installed, and where; else
|
|
11306
|
+
fetched (53 MB, once) from ISORITE_MODEL_BASE_URL, npm, GitHub or
|
|
11307
|
+
Hugging Face, or --url alone; or from the .tgz fetched by hand, or a
|
|
11022
11308
|
folder: its files copied there, or any compatible model
|
|
11309
|
+
isorite opencode OpenCode 2 for isorite term, installed in isorite's own folder
|
|
11310
|
+
(~/.config/isorite/opencode-cli), the global one left as it is
|
|
11023
11311
|
isorite doctor [<notes>] [--report] [--offline]
|
|
11024
11312
|
whether isorite can work here: the configuration, the network, the
|
|
11025
11313
|
provider (asked a few words, a few hundred tokens), the terminal and
|
|
@@ -11117,13 +11405,18 @@ function main(argv) {
|
|
|
11117
11405
|
});
|
|
11118
11406
|
}
|
|
11119
11407
|
if (command === "model") {
|
|
11120
|
-
const from = rest[rest.indexOf("--from") + 1];
|
|
11408
|
+
const from = rest[rest.indexOf("--from") + 1], url = rest[rest.indexOf("--url") + 1];
|
|
11121
11409
|
if (rest.includes("--from") && !from) {
|
|
11122
|
-
console.error("--from takes a folder");
|
|
11410
|
+
console.error("--from takes a folder, or the model's .tgz");
|
|
11411
|
+
return 2;
|
|
11412
|
+
}
|
|
11413
|
+
if (rest.includes("--url") && !/^https?:\/\//.test(url ?? "")) {
|
|
11414
|
+
console.error("--url takes the address of the model's .tgz");
|
|
11123
11415
|
return 2;
|
|
11124
11416
|
}
|
|
11125
|
-
return modelCommand(rest.includes("--from") ? from : void 0);
|
|
11417
|
+
return modelCommand(rest.includes("--from") ? from : void 0, console.log, process.env, {}, rest.includes("--url") ? url : void 0);
|
|
11126
11418
|
}
|
|
11419
|
+
if (command === "opencode") return installOpenCode();
|
|
11127
11420
|
if (command === "memory") return memory(rest.find((a) => !a.startsWith("--")));
|
|
11128
11421
|
if (command === "bases") return bases(rest);
|
|
11129
11422
|
if (command === "--version" || command === "-v") {
|
|
@@ -11182,11 +11475,35 @@ function main(argv) {
|
|
|
11182
11475
|
process.stdout.write(USAGE);
|
|
11183
11476
|
return command && command !== "help" && command !== "--help" ? 2 : 0;
|
|
11184
11477
|
}
|
|
11185
|
-
|
|
11478
|
+
/** The commands that start something, which say so at once (M21): a subprocess that stalls after this is then
|
|
11479
|
+
* seen to stall. To stderr, as `mcp` speaks on stdout; once, not again when the command runs itself again with
|
|
11480
|
+
* the proxy on (ISORITE_STARTING). */
|
|
11481
|
+
const STARTS = /* @__PURE__ */ new Set([
|
|
11482
|
+
"serve",
|
|
11483
|
+
"term",
|
|
11484
|
+
"mcp",
|
|
11485
|
+
"guide",
|
|
11486
|
+
"tutorial",
|
|
11487
|
+
"--tutorial"
|
|
11488
|
+
]);
|
|
11489
|
+
function starting(argv, env = process.env, write = (s) => process.stderr.write(s)) {
|
|
11490
|
+
if (!STARTS.has(argv[0] ?? "") || env.ISORITE_STARTING) return false;
|
|
11491
|
+
write("isorite: starting…\n");
|
|
11492
|
+
return true;
|
|
11493
|
+
}
|
|
11494
|
+
/** The command as run: said to be starting, the proxy turned on (running itself again), the system's authorities
|
|
11495
|
+
* trusted; its exit code. The single executable calls this (M21). */
|
|
11496
|
+
async function run(argv) {
|
|
11497
|
+
starting(argv);
|
|
11498
|
+
delete process.env.ISORITE_STARTING;
|
|
11186
11499
|
const env = proxied(process.env);
|
|
11187
|
-
if (env)
|
|
11500
|
+
if (env) return rerun({
|
|
11501
|
+
...env,
|
|
11502
|
+
ISORITE_STARTING: "1"
|
|
11503
|
+
});
|
|
11188
11504
|
trustSystem();
|
|
11189
|
-
|
|
11505
|
+
return main(argv);
|
|
11190
11506
|
}
|
|
11507
|
+
if (!single() && process.argv[1] && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href) process.exit(await run(process.argv.slice(2)));
|
|
11191
11508
|
//#endregion
|
|
11192
|
-
export { check, main };
|
|
11509
|
+
export { check, main, run, starting };
|