@chibitek/chibi 0.1.154 → 0.1.157
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/assets/{abnfDiagram-O67JEVCF-CbIXKQpc.js → abnfDiagram-O67JEVCF-BkYbog6D.js} +1 -1
- package/dist/assets/{arc-D1xLD8SN.js → arc-QwTuMsxp.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-xZe7iJ9y.js → architectureDiagram-NJMV4G6O-GdicO2PQ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Cq35uL03.js → blockDiagram-BEXU5L5S-Dkr29TFF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DjlN_-0r.js → c4Diagram-YGBWAQC7-Dz_3dstx.js} +1 -1
- package/dist/assets/channel-D2y0i2ps.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BQpywZWX.js → chunk-2Q5K7J3B-CGsfu-PI.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BajOpOBu.js → chunk-3FUC2YCW-CDqI37_6.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DuZpQtDE.js → chunk-5VM5RSS4-DnmSuyTb.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dn8xlUsI.js → chunk-GTNCS2PH-Dh7tXcJT.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Xrus5lAx.js → chunk-GWA4HPMP-D_SM1ZrY.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dhf8RCqM.js → chunk-JWPE2WC7-DmeMuTsI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BQXmOQMl.js → chunk-NETBCI7D-R1UT9b5f.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BeraxyW-.js → chunk-WEXAMYUT-DpPfU1yO.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BsKuu9tP.js → chunk-XXDRQBXY-DlihfTkM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CcR0y3zf.js → classDiagram-v2-NBCMYWYE-BlTV6lSC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Dd2y43ln.js → cose-bilkent-JH36ORCC-Br1kPFWw.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DL4Zf255.js → cynefin-OW5HDTMX-BqPp3ZMd.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-CvfZKzI5.js → cynefinDiagram-VND7K2PF-DFfs_k8-.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CZfm2BmL.js → dagre-6A5THRUB-Ch3xIJA7.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DEEoufNr.js → diagram-22UHCM2B-DIHUD87N.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-C1R6O8j1.js → diagram-3UASUU5V-CAgQQdeF.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-D9dPGToa.js → diagram-ATOU4E4O-9HVKodrI.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BHy3uSmJ.js → diagram-CDSNMT55-DU2kDq_h.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CEo2WbRd.js → diagram-MLGK6HIB-D-qTAhHD.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BrZrCrGC.js → diagram-MPIPVDR6-C31SnI9P.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CtChmI9r.js → ebnfDiagram-ZINNZB2B-BHrdiBVd.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-C-qEle2N.js → elk-276RUBZZ-DWyljRo5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BEdc5DT9.js → erDiagram-OPXOYQCR-DyY59XPp.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CT-fOp9_.js → flowDiagram-KWPJA3E3-BevMIeAy.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-LWgFxnGJ.js → ganttDiagram-FUAMR5RP-Ct18WHf4.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-xHSa3-lE.js → gitGraphDiagram-X574FWY7-yhixO1N3.js} +1 -1
- package/dist/assets/{index-Dy25lFJN.js → index-BEJ8EKQt.js} +1 -1
- package/dist/assets/index-DGWeJcck.css +1 -0
- package/dist/assets/{index-BpHS2C5J.js → index-ziyH_uer.js} +170 -168
- package/dist/assets/{infoDiagram-VRGFBTTK-BOyODyhP.js → infoDiagram-VRGFBTTK-CYa-r_S5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D3F47TMU.js → ishikawaDiagram-OU5B5YK6-CnxwM7gY.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-B8h_aacv.js → journeyDiagram-ZHPQQLJL-RKQfXBrp.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-nqNasrdQ.js → kanban-definition-PNTS6WVX-CSlRDIF6.js} +1 -1
- package/dist/assets/{linear-CDofN9-p.js → linear-Ch19KVyt.js} +1 -1
- package/dist/assets/{mermaid.core-k18eNV07.js → mermaid.core-Bek7I_t6.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BYNmNDXA.js → mindmap-definition-NLK3R4M7-B_LiP8su.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-44c5V9Pa.js → pegDiagram-GJSIUBJH-DhzQeiZf.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BDvznIYT.js → pieDiagram-5QR66LMP-D-BYNKHS.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BWrzS8SI.js → quadrantDiagram-O4NWA36T-DnCm0heP.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-H_3uxTrq.js → railroadDiagram-XR7U4H2S-COqgYnhn.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BiyKTF3R.js → requirementDiagram-PLB6GJNP-Dvt72ajf.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-43RmpN9N.js → sankeyDiagram-IPEJSGJF-CisC9nbp.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B0OUYqhV.js → sequenceDiagram-PO4LG4MO-BO8xqvxa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xiXiEFtk.js → sizeCapture-INFHLROL-DLk1jBzf.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C4-l5TZJ.js → stateDiagram-v2-GCMORJYK-C_wutSE7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B58p5Kvz.js → swimlanes-2SLR337P-hqo1UlEi.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DUgR2Qsq.js → timeline-definition-EJHVYXUP-C_DJldPg.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DcicVGBU.js → usecaseDiagram-POWQR4AR-CsidVXMA.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-9pThMhQ1.js → vennDiagram-UO4OBE2U-Bs-IIqn_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CStNs1Wv.js → wardleyDiagram-VNRHLVJA-CXEUyxaj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-JmQMXi-y.js → xychartDiagram-PMCCYNJV-B9qvmm4P.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +81 -81
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +2447 -1767
- package/dist-server/mcp-gate.js +7 -4
- package/dist-server/openmausbot.js +692 -665
- package/dist-server/pair-cli.js +692 -665
- package/dist-server/server/app-installers.js +107 -7
- package/dist-server/server/cli-help.js +74 -0
- package/dist-server/server/cli.js +32 -74
- package/dist-server/server/device-routing.js +60 -0
- package/dist-server/server/drivers/claude.js +13 -11
- package/dist-server/server/drivers/codex.js +18 -23
- package/dist-server/server/drivers/local-inject.js +3 -4
- package/dist-server/server/engine-tiers.js +5 -14
- package/dist-server/server/incidents.js +8 -4
- package/dist-server/server/index.js +98 -120
- package/dist-server/server/mcp-auth-signal.js +2 -1
- package/dist-server/server/mcp-gate-config.js +1 -0
- package/dist-server/server/mcp-gate.js +7 -4
- package/dist-server/server/mcp-health.js +14 -4
- package/dist-server/server/mcp-turn.js +26 -0
- package/dist-server/server/personal-goal-runtime.js +86 -0
- package/dist-server/server/personal-goal-turn.js +125 -0
- package/dist-server/server/personal-goals.js +195 -0
- package/dist-server/server/routes/local-models.js +3 -2
- package/dist-server/server/skill-fetch.js +14 -0
- package/dist-server/server/system-prompt.js +14 -6
- package/dist-server/shared/engine-choices.js +15 -0
- package/dist-server/shared/engine-tiers.js +74 -2
- package/dist-server/shared/local-models.js +1 -3
- package/dist-server/shared/personal-goal.js +43 -0
- package/dist-server/vps-container-mcp.js +81 -81
- package/package.json +1 -1
- package/dist/assets/channel-Bm-MISs3.js +0 -1
- package/dist/assets/index-C8YP7Zxj.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CPlL77A3.js +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Download and install the two desktop apps the setup wizard offers, after
|
|
2
2
|
// the person clicks: Ollama (free local AI) and Podman (a separate computer
|
|
3
|
-
// for bots).
|
|
3
|
+
// for bots). Ollama installs on macOS, Windows and Linux; Podman on macOS.
|
|
4
4
|
//
|
|
5
5
|
// Every download is the vendor's own GitHub release asset, found through
|
|
6
6
|
// GitHub's release API, and is checked three ways before anything runs:
|
|
@@ -12,16 +12,22 @@
|
|
|
12
12
|
// ollama.com/download redirects to the same GitHub release asset.
|
|
13
13
|
//
|
|
14
14
|
// Ollama is unpacked with ditto, moved to /Applications (or ~/Applications
|
|
15
|
-
// when /Applications is not writable), and opened.
|
|
15
|
+
// when /Applications is not writable), and opened. On Windows its signed
|
|
16
|
+
// OllamaSetup.exe (Authenticode signer O=Ollama Inc., the check Ollama's own
|
|
17
|
+
// install.ps1 makes) runs silently into %LOCALAPPDATA%\Programs\Ollama, no
|
|
18
|
+
// administrator needed. On Linux, where Ollama ships no signature, the
|
|
19
|
+
// SHA-256 alone gates the tarball, unpacked into ~/.local (no sudo, no
|
|
20
|
+
// service): ~/.local/bin/ollama, started as `ollama serve`. Podman's package needs an
|
|
16
21
|
// administrator's approval, so Atomic opens it in Apple's Installer app,
|
|
17
22
|
// which asks for it; nobody types a command.
|
|
18
23
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
24
|
+
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
20
25
|
import { homedir, tmpdir } from "node:os";
|
|
21
26
|
import { join } from "node:path";
|
|
22
27
|
import { Readable } from "node:stream";
|
|
23
28
|
import { pipeline } from "node:stream/promises";
|
|
24
|
-
import {
|
|
29
|
+
import { createZstdDecompress } from "node:zlib";
|
|
30
|
+
import { execFileCommand, spawnCommand } from "./procs.js";
|
|
25
31
|
export const APP_INSTALLERS = Object.freeze({
|
|
26
32
|
ollama: { repo: "ollama/ollama", asset: () => "Ollama-darwin.zip", teamId: "3MU9H2V9Y9", kind: "zip-app", appName: "Ollama.app" },
|
|
27
33
|
podman: {
|
|
@@ -58,7 +64,7 @@ export function runTool(command, args, timeoutMs = 120_000) {
|
|
|
58
64
|
export async function latestReleaseAsset(spec, arch, fetchImpl = fetch, signal) {
|
|
59
65
|
const name = spec.asset(arch);
|
|
60
66
|
if (!name)
|
|
61
|
-
throw new AppInstallError("unsupported", `No ${spec.repo} installer is published for ${arch}
|
|
67
|
+
throw new AppInstallError("unsupported", `No ${spec.repo} installer is published for ${arch} computers.`);
|
|
62
68
|
let response;
|
|
63
69
|
try {
|
|
64
70
|
response = await fetchImpl(`https://api.github.com/repos/${spec.repo}/releases/latest`, {
|
|
@@ -166,10 +172,104 @@ function writableDir(dir) {
|
|
|
166
172
|
return false;
|
|
167
173
|
}
|
|
168
174
|
}
|
|
175
|
+
/** Ollama's release asset on Windows and Linux. */
|
|
176
|
+
export const OLLAMA_WINDOWS_ASSET = "OllamaSetup.exe";
|
|
177
|
+
export const ollamaLinuxAsset = (arch) => (arch === "x64" ? "ollama-linux-amd64.tar.zst" : arch === "arm64" ? "ollama-linux-arm64.tar.zst" : null);
|
|
178
|
+
/** Where Atomic's Ollama lives off macOS: the per-user Windows install, or
|
|
179
|
+
* the ~/.local unpack on Linux. */
|
|
180
|
+
export function ollamaBinaryHere(platform = process.platform, env = process.env) {
|
|
181
|
+
if (platform === "win32")
|
|
182
|
+
return join(env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Programs", "Ollama", "ollama.exe");
|
|
183
|
+
if (platform === "linux")
|
|
184
|
+
return join(homedir(), ".local", "bin", "ollama");
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/** The Windows installer is signed by Ollama Inc. (Authenticode, valid). */
|
|
188
|
+
export async function verifyWindowsSignature(file, run = runTool) {
|
|
189
|
+
const script = `$s = Get-AuthenticodeSignature -LiteralPath '${file.replace(/'/g, "''")}'; "$($s.Status)|$($s.SignerCertificate.Subject)"`;
|
|
190
|
+
let stdout;
|
|
191
|
+
try {
|
|
192
|
+
({ stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw new AppInstallError("verify", `Windows could not check the installer's signature: ${String(error)}`);
|
|
196
|
+
}
|
|
197
|
+
const [status, subject = ""] = stdout.trim().split("|");
|
|
198
|
+
if (status !== "Valid" || !/(^|, )O=Ollama Inc\.(,|$)/.test(subject)) {
|
|
199
|
+
throw new AppInstallError("verify", `The installer is not signed by Ollama Inc. (${status}: ${subject}).`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function installOllamaWindows(deps, run) {
|
|
203
|
+
const asset = await latestReleaseAsset({ ...APP_INSTALLERS.ollama, asset: () => OLLAMA_WINDOWS_ASSET }, deps.arch ?? process.arch, deps.fetch, deps.signal);
|
|
204
|
+
const work = mkdtempSync(join(tmpdir(), "atomic-ollama-"));
|
|
205
|
+
try {
|
|
206
|
+
const setup = join(work, asset.name);
|
|
207
|
+
await downloadAsset(asset, setup, deps);
|
|
208
|
+
deps.onProgress?.({ phase: "verifying" });
|
|
209
|
+
await verifyWindowsSignature(setup, run);
|
|
210
|
+
deps.onProgress?.({ phase: "installing" });
|
|
211
|
+
// Per-user install; the installer opens the Ollama app when it is done.
|
|
212
|
+
await run(setup, ["/VERYSILENT", "/NORESTART", "/SUPPRESSMSGBOXES"]).catch((error) => {
|
|
213
|
+
throw new AppInstallError("install", `The Ollama installer stopped: ${String(error)}`);
|
|
214
|
+
});
|
|
215
|
+
deps.onProgress?.({ phase: "starting" });
|
|
216
|
+
return { appPath: ollamaBinaryHere("win32"), version: asset.version };
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
rmSync(work, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function installOllamaLinux(deps, run) {
|
|
223
|
+
const asset = await latestReleaseAsset({ ...APP_INSTALLERS.ollama, asset: ollamaLinuxAsset }, deps.arch ?? process.arch, deps.fetch, deps.signal);
|
|
224
|
+
const work = mkdtempSync(join(tmpdir(), "atomic-ollama-"));
|
|
225
|
+
try {
|
|
226
|
+
const packed = join(work, asset.name);
|
|
227
|
+
await downloadAsset(asset, packed, deps);
|
|
228
|
+
deps.onProgress?.({ phase: "installing" });
|
|
229
|
+
const tar = join(work, "ollama.tar");
|
|
230
|
+
await pipeline(createReadStream(packed), createZstdDecompress(), createWriteStream(tar), { signal: deps.signal });
|
|
231
|
+
rmSync(packed, { force: true });
|
|
232
|
+
const prefix = join(homedir(), ".local");
|
|
233
|
+
mkdirSync(prefix, { recursive: true });
|
|
234
|
+
await run("tar", ["-xf", tar, "-C", prefix]).catch((error) => {
|
|
235
|
+
throw new AppInstallError("install", `Could not unpack Ollama into ${prefix}: ${String(error)}`);
|
|
236
|
+
});
|
|
237
|
+
deps.onProgress?.({ phase: "starting" });
|
|
238
|
+
startOllamaApp("linux");
|
|
239
|
+
return { appPath: ollamaBinaryHere("linux"), version: asset.version };
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof AppInstallError)
|
|
243
|
+
throw error;
|
|
244
|
+
throw new AppInstallError(deps.signal?.aborted ? "cancelled" : "install", error instanceof Error ? error.message : String(error));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
rmSync(work, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Start the installed Ollama the way each OS runs it: the app on macOS and
|
|
251
|
+
* Windows (it starts its own server), `ollama serve` on Linux. Returns at
|
|
252
|
+
* once; callers wait for it to answer. */
|
|
253
|
+
export function startOllamaApp(platform = process.platform, run = runTool) {
|
|
254
|
+
if (platform === "darwin") {
|
|
255
|
+
void run("/usr/bin/open", ["-a", "Ollama"]).catch(() => undefined);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const binary = ollamaBinaryHere(platform);
|
|
259
|
+
const command = platform === "win32" && binary ? join(binary, "..", "ollama app.exe") : existsSync(binary ?? "") ? binary : "ollama";
|
|
260
|
+
const child = spawnCommand(command, platform === "win32" ? [] : ["serve"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
261
|
+
child.on("error", () => undefined);
|
|
262
|
+
child.unref();
|
|
263
|
+
}
|
|
169
264
|
/** Download, verify, install and open Ollama. Resolves with where it went. */
|
|
170
265
|
export async function installOllamaApp(deps = {}) {
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
const platform = deps.platform ?? process.platform;
|
|
267
|
+
if (platform === "win32" || platform === "linux") {
|
|
268
|
+
deps.onProgress?.({ phase: "finding" });
|
|
269
|
+
return (platform === "win32" ? installOllamaWindows : installOllamaLinux)(deps, deps.run ?? ((command, args) => runTool(command, args, 15 * 60_000)));
|
|
270
|
+
}
|
|
271
|
+
if (platform !== "darwin")
|
|
272
|
+
throw new AppInstallError("unsupported", `Atomic cannot install Ollama on ${platform}.`);
|
|
173
273
|
const spec = APP_INSTALLERS.ollama;
|
|
174
274
|
const run = deps.run ?? runTool;
|
|
175
275
|
deps.onProgress?.({ phase: "finding" });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const USAGE = `chibi — your team of AI bots, ready in a few steps
|
|
2
|
+
|
|
3
|
+
chibi set up once, then open your workspace
|
|
4
|
+
chibi setup [--data-dir DIR]
|
|
5
|
+
chibi start [the same options as serve]
|
|
6
|
+
chibi serve [--port 8799] [--data-dir DIR] [--label NAME]
|
|
7
|
+
[--public-url https://host] [--tailscale | --tunnel | --domain HOST] [--no-pair]
|
|
8
|
+
chibi pair [--label NAME] [--client] [--phone ios|android]
|
|
9
|
+
[--public-url https://host]
|
|
10
|
+
chibi sessions [revoke ID]
|
|
11
|
+
chibi status
|
|
12
|
+
chibi login [--email you@example.com]
|
|
13
|
+
chibi logout
|
|
14
|
+
chibi command --bot ID|--group ID --thread ID --text "/goal status" [--port 8799]
|
|
15
|
+
chibi goal --bot ID --thread ID --text "--tokens 100000 objective"
|
|
16
|
+
chibi access list | add EMAIL [--chat-only] | remove EMAIL
|
|
17
|
+
chibi service install [--domain HOST | --tunnel | --tailscale] [--port N] [--data-dir DIR] | uninstall
|
|
18
|
+
chibi browser install [--with-deps] | status
|
|
19
|
+
chibi fleet init --domain HOST [--operator USER] | create NAME --admin EMAIL [--member EMAIL] [--brand FILE]
|
|
20
|
+
[--anthropic-key-file FILE] [--cap USD] [--license-key KEY] [--memory 1G]
|
|
21
|
+
| list | users NAME add|remove EMAIL [--chat-only] | suspend NAME | resume NAME
|
|
22
|
+
| delete NAME --yes [--keep-data] | upgrade (all take --dry-run)
|
|
23
|
+
| agent [--socket PATH] [--group USER] (root; installed by init --operator)
|
|
24
|
+
|
|
25
|
+
setup choose AI access and optional phone access; keep existing bots and chats
|
|
26
|
+
start same as chibi: use your saved settings and open the workspace
|
|
27
|
+
serve starts the server without prompts and prints a pairing link + QR code
|
|
28
|
+
pair mints a pairing code against a running server (--client: chat only)
|
|
29
|
+
sessions lists paired devices; "sessions revoke ID" signs one out
|
|
30
|
+
status what the server says about itself
|
|
31
|
+
login signs this machine in to an Atomic account (an emailed code)
|
|
32
|
+
and reserves its public address for --tunnel
|
|
33
|
+
logout releases that address and signs out
|
|
34
|
+
access who may sign in with an emailed code at /pair: an address or
|
|
35
|
+
@domain; --chat-only gives chat and approvals without settings.
|
|
36
|
+
Takes effect at once, no restart.
|
|
37
|
+
service keep the server running across reboots: writes a systemd unit
|
|
38
|
+
(Linux) or a launchd agent (macOS) for the same serve options and
|
|
39
|
+
prints the commands that install it. Install the package
|
|
40
|
+
permanently first (npm install -g @chibitek/chibi).
|
|
41
|
+
browser install: the bots' browser engine (agent-browser, pinned) into the
|
|
42
|
+
data dir, and Chrome for Testing into the user's browser cache.
|
|
43
|
+
--with-deps also installs
|
|
44
|
+
the Linux libraries Chrome needs (run as root once). Then run
|
|
45
|
+
browser install as the user running serve, from that user's home.
|
|
46
|
+
status: what the current user and data directory have.
|
|
47
|
+
fleet many client workspaces on one Linux server, each its own account,
|
|
48
|
+
service, data folder, brand, sign-in list and keys at NAME.HOST
|
|
49
|
+
behind the system Caddy. Plans are printed unless run as root;
|
|
50
|
+
--dry-run always prints. Install the package permanently first.
|
|
51
|
+
init --operator USER also installs the fleet agent, a root service
|
|
52
|
+
on a Unix socket only USER may open, so the workspace running as
|
|
53
|
+
USER manages the others from Settings → Workspaces.
|
|
54
|
+
|
|
55
|
+
--tailscale serve over your tailnet: Tailscale terminates HTTPS and the
|
|
56
|
+
link uses this machine's MagicDNS name (needs Tailscale signed in
|
|
57
|
+
and HTTPS certificates enabled for the tailnet)
|
|
58
|
+
--tunnel serve at a public HTTPS address through a Cloudflare tunnel
|
|
59
|
+
from a hosted control plane (set OMB_CONTROL_PLANE_URL; Atomic
|
|
60
|
+
ships none). Run \`chibi login\` once on this machine first.
|
|
61
|
+
--domain serve at https://HOST on your own domain: a pinned Caddy is
|
|
62
|
+
downloaded once and run alongside the server, and gets the
|
|
63
|
+
certificate itself. Point the domain's DNS at this machine and
|
|
64
|
+
open ports 80 and 443.
|
|
65
|
+
|
|
66
|
+
--no-open do not open a browser window
|
|
67
|
+
--no-pair skip phone setup and do not print a pairing code
|
|
68
|
+
--local start locally this time, ignoring saved remote-access settings
|
|
69
|
+
--quiet skip the welcome banner (it never shows when output is piped;
|
|
70
|
+
NO_COLOR=1 shows it without color)
|
|
71
|
+
|
|
72
|
+
Install once with \`npm install -g @chibitek/chibi\`, then type \`chibi\`.
|
|
73
|
+
Or run without a global install: \`npx @chibitek/chibi\`. Node 24+ is required.
|
|
74
|
+
`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { USAGE } from "./cli-help.js";
|
|
2
|
+
export { USAGE } from "./cli-help.js";
|
|
1
3
|
// `chibi` on the command line: run the server anywhere and pair devices
|
|
2
4
|
// to it. One implementation for three homes — `npx @chibitek/chibi` (the npm
|
|
3
5
|
// package), `node dist-server/cli.js` (the container image) and
|
|
@@ -47,7 +49,7 @@ import { defaultSetupIo, SetupCancelled } from "./cli-prompts.js";
|
|
|
47
49
|
import { normalizePhoneOrigin, phonePairingInstructions, runPhoneSetup } from "./cli-phone-setup.js";
|
|
48
50
|
import { cleanupTunnelOrigin, createTunnelAccount, createTunnelOrigin, describeTunnelAccount, describeTunnelState, ensureCloudflared, FLEET_CREDENTIAL_ENV, fleetAccess, fleetCredential, guardianEntry, startTunnel, tunnelAccess, } from "./tunnel.js";
|
|
49
51
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
50
|
-
export const COMMANDS = ["setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "fleet", "help", "--help", "-h"];
|
|
52
|
+
export const COMMANDS = ["command", "goal", "setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "fleet", "help", "--help", "-h"];
|
|
51
53
|
export function parseArgs(argv, env = process.env) {
|
|
52
54
|
const implicitStart = !argv.length || (argv[0].startsWith("--") && argv[0] !== "--help");
|
|
53
55
|
const [command = "start", ...rest] = implicitStart ? ["start", ...argv] : argv;
|
|
@@ -76,7 +78,15 @@ export function parseArgs(argv, env = process.env) {
|
|
|
76
78
|
return v;
|
|
77
79
|
};
|
|
78
80
|
try {
|
|
79
|
-
if (arg === "--
|
|
81
|
+
if ((options.command === "command" || options.command === "goal") && arg === "--bot")
|
|
82
|
+
options.botId = value();
|
|
83
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--group")
|
|
84
|
+
options.groupId = value();
|
|
85
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--thread")
|
|
86
|
+
options.threadId = value();
|
|
87
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--text")
|
|
88
|
+
options.commandText = value();
|
|
89
|
+
else if (arg === "--port")
|
|
80
90
|
options.port = Number(value());
|
|
81
91
|
else if (arg === "--data-dir")
|
|
82
92
|
options.dataDir = resolve(value());
|
|
@@ -182,6 +192,8 @@ export function parseArgs(argv, env = process.env) {
|
|
|
182
192
|
return { error: "--public-url must start with http:// or https://" };
|
|
183
193
|
if (options.tailscale && options.tunnel)
|
|
184
194
|
return { error: "choose one of --tailscale (your tailnet) and --tunnel (a public address)" };
|
|
195
|
+
if ((options.command === "command" || options.command === "goal") && (Boolean(options.botId) === Boolean(options.groupId) || !options.threadId || !options.commandText))
|
|
196
|
+
return { error: "command and goal require exactly one of --bot ID or --group ID, plus --thread ID --text TEXT" };
|
|
185
197
|
if (options.command === "access" && !options.accessAction)
|
|
186
198
|
return { error: "access needs one of: list, add EMAIL [--chat-only], remove EMAIL" };
|
|
187
199
|
if (options.command === "service" && !options.serviceAction)
|
|
@@ -204,78 +216,6 @@ export function parseArgs(argv, env = process.env) {
|
|
|
204
216
|
}
|
|
205
217
|
return options;
|
|
206
218
|
}
|
|
207
|
-
export const USAGE = `chibi — your team of AI bots, ready in a few steps
|
|
208
|
-
|
|
209
|
-
chibi set up once, then open your workspace
|
|
210
|
-
chibi setup [--data-dir DIR]
|
|
211
|
-
chibi start [the same options as serve]
|
|
212
|
-
chibi serve [--port 8799] [--data-dir DIR] [--label NAME]
|
|
213
|
-
[--public-url https://host] [--tailscale | --tunnel | --domain HOST] [--no-pair]
|
|
214
|
-
chibi pair [--label NAME] [--client] [--phone ios|android]
|
|
215
|
-
[--public-url https://host]
|
|
216
|
-
chibi sessions [revoke ID]
|
|
217
|
-
chibi status
|
|
218
|
-
chibi login [--email you@example.com]
|
|
219
|
-
chibi logout
|
|
220
|
-
chibi access list | add EMAIL [--chat-only] | remove EMAIL
|
|
221
|
-
chibi service install [--domain HOST | --tunnel | --tailscale] [--port N] [--data-dir DIR] | uninstall
|
|
222
|
-
chibi browser install [--with-deps] | status
|
|
223
|
-
chibi fleet init --domain HOST [--operator USER] | create NAME --admin EMAIL [--member EMAIL] [--brand FILE]
|
|
224
|
-
[--anthropic-key-file FILE] [--cap USD] [--license-key KEY] [--memory 1G]
|
|
225
|
-
| list | users NAME add|remove EMAIL [--chat-only] | suspend NAME | resume NAME
|
|
226
|
-
| delete NAME --yes [--keep-data] | upgrade (all take --dry-run)
|
|
227
|
-
| agent [--socket PATH] [--group USER] (root; installed by init --operator)
|
|
228
|
-
|
|
229
|
-
setup choose AI access and optional phone access; keep existing bots and chats
|
|
230
|
-
start same as chibi: use your saved settings and open the workspace
|
|
231
|
-
serve starts the server without prompts and prints a pairing link + QR code
|
|
232
|
-
pair mints a pairing code against a running server (--client: chat only)
|
|
233
|
-
sessions lists paired devices; "sessions revoke ID" signs one out
|
|
234
|
-
status what the server says about itself
|
|
235
|
-
login signs this machine in to an Atomic account (an emailed code)
|
|
236
|
-
and reserves its public address for --tunnel
|
|
237
|
-
logout releases that address and signs out
|
|
238
|
-
access who may sign in with an emailed code at /pair: an address or
|
|
239
|
-
@domain; --chat-only gives chat and approvals without settings.
|
|
240
|
-
Takes effect at once, no restart.
|
|
241
|
-
service keep the server running across reboots: writes a systemd unit
|
|
242
|
-
(Linux) or a launchd agent (macOS) for the same serve options and
|
|
243
|
-
prints the commands that install it. Install the package
|
|
244
|
-
permanently first (npm install -g @chibitek/chibi).
|
|
245
|
-
browser install: the bots' browser engine (agent-browser, pinned) into the
|
|
246
|
-
data dir, and Chrome for Testing into the user's browser cache.
|
|
247
|
-
--with-deps also installs
|
|
248
|
-
the Linux libraries Chrome needs (run as root once). Then run
|
|
249
|
-
browser install as the user running serve, from that user's home.
|
|
250
|
-
status: what the current user and data directory have.
|
|
251
|
-
fleet many client workspaces on one Linux server, each its own account,
|
|
252
|
-
service, data folder, brand, sign-in list and keys at NAME.HOST
|
|
253
|
-
behind the system Caddy. Plans are printed unless run as root;
|
|
254
|
-
--dry-run always prints. Install the package permanently first.
|
|
255
|
-
init --operator USER also installs the fleet agent, a root service
|
|
256
|
-
on a Unix socket only USER may open, so the workspace running as
|
|
257
|
-
USER manages the others from Settings → Workspaces.
|
|
258
|
-
|
|
259
|
-
--tailscale serve over your tailnet: Tailscale terminates HTTPS and the
|
|
260
|
-
link uses this machine's MagicDNS name (needs Tailscale signed in
|
|
261
|
-
and HTTPS certificates enabled for the tailnet)
|
|
262
|
-
--tunnel serve at a public HTTPS address through a Cloudflare tunnel
|
|
263
|
-
from a hosted control plane (set OMB_CONTROL_PLANE_URL; Atomic
|
|
264
|
-
ships none). Run \`chibi login\` once on this machine first.
|
|
265
|
-
--domain serve at https://HOST on your own domain: a pinned Caddy is
|
|
266
|
-
downloaded once and run alongside the server, and gets the
|
|
267
|
-
certificate itself. Point the domain's DNS at this machine and
|
|
268
|
-
open ports 80 and 443.
|
|
269
|
-
|
|
270
|
-
--no-open do not open a browser window
|
|
271
|
-
--no-pair skip phone setup and do not print a pairing code
|
|
272
|
-
--local start locally this time, ignoring saved remote-access settings
|
|
273
|
-
--quiet skip the welcome banner (it never shows when output is piped;
|
|
274
|
-
NO_COLOR=1 shows it without color)
|
|
275
|
-
|
|
276
|
-
Install once with \`npm install -g @chibitek/chibi\`, then type \`chibi\`.
|
|
277
|
-
Or run without a global install: \`npx @chibitek/chibi\`. Node 24+ is required.
|
|
278
|
-
`;
|
|
279
219
|
export function defaultIo() {
|
|
280
220
|
return {
|
|
281
221
|
log: (line) => console.log(line),
|
|
@@ -1232,6 +1172,24 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1232
1172
|
console.log(`\n${welcomeBanner(options)}\n`);
|
|
1233
1173
|
}
|
|
1234
1174
|
switch (options.command) {
|
|
1175
|
+
case "command":
|
|
1176
|
+
case "goal": {
|
|
1177
|
+
const text = options.command === "goal" ? `/goal ${options.commandText}` : options.commandText;
|
|
1178
|
+
const roomGoal = options.groupId ? /^\/goal(?:\s+([\s\S]*))?$/.exec(text ?? "") : null;
|
|
1179
|
+
if (roomGoal && (!roomGoal[1]?.trim() || (roomGoal[1].startsWith("--") || /^(status|pause|resume|stop|help)(?:\s|$)/.test(roomGoal[1])))) {
|
|
1180
|
+
console.error("Room goals accept an objective. Personal goal controls and budgets require --bot.");
|
|
1181
|
+
return 2;
|
|
1182
|
+
}
|
|
1183
|
+
const result = await api(options.port, `/api/${options.groupId ? "groups" : "bots"}/${encodeURIComponent(options.groupId ?? options.botId)}/messages`, {
|
|
1184
|
+
method: "POST", body: JSON.stringify({ threadId: options.threadId, text: roomGoal ? roomGoal[1] : text, ...(roomGoal ? { mode: "goal" } : {}), sendId: randomBytes(16).toString("hex") }),
|
|
1185
|
+
});
|
|
1186
|
+
if (result.status >= 400) {
|
|
1187
|
+
console.error(result.body.error ?? `Server returned ${result.status}`);
|
|
1188
|
+
return 1;
|
|
1189
|
+
}
|
|
1190
|
+
console.log(options.json ? JSON.stringify(result.body) : result.body.goalSummary ?? JSON.stringify(result.body));
|
|
1191
|
+
return 0;
|
|
1192
|
+
}
|
|
1235
1193
|
case "setup":
|
|
1236
1194
|
case "start":
|
|
1237
1195
|
return runOnboardingCommand(options);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Device routing for approval requests (docs/specs/remote-approval/spec.md).
|
|
2
|
+
//
|
|
3
|
+
// The server tracks which user endpoint is live — the desktop's event
|
|
4
|
+
// stream or a paired phone's — and an approval request routes to the live
|
|
5
|
+
// user device. A remote session (no live desktop stream) routes to the
|
|
6
|
+
// phone automatically. It never broadcasts: the target is one device kind,
|
|
7
|
+
// picked here, and every client learns it from the stream's hello frame.
|
|
8
|
+
/** Where an approval goes. The desktop wins whenever it is live, so a
|
|
9
|
+
* person sitting at the Mac is never interrupted on the phone; a remote
|
|
10
|
+
* session routes to the phone; nothing live still answers "phone" with
|
|
11
|
+
* live: false, so the ask is queued for the one device that can travel to
|
|
12
|
+
* the person rather than broadcast. */
|
|
13
|
+
export function pickApprovalRoute(liveness) {
|
|
14
|
+
if (liveness.desktop > 0)
|
|
15
|
+
return { device: "desktop", live: true };
|
|
16
|
+
if (liveness.phone > 0)
|
|
17
|
+
return { device: "phone", live: true };
|
|
18
|
+
return { device: "phone", live: false };
|
|
19
|
+
}
|
|
20
|
+
/** The server's count of live user streams by device kind. Written by the
|
|
21
|
+
* SSE route's add/remove, read when an approval is raised or a client
|
|
22
|
+
* asks. Counts, never identities: routing is about which kind of device is
|
|
23
|
+
* present, not who is watching. */
|
|
24
|
+
export class DeviceRouteRegistry {
|
|
25
|
+
liveness = { desktop: 0, phone: 0 };
|
|
26
|
+
/** A stream opened. `kind` is the connection's auth kind: loopback is
|
|
27
|
+
* the desktop app on this machine; a paired session is a phone. */
|
|
28
|
+
streamOpened(kind) {
|
|
29
|
+
if (kind === "loopback")
|
|
30
|
+
this.liveness.desktop += 1;
|
|
31
|
+
else if (kind === "session")
|
|
32
|
+
this.liveness.phone += 1;
|
|
33
|
+
}
|
|
34
|
+
streamClosed(kind) {
|
|
35
|
+
if (kind === "loopback")
|
|
36
|
+
this.liveness.desktop = Math.max(0, this.liveness.desktop - 1);
|
|
37
|
+
else if (kind === "session")
|
|
38
|
+
this.liveness.phone = Math.max(0, this.liveness.phone - 1);
|
|
39
|
+
}
|
|
40
|
+
route() {
|
|
41
|
+
return pickApprovalRoute(this.liveness);
|
|
42
|
+
}
|
|
43
|
+
counts() {
|
|
44
|
+
return { ...this.liveness };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** End one stream: drop it from the fan-out set, end the response, and
|
|
48
|
+
* tell approval routing the device went away. Every removal path goes
|
|
49
|
+
* through here so the routing counts cannot drift from the stream set. */
|
|
50
|
+
export function removeStream(clients, registry, client) {
|
|
51
|
+
if (!clients.delete(client))
|
|
52
|
+
return;
|
|
53
|
+
registry.streamClosed(client.authKind);
|
|
54
|
+
try {
|
|
55
|
+
client.res.end();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* already gone */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1615,6 +1615,8 @@ export const ClaudeDriver = {
|
|
|
1615
1615
|
// A settled turn owns no retry budget. Retained CLI sessions may run
|
|
1616
1616
|
// many later turns on this thread, and each must start fresh.
|
|
1617
1617
|
retryState.delete(threadId);
|
|
1618
|
+
if (t.authFailed)
|
|
1619
|
+
closeSession(threadId, "sign-in required");
|
|
1618
1620
|
emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
|
|
1619
1621
|
if (session.child.exitCode === null && !session.closing)
|
|
1620
1622
|
armIdle(threadId);
|
|
@@ -1772,11 +1774,7 @@ export const ClaudeDriver = {
|
|
|
1772
1774
|
case "assistant": {
|
|
1773
1775
|
const msg = o.message ?? {};
|
|
1774
1776
|
const text = firstText(msg.content);
|
|
1775
|
-
//
|
|
1776
|
-
// only content is the CLI's own "run /login" instruction — a
|
|
1777
|
-
// command this app has no terminal to run, so relaying it as a
|
|
1778
|
-
// reply strands the user. Every other engine reports this as a
|
|
1779
|
-
// setup error; that is what routes them to the sign-in card.
|
|
1777
|
+
// CLI auth diagnostics enter the shared fallback path, never assistant text.
|
|
1780
1778
|
if (claudeAuthFailure(o, text)) {
|
|
1781
1779
|
if (session.turn)
|
|
1782
1780
|
session.turn.authFailed = true;
|
|
@@ -1853,12 +1851,16 @@ export const ClaudeDriver = {
|
|
|
1853
1851
|
if (o.origin?.kind === "task-notification")
|
|
1854
1852
|
break;
|
|
1855
1853
|
{
|
|
1856
|
-
//
|
|
1857
|
-
// so it is the turn's figure. cache reads count as input: they
|
|
1858
|
-
// are billed (at the cache rate) and they fill the window — but
|
|
1859
|
-
// they are reported separately too, so the UI can show how much
|
|
1860
|
-
// of the figure was context re-read rather than new text.
|
|
1854
|
+
// Result usage includes input, output and cached context.
|
|
1861
1855
|
const t = session.turn;
|
|
1856
|
+
// Some CLI failures exist only on the result, with no assistant frame.
|
|
1857
|
+
const resultError = [o.result, ...(Array.isArray(o.errors) ? o.errors : [])]
|
|
1858
|
+
.filter((value) => typeof value === "string").join("\n");
|
|
1859
|
+
if (o.is_error === true && !t?.authFailed && claudeAuthFailure({ error: o.error ?? "api_error" }, resultError)) {
|
|
1860
|
+
if (t)
|
|
1861
|
+
t.authFailed = true;
|
|
1862
|
+
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: resultError, setup: true, failure: "auth_failed" });
|
|
1863
|
+
}
|
|
1862
1864
|
const usage = o.usage
|
|
1863
1865
|
? {
|
|
1864
1866
|
input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
|
|
@@ -1870,7 +1872,7 @@ export const ClaudeDriver = {
|
|
|
1870
1872
|
: undefined;
|
|
1871
1873
|
const earlier = t?.carried;
|
|
1872
1874
|
const result = {
|
|
1873
|
-
ok: o.is_error !== true,
|
|
1875
|
+
ok: o.is_error !== true && !t?.authFailed,
|
|
1874
1876
|
stopReason: t?.authFailed ? "auth_required"
|
|
1875
1877
|
: t?.refreshConflict && o.is_error === true ? "credential_refresh_conflict"
|
|
1876
1878
|
: claudeResultStopReason(o),
|
|
@@ -156,20 +156,13 @@ export function managedCodexArgs(config) {
|
|
|
156
156
|
}
|
|
157
157
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
158
158
|
const DENY_TIMEOUT_NOTE = "Atomic: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (renamedMcpServers.has(name))
|
|
159
|
+
const notedMcpServers = new Set();
|
|
160
|
+
/** Logged once per server and reason: a rename or a left-out server is deliberate, not a lost one. */
|
|
161
|
+
function noteMcpServerOnce(key, line) {
|
|
162
|
+
if (notedMcpServers.has(key))
|
|
164
163
|
return;
|
|
165
|
-
|
|
166
|
-
console.error(`codex: MCP server ${
|
|
167
|
-
}
|
|
168
|
-
function noteSkippedSseServer(name) {
|
|
169
|
-
if (skippedSseServers.has(name))
|
|
170
|
-
return;
|
|
171
|
-
skippedSseServers.add(name);
|
|
172
|
-
console.error(`codex: MCP server ${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
164
|
+
notedMcpServers.add(key);
|
|
165
|
+
console.error(`codex: MCP server ${line}`);
|
|
173
166
|
}
|
|
174
167
|
/** A TOML inline table for a `-c key=value` override; JSON string quoting
|
|
175
168
|
* is valid TOML basic-string quoting. */
|
|
@@ -645,16 +638,18 @@ export const CodexDriver = {
|
|
|
645
638
|
// instance's legacy fullAuto setting. Harness turns always send an
|
|
646
639
|
// explicit mode, which takes precedence.
|
|
647
640
|
const approvalMode = turn.approvalMode ?? (config.fullAuto ? "full" : "ask");
|
|
641
|
+
// The person's servers this turn mounts. One that would clobber a
|
|
642
|
+
// harness-owned variable is left out, not fatal: the rest still mount.
|
|
643
|
+
const customServers = {};
|
|
648
644
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
649
|
-
if ("url" in server)
|
|
650
|
-
continue;
|
|
651
645
|
// The mcp-remote bridge runs on Atomic's own Electron-as-Node and sets
|
|
652
646
|
// ELECTRON_RUN_AS_NODE=1, the value the harness mounts already share.
|
|
653
|
-
const reserved = Object.entries(server.env)
|
|
647
|
+
const reserved = "url" in server ? undefined : Object.entries(server.env)
|
|
654
648
|
.find(([key, value]) => isHarnessOwnedMcpEnvName(key) && !(key === "ELECTRON_RUN_AS_NODE" && value === "1"))?.[0];
|
|
655
|
-
if (reserved)
|
|
656
|
-
|
|
657
|
-
|
|
649
|
+
if (reserved)
|
|
650
|
+
noteMcpServerOnce(`reserved:${name}`, `${JSON.stringify(name)} sets reserved environment variable ${JSON.stringify(reserved)} — left out of this bot's turns until its settings change`);
|
|
651
|
+
else
|
|
652
|
+
customServers[name] = server;
|
|
658
653
|
}
|
|
659
654
|
let autoAcceptPermissions = approvalMode === "full";
|
|
660
655
|
if (active.has(threadId))
|
|
@@ -692,7 +687,7 @@ export const CodexDriver = {
|
|
|
692
687
|
// this child actually loads. The mounts assign the same values again,
|
|
693
688
|
// in the same order, so the final environment is unchanged.
|
|
694
689
|
const childCwd = turn.cwd ?? homedir();
|
|
695
|
-
for (const server of Object.values(
|
|
690
|
+
for (const server of Object.values(customServers)) {
|
|
696
691
|
if (!("url" in server))
|
|
697
692
|
Object.assign(env, server.env);
|
|
698
693
|
}
|
|
@@ -720,17 +715,17 @@ export const CodexDriver = {
|
|
|
720
715
|
// mount name → configured name, so an approval request naming the
|
|
721
716
|
// mounted server can be traced back to the person's own entry
|
|
722
717
|
const customByMountName = new Map();
|
|
723
|
-
for (const [name, server] of Object.entries(
|
|
718
|
+
for (const [name, server] of Object.entries(customServers)) {
|
|
724
719
|
// codex speaks streamable HTTP to a remote server, not the older
|
|
725
720
|
// SSE transport: such an entry still reaches Claude bots, and is
|
|
726
721
|
// left out here rather than mounted as something it is not
|
|
727
722
|
if ("url" in server && server.type === "sse") {
|
|
728
|
-
|
|
723
|
+
noteMcpServerOnce(`sse:${name}`, `${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
729
724
|
continue;
|
|
730
725
|
}
|
|
731
726
|
const mountName = mountedMcpServerName(name, declaredInCodexConfig);
|
|
732
727
|
if (mountName !== name)
|
|
733
|
-
|
|
728
|
+
noteMcpServerOnce(`renamed:${name}`, `${JSON.stringify(name)} is also declared in Codex's own config.toml — mounted as ${JSON.stringify(mountName)} for this bot so the two do not merge`);
|
|
734
729
|
customByMountName.set(mountName, name);
|
|
735
730
|
mountMcpServer(appServerArgs, env, mountName, server, false);
|
|
736
731
|
}
|
|
@@ -213,9 +213,8 @@ function idsFromModelsPayload(payload) {
|
|
|
213
213
|
const low = id.toLowerCase();
|
|
214
214
|
if (low.includes("embed") || low.includes("bge-") || low.includes("nomic"))
|
|
215
215
|
return [];
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
return [];
|
|
216
|
+
// Ollama Cloud tags a signed-in Ollama serves are listed too (Erick,
|
|
217
|
+
// 2026-10-24); the label says they bill a plan.
|
|
219
218
|
return [id];
|
|
220
219
|
});
|
|
221
220
|
}
|
|
@@ -349,7 +348,7 @@ export async function probeLocalInjects(env = process.env, fetchImpl = fetch) {
|
|
|
349
348
|
id: encodeInjectId(host.id, model),
|
|
350
349
|
host: host.id,
|
|
351
350
|
model,
|
|
352
|
-
label: `${model} (${host.label})`,
|
|
351
|
+
label: isOllamaCloudModel(model) ? `${model} (${host.label}, plan-billed)` : `${model} (${host.label})`,
|
|
353
352
|
loaded: loaded.has(model),
|
|
354
353
|
...(contextWindow ? { contextWindow } : {}),
|
|
355
354
|
});
|
|
@@ -3,21 +3,11 @@
|
|
|
3
3
|
// engine of its ladder that is ready. Everything here is pure except the
|
|
4
4
|
// snapshot cache, so the server wires it to the live registry and the tests
|
|
5
5
|
// drive it with plain candidates.
|
|
6
|
-
import { BOT_TIERS, DEFAULT_TIER_CLASSES, MODEL_CLASSES, classBelow, isBotTier, toModelClass } from "../shared/engine-tiers.js";
|
|
6
|
+
import { BOT_TIERS, DEFAULT_LADDERS, DEFAULT_TIER_CLASSES, MODEL_CLASSES, classBelow, isBotTier, toModelClass } from "../shared/engine-tiers.js";
|
|
7
7
|
import { modelHost } from "../shared/engine-choices.js";
|
|
8
|
+
export { DEFAULT_LADDERS };
|
|
8
9
|
import { subscriptionProviderForDriver } from "../shared/subscription-bots.js";
|
|
9
10
|
import { ATOMIC_ENGINE_INSTANCE, engineIntent, localModel, modelBilling, modelProvider, paidCandidate, perTokenModel } from "./harness-import/engine-policy.js";
|
|
10
|
-
/** Hand-written ladders, used only when no model has a class yet (the
|
|
11
|
-
* server always has the bundled baseline, so in practice the generated
|
|
12
|
-
* ladders below apply). Pay-per-token engines need no rung: they form the
|
|
13
|
-
* automatic last band of every ladder (resolveLadder). */
|
|
14
|
-
export const DEFAULT_LADDERS = {
|
|
15
|
-
orchestrator: [{ engine: "claudeAgent" }, { engine: "atomic-engine" }, { engine: "codex" }],
|
|
16
|
-
engineering: [{ engine: "claudeAgent" }, { engine: "codex" }, { engine: "cursorAgent" }, { engine: "kimiAgent" }],
|
|
17
|
-
research: [{ engine: "grokAgent" }, { engine: "antigravityAgent" }, { engine: "kimiAgent" }],
|
|
18
|
-
writing: [{ engine: "antigravityAgent" }, { engine: "kimiAgent" }, { engine: "codex" }, { engine: "claudeAgent" }],
|
|
19
|
-
routine: [{ engine: "qwenAgent" }, { engine: "kimiAgent" }, { engine: "antigravityAgent" }],
|
|
20
|
-
};
|
|
21
11
|
/** Engines a tier tries first, whatever the class catalog order says: Chibi
|
|
22
12
|
* runs on Claude (Erick, 2026-10-13: open-weight models broke its
|
|
23
13
|
* orchestration). Its ladder starts with Claude's best model graded in its
|
|
@@ -379,7 +369,7 @@ export function resolveFailover(request) {
|
|
|
379
369
|
const ids = candidate.models.options.map((option) => option.id);
|
|
380
370
|
const multi = ids.some((id) => modelProvider(candidate, id) !== undefined);
|
|
381
371
|
// (A local host's model, Ollama Cloud tags included, is not one of them:
|
|
382
|
-
//
|
|
372
|
+
// it joins when a ladder names it, else by Ollama Cloud or local rules.)
|
|
383
373
|
const models = multi ? ids.filter((id) => modelProvider(candidate, id) !== undefined && modelBilling(candidate, id) === "subscription")
|
|
384
374
|
: candidate.models.default && modelBilling(candidate, candidate.models.default) === undefined ? [candidate.models.default] : [];
|
|
385
375
|
for (const model of models) {
|
|
@@ -410,7 +400,8 @@ export function resolveFailover(request) {
|
|
|
410
400
|
return "tried";
|
|
411
401
|
if (!keys && (keyBased(candidate) || perTokenModel(candidate, model)))
|
|
412
402
|
return "key-later";
|
|
413
|
-
|
|
403
|
+
// a ladder-named local model is a normal rung (Erick, 2026-10-24)
|
|
404
|
+
if (!named && request.isLocal?.(candidate, model) && !(request.localAllowed && modelClass === "Añejo"))
|
|
414
405
|
return "local";
|
|
415
406
|
const step = named ? ladder.indexOf(entry) : ladder.length;
|
|
416
407
|
return effective(candidate, model, named ? entry : undefined, step) === modelClass ? undefined : "class";
|