@chibitek/chibi 0.1.155 → 0.1.158

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 (98) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
  2. package/dist/assets/{arc-DU5fSmKU.js → arc-CSHQ9Dx3.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
  6. package/dist/assets/channel-bPN03jIt.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
  33. package/dist/assets/{index-CG1OvRgO.js → index-CG4AFPb4.js} +1 -1
  34. package/dist/assets/{index-D_8SD_0K.js → index-CMcr5k3Z.js} +205 -200
  35. package/dist/assets/index-CZjRcUdn.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
  40. package/dist/assets/{linear-DTGq6qn5.js → linear-hX6nFOQr.js} +1 -1
  41. package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-DZObGLN7.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +68 -14
  61. package/dist-server/index.js +5633 -4562
  62. package/dist-server/openmausbot.js +140 -81
  63. package/dist-server/pair-cli.js +140 -81
  64. package/dist-server/server/app-installers.js +107 -7
  65. package/dist-server/server/avatar-subscription.js +51 -0
  66. package/dist-server/server/cli-help.js +74 -0
  67. package/dist-server/server/cli.js +37 -78
  68. package/dist-server/server/colima-docker.js +136 -0
  69. package/dist-server/server/container-computer.js +31 -63
  70. package/dist-server/server/container-mcp.js +5 -0
  71. package/dist-server/server/device-routing.js +60 -0
  72. package/dist-server/server/drivers/claude.js +13 -11
  73. package/dist-server/server/drivers/local-inject.js +3 -4
  74. package/dist-server/server/engine-failover.js +22 -12
  75. package/dist-server/server/engine-tiers.js +5 -14
  76. package/dist-server/server/index.js +124 -120
  77. package/dist-server/server/personal-goal-runtime.js +86 -0
  78. package/dist-server/server/personal-goal-turn.js +125 -0
  79. package/dist-server/server/personal-goals.js +195 -0
  80. package/dist-server/server/port-in-use.js +21 -0
  81. package/dist-server/server/routes/local-models.js +3 -2
  82. package/dist-server/server/session-fallback.js +76 -0
  83. package/dist-server/server/skill-fetch.js +14 -0
  84. package/dist-server/server/vault/routes.js +43 -0
  85. package/dist-server/server/vault/runtime.js +48 -0
  86. package/dist-server/server/vault/store.js +177 -0
  87. package/dist-server/shared/desktop-private-port.js +1 -0
  88. package/dist-server/shared/engine-choices.js +15 -0
  89. package/dist-server/shared/engine-tiers.js +74 -2
  90. package/dist-server/shared/local-models.js +1 -3
  91. package/dist-server/shared/personal-goal.js +43 -0
  92. package/dist-server/shared/subscription-bots.js +3 -0
  93. package/dist-server/shared/vault.js +59 -0
  94. package/dist-server/vps-container-mcp.js +4 -2
  95. package/package.json +1 -1
  96. package/dist/assets/channel-GBaPOaz-.js +0 -1
  97. package/dist/assets/index-C8YP7Zxj.css +0 -1
  98. package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.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). 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,51 @@
1
+ import { decodeGeneratedImage } from "./generated-image.js";
2
+ import { avatarGenerationPrompt, AVATAR_IMAGE_TIMEOUT_MS } from "./avatar-image.js";
3
+ /** Capability and current billing are both required. An authenticated text
4
+ * engine is not evidence that its subscription can generate images. */
5
+ export async function avatarSubscription(instances) {
6
+ for (const instance of instances) {
7
+ if (!instance.enabled || !instance.generateImage)
8
+ continue;
9
+ try {
10
+ const status = await instance.snapshot();
11
+ if (status.state === "available" && status.authenticated === true &&
12
+ status.billing === "subscription" && status.account?.method !== "api-key")
13
+ return instance;
14
+ }
15
+ catch {
16
+ // A broken account must not conceal another eligible signed-in account.
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ /** Never turn a subscription failure into an unexpected API charge. */
22
+ export async function generateSubscriptionAvatar(instance, bot, direction, timeoutMs = AVATAR_IMAGE_TIMEOUT_MS) {
23
+ if (!instance.generateImage || await avatarSubscription([instance]) !== instance) {
24
+ throw Object.assign(new Error("This image subscription is no longer available. Check its sign-in in Settings."), { status: 409 });
25
+ }
26
+ const controller = new AbortController();
27
+ let timer;
28
+ try {
29
+ const encoded = await Promise.race([
30
+ instance.generateImage(avatarGenerationPrompt(bot, direction), { signal: controller.signal }),
31
+ new Promise((_, reject) => {
32
+ timer = setTimeout(() => {
33
+ controller.abort();
34
+ reject(new Error("timeout"));
35
+ }, timeoutMs);
36
+ }),
37
+ ]);
38
+ const image = decodeGeneratedImage(encoded);
39
+ if (image.mime === "image/gif")
40
+ throw new Error("unsupported avatar format");
41
+ return { bytes: image.bytes, mime: image.mime };
42
+ }
43
+ catch {
44
+ throw Object.assign(new Error(controller.signal.aborted
45
+ ? "Subscription avatar generation timed out. Try again."
46
+ : "Your subscription could not generate this avatar. Check its sign-in and usage allowance, then try again."), { status: 502 });
47
+ }
48
+ finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
@@ -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
@@ -38,6 +40,7 @@ import { resolveLoopbackTrust } from "./request-auth.js";
38
40
  import { writeFileAtomic } from "./atomic.js";
39
41
  import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
40
42
  import { runServiceCommand } from "./service-cli.js";
43
+ import { reportBusyPort } from "./port-in-use.js";
41
44
  import { colorDepth, renderBanner, shouldShowBanner, terminalBackground } from "./cli-banner.js";
42
45
  import { runFleetCommand } from "./fleet-cli.js";
43
46
  import { startFleetAgent } from "./fleet-agent.js";
@@ -47,7 +50,7 @@ import { defaultSetupIo, SetupCancelled } from "./cli-prompts.js";
47
50
  import { normalizePhoneOrigin, phonePairingInstructions, runPhoneSetup } from "./cli-phone-setup.js";
48
51
  import { cleanupTunnelOrigin, createTunnelAccount, createTunnelOrigin, describeTunnelAccount, describeTunnelState, ensureCloudflared, FLEET_CREDENTIAL_ENV, fleetAccess, fleetCredential, guardianEntry, startTunnel, tunnelAccess, } from "./tunnel.js";
49
52
  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"];
53
+ export const COMMANDS = ["command", "goal", "setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "fleet", "help", "--help", "-h"];
51
54
  export function parseArgs(argv, env = process.env) {
52
55
  const implicitStart = !argv.length || (argv[0].startsWith("--") && argv[0] !== "--help");
53
56
  const [command = "start", ...rest] = implicitStart ? ["start", ...argv] : argv;
@@ -76,7 +79,15 @@ export function parseArgs(argv, env = process.env) {
76
79
  return v;
77
80
  };
78
81
  try {
79
- if (arg === "--port")
82
+ if ((options.command === "command" || options.command === "goal") && arg === "--bot")
83
+ options.botId = value();
84
+ else if ((options.command === "command" || options.command === "goal") && arg === "--group")
85
+ options.groupId = value();
86
+ else if ((options.command === "command" || options.command === "goal") && arg === "--thread")
87
+ options.threadId = value();
88
+ else if ((options.command === "command" || options.command === "goal") && arg === "--text")
89
+ options.commandText = value();
90
+ else if (arg === "--port")
80
91
  options.port = Number(value());
81
92
  else if (arg === "--data-dir")
82
93
  options.dataDir = resolve(value());
@@ -182,6 +193,8 @@ export function parseArgs(argv, env = process.env) {
182
193
  return { error: "--public-url must start with http:// or https://" };
183
194
  if (options.tailscale && options.tunnel)
184
195
  return { error: "choose one of --tailscale (your tailnet) and --tunnel (a public address)" };
196
+ if ((options.command === "command" || options.command === "goal") && (Boolean(options.botId) === Boolean(options.groupId) || !options.threadId || !options.commandText))
197
+ return { error: "command and goal require exactly one of --bot ID or --group ID, plus --thread ID --text TEXT" };
185
198
  if (options.command === "access" && !options.accessAction)
186
199
  return { error: "access needs one of: list, add EMAIL [--chat-only], remove EMAIL" };
187
200
  if (options.command === "service" && !options.serviceAction)
@@ -204,78 +217,6 @@ export function parseArgs(argv, env = process.env) {
204
217
  }
205
218
  return options;
206
219
  }
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
220
  export function defaultIo() {
280
221
  return {
281
222
  log: (line) => console.log(line),
@@ -890,6 +831,8 @@ export async function runServe(options, log = console.log) {
890
831
  console.error(`something already answers on http://127.0.0.1:${options.port}; use \`chibi pair\` against it, or --port for a second server`);
891
832
  return 1;
892
833
  }
834
+ if (await reportBusyPort(options.port, options.dataDir === join(homedir(), ".atomic-app") ? "" : ` --data-dir ${options.dataDir}`))
835
+ return 1;
893
836
  let publicUrl = options.publicUrl;
894
837
  let tailscale = null;
895
838
  if (options.tailscale) {
@@ -1191,10 +1134,8 @@ export async function runOnboardingCommand(options, io = defaultIo(), startServe
1191
1134
  saveCliStartup(options.dataDir, startupPreferences(launch));
1192
1135
  }
1193
1136
  if (options.command === "setup") {
1194
- io.log("\nAll set. Start with: chibi (or npx @chibitek/chibi without a global install).");
1195
- if (options.dataDir !== join(homedir(), ".atomic-app") || options.port !== 8799) {
1196
- io.log(`Use the same --data-dir (${options.dataDir}) and --port (${options.port}) options when starting.`);
1197
- }
1137
+ const flags = [options.dataDir !== join(homedir(), ".atomic-app") && ` --data-dir ${options.dataDir}`, options.port !== 8799 && ` --port ${options.port}`].filter(Boolean).join("");
1138
+ io.log(`\nAll set. Start with: chibi${flags ? ` start${flags}` : ""} (or npx @chibitek/chibi without a global install).`);
1198
1139
  return 0;
1199
1140
  }
1200
1141
  if (saved)
@@ -1232,6 +1173,24 @@ export async function main(argv = process.argv.slice(2)) {
1232
1173
  console.log(`\n${welcomeBanner(options)}\n`);
1233
1174
  }
1234
1175
  switch (options.command) {
1176
+ case "command":
1177
+ case "goal": {
1178
+ const text = options.command === "goal" ? `/goal ${options.commandText}` : options.commandText;
1179
+ const roomGoal = options.groupId ? /^\/goal(?:\s+([\s\S]*))?$/.exec(text ?? "") : null;
1180
+ if (roomGoal && (!roomGoal[1]?.trim() || (roomGoal[1].startsWith("--") || /^(status|pause|resume|stop|help)(?:\s|$)/.test(roomGoal[1])))) {
1181
+ console.error("Room goals accept an objective. Personal goal controls and budgets require --bot.");
1182
+ return 2;
1183
+ }
1184
+ const result = await api(options.port, `/api/${options.groupId ? "groups" : "bots"}/${encodeURIComponent(options.groupId ?? options.botId)}/messages`, {
1185
+ method: "POST", body: JSON.stringify({ threadId: options.threadId, text: roomGoal ? roomGoal[1] : text, ...(roomGoal ? { mode: "goal" } : {}), sendId: randomBytes(16).toString("hex") }),
1186
+ });
1187
+ if (result.status >= 400) {
1188
+ console.error(result.body.error ?? `Server returned ${result.status}`);
1189
+ return 1;
1190
+ }
1191
+ console.log(options.json ? JSON.stringify(result.body) : result.body.goalSummary ?? JSON.stringify(result.body));
1192
+ return 0;
1193
+ }
1235
1194
  case "setup":
1236
1195
  case "start":
1237
1196
  return runOnboardingCommand(options);
@@ -0,0 +1,136 @@
1
+ // Colima is the only container engine Atomic starts or talks to for the
2
+ // Local VM on macOS. Docker Desktop is never launched, and a docker context
3
+ // it leaves behind (desktop-linux) must not decide where `docker` commands
4
+ // go, so every docker invocation Atomic makes on a Mac is pinned to the
5
+ // Colima profile's socket through DOCKER_HOST.
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ export const COLIMA_DEFAULT_PROFILE = "default";
10
+ const readText = (path) => {
11
+ try {
12
+ return readFileSync(path, "utf8");
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ function homeOf(env) {
19
+ return env.HOME?.trim() || homedir();
20
+ }
21
+ /** The Colima profile Atomic uses: the one named by a `colima-<profile>`
22
+ * docker context (DOCKER_CONTEXT, then ~/.docker/config.json), else the
23
+ * default profile. Any other context, Docker Desktop's included, is ignored:
24
+ * it never redirects Atomic away from Colima. Reads a file only; never runs
25
+ * docker, since docker itself is what this pins. */
26
+ export function colimaProfile(env = process.env, read = readText) {
27
+ let context = env.DOCKER_CONTEXT?.trim() ?? "";
28
+ if (!context) {
29
+ const configDir = env.DOCKER_CONFIG?.trim() || join(homeOf(env), ".docker");
30
+ try {
31
+ const parsed = JSON.parse(read(join(configDir, "config.json")) ?? "{}");
32
+ context = typeof parsed.currentContext === "string" ? parsed.currentContext.trim() : "";
33
+ }
34
+ catch {
35
+ context = "";
36
+ }
37
+ }
38
+ if (context.startsWith("colima-") && /^[A-Za-z0-9_.-]+$/.test(context.slice("colima-".length))) {
39
+ return context.slice("colima-".length);
40
+ }
41
+ return COLIMA_DEFAULT_PROFILE;
42
+ }
43
+ /** Colima's docker socket for a profile. COLIMA_HOME wins; otherwise
44
+ * ~/.colima, or ~/.config/colima when only that one holds the profile. */
45
+ export function colimaDockerSocket(profile = COLIMA_DEFAULT_PROFILE, env = process.env, exists = existsSync) {
46
+ const explicit = env.COLIMA_HOME?.trim();
47
+ if (explicit)
48
+ return join(explicit, profile, "docker.sock");
49
+ const home = homeOf(env);
50
+ const legacy = join(home, ".colima");
51
+ const xdg = join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "colima");
52
+ const base = !exists(join(legacy, profile)) && exists(join(xdg, profile)) ? xdg : legacy;
53
+ return join(base, profile, "docker.sock");
54
+ }
55
+ export function colimaDockerHost(profile = COLIMA_DEFAULT_PROFILE, env = process.env, exists) {
56
+ return `unix://${colimaDockerSocket(profile, env, exists)}`;
57
+ }
58
+ /** The environment for one `docker` invocation. On macOS it always targets
59
+ * Colima: DOCKER_HOST is the Colima socket and DOCKER_CONTEXT is dropped (the
60
+ * CLI refuses both at once). Elsewhere the environment is returned as is. */
61
+ export function dockerEnvForColima(env = process.env, platform = process.platform, options = {}) {
62
+ if (platform !== "darwin")
63
+ return env;
64
+ const profile = colimaProfile(env, options.read);
65
+ const pinned = { ...env, DOCKER_HOST: colimaDockerHost(profile, env, options.exists) };
66
+ delete pinned.DOCKER_CONTEXT;
67
+ return pinned;
68
+ }
69
+ const COLIMA_START_TIMEOUT_MS = 4 * 60_000;
70
+ const COLIMA_STOP_TIMEOUT_MS = 60_000;
71
+ /** Colima on the profile the docker CLI is pinned to. `colima start` on a
72
+ * running profile is a no-op, so an instance the person started with their
73
+ * own flags is left exactly as it is. */
74
+ export function colimaStartPlan(env = process.env) {
75
+ const profile = colimaProfile(env);
76
+ const profileArgs = profile === COLIMA_DEFAULT_PROFILE ? [] : ["--profile", profile];
77
+ return {
78
+ label: "Colima",
79
+ profile,
80
+ start: { command: "colima", args: ["start", ...profileArgs], timeoutMs: COLIMA_START_TIMEOUT_MS },
81
+ stop: { command: "colima", args: ["stop", ...profileArgs], timeoutMs: COLIMA_STOP_TIMEOUT_MS },
82
+ };
83
+ }
84
+ /** Bring docker back on Colima: start and wait for `docker info`; if it
85
+ * does not answer, start again (retry); then restart the profile (`colima
86
+ * stop`, `colima start`); then give up with a clear error. Without a plan
87
+ * (Colima not installed) it says so on a Mac. Docker Desktop is never
88
+ * opened on any path. */
89
+ export async function startColima(plan, before, deps) {
90
+ if (!plan) {
91
+ return {
92
+ status: before,
93
+ started: null,
94
+ error: deps.platform === "darwin"
95
+ ? "Docker is not answering and Colima is not installed. Atomic runs the Local VM on Colima only and never opens Docker Desktop: install Colima (brew install colima), then try again."
96
+ : null,
97
+ };
98
+ }
99
+ const sleep = deps.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
100
+ const quietly = async (step) => {
101
+ // Colima can exit non-zero while its VM is still coming up, and the wait
102
+ // below is the real test; a launch that truly failed times out.
103
+ await deps.runner(step.command, step.args, step.timeoutMs).catch(() => undefined);
104
+ };
105
+ const waitUntilUp = async (ms) => {
106
+ const deadline = Date.now() + ms;
107
+ let status = await deps.status();
108
+ while (!status.daemonUp && Date.now() < deadline) {
109
+ await sleep(deps.pollMs ?? 2000);
110
+ status = await deps.status();
111
+ }
112
+ return status;
113
+ };
114
+ await quietly(plan.start);
115
+ let status = await waitUntilUp(deps.waitMs ?? 120_000);
116
+ if (!status.daemonUp) {
117
+ // A VM still booting often answers late.
118
+ await quietly(plan.start);
119
+ status = await waitUntilUp(deps.retryWaitMs ?? 30_000);
120
+ }
121
+ if (!status.daemonUp) {
122
+ // A profile that reads as running but whose socket is dead only comes
123
+ // back after a stop.
124
+ await quietly(plan.stop);
125
+ await quietly(plan.start);
126
+ status = await waitUntilUp(deps.waitMs ?? 120_000);
127
+ }
128
+ const profileFlag = plan.profile === COLIMA_DEFAULT_PROFILE ? "" : ` --profile ${plan.profile}`;
129
+ return {
130
+ status,
131
+ started: plan.label,
132
+ error: status.daemonUp
133
+ ? null
134
+ : `Colima (profile ${plan.profile}) did not answer after a start, a retry and a restart. Run \`colima status${profileFlag}\` to see why; Atomic does not fall back to Docker Desktop.`,
135
+ };
136
+ }