deepcodex 0.1.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepcodex",
3
- "version": "0.1.1",
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
6
  "name": "DeepCodex"
package/README.md CHANGED
@@ -50,14 +50,6 @@ is `com.deepcodex.router`. Existing private paths (`~/.config/opencodex`,
50
50
  `~/.local/share/opencodex`) and provider IDs are retained, reusing your saved
51
51
  key and backup.
52
52
 
53
- If the previous `com.opencodex.router` LaunchAgent is installed, unload it and
54
- remove its plist before running `deepcodex install` to avoid competing services:
55
-
56
- ```sh
57
- launchctl bootout "gui/$(id -u)/com.opencodex.router"
58
- rm ~/Library/LaunchAgents/com.opencodex.router.plist
59
- ```
60
-
61
53
  Run `deepcodex doctor` and `deepcodex install` to refresh the runtime; there is
62
54
  no need to run `configure` again if your key is already saved.
63
55
 
@@ -100,7 +92,10 @@ result confirms local prerequisites, not provider authentication or account cred
100
92
  `127.0.0.1:4207`;
101
93
  - updates `~/.codex/config.toml` with the local provider, the subagent defaults
102
94
  and the generated model catalog, keeping a pre-install backup at
103
- `~/.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.
104
99
 
105
100
  The installer refuses to run when an unrelated custom provider is active or when
106
101
  an endpoint override is already configured, and it leaves your configuration
@@ -111,10 +106,11 @@ that were already open keep the provider and model catalog they started with.
111
106
 
112
107
  ## Plugin skill
113
108
 
114
- The delegation skill lives in `skills/delegate-flash/SKILL.md` and is loaded by
115
- installing the DeepCodex plugin through a Codex marketplace. Installing the npm
116
- package or running the commands above does not install the skill, and this
117
- checkout is not a marketplace source by itself.
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.
118
114
 
119
115
  ## Commands
120
116
 
@@ -122,7 +118,8 @@ checkout is not a marketplace source by itself.
122
118
  | --- | --- |
123
119
  | `deepcodex configure` | Prompt for the DeepSeek API key and write `~/.config/opencodex/.env`. |
124
120
  | `deepcodex doctor` | Check Codex CLI compatibility and credential presence without inference. |
125
- | `deepcodex install` | Install the router runtime, the LaunchAgent and the user-level Codex settings. |
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`. |
126
123
  | `deepcodex status` | Query the installed router health endpoint without inference. |
127
124
  | `deepcodex run --cwd PATH --task-file PATH [--write]` | Run one bounded isolated worker ticket against DeepSeek. Read-only unless `--write` is given. |
128
125
  | `deepcodex pilot` | Run the opt-in live native-delegation test; consumes Codex and DeepSeek usage. |
package/bin/deepcodex.js CHANGED
@@ -7,7 +7,8 @@ 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 configure Codex
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)
@@ -15,8 +16,8 @@ Commands:
15
16
 
16
17
  Requires macOS, Node.js >=22.15 and a compatible Codex CLI (PATH or Desktop bundle).
17
18
  Run deepcodex configure to save the DeepSeek key in ~/.config/opencodex/.env.
18
- Installing this npm package does not activate the router or install the skill
19
- in Codex. Install the plugin through a Codex marketplace to load its skill.
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.
20
21
  Use deepcodex <command> --help for command options.
21
22
  `;
22
23
 
@@ -26,7 +27,7 @@ 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepcodex",
3
- "version": "0.1.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": [
@@ -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 },
@@ -104,6 +176,8 @@ export async function install() {
104
176
  loadCredentials(env, original);
105
177
  const diagnosis = doctor(original, env);
106
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,7 +205,7 @@ 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' },
@@ -139,21 +214,26 @@ export async function install() {
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
- const updated = mergeConfig(before, sections);
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, ProgramArguments: [process.execPath, path.join(runtime, 'scripts/desktop.js'), 'serve'],
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
- const bootstrap = spawnSync('launchctl', ['bootstrap', domain, plist]);
156
- if (bootstrap.error || bootstrap.status !== 0) throw new Error('Cannot start DeepCodex LaunchAgent');
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
260
  console.log(JSON.stringify({ ...report, service: label, port: config.port, cwd: runtime, owner: 'DeepCodex LaunchAgent',
172
- native_models: nativeModels.length, subagent_model: child.slug, backup: path.join(STATE, 'config.before.toml'), restart_desktop_required: true }));
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: deepcodex <install|status>\nActivate or inspect the local macOS Desktop router.');
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 }));