deepcodex 0.1.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.
- package/.codex-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/bin/opencodex.js +49 -0
- package/config/desktop.json +9 -0
- package/config/pilot.json +16 -0
- package/config/worker.json +78 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +47 -0
- package/prompts/worker.md +20 -0
- package/scripts/credentials.js +102 -0
- package/scripts/desktop.js +199 -0
- package/scripts/pilot-router.js +241 -0
- package/scripts/pilot.js +242 -0
- package/scripts/toml.js +6 -0
- package/scripts/worker.js +637 -0
- package/skills/delegate-flash/SKILL.md +63 -0
- package/vendor/codex-router/LICENSE +21 -0
- package/vendor/codex-router/deepseek-responses.js +55 -0
- package/vendor/codex-router/json-number-rewrite.js +58 -0
- package/vendor/codex-router/namespace-relay.js +4294 -0
- package/vendor/codex-router/sse-prefix.js +115 -0
- package/vendor/codex-router/subagent-completion.js +261 -0
- package/vendor/codex-router/tool-arguments.js +111 -0
- package/vendor/codex-router/tool-schema-root.js +1008 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run a bounded Codex worker against DeepSeek without changing user config.
|
|
3
|
+
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { parseToml } from './toml.js';
|
|
10
|
+
|
|
11
|
+
export const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
12
|
+
|
|
13
|
+
const SQLITE_BUSY = 5;
|
|
14
|
+
const ALLOWED_ENVIRONMENT = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'LANG', 'CODEX_HOME']);
|
|
15
|
+
const REQUIRED_CLI_FLAGS = ['--ignore-user-config', '--ephemeral', '--json', '--strict-config'];
|
|
16
|
+
const USAGE = 'Usage: worker.js <doctor|run> [options]';
|
|
17
|
+
|
|
18
|
+
export const HELP = `${USAGE}
|
|
19
|
+
|
|
20
|
+
Run a bounded Codex worker against DeepSeek without changing user config.
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
doctor Check local prerequisites without inference
|
|
24
|
+
run Execute one ticket; consumes DeepSeek API usage
|
|
25
|
+
|
|
26
|
+
Run options:
|
|
27
|
+
--cwd PATH Workspace directory for the ticket (required)
|
|
28
|
+
--task-file PATH File holding the ticket text (required)
|
|
29
|
+
--write Allow workspace writes for an authorized editing task
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
-h, --help Show this help message
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export function loadConfig() {
|
|
36
|
+
return JSON.parse(fs.readFileSync(path.join(ROOT, 'config/worker.json'), 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function configArgs(values, prefix = '') {
|
|
40
|
+
const args = [];
|
|
41
|
+
for (const [key, value] of Object.entries(values)) {
|
|
42
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
43
|
+
if (isTable(value)) args.push(...configArgs(value, name));
|
|
44
|
+
else args.push('-c', `${name}=${JSON.stringify(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function workerEnvironment(source) {
|
|
50
|
+
const env = {};
|
|
51
|
+
for (const [key, value] of Object.entries(source)) {
|
|
52
|
+
if (value === undefined) continue;
|
|
53
|
+
if (ALLOWED_ENVIRONMENT.has(key) || key.startsWith('LC_')) env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
if (source.DEEPSEEK_API_KEY) env.DEEPSEEK_API_KEY = source.DEEPSEEK_API_KEY;
|
|
56
|
+
return env;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readEnvKey(file, name) {
|
|
60
|
+
let value = null;
|
|
61
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r\n|\r|\n/)) {
|
|
62
|
+
let text = line.trim();
|
|
63
|
+
if (text.startsWith('export ')) text = text.slice(7).replace(/^\s+/, '');
|
|
64
|
+
const separator = text.indexOf('=');
|
|
65
|
+
if (separator === -1 || text.slice(0, separator).trim() !== name) continue;
|
|
66
|
+
if (value !== null) throw new Error(`Duplicate ${name} in credentials file`);
|
|
67
|
+
let parts;
|
|
68
|
+
try {
|
|
69
|
+
parts = shlexSplit(text.slice(separator + 1));
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error(`Invalid ${name} quoting in credentials file`);
|
|
72
|
+
}
|
|
73
|
+
if (parts.length !== 1 || !parts[0]) throw new Error(`Invalid or empty ${name} in credentials file`);
|
|
74
|
+
value = parts[0];
|
|
75
|
+
}
|
|
76
|
+
if (value === null) throw new Error(`Missing ${name} in credentials file`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadCredentials(env, config) {
|
|
81
|
+
const provider = config.codex.model_provider;
|
|
82
|
+
const name = config.codex.model_providers[provider].env_key;
|
|
83
|
+
if (!env[name]) {
|
|
84
|
+
const file = expandUser(config.credentials.env_file);
|
|
85
|
+
try {
|
|
86
|
+
env[name] = readEnvKey(file, name);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code !== 'ENOENT') throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function redact(value, secret) {
|
|
94
|
+
if (typeof value === 'string') return secret ? value.split(secret).join('[REDACTED]') : value;
|
|
95
|
+
if (Array.isArray(value)) return value.map(item => redact(item, secret));
|
|
96
|
+
if (isTable(value)) {
|
|
97
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function doctor(config, env) {
|
|
103
|
+
const binary = which('codex', env.PATH);
|
|
104
|
+
const report = { status: 'ready', codex: binary ?? null, api_key_present: Boolean(env.DEEPSEEK_API_KEY) };
|
|
105
|
+
if (binary) {
|
|
106
|
+
const timeout = config.limits.version_timeout_seconds * 1000;
|
|
107
|
+
const version = probe([binary, '--version'], env, timeout);
|
|
108
|
+
const help = probe([binary, 'exec', '--help'], env, timeout);
|
|
109
|
+
report.version = version.stdout.trim();
|
|
110
|
+
report.compatible_cli = version.status === 0 && help.status === 0
|
|
111
|
+
&& REQUIRED_CLI_FLAGS.every(flag => help.stdout.includes(flag));
|
|
112
|
+
}
|
|
113
|
+
if (!binary || !report.compatible_cli || !report.api_key_present) report.status = 'not_ready';
|
|
114
|
+
return report;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function checkProjectConfig(cwd) {
|
|
118
|
+
const userConfig = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'), 'config.toml');
|
|
119
|
+
for (const directory of ancestors(cwd)) {
|
|
120
|
+
const candidate = path.join(directory, '.codex', 'config.toml');
|
|
121
|
+
if (candidate === userConfig || !isFile(candidate)) continue;
|
|
122
|
+
const data = parseToml(fs.readFileSync(candidate, 'utf8'));
|
|
123
|
+
if (pythonTruthy(data?.mcp_servers)) {
|
|
124
|
+
throw new Error(`Project MCP servers are not supported by this worker: ${candidate}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Exclusive lock released by the kernel when the process dies, so a crashed worker cannot block the
|
|
130
|
+
// next one. The lock file lives in the per-user temporary directory, is created without following
|
|
131
|
+
// symlinks, is owned by this user and stays 0600. node:sqlite is loaded here so help and doctor stay
|
|
132
|
+
// free of its experimental warning.
|
|
133
|
+
export async function workerLock() {
|
|
134
|
+
const file = path.join(os.tmpdir(), `opencodex-worker-${process.getuid()}.lock`);
|
|
135
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NOFOLLOW, 0o600);
|
|
136
|
+
try {
|
|
137
|
+
const stat = fs.fstatSync(fd);
|
|
138
|
+
if (stat.uid !== process.getuid()) throw new Error('Worker lock belongs to another user');
|
|
139
|
+
if (!stat.isFile()) throw new Error('Worker lock is not a regular file');
|
|
140
|
+
if ((stat.mode & 0o777) !== 0o600) fs.fchmodSync(fd, 0o600);
|
|
141
|
+
} finally {
|
|
142
|
+
fs.closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
145
|
+
const database = new DatabaseSync(file, { timeout: 0 });
|
|
146
|
+
try {
|
|
147
|
+
database.exec('BEGIN EXCLUSIVE');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
database.close();
|
|
150
|
+
if (error.errcode === SQLITE_BUSY) throw new Error('Another OpenCodex worker is running; wait for it or cancel that run');
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
let closed = false;
|
|
154
|
+
return {
|
|
155
|
+
close() {
|
|
156
|
+
if (closed) return;
|
|
157
|
+
closed = true;
|
|
158
|
+
try {
|
|
159
|
+
if (database.isTransaction) database.exec('ROLLBACK');
|
|
160
|
+
} catch {
|
|
161
|
+
// Closing the handle releases the lock even when the transaction cannot be rolled back.
|
|
162
|
+
}
|
|
163
|
+
database.close();
|
|
164
|
+
},
|
|
165
|
+
[Symbol.dispose]() {
|
|
166
|
+
this.close();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildCommand(binary, config, cwd, write, runDir) {
|
|
172
|
+
const metadata = { ...config.model_metadata };
|
|
173
|
+
metadata.slug = config.codex.model;
|
|
174
|
+
metadata.base_instructions = fs.readFileSync(path.join(ROOT, 'prompts/worker.md'), 'utf8');
|
|
175
|
+
const catalog = path.join(runDir, 'models.json');
|
|
176
|
+
fs.writeFileSync(catalog, JSON.stringify({ models: [metadata] }));
|
|
177
|
+
const finalPath = path.join(runDir, 'final.txt');
|
|
178
|
+
const args = [binary, 'exec', '--ignore-user-config', '--ephemeral', '--json', '--strict-config',
|
|
179
|
+
'--skip-git-repo-check', '--color', 'never', '--cd', cwd,
|
|
180
|
+
'--sandbox', write ? 'workspace-write' : 'read-only',
|
|
181
|
+
'--output-last-message', finalPath];
|
|
182
|
+
args.push(...configArgs(config.codex));
|
|
183
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(catalog)}`, '-');
|
|
184
|
+
return { args, finalPath };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function parseResult(stdout, stderr, finalText, returncode, stopReason, config) {
|
|
188
|
+
let completed = false;
|
|
189
|
+
const errors = [];
|
|
190
|
+
let usage = null;
|
|
191
|
+
let threadId = null;
|
|
192
|
+
let malformed = false;
|
|
193
|
+
for (const line of stdout.split(/\r\n|\r|\n/)) {
|
|
194
|
+
if (!line.trim()) continue;
|
|
195
|
+
let event;
|
|
196
|
+
try {
|
|
197
|
+
event = JSON.parse(line);
|
|
198
|
+
if (!isTable(event)) throw new Error('event must be an object');
|
|
199
|
+
} catch {
|
|
200
|
+
malformed = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (event.type === 'thread.started') {
|
|
204
|
+
threadId = event.thread_id ?? null;
|
|
205
|
+
} else if (event.type === 'turn.completed') {
|
|
206
|
+
completed = true;
|
|
207
|
+
usage = event.usage ?? null;
|
|
208
|
+
} else if (event.type === 'turn.failed' || event.type === 'error') {
|
|
209
|
+
errors.push(pick(event, 'error', pick(event, 'message', 'Worker error')));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const status = stopReason
|
|
213
|
+
|| (returncode === 0 && completed && finalText.trim() && errors.length === 0 && !malformed ? 'completed' : 'failed');
|
|
214
|
+
const result = {
|
|
215
|
+
status,
|
|
216
|
+
exit_code: returncode,
|
|
217
|
+
thread_id: threadId,
|
|
218
|
+
configured_model: config.codex.model,
|
|
219
|
+
configured_provider: config.codex.model_provider,
|
|
220
|
+
usage,
|
|
221
|
+
};
|
|
222
|
+
if (finalText) {
|
|
223
|
+
const cap = config.limits.max_result_chars;
|
|
224
|
+
result.result = head(finalText, cap);
|
|
225
|
+
result.result_truncated = finalText.length > cap;
|
|
226
|
+
}
|
|
227
|
+
if (malformed) errors.push('Invalid JSONL from Codex');
|
|
228
|
+
if (status !== 'completed') {
|
|
229
|
+
if (errors.length === 0) errors.push('Worker did not produce a complete turn and a non-empty final response');
|
|
230
|
+
result.errors = JSON.stringify(errors).slice(0, config.limits.max_error_chars);
|
|
231
|
+
result.diagnostic = tail(stderr, config.limits.max_error_chars);
|
|
232
|
+
result.partial_changes_possible = true;
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Codex runs in its own process group, so signalling the group also reaches descendants it left behind.
|
|
238
|
+
export async function killGroup(child) {
|
|
239
|
+
if (!child || child.pid === undefined) return;
|
|
240
|
+
try {
|
|
241
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error.code !== 'ESRCH') throw error;
|
|
244
|
+
}
|
|
245
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
246
|
+
await new Promise(resolve => child.once('close', resolve));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function runWorker(binary, config, cwd, task, write, env) {
|
|
251
|
+
checkProjectConfig(cwd);
|
|
252
|
+
const started = monotonic();
|
|
253
|
+
const { limits } = config;
|
|
254
|
+
const lock = await workerLock();
|
|
255
|
+
let runDir = null;
|
|
256
|
+
try {
|
|
257
|
+
runDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencodex-run-'));
|
|
258
|
+
const { args, finalPath } = buildCommand(binary, config, cwd, write, runDir);
|
|
259
|
+
const taskPath = path.join(runDir, 'task.txt');
|
|
260
|
+
const outPath = path.join(runDir, 'events.jsonl');
|
|
261
|
+
const errPath = path.join(runDir, 'stderr.txt');
|
|
262
|
+
const artifacts = [outPath, errPath, finalPath];
|
|
263
|
+
fs.writeFileSync(taskPath, task);
|
|
264
|
+
let stopReason = null;
|
|
265
|
+
let child = null;
|
|
266
|
+
const stdin = fs.openSync(taskPath, 'r');
|
|
267
|
+
const stdout = fs.openSync(outPath, 'w');
|
|
268
|
+
const stderr = fs.openSync(errPath, 'w');
|
|
269
|
+
try {
|
|
270
|
+
// spawn() supplies argv[0] itself, so args[0] (the binary) must not be repeated in the list.
|
|
271
|
+
child = spawn(binary, args.slice(1), { cwd, env, detached: true, stdio: [stdin, stdout, stderr] });
|
|
272
|
+
await waitForSpawn(child);
|
|
273
|
+
process.stderr.write(`${JSON.stringify({ event: 'worker.started', pid: child.pid, cwd, model: config.codex.model })}\n`);
|
|
274
|
+
while (child.exitCode === null && child.signalCode === null) {
|
|
275
|
+
if (cancellationRequested) {
|
|
276
|
+
stopReason = 'cancelled';
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (monotonic() - started >= limits.timeout_seconds) {
|
|
280
|
+
stopReason = 'timeout';
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (artifactBytes(artifacts) > limits.max_output_bytes) {
|
|
284
|
+
stopReason = 'output_limit';
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
await pause(limits.poll_interval_seconds * 1000);
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
// Also terminate descendants left behind after Codex exits.
|
|
291
|
+
await killGroup(child);
|
|
292
|
+
fs.closeSync(stdin);
|
|
293
|
+
fs.closeSync(stdout);
|
|
294
|
+
fs.closeSync(stderr);
|
|
295
|
+
}
|
|
296
|
+
if (artifactBytes(artifacts) > limits.max_output_bytes) stopReason ??= 'output_limit';
|
|
297
|
+
const returncode = child ? exitCodeOf(child) : null;
|
|
298
|
+
const result = stopReason === 'output_limit'
|
|
299
|
+
? parseResult('', 'Worker output exceeded the configured limit', '', returncode, stopReason, config)
|
|
300
|
+
: parseResult(readText(outPath), readText(errPath), readTextIfExists(finalPath), returncode, stopReason, config);
|
|
301
|
+
result.duration_seconds = Math.round((monotonic() - started) * 100) / 100;
|
|
302
|
+
result.worker_pid = child?.pid ?? null;
|
|
303
|
+
result.cwd = cwd;
|
|
304
|
+
result.sandbox = write ? 'workspace-write' : 'read-only';
|
|
305
|
+
return result;
|
|
306
|
+
} finally {
|
|
307
|
+
if (runDir) fs.rmSync(runDir, { recursive: true, force: true });
|
|
308
|
+
lock.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
313
|
+
const parsed = parseArgs(argv);
|
|
314
|
+
if (parsed.help) {
|
|
315
|
+
process.stdout.write(parsed.text);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
if (parsed.error) {
|
|
319
|
+
process.stderr.write(`${USAGE}\n${parsed.error}\n`);
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
const env = workerEnvironment(process.env);
|
|
323
|
+
const config = loadConfig();
|
|
324
|
+
resetCancellation();
|
|
325
|
+
process.on('SIGTERM', requestCancellation);
|
|
326
|
+
process.on('SIGINT', requestCancellation);
|
|
327
|
+
let report;
|
|
328
|
+
try {
|
|
329
|
+
loadCredentials(env, config);
|
|
330
|
+
report = doctor(config, env);
|
|
331
|
+
if (parsed.command === 'run' && report.status === 'ready') {
|
|
332
|
+
const cwd = fs.realpathSync(expandUser(parsed.cwd));
|
|
333
|
+
if (!isDirectory(cwd)) throw new Error('cwd must be a directory');
|
|
334
|
+
const task = readTicket(parsed.taskFile, config.limits.max_task_bytes);
|
|
335
|
+
if (!task.trim()) throw new Error('Ticket must not be empty');
|
|
336
|
+
report = await runWorker(report.codex, config, cwd, task, parsed.write, env);
|
|
337
|
+
}
|
|
338
|
+
if (parsed.command === 'run' && report.status === 'not_ready') {
|
|
339
|
+
report.error = 'Run doctor and resolve missing prerequisites before delegation';
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
report = { status: 'failed', error: errorMessage(error) };
|
|
343
|
+
} finally {
|
|
344
|
+
process.off('SIGTERM', requestCancellation);
|
|
345
|
+
process.off('SIGINT', requestCancellation);
|
|
346
|
+
}
|
|
347
|
+
process.stdout.write(`${JSON.stringify(redact(report, env.DEEPSEEK_API_KEY))}\n`);
|
|
348
|
+
return report.status === 'ready' || report.status === 'completed' ? 0 : 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let cancellationRequested = false;
|
|
352
|
+
let wakeCancellable = null;
|
|
353
|
+
|
|
354
|
+
// main() registers these while it runs, so SIGINT/SIGTERM also cancel a worker started through the
|
|
355
|
+
// CLI, which imports main() instead of executing this file as the entrypoint.
|
|
356
|
+
export function requestCancellation() {
|
|
357
|
+
cancellationRequested = true;
|
|
358
|
+
const wake = wakeCancellable;
|
|
359
|
+
wakeCancellable = null;
|
|
360
|
+
if (wake) wake();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function resetCancellation() {
|
|
364
|
+
cancellationRequested = false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function parseArgs(argv) {
|
|
368
|
+
const command = argv[0];
|
|
369
|
+
const rest = argv.slice(1);
|
|
370
|
+
if (command === undefined || command === '--help' || command === '-h') return { help: true, text: HELP };
|
|
371
|
+
if (command !== 'doctor' && command !== 'run') return { error: `Unknown command: ${command}` };
|
|
372
|
+
const options = { command, write: false };
|
|
373
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
374
|
+
const arg = rest[index];
|
|
375
|
+
if (arg === '--help' || arg === '-h') return { help: true, text: HELP };
|
|
376
|
+
if (command !== 'run') return { error: `Unexpected argument for doctor: ${arg}` };
|
|
377
|
+
if (arg === '--write') {
|
|
378
|
+
options.write = true;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const separator = arg.indexOf('=');
|
|
382
|
+
const flag = separator === -1 ? arg : arg.slice(0, separator);
|
|
383
|
+
if (flag !== '--cwd' && flag !== '--task-file') return { error: `Unknown option: ${arg}` };
|
|
384
|
+
let value;
|
|
385
|
+
if (separator === -1) {
|
|
386
|
+
index += 1;
|
|
387
|
+
value = rest[index];
|
|
388
|
+
} else {
|
|
389
|
+
value = arg.slice(separator + 1);
|
|
390
|
+
}
|
|
391
|
+
if (value === undefined) return { error: `Option ${flag} requires a value` };
|
|
392
|
+
if (flag === '--cwd') options.cwd = value;
|
|
393
|
+
else options.taskFile = value;
|
|
394
|
+
}
|
|
395
|
+
if (command === 'run' && options.cwd === undefined) return { error: 'Option --cwd is required' };
|
|
396
|
+
if (command === 'run' && options.taskFile === undefined) return { error: 'Option --task-file is required' };
|
|
397
|
+
return options;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readTicket(file, maxTaskBytes) {
|
|
401
|
+
const fd = fs.openSync(file, 'r');
|
|
402
|
+
try {
|
|
403
|
+
const buffer = Buffer.alloc(maxTaskBytes + 1);
|
|
404
|
+
const read = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
405
|
+
if (read > maxTaskBytes) throw new Error('Ticket exceeds max_task_bytes');
|
|
406
|
+
// Strict UTF-8 like Python's bytes.decode, keeping a BOM instead of stripping it.
|
|
407
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, read));
|
|
408
|
+
} finally {
|
|
409
|
+
fs.closeSync(fd);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function probe(args, env, timeout) {
|
|
414
|
+
const result = spawnSync(args[0], args.slice(1), { env, timeout, encoding: 'utf8' });
|
|
415
|
+
if (result.error) throw result.error;
|
|
416
|
+
return { status: result.status, stdout: result.stdout ?? '' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function waitForSpawn(child) {
|
|
420
|
+
return new Promise((resolve, reject) => {
|
|
421
|
+
child.once('spawn', resolve);
|
|
422
|
+
child.once('error', reject);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function pause(milliseconds) {
|
|
427
|
+
return new Promise(resolve => {
|
|
428
|
+
const timer = setTimeout(() => {
|
|
429
|
+
wakeCancellable = null;
|
|
430
|
+
resolve();
|
|
431
|
+
}, Math.max(milliseconds, 0));
|
|
432
|
+
wakeCancellable = () => {
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
resolve();
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function exitCodeOf(child) {
|
|
440
|
+
if (typeof child.exitCode === 'number') return child.exitCode;
|
|
441
|
+
const signal = child.signalCode ? os.constants.signals[child.signalCode] : undefined;
|
|
442
|
+
return signal === undefined ? null : -signal;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function artifactBytes(files) {
|
|
446
|
+
let total = 0;
|
|
447
|
+
for (const file of files) {
|
|
448
|
+
try {
|
|
449
|
+
total += fs.statSync(file).size;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error.code !== 'ENOENT') throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return total;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function which(command, searchPath) {
|
|
458
|
+
const directories = String(searchPath ?? process.env.PATH ?? '').split(path.delimiter);
|
|
459
|
+
for (const directory of directories) {
|
|
460
|
+
const candidate = path.join(directory || '.', command);
|
|
461
|
+
try {
|
|
462
|
+
const stat = fs.statSync(candidate);
|
|
463
|
+
if (stat.isFile()) {
|
|
464
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
465
|
+
return candidate;
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
// Not a usable candidate; keep searching.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function ancestors(cwd) {
|
|
475
|
+
const directories = [];
|
|
476
|
+
let directory = cwd;
|
|
477
|
+
while (directory !== path.dirname(directory)) {
|
|
478
|
+
directories.push(directory);
|
|
479
|
+
directory = path.dirname(directory);
|
|
480
|
+
}
|
|
481
|
+
directories.push(directory);
|
|
482
|
+
return directories;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function expandUser(file) {
|
|
486
|
+
if (file === '~') return process.env.HOME ?? os.homedir();
|
|
487
|
+
if (file.startsWith('~/')) return path.join(process.env.HOME ?? os.homedir(), file.slice(2));
|
|
488
|
+
return file;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isTable(value) {
|
|
492
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isFile(target) {
|
|
496
|
+
try {
|
|
497
|
+
return fs.statSync(target).isFile();
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isDirectory(target) {
|
|
504
|
+
try {
|
|
505
|
+
return fs.statSync(target).isDirectory();
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function readText(file) {
|
|
512
|
+
return fs.readFileSync(file, 'utf8');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function readTextIfExists(file) {
|
|
516
|
+
return fs.existsSync(file) ? readText(file) : '';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function monotonic() {
|
|
520
|
+
return Number(process.hrtime.bigint()) / 1e9;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function pick(object, key, fallback) {
|
|
524
|
+
return Object.hasOwn(object, key) ? object[key] : fallback;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function head(text, cap) {
|
|
528
|
+
return cap > 0 ? text.slice(0, cap) : '';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function tail(text, cap) {
|
|
532
|
+
return cap > 0 ? text.slice(-cap) : '';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function pythonTruthy(value) {
|
|
536
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
537
|
+
if (isTable(value)) return Object.keys(value).length > 0;
|
|
538
|
+
return Boolean(value);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function errorMessage(error) {
|
|
542
|
+
return error && typeof error.message === 'string' ? error.message : String(error);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// POSIX shlex.split(text, comments=True): quotes and escapes only, never shell expansion.
|
|
546
|
+
function shlexSplit(text) {
|
|
547
|
+
const tokens = [];
|
|
548
|
+
let current = '';
|
|
549
|
+
let word = false;
|
|
550
|
+
let index = 0;
|
|
551
|
+
while (index < text.length) {
|
|
552
|
+
const char = text[index];
|
|
553
|
+
if (char === ' ' || char === '\t' || char === '\n' || char === '\r' || char === '\f' || char === '\v') {
|
|
554
|
+
if (word) {
|
|
555
|
+
tokens.push(current);
|
|
556
|
+
current = '';
|
|
557
|
+
word = false;
|
|
558
|
+
}
|
|
559
|
+
index += 1;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// shlex also comments out '#' inside an unquoted word, keeping the word read so far.
|
|
563
|
+
if (char === '#') {
|
|
564
|
+
if (word) {
|
|
565
|
+
tokens.push(current);
|
|
566
|
+
current = '';
|
|
567
|
+
word = false;
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (char === '\\') {
|
|
572
|
+
if (index + 1 >= text.length) throw new Error('No escaped character');
|
|
573
|
+
current += text[index + 1];
|
|
574
|
+
word = true;
|
|
575
|
+
index += 2;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === "'") {
|
|
579
|
+
const end = text.indexOf("'", index + 1);
|
|
580
|
+
if (end === -1) throw new Error('No closing quotation');
|
|
581
|
+
current += text.slice(index + 1, end);
|
|
582
|
+
word = true;
|
|
583
|
+
index = end + 1;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (char === '"') {
|
|
587
|
+
let cursor = index + 1;
|
|
588
|
+
let closed = false;
|
|
589
|
+
while (cursor < text.length) {
|
|
590
|
+
const quoted = text[cursor];
|
|
591
|
+
if (quoted === '"') {
|
|
592
|
+
closed = true;
|
|
593
|
+
cursor += 1;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
if (quoted === '\\') {
|
|
597
|
+
const next = text[cursor + 1];
|
|
598
|
+
if (next === undefined) throw new Error('No escaped character');
|
|
599
|
+
// shlex escapes only the quote and the backslash here; every other backslash stays literal.
|
|
600
|
+
current += next === '"' || next === '\\' ? next : `\\${next}`;
|
|
601
|
+
cursor += 2;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
current += quoted;
|
|
605
|
+
cursor += 1;
|
|
606
|
+
}
|
|
607
|
+
if (!closed) throw new Error('No closing quotation');
|
|
608
|
+
word = true;
|
|
609
|
+
index = cursor;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
current += char;
|
|
613
|
+
word = true;
|
|
614
|
+
index += 1;
|
|
615
|
+
}
|
|
616
|
+
if (word) tokens.push(current);
|
|
617
|
+
return tokens;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isEntrypoint() {
|
|
621
|
+
const invoked = process.argv[1];
|
|
622
|
+
if (!invoked) return false;
|
|
623
|
+
try {
|
|
624
|
+
return pathToFileURL(fs.realpathSync(invoked)).href === import.meta.url;
|
|
625
|
+
} catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (isEntrypoint()) {
|
|
631
|
+
main().then(code => {
|
|
632
|
+
process.exitCode = code;
|
|
633
|
+
}, error => {
|
|
634
|
+
process.stderr.write(`${error?.stack ?? error}\n`);
|
|
635
|
+
process.exitCode = 1;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegate-flash
|
|
3
|
+
description: Delegate bounded coding tasks to native DeepSeek Flash subagents in Codex through OpenCodex. Use when OpenCodex or Flash delegation is requested, or when diagnosing the Desktop integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OpenCodex
|
|
7
|
+
|
|
8
|
+
Keep the coordinator on the user's selected native model. DeepSeek Flash runs as
|
|
9
|
+
an actual Codex subagent through the local OpenCodex router. Use the native
|
|
10
|
+
collaboration tools for spawning, follow-ups, waiting, and interruption; the
|
|
11
|
+
normal delegation path does not launch a separate `codex exec` worker.
|
|
12
|
+
|
|
13
|
+
## Delegate
|
|
14
|
+
|
|
15
|
+
Choose an independently verifiable task and give the child the objective,
|
|
16
|
+
necessary context, allowed files, exclusions, acceptance checks, and the user's
|
|
17
|
+
communication language. Preserve the language of existing code. Review the
|
|
18
|
+
actual changes and verification before declaring the user's task complete.
|
|
19
|
+
|
|
20
|
+
When the current tool schema offers `deepseek-flash`, call `spawn_agent` with
|
|
21
|
+
`model="deepseek-flash"`, `reasoning_effort="high"`, and `fork_turns="none"`.
|
|
22
|
+
A fresh child receives the bounded ticket without copying the full conversation.
|
|
23
|
+
Keep its returned identity and use `followup_task` for related work in the same
|
|
24
|
+
agent. Use `wait_agent` and `interrupt_agent` normally. Never invent unavailable
|
|
25
|
+
model overrides or disguise DeepSeek under an OpenAI model name.
|
|
26
|
+
|
|
27
|
+
If the schema does not offer Flash, check installation status and ask the user to
|
|
28
|
+
fully quit/reopen Desktop and start a new task after a configuration update.
|
|
29
|
+
Do not silently switch to a separate process or another provider. Existing tasks
|
|
30
|
+
can retain the previous provider and model catalog.
|
|
31
|
+
|
|
32
|
+
Subagents inherit Codex's permissions. A ticket's allowed files are instructions,
|
|
33
|
+
not a filesystem sandbox. Use isolation when the task needs it. Do not claim the
|
|
34
|
+
worker cannot read secrets accessible to the same user.
|
|
35
|
+
|
|
36
|
+
## Local integration
|
|
37
|
+
|
|
38
|
+
Resolve plugin paths relative to this installed skill (the root is two levels
|
|
39
|
+
above). The source launcher is `scripts/desktop.js`; the installed LaunchAgent
|
|
40
|
+
runs the stable copy under `~/.local/share/opencodex/runtime`. Operational policy
|
|
41
|
+
is in `config/desktop.json`, shared transport defaults in `config/pilot.json`,
|
|
42
|
+
and the DeepSeek model and credential path in `config/worker.json`.
|
|
43
|
+
|
|
44
|
+
Run `node <plugin>/scripts/desktop.js status` to check the local service without
|
|
45
|
+
inference. The service is `com.opencodex.router`, bound only to loopback. Private
|
|
46
|
+
state and bounded metadata receipts live in `~/.config/opencodex/desktop`.
|
|
47
|
+
Receipts contain routing and tool names, not prompts, tool arguments or tokens.
|
|
48
|
+
Do not print `state.json`, provider headers, credential files or full user config.
|
|
49
|
+
|
|
50
|
+
DeepSeek credentials stay in `~/.config/opencodex/.env` outside the plugin and
|
|
51
|
+
are read by the service. Never ask for a key in chat or package `.env` files.
|
|
52
|
+
The router forwards native Codex authentication only to OpenAI and the DeepSeek
|
|
53
|
+
key only to DeepSeek. Native encrypted task handoffs use an additional OpenAI
|
|
54
|
+
relay call; delegation consumes both Codex quota and DeepSeek API usage.
|
|
55
|
+
|
|
56
|
+
`node <plugin>/scripts/desktop.js install` writes the service and user-level
|
|
57
|
+
provider/catalog settings. Run it only when the user authorizes activation or
|
|
58
|
+
repair. It preserves unrelated settings and keeps a private pre-install config
|
|
59
|
+
backup. Do not restore that full backup over later user changes without review.
|
|
60
|
+
|
|
61
|
+
The opt-in `scripts/pilot.js` runs an isolated live test and spends API usage.
|
|
62
|
+
A healthy service or visible model is not proof that a live delegated task ran.
|
|
63
|
+
Report separately configuration, CLI/API execution, and Desktop UI evidence.
|