external-subagent 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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/external-subagent-debug-mcp.mjs +5 -0
- package/bin/external-subagent-debug.mjs +13 -0
- package/bin/external-subagent-mcp.mjs +20 -0
- package/bin/external-subagent.mjs +8 -0
- package/bin/zas.mjs +8 -0
- package/cli/commands/agents.mjs +117 -0
- package/cli/commands/config.mjs +117 -0
- package/cli/commands/daemon.mjs +36 -0
- package/cli/commands/maintenance.mjs +27 -0
- package/cli/commands/plugin.mjs +96 -0
- package/cli/commands/tasks.mjs +64 -0
- package/cli/commands/update.mjs +219 -0
- package/cli/config/read.mjs +24 -0
- package/cli/config/schema.mjs +75 -0
- package/cli/config/write.mjs +39 -0
- package/cli/constants.mjs +24 -0
- package/cli/errors.mjs +7 -0
- package/cli/fs-atomic.mjs +36 -0
- package/cli/install/codex.mjs +352 -0
- package/cli/install/init.mjs +206 -0
- package/cli/install/layout.mjs +54 -0
- package/cli/install/npm-hook.mjs +49 -0
- package/cli/install/path.mjs +43 -0
- package/cli/install/payload.mjs +84 -0
- package/cli/install/plugin-stage.mjs +232 -0
- package/cli/install/reconcile.mjs +209 -0
- package/cli/install/recovery.mjs +79 -0
- package/cli/install/service-activation.mjs +133 -0
- package/cli/install/service-macos.mjs +146 -0
- package/cli/install/update.mjs +170 -0
- package/cli/install/zcode.mjs +239 -0
- package/cli/main.mjs +429 -0
- package/cli/maintenance.mjs +104 -0
- package/cli/paths.mjs +40 -0
- package/cli/rpc.mjs +261 -0
- package/launchd/com.external-subagent.daemon.plist.template +37 -0
- package/npm/native/README.md +19 -0
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +20 -0
- package/package.json +60 -0
- package/plugins/codex/external-subagent/.codex-plugin/plugin.json +17 -0
- package/plugins/codex/external-subagent/.mcp.json +9 -0
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +55 -0
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +19 -0
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +15 -0
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +206 -0
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +149 -0
- package/plugins/codex/external-subagent/scripts/mcp-stdio-bridge.mjs +36 -0
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +44 -0
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +55 -0
- package/schema/external-subagent-public-api.json +185 -0
- package/schema/zas-observation-v1.1.schema.json +58 -0
- package/schema/zas-public-reasoning-source.json +14 -0
- package/scripts/release/build-native-payload.mjs +119 -0
- package/scripts/release/check-native-tarball.mjs +125 -0
- package/scripts/release/stage-debug-plugin.mjs +60 -0
- package/scripts/release/test-installed-tarball.mjs +73 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CliError } from '../errors.mjs';
|
|
5
|
+
import { atomicWrite, jsonBytes, readOptional, restoreOptional } from '../fs-atomic.mjs';
|
|
6
|
+
import { pluginSourceRoot, PLUGIN_NAME } from './layout.mjs';
|
|
7
|
+
import { pluginManifest, treeDigest, preparePluginStage, guardedRestores } from './plugin-stage.mjs';
|
|
8
|
+
|
|
9
|
+
// Managed ZCode host binding. ZCode has no headless CLI for plugin
|
|
10
|
+
// management (the GUI drives internal IPC only), but its user config
|
|
11
|
+
// exposes an inline plugin discovery source: every path in
|
|
12
|
+
// `plugins.dirs` under ~/.zcode/cli/config.json is loaded as a plugin
|
|
13
|
+
// root with id <name>@inline and default-enabled status (verified
|
|
14
|
+
// against ZCode 25.6.0, see docs/compatibility/zcode.md). This module
|
|
15
|
+
// stages the plugin into a product-owned tree and registers exactly that
|
|
16
|
+
// one directory entry; it never touches marketplace, cache, or install
|
|
17
|
+
// record state that belongs to the ZCode client. There is no registry
|
|
18
|
+
// file: the binding is fully derivable from the config (ZCode has a
|
|
19
|
+
// single user config, no multi-home installs), so reconcile and uninstall
|
|
20
|
+
// detect rather than remember it.
|
|
21
|
+
|
|
22
|
+
const ZCODE_PLUGIN_ID = `${PLUGIN_NAME}@inline`;
|
|
23
|
+
const MANIFEST_PROBES = ['.zcode-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json'];
|
|
24
|
+
|
|
25
|
+
// ZCode spawns plugin MCP servers inside a restricted execution context
|
|
26
|
+
// that kills ad-hoc-signed native binaries (observed live: SIGKILL,
|
|
27
|
+
// "Connection closed"), while node processes and unix sockets are
|
|
28
|
+
// allowed. The zcode binding therefore pins the installing node running
|
|
29
|
+
// the staged stdio bridge instead of the native facade binary. It also
|
|
30
|
+
// pins `timeoutMs` on the server entry: the zcode host's MCP client caps
|
|
31
|
+
// every tool call at a default 30000ms unless the server config overrides
|
|
32
|
+
// it, which would cut `external_subagent_wait` (default 290s, ceiling
|
|
33
|
+
// 299s) at 30s; 300000ms covers that ceiling with margin.
|
|
34
|
+
const ZCODE_MCP_TIMEOUT_MS = 300000;
|
|
35
|
+
|
|
36
|
+
export function zcodeMcpBinding(staging) {
|
|
37
|
+
return { command: process.execPath, args: [path.join(staging, 'scripts', 'mcp-stdio-bridge.mjs')], timeoutMs: ZCODE_MCP_TIMEOUT_MS };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function zcodeConfigFor(options = {}, paths) {
|
|
41
|
+
if (typeof options.zcodeConfig === 'string' && options.zcodeConfig.length > 0) return options.zcodeConfig;
|
|
42
|
+
return (paths && paths.zcodeConfig) || path.join((paths && paths.home) || os.homedir(), '.zcode', 'cli', 'config.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Same-path comparison the way the host resolves plugin dirs: absolute,
|
|
46
|
+
// realpath'd when it exists, resolved when it does not.
|
|
47
|
+
function normalizeDir(target) {
|
|
48
|
+
const absolute = path.resolve(target);
|
|
49
|
+
try { return fs.realpathSync(absolute); } catch { return absolute; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameDir(entry, dir) {
|
|
53
|
+
return typeof entry === 'string' && normalizeDir(entry) === normalizeDir(dir);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pluginNameAt(dir) {
|
|
57
|
+
for (const probe of MANIFEST_PROBES) {
|
|
58
|
+
try {
|
|
59
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, probe), 'utf8'));
|
|
60
|
+
return typeof manifest?.name === 'string' ? manifest.name : null;
|
|
61
|
+
} catch { /* probe the next manifest location */ }
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readZcodeConfig(file) {
|
|
67
|
+
const bytes = readOptional(file);
|
|
68
|
+
if (bytes === null) return { doc: {}, bytes: null };
|
|
69
|
+
let doc;
|
|
70
|
+
try { doc = JSON.parse(bytes.toString('utf8')); } catch (error) {
|
|
71
|
+
throw new CliError('ZCODE_CONFIG_INVALID', `ZCode config is not readable JSON and will not be overwritten (${file}: ${error.message})`);
|
|
72
|
+
}
|
|
73
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
74
|
+
throw new CliError('ZCODE_CONFIG_INVALID', `ZCode config must be a JSON object (${file})`);
|
|
75
|
+
}
|
|
76
|
+
return { doc, bytes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract the plugins.dirs array the way the host validates it (zod:
|
|
80
|
+
// array of non-empty strings). Anything else is config this module does
|
|
81
|
+
// not understand; fail closed instead of rewriting it.
|
|
82
|
+
function pluginDirs(doc, file) {
|
|
83
|
+
const plugins = doc.plugins;
|
|
84
|
+
if (plugins === undefined) return [];
|
|
85
|
+
if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins)) {
|
|
86
|
+
throw new CliError('ZCODE_CONFIG_INVALID', `plugins must be a JSON object in ${file}`);
|
|
87
|
+
}
|
|
88
|
+
const dirs = plugins.dirs;
|
|
89
|
+
if (dirs === undefined) return [];
|
|
90
|
+
if (!Array.isArray(dirs) || dirs.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
|
|
91
|
+
throw new CliError('ZCODE_CONFIG_INVALID', `plugins.dirs must be an array of non-empty directory paths in ${file}`);
|
|
92
|
+
}
|
|
93
|
+
return dirs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Read back what this binding actually wrote: the staging entry in the
|
|
97
|
+
// config plus the interpreter command, bridge script, and daemon socket
|
|
98
|
+
// the staging pinned.
|
|
99
|
+
function verifyZcodeBinding({ config, staging, dir }) {
|
|
100
|
+
const doc = JSON.parse(fs.readFileSync(config, 'utf8'));
|
|
101
|
+
if (!pluginDirs(doc, config).some((entry) => sameDir(entry, dir))) {
|
|
102
|
+
throw new CliError('ZCODE_BINDING_UNVERIFIABLE', `the staged plugin directory is absent from plugins.dirs after install (${config})`);
|
|
103
|
+
}
|
|
104
|
+
const server = JSON.parse(fs.readFileSync(path.join(staging, '.mcp.json'), 'utf8')).mcpServers?.external_subagent;
|
|
105
|
+
const binding = zcodeMcpBinding(staging);
|
|
106
|
+
if (!server || server.command !== binding.command || JSON.stringify(server.args || []) !== JSON.stringify(binding.args)
|
|
107
|
+
|| server.timeoutMs !== binding.timeoutMs
|
|
108
|
+
|| server.env?.ZCODE_AGENTD_SOCKET === undefined) {
|
|
109
|
+
throw new CliError('ZCODE_BINDING_UNVERIFIABLE', `the staged plugin MCP binding is not the managed endpoint (${staging})`);
|
|
110
|
+
}
|
|
111
|
+
return server;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function installZcodePlugin(paths, options = {}) {
|
|
115
|
+
const source = options.source || pluginSourceRoot();
|
|
116
|
+
const staging = options.stagingPath || paths.zcodePlugin;
|
|
117
|
+
const config = options.configPath || zcodeConfigFor(options, paths);
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
return { dry_run: true, operation: options.uninstall ? 'uninstall' : 'install', host: 'zcode', source, staging, config, plugin_id: ZCODE_PLUGIN_ID, cli: 'zcode-config' };
|
|
120
|
+
}
|
|
121
|
+
if (options.uninstall) return uninstallZcodePlugin(paths, options);
|
|
122
|
+
pluginManifest(source);
|
|
123
|
+
const prior = readZcodeConfig(config);
|
|
124
|
+
const dirs = pluginDirs(prior.doc, config);
|
|
125
|
+
// Only one external-subagent may be bound through plugins.dirs: a
|
|
126
|
+
// foreign directory exposing the same plugin name would shadow or be
|
|
127
|
+
// shadowed by ours under the shared inline id.
|
|
128
|
+
for (const entry of dirs) {
|
|
129
|
+
if (!sameDir(entry, staging) && pluginNameAt(entry) === PLUGIN_NAME) {
|
|
130
|
+
throw new CliError('ZCODE_PLUGIN_CONFLICT', `another ${PLUGIN_NAME} plugin directory is already registered in plugins.dirs (${entry})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// AUD-002: stage by validated replacement. publish() is self-contained,
|
|
134
|
+
// so unlike the overlay staging it used to be, a failed copy/parse/swap
|
|
135
|
+
// never damages the live managed tree; everything that can fail after the
|
|
136
|
+
// swap stays inside the config transaction below, whose rollback restores
|
|
137
|
+
// both the prior config bytes and the prior coherent tree.
|
|
138
|
+
const staged = preparePluginStage(source, staging, paths, zcodeMcpBinding(staging));
|
|
139
|
+
staged.publish();
|
|
140
|
+
try {
|
|
141
|
+
const next = structuredClone(prior.doc);
|
|
142
|
+
next.plugins ??= {};
|
|
143
|
+
if (!(next.plugins.dirs || []).some((entry) => sameDir(entry, staging))) next.plugins.dirs = [...(next.plugins.dirs || []), path.resolve(staging)];
|
|
144
|
+
atomicWrite(config, jsonBytes(next));
|
|
145
|
+
const server = verifyZcodeBinding({ config, staging, dir: staging });
|
|
146
|
+
const result = {
|
|
147
|
+
installed: true, host: 'zcode', source, staging, config,
|
|
148
|
+
plugin_id: ZCODE_PLUGIN_ID,
|
|
149
|
+
digest: treeDigest(staging),
|
|
150
|
+
config_verified: true,
|
|
151
|
+
binding: { command: server.command, socket: server.env.ZCODE_AGENTD_SOCKET },
|
|
152
|
+
};
|
|
153
|
+
if (prior.doc.plugins?.enabled === false) {
|
|
154
|
+
result.warning = 'plugins.enabled is false in the ZCode config; the binding will not load until plugins are enabled';
|
|
155
|
+
}
|
|
156
|
+
staged.complete();
|
|
157
|
+
return result;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// AUD-002: the config restore and the staging restore are INDEPENDENT
|
|
160
|
+
// guarded attempts. A config parent that stays read-only fails the
|
|
161
|
+
// install write and then the config restore too, but that must neither
|
|
162
|
+
// skip the staging restore nor mask this original error; whatever could
|
|
163
|
+
// not be restored is attached to the thrown error instead of only being
|
|
164
|
+
// logged.
|
|
165
|
+
throw guardedRestores(error, [
|
|
166
|
+
{ resource: config, restore: () => restoreOptional(config, prior.bytes) },
|
|
167
|
+
{ resource: staging, restore: () => {
|
|
168
|
+
if (!staged.restore()) {
|
|
169
|
+
throw new CliError('PLUGIN_STAGING_RESTORE_FAILED', `the prior staging tree could not be restored at ${staging}; the retained prior copy is left beside it as recovery material`);
|
|
170
|
+
}
|
|
171
|
+
} },
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function uninstallZcodePlugin(paths, options = {}) {
|
|
177
|
+
const source = options.source || pluginSourceRoot();
|
|
178
|
+
const staging = options.stagingPath || paths.zcodePlugin;
|
|
179
|
+
const config = options.configPath || zcodeConfigFor(options, paths);
|
|
180
|
+
if (options.dryRun) {
|
|
181
|
+
return { dry_run: true, operation: 'uninstall', host: 'zcode', source, staging, config, plugin_id: ZCODE_PLUGIN_ID, cli: 'zcode-config' };
|
|
182
|
+
}
|
|
183
|
+
const prior = readZcodeConfig(config);
|
|
184
|
+
const dirs = pluginDirs(prior.doc, config);
|
|
185
|
+
const kept = dirs.filter((entry) => !sameDir(entry, staging));
|
|
186
|
+
const bound = kept.length !== dirs.length;
|
|
187
|
+
if (!bound && !fs.existsSync(staging)) {
|
|
188
|
+
return { uninstalled: false, host: 'zcode', config, staging, note: 'no zcode binding present' };
|
|
189
|
+
}
|
|
190
|
+
if (bound) {
|
|
191
|
+
const next = structuredClone(prior.doc);
|
|
192
|
+
if (kept.length === 0) delete next.plugins.dirs; else next.plugins.dirs = kept;
|
|
193
|
+
if (Object.keys(next.plugins).length === 0) delete next.plugins;
|
|
194
|
+
atomicWrite(config, jsonBytes(next));
|
|
195
|
+
const doc = JSON.parse(fs.readFileSync(config, 'utf8'));
|
|
196
|
+
if (pluginDirs(doc, config).some((entry) => sameDir(entry, staging))) {
|
|
197
|
+
restoreOptional(config, prior.bytes);
|
|
198
|
+
throw new CliError('ZCODE_BINDING_UNVERIFIABLE', `the staged plugin directory survived removal from plugins.dirs (${config})`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// The staged tree is only deleted when it is unmistakably ours; a
|
|
202
|
+
// foreign tree at the staging path is left in place and reported.
|
|
203
|
+
let stagingRemoved = false;
|
|
204
|
+
if (fs.existsSync(staging)) {
|
|
205
|
+
if (pluginNameAt(staging) === PLUGIN_NAME) {
|
|
206
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
207
|
+
stagingRemoved = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const result = { uninstalled: true, host: 'zcode', config, staging, staging_removed: stagingRemoved };
|
|
211
|
+
if (fs.existsSync(staging) && !stagingRemoved) result.note = 'staging path is not managed by this product; directory left in place';
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Stateless reconcile for update flows: the binding exists iff the config
|
|
216
|
+
// still points at the staged directory, so detection replaces a registry.
|
|
217
|
+
export function reconcileZcodeBinding(paths, options = {}) {
|
|
218
|
+
// Callers without product-shaped paths (test fixtures, partial states)
|
|
219
|
+
// must not leak into the real user config through the home fallback.
|
|
220
|
+
if (!paths || (!paths.zcodeConfig && !paths.home)) {
|
|
221
|
+
return { bound: false, status: 'absent', note: 'zcode binding paths are unavailable' };
|
|
222
|
+
}
|
|
223
|
+
const config = options.configPath || zcodeConfigFor(options, paths);
|
|
224
|
+
const staging = options.stagingPath || paths.zcodePlugin;
|
|
225
|
+
let bound;
|
|
226
|
+
try {
|
|
227
|
+
const { doc } = readZcodeConfig(config);
|
|
228
|
+
bound = pluginDirs(doc, config).some((entry) => sameDir(entry, staging));
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return { bound: false, status: 'failed', error: { code: error.code || 'ZCODE_CONFIG_INVALID', message: error.message } };
|
|
231
|
+
}
|
|
232
|
+
if (!bound) return { bound: false, status: 'absent' };
|
|
233
|
+
try {
|
|
234
|
+
const install = installZcodePlugin(paths, options);
|
|
235
|
+
return { bound: true, status: 'updated', digest: install.digest };
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return { bound: true, status: 'failed', error: { code: error.code || 'ZCODE_BINDING_FAILED', message: error.message } };
|
|
238
|
+
}
|
|
239
|
+
}
|
package/cli/main.mjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { BUSINESS_COMMANDS, PRODUCT_NAME, VERSION, ZCODE_RUNTIME } from './constants.mjs';
|
|
5
|
+
import { CliError } from './errors.mjs';
|
|
6
|
+
import { installHooks, installPlan, runInit } from './install/init.mjs';
|
|
7
|
+
import { nativeBinary } from './install/layout.mjs';
|
|
8
|
+
import { cleanupLegacy, purge, restoreData, backupData, uninstall } from './commands/maintenance.mjs';
|
|
9
|
+
import { updateCommand } from './commands/update.mjs';
|
|
10
|
+
import { localInstallStatus, startDaemon, stopDaemon } from './commands/daemon.mjs';
|
|
11
|
+
import { serviceRegistrationStatus } from './install/service-macos.mjs';
|
|
12
|
+
import { mcpCommand, pluginCommand } from './commands/plugin.mjs';
|
|
13
|
+
import { platform, productPaths } from './paths.mjs';
|
|
14
|
+
import { callDaemon, parseDaemonInput } from './rpc.mjs';
|
|
15
|
+
import { configCommand } from './commands/config.mjs';
|
|
16
|
+
import { parseConfigArgs } from './commands/config.mjs';
|
|
17
|
+
import { subagentsCommand, parseSubagentsArgs } from './commands/agents.mjs';
|
|
18
|
+
import { parseSpawnArgs, prepareSpawnInput } from './commands/tasks.mjs';
|
|
19
|
+
|
|
20
|
+
const HELP = `external-subagent ${VERSION}\n\nUsage: external-subagent <command> [options]\n\nCommands:\n help, version Show basic product information\n init [--dry-run] [--resume] [--install-hooks] [--skip-service-start]\n Install the standalone daemon service only; bind a\n host afterwards with install-plugin or install-mcp\n hooks install [--dry-run] Install ZCode policy hooks explicitly\n install-plugin [codex|zcode] [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the managed host plugin (MCP + skill);\n the host defaults to codex, --codex-home is codex-only\n install-mcp [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the direct Codex MCP TOML binding\n status, diagnose Inspect local service and runtime state\n start, stop Bootstrap or boot out the daemon LaunchAgent\n backup --output <dir> Back up retained product data\n restore --input <dir> Verify and restore product data\n uninstall Release Codex/ZCode bindings; remove service registration; retain data\n purge --yes Explicitly delete new product data\n cleanup-legacy --yes Delete old unpublished installation (no migration)\n`;
|
|
21
|
+
const DAEMON_HELP = ` config get [key] | config set <key> <value>\n subagents list | subagents status [subagent] | subagents probe/models [subagent]\n create/spawn, wait, list, send, respond, cancel, result, close, observe\n Daemon calls accept --json '<object>' or JSON stdin\n list JSON requires repository (workspace is an alias)\n observe JSON requires only agent_id\n`;
|
|
22
|
+
|
|
23
|
+
function structuredInput(args, parser) {
|
|
24
|
+
return args.length > 0 && !args[0].startsWith('--') ? parser(args) : parseDaemonInput(args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function value(args, name) {
|
|
28
|
+
const index = args.indexOf(name);
|
|
29
|
+
if (index < 0) return undefined;
|
|
30
|
+
const result = args[index + 1];
|
|
31
|
+
if (!result || result.startsWith('--')) throw new CliError('INVALID_ARGUMENT', `${name} requires a value`, 2);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function output(valueToWrite) {
|
|
36
|
+
process.stdout.write(`${JSON.stringify({ ok: true, product: PRODUCT_NAME, ...valueToWrite }, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function summarizeAgentStatus(agent) {
|
|
40
|
+
if (!agent || typeof agent !== 'object') return agent;
|
|
41
|
+
const scopes = {};
|
|
42
|
+
for (const scope of ['local', 'auth', 'hi']) {
|
|
43
|
+
if (agent[scope] && typeof agent[scope] === 'object') {
|
|
44
|
+
scopes[scope] = {
|
|
45
|
+
state: agent[scope].state ?? 'UNKNOWN',
|
|
46
|
+
checked_at_ms: agent[scope].checked_at_ms ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
subagent: agent.subagent ?? agent.agent,
|
|
52
|
+
configured: agent.configured,
|
|
53
|
+
enabled: agent.enabled,
|
|
54
|
+
spawn_supported: agent.spawn_supported,
|
|
55
|
+
...scopes,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function publicDaemonStatus(status, { verbose = false } = {}) {
|
|
60
|
+
if (!status || typeof status !== 'object') return status;
|
|
61
|
+
if (verbose) return status;
|
|
62
|
+
return {
|
|
63
|
+
...(status.mcp_version === undefined ? {} : { mcp_version: status.mcp_version }),
|
|
64
|
+
...(status.components === undefined ? {} : { components: status.components }),
|
|
65
|
+
...(Array.isArray(status.subagents ?? status.agents) ? { subagents: (status.subagents ?? status.agents).map(summarizeAgentStatus) } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DIAGNOSTIC_TAIL_BYTES = 16 * 1024;
|
|
70
|
+
const DIAGNOSTIC_TOTAL_BYTES = 32 * 1024;
|
|
71
|
+
const DIAGNOSTIC_LOG_NAMES = ['daemon.log', 'daemon-error.log'];
|
|
72
|
+
|
|
73
|
+
function fileArtifact(target, source, capturedAtMs = Date.now()) {
|
|
74
|
+
const artifact = { path: target, source, captured_at_ms: capturedAtMs };
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs.lstatSync(target);
|
|
77
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return artifact;
|
|
78
|
+
artifact.sha256 = crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
79
|
+
} catch {}
|
|
80
|
+
return artifact;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function preserveDiagnosticText(text) { return text; }
|
|
84
|
+
|
|
85
|
+
function diagnosticFields(record) {
|
|
86
|
+
return JSON.stringify(Object.fromEntries(
|
|
87
|
+
['agent_id', 'session_id', 'stage', 'error_code', 'message', 'stderr_tail', 'operation', 'remote_code', 'remote_message', 'cleanup_result']
|
|
88
|
+
.filter((field) => typeof record[field] === 'string' || record[field] === null || (field === 'remote_code' && Number.isSafeInteger(record[field])))
|
|
89
|
+
.map((field) => [field, record[field]]),
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Budget the serialized fields, so escaping and UTF-8 cannot invalidate JSON.
|
|
94
|
+
// Metadata has bounded prefixes; stderr receives the remaining budget as a tail.
|
|
95
|
+
function boundedFailureRecord(record) {
|
|
96
|
+
const projected = diagnosticFields(record);
|
|
97
|
+
if (Buffer.byteLength(projected) <= DIAGNOSTIC_TAIL_BYTES) return { text: projected, truncated: false };
|
|
98
|
+
const fields = JSON.parse(projected);
|
|
99
|
+
for (const key of Object.keys(fields)) {
|
|
100
|
+
if (key !== 'stderr_tail' && typeof fields[key] === 'string') {
|
|
101
|
+
fields[key] = Array.from(fields[key]).slice(0, key === 'message' ? 512 : 256).join('');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const tail = Array.from(fields.stderr_tail || '');
|
|
105
|
+
let low = 0;
|
|
106
|
+
let high = tail.length;
|
|
107
|
+
while (low < high) {
|
|
108
|
+
const keep = Math.ceil((low + high) / 2);
|
|
109
|
+
fields.stderr_tail = tail.slice(tail.length - keep).join('');
|
|
110
|
+
if (Buffer.byteLength(JSON.stringify(fields)) <= DIAGNOSTIC_TAIL_BYTES) low = keep;
|
|
111
|
+
else high = keep - 1;
|
|
112
|
+
}
|
|
113
|
+
fields.stderr_tail = tail.slice(tail.length - low).join('');
|
|
114
|
+
return { text: JSON.stringify(fields), truncated: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function diagnosticTail(text) {
|
|
118
|
+
let incomplete = false;
|
|
119
|
+
const decoded = text.split('\n').map((line) => {
|
|
120
|
+
const match = line.match(/^(\[zcode-agentd\] failure agent=[^:\r\n]+: )(.+)$/u);
|
|
121
|
+
if (match && match[2].startsWith('{')) {
|
|
122
|
+
try {
|
|
123
|
+
const record = JSON.parse(match[2]);
|
|
124
|
+
if (record && typeof record.agent_id === 'string') return preserveDiagnosticText(match[1]) + diagnosticFields(record);
|
|
125
|
+
} catch {
|
|
126
|
+
incomplete = true;
|
|
127
|
+
return match[1] + '[INCOMPLETE_FAILURE_RECORD]';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return line;
|
|
131
|
+
}).join('\n');
|
|
132
|
+
return { text: preserveDiagnosticText(decoded), incomplete };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function diagnosticLogs(logDirectory) {
|
|
136
|
+
const incomplete = [];
|
|
137
|
+
if (!fs.existsSync(logDirectory)) return { directory: logDirectory, complete: false, incomplete: ['log_directory_missing'], files: [] };
|
|
138
|
+
const files = [];
|
|
139
|
+
let totalBytes = 0;
|
|
140
|
+
for (const name of DIAGNOSTIC_LOG_NAMES) {
|
|
141
|
+
const target = path.join(logDirectory, name);
|
|
142
|
+
let targetStat;
|
|
143
|
+
try { targetStat = fs.lstatSync(target); } catch (error) { targetStat = null; }
|
|
144
|
+
const rotated = ['.1', '.2', '.old', '.gz'].some((suffix) => {
|
|
145
|
+
try { return fs.lstatSync(`${target}${suffix}`) != null; } catch { return false; }
|
|
146
|
+
});
|
|
147
|
+
if (rotated) incomplete.push(`log_rotated:${name}`);
|
|
148
|
+
if (!fs.existsSync(target)) continue;
|
|
149
|
+
if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
|
|
150
|
+
incomplete.push(`log_unreadable:${name}:symlink_or_non_file`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const stat = fs.statSync(target);
|
|
155
|
+
const remaining = Math.max(0, DIAGNOSTIC_TOTAL_BYTES - totalBytes);
|
|
156
|
+
const take = Math.min(DIAGNOSTIC_TAIL_BYTES, remaining);
|
|
157
|
+
const start = Math.max(0, stat.size - take);
|
|
158
|
+
// A complete producer record can exceed the display window. Keep a
|
|
159
|
+
// bounded record-sized lookbehind so its prefix survives until decoding.
|
|
160
|
+
const readStart = Math.max(0, start - DIAGNOSTIC_RECORD_BYTES);
|
|
161
|
+
const readLength = Math.min(take + (start - readStart), stat.size - readStart);
|
|
162
|
+
const fd = fs.openSync(target, 'r');
|
|
163
|
+
const buffer = Buffer.alloc(readLength);
|
|
164
|
+
const read = fs.readSync(fd, buffer, 0, readLength, readStart);
|
|
165
|
+
fs.closeSync(fd);
|
|
166
|
+
let decodeStart = Math.max(0, start - 256) - readStart;
|
|
167
|
+
if (decodeStart > 0) {
|
|
168
|
+
const lineStart = buffer.lastIndexOf(0x0a, decodeStart - 1) + 1;
|
|
169
|
+
if (buffer.subarray(lineStart, read).toString('utf8').startsWith('[zcode-agentd] failure agent=')) decodeStart = lineStart;
|
|
170
|
+
}
|
|
171
|
+
// Legacy text keeps its original lookbehind; known records are decoded
|
|
172
|
+
// (or marked incomplete) before any display clipping can hide the prefix.
|
|
173
|
+
const projected = diagnosticTail(buffer.subarray(decodeStart, read).toString('utf8'));
|
|
174
|
+
if (projected.incomplete) incomplete.push(`record_incomplete:${name}`);
|
|
175
|
+
const encoded = Buffer.from(projected.text, 'utf8');
|
|
176
|
+
let tailStart = Math.max(0, encoded.length - Math.min(take, remaining));
|
|
177
|
+
while (tailStart < encoded.length && (encoded[tailStart] & 0xc0) === 0x80) tailStart += 1;
|
|
178
|
+
const bounded = encoded.subarray(tailStart);
|
|
179
|
+
const tail = bounded.toString('utf8');
|
|
180
|
+
totalBytes += Buffer.byteLength(tail);
|
|
181
|
+
if (start > 0 || take < stat.size || bounded.length < encoded.length) incomplete.push(`log_truncated:${name}`);
|
|
182
|
+
files.push({ name, read_status: 'read', bytes: stat.size, modified_at_ms: stat.mtimeMs, rotated, truncated: start > 0 || take < stat.size || bounded.length < encoded.length, tail });
|
|
183
|
+
} catch (error) { incomplete.push(`log_unreadable:${name}:${error.code || 'error'}`); }
|
|
184
|
+
}
|
|
185
|
+
if (files.length === 0) incomplete.push('log_files_missing');
|
|
186
|
+
return { directory: logDirectory, complete: incomplete.length === 0, incomplete, total_bytes: totalBytes, files };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The writer retains the current file and two 1 MiB rotations. Search that
|
|
190
|
+
// finite window, independently of the much smaller global display tails.
|
|
191
|
+
const DIAGNOSTIC_RETAINED_BYTES = 1024 * 1024;
|
|
192
|
+
const DIAGNOSTIC_RECORD_BYTES = 192 * 1024;
|
|
193
|
+
function agentDiagnosticLogs(logDirectory, agentId) {
|
|
194
|
+
const report = { status: 'target_record_missing', scope: 'retained_logs', scan_complete: true, scanned_bytes: 0, record: null, incomplete: [] };
|
|
195
|
+
for (const name of ['daemon-error.log', 'daemon-error.log.1', 'daemon-error.log.2']) {
|
|
196
|
+
let fd;
|
|
197
|
+
try {
|
|
198
|
+
const target = path.join(logDirectory, name);
|
|
199
|
+
const stat = fs.lstatSync(target);
|
|
200
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Object.assign(new Error('not a regular file'), { code: 'NON_FILE' });
|
|
201
|
+
fd = fs.openSync(target, 'r');
|
|
202
|
+
const size = fs.fstatSync(fd).size;
|
|
203
|
+
const start = Math.max(0, size - DIAGNOSTIC_RETAINED_BYTES);
|
|
204
|
+
const bytes = Buffer.alloc(Math.min(size, DIAGNOSTIC_RETAINED_BYTES));
|
|
205
|
+
const read = fs.readSync(fd, bytes, 0, bytes.length, start);
|
|
206
|
+
report.scanned_bytes += read;
|
|
207
|
+
if (start > 0 || read < bytes.length) report.incomplete.push(`scan_truncated:${name}`);
|
|
208
|
+
const text = bytes.subarray(0, read).toString('utf8');
|
|
209
|
+
const lines = text.split('\n');
|
|
210
|
+
if (start > 0) lines.shift(); // Never associate a partial first record.
|
|
211
|
+
if (lines.pop()) report.incomplete.push(`record_incomplete:${name}`); // The writer may still be appending.
|
|
212
|
+
for (const line of lines.reverse()) {
|
|
213
|
+
if (Buffer.byteLength(line) > DIAGNOSTIC_RECORD_BYTES) { report.incomplete.push(`record_truncated:${name}`); continue; }
|
|
214
|
+
const prefix = `[zcode-agentd] failure agent=${agentId}: `;
|
|
215
|
+
if (!line.startsWith(prefix)) continue;
|
|
216
|
+
const raw = line.slice(prefix.length);
|
|
217
|
+
// JSON records repeat the identifier; reject misleading prefix matches.
|
|
218
|
+
let structured;
|
|
219
|
+
try { structured = JSON.parse(raw); } catch { structured = null; }
|
|
220
|
+
if (structured && structured.agent_id !== agentId) continue;
|
|
221
|
+
if (structured) {
|
|
222
|
+
report.record = { file: name, ...boundedFailureRecord(structured) };
|
|
223
|
+
} else {
|
|
224
|
+
const encoded = Buffer.from(preserveDiagnosticText(raw));
|
|
225
|
+
let start = Math.max(0, encoded.length - DIAGNOSTIC_TAIL_BYTES);
|
|
226
|
+
while (start < encoded.length && (encoded[start] & 0xc0) === 0x80) start += 1;
|
|
227
|
+
report.record = { file: name, text: encoded.subarray(start).toString('utf8'), truncated: start > 0 };
|
|
228
|
+
}
|
|
229
|
+
report.status = 'found';
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error.code !== 'ENOENT') report.incomplete.push(`scan_unreadable:${name}:${error.code || 'error'}`);
|
|
234
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
235
|
+
if (report.record) break;
|
|
236
|
+
}
|
|
237
|
+
report.scan_complete = report.incomplete.length === 0;
|
|
238
|
+
return report;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function diagnoseInput(args) {
|
|
242
|
+
const rawAgent = value(args, '--agent');
|
|
243
|
+
const agent = rawAgent === undefined ? undefined : Number(rawAgent);
|
|
244
|
+
if (rawAgent !== undefined && (!Number.isInteger(agent) || agent < 10_000_000 || agent > 99_999_999 || String(agent) !== rawAgent)) {
|
|
245
|
+
throw new CliError('INVALID_ARGUMENT', 'agent_id must be an integer between 10000000 and 99999999', 2);
|
|
246
|
+
}
|
|
247
|
+
const outputDirectory = value(args, '--output');
|
|
248
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
249
|
+
const arg = args[index];
|
|
250
|
+
if (!arg.startsWith('--')) throw new CliError('INVALID_ARGUMENT', `unexpected diagnose argument: ${arg}`, 2);
|
|
251
|
+
if (!['--agent', '--output'].includes(arg)) throw new CliError('INVALID_ARGUMENT', `unsupported diagnose option: ${arg}`, 2);
|
|
252
|
+
index += 1;
|
|
253
|
+
}
|
|
254
|
+
return { agent, outputDirectory };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function diagnose(paths, args) {
|
|
258
|
+
const { agent, outputDirectory } = diagnoseInput(args);
|
|
259
|
+
const socket = process.env.ZCODE_AGENTD_SOCKET || paths.socket;
|
|
260
|
+
const report = {
|
|
261
|
+
schema_version: 1,
|
|
262
|
+
scope: agent ? { agent_id: agent } : { kind: 'global' },
|
|
263
|
+
platform: platform(),
|
|
264
|
+
runtime: {
|
|
265
|
+
configured_artifact: fileArtifact(ZCODE_RUNTIME, 'cli_packaged_configuration'),
|
|
266
|
+
running_identity: null,
|
|
267
|
+
running_identity_source: 'not_observed_by_cli',
|
|
268
|
+
},
|
|
269
|
+
facade: {
|
|
270
|
+
running_identity: null,
|
|
271
|
+
running_identity_source: 'not_observed_by_cli',
|
|
272
|
+
packaged_artifact: fileArtifact(nativeBinary('external-subagent-mcp'), 'distributed_payload'),
|
|
273
|
+
},
|
|
274
|
+
daemon: { socket, socket_exists: fs.existsSync(socket), query_status: 'unqueried', available: null },
|
|
275
|
+
logs: diagnosticLogs(paths.logs),
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
report.daemon.status = await callDaemon(socket, 'status', {});
|
|
279
|
+
report.daemon.available = true;
|
|
280
|
+
report.daemon.query_status = 'queried';
|
|
281
|
+
} catch (error) {
|
|
282
|
+
report.daemon.available = Boolean(error.daemonResponded);
|
|
283
|
+
report.daemon.query_status = error.daemonResponded ? 'query_failed' : 'unavailable';
|
|
284
|
+
report.daemon.error = { code: error.code || 'DAEMON_ERROR', message: error.message };
|
|
285
|
+
}
|
|
286
|
+
if (agent) {
|
|
287
|
+
const diagnostics = agentDiagnosticLogs(paths.logs, String(agent));
|
|
288
|
+
try {
|
|
289
|
+
const snapshot = await callDaemon(socket, 'wait', { agent_id: agent, wait_time: 0 });
|
|
290
|
+
report.daemon.available = true;
|
|
291
|
+
report.agent = {
|
|
292
|
+
diagnostics,
|
|
293
|
+
task: snapshot.task,
|
|
294
|
+
activity: snapshot.activity,
|
|
295
|
+
session_id: snapshot.task?.session_id ?? null,
|
|
296
|
+
turn_id: snapshot.task?.turn_id ?? null,
|
|
297
|
+
request_ids: Array.isArray(snapshot.pending_requests) ? snapshot.pending_requests.map((request) => request.request_id).filter(Boolean) : [],
|
|
298
|
+
query_request_id: snapshot.__request_id ?? null,
|
|
299
|
+
identifiers_complete: Boolean(snapshot.task?.turn_id || (Array.isArray(snapshot.pending_requests) && snapshot.pending_requests.some((request) => request.request_id))),
|
|
300
|
+
pending_request_count: Array.isArray(snapshot.pending_requests) ? snapshot.pending_requests.length : null,
|
|
301
|
+
result_available: snapshot.result_available ?? false,
|
|
302
|
+
observed_at_ms: Date.now(),
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
report.daemon.error = { code: error.code || 'DAEMON_ERROR', message: error.message };
|
|
306
|
+
const missing = error.code === 'not_found';
|
|
307
|
+
report.logs.incomplete.push(missing ? 'agent_missing' : 'agent_store_unavailable');
|
|
308
|
+
report.logs.complete = false;
|
|
309
|
+
report.agent = { agent_id: agent, query_status: missing ? 'missing' : 'unavailable', unavailable: !missing, missing, diagnostics };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (outputDirectory) {
|
|
313
|
+
const destination = path.resolve(outputDirectory);
|
|
314
|
+
const target = path.join(destination, 'diagnose.json');
|
|
315
|
+
report.output = { path: target, complete: report.logs.complete && report.daemon.query_status === 'queried' && !report.agent?.unavailable && !report.agent?.missing };
|
|
316
|
+
try {
|
|
317
|
+
fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
318
|
+
fs.writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
|
319
|
+
} catch (error) {
|
|
320
|
+
report.output = { path: target, complete: false, error: { code: error.code || 'OUTPUT_WRITE_FAILED', message: error.message } };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return report;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function main(args) {
|
|
327
|
+
const command = args[0] || 'help';
|
|
328
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
329
|
+
process.stdout.write(HELP.replace('status, diagnose Inspect local service and runtime state', 'status [--verbose] Inspect essential service and runtime state\n diagnose Export bounded diagnostic details') + DAEMON_HELP); return;
|
|
330
|
+
}
|
|
331
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
332
|
+
process.stdout.write(`${VERSION}\n`); return;
|
|
333
|
+
}
|
|
334
|
+
if (!BUSINESS_COMMANDS.has(command)) throw new CliError('UNKNOWN_COMMAND', `unknown command: ${command}`, 2);
|
|
335
|
+
if (platform() !== 'darwin') throw new CliError('UNSUPPORTED_PLATFORM', `${command} is supported only on macOS`);
|
|
336
|
+
|
|
337
|
+
const paths = productPaths();
|
|
338
|
+
if (command === 'init') {
|
|
339
|
+
// AUD-005/D1: init installs the standalone service only. The retired
|
|
340
|
+
// --skip-runtime-probe/--skip-codex-plugin/--codex-home flags are rejected
|
|
341
|
+
// (never silently ignored): there is no runtime probe to skip, init binds
|
|
342
|
+
// no Codex host, and a host home is chosen by the explicit install
|
|
343
|
+
// commands instead.
|
|
344
|
+
const flags = args.slice(1);
|
|
345
|
+
const known = ['--dry-run', '--resume', '--install-hooks', '--skip-service-start'];
|
|
346
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
347
|
+
if (!known.includes(flags[index])) {
|
|
348
|
+
if (flags[index] === '--skip-runtime-probe' || flags[index] === '--skip-codex-plugin' || flags[index] === '--codex-home') {
|
|
349
|
+
throw new CliError('INVALID_ARGUMENT', `${flags[index]} is no longer supported: init installs the standalone service only and binds no host (use install-plugin or install-mcp)`, 2);
|
|
350
|
+
}
|
|
351
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported init option: ${flags[index]}`, 2);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
output(runInit({
|
|
355
|
+
paths,
|
|
356
|
+
dryRun: flags.includes('--dry-run'),
|
|
357
|
+
resume: flags.includes('--resume'),
|
|
358
|
+
installHooks: flags.includes('--install-hooks'),
|
|
359
|
+
skipServiceStart: flags.includes('--skip-service-start'),
|
|
360
|
+
}));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (command === 'hooks') {
|
|
364
|
+
if (args[1] !== 'install') throw new CliError('INVALID_ARGUMENT', 'usage: hooks install [--dry-run]', 2);
|
|
365
|
+
output(installHooks(paths, { dryRun: args.includes('--dry-run') })); return;
|
|
366
|
+
}
|
|
367
|
+
if (command === 'install-plugin') {
|
|
368
|
+
output(pluginCommand(paths, args.slice(1))); return;
|
|
369
|
+
}
|
|
370
|
+
if (command === 'install-mcp') {
|
|
371
|
+
output(mcpCommand(paths, args.slice(1))); return;
|
|
372
|
+
}
|
|
373
|
+
if (command === 'status') {
|
|
374
|
+
const verbose = args.includes('--verbose');
|
|
375
|
+
const local = localInstallStatus(paths, { verbose });
|
|
376
|
+
// `service` is the read-only launchd view (registered job + process);
|
|
377
|
+
// `daemon_status` stays the RPC view, so a loaded-but-unready or
|
|
378
|
+
// ready-but-unregistered install reads differently instead of blurring.
|
|
379
|
+
const serviceRaw = serviceRegistrationStatus();
|
|
380
|
+
// PID is an implementation detail and may be reused by another process;
|
|
381
|
+
// keep it out of the ordinary status projection.
|
|
382
|
+
const { pid: _pid, ...service } = serviceRaw;
|
|
383
|
+
try {
|
|
384
|
+
output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.ZCODE_AGENTD_SOCKET || paths.socket, 'status', {}), { verbose }) });
|
|
385
|
+
} catch (error) {
|
|
386
|
+
output({ ...local, service, daemon_status: null, daemon_error: { code: error.code || 'DAEMON_ERROR', message: error.message } });
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (command === 'config' || command === 'subagents' || command === 'agents') {
|
|
391
|
+
const input = structuredInput(args.slice(1), command === 'config' ? parseConfigArgs : parseSubagentsArgs);
|
|
392
|
+
output(command === 'config'
|
|
393
|
+
? configCommand(paths, input)
|
|
394
|
+
: await subagentsCommand(paths, input, { callDaemon, socket: process.env.ZCODE_AGENTD_SOCKET || paths.socket }));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (command === 'diagnose') {
|
|
398
|
+
const report = await diagnose(paths, args.slice(1));
|
|
399
|
+
output({
|
|
400
|
+
...report,
|
|
401
|
+
daemon_packaged_artifact: fileArtifact(nativeBinary('external-subagentd'), 'distributed_payload'),
|
|
402
|
+
}); return;
|
|
403
|
+
}
|
|
404
|
+
if (command === 'backup') { output(backupData(value(args, '--output'), paths)); return; }
|
|
405
|
+
if (command === 'restore') { output(restoreData(value(args, '--input'), paths)); return; }
|
|
406
|
+
if (command === 'start') { output(startDaemon(paths)); return; }
|
|
407
|
+
if (command === 'stop') { output(stopDaemon(paths)); return; }
|
|
408
|
+
if (command === 'update' || command === 'reconcile') { output(await updateCommand(paths, command === 'reconcile' ? ['reconcile', ...args.slice(1)] : args.slice(1), { socket: process.env.ZCODE_AGENTD_SOCKET || paths.socket })); return; }
|
|
409
|
+
if (command === 'uninstall') { output(uninstall(paths)); return; }
|
|
410
|
+
if (command === 'purge') {
|
|
411
|
+
if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'purge requires --yes');
|
|
412
|
+
output(purge(paths)); return;
|
|
413
|
+
}
|
|
414
|
+
if (command === 'cleanup-legacy') {
|
|
415
|
+
if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'cleanup-legacy requires --yes');
|
|
416
|
+
output(cleanupLegacy(paths.home)); return;
|
|
417
|
+
}
|
|
418
|
+
let input;
|
|
419
|
+
if (command === 'create' || command === 'spawn') {
|
|
420
|
+
const spawnArgs = args.slice(1);
|
|
421
|
+
input = spawnArgs.length === 0 || spawnArgs.some((arg) => arg === '--json' || arg.startsWith('--json='))
|
|
422
|
+
? prepareSpawnInput(parseDaemonInput(spawnArgs))
|
|
423
|
+
: parseSpawnArgs(spawnArgs);
|
|
424
|
+
} else input = parseDaemonInput(args.slice(1));
|
|
425
|
+
const result = await callDaemon(process.env.ZCODE_AGENTD_SOCKET || paths.socket, command, input);
|
|
426
|
+
output({ command, result });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export { DAEMON_HELP, HELP, installPlan, diagnose, diagnosticLogs, fileArtifact, publicDaemonStatus, summarizeAgentStatus };
|