@nodii/dev 0.6.26 → 0.6.27

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/dist/cli.js CHANGED
@@ -766,10 +766,6 @@ export async function main(argv = process.argv.slice(2)) {
766
766
  return;
767
767
  }
768
768
  if (["claude-figma-pre", "figma-pre"].includes(action)) {
769
- // Fail closed: Claude Code only blocks a tool call on exit code 2, and
770
- // treats any other non-zero exit as a non-blocking error that lets the
771
- // write through. Every failure here (bad stdin, missing config, Dev
772
- // error, lock denied) must therefore exit 2.
773
769
  let payload;
774
770
  try {
775
771
  payload = JSON.parse(await stdinText());
@@ -778,6 +774,9 @@ export async function main(argv = process.argv.slice(2)) {
778
774
  if (hookRuntime(payload) !== "codex") return emit(result, json, "allowed");
779
775
  return;
780
776
  } catch (error) {
777
+ // A known policy denial still blocks. Hook/runtime failures fail open
778
+ // and stay silent so a broken environment cannot stop the session.
779
+ if (!["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED"].includes(error?.code)) return;
781
780
  if (payload && hookRuntime(payload) === "codex") {
782
781
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Blocked Figma write: ${error?.message ?? String(error)}` } }));
783
782
  return;
@@ -791,7 +790,6 @@ export async function main(argv = process.argv.slice(2)) {
791
790
  }
792
791
  }
793
792
  if (action === "safety-pre") {
794
- // A malformed or unavailable safety check must not turn into permission.
795
793
  let payload;
796
794
  try {
797
795
  payload = JSON.parse(await stdinText());
@@ -803,6 +801,9 @@ export async function main(argv = process.argv.slice(2)) {
803
801
  else for (const nudge of nudges) console.log(nudge);
804
802
  return;
805
803
  } catch (error) {
804
+ // Only an evaluated safety rule may deny. Discovery, parsing, git,
805
+ // config and Dev errors fail open and produce no output.
806
+ if (!String(error?.code ?? "").startsWith("SAFETY_")) return;
806
807
  if (payload && hookRuntime(payload) === "codex") {
807
808
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: error?.message ?? String(error) } }));
808
809
  return;
@@ -821,14 +822,7 @@ export async function main(argv = process.argv.slice(2)) {
821
822
  payload = JSON.parse(await stdinText());
822
823
  await recordHookFire(payload).catch(() => {});
823
824
  await safetyPostHook(payload);
824
- } catch (error) {
825
- // A post-hook cannot undo a completed command; report state-tracking
826
- // failure without making Claude treat the completed command as failed.
827
- const message = `nodii-dev safety post hook: ${error?.code ?? "ERROR"}: ${error?.message ?? String(error)}`;
828
- if (payload && hookRuntime(payload) === "codex")
829
- console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message } }));
830
- else console.error(message);
831
- }
825
+ } catch {}
832
826
  return;
833
827
  }
834
828
  if (action === "lane-heartbeat") {
@@ -854,6 +848,7 @@ export async function main(argv = process.argv.slice(2)) {
854
848
  return emit(await postHook(payload), json, "logged");
855
849
  } catch (error) {
856
850
  const outcome = postHookFailure(error);
851
+ if (!outcome.exitCode) return;
857
852
  if (hookRuntime(payload) === "codex")
858
853
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: outcome.message } }));
859
854
  else {
package/dist/core.js CHANGED
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
- export const VERSION = "0.6.26";
7
+ export const VERSION = "0.6.27";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -11,7 +11,7 @@ export const CODEX_HOOK_EVENTS = [
11
11
  ];
12
12
 
13
13
  export const managedHooksDir = (home = homedir()) => join(home, ".config", "nodii-dev", "codex-managed-hooks");
14
- export const MANAGED_DISPATCH_MIN_VERSION = "0.6.26";
14
+ export const MANAGED_DISPATCH_MIN_VERSION = "0.6.27";
15
15
 
16
16
  /** Only a single nodii-dev invocation is allowed; shell composition is never accepted. */
17
17
  export function nodiiCommandArgs(command) {
@@ -110,33 +110,36 @@ function responseOf(stdout) {
110
110
 
111
111
  /** Execute the current generated hook set behind the stable managed dispatcher. */
112
112
  export async function dispatchManagedHook(event, payloadText, deps = {}) {
113
- if (!CODEX_HOOK_EVENTS.includes(event)) throw new CliError("USAGE", `Unknown managed Codex hook event: ${event}`, 2);
114
- const env = deps.env ?? process.env;
115
- if (!laneConfig(env)) return emptyResponse(event);
116
- const dir = deps.dir ?? env.NODII_DEV_MANAGED_HOOKS_DIR ?? managedHooksDir();
117
- let config;
118
- try { config = JSON.parse(await (deps.read ?? readFile)(join(dir, "hooks.json"), "utf8")); }
119
- catch (error) { throw new CliError("HARNESS_SETTINGS_INVALID", `Cannot read managed Codex hooks: ${error.message}`, 2); }
120
- validateManagedHooks(config.hooks ?? {});
121
- const payload = JSON.parse(payloadText);
122
- const target = matchTarget(event, payload);
123
- const handlers = (config.hooks?.[event] ?? []).filter((entry) => {
124
- try { return new RegExp(entry.matcher || ".*").test(target); } catch { return false; }
125
- }).flatMap((entry) => entry.hooks ?? []);
126
- const results = await Promise.all(handlers.map((handler) => (deps.run ?? runHandler)(nodiiCommandArgs(handler.command), payloadText, deps)));
127
- const messages = results.map((result) => responseOf(result.stdout)).filter(Boolean);
128
- const errors = results.filter((result) => result.status !== 0).map((result) => result.stderr.trim() || result.stdout.trim()).filter(Boolean);
129
- if (["PreToolUse", "PermissionRequest"].includes(event)) {
130
- const denied = messages.find((message) => message.hookSpecificOutput?.permissionDecision === "deny");
131
- if (denied) return `${JSON.stringify(denied)}\n`;
132
- if (errors.length) return `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, permissionDecision: "deny", permissionDecisionReason: errors.join("\n") } })}\n`;
133
- const contexts = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
134
- return contexts.length ? `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: contexts.join("\n") } })}\n` : "";
113
+ try {
114
+ if (!CODEX_HOOK_EVENTS.includes(event)) return emptyResponse(event);
115
+ const env = deps.env ?? process.env;
116
+ if (!laneConfig(env)) return emptyResponse(event);
117
+ const dir = deps.dir ?? env.NODII_DEV_MANAGED_HOOKS_DIR ?? managedHooksDir();
118
+ const config = JSON.parse(await (deps.read ?? readFile)(join(dir, "hooks.json"), "utf8"));
119
+ validateManagedHooks(config.hooks ?? {});
120
+ const payload = JSON.parse(payloadText);
121
+ const target = matchTarget(event, payload);
122
+ const handlers = (config.hooks?.[event] ?? []).filter((entry) => {
123
+ try { return new RegExp(entry.matcher || ".*").test(target); } catch { return false; }
124
+ }).flatMap((entry) => entry.hooks ?? []);
125
+ const results = await Promise.all(handlers.map((handler) => (deps.run ?? runHandler)(nodiiCommandArgs(handler.command), payloadText, deps)));
126
+ // A failed handler is unavailable policy, not a denial. Ignore both its
127
+ // output and stderr; doctor is where hook health remains visible.
128
+ const successful = results.filter((result) => result.status === 0);
129
+ const messages = successful.map((result) => responseOf(result.stdout)).filter(Boolean);
130
+ if (["PreToolUse", "PermissionRequest"].includes(event)) {
131
+ const denied = messages.find((message) => message.hookSpecificOutput?.permissionDecision === "deny");
132
+ if (denied) return `${JSON.stringify(denied)}\n`;
133
+ const contexts = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
134
+ return contexts.length ? `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: contexts.join("\n") } })}\n` : "";
135
+ }
136
+ const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
137
+ const plain = successful.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
138
+ if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
139
+ return [...structured, ...plain].join("\n") + ([...structured, ...plain].length ? "\n" : "");
140
+ } catch {
141
+ return emptyResponse(event);
135
142
  }
136
- const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
137
- const plain = results.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
138
- if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
139
- return [...structured, ...plain, ...errors].join("\n") + ([...structured, ...plain, ...errors].length ? "\n" : "");
140
143
  }
141
144
 
142
145
  const versionParts = (value) => String(value ?? "").split(".").map((part) => Number.parseInt(part, 10));
package/dist/nodii-dev.js CHANGED
@@ -1,4 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { fail, main } from "./cli.js";
3
3
 
4
- main().catch(fail);
4
+ // Hooks run in the agent's environment, which may be incomplete or may not be
5
+ // a git worktree. An unavailable hook must never turn into a refused tool call.
6
+ // Intentional policy denials are handled inside the hook command before they
7
+ // reach this boundary.
8
+ main().catch((error) => {
9
+ const denial = String(error?.code ?? "").startsWith("SAFETY_") ||
10
+ ["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED", "WRITE_OUT_OF_SCOPE"].includes(error?.code);
11
+ if (process.argv[2] === "hook" && !denial) {
12
+ process.exitCode = 0;
13
+ return;
14
+ }
15
+ fail(error);
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {