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:
Alex Newman
2026-02-16 00:44:21 -05:00
committed by GitHub
parent 8d6581ea13
commit e975555896
18 changed files with 1592 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
import * as p from '@clack/prompts';
import pc from 'picocolors';
export interface SettingsConfig {
workerPort: string;
dataDir: string;
contextObservations: string;
logLevel: string;
pythonVersion: string;
chromaEnabled: boolean;
chromaMode?: 'local' | 'remote';
chromaHost?: string;
chromaPort?: string;
chromaSsl?: boolean;
}
export async function runSettingsConfiguration(): Promise<SettingsConfig> {
const useDefaults = await p.confirm({
message: 'Use default settings? (recommended for most users)',
initialValue: true,
});
if (p.isCancel(useDefaults)) {
p.cancel('Installation cancelled.');
process.exit(0);
}
if (useDefaults) {
return {
workerPort: '37777',
dataDir: '~/.claude-mem',
contextObservations: '50',
logLevel: 'INFO',
pythonVersion: '3.13',
chromaEnabled: true,
chromaMode: 'local',
};
}
// Custom settings
const workerPort = await p.text({
message: 'Worker service port:',
defaultValue: '37777',
placeholder: '37777',
validate: (value = '') => {
const port = parseInt(value, 10);
if (isNaN(port) || port < 1024 || port > 65535) {
return 'Port must be between 1024 and 65535';
}
},
});
if (p.isCancel(workerPort)) { p.cancel('Installation cancelled.'); process.exit(0); }
const dataDir = await p.text({
message: 'Data directory:',
defaultValue: '~/.claude-mem',
placeholder: '~/.claude-mem',
});
if (p.isCancel(dataDir)) { p.cancel('Installation cancelled.'); process.exit(0); }
const contextObservations = await p.text({
message: 'Number of context observations per session:',
defaultValue: '50',
placeholder: '50',
validate: (value = '') => {
const num = parseInt(value, 10);
if (isNaN(num) || num < 1 || num > 200) {
return 'Must be between 1 and 200';
}
},
});
if (p.isCancel(contextObservations)) { p.cancel('Installation cancelled.'); process.exit(0); }
const logLevel = await p.select({
message: 'Log level:',
options: [
{ value: 'DEBUG', label: 'DEBUG', hint: 'verbose' },
{ value: 'INFO', label: 'INFO', hint: 'default' },
{ value: 'WARN', label: 'WARN' },
{ value: 'ERROR', label: 'ERROR', hint: 'errors only' },
],
initialValue: 'INFO',
});
if (p.isCancel(logLevel)) { p.cancel('Installation cancelled.'); process.exit(0); }
const pythonVersion = await p.text({
message: 'Python version (for Chroma):',
defaultValue: '3.13',
placeholder: '3.13',
});
if (p.isCancel(pythonVersion)) { p.cancel('Installation cancelled.'); process.exit(0); }
const chromaEnabled = await p.confirm({
message: 'Enable Chroma vector search?',
initialValue: true,
});
if (p.isCancel(chromaEnabled)) { p.cancel('Installation cancelled.'); process.exit(0); }
let chromaMode: 'local' | 'remote' | undefined;
let chromaHost: string | undefined;
let chromaPort: string | undefined;
let chromaSsl: boolean | undefined;
if (chromaEnabled) {
const mode = await p.select({
message: 'Chroma mode:',
options: [
{ value: 'local' as const, label: 'Local', hint: 'starts local Chroma server' },
{ value: 'remote' as const, label: 'Remote', hint: 'connect to existing server' },
],
});
if (p.isCancel(mode)) { p.cancel('Installation cancelled.'); process.exit(0); }
chromaMode = mode;
if (mode === 'remote') {
const host = await p.text({
message: 'Chroma host:',
defaultValue: '127.0.0.1',
placeholder: '127.0.0.1',
});
if (p.isCancel(host)) { p.cancel('Installation cancelled.'); process.exit(0); }
chromaHost = host;
const port = await p.text({
message: 'Chroma port:',
defaultValue: '8000',
placeholder: '8000',
validate: (value = '') => {
const portNum = parseInt(value, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return 'Port must be between 1 and 65535';
},
});
if (p.isCancel(port)) { p.cancel('Installation cancelled.'); process.exit(0); }
chromaPort = port;
const ssl = await p.confirm({
message: 'Use SSL for Chroma connection?',
initialValue: false,
});
if (p.isCancel(ssl)) { p.cancel('Installation cancelled.'); process.exit(0); }
chromaSsl = ssl;
}
}
const config: SettingsConfig = {
workerPort,
dataDir,
contextObservations,
logLevel,
pythonVersion,
chromaEnabled,
chromaMode,
chromaHost,
chromaPort,
chromaSsl,
};
// Show summary
const summaryLines = [
`Worker port: ${pc.cyan(workerPort)}`,
`Data directory: ${pc.cyan(dataDir)}`,
`Context observations: ${pc.cyan(contextObservations)}`,
`Log level: ${pc.cyan(logLevel)}`,
`Python version: ${pc.cyan(pythonVersion)}`,
`Chroma: ${chromaEnabled ? pc.green('enabled') : pc.dim('disabled')}`,
];
if (chromaEnabled && chromaMode) {
summaryLines.push(`Chroma mode: ${pc.cyan(chromaMode)}`);
}
p.note(summaryLines.join('\n'), 'Settings Summary');
return config;
}