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:
@@ -26,6 +26,9 @@
|
|||||||
"url": "https://github.com/thedotmack/claude-mem/issues"
|
"url": "https://github.com/thedotmack/claude-mem/issues"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"claude-mem": "./dist/npx-cli/index.js"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -97,12 +100,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
||||||
|
"@clack/prompts": "^0.9.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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!'));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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.'));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user