openmausbot 0.1.93 → 0.1.94

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 (140) hide show
  1. package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-BBdBuCgB.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-BkYwilkG.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-bq1PTDTW.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +21 -0
  63. package/dist-server/container-mcp.js +93 -76
  64. package/dist-server/drivers/agents-proxy.js +38 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +16942 -13191
  68. package/dist-server/local-computer.js +1 -0
  69. package/dist-server/mcp-gate.js +147 -19
  70. package/dist-server/mcp-remote-proxy.js +437 -0
  71. package/dist-server/openmausbot.js +8233 -7651
  72. package/dist-server/pair-cli.js +8233 -7651
  73. package/dist-server/permission-proxy.js +65 -1
  74. package/dist-server/proxy-paths.js +1 -0
  75. package/dist-server/server/auto-approve.js +33 -11
  76. package/dist-server/server/browser-live.js +14 -5
  77. package/dist-server/server/browser-runtime.js +6 -0
  78. package/dist-server/server/channel-queue.js +4 -1
  79. package/dist-server/server/cli.js +32 -12
  80. package/dist-server/server/cloud-move-http.js +9 -3
  81. package/dist-server/server/cloud-move.js +18 -0
  82. package/dist-server/server/computer-wait.js +33 -13
  83. package/dist-server/server/config.js +44 -10
  84. package/dist-server/server/decision-log.js +9 -6
  85. package/dist-server/server/delegations.js +47 -22
  86. package/dist-server/server/drivers/acp/core.js +57 -8
  87. package/dist-server/server/drivers/acp/grok.js +208 -3
  88. package/dist-server/server/drivers/agents-call.js +28 -0
  89. package/dist-server/server/drivers/agents-catalog.js +10 -1
  90. package/dist-server/server/drivers/boatagent.js +2 -0
  91. package/dist-server/server/drivers/chat-mcp-tools.js +27 -14
  92. package/dist-server/server/drivers/claude.js +18 -6
  93. package/dist-server/server/drivers/codex.js +84 -10
  94. package/dist-server/server/drivers/openai-chat.js +11 -3
  95. package/dist-server/server/drivers/pi.js +48 -4
  96. package/dist-server/server/environment.js +3 -0
  97. package/dist-server/server/guarded-requests.js +2 -1
  98. package/dist-server/server/index.js +1143 -254
  99. package/dist-server/server/live-call-controller.js +868 -0
  100. package/dist-server/server/live-call.js +216 -0
  101. package/dist-server/server/mcp-gate-config.js +43 -12
  102. package/dist-server/server/mcp-gate.js +127 -25
  103. package/dist-server/server/mcp-http.js +46 -8
  104. package/dist-server/server/mcp-remote-proxy.js +108 -0
  105. package/dist-server/server/new-bot-defaults.js +3 -0
  106. package/dist-server/server/peer-provenance.js +4 -2
  107. package/dist-server/server/permission-proxy.js +15 -1
  108. package/dist-server/server/plan-usage.js +797 -0
  109. package/dist-server/server/proxy-paths.js +1 -0
  110. package/dist-server/server/redact.js +2 -0
  111. package/dist-server/server/request-auth.js +3 -2
  112. package/dist-server/server/routes/live.js +98 -0
  113. package/dist-server/server/skill-library.js +207 -0
  114. package/dist-server/server/skills-library-migration.js +235 -0
  115. package/dist-server/server/skills.js +66 -3
  116. package/dist-server/server/steer-queue.js +13 -1
  117. package/dist-server/server/store.js +32 -4
  118. package/dist-server/server/surface.js +9 -3
  119. package/dist-server/server/team-backup.js +2 -1
  120. package/dist-server/server/thread-events.js +1 -1
  121. package/dist-server/server/tunnel.js +20 -1
  122. package/dist-server/server/turn-log.js +10 -0
  123. package/dist-server/server/turn-resources.js +124 -0
  124. package/dist-server/shared/ask-question.js +13 -0
  125. package/dist-server/shared/call-consent.js +28 -0
  126. package/dist-server/shared/live-approval.js +124 -0
  127. package/dist-server/shared/live-call.js +176 -0
  128. package/dist-server/shared/skill-md.js +10 -0
  129. package/dist-server/shared/team-backup.js +3 -0
  130. package/dist-server/shared/tool-scope-support.js +25 -0
  131. package/dist-server/shared/tool-scope.js +136 -0
  132. package/dist-server/src/lib/thread-refs.js +406 -0
  133. package/dist-server/vps-container-mcp.js +93 -76
  134. package/dist-server/workspace-backup.worker.js +89 -100
  135. package/enterprise/server/index.js +1 -0
  136. package/package.json +1 -1
  137. package/dist/assets/channel-BU94VpxE.js +0 -1
  138. package/dist/assets/index-Biji-bfL.css +0 -1
  139. package/dist/assets/index-DZlhoES0.js +0 -597
  140. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
@@ -0,0 +1,406 @@
1
+ // "#QA PR 245" in prose becomes a link to that thread. Bots are asked to
2
+ // write a thread's title as #Title when they mention one, and the person
3
+ // may type the same. Resolution mirrors @mention resolution for bots:
4
+ // word-start, longest known title first, case-insensitive — titles hold
5
+ // spaces, so the longest known title following the "#" wins. Nothing else
6
+ // is a link: "#123" issue numbers, "# Heading" markers (the parser never
7
+ // hands those here) and unknown titles all stay plain text.
8
+ const WORD_CHAR = /[\p{L}\p{N}_]/u;
9
+ /** a "#" glued to one of these is not a word start: C#Title, ', ##x */
10
+ const NOT_WORD_START = /[\p{L}\p{N}_#&]/u;
11
+ /** Spacing a pasted thread token needs on each side: a lead space after
12
+ * any char the resolver refuses as a word start — word chars, but also #
13
+ * and &, so a paste after them cannot glue into ##Title — and a trail
14
+ * space before a plain word char. */
15
+ export function threadTokenSpacing(text, start, end) {
16
+ return {
17
+ lead: start > 0 && NOT_WORD_START.test(text[start - 1] ?? "") ? " " : "",
18
+ trail: end < text.length && WORD_CHAR.test(text[end] ?? "") ? " " : "",
19
+ };
20
+ }
21
+ /** Only a title that is a real name can be linked: "123" would turn every
22
+ * issue number into a thread link. */
23
+ function linkable(title) {
24
+ return title.length > 0 && !/^\d+$/.test(title);
25
+ }
26
+ /** Known titles, longest first, each with every thread that carries it in
27
+ * the caller's (recency) order. */
28
+ function groupTitles(threads) {
29
+ const groups = new Map();
30
+ const seen = new Set();
31
+ for (const thread of threads) {
32
+ const title = thread.title.trim();
33
+ const key = `${thread.botId}/${thread.threadId}`;
34
+ if (!linkable(title) || seen.has(key))
35
+ continue;
36
+ seen.add(key);
37
+ const lower = title.toLowerCase();
38
+ const group = groups.get(lower);
39
+ if (group)
40
+ group.candidates.push({ ...thread, title });
41
+ else
42
+ groups.set(lower, { lower, candidates: [{ ...thread, title }] });
43
+ }
44
+ return [...groups.values()].sort((a, b) => b.lower.length - a.lower.length);
45
+ }
46
+ /** Which of several same-titled threads a mention means: the current
47
+ * bot's, else the most recently active, else the first — flagged so the
48
+ * link can say which bot it landed on. */
49
+ function pick(candidates, currentBotId) {
50
+ const resolved = (thread, ambiguous) => ({
51
+ botId: thread.botId, botName: thread.botName, threadId: thread.threadId, title: thread.title, ambiguous,
52
+ });
53
+ if (candidates.length === 1)
54
+ return resolved(candidates[0], false);
55
+ const own = candidates.filter((thread) => thread.botId === currentBotId);
56
+ if (own.length === 1)
57
+ return resolved(own[0], false);
58
+ const pool = own.length > 1 ? own : candidates;
59
+ const stamps = pool.map((thread) => thread.activeAt);
60
+ if (stamps.every((stamp) => typeof stamp === "number")) {
61
+ const newest = Math.max(...stamps);
62
+ const latest = pool.filter((thread) => thread.activeAt === newest);
63
+ if (latest.length === 1)
64
+ return resolved(latest[0], false);
65
+ }
66
+ return resolved(pool[0], true);
67
+ }
68
+ /** Split `text` into plain runs and linked "#Title" runs. The linked run
69
+ * keeps the person's own spelling ("#qa pr 245" stays as typed). */
70
+ export function resolveThreadRefs(text, threads, currentBotId) {
71
+ const groups = groupTitles(threads);
72
+ if (!groups.length || !text.includes("#"))
73
+ return [{ text }];
74
+ const spans = [];
75
+ let last = 0;
76
+ let at = text.indexOf("#");
77
+ while (at !== -1) {
78
+ const before = at > 0 ? text[at - 1] : "";
79
+ const start = at + 1;
80
+ let matched = null;
81
+ if (!before || !NOT_WORD_START.test(before)) {
82
+ for (const group of groups) {
83
+ const end = start + group.lower.length;
84
+ if (text.slice(start, end).toLowerCase() !== group.lower)
85
+ continue;
86
+ const after = text[end] ?? "";
87
+ if (after && WORD_CHAR.test(after))
88
+ continue;
89
+ matched = { end, ref: pick(group.candidates, currentBotId) };
90
+ break;
91
+ }
92
+ }
93
+ if (matched) {
94
+ if (at > last)
95
+ spans.push({ text: text.slice(last, at) });
96
+ spans.push({ text: text.slice(at, matched.end), ref: matched.ref });
97
+ last = matched.end;
98
+ at = text.indexOf("#", matched.end);
99
+ }
100
+ else {
101
+ at = text.indexOf("#", at + 1);
102
+ }
103
+ }
104
+ if (last < text.length)
105
+ spans.push({ text: text.slice(last) });
106
+ return spans.length ? spans : [{ text }];
107
+ }
108
+ const THREAD_URL_PREFIX = "openmausbot://thread/";
109
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
110
+ const ID_LIMIT = 256;
111
+ const plainId = (value) => {
112
+ // oxlint-disable-next-line no-control-regex -- a thread id never carries control characters
113
+ if (!value || value.length > ID_LIMIT || /[\u0000-\u001f]/.test(value))
114
+ return null;
115
+ return value;
116
+ };
117
+ /** The canonical link for a thread a person can copy. */
118
+ export function threadRefUrl(ref) {
119
+ return THREAD_URL_PREFIX + encodeURIComponent(ref.threadId) + "?bot=" + encodeURIComponent(ref.botId);
120
+ }
121
+ /** Parse a canonical thread link; null for anything else, including
122
+ * near-misses with extra path, extra query or a foreign scheme. */
123
+ export function parseThreadRefUrl(value) {
124
+ const raw = value.trim();
125
+ if (!raw.toLowerCase().startsWith(THREAD_URL_PREFIX))
126
+ return null;
127
+ let url;
128
+ try {
129
+ url = new URL(raw);
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ if (url.username || url.password || url.port || url.hash)
135
+ return null;
136
+ if (url.hostname.toLowerCase() !== "thread")
137
+ return null;
138
+ let threadId;
139
+ try {
140
+ threadId = decodeURIComponent(url.pathname.slice(1));
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ if (!threadId || threadId.includes("/") || url.pathname.slice(1) !== encodeURIComponent(threadId))
146
+ return null;
147
+ const keys = [...url.searchParams.keys()];
148
+ if (keys.some((key) => key !== "bot"))
149
+ return null;
150
+ const bot = url.searchParams.get("bot");
151
+ if (keys.length > 1 || (bot !== null && !bot))
152
+ return null;
153
+ const thread = plainId(threadId);
154
+ const owner = bot === null ? undefined : plainId(bot);
155
+ if (!thread || owner === null)
156
+ return null;
157
+ return owner ? { threadId: thread, botId: owner } : { threadId: thread };
158
+ }
159
+ /** Whether a value is shaped like a thread link at all — even one that
160
+ * fails strict parsing. Markdown must never hand such a URL to the shell
161
+ * as an external link, live or dead. */
162
+ export function looksLikeThreadRefUrl(value) {
163
+ return value.trim().toLowerCase().startsWith(THREAD_URL_PREFIX);
164
+ }
165
+ /** A raw thread id on its own: the UUID form the server mints. */
166
+ export function isThreadUuid(value) {
167
+ return UUID.test(value.trim());
168
+ }
169
+ /** Resolve an address (or bare UUID) against the visible threads. The
170
+ * link's own bot wins when it names one; otherwise the same preference a
171
+ * #Title mention uses: the open bot's thread, then the newest, then a
172
+ * deterministic first — flagged ambiguous so the chip can name the bot. */
173
+ export function resolveThreadRefAddress(threads, address, currentBotId) {
174
+ const candidates = threads.filter((thread) => thread.threadId === address.threadId);
175
+ if (!candidates.length)
176
+ return null;
177
+ const pinned = address.botId ? candidates.filter((thread) => thread.botId === address.botId) : [];
178
+ if (pinned.length === 1)
179
+ return { ...pinned[0], title: pinned[0].title.trim(), ambiguous: false };
180
+ if (pinned.length > 1)
181
+ return pick(pinned, currentBotId);
182
+ // a pinned owner missing from the visible threads is a dead reference:
183
+ // never fall through to another bot's same-id thread
184
+ if (address.botId)
185
+ return null;
186
+ return pick(candidates, currentBotId);
187
+ }
188
+ // ── sent-message serialization ─────────────────────────────────────────
189
+ // A resolvable #Title becomes a markdown link whose URL carries the thread
190
+ // id, so bots and API consumers keep the machine-readable reference. The
191
+ // label keeps the title readable; escapes keep brackets in titles parseable.
192
+ const escapeThreadLabel = (title) => title.replace(/[[\]\\]/g, "\\$&");
193
+ const unescapeThreadLabel = (label) => label.replace(/\\(.)/g, "$1");
194
+ /** The markdown shape a thread reference is sent as. */
195
+ export function threadRefMarkdown(ref) {
196
+ return "[" + escapeThreadLabel(ref.title) + "](" + threadRefUrl(ref) + ")";
197
+ }
198
+ const MARKDOWN_LINK_LABEL = /\[((?:\\.|[^\\\]])*)\]\(/y;
199
+ const INLINE_LINK_SPACE = " \t\n\r";
200
+ /** The CommonMark inline-link tail after "[label](": optional whitespace,
201
+ * an angle-bracket destination (spaces allowed) or a bare destination with
202
+ * balanced parentheses, optional whitespace, an optional quoted title, and
203
+ * the closing ")". Returns the tail length plus the bare destination for
204
+ * resolution, or null when no complete link closes here. */
205
+ function inlineLinkTail(rest, from) {
206
+ let at = from;
207
+ while (at < rest.length && INLINE_LINK_SPACE.includes(rest.charAt(at)))
208
+ at += 1;
209
+ const destStart = at;
210
+ let destination = "";
211
+ if (rest.charAt(at) === "<") {
212
+ const close = rest.indexOf(">", at + 1);
213
+ if (close === -1)
214
+ return null;
215
+ destination = rest.slice(at + 1, close);
216
+ at = close + 1;
217
+ }
218
+ else {
219
+ let depth = 0;
220
+ while (at < rest.length) {
221
+ const ch = rest.charAt(at);
222
+ if (ch === "(")
223
+ depth += 1;
224
+ else if (ch === ")") {
225
+ if (depth === 0)
226
+ break;
227
+ depth -= 1;
228
+ }
229
+ else if (INLINE_LINK_SPACE.includes(ch))
230
+ break;
231
+ at += 1;
232
+ }
233
+ if (depth !== 0)
234
+ return null;
235
+ destination = rest.slice(destStart, at);
236
+ }
237
+ while (at < rest.length && INLINE_LINK_SPACE.includes(rest.charAt(at)))
238
+ at += 1;
239
+ const titleQuote = rest.charAt(at);
240
+ if (titleQuote === '"' || titleQuote === "'") {
241
+ const close = rest.indexOf(titleQuote, at + 1);
242
+ if (close === -1)
243
+ return null;
244
+ at = close + 1;
245
+ }
246
+ else if (titleQuote === "(") {
247
+ let depth = 1;
248
+ at += 1;
249
+ while (at < rest.length && depth > 0) {
250
+ const ch = rest.charAt(at);
251
+ if (ch === "(")
252
+ depth += 1;
253
+ else if (ch === ")")
254
+ depth -= 1;
255
+ at += 1;
256
+ }
257
+ if (depth !== 0)
258
+ return null;
259
+ }
260
+ while (at < rest.length && INLINE_LINK_SPACE.includes(rest.charAt(at)))
261
+ at += 1;
262
+ if (rest.charAt(at) !== ")")
263
+ return null;
264
+ return { tail: at + 1 - from, destination };
265
+ }
266
+ /** Split text into plain runs and the spans resolution must never touch.
267
+ * Every markdown link is kept verbatim — a #Title inside a label would
268
+ * nest links — and so is code: inline backticks and fenced blocks quote
269
+ * their contents, so only plain runs carry #Title mentions. */
270
+ function splitThreadLinkMarkdown(text) {
271
+ if (!text.includes("](") && !text.includes("`"))
272
+ return [{ raw: text }];
273
+ const spans = [];
274
+ let plain = "";
275
+ const flush = () => {
276
+ if (plain)
277
+ spans.push({ raw: plain });
278
+ plain = "";
279
+ };
280
+ let at = 0;
281
+ while (at < text.length) {
282
+ const rest = text.slice(at);
283
+ const guarded = (end) => {
284
+ flush();
285
+ spans.push({ raw: text.slice(at, end), code: true });
286
+ at = end;
287
+ };
288
+ if (rest.startsWith("```")) {
289
+ const close = rest.indexOf("```", 3);
290
+ guarded(close === -1 ? text.length : at + close + 3);
291
+ continue;
292
+ }
293
+ const ticks = /^`+/.exec(rest)?.[0];
294
+ if (ticks) {
295
+ const close = rest.indexOf(ticks, ticks.length);
296
+ guarded(close === -1 ? text.length : at + close + ticks.length);
297
+ continue;
298
+ }
299
+ MARKDOWN_LINK_LABEL.lastIndex = 0;
300
+ const open = MARKDOWN_LINK_LABEL.exec(rest);
301
+ const tail = open && inlineLinkTail(rest, open[0].length);
302
+ if (open && tail) {
303
+ flush();
304
+ const raw = rest.slice(0, open[0].length + tail.tail);
305
+ spans.push({ raw, link: { label: open[1], address: parseThreadRefUrl(tail.destination) } });
306
+ at += raw.length;
307
+ continue;
308
+ }
309
+ plain += text[at];
310
+ at += 1;
311
+ }
312
+ flush();
313
+ return spans.length ? spans : [{ raw: text }];
314
+ }
315
+ /** The text a send carries: resolvable #Title runs become canonical links;
316
+ * existing links and unknown titles pass through untouched. */
317
+ export function serializeThreadRefs(text, threads, currentBotId) {
318
+ if (!groupTitles(threads).length)
319
+ return text;
320
+ return splitThreadLinkMarkdown(text)
321
+ .map((span) => (span.link || span.code) ? span.raw : resolveThreadRefs(span.raw, threads, currentBotId)
322
+ .map((run) => run.ref ? threadRefMarkdown(run.ref) : run.text)
323
+ .join(""))
324
+ .join("");
325
+ }
326
+ export function splitThreadRefsForDisplay(text, threads, currentBotId) {
327
+ const spans = [];
328
+ for (const span of splitThreadLinkMarkdown(text)) {
329
+ if (span.link) {
330
+ const ref = span.link.address ? resolveThreadRefAddress(threads, span.link.address, currentBotId) : null;
331
+ spans.push(ref ? { text: unescapeThreadLabel(span.link.label), ref } : { text: span.raw });
332
+ }
333
+ else if (span.code) {
334
+ spans.push({ text: span.raw });
335
+ }
336
+ else {
337
+ spans.push(...resolveThreadRefs(span.raw, threads, currentBotId));
338
+ }
339
+ }
340
+ return spans.length ? spans : [{ text }];
341
+ }
342
+ /** The whole paste is one thread reference — a canonical link, the markdown
343
+ * link shape messages carry, or a raw UUID — and it resolves to a visible
344
+ * thread: the #Title token the composer holds, plus what it resolved to.
345
+ * null when the paste is not a reference or names no known thread, so an
346
+ * unknown id stays plain text. */
347
+ export function threadTokenFromPaste(pasted, threads, currentBotId) {
348
+ const trimmed = pasted.trim();
349
+ if (!trimmed)
350
+ return null;
351
+ const wrapped = /^\[((?:\\.|[^\\\]])*)\]\((openmausbot:\/\/thread\/[^()\s]*)\)$/.exec(trimmed);
352
+ const address = wrapped
353
+ ? parseThreadRefUrl(wrapped[2])
354
+ : parseThreadRefUrl(trimmed) ?? (isThreadUuid(trimmed) ? { threadId: trimmed } : null);
355
+ if (!address)
356
+ return null;
357
+ const ref = resolveThreadRefAddress(threads, address, currentBotId);
358
+ if (!ref)
359
+ return null;
360
+ // '#Title' is the token of choice, but a numeric or '#' prefixed title
361
+ // can never re-resolve through that spelling — those pastes hold the
362
+ // canonical markdown link instead, which sends and displays the same.
363
+ const title = ref.title.trim();
364
+ const token = linkable(title) && !title.startsWith("#") ? `#${title}` : threadRefMarkdown(ref);
365
+ return { token, ref };
366
+ }
367
+ const OPAQUE = new Set(["link", "linkReference", "image", "imageReference", "definition", "html", "mention"]);
368
+ export const THREAD_REF_NODE = "threadRef";
369
+ function linkNode(text, ref) {
370
+ return {
371
+ type: THREAD_REF_NODE,
372
+ data: {
373
+ hName: "span",
374
+ hProperties: {
375
+ "data-thread-bot": ref.botId,
376
+ "data-thread-bot-name": ref.botName,
377
+ "data-thread-id": ref.threadId,
378
+ "data-thread-title": ref.title,
379
+ "data-thread-ambiguous": ref.ambiguous ? "true" : "false",
380
+ },
381
+ },
382
+ children: [{ type: "text", value: text }],
383
+ };
384
+ }
385
+ /** A unified attacher: `remarkPlugins={[remarkThreadRefs(threads, botId)]}`. */
386
+ export function remarkThreadRefs(threads, currentBotId) {
387
+ return () => (tree) => {
388
+ if (!threads.length)
389
+ return;
390
+ const visit = (node) => {
391
+ if (!node.children || OPAQUE.has(node.type))
392
+ return;
393
+ node.children = node.children.flatMap((child) => {
394
+ if (child.type !== "text" || typeof child.value !== "string") {
395
+ visit(child);
396
+ return [child];
397
+ }
398
+ const spans = resolveThreadRefs(child.value, threads, currentBotId);
399
+ if (!spans.some((span) => span.ref))
400
+ return [child];
401
+ return spans.map((span) => (span.ref ? linkNode(span.text, span.ref) : { type: "text", value: span.text }));
402
+ });
403
+ };
404
+ visit(tree);
405
+ };
406
+ }
@@ -628,7 +628,7 @@ import { promisify } from "node:util";
628
628
 
629
629
  // server/config.ts
630
630
  import { homedir as homedir2 } from "node:os";
631
- import { join as join3 } from "node:path";
631
+ import { join as join2 } from "node:path";
632
632
 
633
633
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
634
634
  var external_exports = {};
@@ -15930,6 +15930,48 @@ function normalizeCronSchedule(value, after = Date.now()) {
15930
15930
  return schedule;
15931
15931
  }
15932
15932
 
15933
+ // shared/tool-scope.ts
15934
+ var SERVER_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
15935
+ var CONTROL_CHARACTER = /[\x00-\x1f\x7f]/;
15936
+ var MAX_SELECTORS = 256;
15937
+ var MAX_SELECTOR_LENGTH = 1024;
15938
+ function validName(name) {
15939
+ return typeof name === "string" && name.trim().length > 0 && name.length <= MAX_SELECTOR_LENGTH && !CONTROL_CHARACTER.test(name) && !name.includes("*");
15940
+ }
15941
+ function selectorIdentity(selector) {
15942
+ if (selector.length > MAX_SELECTOR_LENGTH || CONTROL_CHARACTER.test(selector)) return;
15943
+ if (selector.startsWith("native:")) {
15944
+ const name = selector.slice(7);
15945
+ if (name === "*" || validName(name)) return { kind: "native", name };
15946
+ } else if (selector.startsWith("mcp:")) {
15947
+ const separator = selector.indexOf(":", 4);
15948
+ if (separator < 0) return;
15949
+ const server = selector.slice(4, separator);
15950
+ const name = selector.slice(separator + 1);
15951
+ if (SERVER_NAME.test(server) && (name === "*" || validName(name))) {
15952
+ return { kind: "mcp", server, name };
15953
+ }
15954
+ }
15955
+ }
15956
+ function parseToolScope(value) {
15957
+ if (value === void 0) return { ok: true, scope: void 0 };
15958
+ const invalid = { ok: false, error: "Tool selection must contain only valid allow and deny lists." };
15959
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
15960
+ const prototype = Object.getPrototypeOf(value);
15961
+ if (prototype !== Object.prototype && prototype !== null) return invalid;
15962
+ const scope = {};
15963
+ for (const key of Reflect.ownKeys(value)) {
15964
+ if (key !== "allow" && key !== "deny") return invalid;
15965
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
15966
+ if (!descriptor || !("value" in descriptor)) return invalid;
15967
+ const list = descriptor.value;
15968
+ if (!Array.isArray(list) || list.length > MAX_SELECTORS) return invalid;
15969
+ if (!Array.from(list).every((entry) => typeof entry === "string" && selectorIdentity(entry))) return invalid;
15970
+ scope[key] = [...new Set(list)];
15971
+ }
15972
+ return { ok: true, scope };
15973
+ }
15974
+
15933
15975
  // server/new-bot-defaults.ts
15934
15976
  var strings = (max, length) => external_exports.array(external_exports.string().max(length)).max(max);
15935
15977
  var clock = external_exports.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/);
@@ -15964,6 +16006,10 @@ var botDefaultsProfileSchema = profilePatchSchema.extend({
15964
16006
  browser: external_exports.boolean().optional(),
15965
16007
  browserProfile: external_exports.string().max(100).nullable().optional(),
15966
16008
  mcpServers: strings(100, 100).nullable().optional(),
16009
+ toolScope: external_exports.custom((value) => value !== void 0 && parseToolScope(value).ok, "Invalid tool selection").transform((value) => {
16010
+ const parsed = parseToolScope(value);
16011
+ return parsed.ok ? parsed.scope : value;
16012
+ }).nullable().optional(),
15967
16013
  parkDirectMessages: external_exports.boolean().optional()
15968
16014
  }).strict();
15969
16015
  var scheduleSchema = external_exports.discriminatedUnion("type", [
@@ -16160,76 +16206,6 @@ var remoteMutationSchema = remoteEntrySchema.extend({
16160
16206
  var CLOUD_SEAT_IDLE_STOP_MS = 5 * 6e4;
16161
16207
  var CLOUD_SEAT_SLEEP_RETRY_MAX_MS = 10 * 6e4;
16162
16208
 
16163
- // server/proxy-paths.ts
16164
- import { existsSync as existsSync2 } from "node:fs";
16165
- import { dirname as dirname2, join as join2 } from "node:path";
16166
- import { fileURLToPath } from "node:url";
16167
- var SERVER_ROOT = dirname2(fileURLToPath(import.meta.url));
16168
- function resolveProxy(relative) {
16169
- const source = join2(SERVER_ROOT, `${relative}.ts`);
16170
- return existsSync2(source) ? source : join2(SERVER_ROOT, `${relative}.js`);
16171
- }
16172
- var SPAWNED_PROXIES = {
16173
- browser: resolveProxy("browser-proxy"),
16174
- localComputer: resolveProxy("local-computer-proxy"),
16175
- permission: resolveProxy("permission-proxy"),
16176
- containerMcp: resolveProxy("container-mcp"),
16177
- vpsContainerMcp: resolveProxy("vps-container-mcp"),
16178
- agents: resolveProxy("drivers/agents-proxy"),
16179
- dweb: resolveProxy("drivers/dweb-proxy"),
16180
- connectors: resolveProxy("connector-proxy"),
16181
- mcpGate: resolveProxy("mcp-gate"),
16182
- phone: resolveProxy("drivers/phone-proxy"),
16183
- hook: resolveProxy("hooks/omb-hook"),
16184
- // Loaded by the external `pi` process via `-e`, not by this server — but
16185
- // resolved through the same single source of truth so the packaged layout
16186
- // check can assert it ships.
16187
- piMcpExtension: resolveProxy("drivers/pi-mcp-extension")
16188
- };
16189
-
16190
- // server/enterprise.ts
16191
- var layerSchema = external_exports.object({
16192
- customer: external_exports.string().min(1),
16193
- features: external_exports.array(external_exports.string().min(1)),
16194
- expiresAt: external_exports.string().nullable()
16195
- });
16196
- var DAY_MS = 24 * 60 * 6e4;
16197
-
16198
- // server/sessions.ts
16199
- var PAIRING_CODE_TTL_MS = 5 * 6e4;
16200
- function daysMs(value, fallbackDays) {
16201
- const days = Number(value);
16202
- const ok = Number.isInteger(days) && days >= 1 && days <= 3650;
16203
- return (ok ? days : fallbackDays) * 24 * 60 * 6e4;
16204
- }
16205
- var SESSION_TTL_MS = daysMs(process.env.OMB_SESSION_TTL_DAYS, 30);
16206
- var SESSION_MAX_AGE_MS = daysMs(process.env.OMB_SESSION_MAX_DAYS, 180);
16207
- var SESSION_RENEW_WHEN_LEFT_MS = SESSION_TTL_MS / 2;
16208
- var STREAM_TICKET_TTL_MS = 5 * 6e4;
16209
- var scopeSchema = external_exports.enum(["admin", "client"]);
16210
- var sessionSchema = external_exports.object({
16211
- id: external_exports.string().min(1),
16212
- tokenHash: external_exports.string().length(64),
16213
- label: external_exports.string().max(80),
16214
- scopes: external_exports.array(scopeSchema).min(1),
16215
- createdAt: external_exports.number(),
16216
- lastSeenAt: external_exports.number(),
16217
- expiresAt: external_exports.number(),
16218
- /** Set when the session came from an account sign-in rather than a code. */
16219
- userId: external_exports.string().max(256).optional(),
16220
- email: external_exports.string().max(320).optional(),
16221
- /** Set only by the internal verified-portal issuance path, never by email or pairing input. */
16222
- membershipAuthority: external_exports.literal("portal").optional(),
16223
- /** A browser sign-in (PairingCode `browser`): accepted only as its browser's cookie, never as a bearer token. */
16224
- cookieOnly: external_exports.literal(true).optional(),
16225
- /** Whose OMB Cloud a browser sign-in signed in to (its PairingCode `owner`). */
16226
- owner: external_exports.string().max(254).optional()
16227
- });
16228
- var fileSchema = external_exports.object({ version: external_exports.literal(1), sessions: external_exports.array(sessionSchema) });
16229
-
16230
- // server/cloud-home.ts
16231
- var CLOUD_PAIRING_NONCE_MS = 10 * 6e4;
16232
-
16233
16209
  // server/config.ts
16234
16210
  var optionalText = external_exports.string().optional();
16235
16211
  var SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
@@ -16437,7 +16413,11 @@ var featureConfigSchema = external_exports.object({
16437
16413
  * may offer a per-second-billed cloud seat, but only through an explicit
16438
16414
  * consent card or a configured allowlist — never a silent Auto default.
16439
16415
  * Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
16440
- cloudOverflow: external_exports.boolean().optional()
16416
+ cloudOverflow: external_exports.boolean().optional(),
16417
+ /** Opt-in shared skills library: one store at the data dir that bots
16418
+ * read by assignment instead of per-workspace copies. Off until
16419
+ * explicitly enabled — see skillsLibraryEnabled. */
16420
+ skillsLibrary: external_exports.boolean().optional()
16441
16421
  });
16442
16422
  var onboardingConfigSchema = external_exports.object({
16443
16423
  /** ISO timestamp of finishing (or skipping to the end of) the welcome flow. */
@@ -16590,6 +16570,15 @@ var appConfigSchema = external_exports.object({
16590
16570
  baseUrl: external_exports.string().trim().max(2048).refine((value) => !value || /^https?:\/\//i.test(value), "the decision model address must start with http:// or https://").optional(),
16591
16571
  jobs: external_exports.object({ roomRouting: external_exports.boolean().optional() }).optional()
16592
16572
  }).optional(),
16573
+ /** Live calls: an OpenAI project key for GPT-Live, kept apart from every
16574
+ * other OpenAI credential so a Live call never bills an image or engine key
16575
+ * the user did not hand to it. `voice` is a GPT-Live built-in voice name. */
16576
+ live: external_exports.object({
16577
+ key: optionalText,
16578
+ voice: external_exports.string().trim().max(40).regex(/^[a-z]*$/, "a Live voice is a lowercase built-in voice name").optional(),
16579
+ readTypedReplies: external_exports.boolean().optional(),
16580
+ idleMinutes: external_exports.number().int().min(1).max(60).optional()
16581
+ }).optional(),
16593
16582
  /** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
16594
16583
  imageGen: external_exports.object({
16595
16584
  provider: external_exports.enum(["openai", "xai", "custom"]).optional(),
@@ -16659,10 +16648,10 @@ var storedAppConfigSchema = appConfigSchema.extend({
16659
16648
  var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true }).extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
16660
16649
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.json());
16661
16650
  var DEFAULT_CAPTURE_QUIET_MS = 2 * 6e4;
16662
- var DATA_DIR = process.env.OMB_DATA_DIR ?? join3(homedir2(), ".openmausbot");
16663
- var LEGACY_DATA_DIR = join3(homedir2(), ".opengrokbot");
16664
- var EVENTS_DIR = join3(DATA_DIR, "events");
16665
- var NATIVE_DIR = join3(DATA_DIR, "native");
16651
+ var DATA_DIR = process.env.OMB_DATA_DIR ?? join2(homedir2(), ".openmausbot");
16652
+ var LEGACY_DATA_DIR = join2(homedir2(), ".opengrokbot");
16653
+ var EVENTS_DIR = join2(DATA_DIR, "events");
16654
+ var NATIVE_DIR = join2(DATA_DIR, "native");
16666
16655
  var OPENAI_API_INSTANCE = "openai";
16667
16656
  var OPENROUTER_API_INSTANCE = "openrouter";
16668
16657
  var XAI_API_INSTANCE = "xaiApi";
@@ -16692,6 +16681,34 @@ var API_KEY_FLEET = {
16692
16681
  }
16693
16682
  };
16694
16683
 
16684
+ // server/proxy-paths.ts
16685
+ import { existsSync as existsSync2 } from "node:fs";
16686
+ import { dirname as dirname2, join as join3 } from "node:path";
16687
+ import { fileURLToPath } from "node:url";
16688
+ var SERVER_ROOT = dirname2(fileURLToPath(import.meta.url));
16689
+ function resolveProxy(relative) {
16690
+ const source = join3(SERVER_ROOT, `${relative}.ts`);
16691
+ return existsSync2(source) ? source : join3(SERVER_ROOT, `${relative}.js`);
16692
+ }
16693
+ var SPAWNED_PROXIES = {
16694
+ browser: resolveProxy("browser-proxy"),
16695
+ localComputer: resolveProxy("local-computer-proxy"),
16696
+ permission: resolveProxy("permission-proxy"),
16697
+ containerMcp: resolveProxy("container-mcp"),
16698
+ vpsContainerMcp: resolveProxy("vps-container-mcp"),
16699
+ agents: resolveProxy("drivers/agents-proxy"),
16700
+ dweb: resolveProxy("drivers/dweb-proxy"),
16701
+ connectors: resolveProxy("connector-proxy"),
16702
+ mcpGate: resolveProxy("mcp-gate"),
16703
+ mcpRemote: resolveProxy("mcp-remote-proxy"),
16704
+ phone: resolveProxy("drivers/phone-proxy"),
16705
+ hook: resolveProxy("hooks/omb-hook"),
16706
+ // Loaded by the external `pi` process via `-e`, not by this server — but
16707
+ // resolved through the same single source of truth so the packaged layout
16708
+ // check can assert it ships.
16709
+ piMcpExtension: resolveProxy("drivers/pi-mcp-extension")
16710
+ };
16711
+
16695
16712
  // server/container-computer.ts
16696
16713
  var run = promisify(execFile2);
16697
16714
  var CUA_DRIVER_VERSION = "0.20.0";