@0xgeorgemathew/zcode 0.3.2 → 0.4.1

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,203 +0,0 @@
1
- 'use strict';
2
- // lib/commands/agent.js — subagent-dispatching turn, verified programmatically
3
- // (DESIGN §11): dispatch-forcing preamble, yolo run, then session/subagents
4
- // RPC joined with the scratch agents dir. Never trusts the model's prose;
5
- // exit 6 when no subagent ran or none ended "success".
6
- const fs = require('node:fs');
7
- const path = require('node:path');
8
- const { executeTurn, TurnError } = require('../appserver.js');
9
- const { bootstrap } = require('../bootstrap.js');
10
-
11
- function readJsonSafe(file) {
12
- try {
13
- return JSON.parse(fs.readFileSync(file, 'utf8'));
14
- } catch {
15
- return null;
16
- }
17
- }
18
-
19
- // Join session/subagents ended items with <home>/.zcode/cli/agents/<parent>/
20
- // agent_<uuid>/{metadata.json,output.txt} (all under the scratch HOME we own).
21
- function collectSubagents({ client, sessionId, isolationHome, scrub }) {
22
- return client
23
- .request('session/subagents', { sessionId }, { timeoutMs: 15000 })
24
- .catch((err) => {
25
- throw new TurnError(`session/subagents failed: ${scrub(err.message)}`, {
26
- kind: 'subagents_rpc',
27
- });
28
- })
29
- .then((result) => {
30
- const items = (result && result.ended && result.ended.items) || [];
31
- const running = (result && result.running) || [];
32
- const agentsDir = path.join(
33
- isolationHome,
34
- '.zcode',
35
- 'cli',
36
- 'agents',
37
- sessionId
38
- );
39
- const out = [];
40
- for (const item of items) {
41
- const dir = path.join(agentsDir, `agent_${(item.agentId || '').replace(/^agent_/, '')}`);
42
- const metadata =
43
- readJsonSafe(path.join(dir, 'metadata.json')) || null;
44
- let output = null;
45
- try {
46
- output = fs.readFileSync(path.join(dir, 'output.txt'), 'utf8');
47
- } catch {
48
- /* subagent may not have flushed output */
49
- }
50
- out.push({
51
- agentId: item.agentId || (metadata && metadata.agentId) || null,
52
- childSessionId:
53
- item.childSessionId || (metadata && metadata.childSessionId) || null,
54
- profileId: (metadata && metadata.profileId) || item.subagentType || null,
55
- title: item.title || (metadata && metadata.description) || null,
56
- status: item.status || null,
57
- summary: item.summary || null,
58
- output,
59
- startedAt: item.startedAt || null,
60
- endedAt: item.endedAt || null,
61
- durationMs:
62
- item.startedAt && item.endedAt ? item.endedAt - item.startedAt : null,
63
- });
64
- }
65
- return { subagents: out, runningCount: running.length };
66
- });
67
- }
68
-
69
- module.exports = async function agent(parsed) {
70
- const flags = parsed.flags;
71
- const instructions = parsed.prompts.join(' ');
72
- if (!instructions.trim()) {
73
- process.stderr.write('zcode: agent needs instructions.\n');
74
- process.exit(2);
75
- }
76
- const profile = flags.profile || 'general-purpose';
77
- const prompt =
78
- `You MUST use the Agent tool (profileId: "${profile}") to accomplish the ` +
79
- `task below — do not do the work yourself. When the subagent finishes, ` +
80
- `report its agent id and result.\n\nTask: ${instructions}`;
81
-
82
- const { client, scrub, isolation, overlay } = bootstrap({
83
- fresh: flags.fresh,
84
- mcpSpecs: flags.mcpSpecs,
85
- mcpInherit: flags.mcpInherit,
86
- skillsDirs: flags.skillsDirs,
87
- noSkills: flags.noSkills,
88
- });
89
- client.start();
90
- process.on('SIGINT', () => {
91
- client.kill();
92
- process.exit(130);
93
- });
94
- const reporter = require('../ui.js').makePlainReporter({ quiet: flags.quiet });
95
- try {
96
- const turn = await executeTurn(client, {
97
- workspacePath: flags.cwd,
98
- modelId: flags.model,
99
- effort: flags.effort,
100
- prompt,
101
- timeoutMs: flags.timeoutMs,
102
- resumeSessionId: flags.session || null,
103
- requiredMcp: overlay.mcp.added,
104
- onEvent: (ev) => {
105
- if (flags.json) process.stdout.write(JSON.stringify(ev) + '\n');
106
- else reporter.onEvent(ev);
107
- },
108
- });
109
-
110
- const { subagents, runningCount } = await collectSubagents({
111
- client,
112
- sessionId: turn.sessionId,
113
- isolationHome: isolation.home,
114
- scrub,
115
- });
116
-
117
- const ok = subagents.length >= 1 && subagents.every((s) => s.status === 'success');
118
-
119
- if (flags.json) {
120
- process.stdout.write(
121
- JSON.stringify({
122
- type: 'result',
123
- ok,
124
- response: turn.response,
125
- model: turn.engineModel || flags.model,
126
- modelRequested: flags.model,
127
- effort: turn.engineReasoningLevel || flags.effort,
128
- effortRequested: flags.effort,
129
- mode: turn.mode,
130
- sessionId: turn.sessionId,
131
- resumed: turn.resumed,
132
- usage: turn.usage
133
- ? {
134
- inputTokens: turn.usage.inputTokens ?? null,
135
- outputTokens: turn.usage.outputTokens ?? null,
136
- }
137
- : null,
138
- reasoningChars: turn.reasoningText.length,
139
- toolCalls: turn.toolCallCount,
140
- mcpServers: Object.keys(overlay.mcp.servers),
141
- skillsInjected: overlay.skills.injected,
142
- subagents,
143
- runningSubagents: runningCount,
144
- durationMs: turn.durationMs,
145
- }) + '\n'
146
- );
147
- } else if (!ok) {
148
- process.stdout.write(turn.response + '\n');
149
- } else {
150
- process.stdout.write(turn.response + '\n');
151
- reporter.footer({
152
- model: turn.engineModel || flags.model,
153
- effort: turn.engineReasoningLevel || flags.effort,
154
- toolCalls: turn.toolCallCount,
155
- durationMs: turn.durationMs,
156
- sessionId: turn.sessionId,
157
- });
158
- for (const s of subagents) {
159
- const secs = typeof s.durationMs === 'number' ? ` ${(s.durationMs / 1000).toFixed(1)}s` : '';
160
- process.stderr.write(
161
- (process.stderr.isTTY ? '\x1b[2m' : '') +
162
- `✓ subagent ${s.profileId}${secs}` +
163
- (process.stderr.isTTY ? '\x1b[0m' : '') + '\n'
164
- );
165
- if (s.summary) {
166
- process.stderr.write(
167
- (process.stderr.isTTY ? '\x1b[2m' : '') +
168
- ` ${s.summary.replace(/\s+/g, ' ').trim().slice(0, 200)}` +
169
- (process.stderr.isTTY ? '\x1b[0m' : '') + '\n'
170
- );
171
- }
172
- }
173
- }
174
- client.kill();
175
- if (!ok) {
176
- const why =
177
- subagents.length === 0
178
- ? 'no subagent was dispatched'
179
- : `subagent statuses: ${subagents.map((s) => s.status).join(', ')}`;
180
- process.stderr.write(`zcode: agent verification failed: ${why}\n`);
181
- process.exit(6);
182
- }
183
- process.exit(0);
184
- } catch (err) {
185
- client.kill();
186
- if (flags.json) {
187
- process.stdout.write(
188
- JSON.stringify({
189
- type: 'result',
190
- ok: false,
191
- error: err.kind || 'error',
192
- message: scrub(err.message),
193
- }) + '\n'
194
- );
195
- }
196
- if (err instanceof TurnError && err.kind === 'timeout') {
197
- process.stderr.write(`zcode: ${scrub(err.message)}\n`);
198
- process.exit(124);
199
- }
200
- process.stderr.write(`zcode: agent turn failed: ${scrub(err.message)}\n`);
201
- process.exit(5);
202
- }
203
- }
@@ -1,126 +0,0 @@
1
- 'use strict';
2
- // lib/commands/run.js — one-shot turn (DESIGN §9, §10), agent-first: NDJSON
3
- // stream on stdout with --json (incl. reasoning_delta = thinking and tool
4
- // lifecycle), reasoning mirrored to stderr live in plain mode, session
5
- // continuity via --session, MCP/skills overlays applied per run.
6
- const { executeTurn, TurnError } = require('../appserver.js');
7
- const { bootstrap } = require('../bootstrap.js');
8
- const { makePlainReporter } = require('../ui.js');
9
-
10
- // Prompt: positional argv joined, else stdin (when piped).
11
- function readPrompt(parsed) {
12
- if (parsed.prompts.length > 0) return parsed.prompts.join(' ');
13
- if (!process.stdin.isTTY) {
14
- return require('node:fs').readFileSync(0, 'utf8');
15
- }
16
- return '';
17
- }
18
-
19
- module.exports = async function run(parsed) {
20
- const flags = parsed.flags;
21
- const prompt = readPrompt(parsed);
22
- if (!prompt.trim()) {
23
- process.stderr.write('zcode: no prompt given (positional args or stdin).\n');
24
- process.exit(2);
25
- }
26
-
27
- const { client, scrub, overlay } = bootstrap({
28
- fresh: flags.fresh,
29
- mcpSpecs: flags.mcpSpecs,
30
- mcpInherit: flags.mcpInherit,
31
- skillsDirs: flags.skillsDirs,
32
- noSkills: flags.noSkills,
33
- });
34
- const workspacePath = flags.cwd;
35
- let streamedText = '';
36
- const reporter = makePlainReporter({ quiet: flags.quiet });
37
- const onEvent = flags.json
38
- ? (ev) => process.stdout.write(JSON.stringify(ev) + '\n')
39
- : (ev) => {
40
- if (ev.type === 'stream' && ev.kind === 'text_delta' && ev.delta) {
41
- reporter.closeThinking();
42
- streamedText += ev.delta;
43
- process.stdout.write(ev.delta);
44
- } else {
45
- reporter.onEvent(ev);
46
- }
47
- };
48
-
49
- client.start();
50
- process.on('SIGINT', () => {
51
- client.kill();
52
- process.exit(130);
53
- });
54
- try {
55
- const turn = await executeTurn(client, {
56
- workspacePath,
57
- modelId: flags.model,
58
- effort: flags.effort,
59
- prompt,
60
- timeoutMs: flags.timeoutMs,
61
- resumeSessionId: flags.session || null,
62
- requiredMcp: overlay.mcp.added,
63
- onEvent,
64
- });
65
- if (flags.json) {
66
- const result = {
67
- type: 'result',
68
- ok: true,
69
- response: turn.response,
70
- model: turn.engineModel || flags.model,
71
- modelRequested: flags.model,
72
- effort: turn.engineReasoningLevel || flags.effort,
73
- effortRequested: flags.effort,
74
- mode: turn.mode,
75
- sessionId: turn.sessionId,
76
- resumed: turn.resumed,
77
- usage: turn.usage
78
- ? {
79
- inputTokens: turn.usage.inputTokens ?? null,
80
- outputTokens: turn.usage.outputTokens ?? null,
81
- }
82
- : null,
83
- reasoningChars: turn.reasoningText.length,
84
- toolCalls: turn.toolCallCount,
85
- toolEvents: turn.toolEvents,
86
- mcpServers: Object.keys(overlay.mcp.servers),
87
- skillsInjected: overlay.skills.injected,
88
- terminalStatus: turn.terminalStatus,
89
- streamingEvents: turn.streamingEvents,
90
- durationMs: turn.durationMs,
91
- };
92
- if (flags.includeReasoning) result.reasoning = turn.reasoningText;
93
- process.stdout.write(JSON.stringify(result) + '\n');
94
- } else {
95
- if (!streamedText.trim()) process.stdout.write(turn.response);
96
- process.stdout.write('\n');
97
- reporter.footer({
98
- model: turn.engineModel || flags.model,
99
- effort: turn.engineReasoningLevel || flags.effort,
100
- toolCalls: turn.toolCallCount,
101
- durationMs: turn.durationMs,
102
- sessionId: turn.sessionId,
103
- });
104
- }
105
- client.kill();
106
- process.exit(0);
107
- } catch (err) {
108
- client.kill();
109
- if (flags.json) {
110
- process.stdout.write(
111
- JSON.stringify({
112
- type: 'result',
113
- ok: false,
114
- error: err.kind || 'error',
115
- message: scrub(err.message),
116
- }) + '\n'
117
- );
118
- }
119
- if (err instanceof TurnError && err.kind === 'timeout') {
120
- process.stderr.write(`zcode: ${scrub(err.message)}\n`);
121
- process.exit(124);
122
- }
123
- process.stderr.write(`zcode: turn failed: ${scrub(err.message)}\n`);
124
- process.exit(5);
125
- }
126
- };
package/lib/overlay.js DELETED
@@ -1,138 +0,0 @@
1
- 'use strict';
2
- // Per-run overlays on the scratch HOME (DESIGN §6 extension): MCP servers
3
- // and extra skills. Both are recomputed from read-only live sources plus the
4
- // caller's flags on EVERY run, so the scratch never leaks a previous run's
5
- // additions — the default state is always "what the desktop has".
6
- const fs = require('node:fs');
7
- const path = require('node:path');
8
- const { LIVE_ZCODE } = require('./isolation.js');
9
-
10
- // --- MCP ---------------------------------------------------------------------
11
-
12
- // Base servers: the live desktop config, read-only, honoring `enabled`.
13
- function liveMcpServers() {
14
- try {
15
- const cfg = JSON.parse(
16
- fs.readFileSync(path.join(LIVE_ZCODE, 'cli', 'config.json'), 'utf8')
17
- );
18
- const servers = cfg && cfg.mcp && cfg.mcp.servers;
19
- if (!servers || typeof servers !== 'object') return {};
20
- const out = {};
21
- for (const [name, def] of Object.entries(servers)) {
22
- if (!def || typeof def !== 'object') continue;
23
- if (def.enabled === false) continue;
24
- out[name] = def;
25
- }
26
- return out;
27
- } catch {
28
- return {};
29
- }
30
- }
31
-
32
- // Effective set for one run. Hermetic by default: ONLY the caller's --mcp
33
- // servers. --mcp-inherit pulls in the desktop's enabled servers as a base
34
- // (specs still win by name). Hermetic default is deliberate: the desktop
35
- // set includes OAuth servers that can never complete headless and stall
36
- // the engine's connect pool, starving later servers nondeterministically.
37
- function effectiveMcpServers({ inherit = false, mcpSpecs = [] } = {}) {
38
- const servers = inherit ? liveMcpServers() : {};
39
- for (const spec of mcpSpecs) {
40
- for (const [name, def] of Object.entries(spec)) {
41
- servers[name] = def;
42
- }
43
- }
44
- return servers;
45
- }
46
-
47
- // Writes the effective set into the scratch cli config so the engine child
48
- // picks it up at session start. Creates a minimal config when the scratch
49
- // has none (fresh-machine synthesized path).
50
- function applyMcpOverlay({ home, inherit = false, mcpSpecs = [] }) {
51
- const servers = effectiveMcpServers({ inherit, mcpSpecs });
52
- // Names the caller explicitly added — the CLI waits for these to settle
53
- // (connected or failed) before sending the prompt, so an explicit server
54
- // is never lost to a slow stdio spawn. Inherited servers are best-effort.
55
- const added = mcpSpecs.flatMap((spec) => Object.keys(spec));
56
- const configPath = path.join(home, '.zcode', 'cli', 'config.json');
57
- let cfg = {};
58
- try {
59
- cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
60
- if (!cfg || typeof cfg !== 'object') cfg = {};
61
- } catch {
62
- cfg = {};
63
- }
64
- cfg.mcp = { ...(cfg.mcp || {}), servers };
65
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
66
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
67
- fs.chmodSync(configPath, 0o600);
68
- return { servers, count: Object.keys(servers).length, added };
69
- }
70
-
71
- // --- skills ------------------------------------------------------------------
72
-
73
- const MANIFEST = 'injected-skills.json';
74
-
75
- function readManifest(rootDir) {
76
- try {
77
- const m = JSON.parse(fs.readFileSync(path.join(rootDir, MANIFEST), 'utf8'));
78
- return Array.isArray(m.skills) ? m : { skills: [] };
79
- } catch {
80
- return { skills: [] };
81
- }
82
- }
83
-
84
- // A --skills dir is either one skill (<dir>/SKILL.md) or a skills root
85
- // (<dir>/<name>/SKILL.md). Returns [{name, dir}] of copyable skills.
86
- function discoverSkillDirs(dir) {
87
- const found = [];
88
- if (fs.existsSync(path.join(dir, 'SKILL.md'))) {
89
- found.push({ name: path.basename(path.resolve(dir)), dir });
90
- return found;
91
- }
92
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
93
- if (!entry.isDirectory()) continue;
94
- if (fs.existsSync(path.join(dir, entry.name, 'SKILL.md'))) {
95
- found.push({ name: entry.name, dir: path.join(dir, entry.name) });
96
- }
97
- }
98
- return found;
99
- }
100
-
101
- // Removes skills injected by a previous run, copies this run's extras in,
102
- // records them in the manifest. --no-skills empties the scratch skills dir
103
- // entirely (per-run hermetic mode).
104
- function applySkillsOverlay({ rootDir, home, skillsDirs = [], noSkills = false }) {
105
- const scratchSkills = path.join(home, '.zcode', 'skills');
106
- const previous = readManifest(rootDir);
107
- for (const name of previous.skills) {
108
- fs.rmSync(path.join(scratchSkills, name), { recursive: true, force: true });
109
- }
110
-
111
- if (noSkills) {
112
- fs.rmSync(scratchSkills, { recursive: true, force: true });
113
- fs.mkdirSync(scratchSkills, { recursive: true });
114
- fs.writeFileSync(
115
- path.join(rootDir, MANIFEST),
116
- JSON.stringify({ skills: [] }, null, 2) + '\n',
117
- { mode: 0o600 }
118
- );
119
- return { injected: [] };
120
- }
121
-
122
- const injected = [];
123
- for (const dir of skillsDirs) {
124
- for (const { name, dir: skillDir } of discoverSkillDirs(dir)) {
125
- fs.rmSync(path.join(scratchSkills, name), { recursive: true, force: true });
126
- fs.cpSync(skillDir, path.join(scratchSkills, name), { recursive: true });
127
- injected.push(name);
128
- }
129
- }
130
- fs.writeFileSync(
131
- path.join(rootDir, MANIFEST),
132
- JSON.stringify({ skills: injected }, null, 2) + '\n',
133
- { mode: 0o600 }
134
- );
135
- return { injected };
136
- }
137
-
138
- module.exports = { effectiveMcpServers, applyMcpOverlay, applySkillsOverlay, discoverSkillDirs };