Skip to content

Commit 619a506

Browse files
committed
feat: omp CLI adapter for AI terminal tools (Aider, Claude Code, any CLI)
1 parent 1204142 commit 619a506

5 files changed

Lines changed: 362 additions & 6 deletions

File tree

README.md

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,16 +242,75 @@ Read the full specification: [SPEC.md](SPEC.md)
242242

243243
## Adapters
244244

245-
| Tool | Status | Install |
246-
|------|--------|---------|
247-
| Claude (MCP) | ✅ Available | `npx omp-mcp` |
248-
| Browser Extension | ✅ Available | [Load unpacked](adapters/browser-extension) — Chrome/Edge/Brave |
249-
| OpenAI Assistants | 🙋 Help wanted | [Open issue](https://github.com/SMJAI/open-memory-protocol/issues) |
245+
| Tool | Status | How |
246+
|------|--------|-----|
247+
| Claude Desktop | ✅ Working | MCP adapter — automatic memory save/recall |
248+
| Claude.ai (web) | ✅ Working | OMP Bridge extension — handoff toast on new chat |
249+
| ChatGPT (web) | ✅ Working | OMP Bridge extension — reads DOM, saves conversation |
250+
| Gemini (web) | ✅ Working | OMP Bridge extension |
251+
| Perplexity (web) | ✅ Working | OMP Bridge extension |
252+
| Claude Code (CLI) | ✅ Working | `claude mcp add omp-mcp` — same MCP tools |
253+
| Any AI CLI (Aider etc.) | ✅ Working | `omp` CLI — inject context, save sessions |
250254
| Cursor | 🙋 Help wanted | [Open issue](https://github.com/SMJAI/open-memory-protocol/issues) |
251255
| Copilot / VS Code | 🙋 Help wanted | [Open issue](https://github.com/SMJAI/open-memory-protocol/issues) |
252-
| Gemini | 🙋 Help wanted | [Open issue](https://github.com/SMJAI/open-memory-protocol/issues) |
253256
| Custom (REST) | ✅ Available | Any HTTP client |
254257

258+
### Claude Code CLI
259+
260+
Claude Code supports MCP servers natively. Add OMP in one command:
261+
262+
```bash
263+
claude mcp add omp-mcp -- node /path/to/omp-mcp/dist/index.js
264+
```
265+
266+
Or add it to your project's `.mcp.json`:
267+
268+
```json
269+
{
270+
"mcpServers": {
271+
"omp": {
272+
"command": "npx",
273+
"args": ["omp-mcp"],
274+
"env": { "OMP_SERVER": "http://localhost:3456" }
275+
}
276+
}
277+
}
278+
```
279+
280+
Claude Code then has the same `omp_remember`, `omp_recall`, and `omp_compress` tools available in every session.
281+
282+
### Any AI CLI (Aider, shell scripts, custom agents)
283+
284+
Install the `omp` CLI:
285+
286+
```bash
287+
npm install -g omp-cli
288+
```
289+
290+
Then use it to bridge OMP with any terminal-based AI:
291+
292+
```bash
293+
# Inject your OMP memory context into any AI CLI
294+
omp context
295+
# → [Memory context from OMP]
296+
# - [semantic] User is building Open Memory Protocol...
297+
298+
# Start Aider with your OMP context pre-loaded
299+
omp context > /tmp/omp.md && aider --read /tmp/omp.md
300+
301+
# Continue a ChatGPT conversation in any CLI
302+
omp handoff --from chatgpt
303+
# → "I was discussing MCP with ChatGPT. My last question was..."
304+
305+
# Pipe directly into Claude Code
306+
claude "$(omp handoff --from chatgpt) — now implement this"
307+
308+
# Save a CLI session to OMP when you're done
309+
omp save --model aider < session.txt
310+
```
311+
312+
See [`adapters/cli`](adapters/cli) for full usage.
313+
255314
### OMP Bridge — Browser Extension
256315

257316
The browser extension brings OMP to the **web versions** of every AI tool with zero setup on their side.

adapters/cli/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "omp-cli",
3+
"version": "0.1.0",
4+
"description": "OMP command-line tool — inject memory context into any AI CLI",
5+
"bin": {
6+
"omp": "./dist/index.js"
7+
},
8+
"main": "dist/index.js",
9+
"files": ["dist", "src"],
10+
"scripts": {
11+
"build": "tsc",
12+
"dev": "ts-node src/index.ts"
13+
},
14+
"keywords": ["omp", "open-memory-protocol", "ai-memory", "cli", "aider", "llm"],
15+
"license": "Apache-2.0",
16+
"repository": {
17+
"type": "git",
18+
"url": "https://github.com/SMJAI/open-memory-protocol",
19+
"directory": "adapters/cli"
20+
},
21+
"devDependencies": {
22+
"@types/node": "^22.10.0",
23+
"typescript": "^5.3.3",
24+
"ts-node": "^10.9.2"
25+
}
26+
}

adapters/cli/src/index.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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()

adapters/cli/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "CommonJS",
5+
"lib": ["ES2022"],
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true
11+
},
12+
"include": ["src"],
13+
"exclude": ["node_modules", "dist"]
14+
}

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)