UNPKG

claude-self-reflect

Version:

Give Claude perfect memory of all your conversations. Single binary, zero dependencies.

334 lines (293 loc) 10.9 kB
#!/usr/bin/env node /** * Post-install hook for npm. * Downloads the csr-engine binary from GitHub Releases. * Detects existing Python CSR installations and guides upgrade. * * Environment variables: * CSR_SKIP_BINARY_DOWNLOAD=1 — Skip binary download (CI, offline, custom builds) */ import { existsSync, readFileSync, mkdirSync, createWriteStream, chmodSync, unlinkSync, mkdtempSync, rmSync, copyFileSync, lstatSync, } from 'fs'; import { dirname, join, resolve } from 'path'; import { homedir, platform, arch, tmpdir } from 'os'; import { execFileSync } from 'child_process'; import { createHash } from 'crypto'; import { get as httpsGet } from 'https'; import { fileURLToPath } from 'url'; const REPO = 'ramakay/claude-self-reflect'; const BINARY_NAME = 'csr-engine'; const INSTALL_DIR = process.env.CSR_INSTALL_DIR || join(homedir(), '.local', 'bin'); const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..'); const MAX_REDIRECTS = 5; function isSourceCheckout() { return existsSync(join(PACKAGE_ROOT, 'csr-engine', 'Cargo.toml')) && existsSync(join(PACKAGE_ROOT, '.git')); } // Skip during development if (isSourceCheckout() && !process.env.CSR_FORCE_POSTINSTALL) { process.exit(0); } // Skip if explicitly disabled if (process.env.CSR_SKIP_BINARY_DOWNLOAD === '1') { console.log('\n CSR_SKIP_BINARY_DOWNLOAD=1 — skipping binary download.'); console.log(' Install manually: curl -fsSL https://raw.githubusercontent.com/ramakay/claude-self-reflect/main/scripts/install.sh | sh\n'); process.exit(0); } // --- Platform detection --- function detectTarget() { const os = platform(); const cpu = arch(); let osName; switch (os) { case 'darwin': osName = 'apple-darwin'; break; case 'linux': osName = 'unknown-linux-gnu'; break; default: console.error(`\n Unsupported platform: ${os}. Only macOS and Linux are supported.`); console.error(' Install manually: curl -fsSL https://raw.githubusercontent.com/ramakay/claude-self-reflect/main/scripts/install.sh | sh\n'); process.exit(0); // Don't fail npm install } let archName; switch (cpu) { case 'arm64': archName = 'aarch64'; break; case 'x64': archName = 'x86_64'; break; default: console.error(`\n Unsupported architecture: ${cpu}.`); process.exit(0); } // Intel Mac: no prebuilt binaries if (archName === 'x86_64' && osName === 'apple-darwin') { console.log('\n Intel Mac (x86_64) — no prebuilt binaries (ONNX limitation).'); console.log(' Build from source:'); console.log(' cd csr-engine && cargo build --release'); console.log(' cp target/release/csr-engine ~/.local/bin/\n'); process.exit(0); } return `${archName}-${osName}`; } // --- HTTP helpers --- function httpsUrl(url) { const parsed = new URL(url); if (parsed.protocol !== 'https:') { throw new Error(`Refusing non-HTTPS URL: ${url}`); } return parsed; } function redirectUrl(currentUrl, location) { const parsed = new URL(location, currentUrl); if (parsed.protocol !== 'https:') { throw new Error(`Refusing non-HTTPS redirect: ${parsed.href}`); } return parsed.href; } function downloadText(url, redirects = 0) { return new Promise((resolve, reject) => { const parsedUrl = httpsUrl(url); httpsGet(parsedUrl, { headers: { 'User-Agent': 'claude-self-reflect-npm' } }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); if (redirects >= MAX_REDIRECTS) { return reject(new Error(`Too many redirects for ${url}`)); } return downloadText(redirectUrl(parsedUrl, res.headers.location), redirects + 1) .then(resolve, reject); } if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); } let data = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { data += chunk; }); res.on('end', () => resolve(data)); res.on('error', reject); }).on('error', reject); }); } function downloadFile(url, dest, redirects = 0) { return new Promise((resolve, reject) => { const parsedUrl = httpsUrl(url); httpsGet(parsedUrl, { headers: { 'User-Agent': 'claude-self-reflect-npm' } }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); if (redirects >= MAX_REDIRECTS) { return reject(new Error(`Too many redirects for ${url}`)); } return downloadFile(redirectUrl(parsedUrl, res.headers.location), dest, redirects + 1) .then(resolve, reject); } if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); } const file = createWriteStream(dest, { flags: 'wx', mode: 0o600 }); let settled = false; const fail = (e) => { if (settled) return; settled = true; file.destroy(); try { unlinkSync(dest); } catch {} reject(e); }; res.pipe(file); res.on('error', fail); file.on('error', fail); file.on('finish', () => { file.close((err) => { if (err) return fail(err); if (!settled) { settled = true; resolve(); } }); }); }).on('error', reject); }); } function findExpectedChecksum(checksumData, filename) { for (const rawLine of checksumData.split('\n')) { const line = rawLine.trim(); if (!line) continue; const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/); if (match && match[2].trim() === filename) { return match[1].toLowerCase(); } } throw new Error(`No checksum entry found for ${filename}`); } // --- Existing installation detection --- function findExistingBinary() { const candidates = [ join(INSTALL_DIR, BINARY_NAME), '/usr/local/bin/csr-engine', ]; try { const which = execFileSync('which', ['csr-engine'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }).trim(); if (which) candidates.unshift(which); } catch {} for (const p of candidates) { if (existsSync(p)) return p; } return null; } function detectPythonCSR() { const signals = []; try { const settingsPath = join(homedir(), '.claude', 'settings.json'); if (existsSync(settingsPath)) { const settings = JSON.parse(readFileSync(settingsPath, 'utf8')); const hooks = settings.hooks || {}; for (const entries of Object.values(hooks)) { const hookStr = JSON.stringify(entries); if (hookStr.includes('.py') && hookStr.includes('claude-self-reflect')) { signals.push('Python hooks in settings.json'); break; } } } } catch {} return signals; } // --- Main --- async function main() { const target = detectTarget(); const existingBinary = findExistingBinary(); const pythonSignals = detectPythonCSR(); // Get version from package.json (pinned to this release) const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; const tag = `v${pkgVersion}`; if (pythonSignals.length > 0) { console.log('\n \x1b[1;33mUpgrading from Python CSR to v8.0 (Rust)\x1b[0m'); console.log(' v8.0 replaces Docker + Python + Qdrant with a single 44MB binary.'); console.log(' Your conversation data is preserved.\n'); } if (existingBinary) { // Check if existing binary is the right version try { const version = execFileSync(existingBinary, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }).trim(); if (version === pkgVersion || version === tag || version.includes(` ${pkgVersion}`)) { console.log(`\n \x1b[1;32mcsr-engine ${pkgVersion} already installed.\x1b[0m\n`); return; } console.log(` Updating ${existingBinary} to ${tag}...`); } catch { console.log(` Updating ${existingBinary}...`); } } // Download binary const tarball = `csr-engine-${target}.tar.gz`; const url = `https://github.com/${REPO}/releases/download/${tag}/${tarball}`; const checksumUrl = `https://github.com/${REPO}/releases/download/${tag}/checksums.txt`; console.log(` Downloading csr-engine ${tag} for ${target}...`); mkdirSync(INSTALL_DIR, { recursive: true }); const tmpDir = mkdtempSync(join(tmpdir(), 'csr-install-')); try { const tarPath = join(tmpDir, tarball); await downloadFile(url, tarPath); // Verify checksum from the release's published checksums.txt. const checksumData = await downloadText(checksumUrl); const expected = findExpectedChecksum(checksumData, tarball); const actual = createHash('sha256').update(readFileSync(tarPath)).digest('hex'); if (expected !== actual) { throw new Error(`Checksum mismatch for ${tarball}. Expected ${expected}, got ${actual}`); } console.log(' \x1b[32mChecksum verified.\x1b[0m'); // Extract execFileSync('tar', ['-xzf', tarPath, '-C', tmpDir], { stdio: 'pipe' }); // Find and install binary const binaryPath = join(tmpDir, BINARY_NAME); if (!existsSync(binaryPath)) { throw new Error('Binary not found in archive'); } if (!lstatSync(binaryPath).isFile()) { throw new Error('Archive entry csr-engine is not a regular file'); } const destPath = join(INSTALL_DIR, BINARY_NAME); copyFileSync(binaryPath, destPath); chmodSync(destPath, 0o755); console.log(` \x1b[1;32mInstalled:\x1b[0m ${destPath}`); // Auto-run setup console.log(' Running setup...'); try { execFileSync(destPath, ['setup'], { stdio: 'inherit', timeout: 60000 }); console.log('\n \x1b[32mDone. Restart Claude Code to activate.\x1b[0m\n'); } catch { console.log('\n Setup encountered errors. Run manually: csr-engine setup\n'); } if (pythonSignals.length > 0) { console.log(' Old Python stack can be cleaned up:'); console.log(' docker stop qdrant 2>/dev/null'); console.log(' rm -rf ~/projects/claude-self-reflect/venv\n'); } } catch (e) { console.error(`\n \x1b[31mBinary download failed:\x1b[0m ${e.message}`); console.error(' Install manually:'); console.error(' curl -fsSL https://raw.githubusercontent.com/ramakay/claude-self-reflect/main/scripts/install.sh | sh\n'); process.exit(1); } finally { // Cleanup temp dir try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} } } main().catch((e) => { console.error(` Postinstall error: ${e.message}`); console.error(' Install manually: curl -fsSL https://raw.githubusercontent.com/ramakay/claude-self-reflect/main/scripts/install.sh | sh'); process.exit(1); });