@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 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 this package owns (`~/.zcode/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.
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
 
@@ -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'
@@ -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. Returns an absolute file path or null.
36
- function resolveCandidate(candidate) {
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()) return path.resolve(candidate);
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()) return path.resolve(inner);
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). Throws EngineNotFoundError listing all probes.
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 → ~/.zcode/credentials.json
3
- // ({"apiKey":"…"}, mode 600, written by `zcode login`). The key is never
4
- // printed, logged, or echoed by anything in this package.
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.0",
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,