d2eb89d27f
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>
233 lines
7.0 KiB
TypeScript
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);
|
|
});
|
|
}
|