deepcodex 0.1.0 → 0.1.2
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 +9 -7
- package/README.md +46 -27
- package/assets/deepcodex.png +0 -0
- package/bin/{opencodex.js → deepcodex.js} +12 -11
- package/config/desktop.json +1 -1
- package/config/worker.json +1 -1
- package/package.json +3 -2
- package/prompts/worker.md +1 -1
- package/scripts/credentials.js +3 -3
- package/scripts/desktop.js +137 -13
- package/scripts/pilot-router.js +1 -1
- package/scripts/pilot.js +3 -3
- package/scripts/worker.js +25 -3
- package/skills/delegate-flash/SKILL.md +4 -4
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
3
|
-
"version": "0.1.
|
|
2
|
+
"name": "deepcodex",
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Delegates tasks to native DeepSeek Flash subagents inside Codex through a local router.",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
6
|
+
"name": "DeepCodex"
|
|
7
7
|
},
|
|
8
8
|
"skills": "./skills/",
|
|
9
9
|
"interface": {
|
|
10
|
-
"displayName": "
|
|
10
|
+
"displayName": "DeepCodex",
|
|
11
|
+
"composerIcon": "./assets/deepcodex.png",
|
|
12
|
+
"logo": "./assets/deepcodex.png",
|
|
11
13
|
"shortDescription": "Astra coordinates; DeepSeek Flash executes.",
|
|
12
14
|
"longDescription": "Keeps the coordinator model and connects DeepSeek Flash as a native subagent, with tracking, tools, and results inside Codex.",
|
|
13
|
-
"developerName": "
|
|
15
|
+
"developerName": "DeepCodex",
|
|
14
16
|
"category": "Productivity",
|
|
15
17
|
"capabilities": [
|
|
16
18
|
"Read",
|
|
17
19
|
"Write"
|
|
18
20
|
],
|
|
19
21
|
"defaultPrompt": [
|
|
20
|
-
"Check whether
|
|
21
|
-
"Delegate this implementation to Flash with
|
|
22
|
+
"Check whether DeepCodex is ready.",
|
|
23
|
+
"Delegate this implementation to Flash with DeepCodex."
|
|
22
24
|
]
|
|
23
25
|
}
|
|
24
26
|
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DeepCodex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepCodex keeps the coordinator on your selected native Codex model and runs
|
|
4
4
|
DeepSeek Flash as an actual Codex subagent through a local loopback router. The
|
|
5
5
|
subagent appears in the normal collaboration tools (`spawn_agent`,
|
|
6
6
|
`followup_task`, `wait_agent`, `interrupt_agent`) under the model name
|
|
@@ -12,9 +12,13 @@ native coordinator model is not replaced.
|
|
|
12
12
|
- macOS. The installer and the LaunchAgent service are macOS-only.
|
|
13
13
|
- Node.js >= 22.15. The worker uses `node:sqlite`, `fetch` and
|
|
14
14
|
`import.meta.resolve`.
|
|
15
|
-
- A Codex CLI on `PATH`
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
- A compatible Codex CLI, either on `PATH` or bundled with Codex Desktop.
|
|
16
|
+
When no CLI is on `PATH`, DeepCodex checks `Codex.app` and `ChatGPT.app`
|
|
17
|
+
under `~/Applications` and `/Applications`, in that order. No separate CLI
|
|
18
|
+
installation is needed when Desktop includes a compatible binary.
|
|
19
|
+
`codex exec` must support `--ignore-user-config`, `--ephemeral`, `--json`
|
|
20
|
+
and `--strict-config`; `deepcodex doctor` verifies this before activation.
|
|
21
|
+
A CLI on `PATH` takes priority, even if it is incompatible.
|
|
18
22
|
- A DeepSeek API key.
|
|
19
23
|
|
|
20
24
|
## Install globally
|
|
@@ -22,24 +26,34 @@ native coordinator model is not replaced.
|
|
|
22
26
|
Once the package is published to npm:
|
|
23
27
|
|
|
24
28
|
```sh
|
|
25
|
-
npm install --global
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
npm install --global deepcodex
|
|
30
|
+
deepcodex configure
|
|
31
|
+
deepcodex doctor
|
|
32
|
+
deepcodex install
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
The global installation adds `
|
|
35
|
+
The global installation adds `deepcodex` to npm's global bin directory, which
|
|
32
36
|
must be on your `PATH`. Run it from any directory; no checkout path is needed.
|
|
33
37
|
|
|
34
38
|
To install the local checkout globally before publication, run these commands
|
|
35
|
-
from the repository, then use the same `
|
|
39
|
+
from the repository, then use the same `deepcodex` commands above:
|
|
36
40
|
|
|
37
41
|
```sh
|
|
38
42
|
pnpm install --frozen-lockfile
|
|
39
43
|
npm install --global .
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
###
|
|
46
|
+
### Upgrading from OpenCodex
|
|
47
|
+
|
|
48
|
+
The npm package, command and plugin are now named `deepcodex`, and the service
|
|
49
|
+
is `com.deepcodex.router`. Existing private paths (`~/.config/opencodex`,
|
|
50
|
+
`~/.local/share/opencodex`) and provider IDs are retained, reusing your saved
|
|
51
|
+
key and backup.
|
|
52
|
+
|
|
53
|
+
Run `deepcodex doctor` and `deepcodex install` to refresh the runtime; there is
|
|
54
|
+
no need to run `configure` again if your key is already saved.
|
|
55
|
+
|
|
56
|
+
### 1. `deepcodex configure`
|
|
43
57
|
|
|
44
58
|
`configure` asks for the DeepSeek API key with hidden input on the terminal and
|
|
45
59
|
stores it in `~/.config/opencodex/.env`. It creates the directory with mode
|
|
@@ -57,7 +71,7 @@ the file, as does the installed desktop service. Credentials stay outside the
|
|
|
57
71
|
repository. The file permissions restrict other users, but programs running as
|
|
58
72
|
your user can still read it.
|
|
59
73
|
|
|
60
|
-
### 2. `
|
|
74
|
+
### 2. `deepcodex doctor`
|
|
61
75
|
|
|
62
76
|
`doctor` checks the local prerequisites without running inference: that a
|
|
63
77
|
compatible Codex CLI is reachable and that the DeepSeek credential is present.
|
|
@@ -66,7 +80,7 @@ unless the doctor reports `ready`.
|
|
|
66
80
|
Neither `configure` nor `doctor` contacts DeepSeek to validate the key. A `ready`
|
|
67
81
|
result confirms local prerequisites, not provider authentication or account credit.
|
|
68
82
|
|
|
69
|
-
### 3. `
|
|
83
|
+
### 3. `deepcodex install`
|
|
70
84
|
|
|
71
85
|
`install` activates the desktop integration:
|
|
72
86
|
|
|
@@ -74,11 +88,14 @@ result confirms local prerequisites, not provider authentication or account cred
|
|
|
74
88
|
dependency) to `~/.local/share/opencodex/runtime`;
|
|
75
89
|
- writes private state, the model catalog and receipts to
|
|
76
90
|
`~/.config/opencodex/desktop`;
|
|
77
|
-
- installs and starts the `com.
|
|
91
|
+
- installs and starts the `com.deepcodex.router` LaunchAgent, bound only to
|
|
78
92
|
`127.0.0.1:4207`;
|
|
79
93
|
- updates `~/.codex/config.toml` with the local provider, the subagent defaults
|
|
80
94
|
and the generated model catalog, keeping a pre-install backup at
|
|
81
|
-
`~/.config/opencodex/desktop/config.before.toml
|
|
95
|
+
`~/.config/opencodex/desktop/config.before.toml`;
|
|
96
|
+
- registers and installs the bundled DeepCodex plugin in the personal marketplace,
|
|
97
|
+
including its current name, description, icon and delegation skill. An existing
|
|
98
|
+
local OpenCodex plugin in that marketplace is replaced after DeepCodex installs.
|
|
82
99
|
|
|
83
100
|
The installer refuses to run when an unrelated custom provider is active or when
|
|
84
101
|
an endpoint override is already configured, and it leaves your configuration
|
|
@@ -89,22 +106,24 @@ that were already open keep the provider and model catalog they started with.
|
|
|
89
106
|
|
|
90
107
|
## Plugin skill
|
|
91
108
|
|
|
92
|
-
The delegation skill lives in `skills/delegate-flash/SKILL.md`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
The delegation skill lives in `skills/delegate-flash/SKILL.md`. `deepcodex install`
|
|
110
|
+
copies the plugin to `~/plugins/deepcodex`, registers it in
|
|
111
|
+
`~/.agents/plugins/marketplace.json`, and installs it through the Codex CLI.
|
|
112
|
+
Repeating the command refreshes the plugin cache even when the package version
|
|
113
|
+
has not changed. Other personal marketplace entries are preserved.
|
|
96
114
|
|
|
97
115
|
## Commands
|
|
98
116
|
|
|
99
117
|
| Command | Purpose |
|
|
100
118
|
| --- | --- |
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
119
|
+
| `deepcodex configure` | Prompt for the DeepSeek API key and write `~/.config/opencodex/.env`. |
|
|
120
|
+
| `deepcodex doctor` | Check Codex CLI compatibility and credential presence without inference. |
|
|
121
|
+
| `deepcodex install` | Install the router runtime, LaunchAgent, Codex settings and current DeepCodex plugin. |
|
|
122
|
+
| `deepcodex uninstall` | Stop and remove the router, restoring the saved Codex configuration. Credentials are preserved. Refuses if config changed since installation. Restart Codex afterward; remove the npm package separately with `npm uninstall -g deepcodex`. |
|
|
123
|
+
| `deepcodex status` | Query the installed router health endpoint without inference. |
|
|
124
|
+
| `deepcodex run --cwd PATH --task-file PATH [--write]` | Run one bounded isolated worker ticket against DeepSeek. Read-only unless `--write` is given. |
|
|
125
|
+
| `deepcodex pilot` | Run the opt-in live native-delegation test; consumes Codex and DeepSeek usage. |
|
|
126
|
+
| `deepcodex --version`, `deepcodex --help` | Print the package version or the command list. |
|
|
108
127
|
|
|
109
128
|
`run` executes a single ticket and consumes DeepSeek API usage. It takes an
|
|
110
129
|
exclusive per-user lock, so only one worker runs at a time.
|
|
Binary file
|
|
@@ -3,21 +3,22 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
|
|
4
4
|
const args = process.argv.slice(2);
|
|
5
5
|
const command = args.shift();
|
|
6
|
-
const help = `Usage:
|
|
6
|
+
const help = `Usage: deepcodex <command>
|
|
7
7
|
|
|
8
8
|
Commands:
|
|
9
9
|
configure Save the DeepSeek key using a hidden terminal prompt
|
|
10
|
-
install Activate the local macOS router and
|
|
10
|
+
install Activate the local macOS router and install the Codex plugin
|
|
11
|
+
uninstall Remove the local router and restore Codex settings
|
|
11
12
|
status Check the installed router without inference
|
|
12
13
|
doctor Check worker prerequisites without inference
|
|
13
14
|
run Run an isolated worker ticket (consumes DeepSeek usage)
|
|
14
15
|
pilot Test native delegation (consumes Codex and DeepSeek usage)
|
|
15
16
|
|
|
16
|
-
Requires macOS, Node.js >=22.15 and a compatible Codex CLI.
|
|
17
|
-
Run
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Use
|
|
17
|
+
Requires macOS, Node.js >=22.15 and a compatible Codex CLI (PATH or Desktop bundle).
|
|
18
|
+
Run deepcodex configure to save the DeepSeek key in ~/.config/opencodex/.env.
|
|
19
|
+
Run deepcodex install after installing this npm package to activate the router
|
|
20
|
+
and install or update the DeepCodex plugin, including its icon and skill.
|
|
21
|
+
Use deepcodex <command> --help for command options.
|
|
21
22
|
`;
|
|
22
23
|
|
|
23
24
|
if (!command || command === '--help' || command === '-h') {
|
|
@@ -26,24 +27,24 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
26
27
|
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
27
28
|
console.log(manifest.version);
|
|
28
29
|
} else {
|
|
29
|
-
const scripts = { configure: 'credentials.js', install: 'desktop.js', status: 'desktop.js', doctor: 'worker.js', run: 'worker.js', pilot: 'pilot.js' };
|
|
30
|
+
const scripts = { configure: 'credentials.js', install: 'desktop.js', uninstall: 'desktop.js', status: 'desktop.js', doctor: 'worker.js', run: 'worker.js', pilot: 'pilot.js' };
|
|
30
31
|
if (!Object.hasOwn(scripts, command)) {
|
|
31
32
|
console.error(`Unknown command: ${command}\n${help}`);
|
|
32
33
|
process.exit(2);
|
|
33
34
|
}
|
|
34
35
|
if (command === 'pilot' && args.length) {
|
|
35
36
|
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
36
|
-
console.log('Usage:
|
|
37
|
+
console.log('Usage: deepcodex pilot\nRuns a live native delegation test; consumes Codex and DeepSeek usage.');
|
|
37
38
|
process.exit(0);
|
|
38
39
|
}
|
|
39
|
-
console.error('Usage:
|
|
40
|
+
console.error('Usage: deepcodex pilot (no arguments)');
|
|
40
41
|
process.exit(2);
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
44
|
const script = await import(new URL(`../scripts/${scripts[command]}`, import.meta.url));
|
|
44
45
|
process.exitCode = command === 'pilot' ? await script.run() : await script.main([command, ...args]);
|
|
45
46
|
} catch (error) {
|
|
46
|
-
console.error(`
|
|
47
|
+
console.error(`DeepCodex failed: ${error.message}`);
|
|
47
48
|
process.exitCode = 1;
|
|
48
49
|
}
|
|
49
50
|
}
|
package/config/desktop.json
CHANGED
package/config/worker.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"sandbox_workspace_write": {"network_access": false}
|
|
40
40
|
},
|
|
41
41
|
"model_metadata": {
|
|
42
|
-
"display_name": "DeepSeek Flash
|
|
42
|
+
"display_name": "DeepSeek Flash",
|
|
43
43
|
"description": "Text worker using the direct DeepSeek API.",
|
|
44
44
|
"default_reasoning_level": "high",
|
|
45
45
|
"supported_reasoning_levels": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepcodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Native DeepSeek Flash subagents for Codex through a local router.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"bin": {
|
|
20
|
-
"deepcodex": "bin/
|
|
20
|
+
"deepcodex": "bin/deepcodex.js"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"bin/*.js",
|
|
24
24
|
".codex-plugin/plugin.json",
|
|
25
|
+
"assets/deepcodex.png",
|
|
25
26
|
"skills/delegate-flash/SKILL.md",
|
|
26
27
|
"config/*.json",
|
|
27
28
|
"prompts/worker.md",
|
package/prompts/worker.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You are the
|
|
1
|
+
You are the DeepCodex worker. Complete only the ticket you received and return
|
|
2
2
|
evidence to the coordinator. Follow the communication language stated in the
|
|
3
3
|
ticket for your reports; keep the language of the code unchanged.
|
|
4
4
|
|
package/scripts/credentials.js
CHANGED
|
@@ -87,16 +87,16 @@ export function saveCredentials(file, name, secret) {
|
|
|
87
87
|
|
|
88
88
|
export async function main(args) {
|
|
89
89
|
if (args.length === 2 && ['--help', '-h'].includes(args[1])) {
|
|
90
|
-
console.log('Usage:
|
|
90
|
+
console.log('Usage: deepcodex configure\nEnter the DeepSeek key in a hidden terminal prompt. Saves a private plaintext file outside the repository.');
|
|
91
91
|
return 0;
|
|
92
92
|
}
|
|
93
|
-
if (args.length !== 1 || args[0] !== 'configure') throw new Error('Usage:
|
|
93
|
+
if (args.length !== 1 || args[0] !== 'configure') throw new Error('Usage: deepcodex configure (no key arguments accepted)');
|
|
94
94
|
const config = loadConfig();
|
|
95
95
|
const name = config.codex.model_providers[config.codex.model_provider].env_key;
|
|
96
96
|
const configured = config.credentials.env_file;
|
|
97
97
|
const file = configured.startsWith('~/') ? path.join(os.homedir(), configured.slice(2)) : configured;
|
|
98
98
|
const secret = await readSecret();
|
|
99
99
|
saveCredentials(file, name, secret);
|
|
100
|
-
console.log('DeepSeek key saved with owner-only permissions. Run
|
|
100
|
+
console.log('DeepSeek key saved with owner-only permissions. Run deepcodex install to activate it or reload an existing router.');
|
|
101
101
|
return 0;
|
|
102
102
|
}
|
package/scripts/desktop.js
CHANGED
|
@@ -11,6 +11,7 @@ import { parseToml } from './toml.js';
|
|
|
11
11
|
import { startPilot } from './pilot-router.js';
|
|
12
12
|
|
|
13
13
|
const STATE = path.join(os.homedir(), '.config/opencodex/desktop');
|
|
14
|
+
const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
|
|
14
15
|
const readJson = filename => JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
15
16
|
const expandHome = filename => filename.startsWith('~/') ? path.join(os.homedir(), filename.slice(2)) : filename;
|
|
16
17
|
|
|
@@ -78,7 +79,7 @@ export function plistDocument(value) {
|
|
|
78
79
|
|
|
79
80
|
export function copyRuntime(root, runtime) {
|
|
80
81
|
fs.mkdirSync(runtime, { recursive: true, mode: 0o700 });
|
|
81
|
-
for (const directory of ['scripts', 'config', 'prompts', 'vendor']) {
|
|
82
|
+
for (const directory of ['scripts', 'config', 'prompts', 'vendor', '.codex-plugin', 'assets', 'skills']) {
|
|
82
83
|
fs.cpSync(path.join(root, directory), path.join(runtime, directory), { recursive: true,
|
|
83
84
|
filter: source => !['__pycache__', '.env', '.DS_Store'].includes(path.basename(source)) && !/\.py[cod]?$/.test(source) });
|
|
84
85
|
}
|
|
@@ -87,6 +88,77 @@ export function copyRuntime(root, runtime) {
|
|
|
87
88
|
fs.cpSync(dependency, path.join(runtime, 'node_modules/smol-toml'), { recursive: true });
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function registerServiceApp(app, action) {
|
|
92
|
+
const result = spawnSync(LSREGISTER, [action, app], { encoding: 'utf8' });
|
|
93
|
+
if (result.error || result.status !== 0) throw new Error(`Cannot ${action === '-u' ? 'unregister' : 'register'} DeepCodex app: ${result.error?.message || result.stderr.trim() || `lsregister exited ${result.status}`}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createServiceApp(runtime, label) {
|
|
97
|
+
const app = path.join(runtime, 'DeepCodex.app');
|
|
98
|
+
const contents = path.join(app, 'Contents');
|
|
99
|
+
const executable = path.join(contents, 'MacOS/DeepCodex');
|
|
100
|
+
fs.mkdirSync(path.dirname(executable), { recursive: true });
|
|
101
|
+
fs.mkdirSync(path.join(contents, 'Resources'), { recursive: true });
|
|
102
|
+
const version = readJson(path.join(runtime, 'package.json')).version;
|
|
103
|
+
privateWrite(path.join(contents, 'Info.plist'), plistDocument({
|
|
104
|
+
CFBundleIdentifier: label, CFBundleName: 'DeepCodex', CFBundleDisplayName: 'DeepCodex',
|
|
105
|
+
CFBundleExecutable: 'DeepCodex', CFBundlePackageType: 'APPL', CFBundleIconFile: 'DeepCodex.icns',
|
|
106
|
+
CFBundleVersion: version, CFBundleShortVersionString: version, LSUIElement: true,
|
|
107
|
+
}));
|
|
108
|
+
const command = [process.execPath, path.join(runtime, 'scripts/desktop.js')]
|
|
109
|
+
.map(value => "'" + value.replaceAll("'", "'\\''") + "'").join(' ');
|
|
110
|
+
privateWrite(executable, `#!/bin/sh\nexec ${command} "$@"\n`);
|
|
111
|
+
fs.chmodSync(executable, 0o700);
|
|
112
|
+
const icon = spawnSync('/usr/bin/sips', ['-z', '512', '512', '-s', 'format', 'icns',
|
|
113
|
+
path.join(runtime, 'assets/deepcodex.png'), '--out', path.join(contents, 'Resources/DeepCodex.icns')], { encoding: 'utf8' });
|
|
114
|
+
if (icon.error || icon.status !== 0) throw new Error(`Cannot create DeepCodex app icon: ${icon.error?.message || icon.stderr.trim() || `sips exited ${icon.status}`}`);
|
|
115
|
+
registerServiceApp(app, '-f');
|
|
116
|
+
return executable;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function installPlugin(root, codex, env) {
|
|
120
|
+
const home = env.HOME || os.homedir();
|
|
121
|
+
const marketplacePath = path.join(home, '.agents/plugins/marketplace.json');
|
|
122
|
+
const marketplace = fs.existsSync(marketplacePath) ? readJson(marketplacePath) : {
|
|
123
|
+
name: 'personal', interface: { displayName: 'Personal' }, plugins: [],
|
|
124
|
+
};
|
|
125
|
+
if (typeof marketplace.name !== 'string' || !/^[A-Za-z0-9_-]+$/.test(marketplace.name) || !Array.isArray(marketplace.plugins)) {
|
|
126
|
+
throw new Error('Invalid personal plugin marketplace');
|
|
127
|
+
}
|
|
128
|
+
const manifest = readJson(path.join(root, '.codex-plugin/plugin.json'));
|
|
129
|
+
if (manifest.name !== 'deepcodex' || typeof manifest.version !== 'string') throw new Error('Invalid DeepCodex plugin manifest');
|
|
130
|
+
const pluginPath = path.join(home, 'plugins', manifest.name);
|
|
131
|
+
copyRuntime(root, pluginPath);
|
|
132
|
+
manifest.version = `${manifest.version.split('+')[0]}+codex.${new Date().toISOString().replace(/\D/g, '')}`;
|
|
133
|
+
privateWrite(path.join(pluginPath, '.codex-plugin/plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
134
|
+
const entry = {
|
|
135
|
+
name: manifest.name, source: { source: 'local', path: `./plugins/${manifest.name}` },
|
|
136
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, category: manifest.interface.category,
|
|
137
|
+
};
|
|
138
|
+
const existing = marketplace.plugins.findIndex(plugin => plugin.name === manifest.name);
|
|
139
|
+
if (existing < 0) marketplace.plugins.push(entry);
|
|
140
|
+
else marketplace.plugins[existing] = { ...marketplace.plugins[existing], source: entry.source };
|
|
141
|
+
fs.mkdirSync(path.dirname(marketplacePath), { recursive: true });
|
|
142
|
+
privateWrite(marketplacePath, JSON.stringify(marketplace, null, 2) + '\n');
|
|
143
|
+
const run = (command, selector) => {
|
|
144
|
+
const result = spawnSync(codex, ['plugin', command, selector, '--json'], { env, encoding: 'utf8' });
|
|
145
|
+
if (result.error || result.status !== 0) throw new Error(`Cannot ${command} Codex plugin ${selector}; rerun deepcodex install after checking Codex plugin support`);
|
|
146
|
+
return JSON.parse(result.stdout);
|
|
147
|
+
};
|
|
148
|
+
const selector = `${manifest.name}@${marketplace.name}`;
|
|
149
|
+
const result = run('add', selector);
|
|
150
|
+
const configPath = path.join(env.CODEX_HOME || path.join(home, '.codex'), 'config.toml');
|
|
151
|
+
const config = parseToml(fs.readFileSync(configPath, 'utf8'));
|
|
152
|
+
const legacySelector = `opencodex@${marketplace.name}`;
|
|
153
|
+
if (Object.hasOwn(config.plugins || {}, legacySelector)) run('remove', legacySelector);
|
|
154
|
+
const legacy = marketplace.plugins.find(plugin => plugin.name === 'opencodex' && plugin.source?.source === 'local');
|
|
155
|
+
if (legacy) {
|
|
156
|
+
marketplace.plugins = marketplace.plugins.filter(plugin => plugin !== legacy);
|
|
157
|
+
privateWrite(marketplacePath, JSON.stringify(marketplace, null, 2) + '\n');
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
export async function health(state) {
|
|
91
163
|
const response = await fetch(`http://127.0.0.1:${state.config.port}/health`, {
|
|
92
164
|
headers: { 'x-opencodex-pilot': state.capability },
|
|
@@ -103,7 +175,9 @@ export async function install() {
|
|
|
103
175
|
const env = workerEnvironment(process.env);
|
|
104
176
|
loadCredentials(env, original);
|
|
105
177
|
const diagnosis = doctor(original, env);
|
|
106
|
-
if (diagnosis.status !== 'ready') throw new Error(
|
|
178
|
+
if (diagnosis.status !== 'ready') throw new Error(`DeepCodex prerequisites are not ready: ${diagnosis.errors.join(' ')}`);
|
|
179
|
+
const pluginSupport = spawnSync(diagnosis.codex, ['plugin', 'add', '--help'], { env, encoding: 'utf8' });
|
|
180
|
+
if (pluginSupport.error || pluginSupport.status !== 0) throw new Error('Codex CLI must support plugin add; update Codex Desktop before installing DeepCodex');
|
|
107
181
|
const configPath = path.join(env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'config.toml');
|
|
108
182
|
const before = fs.readFileSync(configPath, 'utf8');
|
|
109
183
|
const parsed = parseToml(before);
|
|
@@ -113,6 +187,7 @@ export async function install() {
|
|
|
113
187
|
fs.chmodSync(STATE, 0o700);
|
|
114
188
|
const runtime = path.join(os.homedir(), '.local/share/opencodex/runtime');
|
|
115
189
|
copyRuntime(ROOT, runtime);
|
|
190
|
+
const executable = createServiceApp(runtime, config.service_label);
|
|
116
191
|
let catalog;
|
|
117
192
|
if (parsed.model_catalog_json && path.resolve(expandHome(parsed.model_catalog_json)) !== path.join(STATE, 'models.json')) {
|
|
118
193
|
catalog = readJson(expandHome(parsed.model_catalog_json));
|
|
@@ -130,30 +205,35 @@ export async function install() {
|
|
|
130
205
|
receipts: path.join(STATE, 'receipts.jsonl'), markers: [] });
|
|
131
206
|
const statePath = path.join(STATE, 'state.json');
|
|
132
207
|
const capability = fs.existsSync(statePath) ? readJson(statePath).capability : randomBytes(32).toString('base64url');
|
|
133
|
-
const state = { config, capability, node: process.execPath, runtime };
|
|
208
|
+
const state = { config, capability, node: process.execPath, runtime, configPath };
|
|
134
209
|
const sections = {
|
|
135
210
|
'': { model_provider: 'opencodex', model_catalog_json: path.join(STATE, 'models.json') },
|
|
136
211
|
agents: { enabled: true, default_subagent_model: child.slug, default_subagent_reasoning_effort: 'high' },
|
|
137
212
|
features: { multi_agent: true, multi_agent_v2: true },
|
|
138
|
-
'model_providers.opencodex': { name: '
|
|
213
|
+
'model_providers.opencodex': { name: 'DeepCodex', base_url: `http://127.0.0.1:${config.port}`, wire_api: 'responses',
|
|
139
214
|
requires_openai_auth: true, supports_websockets: false, request_max_retries: 0, stream_max_retries: 0, stream_idle_timeout_ms: config.request_timeout_ms },
|
|
140
215
|
'model_providers.opencodex.http_headers': { 'x-opencodex-pilot': capability },
|
|
141
216
|
};
|
|
142
|
-
|
|
217
|
+
let updated = mergeConfig(before, sections);
|
|
143
218
|
privateWrite(path.join(STATE, 'config.proposed.toml'), updated);
|
|
144
219
|
if (!fs.existsSync(path.join(STATE, 'config.before.toml'))) privateWrite(path.join(STATE, 'config.before.toml'), before);
|
|
145
220
|
privateWrite(statePath, JSON.stringify(state));
|
|
146
221
|
const label = config.service_label;
|
|
147
222
|
const plist = path.join(os.homedir(), 'Library/LaunchAgents', label + '.plist');
|
|
148
223
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
149
|
-
privateWrite(plist, plistDocument({ Label: label,
|
|
224
|
+
privateWrite(plist, plistDocument({ Label: label, AssociatedBundleIdentifiers: [label], ProgramArguments: [executable, 'serve'],
|
|
150
225
|
WorkingDirectory: runtime, RunAtLoad: true, KeepAlive: true, ThrottleInterval: config.service_throttle_seconds,
|
|
151
226
|
StandardOutPath: path.join(STATE, 'service.log'), StandardErrorPath: path.join(STATE, 'service-errors.log'),
|
|
152
227
|
EnvironmentVariables: { HOME: os.homedir(), PATH: env.PATH } }));
|
|
153
228
|
const domain = `gui/${process.getuid()}`;
|
|
154
|
-
spawnSync('launchctl', ['bootout', `${domain}/${label}`]);
|
|
155
|
-
|
|
156
|
-
|
|
229
|
+
const stopped = spawnSync('launchctl', ['bootout', `${domain}/${label}`], { encoding: 'utf8' });
|
|
230
|
+
if (stopped.error || (stopped.status !== 0 && stopped.status !== 3)) {
|
|
231
|
+
throw new Error(`Cannot stop ${label}: ${stopped.error?.message || stopped.stderr.trim() || `launchctl exited ${stopped.status}`}`);
|
|
232
|
+
}
|
|
233
|
+
const bootstrap = spawnSync('launchctl', ['bootstrap', domain, plist], { encoding: 'utf8' });
|
|
234
|
+
if (bootstrap.error || bootstrap.status !== 0) {
|
|
235
|
+
throw new Error(`Cannot start DeepCodex LaunchAgent: ${bootstrap.error?.message || bootstrap.stderr.trim() || `launchctl exited ${bootstrap.status}`}`);
|
|
236
|
+
}
|
|
157
237
|
const deadline = Date.now() + config.startup_timeout_seconds * 1000;
|
|
158
238
|
let report;
|
|
159
239
|
while (!report) {
|
|
@@ -167,18 +247,62 @@ export async function install() {
|
|
|
167
247
|
}
|
|
168
248
|
}
|
|
169
249
|
if (fs.readFileSync(configPath, 'utf8') !== before) throw new Error('Codex config changed during installation; proposed config was not applied');
|
|
250
|
+
let plugin;
|
|
251
|
+
try {
|
|
252
|
+
plugin = installPlugin(ROOT, diagnosis.codex, env);
|
|
253
|
+
updated = mergeConfig(fs.readFileSync(configPath, 'utf8'), sections);
|
|
254
|
+
privateWrite(path.join(STATE, 'config.proposed.toml'), updated);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
spawnSync('launchctl', ['bootout', `${domain}/${label}`]);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
170
259
|
privateWrite(configPath, updated);
|
|
171
|
-
console.log(JSON.stringify({ ...report, service: label, port: config.port, cwd: runtime, owner: '
|
|
172
|
-
native_models: nativeModels.length, subagent_model: child.slug, backup: path.join(STATE, 'config.before.toml'), restart_desktop_required: true }));
|
|
260
|
+
console.log(JSON.stringify({ ...report, service: label, port: config.port, cwd: runtime, owner: 'DeepCodex LaunchAgent',
|
|
261
|
+
native_models: nativeModels.length, subagent_model: child.slug, plugin, backup: path.join(STATE, 'config.before.toml'), restart_desktop_required: true }));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function uninstall() {
|
|
265
|
+
if (process.platform !== 'darwin') throw new Error('The Desktop uninstaller requires macOS');
|
|
266
|
+
const statePath = path.join(STATE, 'state.json');
|
|
267
|
+
if (!fs.existsSync(statePath)) {
|
|
268
|
+
console.log(JSON.stringify({ status: 'not_installed' }));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const state = readJson(statePath);
|
|
272
|
+
const configPath = state.configPath || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'config.toml');
|
|
273
|
+
const before = fs.readFileSync(path.join(STATE, 'config.before.toml'), 'utf8');
|
|
274
|
+
const proposed = fs.readFileSync(path.join(STATE, 'config.proposed.toml'), 'utf8');
|
|
275
|
+
const current = fs.readFileSync(configPath, 'utf8');
|
|
276
|
+
if (current !== proposed && current !== before) {
|
|
277
|
+
throw new Error('Codex configuration changed after installation; uninstall stopped without changes. Reconcile config.toml with the backup and proposed config in ' + STATE);
|
|
278
|
+
}
|
|
279
|
+
const runtime = path.join(os.homedir(), '.local/share/opencodex/runtime');
|
|
280
|
+
if (state.runtime !== runtime) throw new Error('Unexpected router runtime path; uninstall stopped');
|
|
281
|
+
const label = readJson(path.join(ROOT, 'config/desktop.json')).service_label;
|
|
282
|
+
if (state.config.service_label !== label) throw new Error('Unexpected router service label; uninstall stopped');
|
|
283
|
+
const service = `gui/${process.getuid()}/${label}`;
|
|
284
|
+
// Restore connectivity before stopping the router; a failed stop can be retried.
|
|
285
|
+
if (current !== before) privateWrite(configPath, before);
|
|
286
|
+
const stopped = spawnSync('launchctl', ['bootout', service]);
|
|
287
|
+
if (stopped.error || (stopped.status !== 0 && stopped.status !== 3)) {
|
|
288
|
+
throw new Error('Cannot stop DeepCodex LaunchAgent; Codex settings restored, runtime retained. Retry uninstall.');
|
|
289
|
+
}
|
|
290
|
+
fs.rmSync(path.join(os.homedir(), 'Library/LaunchAgents', label + '.plist'), { force: true });
|
|
291
|
+
const app = path.join(runtime, 'DeepCodex.app');
|
|
292
|
+
if (fs.existsSync(app)) registerServiceApp(app, '-u');
|
|
293
|
+
fs.rmSync(runtime, { recursive: true, force: true });
|
|
294
|
+
fs.rmSync(STATE, { recursive: true, force: true });
|
|
295
|
+
console.log(JSON.stringify({ status: 'uninstalled', restart_desktop_required: true, credentials_preserved: true }));
|
|
173
296
|
}
|
|
174
297
|
|
|
175
298
|
export async function main(args = process.argv.slice(2)) {
|
|
176
299
|
if (args.includes('--help') || args.includes('-h')) {
|
|
177
|
-
console.log('Usage:
|
|
300
|
+
console.log('Usage: deepcodex <install|uninstall|status>\nActivate or inspect the local macOS Desktop router.');
|
|
178
301
|
return 0;
|
|
179
302
|
}
|
|
180
|
-
if (args.length !== 1 || !['install', 'serve', 'status'].includes(args[0])) throw new Error('Expected install, serve or status');
|
|
303
|
+
if (args.length !== 1 || !['install', 'uninstall', 'serve', 'status'].includes(args[0])) throw new Error('Expected install, uninstall, serve or status');
|
|
181
304
|
if (args[0] === 'install') await install();
|
|
305
|
+
else if (args[0] === 'uninstall') uninstall();
|
|
182
306
|
else {
|
|
183
307
|
const state = readJson(path.join(STATE, 'state.json'));
|
|
184
308
|
if (args[0] === 'status') console.log(JSON.stringify({ ...await health(state), port: state.config.port, cwd: state.runtime }));
|
package/scripts/pilot-router.js
CHANGED
|
@@ -223,7 +223,7 @@ export async function startPilot(config, deepseekKey, capability) {
|
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
225
|
receipt({ route: 'error', message: String(error.message).replaceAll(deepseekKey, '[REDACTED]').replaceAll(capability, '[REDACTED]') });
|
|
226
|
-
if (!response.headersSent) response.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: { message: '
|
|
226
|
+
if (!response.headersSent) response.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: { message: 'DeepCodex request failed; inspect local receipts.' } }));
|
|
227
227
|
else response.destroy();
|
|
228
228
|
} finally { clearTimeout(timer); }
|
|
229
229
|
});
|
package/scripts/pilot.js
CHANGED
|
@@ -120,7 +120,7 @@ export async function run() {
|
|
|
120
120
|
const sourceEnv = workerEnvironment(process.env);
|
|
121
121
|
loadCredentials(sourceEnv, workerConfig);
|
|
122
122
|
const diagnosis = doctor(workerConfig, sourceEnv);
|
|
123
|
-
if (diagnosis.status !== 'ready') throw new Error('
|
|
123
|
+
if (diagnosis.status !== 'ready') throw new Error('DeepCodex doctor is not ready');
|
|
124
124
|
const authSource = authFile(sourceEnv);
|
|
125
125
|
if (!statSync(authSource, { throwIfNoEntry: false })?.isFile()) {
|
|
126
126
|
throw new Error('Pilot requires the existing Codex auth.json login');
|
|
@@ -165,7 +165,7 @@ export async function run() {
|
|
|
165
165
|
{ env, cwd: workspace, stdio: ['pipe', 'pipe', routerErrors], detached: true });
|
|
166
166
|
router.stdin.end(JSON.stringify({ config, capability }));
|
|
167
167
|
const ready = await readRouterReadiness(router, config.startup_timeout_seconds * 1000, signals.signal);
|
|
168
|
-
emit({ event: 'pilot.router_started', ...ready, cwd: workspace, owner: '
|
|
168
|
+
emit({ event: 'pilot.router_started', ...ready, cwd: workspace, owner: 'DeepCodex isolated pilot',
|
|
169
169
|
artifacts: runDir });
|
|
170
170
|
const values = { ...workerConfig.codex };
|
|
171
171
|
Object.assign(values, { model: config.parent_model, model_provider: 'opencodex-pilot',
|
|
@@ -175,7 +175,7 @@ export async function run() {
|
|
|
175
175
|
values.features = { ...values.features, multi_agent: true, multi_agent_v2: true,
|
|
176
176
|
enable_request_compression: false };
|
|
177
177
|
values.model_providers = { 'opencodex-pilot': {
|
|
178
|
-
name: '
|
|
178
|
+
name: 'DeepCodex isolated pilot',
|
|
179
179
|
base_url: `http://127.0.0.1:${ready.port}`,
|
|
180
180
|
wire_api: 'responses',
|
|
181
181
|
requires_openai_auth: true,
|
package/scripts/worker.js
CHANGED
|
@@ -99,8 +99,23 @@ export function redact(value, secret) {
|
|
|
99
99
|
return value;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// Desktop bundles the CLI but does not necessarily expose it on PATH.
|
|
103
|
+
export function resolveCodex(env, { platform = process.platform, applicationDirs = [
|
|
104
|
+
path.join(env.HOME || os.homedir(), 'Applications'), '/Applications',
|
|
105
|
+
] } = {}) {
|
|
103
106
|
const binary = which('codex', env.PATH);
|
|
107
|
+
if (binary || platform !== 'darwin') return binary;
|
|
108
|
+
for (const directory of applicationDirs) {
|
|
109
|
+
for (const app of ['Codex.app', 'ChatGPT.app']) {
|
|
110
|
+
const bundled = which('codex', path.join(directory, app, 'Contents', 'Resources'));
|
|
111
|
+
if (bundled) return bundled;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function doctor(config, env, discoveryOptions) {
|
|
118
|
+
const binary = resolveCodex(env, discoveryOptions);
|
|
104
119
|
const report = { status: 'ready', codex: binary ?? null, api_key_present: Boolean(env.DEEPSEEK_API_KEY) };
|
|
105
120
|
if (binary) {
|
|
106
121
|
const timeout = config.limits.version_timeout_seconds * 1000;
|
|
@@ -110,7 +125,14 @@ export function doctor(config, env) {
|
|
|
110
125
|
report.compatible_cli = version.status === 0 && help.status === 0
|
|
111
126
|
&& REQUIRED_CLI_FLAGS.every(flag => help.stdout.includes(flag));
|
|
112
127
|
}
|
|
113
|
-
|
|
128
|
+
const errors = [];
|
|
129
|
+
if (!binary) errors.push('Codex CLI not found on PATH or in Codex.app/ChatGPT.app under ~/Applications or /Applications. Install Codex Desktop or add a compatible codex CLI to PATH.');
|
|
130
|
+
else if (!report.compatible_cli) errors.push(`Codex CLI at ${binary} is incompatible; required exec flags: ${REQUIRED_CLI_FLAGS.join(', ')}. Update Codex Desktop or the CLI on PATH.`);
|
|
131
|
+
if (!report.api_key_present) errors.push('DeepSeek API key is missing. Run deepcodex configure.');
|
|
132
|
+
if (errors.length) {
|
|
133
|
+
report.status = 'not_ready';
|
|
134
|
+
report.errors = errors;
|
|
135
|
+
}
|
|
114
136
|
return report;
|
|
115
137
|
}
|
|
116
138
|
|
|
@@ -147,7 +169,7 @@ export async function workerLock() {
|
|
|
147
169
|
database.exec('BEGIN EXCLUSIVE');
|
|
148
170
|
} catch (error) {
|
|
149
171
|
database.close();
|
|
150
|
-
if (error.errcode === SQLITE_BUSY) throw new Error('Another
|
|
172
|
+
if (error.errcode === SQLITE_BUSY) throw new Error('Another DeepCodex worker is running; wait for it or cancel that run');
|
|
151
173
|
throw error;
|
|
152
174
|
}
|
|
153
175
|
let closed = false;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: delegate-flash
|
|
3
|
-
description: Delegate bounded coding tasks to native DeepSeek Flash subagents in Codex through
|
|
3
|
+
description: Delegate bounded coding tasks to native DeepSeek Flash subagents in Codex through DeepCodex. Use when DeepCodex or Flash delegation is requested, or when diagnosing the Desktop integration.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# DeepCodex
|
|
7
7
|
|
|
8
8
|
Keep the coordinator on the user's selected native model. DeepSeek Flash runs as
|
|
9
|
-
an actual Codex subagent through the local
|
|
9
|
+
an actual Codex subagent through the local DeepCodex router. Use the native
|
|
10
10
|
collaboration tools for spawning, follow-ups, waiting, and interruption; the
|
|
11
11
|
normal delegation path does not launch a separate `codex exec` worker.
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ is in `config/desktop.json`, shared transport defaults in `config/pilot.json`,
|
|
|
42
42
|
and the DeepSeek model and credential path in `config/worker.json`.
|
|
43
43
|
|
|
44
44
|
Run `node <plugin>/scripts/desktop.js status` to check the local service without
|
|
45
|
-
inference. The service is `com.
|
|
45
|
+
inference. The service is `com.deepcodex.router`, bound only to loopback. Private
|
|
46
46
|
state and bounded metadata receipts live in `~/.config/opencodex/desktop`.
|
|
47
47
|
Receipts contain routing and tool names, not prompts, tool arguments or tokens.
|
|
48
48
|
Do not print `state.json`, provider headers, credential files or full user config.
|