@coderifts/agent-hooks 0.3.4 → 0.3.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hooks",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Ask CodeRifts before Claude Code writes a contract artifact (OpenAPI, AsyncAPI, GraphQL, protobuf, MCP manifest).",
5
5
  "author": {
6
6
  "name": "CodeRifts",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 — 2026-10-09
4
+ ### Fixed
5
+ - **The Claude Code hook runs when invoked through a symlinked path.** The entry check resolves the invoked path and this file. The same check in `scripts/sync-version.mjs` does too. If the check cannot tell, the entry runs.
6
+ - **contract-write 1.3.2 (no decision changes).** The generated copy adds `readPlan` and `pendingListings`.
7
+
3
8
  ## 0.3.4 — 2026-10-09
4
9
  ### Fixed
5
10
  - **A plain `mcp.json` that is both an MCP client configuration and a tool manifest, or that does not parse, is not
@@ -16,9 +16,8 @@
16
16
  * directory holding a contract without naming it (find -exec, xargs, a glob, a tree-wide git
17
17
  * restore, an interpreter, a pipe into sh) asks.
18
18
  */
19
- import { resolve } from "node:path";
20
- import { pathToFileURL } from "node:url";
21
19
  import { afterText, decideToolCall } from "../contract-write.mjs";
20
+ import { invokedDirectly } from "../entry-guard.mjs";
22
21
  import { createGate, DOES_NOT_PROVE, governanceUnavailable, hostIo } from "../gate.js";
23
22
 
24
23
  /** The file after an Edit (`params` is the edit) or a MultiEdit (`params.edits`, in order): contract-write's afterText. */
@@ -166,8 +165,6 @@ async function main() {
166
165
  process.exit(out.exitCode);
167
166
  }
168
167
 
169
- const invokedDirectly =
170
- Boolean(process.argv[1]) && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
171
- if (invokedDirectly) {
168
+ if (invokedDirectly(process.argv[1], import.meta.url)) {
172
169
  main();
173
170
  }
@@ -5,7 +5,7 @@
5
5
  "repo": "coderifts/agent-hooks"
6
6
  },
7
7
  "description": "Host adapters for CodeRifts: Claude Code PreToolUse asks before Write/Edit of OpenAPI, AsyncAPI, GraphQL, protobuf, or MCP manifest files. Not the GitHub Action (coderifts/contract-gate) and not the guard library (@coderifts/agent-guard).",
8
- "version": "0.3.4",
8
+ "version": "0.3.5",
9
9
  "author": {
10
10
  "name": "CodeRifts",
11
11
  "email": "peter@coderifts.com"
@@ -27,7 +27,7 @@
27
27
  // written by scripts/generate-contract-write-copies.js, byte for byte (the CommonJS twin is a
28
28
  // mechanical transform), and its --check fails on any difference. Do not edit a copy.
29
29
 
30
- export const CONTRACT_WRITE_VERSION = '1.3.1';
30
+ export const CONTRACT_WRITE_VERSION = '1.3.2';
31
31
 
32
32
  /** What a shell write to a named contract file gets, and what an unnamed one gets. */
33
33
  export const SHELL_NAMED_DECISION = 'refuse';
@@ -712,6 +712,67 @@ export async function findUnder(listDir, root, match, limits = LISTING_LIMITS) {
712
712
  return { found: null, complete: true };
713
713
  }
714
714
 
715
+ // ---- what a host reads ahead of the decision ---------------------------------------------------
716
+
717
+ /*
718
+ * 1.3.2 (2026-10-09, the CodeRifts mod, plugin 1.2.9): a host that cannot let this module call its
719
+ * readers (the Claude directory reads every `return` in a hook's text as the hook's answer, and every
720
+ * mods API call reached through a helper is listed "via" it) reads AHEAD instead: readPlan names every
721
+ * file and every listing root decideToolCall can ask for on this call, and pendingListings names the
722
+ * directories the walk under those roots can still ask for, given the listings already read. The host
723
+ * reads those, then passes decideToolCall an io that only looks the answers up. Pure: no I/O here.
724
+ */
725
+
726
+ /**
727
+ * The reads decideToolCall can make for one call, from the same functions it decides with:
728
+ * files every path it can pass to io.readFile (verbatim)
729
+ * roots every listing root it can walk with io.listDir (findUnder's start, before normalizing)
730
+ * A superset: decideShell reads the named content-decided files of every target, and walks the
731
+ * scopes only when no target names a contract or a held file.
732
+ */
733
+ export function readPlan(call, io = {}) {
734
+ const kind = toolKind(call && (call.tool ?? call.tool_name));
735
+ const input = (call && (call.input ?? call.tool_input)) || {};
736
+ const opts = { named: io.named || [], excluded: io.excluded || [] };
737
+ if (kind === 'Bash') {
738
+ const files = [];
739
+ const roots = [];
740
+ const root = normalizePath(io.cwd || '');
741
+ for (const { target } of shellWrites(String(field(input, 'command') ?? ''))) {
742
+ const inTree = scopeInTree(target, io);
743
+ if (inTree === null) continue;
744
+ const type = inTree !== UNKNOWN && !/[*?]/.test(inTree) ? contractType(inTree, opts) : null;
745
+ if (type) {
746
+ if (MCP_JSON_BY_CONTENT.test(inTree)) files.push(root ? `${root}/${inTree}` : inTree);
747
+ } else {
748
+ roots.push(listingRoot(inTree));
749
+ }
750
+ }
751
+ return { files: [...new Set(files)], roots: [...new Set(roots)] };
752
+ }
753
+ if (!kind) return { files: [], roots: [] };
754
+ const filePath = filePathOf(input);
755
+ if (!filePath) return { files: [], roots: [] };
756
+ return contractType(relativePath(io.cwd, filePath), opts) ? { files: [filePath], roots: [] } : { files: [], roots: [] };
757
+ }
758
+
759
+ /**
760
+ * The directories the walk under `roots` can still ask for, given `listings` ({ dir: entries | null },
761
+ * the one-level answers already read, keyed as findUnder asks for them). The walk is findUnder itself,
762
+ * never stopping at a match, within the same limits — so the directories decideToolCall's walk asks
763
+ * for are a subset of what this keeps naming until it names none.
764
+ */
765
+ export async function pendingListings(roots, listings = {}, limits = LISTING_LIMITS) {
766
+ const missing = new Set();
767
+ const listDir = async (dir) => {
768
+ if (Object.prototype.hasOwnProperty.call(listings, dir)) return listings[dir];
769
+ missing.add(dir);
770
+ return null;
771
+ };
772
+ for (const r of roots || []) await findUnder(listDir, r, () => false, limits);
773
+ return [...missing];
774
+ }
775
+
715
776
  // ---- the decision ------------------------------------------------------------------------------
716
777
 
717
778
  /**
@@ -0,0 +1,19 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ /**
5
+ * Whether `moduleUrl` is the process entry.
6
+ *
7
+ * Node sets `import.meta.url` to the real path of the module and leaves
8
+ * `process.argv[1]` as the path it was invoked with. Both sides are realpath'd.
9
+ * No argv, no module URL, or a realpath that throws → true (the entry runs).
10
+ * A resolved path that is a different file → false (this module was imported).
11
+ */
12
+ export function invokedDirectly(argv1, moduleUrl) {
13
+ if (!argv1 || !moduleUrl) return true;
14
+ try {
15
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
16
+ } catch {
17
+ return true;
18
+ }
19
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "coderifts-contract-gate",
3
3
  "name": "CodeRifts contract gate",
4
4
  "description": "Ask CodeRifts before an agent writes a contract artifact.",
5
- "version": "0.3.4",
5
+ "version": "0.3.5",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderifts/agent-hooks",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Ask CodeRifts before an agent writes a contract artifact (OpenClaw before_tool_call and Claude Code PreToolUse).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/coderifts/agent-hooks#readme",
@@ -15,6 +15,7 @@
15
15
  "main": "./index.js",
16
16
  "files": [
17
17
  "index.js",
18
+ "entry-guard.mjs",
18
19
  "gate.js",
19
20
  "contract-write.mjs",
20
21
  "openclaw.plugin.json",