@0xgeorgemathew/zcode 0.3.2 → 0.4.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/README.md +179 -130
- package/bin/zcode.js +181 -140
- package/lib/appserver.js +190 -135
- package/lib/bootstrap.js +39 -60
- package/lib/checks.js +103 -0
- package/lib/commands/doctor.js +43 -64
- package/lib/commands/exec.js +327 -0
- package/lib/commands/login.js +2 -9
- package/lib/commands/mcp-list.js +6 -14
- package/lib/commands/sessions.js +181 -0
- package/lib/contract.js +301 -0
- package/lib/executor.js +617 -0
- package/lib/isolation.js +233 -223
- package/lib/key.js +1 -10
- package/lib/store.js +276 -0
- package/lib/ui.js +200 -71
- package/lib/workspace.js +336 -0
- package/package.json +9 -5
- package/skills/README.md +32 -0
- package/skills/claude-code/zcode/SKILL.md +112 -0
- package/skills/codex/zcode/SKILL.md +114 -0
- package/skills/codex/zcode/agents/openai.yaml +4 -0
- package/lib/commands/agent.js +0 -203
- package/lib/commands/run.js +0 -126
- package/lib/overlay.js +0 -138
package/lib/commands/doctor.js
CHANGED
|
@@ -1,62 +1,38 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// lib/commands/doctor.js — engine + isolation + key report
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} = require('../engine.js');
|
|
12
|
-
const { resolveKey, NoKeyError, rootDir } = require('../key.js');
|
|
13
|
-
const { ensureIsolation, engineEnv } = require('../isolation.js');
|
|
14
|
-
const { effectiveMcpServers } = require('../overlay.js');
|
|
2
|
+
// lib/commands/doctor.js — engine + isolation + key report. Builds a probe
|
|
3
|
+
// engine HOME (<root>/doctor/home) exactly the way a session does, so what
|
|
4
|
+
// doctor reports is what a session gets. Never prints the key. Warns when
|
|
5
|
+
// the spawning node major < 26 (R11).
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { sha256File, engineVersion } = require('../engine.js');
|
|
8
|
+
const { resolveRuntime, ConfigError } = require('../bootstrap.js');
|
|
9
|
+
const { buildEngineHome, engineEnv, rootDir, liveDir, effectiveMcpServers } = require('../isolation.js');
|
|
10
|
+
const store = require('../store.js');
|
|
15
11
|
|
|
16
12
|
module.exports = async function doctor(parsed) {
|
|
17
13
|
const flags = parsed.flags;
|
|
18
|
-
let
|
|
14
|
+
let runtime;
|
|
19
15
|
try {
|
|
20
|
-
|
|
16
|
+
runtime = resolveRuntime(process.env);
|
|
21
17
|
} catch (err) {
|
|
22
|
-
if (err instanceof
|
|
18
|
+
if (err instanceof ConfigError) {
|
|
23
19
|
process.stderr.write(`zcode: ${err.message}\n`);
|
|
24
|
-
process.exit(
|
|
20
|
+
process.exit(err.exitCode);
|
|
25
21
|
}
|
|
26
22
|
throw err;
|
|
27
23
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
process.exit(3);
|
|
36
|
-
}
|
|
37
|
-
throw err;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let builtinConfig;
|
|
41
|
-
try {
|
|
42
|
-
builtinConfig = resolveBuiltinProviderConfig(enginePath, process.env);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
if (err instanceof BuiltinConfigNotFoundError) {
|
|
45
|
-
process.stderr.write(`zcode: ${err.message}\n`);
|
|
46
|
-
process.exit(4);
|
|
47
|
-
}
|
|
48
|
-
throw err;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const isolation = ensureIsolation(key, { fresh: flags.fresh, env: process.env });
|
|
52
|
-
const nodeBin = process.env.ZAI_CODE_NODE || process.execPath;
|
|
53
|
-
const childEnv = engineEnv({
|
|
54
|
-
home: isolation.home,
|
|
55
|
-
builtinProviderConfig: builtinConfig,
|
|
24
|
+
const { enginePath, builtinConfig, nodeBin } = runtime;
|
|
25
|
+
const root = rootDir(process.env);
|
|
26
|
+
const built = buildEngineHome({
|
|
27
|
+
home: path.join(root, 'doctor', 'home'),
|
|
28
|
+
root,
|
|
29
|
+
key: runtime.key,
|
|
30
|
+
live: liveDir(process.env),
|
|
56
31
|
});
|
|
57
|
-
const version = engineVersion(nodeBin, enginePath,
|
|
32
|
+
const version = engineVersion(nodeBin, enginePath, engineEnv({ home: built.home, builtinProviderConfig: builtinConfig }));
|
|
58
33
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
59
34
|
const nodeWarning = nodeMajor < 26;
|
|
35
|
+
const sessions = store.listSessions(process.env);
|
|
60
36
|
|
|
61
37
|
const report = {
|
|
62
38
|
ok: true,
|
|
@@ -64,10 +40,9 @@ module.exports = async function doctor(parsed) {
|
|
|
64
40
|
version: process.versions.node,
|
|
65
41
|
major: nodeMajor,
|
|
66
42
|
engineBin: nodeBin,
|
|
67
|
-
recommendation:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: 'ok',
|
|
43
|
+
recommendation: nodeWarning
|
|
44
|
+
? 'engine has only been tested under node >= 26; set ZAI_CODE_NODE to a newer interpreter'
|
|
45
|
+
: 'ok',
|
|
71
46
|
warning: nodeWarning,
|
|
72
47
|
},
|
|
73
48
|
engine: {
|
|
@@ -77,20 +52,23 @@ module.exports = async function doctor(parsed) {
|
|
|
77
52
|
builtinProviderConfig: builtinConfig,
|
|
78
53
|
},
|
|
79
54
|
isolation: {
|
|
80
|
-
root
|
|
81
|
-
home:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: 0) || 0,
|
|
88
|
-
mcpServersInherited: Object.keys(effectiveMcpServers({ inherit: true })).length,
|
|
55
|
+
root,
|
|
56
|
+
home: built.home,
|
|
57
|
+
sessionsDir: store.sessionsDir(process.env),
|
|
58
|
+
mode: built.mode,
|
|
59
|
+
builtNow: true,
|
|
60
|
+
skillsCopied: built.skillsCopied,
|
|
61
|
+
mcpServersInherited: Object.keys(effectiveMcpServers({ inherit: true, live: liveDir(process.env) })).length,
|
|
89
62
|
experimental:
|
|
90
|
-
|
|
63
|
+
built.mode === 'synthesized'
|
|
91
64
|
? 'synthesized config path is unproven; copy a real ~/.zcode/v2 or verify once on a clean HOME'
|
|
92
65
|
: null,
|
|
93
66
|
},
|
|
67
|
+
sessions: {
|
|
68
|
+
total: sessions.length,
|
|
69
|
+
running: sessions.filter((s) => s.live).length,
|
|
70
|
+
interrupted: sessions.filter((s) => s.state === 'interrupted').length,
|
|
71
|
+
},
|
|
94
72
|
key: { configured: true },
|
|
95
73
|
};
|
|
96
74
|
|
|
@@ -99,15 +77,16 @@ module.exports = async function doctor(parsed) {
|
|
|
99
77
|
} else {
|
|
100
78
|
process.stdout.write(
|
|
101
79
|
[
|
|
102
|
-
|
|
80
|
+
'zcode doctor',
|
|
103
81
|
` node ${report.node.version} (${nodeBin})${nodeWarning ? ' ⚠ ' + report.node.recommendation : ''}`,
|
|
104
82
|
` engine ${enginePath}`,
|
|
105
83
|
` sha256 ${report.engine.sha256}`,
|
|
106
84
|
` version ${version ?? 'unknown'}`,
|
|
107
85
|
` builtin provider config ${builtinConfig}`,
|
|
108
|
-
` isolation
|
|
109
|
-
` skills inherited ${
|
|
110
|
-
`
|
|
86
|
+
` isolation root ${root} (one engine HOME per session; config mode: ${built.mode})`,
|
|
87
|
+
` skills inherited ${built.skillsCopied}, mcp servers available with --mcp-inherit ${report.isolation.mcpServersInherited}`,
|
|
88
|
+
` sessions ${report.sessions.total} (${report.sessions.running} running, ${report.sessions.interrupted} interrupted)`,
|
|
89
|
+
' api key configured (never printed)',
|
|
111
90
|
report.isolation.experimental ? ` ⚠ ${report.isolation.experimental}` : '',
|
|
112
91
|
]
|
|
113
92
|
.filter(Boolean)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// lib/commands/exec.js — run | task | agent | resume | __exec (detached
|
|
3
|
+
// worker). Builds the invocation from argv, runs it (in-process, or in a
|
|
4
|
+
// detached worker with --detach), prints the result, exits with the
|
|
5
|
+
// status's exit code.
|
|
6
|
+
//
|
|
7
|
+
// Output contract:
|
|
8
|
+
// --json stdout = NDJSON events + exactly one final {"type":"result"}
|
|
9
|
+
// line (--quiet: only that line). stderr = diagnostics only.
|
|
10
|
+
// plain stdout = the answer (run/agent) or the handback summary (task);
|
|
11
|
+
// stderr = the live feed (header, phases, tools, checks).
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { spawn } = require('node:child_process');
|
|
15
|
+
const store = require('../store.js');
|
|
16
|
+
const { execute, prepareSession, exitCodeFor, ExecError, readSettings } = require('../executor.js');
|
|
17
|
+
const { ConfigError } = require('../bootstrap.js');
|
|
18
|
+
const { normalizeContract, ContractError } = require('../contract.js');
|
|
19
|
+
const { workspaceRoot, normalizeOwns, OwnershipError } = require('../workspace.js');
|
|
20
|
+
const { makeReporter, formatResult, useColor } = require('../ui.js');
|
|
21
|
+
const { normalizeModel, normalizeEffort } = require('../models.js');
|
|
22
|
+
|
|
23
|
+
const BIN = path.join(__dirname, '..', '..', 'bin', 'zcode.js');
|
|
24
|
+
const DEFAULT_TIMEOUT = { run: 600000, agent: 600000, task: 1800000 };
|
|
25
|
+
|
|
26
|
+
function usage(message) {
|
|
27
|
+
process.stderr.write(`zcode: ${message}\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readStdin() {
|
|
32
|
+
try {
|
|
33
|
+
return fs.readFileSync(0, 'utf8');
|
|
34
|
+
} catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --task <file|->: .json (or text starting with "{") is a contract object,
|
|
40
|
+
// anything else is a markdown brief that becomes the objective.
|
|
41
|
+
function loadTaskBase(spec) {
|
|
42
|
+
if (spec === undefined) return {};
|
|
43
|
+
const text = spec === '-' ? readStdin() : (() => {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(spec, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
return usage(`--task file "${spec}" could not be read.`);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
const trimmed = text.trim();
|
|
51
|
+
if (spec.endsWith('.json') || trimmed.startsWith('{')) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(trimmed);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return usage(`--task JSON is invalid: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { objective: trimmed };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildInvocation(parsed) {
|
|
62
|
+
const { command, flags, prompts } = parsed;
|
|
63
|
+
const kind = command;
|
|
64
|
+
const inv = {
|
|
65
|
+
kind,
|
|
66
|
+
id: flags.id || store.newId(),
|
|
67
|
+
label: flags.label || null,
|
|
68
|
+
cwd: path.resolve(flags.cwd),
|
|
69
|
+
model: flags.model,
|
|
70
|
+
effort: flags.effort,
|
|
71
|
+
timeoutMs: flags.timeoutMs ?? DEFAULT_TIMEOUT[kind],
|
|
72
|
+
maxRepairs: flags.maxRepairs ?? 2,
|
|
73
|
+
checkTimeoutMs: flags.checkTimeoutMs ?? 300000,
|
|
74
|
+
checks: [],
|
|
75
|
+
owns: [],
|
|
76
|
+
ownsDisplay: flags.owns,
|
|
77
|
+
acceptanceDeclared: false,
|
|
78
|
+
mcpSpecs: flags.mcpSpecs,
|
|
79
|
+
mcpInherit: flags.mcpInherit,
|
|
80
|
+
skillsDirs: flags.skillsDirs.map((d) => path.resolve(d)),
|
|
81
|
+
noSkills: flags.noSkills,
|
|
82
|
+
profile: flags.profile,
|
|
83
|
+
includeReasoning: flags.includeReasoning,
|
|
84
|
+
allowOverlap: flags.allowOverlap,
|
|
85
|
+
stream: kind !== 'task' || flags.stream,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (kind === 'task') {
|
|
89
|
+
const objective = prompts.join(' ');
|
|
90
|
+
let base = loadTaskBase(flags.task);
|
|
91
|
+
if (!objective.trim() && flags.task === undefined && !process.stdin.isTTY) {
|
|
92
|
+
base = { objective: readStdin().trim() };
|
|
93
|
+
}
|
|
94
|
+
let contract;
|
|
95
|
+
try {
|
|
96
|
+
contract = normalizeContract(base, {
|
|
97
|
+
objective,
|
|
98
|
+
acceptance: flags.accept,
|
|
99
|
+
checks: flags.check,
|
|
100
|
+
files: flags.file,
|
|
101
|
+
context: flags.context,
|
|
102
|
+
owns: flags.owns,
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof ContractError) usage(err.message);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
// Contract-level model/effort/limits apply unless a flag overrides them.
|
|
109
|
+
if (contract.model && !parsed.given.has('model')) {
|
|
110
|
+
inv.model = normalizeModel(contract.model) || usage(`contract model "${contract.model}" is not supported.`);
|
|
111
|
+
}
|
|
112
|
+
if (contract.effort && !parsed.given.has('effort')) {
|
|
113
|
+
inv.effort = normalizeEffort(contract.effort) || usage(`contract effort "${contract.effort}" is not valid.`);
|
|
114
|
+
}
|
|
115
|
+
const lim = contract.limits;
|
|
116
|
+
if (lim.timeoutMs && !parsed.given.has('timeoutMs')) inv.timeoutMs = lim.timeoutMs;
|
|
117
|
+
if (lim.maxRepairs !== undefined && !parsed.given.has('maxRepairs')) inv.maxRepairs = lim.maxRepairs;
|
|
118
|
+
if (lim.checkTimeoutMs && !parsed.given.has('checkTimeoutMs')) inv.checkTimeoutMs = lim.checkTimeoutMs;
|
|
119
|
+
inv.contract = contract;
|
|
120
|
+
inv.checks = contract.checks;
|
|
121
|
+
inv.ownsDisplay = contract.owns;
|
|
122
|
+
inv.acceptanceDeclared = contract.acceptance.length > 0;
|
|
123
|
+
} else {
|
|
124
|
+
let prompt = prompts.join(' ');
|
|
125
|
+
if (!prompt && kind === 'run' && !process.stdin.isTTY) prompt = readStdin();
|
|
126
|
+
if (!prompt.trim()) usage(kind === 'agent' ? 'agent needs instructions.' : 'no prompt given (positional args or stdin).');
|
|
127
|
+
inv.prompt = prompt;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
inv.owns = normalizeOwns(inv.ownsDisplay, inv.cwd, workspaceRoot(inv.cwd));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err instanceof OwnershipError) usage(err.message);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
return inv;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// resume <id> [message] — or run --session <id> <prompt> (0.3 compat).
|
|
140
|
+
function buildResume(parsed, ref, message) {
|
|
141
|
+
const id = store.resolveSessionId(ref);
|
|
142
|
+
if (!id) usage(`no session "${ref}" (see "zcode status").`);
|
|
143
|
+
const settings = readSettings(id);
|
|
144
|
+
if (!settings) usage(`session "${id}" has no stored settings; it cannot be resumed.`);
|
|
145
|
+
const { flags, given } = parsed;
|
|
146
|
+
if (settings.kind !== 'task' && !message.trim()) usage(`resume of a ${settings.kind} session needs a message.`);
|
|
147
|
+
return {
|
|
148
|
+
...settings,
|
|
149
|
+
id,
|
|
150
|
+
resume: { message },
|
|
151
|
+
model: given.has('model') ? flags.model : settings.model,
|
|
152
|
+
effort: given.has('effort') ? flags.effort : settings.effort,
|
|
153
|
+
timeoutMs: given.has('timeoutMs') ? flags.timeoutMs : settings.timeoutMs,
|
|
154
|
+
maxRepairs: given.has('maxRepairs') ? flags.maxRepairs : settings.maxRepairs,
|
|
155
|
+
checkTimeoutMs: given.has('checkTimeoutMs') ? flags.checkTimeoutMs : settings.checkTimeoutMs,
|
|
156
|
+
includeReasoning: flags.includeReasoning,
|
|
157
|
+
stream: settings.kind !== 'task' || flags.stream,
|
|
158
|
+
contract: settings.kind === 'task' ? readTask(id) : undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readTask(id) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(path.join(store.sessionDir(id), 'task.json'), 'utf8'));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --detach: claim the id, hand the invocation to a detached worker
|
|
171
|
+
// (`zcode __exec <id>`), print the handle, exit 0. The worker writes its
|
|
172
|
+
// NDJSON to <session>/stdout.ndjson and diagnostics to stderr.log.
|
|
173
|
+
function detach(inv, flags) {
|
|
174
|
+
if (inv.resume) {
|
|
175
|
+
const view = store.viewSession(store.readSession(inv.id));
|
|
176
|
+
if (view && view.live) {
|
|
177
|
+
process.stderr.write(`zcode: session "${inv.id}" is still running (pid ${view.pid}).\n`);
|
|
178
|
+
process.exit(7);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
try {
|
|
182
|
+
prepareSession(inv);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err instanceof store.SessionExistsError) usage(err.message);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const dir = store.sessionDir(inv.id);
|
|
189
|
+
store.writeJsonAtomic(path.join(dir, 'pending.json'), inv);
|
|
190
|
+
// Mark it queued (owned by us) BEFORE the worker exists, so a concurrent
|
|
191
|
+
// `zcode wait` never mistakes a previous invocation's result for this one.
|
|
192
|
+
store.updateSession(inv.id, { state: 'queued', phase: 'queued', pid: process.pid, status: null });
|
|
193
|
+
const out = fs.openSync(path.join(dir, 'stdout.ndjson'), 'a', 0o600);
|
|
194
|
+
const err = fs.openSync(path.join(dir, 'stderr.log'), 'a', 0o600);
|
|
195
|
+
const child = spawn(process.execPath, [BIN, '__exec', inv.id], {
|
|
196
|
+
cwd: inv.cwd,
|
|
197
|
+
detached: true,
|
|
198
|
+
stdio: ['ignore', out, err],
|
|
199
|
+
env: process.env,
|
|
200
|
+
});
|
|
201
|
+
child.unref();
|
|
202
|
+
store.updateSession(inv.id, { state: 'queued', phase: 'queued', pid: child.pid, status: null });
|
|
203
|
+
const handle = {
|
|
204
|
+
type: 'detached',
|
|
205
|
+
id: inv.id,
|
|
206
|
+
pid: child.pid,
|
|
207
|
+
kind: inv.kind,
|
|
208
|
+
status: 'queued',
|
|
209
|
+
files: { session: dir, stdout: path.join(dir, 'stdout.ndjson'), stderr: path.join(dir, 'stderr.log') },
|
|
210
|
+
};
|
|
211
|
+
if (flags.json) process.stdout.write(JSON.stringify(handle) + '\n');
|
|
212
|
+
else {
|
|
213
|
+
process.stdout.write(`${inv.id}\n`);
|
|
214
|
+
if (!flags.quiet) {
|
|
215
|
+
process.stderr.write(`zcode: started ${inv.id} in the background (pid ${child.pid}). zcode status ${inv.id} · zcode wait ${inv.id}\n`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = async function exec(parsed) {
|
|
222
|
+
const { command, flags } = parsed;
|
|
223
|
+
let inv;
|
|
224
|
+
let worker = false;
|
|
225
|
+
if (command === '__exec') {
|
|
226
|
+
worker = true;
|
|
227
|
+
const id = parsed.prompts[0];
|
|
228
|
+
const file = path.join(store.sessionDir(id), 'pending.json');
|
|
229
|
+
try {
|
|
230
|
+
inv = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
231
|
+
fs.rmSync(file, { force: true });
|
|
232
|
+
} catch {
|
|
233
|
+
usage(`no pending invocation for "${id}".`);
|
|
234
|
+
}
|
|
235
|
+
inv.adopt = true;
|
|
236
|
+
} else if (command === 'resume') {
|
|
237
|
+
const [ref, ...rest] = parsed.prompts;
|
|
238
|
+
if (!ref) usage('resume needs a session id.');
|
|
239
|
+
// `zcode resume <id> -` reads the message from stdin (long answers).
|
|
240
|
+
const message = rest.length === 1 && rest[0] === '-' ? readStdin() : rest.join(' ');
|
|
241
|
+
inv = buildResume(parsed, ref, message);
|
|
242
|
+
} else if (command === 'run' && flags.session) {
|
|
243
|
+
inv = buildResume(parsed, flags.session, parsed.prompts.join(' ') || (!process.stdin.isTTY ? readStdin() : ''));
|
|
244
|
+
} else {
|
|
245
|
+
inv = buildInvocation(parsed);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (flags.detach && !worker) return detach(inv, flags);
|
|
249
|
+
|
|
250
|
+
// Workers always emit NDJSON (to their stdout file) and never a TTY feed.
|
|
251
|
+
const json = worker || flags.json;
|
|
252
|
+
const quiet = flags.quiet && !worker;
|
|
253
|
+
const kind = inv.kind;
|
|
254
|
+
const reporter = makeReporter({
|
|
255
|
+
quiet,
|
|
256
|
+
thinking: flags.thinking ?? kind !== 'task',
|
|
257
|
+
});
|
|
258
|
+
let streamedText = '';
|
|
259
|
+
const onEvent = json
|
|
260
|
+
? (ev) => {
|
|
261
|
+
if (quiet) return;
|
|
262
|
+
if (ev.type === 'stream' && !inv.stream) return;
|
|
263
|
+
process.stdout.write(JSON.stringify(ev) + '\n');
|
|
264
|
+
}
|
|
265
|
+
: (ev) => {
|
|
266
|
+
if (ev.type === 'stream' && ev.kind === 'text_delta' && ev.delta && kind !== 'task') {
|
|
267
|
+
reporter.closeThinking();
|
|
268
|
+
streamedText += ev.delta;
|
|
269
|
+
process.stdout.write(ev.delta);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
reporter.onEvent(ev);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// SIGINT/SIGTERM (Ctrl-C, `zcode cancel`) abort cleanly: the engine is
|
|
276
|
+
// told to stop, checks are killed, the session is recorded "cancelled".
|
|
277
|
+
// A second signal exits immediately.
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
let signals = 0;
|
|
280
|
+
const onSignal = () => {
|
|
281
|
+
signals++;
|
|
282
|
+
if (signals > 1) process.exit(130);
|
|
283
|
+
controller.abort();
|
|
284
|
+
};
|
|
285
|
+
process.on('SIGINT', onSignal);
|
|
286
|
+
process.on('SIGTERM', onSignal);
|
|
287
|
+
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
result = await execute(inv, { onEvent, signal: controller.signal });
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (err instanceof ConfigError || err instanceof ExecError) {
|
|
293
|
+
const failed = {
|
|
294
|
+
type: 'result', ok: false, status: 'failed', id: inv.id, kind,
|
|
295
|
+
error: { kind: err.kind || 'config', message: err.message }, issues: [err.message],
|
|
296
|
+
};
|
|
297
|
+
if (json) process.stdout.write(JSON.stringify(failed) + '\n');
|
|
298
|
+
// A detached worker's caller polls the store: record why it never ran
|
|
299
|
+
// (conflicts already record themselves).
|
|
300
|
+
if (worker && err.kind !== 'conflict' && store.readSession(inv.id)) {
|
|
301
|
+
store.writeResult(inv.id, failed);
|
|
302
|
+
store.updateSession(inv.id, { state: 'finished', phase: 'done', status: 'failed', outcome: { status: 'failed', issues: [err.message] } });
|
|
303
|
+
}
|
|
304
|
+
process.stderr.write(`zcode: ${err.message}\n`);
|
|
305
|
+
process.exit(err.exitCode);
|
|
306
|
+
}
|
|
307
|
+
if (err instanceof store.SessionExistsError) usage(err.message);
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const code = exitCodeFor(result);
|
|
312
|
+
if (json) {
|
|
313
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
314
|
+
} else if (kind === 'task') {
|
|
315
|
+
reporter.closeThinking();
|
|
316
|
+
process.stdout.write(formatResult(result, { color: useColor(process.stdout) }));
|
|
317
|
+
} else {
|
|
318
|
+
reporter.closeThinking();
|
|
319
|
+
if (!streamedText.trim()) process.stdout.write(result.response || '');
|
|
320
|
+
process.stdout.write('\n');
|
|
321
|
+
reporter.footer(result);
|
|
322
|
+
if (result.status !== 'completed' || kind === 'agent') {
|
|
323
|
+
process.stderr.write(formatResult(result, { color: useColor(process.stderr) }));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
process.exit(code);
|
|
327
|
+
};
|
package/lib/commands/login.js
CHANGED
|
@@ -34,15 +34,8 @@ module.exports = async function login() {
|
|
|
34
34
|
mode: 0o600,
|
|
35
35
|
});
|
|
36
36
|
fs.chmodSync(file, 0o600);
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// be explicit when the credentials file is the source).
|
|
40
|
-
const stamp = path.join(root, 'build-stamp.json');
|
|
41
|
-
try {
|
|
42
|
-
fs.rmSync(stamp, { force: true });
|
|
43
|
-
} catch {
|
|
44
|
-
/* nothing to invalidate */
|
|
45
|
-
}
|
|
37
|
+
// No cache to invalidate: every invocation rewrites its session's
|
|
38
|
+
// provider config from the current key.
|
|
46
39
|
process.stdout.write(`zcode login: key stored at ${file} (mode 600)\n`);
|
|
47
40
|
process.exit(0);
|
|
48
41
|
};
|
package/lib/commands/mcp-list.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// lib/commands/mcp-list.js — print the effective MCP server set for this
|
|
3
|
-
// invocation as one JSON line and exit 0. Never spawns the engine
|
|
4
|
-
// value is exactly what a run with the same flags
|
|
5
|
-
const {
|
|
3
|
+
// invocation as one JSON line and exit 0. Never spawns the engine and needs
|
|
4
|
+
// no key; the value is exactly what a run with the same flags registers.
|
|
5
|
+
const { effectiveMcpServers } = require('../isolation.js');
|
|
6
6
|
|
|
7
7
|
module.exports = async function mcpList(parsed) {
|
|
8
|
-
const flags = parsed
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
mcpSpecs: flags.mcpSpecs,
|
|
12
|
-
mcpInherit: flags.mcpInherit,
|
|
13
|
-
skillsDirs: flags.skillsDirs,
|
|
14
|
-
noSkills: flags.noSkills,
|
|
15
|
-
});
|
|
16
|
-
process.stdout.write(
|
|
17
|
-
JSON.stringify({ type: 'mcp', servers: overlay.mcp.servers }) + '\n'
|
|
18
|
-
);
|
|
8
|
+
const { flags } = parsed;
|
|
9
|
+
const servers = effectiveMcpServers({ inherit: flags.mcpInherit, mcpSpecs: flags.mcpSpecs });
|
|
10
|
+
process.stdout.write(JSON.stringify({ type: 'mcp', servers }) + '\n');
|
|
19
11
|
process.exit(0);
|
|
20
12
|
};
|