@sdsrs/code-graph
Advanced tools
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| // Shared "is this a real project?" detector for the plugin's activation gates. | ||
| // | ||
| // Why this exists: code-graph half-activates in non-project working | ||
| // directories — most visibly the ~2035 headless `claude -p` calls | ||
| // claude-mem-lite spawns with cwd=/tmp ("Return ONLY valid JSON"), none of | ||
| // which ever use code-graph. Each one paid an MCP-server spin-up + a ~780B | ||
| // `instructions` block + a SessionStart map probe + an empty | ||
| // /tmp/.code-graph/index.db, plus adopt() writing a decision-table sentinel | ||
| // into ~/.claude/projects/-tmp/memory/MEMORY.md. This module is the single | ||
| // gate the launcher (mcp-launcher.js), the SessionStart hook (session-init.js), | ||
| // and adopt (adopt.js) consult to fully no-op there. | ||
| // | ||
| // Detection is project-MARKER based, NOT a literal "is cwd under os.tmpdir()" | ||
| // check. Rationale: (1) /tmp and Claude Code's $TMPDIR have no .git/manifest, | ||
| // so the marker check already classifies every temp / headless cwd as | ||
| // non-project; (2) a literal under-tmpdir test would wrongly skip a real git | ||
| // repo that happens to be cloned under /tmp AND would break this repo's own | ||
| // tmpdir-based test sandboxes. Markers mirror what Claude Code itself | ||
| // recognizes. `.code-graph` is deliberately NOT a marker — it is created BY | ||
| // this tool, so counting it would let a once-polluted /tmp self-certify as a | ||
| // project on the next session (circular). | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const PROJECT_MARKERS = [ | ||
| '.git', 'package.json', 'Cargo.toml', | ||
| 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle', | ||
| ]; | ||
| function isProjectRoot(cwd) { | ||
| return PROJECT_MARKERS.some(m => fs.existsSync(path.join(cwd, m))); | ||
| } | ||
| // A cwd is "non-project" when it carries none of the recognized project | ||
| // markers. The plugin's activation gates short-circuit there: no MCP tools, | ||
| // no index creation, no SessionStart map injection, no auto-adoption. | ||
| function isNonProjectCwd(cwd = process.cwd()) { | ||
| return !isProjectRoot(cwd); | ||
| } | ||
| module.exports = { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd }; |
| 'use strict'; | ||
| // Tests for project-detect.js — the activation gate shared by mcp-launcher.js, | ||
| // session-init.js, and adopt.js. Run: node --test claude-plugin/scripts/project-detect.test.js | ||
| const test = require('node:test'); | ||
| const assert = require('node:assert/strict'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const os = require('os'); | ||
| const { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd } = require('./project-detect'); | ||
| function mkTmp(t) { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pd-')); | ||
| t.after(() => fs.rmSync(dir, { recursive: true, force: true })); | ||
| return dir; | ||
| } | ||
| test('isNonProjectCwd: bare tmp dir (no markers) → non-project', (t) => { | ||
| const dir = mkTmp(t); | ||
| assert.equal(isNonProjectCwd(dir), true); | ||
| }); | ||
| test('isNonProjectCwd: /tmp root (the mem-lite headless cwd) → non-project', () => { | ||
| // claude-mem-lite spawns `claude -p` with cwd=/tmp; /tmp has no project marker. | ||
| assert.equal(isNonProjectCwd('/tmp'), true); | ||
| }); | ||
| test('isNonProjectCwd: cwd with .git → project (false)', (t) => { | ||
| const dir = mkTmp(t); | ||
| fs.mkdirSync(path.join(dir, '.git')); | ||
| assert.equal(isNonProjectCwd(dir), false); | ||
| }); | ||
| test('isNonProjectCwd: cwd with package.json → project (false)', (t) => { | ||
| const dir = mkTmp(t); | ||
| fs.writeFileSync(path.join(dir, 'package.json'), '{}'); | ||
| assert.equal(isNonProjectCwd(dir), false); | ||
| }); | ||
| test('isNonProjectCwd: a real git repo under /tmp is still a project (marker wins over location)', (t) => { | ||
| // Deliberate: we do NOT do a literal under-tmpdir check, so a repo cloned | ||
| // into /tmp/<x> with .git is correctly treated as a project. | ||
| const dir = mkTmp(t); | ||
| fs.mkdirSync(path.join(dir, '.git')); | ||
| assert.equal(isNonProjectCwd(dir), false); | ||
| }); | ||
| test('isNonProjectCwd: cwd with only .code-graph → non-project (self-created dir is not a marker)', (t) => { | ||
| // Circularity guard: once code-graph (pre-fix) created /tmp/.code-graph, a | ||
| // naive marker set counting .code-graph would self-certify /tmp as a project. | ||
| const dir = mkTmp(t); | ||
| fs.mkdirSync(path.join(dir, '.code-graph')); | ||
| assert.equal(isProjectRoot(dir), false, '.code-graph alone must not qualify as a project'); | ||
| assert.equal(isNonProjectCwd(dir), true); | ||
| }); | ||
| test('PROJECT_MARKERS excludes .code-graph and includes the standard anchors', () => { | ||
| assert.ok(!PROJECT_MARKERS.includes('.code-graph'), '.code-graph must not be a project marker'); | ||
| for (const m of ['.git', 'package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod']) { | ||
| assert.ok(PROJECT_MARKERS.includes(m), `${m} should be a marker`); | ||
| } | ||
| }); | ||
| test('isProjectRoot detects each marker', (t) => { | ||
| for (const marker of PROJECT_MARKERS) { | ||
| const dir = mkTmp(t); | ||
| assert.equal(isProjectRoot(dir), false, 'bare cwd should not be a project'); | ||
| const markerPath = path.join(dir, marker); | ||
| if (marker.startsWith('.')) fs.mkdirSync(markerPath); | ||
| else fs.writeFileSync(markerPath, ''); | ||
| assert.equal(isProjectRoot(dir), true, `${marker} should make cwd a project`); | ||
| } | ||
| }); |
@@ -7,3 +7,3 @@ { | ||
| }, | ||
| "version": "0.32.3", | ||
| "version": "0.33.0", | ||
| "keywords": [ | ||
@@ -10,0 +10,0 @@ "code-graph", |
@@ -11,2 +11,3 @@ #!/usr/bin/env node | ||
| const os = require('os'); | ||
| const { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd } = require('./project-detect'); | ||
@@ -318,12 +319,5 @@ const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->'; | ||
| // Project-marker check: cwd looks like a real project (not /tmp / $HOME). | ||
| // Used to gate auto-mkdir of the auto-memory dir so adopt doesn't pollute | ||
| // random directories. Mirrors the markers Claude Code itself recognizes. | ||
| const PROJECT_MARKERS = [ | ||
| '.git', '.code-graph', 'package.json', 'Cargo.toml', | ||
| 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle', | ||
| ]; | ||
| function isProjectRoot(cwd) { | ||
| return PROJECT_MARKERS.some(m => fs.existsSync(path.join(cwd, m))); | ||
| } | ||
| // Project-marker detection (PROJECT_MARKERS / isProjectRoot / isNonProjectCwd) | ||
| // now lives in project-detect.js — the single activation gate shared with | ||
| // mcp-launcher.js and session-init.js. Imported above and re-exported below. | ||
@@ -335,12 +329,13 @@ function adopt({ cwd, home, templatePath } = {}) { | ||
| const effectiveCwd = cwd || process.cwd(); | ||
| // Gate adoption on a real-project cwd BEFORE touching the filesystem. The | ||
| // check must run even when the memory dir already exists: Claude Code | ||
| // pre-creates ~/.claude/projects/<slug>/memory for every session (including | ||
| // the ~2035 headless /tmp mem-lite calls), and the old guard — nested inside | ||
| // `if (!fs.existsSync(dir))` — was bypassed in exactly that case, letting | ||
| // /tmp get adopted (sentinel written into its MEMORY.md). See project-detect.js. | ||
| if (isNonProjectCwd(effectiveCwd)) { | ||
| return { ok: false, reason: 'not-a-project', dir: memoryDir(cwd, home), cwd: effectiveCwd }; | ||
| } | ||
| const dir = memoryDir(cwd, home); | ||
| if (!fs.existsSync(dir)) { | ||
| // Auto-create only when cwd has a project marker. Without markers the | ||
| // user is likely in /tmp or $HOME, where adopt would litter | ||
| // ~/.claude/projects/ with bogus slugs. | ||
| if (!isProjectRoot(effectiveCwd)) { | ||
| return { ok: false, reason: 'not-a-project', dir, cwd: effectiveCwd }; | ||
| } | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| const target = path.join(dir, TARGET_NAME); | ||
@@ -555,3 +550,3 @@ const tpl = templatePath || TEMPLATE_PATH; | ||
| SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME, | ||
| PROJECT_MARKERS, PROJECT_TYPES, | ||
| PROJECT_MARKERS, PROJECT_TYPES, isNonProjectCwd, | ||
| }; |
@@ -18,2 +18,6 @@ 'use strict'; | ||
| const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-')); | ||
| // Mark the sandbox cwd as a real project — adopt() now gates on a project | ||
| // marker unconditionally (see project-detect.js), so a bare mkdtemp would be | ||
| // treated as a non-project and refused. | ||
| fs.mkdirSync(path.join(cwd, '.git')); | ||
| // Pre-create the memory dir (claude-mem convention — we don't create it). | ||
@@ -90,2 +94,27 @@ const dir = memoryDir(cwd, home); | ||
| test('adopt refuses a non-project cwd even when the memory dir already exists (regression: /tmp adoption)', () => { | ||
| // Bug: the isProjectRoot guard was nested inside `if (!fs.existsSync(dir))`, | ||
| // so when Claude Code had already created ~/.claude/projects/<slug>/memory | ||
| // (it does this for every session, incl. the ~2035 headless /tmp mem-lite | ||
| // calls), adopt() sailed past the guard and wrote its sentinel into /tmp's | ||
| // MEMORY.md. Pre-fix this test FAILS (adopt returns ok:true and writes). | ||
| const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-')); | ||
| const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-')); // no project marker | ||
| const dir = memoryDir(cwd, home); | ||
| fs.mkdirSync(dir, { recursive: true }); // simulate CC pre-creating the memory dir | ||
| try { | ||
| const res = adopt({ cwd, home }); | ||
| assert.strictEqual(res.ok, false); | ||
| assert.strictEqual(res.reason, 'not-a-project'); | ||
| const indexPath = path.join(dir, 'MEMORY.md'); | ||
| assert.ok( | ||
| !fs.existsSync(indexPath) || !fs.readFileSync(indexPath, 'utf8').includes(SENTINEL_BEGIN), | ||
| 'must NOT write the code-graph sentinel into a non-project MEMORY.md' | ||
| ); | ||
| } finally { | ||
| fs.rmSync(home, { recursive: true, force: true }); | ||
| fs.rmSync(cwd, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| test('adopt fails gracefully when cwd is not a project root', () => { | ||
@@ -92,0 +121,0 @@ // v0.16.9: behavior change — adopt now mkdir's the memory dir when cwd has |
@@ -13,2 +13,3 @@ #!/usr/bin/env node | ||
| const fs = require('fs'); | ||
| const { isNonProjectCwd } = require('./project-detect'); | ||
@@ -92,2 +93,18 @@ // Set plugin root so find-binary.js can locate bundled/dev binaries | ||
| // --- Non-project cwd gate --------------------------------------------------- | ||
| // In a non-project working directory (no .git/manifest — e.g. /tmp, where | ||
| // claude-mem-lite spawns ~2035 headless `claude -p` JSON-extraction calls that | ||
| // never use code-graph), don't spawn the binary at all: serve the same 0-tool | ||
| // stub. Eliminates the MCP-server spin-up + the ~780B `instructions` block + | ||
| // an empty .code-graph/index.db being created in throwaway dirs. Same | ||
| // CODE_GRAPH_FORCE_PLUGIN_MCP=1 override as the dedup gate above. | ||
| if (process.env.CODE_GRAPH_FORCE_PLUGIN_MCP !== '1' && isNonProjectCwd(process.cwd())) { | ||
| process.stderr.write( | ||
| '[code-graph] non-project cwd (no .git/manifest); plugin MCP serving 0 tools, ' + | ||
| 'no index created. Set CODE_GRAPH_FORCE_PLUGIN_MCP=1 to override.\n' | ||
| ); | ||
| serveEmptyMcpStub(); | ||
| return; | ||
| } | ||
| const { findBinary, clearCache } = require('./find-binary'); | ||
@@ -94,0 +111,0 @@ |
@@ -36,3 +36,3 @@ #!/usr/bin/env node | ||
| */ | ||
| function runLauncherInitialize(timeoutMs = 15000, extraEnv = {}) { | ||
| function runLauncherInitialize(timeoutMs = 15000, extraEnv = {}, cwd = REPO_ROOT) { | ||
| return new Promise((resolve, reject) => { | ||
@@ -42,3 +42,3 @@ const child = spawn(process.execPath, [LAUNCHER], { | ||
| env: { ...process.env, ...extraEnv }, | ||
| cwd: REPO_ROOT, | ||
| cwd, | ||
| }); | ||
@@ -120,2 +120,26 @@ | ||
| test('mcp-launcher serves 0-tool stub in a non-project cwd (no binary spawn, no index created)', async (t) => { | ||
| const os = require('os'); | ||
| // A bare temp dir with no .git/manifest → isNonProjectCwd → the launcher | ||
| // serves the 0-tool stub WITHOUT spawning the binary, so no .code-graph is | ||
| // created and no `instructions` block is injected. This is the fix for the | ||
| // ~2035 headless /tmp mem-lite calls that half-activated code-graph. | ||
| const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-launcher-nonproj-')); | ||
| t.after(() => fs.rmSync(cwd, { recursive: true, force: true })); | ||
| const { stdout, stderr } = await runLauncherInitialize(15000, {}, cwd); | ||
| const respLine = stdout.trim().split('\n').find((l) => l.includes('"result"')); | ||
| assert.ok(respLine, | ||
| `expected stub JSON-RPC result on stdout, got: ${stdout.slice(0, 400)} | stderr: ${stderr.slice(0, 400)}`); | ||
| const resp = JSON.parse(respLine); | ||
| assert.match(resp.result.serverInfo.name, /stub/i, | ||
| `serverInfo.name should indicate stub mode, got ${JSON.stringify(resp.result.serverInfo)}`); | ||
| assert.equal(resp.result.instructions, undefined, | ||
| 'stub initialize must NOT carry an instructions block (the ~780B NOISY tax)'); | ||
| assert.match(stderr, /non-project cwd/, | ||
| `stderr should explain the non-project gate, got: ${stderr.slice(0, 400)}`); | ||
| assert.ok(!fs.existsSync(path.join(cwd, '.code-graph')), | ||
| 'must NOT create .code-graph in a non-project cwd'); | ||
| }); | ||
| test('mcp-launcher sets _FIND_BINARY_ROOT from __dirname (does not trust CLAUDE_PLUGIN_ROOT)', () => { | ||
@@ -122,0 +146,0 @@ // Static check: the source must derive _FIND_BINARY_ROOT from __dirname so a |
@@ -13,2 +13,3 @@ #!/usr/bin/env node | ||
| const { maybeAutoAdopt, isAdopted } = require('./adopt'); | ||
| const { isNonProjectCwd } = require('./project-detect'); | ||
@@ -272,2 +273,12 @@ // v0.17.0 — quietHooks: unconditional quiet 默认。 | ||
| // Non-project cwd (no .git/manifest — e.g. /tmp, where claude-mem-lite | ||
| // spawns headless `claude -p` calls that never use code-graph): fully no-op. | ||
| // Returns BEFORE syncLifecycleConfig / verifyBinary / ensureIndexFresh / | ||
| // maybeAutoAdopt / injectProjectMap so the plugin leaves zero footprint | ||
| // (no incremental-index spawn, no map injection, no adoption). The MCP | ||
| // launcher applies the same gate — see project-detect.js. | ||
| if (isNonProjectCwd(process.cwd())) { | ||
| return { inactive: false, nonProject: true, lifecycle: 'noop', autoUpdateLaunched: false }; | ||
| } | ||
| const conflict = checkScopeConflict(); | ||
@@ -274,0 +285,0 @@ if (conflict) { |
@@ -75,3 +75,3 @@ 'use strict'; | ||
| const { consistencyCheck } = require('./session-init'); | ||
| const { consistencyCheck, runSessionInit } = require('./session-init'); | ||
@@ -82,2 +82,23 @@ test('consistencyCheck is exported as a function', () => { | ||
| test('runSessionInit no-ops (nonProject) in a non-project cwd', (t) => { | ||
| // /tmp-style cwd (no .git/manifest) → the gate returns BEFORE | ||
| // syncLifecycleConfig / verifyBinary / ensureIndexFresh / maybeAutoAdopt / | ||
| // injectProjectMap, leaving zero footprint. Safe to call: the early return | ||
| // precedes every side-effectful step. | ||
| const os = require('os'); | ||
| const origCwd = process.cwd(); | ||
| const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-si-nonproj-')); | ||
| process.chdir(tmp); | ||
| try { | ||
| const res = runSessionInit(); | ||
| if (res.inactive) { t.skip('plugin seen inactive in this env — gate not reached'); return; } | ||
| assert.equal(res.nonProject, true); | ||
| assert.equal(res.lifecycle, 'noop'); | ||
| assert.equal(res.autoUpdateLaunched, false); | ||
| } finally { | ||
| process.chdir(origCwd); | ||
| fs.rmSync(tmp, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| test('consistencyCheck returns empty array when binary version matches plugin', () => { | ||
@@ -84,0 +105,0 @@ const result = consistencyCheck('/tmp/nonexistent-binary'); |
+6
-6
| { | ||
| "name": "@sdsrs/code-graph", | ||
| "version": "0.32.3", | ||
| "version": "0.33.0", | ||
| "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing", | ||
@@ -38,8 +38,8 @@ "license": "MIT", | ||
| "optionalDependencies": { | ||
| "@sdsrs/code-graph-linux-x64": "0.32.3", | ||
| "@sdsrs/code-graph-linux-arm64": "0.32.3", | ||
| "@sdsrs/code-graph-darwin-x64": "0.32.3", | ||
| "@sdsrs/code-graph-darwin-arm64": "0.32.3", | ||
| "@sdsrs/code-graph-win32-x64": "0.32.3" | ||
| "@sdsrs/code-graph-linux-x64": "0.33.0", | ||
| "@sdsrs/code-graph-linux-arm64": "0.33.0", | ||
| "@sdsrs/code-graph-darwin-x64": "0.33.0", | ||
| "@sdsrs/code-graph-darwin-arm64": "0.33.0", | ||
| "@sdsrs/code-graph-win32-x64": "0.33.0" | ||
| } | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
409881
2.68%48
4.35%8165
2.42%127
1.6%