@firefunc-agent/runner 0.5.0

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.
@@ -0,0 +1,42 @@
1
+ export type RunnerConfig = {
2
+ apiUrl: string;
3
+ token: string;
4
+ name?: string;
5
+ repos?: Record<string, string>;
6
+ reposDir?: string;
7
+ claudeBin?: string;
8
+ engine?: string;
9
+ codexBin?: string;
10
+ codexUseApiKey?: boolean;
11
+ codexArgs?: string[];
12
+ geminiBin?: string;
13
+ geminiApiKey?: string;
14
+ geminiArgs?: string[];
15
+ cursorAgentBin?: string;
16
+ cursorApiKey?: string;
17
+ cursorArgs?: string[];
18
+ quiet?: boolean;
19
+ viewerPort?: number;
20
+ viewerEnabled?: boolean;
21
+ maxParallel?: number;
22
+ devServerBasePort?: number;
23
+ model?: string;
24
+ effort?: string;
25
+ permissionMode?: string;
26
+ allowedTools?: string;
27
+ maxBudgetUsd?: number;
28
+ claudeCodeOAuthToken?: string;
29
+ claudeUseApiKey?: boolean;
30
+ claudeArgs?: string[];
31
+ jobTimeoutMinutes?: number;
32
+ baseBranch?: string;
33
+ claudeEnv?: Record<string, string>;
34
+ setupCommands?: string[];
35
+ autoInstall?: boolean;
36
+ setupTimeoutMinutes?: number;
37
+ envFile?: string;
38
+ backgroundCommands?: string[];
39
+ };
40
+ export declare function configPath(): string;
41
+ export declare function loadConfig(): RunnerConfig | null;
42
+ export declare function saveConfig(cfg: RunnerConfig): void;
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+ export function configPath() {
5
+ return process.env.FIREFUNC_RUNNER_CONFIG ?? join(homedir(), '.firefunc-runner', 'config.json');
6
+ }
7
+ export function loadConfig() {
8
+ const p = configPath();
9
+ if (!existsSync(p))
10
+ return null;
11
+ try {
12
+ return JSON.parse(readFileSync(p, 'utf8'));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function saveConfig(cfg) {
19
+ const p = configPath();
20
+ mkdirSync(dirname(p), { recursive: true });
21
+ writeFileSync(p, JSON.stringify(cfg, null, 2), { mode: 0o600 });
22
+ }
23
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,2 @@
1
+ import type { RunnerConfig } from './config.js';
2
+ export declare function startDaemon(cfg: RunnerConfig): Promise<void>;
package/dist/daemon.js ADDED
@@ -0,0 +1,152 @@
1
+ import { hostname, platform } from 'node:os';
2
+ import { RunnerApi } from './api.js';
3
+ import { availableEngines, getAdapter, supportedEngines } from './engines/index.js';
4
+ import { runJob, resolveRepoPath } from './job.js';
5
+ import { createSessionViewer } from './viewer.js';
6
+ import { runJobPool } from './pool.js';
7
+ const VERSION = '0.6.0';
8
+ function log(msg) {
9
+ console.log(`[firefunc-runner] ${new Date().toISOString()} ${msg}`);
10
+ }
11
+ export async function startDaemon(cfg) {
12
+ const api = new RunnerApi(cfg.apiUrl, cfg.token);
13
+ const viewerPort = cfg.viewerEnabled === false ? null : (cfg.viewerPort ?? 8787);
14
+ const declaredEngines = availableEngines(cfg);
15
+ const skipped = supportedEngines().filter((e) => !declaredEngines.includes(e));
16
+ if (declaredEngines.length > 0) {
17
+ log(`engines available on this machine: ${declaredEngines.join(', ')}`);
18
+ }
19
+ else {
20
+ log('WARNING: no engine CLI found on PATH — this runner cannot run jobs. Install "claude" (or set --claude-bin).');
21
+ }
22
+ if (skipped.length > 0) {
23
+ log(`engines not installed (skipped): ${skipped.join(', ')} — install the CLI to enable them`);
24
+ }
25
+ const meta = {
26
+ version: VERSION,
27
+ hostname: hostname(),
28
+ platform: platform(),
29
+ viewerPort,
30
+ engines: declaredEngines,
31
+ };
32
+ let stopping = false;
33
+ const viewer = cfg.viewerEnabled === false
34
+ ? undefined
35
+ : createSessionViewer({
36
+ port: cfg.viewerPort,
37
+ onListen: (u) => log(`live session viewer → ${u} (local only — watch Claude work in a browser)`),
38
+ onError: (m) => log(`${m} — set "viewerPort" or pass --no-viewer`),
39
+ });
40
+ const stop = (sig) => {
41
+ if (stopping)
42
+ return;
43
+ stopping = true;
44
+ log(`${sig} received — finishing in-flight job(s) then exiting…`);
45
+ };
46
+ process.on('SIGINT', () => stop('SIGINT'));
47
+ process.on('SIGTERM', () => stop('SIGTERM'));
48
+ let serverMax = 1;
49
+ const refreshConfig = async () => {
50
+ const c = await api.config();
51
+ if (c)
52
+ serverMax = Math.max(1, Math.floor(c.maxParallel));
53
+ };
54
+ await refreshConfig().catch(() => { });
55
+ const cfgTimer = setInterval(() => void refreshConfig().catch(() => { }), 60_000);
56
+ let budgets = [];
57
+ const refreshBudgets = async () => {
58
+ const reports = [];
59
+ for (const id of supportedEngines()) {
60
+ const read = getAdapter(id).readBudget;
61
+ if (!read)
62
+ continue;
63
+ try {
64
+ const r = await read(cfg);
65
+ if (r)
66
+ reports.push(r);
67
+ }
68
+ catch {
69
+ }
70
+ }
71
+ budgets = reports;
72
+ };
73
+ await refreshBudgets().catch(() => { });
74
+ const budgetTimer = setInterval(() => void refreshBudgets().catch(() => { }), 5 * 60_000);
75
+ const effectiveMax = () => Math.max(1, Math.floor(cfg.maxParallel ?? serverMax));
76
+ log(`connected to ${cfg.apiUrl} as "${cfg.name ?? meta.hostname}" — watching for work` +
77
+ (effectiveMax() > 1 ? ` (up to ${effectiveMax()} parallel sessions).` : '.'));
78
+ try {
79
+ const routed = await api.repos();
80
+ const unmapped = routed.filter((r) => !resolveRepoPath(r, cfg));
81
+ if (unmapped.length) {
82
+ log(`${unmapped.length} routed repo(s) need a local checkout — run one of:`);
83
+ for (const r of unmapped) {
84
+ log(` firefunc-runner repo ${r} <path-to-your-${r.split('/').pop()}-checkout>`);
85
+ }
86
+ }
87
+ else if (routed.length) {
88
+ log(`all ${routed.length} routed repo(s) are mapped to a local checkout. ✓`);
89
+ }
90
+ }
91
+ catch {
92
+ }
93
+ const portBase = cfg.devServerBasePort ?? 3100;
94
+ const usedPorts = new Set();
95
+ const takePort = () => {
96
+ if (effectiveMax() <= 1)
97
+ return undefined;
98
+ for (let p = portBase; p < portBase + 500; p++) {
99
+ if (!usedPorts.has(p)) {
100
+ usedPorts.add(p);
101
+ return p;
102
+ }
103
+ }
104
+ return undefined;
105
+ };
106
+ let backoffMs = 2_000;
107
+ await runJobPool({
108
+ maxParallel: effectiveMax,
109
+ stopping: () => stopping,
110
+ claim: async () => {
111
+ const job = await api.claim({ ...meta, budget: budgets });
112
+ backoffMs = 2_000;
113
+ return job;
114
+ },
115
+ onClaimError: async (err) => {
116
+ const msg = err.message;
117
+ if (/token rejected/.test(msg)) {
118
+ log(`FATAL: ${msg}. Re-run "firefunc-runner connect <token>".`);
119
+ process.exitCode = 1;
120
+ return 'fatal';
121
+ }
122
+ log(`poll error: ${msg} — retrying in ${Math.round(backoffMs / 1000)}s`);
123
+ await new Promise((r) => setTimeout(r, backoffMs));
124
+ backoffMs = Math.min(backoffMs * 2, 60_000);
125
+ return 'retry';
126
+ },
127
+ onStart: (job, inFlight) => {
128
+ log(`[${inFlight}/${effectiveMax()}] claimed ${job.runId} — ${job.title ?? job.externalId ?? 'a bug'} (${job.repo ?? '?'})`);
129
+ if (viewer?.url)
130
+ log(` watch live → ${viewer.url}/s/${job.runId}`);
131
+ },
132
+ run: async (job) => {
133
+ const devServerPort = takePort();
134
+ try {
135
+ await runJob(job, cfg, api, viewer, { devServerPort });
136
+ }
137
+ catch (e) {
138
+ log(`job ${job.runId} errored: ${e.message}`);
139
+ }
140
+ finally {
141
+ if (devServerPort != null)
142
+ usedPorts.delete(devServerPort);
143
+ log(`done ${job.runId}`);
144
+ }
145
+ },
146
+ });
147
+ clearInterval(cfgTimer);
148
+ clearInterval(budgetTimer);
149
+ viewer?.close();
150
+ log('stopped.');
151
+ }
152
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,4 @@
1
+ import type { RunnerConfig } from '../config.js';
2
+ import type { EngineBudgetReport, EngineBudgetWindow } from './types.js';
3
+ export declare function parseClaudeUsage(json: unknown): EngineBudgetWindow[];
4
+ export declare function readClaudeBudget(cfg: RunnerConfig): Promise<EngineBudgetReport | null>;
@@ -0,0 +1,122 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const execFileAsync = promisify(execFile);
7
+ const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
8
+ const USAGE_BETA = 'oauth-2025-04-20';
9
+ const FETCH_TIMEOUT_MS = 5_000;
10
+ function toPct(v) {
11
+ if (typeof v !== 'number' || !Number.isFinite(v))
12
+ return null;
13
+ const p = v <= 1 ? v * 100 : v;
14
+ return Math.max(0, Math.min(100, Math.round(p)));
15
+ }
16
+ export function parseClaudeUsage(json) {
17
+ if (!json || typeof json !== 'object')
18
+ return [];
19
+ const o = json;
20
+ if (Array.isArray(o.limits)) {
21
+ const out = [];
22
+ for (const lim of o.limits) {
23
+ const pct = toPct(lim?.percent ?? lim?.utilization ?? lim?.used_pct);
24
+ if (pct === null)
25
+ continue;
26
+ const resetsAt = typeof lim?.resets_at === 'string' ? lim.resets_at : null;
27
+ const kind = String(lim?.kind ?? '');
28
+ if (kind === 'session' || kind === 'five_hour') {
29
+ out.push({ window: 'rolling_5h', label: '5-hour limit', pctUsed: pct, resetsAt });
30
+ }
31
+ else if (kind === 'weekly_all' || kind === 'seven_day' || kind === 'weekly') {
32
+ out.push({ window: 'weekly', label: 'Weekly · all models', pctUsed: pct, resetsAt });
33
+ }
34
+ else if (kind === 'weekly_scoped') {
35
+ const scope = lim?.scope;
36
+ const name = typeof scope?.model?.display_name === 'string' ? scope.model.display_name : 'scoped';
37
+ out.push({ window: 'weekly_scoped', label: `Weekly · ${name}`, pctUsed: pct, resetsAt });
38
+ }
39
+ }
40
+ if (out.length > 0)
41
+ return out;
42
+ }
43
+ const out = [];
44
+ const push = (window, label, node) => {
45
+ if (!node || typeof node !== 'object')
46
+ return;
47
+ const n = node;
48
+ const pct = toPct(n.utilization ?? n.percent ?? n.used_pct);
49
+ if (pct === null)
50
+ return;
51
+ const resetsAt = typeof n.resets_at === 'string' ? n.resets_at : null;
52
+ out.push({ window, label, pctUsed: pct, resetsAt });
53
+ };
54
+ push('rolling_5h', '5-hour limit', o.five_hour);
55
+ push('weekly', 'Weekly · all models', o.seven_day ?? o.weekly);
56
+ return out;
57
+ }
58
+ function accessTokenFromBlob(raw) {
59
+ try {
60
+ const j = JSON.parse(raw);
61
+ const tok = j?.claudeAiOauth?.accessToken;
62
+ return typeof tok === 'string' && tok.length > 0 ? tok : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ async function readMacKeychainToken() {
69
+ if (process.platform !== 'darwin')
70
+ return null;
71
+ try {
72
+ const { stdout } = await execFileAsync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], { timeout: 5_000 });
73
+ return accessTokenFromBlob(stdout.trim());
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ async function resolveOAuthToken(cfg) {
80
+ if (cfg.claudeCodeOAuthToken)
81
+ return cfg.claudeCodeOAuthToken;
82
+ try {
83
+ const raw = await readFile(join(homedir(), '.claude', '.credentials.json'), 'utf8');
84
+ const tok = accessTokenFromBlob(raw);
85
+ if (tok)
86
+ return tok;
87
+ }
88
+ catch {
89
+ }
90
+ return readMacKeychainToken();
91
+ }
92
+ export async function readClaudeBudget(cfg) {
93
+ if (cfg.claudeUseApiKey)
94
+ return null;
95
+ const token = await resolveOAuthToken(cfg);
96
+ if (!token)
97
+ return null;
98
+ try {
99
+ const ctl = new AbortController();
100
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
101
+ let res;
102
+ try {
103
+ res = await fetch(USAGE_URL, {
104
+ headers: { authorization: `Bearer ${token}`, 'anthropic-beta': USAGE_BETA },
105
+ signal: ctl.signal,
106
+ });
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ }
111
+ if (!res.ok)
112
+ return null;
113
+ const windows = parseClaudeUsage(await res.json());
114
+ if (windows.length === 0)
115
+ return null;
116
+ return { engine: 'claude_code', planLabel: 'Claude subscription', windows };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ //# sourceMappingURL=claude-budget.js.map
@@ -0,0 +1,2 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ export declare const claudeAdapter: AgentAdapter;
@@ -0,0 +1,119 @@
1
+ import { makeStreamJsonParser } from '../session-stream.js';
2
+ import { readClaudeBudget } from './claude-budget.js';
3
+ function claudeSummary(streamJson) {
4
+ const lines = streamJson.trim().split('\n');
5
+ for (let i = lines.length - 1; i >= 0; i--) {
6
+ try {
7
+ const ev = JSON.parse(lines[i]);
8
+ if (ev.type === 'result' && typeof ev.result === 'string' && ev.result.trim()) {
9
+ return ev.result.trim();
10
+ }
11
+ if (ev.type === 'assistant' && ev.message?.content) {
12
+ const text = ev.message.content
13
+ .filter((c) => c.type === 'text' && c.text)
14
+ .map((c) => c.text)
15
+ .join('')
16
+ .trim();
17
+ if (text)
18
+ return text;
19
+ }
20
+ }
21
+ catch {
22
+ }
23
+ }
24
+ return '';
25
+ }
26
+ export const claudeAdapter = {
27
+ id: 'claude_code',
28
+ displayName: 'Claude Code',
29
+ branchPrefix: 'claude',
30
+ bin(cfg) {
31
+ return cfg.claudeBin ?? 'claude';
32
+ },
33
+ buildInvocation(ctx) {
34
+ const { cfg } = ctx;
35
+ const model = ctx.model ?? 'opus';
36
+ const effortFlag = ctx.ultracode ? 'xhigh' : ctx.effort;
37
+ const tools = ctx.allowedTools ??
38
+ 'Read Edit Write Bash Glob Grep WebFetch WebSearch Task TodoWrite NotebookEdit';
39
+ const args = [
40
+ '-p',
41
+ ...(ctx.isWin ? [] : [ctx.prompt]),
42
+ '--output-format',
43
+ 'stream-json',
44
+ '--verbose',
45
+ '--permission-mode',
46
+ ctx.permissionMode,
47
+ '--allowedTools',
48
+ tools,
49
+ ];
50
+ if (model)
51
+ args.push('--model', model);
52
+ if (effortFlag)
53
+ args.push('--effort', effortFlag);
54
+ if (ctx.attachDir && ctx.attachPaths.length)
55
+ args.push('--add-dir', ctx.attachDir);
56
+ if (ctx.maxBudgetUsd)
57
+ args.push('--max-budget-usd', String(ctx.maxBudgetUsd));
58
+ args.push(...(cfg.claudeArgs ?? []));
59
+ const env = cfg.claudeUseApiKey
60
+ ? ctx.runEnv
61
+ : { ...ctx.runEnv, ANTHROPIC_API_KEY: undefined };
62
+ const authLogLine = cfg.claudeUseApiKey
63
+ ? 'ANTHROPIC_API_KEY (API billing — opted in)'
64
+ : cfg.claudeCodeOAuthToken
65
+ ? 'subscription token'
66
+ : 'your machine Claude login (API key hidden — subscription billing)';
67
+ return {
68
+ bin: this.bin(cfg),
69
+ args,
70
+ env,
71
+ stdinInput: ctx.isWin ? ctx.prompt : undefined,
72
+ shell: ctx.isWin,
73
+ authLogLine,
74
+ };
75
+ },
76
+ makeStreamPrinter() {
77
+ const short = (s, n = 160) => {
78
+ const one = s.replace(/\s+/g, ' ').trim();
79
+ return one.length > n ? `${one.slice(0, n)}…` : one;
80
+ };
81
+ return makeStreamJsonParser((ev) => {
82
+ if (ev.kind === 'text')
83
+ process.stdout.write(` │ ${short(ev.text)}\n`);
84
+ else if (ev.kind === 'tool')
85
+ process.stdout.write(` │ ⚒ ${ev.name}${ev.arg ? ` ${short(ev.arg, 90)}` : ''}\n`);
86
+ else
87
+ process.stdout.write(` └ ${short(ev.text, 240)}\n`);
88
+ });
89
+ },
90
+ summarize(stdout) {
91
+ return claudeSummary(stdout);
92
+ },
93
+ authFailureHint(text) {
94
+ const t = text.toLowerCase();
95
+ if (t.includes('credit balance is too low')) {
96
+ return 'Claude Code billed pay-as-you-go API credits (balance $0), not your subscription. On the runner machine run `claude setup-token` and set it via `--claude-oauth-token` (or clear ANTHROPIC_API_KEY before starting), then retry.';
97
+ }
98
+ if (t.includes('invalid x-api-key') ||
99
+ t.includes('invalid api key') ||
100
+ t.includes('authentication_error') ||
101
+ t.includes('oauth token has expired') ||
102
+ t.includes('oauth token expired') ||
103
+ t.includes('please run /login')) {
104
+ return 'Claude Code could not authenticate. Fix the runner’s Claude auth — run `claude setup-token` and pass `--claude-oauth-token`, or clear a stale/invalid ANTHROPIC_API_KEY — then retry.';
105
+ }
106
+ return null;
107
+ },
108
+ rateLimitHint(text) {
109
+ const t = text.toLowerCase();
110
+ if (/rate limit|rate_limit|too many requests|\b429\b|max concurrent|usage limit|usage_limit|overloaded_error/.test(t)) {
111
+ return 'Claude Code hit a rate / usage limit — likely too many parallel sessions on your subscription. This run was throttled, not failed: retry shortly, or lower "max parallel".';
112
+ }
113
+ return null;
114
+ },
115
+ readBudget(cfg) {
116
+ return readClaudeBudget(cfg);
117
+ },
118
+ };
119
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1,2 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ export declare const codexAdapter: AgentAdapter;
@@ -0,0 +1,117 @@
1
+ function codexSummary(stdout) {
2
+ const lines = stdout.trim().split('\n');
3
+ for (let i = lines.length - 1; i >= 0; i--) {
4
+ try {
5
+ const ev = JSON.parse(lines[i]);
6
+ if (ev.item && (ev.item.type === 'agent_message' || ev.item.type === 'assistant_message')) {
7
+ const t = (ev.item.text ?? ev.item.content ?? '').trim();
8
+ if (t)
9
+ return t;
10
+ }
11
+ if (ev.type === 'turn.failed' && typeof ev.message === 'string' && ev.message.trim()) {
12
+ return ev.message.trim();
13
+ }
14
+ if (ev.type === 'error' && typeof ev.message === 'string' && ev.message.trim()) {
15
+ return ev.message.trim();
16
+ }
17
+ }
18
+ catch {
19
+ }
20
+ }
21
+ return '';
22
+ }
23
+ export const codexAdapter = {
24
+ id: 'codex_cli',
25
+ displayName: 'OpenAI Codex',
26
+ branchPrefix: 'codex',
27
+ bin(cfg) {
28
+ return cfg.codexBin ?? 'codex';
29
+ },
30
+ buildInvocation(ctx) {
31
+ const { cfg } = ctx;
32
+ const args = ['exec', ...(ctx.isWin ? ['-'] : [ctx.prompt]), '--json'];
33
+ args.push('--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true');
34
+ if (ctx.model)
35
+ args.push('--model', ctx.model);
36
+ const rawEffort = ctx.ultracode ? 'xhigh' : ctx.effort;
37
+ const effort = rawEffort === 'max' || rawEffort === 'ultracode' ? 'xhigh' : rawEffort;
38
+ if (effort && ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(effort)) {
39
+ args.push('-c', `model_reasoning_effort="${effort}"`);
40
+ }
41
+ args.push(...(cfg.codexArgs ?? []));
42
+ const env = cfg.codexUseApiKey
43
+ ? ctx.runEnv
44
+ : { ...ctx.runEnv, OPENAI_API_KEY: undefined, CODEX_API_KEY: undefined };
45
+ const authLogLine = cfg.codexUseApiKey
46
+ ? 'OPENAI_API_KEY / CODEX_API_KEY (API billing — opted in)'
47
+ : 'your machine `codex login` (API keys hidden — ChatGPT-plan billing)';
48
+ return {
49
+ bin: this.bin(cfg),
50
+ args,
51
+ env,
52
+ stdinInput: ctx.isWin ? ctx.prompt : undefined,
53
+ shell: ctx.isWin,
54
+ authLogLine,
55
+ };
56
+ },
57
+ makeStreamPrinter() {
58
+ const short = (s, n = 160) => {
59
+ const one = s.replace(/\s+/g, ' ').trim();
60
+ return one.length > n ? `${one.slice(0, n)}…` : one;
61
+ };
62
+ let buf = '';
63
+ return (chunk) => {
64
+ buf += chunk;
65
+ const lines = buf.split('\n');
66
+ buf = lines.pop() ?? '';
67
+ for (const line of lines) {
68
+ if (!line.trim())
69
+ continue;
70
+ try {
71
+ const ev = JSON.parse(line);
72
+ if (ev.item?.type === 'agent_message' && ev.item.text) {
73
+ process.stdout.write(` │ ${short(ev.item.text)}\n`);
74
+ }
75
+ else if (ev.item?.type === 'command_execution' && ev.item.command) {
76
+ process.stdout.write(` │ ⚒ bash ${short(ev.item.command, 90)}\n`);
77
+ }
78
+ else if (ev.type === 'turn.failed' || ev.type === 'error') {
79
+ process.stdout.write(` └ ✗ ${short(ev.message ?? 'turn failed', 240)}\n`);
80
+ }
81
+ else if (ev.type === 'turn.completed') {
82
+ process.stdout.write(` └ turn completed\n`);
83
+ }
84
+ }
85
+ catch {
86
+ }
87
+ }
88
+ };
89
+ },
90
+ summarize(stdout) {
91
+ return codexSummary(stdout);
92
+ },
93
+ authFailureHint(text) {
94
+ const t = text.toLowerCase();
95
+ if (t.includes('not logged in') ||
96
+ t.includes('login required') ||
97
+ t.includes('please run codex login') ||
98
+ t.includes('`codex login`') ||
99
+ /\bunauthorized\b/.test(t) ||
100
+ t.includes('invalid api key') ||
101
+ /\b401\b/.test(t)) {
102
+ return 'OpenAI Codex could not authenticate. On the runner machine run `codex login` (signs into your ChatGPT plan) — or set an API key and enable `codexUseApiKey` — then retry.';
103
+ }
104
+ if (t.includes('insufficient_quota') || t.includes('billing hard limit')) {
105
+ return 'OpenAI Codex hit a billing/quota wall on this account. Check your ChatGPT plan’s agentic usage (or API billing if opted in), then retry.';
106
+ }
107
+ return null;
108
+ },
109
+ rateLimitHint(text) {
110
+ const t = text.toLowerCase();
111
+ if (/rate limit|rate_limit|too many requests|\b429\b|usage limit|usage_limit/.test(t)) {
112
+ return 'OpenAI Codex hit a rate / usage limit — ChatGPT-plan agentic usage is a shared 5-hour window. This run was throttled, not failed: retry shortly, or lower "max parallel".';
113
+ }
114
+ return null;
115
+ },
116
+ };
117
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1,2 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ export declare const cursorAdapter: AgentAdapter;