#!/usr/bin/env node 'use strict'; /** * claude-paif CLI tool * Integrates PAIF (Persistent Agent Identity Framework) with Claude Code */ const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); const MEMORY_BRIDGE_URL = process.env.MEMORY_BRIDGE_URL || 'http://localhost:3722'; const CONFIG_DIR = path.join(os.homedir(), '.claude'); const CONFIG_FILE = path.join(CONFIG_DIR, 'paif.json'); // Load PAIF config function loadConfig() { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return { agents: {}, default_agent: null }; } } // Save PAIF config function saveConfig(config) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); } // Get agent token from memory-bridge function getAgentToken(agentId) { const agentEnvPath = path.join(os.homedir(), '.memory-bridge', 'agents', agentId, '.env'); try { const content = fs.readFileSync(agentEnvPath, 'utf8'); const match = content.match(/^AGENT_TOKEN=(.+)$/m); return match ? match[1].trim() : null; } catch { return null; } } // API helper async function api(endpoint, options = {}) { const url = `${MEMORY_BRIDGE_URL}${endpoint}`; // Build request init explicitly to avoid spread issues const init = { method: options.method || 'GET', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }; if (options.body) { init.body = options.body; } const response = await fetch(url, init); if (!response.ok) { const error = await response.text(); throw new Error(`API error: ${response.status} ${error}`); } return response.json(); } // Commands const commands = { // Initialize agent for Claude Code async init(agentId, options = {}) { console.log(`Initializing PAIF agent: ${agentId}`); // Check if agent exists in memory-bridge const token = getAgentToken(agentId); if (!token) { console.error(`Agent '${agentId}' not found. Register first with master token.`); console.log(`\n curl -X POST ${MEMORY_BRIDGE_URL}/register-agent \\\\`); console.log(` -H "Authorization: Bearer " \\\\`); console.log(` -d '{"agent_id": "${agentId}"}'`); process.exit(1); } // Load identity const identity = await api(`/identity/${agentId}`, { headers: { 'Authorization': `Bearer ${token}` } }); // Save config const config = loadConfig(); config.agents[agentId] = { token: token, identity_loaded: true, gravity_checks: options.gravity !== false, checkpoint_on_exit: options.checkpoint !== false, gravity_interval: parseInt(options.interval) || 10 }; if (options.default) { config.default_agent = agentId; } saveConfig(config); console.log(`✓ Agent '${agentId}' initialized`); console.log(` Identity: ${identity.identity.immutable.purpose.statement}`); console.log(` Gravity checks: ${config.agents[agentId].gravity_checks ? 'enabled' : 'disabled'}`); console.log(` Checkpoint on exit: ${config.agents[agentId].checkpoint_on_exit ? 'enabled' : 'disabled'}`); if (config.default_agent === agentId) { console.log(` Set as default agent`); } }, // Restore session at start async restore(agentId, options) { // Handle case where agentId is actually options object if (typeof agentId === 'object') { options = agentId; agentId = null; } options = options || {}; const config = loadConfig(); agentId = agentId || config.default_agent; if (!agentId) { console.error('No agent specified and no default set. Use: claude-paif restore '); process.exit(1); } const token = getAgentToken(agentId); if (!token) { console.error(`Agent '${agentId}' not configured. Run: claude-paif init ${agentId}`); process.exit(1); } console.log(`Restoring session for ${agentId}...`); // Restore from checkpoint const result = await api(`/restore/${agentId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); // Load identity for display const identity = await api(`/identity/${agentId}`, { headers: { 'Authorization': `Bearer ${token}` } }); const id = identity.identity; const state = id.mutable.state; console.log(`\n╔════════════════════════════════════════════════════════════╗`); console.log(`║ SESSION RESTORED: ${agentId.padEnd(43)}║`); console.log(`╠════════════════════════════════════════════════════════════╣`); console.log(`║ PURPOSE: ${id.immutable.purpose.statement.slice(0, 40).padEnd(45)}║`); console.log(`║ VALUES: ${id.immutable.values.primary.padEnd(47)}║`); console.log(`║ VOICE: ${id.immutable.voice.tone.padEnd(49)}║`); console.log(`╠════════════════════════════════════════════════════════════╣`); console.log(`║ Experience: ${state.accumulated_experience.toString().padEnd(42)}║`); console.log(`║ Focus: ${(state.current_focus || 'none').slice(0, 45).padEnd(47)}║`); console.log(`║ Drift checks: ${state.drift_checks_passed}/${state.drift_checks_passed + state.drift_checks_failed} passed${''.padEnd(28)}║`); console.log(`╚════════════════════════════════════════════════════════════╝`); if (result.checkpoint_found) { console.log(`\n ↳ Loaded checkpoint from ${result.checkpoint.metadata.ended_at}`); if (result.checkpoint.context.pending_items?.length > 0) { console.log(` ↳ ${result.checkpoint.context.pending_items.length} pending items`); } } // Update config with active session config.active_session = { agent_id: agentId, started_at: new Date().toISOString(), message_count: 0 }; saveConfig(config); }, // Checkpoint session at end async checkpoint(agentId, options = {}) { const config = loadConfig(); agentId = agentId || config.default_agent || config.active_session?.agent_id; if (!agentId) { console.error('No active session or agent specified'); process.exit(1); } const token = getAgentToken(agentId); if (!token) { console.error(`Agent '${agentId}' not configured`); process.exit(1); } const session = config.active_session; const startedAt = session?.started_at || new Date().toISOString(); const duration = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000); console.log(`Checkpointing session for ${agentId}...`); const checkpointData = { started_at: startedAt, duration_seconds: duration, current_focus: options.focus || null, active_project_ids: options.projects && typeof options.projects === 'string' ? options.projects.split(',') : [], conversation_summary: options.summary && typeof options.summary === 'string' ? options.summary : 'Session completed', pending_items: options.pending && typeof options.pending === 'string' ? options.pending.split(',') : [], memory_ids: [], drift_checks: { passed: parseInt(options.drift_passed) || 0, failed: parseInt(options.drift_failed) || 0 } }; const result = await api(`/checkpoint/${agentId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify(checkpointData) }); console.log(`✓ Session checkpointed`); console.log(` Session ID: ${result.session_id}`); console.log(` Duration: ${duration}s`); // Clear active session delete config.active_session; saveConfig(config); }, // Quick gravity check async gravity(agentId, text, options) { // Handle case where first arg is text and agentId is omitted if (typeof agentId !== 'string' || typeof text === 'object') { options = text; text = agentId; agentId = null; } options = options || {}; const config = loadConfig(); agentId = agentId || config.default_agent || config.active_session?.agent_id; if (!agentId) { console.error('No agent specified'); process.exit(1); } if (!text || typeof text !== 'string') { console.error('Text required for gravity check'); process.exit(1); } const token = getAgentToken(agentId); const result = await api(`/gravity-check/${agentId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ text, auto_realign: true }) }); if (result.drift_detected) { console.log('\n⚠️ DRIFT DETECTED'); console.log(` Score: ${result.drift_score}/100`); console.log(` Issues: ${result.issues.length}`); if (result.realignment?.prompt) { console.log('\n' + result.realignment.prompt); } } else { console.log('✓ Identity aligned'); console.log(` Score: ${result.drift_score}/100`); } }, // Store memory async store(agentId, text, tags, options) { // Handle flexible argument patterns // store [tags] // store [tags] (uses default agent) // store (uses default agent, no tags) if (typeof agentId !== 'string') { options = agentId; agentId = null; } if (typeof text !== 'string') { options = text; text = null; } if (typeof tags !== 'string') { options = tags; tags = ''; } options = options || {}; const config = loadConfig(); // If agentId doesn't look like an agent (no token), shift args if (agentId && !getAgentToken(agentId)) { tags = text || ''; text = agentId; agentId = null; } agentId = agentId || config.default_agent || config.active_session?.agent_id; // Debug logging for troubleshooting if (!agentId) { console.error('Error: No agent_id resolved'); console.error(' config.default_agent:', config.default_agent); console.error(' config.active_session:', config.active_session); console.error('Usage: claude-paif store [tags]'); process.exit(1); } if (!text) { console.error('Error: No text provided'); console.error('Usage: claude-paif store [tags]'); process.exit(1); } const token = getAgentToken(agentId); const result = await api('/store', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ agent_id: agentId, text, tags: tags.length ? tags.split(',') : [], source: 'claude-paif-cli' }) }); console.log(`✓ Stored memory: ${result.id}`); }, // Recall memories async recall(agentId, query, options = {}) { const config = loadConfig(); agentId = agentId || config.default_agent || config.active_session?.agent_id; if (!agentId || !query) { console.error('Usage: claude-paif recall [options]'); process.exit(1); } const token = getAgentToken(agentId); const result = await api('/recall', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ agent_id: agentId, query, limit: parseInt(options.limit) || 5, tags: options.tags ? options.tags.split(',') : [] }) }); console.log(`\nFound ${result.memories.length} memories:\n`); result.memories.forEach((m, i) => { console.log(`${i + 1}. [${m.score.toFixed(3)}] ${m.text.slice(0, 80)}${m.text.length > 80 ? '...' : ''}`); }); }, // Show current agent status async status(agentId, options) { // Handle case where agentId is actually options object if (typeof agentId === 'object') { options = agentId; agentId = null; } options = options || {}; const config = loadConfig(); agentId = agentId || config.default_agent; if (!agentId) { console.log('PAIF Configuration:'); console.log(` Config file: ${CONFIG_FILE}`); console.log(` Memory bridge: ${MEMORY_BRIDGE_URL}`); console.log(` Default agent: ${config.default_agent || 'none'}`); console.log(` Configured agents: ${Object.keys(config.agents).join(', ') || 'none'}`); console.log(` Active session: ${config.active_session ? config.active_session.agent_id : 'none'}`); return; } const token = getAgentToken(agentId); if (!token) { console.error(`Agent '${agentId}' not configured`); process.exit(1); } const [identity, sessions] = await Promise.all([ api(`/identity/${agentId}`, { headers: { 'Authorization': `Bearer ${token}` } }), api(`/sessions/${agentId}`, { headers: { 'Authorization': `Bearer ${token}` } }) ]); const id = identity.identity; const stats = sessions.stats; console.log(`\nAgent: ${agentId}`); console.log(`Purpose: ${id.immutable.purpose.statement}`); console.log(`Voice: ${id.immutable.voice.tone}`); console.log(`Values: ${id.immutable.values.primary}`); console.log(`\nSessions: ${stats.total_sessions}`); console.log(`Experience: ${stats.accumulated_experience}`); console.log(`Drift checks: ${stats.drift_checks.passed} passed, ${stats.drift_checks.failed} failed`); console.log(`Last session: ${stats.last_session_at || 'never'}`); if (config.active_session?.agent_id === agentId) { console.log(`\n⚡ Active session since ${config.active_session.started_at}`); } } }; // CLI argument parsing async function main() { const args = process.argv.slice(2); const command = args[0]; if (!command || command === 'help' || command === '--help' || command === '-h') { console.log(` claude-paif — PAIF integration for Claude Code Usage: claude-paif [options] Commands: init [--default] [--gravity] [--checkpoint] [--interval N] Initialize agent for Claude Code sessions restore [agent-id] Restore session (run at start of Claude Code session) checkpoint [agent-id] [--summary "text"] [--focus "topic"] [--pending "item1,item2"] Save session checkpoint (run at end of session) gravity [agent-id] Check text for identity drift store [agent-id] [tags] Store memory for agent recall [agent-id] [--limit N] [--tags tag1,tag2] Recall memories matching query status [agent-id] Show agent configuration and status Environment: MEMORY_BRIDGE_URL - memory-bridge server URL (default: http://localhost:3722) `); return; } const handler = commands[command]; if (!handler) { console.error(`Unknown command: ${command}`); console.log('Run "claude-paif help" for usage'); process.exit(1); } // Parse options (--key value or --flag) const options = {}; const positional = []; for (let i = 1; i < args.length; i++) { if (args[i].startsWith('--')) { const key = args[i].slice(2).replace(/-/g, '_'); const next = args[i + 1]; if (next && !next.startsWith('--')) { options[key] = next; i++; } else { options[key] = true; } } else { positional.push(args[i]); } } try { // Pass positional args first, then options as last argument await handler(...positional, options); } catch (err) { console.error(`Error: ${err.message}`); process.exit(1); } } main();