@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,325 @@
1
+ // The Plugins window's MCP Marketplace, backed by the official MCP Registry
2
+ // (https://registry.modelcontextprotocol.io, public, no key). This module
3
+ // fetches and normalizes registry pages and turns one entry, or a featured
4
+ // service from shared/mcp-featured.ts, into a config.json `mcpServers` entry.
5
+ // Adding goes through the same rules as every other MCP server: the entry is
6
+ // saved switched off, secrets are kept in its env or headers like any other
7
+ // server's, and nothing here logs a value.
8
+ import { featuredMcpIcon, featuredMcpServer, MCP_FEATURED } from "../shared/mcp-featured.js";
9
+ import { slugMcpName } from "./mcp-registry.js";
10
+ export const MCP_REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
11
+ /** The OAuth bridge Atomic already runs remote servers through (see
12
+ * electron/chibitek-defaults.mjs MCP_REMOTE_PACKAGE; a test keeps them equal).
13
+ * Pinned so its saved sign-ins survive a new mcp-remote release. */
14
+ export const MCP_REMOTE_PACKAGE = "mcp-remote@0.14.3";
15
+ const CACHE_MS = 60 * 60 * 1000;
16
+ const CACHE_MAX = 200;
17
+ const TIMEOUT_MS = 8_000;
18
+ const PAGE_SIZE = 30;
19
+ const OFFICIAL = "io.modelcontextprotocol.registry/official";
20
+ export const REGISTRY_OFFLINE_MESSAGE = "The MCP Registry could not be reached. Check your internet connection and try again.";
21
+ export class RegistryUnavailableError extends Error {
22
+ constructor() {
23
+ super(REGISTRY_OFFLINE_MESSAGE);
24
+ this.name = "RegistryUnavailableError";
25
+ }
26
+ }
27
+ function record(value) {
28
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
29
+ }
30
+ function text(value) {
31
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
32
+ }
33
+ function httpsUrl(value) {
34
+ const raw = text(value);
35
+ if (!raw)
36
+ return undefined;
37
+ try {
38
+ const url = new URL(raw);
39
+ return url.protocol === "https:" ? url.toString() : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ const featuredByRegistryName = new Map(MCP_FEATURED.filter((server) => server.registryName).map((server) => [server.registryName, server]));
46
+ /** "io.github.acme/thing" -> "acme", "com.notion/mcp" -> "notion.com". */
47
+ export function publisherFor(name) {
48
+ const namespace = name.split("/")[0] ?? name;
49
+ if (namespace.startsWith("io.github."))
50
+ return namespace.slice("io.github.".length);
51
+ return namespace.split(".").reverse().join(".");
52
+ }
53
+ /** "notion-mcp-server" -> "Notion Mcp Server" when the entry has no title. */
54
+ function titleFor(name) {
55
+ const leaf = name.split("/").pop() ?? name;
56
+ return leaf.split(/[-_.]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ") || name;
57
+ }
58
+ function githubOwner(repositoryUrl) {
59
+ if (!repositoryUrl)
60
+ return undefined;
61
+ const match = /^https:\/\/github\.com\/([A-Za-z0-9-]{1,39})(?:\/|$)/.exec(repositoryUrl);
62
+ return match?.[1];
63
+ }
64
+ function input(kind, raw) {
65
+ const item = record(raw);
66
+ const name = text(item.name);
67
+ if (!name)
68
+ return null;
69
+ const description = text(item.description);
70
+ return {
71
+ kind,
72
+ name,
73
+ ...(description ? { description: description.slice(0, 300) } : {}),
74
+ required: item.isRequired === true,
75
+ secret: item.isSecret === true,
76
+ ...(kind === "header" && text(item.value) ? { template: text(item.value) } : {}),
77
+ };
78
+ }
79
+ function remotes(raw) {
80
+ if (!Array.isArray(raw))
81
+ return [];
82
+ return raw.flatMap((entry) => {
83
+ const item = record(entry);
84
+ const url = httpsUrl(item.url);
85
+ // A URL with {placeholders} needs per-person values Atomic cannot guess.
86
+ if (!url || /[{}]|%7B|%7D/i.test(url))
87
+ return [];
88
+ const type = item.type === "sse" ? "sse" : item.type === "streamable-http" ? "http" : null;
89
+ if (!type)
90
+ return [];
91
+ const headers = Array.isArray(item.headers) ? item.headers.map((header) => input("header", header)).filter((header) => header !== null) : [];
92
+ return [{ type, url, headers }];
93
+ });
94
+ }
95
+ function packages(raw) {
96
+ if (!Array.isArray(raw))
97
+ return [];
98
+ return raw.flatMap((entry) => {
99
+ const item = record(entry);
100
+ const registryType = text(item.registryType);
101
+ const identifier = text(item.identifier);
102
+ if (!registryType || !identifier)
103
+ return [];
104
+ const env = Array.isArray(item.environmentVariables)
105
+ ? item.environmentVariables.map((variable) => input("env", variable)).filter((variable) => variable !== null)
106
+ : [];
107
+ return [{
108
+ registryType,
109
+ identifier,
110
+ ...(text(item.version) ? { version: text(item.version) } : {}),
111
+ ...(text(record(item.transport).type) ? { transport: text(record(item.transport).type) } : {}),
112
+ ...(text(item.runtimeHint) ? { runtimeHint: text(item.runtimeHint) } : {}),
113
+ env,
114
+ }];
115
+ });
116
+ }
117
+ const SAFE_PACKAGE = /^(@[a-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*$/;
118
+ const SAFE_VERSION = /^[\w.+-]{1,64}$/;
119
+ /** Remote first (nothing to install), then npm, then PyPI. A remote whose
120
+ * headers are all optional is assumed to sign in with OAuth, so it runs
121
+ * through the mcp-remote bridge; one that requires a header (an API key) is
122
+ * kept as a plain URL entry with that header. */
123
+ function installPlan(server) {
124
+ const remote = server.remotes.find((item) => item.type === "http") ?? server.remotes.find((item) => item.type === "sse");
125
+ if (remote)
126
+ return { kind: "remote", remote, oauth: !remote.headers.some((header) => header.required) };
127
+ const stdio = (item) => !item.transport || item.transport === "stdio";
128
+ const npm = server.packages.find((item) => item.registryType === "npm" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
129
+ if (npm)
130
+ return { kind: "npm", pkg: npm };
131
+ const pypi = server.packages.find((item) => item.registryType === "pypi" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
132
+ if (pypi)
133
+ return { kind: "pypi", pkg: pypi };
134
+ return null;
135
+ }
136
+ function planInputs(plan) {
137
+ if (!plan)
138
+ return [];
139
+ const list = plan.kind === "remote"
140
+ ? plan.oauth ? [] : plan.remote.headers.map(({ template: _template, ...header }) => header)
141
+ : plan.pkg.env;
142
+ return [...list].sort((a, b) => Number(b.required) - Number(a.required));
143
+ }
144
+ /** One registry list entry to the shape the renderer shows, or null when it
145
+ * is not the latest version or not usable at all. */
146
+ export function normalizeRegistryEntry(raw) {
147
+ const entry = record(raw);
148
+ const server = record(entry.server);
149
+ const meta = record(record(entry._meta)[OFFICIAL]);
150
+ if (meta.isLatest === false)
151
+ return null;
152
+ if (typeof meta.status === "string" && meta.status !== "active")
153
+ return null;
154
+ const name = text(server.name);
155
+ if (!name)
156
+ return null;
157
+ const repositoryUrl = httpsUrl(record(server.repository).url);
158
+ const websiteUrl = httpsUrl(server.websiteUrl);
159
+ const featured = featuredByRegistryName.get(name);
160
+ const icons = Array.isArray(server.icons) ? server.icons : [];
161
+ const registryIcon = icons.map((icon) => httpsUrl(record(icon).src)).find(Boolean);
162
+ const owner = githubOwner(repositoryUrl);
163
+ const iconUrl = featured
164
+ ? featuredMcpIcon(featured.id)
165
+ : registryIcon ?? (owner ? `https://github.com/${owner}.png?size=64` : undefined);
166
+ const normalized = {
167
+ id: name,
168
+ name,
169
+ title: text(server.title) ?? featured?.name ?? titleFor(name),
170
+ description: (text(server.description) ?? "").slice(0, 400),
171
+ publisher: publisherFor(name),
172
+ version: text(server.version) ?? "",
173
+ ...(repositoryUrl ? { repositoryUrl } : {}),
174
+ ...(websiteUrl ? { websiteUrl } : {}),
175
+ remotes: remotes(server.remotes),
176
+ packages: packages(server.packages),
177
+ ...(iconUrl ? { iconUrl } : {}),
178
+ ...(featured ? { featured: featured.id } : {}),
179
+ };
180
+ const plan = installPlan(normalized);
181
+ return { ...normalized, install: plan?.kind ?? null, inputs: planInputs(plan) };
182
+ }
183
+ /** Normalize a page: latest versions only, one card per server name. */
184
+ export function normalizeRegistryPage(body) {
185
+ const page = record(body);
186
+ const seen = new Set();
187
+ const servers = [];
188
+ for (const raw of Array.isArray(page.servers) ? page.servers : []) {
189
+ const server = normalizeRegistryEntry(raw);
190
+ if (!server || seen.has(server.id))
191
+ continue;
192
+ seen.add(server.id);
193
+ servers.push(server);
194
+ }
195
+ const nextCursor = text(record(page.metadata).nextCursor);
196
+ return { servers, ...(nextCursor ? { nextCursor } : {}) };
197
+ }
198
+ /** A registry client with a one-hour in-memory cache. Every failure (offline,
199
+ * timeout, a registry error) becomes RegistryUnavailableError. */
200
+ export function createMcpPublicRegistry(options = {}) {
201
+ const doFetch = options.fetch ?? fetch;
202
+ const base = options.base ?? MCP_REGISTRY_BASE;
203
+ const now = options.now ?? Date.now;
204
+ const cache = new Map();
205
+ async function getJson(url) {
206
+ const hit = cache.get(url);
207
+ if (hit && now() - hit.at < CACHE_MS)
208
+ return hit.value;
209
+ let response;
210
+ try {
211
+ response = await doFetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
212
+ }
213
+ catch {
214
+ throw new RegistryUnavailableError();
215
+ }
216
+ if (response.status === 404)
217
+ return null;
218
+ if (!response.ok)
219
+ throw new RegistryUnavailableError();
220
+ let value;
221
+ try {
222
+ value = await response.json();
223
+ }
224
+ catch {
225
+ throw new RegistryUnavailableError();
226
+ }
227
+ if (cache.size >= CACHE_MAX)
228
+ cache.delete(cache.keys().next().value);
229
+ cache.set(url, { at: now(), value });
230
+ return value;
231
+ }
232
+ return {
233
+ async search({ search, cursor }) {
234
+ const params = new URLSearchParams({ limit: String(PAGE_SIZE), version: "latest" });
235
+ const query = search?.trim().slice(0, 100);
236
+ if (query)
237
+ params.set("search", query);
238
+ if (cursor)
239
+ params.set("cursor", cursor.slice(0, 300));
240
+ return normalizeRegistryPage(await getJson(`${base}/v0/servers?${params}`));
241
+ },
242
+ async get(id) {
243
+ const body = await getJson(`${base}/v0/servers/${encodeURIComponent(id)}/versions/latest`);
244
+ return body === null ? null : normalizeRegistryEntry(body);
245
+ },
246
+ };
247
+ }
248
+ /** A free config name from the server's own name: "notion", then "notion-2". */
249
+ export function freeMcpName(preferred, taken) {
250
+ const base = slugMcpName(preferred) || "mcp-server";
251
+ if (!taken(base))
252
+ return base;
253
+ for (let n = 2; n < 100; n += 1) {
254
+ const candidate = `${base.slice(0, 32 - String(n).length - 1)}-${n}`;
255
+ if (!taken(candidate))
256
+ return candidate;
257
+ }
258
+ return `${base.slice(0, 23)}-${Date.now().toString(36).slice(-8)}`;
259
+ }
260
+ function bridgeEntry(url) {
261
+ return { command: "npx", args: ["-y", MCP_REMOTE_PACKAGE, url], env: {} };
262
+ }
263
+ function cleanValues(values) {
264
+ const out = {};
265
+ for (const [key, value] of Object.entries(record(values))) {
266
+ if (typeof value === "string" && value.trim())
267
+ out[key] = value.trim();
268
+ }
269
+ return out;
270
+ }
271
+ /** The config entry a featured service becomes: its official URL through the
272
+ * OAuth bridge. */
273
+ export function planFeaturedAdd(featured, taken) {
274
+ return { ok: true, name: freeMcpName(featured.id, taken), entry: bridgeEntry(featured.url), kind: "remote", oauth: true };
275
+ }
276
+ /** The config entry a registry server becomes, with the person's values for
277
+ * its inputs. Required inputs that are missing are refused with the list, so
278
+ * the renderer can ask for them. */
279
+ export function planRegistryAdd(server, rawValues, taken) {
280
+ const values = cleanValues(rawValues);
281
+ const plan = installPlan(server);
282
+ if (!plan) {
283
+ const docker = server.packages.some((item) => item.registryType === "oci" || item.registryType === "docker");
284
+ return {
285
+ ok: false,
286
+ status: 400,
287
+ code: "unsupported",
288
+ error: docker
289
+ ? `${server.title} only ships as a Docker image, which Atomic can't add for you yet. Add it by hand under MCP servers.`
290
+ : `${server.title} doesn't offer a remote address or an npm or PyPI package, so Atomic can't add it for you. Add it by hand under MCP servers.`,
291
+ };
292
+ }
293
+ const inputs = planInputs(plan);
294
+ const missing = inputs.filter((item) => item.required && !values[item.name]);
295
+ if (missing.length) {
296
+ return { ok: false, status: 400, code: "needs_input", inputs, error: `${server.title} needs ${missing.map((item) => item.name).join(", ")} before it can be added.` };
297
+ }
298
+ const name = freeMcpName(server.featured ?? server.name.split("/").pop() ?? server.title, taken);
299
+ if (plan.kind === "remote") {
300
+ if (plan.oauth)
301
+ return { ok: true, name, entry: bridgeEntry(plan.remote.url), kind: "remote", oauth: true };
302
+ const headers = {};
303
+ for (const header of plan.remote.headers) {
304
+ const value = values[header.name];
305
+ if (!value)
306
+ continue;
307
+ // "Bearer {api_key}" -> "Bearer <value>" unless the value already is the whole header.
308
+ headers[header.name] = header.template && /\{[^}]+\}/.test(header.template) && !/\s/.test(value)
309
+ ? header.template.replace(/\{[^}]+\}/, value)
310
+ : value;
311
+ }
312
+ return { ok: true, name, entry: { type: plan.remote.type, url: plan.remote.url, headers }, kind: "remote", oauth: false };
313
+ }
314
+ const env = {};
315
+ for (const variable of plan.pkg.env) {
316
+ if (values[variable.name])
317
+ env[variable.name] = values[variable.name];
318
+ }
319
+ const version = plan.pkg.version && SAFE_VERSION.test(plan.pkg.version) ? plan.pkg.version : undefined;
320
+ if (plan.kind === "npm") {
321
+ return { ok: true, name, entry: { command: "npx", args: ["-y", version ? `${plan.pkg.identifier}@${version}` : plan.pkg.identifier], env }, kind: "npm", oauth: false };
322
+ }
323
+ return { ok: true, name, entry: { command: "uvx", args: [plan.pkg.identifier], env }, kind: "pypi", oauth: false };
324
+ }
325
+ export { featuredMcpServer };
@@ -0,0 +1,11 @@
1
+ import { isReactionEmoji } from "../shared/emoji.js";
2
+ import { json, readBody } from "./harness/http.js";
3
+ export async function reactToMessage(store, req, res, threadId, messageId) {
4
+ const body = await readBody(req);
5
+ if (!isReactionEmoji(body.emoji))
6
+ return json(res, 400, { error: "Choose one emoji" });
7
+ const message = store.toggleReaction(threadId, messageId, body.emoji, "user");
8
+ if (!message)
9
+ return json(res, 404, { error: "no such message" });
10
+ return json(res, 200, { message });
11
+ }
@@ -0,0 +1,18 @@
1
+ export { ModelEvidenceLedger } from "./model-evidence.js";
2
+ // Snapshot the same class choices the model picker displays before routing mutates the task.
3
+ import { threadEngineMode } from "../shared/engine-tiers.js";
4
+ export function createModelExpectation(context) {
5
+ return (threadId, instanceId, provider) => {
6
+ const profile = context.store.botByThread(threadId) ?? context.store.bot(context.speakerBot(threadId) ?? "");
7
+ if (!profile)
8
+ return null;
9
+ const task = context.store.taskByThread(profile.id, threadId);
10
+ const selection = context.turnEngineOverrides.get(threadId) ?? context.roomTurnSelections.get(threadId) ?? task?.modelSelection ?? profile.modelSelection;
11
+ const modelClass = context.enginePlan(profile, threadId).modelClass;
12
+ const mode = threadEngineMode(profile, task ?? {});
13
+ const preview = mode.kind === "pinned" ? undefined : context.botClassChoices(profile, threadId).options.find(option => option.modelClass === modelClass);
14
+ return { botId: profile.id, threadId, label: `${mode.kind === "tier" ? "Automatic: " : ""}${modelClass} · ${preview?.label ?? selection.model}`, modelClass,
15
+ model: preview?.selection?.model ?? selection.model, requestedModel: selection.model,
16
+ instanceId: instanceId ?? selection.instanceId, provider: provider ?? context.registry.get(selection.instanceId)?.driverKind ?? "unknown" };
17
+ };
18
+ }
@@ -0,0 +1,145 @@
1
+ // Name: model-evidence; author: Atomic Engineering; date: 2026-10-27.
2
+ // Append-only identity history beside the existing usage ledger, without prompts.
3
+ import { randomUUID } from "node:crypto";
4
+ import { mkdirSync, appendFileSync, readdirSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { modelEvidenceMetrics } from "../shared/model-evidence.js";
7
+ export { modelEvidenceMetrics };
8
+ const safe = (value) => typeof value === "string" && /^[\w .:/@+()[\]-]{1,200}$/u.test(value) ? value : undefined;
9
+ export class ModelEvidenceLedger {
10
+ active = new Map();
11
+ byTurn = new Map();
12
+ failures = 0;
13
+ dataDir;
14
+ constructor(dataDir) { this.dataDir = dataDir; }
15
+ begin(expected, continuation = false) {
16
+ const prior = this.active.get(expected.threadId);
17
+ const attempt = { expected: { ...expected, ...(continuation && prior ? { label: prior.expected.label, modelClass: prior.expected.modelClass, model: prior.expected.model } : {}) }, logicalTurnId: continuation && prior ? prior.logicalTurnId : randomUUID(), attemptId: randomUUID() };
18
+ this.active.set(expected.threadId, attempt);
19
+ this.write(attempt, { kind: "dispatch", level: expected.requestedModel || expected.model ? "requested-only" : "unknown" });
20
+ return attempt.attemptId;
21
+ }
22
+ finishDispatch(threadId, attemptId) {
23
+ const attempt = this.active.get(threadId);
24
+ if (attempt?.attemptId === attemptId)
25
+ this.write(attempt, { kind: "completed", level: "unknown", ok: false, reason: "dispatch ended before provider completion" });
26
+ }
27
+ write(attempt, fields) {
28
+ const row = { ...attempt.expected, version: 1, id: randomUUID(), at: new Date().toISOString(),
29
+ logicalTurnId: attempt.logicalTurnId, attemptId: attempt.attemptId, ...(attempt.turnId ? { turnId: attempt.turnId } : {}), ...fields };
30
+ try {
31
+ const directory = join(this.dataDir, "model-evidence");
32
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
33
+ appendFileSync(join(directory, `${row.at.slice(0, 7)}.jsonl`), JSON.stringify(row) + "\n", { mode: 0o600 });
34
+ }
35
+ catch {
36
+ this.failures++;
37
+ if (this.failures === 1)
38
+ console.error("[model-evidence] Cannot persist model history; queries will report incomplete evidence.");
39
+ }
40
+ }
41
+ observe(event, expectation) {
42
+ if (!["turn.started", "session.started", "model.evidence", "model.call", "turn.retrying", "turn.engine_failover", "session.model-fallback", "turn.completed", "session.exited"].includes(event.type))
43
+ return;
44
+ const turnKey = event.turnId ? JSON.stringify([event.threadId, event.providerInstanceId ?? event.provider, event.turnId]) : undefined;
45
+ let attempt = turnKey ? this.byTurn.get(turnKey) : undefined;
46
+ if (!attempt) {
47
+ attempt = this.active.get(event.threadId);
48
+ if (!attempt || (event.type === "turn.started" && attempt.turnId && attempt.turnId !== event.turnId)) {
49
+ const expected = expectation();
50
+ if (!expected)
51
+ return;
52
+ this.begin(expected);
53
+ attempt = this.active.get(event.threadId);
54
+ }
55
+ if (event.turnId) {
56
+ attempt.turnId = event.turnId;
57
+ this.byTurn.set(turnKey, attempt);
58
+ }
59
+ }
60
+ const base = { provider: event.provider, instanceId: event.providerInstanceId ?? attempt.expected.instanceId };
61
+ switch (event.type) {
62
+ case "turn.started":
63
+ this.write(attempt, { ...base, kind: "start", level: "requested-only" });
64
+ break;
65
+ case "model.call":
66
+ this.write(attempt, { ...base, kind: "call", level: "unknown", callId: event.callId });
67
+ break;
68
+ case "model.evidence":
69
+ this.write(attempt, { ...base, kind: "evidence", level: safe(event.model) ? event.level : "unknown",
70
+ ...(safe(event.model) ? { reportedModel: safe(event.model) } : {}), callId: event.callId, responseId: safe(event.responseId) });
71
+ break;
72
+ case "session.started":
73
+ this.write(attempt, { ...base, kind: "evidence", level: safe(event.model) ? event.modelEvidenceLevel ?? "requested-only" : "unknown", reportedModel: safe(event.model) });
74
+ break;
75
+ case "turn.retrying": {
76
+ this.write(attempt, { ...base, kind: "retry", level: "unknown", reason: safe(event.reason) });
77
+ // CLI retries reuse their native turn id, but are distinct attempts.
78
+ this.begin(attempt.expected, true);
79
+ const retry = this.active.get(event.threadId);
80
+ retry.turnId = event.turnId;
81
+ if (turnKey)
82
+ this.byTurn.set(turnKey, retry);
83
+ break;
84
+ }
85
+ case "turn.engine_failover":
86
+ case "session.model-fallback":
87
+ this.write(attempt, { ...base, kind: "fallback", level: "unknown", reason: safe(event.reason) ?? "provider fallback" });
88
+ break;
89
+ case "turn.completed":
90
+ this.write(attempt, { ...base, kind: "completed", level: "unknown", ok: event.ok });
91
+ break;
92
+ case "session.exited":
93
+ this.write(attempt, { ...base, kind: "exited", level: "unknown" });
94
+ break;
95
+ }
96
+ // ponytail: bounded correlation cache; durable history remains on disk.
97
+ if (this.byTurn.size > 10000)
98
+ this.byTurn.delete(this.byTurn.keys().next().value);
99
+ if (this.active.size > 10000)
100
+ this.active.delete(this.active.keys().next().value);
101
+ }
102
+ query(botId, threadId, from, to) {
103
+ const directory = join(this.dataDir, "model-evidence");
104
+ const rows = [];
105
+ let incomplete = this.failures > 0;
106
+ let files = [];
107
+ try {
108
+ files = readdirSync(directory).filter(file => /^\d{4}-\d{2}\.jsonl$/.test(file)).sort();
109
+ }
110
+ catch (error) {
111
+ if (error.code !== "ENOENT")
112
+ incomplete = true;
113
+ }
114
+ for (const file of files) {
115
+ if (file.slice(0, 7) < from.toISOString().slice(0, 7) || file.slice(0, 7) > to.toISOString().slice(0, 7))
116
+ continue;
117
+ try {
118
+ for (const line of readFileSync(join(directory, file), "utf8").split("\n").filter(Boolean)) {
119
+ try {
120
+ const row = JSON.parse(line);
121
+ if (row.version !== 1 || !row.logicalTurnId || !row.attemptId) {
122
+ incomplete = true;
123
+ continue;
124
+ }
125
+ if (row.botId === botId && row.threadId === threadId && row.at >= from.toISOString() && row.at <= to.toISOString())
126
+ rows.push(row);
127
+ }
128
+ catch {
129
+ incomplete = true;
130
+ }
131
+ }
132
+ }
133
+ catch {
134
+ incomplete = true;
135
+ }
136
+ }
137
+ const latestDispatch = rows.findLast(row => row.kind === "dispatch");
138
+ const latest = latestDispatch && rows.findLast(row => row.attemptId === latestDispatch.attemptId);
139
+ // Late exit/events from an older attempt must not settle a newer attempt.
140
+ const running = latest && !rows.some(row => row.attemptId === latest.attemptId && ["completed", "exited"].includes(row.kind));
141
+ return { rows, metrics: modelEvidenceMetrics(rows), incomplete, writeFailures: this.failures,
142
+ current: running && this.active.has(threadId) ? latest.logicalTurnId : null,
143
+ interrupted: Boolean(running && !this.active.has(threadId)), last: latest?.logicalTurnId ?? null };
144
+ }
145
+ }
@@ -0,0 +1,21 @@
1
+ import { connect } from "node:net";
2
+ /** True when anything accepts TCP on 127.0.0.1:port, not just an Atomic server. */
3
+ export function portInUse(port) {
4
+ return new Promise((done) => {
5
+ const socket = connect({ host: "127.0.0.1", port });
6
+ socket.setTimeout(1000);
7
+ socket.once("connect", () => { socket.destroy(); done(true); });
8
+ socket.once("timeout", () => { socket.destroy(); done(false); });
9
+ socket.once("error", () => done(false));
10
+ });
11
+ }
12
+ /** The server binds port and port + 1 (webhooks); report the first busy one. */
13
+ export async function reportBusyPort(port, dataDirFlag) {
14
+ for (const candidate of [port, Number(process.env.OMB_WEBHOOK_PORT || port + 1)]) {
15
+ if (!(await portInUse(candidate)))
16
+ continue;
17
+ console.error(`Port ${candidate} is already in use by another app. Choose a free port, for example: chibi start --port ${port + 20}${dataDirFlag}`);
18
+ return true;
19
+ }
20
+ return false;
21
+ }
@@ -0,0 +1,76 @@
1
+ // Session fallback for company subscription bots (docs/chibitek/engine-tiers.md,
2
+ // "Subscription bots: the one failover exception"). When no account of the
3
+ // bot's own provider can take a turn, one conversation may run on the same
4
+ // class from another provider's subscription for up to SESSION_FALLBACK_TURNS
5
+ // turns. The bot's own default never changes; the next turn tries its own
6
+ // provider first again.
7
+ import { SESSION_FALLBACK_TURNS } from "../shared/subscription-bots.js";
8
+ export { SESSION_FALLBACK_TURNS };
9
+ // ponytail: in memory, so a restart gives a conversation its 50 turns again;
10
+ // persist on the task record if that matters.
11
+ const fallbacks = new Map();
12
+ /** The thread's own provider took this turn: it leaves the fallback (its count stays). */
13
+ export function leaveSessionFallback(threadId) {
14
+ const entry = fallbacks.get(threadId);
15
+ if (entry)
16
+ entry.active = false;
17
+ }
18
+ /** Enter (or stay on) the fallback for this thread. A new turn counts one;
19
+ * a failover within a turn already on it counts nothing. False once the
20
+ * conversation has used its turns. */
21
+ export function enterSessionFallback(threadId, newTurn) {
22
+ const entry = fallbacks.get(threadId) ?? { turns: 0, active: false };
23
+ if (entry.active && !newTurn)
24
+ return true;
25
+ if (entry.turns >= SESSION_FALLBACK_TURNS) {
26
+ entry.active = false;
27
+ fallbacks.set(threadId, entry);
28
+ return false;
29
+ }
30
+ entry.turns += 1;
31
+ entry.active = true;
32
+ fallbacks.set(threadId, entry);
33
+ return true;
34
+ }
35
+ /** Turn start for a subscription bot: its own provider's engine (which
36
+ * leaves the fallback), else, on its stop, the fallback's, counted as a
37
+ * turn. No fallback keeps the stop; past the limit, `spent` of it. */
38
+ export function ownOrFallback(threadId, own, fallback, isStop, spent) {
39
+ try {
40
+ const choice = own();
41
+ leaveSessionFallback(threadId);
42
+ return choice;
43
+ }
44
+ catch (stop) {
45
+ if (!isStop(stop))
46
+ throw stop;
47
+ let choice;
48
+ try {
49
+ choice = fallback();
50
+ }
51
+ catch {
52
+ choice = undefined;
53
+ }
54
+ if (choice === undefined)
55
+ throw stop;
56
+ if (!enterSessionFallback(threadId, true))
57
+ throw spent(stop);
58
+ return choice;
59
+ }
60
+ }
61
+ export function onSessionFallback(threadId) {
62
+ return fallbacks.get(threadId)?.active === true;
63
+ }
64
+ export function sessionFallbackTurns(threadId) {
65
+ return fallbacks.get(threadId)?.turns ?? 0;
66
+ }
67
+ /** The bot as a fallback turn runs it: an ordinary company bot, so the
68
+ * ladder and every gate read it without the provider lock. */
69
+ export function withoutSubscription(bot) {
70
+ const { subscription: _subscription, ...rest } = bot;
71
+ return rest;
72
+ }
73
+ /** The line a company subscription bot stops with once its fallback turns are used. */
74
+ export function sessionFallbackSpentLine(line) {
75
+ return `${line} This conversation already used its ${SESSION_FALLBACK_TURNS} turns on another provider; download a handoff or hand it to another bot.`;
76
+ }
@@ -0,0 +1,43 @@
1
+ import { PASS } from "../routes/table.js";
2
+ import { VaultError } from "./store.js";
3
+ /** The owner is supplied by the authenticated host, never from request JSON.
4
+ * Authorization runs on reads too, and is checked again after reading a body. */
5
+ export function createVaultRoutes(deps) {
6
+ return async ({ req, res, path, method, auth, json, readBody }) => {
7
+ if (path !== "/api/vault" && !path.startsWith("/api/vault/"))
8
+ return PASS;
9
+ res.setHeader("cache-control", "no-store");
10
+ const ownerId = deps.owner(req, auth);
11
+ if (!ownerId)
12
+ return json(res, 403, { error: "Only the Vault owner can manage saved items." });
13
+ const store = deps.store();
14
+ if (!store)
15
+ return json(res, 503, { error: "Vault encryption is unavailable. Unlock the secure store and try again." });
16
+ const item = /^\/api\/vault\/([0-9a-f-]{36})$/.exec(path);
17
+ if (path !== "/api/vault" && !item)
18
+ return json(res, 404, { error: "Vault item is unavailable." });
19
+ if (!(path === "/api/vault" ? ["GET", "POST"] : ["PUT", "DELETE"]).includes(method))
20
+ return json(res, 405, { error: "Method not allowed." });
21
+ try {
22
+ if (method === "GET")
23
+ return json(res, 200, { items: store.list(ownerId) });
24
+ const body = method === "DELETE" ? undefined : await readBody(req).catch(() => null);
25
+ if (deps.owner(req, auth) !== ownerId || deps.store() !== store)
26
+ return json(res, 403, { error: "Vault access changed. Try again." });
27
+ if (method === "POST")
28
+ return json(res, 201, { item: store.create(ownerId, body) });
29
+ if (method === "PUT" && item)
30
+ return json(res, 200, { item: store.update(ownerId, item[1], body) });
31
+ if (method === "DELETE" && item) {
32
+ store.remove(ownerId, item[1]);
33
+ return json(res, 200, { removed: true });
34
+ }
35
+ }
36
+ catch (error) {
37
+ // No raw parser, filesystem, crypto or browser error crosses this route.
38
+ if (error instanceof VaultError)
39
+ return json(res, 400, { error: error.message });
40
+ return json(res, 503, { error: "Vault could not complete the request." });
41
+ }
42
+ };
43
+ }