|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * omp — Open Memory Protocol CLI |
| 4 | + * Brings OMP memory to any AI CLI tool (Aider, Claude Code, custom agents). |
| 5 | + * |
| 6 | + * Usage: |
| 7 | + * omp context Print memory context ready to paste or pipe |
| 8 | + * omp recent List recent saved conversations |
| 9 | + * omp handoff [--from X] Generate a handoff brief from the last conversation |
| 10 | + * omp save [--model X] Read stdin and save as a conversation |
| 11 | + * omp remember "text" Save a single memory |
| 12 | + * omp recall "query" Search memories |
| 13 | + */ |
| 14 | + |
| 15 | +import * as readline from 'readline' |
| 16 | + |
| 17 | +const SERVER = process.env.OMP_SERVER ?? 'http://localhost:3456' |
| 18 | +const API_KEY = process.env.OMP_API_KEY ?? '' |
| 19 | + |
| 20 | +function authHeaders(): Record<string, string> { |
| 21 | + const h: Record<string, string> = { 'Content-Type': 'application/json' } |
| 22 | + if (API_KEY) h['Authorization'] = `Bearer ${API_KEY}` |
| 23 | + return h |
| 24 | +} |
| 25 | + |
| 26 | +async function get(path: string) { |
| 27 | + const res = await fetch(`${SERVER}${path}`, { headers: authHeaders() }) |
| 28 | + if (!res.ok) throw new Error(`OMP error ${res.status}: ${await res.text()}`) |
| 29 | + return res.json() |
| 30 | +} |
| 31 | + |
| 32 | +async function post(path: string, body: unknown) { |
| 33 | + const res = await fetch(`${SERVER}${path}`, { |
| 34 | + method: 'POST', |
| 35 | + headers: authHeaders(), |
| 36 | + body: JSON.stringify(body), |
| 37 | + }) |
| 38 | + if (!res.ok) throw new Error(`OMP error ${res.status}: ${await res.text()}`) |
| 39 | + return res.json() |
| 40 | +} |
| 41 | + |
| 42 | +async function readStdin(): Promise<string> { |
| 43 | + if (process.stdin.isTTY) return '' |
| 44 | + return new Promise(resolve => { |
| 45 | + const chunks: string[] = [] |
| 46 | + const rl = readline.createInterface({ input: process.stdin }) |
| 47 | + rl.on('line', line => chunks.push(line)) |
| 48 | + rl.on('close', () => resolve(chunks.join('\n'))) |
| 49 | + }) |
| 50 | +} |
| 51 | + |
| 52 | +// ─── Commands ──────────────────────────────────────────────────────────────── |
| 53 | + |
| 54 | +async function cmdContext() { |
| 55 | + const data = await get('/v1/memories?limit=20') as { memories: Array<{ type: string; content: string }> } |
| 56 | + if (!data.memories.length) { |
| 57 | + console.log('[No OMP memories yet. Start chatting with Claude Desktop or save memories with: omp remember "text"]') |
| 58 | + return |
| 59 | + } |
| 60 | + const lines = data.memories.map((m: { type: string; content: string }) => `- [${m.type}] ${m.content}`) |
| 61 | + console.log('[Memory context from OMP]') |
| 62 | + console.log(lines.join('\n')) |
| 63 | + console.log('\n---\n') |
| 64 | +} |
| 65 | + |
| 66 | +async function cmdRecent() { |
| 67 | + const data = await get('/v1/conversations?limit=10') as { conversations: Array<{ id: string; model: string; topic: string; message_count: number; created_at: string }> } |
| 68 | + if (!data.conversations.length) { |
| 69 | + console.log('No conversations saved yet.') |
| 70 | + return |
| 71 | + } |
| 72 | + console.log('Recent conversations:\n') |
| 73 | + data.conversations.forEach((c, i) => { |
| 74 | + const ago = Math.round((Date.now() - new Date(c.created_at).getTime()) / 60000) |
| 75 | + console.log(` ${i + 1}. [${c.model}] ${c.topic.slice(0, 70)} (${c.message_count} msgs, ${ago}m ago)`) |
| 76 | + console.log(` id: ${c.id}`) |
| 77 | + }) |
| 78 | +} |
| 79 | + |
| 80 | +async function cmdHandoff(args: string[]) { |
| 81 | + const fromIdx = args.indexOf('--from') |
| 82 | + const fromModel = fromIdx !== -1 ? args[fromIdx + 1] : undefined |
| 83 | + const toModel = (args[args.indexOf('--to') + 1] ?? process.env.OMP_TARGET_MODEL ?? 'claude') |
| 84 | + |
| 85 | + const params = new URLSearchParams({ limit: '1' }) |
| 86 | + if (fromModel) params.set('model', fromModel) |
| 87 | + |
| 88 | + const data = await get(`/v1/conversations?${params}`) as { conversations: Array<{ id: string; model: string }> } |
| 89 | + if (!data.conversations.length) { |
| 90 | + console.error('No saved conversations found.' + (fromModel ? ` (from: ${fromModel})` : '')) |
| 91 | + console.error('Save one first with: omp save --model chatgpt < session.txt') |
| 92 | + process.exit(1) |
| 93 | + } |
| 94 | + |
| 95 | + const conv = data.conversations[0] |
| 96 | + const handoff = await post('/v1/handoff', { |
| 97 | + conversation_id: conv.id, |
| 98 | + target_model: toModel, |
| 99 | + }) as { brief: string; topic: string; source_model: string } |
| 100 | + |
| 101 | + console.log(handoff.brief) |
| 102 | +} |
| 103 | + |
| 104 | +async function cmdSave(args: string[]) { |
| 105 | + const modelIdx = args.indexOf('--model') |
| 106 | + const model = modelIdx !== -1 ? args[modelIdx + 1] : 'cli' |
| 107 | + |
| 108 | + const text = await readStdin() |
| 109 | + if (!text.trim()) { |
| 110 | + console.error('No input. Pipe text into omp save:') |
| 111 | + console.error(' echo "conversation text" | omp save --model aider') |
| 112 | + console.error(' omp save --model aider < session.txt') |
| 113 | + process.exit(1) |
| 114 | + } |
| 115 | + |
| 116 | + // Parse stdin as alternating turns or raw text |
| 117 | + const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [] |
| 118 | + |
| 119 | + // Try to detect "User:" / "AI:" / "Assistant:" patterns |
| 120 | + const lines = text.split('\n') |
| 121 | + let current: { role: 'user' | 'assistant'; lines: string[] } | null = null |
| 122 | + |
| 123 | + for (const line of lines) { |
| 124 | + const userMatch = line.match(/^(User|Human|You|Me):\s*(.*)/i) |
| 125 | + const aiMatch = line.match(/^(AI|Assistant|Claude|ChatGPT|GPT|Aider|Bot):\s*(.*)/i) |
| 126 | + |
| 127 | + if (userMatch) { |
| 128 | + if (current) messages.push({ role: current.role, content: current.lines.join('\n').trim() }) |
| 129 | + current = { role: 'user', lines: [userMatch[2]] } |
| 130 | + } else if (aiMatch) { |
| 131 | + if (current) messages.push({ role: current.role, content: current.lines.join('\n').trim() }) |
| 132 | + current = { role: 'assistant', lines: [aiMatch[2]] } |
| 133 | + } else if (current) { |
| 134 | + current.lines.push(line) |
| 135 | + } |
| 136 | + } |
| 137 | + if (current) messages.push({ role: current.role, content: current.lines.join('\n').trim() }) |
| 138 | + |
| 139 | + // If no structure detected, save whole text as one user message |
| 140 | + if (messages.length === 0) { |
| 141 | + messages.push({ role: 'user', content: text.slice(0, 10000) }) |
| 142 | + } |
| 143 | + |
| 144 | + const saved = await post('/v1/conversations', { model, messages }) as { id: string; message_count: number } |
| 145 | + console.log(`✓ Saved ${saved.message_count} messages from [${model}] — id: ${saved.id}`) |
| 146 | +} |
| 147 | + |
| 148 | +async function cmdRemember(args: string[]) { |
| 149 | + const content = args.filter(a => !a.startsWith('-')).join(' ') |
| 150 | + if (!content) { |
| 151 | + console.error('Usage: omp remember "what to remember"') |
| 152 | + process.exit(1) |
| 153 | + } |
| 154 | + const mem = await post('/v1/memories', { |
| 155 | + content, |
| 156 | + type: 'semantic', |
| 157 | + source: { tool: 'omp-cli', timestamp: new Date().toISOString() }, |
| 158 | + tags: ['cli'], |
| 159 | + }) as { id: string } |
| 160 | + console.log(`✓ Memory saved — id: ${mem.id}`) |
| 161 | +} |
| 162 | + |
| 163 | +async function cmdRecall(args: string[]) { |
| 164 | + const query = args.filter(a => !a.startsWith('-')).join(' ') |
| 165 | + if (!query) { |
| 166 | + console.error('Usage: omp recall "search query"') |
| 167 | + process.exit(1) |
| 168 | + } |
| 169 | + const data = await post('/v1/memories/search', { q: query, limit: 5 }) as { memories: Array<{ type: string; content: string }> } |
| 170 | + if (!data.memories.length) { |
| 171 | + console.log('No matching memories.') |
| 172 | + return |
| 173 | + } |
| 174 | + data.memories.forEach((m: { type: string; content: string }) => console.log(`[${m.type}] ${m.content}`)) |
| 175 | +} |
| 176 | + |
| 177 | +function help() { |
| 178 | + console.log(` |
| 179 | +omp — Open Memory Protocol CLI |
| 180 | +
|
| 181 | +COMMANDS |
| 182 | + omp context Print your OMP memories (pipe into any AI) |
| 183 | + omp recent List recently saved conversations |
| 184 | + omp handoff [--from MODEL] Generate handoff brief from last conversation |
| 185 | + omp save [--model MODEL] Read stdin, save as a conversation |
| 186 | + omp remember "text" Save a single memory |
| 187 | + omp recall "query" Search your memories |
| 188 | +
|
| 189 | +EXAMPLES |
| 190 | + # See your memories |
| 191 | + omp context |
| 192 | +
|
| 193 | + # Start Aider with OMP context |
| 194 | + omp context > /tmp/omp.md && aider --read /tmp/omp.md |
| 195 | +
|
| 196 | + # Continue a ChatGPT conversation in Claude Code |
| 197 | + claude "$(omp handoff --from chatgpt)" |
| 198 | +
|
| 199 | + # Save an Aider session to OMP |
| 200 | + omp save --model aider < session.txt |
| 201 | +
|
| 202 | + # Quick memory |
| 203 | + omp remember "Decided to use PostgreSQL over SQLite for production" |
| 204 | +
|
| 205 | +ENVIRONMENT |
| 206 | + OMP_SERVER OMP server URL (default: http://localhost:3456) |
| 207 | + OMP_API_KEY API key if your server requires one |
| 208 | +`) |
| 209 | +} |
| 210 | + |
| 211 | +// ─── Main ──────────────────────────────────────────────────────────────────── |
| 212 | + |
| 213 | +async function main() { |
| 214 | + const [,, cmd, ...args] = process.argv |
| 215 | + |
| 216 | + try { |
| 217 | + switch (cmd) { |
| 218 | + case 'context': await cmdContext(); break |
| 219 | + case 'recent': await cmdRecent(); break |
| 220 | + case 'handoff': await cmdHandoff(args); break |
| 221 | + case 'save': await cmdSave(args); break |
| 222 | + case 'remember': await cmdRemember(args); break |
| 223 | + case 'recall': await cmdRecall(args); break |
| 224 | + case 'help': |
| 225 | + case '--help': |
| 226 | + case '-h': |
| 227 | + case undefined: help(); break |
| 228 | + default: |
| 229 | + console.error(`Unknown command: ${cmd}`) |
| 230 | + console.error('Run "omp help" for usage.') |
| 231 | + process.exit(1) |
| 232 | + } |
| 233 | + } catch (err) { |
| 234 | + console.error('Error:', err instanceof Error ? err.message : String(err)) |
| 235 | + console.error(`\nIs the OMP server running? Start it with: npx omp-server`) |
| 236 | + process.exit(1) |
| 237 | + } |
| 238 | +} |
| 239 | + |
| 240 | +main() |
0 commit comments