Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@sdsrs/code-graph

Package Overview
Dependencies
Maintainers
1
Versions
154
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sdsrs/code-graph - npm Package Compare versions

Comparing version
0.32.3
to
0.33.0
+43
claude-plugin/scripts/project-detect.js
#!/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`);
}
});
+1
-1

@@ -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');

{
"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"
}
}