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.
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +18 -3
- package/dist-server/container-mcp.js +84 -32
- package/dist-server/drivers/agents-proxy.js +11 -0
- package/dist-server/index.js +25183 -23580
- package/dist-server/openmausbot.js +313 -159
- package/dist-server/pair-cli.js +313 -159
- package/dist-server/server/boat.js +66 -21
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/cloud-home-start.js +23 -5
- package/dist-server/server/cloud-home.js +25 -2
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/config.js +42 -4
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/drivers/agents-catalog.js +16 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/claude.js +1 -0
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +438 -102
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/store.js +17 -2
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/vps-container-mcp.js +84 -32
- package/dist-server/workspace-backup.worker.js +113 -31
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- 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
|
|
10
|
-
// slug after a slash or a provider dot. A longer suffix such
|
|
11
|
-
// `claude-
|
|
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
|
|
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
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
45
|
-
const entry = this.computers.get(id);
|
|
46
|
-
if (!entry ||
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
155
|
+
this.authorize(id, session, secret);
|
|
77
156
|
entry.seen = Date.now();
|
|
78
157
|
return next();
|
|
79
158
|
}
|
|
80
|
-
liveJob(id,
|
|
81
|
-
const entry = this.authorize(id,
|
|
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,
|
|
87
|
-
const entry = this.authorize(id,
|
|
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,
|
|
94
|
-
const entry = this.authorize(id,
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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" }
|
|
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) }
|
|
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
|
|
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" &&
|
|
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
98
|
-
if (!
|
|
104
|
+
const credential = elevenLabs(cfg);
|
|
105
|
+
if (!credential)
|
|
99
106
|
return [];
|
|
100
|
-
return elevenlabs.listVoices(
|
|
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
|
|
146
|
-
if (!
|
|
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,
|
|
158
|
+
return elevenlabs.synthesize(text, voice, credential.token, credential.api);
|
|
152
159
|
}
|