@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/isolation.js
CHANGED
|
@@ -1,93 +1,71 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Scratch-HOME isolation (DESIGN §6): the engine runs with
|
|
3
|
-
//
|
|
2
|
+
// Scratch-HOME isolation (DESIGN §6, reworked in 0.4): the engine runs with
|
|
3
|
+
// HOME pointed at a directory we own so the live ~/.zcode — which the
|
|
4
4
|
// desktop app is actively using — is never written by us, and the user's
|
|
5
5
|
// OAuth credentials.json is never copied.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// 0.4: one engine HOME per zcode session (<root>/sessions/<id>/home) instead
|
|
8
|
+
// of one shared HOME. The shared HOME was rm -rf'd and rebuilt whenever a
|
|
9
|
+
// volatile desktop file (agents-state.json, bot-config.v3.json, …) changed
|
|
10
|
+
// mtime — which deleted the engine's session database under every other
|
|
11
|
+
// session and made --session resume 404 — and per-run MCP/skills overlays
|
|
12
|
+
// rewrote files that concurrent runs were reading. Per-session HOMEs give
|
|
13
|
+
// each session its own engine DB, config and skills; config files are
|
|
14
|
+
// refreshed in place on every invocation and engine state (cli/db,
|
|
15
|
+
// cli/agents, rollout, logs) is never deleted.
|
|
16
|
+
//
|
|
17
|
+
// Invariant (§6.4): zcode performs no writes to the live ~/.zcode, and
|
|
18
|
+
// credentials.json is never copied.
|
|
9
19
|
const fs = require('node:fs');
|
|
10
20
|
const path = require('node:path');
|
|
11
21
|
const os = require('node:os');
|
|
12
|
-
const crypto = require('node:crypto');
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
// The desktop app's live directory. ZAI_CODE_LIVE_DIR points elsewhere
|
|
24
|
+
// (e.g. a second desktop profile, or a fixture in tests).
|
|
25
|
+
function liveDir(env = process.env) {
|
|
26
|
+
return env.ZAI_CODE_LIVE_DIR || path.join(os.homedir(), '.zcode');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LIVE_ZCODE = liveDir({});
|
|
15
30
|
const LIVE_V2 = path.join(LIVE_ZCODE, 'v2');
|
|
16
31
|
|
|
17
32
|
// v2 top-level entries we deliberately never copy.
|
|
18
33
|
const V2_EXCLUDE_FILES = new Set(['credentials.json', '.DS_Store']);
|
|
19
34
|
function isV2Excluded(name) {
|
|
20
|
-
return (
|
|
21
|
-
V2_EXCLUDE_FILES.has(name) || name.startsWith('tasks-index.sqlite')
|
|
22
|
-
);
|
|
35
|
+
return V2_EXCLUDE_FILES.has(name) || name.startsWith('tasks-index.sqlite');
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
// Root of zcode's own state. MUST NOT be ~/.zcode: that is the desktop
|
|
39
|
+
// app's live directory (0.3.0/0.3.1 regression — the package rename made
|
|
40
|
+
// rootDir collide with it). Ours is ~/.zcode-cli.
|
|
41
|
+
function rootDir(env = process.env) {
|
|
42
|
+
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode-cli');
|
|
43
|
+
}
|
|
27
44
|
|
|
28
|
-
// ---
|
|
45
|
+
// --- small fs helpers ---------------------------------------------------------
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sources.push({ file: abs, mtimeMs: fs.statSync(abs).mtimeMs });
|
|
37
|
-
} catch {
|
|
38
|
-
/* source vanished — not copied */
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
for (const entry of fs.readdirSync(LIVE_V2, { withFileTypes: true })) {
|
|
43
|
-
if (!entry.isFile() || isV2Excluded(entry.name)) continue;
|
|
44
|
-
pushFile(path.join(LIVE_V2, entry.name));
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
/* no live v2 — synthesized path */
|
|
48
|
-
}
|
|
49
|
-
pushFile(path.join(LIVE_ZCODE, 'cli', 'config.json'));
|
|
50
|
-
pushFile(path.join(LIVE_ZCODE, 'AGENTS.md'));
|
|
47
|
+
function writeJson600(file, value) {
|
|
48
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
49
|
+
fs.chmodSync(file, 0o600);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJson(file) {
|
|
51
53
|
try {
|
|
52
|
-
|
|
53
|
-
withFileTypes: true,
|
|
54
|
-
})) {
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
pushFile(path.join(LIVE_ZCODE, 'skills', entry.name, 'SKILL.md'));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
54
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
59
55
|
} catch {
|
|
60
|
-
|
|
56
|
+
return null;
|
|
61
57
|
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listDir(dir) {
|
|
62
61
|
try {
|
|
63
|
-
|
|
64
|
-
withFileTypes: true,
|
|
65
|
-
})) {
|
|
66
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
67
|
-
pushFile(path.join(LIVE_ZCODE, 'agents', entry.name));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
62
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
70
63
|
} catch {
|
|
71
|
-
|
|
64
|
+
return [];
|
|
72
65
|
}
|
|
73
|
-
return sources;
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
// ---
|
|
77
|
-
|
|
78
|
-
function writeJson600(file, value) {
|
|
79
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', {
|
|
80
|
-
mode: 0o600,
|
|
81
|
-
});
|
|
82
|
-
fs.chmodSync(file, 0o600);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function copyFileTo(src, destDir) {
|
|
86
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
87
|
-
const dest = path.join(destDir, path.basename(src));
|
|
88
|
-
fs.copyFileSync(src, dest);
|
|
89
|
-
return dest;
|
|
90
|
-
}
|
|
68
|
+
// --- provider/key rewriting -----------------------------------------------------
|
|
91
69
|
|
|
92
70
|
// Prune to exactly the zai-api rule and swap in the package key (§6.2 item 2).
|
|
93
71
|
function rewriteProviderConfig(target, key) {
|
|
@@ -101,32 +79,16 @@ function rewriteProviderConfig(target, key) {
|
|
|
101
79
|
}
|
|
102
80
|
const zaiRules = rules.filter((r) => r && r.providerId === 'zai-api');
|
|
103
81
|
if (zaiRules.length === 0) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`no zai-api rule found in provider_config.json at ${target}`
|
|
106
|
-
);
|
|
82
|
+
throw new Error(`no zai-api rule found in provider_config.json at ${target}`);
|
|
107
83
|
}
|
|
108
84
|
const rule = zaiRules[0];
|
|
109
85
|
rule.config = rule.config || {};
|
|
110
86
|
rule.config.group = rule.config.group || 'standard-personal';
|
|
111
|
-
rule.config.access = {
|
|
112
|
-
type: 'api-key',
|
|
113
|
-
apiKey: key,
|
|
114
|
-
};
|
|
87
|
+
rule.config.access = { type: 'api-key', apiKey: key };
|
|
115
88
|
parsed.config.providerConfigRules.providerRules = [rule];
|
|
116
89
|
writeJson600(target, parsed);
|
|
117
90
|
}
|
|
118
91
|
|
|
119
|
-
// Overwrite builtin:zai apiKey with the package key (§6.2 item 3).
|
|
120
|
-
function rewriteCliConfig(target, key) {
|
|
121
|
-
const parsed = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
122
|
-
const zai = parsed.provider && parsed.provider['builtin:zai'];
|
|
123
|
-
if (zai) {
|
|
124
|
-
zai.options = zai.options || {};
|
|
125
|
-
zai.options.apiKey = key;
|
|
126
|
-
}
|
|
127
|
-
writeJson600(target, parsed);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
92
|
function synthesizedProviderConfig(key) {
|
|
131
93
|
return {
|
|
132
94
|
schemaVersion: 1,
|
|
@@ -147,130 +109,231 @@ function synthesizedProviderConfig(key) {
|
|
|
147
109
|
};
|
|
148
110
|
}
|
|
149
111
|
|
|
150
|
-
//
|
|
151
|
-
// credentials.json, no sqlite. Fresh machines (no live v2) synthesize instead.
|
|
152
|
-
function buildScratch(home, key) {
|
|
153
|
-
fs.rmSync(home, { recursive: true, force: true });
|
|
154
|
-
const scratchV2 = path.join(home, '.zcode', 'v2');
|
|
155
|
-
fs.mkdirSync(scratchV2, { recursive: true });
|
|
112
|
+
// --- skills -------------------------------------------------------------------
|
|
156
113
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
114
|
+
// A --skills dir is either one skill (<dir>/SKILL.md) or a skills root
|
|
115
|
+
// (<dir>/<name>/SKILL.md). Returns [{name, dir}] of copyable skills.
|
|
116
|
+
function discoverSkillDirs(dir) {
|
|
117
|
+
if (fs.existsSync(path.join(dir, 'SKILL.md'))) {
|
|
118
|
+
return [{ name: path.basename(path.resolve(dir)), dir }];
|
|
119
|
+
}
|
|
120
|
+
return listDir(dir)
|
|
121
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
|
|
122
|
+
.map((e) => ({ name: e.name, dir: path.join(dir, e.name) }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- MCP ----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
// The live desktop MCP servers, read-only, honoring `enabled`.
|
|
128
|
+
function liveMcpServers(live = LIVE_ZCODE) {
|
|
129
|
+
const cfg = readJson(path.join(live, 'cli', 'config.json'));
|
|
130
|
+
const servers = cfg && cfg.mcp && cfg.mcp.servers;
|
|
131
|
+
if (!servers || typeof servers !== 'object') return {};
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
134
|
+
if (!def || typeof def !== 'object' || def.enabled === false) continue;
|
|
135
|
+
out[name] = def;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Effective set for one invocation. Hermetic by default: ONLY the caller's
|
|
141
|
+
// --mcp servers. --mcp-inherit pulls in the desktop's enabled servers as a
|
|
142
|
+
// base (specs still win by name). Hermetic default is deliberate: the
|
|
143
|
+
// desktop set includes OAuth servers that can never complete headless and
|
|
144
|
+
// stall the engine's connect pool.
|
|
145
|
+
function effectiveMcpServers({ inherit = false, mcpSpecs = [], live = LIVE_ZCODE } = {}) {
|
|
146
|
+
const servers = inherit ? liveMcpServers(live) : {};
|
|
147
|
+
for (const spec of mcpSpecs) {
|
|
148
|
+
for (const [name, def] of Object.entries(spec)) servers[name] = def;
|
|
149
|
+
}
|
|
150
|
+
return servers;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- shared caches ------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
// Engine-populated caches that are expensive to recreate per session (the
|
|
156
|
+
// plugin marketplace cache is ~30 MB) are shared through symlinks into
|
|
157
|
+
// <root>/shared. A pre-0.4 shared HOME donates its caches once.
|
|
158
|
+
const SHARED_CACHES = [
|
|
159
|
+
{ name: 'plugins', rel: path.join('.zcode', 'cli', 'plugins') },
|
|
160
|
+
{ name: 'runtime', rel: path.join('.zcode', 'v2', 'runtime') },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
function linkSharedCaches(home, root) {
|
|
164
|
+
for (const cache of SHARED_CACHES) {
|
|
165
|
+
const shared = path.join(root, 'shared', cache.name);
|
|
166
|
+
if (!fs.existsSync(shared)) {
|
|
167
|
+
const legacy = path.join(root, 'home', cache.rel);
|
|
168
|
+
fs.mkdirSync(path.dirname(shared), { recursive: true, mode: 0o700 });
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(legacy) && !fs.lstatSync(legacy).isSymbolicLink()) {
|
|
171
|
+
fs.renameSync(legacy, shared);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
/* fall through to an empty cache */
|
|
171
175
|
}
|
|
172
|
-
|
|
176
|
+
fs.mkdirSync(shared, { recursive: true, mode: 0o700 });
|
|
177
|
+
}
|
|
178
|
+
const target = path.join(home, cache.rel);
|
|
179
|
+
if (fs.existsSync(target) || isDanglingLink(target)) continue;
|
|
180
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
181
|
+
try {
|
|
182
|
+
fs.symlinkSync(shared, target, 'dir');
|
|
183
|
+
} catch {
|
|
184
|
+
/* engine recreates the cache itself */
|
|
173
185
|
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isDanglingLink(p) {
|
|
190
|
+
try {
|
|
191
|
+
return fs.lstatSync(p).isSymbolicLink() && !fs.existsSync(p);
|
|
174
192
|
} catch {
|
|
175
|
-
|
|
193
|
+
return false;
|
|
176
194
|
}
|
|
195
|
+
}
|
|
177
196
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
// --- build ------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
// Creates or refreshes one engine HOME. Config is (re)written from the live
|
|
200
|
+
// sources + this invocation's flags; engine state under the HOME (cli/db,
|
|
201
|
+
// cli/agents, cli/rollout, logs, memories) is never touched, so a session
|
|
202
|
+
// stays resumable across invocations. Returns facts for reporting.
|
|
203
|
+
function buildEngineHome({
|
|
204
|
+
home,
|
|
205
|
+
root,
|
|
206
|
+
key,
|
|
207
|
+
live = LIVE_ZCODE,
|
|
208
|
+
mcpSpecs = [],
|
|
209
|
+
mcpInherit = false,
|
|
210
|
+
skillsDirs = [],
|
|
211
|
+
noSkills = false,
|
|
212
|
+
}) {
|
|
213
|
+
const zdir = path.join(home, '.zcode');
|
|
214
|
+
const v2 = path.join(zdir, 'v2');
|
|
215
|
+
const cliDir = path.join(zdir, 'cli');
|
|
216
|
+
fs.mkdirSync(v2, { recursive: true, mode: 0o700 });
|
|
217
|
+
fs.mkdirSync(cliDir, { recursive: true, mode: 0o700 });
|
|
218
|
+
fs.chmodSync(home, 0o700);
|
|
219
|
+
|
|
220
|
+
// v2: top-level files only, no credentials.json, no sqlite (§6.2 item 1).
|
|
221
|
+
const liveV2 = path.join(live, 'v2');
|
|
222
|
+
const v2Files = listDir(liveV2).filter((e) => e.isFile() && !isV2Excluded(e.name));
|
|
223
|
+
for (const entry of v2Files) {
|
|
224
|
+
fs.copyFileSync(path.join(liveV2, entry.name), path.join(v2, entry.name));
|
|
184
225
|
}
|
|
226
|
+
const providerConfig = path.join(v2, 'provider_config.json');
|
|
227
|
+
const mode = v2Files.some((e) => e.name === 'provider_config.json')
|
|
228
|
+
? 'copied-v2-top-level'
|
|
229
|
+
: 'synthesized';
|
|
230
|
+
if (mode === 'synthesized') writeJson600(providerConfig, synthesizedProviderConfig(key));
|
|
231
|
+
else rewriteProviderConfig(providerConfig, key);
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
233
|
+
// cli/config.json: live copy (builtin:zai key swapped) + this run's MCP set.
|
|
234
|
+
const cfg = readJson(path.join(live, 'cli', 'config.json')) || {};
|
|
235
|
+
const zai = cfg.provider && cfg.provider['builtin:zai'];
|
|
236
|
+
if (zai) {
|
|
237
|
+
zai.options = zai.options || {};
|
|
238
|
+
zai.options.apiKey = key;
|
|
190
239
|
}
|
|
240
|
+
const servers = effectiveMcpServers({ inherit: mcpInherit, mcpSpecs, live });
|
|
241
|
+
cfg.mcp = { ...(cfg.mcp || {}), servers };
|
|
242
|
+
writeJson600(path.join(cliDir, 'config.json'), cfg);
|
|
191
243
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
244
|
+
// Agent profiles + AGENTS.md: mirror the live set exactly.
|
|
245
|
+
const agentsDir = path.join(zdir, 'agents');
|
|
246
|
+
fs.rmSync(agentsDir, { recursive: true, force: true });
|
|
247
|
+
for (const entry of listDir(path.join(live, 'agents'))) {
|
|
248
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
249
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
250
|
+
fs.copyFileSync(path.join(live, 'agents', entry.name), path.join(agentsDir, entry.name));
|
|
199
251
|
}
|
|
200
|
-
const
|
|
201
|
-
if (fs.existsSync(
|
|
202
|
-
|
|
252
|
+
const agentsMd = path.join(zdir, 'AGENTS.md');
|
|
253
|
+
if (fs.existsSync(path.join(live, 'AGENTS.md'))) {
|
|
254
|
+
fs.copyFileSync(path.join(live, 'AGENTS.md'), agentsMd);
|
|
255
|
+
} else {
|
|
256
|
+
fs.rmSync(agentsMd, { force: true });
|
|
203
257
|
}
|
|
204
258
|
|
|
205
|
-
// Skills:
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
259
|
+
// Skills: live user skills (unless --no-skills) + this run's --skills.
|
|
260
|
+
// Project skills (<cwd>/.zcode/skills) need no copy — the engine reads
|
|
261
|
+
// them from the real workspace.
|
|
262
|
+
const skillsDir = path.join(zdir, 'skills');
|
|
263
|
+
fs.rmSync(skillsDir, { recursive: true, force: true });
|
|
264
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
209
265
|
let skillsCopied = 0;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const entry of
|
|
266
|
+
if (!noSkills) {
|
|
267
|
+
const liveSkills = path.join(live, 'skills');
|
|
268
|
+
for (const entry of listDir(liveSkills)) {
|
|
213
269
|
if (!entry.isDirectory()) continue;
|
|
214
270
|
if (!fs.existsSync(path.join(liveSkills, entry.name, 'SKILL.md'))) continue;
|
|
215
|
-
fs.cpSync(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
{ recursive: true }
|
|
219
|
-
);
|
|
271
|
+
fs.cpSync(path.join(liveSkills, entry.name), path.join(skillsDir, entry.name), {
|
|
272
|
+
recursive: true,
|
|
273
|
+
});
|
|
220
274
|
skillsCopied++;
|
|
221
275
|
}
|
|
222
276
|
}
|
|
277
|
+
const skillsInjected = [];
|
|
278
|
+
if (!noSkills) {
|
|
279
|
+
for (const dir of skillsDirs) {
|
|
280
|
+
for (const { name, dir: skillDir } of discoverSkillDirs(dir)) {
|
|
281
|
+
fs.rmSync(path.join(skillsDir, name), { recursive: true, force: true });
|
|
282
|
+
fs.cpSync(skillDir, path.join(skillsDir, name), { recursive: true });
|
|
283
|
+
skillsInjected.push(name);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
223
287
|
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// --- public API -------------------------------------------------------------
|
|
288
|
+
if (root) linkSharedCaches(home, root);
|
|
228
289
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
return {
|
|
291
|
+
home,
|
|
292
|
+
mode,
|
|
293
|
+
copiedFiles: v2Files.length,
|
|
294
|
+
skillsCopied,
|
|
295
|
+
skillsInjected,
|
|
296
|
+
mcp: {
|
|
297
|
+
servers,
|
|
298
|
+
// Names the caller explicitly added — the CLI waits for these to
|
|
299
|
+
// settle before sending the prompt. Inherited servers are best-effort.
|
|
300
|
+
added: mcpSpecs.flatMap((spec) => Object.keys(spec)),
|
|
301
|
+
},
|
|
302
|
+
};
|
|
235
303
|
}
|
|
236
304
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
//
|
|
305
|
+
// --- migration ----------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
// The only names pre-0.4 versions created at a root. migrateRoot moves
|
|
308
|
+
// exactly these out of legacy locations and never touches anything else.
|
|
240
309
|
const OUR_ARTIFACTS = ['credentials.json', 'build-stamp.json', 'injected-skills.json', 'home'];
|
|
241
310
|
|
|
242
311
|
// 0.3.2 migration: vacate our artifacts from (a) the live ~/.zcode, where
|
|
243
312
|
// 0.3.0/0.3.1 wrongly wrote them, and (b) the pre-rename ~/.zai-code root,
|
|
244
|
-
// into the
|
|
245
|
-
//
|
|
313
|
+
// into the ~/.zcode-cli root. Idempotent; never writes or deletes anything
|
|
314
|
+
// else in the live directory.
|
|
246
315
|
function migrateRoot(env = process.env) {
|
|
247
316
|
const root = rootDir(env);
|
|
248
317
|
try {
|
|
249
|
-
fs.mkdirSync(root, { recursive: true });
|
|
318
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
250
319
|
} catch {
|
|
251
|
-
return; // unwritable home —
|
|
320
|
+
return; // unwritable home — callers surface it
|
|
252
321
|
}
|
|
253
|
-
const sources = [
|
|
254
|
-
LIVE_ZCODE, // live dir: move ours out, keep everything else
|
|
255
|
-
path.join(os.homedir(), '.zai-code'), // pre-rename root: wholly ours
|
|
256
|
-
];
|
|
322
|
+
const sources = [liveDir(env), path.join(os.homedir(), '.zai-code')];
|
|
257
323
|
for (const dir of sources) {
|
|
258
324
|
let entries;
|
|
259
325
|
try {
|
|
260
326
|
entries = new Set(fs.readdirSync(dir));
|
|
261
327
|
} catch {
|
|
262
|
-
continue;
|
|
328
|
+
continue;
|
|
263
329
|
}
|
|
264
330
|
for (const name of OUR_ARTIFACTS) {
|
|
265
331
|
if (!entries.has(name)) continue;
|
|
266
332
|
const src = path.join(dir, name);
|
|
267
333
|
const dest = path.join(root, name);
|
|
268
334
|
try {
|
|
269
|
-
if (fs.existsSync(dest)) {
|
|
270
|
-
|
|
271
|
-
} else {
|
|
272
|
-
fs.renameSync(src, dest);
|
|
273
|
-
}
|
|
335
|
+
if (fs.existsSync(dest)) fs.rmSync(src, { recursive: true, force: true });
|
|
336
|
+
else fs.renameSync(src, dest);
|
|
274
337
|
} catch {
|
|
275
338
|
/* keep going; a missing artifact just rebuilds */
|
|
276
339
|
}
|
|
@@ -278,64 +341,8 @@ function migrateRoot(env = process.env) {
|
|
|
278
341
|
}
|
|
279
342
|
}
|
|
280
343
|
|
|
281
|
-
// Ensures the scratch HOME exists and is current. Returns
|
|
282
|
-
// {root, home, mode, built, stamp}.
|
|
283
|
-
function ensureIsolation(key, { fresh = false, env = process.env } = {}) {
|
|
284
|
-
migrateRoot(env);
|
|
285
|
-
const root = rootDir(env);
|
|
286
|
-
fs.mkdirSync(root, { recursive: true });
|
|
287
|
-
fs.chmodSync(root, 0o700);
|
|
288
|
-
const home = path.join(root, 'home');
|
|
289
|
-
const stampFile = path.join(root, 'build-stamp.json');
|
|
290
|
-
|
|
291
|
-
const sources = sourceInventory();
|
|
292
|
-
const fingerprint = keySha256(key);
|
|
293
|
-
const desired = {
|
|
294
|
-
version: 1,
|
|
295
|
-
keySha256: fingerprint,
|
|
296
|
-
sources,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (!fresh && fs.existsSync(home) && fs.existsSync(stampFile)) {
|
|
300
|
-
try {
|
|
301
|
-
const stamp = JSON.parse(fs.readFileSync(stampFile, 'utf8'));
|
|
302
|
-
const sameSources =
|
|
303
|
-
Array.isArray(stamp.sources) &&
|
|
304
|
-
stamp.sources.length === desired.sources.length &&
|
|
305
|
-
stamp.sources.every(
|
|
306
|
-
(s, i) =>
|
|
307
|
-
s.file === desired.sources[i].file && s.mtimeMs === desired.sources[i].mtimeMs
|
|
308
|
-
);
|
|
309
|
-
if (stamp.keySha256 === fingerprint && sameSources) {
|
|
310
|
-
return {
|
|
311
|
-
root,
|
|
312
|
-
home,
|
|
313
|
-
mode: stamp.mode,
|
|
314
|
-
built: false,
|
|
315
|
-
stamp,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
} catch {
|
|
319
|
-
/* corrupt stamp — rebuild */
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const { mode, copied, skillsCopied } = buildScratch(home, key);
|
|
324
|
-
fs.chmodSync(home, 0o700);
|
|
325
|
-
const stamp = {
|
|
326
|
-
...desired,
|
|
327
|
-
mode,
|
|
328
|
-
copiedFiles: copied,
|
|
329
|
-
skillsCopied,
|
|
330
|
-
builtAt: new Date().toISOString(),
|
|
331
|
-
};
|
|
332
|
-
writeJson600(stampFile, stamp);
|
|
333
|
-
return { root, home, mode, built: true, stamp };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
344
|
// Env for the engine child process (§6.3): HOME=scratch, pinned builtin
|
|
337
|
-
// config, and the uncontrolled overrides deleted. cwd is
|
|
338
|
-
// the caller sets it to the user's real workspace.
|
|
345
|
+
// config, and the uncontrolled overrides deleted. cwd is set by the caller.
|
|
339
346
|
function engineEnv({ home, builtinProviderConfig, parentEnv = process.env }) {
|
|
340
347
|
const childEnv = { ...parentEnv };
|
|
341
348
|
childEnv.HOME = home;
|
|
@@ -353,11 +360,14 @@ function engineEnv({ home, builtinProviderConfig, parentEnv = process.env }) {
|
|
|
353
360
|
}
|
|
354
361
|
|
|
355
362
|
module.exports = {
|
|
356
|
-
|
|
363
|
+
buildEngineHome,
|
|
357
364
|
engineEnv,
|
|
358
365
|
rootDir,
|
|
366
|
+
liveDir,
|
|
359
367
|
migrateRoot,
|
|
360
|
-
|
|
368
|
+
effectiveMcpServers,
|
|
369
|
+
liveMcpServers,
|
|
370
|
+
discoverSkillDirs,
|
|
361
371
|
LIVE_ZCODE,
|
|
362
372
|
LIVE_V2,
|
|
363
373
|
};
|
package/lib/key.js
CHANGED
|
@@ -5,16 +5,7 @@
|
|
|
5
5
|
// in this package.
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
-
const
|
|
9
|
-
const { migrateRoot } = require('./isolation.js');
|
|
10
|
-
|
|
11
|
-
// Root of zcode's own state (scratch HOME lives under <root>/home).
|
|
12
|
-
// MUST NOT be ~/.zcode — that is the desktop app's live directory the
|
|
13
|
-
// engine itself owns (0.3.0/0.3.1 regression: the rename made the CLI
|
|
14
|
-
// write credentials and the scratch HOME straight into it).
|
|
15
|
-
function rootDir(env = process.env) {
|
|
16
|
-
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode-cli');
|
|
17
|
-
}
|
|
8
|
+
const { migrateRoot, rootDir } = require('./isolation.js');
|
|
18
9
|
|
|
19
10
|
function credentialsPath(env = process.env) {
|
|
20
11
|
return path.join(rootDir(env), 'credentials.json');
|