@chibitek/chibi 0.1.161 → 0.1.162

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 (74) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-Bb-Unz3V.js → abnfDiagram-O67JEVCF-D0_WX2x_.js} +1 -1
  2. package/dist/assets/{arc-HBMH7B4K.js → arc-lzBXqTbJ.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-CYwkb2PE.js → architectureDiagram-NJMV4G6O-KQZbO2M3.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DPYYNxBH.js → blockDiagram-BEXU5L5S-zb3pW64Y.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-DZ9hKDlA.js → c4Diagram-YGBWAQC7-CADaXczd.js} +1 -1
  6. package/dist/assets/channel-Br57lm8F.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-OKrUlts6.js → chunk-2Q5K7J3B-DJP27cEp.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-Tuur0lqz.js → chunk-3FUC2YCW-Cp9Z7Ybl.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-BL8QOZeP.js → chunk-5VM5RSS4-DjXxom_0.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-DMi8-ixt.js → chunk-GTNCS2PH-DpEJ08zA.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dd7yNJ0G.js → chunk-GWA4HPMP-CQ4Kadr-.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-g2dOoIMX.js → chunk-JWPE2WC7-CwXmVMdh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-Cum7feOh.js → chunk-NETBCI7D-Vl_kib_R.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-BzCC1jc4.js → chunk-WEXAMYUT-BhAVbfJe.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-Ck63jUMo.js → chunk-XXDRQBXY-s1a4nKQE.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DFSuRVMZ.js → classDiagram-v2-NBCMYWYE-DeGQSNky.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CegRj_kY.js → cose-bilkent-JH36ORCC-D5sUHYFG.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-cZS_kt1A.js → cynefin-OW5HDTMX-DmTvN9T2.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DfQa9Rht.js → cynefinDiagram-VND7K2PF-Cqtt1jCF.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DJ9qftJu.js → dagre-6A5THRUB-DRZKkSJg.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-KXUGUahJ.js → diagram-22UHCM2B-1od4dKOU.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-BudCYrCp.js → diagram-3UASUU5V-D3_yCQOJ.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-Du2O03NS.js → diagram-ATOU4E4O-D9UgsVGC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DXybXGYf.js → diagram-CDSNMT55-DMxsJ_X5.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-DfrknXGE.js → diagram-MLGK6HIB-CK47Pp3D.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C29FmaJm.js → diagram-MPIPVDR6-D0axYDk-.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BF9wS0Nz.js → ebnfDiagram-ZINNZB2B-hkbV8j2o.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-B2Fah1VF.js → elk-276RUBZZ-DL9jncwy.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-Di3q4bpF.js → erDiagram-OPXOYQCR-etb_6SR6.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-D1UiqwbF.js → flowDiagram-KWPJA3E3-4Dgg4I8f.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-BjZqMt7o.js → ganttDiagram-FUAMR5RP-DRBB4AaO.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DiZtdip7.js → gitGraphDiagram-X574FWY7-CzgKN-tE.js} +1 -1
  33. package/dist/assets/{index-dbQveHan.js → index-BT2giPxq.js} +1 -1
  34. package/dist/assets/{index-XFwW09vd.js → index-fHtcwzVK.js} +179 -179
  35. package/dist/assets/{infoDiagram-VRGFBTTK-BJUJCgio.js → infoDiagram-VRGFBTTK-DwpjLCMe.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BX0XdHUx.js → ishikawaDiagram-OU5B5YK6-H-CWnU1-.js} +1 -1
  37. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bg904IIx.js → journeyDiagram-ZHPQQLJL-CY-oDrpq.js} +1 -1
  38. package/dist/assets/{kanban-definition-PNTS6WVX-hkZMFMHZ.js → kanban-definition-PNTS6WVX-DMGIKH3g.js} +1 -1
  39. package/dist/assets/{linear-Bh29RTbQ.js → linear-BHhtTLK8.js} +1 -1
  40. package/dist/assets/{mermaid.core-aqB1jW_w.js → mermaid.core-BWK_Ui3n.js} +5 -5
  41. package/dist/assets/{mindmap-definition-NLK3R4M7-D5GMV-Zd.js → mindmap-definition-NLK3R4M7-Cc17hw2c.js} +1 -1
  42. package/dist/assets/{pegDiagram-GJSIUBJH-7_wFtkFH.js → pegDiagram-GJSIUBJH-BqFjUBJK.js} +1 -1
  43. package/dist/assets/{pieDiagram-5QR66LMP-DXtZBQ4f.js → pieDiagram-5QR66LMP-ikmgTCa3.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-O4NWA36T-C1v3ydTF.js → quadrantDiagram-O4NWA36T-CC3s5Dsc.js} +1 -1
  45. package/dist/assets/{railroadDiagram-XR7U4H2S-CLmhiOXg.js → railroadDiagram-XR7U4H2S-Dtwf869F.js} +1 -1
  46. package/dist/assets/{requirementDiagram-PLB6GJNP-BKxI2rh_.js → requirementDiagram-PLB6GJNP-C5p7AgwQ.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-IPEJSGJF-gKK-Qs7b.js → sankeyDiagram-IPEJSGJF-8NKN770X.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-PO4LG4MO-BT94Uowp.js → sequenceDiagram-PO4LG4MO-DQucgrWO.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-RDJL6sCM.js → sizeCapture-INFHLROL-Cxmcj9Nl.js} +1 -1
  50. package/dist/assets/{stateDiagram-v2-GCMORJYK-DqHMzv-J.js → stateDiagram-v2-GCMORJYK-DEdvHXnJ.js} +1 -1
  51. package/dist/assets/{swimlanes-2SLR337P-B-NN0ql6.js → swimlanes-2SLR337P-BOGePqj1.js} +1 -1
  52. package/dist/assets/swimlanesDiagram-TC7HE7FX-BSe9TZ6t.js +8 -0
  53. package/dist/assets/{timeline-definition-EJHVYXUP-C51S9tpN.js → timeline-definition-EJHVYXUP-DfpRxJgE.js} +1 -1
  54. package/dist/assets/{usecaseDiagram-POWQR4AR-DyfNMmhq.js → usecaseDiagram-POWQR4AR-BOmQFDT4.js} +1 -1
  55. package/dist/assets/{vennDiagram-UO4OBE2U-D924r33S.js → vennDiagram-UO4OBE2U-84pfbvUi.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VNRHLVJA-DXSfsjUr.js → wardleyDiagram-VNRHLVJA-Cbxnw8ip.js} +1 -1
  57. package/dist/assets/{xychartDiagram-PMCCYNJV-mYX9tnBU.js → xychartDiagram-PMCCYNJV-BLiz8vqY.js} +1 -1
  58. package/dist/index.html +1 -1
  59. package/dist-server/connector-proxy.js +205 -0
  60. package/dist-server/container-mcp.js +7 -6
  61. package/dist-server/index.js +1150 -921
  62. package/dist-server/openmausbot.js +7 -6
  63. package/dist-server/pair-cli.js +7 -6
  64. package/dist-server/server/composio.js +1051 -0
  65. package/dist-server/server/config.js +6 -6
  66. package/dist-server/server/connector-proxy.js +249 -0
  67. package/dist-server/server/index.js +4 -2
  68. package/dist-server/server/mcp-public-registry.js +325 -0
  69. package/dist-server/server/routine-mochii-sync.js +252 -0
  70. package/dist-server/shared/connector-availability.js +12 -0
  71. package/dist-server/vps-container-mcp.js +7 -6
  72. package/package.json +1 -1
  73. package/dist/assets/channel-Nx85fHZ4.js +0 -1
  74. package/dist/assets/swimlanesDiagram-TC7HE7FX-BO2gR34z.js +0 -8
@@ -277,13 +277,13 @@ const featureConfigSchema = z.object({
277
277
  * marketplace checks) and personal skills (~/.agents/skills,
278
278
  * $CODEX_HOME/skills). */
279
279
  codexUserMcp: z.boolean().optional(),
280
- /** LLM-generated titles for new bot threads. Off until explicitly
281
- * enabled; a one-shot that fails or answers junk leaves the first-message
282
- * snippet in place — see llmThreadTitlesEnabled. */
280
+ /** LLM-generated titles for new bot threads. Off until explicitly enabled; a one-shot
281
+ * that fails or answers junk leaves the first-message snippet in place — see llmThreadTitlesEnabled. */
283
282
  llmThreadTitles: z.boolean().optional(),
284
- /** Small internal jobs (thread titles) run on Apple Intelligence on this
285
- * Mac when it is available. On unless set to false. */
283
+ /** Small internal jobs (thread titles) run on Apple Intelligence on this Mac when available. On unless false. */
286
284
  onDeviceHelpers: z.boolean().optional(),
285
+ /** Mirror routines into private Mochii tasks (server/routine-mochii-sync.ts). Off unless true; no Settings toggle. */
286
+ routineMochiiSync: z.boolean().optional(),
287
287
  });
288
288
  /** Settings → Add-ons: one workspace switch per bundled add-on
289
289
  * (shared/addons.ts). Absent means the add-on's own default. Strict, so a
@@ -293,8 +293,8 @@ const addonsConfigSchema = z.object(Object.fromEntries(ADDONS.map((addon) => [ad
293
293
  * it survives cleared site data and is shared by every paired client. Hint
294
294
  * ids are short renderer-chosen slugs; the list is capped so a buggy client
295
295
  * cannot grow the file without bound. */
296
- /** The setup wizard's progress (src/lib/setup-wizard.ts SetupProgress). */
297
296
  const setupProgressSchema = z.object({
297
+ foundationRoles: z.array(z.enum(["assistant", "inbox", "research", "coder", "community", "ops"])).max(3).optional(),
298
298
  picks: z.array(z.string().trim().min(1).max(40)).max(20).optional(),
299
299
  chibitek: z.boolean().optional(),
300
300
  steps: z.record(z.string().trim().min(1).max(60), z.enum(["done", "skipped"]))
@@ -0,0 +1,249 @@
1
+ // Harness-owned Composio MCP bridge.
2
+ //
3
+ // Provider CLIs only see this stdio server. Ordinary MCP traffic is relayed
4
+ // to the configured Composio Session, but connection requests are converted
5
+ // into first-class OpenMausBot chat cards. The agent never authors an auth
6
+ // URL and credentials never pass through its transcript.
7
+ //
8
+ // stdout is the MCP transport. Never log there.
9
+ import readline from "node:readline";
10
+ import { randomUUID } from "node:crypto";
11
+ const UPSTREAM = process.env.OMB_CONNECTOR_UPSTREAM_URL ?? "";
12
+ const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
13
+ const BOT_ID = process.env.OMB_BOT_ID ?? "";
14
+ const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
15
+ const TOKEN = process.env.OMB_CONNECTOR_TOKEN ?? process.env.OMB_COMMS_TOKEN ?? "";
16
+ const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
17
+ const INITIALIZE_RELAY_TIMEOUT_MS = 1_000;
18
+ const RELAY_TIMEOUT_MS = 10 * 60_000;
19
+ function parsedHeaders() {
20
+ try {
21
+ const value = JSON.parse(process.env.OMB_CONNECTOR_UPSTREAM_HEADERS ?? "{}");
22
+ if (!value || typeof value !== "object" || Array.isArray(value))
23
+ return {};
24
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ const upstreamHeaders = parsedHeaders();
31
+ let upstreamSessionId = "";
32
+ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
33
+ function textResult(id, text, isError = false) {
34
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) } };
35
+ }
36
+ function jsonRpcError(id, message) {
37
+ return { jsonrpc: "2.0", id, error: { code: -32000, message } };
38
+ }
39
+ function initializeResult(id, protocolVersion) {
40
+ return {
41
+ jsonrpc: "2.0",
42
+ id,
43
+ result: {
44
+ protocolVersion: typeof protocolVersion === "string" && protocolVersion ? protocolVersion : "2024-11-05",
45
+ capabilities: { tools: {} },
46
+ serverInfo: { name: "openmausbot-connectors", version: "1" },
47
+ },
48
+ };
49
+ }
50
+ async function readBounded(response) {
51
+ const declared = Number(response.headers.get("content-length") ?? "0");
52
+ if (declared > MAX_RESPONSE_BYTES)
53
+ throw new Error("connector response exceeded 20 MB");
54
+ if (!response.body)
55
+ return "";
56
+ const reader = response.body.getReader();
57
+ const decoder = new TextDecoder();
58
+ let size = 0;
59
+ let text = "";
60
+ for (;;) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ size += value.byteLength;
65
+ if (size > MAX_RESPONSE_BYTES) {
66
+ await reader.cancel();
67
+ throw new Error("connector response exceeded 20 MB");
68
+ }
69
+ text += decoder.decode(value, { stream: true });
70
+ }
71
+ return text + decoder.decode();
72
+ }
73
+ function parseUpstream(text, id) {
74
+ const trimmed = text.trim();
75
+ if (!trimmed)
76
+ return null;
77
+ if (trimmed.startsWith("{"))
78
+ return JSON.parse(trimmed);
79
+ const frames = trimmed
80
+ .split(/\r?\n/)
81
+ .filter((line) => line.startsWith("data:"))
82
+ .map((line) => line.slice(5).trim())
83
+ .filter((line) => line && line !== "[DONE]")
84
+ .flatMap((line) => {
85
+ try {
86
+ return [JSON.parse(line)];
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ });
92
+ return frames.findLast((frame) => frame.id === id) ?? frames.at(-1) ?? null;
93
+ }
94
+ async function relay(message, timeoutMs = RELAY_TIMEOUT_MS) {
95
+ if (!UPSTREAM)
96
+ throw new Error("connected apps are unavailable");
97
+ const response = await fetch(UPSTREAM, {
98
+ method: "POST",
99
+ headers: {
100
+ "content-type": "application/json",
101
+ accept: "application/json, text/event-stream",
102
+ ...upstreamHeaders,
103
+ ...(upstreamSessionId ? { "mcp-session-id": upstreamSessionId } : {}),
104
+ },
105
+ body: JSON.stringify(message),
106
+ signal: AbortSignal.timeout(timeoutMs),
107
+ });
108
+ const nextSession = response.headers.get("mcp-session-id");
109
+ if (nextSession)
110
+ upstreamSessionId = nextSession;
111
+ if (!response.ok)
112
+ throw new Error(`connector service returned HTTP ${response.status}`);
113
+ return parseUpstream(await readBounded(response), message.id);
114
+ }
115
+ function connectorAdds(args) {
116
+ if (!args || typeof args !== "object" || Array.isArray(args))
117
+ return [];
118
+ const toolkits = args.toolkits;
119
+ if (!Array.isArray(toolkits))
120
+ return [];
121
+ const seen = new Set();
122
+ const requests = [];
123
+ for (const item of toolkits) {
124
+ if (typeof item === "string") {
125
+ const slug = item.trim().toLowerCase();
126
+ const key = JSON.stringify([slug, ""]);
127
+ if (!seen.has(key)) {
128
+ seen.add(key);
129
+ requests.push({ slug });
130
+ }
131
+ continue;
132
+ }
133
+ if (!item || typeof item !== "object" || Array.isArray(item))
134
+ continue;
135
+ const row = item;
136
+ const action = String(row.action ?? "add").toLowerCase();
137
+ if (!["add", "connect", "initiate"].includes(action))
138
+ continue;
139
+ const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
140
+ if (typeof slug !== "string")
141
+ continue;
142
+ const normalized = slug.trim().toLowerCase();
143
+ const alias = (typeof row.alias === "string" ? row.alias : typeof row.account === "string" ? row.account : "").trim();
144
+ const key = JSON.stringify([normalized, alias.toLowerCase()]);
145
+ if (seen.has(key))
146
+ continue;
147
+ seen.add(key);
148
+ requests.push({ slug: normalized, ...(alias ? { alias } : {}) });
149
+ }
150
+ return requests;
151
+ }
152
+ async function showConnectorCards(items) {
153
+ const response = await fetch(`${HARNESS}/api/internal/connectors/request`, {
154
+ method: "POST",
155
+ headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
156
+ body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID, items, resumeKey: randomUUID() }),
157
+ signal: AbortSignal.timeout(30_000),
158
+ });
159
+ if (!response.ok) {
160
+ const body = (await response.json().catch(() => ({})));
161
+ throw new Error(String(body.error ?? `could not show connection card (HTTP ${response.status})`));
162
+ }
163
+ }
164
+ async function handle(message) {
165
+ const id = message.id;
166
+ const method = String(message.method ?? "");
167
+ // OpenCode (and other MCP clients) mark a stdio server failed unless
168
+ // initialize returns capabilities/serverInfo. Relaying that handshake to
169
+ // Composio can time out, return a newer protocolVersion, or throw when the
170
+ // upstream URL never reached the child env — all of which previously
171
+ // surfaced as a tools/call-shaped {content,isError} payload.
172
+ if (method === "notifications/initialized" || method === "initialized") {
173
+ if (UPSTREAM)
174
+ void relay(message).catch(() => { });
175
+ return;
176
+ }
177
+ if (method === "initialize") {
178
+ if (UPSTREAM) {
179
+ try {
180
+ // Capture the upstream session id when the service is healthy, but
181
+ // never let a stalled provider prevent the local MCP client from
182
+ // mounting the connector tools. The client sends initialized only
183
+ // after this bounded attempt and the local initialize response.
184
+ await relay(message, INITIALIZE_RELAY_TIMEOUT_MS);
185
+ }
186
+ catch {
187
+ // Best-effort session setup. The client still needs a valid result.
188
+ }
189
+ }
190
+ if (id !== undefined) {
191
+ const params = (message.params ?? {});
192
+ send(initializeResult(id, params.protocolVersion));
193
+ }
194
+ return;
195
+ }
196
+ if (method === "tools/call") {
197
+ const params = (message.params ?? {});
198
+ const name = String(params.name ?? "");
199
+ const requests = /MANAGE_CONNECTIONS$/i.test(name) ? connectorAdds(params.arguments) : [];
200
+ if (requests.length) {
201
+ await showConnectorCards(requests);
202
+ const labels = requests.map((r) => (r.alias ? `${r.slug} (${r.alias})` : r.slug)).join(", ");
203
+ send(textResult(id, `Atomic showed the user a secure connection card for ${labels}. End this turn now. The app will continue the task automatically after the connection finishes.`));
204
+ return;
205
+ }
206
+ if (/WAIT_FOR_CONNECTIONS$/i.test(name)) {
207
+ send(textResult(id, "Atomic is handling connection completion and will continue the task automatically."));
208
+ return;
209
+ }
210
+ }
211
+ try {
212
+ const response = await relay(message);
213
+ if (response && id !== undefined)
214
+ send(response);
215
+ }
216
+ catch (error) {
217
+ if (id === undefined)
218
+ return;
219
+ const messageText = error instanceof Error ? error.message : String(error);
220
+ if (method === "tools/call")
221
+ send(textResult(id, messageText, true));
222
+ else
223
+ send(jsonRpcError(id, messageText));
224
+ }
225
+ }
226
+ const input = readline.createInterface({ input: process.stdin, terminal: false });
227
+ input.on("line", (line) => {
228
+ const trimmed = line.trim();
229
+ if (!trimmed)
230
+ return;
231
+ let message;
232
+ try {
233
+ message = JSON.parse(trimmed);
234
+ }
235
+ catch {
236
+ return;
237
+ }
238
+ void handle(message).catch((error) => {
239
+ if (message.id === undefined)
240
+ return;
241
+ const method = String(message.method ?? "");
242
+ const messageText = error instanceof Error ? error.message : String(error);
243
+ if (method === "tools/call")
244
+ send(textResult(message.id, messageText, true));
245
+ else
246
+ send(jsonRpcError(message.id, messageText));
247
+ });
248
+ });
249
+ input.on("close", () => process.exit(0));
@@ -176,6 +176,7 @@ import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey,
176
176
  import { withZipListings } from "./zip-listing.js";
177
177
  import { createScreenFrameSource, startPeriodicCapture } from "./screen-frame-source.js";
178
178
  import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
179
+ import { RoutineMochiiSync } from "./routine-mochii-sync.js";
179
180
  import { RoutineRequestService } from "./routine-requests.js";
180
181
  import { buildBotOverview } from "./bot-overview.js";
181
182
  import { ProfileRequestService } from "./profile-requests.js";
@@ -2431,6 +2432,7 @@ function noteNexusLocalMemory(botId) {
2431
2432
  }
2432
2433
  }
2433
2434
  nexusMemory.start();
2435
+ const routineMochiiSync = RoutineMochiiSync.start({ dataDir: DATA_DIR, config: () => cfg, listRoutines: () => routines?.listRoutines() ?? [], listRuns: () => routines?.listRuns() ?? [], botName: (id) => store.bot(id)?.name, timeZone: () => routineTimeZone() });
2434
2436
  // Register the Private Bot's Nexus key as soon as there is a Private Bot, so
2435
2437
  // nothing else can register first (Nexus trusts the first key).
2436
2438
  // (Deferred: the store is created further down.)
@@ -11273,7 +11275,7 @@ routines = new RoutineManager({
11273
11275
  }
11274
11276
  },
11275
11277
  interruptGoal: interruptRoutineGroupGoal,
11276
- onRunChanged: syncRoutineRunToSource,
11278
+ onRunChanged: (run) => { syncRoutineRunToSource(run); routineMochiiSync.poke(); },
11277
11279
  onRunFailed: (run) => {
11278
11280
  if (run.threadId) {
11279
11281
  pendingDelegationWakes.delete(run.threadId);
@@ -25522,7 +25524,7 @@ const gracefulShutdown = createGracefulShutdown({
25522
25524
  () => setupLogins.stopAll(),
25523
25525
  () => flushAllMemoryJournals(),
25524
25526
  // the outbox is already on disk; this only stops its retry timer
25525
- () => nexusMemory.stop(),
25527
+ () => { nexusMemory.stop(); routineMochiiSync.stop(); },
25526
25528
  () => flushUsageLedger(DATA_DIR),
25527
25529
  () => flushDecisionLog(DATA_DIR),
25528
25530
  () => flushAdminActivity(DATA_DIR),
@@ -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 };