feat: add interactive CLI installer with @clack/prompts (#1093)
* feat: Switch to persistent Chroma HTTP server Replace MCP subprocess approach with persistent Chroma HTTP server for improved performance and reliability. This re-enables Chroma on Windows by eliminating the subprocess spawning that caused console popups. Changes: - NEW: ChromaServerManager.ts - Manages local Chroma server lifecycle via `npx chroma run` - REFACTOR: ChromaSync.ts - Uses chromadb npm package's ChromaClient instead of MCP subprocess (removes Windows disabling) - UPDATE: worker-service.ts - Starts Chroma server on initialization - UPDATE: GracefulShutdown.ts - Stops Chroma server on shutdown - UPDATE: SettingsDefaultsManager.ts - New Chroma configuration options - UPDATE: build-hooks.js - Mark optional chromadb deps as external Benefits: - Eliminates subprocess spawn latency on first query - Single server process instead of per-operation subprocesses - No Python/uvx dependency for local mode - Re-enables Chroma vector search on Windows - Future-ready for cloud-hosted Chroma (claude-mem pro) - Cross-platform: Linux, macOS, Windows Configuration: CLAUDE_MEM_CHROMA_MODE=local|remote CLAUDE_MEM_CHROMA_HOST=127.0.0.1 CLAUDE_MEM_CHROMA_PORT=8000 CLAUDE_MEM_CHROMA_SSL=false Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Use chromadb v3.2.2 with v2 API heartbeat endpoint - Updated chromadb from ^1.9.2 to ^3.2.2 (includes CLI binary) - Changed heartbeat endpoint from /api/v1 to /api/v2 The 1.9.x version did not include the CLI, causing `npx chroma run` to fail. Version 3.2.2 includes the chroma CLI and uses the v2 API. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Add DefaultEmbeddingFunction for local vector embeddings - Added @chroma-core/default-embed dependency for local embeddings - Updated ChromaSync to use DefaultEmbeddingFunction with collections - Added isServerReachable() async method for reliable server detection - Fixed start() to detect and reuse existing Chroma servers - Updated build script to externalize native ONNX binaries - Added runtime dependency to plugin/package.json The embedding function uses all-MiniLM-L6-v2 model locally via ONNX, eliminating need for external embedding API calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update src/services/sync/ChromaServerManager.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: Remove duplicate else block from merge Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Add multi-tenancy support for claude-mem pro Wire tenant, database, and API key settings into ChromaSync for remote/pro mode. In remote mode: - Passes tenant and database to ChromaClient for data isolation - Adds Authorization header when API key is configured - Logs tenant isolation connection details Local mode unchanged - uses default_tenant without explicit params. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add plugin.json to root .claude-plugin directory Claude Code's plugin discovery looks for plugin.json at the marketplace root level in .claude-plugin/, not nested inside plugin/.claude-plugin/. Without this file at the root level, skills and commands are not discovered. This matches the structure of working plugins like claude-research-team. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: resolve SDK spawn failures and sharp native binary crashes - Strip CLAUDECODE env var from SDK subprocesses to prevent "cannot be launched inside another Claude Code session" error (Claude Code 2.1.42+) - Lazy-load @chroma-core/default-embed to avoid eagerly pulling in sharp native binaries at bundle startup (fixes ERR_DLOPEN_FAILED) - Add stderr capture to SDK spawn for diagnosing future process failures - Exclude lockfiles from marketplace rsync and delete stale lockfiles before npm install to prevent native dep version mismatches Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: scaffold installer package with @clack/prompts and esbuild Sets up the claude-mem-installer project structure with build tooling, placeholder step and utility modules, and verified esbuild bundling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement entry point, welcome screen, and dependency checks Adds TTY guard, styled welcome banner with install mode selection, OS detection utilities, and automated dependency checking/installation for Node.js, git, Bun, and uv. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement IDE selection and AI provider configuration Adds multiselect IDE picker (Claude Code, Cursor) and provider configuration with Claude CLI/API, Gemini, and OpenRouter support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement settings configuration wizard and settings file writer Adds interactive settings wizard with default/custom modes, Chroma configuration, and a settings writer that merges with existing settings for upgrade support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement installation execution and worker startup Adds git clone, build, plugin registration (marketplace, cache, settings), and worker startup with health check polling. Fixes TypeScript errors in settings.ts validate callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add completion screen and curl|bash bootstrap script Completion screen shows configuration summary and next steps. Bootstrap shell script enables curl -fsSL install.cmem.ai | bash with TTY reconnection for interactive prompts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: wire up full installer flow in index.ts Connects all steps: welcome → dependency checks → IDE selection → provider config → settings → installation → worker startup → completion. Configure-only mode skips clone/build/worker steps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add animated installer implementation plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: bigphoot <bigphoot@local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alexander Knigge <166455923+bigph00t@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: bigphoot <bigphoot@gmail.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { commandExists, runCommand, expandHome, detectOS } from './system.js';
|
||||
|
||||
export interface BinaryInfo {
|
||||
found: boolean;
|
||||
path: string | null;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
export function findBinary(name: string, extraPaths: string[] = []): BinaryInfo {
|
||||
// Check PATH first
|
||||
if (commandExists(name)) {
|
||||
const result = runCommand('which', [name]);
|
||||
const versionResult = runCommand(name, ['--version']);
|
||||
return {
|
||||
found: true,
|
||||
path: result.stdout,
|
||||
version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr),
|
||||
};
|
||||
}
|
||||
|
||||
// Check extra known locations
|
||||
for (const extraPath of extraPaths) {
|
||||
const fullPath = expandHome(extraPath);
|
||||
if (existsSync(fullPath)) {
|
||||
const versionResult = runCommand(fullPath, ['--version']);
|
||||
return {
|
||||
found: true,
|
||||
path: fullPath,
|
||||
version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false, path: null, version: null };
|
||||
}
|
||||
|
||||
function parseVersion(output: string): string | null {
|
||||
if (!output) return null;
|
||||
const match = output.match(/(\d+\.\d+(\.\d+)?)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function compareVersions(current: string, minimum: string): boolean {
|
||||
const currentParts = current.split('.').map(Number);
|
||||
const minimumParts = minimum.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) {
|
||||
const a = currentParts[i] || 0;
|
||||
const b = minimumParts[i] || 0;
|
||||
if (a > b) return true;
|
||||
if (a < b) return false;
|
||||
}
|
||||
return true; // equal
|
||||
}
|
||||
|
||||
export function installBun(): void {
|
||||
const os = detectOS();
|
||||
if (os === 'windows') {
|
||||
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync('curl -fsSL https://bun.sh/install | bash', { stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
export function installUv(): void {
|
||||
const os = detectOS();
|
||||
if (os === 'windows') {
|
||||
execSync('powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync('curl -fsSL https://astral.sh/uv/install.sh | sh', { stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import type { ProviderConfig } from '../steps/provider.js';
|
||||
import type { SettingsConfig } from '../steps/settings.js';
|
||||
|
||||
export function expandDataDir(dataDir: string): string {
|
||||
if (dataDir.startsWith('~')) {
|
||||
return join(homedir(), dataDir.slice(1));
|
||||
}
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
export function buildSettingsObject(
|
||||
providerConfig: ProviderConfig,
|
||||
settingsConfig: SettingsConfig,
|
||||
): Record<string, string> {
|
||||
const settings: Record<string, string> = {
|
||||
CLAUDE_MEM_WORKER_PORT: settingsConfig.workerPort,
|
||||
CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
|
||||
CLAUDE_MEM_DATA_DIR: expandDataDir(settingsConfig.dataDir),
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: settingsConfig.contextObservations,
|
||||
CLAUDE_MEM_LOG_LEVEL: settingsConfig.logLevel,
|
||||
CLAUDE_MEM_PYTHON_VERSION: settingsConfig.pythonVersion,
|
||||
CLAUDE_MEM_PROVIDER: providerConfig.provider,
|
||||
};
|
||||
|
||||
// Provider-specific settings
|
||||
if (providerConfig.provider === 'claude') {
|
||||
settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = providerConfig.claudeAuthMethod ?? 'cli';
|
||||
}
|
||||
|
||||
if (providerConfig.provider === 'gemini') {
|
||||
if (providerConfig.apiKey) settings.CLAUDE_MEM_GEMINI_API_KEY = providerConfig.apiKey;
|
||||
if (providerConfig.model) settings.CLAUDE_MEM_GEMINI_MODEL = providerConfig.model;
|
||||
settings.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED = providerConfig.rateLimitingEnabled !== false ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (providerConfig.provider === 'openrouter') {
|
||||
if (providerConfig.apiKey) settings.CLAUDE_MEM_OPENROUTER_API_KEY = providerConfig.apiKey;
|
||||
if (providerConfig.model) settings.CLAUDE_MEM_OPENROUTER_MODEL = providerConfig.model;
|
||||
}
|
||||
|
||||
// Chroma settings
|
||||
if (settingsConfig.chromaEnabled) {
|
||||
settings.CLAUDE_MEM_CHROMA_MODE = settingsConfig.chromaMode ?? 'local';
|
||||
if (settingsConfig.chromaMode === 'remote') {
|
||||
if (settingsConfig.chromaHost) settings.CLAUDE_MEM_CHROMA_HOST = settingsConfig.chromaHost;
|
||||
if (settingsConfig.chromaPort) settings.CLAUDE_MEM_CHROMA_PORT = settingsConfig.chromaPort;
|
||||
if (settingsConfig.chromaSsl !== undefined) settings.CLAUDE_MEM_CHROMA_SSL = String(settingsConfig.chromaSsl);
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function writeSettings(
|
||||
providerConfig: ProviderConfig,
|
||||
settingsConfig: SettingsConfig,
|
||||
): void {
|
||||
const dataDir = expandDataDir(settingsConfig.dataDir);
|
||||
const settingsPath = join(dataDir, 'settings.json');
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Merge with existing settings if upgrading
|
||||
let existingSettings: Record<string, string> = {};
|
||||
if (existsSync(settingsPath)) {
|
||||
const raw = readFileSync(settingsPath, 'utf-8');
|
||||
existingSettings = JSON.parse(raw);
|
||||
}
|
||||
|
||||
const newSettings = buildSettingsObject(providerConfig, settingsConfig);
|
||||
|
||||
// Merge: new settings override existing ones
|
||||
const merged = { ...existingSettings, ...newSettings };
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
export type OSType = 'macos' | 'linux' | 'windows';
|
||||
|
||||
export function detectOS(): OSType {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return 'macos';
|
||||
case 'win32': return 'windows';
|
||||
default: return 'linux';
|
||||
}
|
||||
}
|
||||
|
||||
export function commandExists(command: string): boolean {
|
||||
try {
|
||||
execSync(`which ${command}`, { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
export function runCommand(command: string, args: string[] = []): CommandResult {
|
||||
try {
|
||||
const fullCommand = [command, ...args].join(' ');
|
||||
const stdout = execSync(fullCommand, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout?.toString().trim() ?? '',
|
||||
stderr: error.stderr?.toString().trim() ?? '',
|
||||
exitCode: error.status ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function expandHome(filepath: string): string {
|
||||
if (filepath.startsWith('~')) {
|
||||
return join(homedir(), filepath.slice(1));
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
Reference in New Issue
Block a user