kenaz 0.3.1 → 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.
@@ -29,6 +29,7 @@ function skillText(core) {
29
29
  return '---\nname: "kenaz"\ndescription: "Kenaz mode (off|assist|full|status|init) or Yggdrasil (ygg join|status|leave). Use only when the user invokes $kenaz."\n---\n\n'
30
30
  + 'Run exactly this shell command from the current project directory, appending the words the user wrote after $kenaz (none = print the current mode), and report its output, nothing else:\n\n'
31
31
  + '`node "' + script + '" <words after $kenaz>`\n\n'
32
+ + '`init` and `ygg ...` talk to kenaz-server on http://127.0.0.1:18080. The sandbox blocks network, so run the command with network access (request escalated permissions); otherwise it reports the server as unreachable even when it is running.\n\n'
32
33
  + 'Modes: full = everything; assist = silent bookkeeping only; off = every Kenaz hook exits immediately.\n';
33
34
  }
34
35
 
@@ -258,17 +258,26 @@ const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionSt
258
258
 
259
259
  function probeNativeHooksSupported() {
260
260
  if (!isCodexInstalled()) return false;
261
+ const list = () => execSync('codex features list', { stdio: 'pipe', timeout: 8000, windowsHide: true }).toString();
261
262
  try {
262
- const out = execSync('codex features list', { stdio: 'pipe', timeout: 8000, windowsHide: true }).toString();
263
- return /^\s*(?:codex_hooks|hooks)\b.*\btrue\s*$/im.test(out);
263
+ const out = list();
264
+ if (/^\s*(?:codex_hooks|hooks)\b.*\btrue\s*$/im.test(out)) return true;
265
+ // codex 0.160 ships `hooks` as a stable feature that is off by default (field report 2026-10-10):
266
+ // turn it on with codex's own command (writes [features] hooks = true), then re-check.
267
+ const off = out.match(/^\s*(codex_hooks|hooks)\b.*\bfalse\s*$/im);
268
+ if (!off) return false;
269
+ execSync(`codex features enable ${off[1]}`, { stdio: 'pipe', timeout: 8000, windowsHide: true });
270
+ return /^\s*(?:codex_hooks|hooks)\b.*\btrue\s*$/im.test(list());
264
271
  } catch {
265
272
  return false;
266
273
  }
267
274
  }
268
275
 
269
- function getHookShimPath(projectRoot) {
270
- const root = projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
271
- return path.resolve(root, HOOK_SHIM_REL);
276
+ // The shim ships next to this file in whatever core runs setup (repo checkout or ~/.kenaz/core from
277
+ // npm). Resolving from the cwd pointed npm installs at <project>/.formation/... which does not exist.
278
+ // projectRoot is kept in the signature for callers; it no longer changes the result.
279
+ function getHookShimPath(_projectRoot) {
280
+ return path.resolve(__dirname, '..', '..', HOOK_SHIM_REL);
272
281
  }
273
282
 
274
283
  function areHooksAlreadyRegistered(projectRoot) {
@@ -15,6 +15,9 @@ const FAKE = `
15
15
  const fs=require('fs'),path=require('path');const a=process.argv.slice(2);
16
16
  const cfg=path.join(process.env.CODEX_HOME,'config.toml');
17
17
  if(a[0]==='--version'){console.log('codex-cli 0.0.0');process.exit(0);}
18
+ const hf=path.join(process.env.CODEX_HOME,'hooks_on');
19
+ if(a[0]==='features'&&a[1]==='list'){console.log('hooks stable '+(fs.existsSync(hf)?'true':'false'));process.exit(0);}
20
+ if(a[0]==='features'&&a[1]==='enable'&&a[2]==='hooks'){if(process.env.FAKE_NO_ENABLE)process.exit(2);fs.writeFileSync(hf,'');process.exit(0);}
18
21
  if(a[0]==='mcp'&&a[1]==='remove'){const t=fs.readFileSync(cfg,'utf8').split(/\\r?\\n/);let skip=false;const o=[];
19
22
  for(const l of t){if(/^\\s*\\[/.test(l))skip=l.trim().startsWith('[mcp_servers.'+a[2]);if(!skip)o.push(l);}
20
23
  fs.writeFileSync(cfg,o.join('\\n'));process.exit(0);}
@@ -80,3 +83,20 @@ test('setup with the MCP entry already present still repairs the rest (skill ins
80
83
  assert.strictEqual(s.status, 0, s.stderr);
81
84
  assert.ok(fs.existsSync(path.join(f.codexHome, 'skills', 'kenaz', 'SKILL.md')), 'early return on already_registered skipped the $kenaz skill');
82
85
  });
86
+
87
+ test('hook shim path comes from the core that runs setup, not the cwd (npm installs run from a project dir)', () => {
88
+ const s = require('./setup-codex');
89
+ const p = s.getHookShimPath(path.join(os.tmpdir(), 'some-project'));
90
+ assert.ok(fs.existsSync(p), p);
91
+ assert.strictEqual(path.dirname(p), path.join(__dirname, 'openai'));
92
+ });
93
+
94
+ test('codex with the stable hooks feature switched off (0.160 default): setup enables it and registers the hooks', () => {
95
+ const f = fixture();
96
+ fs.writeFileSync(f.cfg, '');
97
+ const s = f.run('setup');
98
+ assert.strictEqual(s.status, 0, s.stderr);
99
+ assert.ok(fs.existsSync(path.join(f.codexHome, 'hooks_on')), 'codex features enable hooks was not run');
100
+ const cfg = fs.readFileSync(f.cfg, 'utf8');
101
+ for (const ev of ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'Stop']) assert.ok(cfg.includes('[[hooks.' + ev + ']]'), ev);
102
+ });
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.3.2
@@ -9,4 +9,4 @@ Run exactly this and report its output (one line for modes; the full block for `
9
9
 
10
10
  Modes: full = guidance + all hooks; assist = silent telemetry/bookkeeping hooks only (no context injection); off = every Kenaz hook exits immediately. init creates .kenaz/project.yaml so hooks act in a fresh project.
11
11
 
12
- `ygg join <key> [--project|--global]` stores the Yggdrasil connection key (default: this project's own slot) and activates collaboration here; `ygg status` shows room, node, relay, peers and mail (key masked); `ygg leave` removes this project's key. Needs the Kenaz backend (Dashboard) running.
12
+ `ygg join <key> [--project|--global]` stores the Yggdrasil connection key (default: this project's own slot) and activates collaboration here; `ygg status` shows room, node, relay, peers and mail (key masked); `ygg leave` removes this project's key. Needs kenaz-server running (installed by `npx kenaz install`).
@@ -29,10 +29,16 @@ if (MODES.includes(arg)) {
29
29
  fetch('http://127.0.0.1:' + port + '/api/health', { signal: AbortSignal.timeout(2000) })
30
30
  .then((r) => r.ok, () => false)
31
31
  .then((up) => {
32
- console.log('Kenaz backend: ' + (up ? 'running on port ' + port : 'NOT reachable on port ' + port + ' (run: node dashboard/scripts/install-kenaz-cli.js install, or start the Dashboard)'));
33
- console.log('Optional: /kenaz ygg join <key> multi-device collaboration (Yggdrasil)');
34
- console.log('Optional: k-cli alaya register cross-project memory (Alaya, device-code login)');
35
- console.log('Next step: /Ansuz');
32
+ // Codex runs shell commands in a sandbox without network (it sets CODEX_SANDBOX_NETWORK_DISABLED=1),
33
+ // so 127.0.0.1 is unreachable from here even when kenaz-server is fine (field report, macOS Codex).
34
+ const codex = !!(process.env.CODEX_SANDBOX_NETWORK_DISABLED || process.env.CODEX_THREAD_ID || process.env.CODEX_SANDBOX);
35
+ const cmd = codex ? '$kenaz' : '/kenaz:kenaz';
36
+ const down = codex && process.env.CODEX_SANDBOX_NETWORK_DISABLED
37
+ ? 'not reachable from the Codex sandbox (no network). Check from a normal terminal: npx kenaz doctor'
38
+ : 'NOT reachable on port ' + port + ' (check: npx kenaz doctor; reinstall: npx kenaz@latest install)';
39
+ console.log('kenaz-server: ' + (up ? 'running on port ' + port : down));
40
+ console.log('Optional: ' + cmd + ' ygg join <key> multi-device collaboration (Yggdrasil)');
41
+ console.log('Next step: ' + (codex ? 'ask Codex to start Ansuz for this project' : '/kenaz:Ansuz'));
36
42
  });
37
43
  } else {
38
44
  const env = process.env.KENAZ_MODE ? ' (env KENAZ_MODE overrides files)' : '';
package/lib/cli.js CHANGED
@@ -14,7 +14,7 @@ const dash = require('./dashboard');
14
14
  const PKG_ROOT = path.resolve(__dirname, '..');
15
15
  const PKG = require('../package.json');
16
16
  const MIN_NODE = 18;
17
- const USAGE = 'usage: kenaz <install|uninstall|status|doctor> [--dry-run] [--home <dir>] [--no-service] [--no-claude] [--no-codex] [--keep-dashboard] [--server-bin <path>]';
17
+ const USAGE = 'usage: kenaz <install|uninstall|status|doctor|init|ygg join <key>|ygg status|ygg leave> [--dry-run] [--home <dir>] [--no-service] [--no-claude] [--no-codex] [--keep-dashboard] [--server-bin <path>]';
18
18
 
19
19
  function parseArgs(argv) {
20
20
  const o = { cmd: argv[0], flags: {} };
@@ -226,7 +226,20 @@ async function doctor(p, flags, out, deps = {}) {
226
226
  return rows.some((r) => r[1] === 'FAIL') ? 1 : 0;
227
227
  }
228
228
 
229
+ // `kenaz ygg join|status|leave ...` and `kenaz init`: short forms of the same core scripts the
230
+ // /kenaz (Claude) and $kenaz (Codex) commands run, from a normal terminal in the project folder.
231
+ function passthrough(argv, deps) {
232
+ const p = paths(os.homedir());
233
+ const core = fs.existsSync(path.join(p.core, 'plugins', 'kenaz')) ? p.core : path.join(PKG_ROOT, 'core');
234
+ const script = argv[0] === 'ygg' ? 'ygg.js' : 'kenaz-mode.js';
235
+ const args = argv[0] === 'ygg' ? argv.slice(1) : argv;
236
+ const spawn = deps.spawnSync || require('child_process').spawnSync;
237
+ const r = spawn(process.execPath, [path.join(core, 'plugins', 'kenaz', 'scripts', script), ...args], { stdio: 'inherit', cwd: process.cwd() });
238
+ return r.status === null ? 1 : r.status;
239
+ }
240
+
229
241
  function run(argv, out = console.log, deps = {}) {
242
+ if (argv[0] === 'ygg' || argv[0] === 'init') return passthrough(argv, deps);
230
243
  const { cmd, flags } = parseArgs(argv);
231
244
  const fn = { install, uninstall, status, doctor }[cmd];
232
245
  if (!fn) { out(USAGE); return 2; }
package/lib/cli.test.js CHANGED
@@ -46,3 +46,14 @@ test('setup-codex uninstall strips only kenaz shim hook blocks and keeps user ho
46
46
  fs.rmSync(codexHome, { recursive: true, force: true });
47
47
  }
48
48
  });
49
+
50
+ test('kenaz ygg / init pass through to the core scripts with the remaining args', () => {
51
+ const calls = [];
52
+ const spawnSync = (exe, args) => { calls.push(args); return { status: 0 }; };
53
+ assert.strictEqual(run(['ygg', 'join', 'KEY', '--global'], () => {}, { spawnSync }), 0);
54
+ assert.strictEqual(run(['init'], () => {}, { spawnSync }), 0);
55
+ assert.match(calls[0][0].split(path.sep).join('/'), /plugins\/kenaz\/scripts\/ygg\.js$/);
56
+ assert.deepStrictEqual(calls[0].slice(1), ['join', 'KEY', '--global']);
57
+ assert.match(calls[1][0].split(path.sep).join('/'), /plugins\/kenaz\/scripts\/kenaz-mode\.js$/);
58
+ assert.deepStrictEqual(calls[1].slice(1), ['init']);
59
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kenaz",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Install Kenaz (plugin, hooks, kenaz-server) for Claude Code and Codex: npx kenaz install",
5
5
  "bin": {
6
6
  "kenaz": "bin/kenaz.js"
@@ -20,11 +20,11 @@
20
20
  "test": "node --test lib/cli.test.js"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@kenazai/server-darwin-arm64": "0.3.1",
24
- "@kenazai/server-darwin-x64": "0.3.1",
25
- "@kenazai/server-linux-arm64": "0.3.1",
26
- "@kenazai/server-linux-x64": "0.3.1",
27
- "@kenazai/server-win32-x64": "0.3.1"
23
+ "@kenazai/server-darwin-arm64": "0.3.2",
24
+ "@kenazai/server-darwin-x64": "0.3.2",
25
+ "@kenazai/server-linux-arm64": "0.3.2",
26
+ "@kenazai/server-linux-x64": "0.3.2",
27
+ "@kenazai/server-win32-x64": "0.3.2"
28
28
  },
29
29
  "license": "UNLICENSED"
30
30
  }