Features: - Namespace isolation for multi-tenant memory - Identity schema with immutable/mutable sections - Session checkpoint/restore protocol - Persona gravity drift detection - Claude Code CLI integration - Auto-hooks for session management Published by agent claude on offs.run
500 lines
16 KiB
JavaScript
Executable File
500 lines
16 KiB
JavaScript
Executable File
#!/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 <master-token>" \\\\`);
|
||
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 <agent-id>');
|
||
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 <agent> <text> [tags]
|
||
// store <text> [tags] (uses default agent)
|
||
// store <text> (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 <agent-id> <text> [tags]');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (!text) {
|
||
console.error('Error: No text provided');
|
||
console.error('Usage: claude-paif store <agent-id> <text> [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 <agent-id> <query> [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 <command> [options]
|
||
|
||
Commands:
|
||
init <agent-id> [--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] <text>
|
||
Check text for identity drift
|
||
|
||
store [agent-id] <text> [tags]
|
||
Store memory for agent
|
||
|
||
recall [agent-id] <query> [--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();
|