@addai/node 0.30.12 → 0.30.14
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/auto-update.d.ts +3 -0
- package/dist/auto-update.js +15 -0
- package/dist/capabilities.d.ts +31 -6
- package/dist/capabilities.js +48 -26
- package/dist/claude-models.d.ts +71 -0
- package/dist/claude-models.js +330 -0
- package/dist/claude-print.d.ts +15 -0
- package/dist/claude-print.js +16 -0
- package/dist/codex-models.d.ts +81 -0
- package/dist/codex-models.js +385 -0
- package/dist/codex-spawn.d.ts +14 -8
- package/dist/codex-spawn.js +30 -10
- package/dist/command-runner.js +31 -9
- package/dist/desktop/docker.js +2 -0
- package/dist/desktop/manager.d.ts +2 -1
- package/dist/desktop/manager.js +51 -1
- package/dist/desktop/provider.d.ts +1 -1
- package/dist/desktop/provider.js +2 -0
- package/dist/desktop/relay-client.js +7 -0
- package/dist/desktop/spec.d.ts +2 -0
- package/dist/direct/aios-bridge.d.ts +13 -0
- package/dist/direct/aios-bridge.js +177 -0
- package/dist/direct/aios-mcp.d.ts +2 -0
- package/dist/direct/aios-mcp.js +132 -0
- package/dist/direct/aios-tools.d.ts +351 -0
- package/dist/direct/aios-tools.js +120 -0
- package/dist/direct/board-chat-provider.d.ts +64 -0
- package/dist/direct/board-chat-provider.js +151 -0
- package/dist/direct/board-chat.d.ts +40 -0
- package/dist/direct/board-chat.js +414 -0
- package/dist/direct/config.d.ts +2 -0
- package/dist/direct/config.js +60 -0
- package/dist/direct/launch.d.ts +14 -0
- package/dist/direct/launch.js +121 -0
- package/dist/direct/protocol.d.ts +46 -0
- package/dist/direct/protocol.js +84 -0
- package/dist/direct/relay-main.d.ts +1 -0
- package/dist/direct/relay-main.js +21 -0
- package/dist/direct/relay.d.ts +26 -0
- package/dist/direct/relay.js +334 -0
- package/dist/direct/sessions.d.ts +61 -0
- package/dist/direct/sessions.js +368 -0
- package/dist/direct/supervisor.d.ts +9 -0
- package/dist/direct/supervisor.js +137 -0
- package/dist/direct/terminal.d.ts +1 -0
- package/dist/direct/terminal.js +106 -0
- package/dist/direct/worker.d.ts +1 -0
- package/dist/direct/worker.js +278 -0
- package/dist/entitybox/container-action.cjs +85 -0
- package/dist/entitybox/container-watchdog.cjs +70 -0
- package/dist/entitybox/model-entry.d.ts +3 -0
- package/dist/entitybox/model-entry.js +233 -0
- package/dist/entitybox/model-mcp-proxy.d.ts +3 -0
- package/dist/entitybox/model-mcp-proxy.js +85 -0
- package/dist/entitybox-application-boundary.d.ts +59 -0
- package/dist/entitybox-application-boundary.js +161 -0
- package/dist/entitybox-application-guard-core.d.ts +44 -0
- package/dist/entitybox-application-guard-core.js +164 -0
- package/dist/entitybox-application-guard-process.d.ts +1 -0
- package/dist/entitybox-application-guard-process.js +83 -0
- package/dist/entitybox-application-guard.d.ts +46 -0
- package/dist/entitybox-application-guard.js +239 -0
- package/dist/entitybox-capacity-health-host.d.ts +12 -0
- package/dist/entitybox-capacity-health-host.js +80 -0
- package/dist/entitybox-capacity-health-worker.d.ts +22 -0
- package/dist/entitybox-capacity-health-worker.js +168 -0
- package/dist/entitybox-capacity-host.d.ts +17 -0
- package/dist/entitybox-capacity-host.js +155 -0
- package/dist/entitybox-capacity-owner-process.d.ts +1 -0
- package/dist/entitybox-capacity-owner-process.js +32 -0
- package/dist/entitybox-capacity-owner.d.ts +16 -0
- package/dist/entitybox-capacity-owner.js +90 -0
- package/dist/entitybox-capacity-programs.d.ts +7 -0
- package/dist/entitybox-capacity-programs.js +49 -0
- package/dist/entitybox-capacity-provisioner.d.ts +82 -0
- package/dist/entitybox-capacity-provisioner.js +404 -0
- package/dist/entitybox-capacity-publication.d.ts +29 -0
- package/dist/entitybox-capacity-publication.js +199 -0
- package/dist/entitybox-capacity-registration.d.ts +36 -0
- package/dist/entitybox-capacity-registration.js +75 -0
- package/dist/entitybox-capacity-worker.d.ts +29 -0
- package/dist/entitybox-capacity-worker.js +113 -0
- package/dist/entitybox-computer-task.d.ts +69 -0
- package/dist/entitybox-computer-task.js +176 -0
- package/dist/entitybox-controller-recovery.d.ts +40 -0
- package/dist/entitybox-controller-recovery.js +156 -0
- package/dist/entitybox-desktop-action-server.d.ts +34 -0
- package/dist/entitybox-desktop-action-server.js +176 -0
- package/dist/entitybox-desktop-admission.d.ts +32 -0
- package/dist/entitybox-desktop-admission.js +79 -0
- package/dist/entitybox-display-binding.d.ts +13 -0
- package/dist/entitybox-display-binding.js +31 -0
- package/dist/entitybox-execution-container.d.ts +82 -0
- package/dist/entitybox-execution-container.js +450 -0
- package/dist/entitybox-execution-host.d.ts +40 -0
- package/dist/entitybox-execution-host.js +168 -0
- package/dist/entitybox-execution-lease.d.ts +81 -0
- package/dist/entitybox-execution-lease.js +332 -0
- package/dist/entitybox-file-inputs.d.ts +51 -0
- package/dist/entitybox-file-inputs.js +182 -0
- package/dist/entitybox-file-session.d.ts +38 -0
- package/dist/entitybox-file-session.js +82 -0
- package/dist/entitybox-host-process.d.ts +12 -0
- package/dist/entitybox-host-process.js +120 -0
- package/dist/entitybox-install.d.ts +10 -0
- package/dist/entitybox-install.js +40 -0
- package/dist/entitybox-mcp-broker.d.ts +77 -0
- package/dist/entitybox-mcp-broker.js +420 -0
- package/dist/entitybox-mcp-contract.d.ts +10 -0
- package/dist/entitybox-mcp-contract.js +24 -0
- package/dist/entitybox-mcp-guard.d.ts +3 -0
- package/dist/entitybox-mcp-guard.js +167 -0
- package/dist/entitybox-model-artifact.d.ts +9 -0
- package/dist/entitybox-model-artifact.js +83 -0
- package/dist/entitybox-native-services.d.ts +33 -0
- package/dist/entitybox-native-services.js +172 -0
- package/dist/entitybox-operation.d.ts +4 -0
- package/dist/entitybox-operation.js +34 -0
- package/dist/entitybox-provider-broker.d.ts +41 -0
- package/dist/entitybox-provider-broker.js +149 -0
- package/dist/entitybox-queue-request.d.ts +32 -0
- package/dist/entitybox-queue-request.js +141 -0
- package/dist/entitybox-recovery-host.d.ts +12 -0
- package/dist/entitybox-recovery-host.js +80 -0
- package/dist/entitybox-recovery-store.d.ts +33 -0
- package/dist/entitybox-recovery-store.js +150 -0
- package/dist/entitybox-recovery-worker.d.ts +30 -0
- package/dist/entitybox-recovery-worker.js +134 -0
- package/dist/entitybox-vault-provider.d.ts +27 -0
- package/dist/entitybox-vault-provider.js +127 -0
- package/dist/harness-registry.js +3 -1
- package/dist/index.js +72 -38
- package/dist/install.d.ts +14 -2
- package/dist/install.js +115 -10
- package/dist/model-cache.d.ts +38 -0
- package/dist/model-cache.js +137 -0
- package/dist/request-pump.d.ts +3 -0
- package/dist/request-pump.js +21 -7
- package/dist/session-runner.d.ts +8 -1
- package/dist/session-runner.js +48 -3
- package/dist/supabase-client.js +4 -1
- package/package.json +5 -1
- package/scripts/copy-assets.js +4 -2
package/dist/claude-print.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// our normalised RuntimeEvent shape, and resolves when the process exits.
|
|
6
6
|
// No PTY needed — Claude detects non-TTY and runs headless.
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.claudePrintInputMessage = claudePrintInputMessage;
|
|
8
9
|
exports.blockDelta = blockDelta;
|
|
9
10
|
exports.lineToEvents = lineToEvents;
|
|
10
11
|
exports.spawnClaudePrint = spawnClaudePrint;
|
|
@@ -13,6 +14,16 @@ const claude_binary_1 = require("./claude-binary");
|
|
|
13
14
|
const win_1 = require("./win");
|
|
14
15
|
const claude_system_prompt_1 = require("./claude-system-prompt");
|
|
15
16
|
const prompt_transport_1 = require("./prompt-transport");
|
|
17
|
+
/** Stream JSON reaches stdin only; files never become command-line arguments. */
|
|
18
|
+
function claudePrintInputMessage(prompt, blocks) {
|
|
19
|
+
if (!Array.isArray(blocks) || blocks.length < 1 || blocks.length > 12 || !blocks.every(b => b && (b.type === 'text' && typeof b.text === 'string' || ['image', 'document'].includes(b.type) && 'source' in b && b.source?.type === 'base64' && typeof b.source.data === 'string' && /^[A-Za-z0-9+/]*={0,2}$/.test(b.source.data)
|
|
20
|
+
&& (b.type === 'document' ? b.source.media_type === 'application/pdf' : ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(b.source.media_type)))))
|
|
21
|
+
throw new Error('Invalid file input');
|
|
22
|
+
const message = JSON.stringify({ type: 'user', session_id: '', parent_tool_use_id: null, message: { role: 'user', content: [...blocks, { type: 'text', text: prompt }] } }) + '\n';
|
|
23
|
+
if (Buffer.byteLength(message) > 30 * 1024 * 1024)
|
|
24
|
+
throw new Error('File input is too large');
|
|
25
|
+
return message;
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* The text to emit for one flushed block, given what came before.
|
|
18
29
|
*
|
|
@@ -113,6 +124,7 @@ function lineToEvents(line, suppressText = false) {
|
|
|
113
124
|
return [{ type: `claude:${type || 'unknown'}`, raw: line }];
|
|
114
125
|
}
|
|
115
126
|
function spawnClaudePrint(input) {
|
|
127
|
+
const inputMessage = input.inputBlocks ? claudePrintInputMessage(input.prompt, input.inputBlocks) : undefined;
|
|
116
128
|
const bin = (0, claude_binary_1.findClaudeBinary)();
|
|
117
129
|
// No claude on this runtime — fail THIS request cleanly rather than
|
|
118
130
|
// spawning a bare 'claude' that ENOENTs and crashes the daemon. Both
|
|
@@ -138,6 +150,8 @@ function spawnClaudePrint(input) {
|
|
|
138
150
|
];
|
|
139
151
|
if (partialStream)
|
|
140
152
|
args.push('--include-partial-messages');
|
|
153
|
+
if (input.inputBlocks)
|
|
154
|
+
args.push('--input-format', 'stream-json');
|
|
141
155
|
if (input.permissionMode === 'bypassPermissions') {
|
|
142
156
|
args.push('--dangerously-skip-permissions');
|
|
143
157
|
}
|
|
@@ -183,6 +197,8 @@ function spawnClaudePrint(input) {
|
|
|
183
197
|
const isWindows = process.platform === 'win32';
|
|
184
198
|
const sysPrompt = (0, claude_system_prompt_1.offloadLongSystemPrompt)(bin, args, isWindows, (0, claude_system_prompt_1.claudeSupportsPromptFile)(bin));
|
|
185
199
|
const transport = (0, prompt_transport_1.offloadLongPrompt)(bin, sysPrompt.args, sysPrompt.args.length - 1, null, isWindows, true);
|
|
200
|
+
if (inputMessage !== undefined)
|
|
201
|
+
transport.stdin = inputMessage;
|
|
186
202
|
let proc;
|
|
187
203
|
try {
|
|
188
204
|
const inv = (0, win_1.resolveCliInvocation)(bin, transport.args);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type ModelProbeResult } from './model-cache';
|
|
2
|
+
import type { RuntimeEffortLevel } from './types';
|
|
3
|
+
import { type CliInvocation } from './win';
|
|
4
|
+
export interface CodexModelEntry {
|
|
5
|
+
/** What goes to `--model`. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Always `id` — codex has no aliases. Here so Studio reads both CLIs alike. */
|
|
8
|
+
resolved: string;
|
|
9
|
+
/** Codex's own label, e.g. 'GPT-6-Astra'. */
|
|
10
|
+
name: string;
|
|
11
|
+
alias: false;
|
|
12
|
+
/** Effort levels this model takes, in codex's spelling. */
|
|
13
|
+
efforts?: string[];
|
|
14
|
+
/** What a run with no model set gets. */
|
|
15
|
+
default?: true;
|
|
16
|
+
}
|
|
17
|
+
export interface CodexModelReport {
|
|
18
|
+
/** Plain ids, the default first — the shape `capabilities.codex.models`
|
|
19
|
+
* has always had. */
|
|
20
|
+
models: string[];
|
|
21
|
+
catalog: CodexModelEntry[];
|
|
22
|
+
defaultModel?: string;
|
|
23
|
+
/** 'account': what this node's login can run. 'bundled': what the CLI
|
|
24
|
+
* ships with, read without the login. */
|
|
25
|
+
scope: 'account' | 'bundled';
|
|
26
|
+
}
|
|
27
|
+
export type ProbeResult = ModelProbeResult<CodexModelReport>;
|
|
28
|
+
/**
|
|
29
|
+
* Turn the `data` of a `model/list` answer into a report. Hidden models
|
|
30
|
+
* (codex's internal reviewers and the like) are left out. Returns null when
|
|
31
|
+
* nothing usable is in there.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseModelList(raw: unknown, scope: CodexModelReport['scope']): CodexModelReport | null;
|
|
34
|
+
/** What a picker can offer: every level some listed model takes, in order. */
|
|
35
|
+
export declare function offeredEfforts(report: CodexModelReport): RuntimeEffortLevel[];
|
|
36
|
+
export type ProbeLogin = {
|
|
37
|
+
use: true;
|
|
38
|
+
file: string;
|
|
39
|
+
} | {
|
|
40
|
+
use: false;
|
|
41
|
+
why: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* May the probe use this login? Only if nothing about it can change while the
|
|
45
|
+
* probe runs: an API key never does, and a ChatGPT login only while its token
|
|
46
|
+
* is fresh. Never throws.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loginForProbe(codexHome: string, now?: number): ProbeLogin;
|
|
49
|
+
/**
|
|
50
|
+
* Ask one codex binary for its models. Never throws; resolves within
|
|
51
|
+
* `timeoutMs` however the CLI behaves, and leaves no process behind.
|
|
52
|
+
*/
|
|
53
|
+
export declare function probeCodexModels(bin: string, opts?: {
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Where the login lives. Runs link it from ~/.codex (writeCodexHome), so
|
|
56
|
+
* the probe does too. */
|
|
57
|
+
codexHome?: string;
|
|
58
|
+
now?: number;
|
|
59
|
+
/** Tests only: run something else in place of the CLI. */
|
|
60
|
+
resolve?: (bin: string, args: string[]) => CliInvocation;
|
|
61
|
+
/** Tests only: where the throwaway CODEX_HOME goes. */
|
|
62
|
+
dir?: string;
|
|
63
|
+
}): Promise<ProbeResult>;
|
|
64
|
+
export interface CodexModelCacheDeps {
|
|
65
|
+
probe: (bin: string) => Promise<ProbeResult>;
|
|
66
|
+
now: () => number;
|
|
67
|
+
log: (line: string) => void;
|
|
68
|
+
}
|
|
69
|
+
/** Refreshed every 6h, retried 30 min after a failure — see model-cache.ts. */
|
|
70
|
+
export declare function createCodexModelCache(deps: CodexModelCacheDeps): {
|
|
71
|
+
get(bin: string, key: string): CodexModelReport | null;
|
|
72
|
+
last(): CodexModelReport | null;
|
|
73
|
+
};
|
|
74
|
+
/** What capabilities.ts reports for codex's models. */
|
|
75
|
+
export declare function codexModelsFor(bin: string, key: string): CodexModelReport | null;
|
|
76
|
+
/**
|
|
77
|
+
* The effort levels a model takes, from the last list codex gave: the
|
|
78
|
+
* default model's when none is named. Null when codex never said, or the
|
|
79
|
+
* model is not in its list.
|
|
80
|
+
*/
|
|
81
|
+
export declare function codexModelEfforts(model?: string | null, report?: CodexModelReport | null): string[] | null;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Which models this node's Codex login can run — asked of the CLI itself.
|
|
3
|
+
//
|
|
4
|
+
// Until 0.30.14 this was the CODEX_MODELS constant in capabilities.ts, last
|
|
5
|
+
// edited in May: gpt-5, gpt-5-mini, o4, o4-mini. A ChatGPT login refuses every
|
|
6
|
+
// one of them ("The 'gpt-5' model is not supported when using Codex with a
|
|
7
|
+
// ChatGPT account"), so the pickers offered nothing that would run, and none
|
|
8
|
+
// of the models codex has shipped since.
|
|
9
|
+
//
|
|
10
|
+
// Codex's app-server answers `model/list` with each model's id, name, the
|
|
11
|
+
// effort levels it takes and which one is the default. No thread is started
|
|
12
|
+
// and no turn is sent, so nothing is billed.
|
|
13
|
+
//
|
|
14
|
+
// Unlike Claude's, the useful answer needs the login. Logged out, codex lists
|
|
15
|
+
// only the models bundled with that CLI version; logged in, it lists what the
|
|
16
|
+
// account can run. On popadom's 0.155 (2026-10-30) the bundled list had
|
|
17
|
+
// gpt-5.5 but not gpt-6-sol or gpt-6-luna, and the account list the other way
|
|
18
|
+
// round. So the probe uses the login, but never in a way that can rotate it:
|
|
19
|
+
//
|
|
20
|
+
// * auth.json is LINKED into a throwaway CODEX_HOME (a symlink, or a hard
|
|
21
|
+
// link on Windows, where file symlinks need admin rights), never copied.
|
|
22
|
+
// A token refreshed into a copy is lost, and the next refresh from the
|
|
23
|
+
// real file reuses a refresh token that was already rotated, which gets
|
|
24
|
+
// the whole login revoked.
|
|
25
|
+
// * A ChatGPT login is only used while its token is fresh: refreshed under
|
|
26
|
+
// 5 days ago with more than 3 days left on the access token. They last 10
|
|
27
|
+
// days and codex renews them days before that, so it has no reason to
|
|
28
|
+
// refresh one during a probe.
|
|
29
|
+
//
|
|
30
|
+
// Otherwise (no login, a login due a refresh, or no way to link it) the probe
|
|
31
|
+
// runs logged out and says the list is the bundled one (scope 'bundled'). The
|
|
32
|
+
// cache keeps an account list it already has for the same binary over that.
|
|
33
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
36
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
37
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
38
|
+
}
|
|
39
|
+
Object.defineProperty(o, k2, desc);
|
|
40
|
+
}) : (function(o, m, k, k2) {
|
|
41
|
+
if (k2 === undefined) k2 = k;
|
|
42
|
+
o[k2] = m[k];
|
|
43
|
+
}));
|
|
44
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
45
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
46
|
+
}) : function(o, v) {
|
|
47
|
+
o["default"] = v;
|
|
48
|
+
});
|
|
49
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
50
|
+
var ownKeys = function(o) {
|
|
51
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
52
|
+
var ar = [];
|
|
53
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
54
|
+
return ar;
|
|
55
|
+
};
|
|
56
|
+
return ownKeys(o);
|
|
57
|
+
};
|
|
58
|
+
return function (mod) {
|
|
59
|
+
if (mod && mod.__esModule) return mod;
|
|
60
|
+
var result = {};
|
|
61
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
62
|
+
__setModuleDefault(result, mod);
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
})();
|
|
66
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.parseModelList = parseModelList;
|
|
68
|
+
exports.offeredEfforts = offeredEfforts;
|
|
69
|
+
exports.loginForProbe = loginForProbe;
|
|
70
|
+
exports.probeCodexModels = probeCodexModels;
|
|
71
|
+
exports.createCodexModelCache = createCodexModelCache;
|
|
72
|
+
exports.codexModelsFor = codexModelsFor;
|
|
73
|
+
exports.codexModelEfforts = codexModelEfforts;
|
|
74
|
+
const child_process_1 = require("child_process");
|
|
75
|
+
const fs = __importStar(require("fs"));
|
|
76
|
+
const os = __importStar(require("os"));
|
|
77
|
+
const path = __importStar(require("path"));
|
|
78
|
+
const model_cache_1 = require("./model-cache");
|
|
79
|
+
const win_1 = require("./win");
|
|
80
|
+
// Same rule as claude-models.ts: anything that does not look like a model id
|
|
81
|
+
// is dropped rather than shipped to every picker.
|
|
82
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:[\]-]{0,119}$/;
|
|
83
|
+
const MAX_ENTRIES = 64;
|
|
84
|
+
const MAX_PAGES = 5;
|
|
85
|
+
/**
|
|
86
|
+
* Turn the `data` of a `model/list` answer into a report. Hidden models
|
|
87
|
+
* (codex's internal reviewers and the like) are left out. Returns null when
|
|
88
|
+
* nothing usable is in there.
|
|
89
|
+
*/
|
|
90
|
+
function parseModelList(raw, scope) {
|
|
91
|
+
if (!Array.isArray(raw))
|
|
92
|
+
return null;
|
|
93
|
+
const catalog = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const item of raw.slice(0, MAX_ENTRIES)) {
|
|
96
|
+
if (!item || typeof item !== 'object')
|
|
97
|
+
continue;
|
|
98
|
+
const o = item;
|
|
99
|
+
if (o.hidden === true)
|
|
100
|
+
continue;
|
|
101
|
+
const id = [o.model, o.id].find((v) => typeof v === 'string' && SAFE_ID.test(v.trim()))?.trim();
|
|
102
|
+
if (!id || seen.has(id))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(id);
|
|
105
|
+
const name = typeof o.displayName === 'string' && o.displayName.trim() ? o.displayName.trim().slice(0, 80) : id;
|
|
106
|
+
const efforts = Array.isArray(o.supportedReasoningEfforts)
|
|
107
|
+
? o.supportedReasoningEfforts
|
|
108
|
+
.map(e => (typeof e === 'string' ? e : e?.reasoningEffort))
|
|
109
|
+
.filter((e) => typeof e === 'string' && /^[a-z]{1,20}$/.test(e))
|
|
110
|
+
: [];
|
|
111
|
+
catalog.push({
|
|
112
|
+
id, resolved: id, name, alias: false,
|
|
113
|
+
...(efforts.length ? { efforts } : {}),
|
|
114
|
+
...(o.isDefault === true && !catalog.some(e => e.default) ? { default: true } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (catalog.length === 0)
|
|
118
|
+
return null;
|
|
119
|
+
// The default first — it is what a blank model runs. The rest stay in
|
|
120
|
+
// codex's own order (newest first).
|
|
121
|
+
catalog.sort((a, b) => Number(Boolean(b.default)) - Number(Boolean(a.default)));
|
|
122
|
+
const defaultModel = catalog.find(e => e.default)?.id;
|
|
123
|
+
return { models: catalog.map(e => e.id), catalog, ...(defaultModel ? { defaultModel } : {}), scope };
|
|
124
|
+
}
|
|
125
|
+
/** The effort levels this platform stores (the DB's check constraints) and
|
|
126
|
+
* codex also knows. Codex's `ultra` is not one of them. */
|
|
127
|
+
const PLATFORM_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
128
|
+
/** What a picker can offer: every level some listed model takes, in order. */
|
|
129
|
+
function offeredEfforts(report) {
|
|
130
|
+
const any = new Set(report.catalog.flatMap(e => e.efforts ?? []));
|
|
131
|
+
return PLATFORM_EFFORTS.filter(e => any.has(e));
|
|
132
|
+
}
|
|
133
|
+
/* ── the login ─────────────────────────────────────────────────────────── */
|
|
134
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
135
|
+
const FRESH_FOR_MS = 5 * DAY_MS;
|
|
136
|
+
const MIN_LEFT_MS = 3 * DAY_MS;
|
|
137
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
138
|
+
/** When the access token runs out, from its own `exp` claim; null if it is
|
|
139
|
+
* not a JWT we can read. Only the expiry is looked at. */
|
|
140
|
+
function accessTokenExpiry(token) {
|
|
141
|
+
if (!nonEmpty(token))
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8'));
|
|
145
|
+
return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* May the probe use this login? Only if nothing about it can change while the
|
|
153
|
+
* probe runs: an API key never does, and a ChatGPT login only while its token
|
|
154
|
+
* is fresh. Never throws.
|
|
155
|
+
*/
|
|
156
|
+
function loginForProbe(codexHome, now = Date.now()) {
|
|
157
|
+
const file = path.join(codexHome, 'auth.json');
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { use: false, why: 'no login' };
|
|
164
|
+
}
|
|
165
|
+
const tokens = (parsed.tokens ?? null);
|
|
166
|
+
if (!tokens || !(nonEmpty(tokens.access_token) || nonEmpty(tokens.refresh_token))) {
|
|
167
|
+
return nonEmpty(parsed.OPENAI_API_KEY) ? { use: true, file } : { use: false, why: 'no login' };
|
|
168
|
+
}
|
|
169
|
+
const refreshed = nonEmpty(parsed.last_refresh) ? Date.parse(parsed.last_refresh) : NaN;
|
|
170
|
+
if (!Number.isFinite(refreshed) || now - refreshed > FRESH_FOR_MS) {
|
|
171
|
+
return { use: false, why: 'the login is due a refresh, so it was left alone' };
|
|
172
|
+
}
|
|
173
|
+
const expires = accessTokenExpiry(tokens.access_token);
|
|
174
|
+
if (expires !== null && expires - now < MIN_LEFT_MS) {
|
|
175
|
+
return { use: false, why: 'the access token runs out soon, so the login was left alone' };
|
|
176
|
+
}
|
|
177
|
+
return { use: true, file };
|
|
178
|
+
}
|
|
179
|
+
/** Put the login in the probe's CODEX_HOME without copying it. */
|
|
180
|
+
function linkLogin(file, dir) {
|
|
181
|
+
const target = path.join(dir, 'auth.json');
|
|
182
|
+
try {
|
|
183
|
+
if (process.platform === 'win32')
|
|
184
|
+
fs.linkSync(file, target);
|
|
185
|
+
else
|
|
186
|
+
fs.symlinkSync(file, target);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/* ── the probe ─────────────────────────────────────────────────────────── */
|
|
194
|
+
const PROBE_ARGS = ['app-server', '--listen', 'stdio://'];
|
|
195
|
+
const MAX_LINE = 2 * 1024 * 1024;
|
|
196
|
+
/**
|
|
197
|
+
* Ask one codex binary for its models. Never throws; resolves within
|
|
198
|
+
* `timeoutMs` however the CLI behaves, and leaves no process behind.
|
|
199
|
+
*/
|
|
200
|
+
function probeCodexModels(bin, opts = {}) {
|
|
201
|
+
return new Promise(done => {
|
|
202
|
+
let settled = false;
|
|
203
|
+
let inv;
|
|
204
|
+
try {
|
|
205
|
+
inv = (opts.resolve ?? win_1.resolveCliInvocation)(bin, PROBE_ARGS);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
done({ ok: false, reason: err.message || String(err) });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// The probe's cwd and its CODEX_HOME: no config.toml, no MCP servers, and
|
|
212
|
+
// the state codex writes (sqlite logs and the like) stays out of ~/.codex.
|
|
213
|
+
const dir = opts.dir ?? (0, model_cache_1.makeProbeDir)('codex-');
|
|
214
|
+
if (!dir) {
|
|
215
|
+
done({ ok: false, reason: 'no folder to probe from' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const login = loginForProbe(opts.codexHome ?? path.join(os.homedir(), '.codex'), opts.now ?? Date.now());
|
|
219
|
+
const linked = login.use && linkLogin(login.file, dir);
|
|
220
|
+
const scope = linked ? 'account' : 'bundled';
|
|
221
|
+
// Removing the link never touches the real file. It goes as soon as there
|
|
222
|
+
// is an answer, not when codex gets round to exiting.
|
|
223
|
+
const unlinkLogin = () => {
|
|
224
|
+
if (linked) {
|
|
225
|
+
try {
|
|
226
|
+
fs.unlinkSync(path.join(dir, 'auth.json'));
|
|
227
|
+
}
|
|
228
|
+
catch { /* already gone, or swept later */ }
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const cleanup = () => {
|
|
232
|
+
unlinkLogin();
|
|
233
|
+
if (opts.dir)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
237
|
+
}
|
|
238
|
+
catch { /* swept later */ }
|
|
239
|
+
};
|
|
240
|
+
const env = { ...process.env, CODEX_HOME: dir };
|
|
241
|
+
let child;
|
|
242
|
+
try {
|
|
243
|
+
child = (0, child_process_1.spawn)(inv.file, inv.args, { cwd: dir, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
cleanup();
|
|
247
|
+
done({ ok: false, reason: err.message || String(err) });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
child.once('close', cleanup);
|
|
251
|
+
const finish = (result) => {
|
|
252
|
+
if (settled)
|
|
253
|
+
return;
|
|
254
|
+
settled = true;
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
unlinkLogin();
|
|
257
|
+
// The app-server exits when its input ends; the kill is the backstop.
|
|
258
|
+
try {
|
|
259
|
+
child.stdin?.end();
|
|
260
|
+
}
|
|
261
|
+
catch { /* already closed */ }
|
|
262
|
+
const reap = setTimeout(() => {
|
|
263
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
264
|
+
(0, win_1.killProcessTree)(child);
|
|
265
|
+
}, 3000);
|
|
266
|
+
reap.unref();
|
|
267
|
+
child.once('exit', () => clearTimeout(reap));
|
|
268
|
+
done(result);
|
|
269
|
+
};
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
finish({ ok: false, reason: `no answer within ${opts.timeoutMs ?? 30000}ms` });
|
|
272
|
+
(0, win_1.killProcessTree)(child);
|
|
273
|
+
}, opts.timeoutMs ?? 30000);
|
|
274
|
+
timer.unref();
|
|
275
|
+
child.on('error', err => finish({ ok: false, reason: err.message || String(err) }));
|
|
276
|
+
child.on('close', (code, signal) => {
|
|
277
|
+
finish({ ok: false, reason: `exited ${code ?? signal} before answering${stderr ? `: ${stderr.trim().slice(0, 200)}` : ''}` });
|
|
278
|
+
});
|
|
279
|
+
child.stdin?.on('error', () => { });
|
|
280
|
+
let stderr = '';
|
|
281
|
+
child.stderr?.on('data', (d) => { if (stderr.length < 4096)
|
|
282
|
+
stderr += d.toString('utf8'); });
|
|
283
|
+
const send = (msg) => {
|
|
284
|
+
try {
|
|
285
|
+
child.stdin?.write(JSON.stringify(msg) + '\n');
|
|
286
|
+
}
|
|
287
|
+
catch { /* reported via 'close' / 'error' */ }
|
|
288
|
+
};
|
|
289
|
+
const models = [];
|
|
290
|
+
let page = 0;
|
|
291
|
+
const listPage = (cursor) => {
|
|
292
|
+
page += 1;
|
|
293
|
+
send({ id: 1 + page, method: 'model/list', params: { includeHidden: false, limit: 100, ...(cursor ? { cursor } : {}) } });
|
|
294
|
+
};
|
|
295
|
+
let buf = '';
|
|
296
|
+
child.stdout?.on('data', (d) => {
|
|
297
|
+
buf += d.toString('utf8');
|
|
298
|
+
if (buf.length > MAX_LINE) {
|
|
299
|
+
finish({ ok: false, reason: 'oversized output' });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
let nl;
|
|
303
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
304
|
+
const line = buf.slice(0, nl).trim();
|
|
305
|
+
buf = buf.slice(nl + 1);
|
|
306
|
+
if (!line.startsWith('{'))
|
|
307
|
+
continue;
|
|
308
|
+
let msg;
|
|
309
|
+
try {
|
|
310
|
+
msg = JSON.parse(line);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Notifications and requests from the server carry a method; only
|
|
316
|
+
// answers to our own requests matter here.
|
|
317
|
+
if (typeof msg.method === 'string' || typeof msg.id !== 'number')
|
|
318
|
+
continue;
|
|
319
|
+
if (msg.error) {
|
|
320
|
+
const what = msg.id === 1 ? 'initialize' : 'model/list';
|
|
321
|
+
finish({ ok: false, reason: `${what} refused: ${String(msg.error?.message ?? 'unknown').slice(0, 200)}` });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (msg.id === 1) {
|
|
325
|
+
send({ method: 'initialized', params: {} });
|
|
326
|
+
listPage();
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (msg.id !== 1 + page)
|
|
330
|
+
continue;
|
|
331
|
+
const data = msg.result?.data;
|
|
332
|
+
if (Array.isArray(data))
|
|
333
|
+
models.push(...data);
|
|
334
|
+
const next = msg.result?.nextCursor;
|
|
335
|
+
if (typeof next === 'string' && next && page < MAX_PAGES && models.length < MAX_ENTRIES) {
|
|
336
|
+
listPage(next);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const report = parseModelList(models, scope);
|
|
340
|
+
finish(report
|
|
341
|
+
? { ok: true, report }
|
|
342
|
+
: { ok: false, reason: `model/list answered without a usable model${scope === 'bundled' && !login.use ? ` (${login.why})` : ''}` });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
send({
|
|
347
|
+
id: 1, method: 'initialize',
|
|
348
|
+
params: { clientInfo: { name: 'ainode_model_probe', title: '+Ai node model probe', version: '1.0.0' }, capabilities: { experimentalApi: true } },
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** Refreshed every 6h, retried 30 min after a failure — see model-cache.ts. */
|
|
353
|
+
function createCodexModelCache(deps) {
|
|
354
|
+
return (0, model_cache_1.createModelCache)({
|
|
355
|
+
...deps,
|
|
356
|
+
agent: 'codex',
|
|
357
|
+
describe: report => [
|
|
358
|
+
report.defaultModel ? `default ${report.defaultModel}` : '',
|
|
359
|
+
report.scope === 'bundled' ? 'the list bundled with the CLI; the login was left alone' : '',
|
|
360
|
+
].filter(Boolean).join('; '),
|
|
361
|
+
// A probe that had to leave the login alone sees less than one that used
|
|
362
|
+
// it; the account's list stays until the next probe can read it again.
|
|
363
|
+
keep: (held, next) => held.scope === 'account' && next.scope === 'bundled',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const sharedCache = createCodexModelCache({
|
|
367
|
+
probe: bin => probeCodexModels(bin),
|
|
368
|
+
now: () => Date.now(),
|
|
369
|
+
log: line => console.log(line),
|
|
370
|
+
});
|
|
371
|
+
/** What capabilities.ts reports for codex's models. */
|
|
372
|
+
function codexModelsFor(bin, key) {
|
|
373
|
+
return sharedCache.get(bin, key);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The effort levels a model takes, from the last list codex gave: the
|
|
377
|
+
* default model's when none is named. Null when codex never said, or the
|
|
378
|
+
* model is not in its list.
|
|
379
|
+
*/
|
|
380
|
+
function codexModelEfforts(model, report = sharedCache.last()) {
|
|
381
|
+
if (!report)
|
|
382
|
+
return null;
|
|
383
|
+
const entry = model ? report.catalog.find(e => e.id === model) : report.catalog.find(e => e.default);
|
|
384
|
+
return entry?.efforts?.length ? entry.efforts : null;
|
|
385
|
+
}
|
package/dist/codex-spawn.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface CodexInput {
|
|
|
12
12
|
prompt: string;
|
|
13
13
|
model?: string;
|
|
14
14
|
effortLevel?: RuntimeEffortLevel;
|
|
15
|
+
/** The effort levels codex listed for this model (codexModelEfforts), so
|
|
16
|
+
* effortLevel can be clamped to one it takes. */
|
|
17
|
+
supportedEfforts?: readonly string[] | null;
|
|
15
18
|
/** Sandbox for normal non-bypass runs. Defaults to workspace-write. */
|
|
16
19
|
sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
17
20
|
/** Skip Codex approvals and sandboxing. Codex's "yolo" equivalent. */
|
|
@@ -73,12 +76,15 @@ export interface CodexHandle {
|
|
|
73
76
|
onActivity(cb: () => void): void;
|
|
74
77
|
done: Promise<number>;
|
|
75
78
|
}
|
|
76
|
-
/** Clamp our effort ladder to a value codex
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
79
|
+
/** Clamp our effort ladder to a value this codex accepts. A codex that does
|
|
80
|
+
* not know a level FAIL-FASTS (exit 2, dead-letter), and a ladder failover
|
|
81
|
+
* (e.g. claude(max) → codex) forwards req.effort across agents unchanged.
|
|
82
|
+
*
|
|
83
|
+
* `supported` is what codex listed for the model about to run
|
|
84
|
+
* (codexModelEfforts): the level asked for if the model takes it, else the
|
|
85
|
+
* highest it takes below that. Without a list — an older codex, or one that
|
|
86
|
+
* has not answered yet — only `minimal|low|medium|high` is safe, so xhigh
|
|
87
|
+
* and max become high, as they always did. Anything unrecognised emits no
|
|
88
|
+
* effort at all rather than a bad one. Mirrors grokEffort() in grok-spawn.ts. */
|
|
89
|
+
export declare function codexEffort(level?: RuntimeEffortLevel, supported?: readonly string[] | null): string | null;
|
|
84
90
|
export declare function spawnCodex(input: CodexInput): CodexHandle;
|
package/dist/codex-spawn.js
CHANGED
|
@@ -328,14 +328,31 @@ function lineToEvents(line) {
|
|
|
328
328
|
// new "codex:" namespace which adds zero value.
|
|
329
329
|
return [{ type: `claude:codex.${type || 'unknown'}`, raw: line }];
|
|
330
330
|
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
|
|
331
|
+
/** codex's effort levels, lowest first. */
|
|
332
|
+
const CODEX_EFFORT_LADDER = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
333
|
+
/** Clamp our effort ladder to a value this codex accepts. A codex that does
|
|
334
|
+
* not know a level FAIL-FASTS (exit 2, dead-letter), and a ladder failover
|
|
335
|
+
* (e.g. claude(max) → codex) forwards req.effort across agents unchanged.
|
|
336
|
+
*
|
|
337
|
+
* `supported` is what codex listed for the model about to run
|
|
338
|
+
* (codexModelEfforts): the level asked for if the model takes it, else the
|
|
339
|
+
* highest it takes below that. Without a list — an older codex, or one that
|
|
340
|
+
* has not answered yet — only `minimal|low|medium|high` is safe, so xhigh
|
|
341
|
+
* and max become high, as they always did. Anything unrecognised emits no
|
|
342
|
+
* effort at all rather than a bad one. Mirrors grokEffort() in grok-spawn.ts. */
|
|
343
|
+
function codexEffort(level, supported) {
|
|
344
|
+
const want = CODEX_EFFORT_LADDER.indexOf(level ?? '');
|
|
345
|
+
if (want < 0)
|
|
346
|
+
return null;
|
|
347
|
+
if (supported?.length) {
|
|
348
|
+
let best = -1;
|
|
349
|
+
for (const s of supported) {
|
|
350
|
+
const i = CODEX_EFFORT_LADDER.indexOf(s);
|
|
351
|
+
if (i >= 0 && i <= want && i > best)
|
|
352
|
+
best = i;
|
|
353
|
+
}
|
|
354
|
+
return best >= 0 ? CODEX_EFFORT_LADDER[best] : null;
|
|
355
|
+
}
|
|
339
356
|
switch (level) {
|
|
340
357
|
case 'low': return 'low';
|
|
341
358
|
case 'medium': return 'medium';
|
|
@@ -363,9 +380,12 @@ function spawnCodex(input) {
|
|
|
363
380
|
baseFlags.push('--sandbox', input.sandboxMode ?? 'workspace-write');
|
|
364
381
|
if (input.model)
|
|
365
382
|
baseFlags.push('--model', input.model);
|
|
366
|
-
const effort = codexEffort(input.effortLevel);
|
|
383
|
+
const effort = codexEffort(input.effortLevel, input.supportedEfforts);
|
|
367
384
|
if (effort) {
|
|
368
|
-
|
|
385
|
+
// `model_reasoning_effort` is the key codex reads. The `reasoning.effort`
|
|
386
|
+
// this sent before was ignored ("session-flags: `reasoning` is ignored",
|
|
387
|
+
// codex 0.164), so every codex run went out at its model's default.
|
|
388
|
+
baseFlags.push('-c', `model_reasoning_effort=${JSON.stringify(effort)}`);
|
|
369
389
|
}
|
|
370
390
|
const args = input.resumeThreadId
|
|
371
391
|
? ['exec', ...baseFlags, 'resume', input.resumeThreadId, input.prompt]
|