@chibitek/chibi 0.1.155 → 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.
Files changed (91) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-BkYbog6D.js} +1 -1
  2. package/dist/assets/{arc-DU5fSmKU.js → arc-QwTuMsxp.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-GdicO2PQ.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-Dkr29TFF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-Dz_3dstx.js} +1 -1
  6. package/dist/assets/channel-D2y0i2ps.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-CGsfu-PI.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-CDqI37_6.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-DnmSuyTb.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-Dh7tXcJT.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-D_SM1ZrY.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-DmeMuTsI.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-R1UT9b5f.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-DpPfU1yO.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-DlihfTkM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-BlTV6lSC.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-Br1kPFWw.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-BqPp3ZMd.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-DFfs_k8-.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-Ch3xIJA7.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DIHUD87N.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-CAgQQdeF.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-9HVKodrI.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-DU2kDq_h.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-D-qTAhHD.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-C31SnI9P.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-BHrdiBVd.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DWyljRo5.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-DyY59XPp.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-BevMIeAy.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-Ct18WHf4.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-yhixO1N3.js} +1 -1
  33. package/dist/assets/{index-CG1OvRgO.js → index-BEJ8EKQt.js} +1 -1
  34. package/dist/assets/index-DGWeJcck.css +1 -0
  35. package/dist/assets/{index-D_8SD_0K.js → index-ziyH_uer.js} +170 -168
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-CYa-r_S5.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-CnxwM7gY.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-RKQfXBrp.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-CSlRDIF6.js} +1 -1
  40. package/dist/assets/{linear-DTGq6qn5.js → linear-Ch19KVyt.js} +1 -1
  41. package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-Bek7I_t6.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-B_LiP8su.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-DhzQeiZf.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-D-BYNKHS.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-DnCm0heP.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-COqgYnhn.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP-Dvt72ajf.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-CisC9nbp.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-BO8xqvxa.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-DLk1jBzf.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-C_wutSE7.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-hqo1UlEi.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-C_DJldPg.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-CsidVXMA.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-Bs-IIqn_.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-CXEUyxaj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-B9qvmm4P.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +81 -81
  61. package/dist-server/drivers/agents-proxy.js +81 -81
  62. package/dist-server/index.js +2354 -1729
  63. package/dist-server/openmausbot.js +676 -643
  64. package/dist-server/pair-cli.js +676 -643
  65. package/dist-server/server/app-installers.js +107 -7
  66. package/dist-server/server/cli-help.js +74 -0
  67. package/dist-server/server/cli.js +32 -74
  68. package/dist-server/server/device-routing.js +60 -0
  69. package/dist-server/server/drivers/claude.js +13 -11
  70. package/dist-server/server/drivers/local-inject.js +3 -4
  71. package/dist-server/server/engine-tiers.js +5 -14
  72. package/dist-server/server/index.js +84 -104
  73. package/dist-server/server/personal-goal-runtime.js +86 -0
  74. package/dist-server/server/personal-goal-turn.js +125 -0
  75. package/dist-server/server/personal-goals.js +195 -0
  76. package/dist-server/server/routes/local-models.js +3 -2
  77. package/dist-server/server/skill-fetch.js +14 -0
  78. package/dist-server/shared/engine-choices.js +15 -0
  79. package/dist-server/shared/engine-tiers.js +74 -2
  80. package/dist-server/shared/local-models.js +1 -3
  81. package/dist-server/shared/personal-goal.js +43 -0
  82. package/dist-server/vps-container-mcp.js +81 -81
  83. package/package.json +1 -1
  84. package/dist/assets/channel-GBaPOaz-.js +0 -1
  85. package/dist/assets/index-C8YP7Zxj.css +0 -1
  86. package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
  87. package/dist-server/connector-proxy.js +0 -205
  88. package/dist-server/server/composio.js +0 -1051
  89. package/dist-server/server/connector-proxy.js +0 -249
  90. package/dist-server/server/mcp-public-registry.js +0 -325
  91. package/dist-server/shared/connector-availability.js +0 -12
@@ -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). macOS only; elsewhere the wizard links to the vendor's page.
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. Podman's package needs an
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 { execFileCommand } from "./procs.js";
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} Macs.`);
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
- if ((deps.platform ?? process.platform) !== "darwin")
172
- throw new AppInstallError("unsupported", "Atomic installs Ollama for you on macOS only.");
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 === "--port")
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
- // An unauthenticated turn comes back as an api-error frame whose
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
- // result.usage is this invocation's total — one process per turn,
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),
@@ -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
- // An Ollama Cloud model is not this machine's: it bills a plan.
217
- if (isOllamaCloudModel(record))
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
- // Ollama Cloud joins as its own engine, local models only by local rules.)
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
- if (request.isLocal?.(candidate, model) && !(request.localAllowed && modelClass === "Añejo"))
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";