openmausbot 0.1.91 → 0.1.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Hand-kept facts about model providers that models.dev does not carry:
|
|
2
|
+
// - which providers we recommend, in what order, and under what name;
|
|
3
|
+
// - a pinned OpenAI-compatible base URL for each (models.dev lists some
|
|
4
|
+
// providers without one, and a pinned URL means a catalog refresh can never
|
|
5
|
+
// change where a newly pasted key for a recommended provider is sent);
|
|
6
|
+
// - the Anthropic-compatible endpoint Claude Code can use, where one exists;
|
|
7
|
+
// - whether the provider documents Codex support, and its exceptions;
|
|
8
|
+
// - how to prove a key when the provider's model list is public.
|
|
9
|
+
// Every row was checked by hand on its `checkedAt` date.
|
|
10
|
+
//
|
|
11
|
+
// Codex rules (see codexVerdict): Codex is offered only when the provider's
|
|
12
|
+
// /responses route passed the check AND either the provider documents Codex
|
|
13
|
+
// or the person turned on the experimental switch. Documented exceptions
|
|
14
|
+
// (Fireworks: no MiniMax models, no Fire Pass keys) always apply.
|
|
15
|
+
const CHECKED = "2026-09-30";
|
|
16
|
+
/** The AI SDK package for any OpenAI-compatible base URL. */
|
|
17
|
+
export const OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
|
|
18
|
+
export const PROVIDER_PRESETS = [
|
|
19
|
+
{
|
|
20
|
+
id: "openrouter",
|
|
21
|
+
catalogId: "openrouter",
|
|
22
|
+
label: "OpenRouter",
|
|
23
|
+
recommended: 1,
|
|
24
|
+
api: "https://openrouter.ai/api/v1",
|
|
25
|
+
anthropic: {
|
|
26
|
+
baseUrl: "https://openrouter.ai/api",
|
|
27
|
+
auth: "bearer",
|
|
28
|
+
doc: "https://openrouter.ai/docs/guides/guides/claude-code-integration",
|
|
29
|
+
},
|
|
30
|
+
codex: { kind: "documented", doc: "https://openrouter.ai/docs/cookbook/coding-agents/codex-cli" },
|
|
31
|
+
// /models is public; /key answers only with a valid key.
|
|
32
|
+
keyCheck: "/key",
|
|
33
|
+
publicModels: true,
|
|
34
|
+
checkedAt: CHECKED,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "fireworks-ai",
|
|
38
|
+
catalogId: "fireworks-ai",
|
|
39
|
+
label: "Fireworks AI",
|
|
40
|
+
recommended: 2,
|
|
41
|
+
api: "https://api.fireworks.ai/inference/v1",
|
|
42
|
+
anthropic: {
|
|
43
|
+
baseUrl: "https://api.fireworks.ai/inference",
|
|
44
|
+
auth: "bearer",
|
|
45
|
+
doc: "https://docs.fireworks.ai/tools-sdks/anthropic-compatibility",
|
|
46
|
+
},
|
|
47
|
+
codex: {
|
|
48
|
+
kind: "documented",
|
|
49
|
+
doc: "https://docs.fireworks.ai/ecosystem/fireconnect/codex",
|
|
50
|
+
// Fireworks: MiniMax models and Fire Pass keys do not work with Codex.
|
|
51
|
+
denyModels: /minimax/i,
|
|
52
|
+
denyKeyPrefixes: ["fpk_"],
|
|
53
|
+
},
|
|
54
|
+
checkedAt: CHECKED,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "deepseek",
|
|
58
|
+
catalogId: "deepseek",
|
|
59
|
+
label: "DeepSeek",
|
|
60
|
+
recommended: 3,
|
|
61
|
+
api: "https://api.deepseek.com",
|
|
62
|
+
anthropic: { baseUrl: "https://api.deepseek.com/anthropic", doc: "https://api-docs.deepseek.com/guides/anthropic_api" },
|
|
63
|
+
codex: { kind: "unsupported", reason: "no-responses-api" },
|
|
64
|
+
checkedAt: CHECKED,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "cline-pass",
|
|
68
|
+
catalogId: "cline-pass",
|
|
69
|
+
label: "Cline (ClinePass)",
|
|
70
|
+
recommended: 4,
|
|
71
|
+
api: "https://api.cline.bot/api/v1",
|
|
72
|
+
codex: { kind: "unsupported", reason: "not-supported" },
|
|
73
|
+
publicModels: true,
|
|
74
|
+
checkedAt: CHECKED,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
// Cline's pay-as-you-go credits: the same API as ClinePass with a larger,
|
|
78
|
+
// OpenRouter-style model list. Not in models.dev.
|
|
79
|
+
id: "cline-credits",
|
|
80
|
+
label: "Cline (credits)",
|
|
81
|
+
recommended: 5,
|
|
82
|
+
api: "https://api.cline.bot/api/v1",
|
|
83
|
+
env: "CLINE_API_KEY",
|
|
84
|
+
doc: "https://docs.cline.bot/api/overview",
|
|
85
|
+
codex: { kind: "unsupported", reason: "not-supported" },
|
|
86
|
+
publicModels: true,
|
|
87
|
+
checkedAt: CHECKED,
|
|
88
|
+
},
|
|
89
|
+
{ id: "groq", catalogId: "groq", label: "Groq", recommended: 6, api: "https://api.groq.com/openai/v1", checkedAt: CHECKED },
|
|
90
|
+
{ id: "togetherai", catalogId: "togetherai", label: "Together AI", recommended: 7, api: "https://api.together.xyz/v1", checkedAt: CHECKED },
|
|
91
|
+
{ id: "mistral", catalogId: "mistral", label: "Mistral", recommended: 8, api: "https://api.mistral.ai/v1", checkedAt: CHECKED },
|
|
92
|
+
{ id: "xai", catalogId: "xai", label: "xAI", recommended: 9, api: "https://api.x.ai/v1", checkedAt: CHECKED },
|
|
93
|
+
{
|
|
94
|
+
id: "moonshotai",
|
|
95
|
+
catalogId: "moonshotai",
|
|
96
|
+
label: "Moonshot (Kimi)",
|
|
97
|
+
recommended: 10,
|
|
98
|
+
api: "https://api.moonshot.ai/v1",
|
|
99
|
+
anthropic: { baseUrl: "https://api.moonshot.ai/anthropic" },
|
|
100
|
+
checkedAt: CHECKED,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "zai",
|
|
104
|
+
catalogId: "zai",
|
|
105
|
+
label: "Z.ai",
|
|
106
|
+
recommended: 11,
|
|
107
|
+
api: "https://api.z.ai/api/paas/v4",
|
|
108
|
+
anthropic: { baseUrl: "https://api.z.ai/api/anthropic" },
|
|
109
|
+
checkedAt: CHECKED,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
// models.dev lists MiniMax's Anthropic endpoint as its `api` (with
|
|
113
|
+
// @ai-sdk/anthropic); the chat engine and OpenCode need the OpenAI one,
|
|
114
|
+
// and OpenCode needs the matching SDK with it.
|
|
115
|
+
id: "minimax",
|
|
116
|
+
catalogId: "minimax",
|
|
117
|
+
label: "MiniMax",
|
|
118
|
+
recommended: 12,
|
|
119
|
+
api: "https://api.minimax.io/v1",
|
|
120
|
+
npm: OPENAI_COMPATIBLE_NPM,
|
|
121
|
+
anthropic: { baseUrl: "https://api.minimax.io/anthropic" },
|
|
122
|
+
chat: "after-check",
|
|
123
|
+
checkedAt: CHECKED,
|
|
124
|
+
},
|
|
125
|
+
{ id: "cerebras", catalogId: "cerebras", label: "Cerebras", recommended: 13, api: "https://api.cerebras.ai/v1", checkedAt: CHECKED },
|
|
126
|
+
{
|
|
127
|
+
id: "openai",
|
|
128
|
+
catalogId: "openai",
|
|
129
|
+
label: "OpenAI",
|
|
130
|
+
api: "https://api.openai.com/v1",
|
|
131
|
+
codex: { kind: "documented", doc: "https://github.com/openai/codex" },
|
|
132
|
+
checkedAt: CHECKED,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "opencode",
|
|
136
|
+
catalogId: "opencode",
|
|
137
|
+
label: "OpenCode Zen",
|
|
138
|
+
api: "https://opencode.ai/zen/v1",
|
|
139
|
+
anthropic: { baseUrl: "https://opencode.ai/zen" },
|
|
140
|
+
checkedAt: CHECKED,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
const BY_ID = new Map(PROVIDER_PRESETS.map((preset) => [preset.id, preset]));
|
|
144
|
+
export function providerPreset(id) {
|
|
145
|
+
return BY_ID.get(id);
|
|
146
|
+
}
|
|
147
|
+
export function codexKeyAllowed(preset, key) {
|
|
148
|
+
const codex = preset?.codex;
|
|
149
|
+
return !(codex?.kind === "documented" && codex.denyKeyPrefixes?.some((prefix) => key.startsWith(prefix)));
|
|
150
|
+
}
|
|
151
|
+
export function codexModelAllowed(preset, model) {
|
|
152
|
+
const codex = preset?.codex;
|
|
153
|
+
return !(codex?.kind === "documented" && codex.denyModels?.test(model));
|
|
154
|
+
}
|
|
155
|
+
/** The models Codex may be offered for this provider. */
|
|
156
|
+
export function codexModels(preset, models) {
|
|
157
|
+
return models.filter((model) => codexModelAllowed(preset, model));
|
|
158
|
+
}
|
|
159
|
+
/** Whether Codex can use a provider. `preset` is undefined for custom providers. */
|
|
160
|
+
export function codexVerdict(input) {
|
|
161
|
+
const codex = input.preset?.codex;
|
|
162
|
+
if (codex?.kind === "unsupported") {
|
|
163
|
+
return { use: "no", reason: codex.reason === "no-responses-api" ? "provider-no-responses-api" : "provider-unsupported" };
|
|
164
|
+
}
|
|
165
|
+
if (input.key !== undefined && !codexKeyAllowed(input.preset, input.key))
|
|
166
|
+
return { use: "no", reason: "key-type" };
|
|
167
|
+
if (input.responses === "missing")
|
|
168
|
+
return { use: "no", reason: "route-missing" };
|
|
169
|
+
if (input.responses === "key-refused")
|
|
170
|
+
return { use: "no", reason: "key-refused" };
|
|
171
|
+
if (input.responses !== "works")
|
|
172
|
+
return { use: "no", reason: "not-checked" };
|
|
173
|
+
if (codex?.kind === "documented")
|
|
174
|
+
return { use: "yes" };
|
|
175
|
+
return input.experimental ? { use: "experimental" } : { use: "no", reason: "experimental-off" };
|
|
176
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Trims models.dev's api.json down to what OpenMausBot needs and drops
|
|
2
|
+
// anything unsafe to hand to OpenCode. Both the build script that makes the
|
|
3
|
+
// bundled snapshot (scripts/build-model-catalog.mjs) and the runtime refresh
|
|
4
|
+
// (catalog.ts) run their data through trimModelsDevCatalog, so a catalog
|
|
5
|
+
// fetched later passes exactly the same filter as the one we ship.
|
|
6
|
+
//
|
|
7
|
+
// Why braces matter: OpenCode expands `{env:NAME}` and `{file:path}` anywhere
|
|
8
|
+
// in its config text, and writing the brace as { in JSON does not stop it
|
|
9
|
+
// (verified on OpenCode 1.18.27). A provider or model string carrying a brace
|
|
10
|
+
// could make OpenCode read another provider's key or any file the user can
|
|
11
|
+
// read into a model name. So any provider or model with a `{` or `}` in any of
|
|
12
|
+
// its strings (or object keys) is dropped here, before anything else sees it.
|
|
13
|
+
//
|
|
14
|
+
// The trimmed shape keeps models.dev's own field names, so trimming is
|
|
15
|
+
// idempotent: a trimmed catalog passes through unchanged. That lets catalog.ts
|
|
16
|
+
// run every source (our cache, OpenCode's raw cache, the snapshot) through the
|
|
17
|
+
// same function. On disk each model also leaves out `id` (it is the key) and
|
|
18
|
+
// `tool_call` (every kept model has it); packCatalog/unpackCatalog convert,
|
|
19
|
+
// which keeps the shipped snapshot under its 150 KB gzipped budget.
|
|
20
|
+
import { normalizeApiUrl } from "../cli-api-setup.js";
|
|
21
|
+
export const MODEL_CATALOG_SCHEMA = 1;
|
|
22
|
+
// Generous caps: models.dev's largest provider lists about 600 models today.
|
|
23
|
+
const MAX_PROVIDERS = 2_000;
|
|
24
|
+
const MAX_MODELS_PER_PROVIDER = 5_000;
|
|
25
|
+
const MAX_DEPTH = 8;
|
|
26
|
+
const PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
27
|
+
const ENV_NAME = /^[A-Za-z0-9_]{1,128}$/;
|
|
28
|
+
// eslint-disable-next-line no-control-regex
|
|
29
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
30
|
+
const RELEASE_DATE = /^\d{4}-\d{2}(?:-\d{2})?$/;
|
|
31
|
+
/** True when the text holds a brace OpenCode could treat as a substitution. */
|
|
32
|
+
export function hasBrace(value) {
|
|
33
|
+
return value.includes("{") || value.includes("}");
|
|
34
|
+
}
|
|
35
|
+
/** True when any string or object key inside `value` holds a brace. Values
|
|
36
|
+
* nested deeper than models.dev ever goes count as unsafe too. */
|
|
37
|
+
export function containsBrace(value, depth = 0) {
|
|
38
|
+
if (typeof value === "string")
|
|
39
|
+
return hasBrace(value);
|
|
40
|
+
if (value === null || typeof value !== "object")
|
|
41
|
+
return false;
|
|
42
|
+
if (depth > MAX_DEPTH)
|
|
43
|
+
return true;
|
|
44
|
+
if (Array.isArray(value))
|
|
45
|
+
return value.some((item) => containsBrace(item, depth + 1));
|
|
46
|
+
return Object.entries(value).some(([key, item]) => hasBrace(key) || containsBrace(item, depth + 1));
|
|
47
|
+
}
|
|
48
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
49
|
+
/** A map keyed by provider or model id. It has no prototype, so an id such as
|
|
50
|
+
* `constructor` or `toString` finds nothing unless the catalog lists it, and a
|
|
51
|
+
* `__proto__` key could not replace the prototype (it is also rejected). */
|
|
52
|
+
const idMap = () => Object.create(null);
|
|
53
|
+
/** Never a real id; as a plain-object key it would set the prototype. */
|
|
54
|
+
const RESERVED_KEY = "__proto__";
|
|
55
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
56
|
+
/** A display name: control characters removed, trimmed, capped. */
|
|
57
|
+
function displayName(value, fallback) {
|
|
58
|
+
if (typeof value !== "string")
|
|
59
|
+
return fallback;
|
|
60
|
+
const clean = value.replace(new RegExp(CONTROL.source, "gu"), "").trim().slice(0, 128);
|
|
61
|
+
return clean || fallback;
|
|
62
|
+
}
|
|
63
|
+
function httpsUrl(value) {
|
|
64
|
+
if (typeof value !== "string")
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(value);
|
|
68
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function apiUrl(value) {
|
|
75
|
+
if (typeof value !== "string")
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
return normalizeApiUrl(value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function trimModel(key, raw) {
|
|
85
|
+
if (!isRecord(raw))
|
|
86
|
+
return "invalid";
|
|
87
|
+
if (raw.id !== undefined && raw.id !== key)
|
|
88
|
+
return "invalid";
|
|
89
|
+
// The caller already drops a key with a brace; this is the backstop.
|
|
90
|
+
if (!key || key.length > 256 || key === RESERVED_KEY || hasBrace(key) || CONTROL.test(key) || /\s/u.test(key))
|
|
91
|
+
return "invalid";
|
|
92
|
+
// Only models an agent can drive: current, tool-calling, text in.
|
|
93
|
+
if (raw.status === "deprecated" || raw.tool_call !== true)
|
|
94
|
+
return "skip";
|
|
95
|
+
const modalities = isRecord(raw.modalities) ? raw.modalities : undefined;
|
|
96
|
+
const input = Array.isArray(modalities?.input)
|
|
97
|
+
? modalities.input.filter((item) => typeof item === "string" && item.length <= 32).slice(0, 8)
|
|
98
|
+
: [];
|
|
99
|
+
if (!input.includes("text"))
|
|
100
|
+
return "skip";
|
|
101
|
+
const model = {
|
|
102
|
+
id: key,
|
|
103
|
+
name: displayName(raw.name, key),
|
|
104
|
+
tool_call: true,
|
|
105
|
+
reasoning: raw.reasoning === true,
|
|
106
|
+
attachment: raw.attachment === true,
|
|
107
|
+
modalities: { input },
|
|
108
|
+
};
|
|
109
|
+
if (typeof raw.release_date === "string" && RELEASE_DATE.test(raw.release_date))
|
|
110
|
+
model.release_date = raw.release_date;
|
|
111
|
+
const context = finite(isRecord(raw.limit) ? raw.limit.context : undefined);
|
|
112
|
+
if (context !== undefined)
|
|
113
|
+
model.limit = { context };
|
|
114
|
+
if (isRecord(raw.cost)) {
|
|
115
|
+
const cost = {};
|
|
116
|
+
for (const field of ["input", "output"]) {
|
|
117
|
+
const price = finite(raw.cost[field]);
|
|
118
|
+
if (price !== undefined)
|
|
119
|
+
cost[field] = price;
|
|
120
|
+
}
|
|
121
|
+
if (Object.keys(cost).length)
|
|
122
|
+
model.cost = cost;
|
|
123
|
+
}
|
|
124
|
+
if (raw.status === "alpha" || raw.status === "beta")
|
|
125
|
+
model.status = raw.status;
|
|
126
|
+
return model;
|
|
127
|
+
}
|
|
128
|
+
function trimProvider(key, raw, options) {
|
|
129
|
+
const drop = (reason) => {
|
|
130
|
+
options.onDrop?.({ provider: key, reason });
|
|
131
|
+
return undefined;
|
|
132
|
+
};
|
|
133
|
+
if (!isRecord(raw))
|
|
134
|
+
return drop("invalid");
|
|
135
|
+
// Every provider-level string, not only the fields kept below: a brace
|
|
136
|
+
// anywhere means the entry was written for a templating system.
|
|
137
|
+
const { models: rawModels, ...fields } = raw;
|
|
138
|
+
if (hasBrace(key) || containsBrace(fields))
|
|
139
|
+
return drop("brace");
|
|
140
|
+
if (!PROVIDER_ID.test(key) || (raw.id !== undefined && raw.id !== key))
|
|
141
|
+
return drop("invalid");
|
|
142
|
+
const env = Array.isArray(raw.env) ? raw.env : [];
|
|
143
|
+
if (!env.every((name) => typeof name === "string" && ENV_NAME.test(name)))
|
|
144
|
+
return drop("invalid");
|
|
145
|
+
const models = idMap();
|
|
146
|
+
if (isRecord(rawModels)) {
|
|
147
|
+
let kept = 0;
|
|
148
|
+
for (const modelKey of Object.keys(rawModels).sort()) {
|
|
149
|
+
const rawModel = rawModels[modelKey];
|
|
150
|
+
if (hasBrace(modelKey) || containsBrace(rawModel)) {
|
|
151
|
+
options.onDrop?.({ provider: key, model: modelKey, reason: "brace" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const model = trimModel(modelKey, rawModel);
|
|
155
|
+
if (model === "invalid")
|
|
156
|
+
options.onDrop?.({ provider: key, model: modelKey, reason: "invalid" });
|
|
157
|
+
if (typeof model === "string")
|
|
158
|
+
continue;
|
|
159
|
+
models[modelKey] = model;
|
|
160
|
+
if (++kept >= MAX_MODELS_PER_PROVIDER)
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Field order is fixed (models last) so the snapshot text is stable.
|
|
165
|
+
const provider = { id: key, name: displayName(raw.name, key), env: env.slice(0, 8) };
|
|
166
|
+
if (typeof raw.npm === "string" && raw.npm.length <= 214 && !CONTROL.test(raw.npm))
|
|
167
|
+
provider.npm = raw.npm;
|
|
168
|
+
const api = apiUrl(raw.api);
|
|
169
|
+
if (api)
|
|
170
|
+
provider.api = api;
|
|
171
|
+
const doc = httpsUrl(raw.doc);
|
|
172
|
+
if (doc)
|
|
173
|
+
provider.doc = doc;
|
|
174
|
+
return { ...provider, models };
|
|
175
|
+
}
|
|
176
|
+
/** models.dev api.json (or an already-trimmed catalog) → the trimmed,
|
|
177
|
+
* brace-free provider map, sorted by provider and model id. Anything that is
|
|
178
|
+
* not an object yields an empty map. The provider map and every model map
|
|
179
|
+
* have no prototype, so looking up an id the catalog lacks gives undefined. */
|
|
180
|
+
export function trimModelsDevCatalog(raw, options = {}) {
|
|
181
|
+
const providers = idMap();
|
|
182
|
+
if (!isRecord(raw))
|
|
183
|
+
return providers;
|
|
184
|
+
for (const key of Object.keys(raw).sort().slice(0, MAX_PROVIDERS)) {
|
|
185
|
+
const provider = trimProvider(key, raw[key], options);
|
|
186
|
+
if (provider)
|
|
187
|
+
providers[key] = provider;
|
|
188
|
+
}
|
|
189
|
+
return providers;
|
|
190
|
+
}
|
|
191
|
+
/** Counts for logs and the build script's report. */
|
|
192
|
+
export function catalogStats(providers) {
|
|
193
|
+
let models = 0;
|
|
194
|
+
for (const provider of Object.values(providers))
|
|
195
|
+
models += Object.keys(provider.models).length;
|
|
196
|
+
return { providers: Object.keys(providers).length, models };
|
|
197
|
+
}
|
|
198
|
+
/** The on-disk form of a trimmed catalog. */
|
|
199
|
+
export function packCatalog(providers) {
|
|
200
|
+
return Object.fromEntries(Object.entries(providers).map(([id, provider]) => [id, {
|
|
201
|
+
...provider,
|
|
202
|
+
models: Object.fromEntries(Object.entries(provider.models).map(([modelId, model]) => {
|
|
203
|
+
const { id: _id, tool_call: _toolCall, ...stored } = model;
|
|
204
|
+
return [modelId, stored];
|
|
205
|
+
})),
|
|
206
|
+
}]));
|
|
207
|
+
}
|
|
208
|
+
/** A stored catalog back in models.dev's shape, ready for trimModelsDevCatalog.
|
|
209
|
+
* It only restores the two packed fields; trimming still validates the rest. */
|
|
210
|
+
export function unpackCatalog(stored) {
|
|
211
|
+
if (!isRecord(stored))
|
|
212
|
+
return stored;
|
|
213
|
+
return Object.fromEntries(Object.entries(stored).map(([id, provider]) => {
|
|
214
|
+
if (!isRecord(provider) || !isRecord(provider.models))
|
|
215
|
+
return [id, provider];
|
|
216
|
+
const models = Object.fromEntries(Object.entries(provider.models).map(([modelId, model]) => [modelId, isRecord(model) ? { ...model, id: modelId, tool_call: true } : model]));
|
|
217
|
+
return [id, { ...provider, models }];
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
|
|
1
|
+
export const PROVIDER_KEY_KINDS = ["anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral", "cerebras"];
|
|
2
2
|
const DEFAULT_URLS = {
|
|
3
3
|
anthropic: "https://api.anthropic.com",
|
|
4
|
+
openai: "https://api.openai.com/v1",
|
|
5
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
4
6
|
openaiCompat: "https://openrouter.ai/api/v1",
|
|
5
7
|
xai: "https://api.x.ai/v1",
|
|
6
8
|
mistral: "https://api.mistral.ai/v1",
|
|
9
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
7
10
|
};
|
|
8
11
|
const MAX_MODELS = 5;
|
|
9
12
|
function isLoopback(hostname) {
|
|
@@ -44,7 +47,7 @@ export async function checkProviderKey(input, fetchImpl = fetch, timeoutMs = 10_
|
|
|
44
47
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) {
|
|
45
48
|
return { ok: false, reason: "unexpected" };
|
|
46
49
|
}
|
|
47
|
-
const authenticate = input.provider === "openaiCompat"
|
|
50
|
+
const authenticate = (input.provider === "openaiCompat" || input.provider === "openrouter")
|
|
48
51
|
&& url.origin === "https://openrouter.ai" && url.pathname === "/api/v1/models";
|
|
49
52
|
if (authenticate)
|
|
50
53
|
url.pathname = "/api/v1/key";
|
|
@@ -64,7 +64,7 @@ export function recentWorkLines(threads, latest) {
|
|
|
64
64
|
}
|
|
65
65
|
/** The bot's recent work outside the conversation it is in now. */
|
|
66
66
|
export function recentWork(store, bot, opts) {
|
|
67
|
-
const threads = botThreads(store, bot, opts.userName).filter((thread) => thread.threadId !== opts.currentThreadId);
|
|
67
|
+
const threads = botThreads(store, bot, opts.userName).filter((thread) => thread.threadId !== opts.currentThreadId && (!opts.include || opts.include(thread.threadId)));
|
|
68
68
|
if (!threads.length)
|
|
69
69
|
return [];
|
|
70
70
|
const since = (opts.now ?? Date.now()) - (opts.windowMs ?? RECENT_WORK_WINDOW_MS);
|
|
@@ -60,6 +60,9 @@ export function serviceAllowed(method, path) {
|
|
|
60
60
|
export function resolveLoopbackTrust(input) {
|
|
61
61
|
const raw = (input.env ?? process.env).OMB_LOOPBACK_TRUST;
|
|
62
62
|
const requested = raw?.trim().toLowerCase();
|
|
63
|
+
if (input.cloudHome) {
|
|
64
|
+
return { trust: "service", reason: "OMB Cloud home", ...(raw !== undefined && requested !== "service" ? { warning: "OMB_LOOPBACK_TRUST is ignored on an OMB Cloud home: a local request is always a service" } : {}) };
|
|
65
|
+
}
|
|
63
66
|
if (input.desktopManaged) {
|
|
64
67
|
return { trust: "owner", reason: "desktop app", ...(raw !== undefined ? { warning: "OMB_LOOPBACK_TRUST is ignored in the desktop app" } : {}) };
|
|
65
68
|
}
|
|
@@ -156,6 +159,14 @@ export function isSameOrigin(req) {
|
|
|
156
159
|
const own = requestOrigin(req);
|
|
157
160
|
return own !== null && origin.trim().toLowerCase() === own;
|
|
158
161
|
}
|
|
162
|
+
/** A browser says this request came from this origin's own page: an
|
|
163
|
+
* `Origin` equal to this request's origin, or `Sec-Fetch-Site: same-origin`.
|
|
164
|
+
* Unlike isSameOrigin, a request that carries neither does not pass. */
|
|
165
|
+
export function provesSameOrigin(req) {
|
|
166
|
+
if (headerValue(req.headers.origin))
|
|
167
|
+
return isSameOrigin(req);
|
|
168
|
+
return headerValue(req.headers["sec-fetch-site"])?.trim().toLowerCase() === "same-origin";
|
|
169
|
+
}
|
|
159
170
|
/** Who to count a pairing attempt against. The server binds loopback, so a
|
|
160
171
|
* remote client always arrives through a proxy or tunnel on this machine;
|
|
161
172
|
* that proxy's X-Forwarded-For (Caddy overwrites any the client sent) names
|
|
@@ -266,6 +277,7 @@ export const CLIENT_ALLOW = [
|
|
|
266
277
|
{ methods: ["DELETE"], path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
|
|
267
278
|
{ methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks$/ },
|
|
268
279
|
{ methods: ["POST", "PATCH", "DELETE"], path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
280
|
+
{ methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+\/title$/ }, // Regenerate title: a rename by the bot's own engine
|
|
269
281
|
{ methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+\/profile$/ },
|
|
270
282
|
{ methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+$/ }, // display fields only: see clientBotPatchViolation
|
|
271
283
|
// approvals and cards
|
|
@@ -349,6 +361,10 @@ function mutatingPublicRoute(method, path) {
|
|
|
349
361
|
if (upper === "GET" &&
|
|
350
362
|
/^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/status$/.test(path))
|
|
351
363
|
return true;
|
|
364
|
+
// Remote viewer config and its WebSocket upgrade hand out a desktop's VNC
|
|
365
|
+
// password and control. Native owners open the direct viewer instead.
|
|
366
|
+
if (path.startsWith("/api/desktop-viewer/"))
|
|
367
|
+
return true;
|
|
352
368
|
if (["GET", "HEAD", "OPTIONS"].includes(upper))
|
|
353
369
|
return false;
|
|
354
370
|
// Agent integrations have their own high-entropy, per-boot authorization
|
|
@@ -394,6 +410,13 @@ export function resolveRequestAuth(req, options) {
|
|
|
394
410
|
if (session && via) {
|
|
395
411
|
if (via === "cookie" && !isSameOrigin(req))
|
|
396
412
|
return deny(403, "forbidden: cross-origin request");
|
|
413
|
+
// A browser sign-in's session is its browser's cookie, never a bearer token, and
|
|
414
|
+
// its changes come from this origin's own page, which a browser says so.
|
|
415
|
+
if (via === "bearer" && session.cookieOnly)
|
|
416
|
+
return deny(401, "unauthorized: this session belongs to a browser; sign in with a pairing code");
|
|
417
|
+
if (via === "cookie" && session.cookieOnly && !["GET", "HEAD", "OPTIONS"].includes(method) && !provesSameOrigin(req)) {
|
|
418
|
+
return deny(403, "forbidden: this browser's session makes changes only from its own page");
|
|
419
|
+
}
|
|
397
420
|
const needed = requiredScope(method, path, options.features ?? {});
|
|
398
421
|
if (!session.scopes.includes(needed)) {
|
|
399
422
|
return deny(403, `forbidden: this session lacks the ${needed} scope`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { HOSTED_PROVIDER_SETTINGS_ERROR } from "../hosted-models.js";
|
|
2
|
+
import { PASS } from "./table.js";
|
|
3
|
+
const ROUTE = /^\/api\/instances\/([\w.-]+)\/leftover-files(\/remove)?$/u;
|
|
4
|
+
export const BUSY_LEFTOVER_ERROR = "Already looking for leftover files. Try again in a moment.";
|
|
5
|
+
export function createAntigravityLeftoverRoutes(deps) {
|
|
6
|
+
// One scan or delete at a time: both walk thousands of files.
|
|
7
|
+
let busy = false;
|
|
8
|
+
return async ({ req, res, path, method, json, readBody }) => {
|
|
9
|
+
const match = ROUTE.exec(path);
|
|
10
|
+
if (!match)
|
|
11
|
+
return PASS;
|
|
12
|
+
const removing = Boolean(match[2]);
|
|
13
|
+
if (method !== (removing ? "POST" : "GET"))
|
|
14
|
+
return PASS;
|
|
15
|
+
if (deps.hosted)
|
|
16
|
+
return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
|
|
17
|
+
if (removing) {
|
|
18
|
+
// Same non-simple-request gate as the other instance actions.
|
|
19
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
20
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
21
|
+
}
|
|
22
|
+
await readBody(req, 4096);
|
|
23
|
+
}
|
|
24
|
+
if (!deps.isAntigravity(match[1]))
|
|
25
|
+
return json(res, 404, { error: "Only Antigravity has leftover files to clear." });
|
|
26
|
+
if (busy)
|
|
27
|
+
return json(res, 409, { error: BUSY_LEFTOVER_ERROR });
|
|
28
|
+
busy = true;
|
|
29
|
+
res.setHeader("cache-control", "no-store");
|
|
30
|
+
try {
|
|
31
|
+
if (!removing) {
|
|
32
|
+
const scan = await deps.find();
|
|
33
|
+
return json(res, 200, { bytes: scan.bytes, folders: scan.folders.length, complete: scan.complete });
|
|
34
|
+
}
|
|
35
|
+
const removed = await deps.remove();
|
|
36
|
+
return json(res, 200, { freedBytes: removed.freedBytes, removed: removed.removed, remaining: removed.remaining });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
busy = false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -41,6 +41,7 @@ export function createBotMemoryRoutes(deps) {
|
|
|
41
41
|
return async ({ req, res, url, path, method, auth, json, readBody }) => {
|
|
42
42
|
/** scratch for route matches, shared by every `path.match` below */
|
|
43
43
|
let m = null;
|
|
44
|
+
const write = (botId, change) => deps.ownersWrite ? deps.ownersWrite(auth, botId, change) : change();
|
|
44
45
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
|
|
45
46
|
if (m && method === "GET") {
|
|
46
47
|
if (!deps.bot(m[1]))
|
|
@@ -49,7 +50,9 @@ export function createBotMemoryRoutes(deps) {
|
|
|
49
50
|
const overview = memoryOverview(m[1]);
|
|
50
51
|
// `text` and `truncated` ride along one release for clients of the
|
|
51
52
|
// old whole-file shape; the panel reads the file through /memory/file
|
|
52
|
-
|
|
53
|
+
const review = deps.lendingReview?.needed(m[1]);
|
|
54
|
+
return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated,
|
|
55
|
+
...(review ? { lendingReview: review } : {}) });
|
|
53
56
|
}
|
|
54
57
|
catch (error) {
|
|
55
58
|
return replyMemoryError(res, error);
|
|
@@ -65,7 +68,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
65
68
|
if (!parsed.success)
|
|
66
69
|
return json(res, 400, { error: "text must be a string" });
|
|
67
70
|
try {
|
|
68
|
-
const
|
|
71
|
+
const botId = m[1];
|
|
72
|
+
const { doc } = write(botId, () => journalMemoryWrite(botId, MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" }));
|
|
69
73
|
return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
|
|
70
74
|
}
|
|
71
75
|
catch (error) {
|
|
@@ -76,6 +80,24 @@ export function createBotMemoryRoutes(deps) {
|
|
|
76
80
|
return replyMemoryError(res, error);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
83
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/reviewed$/);
|
|
84
|
+
if (m && method === "POST" && deps.lendingReview) {
|
|
85
|
+
if (!deps.bot(m[1]))
|
|
86
|
+
return json(res, 404, { error: "no such bot" });
|
|
87
|
+
if (!deps.lendingReview.owner(auth)) {
|
|
88
|
+
return json(res, 403, { error: "Only the owner of this Cloud can review this bot's memory, from one of their own devices." });
|
|
89
|
+
}
|
|
90
|
+
// Bound to what the owner was shown: the token names the files exactly
|
|
91
|
+
// as the panel read them. Anything changed since is shown again.
|
|
92
|
+
const parsed = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) }).safeParse(await readBody(req));
|
|
93
|
+
if (!parsed.success)
|
|
94
|
+
return json(res, 400, { error: "token is required: the lendingReview token the Memory panel was shown" });
|
|
95
|
+
if (!deps.lendingReview.accept(m[1], parsed.data.token)) {
|
|
96
|
+
return json(res, 409, { error: "This bot's memory changed since you looked. Review it again.", code: "changed",
|
|
97
|
+
lendingReview: deps.lendingReview.needed(m[1]) });
|
|
98
|
+
}
|
|
99
|
+
return json(res, 200, { ok: true, lendingReview: false });
|
|
100
|
+
}
|
|
79
101
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
|
|
80
102
|
if (m && method === "GET") {
|
|
81
103
|
if (!deps.bot(m[1]))
|
|
@@ -96,11 +118,12 @@ export function createBotMemoryRoutes(deps) {
|
|
|
96
118
|
if (!parsed.success)
|
|
97
119
|
return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
|
|
98
120
|
try {
|
|
99
|
-
const
|
|
121
|
+
const botId = m[1];
|
|
122
|
+
const { doc, entry } = write(botId, () => journalMemoryWrite(botId, parsed.data.path, parsed.data.text, {
|
|
100
123
|
actor: "person",
|
|
101
124
|
via: "ui",
|
|
102
125
|
expectedHash: parsed.data.expectedHash,
|
|
103
|
-
});
|
|
126
|
+
}));
|
|
104
127
|
return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
105
128
|
}
|
|
106
129
|
catch (error) {
|
|
@@ -112,7 +135,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
112
135
|
return json(res, 404, { error: "no such bot" });
|
|
113
136
|
const file = url.searchParams.get("path") ?? "";
|
|
114
137
|
try {
|
|
115
|
-
const
|
|
138
|
+
const botId = m[1];
|
|
139
|
+
const entry = write(botId, () => journalMemoryDelete(botId, file, { actor: "person", via: "ui" }));
|
|
116
140
|
return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
117
141
|
}
|
|
118
142
|
catch (error) {
|
|
@@ -134,7 +158,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
134
158
|
if (!deps.bot(m[1]))
|
|
135
159
|
return json(res, 404, { error: "no such bot" });
|
|
136
160
|
await flushMemoryJournal(m[1]);
|
|
137
|
-
const
|
|
161
|
+
const botId = m[1], changeId = m[2];
|
|
162
|
+
const result = write(botId, () => revertMemoryChange(botId, changeId));
|
|
138
163
|
if (!result.ok)
|
|
139
164
|
return json(res, result.status, { error: result.error });
|
|
140
165
|
return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
|