@0xgeorgemathew/zcode 0.3.0 → 0.3.2
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 +2 -2
- package/lib/commands/doctor.js +1 -1
- package/lib/commands/run.js +2 -2
- package/lib/engine.js +47 -12
- package/lib/isolation.js +51 -1
- package/lib/key.js +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,9 +213,9 @@ Event types on the way in:
|
|
|
213
213
|
|
|
214
214
|
## Under the hood
|
|
215
215
|
|
|
216
|
-
**Key handling.** `ZAI_API_KEY` env var, or `zcode login` which stores it at `~/.zcode/credentials.json` (mode 600). The key is never printed or logged. The only file it ever lands in is the scratch provider config, which the engine requires to register the provider.
|
|
216
|
+
**Key handling.** `ZAI_API_KEY` env var, or `zcode login` which stores it at `~/.zcode-cli/credentials.json` (mode 600). The key is never printed or logged. The only file it ever lands in is the scratch provider config, which the engine requires to register the provider.
|
|
217
217
|
|
|
218
|
-
**Isolation.** The engine runs with `HOME` pointed at a scratch copy
|
|
218
|
+
**Isolation.** This package's own state lives under `~/.zcode-cli` (never under `~/.zcode`, which belongs to the desktop app; if an older 0.3.x left files there, they are migrated out automatically on first run). The engine runs with `HOME` pointed at a scratch copy we own (`~/.zcode-cli/home`, mode 700, mtime-cached). Your live `~/.zcode`, which the desktop app is using right now, is never written to. Your OAuth `credentials.json` is deliberately never copied. The scratch keeps exactly the `zai-api` provider rule with your key, your agents, and your skills.
|
|
219
219
|
|
|
220
220
|
**Yolo mode.** Sessions run with auto-approved tool side effects, matching the engine's own headless default; `--cwd` is the declared blast radius. A headless run that requests interactive user input fails fast with exit 5 rather than hanging.
|
|
221
221
|
|
package/lib/commands/doctor.js
CHANGED
|
@@ -85,7 +85,7 @@ module.exports = async function doctor(parsed) {
|
|
|
85
85
|
(isolation.stamp && typeof isolation.stamp.skillsCopied === 'number'
|
|
86
86
|
? isolation.stamp.skillsCopied
|
|
87
87
|
: 0) || 0,
|
|
88
|
-
mcpServersInherited: Object.keys(effectiveMcpServers({})).length,
|
|
88
|
+
mcpServersInherited: Object.keys(effectiveMcpServers({ inherit: true })).length,
|
|
89
89
|
experimental:
|
|
90
90
|
isolation.mode === 'synthesized'
|
|
91
91
|
? 'synthesized config path is unproven; copy a real ~/.zcode/v2 or verify once on a clean HOME'
|
package/lib/commands/run.js
CHANGED
|
@@ -92,6 +92,8 @@ module.exports = async function run(parsed) {
|
|
|
92
92
|
if (flags.includeReasoning) result.reasoning = turn.reasoningText;
|
|
93
93
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
94
94
|
} else {
|
|
95
|
+
if (!streamedText.trim()) process.stdout.write(turn.response);
|
|
96
|
+
process.stdout.write('\n');
|
|
95
97
|
reporter.footer({
|
|
96
98
|
model: turn.engineModel || flags.model,
|
|
97
99
|
effort: turn.engineReasoningLevel || flags.effort,
|
|
@@ -99,8 +101,6 @@ module.exports = async function run(parsed) {
|
|
|
99
101
|
durationMs: turn.durationMs,
|
|
100
102
|
sessionId: turn.sessionId,
|
|
101
103
|
});
|
|
102
|
-
if (!streamedText.trim()) process.stdout.write(turn.response);
|
|
103
|
-
process.stdout.write('\n');
|
|
104
104
|
}
|
|
105
105
|
client.kill();
|
|
106
106
|
process.exit(0);
|
package/lib/engine.js
CHANGED
|
@@ -31,20 +31,50 @@ class BuiltinConfigNotFoundError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Our own wrapper's real path. The engine resolver must never resolve to
|
|
35
|
+
// itself: `npm install -g` puts a `zcode` symlink on PATH, and before the
|
|
36
|
+
// 0.3.1 fix the PATH probe grabbed it as "the engine" (0.3.0 regression:
|
|
37
|
+
// every command then exited 4 probing <prefix>/config/provider/…).
|
|
38
|
+
const SELF_BIN_PATH = (() => {
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync(path.join(__dirname, '..', 'bin', 'zcode.js'));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
function isSelf(candidate) {
|
|
47
|
+
if (!SELF_BIN_PATH) return false;
|
|
48
|
+
try {
|
|
49
|
+
return fs.realpathSync(candidate) === SELF_BIN_PATH;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
// Resolves a candidate that is either the zcode.cjs file itself or a
|
|
35
|
-
// directory containing it.
|
|
36
|
-
|
|
56
|
+
// directory containing it. strictFile (default true for everything except
|
|
57
|
+
// an explicit ZAI_CODE_ENGINE): a bare file is only accepted when it is
|
|
58
|
+
// literally named zcode.cjs — the engine bundle — so wrappers and launchers
|
|
59
|
+
// named `zcode` never pass. Our own binary is always rejected.
|
|
60
|
+
function resolveCandidate(candidate, { strictFile = true } = {}) {
|
|
61
|
+
if (isSelf(candidate)) return null;
|
|
37
62
|
let st;
|
|
38
63
|
try {
|
|
39
64
|
st = fs.statSync(candidate);
|
|
40
65
|
} catch {
|
|
41
66
|
return null;
|
|
42
67
|
}
|
|
43
|
-
if (st.isFile())
|
|
68
|
+
if (st.isFile()) {
|
|
69
|
+
if (strictFile && path.basename(candidate) !== 'zcode.cjs') return null;
|
|
70
|
+
return path.resolve(candidate);
|
|
71
|
+
}
|
|
44
72
|
if (st.isDirectory()) {
|
|
45
73
|
const inner = path.join(candidate, 'zcode.cjs');
|
|
46
74
|
try {
|
|
47
|
-
if (fs.statSync(inner).isFile()
|
|
75
|
+
if (fs.statSync(inner).isFile() && !isSelf(inner)) {
|
|
76
|
+
return path.resolve(inner);
|
|
77
|
+
}
|
|
48
78
|
} catch {
|
|
49
79
|
/* not there */
|
|
50
80
|
}
|
|
@@ -66,20 +96,25 @@ function whichOnPath(bin, envPath) {
|
|
|
66
96
|
return null;
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
// First hit wins (DESIGN §4)
|
|
99
|
+
// First hit wins (DESIGN §4, revised 0.3.1): explicit env override, then
|
|
100
|
+
// the app bundle (the canonical macOS location), then PATH/home probes —
|
|
101
|
+
// which now only accept a real zcode.cjs, never a same-named wrapper.
|
|
102
|
+
// Throws EngineNotFoundError listing all probes.
|
|
70
103
|
function resolveEngine(env = process.env) {
|
|
71
104
|
const probed = [];
|
|
72
105
|
const candidates = [];
|
|
73
|
-
if (env.ZAI_CODE_ENGINE) candidates.push(env.ZAI_CODE_ENGINE);
|
|
106
|
+
if (env.ZAI_CODE_ENGINE) candidates.push({ path: env.ZAI_CODE_ENGINE, strictFile: false });
|
|
107
|
+
candidates.push({ path: APP_BUNDLE_ENGINE });
|
|
74
108
|
const onPath = whichOnPath('zcode', env.PATH);
|
|
75
|
-
if (onPath) candidates.push(onPath);
|
|
76
|
-
candidates.push(path.join(os.homedir(), '.local', 'bin', 'zcode'));
|
|
77
|
-
candidates.push(path.join(os.homedir(), '.zcode', 'runtime', 'zcode.cjs'));
|
|
78
|
-
candidates.push(APP_BUNDLE_ENGINE);
|
|
109
|
+
if (onPath) candidates.push({ path: onPath });
|
|
110
|
+
candidates.push({ path: path.join(os.homedir(), '.local', 'bin', 'zcode') });
|
|
111
|
+
candidates.push({ path: path.join(os.homedir(), '.zcode', 'runtime', 'zcode.cjs') });
|
|
79
112
|
for (const candidate of candidates) {
|
|
80
|
-
const resolved = resolveCandidate(candidate
|
|
113
|
+
const resolved = resolveCandidate(candidate.path, {
|
|
114
|
+
strictFile: candidate.strictFile ?? true,
|
|
115
|
+
});
|
|
81
116
|
if (resolved) return resolved;
|
|
82
|
-
probed.push(candidate);
|
|
117
|
+
probed.push(candidate.path);
|
|
83
118
|
}
|
|
84
119
|
throw new EngineNotFoundError(probed);
|
|
85
120
|
}
|
package/lib/isolation.js
CHANGED
|
@@ -226,13 +226,62 @@ function buildScratch(home, key) {
|
|
|
226
226
|
|
|
227
227
|
// --- public API -------------------------------------------------------------
|
|
228
228
|
|
|
229
|
+
// Root of zcode's own state. MUST NOT be ~/.zcode: that is the desktop
|
|
230
|
+
// app's live directory (0.3.0/0.3.1 regression — the package rename made
|
|
231
|
+
// rootDir collide with it, so login/scratch files landed inside the live
|
|
232
|
+
// config). Ours is ~/.zcode-cli.
|
|
229
233
|
function rootDir(env = process.env) {
|
|
230
|
-
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode');
|
|
234
|
+
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode-cli');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The only file/dir names this package ever creates at its root.
|
|
238
|
+
// migrateRoot moves exactly these out of legacy locations and never
|
|
239
|
+
// touches anything else.
|
|
240
|
+
const OUR_ARTIFACTS = ['credentials.json', 'build-stamp.json', 'injected-skills.json', 'home'];
|
|
241
|
+
|
|
242
|
+
// 0.3.2 migration: vacate our artifacts from (a) the live ~/.zcode, where
|
|
243
|
+
// 0.3.0/0.3.1 wrongly wrote them, and (b) the pre-rename ~/.zai-code root,
|
|
244
|
+
// into the new ~/.zcode-cli root. Idempotent; never writes or deletes
|
|
245
|
+
// anything else in the live directory.
|
|
246
|
+
function migrateRoot(env = process.env) {
|
|
247
|
+
const root = rootDir(env);
|
|
248
|
+
try {
|
|
249
|
+
fs.mkdirSync(root, { recursive: true });
|
|
250
|
+
} catch {
|
|
251
|
+
return; // unwritable home — leave everything as-is; ensureIsolation surfaces it
|
|
252
|
+
}
|
|
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
|
+
];
|
|
257
|
+
for (const dir of sources) {
|
|
258
|
+
let entries;
|
|
259
|
+
try {
|
|
260
|
+
entries = new Set(fs.readdirSync(dir));
|
|
261
|
+
} catch {
|
|
262
|
+
continue; // nothing to migrate from this source
|
|
263
|
+
}
|
|
264
|
+
for (const name of OUR_ARTIFACTS) {
|
|
265
|
+
if (!entries.has(name)) continue;
|
|
266
|
+
const src = path.join(dir, name);
|
|
267
|
+
const dest = path.join(root, name);
|
|
268
|
+
try {
|
|
269
|
+
if (fs.existsSync(dest)) {
|
|
270
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
271
|
+
} else {
|
|
272
|
+
fs.renameSync(src, dest);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
/* keep going; a missing artifact just rebuilds */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
231
279
|
}
|
|
232
280
|
|
|
233
281
|
// Ensures the scratch HOME exists and is current. Returns
|
|
234
282
|
// {root, home, mode, built, stamp}.
|
|
235
283
|
function ensureIsolation(key, { fresh = false, env = process.env } = {}) {
|
|
284
|
+
migrateRoot(env);
|
|
236
285
|
const root = rootDir(env);
|
|
237
286
|
fs.mkdirSync(root, { recursive: true });
|
|
238
287
|
fs.chmodSync(root, 0o700);
|
|
@@ -307,6 +356,7 @@ module.exports = {
|
|
|
307
356
|
ensureIsolation,
|
|
308
357
|
engineEnv,
|
|
309
358
|
rootDir,
|
|
359
|
+
migrateRoot,
|
|
310
360
|
keySha256,
|
|
311
361
|
LIVE_ZCODE,
|
|
312
362
|
LIVE_V2,
|
package/lib/key.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// API key resolution (DESIGN §5): ZAI_API_KEY env →
|
|
3
|
-
// ({"apiKey":"…"}, mode 600, written by
|
|
4
|
-
// printed, logged, or echoed by anything
|
|
2
|
+
// API key resolution (DESIGN §5): ZAI_API_KEY env →
|
|
3
|
+
// ~/.zcode-cli/credentials.json ({"apiKey":"…"}, mode 600, written by
|
|
4
|
+
// `zcode login`). The key is never printed, logged, or echoed by anything
|
|
5
|
+
// in this package.
|
|
5
6
|
const fs = require('node:fs');
|
|
6
7
|
const path = require('node:path');
|
|
7
8
|
const os = require('node:os');
|
|
9
|
+
const { migrateRoot } = require('./isolation.js');
|
|
8
10
|
|
|
9
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).
|
|
10
15
|
function rootDir(env = process.env) {
|
|
11
|
-
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode');
|
|
16
|
+
return env.ZAI_CODE_HOME || path.join(os.homedir(), '.zcode-cli');
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
function credentialsPath(env = process.env) {
|
|
@@ -41,6 +46,7 @@ function readCredentialsKey(file) {
|
|
|
41
46
|
// Sources, in order: ZAI_API_KEY (non-empty) → credentials.json.
|
|
42
47
|
// Throws NoKeyError when neither holds a key.
|
|
43
48
|
function resolveKey(env = process.env) {
|
|
49
|
+
migrateRoot(env); // 0.3.2: vacate our artifacts out of the live ~/.zcode first
|
|
44
50
|
const fromEnv = (env.ZAI_API_KEY || '').trim();
|
|
45
51
|
if (fromEnv) return fromEnv;
|
|
46
52
|
const fromFile = readCredentialsKey(credentialsPath(env));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xgeorgemathew/zcode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Headless GLM-5.3 / GLM-5.3-Flash coding CLI for agents: streaming thinking, subagents, MCP servers, skills, session continuity. Wraps the ZCode engine. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|