@chibitek/chibi 0.1.157 → 0.1.160

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 (107) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-CpuRvzQ3.js} +1 -1
  2. package/dist/assets/{arc-QwTuMsxp.js → arc-ko1e0R6h.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-OZ3SsOOa.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-DLRkBkjs.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-e8Q3qMnw.js} +1 -1
  6. package/dist/assets/channel-Lp7erhx-.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-hCDqaM4Q.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-Dy3Pahu-.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-QMK3iFBF.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-6fyp9ERp.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-DWvOhTaO.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-DgohMW2S.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-sRGeD2Y7.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-CZslD_VF.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-D7ocO3VN.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-T1lLqHHW.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BdeLiJr3.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-BydCA0pv.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-DBxckjwQ.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-CAsvq6Zk.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DTwH3rA_.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-BNYj1bey.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CihwYFxb.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-nouwk3s2.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-BoKewJeA.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-CrE1m9DQ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DRj0aU4Y.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-Do0JzCG5.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CRsI9pXb.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-dtD6wm3G.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-Cn7KyLCo.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-CGH_VWzq.js} +1 -1
  33. package/dist/assets/index-B2fJMh8N.css +1 -0
  34. package/dist/assets/{index-ziyH_uer.js → index-BIcziktH.js} +253 -250
  35. package/dist/assets/{index-BEJ8EKQt.js → index-Bk8iAodB.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-CQKsYTCL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-D5catDe0.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-KjY8mki9.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-BKCyJtge.js} +1 -1
  40. package/dist/assets/{linear-Ch19KVyt.js → linear-B-_obUkq.js} +1 -1
  41. package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DlSzuDKt.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-BFQSaYKh.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-l51izlev.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-6NEXRoZX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-BCC9XlOe.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-Dd4TmJ86.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP-C1E5DaDb.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ci6r7abb.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-BJ-I6WOh.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-Cs5QbLOP.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-Bw--rdvq.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1O9IEz5.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-CTZ7DXaw.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DjwgNG0H.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-DrdJG55h.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-HVq776nz.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-DPf9VEua.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/connector-proxy.js +205 -0
  61. package/dist-server/container-mcp.js +150 -96
  62. package/dist-server/drivers/agents-proxy.js +81 -81
  63. package/dist-server/index.js +7054 -6099
  64. package/dist-server/openmausbot.js +940 -727
  65. package/dist-server/pair-cli.js +940 -727
  66. package/dist-server/server/attachment-mimes.js +58 -0
  67. package/dist-server/server/attachments.js +28 -67
  68. package/dist-server/server/avatar-image.js +1 -0
  69. package/dist-server/server/avatar-subscription.js +74 -0
  70. package/dist-server/server/cli.js +5 -4
  71. package/dist-server/server/colima-docker.js +136 -0
  72. package/dist-server/server/composio.js +1051 -0
  73. package/dist-server/server/connector-proxy.js +249 -0
  74. package/dist-server/server/container-computer.js +31 -63
  75. package/dist-server/server/container-mcp.js +5 -0
  76. package/dist-server/server/drivers/claude-message.js +12 -0
  77. package/dist-server/server/drivers/claude.js +9 -12
  78. package/dist-server/server/drivers/codex-image.js +141 -0
  79. package/dist-server/server/drivers/codex.js +6 -9
  80. package/dist-server/server/drivers/openai-chat.js +13 -2
  81. package/dist-server/server/engine-failover.js +62 -12
  82. package/dist-server/server/engine-failure.js +2 -2
  83. package/dist-server/server/harness-import/history.js +5 -5
  84. package/dist-server/server/harness-import/test-fixtures.js +1 -1
  85. package/dist-server/server/index.js +75 -36
  86. package/dist-server/server/mcp-public-registry.js +325 -0
  87. package/dist-server/server/message-reactions.js +11 -0
  88. package/dist-server/server/model-evidence-context.js +18 -0
  89. package/dist-server/server/model-evidence.js +145 -0
  90. package/dist-server/server/port-in-use.js +21 -0
  91. package/dist-server/server/session-fallback.js +76 -0
  92. package/dist-server/server/vault/routes.js +43 -0
  93. package/dist-server/server/vault/runtime.js +48 -0
  94. package/dist-server/server/vault/store.js +177 -0
  95. package/dist-server/shared/connector-availability.js +12 -0
  96. package/dist-server/shared/desktop-private-port.js +1 -0
  97. package/dist-server/shared/emoji-catalog.json +1 -0
  98. package/dist-server/shared/emoji.js +27 -0
  99. package/dist-server/shared/mascot-bodies.js +9 -1
  100. package/dist-server/shared/model-evidence.js +25 -0
  101. package/dist-server/shared/subscription-bots.js +3 -0
  102. package/dist-server/shared/vault.js +59 -0
  103. package/dist-server/vps-container-mcp.js +86 -84
  104. package/package.json +1 -1
  105. package/dist/assets/channel-D2y0i2ps.js +0 -1
  106. package/dist/assets/index-DGWeJcck.css +0 -1
  107. package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +0 -8
@@ -0,0 +1,58 @@
1
+ /** Accepted upload mimes and the extensions stored on disk for them. */
2
+ /** Mimes the endpoint accepts, mapped to the extension stored on disk.
3
+ * Sniffing is not attempted — a lie here only changes the filename. */
4
+ export const IMAGE_MIMES = {
5
+ "image/png": ".png",
6
+ "image/jpeg": ".jpg",
7
+ "image/gif": ".gif",
8
+ "image/webp": ".webp",
9
+ };
10
+ /** Useful document and audio formats accepted by the upload endpoint.
11
+ * Generic archives, binaries, HTML, SVG, and executable/script mimes stay
12
+ * out. Office/OpenDocument packages are allowed because they are documents,
13
+ * despite using ZIP internally. The claimed mime determines the extension;
14
+ * an attacker-controlled filename never does. */
15
+ export const FILE_MIMES = {
16
+ "audio/opus": ".opus",
17
+ "audio/ogg": ".ogg",
18
+ "audio/mpeg": ".mp3",
19
+ "audio/mp4": ".m4a",
20
+ "audio/x-m4a": ".m4a",
21
+ "audio/aac": ".aac",
22
+ "audio/wav": ".wav",
23
+ "audio/x-wav": ".wav",
24
+ "audio/wave": ".wav",
25
+ "audio/flac": ".flac",
26
+ "audio/x-flac": ".flac",
27
+ "audio/webm": ".webm",
28
+ "text/plain": ".txt",
29
+ "text/markdown": ".md",
30
+ "text/csv": ".csv",
31
+ "text/tab-separated-values": ".tsv",
32
+ "application/json": ".json",
33
+ "application/pdf": ".pdf",
34
+ "application/rtf": ".rtf",
35
+ "text/rtf": ".rtf",
36
+ "application/msword": ".doc",
37
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
38
+ "application/vnd.ms-excel": ".xls",
39
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
40
+ "application/vnd.ms-powerpoint": ".ppt",
41
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
42
+ "application/vnd.oasis.opendocument.text": ".odt",
43
+ "application/vnd.oasis.opendocument.spreadsheet": ".ods",
44
+ "application/vnd.oasis.opendocument.presentation": ".odp",
45
+ };
46
+ export function extensionForMime(mime) {
47
+ if (!mime)
48
+ return null;
49
+ return IMAGE_MIMES[mime.split(";")[0].trim().toLowerCase()] ?? null;
50
+ }
51
+ export function normalizedMime(mime) {
52
+ const value = mime?.split(";")[0]?.trim().toLowerCase();
53
+ return value || null;
54
+ }
55
+ export function extensionForFileMime(mime) {
56
+ const value = normalizedMime(mime);
57
+ return value ? FILE_MIMES[value] ?? null : null;
58
+ }
@@ -2,10 +2,12 @@
2
2
  // ~/.openmausbot/attachments so every CLI engine can open them by path —
3
3
  // the app never ships image bytes through the prompt itself.
4
4
  import { createHash, randomUUID } from "node:crypto";
5
- import { createReadStream, linkSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
5
+ import { createReadStream, linkSync, mkdirSync, readFileSync, readdirSync, statSync, statfsSync, unlinkSync, writeFileSync, } from "node:fs";
6
6
  import { link, open, unlink } from "node:fs/promises";
7
7
  import { join, extname } from "node:path";
8
8
  import { DATA_DIR } from "./config.js";
9
+ import { FILE_MIMES, IMAGE_MIMES, extensionForFileMime, extensionForMime, normalizedMime } from "./attachment-mimes.js";
10
+ export { extensionForFileMime, extensionForMime };
9
11
  export const ATTACHMENTS_DIR = join(DATA_DIR, "attachments");
10
12
  /** The spec's ceiling: a screenshot bigger than this is rejected before it
11
13
  * is ever buffered, matching the composer's existing size discipline. */
@@ -14,9 +16,14 @@ export const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
14
16
  * ceiling. This is a local inbox, not unbounded remote storage. */
15
17
  export const FILE_MAX_BYTES = 25 * 1024 * 1024;
16
18
  /** Attachments are durable because prompts refer to their paths. Never
17
- * silently evict them: once this ceiling is reached, a new upload gets an
18
- * explicit 507 and the person can decide what to remove. */
19
- export const ATTACHMENTS_MAX_BYTES = 512 * 1024 * 1024;
19
+ * silently evict them. There is no fixed quota by default: the store grows
20
+ * until the disk itself runs low (see ATTACHMENTS_DISK_RESERVE_BYTES), then a
21
+ * new upload gets an explicit 507 and the person can decide what to remove.
22
+ * OMB_ATTACHMENTS_MAX_BYTES sets an optional hard ceiling (tests use it). */
23
+ export const ATTACHMENTS_MAX_BYTES = Number(process.env.OMB_ATTACHMENTS_MAX_BYTES) || Number.POSITIVE_INFINITY;
24
+ /** Free space always left on the attachments volume, so uploads can never
25
+ * fill the disk the app, its database and the OS depend on. */
26
+ export const ATTACHMENTS_DISK_RESERVE_BYTES = 2 * 1024 * 1024 * 1024;
20
27
  /** Interrupted streamed uploads use private partial files. A crash can leave
21
28
  * one behind, so future uploads remove only our stale partials — never a
22
29
  * committed attachment that a transcript may still reference. */
@@ -47,55 +54,6 @@ function invalidateAttachmentAccounting() {
47
54
  committedAttachmentBytes = null;
48
55
  attachmentAccountingVersion += 1;
49
56
  }
50
- /** Mimes the endpoint accepts, mapped to the extension stored on disk.
51
- * Sniffing is not attempted — a lie here only changes the filename. */
52
- const IMAGE_MIMES = {
53
- "image/png": ".png",
54
- "image/jpeg": ".jpg",
55
- "image/gif": ".gif",
56
- "image/webp": ".webp",
57
- };
58
- /** Useful document and audio formats accepted by the upload endpoint.
59
- * Generic archives, binaries, HTML, SVG, and executable/script mimes stay
60
- * out. Office/OpenDocument packages are allowed because they are documents,
61
- * despite using ZIP internally. The claimed mime determines the extension;
62
- * an attacker-controlled filename never does. */
63
- const FILE_MIMES = {
64
- "audio/opus": ".opus",
65
- "audio/ogg": ".ogg",
66
- "audio/mpeg": ".mp3",
67
- "audio/mp4": ".m4a",
68
- "audio/x-m4a": ".m4a",
69
- "audio/aac": ".aac",
70
- "audio/wav": ".wav",
71
- "audio/x-wav": ".wav",
72
- "audio/wave": ".wav",
73
- "audio/flac": ".flac",
74
- "audio/x-flac": ".flac",
75
- "audio/webm": ".webm",
76
- "text/plain": ".txt",
77
- "text/markdown": ".md",
78
- "text/csv": ".csv",
79
- "text/tab-separated-values": ".tsv",
80
- "application/json": ".json",
81
- "application/pdf": ".pdf",
82
- "application/rtf": ".rtf",
83
- "text/rtf": ".rtf",
84
- "application/msword": ".doc",
85
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
86
- "application/vnd.ms-excel": ".xls",
87
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
88
- "application/vnd.ms-powerpoint": ".ppt",
89
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
90
- "application/vnd.oasis.opendocument.text": ".odt",
91
- "application/vnd.oasis.opendocument.spreadsheet": ".ods",
92
- "application/vnd.oasis.opendocument.presentation": ".odp",
93
- };
94
- export function extensionForMime(mime) {
95
- if (!mime)
96
- return null;
97
- return IMAGE_MIMES[mime.split(";")[0].trim().toLowerCase()] ?? null;
98
- }
99
57
  export function ensureAttachmentsDir() {
100
58
  mkdirSync(ATTACHMENTS_DIR, { recursive: true, mode: 0o700 });
101
59
  }
@@ -160,11 +118,23 @@ function addCommittedBytes(delta, version = attachmentAccountingVersion) {
160
118
  }
161
119
  committedAttachmentBytes += delta;
162
120
  }
163
- /** Bytes that count against ATTACHMENTS_MAX_BYTES right now, for callers
164
- * that plan a batch (the Grok Bot history import) before saving any of it. */
165
- export function attachmentBytesUsed() {
121
+ /** Bytes that count against ATTACHMENTS_MAX_BYTES right now. */
122
+ function attachmentBytesUsed() {
166
123
  return committedBytes() + reservedAttachmentBytes;
167
124
  }
125
+ /** Bytes the disk can still take before dropping to the reserve. Committed
126
+ * files are already off the free count; reservations are not written yet. */
127
+ function diskHeadroomBytes() {
128
+ ensureAttachmentsDir();
129
+ const { bavail, bsize } = statfsSync(ATTACHMENTS_DIR);
130
+ return Number(bavail) * Number(bsize) - ATTACHMENTS_DISK_RESERVE_BYTES - reservedAttachmentBytes;
131
+ }
132
+ /** How many more bytes uploads may add right now: the smaller of the
133
+ * optional ceiling and the disk headroom. Callers that plan a batch (the
134
+ * Grok Bot history import) read this before saving any of it. */
135
+ export function attachmentBytesAvailable() {
136
+ return Math.max(0, Math.min(ATTACHMENTS_MAX_BYTES - attachmentBytesUsed(), diskHeadroomBytes()));
137
+ }
168
138
  /** Remove only abandoned temporary uploads, and refresh the cached usage
169
139
  * total from the same fresh scan. Outside accounting recovery, this is the
170
140
  * only full directory walk after start-up; nothing on the upload path calls it
@@ -239,10 +209,9 @@ class AttachmentReservation {
239
209
  this.reserveAtLeast(bytes, bytes);
240
210
  }
241
211
  reserveAtLeast(required, preferred) {
242
- const used = committedBytes();
243
- const available = ATTACHMENTS_MAX_BYTES - used - reservedAttachmentBytes;
212
+ const available = attachmentBytesAvailable();
244
213
  if (available < required) {
245
- throw statusError(507, `attachments storage is full (limit ${ATTACHMENTS_MAX_BYTES} bytes)`);
214
+ throw statusError(507, "attachments storage is full (the disk is nearly out of free space)");
246
215
  }
247
216
  // Unknown-length HTTP bodies reserve in bounded chunks. Taking whatever
248
217
  // remains below the preferred increment avoids falsely rejecting a small
@@ -328,14 +297,6 @@ async function digestFile(path) {
328
297
  function statusError(status, message) {
329
298
  return Object.assign(new Error(message), { status });
330
299
  }
331
- function normalizedMime(mime) {
332
- const value = mime?.split(";")[0]?.trim().toLowerCase();
333
- return value || null;
334
- }
335
- export function extensionForFileMime(mime) {
336
- const value = normalizedMime(mime);
337
- return value ? FILE_MIMES[value] ?? null : null;
338
- }
339
300
  /** Strip platform-reserved characters and replace any sender-provided
340
301
  * extension with the canonical one for the accepted mime. Separators and
341
302
  * encoded separators are rejected rather than massaged so traversal attempts
@@ -6,6 +6,7 @@ export const AVATAR_IMAGE_TIMEOUT_MS = 120_000;
6
6
  const MAX_UPSTREAM_RESPONSE_BYTES = 15 * 1024 * 1024;
7
7
  export const avatarGenerationRequestSchema = z.object({
8
8
  prompt: z.string().trim().max(AVATAR_DIRECTION_MAX_CHARS).default(""),
9
+ source: z.enum(["auto", "subscription", "api"]).default("auto"),
9
10
  });
10
11
  const generatedImageResponseSchema = z.object({
11
12
  data: z.array(z.object({ b64_json: z.string().min(1).optional(), url: z.string().optional() })).min(1),
@@ -0,0 +1,74 @@
1
+ import { decodeGeneratedImage } from "./generated-image.js";
2
+ import { avatarGenerationPrompt, generateAvatarImage, AVATAR_IMAGE_TIMEOUT_MS } from "./avatar-image.js";
3
+ import { PASS, ROUTES } from "./routes/table.js";
4
+ export async function generateRoutedAvatar(cfg, instances, bot, direction, source = "auto") {
5
+ if (source !== "api") {
6
+ const subscription = await avatarSubscription(instances);
7
+ if (subscription)
8
+ return generateSubscriptionAvatar(subscription, bot, direction);
9
+ if (source === "subscription" || instances.some(instance => instance.enabled && instance.generateImage)) {
10
+ throw Object.assign(new Error("Sign in to an image-capable subscription in Settings, or explicitly choose an image API connection."), { status: 409 });
11
+ }
12
+ }
13
+ return generateAvatarImage(cfg, bot, direction);
14
+ }
15
+ export function installAvatarSubscriptionRoute(instances) {
16
+ ROUTES.push(async (context) => {
17
+ if (context.path !== "/api/avatar-subscription" || context.method !== "GET")
18
+ return PASS;
19
+ const available = instances();
20
+ const subscription = await avatarSubscription(available);
21
+ context.res.setHeader("Cache-Control", "no-store");
22
+ context.json(context.res, 200, { offered: available.some(instance => instance.enabled && instance.generateImage),
23
+ available: Boolean(subscription), label: subscription?.displayName ?? "Signed-in subscription" });
24
+ });
25
+ }
26
+ /** Capability and current billing are both required. An authenticated text
27
+ * engine is not evidence that its subscription can generate images. */
28
+ export async function avatarSubscription(instances) {
29
+ for (const instance of instances) {
30
+ if (!instance.enabled || !instance.generateImage)
31
+ continue;
32
+ try {
33
+ const status = await instance.snapshot();
34
+ if (status.state === "available" && status.authenticated === true &&
35
+ status.billing === "subscription" && status.account?.method !== "api-key")
36
+ return instance;
37
+ }
38
+ catch {
39
+ // A broken account must not conceal another eligible signed-in account.
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ /** Never turn a subscription failure into an unexpected API charge. */
45
+ export async function generateSubscriptionAvatar(instance, bot, direction, timeoutMs = AVATAR_IMAGE_TIMEOUT_MS) {
46
+ if (!instance.generateImage || await avatarSubscription([instance]) !== instance) {
47
+ throw Object.assign(new Error("This image subscription is no longer available. Check its sign-in in Settings."), { status: 409 });
48
+ }
49
+ const controller = new AbortController();
50
+ let timer;
51
+ try {
52
+ const encoded = await Promise.race([
53
+ instance.generateImage(avatarGenerationPrompt(bot, direction), { signal: controller.signal }),
54
+ new Promise((_, reject) => {
55
+ timer = setTimeout(() => {
56
+ controller.abort();
57
+ reject(new Error("timeout"));
58
+ }, timeoutMs);
59
+ }),
60
+ ]);
61
+ const image = decodeGeneratedImage(encoded);
62
+ if (image.mime === "image/gif")
63
+ throw new Error("unsupported avatar format");
64
+ return { bytes: image.bytes, mime: image.mime };
65
+ }
66
+ catch {
67
+ throw Object.assign(new Error(controller.signal.aborted
68
+ ? "Subscription avatar generation timed out. Try again."
69
+ : "Your subscription could not generate this avatar. Check its sign-in and usage allowance, then try again."), { status: 502 });
70
+ }
71
+ finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
@@ -40,6 +40,7 @@ import { resolveLoopbackTrust } from "./request-auth.js";
40
40
  import { writeFileAtomic } from "./atomic.js";
41
41
  import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
42
42
  import { runServiceCommand } from "./service-cli.js";
43
+ import { reportBusyPort } from "./port-in-use.js";
43
44
  import { colorDepth, renderBanner, shouldShowBanner, terminalBackground } from "./cli-banner.js";
44
45
  import { runFleetCommand } from "./fleet-cli.js";
45
46
  import { startFleetAgent } from "./fleet-agent.js";
@@ -830,6 +831,8 @@ export async function runServe(options, log = console.log) {
830
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`);
831
832
  return 1;
832
833
  }
834
+ if (await reportBusyPort(options.port, options.dataDir === join(homedir(), ".atomic-app") ? "" : ` --data-dir ${options.dataDir}`))
835
+ return 1;
833
836
  let publicUrl = options.publicUrl;
834
837
  let tailscale = null;
835
838
  if (options.tailscale) {
@@ -1131,10 +1134,8 @@ export async function runOnboardingCommand(options, io = defaultIo(), startServe
1131
1134
  saveCliStartup(options.dataDir, startupPreferences(launch));
1132
1135
  }
1133
1136
  if (options.command === "setup") {
1134
- io.log("\nAll set. Start with: chibi (or npx @chibitek/chibi without a global install).");
1135
- if (options.dataDir !== join(homedir(), ".atomic-app") || options.port !== 8799) {
1136
- io.log(`Use the same --data-dir (${options.dataDir}) and --port (${options.port}) options when starting.`);
1137
- }
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).`);
1138
1139
  return 0;
1139
1140
  }
1140
1141
  if (saved)
@@ -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
+ }