openmausbot 0.1.91 → 0.1.92
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-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.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/container-mcp.js +195 -15
- package/dist-server/drivers/agents-proxy.js +5 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +9300 -4155
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +3910 -1182
- package/dist-server/pair-cli.js +3910 -1182
- package/dist-server/server/boat.js +2 -2
- 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/config.js +174 -14
- package/dist-server/server/container-computer.js +3 -2
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- 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/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +312 -34
- 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 +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1338 -184
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- 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 +4 -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 +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +62 -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 +79 -12
- 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 +3 -0
- package/dist-server/server/workspace.js +65 -15
- 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 +196 -15
- package/dist-server/workspace-backup.worker.js +73 -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
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
// The maintained OpenCode CLI through its ACP stdio interface. OpenCode is
|
|
2
2
|
// the harness; Zen, Go, OpenRouter, and user-configured/local providers are
|
|
3
3
|
// models discovered from that harness rather than separate OpenMaus drivers.
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
8
|
+
import { ATTACHMENTS_DIR } from "../../attachments.js";
|
|
9
|
+
import { cloudHomeConfigured } from "../../cloud-home.js";
|
|
10
|
+
import { DATA_DIR } from "../../config.js";
|
|
11
|
+
import { hostedWorkspaceConfigured } from "../../enterprise.js";
|
|
7
12
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
8
|
-
import { createAcpDriver } from "./core.js";
|
|
13
|
+
import { createAcpDriver, offeredModels } from "./core.js";
|
|
9
14
|
import { titleCaseModelId } from "../../contracts.js";
|
|
10
|
-
import { execCli } from "../../procs.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
15
|
+
import { execCli, killCliTree, spawnCli } from "../../procs.js";
|
|
16
|
+
/** Nothing is offered until OpenCode itself answers. An invented default is
|
|
17
|
+
* exactly what broke bots when Zen retired `x-preview-f-free`. */
|
|
18
|
+
const NO_MODELS = { default: "", options: [] };
|
|
19
|
+
/** The last catalog each OpenCode binary reported, so a failed refresh keeps
|
|
20
|
+
* a usable picker and the setup check needs no process of its own. */
|
|
21
|
+
const lastSuccessfulCatalog = new Map();
|
|
22
|
+
/** Zen prices from `models --verbose` (V1): cost 0 means free. V2 prints no
|
|
23
|
+
* metadata, so an id ending in `-free` is the fallback signal. */
|
|
24
|
+
const freeModels = new Set();
|
|
25
|
+
const paidModels = new Set();
|
|
26
|
+
const DISCOVERY_TIMEOUT_MS = 20_000;
|
|
27
|
+
const DISCOVERY_PROBE_TTL_MS = 30_000;
|
|
28
|
+
const discoveryProbeCache = new Map();
|
|
24
29
|
function labelForModel(id) {
|
|
25
30
|
return titleCaseModelId(id, /[-_.]+/);
|
|
26
31
|
}
|
|
@@ -53,14 +58,39 @@ function localModelRecord(record) {
|
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
|
-
/**
|
|
61
|
+
/** Whether a model costs nothing to run: Zen's own price when `models
|
|
62
|
+
* --verbose` reported one, else the `-free` suffix Zen gives free models. */
|
|
63
|
+
export function isFreeOpenCodeModel(id) {
|
|
64
|
+
if (freeModels.has(id))
|
|
65
|
+
return true;
|
|
66
|
+
if (paidModels.has(id))
|
|
67
|
+
return false;
|
|
68
|
+
return /(?:^|[-_/])free$/iu.test(id);
|
|
69
|
+
}
|
|
70
|
+
/** The model OpenMaus runs when nobody chose one, or when the chosen one is
|
|
71
|
+
* gone. OpenCode's own pick (`current`) wins when it is free. Otherwise a
|
|
72
|
+
* free model does: OpenCode 2 with a key picks a paid model on its own, and
|
|
73
|
+
* a silent switch to per-token billing is not ours to make. Only an
|
|
74
|
+
* all-paid catalog (the person's own providers) falls back to OpenCode's
|
|
75
|
+
* pick. */
|
|
76
|
+
export function preferredOpenCodeModel(offered, current) {
|
|
77
|
+
if (current && offered.includes(current) && isFreeOpenCodeModel(current))
|
|
78
|
+
return current;
|
|
79
|
+
const free = offered.filter(isFreeOpenCodeModel);
|
|
80
|
+
return free.find((id) => id.startsWith("opencode/"))
|
|
81
|
+
?? free[0]
|
|
82
|
+
?? (current && offered.includes(current) ? current : offered[0] ?? "");
|
|
83
|
+
}
|
|
84
|
+
/** Parse the metadata printed by `opencode models --verbose` (V1 only).
|
|
57
85
|
*
|
|
58
|
-
*
|
|
86
|
+
* The output is a sequence of `provider/model` header lines, each followed
|
|
59
87
|
* by one JSON object. Model IDs can themselves contain `/` (OpenRouter), so
|
|
60
88
|
* only the first separator identifies the provider. Older CLIs may print just
|
|
61
89
|
* the headers; those still produce a usable catalog without metadata. */
|
|
62
|
-
|
|
90
|
+
function parseOpenCodeModels(stdout) {
|
|
63
91
|
const options = [];
|
|
92
|
+
const free = [];
|
|
93
|
+
const paid = [];
|
|
64
94
|
const seen = new Set();
|
|
65
95
|
let slug = null;
|
|
66
96
|
let jsonLines = [];
|
|
@@ -85,6 +115,12 @@ export function parseOpenCodeModelsOutput(stdout) {
|
|
|
85
115
|
}
|
|
86
116
|
if (record.status === "deprecated")
|
|
87
117
|
return;
|
|
118
|
+
const cost = record.cost && typeof record.cost === "object" && !Array.isArray(record.cost)
|
|
119
|
+
? record.cost
|
|
120
|
+
: null;
|
|
121
|
+
if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
|
|
122
|
+
(cost.input === 0 && cost.output === 0 ? free : paid).push(slug);
|
|
123
|
+
}
|
|
88
124
|
const name = typeof record.name === "string" && record.name.trim()
|
|
89
125
|
? record.name.trim()
|
|
90
126
|
: labelForModel(model);
|
|
@@ -119,56 +155,322 @@ export function parseOpenCodeModelsOutput(stdout) {
|
|
|
119
155
|
jsonLines.push(line);
|
|
120
156
|
}
|
|
121
157
|
flush();
|
|
122
|
-
|
|
158
|
+
return { options, free, paid };
|
|
159
|
+
}
|
|
160
|
+
function rememberPrices(parsed) {
|
|
161
|
+
for (const id of parsed.free) {
|
|
162
|
+
freeModels.add(id);
|
|
163
|
+
paidModels.delete(id);
|
|
164
|
+
}
|
|
165
|
+
for (const id of parsed.paid) {
|
|
166
|
+
paidModels.add(id);
|
|
167
|
+
freeModels.delete(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** The `models --verbose` inventory as a catalog (V1). Discovery prefers the
|
|
171
|
+
* ACP session's own list and uses this only for metadata; it remains the
|
|
172
|
+
* catalog when the ACP probe cannot answer. */
|
|
173
|
+
export function parseOpenCodeModelsOutput(stdout) {
|
|
174
|
+
const parsed = parseOpenCodeModels(stdout);
|
|
175
|
+
if (!parsed.options.length)
|
|
123
176
|
return null;
|
|
124
|
-
|
|
125
|
-
return { default: (
|
|
177
|
+
rememberPrices(parsed);
|
|
178
|
+
return { default: preferredOpenCodeModel(parsed.options.map((option) => option.id)), options: parsed.options };
|
|
126
179
|
}
|
|
127
|
-
|
|
180
|
+
/** The catalog OpenCode itself offers a session, from the `model` select in
|
|
181
|
+
* session/new's configOptions: the exact ids `session/set_config_option`
|
|
182
|
+
* accepts, in the exact environment of the process. Same list as `opencode
|
|
183
|
+
* models` on V1 and the only reliable one on V2. */
|
|
184
|
+
export function catalogFromOpenCodeSession(result) {
|
|
185
|
+
const configOptions = result?.configOptions;
|
|
186
|
+
const model = Array.isArray(configOptions)
|
|
187
|
+
? configOptions.find((option) => option?.id === "model" && option?.type === "select")
|
|
188
|
+
: null;
|
|
189
|
+
const ids = offeredModels(result, "model");
|
|
190
|
+
if (!model || !ids.length)
|
|
191
|
+
return null;
|
|
192
|
+
const names = new Map();
|
|
193
|
+
const visit = (entries) => {
|
|
194
|
+
if (!Array.isArray(entries))
|
|
195
|
+
return;
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (typeof entry?.value === "string" && typeof entry.name === "string")
|
|
198
|
+
names.set(entry.value, entry.name);
|
|
199
|
+
else if (Array.isArray(entry?.options))
|
|
200
|
+
visit(entry.options);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
visit(model.options);
|
|
204
|
+
const options = [...new Set(ids)].filter(validModelSlug).map((id) => {
|
|
205
|
+
const separator = id.indexOf("/");
|
|
206
|
+
// V1 names "OpenCode Zen/Big Pickle", V2 "opencode/Big Pickle": the
|
|
207
|
+
// provider comes from the id either way.
|
|
208
|
+
const raw = names.get(id)?.trim() ?? "";
|
|
209
|
+
const name = (raw.includes("/") ? raw.slice(raw.indexOf("/") + 1) : raw).trim() || labelForModel(id.slice(separator + 1));
|
|
210
|
+
return { id, label: `${providerLabel(id.slice(0, separator))} · ${name}` };
|
|
211
|
+
});
|
|
212
|
+
return options.length
|
|
213
|
+
? { options, current: typeof model.currentValue === "string" ? model.currentValue : null }
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
function runOpenCodeModelsVerbose(cli, environment) {
|
|
128
217
|
return new Promise((resolve, reject) => {
|
|
129
|
-
execCli(cli, ["models",
|
|
218
|
+
execCli(cli, ["models", "--verbose"], { timeout: 20_000, maxBuffer: 8 * 1024 * 1024, env: environment }, (error, stdout, stderr) => {
|
|
130
219
|
if (error) {
|
|
131
|
-
reject(new Error(stderr?.trim() || error.message, { cause: error })
|
|
220
|
+
reject(Object.assign(new Error(stderr?.trim() || error.message, { cause: error }), {
|
|
221
|
+
timedOut: error.killed === true,
|
|
222
|
+
}));
|
|
132
223
|
return;
|
|
133
224
|
}
|
|
134
225
|
resolve(stdout);
|
|
135
226
|
});
|
|
136
227
|
});
|
|
137
228
|
}
|
|
229
|
+
function runGit(directory, args) {
|
|
230
|
+
// The person's git settings stay out: no signing prompt, no hooks, no
|
|
231
|
+
// repository named by a GIT_* variable.
|
|
232
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_")));
|
|
233
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
234
|
+
execFile("git", [
|
|
235
|
+
"-c", "user.name=OpenMaus", "-c", "user.email=model-discovery@openmaus.invalid",
|
|
236
|
+
"-c", "commit.gpgsign=false", "-c", `core.hooksPath=${join(directory, ".git", "no-hooks")}`,
|
|
237
|
+
...args,
|
|
238
|
+
], { cwd: directory, env: { ...env, GIT_TERMINAL_PROMPT: "0" }, timeout: 10_000, windowsHide: true }, (error) => {
|
|
239
|
+
if (error)
|
|
240
|
+
rejectRun(error);
|
|
241
|
+
else
|
|
242
|
+
resolveRun();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
let discoveryFolder = null;
|
|
247
|
+
/** A folder OpenMaus owns for catalog probes, made its own git project once.
|
|
248
|
+
* OpenCode records every session, empty ones included, under the project of
|
|
249
|
+
* its working folder, and every folder outside git shares one global project:
|
|
250
|
+
* `opencode run --continue` in any of them would resume OpenMaus's empty probe
|
|
251
|
+
* instead of the person's last session. Without git the probe still works and
|
|
252
|
+
* its session lands in the global list, as before. */
|
|
253
|
+
function discoveryDirectory() {
|
|
254
|
+
discoveryFolder ??= (async () => {
|
|
255
|
+
const directory = join(DATA_DIR, "providers", "opencode", "discovery");
|
|
256
|
+
mkdirSync(directory, { recursive: true });
|
|
257
|
+
const marker = join(directory, ".openmaus-project");
|
|
258
|
+
if (!existsSync(marker)) {
|
|
259
|
+
try {
|
|
260
|
+
await runGit(directory, ["init", "-q"]);
|
|
261
|
+
await runGit(directory, ["commit", "-q", "--allow-empty", "--no-verify", "-m", "OpenMaus model discovery"]);
|
|
262
|
+
writeFileSync(marker, "OpenCode files this folder's sessions under its own project.\n");
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// No git: probes run here all the same.
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return directory;
|
|
269
|
+
})();
|
|
270
|
+
return discoveryFolder;
|
|
271
|
+
}
|
|
272
|
+
/** Open one ACP session on the binary and environment a turn uses, and
|
|
273
|
+
* return what session/new answered. No prompt is sent and the process is
|
|
274
|
+
* stopped as soon as the answer arrives. */
|
|
275
|
+
export async function probeOpenCodeSession(cli, environment, folder, timeoutMs = DISCOVERY_TIMEOUT_MS) {
|
|
276
|
+
const cwd = folder ?? await discoveryDirectory();
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
let child;
|
|
279
|
+
try {
|
|
280
|
+
child = spawnCli(cli, ["acp"], { cwd, env: environment, stdio: ["pipe", "pipe", "pipe"] });
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
reject(error);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
let settled = false;
|
|
287
|
+
let buffer = "";
|
|
288
|
+
let nextId = 0;
|
|
289
|
+
const pending = new Map();
|
|
290
|
+
const write = (message) => {
|
|
291
|
+
try {
|
|
292
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
};
|
|
296
|
+
const finish = (error, value) => {
|
|
297
|
+
if (settled)
|
|
298
|
+
return;
|
|
299
|
+
settled = true;
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
try {
|
|
302
|
+
child.stdin.end();
|
|
303
|
+
}
|
|
304
|
+
catch { }
|
|
305
|
+
void killCliTree(child);
|
|
306
|
+
if (error)
|
|
307
|
+
reject(error);
|
|
308
|
+
else
|
|
309
|
+
resolve(value);
|
|
310
|
+
};
|
|
311
|
+
const timer = setTimeout(() => finish(Object.assign(new Error("OpenCode did not list its models in time"), { timedOut: true })), timeoutMs);
|
|
312
|
+
timer.unref?.();
|
|
313
|
+
child.on("error", (error) => finish(error));
|
|
314
|
+
child.on("close", (code) => finish(new Error(`OpenCode exited ${code} while listing its models`)));
|
|
315
|
+
child.stderr.resume();
|
|
316
|
+
child.stdout.setEncoding("utf8");
|
|
317
|
+
child.stdout.on("data", (chunk) => {
|
|
318
|
+
buffer += chunk;
|
|
319
|
+
let newline;
|
|
320
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
321
|
+
const line = buffer.slice(0, newline);
|
|
322
|
+
buffer = buffer.slice(newline + 1);
|
|
323
|
+
let message;
|
|
324
|
+
try {
|
|
325
|
+
message = JSON.parse(line);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (message?.id !== undefined && typeof message.method === "string") {
|
|
331
|
+
// A probe answers no request: refuse rather than leave it hanging.
|
|
332
|
+
write({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "method not found" } });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const waiter = typeof message?.id === "number" ? pending.get(message.id) : undefined;
|
|
336
|
+
if (!waiter)
|
|
337
|
+
continue;
|
|
338
|
+
pending.delete(message.id);
|
|
339
|
+
if (message.error) {
|
|
340
|
+
waiter.reject(Object.assign(new Error(String(message.error.message ?? "ACP request failed")), {
|
|
341
|
+
code: message.error.code,
|
|
342
|
+
data: message.error.data,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
waiter.resolve(message.result);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const request = (method, params) => new Promise((resolveRequest, rejectRequest) => {
|
|
350
|
+
const id = ++nextId;
|
|
351
|
+
pending.set(id, { resolve: resolveRequest, reject: rejectRequest });
|
|
352
|
+
write({ jsonrpc: "2.0", id, method, params });
|
|
353
|
+
});
|
|
354
|
+
void (async () => {
|
|
355
|
+
await request("initialize", {
|
|
356
|
+
protocolVersion: 1,
|
|
357
|
+
clientInfo: { name: "openmausbot", version: "0.0.0" },
|
|
358
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
359
|
+
});
|
|
360
|
+
return request("session/new", { cwd, mcpServers: [] });
|
|
361
|
+
})().then((result) => finish(null, result), (error) => finish(error));
|
|
362
|
+
});
|
|
363
|
+
}
|
|
138
364
|
/** Ask the same OpenCode binary that will run ACP for its effective catalog.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
365
|
+
*
|
|
366
|
+
* The list comes from an ACP session/new in the turn's own environment: it
|
|
367
|
+
* is what the session will accept, on OpenCode 1 and 2 alike. `models
|
|
368
|
+
* --verbose` (OpenCode 1 only; 2 rejects the flag) runs alongside for
|
|
369
|
+
* metadata the session does not carry: prices, context windows, reasoning
|
|
370
|
+
* variants, and which models are local. Plain `opencode models` is not a
|
|
371
|
+
* fallback: on OpenCode 2 it answers from a shared background service that
|
|
372
|
+
* keeps whatever environment it started with, and its first answer after
|
|
373
|
+
* that service starts is empty. */
|
|
141
374
|
export async function discoverOpenCodeModels(environment, cli = "opencode") {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
375
|
+
const probeSession = () => probeOpenCodeSession(cli, environment);
|
|
376
|
+
const readMetadata = () => runOpenCodeModelsVerbose(cli, environment).then(parseOpenCodeModels);
|
|
377
|
+
let [session, verbose] = await Promise.allSettled([probeSession(), readMetadata()]);
|
|
378
|
+
// Two OpenCode processes starting together in a home that has never run
|
|
379
|
+
// OpenCode race to create its database, and one of them fails (seen 1 in 8
|
|
380
|
+
// fresh homes). Once is enough to lose Zen's prices, which is what keeps a
|
|
381
|
+
// paid model from becoming the default, so a failed half runs again alone.
|
|
382
|
+
// A half that timed out is not retried: a wedged CLI must not double the
|
|
383
|
+
// wait at startup.
|
|
384
|
+
const retry = (result) => result.status === "rejected" && result.reason?.timedOut !== true;
|
|
385
|
+
if (retry(session))
|
|
386
|
+
[session] = await Promise.allSettled([probeSession()]);
|
|
387
|
+
if (retry(verbose))
|
|
388
|
+
[verbose] = await Promise.allSettled([readMetadata()]);
|
|
389
|
+
const metadata = verbose.status === "fulfilled" ? verbose.value : null;
|
|
390
|
+
if (metadata)
|
|
391
|
+
rememberPrices(metadata);
|
|
392
|
+
const live = session.status === "fulfilled" ? catalogFromOpenCodeSession(session.value) : null;
|
|
393
|
+
let catalog = null;
|
|
394
|
+
if (live) {
|
|
395
|
+
const details = new Map((metadata?.options ?? []).map((option) => [option.id, option]));
|
|
396
|
+
const options = live.options.map((option) => ({ ...option, ...details.get(option.id), id: option.id }));
|
|
397
|
+
catalog = { default: preferredOpenCodeModel(options.map((option) => option.id), live.current), options };
|
|
148
398
|
}
|
|
149
|
-
|
|
150
|
-
|
|
399
|
+
else if (metadata?.options.length) {
|
|
400
|
+
catalog = { default: preferredOpenCodeModel(metadata.options.map((option) => option.id)), options: metadata.options };
|
|
151
401
|
}
|
|
402
|
+
if (!catalog)
|
|
403
|
+
return lastSuccessfulCatalog.get(cli) ?? NO_MODELS;
|
|
404
|
+
lastSuccessfulCatalog.set(cli, catalog);
|
|
405
|
+
return catalog;
|
|
406
|
+
}
|
|
407
|
+
/** Whether this OpenCode can run a turn at all: it offers at least one
|
|
408
|
+
* model. Zen's free models need no login, so a working CLI almost always
|
|
409
|
+
* can. Answered from the last discovery when there is one. */
|
|
410
|
+
export async function canRunOpenCode(environment, cli, discover = discoverOpenCodeModels) {
|
|
411
|
+
if (lastSuccessfulCatalog.get(cli)?.options.length)
|
|
412
|
+
return true;
|
|
413
|
+
const cached = discoveryProbeCache.get(cli);
|
|
414
|
+
if (cached && cached.expiresAt > Date.now())
|
|
415
|
+
return cached.result;
|
|
416
|
+
const entry = { expiresAt: Number.POSITIVE_INFINITY, result: Promise.resolve(false) };
|
|
417
|
+
entry.result = discover(environment, cli)
|
|
418
|
+
.then((catalog) => catalog.options.length > 0, () => false)
|
|
419
|
+
.finally(() => {
|
|
420
|
+
entry.expiresAt = Date.now() + DISCOVERY_PROBE_TTL_MS;
|
|
421
|
+
});
|
|
422
|
+
discoveryProbeCache.set(cli, entry);
|
|
423
|
+
return entry.result;
|
|
152
424
|
}
|
|
153
425
|
export function resetOpenCodeModelCache() {
|
|
154
|
-
|
|
155
|
-
|
|
426
|
+
discoveryFolder = null;
|
|
427
|
+
lastSuccessfulCatalog.clear();
|
|
428
|
+
discoveryProbeCache.clear();
|
|
429
|
+
freeModels.clear();
|
|
430
|
+
paidModels.clear();
|
|
156
431
|
}
|
|
157
432
|
/** Compatibility export for older tests/imports while the product migrates
|
|
158
433
|
* from the Go-only name. */
|
|
159
434
|
export const resetOpenCodeGoModelCache = resetOpenCodeModelCache;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
435
|
+
/** Provider keys OpenCode reads from its environment, as it does in a
|
|
436
|
+
* terminal: with ANTHROPIC_API_KEY set, `opencode` lists Anthropic's models.
|
|
437
|
+
* Keys OpenMaus saves for another engine (xAI, Mistral, the workspace
|
|
438
|
+
* Anthropic key) are workspace credentials under other names and never
|
|
439
|
+
* ride along. */
|
|
440
|
+
export const OPENCODE_PROVIDER_ENV = [
|
|
441
|
+
"ANTHROPIC_API_KEY",
|
|
442
|
+
"OPENAI_API_KEY",
|
|
443
|
+
"GEMINI_API_KEY",
|
|
444
|
+
"GOOGLE_API_KEY",
|
|
445
|
+
"KIMI_API_KEY",
|
|
446
|
+
"MOONSHOT_API_KEY",
|
|
447
|
+
"MINIMAX_API_KEY",
|
|
448
|
+
];
|
|
449
|
+
/** Whether OpenCode may read provider keys from the server's own
|
|
450
|
+
* environment. Only when that environment is the person's own shell: not on a
|
|
451
|
+
* Cloud home, a hosted team workspace or an organisation-managed desktop, and
|
|
452
|
+
* not on a server whose sign-in list lets other people in, where every
|
|
453
|
+
* member's bot would otherwise run on, and bill, the operator's key. */
|
|
454
|
+
export function openCodeProviderKeysAllowed(state) {
|
|
455
|
+
return !state.cloudHome && !state.hostedWorkspace && !state.organisationManaged && !state.sharedSignIn;
|
|
456
|
+
}
|
|
457
|
+
let providerKeysAllowed = () => openCodeProviderKeysAllowed({
|
|
458
|
+
cloudHome: cloudHomeConfigured(),
|
|
459
|
+
hostedWorkspace: hostedWorkspaceConfigured(),
|
|
460
|
+
organisationManaged: false,
|
|
461
|
+
sharedSignIn: false,
|
|
462
|
+
});
|
|
463
|
+
/** The server sets this before any driver is created, and narrows it with
|
|
464
|
+
* Company enrollment and its sign-in list. */
|
|
465
|
+
export function setOpenCodeProviderKeyPolicy(allowed) {
|
|
466
|
+
providerKeysAllowed = allowed;
|
|
467
|
+
}
|
|
468
|
+
function withholdProviderKeysWhenManaged(env) {
|
|
469
|
+
if (providerKeysAllowed())
|
|
470
|
+
return;
|
|
471
|
+
for (const key of OPENCODE_PROVIDER_ENV)
|
|
170
472
|
delete env[key];
|
|
171
|
-
}
|
|
473
|
+
}
|
|
172
474
|
function opencodeConfigDir(env) {
|
|
173
475
|
const home = env.HOME || env.USERPROFILE || homedir();
|
|
174
476
|
return join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
|
|
@@ -281,41 +583,232 @@ function hasStoredOpenCodeAuth(env) {
|
|
|
281
583
|
}
|
|
282
584
|
});
|
|
283
585
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
586
|
+
/** Whether `path` is `folder` itself or somewhere inside it. */
|
|
587
|
+
function withinFolder(path, folder) {
|
|
588
|
+
const rest = relative(resolvePath(folder), resolvePath(path));
|
|
589
|
+
return rest === "" || (rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest));
|
|
590
|
+
}
|
|
591
|
+
/** Whether a turn works in one of OpenMaus's own folders: a bot's shared
|
|
592
|
+
* folder or a conversation's private one (server/workspace.ts). Everything in
|
|
593
|
+
* them was written by bots, so none of it may become OpenCode configuration. */
|
|
594
|
+
export function openMausOwnsWorkingFolder(cwd) {
|
|
595
|
+
return [join(DATA_DIR, "workspaces"), join(DATA_DIR, "task-workspaces")].some((root) => withinFolder(cwd, root) && resolvePath(cwd) !== resolvePath(root));
|
|
596
|
+
}
|
|
597
|
+
/** Folders OpenMaus owns that an OpenCode bot may use outside its working
|
|
598
|
+
* folder without an approval card: the attachments people send (every turn
|
|
599
|
+
* that read one stalled on a card) and this bot's own shared folder, which
|
|
600
|
+
* holds the memory its prompt points at.
|
|
601
|
+
*
|
|
602
|
+
* Never a folder at or above the turn's working folder. OpenCode reads
|
|
603
|
+
* opencode.json, .opencode/ and AGENTS.md from every folder above its working
|
|
604
|
+
* folder, so a file planted there would configure the bot's later
|
|
605
|
+
* conversations: one prompt-injected turn could allow every folder for good.
|
|
606
|
+
* That is also why earlier conversations (task-workspaces/<bot>, the parent of
|
|
607
|
+
* every conversation's folder) still ask.
|
|
608
|
+
*
|
|
609
|
+
* Accepted: OpenCode's folder rule covers writes as well as reads, and it
|
|
610
|
+
* allows edits and shell by default, so a bot can also change or delete files
|
|
611
|
+
* in the attachments folder, which holds every conversation's uploads under
|
|
612
|
+
* random names. Its edit rules match paths relative to the project and its
|
|
613
|
+
* shell rules match command text, so neither can fence one folder reliably. */
|
|
614
|
+
export function openCodeOwnedDirectories(botId, cwd) {
|
|
615
|
+
const directories = [ATTACHMENTS_DIR];
|
|
616
|
+
// One path segment only: a bot id never names a folder outside its own.
|
|
617
|
+
if (botId && /^[\w-][\w.-]*$/u.test(botId) && botId !== "..")
|
|
618
|
+
directories.push(join(DATA_DIR, "workspaces", botId));
|
|
619
|
+
return cwd ? directories.filter((directory) => !withinFolder(cwd, directory)) : directories;
|
|
620
|
+
}
|
|
621
|
+
/** JSON with comments and trailing commas, as OpenCode reads its config. */
|
|
622
|
+
function parseJsonc(text) {
|
|
623
|
+
let plain = "";
|
|
624
|
+
let inString = false;
|
|
625
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
626
|
+
const character = text[index];
|
|
627
|
+
if (inString) {
|
|
628
|
+
plain += character;
|
|
629
|
+
if (character === "\\")
|
|
630
|
+
plain += text[++index] ?? "";
|
|
631
|
+
else if (character === '"')
|
|
632
|
+
inString = false;
|
|
633
|
+
}
|
|
634
|
+
else if (character === '"') {
|
|
635
|
+
inString = true;
|
|
636
|
+
plain += character;
|
|
637
|
+
}
|
|
638
|
+
else if (character === "/" && text[index + 1] === "/") {
|
|
639
|
+
while (index < text.length && text[index] !== "\n")
|
|
640
|
+
index += 1;
|
|
641
|
+
plain += "\n";
|
|
642
|
+
}
|
|
643
|
+
else if (character === "/" && text[index + 1] === "*") {
|
|
644
|
+
index += 2;
|
|
645
|
+
while (index < text.length && !(text[index] === "*" && text[index + 1] === "/"))
|
|
646
|
+
index += 1;
|
|
647
|
+
index += 1;
|
|
648
|
+
}
|
|
649
|
+
else
|
|
650
|
+
plain += character;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return JSON.parse(plain.replace(/,(\s*[}\]])/gu, "$1"));
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function readText(path) {
|
|
660
|
+
try {
|
|
661
|
+
return readFileSync(path, "utf8");
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** The folder rule the person's own OpenCode config ends with, when it is a
|
|
668
|
+
* single action ("deny", "ask" or "allow") rather than a folder map.
|
|
669
|
+
* OPENCODE_PERMISSION is merged over the config and an object replaces a
|
|
670
|
+
* string, so without this OpenMaus's folder list would quietly turn a "deny"
|
|
671
|
+
* into OpenCode's default "ask". A folder map needs nothing: the two maps
|
|
672
|
+
* merge. Sources in OpenCode's own order: global config, OPENCODE_CONFIG,
|
|
673
|
+
* project files from the root down (unless project config is off), the
|
|
674
|
+
* .opencode folders, then OPENCODE_CONFIG_CONTENT. */
|
|
675
|
+
export function configuredOpenCodeFolderAction(env, cwd, projectConfig) {
|
|
676
|
+
let rule;
|
|
677
|
+
const apply = (raw) => {
|
|
678
|
+
if (!raw)
|
|
679
|
+
return;
|
|
680
|
+
const config = parseJsonc(raw);
|
|
681
|
+
const permission = config && typeof config === "object" ? config.permission : undefined;
|
|
682
|
+
if (!permission || typeof permission !== "object" || Array.isArray(permission))
|
|
683
|
+
return;
|
|
684
|
+
const value = permission.external_directory;
|
|
685
|
+
if (typeof value === "string")
|
|
686
|
+
rule = value;
|
|
687
|
+
else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
688
|
+
rule = rule && typeof rule === "object" ? { ...rule, ...value } : value;
|
|
689
|
+
}
|
|
291
690
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
691
|
+
const global = opencodeConfigDir(env);
|
|
692
|
+
for (const name of ["config.json", "opencode.json", "opencode.jsonc"])
|
|
693
|
+
apply(readText(join(global, name)));
|
|
694
|
+
if (env.OPENCODE_CONFIG)
|
|
695
|
+
apply(readText(env.OPENCODE_CONFIG));
|
|
696
|
+
const projectFolders = [];
|
|
697
|
+
if (projectConfig) {
|
|
698
|
+
// Up to the git root, or the top of the disk outside git.
|
|
699
|
+
for (let folder = resolvePath(cwd);; folder = dirname(folder)) {
|
|
700
|
+
projectFolders.push(folder);
|
|
701
|
+
if (existsSync(join(folder, ".git")) || dirname(folder) === folder)
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
for (const folder of [...projectFolders].reverse()) {
|
|
705
|
+
for (const name of ["opencode.json", "opencode.jsonc"])
|
|
706
|
+
apply(readText(join(folder, name)));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
710
|
+
const configFolders = [
|
|
711
|
+
...projectFolders.map((folder) => join(folder, ".opencode")),
|
|
712
|
+
join(home, ".opencode"),
|
|
713
|
+
...(env.OPENCODE_CONFIG_DIR ? [env.OPENCODE_CONFIG_DIR] : []),
|
|
714
|
+
];
|
|
715
|
+
for (const folder of configFolders) {
|
|
716
|
+
for (const name of ["opencode.json", "opencode.jsonc"])
|
|
717
|
+
apply(readText(join(folder, name)));
|
|
718
|
+
}
|
|
719
|
+
apply(env.OPENCODE_CONFIG_CONTENT);
|
|
720
|
+
return typeof rule === "string" ? rule : undefined;
|
|
721
|
+
}
|
|
722
|
+
/** OpenCode asks before a tool touches a folder outside the session's
|
|
723
|
+
* working folder (its `external_directory` permission), which stalled every
|
|
724
|
+
* turn that read an attachment on an approval card. Allow exactly the
|
|
725
|
+
* folders OpenMaus owns for this bot; every other folder keeps the person's
|
|
726
|
+
* rule, or OpenCode's default "ask". The rules depend only on the bot and the
|
|
727
|
+
* working folder, so a conversation keeps one process. */
|
|
728
|
+
function allowOwnedDirectories(env, botId, cwd) {
|
|
729
|
+
let permission = {};
|
|
730
|
+
if (env.OPENCODE_PERMISSION) {
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(env.OPENCODE_PERMISSION);
|
|
733
|
+
// A policy the person set that is not an object is theirs to keep.
|
|
734
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
735
|
+
return;
|
|
736
|
+
permission = parsed;
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const directories = openCodeOwnedDirectories(botId, cwd);
|
|
743
|
+
if (!directories.length)
|
|
744
|
+
return;
|
|
745
|
+
const existing = permission.external_directory;
|
|
746
|
+
// OpenCode evaluates rules in order and the last match wins, so these go
|
|
747
|
+
// after anything already there: the person's single action first as "*",
|
|
748
|
+
// then OpenMaus's folders.
|
|
749
|
+
let rules;
|
|
750
|
+
if (typeof existing === "string")
|
|
751
|
+
rules = { "*": existing };
|
|
752
|
+
else if (existing && typeof existing === "object" && !Array.isArray(existing))
|
|
753
|
+
rules = { ...existing };
|
|
754
|
+
else {
|
|
755
|
+
const configured = configuredOpenCodeFolderAction(env, cwd, env.OPENCODE_DISABLE_PROJECT_CONFIG !== "1");
|
|
756
|
+
rules = configured ? { "*": configured } : {};
|
|
757
|
+
}
|
|
758
|
+
for (const directory of directories) {
|
|
759
|
+
rules[directory] = "allow";
|
|
760
|
+
rules[join(directory, "*")] = "allow";
|
|
761
|
+
}
|
|
762
|
+
permission.external_directory = rules;
|
|
763
|
+
env.OPENCODE_PERMISSION = JSON.stringify(permission);
|
|
302
764
|
}
|
|
303
|
-
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
765
|
+
/** Account failures in plain words, with the fix, naming the provider that
|
|
766
|
+
* refused: with provider keys and `opencode auth login` a model may be
|
|
767
|
+
* OpenRouter's or Anthropic's, not Zen's. Each stays under the 160
|
|
768
|
+
* characters a chat error row shows. */
|
|
769
|
+
export function describeOpenCodeAccountError(code, model) {
|
|
770
|
+
const provider = model && model.includes("/") ? model.slice(0, model.indexOf("/")) : "";
|
|
771
|
+
const zen = provider === "opencode";
|
|
772
|
+
const go = provider === "opencode-go";
|
|
773
|
+
const label = providerLabel(provider);
|
|
774
|
+
const name = zen ? "OpenCode Zen" : go ? "OpenCode Go" : provider ? (label.length > 24 ? provider.slice(0, 24) : label) : "OpenCode";
|
|
775
|
+
switch (code) {
|
|
776
|
+
case "invalid_credentials":
|
|
777
|
+
return zen || go || !provider
|
|
778
|
+
? "OpenCode rejected its key, or has none for this model. Fix it in Settings → API keys or with `opencode auth login`."
|
|
779
|
+
: `OpenCode's ${name} key for this model is missing or was rejected. Fix it with \`opencode auth login\`, or choose another model.`;
|
|
780
|
+
case "insufficient_funds":
|
|
781
|
+
return zen || !provider
|
|
782
|
+
? "Your OpenCode Zen balance has run out. Add credit at opencode.ai, or choose one of Zen's free models for this bot."
|
|
783
|
+
: `Your ${name} account has run out of credit. Add credit there, or choose another model for this bot.`;
|
|
784
|
+
case "inactive_subscription":
|
|
785
|
+
return go || !provider
|
|
786
|
+
? "This model needs an active OpenCode Go subscription. Subscribe at opencode.ai, or choose a Zen model for this bot."
|
|
787
|
+
: `This model needs an active ${name} subscription. Renew it there, or choose another model for this bot.`;
|
|
788
|
+
case "quota_or_region_restriction":
|
|
789
|
+
return `${name} says this account's usage limit has been reached. Wait for it to reset, or choose another model for this bot.`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** OpenCode's logins, as one value that changes when `opencode auth login`
|
|
793
|
+
* adds or replaces one. A running `opencode acp` never reads auth.json again,
|
|
794
|
+
* so a conversation's process must be replaced once it changes. */
|
|
795
|
+
function openCodeLoginsFingerprint(env) {
|
|
796
|
+
return storedAuthPaths(env).map((path) => {
|
|
797
|
+
try {
|
|
798
|
+
const stat = statSync(path);
|
|
799
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
return "-";
|
|
803
|
+
}
|
|
804
|
+
}).join("|");
|
|
312
805
|
}
|
|
313
806
|
const support = (loadCatalog) => ({
|
|
314
807
|
driverKind: "opencodeGo",
|
|
315
808
|
// Keep the historical driver kind so existing bots and instance config do
|
|
316
809
|
// not break; only the product name/catalog expand from Go to OpenCode.
|
|
317
810
|
displayName: "OpenCode",
|
|
318
|
-
models:
|
|
811
|
+
models: NO_MODELS,
|
|
319
812
|
defaultCli: "opencode",
|
|
320
813
|
nativeSource: "opencode.acp",
|
|
321
814
|
loginNote: "OpenCode has no usable models — run `opencode auth login` or connect a provider in the OpenCode app",
|
|
@@ -330,16 +823,29 @@ const support = (loadCatalog) => ({
|
|
|
330
823
|
needsNode: true,
|
|
331
824
|
},
|
|
332
825
|
spawnArgs: () => ["acp"],
|
|
333
|
-
credentialEnv: ["OPENCODE_API_KEY"],
|
|
826
|
+
credentialEnv: ["OPENCODE_API_KEY", ...OPENCODE_PROVIDER_ENV],
|
|
334
827
|
selectModel: { configId: "model" },
|
|
828
|
+
// A retired id (x-preview-f-free, the Ox Alpha preview) or none at all:
|
|
829
|
+
// the catalog default when this session offers it, else the session's own.
|
|
830
|
+
fallbackModel: (sessionConfig, catalogDefault) => {
|
|
831
|
+
const offered = offeredModels(sessionConfig, "model");
|
|
832
|
+
if (catalogDefault && offered.includes(catalogDefault))
|
|
833
|
+
return catalogDefault;
|
|
834
|
+
return preferredOpenCodeModel(offered, catalogFromOpenCodeSession(sessionConfig)?.current) || null;
|
|
835
|
+
},
|
|
335
836
|
modelVariants: true,
|
|
336
|
-
resolveTurnModel: (model, env) => model
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
837
|
+
resolveTurnModel: (model, env) => model ? ensureOpenCodeInjectModel(model, env) : model,
|
|
838
|
+
transformEnv: withholdProviderKeysWhenManaged,
|
|
839
|
+
spawnFingerprint: openCodeLoginsFingerprint,
|
|
840
|
+
applyTurnEnv: (env, { fullAuto, botId, cwd }) => {
|
|
841
|
+
// In OpenMaus's own folders nothing is the person's project: a bot wrote
|
|
842
|
+
// it, and OpenCode would otherwise take it as configuration.
|
|
843
|
+
if (openMausOwnsWorkingFolder(cwd))
|
|
844
|
+
env.OPENCODE_DISABLE_PROJECT_CONFIG = "1";
|
|
845
|
+
if (!fullAuto) {
|
|
846
|
+
allowOwnedDirectories(env, botId, cwd);
|
|
342
847
|
return;
|
|
848
|
+
}
|
|
343
849
|
// Scope native permissions to this child, not the user's OpenCode config.
|
|
344
850
|
// A wildcard alone leaves OpenCode's more-specific external-directory and
|
|
345
851
|
// read rules in place. Replace the built-in rules as well, including path
|
|
@@ -352,14 +858,30 @@ const support = (loadCatalog) => ({
|
|
|
352
858
|
},
|
|
353
859
|
pickAuthMethod: () => null,
|
|
354
860
|
authFailure: "continue",
|
|
861
|
+
// Setup status only. Turns are never gated on it: a missing or rejected
|
|
862
|
+
// key comes back from the prompt itself and is shown with its fix.
|
|
355
863
|
isAuthenticated: async (env, config) => (Boolean(env.OPENCODE_API_KEY)
|
|
356
864
|
|| hasStoredOpenCodeAuth(env)
|
|
357
|
-
|| await
|
|
358
|
-
requireAuthenticationBeforeSpawn: true,
|
|
865
|
+
|| await canRunOpenCode(env, config.cli, loadCatalog)),
|
|
359
866
|
classifyError: classifyOpenCodeError,
|
|
867
|
+
describeAccountError: describeOpenCodeAccountError,
|
|
360
868
|
resolveModels: async (environment, config) => mergeLocalInject(await loadCatalog(environment, config.cli), environment),
|
|
361
869
|
buildPromptText: (turn) => turn.system ? `${turn.system}\n\n${turn.text}` : turn.text,
|
|
362
870
|
});
|
|
871
|
+
/** OpenCode 1.18 reports a provider's account refusal as a JSON-RPC internal
|
|
872
|
+
* error wrapping an APIError; only the text says which. Recorded from the
|
|
873
|
+
* real CLI: `{code: -32603, message: "Internal error: Invalid API key.",
|
|
874
|
+
* data: {service: "session", errorName: "APIError"}}`. */
|
|
875
|
+
const ACCOUNT_ERROR_TEXT = [
|
|
876
|
+
[/insufficient (?:account )?(?:funds|balance|credits?)|out of credits?|no credits? (?:left|remaining)|payment required|\b402\b/iu, "insufficient_funds"],
|
|
877
|
+
// Not plain "rate limit exceeded": that is a transient 429, which OpenCode
|
|
878
|
+
// retries itself and automatic recovery may still route around.
|
|
879
|
+
[/\bquota\b|usage limit|(?:hourly|daily|weekly|monthly|5-hour) limit/iu, "quota_or_region_restriction"],
|
|
880
|
+
[/\bsubscription\b/iu, "inactive_subscription"],
|
|
881
|
+
// "User not found." is OpenRouter's refusal of an unknown key (recorded
|
|
882
|
+
// through opencode 1.18.27 after `opencode auth login` with a dummy key).
|
|
883
|
+
[/invalid api key|api key (?:is )?(?:invalid|missing|revoked|expired)|incorrect api key|unauthori[sz]ed|\b401\b|authentication (?:failed|required)|(?:^|internal error: )user not found\b/iu, "invalid_credentials"],
|
|
884
|
+
];
|
|
363
885
|
export function classifyOpenCodeError(error) {
|
|
364
886
|
const value = error && typeof error === "object" ? error : {};
|
|
365
887
|
const code = value.code;
|
|
@@ -375,6 +897,13 @@ export function classifyOpenCodeError(error) {
|
|
|
375
897
|
return "upstream_outage";
|
|
376
898
|
if (code === "MODEL_CATALOG_UNAVAILABLE")
|
|
377
899
|
return "model_catalog_outage";
|
|
900
|
+
const data = value.data && typeof value.data === "object" && !Array.isArray(value.data)
|
|
901
|
+
? value.data
|
|
902
|
+
: {};
|
|
903
|
+
if (code === -32603 && data.errorName === "APIError") {
|
|
904
|
+
const text = [value.message, data.message, data.details].filter((part) => typeof part === "string").join(" ");
|
|
905
|
+
return ACCOUNT_ERROR_TEXT.find(([pattern]) => pattern.test(text))?.[1];
|
|
906
|
+
}
|
|
378
907
|
return undefined;
|
|
379
908
|
}
|
|
380
909
|
export const classifyOpenCodeGoError = classifyOpenCodeError;
|