Files
claude-mem/src/npx-cli/commands/runtime.ts
T
Alex Newman d2eb89d27f fix: resolve all CodeRabbit review comments
Critical fixes:
- GeminiCliHooksInstaller: wrap install/uninstall prep in try/catch to maintain
  numeric return contract
- McpIntegrations: move mkdirSync inside try block for Goose installer

Major fixes:
- import-xml-observations: guard invalid dates before toISOString()
- runtime.ts: guard response.json() parsing
- WorktreeAdoption: delay adoptedSqliteIds mutation until SQL succeeds
- CursorHooksInstaller: move mkdirSync inside try block
- McpIntegrations: throw on failed claude-mem block replacement
- OpenClawInstaller: propagate parse failure instead of returning {}
- OpenClawInstaller: move mkdirSync inside try block
- WindsurfHooksInstaller: validate hooks.json shape with optional chaining
- timeline/queries: pass normalized Error directly to logger
- ChromaSync: use composite dedup key (entityType:id) to prevent cross-type collisions
- EnvManager: wrap preflight directory/file ops in try/catch with ENV logging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 20:17:57 -07:00

233 lines
7.0 KiB
TypeScript

/**
* Runtime command routing for `npx claude-mem start|stop|restart|status|search|transcript`.
*
* These commands delegate to the installed plugin's worker-service.cjs via Bun,
* or hit the worker's HTTP API directly (for `search`).
*
* Pure Node.js — no Bun APIs used.
*/
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import pc from 'picocolors';
import { resolveBunBinaryPath } from '../utils/bun-resolver.js';
import { isPluginInstalled, marketplaceDirectory } from '../utils/paths.js';
// ---------------------------------------------------------------------------
// Installation guard
// ---------------------------------------------------------------------------
function ensureInstalledOrExit(): void {
if (!isPluginInstalled()) {
console.error(pc.red('claude-mem is not installed.'));
console.error(`Run: ${pc.bold('npx claude-mem install')}`);
process.exit(1);
}
}
// ---------------------------------------------------------------------------
// Bun guard
// ---------------------------------------------------------------------------
function resolveBunOrExit(): string {
const bunPath = resolveBunBinaryPath();
if (!bunPath) {
console.error(pc.red('Bun not found.'));
console.error('Install Bun: https://bun.sh');
console.error('After installation, restart your terminal.');
process.exit(1);
}
return bunPath;
}
// ---------------------------------------------------------------------------
// Worker-service path
// ---------------------------------------------------------------------------
function workerServiceScriptPath(): string {
return join(marketplaceDirectory(), 'plugin', 'scripts', 'worker-service.cjs');
}
// ---------------------------------------------------------------------------
// Spawn helper
// ---------------------------------------------------------------------------
function spawnBunWorkerCommand(command: string, extraArgs: string[] = []): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const workerScript = workerServiceScriptPath();
if (!existsSync(workerScript)) {
console.error(pc.red(`Worker script not found at: ${workerScript}`));
console.error('The installation may be corrupted. Try: npx claude-mem install');
process.exit(1);
}
const args = [workerScript, command, ...extraArgs];
const child = spawn(bunPath, args, {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start Bun: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function runStartCommand(): void {
spawnBunWorkerCommand('start');
}
export function runStopCommand(): void {
spawnBunWorkerCommand('stop');
}
export function runRestartCommand(): void {
spawnBunWorkerCommand('restart');
}
export function runStatusCommand(): void {
spawnBunWorkerCommand('status');
}
/**
* Stamp merged-worktree provenance on observations/summaries and keep Chroma
* metadata in lockstep. Delegates to the worker-service.cjs `adopt` subcommand
* so adoption runs in Bun (needed for bun:sqlite) while preserving the user's
* working directory — that's what the engine uses to locate the parent repo.
*/
export function runAdoptCommand(extraArgs: string[] = []): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const workerScript = workerServiceScriptPath();
if (!existsSync(workerScript)) {
console.error(pc.red(`Worker script not found at: ${workerScript}`));
console.error('The installation may be corrupted. Try: npx claude-mem install');
process.exit(1);
}
// Pass user's cwd explicitly via --cwd because we override cwd on spawn to
// marketplaceDirectory() (required for the worker's own file resolution).
const userCwd = process.cwd();
const args = [workerScript, 'adopt', '--cwd', userCwd, ...extraArgs];
const child = spawn(bunPath, args, {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start Bun: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}
/**
* Search the worker API at `GET /api/search?query=<query>`.
*/
export async function runSearchCommand(queryParts: string[]): Promise<void> {
ensureInstalledOrExit();
const query = queryParts.join(' ').trim();
if (!query) {
console.error(pc.red('Usage: npx claude-mem search <query>'));
process.exit(1);
}
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
const searchUrl = `http://127.0.0.1:${workerPort}/api/search?query=${encodeURIComponent(query)}`;
let response: Response;
try {
response = await fetch(searchUrl);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const cause = error instanceof Error ? (error as any).cause : undefined;
if (cause?.code === 'ECONNREFUSED' || message.includes('ECONNREFUSED')) {
console.error(pc.red('Worker is not running.'));
console.error(`Start it with: ${pc.bold('npx claude-mem start')}`);
process.exit(1);
}
console.error(pc.red(`Search failed: ${message}`));
process.exit(1);
}
if (!response.ok) {
if (response.status === 404) {
console.error(pc.red('Search endpoint not found. Is the worker running?'));
console.error(`Try: ${pc.bold('npx claude-mem start')}`);
process.exit(1);
}
console.error(pc.red(`Search failed: HTTP ${response.status}`));
process.exit(1);
}
let data: unknown;
try {
data = await response.json();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error(pc.red(`Search failed: invalid JSON response (${message})`));
process.exit(1);
}
if (typeof data === 'object' && data !== null) {
console.log(JSON.stringify(data, null, 2));
} else {
console.log(data);
}
}
/**
* Start the transcript watcher via Bun.
*/
export function runTranscriptWatchCommand(): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const transcriptWatcherPath = join(
marketplaceDirectory(),
'plugin',
'scripts',
'transcript-watcher.cjs',
);
if (!existsSync(transcriptWatcherPath)) {
// Fall back to worker-service with transcript subcommand
spawnBunWorkerCommand('transcript', ['watch']);
return;
}
const child = spawn(bunPath, [transcriptWatcherPath, 'watch'], {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start transcript watcher: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}