ocean-crew 0.0.0-stage → 0.4.0

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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -2
  3. package/THIRD_PARTY_NOTICES.md +159 -0
  4. package/client.js +31389 -0
  5. package/cordis.patch.yml +6 -0
  6. package/dist/host/chat/btw.js +436 -0
  7. package/dist/host/chat/channel.js +1241 -0
  8. package/dist/host/chat/chat-index.js +344 -0
  9. package/dist/host/chat/delivery.js +231 -0
  10. package/dist/host/chat/feed.js +165 -0
  11. package/dist/host/chat/handoffs.js +625 -0
  12. package/dist/host/chat/parts.js +2033 -0
  13. package/dist/host/chat/requests.js +155 -0
  14. package/dist/host/chat/resume.js +364 -0
  15. package/dist/host/chat/side.js +457 -0
  16. package/dist/host/chat/steer.js +346 -0
  17. package/dist/host/chat/turns.js +1457 -0
  18. package/dist/host/connectors/catalog.js +331 -0
  19. package/dist/host/connectors/gateway.js +339 -0
  20. package/dist/host/connectors/index.js +1235 -0
  21. package/dist/host/connectors/mail/index.js +912 -0
  22. package/dist/host/connectors/mail/presets.js +53 -0
  23. package/dist/host/connectors/mail/transport.js +519 -0
  24. package/dist/host/connectors/oauth.js +337 -0
  25. package/dist/host/core/constants.js +123 -0
  26. package/dist/host/core/diagnostics.js +310 -0
  27. package/dist/host/core/dsh-update.js +212 -0
  28. package/dist/host/core/events.js +16 -0
  29. package/dist/host/core/files.js +37 -0
  30. package/dist/host/core/folders.js +283 -0
  31. package/dist/host/core/home-lock.js +137 -0
  32. package/dist/host/core/links.js +52 -0
  33. package/dist/host/core/runtime.js +5 -0
  34. package/dist/host/core/store.js +662 -0
  35. package/dist/host/core/text.js +59 -0
  36. package/dist/host/core/thinking.js +60 -0
  37. package/dist/host/core/time-context.js +69 -0
  38. package/dist/host/core/update.js +466 -0
  39. package/dist/host/groups/calls.js +684 -0
  40. package/dist/host/groups/index.js +79 -0
  41. package/dist/host/groups/log.js +334 -0
  42. package/dist/host/groups/prompt.js +124 -0
  43. package/dist/host/groups/relay.js +489 -0
  44. package/dist/host/groups/round.js +992 -0
  45. package/dist/host/groups/tasks.js +217 -0
  46. package/dist/host/groups/tools.js +478 -0
  47. package/dist/host/groups/types.js +8 -0
  48. package/dist/host/im/format.js +228 -0
  49. package/dist/host/im/index.js +2461 -0
  50. package/dist/host/im/platforms/feishu.js +1134 -0
  51. package/dist/host/im/platforms/index.js +6 -0
  52. package/dist/host/im/platforms/media-type.js +70 -0
  53. package/dist/host/im/platforms/weixin.js +1117 -0
  54. package/dist/host/im/reaction.js +51 -0
  55. package/dist/host/im/supervisor.js +56 -0
  56. package/dist/host/team/agents.js +161 -0
  57. package/dist/host/team/models.js +427 -0
  58. package/dist/host/team/permissions.js +263 -0
  59. package/dist/host/team/privacy.js +108 -0
  60. package/dist/host/team/prompt.js +86 -0
  61. package/dist/host/team/providers.js +461 -0
  62. package/dist/host/team/roster.js +1011 -0
  63. package/dist/host/team/tools.js +1638 -0
  64. package/dist/host/work/approvals.js +290 -0
  65. package/dist/host/work/avatar-library.js +155 -0
  66. package/dist/host/work/avatar.js +132 -0
  67. package/dist/host/work/capabilities.js +862 -0
  68. package/dist/host/work/companion.js +306 -0
  69. package/dist/host/work/goal-skills.js +91 -0
  70. package/dist/host/work/goals.js +1337 -0
  71. package/dist/host/work/images.js +283 -0
  72. package/dist/host/work/learning.js +1021 -0
  73. package/dist/host/work/memory-pending.js +108 -0
  74. package/dist/host/work/memory.js +3237 -0
  75. package/dist/host/work/notify.js +146 -0
  76. package/dist/host/work/reset.js +43 -0
  77. package/dist/host/work/schedules.js +309 -0
  78. package/dist/host/work/secrets.js +702 -0
  79. package/dist/host/work/steward.js +166 -0
  80. package/dist/host/work/tts.js +192 -0
  81. package/dist/host/work/usage.js +84 -0
  82. package/dist/index.js +137 -0
  83. package/dist/shared/api.js +32 -0
  84. package/dist/shared/companion.js +110 -0
  85. package/dist/shared/config.js +3 -0
  86. package/dist/shared/coordination.js +6 -0
  87. package/dist/shared/group-replies.js +95 -0
  88. package/dist/shared/im-platforms.js +35 -0
  89. package/dist/shared/im.js +3 -0
  90. package/dist/shared/mentions.js +85 -0
  91. package/dist/shared/state.js +25 -0
  92. package/package.json +81 -5
@@ -0,0 +1,1241 @@
1
+ // Channel: the `POST /api/bot` connection the desktop shell uses. A buffered JSON request
2
+ // names one endpoint in a closed set and always receives HTTP 200 carrying either
3
+ // `{ ok: true, value }` or `{ ok: false, error: { code, message, details } }`. The code is
4
+ // the machine field the client branches on; the message is text a user may read.
5
+ //
6
+ // The channel itself answers the roster, session, group, card and preference endpoints the
7
+ // shell needs to draw and drive a team. Endpoints owned by another host module are wired
8
+ // dynamically: each answers from the runtime member its module publishes and keeps
9
+ // `bot/unavailable` (naming the missing member) until that module is installed, so one
10
+ // channel serves every combination of installed and missing modules. There is no fake
11
+ // success and no silent omission. A second host without the team lock still answers READS,
12
+ // and every other endpoint keeps its `bot/read-only` refusal.
13
+ //
14
+ // - `run-stats` reads DSH's own session projection;
15
+ // - `view-reply` needs the parts/cards module to publish `validateViewAnswer` and `viewReply`;
16
+ // until then it refuses with `bot/unavailable` rather than queueing an unvalidated answer.
17
+ // for the optional member, which this build treats as "the module has not landed".
18
+ // - `folderEndpoint` receives the request signal, so a folder pick is abortable as before.
19
+ // - `browser-*`: a host without the browser module refuses `bot/unavailable`; a host whose
20
+ // default branch did; an unknown suffix is still the module's call.
21
+ // - `archivesView`, `capabilityLibraryView`, `secretsView`, `imView`, the
22
+ // live views and the activity/work views are optional runtime callers: when their module
23
+ // is absent the field keeps its honest empty value, and only an endpoint that needs the
24
+ // module refuses with `bot/unavailable`.
25
+ import { randomUUID } from "node:crypto";
26
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
27
+ import { COLORS, EXTRA_COLORS, REPLY_KIND, ROUTE } from "../core/constants.js";
28
+ const LANGUAGES = { zh: "Chinese (简体中文)", en: "English" };
29
+ const RUN_STATS = ["sessionStats", "tokenUsage", "contextPressure"];
30
+ /** The read endpoints a host without the team lock still answers (docs/contracts.md). */
31
+ export const READS = new Set([
32
+ "state",
33
+ "models",
34
+ "efforts",
35
+ "providers",
36
+ "provider-models",
37
+ "earlier",
38
+ "chat",
39
+ "run-stats",
40
+ "room-progress",
41
+ "exchange",
42
+ "exchange-text",
43
+ "file-text",
44
+ "usage",
45
+ "companion-view",
46
+ "schedules",
47
+ "memory-view",
48
+ "diagnostics",
49
+ "diagnostics-log",
50
+ "diagnostics-clear",
51
+ "update-check",
52
+ "update-install",
53
+ "dsh-update-install",
54
+ "repo-stars",
55
+ "approval-answer",
56
+ "thinking",
57
+ "avatar-library",
58
+ ]);
59
+ /** The endpoints the channel itself answers. Every other known name is deferred or unknown. */
60
+ export const CHANNEL_ENDPOINTS = [
61
+ "state",
62
+ "models",
63
+ "efforts",
64
+ "set-prefs",
65
+ "exchange",
66
+ "exchange-text",
67
+ "file-text",
68
+ "room-progress",
69
+ "create-bot",
70
+ "update-bot",
71
+ "duplicate-bot",
72
+ "delete-bot",
73
+ "set-main",
74
+ "set-flags",
75
+ "resume",
76
+ "create-room",
77
+ "update-room",
78
+ "delete-room",
79
+ "send",
80
+ "answer-question",
81
+ "dismiss-question",
82
+ "dismiss-view",
83
+ "run-stats",
84
+ "stop-member-work",
85
+ "stop-room-work",
86
+ ];
87
+ /**
88
+ * Endpoints another host module answers: wired dynamically. Each answers from the runtime
89
+ * member its module publishes and stays `bot/unavailable` until that module is installed,
90
+ * so one channel serves every combination of installed and missing modules.
91
+ */
92
+ export const MODULE_ENDPOINTS = [
93
+ "providers",
94
+ "provider-key",
95
+ "provider-add",
96
+ "provider-models",
97
+ "provider-remove",
98
+ "thinking",
99
+ "earlier",
100
+ "chat",
101
+ "bot-new-part",
102
+ "bot-open-part",
103
+ "usage",
104
+ // folders and archives (folders.ts answers them)
105
+ "folders",
106
+ "folder-pick",
107
+ "folder-list",
108
+ "team-workspace",
109
+ "private-release",
110
+ "reset-team",
111
+ "avatar-library",
112
+ "avatar-import",
113
+ "avatar-delete",
114
+ "add-agent",
115
+ "agent-session",
116
+ "agent-adopt",
117
+ "agent-archive",
118
+ "agent-rename",
119
+ "remove-agent",
120
+ "approval-answer",
121
+ "goal-draft",
122
+ "goal-control",
123
+ "room-budget",
124
+ "companion-view",
125
+ "companion-set",
126
+ "companion-audio",
127
+ "tts-settings",
128
+ "tts-set",
129
+ "tts-voices",
130
+ "tts-test",
131
+ "secret-set",
132
+ "secret-allow",
133
+ "secret-cancel",
134
+ "secret-update",
135
+ "secret-delete",
136
+ "connector-connect",
137
+ "connector-disconnect",
138
+ "connector-retry",
139
+ "connector-sign-in-code",
140
+ "connector-cancel",
141
+ "connector-client",
142
+ "connector-set",
143
+ "connector-add",
144
+ "connector-remove",
145
+ "connector-import-scan",
146
+ "connector-import",
147
+ "connector-request-cancel",
148
+ "mail-account-add",
149
+ "mail-account-remove",
150
+ "mail-rule-set",
151
+ "mail-send-answer",
152
+ "mail-sign-in-code",
153
+ "capability-save",
154
+ "capability-delete",
155
+ "capability-approve",
156
+ "bot-capability",
157
+ "schedules",
158
+ "schedule-create",
159
+ "schedule-delete",
160
+ "memory-view",
161
+ "memory-ask",
162
+ "memory-forget",
163
+ "memory-history",
164
+ "memory-restore",
165
+ "learning-answer",
166
+ "view-reply",
167
+ "btw-delete",
168
+ "side-new",
169
+ "side-ask",
170
+ "side-cancel",
171
+ "side-close",
172
+ "update-check",
173
+ "update-install",
174
+ "dsh-update-install",
175
+ "repo-stars",
176
+ "diagnostics",
177
+ "diagnostics-log",
178
+ "diagnostics-clear",
179
+ ];
180
+ // Endpoint families whose module answers the whole group through one `*Endpoint` member.
181
+ const SECRET_ENDPOINTS = [
182
+ "secret-set",
183
+ "secret-allow",
184
+ "secret-cancel",
185
+ "secret-update",
186
+ "secret-delete",
187
+ ];
188
+ const CONNECTOR_MAIL_ENDPOINTS = [
189
+ "connector-connect",
190
+ "connector-disconnect",
191
+ "connector-retry",
192
+ "connector-sign-in-code",
193
+ "connector-cancel",
194
+ "connector-client",
195
+ "connector-set",
196
+ "connector-add",
197
+ "connector-remove",
198
+ "connector-import-scan",
199
+ "connector-import",
200
+ "connector-request-cancel",
201
+ "mail-account-add",
202
+ "mail-account-remove",
203
+ "mail-rule-set",
204
+ "mail-send-answer",
205
+ "mail-sign-in-code",
206
+ ];
207
+ const CAPABILITY_ENDPOINTS = [
208
+ "capability-save",
209
+ "capability-delete",
210
+ "capability-approve",
211
+ "bot-capability",
212
+ ];
213
+ const SCHEDULE_ENDPOINTS = ["schedules", "schedule-create", "schedule-delete"];
214
+ const MEMORY_ENDPOINTS = [
215
+ "memory-view",
216
+ "memory-ask",
217
+ "memory-forget",
218
+ "memory-history",
219
+ "memory-restore",
220
+ ];
221
+ /** IM endpoints, one `imEndpoint(endpoint, body)` module call each. */
222
+ export const IM_ENDPOINTS = [
223
+ "im-add",
224
+ "im-update",
225
+ "im-remove",
226
+ "im-pair",
227
+ "im-unpair",
228
+ "im-chat",
229
+ "im-retry",
230
+ "im-login-start",
231
+ "im-login-wait",
232
+ ];
233
+ /** Every IM endpoint; each refuses by missing member until its module loads. */
234
+ export const SERVICE_ENDPOINTS = [...IM_ENDPOINTS];
235
+ /**
236
+ * Endpoints still only placeholders. Every known endpoint is wired in `endpoints` and
237
+ * refuses from `missingMember` until its module publishes the member; the export stays so a
238
+ * module can register its surface here first.
239
+ */
240
+ export const UNAVAILABLE_ENDPOINTS = new Map();
241
+ const invalid = (message) => Object.assign(new Error(message), { code: "bot/invalid" });
242
+ const unavailable = (what) => Object.assign(new Error(`${what} is not available in this build yet.`), { code: "bot/unavailable" });
243
+ const stale = (message) => Object.assign(new Error(message), { code: "bot/stale" });
244
+ /** An endpoint whose host module has not been installed. */
245
+ const missingMember = (endpoint, member) => Object.assign(new Error(`${endpoint}: the ${member} host member is not installed in this build.`), {
246
+ code: "bot/unavailable",
247
+ });
248
+ // Programming and file-system errors are Ocean Crew's own faults and are logged with their
249
+ // stack; every other error is a refusal whose message is meant for the user.
250
+ const isInternal = (error) => {
251
+ const code = error?.code;
252
+ return (error instanceof TypeError ||
253
+ error instanceof ReferenceError ||
254
+ error instanceof RangeError ||
255
+ error instanceof SyntaxError ||
256
+ (typeof code === "string" && /^E[A-Z_]+$/.test(code)));
257
+ };
258
+ /** The machine code a thrown error maps to (docs/contracts.md - `bot/` error codes). */
259
+ export function errorCode(error) {
260
+ const code = error?.code;
261
+ if (typeof code === "string" && code.startsWith("bot/"))
262
+ return code;
263
+ return isInternal(error) ? "bot/internal" : "bot/refused";
264
+ }
265
+ const ok = (value) => ({ ok: true, value });
266
+ const fail = (message, code = "bot/error") => ({
267
+ ok: false,
268
+ error: { code, message, details: {} },
269
+ });
270
+ const own = (table, key) => table != null && Object.hasOwn(table, key) ? table[key] : undefined;
271
+ export function install(rt) {
272
+ const { ctx } = rt;
273
+ const state = rt.state;
274
+ let uiLocale;
275
+ // The DSH interface language of the last browser that asked for the team.
276
+ const uiLanguage = () => uiLocale === undefined ? undefined : (LANGUAGES[uiLocale.split("-")[0].toLowerCase()] ?? uiLocale);
277
+ /** An optional cross-module function; undefined until its module is installed. */
278
+ const fn = (name) => {
279
+ const value = rt[name];
280
+ return typeof value === "function" ? value : undefined;
281
+ };
282
+ const sync = (name, ...args) => fn(name)?.(...args);
283
+ const call = async (name, ...args) => {
284
+ const wanted = fn(name);
285
+ if (wanted === undefined)
286
+ throw unavailable(`The ${name} host module`);
287
+ return await wanted(...args);
288
+ };
289
+ const record = async (name, ...args) => {
290
+ const wanted = fn(name);
291
+ if (wanted === undefined)
292
+ return;
293
+ await wanted(...args);
294
+ };
295
+ /** A member an endpoint needs; a missing one refuses the endpoint, not the host. */
296
+ const member = (endpoint, name) => {
297
+ const found = fn(name);
298
+ if (found === undefined)
299
+ throw missingMember(endpoint, name);
300
+ return found;
301
+ };
302
+ const callMember = async (endpoint, name, ...args) => await member(endpoint, name)(...args);
303
+ const liveMap = () => rt.live;
304
+ /** The diagnostics object; it publishes `report`, `logText` and `clear` together. */
305
+ const diagnosticsService = (endpoint, method) => {
306
+ const service = rt.diagnostics;
307
+ if (typeof service?.[method] !== "function")
308
+ throw missingMember(endpoint, `diagnostics.${method}`);
309
+ return service;
310
+ };
311
+ const botOf = (id) => (id === undefined ? undefined : fn("botOf")?.(id));
312
+ const roomOf = (id) => (id === undefined ? undefined : fn("roomOf")?.(id));
313
+ const chatOf = (id) => fn("chatOf")?.(id) ?? id;
314
+ const selfOf = (id) => fn("selfOf")?.(id);
315
+ const sessionOfRoom = (room) => fn("sessionOfRoom")?.(room) ?? room.sessionId ?? room.id;
316
+ const pastOwner = (ref) => rt.pastOwners?.get(ref);
317
+ const roomProgress = () => rt.roomProgress;
318
+ const prefs = () => state.prefs;
319
+ const load = async () => {
320
+ const wanted = fn("load");
321
+ if (wanted !== undefined)
322
+ await wanted();
323
+ };
324
+ const save = async () => {
325
+ const wanted = fn("save");
326
+ if (wanted !== undefined)
327
+ await wanted();
328
+ };
329
+ // Models come from DSH, not state.json, so a provider added in Harness settings changes
330
+ // the answer without a new revision; the serialized pair is what `state` compares.
331
+ const modelsKey = () => {
332
+ const catalog = sync("cachedCatalog");
333
+ const entries = Array.isArray(catalog) ? catalog : [];
334
+ const refs = entries.map((entry) => `${entry?.ref ?? ""}${entry?.needsKey === true ? "!" : ""}${entry?.reasoning ? `:${JSON.stringify(entry.reasoning)}` : ""}`);
335
+ return `${refs.join(",")}|${sync("cachedDefault") ?? ""}`;
336
+ };
337
+ // Fields with no change counter of their own: the serialized answer is the detector, so
338
+ // an unchanged poll gets the key back instead of the fields again. The key carries a
339
+ // per-run prefix so a window that outlives this host cannot match a stale key.
340
+ const liveRun = randomUUID();
341
+ let liveSeq = 0;
342
+ let liveCache;
343
+ const live = () => ({
344
+ activity: sync("activityView") ?? {},
345
+ work: {
346
+ bots: sync("workView") ?? {},
347
+ rooms: sync("roomWorkView") ?? {},
348
+ approvals: sync("approvalsView") ?? [],
349
+ waiting: sync("waitingView") ?? {},
350
+ turns: sync("turnsView") ?? {},
351
+ steered: sync("steeredView") ?? {},
352
+ tasks: sync("tasksView") ?? [],
353
+ },
354
+ memoryNews: sync("memoryNews") ?? {},
355
+ memoryReviews: sync("memoryReviews") ?? {},
356
+ memoryReviewStates: sync("memoryReviewStates") ?? {},
357
+ learning: sync("learningView") ?? {},
358
+ learningSettled: sync("learningSettledView") ?? {},
359
+ connectors: sync("connectorsView") ?? {},
360
+ capabilityStatuses: sync("capabilityStatusesView") ?? {},
361
+ imStatus: sync("imStatusView") ?? {},
362
+ imVia: sync("imViaView") ?? {},
363
+ computerLive: sync("computerLive") ?? {},
364
+ });
365
+ const liveAnswer = () => {
366
+ const value = live();
367
+ const json = JSON.stringify(value);
368
+ if (liveCache?.json === json)
369
+ return liveCache;
370
+ liveCache = { key: `${liveRun}:${++liveSeq}`, value, json };
371
+ return liveCache;
372
+ };
373
+ // What the browser renders. a field
374
+ const view = () => {
375
+ const companions = sync("companionMap") ?? {};
376
+ const bots = Object.values(state.bots).map((bot) => {
377
+ const { segments, kickoff, ...rest } = bot;
378
+ return {
379
+ ...rest,
380
+ sessionId: chatOf(bot.id),
381
+ parts: (segments ?? []).map((segment) => segment.sessionId),
382
+ ...(kickoff ? { waiting: true } : {}),
383
+ ...(companions[bot.id] === undefined ? {} : { companion: companions[bot.id] }),
384
+ };
385
+ });
386
+ const rooms = Object.values(state.rooms).map((room) => {
387
+ const { log, sessions, segments, parts: ownParts, ...rest } = room;
388
+ // The log is rendered as `preview`; the read marks and the pause flag stay internal.
389
+ const roomView = { ...rest };
390
+ delete roomView.seen;
391
+ delete roomView.paused;
392
+ const parts = [
393
+ ...Object.values(sessions ?? {}),
394
+ ...Object.values(segments ?? {})
395
+ .flat()
396
+ .map((segment) => segment.sessionId),
397
+ ...(ownParts ?? []).map((segment) => segment.sessionId),
398
+ ];
399
+ const last = (Array.isArray(log) ? log : []).filter((line) => line.kind === undefined).at(-1);
400
+ return {
401
+ ...roomView,
402
+ sessionId: sessionOfRoom(room),
403
+ ownParts: (ownParts ?? []).map((segment) => segment.sessionId),
404
+ parts,
405
+ ...(last ? { preview: last.who === "User" ? last.text : `${String(last.who)}:${last.text ?? ""}` } : {}),
406
+ };
407
+ });
408
+ return {
409
+ revision: state.revision,
410
+ workspaceId: state.workspaceId ?? null,
411
+ // `mainBotId` is the first Main Bot, the sidebar's tile; the list keeps the order.
412
+ mainBotId: state.mainBotIds[0] ?? null,
413
+ mainBotIds: state.mainBotIds,
414
+ bots,
415
+ rooms,
416
+ // Deleted private Bots whose folder, memory and chats stay closed to the others.
417
+ privateArchives: sync("archivesView") ?? [],
418
+ exchangeTurns: state.exchangeTurns,
419
+ questions: state.questions,
420
+ views: state.views,
421
+ closedCards: state.closedCards,
422
+ // Aside answers and side chats of each Bot's current part, once parts is installed.
423
+ btwCards: sync("btwView") ?? {},
424
+ sideChats: sync("sideView") ?? {},
425
+ agents: Object.values(state.agents),
426
+ // Key names and who may use them; the values never leave the host.
427
+ secrets: sync("secretsView") ?? [],
428
+ capabilities: sync("capabilityLibraryView") ?? [],
429
+ secretRequests: state.secretRequests ?? {},
430
+ connectorRequests: state.connectorRequests ?? {},
431
+ mailRequests: state.mailRequests ?? {},
432
+ colors: [...COLORS, ...EXTRA_COLORS],
433
+ models: sync("cachedCatalog") ?? [],
434
+ defaultModel: sync("cachedDefault") ?? null,
435
+ permissions: sync("permissionCatalog") ?? null,
436
+ modelsKey: modelsKey(),
437
+ prefs: state.prefs,
438
+ // False when config.memory turns memory off: no Memory row or panel.
439
+ memory: rt.memoryEnabled !== false,
440
+ // Set while another dsh host holds the team: why this one cannot change it.
441
+ readOnly: sync("readOnly") ?? null,
442
+ im: sync("imView") ?? { connections: [], platforms: [] },
443
+ computer: sync("computerView") ?? { enabled: false },
444
+ };
445
+ };
446
+ // either resolves to its current part. The request id lands on the message's source.
447
+ async function sendUser({ sessionId: ref, content, requestId = randomUUID(), }) {
448
+ const past = pastOwner(ref);
449
+ const room = roomOf(ref);
450
+ const bot = botOf(ref);
451
+ const sessionId = bot !== undefined
452
+ ? chatOf(ref)
453
+ : room !== undefined
454
+ ? sessionOfRoom(room)
455
+ : past?.roomId === undefined && past?.botId !== undefined
456
+ ? chatOf(past.botId)
457
+ : past?.roomId !== undefined
458
+ ? sessionOfRoom(roomOf(past.roomId))
459
+ : ref;
460
+ await record("ensureCapabilities", sessionId);
461
+ const controller = ctx.sessionController;
462
+ if (typeof controller?.prompt !== "function")
463
+ throw unavailable("Sending a message");
464
+ await controller.prompt({ requestId, sessionId, mode: "queue", content }, AbortSignal.timeout(30_000));
465
+ }
466
+ // The user's answer to an `ask_user` card. The card is claimed before queueing, so a
467
+ // second answer is refused; a failed queue puts it back.
468
+ function answerReply(question, answer) {
469
+ const record = question;
470
+ const options = Array.isArray(record.options) ? record.options : [];
471
+ const labels = new Set(options.map((option) => option?.label).filter((label) => typeof label === "string"));
472
+ const given = (answer ?? {});
473
+ const picked = (Array.isArray(given.selected) ? given.selected : []).filter((label) => typeof label === "string" && labels.has(label));
474
+ const selected = record.multiSelect === true ? [...new Set(picked)] : picked.slice(0, 1);
475
+ const custom = typeof given.custom === "string" ? given.custom.trim() : "";
476
+ if (selected.length === 0 && custom === "")
477
+ return undefined;
478
+ const callId = typeof record.callId === "string" ? record.callId : question.id;
479
+ const asked = {
480
+ id: question.id,
481
+ question: record.question ?? record.title,
482
+ ...(record.detail ? { detail: record.detail } : {}),
483
+ options: record.options,
484
+ multiSelect: record.multiSelect === true,
485
+ };
486
+ const givenAnswer = { id: question.id, selected, ...(custom !== "" ? { custom } : {}) };
487
+ return {
488
+ text: JSON.stringify({
489
+ kind: "answer_to_pending_question",
490
+ tool: "ask_user",
491
+ callId,
492
+ questions: [asked],
493
+ answers: [givenAnswer],
494
+ }),
495
+ source: { kind: REPLY_KIND, callId, outcome: "answered" },
496
+ };
497
+ }
498
+ async function answerQuestion(sessionRef, questionId, answer) {
499
+ const past = pastOwner(sessionRef);
500
+ const sessionId = botOf(sessionRef) !== undefined
501
+ ? chatOf(sessionRef)
502
+ : past?.roomId === undefined && past?.botId !== undefined
503
+ ? chatOf(past.botId)
504
+ : sessionRef;
505
+ const question = own(state.questions, sessionId);
506
+ if (question === undefined || question.id !== questionId)
507
+ throw stale("This question is no longer open");
508
+ const reply = answerReply(question, answer);
509
+ if (reply === undefined)
510
+ throw invalid("Nothing to send");
511
+ // Claim the question before queueing so a second answer is refused.
512
+ delete state.questions[sessionId];
513
+ try {
514
+ await record("ensureCapabilities", sessionId);
515
+ const agent = await call("resolveAgent", sessionId);
516
+ // DSH source kinds are merge-extensible per producer. Until the card layer owns
517
+ // shape and the cast states the invariant.
518
+ const source = reply.source;
519
+ agent.followup(createUserMessage({ content: [{ type: "text", text: reply.text }], source }));
520
+ const sessions = ctx.sessions;
521
+ await sessions?.flush?.(agent.session);
522
+ }
523
+ catch (error) {
524
+ state.questions[sessionId] ??= question;
525
+ throw error;
526
+ }
527
+ await save();
528
+ return true;
529
+ }
530
+ // An answer to an interactive view: validated by the view layer, claimed before the
531
+ // follow-up is queued so a second answer is refused, and put back when queueing fails.
532
+ async function answerView(sessionRef, viewId, action, values) {
533
+ const validate = member("view-reply", "validateViewAnswer");
534
+ const build = member("view-reply", "viewReply");
535
+ await load();
536
+ const past = pastOwner(sessionRef);
537
+ const sessionId = botOf(sessionRef) !== undefined
538
+ ? chatOf(sessionRef)
539
+ : past?.roomId === undefined && past?.botId !== undefined
540
+ ? chatOf(past.botId)
541
+ : sessionRef;
542
+ const pending = own(state.views, sessionId);
543
+ if (pending === undefined || pending.id !== viewId)
544
+ throw stale("This view is no longer open");
545
+ const checked = validate(pending.view, action, values);
546
+ if (checked !== undefined && checked.error !== undefined)
547
+ throw invalid(checked.error);
548
+ const reply = build(pending, action, values);
549
+ if (reply === undefined)
550
+ throw invalid("Nothing to send");
551
+ // Claim the view before queueing so a second answer is refused.
552
+ delete state.views[sessionId];
553
+ try {
554
+ await record("ensureCapabilities", sessionId);
555
+ const agent = await call("resolveAgent", sessionId);
556
+ const source = reply.source;
557
+ agent.followup(createUserMessage({ content: [{ type: "text", text: reply.text }], source }));
558
+ const sessions = ctx.sessions;
559
+ await sessions?.flush?.(agent.session);
560
+ }
561
+ catch (error) {
562
+ state.views[sessionId] ??= pending;
563
+ throw error;
564
+ }
565
+ await save();
566
+ return true;
567
+ }
568
+ const setPrefs = async (body) => {
569
+ const current = prefs();
570
+ if (body.theme !== undefined) {
571
+ if (typeof body.theme !== "string" || !/^[a-z][\w-]{0,31}$/.test(body.theme))
572
+ throw invalid("Unknown theme");
573
+ current.theme = body.theme;
574
+ }
575
+ if (body.accent !== undefined) {
576
+ if (body.accent === null || body.accent === "")
577
+ delete current.accent;
578
+ else if (/^#[0-9a-f]{6}$/i.test(body.accent))
579
+ current.accent = body.accent;
580
+ else
581
+ throw invalid("Accent must be a #rrggbb color");
582
+ }
583
+ if (body.motion !== undefined) {
584
+ if (body.motion === null || body.motion === "normal")
585
+ delete current.motion;
586
+ else if (body.motion === "quiet" || body.motion === "lively")
587
+ current.motion = body.motion;
588
+ else
589
+ throw invalid("Motion must be quiet, normal or lively");
590
+ }
591
+ if (body.layout !== undefined) {
592
+ if (body.layout === null || body.layout === "bubble")
593
+ delete current.layout;
594
+ else if (body.layout === "flat")
595
+ current.layout = body.layout;
596
+ else
597
+ throw invalid("Layout must be bubble or flat");
598
+ }
599
+ if (body.width !== undefined) {
600
+ if (body.width === null || body.width === 80)
601
+ delete current.width;
602
+ else if (Number.isInteger(body.width) && body.width >= 50 && body.width <= 100)
603
+ current.width = body.width;
604
+ else
605
+ throw invalid("Width must be a whole percent from 50 to 100");
606
+ }
607
+ if (body.flatWidth !== undefined) {
608
+ if (body.flatWidth === null || body.flatWidth === 100)
609
+ delete current.flatWidth;
610
+ else if (Number.isInteger(body.flatWidth) && body.flatWidth >= 50 && body.flatWidth <= 100)
611
+ current.flatWidth = body.flatWidth;
612
+ else
613
+ throw invalid("Flat text width must be a whole percent from 50 to 100");
614
+ }
615
+ if (body.defaultBotModel !== undefined) {
616
+ // The saved ref is one the catalog served at save time; a create re-checks it.
617
+ if (body.defaultBotModel === null || body.defaultBotModel === "")
618
+ delete current.defaultBotModel;
619
+ else {
620
+ if (fn("isLocalAgentRef")?.(body.defaultBotModel) === true) {
621
+ throw invalid("A local agent cannot be the default model: a new Bot chooses to run on one when it is created");
622
+ }
623
+ const resolve = fn("resolveModel");
624
+ if (resolve === undefined)
625
+ throw unavailable("Choosing a default model");
626
+ current.defaultBotModel = await resolve(body.defaultBotModel);
627
+ }
628
+ }
629
+ if (body.thinking !== undefined) {
630
+ // A value this version does not know is ignored, not refused: the toggle may grow.
631
+ if (body.thinking === true)
632
+ current.thinking = true;
633
+ else if (body.thinking === false || body.thinking === null)
634
+ delete current.thinking;
635
+ }
636
+ if (body.enterToSend !== undefined) {
637
+ if (body.enterToSend === false)
638
+ current.enterToSend = false;
639
+ else if (body.enterToSend === true || body.enterToSend === null)
640
+ delete current.enterToSend;
641
+ }
642
+ await save();
643
+ };
644
+ const endpoints = {
645
+ async state(body) {
646
+ if (typeof body.locale === "string" && /^[a-z]{2,3}(-[\w-]+)?$/i.test(body.locale))
647
+ uiLocale = body.locale;
648
+ if (sync("readOnly") === undefined) {
649
+ await record("bootstrap");
650
+ }
651
+ await record("catalog");
652
+ await record("loadSecrets");
653
+ const answer = liveAnswer();
654
+ if (body.since === state.revision && (body.models === undefined || body.models === modelsKey())) {
655
+ if (body.live === answer.key) {
656
+ return ok({ revision: state.revision, unchanged: true, liveKey: answer.key });
657
+ }
658
+ return ok({ revision: state.revision, unchanged: true, liveKey: answer.key, ...answer.value });
659
+ }
660
+ return ok({ ...view(), liveKey: answer.key, ...answer.value });
661
+ },
662
+ async models() {
663
+ await call("catalog", { fresh: true });
664
+ return ok({ modelsKey: modelsKey() });
665
+ },
666
+ async efforts(body) {
667
+ const efforts = await call("reasoningOf", String(body.ref ?? ""));
668
+ return ok(efforts ?? { efforts: [] });
669
+ },
670
+ async "set-prefs"(body) {
671
+ await load();
672
+ await setPrefs(body);
673
+ return ok(state.prefs);
674
+ },
675
+ async exchange(body) {
676
+ await load();
677
+ const pair = new Set([body.a, body.b]);
678
+ return ok(state.exchanges.filter((entry) => pair.has(entry.from) && pair.has(entry.to)));
679
+ },
680
+ async "exchange-text"(body) {
681
+ await load();
682
+ const entry = state.exchanges.find((item) => item.id === body.id);
683
+ if (entry === undefined)
684
+ return fail("This message is no longer kept", "bot/stale");
685
+ if (fn("exchangeText") === undefined)
686
+ throw unavailable("Reading a kept message");
687
+ return ok(await call("exchangeText", entry));
688
+ },
689
+ async "file-text"(body) {
690
+ if (typeof body.file?.attachmentId !== "string")
691
+ return fail("No file named", "bot/invalid");
692
+ if (fn("fileText") === undefined)
693
+ throw unavailable("Reading a file");
694
+ const read = await call("fileText", body.file);
695
+ return read === undefined ? fail("This file cannot be read here", "bot/unavailable") : ok(read);
696
+ },
697
+ async "room-progress"(body) {
698
+ // Any part id of the group's own Session means the group itself.
699
+ const room = roomOf(body.roomId) ?? roomOf(pastOwner(body.roomId)?.roomId);
700
+ const progress = roomProgress();
701
+ const wanted = room?.id ?? body.roomId;
702
+ const rows = [...(progress ?? new Map()).entries()]
703
+ .filter(([, entry]) => entry?.roomId === wanted)
704
+ .map(([callId, entry]) => ({
705
+ callId,
706
+ replies: entry.replies,
707
+ speaking: (entry.slots ?? [])
708
+ .filter((slot) => slot.status === "speaking")
709
+ .map((slot) => slot.id),
710
+ }));
711
+ return ok(rows);
712
+ },
713
+ async "create-bot"(body) {
714
+ await record("bootstrap");
715
+ const bot = await call("createBot", {
716
+ name: body.name,
717
+ role: body.role,
718
+ color: body.color,
719
+ instructions: body.instructions,
720
+ brief: body.brief,
721
+ avatar: body.avatar,
722
+ model: body.model,
723
+ effort: body.effort,
724
+ workDir: body.workDir,
725
+ });
726
+ return ok(bot);
727
+ },
728
+ async "update-bot"(body) {
729
+ await load();
730
+ return ok(await call("updateBot", body.id, body));
731
+ },
732
+ async "duplicate-bot"(body) {
733
+ await load();
734
+ return ok(await call("duplicateBot", body.id));
735
+ },
736
+ async "delete-bot"(body) {
737
+ await load();
738
+ await call("deleteBot", body.id);
739
+ return ok(view());
740
+ },
741
+ async "set-main"(body) {
742
+ await load();
743
+ await call("setMain", body.id, body.main !== false, {
744
+ primary: body.primary === true,
745
+ replace: typeof body.replace === "string" ? body.replace : undefined,
746
+ });
747
+ return ok(view());
748
+ },
749
+ async "set-flags"(body) {
750
+ await load();
751
+ return ok(await call("setFlags", body.id, body));
752
+ },
753
+ async resume(body) {
754
+ await load();
755
+ const bot = botOf(String(body.botId ?? ""));
756
+ if (bot === undefined)
757
+ return fail("Unknown Bot", "bot/not-found");
758
+ await call("resumeBot", bot);
759
+ return ok(true);
760
+ },
761
+ async "create-room"(body) {
762
+ await load();
763
+ const room = await call("createRoom", {
764
+ members: Array.isArray(body.members) ? body.members : [],
765
+ name: body.name,
766
+ ...(typeof body.admin === "string" ? { admin: body.admin } : {}),
767
+ });
768
+ if (body.greet === true) {
769
+ const greet = fn("greetRoom");
770
+ if (greet !== undefined) {
771
+ await greet(room.id).catch((error) => rt.warn("ocean-crew: greeting in %s failed: %s", room.name, error));
772
+ }
773
+ }
774
+ return ok(room);
775
+ },
776
+ async "update-room"(body) {
777
+ await load();
778
+ const { room, changes } = await call("updateRoom", body.id, {
779
+ name: body.name,
780
+ notice: body.notice,
781
+ mode: body.mode,
782
+ admin: body.admin,
783
+ members: Array.isArray(body.members) ? body.members : undefined,
784
+ add: Array.isArray(body.add) ? body.add : undefined,
785
+ remove: Array.isArray(body.remove) ? body.remove : undefined,
786
+ workDir: body.workDir,
787
+ permission: body.permission,
788
+ });
789
+ const rooms = view().rooms;
790
+ return ok({ room: rooms.find((entry) => entry.id === room.id), changes });
791
+ },
792
+ async "delete-room"(body) {
793
+ await load();
794
+ await call("deleteRoom", body.id);
795
+ return ok(view());
796
+ },
797
+ async send(body) {
798
+ const text = String(body.text ?? "").trim();
799
+ if (text === "" || typeof body.sessionId !== "string")
800
+ return fail("Nothing to send", "bot/invalid");
801
+ await load();
802
+ await sendUser({ sessionId: body.sessionId, content: [{ type: "text", text }] });
803
+ return ok(true);
804
+ },
805
+ async "answer-question"(body) {
806
+ if (typeof body.sessionId !== "string")
807
+ return fail("Nothing to send", "bot/invalid");
808
+ await load();
809
+ return ok(await answerQuestion(body.sessionId, body.questionId, body.answer));
810
+ },
811
+ async "dismiss-question"(body) {
812
+ await load();
813
+ const sessionId = String(body.sessionId ?? "");
814
+ const question = own(state.questions, sessionId);
815
+ if (question !== undefined) {
816
+ sync("closeCard", sessionId, question, "dismissed");
817
+ delete state.questions[sessionId];
818
+ await save();
819
+ }
820
+ return ok(true);
821
+ },
822
+ async "dismiss-view"(body) {
823
+ await load();
824
+ const sessionId = String(body.sessionId ?? "");
825
+ const pending = own(state.views, sessionId);
826
+ if (pending !== undefined) {
827
+ sync("closeCard", sessionId, pending, "dismissed");
828
+ delete state.views[sessionId];
829
+ await save();
830
+ }
831
+ return ok(true);
832
+ },
833
+ async "run-stats"(body) {
834
+ await load();
835
+ const bot = botOf(String(body.botId ?? ""));
836
+ if (bot === undefined)
837
+ return fail("Unknown Bot", "bot/not-found");
838
+ const projections = ctx.get?.("sessionProjections");
839
+ if (typeof projections?.snapshot !== "function")
840
+ return ok(null);
841
+ const agent = await call("resolveAgent", chatOf(bot.id));
842
+ return ok(projections.snapshot(agent.session, RUN_STATS).values);
843
+ },
844
+ async "stop-member-work"(body) {
845
+ const roomId = String(body.roomId ?? "");
846
+ try {
847
+ return ok(await call("stopMemberWork", roomId, String(body.botId ?? "")));
848
+ }
849
+ finally {
850
+ sync("goalUserStopped", roomId);
851
+ }
852
+ },
853
+ async "stop-room-work"(body) {
854
+ const roomId = String(body.roomId ?? "");
855
+ try {
856
+ return ok(await call("stopRoomWork", roomId));
857
+ }
858
+ finally {
859
+ sync("goalUserStopped", roomId);
860
+ }
861
+ },
862
+ // --- Module wiring -----------------------------------------------------
863
+ // Each branch calls the runtime member its module publishes; the module landing is
864
+ // the only change needed for the endpoint to answer, and a missing one refuses only
865
+ // this endpoint through `missingMember`.
866
+ async providers() {
867
+ return ok(await callMember("providers", "providersView"));
868
+ },
869
+ async "provider-key"(body) {
870
+ const providersView = member("provider-key", "providersView");
871
+ if (body.value === null)
872
+ await callMember("provider-key", "clearProviderKey", String(body.provider ?? ""));
873
+ else
874
+ await callMember("provider-key", "setProviderKey", String(body.provider ?? ""), body.value);
875
+ return ok(await providersView());
876
+ },
877
+ async "provider-add"(body) {
878
+ return ok(await callMember("provider-add", "addProvider", body));
879
+ },
880
+ async "provider-models"(body) {
881
+ return ok(await callMember("provider-models", "discoverModels", body));
882
+ },
883
+ async "provider-remove"(body) {
884
+ return ok(await callMember("provider-remove", "removeProvider", String(body.provider ?? "")));
885
+ },
886
+ // A Turn's exact thinking time, from the timed stream in its events; `ms` omitted
887
+ // when the stream carries no reasoning records.
888
+ async thinking(body) {
889
+ const thinkingOf = member("thinking", "thinkingOf");
890
+ const sessionId = String(body.sessionId ?? "");
891
+ const turn = Number(body.turn);
892
+ if (!Number.isFinite(turn) || selfOf(sessionId) === undefined)
893
+ return ok(null);
894
+ await load();
895
+ const ms = await thinkingOf(sessionId, turn);
896
+ return ok(ms === undefined ? {} : { ms });
897
+ },
898
+ async earlier(body) {
899
+ await load();
900
+ return ok(await callMember("earlier", "earlierPage", String(body.sessionId ?? ""), body.cursor ?? undefined));
901
+ },
902
+ async chat(body) {
903
+ await load();
904
+ return ok(await callMember("chat", "chatPage", String(body.botId ?? ""), body.cursor ?? undefined));
905
+ },
906
+ async "bot-new-part"(body) {
907
+ await load();
908
+ const bot = botOf(String(body.botId ?? ""));
909
+ if (bot === undefined)
910
+ return fail("Unknown Bot", "bot/not-found");
911
+ return ok(await callMember("bot-new-part", "newPart", chatOf(bot.id)));
912
+ },
913
+ async "bot-open-part"(body) {
914
+ await load();
915
+ const bot = botOf(String(body.botId ?? ""));
916
+ if (bot === undefined)
917
+ return fail("Unknown Bot", "bot/not-found");
918
+ return ok(await callMember("bot-open-part", "switchPart", bot.id, String(body.sessionId ?? "")));
919
+ },
920
+ async usage(body) {
921
+ await load();
922
+ return ok(await callMember("usage", "usageView", {
923
+ timeZone: typeof body.timeZone === "string" ? body.timeZone.slice(0, 64) : undefined,
924
+ }));
925
+ },
926
+ async folders(body, signal) {
927
+ await load();
928
+ return ok(await callMember("folders", "folderEndpoint", "folders", body, signal));
929
+ },
930
+ async "folder-pick"(body, signal) {
931
+ await load();
932
+ return ok(await callMember("folder-pick", "folderEndpoint", "folder-pick", body, signal));
933
+ },
934
+ async "folder-list"(body, signal) {
935
+ await load();
936
+ return ok(await callMember("folder-list", "folderEndpoint", "folder-list", body, signal));
937
+ },
938
+ async "team-workspace"(body) {
939
+ await load();
940
+ return ok({ team: await callMember("team-workspace", "setTeamWorkspace", String(body.path ?? "")) });
941
+ },
942
+ async "reset-team"() {
943
+ return ok(await callMember("reset-team", "resetTeam"));
944
+ },
945
+ async "private-release"(body) {
946
+ await load();
947
+ await callMember("private-release", "releaseArchive", String(body.id ?? ""));
948
+ return ok(view());
949
+ },
950
+ // The avatar library: photos the user imported once, kept beside the team.
951
+ async "avatar-library"(body) {
952
+ await load();
953
+ return ok(await callMember("avatar-library", "avatarLibraryView", { offset: body.offset, limit: body.limit }));
954
+ },
955
+ async "avatar-import"(body) {
956
+ await load();
957
+ return ok(await callMember("avatar-import", "avatarLibraryImport", Array.isArray(body.items) ? body.items : []));
958
+ },
959
+ async "avatar-delete"(body) {
960
+ await load();
961
+ return ok(await callMember("avatar-delete", "avatarLibraryDelete", String(body.id ?? "")));
962
+ },
963
+ // The team's one DSH Agent.
964
+ async "add-agent"() {
965
+ await load();
966
+ return ok(await callMember("add-agent", "addAgent"));
967
+ },
968
+ async "agent-session"(body) {
969
+ await load();
970
+ return ok({
971
+ sessionId: await callMember("agent-session", "createAgentSession", body.agentId, body.workspaceId),
972
+ });
973
+ },
974
+ async "agent-adopt"(body) {
975
+ await load();
976
+ return ok({
977
+ sessionId: await callMember("agent-adopt", "adoptAgentSession", body.agentId, body.sessionId),
978
+ });
979
+ },
980
+ async "agent-archive"(body) {
981
+ await load();
982
+ await callMember("agent-archive", "archiveAgentSession", body.agentId, body.sessionId);
983
+ return ok(true);
984
+ },
985
+ async "agent-rename"(body) {
986
+ await load();
987
+ await callMember("agent-rename", "renameAgentSession", body.agentId, body.sessionId, body.title);
988
+ return ok(true);
989
+ },
990
+ async "remove-agent"(body) {
991
+ await load();
992
+ await callMember("remove-agent", "removeAgent", body.agentId);
993
+ return ok(view());
994
+ },
995
+ async "approval-answer"(body) {
996
+ member("approval-answer", "answerApproval")(body.id, body.choice);
997
+ return ok(true);
998
+ },
999
+ async "goal-draft"(body) {
1000
+ await load();
1001
+ const result = await callMember("goal-draft", "goalDraft", String(body.roomId ?? ""), body.text);
1002
+ return result?.error !== undefined ? fail(result.error, result.code ?? "bot/refused") : ok(view());
1003
+ },
1004
+ async "room-budget"(body) {
1005
+ const roomId = String(body.roomId ?? "");
1006
+ const changing = body.clear === true ||
1007
+ (body.members !== undefined && body.members !== null) ||
1008
+ (body.summary !== undefined && body.summary !== null);
1009
+ if (changing)
1010
+ await callMember("room-budget", "setGroupBudget", roomId, body);
1011
+ const view = await callMember("room-budget", "groupBudgetView", roomId);
1012
+ if (view === undefined)
1013
+ return fail("No such group chat", "bot/not-found");
1014
+ return ok(view);
1015
+ },
1016
+ async "goal-control"(body) {
1017
+ await load();
1018
+ const result = await callMember("goal-control", "goalControl", String(body.roomId ?? ""), body);
1019
+ return result?.error !== undefined ? fail(result.error, result.code ?? "bot/refused") : ok(view());
1020
+ },
1021
+ async "view-reply"(body) {
1022
+ if (typeof body.sessionId !== "string")
1023
+ return fail("Nothing to send", "bot/invalid");
1024
+ try {
1025
+ await answerView(body.sessionId, body.viewId, body.action, body.values);
1026
+ }
1027
+ catch (error) {
1028
+ // The endpoint's own codes survive: a missing module is unavailable, a refused
1029
+ // queue is refused; anything else is the caller's payload.
1030
+ return fail(String(error?.message ?? error), errorCode(error));
1031
+ }
1032
+ return ok(true);
1033
+ },
1034
+ async "btw-delete"(body) {
1035
+ if (typeof body.sessionId !== "string" ||
1036
+ body.sessionId === "" ||
1037
+ typeof body.cardId !== "string" ||
1038
+ body.cardId === "") {
1039
+ return fail("Nothing to delete", "bot/invalid");
1040
+ }
1041
+ await load();
1042
+ return ok(await callMember("btw-delete", "btwDelete", { sessionId: body.sessionId, cardId: body.cardId }));
1043
+ },
1044
+ // Side chats: each question is an aside on a fresh snapshot of this Bot's private
1045
+ // chat; Nothing is written into the Session.
1046
+ async "side-new"(body) {
1047
+ if (typeof body.sessionId !== "string" || body.sessionId === "")
1048
+ return fail("Nothing to open", "bot/invalid");
1049
+ await load();
1050
+ return ok(await callMember("side-new", "sideNew", { sessionId: body.sessionId }));
1051
+ },
1052
+ async "side-ask"(body) {
1053
+ if (typeof body.sessionId !== "string" || body.sessionId === "")
1054
+ return fail("Nothing to ask", "bot/invalid");
1055
+ if (body.chatId !== undefined && typeof body.chatId !== "string")
1056
+ return fail("Nothing to ask", "bot/invalid");
1057
+ if (String(body.text ?? "").trim() === "")
1058
+ return fail("Write a question.", "bot/invalid");
1059
+ await load();
1060
+ return ok(await callMember("side-ask", "sideAsk", {
1061
+ sessionId: body.sessionId,
1062
+ chatId: body.chatId,
1063
+ recent: body.recent === true,
1064
+ text: String(body.text),
1065
+ }));
1066
+ },
1067
+ async "side-cancel"(body) {
1068
+ if (typeof body.chatId !== "string" ||
1069
+ body.chatId === "" ||
1070
+ typeof body.entryId !== "string" ||
1071
+ body.entryId === "") {
1072
+ return fail("Nothing to cancel", "bot/invalid");
1073
+ }
1074
+ await load();
1075
+ return ok(await callMember("side-cancel", "sideCancel", { chatId: body.chatId, entryId: body.entryId }));
1076
+ },
1077
+ async "side-close"(body) {
1078
+ if (typeof body.chatId !== "string" || body.chatId === "")
1079
+ return fail("Nothing to close", "bot/invalid");
1080
+ await load();
1081
+ return ok(await callMember("side-close", "sideClose", { chatId: body.chatId }));
1082
+ },
1083
+ async "learning-answer"(body) {
1084
+ await load();
1085
+ return ok(await callMember("learning-answer", "learningEndpoint", "learning-answer", body));
1086
+ },
1087
+ async "update-check"(body) {
1088
+ const fresh = body.fresh === true;
1089
+ const updateCheck = member("update-check", "updateCheck");
1090
+ const dshUpdateCheck = fn("dshUpdateCheck");
1091
+ return ok({
1092
+ ...(await updateCheck({ fresh })),
1093
+ dsh: (dshUpdateCheck === undefined ? undefined : await dshUpdateCheck({ fresh })) ?? null,
1094
+ });
1095
+ },
1096
+ async "update-install"() {
1097
+ return ok(member("update-install", "updateStart")());
1098
+ },
1099
+ async "dsh-update-install"() {
1100
+ return ok(member("dsh-update-install", "dshUpdateStart")());
1101
+ },
1102
+ async "repo-stars"() {
1103
+ return ok(await callMember("repo-stars", "repoStars"));
1104
+ },
1105
+ async "companion-audio"(body) {
1106
+ const botId = String(body?.botId ?? "");
1107
+ const text = String(body?.text ?? "");
1108
+ if (botId === "" || text === "")
1109
+ throw Object.assign(new Error("Nothing to speak"), { code: "bot/invalid" });
1110
+ return ok(await callMember("companion-audio", "companionAudio", botId, text));
1111
+ },
1112
+ async "companion-view"() {
1113
+ return ok(await callMember("companion-view", "companionView"));
1114
+ },
1115
+ async "companion-set"(body) {
1116
+ const botId = String(body?.botId ?? "");
1117
+ if (botId === "")
1118
+ throw Object.assign(new Error("Choose a Bot first"), { code: "bot/invalid" });
1119
+ const patch = body?.patch !== null &&
1120
+ typeof body?.patch === "object"
1121
+ ? body.patch
1122
+ : {};
1123
+ return ok(await callMember("companion-set", "companionSet", botId, patch));
1124
+ },
1125
+ async "tts-settings"() {
1126
+ return ok(await callMember("tts-settings", "ttsSettingsView"));
1127
+ },
1128
+ async "tts-set"(body) {
1129
+ const patch = body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
1130
+ return ok(await callMember("tts-set", "ttsSettingsSet", patch));
1131
+ },
1132
+ async "tts-voices"() {
1133
+ return ok(await callMember("tts-voices", "ttsVoices"));
1134
+ },
1135
+ async "tts-test"() {
1136
+ return ok(await callMember("tts-test", "ttsTest"));
1137
+ },
1138
+ async diagnostics(body) {
1139
+ const service = diagnosticsService("diagnostics", "report");
1140
+ await load();
1141
+ return ok(await service.report(body));
1142
+ },
1143
+ async "diagnostics-log"() {
1144
+ return ok(await diagnosticsService("diagnostics-log", "logText").logText());
1145
+ },
1146
+ async "diagnostics-clear"() {
1147
+ await diagnosticsService("diagnostics-clear", "clear").clear();
1148
+ return ok(true);
1149
+ },
1150
+ // --- IM wiring ---------------------------------------------------------
1151
+ };
1152
+ // Groups whose whole family is one pass-through member. The endpoint name and the body
1153
+ for (const [endpoint, memberName] of [
1154
+ ...SECRET_ENDPOINTS.map((name) => [name, "secretEndpoint"]),
1155
+ ...CONNECTOR_MAIL_ENDPOINTS.map((name) => [name, "connectorEndpoint"]),
1156
+ ...CAPABILITY_ENDPOINTS.map((name) => [name, "capabilityEndpoint"]),
1157
+ ...SCHEDULE_ENDPOINTS.map((name) => [name, "scheduleEndpoint"]),
1158
+ ...MEMORY_ENDPOINTS.map((name) => [name, "memoryEndpoint"]),
1159
+ ]) {
1160
+ endpoints[endpoint] = async (body) => {
1161
+ await load();
1162
+ return ok(await callMember(endpoint, memberName, endpoint, body));
1163
+ };
1164
+ }
1165
+ for (const endpoint of IM_ENDPOINTS) {
1166
+ endpoints[endpoint] = async (body) => {
1167
+ await load();
1168
+ return ok(await member(endpoint, "imEndpoint")(endpoint, body));
1169
+ };
1170
+ }
1171
+ const handle = async (endpoint, payload, signal) => {
1172
+ try {
1173
+ const body = (payload ?? {});
1174
+ await record("refresh");
1175
+ const readOnly = sync("readOnly");
1176
+ // A reading host keeps the read surface; every write endpoint names the other host.
1177
+ if (readOnly !== undefined && !READS.has(endpoint))
1178
+ return fail(readOnly, "bot/read-only");
1179
+ const handler = endpoints[endpoint];
1180
+ if (handler !== undefined)
1181
+ return await handler(body, signal);
1182
+ const module = UNAVAILABLE_ENDPOINTS.get(endpoint);
1183
+ if (module !== undefined) {
1184
+ return fail(`${endpoint}: the ${module} module is not installed in this build.`, "bot/unavailable");
1185
+ }
1186
+ return fail(`Unknown endpoint ${endpoint}`, "bot/not-found");
1187
+ }
1188
+ catch (error) {
1189
+ const code = errorCode(error);
1190
+ const message = error instanceof Error ? error.message : String(error);
1191
+ if (code === "bot/internal") {
1192
+ const failure = error;
1193
+ sync("noteFailure", {
1194
+ source: "api",
1195
+ text: `${endpoint}: ${message}`,
1196
+ code: failure.code ?? error?.name,
1197
+ stack: failure.stack,
1198
+ });
1199
+ }
1200
+ return fail(message, code);
1201
+ }
1202
+ };
1203
+ // The buffered route on the shared /api channel; the browser authenticates with its
1204
+ // session cookie and always reads HTTP 200, the envelope says what happened.
1205
+ const route = {
1206
+ path: ROUTE,
1207
+ methods: ["POST"],
1208
+ requestBody: "buffered",
1209
+ async fetch(request) {
1210
+ let body = {};
1211
+ try {
1212
+ body = (await request.json());
1213
+ }
1214
+ catch {
1215
+ body = {};
1216
+ }
1217
+ const result = await handle(String(body?.endpoint ?? ""), body?.payload, request.signal);
1218
+ return new Response(JSON.stringify(result), {
1219
+ status: 200,
1220
+ headers: { "content-type": "application/json" },
1221
+ });
1222
+ },
1223
+ };
1224
+ ctx.effect(() => {
1225
+ const connection = ctx.connection ?? ctx.get?.("connection");
1226
+ if (connection === undefined || typeof connection.fetch?.register !== "function") {
1227
+ rt.warn("ocean-crew: /api/bot not registered: the connection service is missing");
1228
+ return undefined;
1229
+ }
1230
+ return connection.fetch.register(route);
1231
+ }, "ocean-crew: api route");
1232
+ ctx.effect(() => {
1233
+ // Registered after the route, so this disposer runs before the state lock is released.
1234
+ const wanted = fn("load");
1235
+ if (wanted !== undefined) {
1236
+ void wanted().catch((error) => rt.warn("ocean-crew: state not loaded: %s", error));
1237
+ }
1238
+ return undefined;
1239
+ }, "ocean-crew: load state");
1240
+ Object.assign(rt, { sendUser, answerQuestion, answerView, uiLanguage });
1241
+ }