@remnic/plugin-claude-code 9.63.5 → 9.63.7

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.
@@ -21,6 +21,9 @@ const path = require("node:path");
21
21
  const { spawn } = require("node:child_process");
22
22
 
23
23
  const RUNNER = path.join(__dirname, "remnic-cc-hook.cjs");
24
+ // Shared runner source (issue #2483): implementation-detail assertions read
25
+ // the core, while behavioral tests spawn the wrapper entry (RUNNER).
26
+ const CORE = path.join(__dirname, "remnic-hook-core.cjs");
24
27
 
25
28
  function startServer(handler) {
26
29
  const calls = [];
@@ -76,6 +79,12 @@ function runHook(event, input, { port, home, env = {} } = {}) {
76
79
  // Internal worker-propagation channel, not a user credential. Pinned
77
80
  // so an inherited value cannot reach the detached observe worker.
78
81
  REMNIC_HOOK_TOKEN: "",
82
+ // Clear daemon URL env so a developer shell with REMNIC_DAEMON_URL
83
+ // set can't route tests away from the mock server (the shared core
84
+ // honors it since issue #2483). Tests that WANT a daemon URL
85
+ // override it via env.extra (which spreads last).
86
+ REMNIC_DAEMON_URL: "",
87
+ ENGRAM_DAEMON_URL: "",
79
88
  ...env.extra,
80
89
  },
81
90
  stdio: ["pipe", "pipe", "pipe"],
@@ -167,6 +176,44 @@ test("session-start: health probe carries the bearer token (auth-gated daemons)"
167
176
  }
168
177
  });
169
178
 
179
+ test("session-start: REMNIC_DAEMON_URL routes recall to the remote base URL (#2483)", async () => {
180
+ const home = mkHome();
181
+ const { server, port, calls } = await startServer((req, res) => {
182
+ if (req.url === "/engram/v1/health") return res.writeHead(200).end("ok");
183
+ if (req.url === "/engram/v1/recall") {
184
+ return res.writeHead(200, { "Content-Type": "application/json" }).end(
185
+ JSON.stringify({ context: "remote recall", count: 1, mode: "auto" }),
186
+ );
187
+ }
188
+ res.writeHead(404).end();
189
+ });
190
+ try {
191
+ // A full REMNIC_DAEMON_URL must take precedence over the (wrong) PORT —
192
+ // the same remote/central-daemon contract the Codex runner has had since
193
+ // #1571. Issue #2483: the Claude Code hook inherits it via the shared
194
+ // core, so a Claude Code host can also reach a remote daemon over
195
+ // Tailscale/LAN/VPN (plain or TLS).
196
+ const { json } = await runHook(
197
+ "session-start",
198
+ { session_id: "remote-1", cwd: home },
199
+ {
200
+ port: 1, // deliberately unused; daemon URL wins
201
+ home,
202
+ env: { extra: { REMNIC_DAEMON_URL: `http://127.0.0.1:${port}` } },
203
+ },
204
+ );
205
+ assert.equal(json.continue, true);
206
+ assert.match(json.hookSpecificOutput.additionalContext, /remote recall/);
207
+ assert.ok(
208
+ calls.some((c) => c.url === "/engram/v1/recall"),
209
+ "recall reached the REMNIC_DAEMON_URL host",
210
+ );
211
+ } finally {
212
+ server.close();
213
+ fs.rmSync(home, { recursive: true, force: true });
214
+ }
215
+ });
216
+
170
217
  test("recall body targets REMNIC_NAMESPACE when set, and omits namespace when unset", async () => {
171
218
  const mk = () =>
172
219
  startServer((req, res) => {
@@ -471,7 +518,7 @@ test("post-tool-observe: worker payload travels via STDIN, not the environment",
471
518
  // (big file edits) would E2BIG; the worker reads stdin instead. We assert
472
519
  // the source rather than running an E2BIG payload because reproducing the
473
520
  // limit cross-platform is impractical.
474
- const src = fs.readFileSync(RUNNER, "utf8");
521
+ const src = fs.readFileSync(CORE, "utf8");
475
522
  assert.doesNotMatch(
476
523
  src,
477
524
  /REMNIC_HOOK_INPUT:\s*rawInput/,
@@ -804,7 +851,7 @@ test("plugin.json: manifest conforms to the Claude Code plugin schema (author is
804
851
  });
805
852
 
806
853
  test("runner source: payload fields never reach a shell — spawn uses fixed argv (#1518 guard shell interpolation)", () => {
807
- const src = fs.readFileSync(RUNNER, "utf8");
854
+ const src = fs.readFileSync(CORE, "utf8");
808
855
  // Every spawn/spawnSync must use a fixed literal argument array, never a
809
856
  // string command, so a payload value (session id, cwd, transcript path,
810
857
  // prompt, tool name) cannot achieve command injection.
@@ -831,7 +878,7 @@ test("runner source: payload fields never reach a shell — spawn uses fixed arg
831
878
  });
832
879
 
833
880
  test("runner source: stdin is the single payload source — no env-var override", () => {
834
- const src = fs.readFileSync(RUNNER, "utf8");
881
+ const src = fs.readFileSync(CORE, "utf8");
835
882
  assert.doesNotMatch(
836
883
  src,
837
884
  /process\.env\.REMNIC_HOOK_INPUT/,
@@ -840,7 +887,7 @@ test("runner source: stdin is the single payload source — no env-var override"
840
887
  });
841
888
 
842
889
  test("runner source: path inputs are type-validated before use (#1518 validate path types)", () => {
843
- const src = fs.readFileSync(RUNNER, "utf8");
890
+ const src = fs.readFileSync(CORE, "utf8");
844
891
  // cwd / transcript_path are coerced via `input.X || ""` so an unexpected
845
892
  // non-string payload field (number, object, array) cannot reach fs / git
846
893
  // and throw an opaque error or, worse, be coerced by Node to a path.