@sdsrs/code-graph 0.7.16 → 0.8.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.7.16",
7
+ "version": "0.8.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -1,16 +1,5 @@
1
1
  {
2
- "hooks": {
3
- "SessionStart": [
4
- {
5
- "matcher": "startup|clear|compact",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
10
- "timeout": 5
11
- }
12
- ]
13
- }
14
- ]
15
- }
2
+ "description": "code-graph-mcp hooks",
3
+ "_note": "Hooks are registered to ~/.claude/settings.json by scripts/lifecycle.js. This file is intentionally empty to prevent double-firing — Claude Code would otherwise load hooks from both the plugin cache copy AND settings.json, causing each hook to run twice per event.",
4
+ "hooks": {}
16
5
  }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // adopt / unadopt — writes plugin_code_graph_mcp.md into this project's
4
+ // claude-mem memory dir and maintains a sentinel-bracketed index entry in
5
+ // MEMORY.md. Idempotent. Used by invited-memory pattern with CODE_GRAPH_QUIET_HOOKS=1.
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->';
11
+ const SENTINEL_END = '<!-- code-graph-mcp:end -->';
12
+ const INDEX_LINE = '- [code-graph-mcp](plugin_code_graph_mcp.md) — "谁调 X / 改 X 炸啥 / 模块结构" → `get_call_graph` / `impact_analysis` / `module_overview`,替代多步 Grep/Read';
13
+ const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
14
+ const TARGET_NAME = 'plugin_code_graph_mcp.md';
15
+
16
+ function memoryDir(cwd = process.cwd(), home = os.homedir()) {
17
+ const slug = cwd.replace(/\//g, '-');
18
+ return path.join(home, '.claude', 'projects', slug, 'memory');
19
+ }
20
+
21
+ function escapeRegex(s) {
22
+ return s.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&');
23
+ }
24
+
25
+ function adopt({ cwd, home, templatePath } = {}) {
26
+ const dir = memoryDir(cwd, home);
27
+ if (!fs.existsSync(dir)) {
28
+ return { ok: false, reason: 'no-memory-dir', dir };
29
+ }
30
+ const target = path.join(dir, TARGET_NAME);
31
+ const tpl = templatePath || TEMPLATE_PATH;
32
+ if (!fs.existsSync(tpl)) {
33
+ return { ok: false, reason: 'no-template', template: tpl };
34
+ }
35
+ fs.copyFileSync(tpl, target);
36
+
37
+ const indexPath = path.join(dir, 'MEMORY.md');
38
+ let index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
39
+ let indexed = false;
40
+ if (!index.includes(SENTINEL_BEGIN)) {
41
+ if (!index.endsWith('\n')) index += '\n';
42
+ index += `${SENTINEL_BEGIN}\n${INDEX_LINE}\n${SENTINEL_END}\n`;
43
+ fs.writeFileSync(indexPath, index);
44
+ indexed = true;
45
+ }
46
+ return { ok: true, target, indexPath, indexed };
47
+ }
48
+
49
+ function unadopt({ cwd, home } = {}) {
50
+ const dir = memoryDir(cwd, home);
51
+ const target = path.join(dir, TARGET_NAME);
52
+ const indexPath = path.join(dir, 'MEMORY.md');
53
+ let fileRemoved = false;
54
+ let indexPruned = false;
55
+
56
+ if (fs.existsSync(target)) {
57
+ fs.unlinkSync(target);
58
+ fileRemoved = true;
59
+ }
60
+ if (fs.existsSync(indexPath)) {
61
+ const before = fs.readFileSync(indexPath, 'utf8');
62
+ const re = new RegExp(`${escapeRegex(SENTINEL_BEGIN)}[\\s\\S]*?${escapeRegex(SENTINEL_END)}\\n?`, 'g');
63
+ const after = before.replace(re, '');
64
+ if (after !== before) {
65
+ fs.writeFileSync(indexPath, after);
66
+ indexPruned = true;
67
+ }
68
+ }
69
+ return { ok: true, fileRemoved, indexPruned, target, indexPath };
70
+ }
71
+
72
+ function formatResult(action, result) {
73
+ if (action === 'adopt') {
74
+ if (!result.ok) {
75
+ if (result.reason === 'no-memory-dir') {
76
+ return `[code-graph] Memory dir not found: ${result.dir}\n` +
77
+ ' Run \`claude\` at least once in this project to create it.';
78
+ }
79
+ if (result.reason === 'no-template') {
80
+ return `[code-graph] Template missing: ${result.template}`;
81
+ }
82
+ return `[code-graph] adopt failed: ${result.reason || 'unknown'}`;
83
+ }
84
+ const lines = [`[code-graph] Adopted → ${result.target}`];
85
+ if (result.indexed) lines.push(`[code-graph] Indexed → ${result.indexPath}`);
86
+ else lines.push(`[code-graph] Index already contains sentinel — left as-is`);
87
+ lines.push('[code-graph] Activate: set CODE_GRAPH_QUIET_HOOKS=1 in ~/.claude/settings.json env');
88
+ return lines.join('\n');
89
+ }
90
+ if (action === 'unadopt') {
91
+ const lines = [];
92
+ if (result.fileRemoved) lines.push(`[code-graph] Removed → ${result.target}`);
93
+ if (result.indexPruned) lines.push(`[code-graph] De-indexed → ${result.indexPath}`);
94
+ if (!result.fileRemoved && !result.indexPruned) lines.push('[code-graph] Nothing to unadopt');
95
+ return lines.join('\n');
96
+ }
97
+ return '';
98
+ }
99
+
100
+ if (require.main === module) {
101
+ const action = process.argv[2] === 'unadopt' ? 'unadopt' : 'adopt';
102
+ const result = action === 'unadopt' ? unadopt() : adopt();
103
+ process.stdout.write(formatResult(action, result) + '\n');
104
+ process.exit(result.ok === false ? 1 : 0);
105
+ }
106
+
107
+ module.exports = {
108
+ adopt, unadopt, memoryDir, formatResult,
109
+ SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
110
+ };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const {
8
+ adopt, unadopt, memoryDir, SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH,
9
+ } = require('./adopt');
10
+
11
+ function makeSandbox() {
12
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
13
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
14
+ // Pre-create the memory dir (claude-mem convention — we don't create it).
15
+ const dir = memoryDir(cwd, home);
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ return { home, cwd, dir, cleanup: () => {
18
+ fs.rmSync(home, { recursive: true, force: true });
19
+ fs.rmSync(cwd, { recursive: true, force: true });
20
+ }};
21
+ }
22
+
23
+ test('memoryDir slugifies cwd path', () => {
24
+ const dir = memoryDir('/home/alice/proj', '/home/alice');
25
+ assert.strictEqual(dir, '/home/alice/.claude/projects/-home-alice-proj/memory');
26
+ });
27
+
28
+ test('adopt writes template and appends sentinel block when index absent', () => {
29
+ const sb = makeSandbox();
30
+ try {
31
+ const res = adopt({ cwd: sb.cwd, home: sb.home });
32
+ assert.strictEqual(res.ok, true);
33
+ assert.strictEqual(res.indexed, true);
34
+ assert.ok(fs.existsSync(res.target), 'plugin file written');
35
+ const index = fs.readFileSync(res.indexPath, 'utf8');
36
+ assert.match(index, /^# Memory Index/);
37
+ assert.ok(index.includes(SENTINEL_BEGIN));
38
+ assert.ok(index.includes(SENTINEL_END));
39
+ assert.ok(index.includes(INDEX_LINE));
40
+ } finally { sb.cleanup(); }
41
+ });
42
+
43
+ test('adopt is idempotent — no duplicate sentinel on re-run', () => {
44
+ const sb = makeSandbox();
45
+ try {
46
+ adopt({ cwd: sb.cwd, home: sb.home });
47
+ const res2 = adopt({ cwd: sb.cwd, home: sb.home });
48
+ assert.strictEqual(res2.indexed, false, 'second run leaves index alone');
49
+ const index = fs.readFileSync(res2.indexPath, 'utf8');
50
+ const matches = index.match(new RegExp(SENTINEL_BEGIN.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&'), 'g'));
51
+ assert.strictEqual(matches.length, 1, 'sentinel appears exactly once');
52
+ } finally { sb.cleanup(); }
53
+ });
54
+
55
+ test('adopt preserves existing MEMORY.md content and appends', () => {
56
+ const sb = makeSandbox();
57
+ try {
58
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
59
+ fs.writeFileSync(indexPath, '# Memory Index\n\n- [foo.md](foo.md) — existing entry\n');
60
+ adopt({ cwd: sb.cwd, home: sb.home });
61
+ const index = fs.readFileSync(indexPath, 'utf8');
62
+ assert.ok(index.includes('existing entry'), 'preserves prior entries');
63
+ assert.ok(index.includes(SENTINEL_BEGIN), 'appends sentinel');
64
+ } finally { sb.cleanup(); }
65
+ });
66
+
67
+ test('adopt fails gracefully when memory dir missing', () => {
68
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
69
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
70
+ try {
71
+ const res = adopt({ cwd, home });
72
+ assert.strictEqual(res.ok, false);
73
+ assert.strictEqual(res.reason, 'no-memory-dir');
74
+ } finally {
75
+ fs.rmSync(home, { recursive: true, force: true });
76
+ fs.rmSync(cwd, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test('unadopt removes file and sentinel block, preserves other entries', () => {
81
+ const sb = makeSandbox();
82
+ try {
83
+ adopt({ cwd: sb.cwd, home: sb.home });
84
+ // add a neighboring entry
85
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
86
+ const withNeighbor = fs.readFileSync(indexPath, 'utf8') + '- [bar.md](bar.md) — neighbor\n';
87
+ fs.writeFileSync(indexPath, withNeighbor);
88
+
89
+ const res = unadopt({ cwd: sb.cwd, home: sb.home });
90
+ assert.strictEqual(res.fileRemoved, true);
91
+ assert.strictEqual(res.indexPruned, true);
92
+ assert.ok(!fs.existsSync(res.target), 'plugin file gone');
93
+ const final = fs.readFileSync(indexPath, 'utf8');
94
+ assert.ok(!final.includes(SENTINEL_BEGIN), 'sentinel removed');
95
+ assert.ok(final.includes('neighbor'), 'neighbor preserved');
96
+ } finally { sb.cleanup(); }
97
+ });
98
+
99
+ test('unadopt is a no-op when never adopted', () => {
100
+ const sb = makeSandbox();
101
+ try {
102
+ const res = unadopt({ cwd: sb.cwd, home: sb.home });
103
+ assert.strictEqual(res.fileRemoved, false);
104
+ assert.strictEqual(res.indexPruned, false);
105
+ } finally { sb.cleanup(); }
106
+ });
107
+
108
+ test('template file exists and contains decision table', () => {
109
+ assert.ok(fs.existsSync(TEMPLATE_PATH), `template at ${TEMPLATE_PATH}`);
110
+ const content = fs.readFileSync(TEMPLATE_PATH, 'utf8');
111
+ assert.ok(content.includes('get_call_graph'), 'mentions get_call_graph');
112
+ assert.ok(content.includes('impact_analysis'), 'mentions impact_analysis');
113
+ assert.ok(content.includes('CODE_GRAPH_QUIET_HOOKS'), 'mentions env gate');
114
+ });
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const os = require('os');
8
8
  const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
9
9
  const { clearCache: clearBinaryCache } = require('./find-binary');
10
+ const { readBinaryVersion, isDevMode } = require('./version-utils');
10
11
 
11
12
  // ── Environment Checks ────────────────────────────────────
12
13
 
@@ -32,7 +33,6 @@ const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
32
33
  const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
33
34
  const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
34
35
  const FETCH_TIMEOUT_MS = 3000;
35
- const VERSION_OUTPUT_RE = /^code-graph-mcp\s+(\d+\.\d+\.\d+)$/;
36
36
 
37
37
  function isSilentMode(argv = process.argv.slice(2), env = process.env) {
38
38
  return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
@@ -65,18 +65,6 @@ function saveState(state) {
65
65
  } catch { /* ok */ }
66
66
  }
67
67
 
68
- // ── Dev Mode Detection ─────────────────────────────────────
69
-
70
- function isDevMode() {
71
- // Always derive from __dirname — CLAUDE_PLUGIN_ROOT can leak from other plugins
72
- const pluginRoot = path.resolve(__dirname, '..');
73
- // Dev mode: running from source repo (has Cargo.toml nearby)
74
- if (fs.existsSync(path.join(pluginRoot, '..', 'Cargo.toml'))) return true;
75
- // Dev mode: plugin root is a symlink
76
- try { if (fs.lstatSync(pluginRoot).isSymbolicLink()) return true; } catch { /* ok */ }
77
- return false;
78
- }
79
-
80
68
  // ── Throttle ───────────────────────────────────────────────
81
69
 
82
70
  function shouldCheck(state) {
@@ -184,19 +172,6 @@ function getExtractedPluginVersion(pluginSrc) {
184
172
  return manifest && typeof manifest.version === 'string' ? manifest.version : null;
185
173
  }
186
174
 
187
- function readBinaryVersion(binaryPath) {
188
- try {
189
- const out = execFileSync(binaryPath, ['--version'], {
190
- timeout: 2000,
191
- stdio: ['pipe', 'pipe', 'pipe'],
192
- }).toString().trim();
193
- const match = out.match(VERSION_OUTPUT_RE);
194
- return match ? match[1] : null;
195
- } catch {
196
- return null;
197
- }
198
- }
199
-
200
175
  function promoteVerifiedBinary(binaryTmp, binaryDst, expectedVersion) {
201
176
  try {
202
177
  const stat = fs.statSync(binaryTmp);