Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ Or install for Antigravity CLI ([setup guide](https://docs.claude-mem.ai/antigra
npx claude-mem install --ide antigravity
```

Or install for Codex CLI:

```bash
npx claude-mem install --ide codex-cli
```

Or install from the plugin marketplace inside Claude Code:

```bash
Expand All @@ -156,7 +162,7 @@ Or install from the plugin marketplace inside Claude Code:
/plugin install claude-mem
```

Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
Restart the selected IDE. Context from previous sessions will automatically appear in new sessions.

> **Note:** Claude-Mem is also published on npm, but `npm install -g claude-mem` installs the **SDK/library only** — it does not register the plugin hooks or set up the worker service. Always install via `npx claude-mem install` or the `/plugin` commands above.

Expand Down
90 changes: 45 additions & 45 deletions plugin/scripts/mcp-server.cjs

Large diffs are not rendered by default.

98 changes: 95 additions & 3 deletions src/npx-cli/install/setup-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { exec, execSync, spawnSync } from 'child_process';
import { exec, execFile, execSync, spawnSync } from 'child_process';
import { createRequire } from 'module';
import { join } from 'path';
import { homedir } from 'os';
Expand Down Expand Up @@ -92,7 +92,7 @@ function isBunInstalled(): boolean {
return getBunPath() !== null;
}

function getBunVersion(): string | null {
export function getBunVersion(): string | null {
const bunPath = getBunPath();
if (!bunPath) return null;

Expand Down Expand Up @@ -129,7 +129,7 @@ function isUvInstalled(): boolean {
return getUvPath() !== null;
}

function getUvVersion(): string | null {
export function getUvVersion(): string | null {
const uvPath = getUvPath();
if (!uvPath) return null;

Expand Down Expand Up @@ -246,6 +246,66 @@ function installUv(): void {
* we resolve subpaths, never a pinned version.
*/
const ZOD_REQUIRED_SUBPATHS = ['zod/v3', 'zod/v4', 'zod/v4-mini'] as const;
const TREE_SITTER_VERSION_TIMEOUT_MS = 10_000;

export function treeSitterCliBinaryPath(targetDir: string): string {
return join(
targetDir,
'node_modules',
'tree-sitter-cli',
IS_WINDOWS ? 'tree-sitter.exe' : 'tree-sitter',
);
}

export function isTreeSitterCliBinaryUsable(targetDir: string): boolean {
const result = spawnSync(treeSitterCliBinaryPath(targetDir), ['--version'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: TREE_SITTER_VERSION_TIMEOUT_MS,
windowsHide: true,
});
return result.status === 0
&& /^tree-sitter \d+\.\d+\.\d+(?:\s|$)/.test((result.stdout ?? '').trim());
}

/**
* Runtime installs suppress all dependency lifecycle scripts to avoid running
* unbounded transitive postinstalls. tree-sitter-cli is the one intentional
* exception: its install script downloads the platform CLI used by smart-explore.
* Run only that top-level script, with the installer's existing timeout.
*/
export async function ensureTreeSitterCliBinary(
targetDir: string,
isUsable: (targetDir: string) => boolean = isTreeSitterCliBinaryUsable,
): Promise<void> {
const binaryPath = treeSitterCliBinaryPath(targetDir);
if (isUsable(targetDir)) return;

const cliDir = join(targetDir, 'node_modules', 'tree-sitter-cli');
const installScript = join(cliDir, 'install.js');
if (!existsSync(installScript)) {
throw new Error(`tree-sitter-cli install script not found: ${installScript}`);
}

await new Promise<void>((resolve, reject) => {
execFile(process.execPath, [installScript], {
cwd: cliDir,
timeout: INSTALL_TIMEOUT_MS,
maxBuffer: 16 * 1024 * 1024,
windowsHide: true,
}, (error, stdout, stderr) => {
if (error) {
reject(Object.assign(error, { stdout, stderr }));
return;
}
resolve();
});
});

if (!isUsable(targetDir)) {
throw new Error(`tree-sitter-cli install completed without creating a working executable ${binaryPath}`);
}
}

export function verifyCriticalModules(targetDir: string): void {
const pkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
Expand Down Expand Up @@ -297,6 +357,10 @@ export function verifyCriticalModules(targetDir: string): void {
}
}

if (dependencies.includes('tree-sitter-cli') && !isTreeSitterCliBinaryUsable(targetDir)) {
unresolvable.push('tree-sitter-cli executable');
}

if (unresolvable.length > 0) {
throw new Error(
`Post-install check failed: unresolvable modules: ${unresolvable.join(', ')}`,
Expand Down Expand Up @@ -460,6 +524,13 @@ export async function installPluginDependencies(targetDir: string, bunPath: stri
throw new Error(`bun install failed in ${targetDir}\n${describeExecError(err)}`);
}

try {
await ensureTreeSitterCliBinary(targetDir);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
throw new Error(`tree-sitter-cli setup failed in ${targetDir}\n${describeExecError(err)}`);
}

verifyCriticalModules(targetDir);
}

Expand Down Expand Up @@ -499,6 +570,19 @@ export function writeInstallMarker(
writeFileSync(markerPath(targetDir), JSON.stringify(payload));
}

function pluginDeclaresTreeSitterCli(targetDir: string): boolean {
try {
const pkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
return Object.keys(pkg.dependencies || {}).includes('tree-sitter-cli');
} catch {
// [ANTI-PATTERN IGNORED]: a missing or unparseable package.json in an
// incomplete/partial install is an expected negative — treat it as "does
// not declare tree-sitter-cli" so the caller re-provisions rather than
// throwing.
return false;
}
}

export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean {
if (!existsSync(join(targetDir, 'node_modules'))) return false;
const marker = readInstallMarker(targetDir);
Expand All @@ -508,5 +592,13 @@ export function isInstallCurrent(targetDir: string, expectedVersion: string): bo
if (currentBun && !marker.bun) return false;
if (!currentBun && marker.bun) return false;
if (currentBun && marker.bun && currentBun !== marker.bun) return false;
// A present marker is not sufficient if the smart-explore runtime it vouches
// for is missing. A prior install can write .install-version and later lose
// (or never have downloaded) the tree-sitter-cli executable; without this
// check the install fast path treats that cache as current and skips the
// provisioning/validation that would repair it, leaving smart-explore broken.
if (pluginDeclaresTreeSitterCli(targetDir) && !isTreeSitterCliBinaryUsable(targetDir)) {
return false;
}
return true;
}
79 changes: 72 additions & 7 deletions src/services/integrations/CodexCliInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { logger } from '../../utils/logger.js';
import { paths } from '../../shared/paths.js';
import {
ensureTreeSitterCliBinary,
getBunVersion,
getUvVersion,
writeInstallMarker,
} from '../../npx-cli/install/setup-runtime.js';

const CODEX_DIR = path.join(homedir(), '.codex');
const CODEX_AGENTS_MD_PATH = path.join(CODEX_DIR, 'AGENTS.md');
Expand Down Expand Up @@ -94,6 +100,69 @@ function resolvePluginMarketplaceRoot(preferredRoot?: string): string {
throw new Error('Could not locate a Codex marketplace root with .agents/plugins/marketplace.json and plugin/.codex-plugin/plugin.json. Run npx claude-mem@latest install from the package or repo root.');
}

function readCodexPluginVersion(marketplaceRoot: string): string {
const manifestPath = path.join(marketplaceRoot, 'plugin', '.codex-plugin', 'plugin.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { version?: unknown };
if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
throw new Error(`Codex plugin manifest has no version: ${manifestPath}`);
}
return manifest.version;
}

export function resolveCodexPluginCacheDirectory(
marketplaceRoot: string,
codexDir: string = CODEX_DIR,
): string {
return path.join(
codexDir,
'plugins',
'cache',
MARKETPLACE_NAME,
'claude-mem',
readCodexPluginVersion(marketplaceRoot),
);
}

type CodexPluginCacheInstallOptions = {
codexDir?: string;
runBestEffort?: typeof runCodexBestEffort;
ensureRuntime?: typeof ensureTreeSitterCliBinary;
};

export async function installCodexPluginCache(
marketplaceRoot: string,
options: CodexPluginCacheInstallOptions = {},
): Promise<void> {
const runBestEffort = options.runBestEffort ?? runCodexBestEffort;
const ensureRuntime = options.ensureRuntime ?? ensureTreeSitterCliBinary;
const installed = runBestEffort(
['plugin', 'add', CODEX_PLUGIN_ID],
'Installed plugin from the local Codex marketplace.',
'Could not install claude-mem from the local Codex marketplace',
);
if (!installed) {
throw new Error('Codex plugin cache could not be installed');
}

const codexCacheDir = resolveCodexPluginCacheDirectory(
marketplaceRoot,
options.codexDir,
);
await ensureRuntime(codexCacheDir);

// Write the install marker only after the runtime is provisioned, so
// `.install-version` is a truthful "runtime ready" signal rather than an
// optimistic one copied before `plugin add`. Without it, version-check.js
// emits a misleading "runtime not yet set up" hint for a working Codex cache.
writeInstallMarker(
codexCacheDir,
readCodexPluginVersion(marketplaceRoot),
getBunVersion() ?? '',
getUvVersion() ?? '',
);
console.log(' Smart-explore Tree-sitter runtime ready.');
}

function lookupCodexOnWindows(): string | null {
let stdout: string;
try {
Expand Down Expand Up @@ -481,26 +550,22 @@ export async function installCodexCli(marketplaceRootOverride?: string): Promise
}

try {
return performCodexInstall(marketplaceRootOverride);
return await performCodexInstall(marketplaceRootOverride);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`\nInstallation failed: ${message}`);
return 1;
}
}

function performCodexInstall(marketplaceRootOverride?: string): number {
async function performCodexInstall(marketplaceRootOverride?: string): Promise<number> {
assertCodexMarketplaceSupported();
const marketplaceRoot = resolvePluginMarketplaceRoot(marketplaceRootOverride);

console.log(` Registering Codex plugin marketplace: ${marketplaceRoot}`);
registerCodexMarketplace(marketplaceRoot);
enableCodexPluginConfig();
runCodexBestEffort(
['plugin', 'marketplace', 'upgrade', MARKETPLACE_NAME],
'Refreshed Codex marketplace and installed plugin cache.',
'Could not refresh Codex marketplace cache; reinstall or upgrade claude-mem from /plugins if Codex still uses old MCP config',
);
await installCodexPluginCache(marketplaceRoot);
if (!cleanupLegacyCodexAgentsMdContext()) {
console.warn(` Native Codex hooks registered, but failed to remove legacy AGENTS.md context from ${CODEX_AGENTS_MD_PATH}.`);
}
Expand Down
67 changes: 60 additions & 7 deletions src/services/smart-file-read/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,25 +353,70 @@ function getQueryFile(queryKey: string): string {
}

let cachedBinPath: string | null = null;
let treeSitterCacheDir: string | null = null;

function getTreeSitterBin(): string {
type TreeSitterBinDeps = {
platform: NodeJS.Platform;
fileExists: typeof existsSync;
resolvePackageJson: (id: string) => string;
};

const treeSitterBinDeps: TreeSitterBinDeps = {
platform: process.platform,
fileExists: existsSync,
resolvePackageJson: (id: string) => _require.resolve(id),
};

export function resetTreeSitterBinCacheForTests(): void {
cachedBinPath = null;
}

export function setTreeSitterBinDepsForTests(
overrides?: Partial<TreeSitterBinDeps>,
): void {
treeSitterBinDeps.platform = overrides?.platform ?? process.platform;
treeSitterBinDeps.fileExists = overrides?.fileExists ?? existsSync;
treeSitterBinDeps.resolvePackageJson = overrides?.resolvePackageJson ?? ((id: string) => _require.resolve(id));
}

export function getTreeSitterBin(): string {
if (cachedBinPath) return cachedBinPath;

let pkgPath: string;
try {
const pkgPath = _require.resolve("tree-sitter-cli/package.json");
const binPath = join(dirname(pkgPath), "tree-sitter");
if (existsSync(binPath)) {
pkgPath = treeSitterBinDeps.resolvePackageJson("tree-sitter-cli/package.json");
} catch {
// [ANTI-PATTERN IGNORED]: tree-sitter-cli not in node_modules is expected; falls back to PATH
cachedBinPath = "tree-sitter";
return cachedBinPath;
}

// tree-sitter-cli's install.js writes `tree-sitter.exe` on Windows and an
// extensionless `tree-sitter` elsewhere. Checking only the extensionless
// name discards a valid Windows binary and falls through to a bare-PATH
// lookup that usually fails, producing silent 0-symbol results (#2910 / #1247).
const candidateNames = treeSitterBinDeps.platform === "win32"
? ["tree-sitter.exe", "tree-sitter"]
: ["tree-sitter"];
for (const candidateName of candidateNames) {
const binPath = join(dirname(pkgPath), candidateName);
if (treeSitterBinDeps.fileExists(binPath)) {
cachedBinPath = binPath;
return binPath;
}
} catch {
// [ANTI-PATTERN IGNORED]: tree-sitter-cli not in node_modules is expected; falls back to PATH
}

cachedBinPath = "tree-sitter";
return cachedBinPath;
}

function getTreeSitterCacheDir(): string {
if (!treeSitterCacheDir) {
treeSitterCacheDir = mkdtempSync(join(tmpdir(), "claude-mem-tree-sitter-cache-"));
}
return treeSitterCacheDir;
}

interface RawCapture {
tag: string;
startRow: number;
Expand Down Expand Up @@ -399,7 +444,15 @@ function runBatchQuery(queryFile: string, sourceFiles: string[], grammarPath: st

let output: string;
try {
output = execFileSync(bin, execArgs, { encoding: "utf-8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] });
output = execFileSync(bin, execArgs, {
encoding: "utf-8",
timeout: 30000,
stdio: ["pipe", "pipe", "pipe"],
// `tree-sitter query -p` compiles grammars and writes lock/library files
// below XDG_CACHE_HOME. Agent sandboxes commonly make the user's home
// read-only, so use a process-local writable cache instead.
env: { ...process.env, XDG_CACHE_HOME: getTreeSitterCacheDir() },
});
} catch (error) {
logger.debug('WORKER', `tree-sitter query failed for ${sourceFiles.length} file(s)`, undefined, error instanceof Error ? error : undefined);
return new Map();
Expand Down
Loading
Loading