openmausbot 0.1.86 → 0.1.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +19 -2
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -1,10 +1,35 @@
|
|
|
1
|
-
// Codex owns history; this receipt only remembers which bot rules
|
|
1
|
+
// Codex owns history; this receipt only remembers which bot rules and which
|
|
2
|
+
// volatile context digest the native thread last carried.
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { DATA_DIR } from "../config.js";
|
|
6
7
|
import { writeFileAtomic } from "../atomic.js";
|
|
7
8
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
9
|
+
const isFingerprint = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
10
|
+
function parseReceipt(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return null;
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
if (isFingerprint(trimmed))
|
|
15
|
+
return { instructions: trimmed };
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(trimmed);
|
|
18
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
19
|
+
const record = parsed;
|
|
20
|
+
if (typeof record.instructions === "string" && isFingerprint(record.instructions)) {
|
|
21
|
+
return {
|
|
22
|
+
instructions: record.instructions,
|
|
23
|
+
...(typeof record.volatile === "string" && isFingerprint(record.volatile) ? { volatile: record.volatile } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* a corrupt receipt is simply unknown */
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
8
33
|
/** Preserve native configured rules when overriding Codex's developer slot. */
|
|
9
34
|
export function codexDeveloperInstructions(config, botInstructions) {
|
|
10
35
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -20,19 +45,21 @@ export function codexDeveloperInstructions(config, botInstructions) {
|
|
|
20
45
|
? `${botInstructions || "No OpenMausBot bot-specific instructions remain."}\n\n${configured}`
|
|
21
46
|
: botInstructions;
|
|
22
47
|
}
|
|
23
|
-
export async function syncCodexInstructions(key, nativeThreadId, instructions, resumed, request) {
|
|
48
|
+
export async function syncCodexInstructions(key, nativeThreadId, instructions, volatile, resumed, request, mentionTurn = false) {
|
|
24
49
|
const directory = join(DATA_DIR, "codex-instructions");
|
|
25
50
|
const path = join(directory, `${digest(JSON.stringify([key, nativeThreadId]))}.sha256`);
|
|
26
51
|
const fingerprint = digest(instructions);
|
|
52
|
+
const volatileFingerprint = digest(volatile);
|
|
27
53
|
let previous;
|
|
28
54
|
try {
|
|
29
|
-
previous = readFileSync(path, "utf8");
|
|
55
|
+
previous = parseReceipt(readFileSync(path, "utf8"));
|
|
30
56
|
}
|
|
31
57
|
catch (error) {
|
|
32
58
|
if (error.code !== "ENOENT")
|
|
33
59
|
throw error;
|
|
60
|
+
previous = null;
|
|
34
61
|
}
|
|
35
|
-
if (resumed && previous !== fingerprint) {
|
|
62
|
+
if (resumed && previous?.instructions !== fingerprint) {
|
|
36
63
|
// thread/resume config overrides are used after compaction, but Codex
|
|
37
64
|
// 0.153.4 keeps the old initial developer message until then. Append a
|
|
38
65
|
// developer update ONLY on change (or first adoption of an old session).
|
|
@@ -59,8 +86,23 @@ export async function syncCodexInstructions(key, nativeThreadId, instructions, r
|
|
|
59
86
|
throw error;
|
|
60
87
|
}
|
|
61
88
|
}
|
|
62
|
-
|
|
89
|
+
// A mention describes the turn it rides: deliver it on every tagged turn,
|
|
90
|
+
// even when the volatile text is byte-identical to the previous turn's.
|
|
91
|
+
const deliverVolatile = !resumed || previous?.volatile !== volatileFingerprint || mentionTurn;
|
|
92
|
+
const hadVolatile = typeof previous?.volatile === "string" && previous.volatile !== digest("");
|
|
93
|
+
if (!resumed || previous?.instructions !== fingerprint) {
|
|
94
|
+
// Instructions-only, and only after inject_items acknowledged: the
|
|
95
|
+
// volatile digest stays pending until the driver commits it after the
|
|
96
|
+
// provider accepts the turn, so a submission the provider rejected
|
|
97
|
+
// redelivers its context on the next attempt.
|
|
63
98
|
mkdirSync(directory, { recursive: true });
|
|
64
|
-
writeFileAtomic(path, fingerprint, { mode: 0o600 });
|
|
99
|
+
writeFileAtomic(path, JSON.stringify({ instructions: fingerprint }), { mode: 0o600 });
|
|
65
100
|
}
|
|
101
|
+
const commitVolatile = previous?.instructions === fingerprint && previous?.volatile === volatileFingerprint
|
|
102
|
+
? null
|
|
103
|
+
: () => {
|
|
104
|
+
mkdirSync(directory, { recursive: true });
|
|
105
|
+
writeFileAtomic(path, JSON.stringify({ instructions: fingerprint, volatile: volatileFingerprint }), { mode: 0o600 });
|
|
106
|
+
};
|
|
107
|
+
return { deliverVolatile, hadVolatile, commitVolatile };
|
|
66
108
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Release freshness is independent of account-specific model availability.
|
|
2
|
+
function parseCodexReleaseVersion(value) {
|
|
3
|
+
const match = /^(?:.*\bcodex-cli\s+)?v?(\d+)\.(\d+)\.(\d+)(-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i.exec(value.trim());
|
|
4
|
+
return match ? { parts: match.slice(1, 4).map(Number), prerelease: Boolean(match[4]) } : null;
|
|
5
|
+
}
|
|
6
|
+
export function codexVersionBehind(installed, latest) {
|
|
7
|
+
const a = parseCodexReleaseVersion(installed);
|
|
8
|
+
const b = parseCodexReleaseVersion(latest);
|
|
9
|
+
if (!a || !b || b.prerelease)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if (a.parts[i] !== b.parts[i])
|
|
13
|
+
return a.parts[i] < b.parts[i];
|
|
14
|
+
}
|
|
15
|
+
return a.prerelease;
|
|
16
|
+
}
|
|
17
|
+
export function createCodexReleaseReader(fetchImpl = fetch, now = Date.now) {
|
|
18
|
+
let cached;
|
|
19
|
+
let expires = 0;
|
|
20
|
+
return () => {
|
|
21
|
+
if (cached && now() < expires)
|
|
22
|
+
return cached;
|
|
23
|
+
expires = now() + 60 * 60 * 1000;
|
|
24
|
+
cached = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchImpl("https://registry.npmjs.org/@openai/codex/latest", {
|
|
27
|
+
signal: AbortSignal.timeout(3000),
|
|
28
|
+
headers: { Accept: "application/json" },
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw new Error("Release lookup failed");
|
|
32
|
+
const payload = await response.json();
|
|
33
|
+
const version = typeof payload.version === "string" ? payload.version : "";
|
|
34
|
+
const parsed = parseCodexReleaseVersion(version);
|
|
35
|
+
if (!parsed || parsed.prerelease)
|
|
36
|
+
throw new Error("Invalid stable release");
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Offline checks must not make an otherwise usable engine unavailable.
|
|
41
|
+
expires = now() + 5 * 60 * 1000;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return cached;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const readLatestCodexRelease = createCodexReleaseReader();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// OpenMausBot owns computer selection and mounts its scoped MCP servers.
|
|
2
|
+
// Keep the standalone Codex installation untouched: these overrides apply
|
|
3
|
+
// only to the child process, including resumed conversations and retries.
|
|
4
|
+
// The desktop-app browser is not available in Codex CLI. Enabling its tool
|
|
5
|
+
// without a desktop connection exposes an unusable route; native search is
|
|
6
|
+
// independent and must remain governed by the user's/provider's config.
|
|
7
|
+
// https://developers.openai.com/codex/browser
|
|
8
|
+
export function codexToolSurfaceArgs() {
|
|
9
|
+
return [
|
|
10
|
+
"-c", "features.browser_use=false",
|
|
11
|
+
"-c", "features.browser_use_external=false",
|
|
12
|
+
"-c", "features.computer_use=false",
|
|
13
|
+
// Disable the matching instructions as well as the native tool. Use an
|
|
14
|
+
// inline table: quoted dotted keys are interpreted literally by some CLIs.
|
|
15
|
+
"-c", 'plugins={ "browser@openai-bundled" = { enabled = false }, "computer-use@openai-bundled" = { enabled = false }, "unified-computer-use@openai-bundled" = { enabled = false } }',
|
|
16
|
+
// Native web search does not require the desktop browser connection.
|
|
17
|
+
// Preserve the user's/provider's search mode instead of disabling it for
|
|
18
|
+
// every OpenMausBot process after a failure in one search route.
|
|
19
|
+
];
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
|
|
1
2
|
// Codex driver — upstream CodexDriver skeleton over agentcal's
|
|
2
3
|
// drivers/codex.js runtime: the official `codex` CLI headless over its
|
|
3
4
|
// app-server JSON-RPC protocol (newline-delimited JSON on stdio).
|
|
@@ -21,16 +22,18 @@ import { codexLocalProviderArgs } from "./local-inject.js";
|
|
|
21
22
|
import { augmentedPath, splitCliString } from "../env-path.js";
|
|
22
23
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
23
24
|
import { appendNative } from "./native.js";
|
|
25
|
+
import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
|
|
24
26
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
25
27
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
28
|
+
import { volatileContextNote, withContextNote } from "./prompt-split.js";
|
|
26
29
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
27
30
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
28
31
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
29
32
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
33
|
+
import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
|
|
34
|
+
import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
|
|
30
35
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
31
36
|
const DRIVER_KIND = "codex";
|
|
32
|
-
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
33
|
-
const ASTRA_MIN_CODEX_VERSION = [0, 153, 1];
|
|
34
37
|
class CodexRpcError extends Error {
|
|
35
38
|
code;
|
|
36
39
|
constructor(error) {
|
|
@@ -45,23 +48,6 @@ function missingNativeCodexThread(error, cursor) {
|
|
|
45
48
|
return error instanceof CodexRpcError && error.code === -32600 &&
|
|
46
49
|
error.message === `no rollout found for thread id ${cursor}`;
|
|
47
50
|
}
|
|
48
|
-
/** Whether an installed Codex predates the release that exposes GPT-6 Astra
|
|
49
|
-
* through app-server. Unknown version formats stay quiet: a bad guess should
|
|
50
|
-
* never nag someone whose custom build may already support the model. */
|
|
51
|
-
export function codexPredatesAstra(version) {
|
|
52
|
-
const value = version.trim();
|
|
53
|
-
const match = /\bcodex-cli\s+v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?(?![\d.])\b/i.exec(value)
|
|
54
|
-
?? /^v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?$/i.exec(value);
|
|
55
|
-
if (!match)
|
|
56
|
-
return false;
|
|
57
|
-
const installed = match.slice(1, 4).map(Number);
|
|
58
|
-
for (let i = 0; i < ASTRA_MIN_CODEX_VERSION.length; i += 1) {
|
|
59
|
-
if (installed[i] !== ASTRA_MIN_CODEX_VERSION[i]) {
|
|
60
|
-
return installed[i] < ASTRA_MIN_CODEX_VERSION[i];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
51
|
/** Ask the configured executable to update itself. This matters when the user
|
|
66
52
|
* selected a non-PATH Codex: installing a second global copy would leave
|
|
67
53
|
* OpenMausBot pointing at the old binary. */
|
|
@@ -81,13 +67,13 @@ export function codexUpdateCommand(cli, platform = process.platform) {
|
|
|
81
67
|
const command = (tokens.length > 0 ? tokens : [trimmed]).map(quote).join(" ");
|
|
82
68
|
return platform === "win32" ? `& ${command} update` : `${command} update`;
|
|
83
69
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
70
|
+
async function codexReleaseUpdate(version, cli) {
|
|
71
|
+
const latest = await readLatestCodexRelease();
|
|
72
|
+
if (!latest || !codexVersionBehind(version, latest))
|
|
86
73
|
return undefined;
|
|
87
|
-
}
|
|
88
74
|
return {
|
|
89
|
-
title:
|
|
90
|
-
message:
|
|
75
|
+
title: `Update Codex to ${latest}`,
|
|
76
|
+
message: `A newer stable Codex CLI is available (installed: ${version}). Update it, then refresh models. Model availability also depends on your signed-in account.`,
|
|
91
77
|
command: codexUpdateCommand(cli),
|
|
92
78
|
};
|
|
93
79
|
}
|
|
@@ -126,7 +112,6 @@ export function managedCodexArgs(config) {
|
|
|
126
112
|
"-c", "shell_environment_policy.ignore_default_excludes=false",
|
|
127
113
|
];
|
|
128
114
|
}
|
|
129
|
-
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
130
115
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
131
116
|
const skippedSseServers = new Set();
|
|
132
117
|
const renamedMcpServers = new Set();
|
|
@@ -275,6 +260,21 @@ function namedApprovalParams(mode) {
|
|
|
275
260
|
},
|
|
276
261
|
};
|
|
277
262
|
}
|
|
263
|
+
/** Keep the turn on the complete sandbox resolved by native start/resume. */
|
|
264
|
+
function withResolvedSandbox(params, session) {
|
|
265
|
+
const requested = plainRecord(params.turn.sandboxPolicy);
|
|
266
|
+
// Named permission profiles own their policy; do not mix both selectors.
|
|
267
|
+
if (!requested)
|
|
268
|
+
return params;
|
|
269
|
+
const sandbox = plainRecord(plainRecord(session)?.sandbox);
|
|
270
|
+
if (!sandbox || typeof sandbox.type !== "string") {
|
|
271
|
+
throw new Error("Codex did not return its resolved sandbox policy. Update Codex, then retry; the turn was not started because its permissions could not be verified.");
|
|
272
|
+
}
|
|
273
|
+
if (sandbox.type !== requested.type) {
|
|
274
|
+
throw new Error("Codex did not apply the requested sandbox mode; cannot safely start the turn.");
|
|
275
|
+
}
|
|
276
|
+
return { ...params, turn: { ...params.turn, sandboxPolicy: sandbox } };
|
|
277
|
+
}
|
|
278
278
|
function effectiveApprovalPolicy(value) {
|
|
279
279
|
if (value === "untrusted" || value === "on-request" || value === "never")
|
|
280
280
|
return value;
|
|
@@ -587,7 +587,7 @@ export const CodexDriver = {
|
|
|
587
587
|
const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
|
|
588
588
|
const launchAttempt = async (attempt) => {
|
|
589
589
|
const env = childEnv();
|
|
590
|
-
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
|
|
590
|
+
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs()];
|
|
591
591
|
if (turn.integrations?.composio) {
|
|
592
592
|
mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
|
|
593
593
|
}
|
|
@@ -626,6 +626,7 @@ export const CodexDriver = {
|
|
|
626
626
|
const prefix = "mcp_servers.openmausbot_phone";
|
|
627
627
|
appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(bridge.command)}`, "-c", `${prefix}.args=${JSON.stringify(bridge.args)}`, "-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(bridge.env))}`, "-c", `${prefix}.default_tools_approval_mode="auto"`);
|
|
628
628
|
}
|
|
629
|
+
const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
|
|
629
630
|
const child = spawnCli(config.cli, appServerArgs, {
|
|
630
631
|
cwd: turn.cwd ?? homedir(),
|
|
631
632
|
env,
|
|
@@ -838,23 +839,22 @@ export const CodexDriver = {
|
|
|
838
839
|
}
|
|
839
840
|
return;
|
|
840
841
|
}
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
842
|
+
// The whole set rides one card, answered per id: the protocol pairs
|
|
843
|
+
// each question with its own id, and the card's single reply is
|
|
844
|
+
// mapped back block by block (or as the flat single-question
|
|
845
|
+
// fallback). The only refusal left is an ask with nothing answerable
|
|
846
|
+
// — #1237 is closed: a bundle no longer copies one answer into
|
|
847
|
+
// every id.
|
|
848
|
+
const protocolQuestions = isQuestion ? parseProtocolAskQuestions(params.questions) : null;
|
|
849
|
+
if (isQuestion && !protocolQuestions) {
|
|
848
850
|
send({
|
|
849
851
|
jsonrpc: "2.0",
|
|
850
852
|
id: msg.id,
|
|
851
853
|
error: {
|
|
852
854
|
code: -32602,
|
|
853
|
-
message:
|
|
854
|
-
?
|
|
855
|
-
:
|
|
856
|
-
? "ask supports one question per call; this request sent none."
|
|
857
|
-
: "ask supports one question per call; params.questions must be an array with exactly one question.",
|
|
855
|
+
message: Array.isArray(params.questions)
|
|
856
|
+
? "ask needs at least one answerable question — a string id and question text each; this request sent none."
|
|
857
|
+
: "ask needs params.questions to be an array of questions, each with a string id and question text.",
|
|
858
858
|
},
|
|
859
859
|
});
|
|
860
860
|
return;
|
|
@@ -894,23 +894,36 @@ export const CodexDriver = {
|
|
|
894
894
|
? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
|
|
895
895
|
: typeof params.command === "string"
|
|
896
896
|
? params.command
|
|
897
|
-
:
|
|
898
|
-
?
|
|
899
|
-
:
|
|
900
|
-
? params.
|
|
901
|
-
:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
897
|
+
: protocolQuestions
|
|
898
|
+
? protocolQuestions.map(({ question }) => question.question).join(" · ")
|
|
899
|
+
: Array.isArray(params.questions)
|
|
900
|
+
? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
|
|
901
|
+
: typeof params.reason === "string"
|
|
902
|
+
? params.reason
|
|
903
|
+
: tool;
|
|
904
|
+
// Flat choices only when one non-multiselect question can actually
|
|
905
|
+
// be answered by a bare reply; a bundle's first-question buttons
|
|
906
|
+
// would be an unusable lie for flat clients.
|
|
907
|
+
const choices = isQuestion && protocolQuestions ? questionChoices(protocolQuestions.map(({ question }) => question)) : undefined;
|
|
905
908
|
const finish = (behavior, message, source = "user") => {
|
|
906
909
|
if (!asks.delete(requestId))
|
|
907
910
|
return;
|
|
908
911
|
clearTimeout(timer);
|
|
909
912
|
if (isQuestion) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
913
|
+
// Only the person's reply is filed: Q:/A: blocks mapped to the
|
|
914
|
+
// id that asked them, or the flat fallback for a single
|
|
915
|
+
// question. Timeout and turn teardown send empty answers so
|
|
916
|
+
// every id reads unanswered — system notes never occupy the
|
|
917
|
+
// slot the model reads as the person's words (the rule
|
|
918
|
+
// permission-proxy already follows).
|
|
919
|
+
const mapped = behavior === "answer" && source === "user" && typeof message === "string"
|
|
920
|
+
? questionAnswersById(message, protocolQuestions ?? [])
|
|
921
|
+
: {};
|
|
922
|
+
// Null prototype so an opaque id like __proto__ becomes a real
|
|
923
|
+
// answer key instead of hitting the inherited setter.
|
|
924
|
+
const answers = Object.create(null);
|
|
925
|
+
for (const [id, answer] of Object.entries(mapped))
|
|
926
|
+
answers[id] = { answers: [answer] };
|
|
914
927
|
send({ jsonrpc: "2.0", id: msg.id, result: { answers } });
|
|
915
928
|
}
|
|
916
929
|
else {
|
|
@@ -922,7 +935,11 @@ export const CodexDriver = {
|
|
|
922
935
|
}
|
|
923
936
|
emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source });
|
|
924
937
|
};
|
|
925
|
-
const timer = setTimeout(
|
|
938
|
+
const timer = setTimeout(
|
|
939
|
+
// A timed-out question resolves as denied, not answered: the card
|
|
940
|
+
// closes with no reply (answers stay empty either way), and the
|
|
941
|
+
// resolve event must not claim an answer that never happened.
|
|
942
|
+
() => finish("deny", isQuestion ? undefined : DENY_TIMEOUT_NOTE, "timeout"), 15 * 60_000);
|
|
926
943
|
timer.unref?.();
|
|
927
944
|
asks.set(requestId, finish);
|
|
928
945
|
emit({
|
|
@@ -932,9 +949,15 @@ export const CodexDriver = {
|
|
|
932
949
|
requestType: isQuestion ? "question" : "permission",
|
|
933
950
|
tool,
|
|
934
951
|
summary,
|
|
952
|
+
command: method === "execCommandApproval" || method === "item/commandExecution/requestApproval"
|
|
953
|
+
? permissionCommand(params.command, params.cwd ?? (
|
|
954
|
+
// Helpers may have a different workspace from their parent.
|
|
955
|
+
!params.threadId || params.threadId === codexThreadId ? commandCwd : undefined)) : undefined,
|
|
935
956
|
choices,
|
|
957
|
+
...(protocolQuestions ? { questions: protocolQuestions.map((pair) => pair.question) } : {}),
|
|
936
958
|
approvalScope: controlsHost ? "local-computer" : undefined,
|
|
937
|
-
requiresExplicitApproval: isAdditionalPermission ||
|
|
959
|
+
requiresExplicitApproval: isAdditionalPermission || ((method === "execCommandApproval" || method === "item/commandExecution/requestApproval") &&
|
|
960
|
+
(params.additionalPermissions != null || params.networkApprovalContext != null)) || undefined,
|
|
938
961
|
});
|
|
939
962
|
};
|
|
940
963
|
const handleNotification = (msg) => {
|
|
@@ -1315,7 +1338,18 @@ export const CodexDriver = {
|
|
|
1315
1338
|
? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
|
|
1316
1339
|
: "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
|
|
1317
1340
|
}
|
|
1318
|
-
|
|
1341
|
+
// Only the stable half of the prompt belongs in the developer slot:
|
|
1342
|
+
// it is the part that must survive compaction unchanged, and any
|
|
1343
|
+
// change to it invalidates the provider's cached prefix. The volatile
|
|
1344
|
+
// half (memory, mentions, outstanding teammate work) is delivered
|
|
1345
|
+
// inside the turn that changed it, after the cached prefix, the same
|
|
1346
|
+
// contract SendTurnInput.systemStable documents. Without the split
|
|
1347
|
+
// the driver keeps the previous single-block behaviour.
|
|
1348
|
+
const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
|
|
1349
|
+
? turn.systemStable
|
|
1350
|
+
: null;
|
|
1351
|
+
const promptSplit = stableInstructions !== null;
|
|
1352
|
+
const developerInstructions = codexDeveloperInstructions(effectiveConfig, stableInstructions ?? turn.system ?? "");
|
|
1319
1353
|
let approvalParams;
|
|
1320
1354
|
if (approvalMode === "custom") {
|
|
1321
1355
|
// config/read returns the effective global + project config for this
|
|
@@ -1358,6 +1392,7 @@ export const CodexDriver = {
|
|
|
1358
1392
|
approvalParams = approvalParams.fallback;
|
|
1359
1393
|
resumed = await resumeThread();
|
|
1360
1394
|
}
|
|
1395
|
+
approvalParams = withResolvedSandbox(approvalParams, resumed);
|
|
1361
1396
|
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1362
1397
|
resumedNativeThread = true;
|
|
1363
1398
|
}
|
|
@@ -1403,12 +1438,20 @@ export const CodexDriver = {
|
|
|
1403
1438
|
approvalParams = approvalParams.fallback;
|
|
1404
1439
|
started = await startThread();
|
|
1405
1440
|
}
|
|
1441
|
+
approvalParams = withResolvedSandbox(approvalParams, started);
|
|
1406
1442
|
codexThreadId = started?.thread?.id ?? null;
|
|
1407
1443
|
startedModel = started?.model ?? null;
|
|
1408
1444
|
}
|
|
1409
1445
|
if (!codexThreadId)
|
|
1410
1446
|
throw new Error("Codex did not return a native thread id");
|
|
1411
|
-
await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
|
|
1447
|
+
const { deliverVolatile, hadVolatile, commitVolatile } = await syncCodexInstructions(threadId, codexThreadId, developerInstructions, promptSplit ? turn.systemVolatile ?? "" : "", resumedNativeThread, request, Boolean(turn.mentionTurn));
|
|
1448
|
+
// A changed volatile half rides the next user input as a labelled
|
|
1449
|
+
// context block. It never touches the developer slot, so an
|
|
1450
|
+
// ordinary memory write or roster change neither appends a second
|
|
1451
|
+
// copy of the prompt to history nor re-uploads the conversation.
|
|
1452
|
+
if (deliverVolatile) {
|
|
1453
|
+
promptText = withContextNote(volatileContextNote(promptSplit ? turn.systemVolatile ?? "" : "", hadVolatile), promptText);
|
|
1454
|
+
}
|
|
1412
1455
|
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
|
|
1413
1456
|
const turnInput = [
|
|
1414
1457
|
...(promptText ? [{ type: "text", text: promptText }] : []),
|
|
@@ -1442,6 +1485,10 @@ export const CodexDriver = {
|
|
|
1442
1485
|
approvalParams = approvalParams.fallback;
|
|
1443
1486
|
await startTurn();
|
|
1444
1487
|
}
|
|
1488
|
+
// turn/start accepted the input: only now may the receipt claim the
|
|
1489
|
+
// volatile half was delivered, so a rejected turn redelivers on retry.
|
|
1490
|
+
if (commitVolatile)
|
|
1491
|
+
commitVolatile();
|
|
1445
1492
|
}
|
|
1446
1493
|
catch (e) {
|
|
1447
1494
|
const failure = e instanceof Error ? e : { text: String(e) };
|
|
@@ -1514,7 +1561,7 @@ export const CodexDriver = {
|
|
|
1514
1561
|
version,
|
|
1515
1562
|
authenticated,
|
|
1516
1563
|
...(email ? { account: { email } } : {}),
|
|
1517
|
-
update:
|
|
1564
|
+
update: await codexReleaseUpdate(version, config.cli),
|
|
1518
1565
|
billing: "subscription",
|
|
1519
1566
|
};
|
|
1520
1567
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
3
|
+
const DEFAULT_URL = "https://api.mistral.ai/v1";
|
|
4
|
+
const DEFAULT_MODELS = {
|
|
5
|
+
default: "mistral-large-latest",
|
|
6
|
+
options: [
|
|
7
|
+
{ id: "mistral-large-latest", label: "Mistral Large (latest)" },
|
|
8
|
+
{ id: "mistral-small-latest", label: "Mistral Small (latest)" },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
const configSchema = z.object({
|
|
12
|
+
url: z.string().trim().url().default(DEFAULT_URL),
|
|
13
|
+
model: z.string().trim().min(1).optional(),
|
|
14
|
+
tools: z.boolean().optional(),
|
|
15
|
+
});
|
|
16
|
+
function decodeConfig(raw) {
|
|
17
|
+
const config = configSchema.parse(raw ?? {});
|
|
18
|
+
config.url = config.url.replace(/\/+$/, "");
|
|
19
|
+
const url = new URL(config.url);
|
|
20
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" &&
|
|
21
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
22
|
+
throw new Error("Mistral requires HTTPS, except for a local test endpoint.");
|
|
23
|
+
}
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
const modelCard = z.object({
|
|
27
|
+
id: z.string().min(1),
|
|
28
|
+
name: z.string().nullable().optional(),
|
|
29
|
+
aliases: z.array(z.string()).optional(),
|
|
30
|
+
archived: z.boolean().optional(),
|
|
31
|
+
capabilities: z.object({ completion_chat: z.boolean().optional() }).optional(),
|
|
32
|
+
max_context_length: z.number().int().positive().optional(),
|
|
33
|
+
});
|
|
34
|
+
/** Mistral can return plain text or a list of typed content chunks. */
|
|
35
|
+
function contentText(content) {
|
|
36
|
+
if (typeof content === "string")
|
|
37
|
+
return content;
|
|
38
|
+
if (!Array.isArray(content))
|
|
39
|
+
return "";
|
|
40
|
+
return content.map((chunk) => chunk?.type === "text" && typeof chunk.text === "string" ? chunk.text : "").join("");
|
|
41
|
+
}
|
|
42
|
+
export const MistralDriver = {
|
|
43
|
+
driverKind: "mistral",
|
|
44
|
+
metadata: { displayName: "Mistral (API)", supportsMultipleInstances: true, access: "api" },
|
|
45
|
+
models: DEFAULT_MODELS,
|
|
46
|
+
install: {
|
|
47
|
+
docsUrl: "https://console.mistral.ai/api-keys",
|
|
48
|
+
signInCommand: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY on the server.",
|
|
49
|
+
},
|
|
50
|
+
decodeConfig,
|
|
51
|
+
defaultConfig: () => decodeConfig({}),
|
|
52
|
+
async create(input) {
|
|
53
|
+
const { config } = input;
|
|
54
|
+
const apiKey = (input.environment.MISTRAL_API_KEY ?? process.env.MISTRAL_API_KEY ?? "").trim();
|
|
55
|
+
const withConfiguredModel = (options) => {
|
|
56
|
+
const preferred = config.model ?? DEFAULT_MODELS.default;
|
|
57
|
+
if (config.model && !options.some((option) => option.id === config.model)) {
|
|
58
|
+
options = [{ id: config.model, label: config.model }, ...options];
|
|
59
|
+
}
|
|
60
|
+
return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
|
|
61
|
+
};
|
|
62
|
+
let catalog = withConfiguredModel(DEFAULT_MODELS.options);
|
|
63
|
+
const refreshModels = async () => {
|
|
64
|
+
if (!apiKey)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`${config.url}/models`, {
|
|
68
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
69
|
+
redirect: "error",
|
|
70
|
+
signal: AbortSignal.timeout(8_000),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
return;
|
|
74
|
+
const json = await response.json();
|
|
75
|
+
const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
|
|
76
|
+
const options = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const parsed = modelCard.safeParse(row);
|
|
80
|
+
if (!parsed.success || parsed.data.archived || parsed.data.capabilities?.completion_chat === false)
|
|
81
|
+
continue;
|
|
82
|
+
const card = parsed.data;
|
|
83
|
+
for (const id of [card.id, ...(card.aliases ?? [])]) {
|
|
84
|
+
if (!id.trim() || seen.has(id))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
options.push({ id, label: id === card.id ? card.name?.trim() || id : id,
|
|
88
|
+
...(card.max_context_length ? { contextWindow: card.max_context_length } : {}) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.length)
|
|
92
|
+
catalog = withConfiguredModel(options);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A failed refresh keeps the last usable catalog, including custom models.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (apiKey)
|
|
99
|
+
void refreshModels();
|
|
100
|
+
return createOpenAIChatRuntime({
|
|
101
|
+
input, driverKind: "mistral", apiKey, apiUrl: config.url,
|
|
102
|
+
tools: config.tools, models: () => catalog, refreshModels, contentText,
|
|
103
|
+
requestBody: (model, messages, stream) => ({ model, messages, stream }),
|
|
104
|
+
httpErrorLabel: "Mistral",
|
|
105
|
+
missingKeyError: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY.",
|
|
106
|
+
unavailableReason: "No Mistral API key — open Settings → Connections.",
|
|
107
|
+
timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
|
|
108
|
+
nativeLog: {
|
|
109
|
+
source: "mistral.chat.completions",
|
|
110
|
+
outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
|
|
111
|
+
incoming: ({ text, usage }) => ({ textLength: text.length, usage }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|