@sdsrs/code-graph 0.7.14 → 0.7.16
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +14 -2
- package/claude-plugin/scripts/auto-update.js +16 -1
- package/claude-plugin/scripts/lifecycle.e2e.test.js +5 -1
- package/claude-plugin/scripts/lifecycle.js +95 -14
- package/claude-plugin/scripts/mcp-launcher.js +2 -1
- package/claude-plugin/scripts/pre-edit-guide.js +5 -1
- package/claude-plugin/scripts/pre-edit-guide.test.js +161 -0
- package/claude-plugin/scripts/session-init.js +24 -2
- package/claude-plugin/scripts/statusline-composite.js +2 -2
- package/claude-plugin/scripts/user-prompt-context.js +90 -81
- package/claude-plugin/scripts/user-prompt-context.test.js +466 -0
- package/claude-plugin/skills/explore.md +22 -0
- package/claude-plugin/skills/index.md +24 -0
- package/package.json +6 -6
- package/claude-plugin/commands/impact.md +0 -9
- package/claude-plugin/commands/rebuild.md +0 -7
- package/claude-plugin/commands/status.md +0 -5
- package/claude-plugin/commands/trace.md +0 -12
- package/claude-plugin/commands/understand.md +0 -12
- package/claude-plugin/skills/code-navigation.md +0 -20
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
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
|
+
}
|
|
4
16
|
}
|
|
@@ -68,7 +68,8 @@ function saveState(state) {
|
|
|
68
68
|
// ── Dev Mode Detection ─────────────────────────────────────
|
|
69
69
|
|
|
70
70
|
function isDevMode() {
|
|
71
|
-
|
|
71
|
+
// Always derive from __dirname — CLAUDE_PLUGIN_ROOT can leak from other plugins
|
|
72
|
+
const pluginRoot = path.resolve(__dirname, '..');
|
|
72
73
|
// Dev mode: running from source repo (has Cargo.toml nearby)
|
|
73
74
|
if (fs.existsSync(path.join(pluginRoot, '..', 'Cargo.toml'))) return true;
|
|
74
75
|
// Dev mode: plugin root is a symlink
|
|
@@ -281,6 +282,20 @@ async function downloadAndInstall(latest) {
|
|
|
281
282
|
writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
|
|
282
283
|
} catch { /* not fatal */ }
|
|
283
284
|
|
|
285
|
+
// Run the NEW lifecycle.js to update settings.json hooks with new paths.
|
|
286
|
+
// Without this, settings.json hooks still point to the old version directory
|
|
287
|
+
// until the next session's self-heal corrects them.
|
|
288
|
+
if (pluginUpdated) {
|
|
289
|
+
try {
|
|
290
|
+
const newLifecycle = path.join(pluginDst, 'scripts', 'lifecycle.js');
|
|
291
|
+
if (fs.existsSync(newLifecycle)) {
|
|
292
|
+
execFileSync(process.execPath, [newLifecycle, 'update'], {
|
|
293
|
+
timeout: 5000, stdio: 'pipe',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch { /* not fatal — syncLifecycleConfig will self-heal on next session */ }
|
|
297
|
+
}
|
|
298
|
+
|
|
284
299
|
// ── Step 2: Download platform binary directly from GitHub release ──
|
|
285
300
|
if (latest.binaryUrl) {
|
|
286
301
|
try {
|
|
@@ -26,9 +26,13 @@ function readJson(filePath) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function runScript(homeDir, scriptPath, args = [], options = {}) {
|
|
29
|
+
const env = { ...process.env, HOME: homeDir };
|
|
30
|
+
// Do NOT set CLAUDE_PLUGIN_ROOT — lifecycle.js derives PLUGIN_ROOT from __dirname
|
|
31
|
+
// to avoid env var leakage from other plugins in shared hook execution context.
|
|
32
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
29
33
|
return execFileSync(process.execPath, [scriptPath, ...args], {
|
|
30
34
|
cwd: options.cwd || repoRoot,
|
|
31
|
-
env
|
|
35
|
+
env,
|
|
32
36
|
input: options.input,
|
|
33
37
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
38
|
}).toString();
|
|
@@ -11,7 +11,10 @@ const OLD_PLUGIN_IDS = [
|
|
|
11
11
|
];
|
|
12
12
|
const MARKETPLACE_NAME = 'code-graph-mcp';
|
|
13
13
|
const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
|
|
14
|
-
|
|
14
|
+
// Always derive from __dirname — CLAUDE_PLUGIN_ROOT env var can leak from other
|
|
15
|
+
// plugins when hooks run in shared process context (e.g. claude-mem-lite sets it
|
|
16
|
+
// to its own marketplace path, polluting all subsequent settings.json hook processes).
|
|
17
|
+
const PLUGIN_ROOT = path.resolve(__dirname, '..');
|
|
15
18
|
const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
|
|
16
19
|
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
17
20
|
const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
@@ -223,9 +226,18 @@ function migrateOldPluginIds(settings) {
|
|
|
223
226
|
// Same strategy as claude-mem-lite. hooks.json is kept empty to prevent double-firing.
|
|
224
227
|
|
|
225
228
|
const OUR_HOOK_SCRIPTS = ['session-init.js', 'incremental-index.js', 'user-prompt-context.js', 'pre-edit-guide.js'];
|
|
229
|
+
const OUR_DESCRIPTIONS = [
|
|
230
|
+
'StatusLine self-heal, lifecycle sync, project map injection',
|
|
231
|
+
'Auto-inject impact analysis when editing functions with 2+ callers',
|
|
232
|
+
'Auto-update code graph index after file edits',
|
|
233
|
+
'Inject code-graph structural context based on user intent',
|
|
234
|
+
];
|
|
226
235
|
|
|
227
236
|
function isOurHookEntry(entry) {
|
|
228
237
|
if (!entry || !entry.hooks) return false;
|
|
238
|
+
// Primary: match by description (immune to path pollution)
|
|
239
|
+
if (entry.description && OUR_DESCRIPTIONS.includes(entry.description)) return true;
|
|
240
|
+
// Fallback: match by script name + 'code-graph' in path
|
|
229
241
|
return entry.hooks.some(h =>
|
|
230
242
|
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
231
243
|
h.command.includes('code-graph')
|
|
@@ -269,17 +281,17 @@ function registerHooksToSettings(settings) {
|
|
|
269
281
|
for (const [event, newEntries] of Object.entries(defs)) {
|
|
270
282
|
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
271
283
|
|
|
284
|
+
// First, remove ALL existing entries that match ours (cleans up duplicates
|
|
285
|
+
// from prior PLUGIN_ROOT pollution where isOurHookEntry couldn't match,
|
|
286
|
+
// causing infinite re-adds each session).
|
|
287
|
+
const beforeLen = settings.hooks[event].length;
|
|
288
|
+
settings.hooks[event] = settings.hooks[event].filter(e => !isOurHookEntry(e));
|
|
289
|
+
if (settings.hooks[event].length !== beforeLen) changed = true;
|
|
290
|
+
|
|
291
|
+
// Then add our entries fresh with correct paths
|
|
272
292
|
for (const newEntry of newEntries) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (JSON.stringify(settings.hooks[event][existingIdx]) !== JSON.stringify(newEntry)) {
|
|
276
|
-
settings.hooks[event][existingIdx] = newEntry;
|
|
277
|
-
changed = true;
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
settings.hooks[event].push(newEntry);
|
|
281
|
-
changed = true;
|
|
282
|
-
}
|
|
293
|
+
settings.hooks[event].push(newEntry);
|
|
294
|
+
changed = true;
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -328,6 +340,13 @@ function install() {
|
|
|
328
340
|
settings.statusLine = { type: 'command', command: compositeCommand() };
|
|
329
341
|
settingsChanged = true;
|
|
330
342
|
manifest.config.statusLine = true;
|
|
343
|
+
} else {
|
|
344
|
+
// Composite exists — ensure path is correct (may have been polluted by env leak)
|
|
345
|
+
const cmd = compositeCommand();
|
|
346
|
+
if (settings.statusLine.command !== cmd) {
|
|
347
|
+
settings.statusLine.command = cmd;
|
|
348
|
+
settingsChanged = true;
|
|
349
|
+
}
|
|
331
350
|
}
|
|
332
351
|
|
|
333
352
|
// Register code-graph provider
|
|
@@ -506,8 +525,60 @@ function cleanupOldCacheVersions(keep = 3) {
|
|
|
506
525
|
} catch { /* cache dir doesn't exist — nothing to clean */ }
|
|
507
526
|
}
|
|
508
527
|
|
|
528
|
+
// --- Health Check ---
|
|
529
|
+
// Validates all registered paths in settings.json point to existing scripts.
|
|
530
|
+
// Returns { healthy, issues, repaired }.
|
|
531
|
+
|
|
532
|
+
function healthCheck() {
|
|
533
|
+
const settings = readJson(SETTINGS_PATH) || {};
|
|
534
|
+
const issues = [];
|
|
535
|
+
|
|
536
|
+
// Check statusLine path
|
|
537
|
+
if (isOurComposite(settings)) {
|
|
538
|
+
const m = settings.statusLine.command.match(/node\s+"([^"]+)"/);
|
|
539
|
+
if (m && m[1] && !fs.existsSync(m[1])) {
|
|
540
|
+
issues.push({ type: 'statusLine', path: m[1] });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Check hook paths
|
|
545
|
+
if (settings.hooks) {
|
|
546
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
547
|
+
if (!Array.isArray(entries)) continue;
|
|
548
|
+
for (const entry of entries) {
|
|
549
|
+
if (!isOurHookEntry(entry) || !entry.hooks) continue;
|
|
550
|
+
for (const h of entry.hooks) {
|
|
551
|
+
const m = h.command && h.command.match(/node\s+"([^"]+)"/);
|
|
552
|
+
if (m && m[1] && !fs.existsSync(m[1])) {
|
|
553
|
+
issues.push({ type: 'hook', event, path: m[1] });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check registry paths
|
|
561
|
+
const registry = readRegistry();
|
|
562
|
+
for (const provider of registry) {
|
|
563
|
+
if (provider.id === '_previous') continue;
|
|
564
|
+
const m = provider.command && provider.command.match(/node\s+"([^"]+)"/);
|
|
565
|
+
if (m && m[1] && !fs.existsSync(m[1])) {
|
|
566
|
+
issues.push({ type: 'registry', id: provider.id, path: m[1] });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Auto-repair if issues found
|
|
571
|
+
let repaired = false;
|
|
572
|
+
if (issues.length > 0) {
|
|
573
|
+
install();
|
|
574
|
+
repaired = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { healthy: issues.length === 0, issues, repaired };
|
|
578
|
+
}
|
|
579
|
+
|
|
509
580
|
module.exports = {
|
|
510
|
-
install, uninstall, update, checkScopeConflict,
|
|
581
|
+
install, uninstall, update, healthCheck, checkScopeConflict,
|
|
511
582
|
isPluginExplicitlyDisabled, isPluginInactive, cleanupDisabledStatusline,
|
|
512
583
|
readManifest, readJson, writeJsonAtomic,
|
|
513
584
|
readRegistry, writeRegistry,
|
|
@@ -516,7 +587,7 @@ module.exports = {
|
|
|
516
587
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
517
588
|
};
|
|
518
589
|
|
|
519
|
-
// CLI: node lifecycle.js <install|uninstall|update>
|
|
590
|
+
// CLI: node lifecycle.js <install|uninstall|update|health>
|
|
520
591
|
if (require.main === module) {
|
|
521
592
|
const cmd = process.argv[2];
|
|
522
593
|
if (cmd === 'install') {
|
|
@@ -528,8 +599,18 @@ if (require.main === module) {
|
|
|
528
599
|
} else if (cmd === 'update') {
|
|
529
600
|
const r = update();
|
|
530
601
|
console.log(`Updated ${r.oldVersion} → ${r.version} | settings=${r.settingsChanged}`);
|
|
602
|
+
} else if (cmd === 'health') {
|
|
603
|
+
const r = healthCheck();
|
|
604
|
+
if (r.healthy) {
|
|
605
|
+
console.log('Health: OK — all paths valid');
|
|
606
|
+
} else {
|
|
607
|
+
console.log(`Health: ${r.issues.length} issue(s) found${r.repaired ? ' — repaired' : ''}`);
|
|
608
|
+
for (const issue of r.issues) {
|
|
609
|
+
console.log(` ${issue.type}: ${issue.path || issue.id}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
531
612
|
} else {
|
|
532
|
-
console.error('Usage: lifecycle.js <install|uninstall|update>');
|
|
613
|
+
console.error('Usage: lifecycle.js <install|uninstall|update|health>');
|
|
533
614
|
process.exit(1);
|
|
534
615
|
}
|
|
535
616
|
}
|
|
@@ -12,7 +12,8 @@ const path = require('path');
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
|
|
14
14
|
// Set plugin root so find-binary.js can locate bundled/dev binaries
|
|
15
|
-
|
|
15
|
+
// Always derive from __dirname — CLAUDE_PLUGIN_ROOT can leak from other plugins
|
|
16
|
+
process.env._FIND_BINARY_ROOT = path.resolve(__dirname, '..');
|
|
16
17
|
|
|
17
18
|
const { findBinary, clearCache } = require('./find-binary');
|
|
18
19
|
|
|
@@ -89,10 +89,14 @@ if (!symbol || symbol.length < 3) {
|
|
|
89
89
|
if (!symbol || symbol.length < 3) process.exit(0);
|
|
90
90
|
|
|
91
91
|
// Skip common patterns that aren't real function names
|
|
92
|
-
if (
|
|
92
|
+
if (isCommonKeyword(symbol)) {
|
|
93
93
|
process.exit(0);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function isCommonKeyword(s) {
|
|
97
|
+
return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
// --- Per-symbol cooldown: 2 minutes ---
|
|
97
101
|
const cooldownFile = path.join(os.tmpdir(), `.cg-impact-${symbol}`);
|
|
98
102
|
try {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
|
|
5
|
+
// Pre-edit-guide.js is a script with side effects (reads stdin, checks db).
|
|
6
|
+
// We test its PATTERNS directly without requiring the module.
|
|
7
|
+
|
|
8
|
+
// --- Function signature patterns (copied from pre-edit-guide.js) ---
|
|
9
|
+
const fnPatterns = [
|
|
10
|
+
/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/, // Rust
|
|
11
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // JS/TS
|
|
12
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|_)\s*=>/, // JS arrow
|
|
13
|
+
/(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // JS method / Go func
|
|
14
|
+
/def\s+(\w+)/, // Python/Ruby
|
|
15
|
+
/func\s+(\w+)/, // Go/Swift
|
|
16
|
+
/(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S+\s+(\w+)\s*\(/, // Java/C#/Kotlin
|
|
17
|
+
/(?:public\s+)?function\s+(\w+)/, // PHP
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function extractFunctionName(code) {
|
|
21
|
+
for (const pat of fnPatterns) {
|
|
22
|
+
const m = code.match(pat);
|
|
23
|
+
if (m) return m[1] || m[2];
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isCommonKeyword(s) {
|
|
29
|
+
return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Rust ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
test('fn-extract: Rust pub fn', () => {
|
|
35
|
+
assert.equal(extractFunctionName('pub fn parse_code(input: &str) -> Vec<Node> {'), 'parse_code');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('fn-extract: Rust pub async fn', () => {
|
|
39
|
+
assert.equal(extractFunctionName('pub async fn handle_message(&self, msg: &str) -> Result<()> {'), 'handle_message');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('fn-extract: Rust fn (no pub)', () => {
|
|
43
|
+
assert.equal(extractFunctionName('fn helper_func(x: i32) -> i32 {'), 'helper_func');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── JavaScript/TypeScript ───────────────────────────────
|
|
47
|
+
|
|
48
|
+
test('fn-extract: JS function', () => {
|
|
49
|
+
assert.equal(extractFunctionName('function handleRequest(req, res) {'), 'handleRequest');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('fn-extract: JS export function', () => {
|
|
53
|
+
assert.equal(extractFunctionName('export function processData(input) {'), 'processData');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('fn-extract: JS async function', () => {
|
|
57
|
+
assert.equal(extractFunctionName('async function fetchData(url) {'), 'fetchData');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('fn-extract: JS export async function', () => {
|
|
61
|
+
assert.equal(extractFunctionName('export async function loadConfig(path) {'), 'loadConfig');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('fn-extract: JS arrow function (const)', () => {
|
|
65
|
+
assert.equal(extractFunctionName('const handleError = (err) => {'), 'handleError');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('fn-extract: JS arrow function (async)', () => {
|
|
69
|
+
assert.equal(extractFunctionName('const fetchUser = async (id) => {'), 'fetchUser');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('fn-extract: JS method', () => {
|
|
73
|
+
assert.equal(extractFunctionName(' handleMessage(msg) {'), 'handleMessage');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Python ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('fn-extract: Python def', () => {
|
|
79
|
+
assert.equal(extractFunctionName('def process_data(self, items):'), 'process_data');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('fn-extract: Python async def', () => {
|
|
83
|
+
assert.equal(extractFunctionName('async def fetch_data(url):'), 'fetch_data');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── Go ──────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
test('fn-extract: Go func', () => {
|
|
89
|
+
assert.equal(extractFunctionName('func HandleRequest(w http.ResponseWriter, r *http.Request) {'), 'HandleRequest');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Java/C#/Kotlin ──────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
test('fn-extract: Java public method', () => {
|
|
95
|
+
assert.equal(extractFunctionName('public void processItem(Item item) {'), 'processItem');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('fn-extract: Java private method', () => {
|
|
99
|
+
assert.equal(extractFunctionName('private String formatOutput(Data data) {'), 'formatOutput');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('fn-extract: C# static method', () => {
|
|
103
|
+
assert.equal(extractFunctionName('static int CalculateTotal(List<int> items) {'), 'CalculateTotal');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── PHP ─────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test('fn-extract: PHP function', () => {
|
|
109
|
+
assert.equal(extractFunctionName('function handleUpload($file) {'), 'handleUpload');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('fn-extract: PHP public function', () => {
|
|
113
|
+
assert.equal(extractFunctionName('public function getUser($id) {'), 'getUser');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── Ruby ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
test('fn-extract: Ruby def', () => {
|
|
119
|
+
assert.equal(extractFunctionName('def process_request(params)'), 'process_request');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Keyword filter ──────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test('keyword-filter: common keywords rejected', () => {
|
|
125
|
+
for (const kw of ['if', 'for', 'while', 'switch', 'catch', 'else', 'return', 'new', 'get', 'set', 'try']) {
|
|
126
|
+
assert.ok(isCommonKeyword(kw), `"${kw}" should be rejected`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('keyword-filter: real function names pass', () => {
|
|
131
|
+
for (const name of ['parse_code', 'handleMessage', 'process_data', 'fetchUser']) {
|
|
132
|
+
assert.ok(!isCommonKeyword(name), `"${name}" should pass`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── No false positives ──────────────────────────────────
|
|
137
|
+
|
|
138
|
+
test('fn-extract: plain code body returns null', () => {
|
|
139
|
+
assert.equal(extractFunctionName('let x = 42;\nreturn x + 1;'), null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('fn-extract: comment returns null', () => {
|
|
143
|
+
assert.equal(extractFunctionName('// This is a comment about the function'), null);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('fn-extract: short strings return null', () => {
|
|
147
|
+
assert.equal(extractFunctionName('x = 1'), null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Pattern consistency check ───────────────────────────
|
|
151
|
+
// Verify fnPatterns in this test match what's in pre-edit-guide.js
|
|
152
|
+
|
|
153
|
+
test('pattern-sync: fnPatterns count matches source', () => {
|
|
154
|
+
const fs = require('node:fs');
|
|
155
|
+
const path = require('node:path');
|
|
156
|
+
const source = fs.readFileSync(path.join(__dirname, 'pre-edit-guide.js'), 'utf8');
|
|
157
|
+
// Count regex pattern lines in the fnPatterns array (lines containing // Language comment)
|
|
158
|
+
const sourcePatternCount = (source.match(/\/\/\s*(Rust|JS|Python|Go|Java|C#|PHP|Ruby|Swift|Kotlin)/g) || []).length;
|
|
159
|
+
assert.ok(fnPatterns.length === 8, `Expected 8 patterns, got ${fnPatterns.length}`);
|
|
160
|
+
assert.ok(sourcePatternCount >= 7, `Source should have >= 7 language comments, found ${sourcePatternCount}`);
|
|
161
|
+
});
|
|
@@ -35,8 +35,8 @@ function syncLifecycleConfig() {
|
|
|
35
35
|
update();
|
|
36
36
|
return 'updated';
|
|
37
37
|
}
|
|
38
|
-
// Self-heal: version matches but statusLine may have been lost
|
|
39
|
-
// (e.g. plugin removed and reinstalled
|
|
38
|
+
// Self-heal: version matches but statusLine may have been lost or path corrupted
|
|
39
|
+
// (e.g. plugin removed and reinstalled, or CLAUDE_PLUGIN_ROOT leaked from another plugin).
|
|
40
40
|
// install() is idempotent — isOurComposite guard prevents duplicate work.
|
|
41
41
|
const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')) || {};
|
|
42
42
|
if (!settings.statusLine || !settings.statusLine.command ||
|
|
@@ -44,6 +44,28 @@ function syncLifecycleConfig() {
|
|
|
44
44
|
install();
|
|
45
45
|
return 'self-healed';
|
|
46
46
|
}
|
|
47
|
+
// Also self-heal if composite path points to a non-existent script (path pollution)
|
|
48
|
+
const scriptMatch = settings.statusLine.command.match(/node\s+"([^"]+)"/);
|
|
49
|
+
if (scriptMatch && scriptMatch[1] && !fs.existsSync(scriptMatch[1])) {
|
|
50
|
+
install();
|
|
51
|
+
return 'self-healed-bad-path';
|
|
52
|
+
}
|
|
53
|
+
// Self-heal if any hook command points to a non-existent script (path pollution)
|
|
54
|
+
if (settings.hooks) {
|
|
55
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
56
|
+
if (!Array.isArray(entries)) continue;
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!entry.hooks) continue;
|
|
59
|
+
for (const h of entry.hooks) {
|
|
60
|
+
const m = h.command && h.command.match(/node\s+"([^"]+)"/);
|
|
61
|
+
if (m && m[1] && m[1].includes('code-graph') && !fs.existsSync(m[1])) {
|
|
62
|
+
install();
|
|
63
|
+
return 'self-healed-bad-hook';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
47
69
|
return 'noop';
|
|
48
70
|
}
|
|
49
71
|
|
|
@@ -82,8 +82,8 @@ function parseCommand(cmd) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function codeGraphCommand() {
|
|
85
|
-
|
|
86
|
-
return `node "${path.join(
|
|
85
|
+
// Always derive from __dirname — CLAUDE_PLUGIN_ROOT can leak from other plugins
|
|
86
|
+
return `node "${path.join(__dirname, 'statusline.js')}"`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
module.exports = { run };
|