plans/2026-04-29-installer-streamline.md
Goal: Move all heavy install work (Bun/uv install, bun install in plugin cache) into the npx claude-mem install flow with a visible spinner. Make hooks runtime-only — never installers.
Net effect:
smart-install.js runs in normal Claude Code lifecycle: 3 → 0 (or 1 via npx claude-mem repair after claude plugin update)npxnpx claude-mem repair becomes the canonical recovery entry pointOut of scope: bun-runner.js deletion (independent rework with Windows/stdin verification needs — ship later).
These facts came from a discovery agent + direct file reads. Each implementation phase below cites them by line number; do not re-derive.
| Item | Location | What to copy |
|---|---|---|
| NPX command dispatcher | src/npx-cli/index.ts:39–141 | Manual switch (command) on process.argv.slice(2). Each case dynamic-imports its handler. |
install case (template for repair) | src/npx-cli/index.ts:46–52 | const { runInstallCommand } = await import('./commands/install.js'); await runInstallCommand({ ide: ideValue }); |
| Plugin cache dir helper | src/npx-cli/utils/paths.ts:32–34 | pluginCacheDirectory(version) → ~/.claude/plugins/cache/thedotmack/claude-mem/{version}/ |
.install-version marker readers | src/services/context/ContextBuilder.ts:36,45 and src/services/worker/BranchManager.ts:173,228 | These read/delete the marker. Marker schema ({ version, bun, uv, installedAt }) MUST be preserved. |
clack task pattern | src/npx-cli/commands/install.ts:604–664 | runTasks([{ title, task: async (message) => { … return 'Done OK' } }]) |
version-check.js helper in plugin/scripts/. Phase 4 must create it.package.json#files already globs plugin/scripts/*.js (line 50), so deleting plugin/scripts/smart-install.js requires no package.json change.scripts/smart-install.js and plugin/scripts/smart-install.js are both source files kept in sync manually — there is no build step that copies one to the other. Both must be deleted in Phase 5.runSmartInstall() (install.ts:325–345) shells node smart-install.js. After Phase 1 you can call the new module directly — do NOT shell out.claude plugin install exec at install.ts:113 has only one caller in the entire repo. Safe to remove.| File | Lines | Disposition |
|---|---|---|
src/npx-cli/commands/install.ts | 761 | Edited heavily (Phase 2) |
src/npx-cli/index.ts | 147 | One case added (Phase 3) |
plugin/hooks/hooks.json | 93 | Setup hook command rewritten, SessionStart smart-install entry deleted (Phase 4) |
scripts/smart-install.js | 264 | DELETED (Phase 5) |
plugin/scripts/smart-install.js | ≈264 | DELETED (Phase 5) |
tests/smart-install.test.ts | 310 | DELETED (Phase 5) |
tests/plugin-scripts-line-endings.test.ts | 33 | One array entry removed (Phase 5) |
plugin/scripts/version-check.js | NEW | CREATED (Phase 4) |
src/npx-cli/install/setup-runtime.ts | NEW | CREATED (Phase 1) |
Docs (docs/public/*.mdx, docs/architecture-overview.md) | misc | Light edit (Phase 6) |
src/npx-cli/install/setup-runtime.tsWhat to implement: Port the smart-install.js logic to a TypeScript module that takes a target directory parameter (so it can install into the plugin cache dir, not just the marketplace dir). Three exported functions plus internal helpers.
File to create: src/npx-cli/install/setup-runtime.ts
API surface (these names are used by Phase 2 and Phase 3 — do not rename):
export async function ensureBun(): Promise<{ bunPath: string; version: string }>;
export async function ensureUv(): Promise<{ uvPath: string; version: string }>;
export async function installPluginDependencies(targetDir: string, bunPath: string): Promise<void>;
export function readInstallMarker(targetDir: string): { version: string; bun?: string; uv?: string; installedAt?: string } | null;
export function writeInstallMarker(targetDir: string, version: string, bunVersion: string, uvVersion: string): void;
export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean;
Reference implementation to port from: scripts/smart-install.js:1–264. Map old → new:
| smart-install.js | setup-runtime.ts |
|---|---|
getBunPath() / isBunInstalled() / installBun() (lines 42–152) | private helpers consumed by ensureBun() |
getUvPath() / isUvInstalled() / installUv() (lines 77–194) | private helpers consumed by ensureUv() |
needsInstall() (lines 196–205) | isInstallCurrent() + readInstallMarker() |
installDeps() (lines 207–226) | installPluginDependencies(targetDir, bunPath) — accepts target dir as parameter |
verifyCriticalModules() (lines 228–246) | private helper called inside installPluginDependencies |
MARKER constant (line 32) | derive inside each function: join(targetDir, '.install-version') |
Top-level try { … } (lines 248–264) | DELETE — caller orchestrates |
Key behavioral differences from smart-install.js:
targetDir as a parameter (was a top-level ROOT constant).ensureBun() / ensureUv() return their version strings rather than logging — caller decides what to display.Error.message. The clack runTasks wrapper in Phase 2 catches and renders.console.error calls in install/uninstall paths become structured: throw a single Error with the manual install instructions in the message body.{ version, bun, uv, installedAt }) so existing readers in ContextBuilder.ts:36 and BranchManager.ts:173,228 continue to work.Verification checklist:
bun build src/npx-cli/install/setup-runtime.ts --target=node succeeds (or whatever the project's TS check command is — confirm via package.json#scripts)grep -rn "ROOT" src/npx-cli/install/setup-runtime.ts returns nothing — no top-level constantsAnti-pattern guards:
bunInstall.ts or uvInstall.ts split — keep all three in one file. They share helper code (paths, version probing).plugin/scripts/smart-install.js — it gets deleted in Phase 5.{ version } field.src/npx-cli/commands/install.tsWhat to implement: Drop needsManualInstall gating (always run copy/register/enable for every IDE), add a new unconditional "Setting up runtime" task before setupIDEs, neuter the claude-code execSync shell-out, delete runSmartInstall(), and add a runRepairCommand() export.
File to edit: src/npx-cli/commands/install.ts
Insert after line 10 (after the ensureWorkerStarted import):
import {
ensureBun,
ensureUv,
installPluginDependencies,
writeInstallMarker,
isInstallCurrent,
} from '../install/setup-runtime.js';
runSmartInstall() functionDelete lines 325–345 (the entire function runSmartInstall(): boolean { … } block).
needsManualInstall gating, ungate the runTasks blockLine 589 currently reads:
const needsManualInstall = selectedIDEs.some((id) => id !== 'claude-code');
Delete line 589. Update line 593's if (needsManualInstall) { to just { (or unwrap the block — preferred). The runTasks block at lines 604–664 now runs unconditionally.
Within that runTasks block: delete the "Setting up Bun and uv" task entry (lines 656–663). Replace its slot with the new "Setting up runtime" task (Edit 2D).
Replace the deleted "Setting up Bun and uv" task (lines 656–663) with:
{
title: 'Setting up runtime (first install can take ~30s)',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
const cacheDir = pluginCacheDirectory(version);
if (!isInstallCurrent(cacheDir, version)) {
message('Installing plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
}
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
Place this AFTER the "Installing dependencies" (npm install) task — same ordering position the deleted task occupied.
setupIDEsLines 110–123 currently:
case 'claude-code': {
try {
execSync(
'claude plugin marketplace add thedotmack/claude-mem && claude plugin install claude-mem',
{ stdio: 'inherit' },
);
log.success('Claude Code: plugin installed via CLI.');
} catch (error: unknown) {
console.error('[install] Claude Code plugin install error:', …);
log.error('Claude Code: plugin install failed. Is `claude` CLI on your PATH?');
failedIDEs.push(ideId);
}
break;
}
Replace with:
case 'claude-code': {
log.success('Claude Code: plugin registered (cache + settings written by npx).');
break;
}
The cache dir, marketplace registration, plugin registration, and enabledPlugins flag have all been written by the (now ungated) runTasks block before setupIDEs is called. claude plugin install was duplicating that work and triggering the silent Setup hook — both reasons to drop it.
runRepairCommand() exportAfter runInstallCommand() (after line 761), append:
export async function runRepairCommand(): Promise<void> {
const version = readPluginVersion();
const cacheDir = pluginCacheDirectory(version);
if (isInteractive) {
p.intro(pc.bgCyan(pc.black(' claude-mem repair ')));
} else {
console.log('claude-mem repair');
}
log.info(`Version: ${pc.cyan(version)}`);
await runTasks([
{
title: 'Setting up runtime',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
message('Reinstalling plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
]);
if (isInteractive) {
p.outro(pc.green('claude-mem repair complete.'));
} else {
console.log('claude-mem repair complete.');
}
}
runRepairCommand always runs the install (no isInstallCurrent short-circuit) — the user invoked repair because something is wrong, so don't gate on the marker.
Verification checklist:
grep -n "needsManualInstall" src/npx-cli/commands/install.ts returns nothinggrep -n "runSmartInstall" src/npx-cli/commands/install.ts returns nothinggrep -n "claude plugin install" src/npx-cli/commands/install.ts returns nothinggrep -n "claude plugin marketplace add" src/npx-cli/commands/install.ts returns nothingrunRepairCommand is exported and TypeScript compilesrunInstallCommand still exports the same InstallOptions shape (Phase 3 needs it untouched)Anti-pattern guards:
runNpmInstallInMarketplace() — it's still needed for the marketplace dir copy step (other IDEs use that dir).copyPluginToMarketplace() — non-claude-code IDEs read from marketplaceDirectory().if (alreadyInstalled) overwrite-confirm block (lines 538–562) — user-facing UX preserved.npx claude-mem repairWhat to implement: Add a repair case to the npx-cli command dispatcher.
File to edit: src/npx-cli/index.ts
repair caseIn the switch block (lines 39–141), copy the install case pattern from lines 46–52 and adapt:
case 'repair': {
const { runRepairCommand } = await import('./commands/install.js');
await runRepairCommand();
break;
}
Place it adjacent to the install case for discoverability.
If src/npx-cli/index.ts has a help/usage block (look for case 'help': or default case), add repair to the list of commands with description: Repair claude-mem runtime (re-runs Bun/uv setup and bun install in plugin cache).
Verification checklist:
npx claude-mem repair --help (after build) shows the commandnpx claude-mem repair runs runRepairCommand end to end on a corrupted cache (delete .install-version then run; should reinstall)repairAnti-pattern guards:
repair (no flags needed).runRepairCommand body in index.ts — dynamic import only.version-check.jsWhat to implement: Replace the Setup hook's node smart-install.js call with a fast version-marker check. Delete the SessionStart smart-install hook entry entirely.
plugin/scripts/version-check.jsFile to create: plugin/scripts/version-check.js (new)
#!/usr/bin/env node
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
function resolveRoot() {
if (process.env.CLAUDE_PLUGIN_ROOT) {
const root = process.env.CLAUDE_PLUGIN_ROOT;
if (existsSync(join(root, 'package.json'))) return root;
}
try {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const candidate = dirname(scriptDir);
if (existsSync(join(candidate, 'package.json'))) return candidate;
} catch {}
return null;
}
const ROOT = resolveRoot();
if (!ROOT) process.exit(0);
try {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const markerPath = join(ROOT, '.install-version');
if (!existsSync(markerPath)) {
console.error('claude-mem: runtime not yet set up — run: npx claude-mem repair');
process.exit(0);
}
const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
if (marker.version !== pkg.version) {
console.error(`claude-mem: upgraded to v${pkg.version} — run: npx claude-mem repair`);
}
} catch {
console.error('claude-mem: install marker unreadable — run: npx claude-mem repair');
}
process.exit(0);
Behavior:
plugin/hooks/hooks.jsonLines 4–15 — replace the existing Setup hook command. Current command ends with node "$_R/scripts/smart-install.js". Change it to node "$_R/scripts/version-check.js". Everything before that (PATH export, _R resolution, cygpath) stays.
Concretely: the only change to line 11 is the trailing smart-install.js → version-check.js.
plugin/hooks/hooks.jsonLines 17–40 — the SessionStart hook array currently has THREE hook entries:
node "$_R/scripts/smart-install.js" (lines 21–26) — DELETE this entire entrynode "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" start (lines 27–32) — KEEPnode "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" hook claude-code context (lines 33–38) — KEEPAfter edit, the SessionStart hooks array has 2 entries instead of 3.
Verification checklist:
cat plugin/hooks/hooks.json | jq '.hooks.Setup[0].hooks[0].command' | grep version-check.js succeedscat plugin/hooks/hooks.json | jq '.hooks.SessionStart[0].hooks | length' returns 2grep -c "smart-install" plugin/hooks/hooks.json returns 0node plugin/scripts/version-check.js exits 0 in <500ms (time it).install-version marker), version-check stderr says "run: npx claude-mem repair"Anti-pattern guards:
What to implement: Delete smart-install source files and update tests.
rm scripts/smart-install.js
rm plugin/scripts/smart-install.js
rm tests/smart-install.test.ts
tests/plugin-scripts-line-endings.test.tsLine 12 (the SHEBANG_SCRIPTS array): remove the 'smart-install.js' entry. Keep the rest of the array intact.
If the array becomes empty after the removal, also remove the entry — but per discovery report it has multiple entries, so just delete the one line.
File to create: tests/setup-runtime.test.ts
Cover:
readInstallMarker returns null for missing filewriteInstallMarker produces a JSON object matching the smart-install.js schema ({ version, bun, uv, installedAt })isInstallCurrent returns false for missing marker, false for version mismatch, true for matchIf you skip this, document why in the PR description.
Verification checklist:
find . -name "smart-install*" -not -path "*/node_modules/*" returns no resultsgrep -rn "smart-install" tests/ returns no resultsnpm test (or whatever the project uses) passestests/setup-runtime.test.ts was added, it passesAnti-pattern guards:
tests/plugin-scripts-line-endings.test.ts entirely — it tests other scripts too.tests/bun-runner.test.ts — bun-runner.js stays in this PR.What to implement: Sweep documentation to reflect the new install flow.
docs/architecture-overview.md:36Update reference to smart-install. New copy: "On first install, npx claude-mem install sets up Bun and uv globally and runs bun install in the plugin cache. The Setup hook then runs a sub-100ms version check on every Claude Code startup; if the plugin was upgraded externally, the user is prompted to run npx claude-mem repair."
docs/public/configuration.mdx:139,163 and docs/public/development.mdx:42Replace any mention of smart-install behavior with the version-check + repair model. Two-sentence patches; preserve surrounding context.
docs/public/hooks-architecture.mdx (11 references)This is the largest doc change. Walk each reference (lines 77, 103, 119, 127, 432, 695–696, 703 per discovery report). Update text describing the Setup hook to say it runs version-check.js (sub-100ms) instead of smart-install.js. Update SessionStart description to reflect 2 entries (worker start + context fetch) instead of 3.
docs/public/architecture/ references (lines 149, 193)Same pattern — replace smart-install lifecycle description with the npx-installer + version-check model.
CLAUDE.md says: "No need to edit the changelog ever, it's generated automatically." Don't touch it.
The old docs/reports/ archive was removed during later cleanup. Do not recreate it as part of this installer work.
Verification checklist:
grep -rn "smart-install" docs/public/ returns no resultsgrep -rn "smart-install" docs/architecture-overview.md returns no resultsAnti-pattern guards:
docs/reports/ archive from this plan.What to implement: End-to-end validation. This phase is run by the implementer before opening the PR.
npm run build-and-sync
This must succeed. If TypeScript fails on the new setup-runtime.ts, fix in place.
npm test
Must be green. Likely failures to anticipate:
plugin-scripts-line-endings.test.ts if the 'smart-install.js' entry was missed in Phase 5scripts/smart-install.js (discovery report says only tests/smart-install.test.ts, which Phase 5 deletes)rm -rf ~/.claude/plugins/marketplaces/thedotmack ~/.claude/plugins/cache/thedotmack ~/.claude-mem):
npx claude-mem install
rm ~/.claude/plugins/cache/thedotmack/claude-mem/<version>/.install-version
npx claude-mem repair
Per the PR creation flow in the user's outer task. Don't auto-merge; the user wants a review loop.
Verification checklist:
npm run build-and-sync exits 0npm test exits 0npx claude-mem repair runs end-to-endAnti-pattern guards:
| Type | Path | Phase |
|---|---|---|
| Created | src/npx-cli/install/setup-runtime.ts | 1 |
| Edited | src/npx-cli/commands/install.ts | 2 |
| Edited | src/npx-cli/index.ts | 3 |
| Created | plugin/scripts/version-check.js | 4 |
| Edited | plugin/hooks/hooks.json | 4 |
| Deleted | scripts/smart-install.js | 5 |
| Deleted | plugin/scripts/smart-install.js | 5 |
| Deleted | tests/smart-install.test.ts | 5 |
| Edited | tests/plugin-scripts-line-endings.test.ts | 5 |
| Created | tests/setup-runtime.test.ts (optional) | 5 |
| Edited | docs/architecture-overview.md | 6 |
| Edited | docs/public/configuration.mdx | 6 |
| Edited | docs/public/development.mdx | 6 |
| Edited | docs/public/hooks-architecture.mdx | 6 |
| Edited | docs/public/architecture/*.md | 6 |
Estimated diff: +250 / −500 lines (net deletion).