@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.
- package/README.md +184 -138
- 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/install-skills.js +54 -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 +22 -7
- package/skills/README.md +34 -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/agent.js
DELETED
|
@@ -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
|
-
}
|
package/lib/commands/run.js
DELETED
|
@@ -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 };
|