openmausbot 0.1.86 → 0.1.87
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 +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -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.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- 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 +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -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 +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- 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 +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- 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/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -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-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- 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 +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- 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 +16 -5
- package/dist-server/server/thread-events.js +17 -1
- 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 +12 -1
- 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 +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -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 +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- 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
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Preset bots: named starting points in the New bot dialog, carried by a
|
|
2
|
+
// shared package (a file someone sent, or the organization's shelf).
|
|
3
|
+
//
|
|
4
|
+
// A preset is an ALLOWLIST (critique #3): a name and a short description,
|
|
5
|
+
// the new bot's name, title, description, standing instructions and look,
|
|
6
|
+
// playbooks, skills (SKILL.md text only) and starter notes. It has no field
|
|
7
|
+
// for a model, a folder, a computer, an approval level, connected apps, MCP
|
|
8
|
+
// servers, a browser or anything else that grants reach, so nothing a
|
|
9
|
+
// preset says can widen what a bot may do. The person creating the bot
|
|
10
|
+
// keeps their own choices for all of that.
|
|
11
|
+
//
|
|
12
|
+
// Where presets live: `DATA_DIR/org-library/presets.json` (contract §3.4).
|
|
13
|
+
// One row per preset, stamped with where it came from ("file" or "org") and
|
|
14
|
+
// the install it belongs to, plus the skill and playbook definitions each
|
|
15
|
+
// install's presets use, so a bot can be created from a preset long after
|
|
16
|
+
// the file or the organization's release is gone. Every use re-reads a row
|
|
17
|
+
// through the same package parser every import uses, so a hand-edited file
|
|
18
|
+
// can still only say what the package format allows.
|
|
19
|
+
//
|
|
20
|
+
// Trust is where the preset came from, never what it says: skills from an
|
|
21
|
+
// organization preset are added switched on (the organization's Admin
|
|
22
|
+
// published them), skills from a file preset switched off. Starter notes are
|
|
23
|
+
// written once, through the normal memory writers (which scrub secrets).
|
|
24
|
+
//
|
|
25
|
+
// There is no confirm step anywhere: choosing a preset in New bot and
|
|
26
|
+
// pressing Create is the decision.
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
32
|
+
import { DATA_DIR } from "./config.js";
|
|
33
|
+
import { AGENT_MAX_SKILLS, canonicalJson, PACKAGE_FORMAT, PACKAGE_VERSION, parsePackageDocument, SEED_FILE_MAX_BYTES, SEED_MAX_BYTES, SEED_MAX_FILES, utf8Bytes, } from "../shared/package-format.js";
|
|
34
|
+
import { parseSkillMd } from "../shared/skill-md.js";
|
|
35
|
+
export const PRESETS_FILE = join(DATA_DIR, "org-library", "presets.json");
|
|
36
|
+
/** The organization library's state (W2-1, contract §3.4); read only here. */
|
|
37
|
+
export const ORG_LIBRARY_STATE_FILE = join(DATA_DIR, "org-library", "state.json");
|
|
38
|
+
/** What this installation last shared as a preset file (skills + presets, no team). */
|
|
39
|
+
export const PUBLISHED_LIBRARY_FILE = join(DATA_DIR, "published-library.json");
|
|
40
|
+
/** Enough for every preset of ten full packages; a runaway import cannot grow the file without bound. */
|
|
41
|
+
export const MAX_STORED_PRESETS = 200;
|
|
42
|
+
/** The key "Include my New bot defaults as a preset" is shared under. Fixed,
|
|
43
|
+
* so renaming the preset keeps its identity across releases. */
|
|
44
|
+
export const DEFAULTS_PRESET_KEY = "new-bot-defaults";
|
|
45
|
+
export const PRESET_UNAVAILABLE_MESSAGE = "That preset is no longer available. Choose another starting role.";
|
|
46
|
+
export const ORG_PRESET_REMOVE_MESSAGE = "Presets from your organization are managed in Admin.";
|
|
47
|
+
// ── the stored file ─────────────────────────────────────────────────────────
|
|
48
|
+
const text = (max) => z.string().max(max);
|
|
49
|
+
const storedPresetSchema = z.object({
|
|
50
|
+
id: z.string().regex(/^[\w-]{1,64}$/),
|
|
51
|
+
source: z.enum(["file", "org"]),
|
|
52
|
+
installId: text(100).min(1),
|
|
53
|
+
packageId: text(80).min(1),
|
|
54
|
+
packageName: text(100).min(1),
|
|
55
|
+
release: text(30).min(1),
|
|
56
|
+
publisherName: text(100).optional(),
|
|
57
|
+
publisher: z.object({ organizationId: text(200), slug: text(40), name: text(100) }).optional(),
|
|
58
|
+
ref: text(120).optional(),
|
|
59
|
+
sha256: text(64).optional(),
|
|
60
|
+
key: text(64).min(1),
|
|
61
|
+
name: text(100).min(1),
|
|
62
|
+
description: text(300).optional(),
|
|
63
|
+
// Checked field by field through the package parser on every use.
|
|
64
|
+
bot: z.record(z.string(), z.unknown()),
|
|
65
|
+
playbooks: z.array(z.string()).optional(),
|
|
66
|
+
skills: z.array(z.string()).optional(),
|
|
67
|
+
seed: z.object({ memory: z.record(z.string(), z.string()) }).optional(),
|
|
68
|
+
addedAt: z.number(),
|
|
69
|
+
});
|
|
70
|
+
const fileSchema = z.object({
|
|
71
|
+
version: z.literal(1),
|
|
72
|
+
presets: z.array(z.unknown()),
|
|
73
|
+
content: z.record(z.string(), z.object({ skills: z.array(z.unknown()), playbooks: z.array(z.unknown()) })).optional(),
|
|
74
|
+
});
|
|
75
|
+
/** A preset exactly as the package carried it: what `preset:<key>` hashes
|
|
76
|
+
* over (contract §1.6), with the row's bookkeeping left out. */
|
|
77
|
+
export function storedPresetObject(row) {
|
|
78
|
+
return {
|
|
79
|
+
key: row.key,
|
|
80
|
+
name: row.name,
|
|
81
|
+
...(row.description !== undefined ? { description: row.description } : {}),
|
|
82
|
+
bot: row.bot,
|
|
83
|
+
...(row.playbooks ? { playbooks: row.playbooks } : {}),
|
|
84
|
+
...(row.skills ? { skills: row.skills } : {}),
|
|
85
|
+
...(row.seed ? { seed: row.seed } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function resolveRow(row, content) {
|
|
89
|
+
const wantedSkills = new Set(row.skills ?? []);
|
|
90
|
+
const wantedPlaybooks = new Set(row.playbooks ?? []);
|
|
91
|
+
const skills = (content?.skills ?? []).filter((skill) => wantedSkills.has(skill.name));
|
|
92
|
+
const playbooks = (content?.playbooks ?? []).filter((playbook) => wantedPlaybooks.has(playbook.key));
|
|
93
|
+
let document;
|
|
94
|
+
try {
|
|
95
|
+
document = parsePackageDocument({
|
|
96
|
+
format: PACKAGE_FORMAT,
|
|
97
|
+
version: PACKAGE_VERSION,
|
|
98
|
+
package: {
|
|
99
|
+
id: row.packageId,
|
|
100
|
+
release: row.release,
|
|
101
|
+
name: row.packageName,
|
|
102
|
+
tagline: row.packageName,
|
|
103
|
+
summary: row.packageName,
|
|
104
|
+
category: "Presets",
|
|
105
|
+
author: { name: row.publisherName ?? row.packageName },
|
|
106
|
+
license: "Unspecified",
|
|
107
|
+
outcomes: [row.name],
|
|
108
|
+
setupMinutes: 1,
|
|
109
|
+
requirements: { apps: [], capabilities: [] },
|
|
110
|
+
presets: [storedPresetObject(row)],
|
|
111
|
+
...(skills.length ? { skills: { version: 1, entries: skills } } : {}),
|
|
112
|
+
...(playbooks.length ? { playbooks } : {}),
|
|
113
|
+
},
|
|
114
|
+
}, { trust: "file" });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const pkg = document.package;
|
|
120
|
+
return { row, preset: pkg.presets[0], skills: pkg.skills?.entries ?? [], playbooks: pkg.playbooks ?? [] };
|
|
121
|
+
}
|
|
122
|
+
export function createPresetStore(file = PRESETS_FILE) {
|
|
123
|
+
const load = () => {
|
|
124
|
+
const empty = { version: 1, presets: [], content: Object.create(null) };
|
|
125
|
+
if (!existsSync(file))
|
|
126
|
+
return empty;
|
|
127
|
+
try {
|
|
128
|
+
const parsed = fileSchema.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
129
|
+
const presets = parsed.presets.flatMap((value) => {
|
|
130
|
+
const row = storedPresetSchema.safeParse(value);
|
|
131
|
+
return row.success ? [row.data] : [];
|
|
132
|
+
});
|
|
133
|
+
const content = Object.create(null);
|
|
134
|
+
// Own entries only: an install id is data from a file.
|
|
135
|
+
for (const [installId, value] of Object.entries(parsed.content ?? {})) {
|
|
136
|
+
content[installId] = { skills: value.skills, playbooks: value.playbooks };
|
|
137
|
+
}
|
|
138
|
+
return { version: 1, presets, content };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// An unreadable file offers no presets. It is left alone (never
|
|
142
|
+
// overwritten by a read), so nothing is lost to a transient error.
|
|
143
|
+
return empty;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const save = (state) => {
|
|
147
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
148
|
+
// Content no preset uses any more is dropped with its last preset.
|
|
149
|
+
const used = new Set(state.presets.map((row) => row.installId));
|
|
150
|
+
const content = Object.create(null);
|
|
151
|
+
for (const [installId, value] of Object.entries(state.content))
|
|
152
|
+
if (used.has(installId))
|
|
153
|
+
content[installId] = value;
|
|
154
|
+
writeFileAtomic(file, JSON.stringify({ version: 1, presets: state.presets, content }, null, 2), { mode: 0o600 });
|
|
155
|
+
};
|
|
156
|
+
const sameFilePreset = (row, candidate, state, content) => row.source === "file" && candidate.source === "file" && row.packageId === candidate.packageId && row.release === candidate.release && row.key === candidate.key &&
|
|
157
|
+
canonicalJson(storedPresetObject(row)) === canonicalJson(storedPresetObject(candidate)) &&
|
|
158
|
+
canonicalJson(usedContent(row, state.content[row.installId])) === canonicalJson(usedContent(candidate, content));
|
|
159
|
+
return {
|
|
160
|
+
list: () => load().presets,
|
|
161
|
+
resolve(id) {
|
|
162
|
+
const state = load();
|
|
163
|
+
const row = state.presets.find((candidate) => candidate.id === id);
|
|
164
|
+
return row ? resolveRow(row, state.content[row.installId]) : null;
|
|
165
|
+
},
|
|
166
|
+
register(document, context) {
|
|
167
|
+
const pkg = document.package;
|
|
168
|
+
const presets = pkg.presets ?? [];
|
|
169
|
+
const added = [];
|
|
170
|
+
const existing = [];
|
|
171
|
+
const skipped = [];
|
|
172
|
+
const org = context.source === "org" ? context.org : undefined;
|
|
173
|
+
if (!presets.length && !org)
|
|
174
|
+
return { added, existing, skipped };
|
|
175
|
+
const state = load();
|
|
176
|
+
const used = new Set(presets.flatMap((preset) => preset.skills ?? []));
|
|
177
|
+
const usedPlaybooks = new Set(presets.flatMap((preset) => preset.playbooks ?? []));
|
|
178
|
+
const content = {
|
|
179
|
+
skills: (pkg.skills?.entries ?? []).filter((skill) => used.has(skill.name)),
|
|
180
|
+
playbooks: (pkg.playbooks ?? []).filter((playbook) => usedPlaybooks.has(playbook.key)),
|
|
181
|
+
};
|
|
182
|
+
// An organization install owns one set of rows. Adding it again (a
|
|
183
|
+
// removed team re-added, or an Add retried after the app stopped
|
|
184
|
+
// before state.json was written) refreshes them in place, keeping
|
|
185
|
+
// their ids, and drops keys the release no longer has. Nothing is
|
|
186
|
+
// duplicated, and nothing here decides whether it was added before:
|
|
187
|
+
// that is org-library.ts's index, rebuilt from the records.
|
|
188
|
+
const ownRow = (row) => row.source === "org" && row.installId === context.installId;
|
|
189
|
+
let refreshed = false;
|
|
190
|
+
if (org) {
|
|
191
|
+
const keys = new Set(presets.map((preset) => preset.key));
|
|
192
|
+
const kept = state.presets.filter((row) => !ownRow(row) || keys.has(row.key));
|
|
193
|
+
refreshed = kept.length !== state.presets.length;
|
|
194
|
+
state.presets = kept;
|
|
195
|
+
}
|
|
196
|
+
for (const preset of presets) {
|
|
197
|
+
const row = {
|
|
198
|
+
id: randomUUID(),
|
|
199
|
+
source: context.source,
|
|
200
|
+
installId: context.installId,
|
|
201
|
+
packageId: pkg.id,
|
|
202
|
+
packageName: pkg.name,
|
|
203
|
+
release: pkg.release,
|
|
204
|
+
...(org ? { publisherName: org.publisher.name, publisher: { ...org.publisher }, ref: org.ref, sha256: org.sha256 } : {}),
|
|
205
|
+
key: preset.key,
|
|
206
|
+
name: preset.name,
|
|
207
|
+
...(preset.description !== undefined ? { description: preset.description } : {}),
|
|
208
|
+
bot: structuredClone(preset.bot),
|
|
209
|
+
...(preset.playbooks ? { playbooks: [...preset.playbooks] } : {}),
|
|
210
|
+
...(preset.skills ? { skills: [...preset.skills] } : {}),
|
|
211
|
+
...(preset.seed ? { seed: structuredClone(preset.seed) } : {}),
|
|
212
|
+
addedAt: Date.now(),
|
|
213
|
+
};
|
|
214
|
+
const own = org ? state.presets.findIndex((candidate) => ownRow(candidate) && candidate.key === preset.key) : -1;
|
|
215
|
+
if (own >= 0) {
|
|
216
|
+
const previous = state.presets[own];
|
|
217
|
+
state.presets[own] = { ...row, id: previous.id, addedAt: previous.addedAt };
|
|
218
|
+
existing.push({ id: previous.id, key: row.key, name: row.name });
|
|
219
|
+
refreshed = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// The same file added twice offers its presets once.
|
|
223
|
+
const same = state.presets.find((candidate) => sameFilePreset(candidate, row, state, content));
|
|
224
|
+
if (same) {
|
|
225
|
+
existing.push({ id: same.id, key: same.key, name: same.name });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (state.presets.length >= MAX_STORED_PRESETS) {
|
|
229
|
+
skipped.push({ part: `presets[${preset.key}]`, reason: "too_many_presets" });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
state.presets.push(row);
|
|
233
|
+
added.push({ id: row.id, key: row.key, name: row.name });
|
|
234
|
+
}
|
|
235
|
+
if (added.length || refreshed) {
|
|
236
|
+
state.content[context.installId] = content;
|
|
237
|
+
save(state);
|
|
238
|
+
}
|
|
239
|
+
return { added, existing, skipped };
|
|
240
|
+
},
|
|
241
|
+
remove(ids) {
|
|
242
|
+
if (!ids.length)
|
|
243
|
+
return;
|
|
244
|
+
const drop = new Set(ids);
|
|
245
|
+
const state = load();
|
|
246
|
+
const kept = state.presets.filter((row) => !drop.has(row.id));
|
|
247
|
+
if (kept.length === state.presets.length)
|
|
248
|
+
return;
|
|
249
|
+
save({ ...state, presets: kept });
|
|
250
|
+
},
|
|
251
|
+
removeFilePreset(id) {
|
|
252
|
+
const state = load();
|
|
253
|
+
const row = state.presets.find((candidate) => candidate.id === id);
|
|
254
|
+
if (!row)
|
|
255
|
+
return "not_found";
|
|
256
|
+
if (row.source !== "file")
|
|
257
|
+
return "organization";
|
|
258
|
+
save({ ...state, presets: state.presets.filter((candidate) => candidate.id !== id) });
|
|
259
|
+
return "removed";
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function usedContent(row, content) {
|
|
264
|
+
const skills = new Set(row.skills ?? []);
|
|
265
|
+
const playbooks = new Set(row.playbooks ?? []);
|
|
266
|
+
return {
|
|
267
|
+
skills: (content?.skills ?? []).filter((skill) => skills.has(skill.name)),
|
|
268
|
+
playbooks: (content?.playbooks ?? []).filter((playbook) => playbooks.has(playbook.key)),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** Each organization install's status from `org-library/state.json`
|
|
272
|
+
* (contract §3.4). No file, or an unreadable one, is an empty map: an
|
|
273
|
+
* organization preset is then shown like any copy that stays after sign-out. */
|
|
274
|
+
export function readOrgInstallStatuses(file = ORG_LIBRARY_STATE_FILE) {
|
|
275
|
+
const statuses = new Map();
|
|
276
|
+
try {
|
|
277
|
+
const state = JSON.parse(readFileSync(file, "utf8"));
|
|
278
|
+
if (!state.installs || typeof state.installs !== "object" || Array.isArray(state.installs))
|
|
279
|
+
return statuses;
|
|
280
|
+
for (const [installId, value] of Object.entries(state.installs)) {
|
|
281
|
+
const status = value && typeof value === "object" ? value.status : undefined;
|
|
282
|
+
if (status === "installed" || status === "withdrawn" || status === "removed")
|
|
283
|
+
statuses.set(installId, status);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
/* no organization state yet */
|
|
288
|
+
}
|
|
289
|
+
return statuses;
|
|
290
|
+
}
|
|
291
|
+
/** Whether New bot may use this preset: not one from an organization
|
|
292
|
+
* release that was withdrawn or an install that was removed. */
|
|
293
|
+
export function presetOffered(row, statuses) {
|
|
294
|
+
if (row.source !== "org")
|
|
295
|
+
return true;
|
|
296
|
+
const status = statuses.get(row.installId);
|
|
297
|
+
return status !== "withdrawn" && status !== "removed";
|
|
298
|
+
}
|
|
299
|
+
/** Every preset the New bot dialog offers: the organization's first (by
|
|
300
|
+
* publisher, then name), then imported files (newest first). A preset from a
|
|
301
|
+
* release the organization withdrew, or an install removed, is not offered. */
|
|
302
|
+
export function listBotPresets(store, statuses) {
|
|
303
|
+
const wire = [];
|
|
304
|
+
for (const row of store.list()) {
|
|
305
|
+
if (!presetOffered(row, statuses))
|
|
306
|
+
continue;
|
|
307
|
+
const resolved = store.resolve(row.id);
|
|
308
|
+
if (!resolved)
|
|
309
|
+
continue;
|
|
310
|
+
const { preset } = resolved;
|
|
311
|
+
wire.push({
|
|
312
|
+
id: row.id,
|
|
313
|
+
source: row.source,
|
|
314
|
+
key: preset.key,
|
|
315
|
+
name: preset.name,
|
|
316
|
+
...(preset.description ? { description: preset.description } : {}),
|
|
317
|
+
packageName: row.packageName,
|
|
318
|
+
release: row.release,
|
|
319
|
+
...(row.source === "org" && row.publisherName ? { publisherName: row.publisherName } : {}),
|
|
320
|
+
bot: preset.bot,
|
|
321
|
+
skills: resolved.skills.map((skill) => ({ name: skill.name, description: skill.description })),
|
|
322
|
+
skillsEnabled: row.source === "org",
|
|
323
|
+
playbooks: resolved.playbooks.map((playbook) => playbook.name),
|
|
324
|
+
notes: Object.keys(preset.seed?.memory ?? {}),
|
|
325
|
+
addedAt: row.addedAt,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const org = wire.filter((preset) => preset.source === "org")
|
|
329
|
+
.sort((a, b) => (a.publisherName ?? "").localeCompare(b.publisherName ?? "") || a.name.localeCompare(b.name));
|
|
330
|
+
const files = wire.filter((preset) => preset.source === "file").sort((a, b) => b.addedAt - a.addedAt);
|
|
331
|
+
return [...org, ...files].map(({ addedAt: _addedAt, ...preset }) => preset);
|
|
332
|
+
}
|
|
333
|
+
/** The preset's content onto a bot that was just created: playbooks, skills
|
|
334
|
+
* (switched on only for an organization preset), starter notes, and the
|
|
335
|
+
* provenance stamp. Persona fields are the caller's: the New bot dialog
|
|
336
|
+
* prefilled them from the preset and the person may have edited them.
|
|
337
|
+
* Throws on failure; the caller removes the half-made bot. */
|
|
338
|
+
export function applyPresetToBot(botId, resolved, deps) {
|
|
339
|
+
const { row, preset } = resolved;
|
|
340
|
+
const org = row.source === "org";
|
|
341
|
+
const playbooks = (preset.playbooks ?? []).flatMap((key) => {
|
|
342
|
+
const playbook = resolved.playbooks.find((candidate) => candidate.key === key);
|
|
343
|
+
return playbook ? [{ ...playbook, triggers: [...playbook.triggers] }] : [];
|
|
344
|
+
});
|
|
345
|
+
const installed = {
|
|
346
|
+
id: row.packageId,
|
|
347
|
+
name: row.packageName,
|
|
348
|
+
release: row.release,
|
|
349
|
+
requiredApps: [],
|
|
350
|
+
source: row.source,
|
|
351
|
+
installId: row.installId,
|
|
352
|
+
presetKey: row.key,
|
|
353
|
+
...(org && row.publisher ? { publisher: { ...row.publisher } } : {}),
|
|
354
|
+
...(org && row.ref ? { ref: row.ref } : {}),
|
|
355
|
+
...(org && row.sha256 ? { sha256: row.sha256 } : {}),
|
|
356
|
+
};
|
|
357
|
+
deps.store.patchBot(botId, { installedPackage: installed, ...(playbooks.length ? { playbooks } : {}) });
|
|
358
|
+
const skills = [];
|
|
359
|
+
for (const name of preset.skills ?? []) {
|
|
360
|
+
const skill = resolved.skills.find((candidate) => candidate.name === name);
|
|
361
|
+
if (!skill)
|
|
362
|
+
throw new Error(`The preset's skill "${name}" is unavailable`);
|
|
363
|
+
// The organization channel names its own source; a file's is display only.
|
|
364
|
+
const source = org && row.ref ? `org:${row.ref}@${row.release}` : skill.source ?? `package:${row.packageId}`;
|
|
365
|
+
const added = deps.skills.install(botId, source, [{ path: "SKILL.md", content: skill.instructions }]);
|
|
366
|
+
if ("error" in added)
|
|
367
|
+
throw new Error(`The preset's skill "${name}" could not be added: ${added.error}`);
|
|
368
|
+
if (org) {
|
|
369
|
+
const enabled = deps.skills.setEnabled(botId, name, true);
|
|
370
|
+
if ("error" in enabled)
|
|
371
|
+
throw new Error(`The preset's skill "${name}" could not be switched on: ${enabled.error}`);
|
|
372
|
+
}
|
|
373
|
+
skills.push(name);
|
|
374
|
+
}
|
|
375
|
+
let notes = 0;
|
|
376
|
+
for (const [path, value] of Object.entries(preset.seed?.memory ?? {})) {
|
|
377
|
+
if (path === "MEMORY.md")
|
|
378
|
+
deps.memory.writeIndex(botId, value);
|
|
379
|
+
else
|
|
380
|
+
deps.memory.writeTopic(botId, path.slice("memory/".length), value);
|
|
381
|
+
notes += 1;
|
|
382
|
+
}
|
|
383
|
+
return { skills, notes };
|
|
384
|
+
}
|
|
385
|
+
function isPortableSource(value) {
|
|
386
|
+
return !/^(?:[a-z]:[\\/]|[\\/]|\\\\|file:)/i.test(value);
|
|
387
|
+
}
|
|
388
|
+
/** "Include my New bot defaults as a preset": the saved defaults, reduced to
|
|
389
|
+
* the preset allowlist. Name, look, standing instructions, skills and (when
|
|
390
|
+
* `includeNotes`) starter notes. Never the model, folder, computer, approval
|
|
391
|
+
* level, connected apps, MCP servers, browser, peers, Chief settings or
|
|
392
|
+
* routines. Null when nothing is left worth sharing. `avatar` is the
|
|
393
|
+
* picture the dialog prepared (a data URL), checked by the exporter. */
|
|
394
|
+
export function presetFromDefaults(defaults, options) {
|
|
395
|
+
const skipped = [];
|
|
396
|
+
const profile = defaults?.profile ?? {};
|
|
397
|
+
const part = `presets[${DEFAULTS_PRESET_KEY}]`;
|
|
398
|
+
const trimmed = (value) => (value?.trim() ? value : undefined);
|
|
399
|
+
const bot = {
|
|
400
|
+
...(trimmed(profile.name) ? { name: profile.name.trim() } : {}),
|
|
401
|
+
...(trimmed(profile.title) ? { title: profile.title.trim() } : {}),
|
|
402
|
+
...(trimmed(profile.description) ? { description: profile.description.trim() } : {}),
|
|
403
|
+
...(trimmed(profile.soul) ? { soul: profile.soul } : {}),
|
|
404
|
+
};
|
|
405
|
+
const mascotExpression = profile.mascotExpression?.trim() || undefined;
|
|
406
|
+
const mascotBody = profile.mascotBody?.trim() || undefined;
|
|
407
|
+
if (profile.color || mascotExpression || mascotBody || options.avatar) {
|
|
408
|
+
const crop = profile.avatarCrop === "rounded" || profile.avatarCrop === "square" ? profile.avatarCrop : "circle";
|
|
409
|
+
bot.appearance = {
|
|
410
|
+
color: profile.color ?? "green",
|
|
411
|
+
...(mascotExpression ? { mascotExpression } : {}),
|
|
412
|
+
...(mascotBody ? { mascotBody } : {}),
|
|
413
|
+
...(options.avatar ? { avatar: { ...options.avatar, crop } } : {}),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const skills = [];
|
|
417
|
+
for (const template of defaults?.skills ?? []) {
|
|
418
|
+
const parsed = parseSkillMd(template.text);
|
|
419
|
+
if ("error" in parsed || parsed.name !== template.name) {
|
|
420
|
+
skipped.push({ part: `${part}.skills[${template.name}]`, reason: "skill_changed" });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (skills.length >= AGENT_MAX_SKILLS) {
|
|
424
|
+
skipped.push({ part: `${part}.skills[${template.name}]`, reason: "bot_skill_limit" });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
skills.push({
|
|
428
|
+
name: parsed.name,
|
|
429
|
+
description: parsed.description,
|
|
430
|
+
...(template.source && isPortableSource(template.source) ? { source: template.source } : {}),
|
|
431
|
+
...(parsed.license ? { license: parsed.license } : {}),
|
|
432
|
+
...(parsed.compatibility ? { compatibility: parsed.compatibility } : {}),
|
|
433
|
+
instructions: template.text,
|
|
434
|
+
enabled: template.enabled,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const memory = {};
|
|
438
|
+
if (options.includeNotes) {
|
|
439
|
+
let files = 0;
|
|
440
|
+
let total = 0;
|
|
441
|
+
for (const path of Object.keys(defaults?.memory ?? {}).sort((a, b) => (a === "MEMORY.md" ? -1 : b === "MEMORY.md" ? 1 : a.localeCompare(b)))) {
|
|
442
|
+
const value = defaults.memory[path];
|
|
443
|
+
if (!value.trim())
|
|
444
|
+
continue;
|
|
445
|
+
const bytes = utf8Bytes(value);
|
|
446
|
+
if (bytes > SEED_FILE_MAX_BYTES || files >= SEED_MAX_FILES || total + bytes > SEED_MAX_BYTES) {
|
|
447
|
+
skipped.push({ part: `${part}.seed.memory[${JSON.stringify(path)}]`, reason: "notes_too_large" });
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
memory[path] = value;
|
|
451
|
+
files += 1;
|
|
452
|
+
total += bytes;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const hasContent = Object.keys(bot).length > 0 || skills.length > 0 || Object.keys(memory).length > 0;
|
|
456
|
+
if (!hasContent) {
|
|
457
|
+
skipped.push({ part, reason: "preset_empty" });
|
|
458
|
+
return { value: null, skipped };
|
|
459
|
+
}
|
|
460
|
+
const name = (options.name?.trim() || profile.name?.trim() || "New bot").slice(0, 100).trim();
|
|
461
|
+
const description = options.description?.trim().slice(0, 300) || undefined;
|
|
462
|
+
return {
|
|
463
|
+
value: {
|
|
464
|
+
preset: {
|
|
465
|
+
key: DEFAULTS_PRESET_KEY,
|
|
466
|
+
name,
|
|
467
|
+
...(description ? { description } : {}),
|
|
468
|
+
bot,
|
|
469
|
+
...(skills.length ? { skills: skills.map((skill) => skill.name) } : {}),
|
|
470
|
+
...(Object.keys(memory).length ? { seed: { memory } } : {}),
|
|
471
|
+
},
|
|
472
|
+
skills,
|
|
473
|
+
},
|
|
474
|
+
skipped,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// ── what this installation last shared as a preset file ────────────────────
|
|
478
|
+
const publishedLibrarySchema = z.object({ version: z.literal(1), packageId: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).max(80), lastRelease: z.string().min(1).max(30) });
|
|
479
|
+
export function readPublishedLibrary(file = PUBLISHED_LIBRARY_FILE) {
|
|
480
|
+
try {
|
|
481
|
+
const parsed = publishedLibrarySchema.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
482
|
+
return { packageId: parsed.packageId, lastRelease: parsed.lastRelease };
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Nothing shared yet (or unreadable): the next share starts at 1.0.0.
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
export function writePublishedLibrary(entry, file = PUBLISHED_LIBRARY_FILE) {
|
|
490
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
491
|
+
writeFileAtomic(file, JSON.stringify({ version: 1, ...entry }, null, 2), { mode: 0o600 });
|
|
492
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai"];
|
|
1
|
+
export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
|
|
2
2
|
const DEFAULT_URLS = {
|
|
3
3
|
anthropic: "https://api.anthropic.com",
|
|
4
4
|
openaiCompat: "https://openrouter.ai/api/v1",
|
|
5
5
|
xai: "https://api.x.ai/v1",
|
|
6
|
+
mistral: "https://api.mistral.ai/v1",
|
|
6
7
|
};
|
|
7
8
|
const MAX_MODELS = 5;
|
|
8
9
|
function isLoopback(hostname) {
|
|
@@ -18,7 +19,7 @@ export function providerModelsUrl(provider, base) {
|
|
|
18
19
|
}
|
|
19
20
|
function modelIds(body) {
|
|
20
21
|
const record = body && typeof body === "object" ? body : null;
|
|
21
|
-
const list = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
22
|
+
const list = Array.isArray(body) ? body : Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
22
23
|
const ids = [];
|
|
23
24
|
for (const entry of list) {
|
|
24
25
|
const item = entry && typeof entry === "object" ? entry : null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// What this installation has shared, per team: the package id and the
|
|
2
|
+
// release last exported, plus the key each bot, group chat and routine was
|
|
3
|
+
// published under.
|
|
4
|
+
//
|
|
5
|
+
// Keys are a package's identity across releases (Admin refuses a release
|
|
6
|
+
// that silently drops one, and later updates match on them). They must not
|
|
7
|
+
// come from names once they exist: renaming a bot, a room, a routine or the
|
|
8
|
+
// team would otherwise read as "everything removed, everything added". So
|
|
9
|
+
// the first export records them here, keyed by record id, and every later
|
|
10
|
+
// export of the same team reuses them. Teams are name strings in OMB, so the
|
|
11
|
+
// team entry is renamed with the section (section-context.ts calls
|
|
12
|
+
// renamePublishedTeam from the one function every rename goes through).
|
|
13
|
+
//
|
|
14
|
+
// Server-private: nothing here is on the wire, and nothing here is a secret.
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
19
|
+
import { DATA_DIR } from "./config.js";
|
|
20
|
+
export const PUBLISHED_TEAMS_FILE = join(DATA_DIR, "published-teams.json");
|
|
21
|
+
const keyMap = z.record(z.string(), z.string());
|
|
22
|
+
const publishedTeamSchema = z.object({
|
|
23
|
+
packageId: z.string().min(1),
|
|
24
|
+
lastRelease: z.string().min(1),
|
|
25
|
+
keys: z.object({ bots: keyMap, rooms: keyMap, routines: keyMap }).default({ bots: {}, rooms: {}, routines: {} }),
|
|
26
|
+
});
|
|
27
|
+
const fileSchema = z.object({ version: z.literal(1), teams: z.record(z.string(), z.unknown()) });
|
|
28
|
+
function load() {
|
|
29
|
+
const teams = new Map();
|
|
30
|
+
if (!existsSync(PUBLISHED_TEAMS_FILE))
|
|
31
|
+
return teams;
|
|
32
|
+
try {
|
|
33
|
+
const file = fileSchema.parse(JSON.parse(readFileSync(PUBLISHED_TEAMS_FILE, "utf8")));
|
|
34
|
+
// Own entries only: a team may be called anything, including __proto__.
|
|
35
|
+
for (const [section, value] of Object.entries(file.teams)) {
|
|
36
|
+
const entry = publishedTeamSchema.safeParse(value);
|
|
37
|
+
if (entry.success)
|
|
38
|
+
teams.set(section, entry.data);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// An unreadable record only costs key stability on the next export,
|
|
43
|
+
// which then derives keys from names as a first export does.
|
|
44
|
+
}
|
|
45
|
+
return teams;
|
|
46
|
+
}
|
|
47
|
+
function save(teams) {
|
|
48
|
+
mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
49
|
+
const out = Object.create(null);
|
|
50
|
+
for (const [section, entry] of teams)
|
|
51
|
+
out[section] = entry;
|
|
52
|
+
writeFileAtomic(PUBLISHED_TEAMS_FILE, JSON.stringify({ version: 1, teams: out }, null, 2), { mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
export function readPublishedTeam(section) {
|
|
55
|
+
return load().get(section) ?? null;
|
|
56
|
+
}
|
|
57
|
+
export function writePublishedTeam(section, entry) {
|
|
58
|
+
const teams = load();
|
|
59
|
+
teams.set(section, entry);
|
|
60
|
+
save(teams);
|
|
61
|
+
}
|
|
62
|
+
/** Follow a team rename; null forgets the team (it was deleted). */
|
|
63
|
+
export function renamePublishedTeam(name, nextName) {
|
|
64
|
+
const teams = load();
|
|
65
|
+
const entry = teams.get(name);
|
|
66
|
+
if (!entry || name === nextName)
|
|
67
|
+
return;
|
|
68
|
+
teams.delete(name);
|
|
69
|
+
if (nextName !== null)
|
|
70
|
+
teams.set(nextName, entry);
|
|
71
|
+
save(teams);
|
|
72
|
+
}
|
|
73
|
+
/** "1.2.3" → "1.2.4"; nothing published yet (or unreadable) starts at 1.0.0. */
|
|
74
|
+
export function nextPatchRelease(release) {
|
|
75
|
+
const match = release?.match(/^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/);
|
|
76
|
+
if (!match)
|
|
77
|
+
return "1.0.0";
|
|
78
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
79
|
+
return patch < 999_999 ? `${major}.${minor}.${patch + 1}` : `${major}.${minor + 1}.0`;
|
|
80
|
+
}
|
|
@@ -6,6 +6,7 @@ const nodeSchema = z.object({
|
|
|
6
6
|
id: z.string(), rootId: z.string(), parentId: z.string().optional(),
|
|
7
7
|
groupId: z.string().optional(), threadId: z.string(), botId: z.string(),
|
|
8
8
|
key: z.string(), text: z.string(), createdAt: z.number(),
|
|
9
|
+
requestBatchKey: z.string().optional(),
|
|
9
10
|
status: z.enum(["source", "queued", "running", "waiting", "resume", "completed", "failed", "cancelled"]),
|
|
10
11
|
result: z.string().default(""), reported: z.boolean().default(false),
|
|
11
12
|
executions: z.number().int().nonnegative().default(0), startedAt: z.number().optional(),
|
|
@@ -159,7 +160,7 @@ export class RoomHandoffs {
|
|
|
159
160
|
const root = this.root(n);
|
|
160
161
|
return `Room handoff hard cap exhausted: node was ${n.status} after ${duration(this.now() - root.createdAt)} of the ${duration(this.limits.hardCapMs)} wall-clock cap`;
|
|
161
162
|
}
|
|
162
|
-
enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "") {
|
|
163
|
+
enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "", requestBatchKey) {
|
|
163
164
|
if (this.loadError)
|
|
164
165
|
throw new Error(this.loadError);
|
|
165
166
|
let parent = parentId ? this.nodes.get(parentId) : this.nodes.get(generation);
|
|
@@ -178,10 +179,17 @@ export class RoomHandoffs {
|
|
|
178
179
|
const existing = this.children(parent.id).find(n => n.key === key);
|
|
179
180
|
if (existing) {
|
|
180
181
|
if (existing.groupId !== target.groupId || existing.botId !== target.botId || existing.text !== text ||
|
|
181
|
-
existing.kind !== kind)
|
|
182
|
+
existing.kind !== kind || existing.requestBatchKey !== requestBatchKey)
|
|
182
183
|
throw new Error("request_key was already used for different work");
|
|
183
184
|
return { node: existing, duplicate: true };
|
|
184
185
|
}
|
|
186
|
+
if (requestBatchKey && target.groupId) {
|
|
187
|
+
const batch = this.children(parent.id).filter(n => n.requestBatchKey === requestBatchKey && n.groupId === target.groupId);
|
|
188
|
+
if (batch.some(n => n.text !== text || n.threadId !== target.threadId))
|
|
189
|
+
throw new Error("request_key was already used for different room work");
|
|
190
|
+
if (batch.some(n => n.startedAt !== undefined))
|
|
191
|
+
throw new Error("This shared room request has already started; use a new request_key for additional recipients");
|
|
192
|
+
}
|
|
185
193
|
if (!rework && this.children(parent.id).some(n => n.kind === kind &&
|
|
186
194
|
n.groupId === target.groupId && n.botId === target.botId && n.status === "completed")) {
|
|
187
195
|
throw new Error("This agent already completed your assignment. Do not send acknowledgements or approvals as new work. Finish with your decision; results return automatically. Only use rework=true for concrete additional work.");
|
|
@@ -221,7 +229,7 @@ export class RoomHandoffs {
|
|
|
221
229
|
}
|
|
222
230
|
const node = { ...target, id: randomUUID(), rootId: parent.rootId, parentId: parent.id,
|
|
223
231
|
key, text, createdAt: this.now(), status: "queued", result: "", reported: false, executions: 0, approvalGranted,
|
|
224
|
-
kind };
|
|
232
|
+
kind, ...(target.groupId && requestBatchKey ? { requestBatchKey } : {}) };
|
|
225
233
|
const problem = this.hooks.validate(node, parent);
|
|
226
234
|
if (problem)
|
|
227
235
|
throw new Error(problem);
|
|
@@ -239,6 +247,14 @@ export class RoomHandoffs {
|
|
|
239
247
|
}
|
|
240
248
|
return { node, duplicate: false };
|
|
241
249
|
}
|
|
250
|
+
/** A room brief is displayed once; each recipient keeps its own execution and result. */
|
|
251
|
+
sharedRequest(node) {
|
|
252
|
+
const batch = node.groupId && node.requestBatchKey && node.parentId
|
|
253
|
+
? this.children(node.parentId).filter(n => n.requestBatchKey === node.requestBatchKey &&
|
|
254
|
+
n.groupId === node.groupId && n.threadId === node.threadId && n.text === node.text)
|
|
255
|
+
: [node];
|
|
256
|
+
return { id: batch[0]?.id ?? node.id, botIds: batch.map(n => n.botId) };
|
|
257
|
+
}
|
|
242
258
|
sourceSettled(generation, ok) {
|
|
243
259
|
const node = this.nodes.get(generation);
|
|
244
260
|
if (!node || node.status !== "source")
|