openmausbot 0.1.86 → 0.1.88
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/README.md +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +19 -2
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { parseBotPackage } from "./bot-package.js";
|
|
2
|
+
import { nextPatchRelease } from "./published-teams.js";
|
|
3
|
+
import { AGENT_MAX_SKILLS, AVATAR_MAX_BYTES, decodeBase64, PACKAGE_FORMAT, PACKAGE_MAX_SKILLS, PACKAGE_VERSION, PackageFormatError, parsePackageDocument, pictureMatchesMime, RELEASE_NOTES_MAX, redactPackageSecrets, SEED_FILE_MAX_BYTES, SEED_MAX_BYTES, SEED_MAX_FILES, utf8Bytes, } from "../shared/package-format.js";
|
|
2
4
|
function portableKey(value, fallback, used) {
|
|
3
5
|
const stem = value
|
|
4
6
|
.normalize("NFKD")
|
|
5
|
-
.replace(/[
|
|
7
|
+
.replace(/[̀-ͯ]/g, "")
|
|
6
8
|
.toLowerCase()
|
|
7
9
|
.replace(/[^a-z0-9]+/g, "-")
|
|
8
10
|
.replace(/^-|-$/g, "")
|
|
@@ -17,23 +19,16 @@ function samePlaybook(a, b) {
|
|
|
17
19
|
return a.name === b.name && a.summary === b.summary && a.instructions === b.instructions &&
|
|
18
20
|
a.triggers.join("\n") === b.triggers.join("\n");
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const packageKeys = new Set();
|
|
28
|
-
const idToKey = new Map();
|
|
29
|
-
for (const [index, bot] of bots.entries()) {
|
|
30
|
-
idToKey.set(bot.id, portableKey(bot.name, `bot-${index + 1}`, packageKeys));
|
|
31
|
-
}
|
|
22
|
+
function sameSkill(a, b) {
|
|
23
|
+
return a.instructions === b.instructions && a.description === b.description &&
|
|
24
|
+
a.source === b.source && a.license === b.license && a.compatibility === b.compatibility;
|
|
25
|
+
}
|
|
26
|
+
/** One playbook definition per distinct content; same-key conflicts get the
|
|
27
|
+
* bot's key as a prefix. Shared by the v1 and v2 exporters. */
|
|
28
|
+
function assignPlaybooks(bots, idToKey) {
|
|
32
29
|
const playbooks = [];
|
|
33
30
|
const playbookKeys = new Set();
|
|
34
31
|
const agentPlaybooks = new Map();
|
|
35
|
-
const packageSkills = new Map();
|
|
36
|
-
const agentSkills = new Map();
|
|
37
32
|
for (const bot of bots) {
|
|
38
33
|
const agentKey = idToKey.get(bot.id);
|
|
39
34
|
const assigned = [];
|
|
@@ -52,21 +47,32 @@ export function createBotPackageExport(input) {
|
|
|
52
47
|
assigned.push(key);
|
|
53
48
|
}
|
|
54
49
|
agentPlaybooks.set(bot.id, assigned);
|
|
50
|
+
}
|
|
51
|
+
return { playbooks, agentPlaybooks };
|
|
52
|
+
}
|
|
53
|
+
/** Skills are keyed by name; the same name with different content refuses. */
|
|
54
|
+
function assignSkills(bots, skillsByBot) {
|
|
55
|
+
const packageSkills = new Map();
|
|
56
|
+
const agentSkills = new Map();
|
|
57
|
+
for (const bot of bots) {
|
|
55
58
|
const assignedSkills = [];
|
|
56
|
-
for (const skill of
|
|
59
|
+
for (const skill of skillsByBot?.get(bot.id) ?? []) {
|
|
57
60
|
const existing = packageSkills.get(skill.name);
|
|
58
|
-
if (existing && (existing
|
|
59
|
-
existing.source !== skill.source || existing.license !== skill.license ||
|
|
60
|
-
existing.compatibility !== skill.compatibility)) {
|
|
61
|
+
if (existing && !sameSkill(existing, skill)) {
|
|
61
62
|
throw new Error(`Skill "${skill.name}" has conflicting content across selected bots`);
|
|
62
63
|
}
|
|
63
|
-
if (!existing)
|
|
64
|
-
|
|
64
|
+
if (!existing) {
|
|
65
|
+
const { enabled: _enabled, ...entry } = skill;
|
|
66
|
+
packageSkills.set(skill.name, entry);
|
|
67
|
+
}
|
|
65
68
|
if (!assignedSkills.includes(skill.name))
|
|
66
69
|
assignedSkills.push(skill.name);
|
|
67
70
|
}
|
|
68
71
|
agentSkills.set(bot.id, assignedSkills);
|
|
69
72
|
}
|
|
73
|
+
return { packageSkills, agentSkills };
|
|
74
|
+
}
|
|
75
|
+
function requiredApps(bots) {
|
|
70
76
|
const requirements = new Map();
|
|
71
77
|
for (const bot of bots) {
|
|
72
78
|
for (const app of bot.installedPackage?.requiredApps ?? []) {
|
|
@@ -74,6 +80,42 @@ export function createBotPackageExport(input) {
|
|
|
74
80
|
requirements.set(app.slug, { ...app });
|
|
75
81
|
}
|
|
76
82
|
}
|
|
83
|
+
return requirements;
|
|
84
|
+
}
|
|
85
|
+
function portableSchedule(schedule) {
|
|
86
|
+
if (schedule.type === "once")
|
|
87
|
+
return { type: "once", at: schedule.at };
|
|
88
|
+
if (schedule.type === "cron")
|
|
89
|
+
return { ...schedule };
|
|
90
|
+
if (schedule.type === "interval") {
|
|
91
|
+
return {
|
|
92
|
+
type: "interval",
|
|
93
|
+
everyMinutes: schedule.everyMinutes,
|
|
94
|
+
anchorAt: schedule.anchorAt,
|
|
95
|
+
...(schedule.weekdays === undefined ? {} : { weekdays: [...schedule.weekdays] }),
|
|
96
|
+
...(schedule.window === undefined ? {} : { window: { ...schedule.window } }),
|
|
97
|
+
...(schedule.endsAt === undefined ? {} : { endsAt: schedule.endsAt }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { type: "daily", time: schedule.time, weekdays: [...schedule.weekdays] };
|
|
101
|
+
}
|
|
102
|
+
/** Export a workspace definition, never its runtime state. Connected-app
|
|
103
|
+
* labels are retained as setup intent, but grants, credentials, approvals,
|
|
104
|
+
* transcripts, memory, paths, engines, and schedules' active state are not.
|
|
105
|
+
* This is the v1 (whole-installation, Markdown) export; one team with
|
|
106
|
+
* everything but its chat history is createTeamPackageExport below. */
|
|
107
|
+
export function createBotPackageExport(input) {
|
|
108
|
+
const bots = input.bots.filter((bot) => !bot.hidden);
|
|
109
|
+
if (!bots.length)
|
|
110
|
+
throw new Error("Create a bot before exporting your package");
|
|
111
|
+
const packageKeys = new Set();
|
|
112
|
+
const idToKey = new Map();
|
|
113
|
+
for (const [index, bot] of bots.entries()) {
|
|
114
|
+
idToKey.set(bot.id, portableKey(bot.name, `bot-${index + 1}`, packageKeys));
|
|
115
|
+
}
|
|
116
|
+
const { playbooks, agentPlaybooks } = assignPlaybooks(bots, idToKey);
|
|
117
|
+
const { packageSkills, agentSkills } = assignSkills(bots, input.skillsByBot);
|
|
118
|
+
const requirements = requiredApps(bots);
|
|
77
119
|
const roomKeys = new Set();
|
|
78
120
|
const rooms = [];
|
|
79
121
|
for (const [index, group] of input.groups.filter((group) => !group.dm).entries()) {
|
|
@@ -109,24 +151,7 @@ export function createBotPackageExport(input) {
|
|
|
109
151
|
agent,
|
|
110
152
|
prompt: routine.prompt,
|
|
111
153
|
runOn: routine.runOn,
|
|
112
|
-
schedule: routine.schedule
|
|
113
|
-
? { type: "once", at: routine.schedule.at }
|
|
114
|
-
: routine.schedule.type === "cron"
|
|
115
|
-
? { ...routine.schedule }
|
|
116
|
-
: routine.schedule.type === "interval"
|
|
117
|
-
? {
|
|
118
|
-
type: "interval",
|
|
119
|
-
everyMinutes: routine.schedule.everyMinutes,
|
|
120
|
-
anchorAt: routine.schedule.anchorAt,
|
|
121
|
-
...(routine.schedule.weekdays === undefined
|
|
122
|
-
? {}
|
|
123
|
-
: { weekdays: [...routine.schedule.weekdays] }),
|
|
124
|
-
...(routine.schedule.window === undefined
|
|
125
|
-
? {}
|
|
126
|
-
: { window: { ...routine.schedule.window } }),
|
|
127
|
-
...(routine.schedule.endsAt === undefined ? {} : { endsAt: routine.schedule.endsAt }),
|
|
128
|
-
}
|
|
129
|
-
: { type: "daily", time: routine.schedule.time, weekdays: [...routine.schedule.weekdays] },
|
|
154
|
+
schedule: portableSchedule(routine.schedule),
|
|
130
155
|
durationMinutes: routine.durationMinutes,
|
|
131
156
|
...(routine.timeoutMinutes === undefined ? {} : { timeoutMinutes: routine.timeoutMinutes }),
|
|
132
157
|
...(routine.overlap === "queue" ? { overlap: "queue" } : {}),
|
|
@@ -191,3 +216,566 @@ export function createBotPackageExport(input) {
|
|
|
191
216
|
package: definition,
|
|
192
217
|
});
|
|
193
218
|
}
|
|
219
|
+
/** A plain sentence the Share dialog shows as it is. */
|
|
220
|
+
export class TeamExportError extends Error {
|
|
221
|
+
status = 400;
|
|
222
|
+
}
|
|
223
|
+
export const TEAM_TOO_LARGE_MESSAGE = "This team is too large to share (4 MB). Leave out pictures, starter notes or some skills and try again.";
|
|
224
|
+
const KEY_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
225
|
+
const CONNECTION_KEY = /^[a-z][a-z0-9_-]{0,27}$/;
|
|
226
|
+
const SEMVER = /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/;
|
|
227
|
+
const DATA_URL = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/]+={0,2})$/;
|
|
228
|
+
const teamOf = (section) => section?.trim() ?? "";
|
|
229
|
+
/** Keys never come from names once they exist: a recorded key first, then
|
|
230
|
+
* the key the bot was installed under from this same package, then a new
|
|
231
|
+
* key from the name. Reserved keys are claimed before any are derived. */
|
|
232
|
+
function stableKeys(records, recorded, installed, fallback) {
|
|
233
|
+
const used = new Set();
|
|
234
|
+
const keys = new Map();
|
|
235
|
+
const claim = (record, key) => {
|
|
236
|
+
if (keys.has(record.id) || !key || key.length > 64 || !KEY_PATTERN.test(key) || used.has(key))
|
|
237
|
+
return;
|
|
238
|
+
keys.set(record.id, key);
|
|
239
|
+
used.add(key);
|
|
240
|
+
};
|
|
241
|
+
for (const record of records)
|
|
242
|
+
claim(record, recorded?.[record.id]);
|
|
243
|
+
for (const record of records)
|
|
244
|
+
claim(record, installed(record));
|
|
245
|
+
// A new key never reuses one recorded for a record that is not here now:
|
|
246
|
+
// an update would otherwise treat a different bot as the one that left.
|
|
247
|
+
const taken = new Set([...used, ...Object.values(recorded ?? {})]);
|
|
248
|
+
for (const [index, record] of records.entries()) {
|
|
249
|
+
if (!keys.has(record.id))
|
|
250
|
+
keys.set(record.id, portableKey(record.name, `${fallback}-${index + 1}`, taken));
|
|
251
|
+
}
|
|
252
|
+
return keys;
|
|
253
|
+
}
|
|
254
|
+
/** A picture the dialog prepared (a data URL), or why it stays out. */
|
|
255
|
+
export function picture(value) {
|
|
256
|
+
const match = typeof value === "string" ? DATA_URL.exec(value) : null;
|
|
257
|
+
if (!match)
|
|
258
|
+
return "picture_invalid";
|
|
259
|
+
const bytes = decodeBase64(match[2]);
|
|
260
|
+
if (!bytes)
|
|
261
|
+
return "picture_invalid";
|
|
262
|
+
if (bytes.length > AVATAR_MAX_BYTES)
|
|
263
|
+
return "picture_too_large";
|
|
264
|
+
if (!pictureMatchesMime(bytes, match[1]))
|
|
265
|
+
return "picture_invalid";
|
|
266
|
+
return { mime: match[1], data: match[2] };
|
|
267
|
+
}
|
|
268
|
+
function remoteServer(raw) {
|
|
269
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
270
|
+
return null;
|
|
271
|
+
const server = raw;
|
|
272
|
+
if (typeof server.url !== "string")
|
|
273
|
+
return typeof server.command === "string" ? "stdio" : null;
|
|
274
|
+
const headers = server.headers && typeof server.headers === "object" && !Array.isArray(server.headers) ? Object.keys(server.headers) : [];
|
|
275
|
+
return { transport: server.type === "sse" ? "sse" : "http", url: server.url, valueNames: headers };
|
|
276
|
+
}
|
|
277
|
+
function sharableAddress(value) {
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(value);
|
|
280
|
+
return url.protocol === "https:" || (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname)));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** A run of token characters long enough to be a key. */
|
|
287
|
+
const TOKEN_RUN = /[A-Za-z0-9_-]{16,}/g;
|
|
288
|
+
/** One word of a readable name: letters with at most a short number after
|
|
289
|
+
* them ("server", "v2", "oauth2"), or a short number alone ("2024"). */
|
|
290
|
+
const NAME_WORD = /^[A-Za-z]*\d{0,4}$/;
|
|
291
|
+
/** Whether one piece of an address (a path segment, a host label, a query or
|
|
292
|
+
* matrix parameter name) holds a key: somewhere in it, a run of 16 or more
|
|
293
|
+
* token characters that mixes letters and digits, or that has 24 or more
|
|
294
|
+
* letters or 24 or more digits in a row. A run that reads as words joined by
|
|
295
|
+
* hyphens or underscores ("github-mcp-server-2024") is a name, not a key.
|
|
296
|
+
* Percent-escapes are decoded first: "path%20with%20spaces" is words, and an
|
|
297
|
+
* escaped key is still a key. Deliberately broad; the sharer sees the
|
|
298
|
+
* address before saving. */
|
|
299
|
+
function keyShaped(piece) {
|
|
300
|
+
let text = piece;
|
|
301
|
+
try {
|
|
302
|
+
text = decodeURIComponent(piece);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// A stray "%" is not an escape; test the text as it is.
|
|
306
|
+
}
|
|
307
|
+
return (text.match(TOKEN_RUN) ?? []).some((run) => {
|
|
308
|
+
if (/[A-Za-z]{24,}|\d{24,}/.test(run))
|
|
309
|
+
return true;
|
|
310
|
+
if (!/[A-Za-z]/.test(run) || !/\d/.test(run))
|
|
311
|
+
return false;
|
|
312
|
+
const words = run.split(/[-_]+/).filter(Boolean);
|
|
313
|
+
return !(words.length > 1 && words.every((word) => NAME_WORD.test(word)));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** What replaces a key-shaped part of an address. */
|
|
317
|
+
export const ADDRESS_KEY_PLACEHOLDER = "redacted";
|
|
318
|
+
/** One path segment as it may travel. A segment with a key-shaped part is
|
|
319
|
+
* replaced whole, so no piece of a key split by ":" or ";" is left behind;
|
|
320
|
+
* otherwise its `;name=value` matrix parameters and `name=value` pieces keep
|
|
321
|
+
* their names and lose their values, like query parameters. */
|
|
322
|
+
function shareableSegment(segment) {
|
|
323
|
+
const parts = segment.split(";");
|
|
324
|
+
if (parts.some((part) => keyShaped(part.split("=")[0])))
|
|
325
|
+
return ADDRESS_KEY_PLACEHOLDER;
|
|
326
|
+
return parts.map((part) => (part.includes("=") ? `${part.slice(0, part.indexOf("="))}=` : part)).join(";");
|
|
327
|
+
}
|
|
328
|
+
/** A connection address as it may travel. Hosted MCP servers often carry
|
|
329
|
+
* their credential in the address itself, where text redaction does not
|
|
330
|
+
* look, so an address loses its sign-in part and fragment; keeps its query
|
|
331
|
+
* and matrix parameter names with the values emptied (like header names: the
|
|
332
|
+
* recipient fills them in); and has every key-shaped path segment, parameter
|
|
333
|
+
* name and host label left of the registrable domain (taken as the last two
|
|
334
|
+
* labels) replaced. An address with none of these is returned exactly as it
|
|
335
|
+
* was. */
|
|
336
|
+
export function shareableAddress(value) {
|
|
337
|
+
let url;
|
|
338
|
+
try {
|
|
339
|
+
url = new URL(value);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return { url: value, changed: false };
|
|
343
|
+
}
|
|
344
|
+
let changed = false;
|
|
345
|
+
if (url.username || url.password) {
|
|
346
|
+
url.username = "";
|
|
347
|
+
url.password = "";
|
|
348
|
+
changed = true;
|
|
349
|
+
}
|
|
350
|
+
if (url.hash) {
|
|
351
|
+
url.hash = "";
|
|
352
|
+
changed = true;
|
|
353
|
+
}
|
|
354
|
+
const labels = url.hostname.split(".");
|
|
355
|
+
const hostLabels = labels.map((label, index) => (index < labels.length - 2 && keyShaped(label) ? ADDRESS_KEY_PLACEHOLDER : label));
|
|
356
|
+
if (hostLabels.some((label, index) => label !== labels[index])) {
|
|
357
|
+
url.hostname = hostLabels.join(".");
|
|
358
|
+
changed = true;
|
|
359
|
+
}
|
|
360
|
+
const params = [...url.searchParams];
|
|
361
|
+
const names = params.map(([name]) => (keyShaped(name) ? ADDRESS_KEY_PLACEHOLDER : name));
|
|
362
|
+
if (params.some(([name, value], index) => value || names[index] !== name)) {
|
|
363
|
+
url.search = new URLSearchParams(names.map((name) => [name, ""])).toString();
|
|
364
|
+
changed = true;
|
|
365
|
+
}
|
|
366
|
+
const segments = url.pathname.split("/");
|
|
367
|
+
const shareable = segments.map(shareableSegment);
|
|
368
|
+
if (shareable.some((segment, index) => segment !== segments[index])) {
|
|
369
|
+
url.pathname = shareable.join("/");
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
372
|
+
return changed ? { url: url.toString(), changed } : { url: value, changed };
|
|
373
|
+
}
|
|
374
|
+
/** The skills that fit one file, for skillSelection "all": a name two bots
|
|
375
|
+
* hold with different content stays out whole; each bot keeps at most
|
|
376
|
+
* AGENT_MAX_SKILLS (switched-on skills first, then by name) and the team at
|
|
377
|
+
* most PACKAGE_MAX_SKILLS names (the same order). Everything left out is
|
|
378
|
+
* listed, so the person sees it and can choose instead. */
|
|
379
|
+
function fitSkills(bots, botKeys, skillsByBot, skipped) {
|
|
380
|
+
const firstByName = new Map();
|
|
381
|
+
const conflicted = new Set();
|
|
382
|
+
for (const bot of bots) {
|
|
383
|
+
for (const skill of skillsByBot?.get(bot.id) ?? []) {
|
|
384
|
+
const first = firstByName.get(skill.name);
|
|
385
|
+
if (!first)
|
|
386
|
+
firstByName.set(skill.name, skill);
|
|
387
|
+
else if (!sameSkill(first, skill))
|
|
388
|
+
conflicted.add(skill.name);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const name of [...conflicted].sort())
|
|
392
|
+
skipped.push({ part: `skills[${name}]`, reason: "skill_conflict" });
|
|
393
|
+
const on = (skill) => skill.enabled !== false;
|
|
394
|
+
const fitted = new Map();
|
|
395
|
+
for (const bot of bots) {
|
|
396
|
+
const own = (skillsByBot?.get(bot.id) ?? []).filter((skill) => !conflicted.has(skill.name));
|
|
397
|
+
const ranked = [...own].sort((a, b) => Number(on(b)) - Number(on(a)) || a.name.localeCompare(b.name));
|
|
398
|
+
for (const skill of ranked.slice(AGENT_MAX_SKILLS)) {
|
|
399
|
+
skipped.push({ part: `agents[${botKeys.get(bot.id)}].skills[${skill.name}]`, reason: "bot_skill_limit" });
|
|
400
|
+
}
|
|
401
|
+
const kept = new Set(ranked.slice(0, AGENT_MAX_SKILLS).map((skill) => skill.name));
|
|
402
|
+
fitted.set(bot.id, own.filter((skill) => kept.has(skill.name)));
|
|
403
|
+
}
|
|
404
|
+
const names = new Map();
|
|
405
|
+
for (const list of fitted.values()) {
|
|
406
|
+
for (const skill of list)
|
|
407
|
+
names.set(skill.name, names.get(skill.name) === true || on(skill));
|
|
408
|
+
}
|
|
409
|
+
if (names.size > PACKAGE_MAX_SKILLS) {
|
|
410
|
+
const ranked = [...names].sort(([a, aOn], [b, bOn]) => Number(bOn) - Number(aOn) || a.localeCompare(b));
|
|
411
|
+
const left = new Set(ranked.slice(PACKAGE_MAX_SKILLS).map(([name]) => name));
|
|
412
|
+
for (const name of [...left].sort())
|
|
413
|
+
skipped.push({ part: `skills[${name}]`, reason: "team_skill_limit" });
|
|
414
|
+
for (const [id, list] of fitted)
|
|
415
|
+
fitted.set(id, list.filter((skill) => !left.has(skill.name)));
|
|
416
|
+
}
|
|
417
|
+
return fitted;
|
|
418
|
+
}
|
|
419
|
+
/** For an exact choice: the same name with different content on two bots
|
|
420
|
+
* cannot be one skill in the file. */
|
|
421
|
+
function refuseSkillConflicts(bots, skillsByBot) {
|
|
422
|
+
const firstByName = new Map();
|
|
423
|
+
for (const bot of bots) {
|
|
424
|
+
for (const skill of skillsByBot?.get(bot.id) ?? []) {
|
|
425
|
+
const first = firstByName.get(skill.name);
|
|
426
|
+
if (!first)
|
|
427
|
+
firstByName.set(skill.name, skill);
|
|
428
|
+
else if (!sameSkill(first, skill)) {
|
|
429
|
+
throw new TeamExportError(`Two bots in this team have different skills named "${skill.name}". Leave that skill out and try again.`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** One team, whole, except its chat history: bots with their standing
|
|
435
|
+
* instructions, looks and pictures, playbooks, skills (SKILL.md only),
|
|
436
|
+
* group chats, routines and group chat goals, the team brief and its Chief,
|
|
437
|
+
* connection slots (addresses and value names, never values) and, when
|
|
438
|
+
* asked, starter notes. Every text part passes through secret redaction and
|
|
439
|
+
* the result through the same parser every import uses. */
|
|
440
|
+
export function createTeamPackageExport(input) {
|
|
441
|
+
const team = teamOf(input.team);
|
|
442
|
+
const bots = input.bots.filter((bot) => !bot.hidden && teamOf(bot.section) === team);
|
|
443
|
+
if (!bots.length)
|
|
444
|
+
throw new TeamExportError("Add a bot to this team before sharing it.");
|
|
445
|
+
const skipped = [...(input.skipped ?? [])];
|
|
446
|
+
const release = input.release?.trim() || nextPatchRelease(input.published?.lastRelease);
|
|
447
|
+
if (!SEMVER.test(release))
|
|
448
|
+
throw new TeamExportError("Use a version like 1.2.3.");
|
|
449
|
+
const notes = input.notes?.trim();
|
|
450
|
+
if (notes && notes.length > RELEASE_NOTES_MAX)
|
|
451
|
+
throw new TeamExportError(`Release notes must be at most ${RELEASE_NOTES_MAX} characters.`);
|
|
452
|
+
const displayName = input.name?.trim() || team || "General";
|
|
453
|
+
// The package id: what this team was shared as; else the one package all
|
|
454
|
+
// of its bots came from ("start from the published release"); else new.
|
|
455
|
+
const installedIds = new Set(bots.map((bot) => bot.installedPackage?.id));
|
|
456
|
+
const inherited = installedIds.size === 1 ? [...installedIds][0] : undefined;
|
|
457
|
+
const packageId = input.published?.packageId ?? inherited ?? portableKey(team || displayName, "team", new Set());
|
|
458
|
+
const recorded = input.published?.packageId === packageId ? input.published.keys : undefined;
|
|
459
|
+
const botKeys = stableKeys(bots, recorded?.bots, (bot) => (bot.installedPackage?.id === packageId ? bot.installedPackage.agentKey : undefined), "bot");
|
|
460
|
+
const { playbooks, agentPlaybooks } = assignPlaybooks(bots, botKeys);
|
|
461
|
+
let skillsByBot = input.skillsByBot;
|
|
462
|
+
if (input.skillSelection === "all")
|
|
463
|
+
skillsByBot = fitSkills(bots, botKeys, skillsByBot, skipped);
|
|
464
|
+
else
|
|
465
|
+
refuseSkillConflicts(bots, skillsByBot);
|
|
466
|
+
const { packageSkills, agentSkills } = assignSkills(bots, skillsByBot);
|
|
467
|
+
if (packageSkills.size > PACKAGE_MAX_SKILLS) {
|
|
468
|
+
throw new TeamExportError(`A team can share at most ${PACKAGE_MAX_SKILLS} skills. Choose fewer skills and try again.`);
|
|
469
|
+
}
|
|
470
|
+
const crowded = bots.find((bot) => (agentSkills.get(bot.id)?.length ?? 0) > AGENT_MAX_SKILLS);
|
|
471
|
+
if (crowded)
|
|
472
|
+
throw new TeamExportError(`${crowded.name} has more than ${AGENT_MAX_SKILLS} skills. Choose fewer skills and try again.`);
|
|
473
|
+
const requirements = requiredApps(bots);
|
|
474
|
+
// Connection slots: each remote MCP server a bot names, described by its
|
|
475
|
+
// address and the header names it needs. Values stay on this computer.
|
|
476
|
+
const connections = [];
|
|
477
|
+
const connectionKeyByServer = new Map();
|
|
478
|
+
const connectionUsers = new Map();
|
|
479
|
+
const connectionKeys = new Set();
|
|
480
|
+
const agentConnections = new Map();
|
|
481
|
+
const addressRedactions = [];
|
|
482
|
+
for (const bot of bots) {
|
|
483
|
+
const assigned = [];
|
|
484
|
+
for (const name of bot.mcpServers ?? []) {
|
|
485
|
+
if (!connectionKeyByServer.has(name)) {
|
|
486
|
+
const server = remoteServer(input.mcpServers?.[name]);
|
|
487
|
+
if (!server)
|
|
488
|
+
continue;
|
|
489
|
+
const part = `connections[${name}]`;
|
|
490
|
+
if (server === "stdio") {
|
|
491
|
+
if (!skipped.some((skip) => skip.part === part))
|
|
492
|
+
skipped.push({ part, reason: "stdio_server" });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!sharableAddress(server.url)) {
|
|
496
|
+
if (!skipped.some((skip) => skip.part === part))
|
|
497
|
+
skipped.push({ part, reason: "insecure_address" });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
let key = CONNECTION_KEY.test(name) ? name : portableKey(name, "connection", new Set()).replace(/^[^a-z]+/, "").slice(0, 28) || "connection";
|
|
501
|
+
const stem = key.slice(0, 25);
|
|
502
|
+
for (let suffix = 2; connectionKeys.has(key); suffix++)
|
|
503
|
+
key = `${stem}-${suffix}`;
|
|
504
|
+
connectionKeys.add(key);
|
|
505
|
+
connectionKeyByServer.set(name, key);
|
|
506
|
+
const address = shareableAddress(server.url);
|
|
507
|
+
if (address.changed)
|
|
508
|
+
addressRedactions.push(`connections[${key}].mcp.url`);
|
|
509
|
+
connections.push({
|
|
510
|
+
key,
|
|
511
|
+
label: name.slice(0, 100),
|
|
512
|
+
reason: "",
|
|
513
|
+
mcp: { transport: server.transport, url: address.url, valueNames: server.valueNames },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const key = connectionKeyByServer.get(name);
|
|
517
|
+
if (!key)
|
|
518
|
+
continue;
|
|
519
|
+
if (!assigned.includes(key))
|
|
520
|
+
assigned.push(key);
|
|
521
|
+
connectionUsers.set(key, [...(connectionUsers.get(key) ?? []), bot.name]);
|
|
522
|
+
}
|
|
523
|
+
agentConnections.set(bot.id, assigned);
|
|
524
|
+
}
|
|
525
|
+
for (const connection of connections) {
|
|
526
|
+
connection.reason = `Used by ${[...new Set(connectionUsers.get(connection.key) ?? [])].join(", ")}`.slice(0, 240);
|
|
527
|
+
}
|
|
528
|
+
const agents = bots.map((bot) => {
|
|
529
|
+
const key = botKeys.get(bot.id);
|
|
530
|
+
const appearance = { color: bot.color };
|
|
531
|
+
if (bot.mascotExpression)
|
|
532
|
+
appearance.mascotExpression = bot.mascotExpression;
|
|
533
|
+
if (bot.mascotBody)
|
|
534
|
+
appearance.mascotBody = bot.mascotBody;
|
|
535
|
+
const offered = input.avatars?.[bot.id];
|
|
536
|
+
if (offered !== undefined) {
|
|
537
|
+
const crop = bot.avatarCrop === "rounded" || bot.avatarCrop === "square" ? bot.avatarCrop : "circle";
|
|
538
|
+
const prepared = picture(offered);
|
|
539
|
+
if (typeof prepared === "string")
|
|
540
|
+
skipped.push({ part: `agents[${key}].appearance.avatar`, reason: prepared });
|
|
541
|
+
else
|
|
542
|
+
appearance.avatar = { ...prepared, crop };
|
|
543
|
+
}
|
|
544
|
+
const agent = {
|
|
545
|
+
key,
|
|
546
|
+
name: bot.name,
|
|
547
|
+
title: bot.title || undefined,
|
|
548
|
+
description: bot.description || undefined,
|
|
549
|
+
...(bot.soul ? { soul: bot.soul } : {}),
|
|
550
|
+
appearance,
|
|
551
|
+
};
|
|
552
|
+
const assignedPlaybooks = agentPlaybooks.get(bot.id);
|
|
553
|
+
if (assignedPlaybooks?.length)
|
|
554
|
+
agent.playbooks = assignedPlaybooks;
|
|
555
|
+
const assignedSkills = agentSkills.get(bot.id);
|
|
556
|
+
if (assignedSkills?.length)
|
|
557
|
+
agent.skills = assignedSkills;
|
|
558
|
+
const assignedConnections = agentConnections.get(bot.id);
|
|
559
|
+
if (assignedConnections?.length)
|
|
560
|
+
agent.connections = assignedConnections;
|
|
561
|
+
// Starter notes, within the caps an import accepts; anything over them
|
|
562
|
+
// is listed rather than silently cut.
|
|
563
|
+
const memory = {};
|
|
564
|
+
let files = 0;
|
|
565
|
+
let total = 0;
|
|
566
|
+
for (const note of input.memoryByBot?.get(bot.id) ?? []) {
|
|
567
|
+
if (!note.text.trim())
|
|
568
|
+
continue;
|
|
569
|
+
const bytes = utf8Bytes(note.text);
|
|
570
|
+
if (bytes > SEED_FILE_MAX_BYTES || files >= SEED_MAX_FILES || total + bytes > SEED_MAX_BYTES) {
|
|
571
|
+
skipped.push({ part: `agents[${key}].seed.memory[${JSON.stringify(note.path)}]`, reason: "notes_too_large" });
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
memory[note.path] = note.text;
|
|
575
|
+
files += 1;
|
|
576
|
+
total += bytes;
|
|
577
|
+
}
|
|
578
|
+
if (files)
|
|
579
|
+
agent.seed = { memory };
|
|
580
|
+
return agent;
|
|
581
|
+
});
|
|
582
|
+
const groups = input.groups.filter((group) => !group.dm && teamOf(group.section) === team);
|
|
583
|
+
const roomKeys = stableKeys(groups, recorded?.rooms, () => undefined, "group-chat");
|
|
584
|
+
const rooms = [];
|
|
585
|
+
const roomMembers = new Map();
|
|
586
|
+
for (const group of groups) {
|
|
587
|
+
const members = group.memberIds.flatMap((id) => botKeys.has(id) ? [botKeys.get(id)] : []);
|
|
588
|
+
if (!members.length)
|
|
589
|
+
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" };
|
|
595
|
+
const key = roomKeys.get(group.id);
|
|
596
|
+
rooms.push({ key, name: group.name, members, ...(group.bulletin ? { bulletin: group.bulletin } : {}), defaultResponder });
|
|
597
|
+
roomMembers.set(group.id, new Set(members));
|
|
598
|
+
}
|
|
599
|
+
const owned = input.routines.filter((routine) => botKeys.has(routine.botId) &&
|
|
600
|
+
(routine.target !== "room-goal" || (routine.groupId !== undefined && roomMembers.has(routine.groupId))));
|
|
601
|
+
const routineKeys = stableKeys(owned, recorded?.routines, () => undefined, "routine");
|
|
602
|
+
const routines = owned.flatMap((routine) => {
|
|
603
|
+
const key = routineKeys.get(routine.id);
|
|
604
|
+
const agent = botKeys.get(routine.botId);
|
|
605
|
+
const goal = routine.target === "room-goal";
|
|
606
|
+
if (goal && !roomMembers.get(routine.groupId).has(agent)) {
|
|
607
|
+
skipped.push({ part: `routines[${key}]`, reason: "lead_not_in_group_chat" });
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
if (routine.attachments?.length)
|
|
611
|
+
skipped.push({ part: `routines[${key}].attachments`, reason: "files_not_shared" });
|
|
612
|
+
return [{
|
|
613
|
+
key,
|
|
614
|
+
name: routine.name,
|
|
615
|
+
agent,
|
|
616
|
+
...(goal ? { room: roomKeys.get(routine.groupId) } : {}),
|
|
617
|
+
prompt: routine.prompt,
|
|
618
|
+
runOn: goal ? "maus" : routine.runOn,
|
|
619
|
+
schedule: portableSchedule(routine.schedule),
|
|
620
|
+
durationMinutes: routine.durationMinutes,
|
|
621
|
+
...(routine.timeoutMinutes === undefined ? {} : { timeoutMinutes: routine.timeoutMinutes }),
|
|
622
|
+
...(routine.overlap === "queue" ? { overlap: "queue" } : {}),
|
|
623
|
+
...(!goal && routine.continuity ? { continuity: true } : {}),
|
|
624
|
+
enabledAfterInstall: routine.enabled,
|
|
625
|
+
}];
|
|
626
|
+
});
|
|
627
|
+
const chief = bots.find((bot) => bot.chiefOfStaff);
|
|
628
|
+
const brief = input.brief?.trim() ? input.brief : undefined;
|
|
629
|
+
const definition = {
|
|
630
|
+
id: packageId,
|
|
631
|
+
release,
|
|
632
|
+
name: displayName,
|
|
633
|
+
tagline: input.tagline?.trim() || `A portable OpenMausBot team with ${bots.length} ${bots.length === 1 ? "bot" : "bots"}.`,
|
|
634
|
+
summary: input.summary?.trim() ||
|
|
635
|
+
"Shared from OpenMausBot. Bots, skills, group chats and routines arrive as new copies; routines start paused and chat history never travels.",
|
|
636
|
+
...(notes ? { notes } : {}),
|
|
637
|
+
category: "Community",
|
|
638
|
+
author: { name: input.authorName?.trim() || "OpenMausBot user" },
|
|
639
|
+
license: "Unspecified",
|
|
640
|
+
outcomes: ["Recreate this team without copying chat history or private settings."],
|
|
641
|
+
setupMinutes: Math.min(240, Math.max(2, bots.length + requirements.size * 2 + connections.length * 2)),
|
|
642
|
+
requirements: { apps: [...requirements.values()], capabilities: [] },
|
|
643
|
+
team: {
|
|
644
|
+
name: (team || displayName).slice(0, 60).trim(),
|
|
645
|
+
...(brief ? { brief } : {}),
|
|
646
|
+
...(chief ? { leader: botKeys.get(chief.id) } : {}),
|
|
647
|
+
},
|
|
648
|
+
agents,
|
|
649
|
+
};
|
|
650
|
+
if (rooms.length)
|
|
651
|
+
definition.rooms = rooms;
|
|
652
|
+
if (routines.length)
|
|
653
|
+
definition.routines = routines;
|
|
654
|
+
if (playbooks.length)
|
|
655
|
+
definition.playbooks = playbooks;
|
|
656
|
+
if (input.preset)
|
|
657
|
+
definition.presets = [withPresetSkills(input.preset, packageSkills, skipped)];
|
|
658
|
+
if (packageSkills.size)
|
|
659
|
+
definition.skills = { version: 1, entries: [...packageSkills.values()].map((skill) => ({ ...skill })) };
|
|
660
|
+
if (connections.length)
|
|
661
|
+
definition.connections = connections;
|
|
662
|
+
const { document: redactedDocument, redacted } = redactPackageSecrets({
|
|
663
|
+
format: PACKAGE_FORMAT, version: PACKAGE_VERSION, package: definition,
|
|
664
|
+
});
|
|
665
|
+
let document;
|
|
666
|
+
try {
|
|
667
|
+
document = parsePackageDocument(redactedDocument, { trust: "file" });
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (error instanceof PackageFormatError && error.code === "too_large")
|
|
671
|
+
throw new TeamExportError(TEAM_TOO_LARGE_MESSAGE);
|
|
672
|
+
throw new TeamExportError(error instanceof Error ? error.message : "This team could not be shared.");
|
|
673
|
+
}
|
|
674
|
+
const toRecord = (keys, include) => Object.fromEntries([...keys].filter(([id]) => include(id)));
|
|
675
|
+
const exportedRooms = new Set(rooms.map((room) => room.key));
|
|
676
|
+
const exportedRoutines = new Set(routines.map((routine) => routine.key));
|
|
677
|
+
return {
|
|
678
|
+
document,
|
|
679
|
+
filename: `${document.package.id}-${document.package.release}.openmaus.json`,
|
|
680
|
+
redacted: [...new Set([...addressRedactions, ...redacted])],
|
|
681
|
+
skipped,
|
|
682
|
+
published: {
|
|
683
|
+
packageId,
|
|
684
|
+
lastRelease: release,
|
|
685
|
+
keys: {
|
|
686
|
+
// Keep keys of bots not in this release too: a bot moved out and back
|
|
687
|
+
// keeps its identity.
|
|
688
|
+
bots: { ...recorded?.bots, ...toRecord(botKeys, () => true) },
|
|
689
|
+
rooms: { ...recorded?.rooms, ...toRecord(roomKeys, (id) => exportedRooms.has(roomKeys.get(id))) },
|
|
690
|
+
routines: { ...recorded?.routines, ...toRecord(routineKeys, (id) => exportedRoutines.has(routineKeys.get(id))) },
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
// ── presets: my New bot defaults, with a team or on their own ──────────────
|
|
696
|
+
/** Add a preset's skills to the file's skills. A preset never pushes the
|
|
697
|
+
* team's own skills out: a name the team holds with different content, or
|
|
698
|
+
* room past the team's 60, leaves that skill out of the preset and says so.
|
|
699
|
+
* The skill entries never carry whether a skill was switched on; whoever
|
|
700
|
+
* adds the file decides that by where it came from. */
|
|
701
|
+
function withPresetSkills(value, packageSkills, skipped) {
|
|
702
|
+
const kept = [];
|
|
703
|
+
for (const skill of value.skills) {
|
|
704
|
+
const part = `presets[${value.preset.key}].skills[${skill.name}]`;
|
|
705
|
+
const existing = packageSkills.get(skill.name);
|
|
706
|
+
if (existing) {
|
|
707
|
+
if (sameSkill(existing, skill))
|
|
708
|
+
kept.push(skill.name);
|
|
709
|
+
else
|
|
710
|
+
skipped.push({ part, reason: "preset_skill_conflict" });
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (packageSkills.size >= PACKAGE_MAX_SKILLS) {
|
|
714
|
+
skipped.push({ part, reason: "team_skill_limit" });
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const { enabled: _enabled, ...entry } = skill;
|
|
718
|
+
packageSkills.set(skill.name, entry);
|
|
719
|
+
kept.push(skill.name);
|
|
720
|
+
}
|
|
721
|
+
const { skills: _skills, ...preset } = value.preset;
|
|
722
|
+
return { ...preset, ...(kept.length ? { skills: kept } : {}) };
|
|
723
|
+
}
|
|
724
|
+
export const PRESET_TOO_LARGE_MESSAGE = "This preset is too large to share (4 MB). Leave out starter notes or some skills and try again.";
|
|
725
|
+
/** A library package: preset bots and their skills, no team (contract §1.3
|
|
726
|
+
* rule 1). Everything a team file promises holds here too: redaction, the
|
|
727
|
+
* one parser, the 4 MB limit, and a stable package id with the next patch
|
|
728
|
+
* release suggested. */
|
|
729
|
+
export function createLibraryPackageExport(input) {
|
|
730
|
+
if (!input.preset)
|
|
731
|
+
throw new TeamExportError("Your New bot defaults are empty. Give them a name, instructions, skills or starter notes first.");
|
|
732
|
+
const skipped = [...(input.skipped ?? [])];
|
|
733
|
+
const release = input.release?.trim() || nextPatchRelease(input.published?.lastRelease);
|
|
734
|
+
if (!SEMVER.test(release))
|
|
735
|
+
throw new TeamExportError("Use a version like 1.2.3.");
|
|
736
|
+
const notes = input.notes?.trim();
|
|
737
|
+
if (notes && notes.length > RELEASE_NOTES_MAX)
|
|
738
|
+
throw new TeamExportError(`Release notes must be at most ${RELEASE_NOTES_MAX} characters.`);
|
|
739
|
+
const displayName = (input.name?.trim() || input.preset.preset.name).slice(0, 100).trim();
|
|
740
|
+
const packageId = input.published?.packageId ?? portableKey(displayName, "preset", new Set());
|
|
741
|
+
const packageSkills = new Map();
|
|
742
|
+
const preset = withPresetSkills(input.preset, packageSkills, skipped);
|
|
743
|
+
const definition = {
|
|
744
|
+
id: packageId,
|
|
745
|
+
release,
|
|
746
|
+
name: displayName,
|
|
747
|
+
tagline: input.tagline?.trim() || "A preset bot for OpenMausBot's New bot dialog.",
|
|
748
|
+
summary: input.summary?.trim() ||
|
|
749
|
+
"Shared from OpenMausBot. Adds a preset bot to New bot. Its skills arrive switched off; model choices, computers, approval levels and connected apps never travel.",
|
|
750
|
+
...(notes ? { notes } : {}),
|
|
751
|
+
category: "Community",
|
|
752
|
+
author: { name: input.authorName?.trim() || "OpenMausBot user" },
|
|
753
|
+
license: "Unspecified",
|
|
754
|
+
outcomes: ["Start new bots from a shared preset."],
|
|
755
|
+
setupMinutes: 2,
|
|
756
|
+
requirements: { apps: [], capabilities: [] },
|
|
757
|
+
agents: [],
|
|
758
|
+
presets: [preset],
|
|
759
|
+
};
|
|
760
|
+
if (packageSkills.size)
|
|
761
|
+
definition.skills = { version: 1, entries: [...packageSkills.values()].map((skill) => ({ ...skill })) };
|
|
762
|
+
const { document: redactedDocument, redacted } = redactPackageSecrets({
|
|
763
|
+
format: PACKAGE_FORMAT, version: PACKAGE_VERSION, package: definition,
|
|
764
|
+
});
|
|
765
|
+
let document;
|
|
766
|
+
try {
|
|
767
|
+
document = parsePackageDocument(redactedDocument, { trust: "file" });
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
if (error instanceof PackageFormatError && error.code === "too_large")
|
|
771
|
+
throw new TeamExportError(PRESET_TOO_LARGE_MESSAGE);
|
|
772
|
+
throw new TeamExportError(error instanceof Error ? error.message : "This preset could not be shared.");
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
document,
|
|
776
|
+
filename: `${document.package.id}-${document.package.release}.openmaus.json`,
|
|
777
|
+
redacted,
|
|
778
|
+
skipped,
|
|
779
|
+
published: { packageId, lastRelease: release },
|
|
780
|
+
};
|
|
781
|
+
}
|