@sdsrs/code-graph 0.37.0 → 0.39.0

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
@@ -281,6 +281,8 @@ All tools are also available as CLI subcommands for shell scripts, hooks, and te
281
281
 
282
282
  Common options: `--json` (JSON output), `--compact` (compact output), `--limit N`, `--depth N`, `--file <path>`.
283
283
 
284
+ As of **v0.37.0** the CLI is [clap](https://docs.rs/clap)-based: **every subcommand has `--help`** for its full flag list (`code-graph-mcp <command> --help`), value flags accept both `--flag value` and `--flag=value`, and unknown flags or malformed arguments fail fast with a clear error and a non-zero exit code (`2`) instead of being silently ignored. For example, `trace` hides downstream middleware with `--no-middleware` (shown by default), and `snapshot` is a `create`/`inspect` subcommand pair.
285
+
284
286
  ## Plugin Slash Commands
285
287
 
286
288
  Available when installed as a Claude Code plugin:
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.37.0",
7
+ "version": "0.39.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -29,6 +29,7 @@ const fs = require('fs');
29
29
  const path = require('path');
30
30
  const crypto = require('crypto');
31
31
  const { cgTmpDir } = require('./tmp-dir');
32
+ const { recordRecommendation } = require('./recommendation-log');
32
33
 
33
34
  // --- Pure logic (testable) ---
34
35
 
@@ -194,6 +195,7 @@ function runMain() {
194
195
  // ignored by Claude Code — the grep ran anyway. The hookSpecificOutput form
195
196
  // is the documented modern path. Exit 0 — this is a routing decision, not
196
197
  // a hook failure (exit 2 would mark the tool call as "hook errored").
198
+ recordRecommendation(cwd, { hook: 'grep', action: 'deny' });
197
199
  process.stdout.write(JSON.stringify({
198
200
  hookSpecificOutput: {
199
201
  hookEventName: 'PreToolUse',
@@ -204,6 +206,7 @@ function runMain() {
204
206
  return;
205
207
  }
206
208
 
209
+ recordRecommendation(cwd, { hook: 'grep', action: 'hint' });
207
210
  process.stdout.write(buildHint() + '\n');
208
211
  }
209
212
 
@@ -26,6 +26,7 @@ const fs = require('fs');
26
26
  const path = require('path');
27
27
  const crypto = require('crypto');
28
28
  const { cgTmpDir } = require('./tmp-dir');
29
+ const { recordRecommendation } = require('./recommendation-log');
29
30
 
30
31
  // --- Configuration ---
31
32
 
@@ -158,7 +159,10 @@ function runMain() {
158
159
  fired = true;
159
160
  }
160
161
  saveState(cwd, state);
161
- if (fired) process.stdout.write(buildHint(dir) + '\n');
162
+ if (fired) {
163
+ recordRecommendation(cwd, { hook: 'read', action: 'hint' });
164
+ process.stdout.write(buildHint(dir) + '\n');
165
+ }
162
166
  }
163
167
 
164
168
  if (require.main === module) {
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Real-session conversion metric (JS counterpart to the Rust MCP usage.jsonl).
4
+ //
5
+ // The PreToolUse hooks (pre-grep-guide / pre-read-guide) RECOMMEND a code-graph
6
+ // tool when Claude reaches for raw grep / fans out Reads. This module records
7
+ // each emitted recommendation so `code-graph-mcp stats` can compute the *field*
8
+ // conversion rate (recommend → actual cg tool call) — the metric the synthetic
9
+ // routing_bench oracle can't see (memory: self-dogfood-blindspot / feedback_routing_bench).
10
+ //
11
+ // Bounded + best-effort by construction:
12
+ // - appends to <cwd>/.code-graph/recommendations.jsonl and NEVER creates the
13
+ // `.code-graph` dir — so a non-project / tmp cwd (no index) leaves zero
14
+ // footprint, mirroring each hook's existing `.code-graph/index.db` guard.
15
+ // - swallows every error: telemetry must never break or delay a tool call.
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const REC_FILE = 'recommendations.jsonl';
20
+
21
+ /**
22
+ * Append one recommendation event to <cwd>/.code-graph/recommendations.jsonl.
23
+ * @param {string} cwd project root (the hook's process.cwd())
24
+ * @param {object} event e.g. { hook: 'grep', action: 'deny' }
25
+ * @returns {boolean} true if a line was written
26
+ */
27
+ function recordRecommendation(cwd, event = {}) {
28
+ try {
29
+ const dir = path.join(cwd, '.code-graph');
30
+ // Append-only: do NOT create .code-graph. Its absence means "not an indexed
31
+ // project" — recording there would pollute non-project cwds.
32
+ if (!fs.existsSync(dir)) return false;
33
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
34
+ fs.appendFileSync(path.join(dir, REC_FILE), line);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ module.exports = { recordRecommendation, REC_FILE };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+ const { test } = require('node:test');
3
+ const assert = require('node:assert');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { recordRecommendation, REC_FILE } = require('./recommendation-log');
8
+
9
+ function tmpProject(t, withCodeGraph) {
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rec-'));
11
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
12
+ if (withCodeGraph) fs.mkdirSync(path.join(dir, '.code-graph'));
13
+ return dir;
14
+ }
15
+
16
+ test('recordRecommendation appends a JSON line with ts + fields', (t) => {
17
+ const cwd = tmpProject(t, true);
18
+ assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'deny' }), true);
19
+ const content = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8');
20
+ const lines = content.trim().split('\n');
21
+ assert.equal(lines.length, 1);
22
+ const rec = JSON.parse(lines[0]);
23
+ assert.equal(rec.hook, 'grep');
24
+ assert.equal(rec.action, 'deny');
25
+ assert.ok(typeof rec.ts === 'string' && rec.ts.length > 0, 'ts should be a timestamp');
26
+ });
27
+
28
+ test('recordRecommendation is a no-op (no dir created) when .code-graph absent', (t) => {
29
+ const cwd = tmpProject(t, false);
30
+ assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'hint' }), false);
31
+ // Must NOT create the dir or file — zero footprint in non-project cwd.
32
+ assert.equal(fs.existsSync(path.join(cwd, '.code-graph')), false);
33
+ });
34
+
35
+ test('recordRecommendation appends across calls (one line each)', (t) => {
36
+ const cwd = tmpProject(t, true);
37
+ recordRecommendation(cwd, { hook: 'grep', action: 'hint' });
38
+ recordRecommendation(cwd, { hook: 'read', action: 'hint' });
39
+ recordRecommendation(cwd, { hook: 'grep', action: 'deny' });
40
+ const lines = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8').trim().split('\n');
41
+ assert.equal(lines.length, 3);
42
+ const hooks = lines.map((l) => JSON.parse(l).hook);
43
+ assert.deepEqual(hooks, ['grep', 'read', 'grep']);
44
+ });
@@ -30,6 +30,16 @@ function computeQuietHooks({ env = {} } = {}) {
30
30
  return true;
31
31
  }
32
32
 
33
+ // SessionStart project-map injection gate. Beyond the (already default-quiet)
34
+ // verbose opt-in, the map is now also ADOPTED-ONLY: cross-project measurement
35
+ // (memory cross-project-interference) found the ≈2 KB dump is zero-referenced
36
+ // in projects the user hasn't adopted into their MEMORY.md workflow, so it only
37
+ // earns its standing-context cost for adopted projects. Unadopted projects get
38
+ // no map even under CODE_GRAPH_VERBOSE_HOOKS / legacy QUIET_HOOKS=0.
39
+ function shouldInjectMap({ available, quietHooks, adopted } = {}) {
40
+ return !!(available && !quietHooks && adopted);
41
+ }
42
+
33
43
  function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
34
44
  try {
35
45
  const child = spawnFn(process.execPath, [path.join(__dirname, 'auto-update.js'), 'check', '--silent'], {
@@ -316,11 +326,14 @@ function runSessionInit() {
316
326
 
317
327
  // quietHooks: default quiet (project_map injection duplicates MEMORY.md +
318
328
  // on-demand tool). CODE_GRAPH_VERBOSE_HOOKS=1 to opt in to the dump;
319
- // legacy CODE_GRAPH_QUIET_HOOKS=0/1 still force the old behavior.
329
+ // legacy CODE_GRAPH_QUIET_HOOKS=0/1 still force the old behavior. The opt-in
330
+ // dump is further gated to adopted projects (shouldInjectMap).
320
331
  const adopted = isAdopted();
321
332
  const quietHooks = computeQuietHooks({ env: process.env });
322
333
 
323
- const mapInjected = binaryCheck.available && !quietHooks ? injectProjectMap() : false;
334
+ const mapInjected = shouldInjectMap({ available: binaryCheck.available, quietHooks, adopted })
335
+ ? injectProjectMap()
336
+ : false;
324
337
  const consistencyIssues = binaryCheck.available
325
338
  ? consistencyCheck(binaryCheck.binary)
326
339
  : [];
@@ -369,6 +382,7 @@ module.exports = {
369
382
  consistencyCheck,
370
383
  runSessionInit,
371
384
  computeQuietHooks,
385
+ shouldInjectMap,
372
386
  };
373
387
 
374
388
  if (require.main === module) {
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
- const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary, computeQuietHooks } = require('./session-init');
7
+ const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary, computeQuietHooks, shouldInjectMap } = require('./session-init');
8
8
 
9
9
  test('syncLifecycleConfig is exported as a callable helper', () => {
10
10
  assert.equal(typeof syncLifecycleConfig, 'function');
@@ -147,6 +147,20 @@ test('computeQuietHooks: legacy `adopted` param is ignored under new default', (
147
147
  assert.equal(computeQuietHooks({ adopted: false, env: {} }), true);
148
148
  });
149
149
 
150
+ test('shouldInjectMap: only injects when available + not-quiet + adopted', () => {
151
+ // The single positive case: opted into verbose AND adopted.
152
+ assert.equal(shouldInjectMap({ available: true, quietHooks: false, adopted: true }), true);
153
+ // Adopted-only gate: verbose but unadopted → no injection (the zero-referenced
154
+ // case cross-project-interference flagged).
155
+ assert.equal(shouldInjectMap({ available: true, quietHooks: false, adopted: false }), false);
156
+ // Quiet default suppresses regardless of adoption.
157
+ assert.equal(shouldInjectMap({ available: true, quietHooks: true, adopted: true }), false);
158
+ // No binary → nothing to inject.
159
+ assert.equal(shouldInjectMap({ available: false, quietHooks: false, adopted: true }), false);
160
+ // Missing args default to falsey → no injection.
161
+ assert.equal(shouldInjectMap(), false);
162
+ });
163
+
150
164
  test('consistencyCheck returns version-mismatch when versions differ', (t) => {
151
165
  const os = require('os');
152
166
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.37.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.37.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.37.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.37.0",
42
- "@sdsrs/code-graph-win32-x64": "0.37.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.39.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.39.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.39.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.39.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.39.0"
43
43
  }
44
44
  }