openmausbot 0.1.90 → 0.1.91

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 (112) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +18 -3
  61. package/dist-server/container-mcp.js +84 -32
  62. package/dist-server/drivers/agents-proxy.js +11 -0
  63. package/dist-server/index.js +25183 -23580
  64. package/dist-server/openmausbot.js +313 -159
  65. package/dist-server/pair-cli.js +313 -159
  66. package/dist-server/server/boat.js +66 -21
  67. package/dist-server/server/bot-overview.js +7 -4
  68. package/dist-server/server/cloud-home-start.js +23 -5
  69. package/dist-server/server/cloud-home.js +25 -2
  70. package/dist-server/server/cloud-lending.js +103 -0
  71. package/dist-server/server/cloud-move-http.js +268 -0
  72. package/dist-server/server/cloud-move.js +418 -0
  73. package/dist-server/server/config.js +42 -4
  74. package/dist-server/server/decider/index.js +238 -0
  75. package/dist-server/server/decider/jev.js +183 -0
  76. package/dist-server/server/decider/log.js +62 -0
  77. package/dist-server/server/decider/relay.js +52 -0
  78. package/dist-server/server/decider/room-routing.js +88 -0
  79. package/dist-server/server/decider/types.js +5 -0
  80. package/dist-server/server/drivers/agents-catalog.js +16 -0
  81. package/dist-server/server/drivers/boatagent.js +20 -12
  82. package/dist-server/server/drivers/claude.js +1 -0
  83. package/dist-server/server/group-goal-run.js +6 -4
  84. package/dist-server/server/harness-capabilities.js +2 -0
  85. package/dist-server/server/included-services.js +70 -0
  86. package/dist-server/server/index.js +438 -102
  87. package/dist-server/server/model-context-window.js +4 -4
  88. package/dist-server/server/package-export.js +17 -10
  89. package/dist-server/server/package-import.js +6 -3
  90. package/dist-server/server/package-parts.js +3 -1
  91. package/dist-server/server/request-auth.js +2 -0
  92. package/dist-server/server/routes/decider.js +19 -0
  93. package/dist-server/server/shared-computers.js +116 -30
  94. package/dist-server/server/store.js +17 -2
  95. package/dist-server/server/surface.js +13 -0
  96. package/dist-server/server/system-prompt.js +11 -0
  97. package/dist-server/server/team-backup.js +6 -2
  98. package/dist-server/server/team-manifest.js +11 -3
  99. package/dist-server/server/tts/elevenlabs.js +11 -6
  100. package/dist-server/server/tts/index.js +17 -10
  101. package/dist-server/server/workspace-backup-policy.js +22 -3
  102. package/dist-server/server/workspace-backup.js +25 -8
  103. package/dist-server/shared/cloud-home.js +4 -0
  104. package/dist-server/shared/package-format.js +30 -4
  105. package/dist-server/shared/team-backup.js +20 -3
  106. package/dist-server/vps-container-mcp.js +84 -32
  107. package/dist-server/workspace-backup.worker.js +113 -31
  108. package/package.json +1 -1
  109. package/dist/assets/channel-BCuyHjFV.js +0 -1
  110. package/dist/assets/index-CRvo-DS2.css +0 -1
  111. package/dist/assets/index-CWYGlA63.js +0 -588
  112. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -6,10 +6,10 @@ export function modelContextWindow(model) {
6
6
  if (!model)
7
7
  return undefined;
8
8
  const id = model.toLowerCase();
9
- // Opus 5.5 is 1M without a `[1m]` suffix. Only the exact slug, or that
10
- // slug after a slash or a provider dot. A longer suffix such as
11
- // `claude-opus-5-5-local` stays on the generic Claude rule, as does `host::`.
12
- if (/(?:^|[/.])claude-opus-5(?:-5|\.5)$/.test(id))
9
+ // Opus 5.5 and Sonnet 5.5 are 1M without a `[1m]` suffix. Only the exact
10
+ // slug, or that slug after a slash or a provider dot. A longer suffix such
11
+ // as `claude-sonnet-5-5-local` stays on the generic Claude rule, as does `host::`.
12
+ if (/(?:^|[/.])claude-(?:opus|sonnet)-5(?:-5|\.5)$/.test(id))
13
13
  return 1_000_000;
14
14
  if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
15
15
  return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
@@ -15,6 +15,21 @@ function portableKey(value, fallback, used) {
15
15
  used.add(key);
16
16
  return key;
17
17
  }
18
+ /** A room's responder in package terms. An Auto room is written as its lead
19
+ * (its fallback, else its first exported member): that is how it behaves on
20
+ * any install without the decision model, and every older app can read it,
21
+ * where a new `auto` kind would make them refuse the whole package. */
22
+ function packageRoomResponder(responder, keys, members) {
23
+ if (responder.kind === "member" && keys.has(responder.botId))
24
+ return { kind: "agent", agent: keys.get(responder.botId) };
25
+ if (responder.kind === "everyone")
26
+ return { kind: "everyone" };
27
+ if (responder.kind === "auto") {
28
+ const lead = responder.fallbackBotId ? keys.get(responder.fallbackBotId) : undefined;
29
+ return { kind: "agent", agent: lead ?? members[0] };
30
+ }
31
+ return { kind: "mentions" };
32
+ }
18
33
  function samePlaybook(a, b) {
19
34
  return a.name === b.name && a.summary === b.summary && a.instructions === b.instructions &&
20
35
  a.triggers.join("\n") === b.triggers.join("\n");
@@ -122,11 +137,7 @@ export function createBotPackageExport(input) {
122
137
  const members = group.memberIds.flatMap((id) => idToKey.has(id) ? [idToKey.get(id)] : []);
123
138
  if (!members.length)
124
139
  continue;
125
- const defaultResponder = group.defaultResponder.kind === "member" && idToKey.has(group.defaultResponder.botId)
126
- ? { kind: "agent", agent: idToKey.get(group.defaultResponder.botId) }
127
- : group.defaultResponder.kind === "everyone"
128
- ? { kind: "everyone" }
129
- : { kind: "mentions" };
140
+ const defaultResponder = packageRoomResponder(group.defaultResponder, idToKey, members);
130
141
  rooms.push({
131
142
  key: portableKey(group.name, `room-${index + 1}`, roomKeys),
132
143
  name: group.name,
@@ -587,11 +598,7 @@ export function createTeamPackageExport(input) {
587
598
  const members = group.memberIds.flatMap((id) => botKeys.has(id) ? [botKeys.get(id)] : []);
588
599
  if (!members.length)
589
600
  continue;
590
- const defaultResponder = group.defaultResponder.kind === "member" && botKeys.has(group.defaultResponder.botId)
591
- ? { kind: "agent", agent: botKeys.get(group.defaultResponder.botId) }
592
- : group.defaultResponder.kind === "everyone"
593
- ? { kind: "everyone" }
594
- : { kind: "mentions" };
601
+ const defaultResponder = packageRoomResponder(group.defaultResponder, botKeys, members);
595
602
  const key = roomKeys.get(group.id);
596
603
  rooms.push({ key, name: group.name, members, ...(group.bulletin ? { bulletin: group.bulletin } : {}), defaultResponder });
597
604
  roomMembers.set(group.id, new Set(members));
@@ -318,9 +318,12 @@ function runImport(source, options, deps) {
318
318
  const ids = room.members.map((key) => botIds.get(key));
319
319
  let created = store.createGroup(room.name, ids, false, section);
320
320
  createdGroups.push(created);
321
- const defaultResponder = room.defaultResponder.kind === "agent"
322
- ? { kind: "member", botId: botIds.get(room.defaultResponder.agent) }
323
- : { kind: room.defaultResponder.kind };
321
+ const responder = room.defaultResponder;
322
+ const defaultResponder = responder.kind === "agent"
323
+ ? { kind: "member", botId: botIds.get(responder.agent) }
324
+ : responder.kind === "auto"
325
+ ? { kind: "auto", ...(responder.agent ? { fallbackBotId: botIds.get(responder.agent) } : {}) }
326
+ : { kind: responder.kind };
324
327
  created = store.patchGroup(created.id, {
325
328
  bulletin: room.bulletin ?? "",
326
329
  defaultResponder,
@@ -53,7 +53,9 @@ export const roomReleaseValues = (room) => ({
53
53
  members: [...room.members].sort(),
54
54
  defaultResponder: room.defaultResponder.kind === "agent"
55
55
  ? { kind: "agent", agent: room.defaultResponder.agent }
56
- : { kind: room.defaultResponder.kind },
56
+ : room.defaultResponder.kind === "auto" && room.defaultResponder.agent
57
+ ? { kind: "auto", agent: room.defaultResponder.agent }
58
+ : { kind: room.defaultResponder.kind },
57
59
  });
58
60
  /** One normal form for a routine's timing on both sides, so an untouched
59
61
  * routine compares equal: absent cap → null, absent overlap → "skip". */
@@ -239,6 +239,8 @@ export const CLIENT_ALLOW = [
239
239
  // with the feature off these paths are as unlisted as any other, so a
240
240
  // client session is refused exactly the way an unknown route refuses it.
241
241
  { methods: ["POST"], path: /^\/api\/shared-computers\/(?:connect|[\w-]+\/(?:poll|lease|result|disconnect))$/, feature: "sharedComputers" },
242
+ // What this person has lent (owner-scoped status, no secrets). Same gate.
243
+ { methods: ["GET"], path: /^\/api\/shared-computers$/, feature: "sharedComputers" },
242
244
  // liveness, identity, the stream
243
245
  { methods: ["GET"], path: /^\/api\/health$/ },
244
246
  { methods: ["GET"], path: /^\/api\/edition$/ },
@@ -0,0 +1,19 @@
1
+ import { PASS } from "./table.js";
2
+ const MAX_KEY_LENGTH = 512;
3
+ export function createDeciderRoutes(deps) {
4
+ return async ({ req, res, path, method, json, readBody }) => {
5
+ if (path !== "/api/decider/test" || method !== "POST")
6
+ return PASS;
7
+ const body = await readBody(req, 8192);
8
+ if (body?.key !== undefined && typeof body.key !== "string")
9
+ return json(res, 400, { error: "key must be a string" });
10
+ const draft = typeof body?.key === "string" ? body.key.trim() : "";
11
+ if (draft.length > MAX_KEY_LENGTH)
12
+ return json(res, 400, { error: "That does not look like an API key." });
13
+ res.setHeader("cache-control", "no-store");
14
+ const result = await deps.decider.testKey(draft ? { key: draft } : {});
15
+ if (result.ok)
16
+ return json(res, 200, { ok: true, latencyMs: result.latencyMs });
17
+ return json(res, 200, { ok: false, reason: result.reason, ...(result.status ? { status: result.status } : {}) });
18
+ };
19
+ }
@@ -15,41 +15,120 @@ export const sharedComputerOperation = z.object({
15
15
  arguments: z.record(z.string(), z.unknown()).optional(),
16
16
  }).strict();
17
17
  const failure = (message, status = 409) => Object.assign(new Error(message), { status });
18
+ /** Bounds memory, not anyone's share: a desktop session holds one entry. */
19
+ const TOTAL = 100;
20
+ /** How long a lent computer that stopped polling (asleep, app closed) stays
21
+ * known, so a status reader can tell "lent but offline" from "not lent". */
22
+ const RETAIN_MS = 14 * 24 * 60 * 60_000;
23
+ const OFFLINE = "This shared computer is offline. Keep its desktop app open; do not substitute files on the server.";
24
+ /** The scopes the desktop registered, checked by the server as well. The
25
+ * desktop's own grant is the authority and checks every operation again;
26
+ * this only means the server never even queues work outside what the desktop
27
+ * said it lends. */
28
+ export function sharedComputerScopeRefusal(registration, operation) {
29
+ switch (operation.action) {
30
+ case "run_command":
31
+ return registration.terminal ? null : "Terminal access is not lent to this server.";
32
+ case "computer_tools":
33
+ case "computer_call":
34
+ return registration.computer ? null : "Apps and screen are not lent to this server.";
35
+ case "list_files":
36
+ case "read_file":
37
+ case "write_file": {
38
+ const lent = registration.folders.find(entry => entry.id === operation.folder_id);
39
+ if (!lent)
40
+ return "This folder has not been shared with this server.";
41
+ return operation.action === "write_file" && !lent.write ? "This folder is read-only." : null;
42
+ }
43
+ }
44
+ }
45
+ /** The person a bot turn acts for, from the harness's own record of which
46
+ * message started it: that request must name this exact turn generation (a
47
+ * continuation of the same request adds its generation), must not have been
48
+ * stopped, and its message must come from a person. Anything else is nobody. */
49
+ export function provenRequestPerson(request, generation, personOf) {
50
+ if (!request?.messageId || request.stopped || !request.generations.has(generation))
51
+ return null;
52
+ return personOf(request.messageId) ?? null;
53
+ }
18
54
  /** In-memory rendezvous only. Authority over local resources stays on the
19
- * desktop. Restart/disconnect NEVER replays an operation with an unknown outcome. */
55
+ * desktop. Restart/disconnect NEVER replays an operation with an unknown outcome.
56
+ *
57
+ * Partitioned by owner: an entry is keyed by the registering session and the
58
+ * desktop's id, so another session can never take over an id, and a bot turn
59
+ * sees only the computers of the person it acts for. A turn that acts for
60
+ * nobody provable (principal null) sees none. */
20
61
  export class SharedComputers {
21
62
  computers = new Map();
22
63
  sessionLive;
23
64
  constructor(sessionLive) { this.sessionLive = sessionLive; }
65
+ static key(session, id) { return `${session}\n${id}`; }
24
66
  register(registration, owner, secret) {
25
67
  if (!/^[a-f0-9]{64}$/.test(secret))
26
68
  throw failure("Invalid computer credential", 400);
27
- const old = this.computers.get(registration.id);
69
+ const key = SharedComputers.key(owner.session, registration.id);
70
+ const old = this.computers.get(key);
28
71
  if (old) {
29
- this.authorize(registration.id, owner, secret);
72
+ this.authorize(registration.id, owner.session, secret);
30
73
  old.registration = registration;
31
74
  old.seen = Date.now();
75
+ return;
76
+ }
77
+ this.reap();
78
+ // A paired desktop runs one grant per server, so a new id from the same
79
+ // session is a new save: its earlier entry is stale and goes. No other
80
+ // session can reach, replace or squat this key.
81
+ for (const [stale, entry] of this.computers) {
82
+ if (entry.owner.session !== owner.session)
83
+ continue;
84
+ this.computers.delete(stale);
85
+ entry.wake?.();
86
+ for (const job of entry.jobs.values())
87
+ this.finish(entry, job, failure("This computer's access changed. An in-flight action may have completed; inspect before retrying."));
32
88
  }
33
- else {
34
- // Reap dead devices before applying the bounded registration limit.
35
- for (const [id, entry] of this.computers)
36
- if (!this.online(entry) && entry.jobs.size === 0)
37
- this.computers.delete(id);
38
- if (this.computers.size >= 20)
39
- throw failure("Too many shared computers");
40
- this.computers.set(registration.id, { registration, owner, secret, seen: Date.now(), jobs: new Map() });
89
+ if (this.computers.size >= TOTAL)
90
+ throw failure("Too many shared computers");
91
+ this.computers.set(key, { registration, owner: { ...owner }, secret, seen: Date.now(), jobs: new Map() });
92
+ }
93
+ /** Forget entries whose session ended or that have been silent too long. */
94
+ reap() {
95
+ for (const [key, entry] of this.computers) {
96
+ if (entry.jobs.size === 0 && (!this.sessionLive(entry.owner.session) || Date.now() - entry.seen > RETAIN_MS))
97
+ this.computers.delete(key);
41
98
  }
42
99
  }
43
- online(entry) { return this.sessionLive(entry.owner) && Date.now() - entry.seen < 40_000; }
44
- authorize(id, owner, secret) {
45
- const entry = this.computers.get(id);
46
- if (!entry || entry.owner !== owner || !this.sessionLive(owner) || !/^[a-f0-9]{64}$/.test(secret) || !timingSafeEqual(Buffer.from(secret), Buffer.from(entry.secret)))
100
+ online(entry) { return this.sessionLive(entry.owner.session) && Date.now() - entry.seen < 40_000; }
101
+ authorize(id, session, secret) {
102
+ const entry = this.computers.get(SharedComputers.key(session, id));
103
+ if (!entry || !this.sessionLive(session) || !/^[a-f0-9]{64}$/.test(secret) || !timingSafeEqual(Buffer.from(secret), Buffer.from(entry.secret)))
47
104
  throw failure("Shared computer is disconnected or not authorized", 403);
48
105
  return entry;
49
106
  }
50
- list() { return [...this.computers.values()].filter(entry => this.online(entry)).map(entry => entry.registration); }
51
- async poll(id, owner, secret) {
52
- const entry = this.authorize(id, owner, secret);
107
+ /** The online computers one person lent. */
108
+ list(principal) {
109
+ if (!principal)
110
+ return [];
111
+ return [...this.computers.values()].filter(entry => entry.owner.person === principal && this.online(entry)).map(entry => entry.registration);
112
+ }
113
+ /** What one person has lent, online or not: the documented status a
114
+ * reader (the Cloud UI, the next step's placement) uses to tell "your Mac
115
+ * is lent and awake" from "lent but asleep" and "not lent". No secret, no
116
+ * session. Resets when this server restarts; a lending desktop registers
117
+ * again within seconds of being online. */
118
+ status(principal) {
119
+ if (!principal)
120
+ return [];
121
+ this.reap();
122
+ return [...this.computers.values()].filter(entry => entry.owner.person === principal).map(entry => ({
123
+ id: entry.registration.id, name: entry.registration.name, online: this.online(entry), busy: entry.jobs.size > 0, lastSeenAt: entry.seen,
124
+ scopes: {
125
+ folders: entry.registration.folders.map(({ id, name, write }) => ({ id, name, write })),
126
+ terminal: entry.registration.terminal, screen: entry.registration.computer,
127
+ },
128
+ }));
129
+ }
130
+ async poll(id, session, secret) {
131
+ const entry = this.authorize(id, session, secret);
53
132
  if (entry.wake)
54
133
  throw failure("A computer poll is already running");
55
134
  entry.seen = Date.now();
@@ -73,34 +152,41 @@ export class SharedComputers {
73
152
  const timer = setTimeout(() => { entry.wake = undefined; resolve(); }, 20_000);
74
153
  entry.wake = () => { clearTimeout(timer); entry.wake = undefined; resolve(); };
75
154
  });
76
- this.authorize(id, owner, secret);
155
+ this.authorize(id, session, secret);
77
156
  entry.seen = Date.now();
78
157
  return next();
79
158
  }
80
- liveJob(id, owner, secret, jobId) {
81
- const entry = this.authorize(id, owner, secret);
159
+ liveJob(id, session, secret, jobId) {
160
+ const entry = this.authorize(id, session, secret);
82
161
  entry.seen = Date.now();
83
162
  const job = entry.jobs.get(jobId);
84
163
  return job?.sent === true && job.active();
85
164
  }
86
- complete(id, owner, secret, jobId, result) {
87
- const entry = this.authorize(id, owner, secret);
165
+ complete(id, session, secret, jobId, result) {
166
+ const entry = this.authorize(id, session, secret);
88
167
  const job = entry.jobs.get(jobId);
89
168
  if (!job || !job.sent)
90
169
  throw failure("This computer operation expired; it will not be replayed");
91
170
  this.finish(entry, job, job.active() ? null : failure("The requesting turn ended"), result);
92
171
  }
93
- disconnect(id, owner, secret) {
94
- const entry = this.authorize(id, owner, secret);
95
- this.computers.delete(id);
172
+ disconnect(id, session, secret) {
173
+ const entry = this.authorize(id, session, secret);
174
+ this.computers.delete(SharedComputers.key(session, id));
96
175
  entry.wake?.();
97
176
  for (const job of entry.jobs.values())
98
177
  this.finish(entry, job, failure("Computer disconnected. An in-flight action may have completed; inspect before retrying."));
99
178
  }
100
- request(operation, active) {
101
- const entry = this.computers.get(operation.computer_id);
102
- if (!entry || !this.online(entry))
103
- return Promise.reject(failure("This shared computer is offline. Keep its desktop app open; do not substitute files on the server."));
179
+ /** One bot operation for the person the turn acts for. Another person's
180
+ * computer answers exactly like one that does not exist. */
181
+ request(operation, principal, active) {
182
+ if (!principal)
183
+ return Promise.reject(failure("This conversation cannot use a shared computer: only a conversation started by the person who shared it can.", 403));
184
+ const entry = [...this.computers.values()].find(candidate => candidate.registration.id === operation.computer_id && candidate.owner.person === principal && this.online(candidate));
185
+ if (!entry)
186
+ return Promise.reject(failure(OFFLINE));
187
+ const refusal = sharedComputerScopeRefusal(entry.registration, operation);
188
+ if (refusal)
189
+ return Promise.reject(failure(refusal, 403));
104
190
  if (entry.jobs.size >= 1)
105
191
  return Promise.reject(failure("This shared computer is busy. Wait for its current action to finish."));
106
192
  if (!active())
@@ -384,7 +384,8 @@ export function mentionedBots(text, peers) {
384
384
  }
385
385
  /** Normalize persisted or API-provided routing. Old rooms did not have this
386
386
  * field; giving them their first member as lead fixes the old silent-send
387
- * behavior without making every prompt fan out to every model. */
387
+ * behavior without making every prompt fan out to every model. A kind this
388
+ * build does not know degrades the same way. */
388
389
  export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
389
390
  if (dm)
390
391
  return { kind: "mentions" };
@@ -394,6 +395,12 @@ export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
394
395
  return { kind: "everyone" };
395
396
  if (candidate.kind === "mentions")
396
397
  return { kind: "mentions" };
398
+ if (candidate.kind === "auto") {
399
+ // A fallback who left the room reverts to "the first member".
400
+ return typeof candidate.fallbackBotId === "string" && memberIds.includes(candidate.fallbackBotId)
401
+ ? { kind: "auto", fallbackBotId: candidate.fallbackBotId }
402
+ : { kind: "auto" };
403
+ }
397
404
  if (candidate.kind === "member" &&
398
405
  typeof candidate.botId === "string" &&
399
406
  memberIds.includes(candidate.botId)) {
@@ -426,6 +433,13 @@ export function roomResponders(text, members, defaultResponder) {
426
433
  const lead = available.find((member) => member.id === defaultResponder.botId);
427
434
  return lead ? [lead] : [];
428
435
  }
436
+ // Auto without a decision (the decision model off, unsure or failing) is
437
+ // lead mode: its fallback member, else the first active member. The
438
+ // decision itself is asked asynchronously by the room turn.
439
+ if (defaultResponder.kind === "auto") {
440
+ const fallback = available.find((member) => member.id === defaultResponder.fallbackBotId) ?? available[0];
441
+ return fallback ? [fallback] : [];
442
+ }
429
443
  return [];
430
444
  }
431
445
  export class Store {
@@ -1465,7 +1479,7 @@ export class Store {
1465
1479
  * (same parent, new text) and becomes the active leaf. `sendId` is the
1466
1480
  * client's identity for this edit, so its instant bubble reconciles onto
1467
1481
  * the canonical message and a network retry cannot fork twice. */
1468
- branchMessage(threadId, sourceId, text, sendId) {
1482
+ branchMessage(threadId, sourceId, text, sendId, sender) {
1469
1483
  const t = this.thread(threadId);
1470
1484
  const source = t.messages.find((m) => m.id === sourceId);
1471
1485
  if (!source)
@@ -1479,6 +1493,7 @@ export class Store {
1479
1493
  parentId: source.parentId ?? null,
1480
1494
  replyToId: source.replyToId,
1481
1495
  ...(sendId ? { sendId } : {}),
1496
+ ...(sender ? { sender } : {}),
1482
1497
  };
1483
1498
  t.messages.push(full);
1484
1499
  t.activeLeafId = full.id;
@@ -86,6 +86,16 @@ export function surfaceLabel(surface) {
86
86
  * lands there. Same words as the panel and the composer chip. */
87
87
  const RESTATE_SENTENCE = " Before your first action on a screen or page in a task, say in one short sentence where you are working, using that same name.";
88
88
  const SURFACE_AUTHORITY = " For browser and computer tasks, use OpenMausBot's mounted browser/computer tools first: inspect the target, perform the action, and verify its result before claiming success. Discover deferred tools by their server/name when needed. Do not substitute the provider's own desktop, a shell-launched browser, or another automation path for the selected OpenMausBot surface. Ordinary code and file tasks may still use their normal tools. Announcing an action is not performing it. A request naming another place does not move these tools: this computer is the user's host, Local VM is an isolated desktop, the cloud computer is remote, and the built-in browser is a separate browser. If the requested place differs from the mounted one, explain the mismatch and ask the user to change the conversation's computer selector; never act on a different computer or describe a host window as a VM.";
89
+ /** A Cloud home offers neither this computer nor a Local VM
90
+ * (server/cloud-home.ts), so its bots are told only about the places it has.
91
+ * Each pair rewrites one phrase of the desktop wording above. */
92
+ const CLOUD_HOME_WORDING = [
93
+ ["this computer is the user's host, Local VM is an isolated desktop, the cloud computer is remote, and the built-in browser is a separate browser",
94
+ "the cloud computer is remote, the built-in browser is a separate browser, and the user's own computer cannot be reached from here"],
95
+ [" or describe a host window as a VM", ""],
96
+ ["select an available Local VM without asking", "select an available cloud computer without asking"],
97
+ [" Never silently replace an explicitly requested VM with the host desktop.", ""],
98
+ ];
89
99
  /** The one paragraph that says where this turn's work happens. Assembled
90
100
  * from what was actually mounted, never from the setting, so the model is
91
101
  * only ever told about tools it can call. */
@@ -122,6 +132,9 @@ export function surfacePrompt(mounted, opts = {}) {
122
132
  text += " No computer or browser tools are mounted yet; select_computer is the way to connect them before screen work, not a reason to claim you already performed it.";
123
133
  if (computer || mounted.browser)
124
134
  text += " For online research, use the selected OpenMausBot browser when a search service is unavailable. A failed tool proves only that this attempt failed, not that every browser is unavailable. Inspect the current page after navigation; report a sign-in page, redirect or error as such. A completed model turn is not proof that the user's task succeeded.";
135
+ if (opts.cloudHome)
136
+ for (const [desktop, cloudHome] of CLOUD_HOME_WORDING)
137
+ text = text.replace(desktop, cloudHome);
125
138
  return text + (opts.note ?? "");
126
139
  }
127
140
  /** Tool names that touch a screen, for engines that report bare names.
@@ -68,6 +68,17 @@ export function computerPrompt(kind) {
68
68
  return "";
69
69
  return COMPUTER_PARAGRAPH[kind] + SIGN_IN_PROMPT;
70
70
  }
71
+ /** Every turn on a Cloud home (server/cloud-home.ts). The bot runs in the
72
+ * cloud, so asked about the person's own computer it says what is true instead
73
+ * of sending them to set up places that cannot exist there. Their Mac is
74
+ * reachable only when they lend it (docs/cloud-pro.md), through the
75
+ * shared-computer tools, so only a turn that has those tools is told to use them. */
76
+ export function cloudHomePrompt(sharedComputerTools) {
77
+ return " You run on the user's OMB Cloud, a server in the cloud, not on their own computer." + (sharedComputerTools
78
+ ? " If they ask for something on their own Mac or PC, check list_shared_computers: a Mac they lend to their Cloud is reachable through shared_computer, within the folders and apps it allows. If none is lent and online, say so in one sentence: they can turn on Let my Cloud use this Mac under Settings → OMB Cloud in the desktop app on that Mac."
79
+ : " You cannot see or use their Mac or PC, its screen or its files from here. If they ask for something on it, say so in one sentence.")
80
+ + " Offer what works here: the built-in browser and cloud computers. Never ask them to set up this computer or a Local VM; neither exists here.";
81
+ }
71
82
  export const COMPOSIO_PROMPT = " The user's connected apps (Gmail, Calendar, Slack, Notion, and the rest) are reachable through the composio tools — find the right one with COMPOSIO_SEARCH_TOOLS, read its arguments with COMPOSIO_GET_TOOL_SCHEMAS, then run it with COMPOSIO_MULTI_EXECUTE_TOOL. Reach for them before telling the user you have no access to a service.";
72
83
  /** The connected-apps paragraph, personalized to the bot's grants (issue
73
84
  * #1737). A bot with no grants record keeps the legacy all-tools sentence;
@@ -94,7 +94,9 @@ export function createTeamBackup(store, routines, name) {
94
94
  key: group.id, name: group.name, section: group.section, dm: Boolean(group.dm) && memberIds.length === 2,
95
95
  bulletin: group.bulletin, memberIds,
96
96
  defaultResponder: group.defaultResponder.kind === "member" && !memberIds.includes(group.defaultResponder.botId)
97
- ? { kind: "mentions" } : group.defaultResponder,
97
+ ? { kind: "mentions" }
98
+ : group.defaultResponder.kind === "auto" && group.defaultResponder.fallbackBotId && !memberIds.includes(group.defaultResponder.fallbackBotId)
99
+ ? { kind: "auto" } : group.defaultResponder,
98
100
  activeTask: group.threadId, tasks: history(group),
99
101
  };
100
102
  });
@@ -226,7 +228,9 @@ export function importTeamBackup(store, routines, input, selection, options = {}
226
228
  groups.push(group);
227
229
  groupIds.set(source.key, group.id);
228
230
  const responder = source.defaultResponder;
229
- store.patchGroup(group.id, { bulletin: source.bulletin, setupCompletedAt: Date.now(), defaultResponder: responder.kind === "member" ? { kind: "member", botId: botIds.get(responder.botId) } : responder });
231
+ store.patchGroup(group.id, { bulletin: source.bulletin, setupCompletedAt: Date.now(), defaultResponder: responder.kind === "member" ? { kind: "member", botId: botIds.get(responder.botId) }
232
+ : responder.kind === "auto" ? { kind: "auto", ...(responder.fallbackBotId ? { fallbackBotId: botIds.get(responder.fallbackBotId) } : {}) }
233
+ : responder });
230
234
  // Use the existing task APIs for rooms; direct-message rooms have one.
231
235
  const threads = source.tasks.map((task, i) => {
232
236
  if (i === 0) {
@@ -25,11 +25,18 @@ const optionalText = (max) => z
25
25
  .transform((value) => value?.trim() || undefined)
26
26
  .refine((value) => value === undefined || value.length <= max, { message: "is too long" })
27
27
  .optional();
28
- const responderSchema = z.discriminatedUnion("kind", [
28
+ const RESPONDER_KINDS = new Set(["member", "everyone", "mentions", "auto"]);
29
+ /** A responder kind from a newer release reads as "no responder": the room
30
+ * is created with the default lead, never refused. */
31
+ const responderSchema = z.preprocess((value) => {
32
+ const kind = value && typeof value === "object" ? value.kind : undefined;
33
+ return typeof kind === "string" && !RESPONDER_KINDS.has(kind) ? { kind: "mentions" } : value;
34
+ }, z.discriminatedUnion("kind", [
29
35
  z.object({ kind: z.literal("member"), member: requiredText(64) }),
30
36
  z.object({ kind: z.literal("everyone") }),
31
37
  z.object({ kind: z.literal("mentions") }),
32
- ]);
38
+ z.object({ kind: z.literal("auto"), member: requiredText(64).optional() }),
39
+ ]));
33
40
  const memberSchema = z.object({
34
41
  key: requiredText(64).regex(/^[a-z0-9][a-z0-9_-]*$/, {
35
42
  message: "may only contain lowercase letters, numbers, - and _",
@@ -117,7 +124,8 @@ export function parseTeamManifest(value) {
117
124
  result.team.description = parsed.data.team.description;
118
125
  if (parsed.data.version === LEGACY_TEAM_MANIFEST_VERSION) {
119
126
  const defaultResponder = parsed.data.team.room.defaultResponder;
120
- if (defaultResponder.kind === "member" && !seenKeys.has(defaultResponder.member)) {
127
+ if ((defaultResponder.kind === "member" || defaultResponder.kind === "auto") && defaultResponder.member !== undefined &&
128
+ !seenKeys.has(defaultResponder.member)) {
121
129
  throw new Error(`Unknown default responder: ${defaultResponder.member}`);
122
130
  }
123
131
  result.team.room = {
@@ -10,7 +10,12 @@
10
10
  // client already splits text into utterances and fetches the next while the
11
11
  // current one plays, which gets the same perceived latency with far fewer
12
12
  // moving parts — and no socket to leak when a turn is interrupted.
13
- const API = process.env.OMB_ELEVENLABS_API || "https://api.elevenlabs.io/v1";
13
+ //
14
+ // The base URL is the caller's: ElevenLabs itself for the person's own key
15
+ // (OMB_ELEVENLABS_API overrides it for dev and tests), Cloud Pro's relay for
16
+ // its included token (included-services.ts). Only a key being saved is
17
+ // verified, and that is always an own key.
18
+ import { elevenLabsProviderApi } from "../included-services.js";
14
19
  const MODEL = "eleven_flash_v2_5";
15
20
  // 64kbps mono is indistinguishable for speech and a third of the bytes
16
21
  const FORMAT = "mp3_44100_64";
@@ -51,7 +56,7 @@ function message(status, what, body) {
51
56
  * picker needs next, so it tests exactly the capability that matters. */
52
57
  export async function verifyKey(key) {
53
58
  try {
54
- const res = await fetch(`${API}/voices`, {
59
+ const res = await fetch(`${elevenLabsProviderApi()}/voices`, {
55
60
  headers: { "xi-api-key": key },
56
61
  signal: AbortSignal.timeout(20_000),
57
62
  });
@@ -63,8 +68,8 @@ export async function verifyKey(key) {
63
68
  return { ok: false, message: "Couldn't reach ElevenLabs to check that key — check your connection." };
64
69
  }
65
70
  }
66
- export async function listVoices(key) {
67
- const res = await fetch(`${API}/voices`, {
71
+ export async function listVoices(key, api) {
72
+ const res = await fetch(`${api}/voices`, {
68
73
  headers: { "xi-api-key": key },
69
74
  signal: AbortSignal.timeout(20_000),
70
75
  });
@@ -79,8 +84,8 @@ export async function listVoices(key) {
79
84
  }))
80
85
  .filter((v) => v.id);
81
86
  }
82
- export async function synthesize(text, voiceId, key) {
83
- const res = await fetch(`${API}/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${FORMAT}`, {
87
+ export async function synthesize(text, voiceId, key, api) {
88
+ const res = await fetch(`${api}/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${FORMAT}`, {
84
89
  method: "POST",
85
90
  headers: { "xi-api-key": key, "content-type": "application/json", accept: "audio/mpeg" },
86
91
  body: JSON.stringify({ text, model_id: MODEL }),
@@ -1,3 +1,4 @@
1
+ import { voiceCredential } from "../included-services.js";
1
2
  import * as chatterbox from "./chatterbox.js";
2
3
  import * as elevenlabs from "./elevenlabs.js";
3
4
  import * as grok from "./grok.js";
@@ -16,6 +17,9 @@ export class NoVoiceConfigured extends Error {
16
17
  this.reason = reason;
17
18
  }
18
19
  }
20
+ /** The ElevenLabs credential in use: the person's own key, else the one
21
+ * included with Cloud Pro. Settings' own-key flows read cfg.tts.key instead. */
22
+ const elevenLabs = (cfg) => voiceCredential(cfg.tts?.key);
19
23
  export function voiceProvider(cfg) {
20
24
  const provider = cfg.tts?.provider;
21
25
  return provider === "xai" || provider === "fish" || provider === "system" || provider === "chatterbox" ? provider : "elevenlabs";
@@ -33,7 +37,7 @@ export function providerConfigured(cfg) {
33
37
  return Boolean(cfg.tts?.baseUrl?.trim());
34
38
  if (provider === "fish")
35
39
  return Boolean(cfg.tts?.fishKey);
36
- return Boolean(cfg.tts?.key);
40
+ return Boolean(elevenLabs(cfg));
37
41
  }
38
42
  export function voiceConfigured(cfg) {
39
43
  const provider = voiceProvider(cfg);
@@ -46,7 +50,7 @@ export function voiceConfigured(cfg) {
46
50
  return Boolean(cfg.tts?.baseUrl?.trim() && cfg.tts?.voice);
47
51
  if (provider === "fish")
48
52
  return Boolean(cfg.tts?.fishKey && cfg.tts?.voice);
49
- return Boolean(cfg.tts?.key && cfg.tts?.voice);
53
+ return Boolean(elevenLabs(cfg) && cfg.tts?.voice);
50
54
  }
51
55
  /** A per-bot voice is a complete choice too; it should not be blocked just
52
56
  * because the app-wide fallback has not been selected yet. */
@@ -61,13 +65,15 @@ export function voiceReady(cfg, voiceId) {
61
65
  return Boolean(cfg.tts?.baseUrl?.trim() && (voiceId || cfg.tts?.voice));
62
66
  if (provider === "fish")
63
67
  return Boolean(cfg.tts?.fishKey && (voiceId || cfg.tts?.voice));
64
- return Boolean(cfg.tts?.key && (voiceId || cfg.tts?.voice));
68
+ return Boolean(elevenLabs(cfg) && (voiceId || cfg.tts?.voice));
65
69
  }
66
70
  /** What the settings panel needs. Never includes the key — same write-only
67
71
  * rule as every other credential. baseUrl and model are Chatterbox
68
- * settings, not credentials, so they come back in full. */
72
+ * settings, not credentials, so they come back in full. `included` says the
73
+ * voice is Cloud Pro's, not a saved key. */
69
74
  export function describeVoice(cfg) {
70
75
  const provider = voiceProvider(cfg);
76
+ const included = provider === "elevenlabs" && elevenLabs(cfg)?.included === true;
71
77
  return {
72
78
  configured: providerConfigured(cfg),
73
79
  ready: voiceConfigured(cfg),
@@ -75,6 +81,7 @@ export function describeVoice(cfg) {
75
81
  provider,
76
82
  baseUrl: provider === "chatterbox" ? (cfg.tts?.baseUrl ?? "") : "",
77
83
  model: provider === "chatterbox" ? (cfg.tts?.model ?? "") : "",
84
+ ...(included ? { included: true } : {}),
78
85
  };
79
86
  }
80
87
  export function verifyKey(provider, key) {
@@ -94,10 +101,10 @@ export async function listVoices(cfg, run) {
94
101
  const key = cfg.tts?.fishKey;
95
102
  return key ? fish.listVoices(key) : [];
96
103
  }
97
- const key = cfg.tts?.key;
98
- if (!key)
104
+ const credential = elevenLabs(cfg);
105
+ if (!credential)
99
106
  return [];
100
- return elevenlabs.listVoices(key);
107
+ return elevenlabs.listVoices(credential.token, credential.api);
101
108
  }
102
109
  /** Synthesize one utterance. Throws NoVoiceConfigured when there is nothing
103
110
  * to speak with, which the route turns into a 409 the client can explain. */
@@ -142,11 +149,11 @@ export function speak(cfg, text, voiceId, run) {
142
149
  throw new NoVoiceConfigured("voice");
143
150
  return fish.synthesize(text, voice, key);
144
151
  }
145
- const key = cfg.tts?.key;
146
- if (!key)
152
+ const credential = elevenLabs(cfg);
153
+ if (!credential)
147
154
  throw new NoVoiceConfigured("key");
148
155
  const voice = voiceId || cfg.tts?.voice;
149
156
  if (!voice)
150
157
  throw new NoVoiceConfigured("voice");
151
- return elevenlabs.synthesize(text, voice, key);
158
+ return elevenlabs.synthesize(text, voice, credential.token, credential.api);
152
159
  }