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.30.0
to
0.31.0
+21
claude-plugin/scripts/claude-config.js
'use strict';
const path = require('path');
const os = require('os');
// Resolve Claude Code's config directory. Honors CLAUDE_CONFIG_DIR — when set
// (commonly used to keep multiple accounts isolated, e.g. personal vs work),
// Claude Code reads `settings.json`, `plugins/`, `projects/`, etc. from there
// instead of `~/.claude/`. Our plugin must follow the same override so its
// hook registrations, statusline, adoption files, and cache cleanup land in
// the directory Claude Code is actually using.
//
// Read fresh on every call so per-process env mutation (tests, child procs
// spawned with a different env) takes effect immediately. Unlike
// CLAUDE_PLUGIN_ROOT (which leaks across plugins — see
// feedback_plugin_env_isolation.md), CLAUDE_CONFIG_DIR is user-set and
// process-wide, so reading it is safe.
function claudeHome() {
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
}
module.exports = { claudeHome };
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const path = require('path');
const os = require('os');
const { claudeHome } = require('./claude-config');
test('claudeHome defaults to ~/.claude when CLAUDE_CONFIG_DIR unset', () => {
const prev = process.env.CLAUDE_CONFIG_DIR;
delete process.env.CLAUDE_CONFIG_DIR;
try {
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
} finally {
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
}
});
test('claudeHome honors CLAUDE_CONFIG_DIR when set', () => {
const prev = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = '/tmp/work-claude';
try {
assert.strictEqual(claudeHome(), '/tmp/work-claude');
} finally {
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prev;
}
});
test('claudeHome re-reads env on every call (not cached)', () => {
const prev = process.env.CLAUDE_CONFIG_DIR;
delete process.env.CLAUDE_CONFIG_DIR;
try {
const before = claudeHome();
process.env.CLAUDE_CONFIG_DIR = '/tmp/account-A';
const during = claudeHome();
delete process.env.CLAUDE_CONFIG_DIR;
const after = claudeHome();
assert.strictEqual(before, path.join(os.homedir(), '.claude'));
assert.strictEqual(during, '/tmp/account-A');
assert.strictEqual(after, path.join(os.homedir(), '.claude'));
} finally {
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
}
});
test('claudeHome ignores empty CLAUDE_CONFIG_DIR (falls back to ~/.claude)', () => {
// Empty string is falsy in JS — sanity-check the `||` fallback path so an
// accidentally `CLAUDE_CONFIG_DIR=` (unset-style) shell line does not strand
// us writing to the literal repository root `/`.
const prev = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = '';
try {
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
} finally {
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prev;
}
});
+1
-1

@@ -7,3 +7,3 @@ {

},
"version": "0.30.0",
"version": "0.31.0",
"keywords": [

@@ -10,0 +10,0 @@ "code-graph",

@@ -272,5 +272,10 @@ #!/usr/bin/env node

// `/home/sds/.claude/x` → `-home-sds--claude-x` (double-dash from `/.`)
//
// `home` is the OS home dir (default `os.homedir()`). When `CLAUDE_CONFIG_DIR`
// is set it overrides `home/.claude`, so multi-account users (personal vs work)
// land in the directory Claude Code itself is using for `projects/`.
function memoryDir(cwd = process.cwd(), home = os.homedir()) {
const slug = cwd.replace(/[^a-zA-Z0-9-]/g, '-');
return path.join(home, '.claude', 'projects', slug, 'memory');
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude');
return path.join(claudeDir, 'projects', slug, 'memory');
}

@@ -425,5 +430,13 @@

// (见 feedback_plugin_env_isolation.md)。
// 默认匹配 `.claude/plugins/` 路径;CLAUDE_CONFIG_DIR 自定义目录时
// 走 startsWith(CLAUDE_CONFIG_DIR/plugins/) 兜底。
function isPluginModeInstall(scriptPath = __dirname) {
const sep = path.sep;
return scriptPath.includes(`${sep}.claude${sep}plugins${sep}`);
if (scriptPath.includes(`${sep}.claude${sep}plugins${sep}`)) return true;
const envDir = process.env.CLAUDE_CONFIG_DIR;
if (envDir) {
const marker = path.join(envDir, 'plugins') + sep;
if (scriptPath.startsWith(marker)) return true;
}
return false;
}

@@ -430,0 +443,0 @@

@@ -281,2 +281,40 @@ 'use strict';

test('memoryDir honors CLAUDE_CONFIG_DIR override (multi-account isolation)', () => {
const prev = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = '/home/alice/work-claude';
try {
// home arg is irrelevant when env var is set — projects live under the
// configured claude dir, not home/.claude.
assert.strictEqual(
memoryDir('/home/alice/proj', '/home/alice'),
'/home/alice/work-claude/projects/-home-alice-proj/memory'
);
} finally {
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prev;
}
});
test('isPluginModeInstall recognizes CLAUDE_CONFIG_DIR/plugins/... paths', () => {
const prev = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = '/home/alice/work-claude';
try {
const pluginPath = '/home/alice/work-claude/plugins/cache/code-graph-mcp@0.31.0/scripts';
assert.strictEqual(isPluginModeInstall(pluginPath), true);
// Legacy ~/.claude/plugins/ path still works even with env var set.
assert.strictEqual(
isPluginModeInstall('/home/user/.claude/plugins/cache/code-graph-mcp/scripts'),
true
);
// Unrelated path under same prefix is still rejected.
assert.strictEqual(
isPluginModeInstall('/home/alice/work-claude/projects/foo/memory'),
false
);
} finally {
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prev;
}
});
test('maybeAutoAdopt skips when CODE_GRAPH_NO_AUTO_ADOPT=1', () => {

@@ -283,0 +321,0 @@ const sb = makeSandbox();

@@ -8,3 +8,3 @@ #!/usr/bin/env node

const os = require('os');
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
const { clearCache: clearBinaryCache } = require('./find-binary');

@@ -276,3 +276,3 @@ const { readBinaryVersion, isDevMode } = require('./version-utils');

const pluginDst = path.join(
os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, 'code-graph-mcp', latest.version
pluginsCacheDir(), MARKETPLACE_NAME, 'code-graph-mcp', latest.version
);

@@ -287,3 +287,3 @@

// Update installed_plugins.json to point to new version
const installedPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
const installedPath = installedPluginsPath();
try {

@@ -290,0 +290,0 @@ const installed = readJson(installedPath);

@@ -11,2 +11,3 @@ #!/usr/bin/env node

removeHooksFromSettings, isOurHookEntry, writeJsonAtomic,
settingsPath,
} = require('./lifecycle');

@@ -194,4 +195,3 @@ const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');

try {
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
const settings = readJson(SETTINGS_PATH) || {};
const settings = readJson(settingsPath()) || {};
const legacyCount = countLegacyHookEntries(settings);

@@ -382,6 +382,6 @@ if (legacyCount === 0) {

console.log('\n Removing legacy code-graph hooks from settings.json...');
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
const settings = readJson(SETTINGS_PATH) || {};
const settingsFile = settingsPath();
const settings = readJson(settingsFile) || {};
if (removeHooksFromSettings(settings)) {
writeJsonAtomic(SETTINGS_PATH, settings);
writeJsonAtomic(settingsFile, settings);
console.log(' \u2705 settings.json cleaned — restart Claude Code to apply');

@@ -388,0 +388,0 @@ fixed++;

@@ -98,1 +98,47 @@ 'use strict';

test('lifecycle install writes to CLAUDE_CONFIG_DIR instead of ~/.claude when set', (t) => {
// Multi-account isolation: a user with CLAUDE_CONFIG_DIR=~/work-claude
// expects all plugin config (settings.json, installed_plugins.json,
// statusline-providers backup) to land under that directory, not the
// default ~/.claude. Default path must remain untouched.
const homeDir = mkHome(t);
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cfgdir-'));
t.after(() => fs.rmSync(configDir, { recursive: true, force: true }));
const cfgSettings = path.join(configDir, 'settings.json');
const cfgInstalled = path.join(configDir, 'plugins', 'installed_plugins.json');
const cfgBackup = path.join(configDir, 'statusline-providers.json');
const defaultSettings = path.join(homeDir, '.claude', 'settings.json');
writeJson(cfgSettings, {
statusLine: { type: 'command', command: 'echo prior-work-status' },
enabledPlugins: { 'code-graph-mcp@code-graph-mcp': true },
});
writeJson(cfgInstalled, {
plugins: {
'code-graph-mcp@code-graph-mcp': [{
installPath: pluginRoot,
version: currentVersion,
scope: 'user',
}],
},
});
// Run install with CLAUDE_CONFIG_DIR set; HOME points elsewhere.
const env = { ...process.env, HOME: homeDir, CLAUDE_CONFIG_DIR: configDir };
delete env.CLAUDE_PLUGIN_ROOT;
execFileSync(process.execPath, [lifecycleCli, 'install'], {
cwd: repoRoot, env, stdio: ['pipe', 'pipe', 'pipe'],
});
// Config landed in the override dir...
const settings = readJson(cfgSettings);
assert.match(settings.statusLine.command, /statusline-composite\.js/);
assert.equal(fs.existsSync(cfgBackup), true,
'statusline-providers backup should land in CLAUDE_CONFIG_DIR');
// ...and default ~/.claude was never touched.
assert.equal(fs.existsSync(defaultSettings), false,
'default ~/.claude/settings.json must not be written when override is set');
});

@@ -6,2 +6,3 @@ #!/usr/bin/env node

const os = require('os');
const { claudeHome } = require('./claude-config');

@@ -20,9 +21,14 @@ const PLUGIN_ID = 'code-graph-mcp@code-graph-mcp';

const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
const REGISTRY_FILE = path.join(CACHE_DIR, 'statusline-registry.json');
// Lazy resolvers — Claude Code's config dir can be overridden by CLAUDE_CONFIG_DIR
// (multi-account isolation). Re-read every call so test subprocesses with a
// different env see the right path.
function settingsPath() { return path.join(claudeHome(), 'settings.json'); }
function installedPluginsPath() { return path.join(claudeHome(), 'plugins', 'installed_plugins.json'); }
// Durable mirror outside ~/.cache/ — survives cache cleanup. Captures the
// `_previous` snapshot (pre-install statusline) and any third-party providers
// (GSD, etc.). readRegistry() self-heals from this file when primary is missing.
const PROVIDERS_BACKUP_FILE = path.join(os.homedir(), '.claude', 'statusline-providers.json');
function providersBackupFile() { return path.join(claudeHome(), 'statusline-providers.json'); }
function pluginsCacheDir() { return path.join(claudeHome(), 'plugins', 'cache'); }

@@ -70,3 +76,3 @@ // --- Helpers ---

function hasInstalledPluginRecord() {
const installed = readJson(INSTALLED_PLUGINS_PATH);
const installed = readJson(installedPluginsPath());
return !!(installed && installed.plugins && Array.isArray(installed.plugins[PLUGIN_ID]) && installed.plugins[PLUGIN_ID].length > 0);

@@ -89,3 +95,3 @@ }

// Durable backup in ~/.claude/ retains `_previous` + third-party providers.
const backup = readJson(PROVIDERS_BACKUP_FILE);
const backup = readJson(providersBackupFile());
if (backup && Array.isArray(backup) && backup.length > 0) {

@@ -101,3 +107,3 @@ try { writeJsonAtomic(REGISTRY_FILE, backup); } catch { /* ok */ }

try { fs.unlinkSync(REGISTRY_FILE); } catch { /* ok */ }
try { fs.unlinkSync(PROVIDERS_BACKUP_FILE); } catch { /* ok */ }
try { fs.unlinkSync(providersBackupFile()); } catch { /* ok */ }
return;

@@ -108,3 +114,3 @@ }

// or third-party provider entries.
try { writeJsonAtomic(PROVIDERS_BACKUP_FILE, registry); } catch { /* ok */ }
try { writeJsonAtomic(providersBackupFile(), registry); } catch { /* ok */ }
}

@@ -135,7 +141,7 @@

function isPluginExplicitlyDisabled(settings = readJson(SETTINGS_PATH) || {}) {
function isPluginExplicitlyDisabled(settings = readJson(settingsPath()) || {}) {
return hasOwn(settings.enabledPlugins, PLUGIN_ID) && settings.enabledPlugins[PLUGIN_ID] === false;
}
function isPluginInactive(settings = readJson(SETTINGS_PATH) || {}) {
function isPluginInactive(settings = readJson(settingsPath()) || {}) {
if (isPluginExplicitlyDisabled(settings)) return true;

@@ -147,3 +153,3 @@

const installed = readJson(INSTALLED_PLUGINS_PATH);
const installed = readJson(installedPluginsPath());
if (!installed || !installed.plugins) return false;

@@ -176,3 +182,3 @@ return !hasInstalledPluginRecord();

function cleanupDisabledStatusline() {
const settings = readJson(SETTINGS_PATH);
const settings = readJson(settingsPath());
if (!settings || !isPluginInactive(settings)) {

@@ -185,3 +191,3 @@ return { cleaned: false, settingsChanged: false };

if (settingsChanged) {
writeJsonAtomic(SETTINGS_PATH, settings);
writeJsonAtomic(settingsPath(), settings);
}

@@ -195,3 +201,3 @@

function checkScopeConflict() {
const installed = readJson(INSTALLED_PLUGINS_PATH);
const installed = readJson(installedPluginsPath());
if (!installed || !installed.plugins) return null;

@@ -221,6 +227,6 @@ for (const [key, entries] of Object.entries(installed.plugins)) {

// Clean old ID from installed_plugins.json
const installed = readJson(INSTALLED_PLUGINS_PATH);
const installed = readJson(installedPluginsPath());
if (installed && installed.plugins && oldId in installed.plugins) {
delete installed.plugins[oldId];
writeJsonAtomic(INSTALLED_PLUGINS_PATH, installed);
writeJsonAtomic(installedPluginsPath(), installed);
}

@@ -240,6 +246,7 @@ }

// Clean old cache paths
const cacheRoot = pluginsCacheDir();
const oldCacheDirs = [
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph'),
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss-code-graph', 'code-graph'),
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss-code-graph'),
path.join(cacheRoot, 'sdsrss', 'code-graph'),
path.join(cacheRoot, 'sdsrss-code-graph', 'code-graph'),
path.join(cacheRoot, 'sdsrss-code-graph'),
];

@@ -300,3 +307,3 @@ for (const dir of oldCacheDirs) {

const manifest = readManifest();
const settings = readJson(SETTINGS_PATH) || {};
const settings = readJson(settingsPath()) || {};
let settingsChanged = false;

@@ -346,3 +353,3 @@

if (settingsChanged) {
writeJsonAtomic(SETTINGS_PATH, settings);
writeJsonAtomic(settingsPath(), settings);
}

@@ -362,3 +369,3 @@

function uninstall() {
const settings = readJson(SETTINGS_PATH);
const settings = readJson(settingsPath());
let settingsChanged = false;

@@ -389,3 +396,3 @@

if (settingsChanged) {
writeJsonAtomic(SETTINGS_PATH, settings);
writeJsonAtomic(settingsPath(), settings);
}

@@ -395,3 +402,3 @@ }

// 5. Remove all known IDs from installed_plugins.json
const installedPlugins = readJson(INSTALLED_PLUGINS_PATH);
const installedPlugins = readJson(installedPluginsPath());
if (installedPlugins && installedPlugins.plugins) {

@@ -405,3 +412,3 @@ let ipChanged = false;

}
if (ipChanged) writeJsonAtomic(INSTALLED_PLUGINS_PATH, installedPlugins);
if (ipChanged) writeJsonAtomic(installedPluginsPath(), installedPlugins);
}

@@ -413,6 +420,7 @@

// 7. Remove plugin files from cache (all known paths, including parent dirs)
const cacheRoot = pluginsCacheDir();
const pluginCacheDirs = [
path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME),
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss-code-graph'),
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph'),
path.join(cacheRoot, MARKETPLACE_NAME),
path.join(cacheRoot, 'sdsrss-code-graph'),
path.join(cacheRoot, 'sdsrss', 'code-graph'),
];

@@ -432,3 +440,3 @@ for (const dir of pluginCacheDirs) {

const oldVersion = manifest.version;
const settings = readJson(SETTINGS_PATH) || {};
const settings = readJson(settingsPath()) || {};
let settingsChanged = false;

@@ -462,3 +470,3 @@

if (settingsChanged) {
writeJsonAtomic(SETTINGS_PATH, settings);
writeJsonAtomic(settingsPath(), settings);
}

@@ -488,3 +496,3 @@

function cleanupOldCacheVersions(keep = 3) {
const cacheParent = path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME);
const cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME);
try {

@@ -524,3 +532,3 @@ // List all subdirectories under the marketplace cache

function healthCheck() {
const settings = readJson(SETTINGS_PATH) || {};
const settings = readJson(settingsPath()) || {};
const issues = [];

@@ -581,3 +589,3 @@

PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
PROVIDERS_BACKUP_FILE,
settingsPath, installedPluginsPath, providersBackupFile, pluginsCacheDir,
};

@@ -584,0 +592,0 @@

@@ -5,3 +5,2 @@ #!/usr/bin/env node

const path = require('path');
const os = require('os');
const fs = require('fs');

@@ -11,2 +10,3 @@ const {

cleanupDisabledStatusline, isPluginInactive, readJson, CACHE_DIR,
settingsPath,
} = require('./lifecycle');

@@ -63,3 +63,3 @@ const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');

// install() is idempotent — isOurComposite guard prevents duplicate work.
const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')) || {};
const settings = readJson(settingsPath()) || {};
if (!settings.statusLine || !settings.statusLine.command ||

@@ -66,0 +66,0 @@ !settings.statusLine.command.includes('statusline-composite')) {

{
"name": "@sdsrs/code-graph",
"version": "0.30.0",
"version": "0.31.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.30.0",
"@sdsrs/code-graph-linux-arm64": "0.30.0",
"@sdsrs/code-graph-darwin-x64": "0.30.0",
"@sdsrs/code-graph-darwin-arm64": "0.30.0",
"@sdsrs/code-graph-win32-x64": "0.30.0"
"@sdsrs/code-graph-linux-x64": "0.31.0",
"@sdsrs/code-graph-linux-arm64": "0.31.0",
"@sdsrs/code-graph-darwin-x64": "0.31.0",
"@sdsrs/code-graph-darwin-arm64": "0.31.0",
"@sdsrs/code-graph-win32-x64": "0.31.0"
}
}