@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.
- package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-CpuRvzQ3.js} +1 -1
- package/dist/assets/{arc-QwTuMsxp.js → arc-ko1e0R6h.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-OZ3SsOOa.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-DLRkBkjs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-e8Q3qMnw.js} +1 -1
- package/dist/assets/channel-Lp7erhx-.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-hCDqaM4Q.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-Dy3Pahu-.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-QMK3iFBF.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-6fyp9ERp.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-DWvOhTaO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-DgohMW2S.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-sRGeD2Y7.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-CZslD_VF.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-D7ocO3VN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-T1lLqHHW.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BdeLiJr3.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-BydCA0pv.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-DBxckjwQ.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-CAsvq6Zk.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DTwH3rA_.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-BNYj1bey.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CihwYFxb.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-nouwk3s2.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-BoKewJeA.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-CrE1m9DQ.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DRj0aU4Y.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-Do0JzCG5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CRsI9pXb.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-dtD6wm3G.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-Cn7KyLCo.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-CGH_VWzq.js} +1 -1
- package/dist/assets/index-B2fJMh8N.css +1 -0
- package/dist/assets/{index-ziyH_uer.js → index-BIcziktH.js} +253 -250
- package/dist/assets/{index-BEJ8EKQt.js → index-Bk8iAodB.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-CQKsYTCL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-D5catDe0.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-KjY8mki9.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-BKCyJtge.js} +1 -1
- package/dist/assets/{linear-Ch19KVyt.js → linear-B-_obUkq.js} +1 -1
- package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DlSzuDKt.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-BFQSaYKh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-l51izlev.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-6NEXRoZX.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-BCC9XlOe.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-Dd4TmJ86.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP-C1E5DaDb.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ci6r7abb.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-BJ-I6WOh.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-Cs5QbLOP.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-Bw--rdvq.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1O9IEz5.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-CTZ7DXaw.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DjwgNG0H.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-DrdJG55h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-HVq776nz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-DPf9VEua.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/connector-proxy.js +205 -0
- package/dist-server/container-mcp.js +150 -96
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +7054 -6099
- package/dist-server/openmausbot.js +940 -727
- package/dist-server/pair-cli.js +940 -727
- package/dist-server/server/attachment-mimes.js +58 -0
- package/dist-server/server/attachments.js +28 -67
- package/dist-server/server/avatar-image.js +1 -0
- package/dist-server/server/avatar-subscription.js +74 -0
- package/dist-server/server/cli.js +5 -4
- package/dist-server/server/colima-docker.js +136 -0
- package/dist-server/server/composio.js +1051 -0
- package/dist-server/server/connector-proxy.js +249 -0
- package/dist-server/server/container-computer.js +31 -63
- package/dist-server/server/container-mcp.js +5 -0
- package/dist-server/server/drivers/claude-message.js +12 -0
- package/dist-server/server/drivers/claude.js +9 -12
- package/dist-server/server/drivers/codex-image.js +141 -0
- package/dist-server/server/drivers/codex.js +6 -9
- package/dist-server/server/drivers/openai-chat.js +13 -2
- package/dist-server/server/engine-failover.js +62 -12
- package/dist-server/server/engine-failure.js +2 -2
- package/dist-server/server/harness-import/history.js +5 -5
- package/dist-server/server/harness-import/test-fixtures.js +1 -1
- package/dist-server/server/index.js +75 -36
- package/dist-server/server/mcp-public-registry.js +325 -0
- package/dist-server/server/message-reactions.js +11 -0
- package/dist-server/server/model-evidence-context.js +18 -0
- package/dist-server/server/model-evidence.js +145 -0
- package/dist-server/server/port-in-use.js +21 -0
- package/dist-server/server/session-fallback.js +76 -0
- package/dist-server/server/vault/routes.js +43 -0
- package/dist-server/server/vault/runtime.js +48 -0
- package/dist-server/server/vault/store.js +177 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/desktop-private-port.js +1 -0
- package/dist-server/shared/emoji-catalog.json +1 -0
- package/dist-server/shared/emoji.js +27 -0
- package/dist-server/shared/mascot-bodies.js +9 -1
- package/dist-server/shared/model-evidence.js +25 -0
- package/dist-server/shared/subscription-bots.js +3 -0
- package/dist-server/shared/vault.js +59 -0
- package/dist-server/vps-container-mcp.js +86 -84
- package/package.json +1 -1
- package/dist/assets/channel-D2y0i2ps.js +0 -1
- package/dist/assets/index-DGWeJcck.css +0 -1
- 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
|
|
18
|
-
*
|
|
19
|
-
|
|
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
|
|
164
|
-
|
|
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
|
|
243
|
-
const available = ATTACHMENTS_MAX_BYTES - used - reservedAttachmentBytes;
|
|
212
|
+
const available = attachmentBytesAvailable();
|
|
244
213
|
if (available < required) {
|
|
245
|
-
throw statusError(507,
|
|
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
|
-
|
|
1135
|
-
|
|
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
|
+
}
|