feat: add npx CLI entry point with install, runtime, and IDE detection commands

Replaces the old git-clone installer with a direct npm package copy workflow.
Supports 13 IDE auto-detection targets, runtime delegation to Bun worker,
and pure Node.js install path (no Bun required for installation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-26 23:45:00 -05:00
parent e2a230286d
commit 85eb796b18
8 changed files with 1320 additions and 0 deletions
+5
View File
@@ -26,6 +26,9 @@
"url": "https://github.com/thedotmack/claude-mem/issues"
},
"type": "module",
"bin": {
"claude-mem": "./dist/npx-cli/index.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -97,12 +100,14 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
"@clack/prompts": "^0.9.1",
"@modelcontextprotocol/sdk": "^1.25.1",
"ansi-to-html": "^0.7.2",
"dompurify": "^3.3.1",
"express": "^4.18.2",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
"picocolors": "^1.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"yaml": "^2.8.2",
+174
View File
@@ -0,0 +1,174 @@
/**
* IDE Auto-Detection
*
* Detects which AI coding IDEs / tools are installed on the system by
* probing known config directories and checking for binaries in PATH.
*
* Pure Node.js — no Bun APIs used.
*/
import { execSync } from 'child_process';
import { existsSync, readdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { IS_WINDOWS } from '../utils/paths.js';
// ---------------------------------------------------------------------------
// IDE type and metadata
// ---------------------------------------------------------------------------
export interface IDEInfo {
/** Machine-readable identifier. */
id: string;
/** Human-readable label for display in prompts. */
label: string;
/** Whether the IDE was detected on this system. */
detected: boolean;
/** Whether claude-mem has implemented setup for this IDE. */
supported: boolean;
/** Short hint text shown in the multi-select. */
hint?: string;
}
// ---------------------------------------------------------------------------
// PATH helper
// ---------------------------------------------------------------------------
function isCommandInPath(command: string): boolean {
try {
const whichCommand = IS_WINDOWS ? 'where' : 'which';
execSync(`${whichCommand} ${command}`, { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
// ---------------------------------------------------------------------------
// VS Code extension directory scanner
// ---------------------------------------------------------------------------
function hasVscodeExtension(extensionNameFragment: string): boolean {
const extensionsDirectory = join(homedir(), '.vscode', 'extensions');
if (!existsSync(extensionsDirectory)) return false;
try {
const entries = readdirSync(extensionsDirectory);
return entries.some((entry) => entry.toLowerCase().includes(extensionNameFragment.toLowerCase()));
} catch {
return false;
}
}
// ---------------------------------------------------------------------------
// Detection map
// ---------------------------------------------------------------------------
/**
* Detect all known IDEs and return an array of `IDEInfo` objects.
* Each entry indicates whether the IDE was found and whether claude-mem
* currently supports setting it up.
*/
export function detectInstalledIDEs(): IDEInfo[] {
const home = homedir();
return [
{
id: 'claude-code',
label: 'Claude Code',
detected: existsSync(join(home, '.claude')),
supported: true,
hint: 'recommended',
},
{
id: 'gemini-cli',
label: 'Gemini CLI',
detected: existsSync(join(home, '.gemini')),
supported: false,
hint: 'coming soon',
},
{
id: 'opencode',
label: 'OpenCode',
detected:
existsSync(join(home, '.config', 'opencode')) || isCommandInPath('opencode'),
supported: false,
hint: 'coming soon',
},
{
id: 'openclaw',
label: 'OpenClaw',
detected: existsSync(join(home, '.openclaw')),
supported: false,
hint: 'coming soon',
},
{
id: 'windsurf',
label: 'Windsurf',
detected: existsSync(join(home, '.codeium', 'windsurf')),
supported: false,
hint: 'coming soon',
},
{
id: 'codex-cli',
label: 'Codex CLI',
detected: existsSync(join(home, '.codex')),
supported: false,
hint: 'coming soon',
},
{
id: 'cursor',
label: 'Cursor',
detected: existsSync(join(home, '.cursor')),
supported: true,
},
{
id: 'copilot-cli',
label: 'Copilot CLI',
detected: isCommandInPath('copilot'),
supported: false,
hint: 'coming soon',
},
{
id: 'antigravity',
label: 'Antigravity',
detected: existsSync(join(home, '.gemini', 'antigravity')),
supported: false,
hint: 'coming soon',
},
{
id: 'goose',
label: 'Goose',
detected:
existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'),
supported: false,
hint: 'coming soon',
},
{
id: 'crush',
label: 'Crush',
detected: isCommandInPath('crush'),
supported: false,
hint: 'coming soon',
},
{
id: 'roo-code',
label: 'Roo Code',
detected: hasVscodeExtension('roo-code'),
supported: false,
hint: 'coming soon',
},
{
id: 'warp',
label: 'Warp',
detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'),
supported: false,
hint: 'coming soon',
},
];
}
/**
* Return only the IDEs that were detected on this system.
*/
export function getDetectedIDEs(): IDEInfo[] {
return detectInstalledIDEs().filter((ide) => ide.detected);
}
+374
View File
@@ -0,0 +1,374 @@
/**
* Install command for `npx claude-mem install`.
*
* Replaces the git-clone + build workflow. The npm package already ships
* a pre-built `plugin/` directory; this command copies it into the right
* locations and registers it with Claude Code.
*
* Pure Node.js — no Bun APIs used.
*/
import * as p from '@clack/prompts';
import pc from 'picocolors';
import { execSync } from 'child_process';
import { cpSync, existsSync, readFileSync } from 'fs';
import { join } from 'path';
import {
claudeSettingsPath,
ensureDirectoryExists,
installedPluginsPath,
IS_WINDOWS,
knownMarketplacesPath,
marketplaceDirectory,
npmPackagePluginDirectory,
npmPackageRootDirectory,
pluginCacheDirectory,
pluginsDirectory,
readJsonFileSafe,
readPluginVersion,
writeJsonFileAtomic,
} from '../utils/paths.js';
import { detectInstalledIDEs } from './ide-detection.js';
// ---------------------------------------------------------------------------
// Registration helpers (mirror installer/src/steps/install.ts)
// ---------------------------------------------------------------------------
function registerMarketplace(): void {
const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath());
knownMarketplaces['thedotmack'] = {
source: {
source: 'github',
repo: 'thedotmack/claude-mem',
},
installLocation: marketplaceDirectory(),
lastUpdated: new Date().toISOString(),
autoUpdate: true,
};
ensureDirectoryExists(pluginsDirectory());
writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces);
}
function registerPlugin(version: string): void {
const installedPlugins = readJsonFileSafe(installedPluginsPath());
if (!installedPlugins.version) installedPlugins.version = 2;
if (!installedPlugins.plugins) installedPlugins.plugins = {};
const cachePath = pluginCacheDirectory(version);
const now = new Date().toISOString();
installedPlugins.plugins['claude-mem@thedotmack'] = [
{
scope: 'user',
installPath: cachePath,
version,
installedAt: now,
lastUpdated: now,
},
];
writeJsonFileAtomic(installedPluginsPath(), installedPlugins);
}
function enablePluginInClaudeSettings(): void {
const settings = readJsonFileSafe(claudeSettingsPath());
if (!settings.enabledPlugins) settings.enabledPlugins = {};
settings.enabledPlugins['claude-mem@thedotmack'] = true;
writeJsonFileAtomic(claudeSettingsPath(), settings);
}
// ---------------------------------------------------------------------------
// IDE setup dispatcher
// ---------------------------------------------------------------------------
function setupIDEs(selectedIDEs: string[]): void {
for (const ideId of selectedIDEs) {
switch (ideId) {
case 'claude-code':
// Claude Code picks up the plugin via marketplace registration — nothing
// else to do beyond what registerMarketplace / registerPlugin already did.
p.log.success('Claude Code: plugin registered via marketplace.');
break;
case 'cursor':
p.log.info('Cursor: hook configuration available after first launch.');
p.log.info(` Run: npx claude-mem cursor-setup (coming soon)`);
break;
default: {
const allIDEs = detectInstalledIDEs();
const ide = allIDEs.find((i) => i.id === ideId);
if (ide && !ide.supported) {
p.log.warn(`Support for ${ide.label} coming soon.`);
}
break;
}
}
}
}
// ---------------------------------------------------------------------------
// Interactive IDE selection
// ---------------------------------------------------------------------------
async function promptForIDESelection(): Promise<string[]> {
const detectedIDEs = detectInstalledIDEs();
const detected = detectedIDEs.filter((ide) => ide.detected);
if (detected.length === 0) {
p.log.warn('No supported IDEs detected. Installing for Claude Code by default.');
return ['claude-code'];
}
const options = detected.map((ide) => ({
value: ide.id,
label: ide.label,
hint: ide.supported ? ide.hint : 'coming soon',
}));
const result = await p.multiselect({
message: 'Which IDEs do you use?',
options,
initialValues: detected
.filter((ide) => ide.supported)
.map((ide) => ide.id),
required: true,
});
if (p.isCancel(result)) {
p.cancel('Installation cancelled.');
process.exit(0);
}
return result as string[];
}
// ---------------------------------------------------------------------------
// Core copy logic
// ---------------------------------------------------------------------------
function copyPluginToMarketplace(): void {
const marketplaceDir = marketplaceDirectory();
ensureDirectoryExists(marketplaceDir);
// Copy the entire npm package (not just plugin/) so that package.json,
// node_modules, and scripts are all present in the marketplace dir.
const packageRoot = npmPackageRootDirectory();
cpSync(packageRoot, marketplaceDir, {
recursive: true,
force: true,
filter: (source) => {
// Skip .git and other unnecessary directories
if (source.includes('.git') && !source.includes('.claude-plugin')) return false;
if (source.endsWith('.tgz')) return false;
return true;
},
});
}
function copyPluginToCache(version: string): void {
const sourcePluginDirectory = npmPackagePluginDirectory();
const cachePath = pluginCacheDirectory(version);
ensureDirectoryExists(cachePath);
cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true });
}
// ---------------------------------------------------------------------------
// npm install in marketplace dir
// ---------------------------------------------------------------------------
function runNpmInstallInMarketplace(): void {
const marketplaceDir = marketplaceDirectory();
const packageJsonPath = join(marketplaceDir, 'package.json');
if (!existsSync(packageJsonPath)) return;
execSync('npm install --production', {
cwd: marketplaceDir,
stdio: 'pipe',
...(IS_WINDOWS ? { shell: true as const } : {}),
});
}
// ---------------------------------------------------------------------------
// Trigger smart-install for Bun / uv
// ---------------------------------------------------------------------------
function runSmartInstall(): void {
const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js');
if (!existsSync(smartInstallPath)) {
p.log.warn('smart-install.js not found — skipping Bun/uv auto-install.');
return;
}
try {
execSync(`node "${smartInstallPath}"`, {
stdio: 'inherit',
...(IS_WINDOWS ? { shell: true as const } : {}),
});
} catch {
p.log.warn('smart-install encountered an issue. You may need to install Bun/uv manually.');
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export interface InstallOptions {
/** When provided, skip the interactive IDE multi-select and use this IDE. */
ide?: string;
}
export async function runInstallCommand(options: InstallOptions = {}): Promise<void> {
const version = readPluginVersion();
p.intro(pc.bgCyan(pc.black(' claude-mem install ')));
p.log.info(`Version: ${pc.cyan(version)}`);
p.log.info(`Platform: ${process.platform} (${process.arch})`);
// Check for existing installation
const marketplaceDir = marketplaceDirectory();
const alreadyInstalled = existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'));
if (alreadyInstalled) {
// Read existing version
try {
const existingPluginJson = JSON.parse(
readFileSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'), 'utf-8'),
);
p.log.warn(`Existing installation detected (v${existingPluginJson.version ?? 'unknown'}).`);
} catch {
p.log.warn('Existing installation detected.');
}
if (process.stdin.isTTY) {
const shouldContinue = await p.confirm({
message: 'Overwrite existing installation?',
initialValue: true,
});
if (p.isCancel(shouldContinue) || !shouldContinue) {
p.cancel('Installation cancelled.');
process.exit(0);
}
}
}
// IDE selection
let selectedIDEs: string[];
if (options.ide) {
selectedIDEs = [options.ide];
const allIDEs = detectInstalledIDEs();
const match = allIDEs.find((i) => i.id === options.ide);
if (match && !match.supported) {
p.log.error(`Support for ${match.label} coming soon.`);
process.exit(1);
}
if (!match) {
p.log.error(`Unknown IDE: ${options.ide}`);
p.log.info(`Available IDEs: ${allIDEs.map((i) => i.id).join(', ')}`);
process.exit(1);
}
} else if (process.stdin.isTTY) {
selectedIDEs = await promptForIDESelection();
} else {
// Non-interactive: default to claude-code
selectedIDEs = ['claude-code'];
}
// Run tasks
await p.tasks([
{
title: 'Copying plugin files',
task: async (message) => {
message('Copying to marketplace directory...');
copyPluginToMarketplace();
return `Plugin files copied ${pc.green('OK')}`;
},
},
{
title: 'Caching plugin version',
task: async (message) => {
message(`Caching v${version}...`);
copyPluginToCache(version);
return `Plugin cached (v${version}) ${pc.green('OK')}`;
},
},
{
title: 'Registering marketplace',
task: async () => {
registerMarketplace();
return `Marketplace registered ${pc.green('OK')}`;
},
},
{
title: 'Registering plugin',
task: async () => {
registerPlugin(version);
return `Plugin registered ${pc.green('OK')}`;
},
},
{
title: 'Enabling plugin in Claude settings',
task: async () => {
enablePluginInClaudeSettings();
return `Plugin enabled ${pc.green('OK')}`;
},
},
{
title: 'Installing dependencies',
task: async (message) => {
message('Running npm install...');
try {
runNpmInstallInMarketplace();
return `Dependencies installed ${pc.green('OK')}`;
} catch {
return `Dependencies may need manual install ${pc.yellow('!')}`;
}
},
},
{
title: 'Setting up Bun and uv',
task: async (message) => {
message('Running smart-install...');
try {
runSmartInstall();
return `Runtime dependencies ready ${pc.green('OK')}`;
} catch {
return `Runtime setup may need attention ${pc.yellow('!')}`;
}
},
},
]);
// IDE-specific setup
setupIDEs(selectedIDEs);
// Summary
const summaryLines = [
`Version: ${pc.cyan(version)}`,
`Plugin dir: ${pc.cyan(marketplaceDir)}`,
`IDEs: ${pc.cyan(selectedIDEs.join(', '))}`,
];
p.note(summaryLines.join('\n'), 'Installation Complete');
const nextSteps = [
'Open Claude Code and start a conversation -- memory is automatic!',
`View your memories: ${pc.underline('http://localhost:37777')}`,
`Search past work: use ${pc.bold('/mem-search')} in Claude Code`,
`Start worker: ${pc.bold('npx claude-mem start')}`,
];
p.note(nextSteps.join('\n'), 'Next Steps');
p.outro(pc.green('claude-mem installed successfully!'));
}
+184
View File
@@ -0,0 +1,184 @@
/**
* 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');
}
/**
* Search the worker API at `GET /api/search?q=<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?q=${encodeURIComponent(query)}`;
try {
const response = await fetch(searchUrl);
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);
}
const data = await response.json();
if (typeof data === 'object' && data !== null) {
console.log(JSON.stringify(data, null, 2));
} else {
console.log(data);
}
} catch (error: any) {
if (error?.cause?.code === 'ECONNREFUSED' || error?.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: ${error.message}`));
process.exit(1);
}
}
/**
* 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);
});
}
+171
View File
@@ -0,0 +1,171 @@
/**
* Uninstall command for `npx claude-mem uninstall`.
*
* Removes the plugin from the marketplace directory, cache, plugin
* registrations, and Claude settings. Optionally cleans up IDE-specific
* configurations.
*
* Pure Node.js — no Bun APIs used.
*/
import * as p from '@clack/prompts';
import pc from 'picocolors';
import { existsSync, rmSync } from 'fs';
import { join } from 'path';
import {
claudeSettingsPath,
installedPluginsPath,
isPluginInstalled,
knownMarketplacesPath,
marketplaceDirectory,
pluginsDirectory,
readJsonFileSafe,
writeJsonFileAtomic,
} from '../utils/paths.js';
// ---------------------------------------------------------------------------
// Cleanup helpers
// ---------------------------------------------------------------------------
function removeMarketplaceDirectory(): boolean {
const marketplaceDir = marketplaceDirectory();
if (existsSync(marketplaceDir)) {
rmSync(marketplaceDir, { recursive: true, force: true });
return true;
}
return false;
}
function removeCacheDirectory(): boolean {
const cacheBaseDirectory = join(pluginsDirectory(), 'cache', 'thedotmack');
if (existsSync(cacheBaseDirectory)) {
rmSync(cacheBaseDirectory, { recursive: true, force: true });
return true;
}
return false;
}
function removeFromKnownMarketplaces(): void {
const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath());
if (knownMarketplaces['thedotmack']) {
delete knownMarketplaces['thedotmack'];
writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces);
}
}
function removeFromInstalledPlugins(): void {
const installedPlugins = readJsonFileSafe(installedPluginsPath());
if (installedPlugins.plugins?.['claude-mem@thedotmack']) {
delete installedPlugins.plugins['claude-mem@thedotmack'];
writeJsonFileAtomic(installedPluginsPath(), installedPlugins);
}
}
function removeFromClaudeSettings(): void {
const settings = readJsonFileSafe(claudeSettingsPath());
if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) {
delete settings.enabledPlugins['claude-mem@thedotmack'];
writeJsonFileAtomic(claudeSettingsPath(), settings);
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export async function runUninstallCommand(): Promise<void> {
p.intro(pc.bgRed(pc.white(' claude-mem uninstall ')));
if (!isPluginInstalled()) {
p.log.warn('claude-mem does not appear to be installed.');
// Still offer to clean up partial state
if (process.stdin.isTTY) {
const shouldCleanup = await p.confirm({
message: 'Clean up any remaining registration data anyway?',
initialValue: false,
});
if (p.isCancel(shouldCleanup) || !shouldCleanup) {
p.outro('Nothing to do.');
return;
}
} else {
p.outro('Nothing to do.');
return;
}
} else if (process.stdin.isTTY) {
const shouldContinue = await p.confirm({
message: 'Are you sure you want to uninstall claude-mem?',
initialValue: false,
});
if (p.isCancel(shouldContinue) || !shouldContinue) {
p.cancel('Uninstall cancelled.');
return;
}
}
// Stop the worker first (best-effort)
try {
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, {
method: 'POST',
signal: AbortSignal.timeout(5000),
});
p.log.info('Worker service stopped.');
} catch {
// Worker may not be running — that is fine
}
await p.tasks([
{
title: 'Removing marketplace directory',
task: async () => {
const removed = removeMarketplaceDirectory();
return removed
? `Marketplace directory removed ${pc.green('OK')}`
: `Marketplace directory not found ${pc.dim('skipped')}`;
},
},
{
title: 'Removing cache directory',
task: async () => {
const removed = removeCacheDirectory();
return removed
? `Cache directory removed ${pc.green('OK')}`
: `Cache directory not found ${pc.dim('skipped')}`;
},
},
{
title: 'Removing marketplace registration',
task: async () => {
removeFromKnownMarketplaces();
return `Marketplace registration removed ${pc.green('OK')}`;
},
},
{
title: 'Removing plugin registration',
task: async () => {
removeFromInstalledPlugins();
return `Plugin registration removed ${pc.green('OK')}`;
},
},
{
title: 'Removing from Claude settings',
task: async () => {
removeFromClaudeSettings();
return `Claude settings updated ${pc.green('OK')}`;
},
},
]);
p.note(
[
`Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`,
'To remove it manually: rm -rf ~/.claude-mem',
].join('\n'),
'Note',
);
p.outro(pc.green('claude-mem has been uninstalled.'));
}
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* NPX CLI entry point for claude-mem.
*
* Usage:
* npx claude-mem → interactive install
* npx claude-mem install → interactive install
* npx claude-mem install --ide <id> → direct IDE setup
* npx claude-mem update → update to latest version
* npx claude-mem uninstall → remove plugin and IDE configs
* npx claude-mem version → print version
* npx claude-mem start → start worker service
* npx claude-mem stop → stop worker service
* npx claude-mem restart → restart worker service
* npx claude-mem status → show worker status
* npx claude-mem search <query> → search observations
* npx claude-mem transcript watch → start transcript watcher
*
* This file is pure Node.js — Bun is NOT required for install commands.
* Runtime commands (`start`, `stop`, etc.) delegate to Bun via the installed plugin.
*/
import pc from 'picocolors';
import { readPluginVersion } from './utils/paths.js';
// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const command = args[0]?.toLowerCase() ?? '';
// ---------------------------------------------------------------------------
// Help text
// ---------------------------------------------------------------------------
function printHelp(): void {
const version = readPluginVersion();
console.log(`
${pc.bold('claude-mem')} v${version} — persistent memory for AI coding assistants
${pc.bold('Install Commands')} (no Bun required):
${pc.cyan('npx claude-mem')} Interactive install
${pc.cyan('npx claude-mem install')} Interactive install
${pc.cyan('npx claude-mem install --ide <id>')} Install for specific IDE
${pc.cyan('npx claude-mem update')} Update to latest version
${pc.cyan('npx claude-mem uninstall')} Remove plugin and configs
${pc.cyan('npx claude-mem version')} Print version
${pc.bold('Runtime Commands')} (requires Bun, delegates to installed plugin):
${pc.cyan('npx claude-mem start')} Start worker service
${pc.cyan('npx claude-mem stop')} Stop worker service
${pc.cyan('npx claude-mem restart')} Restart worker service
${pc.cyan('npx claude-mem status')} Show worker status
${pc.cyan('npx claude-mem search <query>')} Search observations
${pc.cyan('npx claude-mem transcript watch')} Start transcript watcher
${pc.bold('IDE Identifiers')}:
claude-code, cursor, gemini-cli, opencode, openclaw,
windsurf, codex-cli, copilot-cli, antigravity, goose,
crush, roo-code, warp
`);
}
// ---------------------------------------------------------------------------
// Command routing
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
switch (command) {
// -- No command: default to install ------------------------------------
case '': {
const { runInstallCommand } = await import('./commands/install.js');
await runInstallCommand();
break;
}
// -- Install -----------------------------------------------------------
case 'install': {
const ideIndex = args.indexOf('--ide');
const ideValue = ideIndex !== -1 ? args[ideIndex + 1] : undefined;
const { runInstallCommand } = await import('./commands/install.js');
await runInstallCommand({ ide: ideValue });
break;
}
// -- Update (alias for install — overwrite with latest) ----------------
case 'update':
case 'upgrade': {
const { runInstallCommand } = await import('./commands/install.js');
await runInstallCommand();
break;
}
// -- Uninstall ---------------------------------------------------------
case 'uninstall':
case 'remove': {
const { runUninstallCommand } = await import('./commands/uninstall.js');
await runUninstallCommand();
break;
}
// -- Version -----------------------------------------------------------
case 'version':
case '--version':
case '-v': {
console.log(readPluginVersion());
break;
}
// -- Help --------------------------------------------------------------
case 'help':
case '--help':
case '-h': {
printHelp();
break;
}
// -- Runtime: start / stop / restart / status --------------------------
case 'start': {
const { runStartCommand } = await import('./commands/runtime.js');
runStartCommand();
break;
}
case 'stop': {
const { runStopCommand } = await import('./commands/runtime.js');
runStopCommand();
break;
}
case 'restart': {
const { runRestartCommand } = await import('./commands/runtime.js');
runRestartCommand();
break;
}
case 'status': {
const { runStatusCommand } = await import('./commands/runtime.js');
runStatusCommand();
break;
}
// -- Search ------------------------------------------------------------
case 'search': {
const { runSearchCommand } = await import('./commands/runtime.js');
await runSearchCommand(args.slice(1));
break;
}
// -- Transcript --------------------------------------------------------
case 'transcript': {
const subCommand = args[1]?.toLowerCase();
if (subCommand === 'watch') {
const { runTranscriptWatchCommand } = await import('./commands/runtime.js');
runTranscriptWatchCommand();
} else {
console.error(pc.red(`Unknown transcript subcommand: ${subCommand ?? '(none)'}`));
console.error(`Usage: npx claude-mem transcript watch`);
process.exit(1);
}
break;
}
// -- Unknown -----------------------------------------------------------
default: {
console.error(pc.red(`Unknown command: ${command}`));
console.error(`Run ${pc.bold('npx claude-mem --help')} for usage information.`);
process.exit(1);
}
}
}
main().catch((error) => {
console.error(pc.red('Fatal error:'), error.message || error);
process.exit(1);
});
+85
View File
@@ -0,0 +1,85 @@
/**
* Bun binary resolution utility.
*
* Extracted from `plugin/scripts/bun-runner.js` so that the NPX CLI
* can locate Bun without duplicating the search logic.
*
* Pure Node.js — no Bun APIs used.
*/
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { IS_WINDOWS } from './paths.js';
/**
* Well-known locations where Bun might be installed, beyond PATH.
* Order matches the search priority in bun-runner.js and smart-install.js.
*/
function bunCandidatePaths(): string[] {
if (IS_WINDOWS) {
return [
join(homedir(), '.bun', 'bin', 'bun.exe'),
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
];
}
return [
join(homedir(), '.bun', 'bin', 'bun'),
'/usr/local/bin/bun',
'/opt/homebrew/bin/bun',
'/home/linuxbrew/.linuxbrew/bin/bun',
];
}
/**
* Attempt to locate the Bun executable.
*
* 1. Check PATH via `which` / `where`.
* 2. Probe well-known installation directories.
*
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
* or `null` if Bun cannot be found.
*/
export function resolveBunBinaryPath(): string | null {
// Try PATH first
const whichCommand = IS_WINDOWS ? 'where' : 'which';
const pathCheck = spawnSync(whichCommand, ['bun'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
return 'bun'; // Available in PATH — use short name
}
// Probe known install locations
for (const candidatePath of bunCandidatePaths()) {
if (existsSync(candidatePath)) {
return candidatePath;
}
}
return null;
}
/**
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
* if Bun is not available.
*/
export function getBunVersionString(): string | null {
const bunPath = resolveBunBinaryPath();
if (!bunPath) return null;
try {
const result = spawnSync(bunPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
return result.status === 0 ? result.stdout.trim() : null;
} catch {
return null;
}
}
+152
View File
@@ -0,0 +1,152 @@
/**
* Shared path utilities for the NPX CLI.
*
* All platform-specific path logic is centralized here so that every command
* resolves directories in exactly the same way, regardless of OS.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
// ---------------------------------------------------------------------------
// Platform detection
// ---------------------------------------------------------------------------
export const IS_WINDOWS = process.platform === 'win32';
// ---------------------------------------------------------------------------
// Core paths
// ---------------------------------------------------------------------------
/** Root of the Claude Code config directory. */
export function claudeConfigDirectory(): string {
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
}
/** Marketplace install directory for thedotmack. */
export function marketplaceDirectory(): string {
return join(claudeConfigDirectory(), 'plugins', 'marketplaces', 'thedotmack');
}
/** Top-level plugins directory. */
export function pluginsDirectory(): string {
return join(claudeConfigDirectory(), 'plugins');
}
/** Path to `known_marketplaces.json`. */
export function knownMarketplacesPath(): string {
return join(pluginsDirectory(), 'known_marketplaces.json');
}
/** Path to `installed_plugins.json`. */
export function installedPluginsPath(): string {
return join(pluginsDirectory(), 'installed_plugins.json');
}
/** Path to `~/.claude/settings.json`. */
export function claudeSettingsPath(): string {
return join(claudeConfigDirectory(), 'settings.json');
}
/** Plugin cache directory for a specific version. */
export function pluginCacheDirectory(version: string): string {
return join(pluginsDirectory(), 'cache', 'thedotmack', 'claude-mem', version);
}
/** claude-mem data directory (default `~/.claude-mem`). */
export function claudeMemDataDirectory(): string {
return join(homedir(), '.claude-mem');
}
// ---------------------------------------------------------------------------
// NPM package root (where the NPX package lives on disk)
// ---------------------------------------------------------------------------
/**
* Resolve the root of the installed npm package.
*
* The CLI entry point lives at `<pkg>/dist/cli/index.js`. Walking up
* from `import.meta.url` (or `__dirname` equivalent) we reach the
* package root where `plugin/` can be found.
*/
export function npmPackageRootDirectory(): string {
const currentFilePath = fileURLToPath(import.meta.url);
// <pkg>/dist/npx-cli/utils/paths.js -> up 3 levels -> <pkg>
return join(dirname(currentFilePath), '..', '..', '..');
}
/**
* Path to the `plugin/` directory bundled inside the npm package.
*/
export function npmPackagePluginDirectory(): string {
return join(npmPackageRootDirectory(), 'plugin');
}
// ---------------------------------------------------------------------------
// Version helpers
// ---------------------------------------------------------------------------
/**
* Read the current plugin version from the npm package's
* `plugin/.claude-plugin/plugin.json` (preferred) or from `package.json`.
*/
export function readPluginVersion(): string {
// Try plugin.json first (authoritative for plugin version)
const pluginJsonPath = join(npmPackagePluginDirectory(), '.claude-plugin', 'plugin.json');
if (existsSync(pluginJsonPath)) {
try {
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
if (pluginJson.version) return pluginJson.version;
} catch {
// Fall through to package.json
}
}
// Fall back to package.json at package root
const packageJsonPath = join(npmPackageRootDirectory(), 'package.json');
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
if (packageJson.version) return packageJson.version;
} catch {
// Unable to read
}
}
return '0.0.0';
}
// ---------------------------------------------------------------------------
// Installation detection
// ---------------------------------------------------------------------------
/** Returns true if the plugin appears to be installed in the marketplace dir. */
export function isPluginInstalled(): boolean {
const marketplaceDir = marketplaceDirectory();
return existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'));
}
// ---------------------------------------------------------------------------
// JSON file helpers
// ---------------------------------------------------------------------------
export function ensureDirectoryExists(directoryPath: string): void {
if (!existsSync(directoryPath)) {
mkdirSync(directoryPath, { recursive: true });
}
}
export function readJsonFileSafe(filepath: string): any {
if (!existsSync(filepath)) return {};
try {
return JSON.parse(readFileSync(filepath, 'utf-8'));
} catch {
return {};
}
}
export function writeJsonFileAtomic(filepath: string, data: any): void {
ensureDirectoryExists(dirname(filepath));
writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}