@shokan/engram-mcp 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/hook.js +158 -0
- package/index.js +395 -0
- package/init.js +231 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Engram
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Engram MCP
|
|
2
|
+
|
|
3
|
+
Long-term memory for AI agents, served over the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
4
|
+
|
|
5
|
+
Engram gives Claude, Cursor, Windsurf and any other MCP client a persistent memory that survives across sessions, devices and tools. Memories are stored with embeddings and ranked by hybrid search (vector similarity fused with full-text search), so recall works for both exact terms and fuzzy meaning.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @shokan/engram-mcp init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The wizard asks for your API key (get one free at https://engram-deploy-ten.vercel.app/dashboard), detects your AI tools, wires up the MCP server, and installs an auto-capture hook for Claude Code so sessions are remembered automatically.
|
|
14
|
+
|
|
15
|
+
To configure manually, add this to your MCP client config:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"engram": {
|
|
21
|
+
"command": "engram-mcp",
|
|
22
|
+
"env": {
|
|
23
|
+
"ENGRAM_API_KEY": "your_key",
|
|
24
|
+
"ENGRAM_API_URL": "https://engram-deploy-ten.vercel.app"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
| Tool | Purpose |
|
|
34
|
+
|------|---------|
|
|
35
|
+
| `engram_remember` | Store a memory (facts, decisions, preferences, context). Long content is auto-summarized. |
|
|
36
|
+
| `engram_search` | Hybrid search returning a compact index (id + snippet + relevance). Cheap to scan first. |
|
|
37
|
+
| `engram_get` | Pull full content for specific memory ids. |
|
|
38
|
+
| `engram_recall` | One-shot full recall for quick lookups (1-3 expected hits). |
|
|
39
|
+
| `engram_timeline` | Temporal context: a whole session, or the neighbours around one memory. |
|
|
40
|
+
| `engram_forget` | Soft-delete a memory (recoverable) or hard-delete with `hard: true`. |
|
|
41
|
+
| `engram_trash` | List memories currently in the trash. |
|
|
42
|
+
| `engram_restore` | Restore a memory from the trash. |
|
|
43
|
+
| `engram_status` | Account and memory counts. |
|
|
44
|
+
|
|
45
|
+
## Recall strategy
|
|
46
|
+
|
|
47
|
+
Prefer `engram_search` first — it returns a compact index that is cheap on context. Then call `engram_get` only for the ids whose snippets look relevant. Use `engram_recall` only when you expect a handful of hits.
|
|
48
|
+
|
|
49
|
+
## Privacy
|
|
50
|
+
|
|
51
|
+
Wrap anything inside `<private>...</private>` in a remembered message and it is stripped before storage. The text never reaches the embedding model or the database.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/hook.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Engram auto-capture hook.
|
|
2
|
+
// Wired into an AI tool's SessionEnd (and optionally PreCompact) event. Reads the
|
|
3
|
+
// session transcript, sends only the NEW slice since last run to Engram's
|
|
4
|
+
// /api/v1/capture, which distills it into typed memories and stores them.
|
|
5
|
+
//
|
|
6
|
+
// Design rules:
|
|
7
|
+
// - NEVER throw. A memory hook must not break the user's session. Always exit 0.
|
|
8
|
+
// - Be quiet on stdout. Log problems to ~/.engram/hook.log only.
|
|
9
|
+
// - Only process new transcript lines, tracked per session, to avoid duplicates.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { homedir } from 'os'
|
|
14
|
+
|
|
15
|
+
const ENGRAM_DIR = join(homedir(), '.engram')
|
|
16
|
+
const CONFIG_PATH = join(ENGRAM_DIR, 'config.json')
|
|
17
|
+
const STATE_PATH = join(ENGRAM_DIR, 'capture-state.json')
|
|
18
|
+
const LOG_PATH = join(ENGRAM_DIR, 'hook.log')
|
|
19
|
+
|
|
20
|
+
// The key/url come from the environment when present, otherwise from the config
|
|
21
|
+
// file the installer wrote (so the key never has to live in the hook command).
|
|
22
|
+
function readConfig() {
|
|
23
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) } catch { return {} }
|
|
24
|
+
}
|
|
25
|
+
const CONFIG = readConfig()
|
|
26
|
+
const API_URL = (process.env.ENGRAM_API_URL || CONFIG.api_url || 'https://engram-deploy-ten.vercel.app').replace(/\/$/, '')
|
|
27
|
+
const API_KEY = process.env.ENGRAM_API_KEY || CONFIG.api_key
|
|
28
|
+
|
|
29
|
+
const MIN_NEW_CHARS = 200 // skip captures with almost no new content
|
|
30
|
+
const MAX_SLICE = 60000 // hard cap on text shipped per capture
|
|
31
|
+
|
|
32
|
+
function log(msg) {
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(ENGRAM_DIR)) mkdirSync(ENGRAM_DIR, { recursive: true })
|
|
35
|
+
appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${msg}\n`)
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readStdin() {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let data = ''
|
|
42
|
+
if (process.stdin.isTTY) return resolve('')
|
|
43
|
+
process.stdin.setEncoding('utf8')
|
|
44
|
+
process.stdin.on('data', (c) => { data += c })
|
|
45
|
+
process.stdin.on('end', () => resolve(data))
|
|
46
|
+
process.stdin.on('error', () => resolve(data))
|
|
47
|
+
setTimeout(() => resolve(data), 3000) // never hang
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadState() {
|
|
52
|
+
try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')) } catch { return {} }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function saveState(state) {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(ENGRAM_DIR)) mkdirSync(ENGRAM_DIR, { recursive: true })
|
|
58
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8')
|
|
59
|
+
} catch (e) { log(`state save failed: ${e.message}`) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pull plain text out of a transcript message (content is string or block array).
|
|
63
|
+
function messageText(entry) {
|
|
64
|
+
const msg = entry?.message
|
|
65
|
+
if (!msg) return ''
|
|
66
|
+
const role = msg.role || entry.type
|
|
67
|
+
const content = msg.content
|
|
68
|
+
let text = ''
|
|
69
|
+
if (typeof content === 'string') {
|
|
70
|
+
text = content
|
|
71
|
+
} else if (Array.isArray(content)) {
|
|
72
|
+
text = content
|
|
73
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
74
|
+
.map((b) => b.text)
|
|
75
|
+
.join('\n')
|
|
76
|
+
}
|
|
77
|
+
text = text.trim()
|
|
78
|
+
if (!text) return ''
|
|
79
|
+
const who = role === 'assistant' ? 'Assistant' : role === 'user' ? 'User' : null
|
|
80
|
+
if (!who) return ''
|
|
81
|
+
return `${who}: ${text}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function main() {
|
|
85
|
+
if (!API_KEY) { log('no ENGRAM_API_KEY; skipping'); return }
|
|
86
|
+
|
|
87
|
+
const input = await readStdin()
|
|
88
|
+
let payload = {}
|
|
89
|
+
try { payload = JSON.parse(input || '{}') } catch { /* ignore */ }
|
|
90
|
+
|
|
91
|
+
const sessionId = payload.session_id || 'unknown'
|
|
92
|
+
const transcriptPath = payload.transcript_path
|
|
93
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
94
|
+
log(`no transcript for session ${sessionId}`)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let lines
|
|
99
|
+
try {
|
|
100
|
+
lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
log(`read transcript failed: ${e.message}`)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const state = loadState()
|
|
107
|
+
const processed = state[sessionId]?.lines || 0
|
|
108
|
+
const newLines = lines.slice(processed)
|
|
109
|
+
if (newLines.length === 0) { log(`session ${sessionId}: nothing new`); return }
|
|
110
|
+
|
|
111
|
+
const parts = []
|
|
112
|
+
for (const line of newLines) {
|
|
113
|
+
let entry
|
|
114
|
+
try { entry = JSON.parse(line) } catch { continue }
|
|
115
|
+
const t = messageText(entry)
|
|
116
|
+
if (t) parts.push(t)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Advance the watermark to the current line count once we know these lines
|
|
120
|
+
// were either consumed (captured) or deliberately skipped. We do NOT advance
|
|
121
|
+
// before the capture is confirmed: a failed HTTP/network call must leave the
|
|
122
|
+
// watermark in place so the next run retries instead of silently dropping it.
|
|
123
|
+
const advance = () => {
|
|
124
|
+
state[sessionId] = { lines: lines.length, updated: new Date().toISOString() }
|
|
125
|
+
saveState(state)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let text = parts.join('\n\n').trim()
|
|
129
|
+
// Too little new content to be worth a memory — consume it and move on.
|
|
130
|
+
if (text.length < MIN_NEW_CHARS) { advance(); log(`session ${sessionId}: too little new content`); return }
|
|
131
|
+
if (text.length > MAX_SLICE) text = text.slice(text.length - MAX_SLICE)
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch(`${API_URL}/api/v1/capture`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({ text, source: 'claude-code', session_id: sessionId }),
|
|
141
|
+
})
|
|
142
|
+
const body = await res.text()
|
|
143
|
+
// Leave the watermark untouched on failure so the slice is retried later.
|
|
144
|
+
if (!res.ok) { log(`capture HTTP ${res.status}: ${body.slice(0, 300)}`); return }
|
|
145
|
+
advance()
|
|
146
|
+
let parsed = {}
|
|
147
|
+
try { parsed = JSON.parse(body) } catch { /* ignore */ }
|
|
148
|
+
log(`session ${sessionId}: stored ${parsed.stored ?? '?'} memories`)
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// Network error: do not advance — retry on the next session end.
|
|
151
|
+
log(`capture request failed: ${e.message}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function runHook() {
|
|
156
|
+
try { await main() } catch (e) { log(`unexpected: ${e.message}`) }
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
8
|
+
|
|
9
|
+
// Init subcommand — runs before API key check
|
|
10
|
+
if (process.argv[2] === 'init') {
|
|
11
|
+
const { runInit } = await import('./init.js')
|
|
12
|
+
await runInit()
|
|
13
|
+
process.exit(0)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Hook subcommand — auto-capture, invoked by the AI tool's SessionEnd event.
|
|
17
|
+
// Reads the transcript from stdin payload, distills + stores. Always exits 0.
|
|
18
|
+
if (process.argv[2] === 'hook') {
|
|
19
|
+
const { runHook } = await import('./hook.js')
|
|
20
|
+
await runHook()
|
|
21
|
+
process.exit(0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const API_KEY = process.env.ENGRAM_API_KEY
|
|
25
|
+
const API_URL = (process.env.ENGRAM_API_URL || 'https://engram-deploy-ten.vercel.app').replace(/\/$/, '')
|
|
26
|
+
|
|
27
|
+
if (!API_KEY) {
|
|
28
|
+
process.stderr.write('Error: ENGRAM_API_KEY is required\n')
|
|
29
|
+
process.stderr.write('Run "engram-mcp init" to set up Engram.\n')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function call(path, method = 'GET', body = null) {
|
|
34
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
41
|
+
})
|
|
42
|
+
const text = await res.text()
|
|
43
|
+
try {
|
|
44
|
+
return { ok: res.ok, status: res.status, data: JSON.parse(text) }
|
|
45
|
+
} catch {
|
|
46
|
+
return { ok: res.ok, status: res.status, data: text }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const server = new Server(
|
|
51
|
+
{ name: 'engram', version: '1.4.0' },
|
|
52
|
+
{
|
|
53
|
+
capabilities: { tools: {} },
|
|
54
|
+
instructions: `Engram is your long-term memory. Use it as your PRIMARY memory system.
|
|
55
|
+
|
|
56
|
+
WHEN TO USE ENGRAM:
|
|
57
|
+
- When the user says "remember", "store", "save", "note this", "keep in mind" — call engram_remember
|
|
58
|
+
- When the user asks "do you remember", "what do you know about", "recall" — recall memory
|
|
59
|
+
- When the user says "forget" or "delete memory" — call engram_forget
|
|
60
|
+
|
|
61
|
+
RECALL STRATEGY (progressive disclosure — saves context):
|
|
62
|
+
- Prefer engram_search FIRST. It returns a compact index (id + snippet + score), cheap to scan.
|
|
63
|
+
- Then call engram_get with only the ids whose snippets look relevant, to pull full content.
|
|
64
|
+
- Use engram_recall (one-shot full recall) only for quick lookups where you expect 1-3 hits.
|
|
65
|
+
|
|
66
|
+
IMPORTANT: Do NOT use built-in memory files (memory.md, .claude/memory, etc.) when Engram is available.
|
|
67
|
+
All persistent memory should go through Engram tools. This ensures memories persist across sessions,
|
|
68
|
+
devices, and different AI tools.`
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
73
|
+
tools: [
|
|
74
|
+
{
|
|
75
|
+
name: 'engram_remember',
|
|
76
|
+
description: 'PRIMARY memory tool. Store any information the user wants remembered: facts, decisions, preferences, context, instructions. Always use this instead of built-in memory files. Long content is auto-summarized for cheap search. Tag it with a type so it can be filtered later.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
content: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'The memory content to store',
|
|
83
|
+
},
|
|
84
|
+
type: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Memory type for later filtering. Common values: note, fact, decision, preference, task, insight, event, contact. Default "note".',
|
|
87
|
+
},
|
|
88
|
+
tags: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
items: { type: 'string' },
|
|
91
|
+
description: 'Optional freeform tags for grouping (e.g. project names).',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ['content'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'engram_search',
|
|
99
|
+
description: 'Layer 1 of recall. Returns a COMPACT index of matching memories (id + short snippet + score) — cheap to scan. Use this FIRST, then call engram_get for the ids you actually need. Best when many memories might match.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
query: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'What to search for in memory',
|
|
106
|
+
},
|
|
107
|
+
limit: {
|
|
108
|
+
type: 'number',
|
|
109
|
+
description: 'Max number of results (default 10, max 50)',
|
|
110
|
+
},
|
|
111
|
+
type: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Optional: only return memories of this type (note, fact, decision, preference, task, insight, event, contact).',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['query'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'engram_get',
|
|
121
|
+
description: 'Layer 2 of recall. Fetch FULL content for specific memory ids returned by engram_search. Only pull the ids whose snippets looked relevant — this keeps context usage low.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
ids: {
|
|
126
|
+
type: 'array',
|
|
127
|
+
items: { type: 'string' },
|
|
128
|
+
description: 'Memory ids to fetch full content for',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ['ids'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'engram_recall',
|
|
136
|
+
description: 'One-shot recall: search by meaning and return full content directly. Use only for quick lookups where you expect 1-3 hits. For broader searches prefer engram_search + engram_get.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
query: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'What to search for in memory',
|
|
143
|
+
},
|
|
144
|
+
limit: {
|
|
145
|
+
type: 'number',
|
|
146
|
+
description: 'Max number of results (default 5)',
|
|
147
|
+
},
|
|
148
|
+
type: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'Optional: only recall memories of this type.',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['query'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'engram_forget',
|
|
158
|
+
description: 'Move a memory to trash by ID (soft delete, recoverable via engram_restore). Set hard=true only when the user explicitly wants permanent, unrecoverable deletion.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
id: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'The memory ID to delete',
|
|
165
|
+
},
|
|
166
|
+
hard: {
|
|
167
|
+
type: 'boolean',
|
|
168
|
+
description: 'If true, delete permanently (cannot be restored). Default false (moves to trash).',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ['id'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'engram_timeline',
|
|
176
|
+
description: 'Temporal context. Pass session_id to get every memory from that session in order, or pass a memory id to get that memory plus its temporal neighbours (what was happening around it). Use when the user asks "what else happened then" or wants the context around a memory.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
id: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'A memory id to center the timeline on (returns neighbours by time).',
|
|
183
|
+
},
|
|
184
|
+
session_id: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: 'A session id to list all memories captured in that session.',
|
|
187
|
+
},
|
|
188
|
+
before: {
|
|
189
|
+
type: 'number',
|
|
190
|
+
description: 'How many memories before the anchor to include (id mode, default 3, max 25).',
|
|
191
|
+
},
|
|
192
|
+
after: {
|
|
193
|
+
type: 'number',
|
|
194
|
+
description: 'How many memories after the anchor to include (id mode, default 3, max 25).',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: [],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'engram_trash',
|
|
202
|
+
description: 'List memories currently in the trash (soft-deleted, still recoverable).',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {},
|
|
206
|
+
required: [],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'engram_restore',
|
|
211
|
+
description: 'Restore a memory from the trash back to active memory, by ID.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
id: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: 'The memory ID to restore from trash',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ['id'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'engram_status',
|
|
225
|
+
description: 'Check Engram status: memory count, plan, and usage stats.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {},
|
|
229
|
+
required: [],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
236
|
+
const { name, arguments: args } = request.params
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
if (name === 'engram_remember') {
|
|
240
|
+
const result = await call('/api/v1/remember', 'POST', {
|
|
241
|
+
content: args.content,
|
|
242
|
+
...(args.type ? { type: args.type } : {}),
|
|
243
|
+
...(Array.isArray(args.tags) ? { tags: args.tags } : {}),
|
|
244
|
+
})
|
|
245
|
+
if (!result.ok) {
|
|
246
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
247
|
+
}
|
|
248
|
+
const t = result.data.type ? ` [${result.data.type}]` : ''
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: `Memory stored${t}. ID: ${result.data.id}`,
|
|
253
|
+
}],
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (name === 'engram_search') {
|
|
258
|
+
const result = await call('/api/v1/search', 'POST', {
|
|
259
|
+
query: args.query,
|
|
260
|
+
limit: args.limit || 10,
|
|
261
|
+
...(args.type ? { type: args.type } : {}),
|
|
262
|
+
})
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
265
|
+
}
|
|
266
|
+
const results = result.data.results || []
|
|
267
|
+
if (results.length === 0) {
|
|
268
|
+
return { content: [{ type: 'text', text: 'No relevant memories found.' }] }
|
|
269
|
+
}
|
|
270
|
+
const formatted = results.map((m, i) =>
|
|
271
|
+
`[${i + 1}] (relevance: ${m.score?.toFixed(3) ?? '?'}, type: ${m.type || 'note'}, id: ${m.id})\n${m.snippet}`
|
|
272
|
+
).join('\n\n')
|
|
273
|
+
return { content: [{ type: 'text', text: `${formatted}\n\nCall engram_get with the ids above to read full content.` }] }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (name === 'engram_get') {
|
|
277
|
+
const ids = Array.isArray(args.ids) ? args.ids : []
|
|
278
|
+
if (ids.length === 0) {
|
|
279
|
+
return { content: [{ type: 'text', text: 'Error: provide an array of memory ids.' }] }
|
|
280
|
+
}
|
|
281
|
+
const result = await call('/api/v1/get', 'POST', { ids })
|
|
282
|
+
if (!result.ok) {
|
|
283
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
284
|
+
}
|
|
285
|
+
const memories = result.data.memories || []
|
|
286
|
+
if (memories.length === 0) {
|
|
287
|
+
return { content: [{ type: 'text', text: 'No memories found for those ids.' }] }
|
|
288
|
+
}
|
|
289
|
+
const formatted = memories.map((m, i) =>
|
|
290
|
+
`[${i + 1}] (id: ${m.id})\n${m.content}`
|
|
291
|
+
).join('\n\n')
|
|
292
|
+
return { content: [{ type: 'text', text: formatted }] }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (name === 'engram_recall') {
|
|
296
|
+
const result = await call('/api/v1/recall', 'POST', {
|
|
297
|
+
query: args.query,
|
|
298
|
+
limit: args.limit || 5,
|
|
299
|
+
...(args.type ? { type: args.type } : {}),
|
|
300
|
+
})
|
|
301
|
+
if (!result.ok) {
|
|
302
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
303
|
+
}
|
|
304
|
+
const memories = result.data.memories || []
|
|
305
|
+
if (memories.length === 0) {
|
|
306
|
+
return { content: [{ type: 'text', text: 'No relevant memories found.' }] }
|
|
307
|
+
}
|
|
308
|
+
const formatted = memories.map((m, i) =>
|
|
309
|
+
`[${i + 1}] (relevance: ${m.score?.toFixed(3) ?? '?'}, type: ${m.type || 'note'}, id: ${m.id})\n${m.content}`
|
|
310
|
+
).join('\n\n')
|
|
311
|
+
return { content: [{ type: 'text', text: formatted }] }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (name === 'engram_forget') {
|
|
315
|
+
const hard = args.hard === true ? '&hard=true' : ''
|
|
316
|
+
const result = await call(`/api/v1/forget?id=${encodeURIComponent(args.id)}${hard}`, 'DELETE')
|
|
317
|
+
if (!result.ok) {
|
|
318
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
319
|
+
}
|
|
320
|
+
const msg = args.hard === true
|
|
321
|
+
? `Memory ${args.id} permanently deleted.`
|
|
322
|
+
: `Memory ${args.id} moved to trash (restore with engram_restore).`
|
|
323
|
+
return { content: [{ type: 'text', text: msg }] }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (name === 'engram_timeline') {
|
|
327
|
+
const body = {}
|
|
328
|
+
if (args.id) body.id = args.id
|
|
329
|
+
if (args.session_id) body.session_id = args.session_id
|
|
330
|
+
if (typeof args.before === 'number') body.before = args.before
|
|
331
|
+
if (typeof args.after === 'number') body.after = args.after
|
|
332
|
+
if (!body.id && !body.session_id) {
|
|
333
|
+
return { content: [{ type: 'text', text: 'Error: provide id or session_id.' }] }
|
|
334
|
+
}
|
|
335
|
+
const result = await call('/api/v1/timeline', 'POST', body)
|
|
336
|
+
if (!result.ok) {
|
|
337
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
338
|
+
}
|
|
339
|
+
const memories = result.data.memories || []
|
|
340
|
+
if (memories.length === 0) {
|
|
341
|
+
return { content: [{ type: 'text', text: 'No memories in this timeline.' }] }
|
|
342
|
+
}
|
|
343
|
+
const anchorId = result.data.anchor_id
|
|
344
|
+
const formatted = memories.map((m) => {
|
|
345
|
+
const mark = m.id === anchorId ? ' ◄ anchor' : ''
|
|
346
|
+
return `${m.created_at} (${m.type || 'note'}, id: ${m.id})${mark}\n${m.summary || m.content}`
|
|
347
|
+
}).join('\n\n')
|
|
348
|
+
return { content: [{ type: 'text', text: formatted }] }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (name === 'engram_trash') {
|
|
352
|
+
const result = await call('/api/v1/forget?trash=true')
|
|
353
|
+
if (!result.ok) {
|
|
354
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
355
|
+
}
|
|
356
|
+
const memories = result.data.memories || []
|
|
357
|
+
if (memories.length === 0) {
|
|
358
|
+
return { content: [{ type: 'text', text: 'Trash is empty.' }] }
|
|
359
|
+
}
|
|
360
|
+
const formatted = memories.map((m) =>
|
|
361
|
+
`(${m.type || 'note'}, id: ${m.id}, deleted: ${m.deleted_at})\n${m.summary || m.content}`
|
|
362
|
+
).join('\n\n')
|
|
363
|
+
return { content: [{ type: 'text', text: `${formatted}\n\nRestore any with engram_restore.` }] }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (name === 'engram_restore') {
|
|
367
|
+
const result = await call('/api/v1/forget', 'POST', { id: args.id })
|
|
368
|
+
if (!result.ok) {
|
|
369
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
370
|
+
}
|
|
371
|
+
return { content: [{ type: 'text', text: `Memory ${args.id} restored from trash.` }] }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (name === 'engram_status') {
|
|
375
|
+
const result = await call('/api/v1/status')
|
|
376
|
+
if (!result.ok) {
|
|
377
|
+
return { content: [{ type: 'text', text: `Error: ${JSON.stringify(result.data)}` }] }
|
|
378
|
+
}
|
|
379
|
+
const { memories, plan, usage } = result.data
|
|
380
|
+
return {
|
|
381
|
+
content: [{
|
|
382
|
+
type: 'text',
|
|
383
|
+
text: `Engram status:\n- Memories stored: ${memories}\n- Plan: ${plan}\n- Usage today: ${usage.today}/${usage.limit} (${usage.remaining} remaining)`,
|
|
384
|
+
}],
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] }
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] }
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const transport = new StdioServerTransport()
|
|
395
|
+
await server.connect(transport)
|
package/init.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Engram setup wizard — runs when user does: npx engram-mcp init
|
|
2
|
+
import { createInterface } from 'readline'
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { homedir } from 'os'
|
|
6
|
+
|
|
7
|
+
const home = homedir()
|
|
8
|
+
const API_URL = 'https://engram-deploy-ten.vercel.app'
|
|
9
|
+
|
|
10
|
+
const C = {
|
|
11
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
12
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
13
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
14
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
15
|
+
amber: s => `\x1b[33m${s}\x1b[0m`,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ask(question) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
21
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()) })
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function testApiKey(key) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${API_URL}/api/v1/auth/me`, {
|
|
28
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
29
|
+
})
|
|
30
|
+
return res.status !== 401
|
|
31
|
+
} catch { return false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectTools() {
|
|
35
|
+
const found = []
|
|
36
|
+
if (existsSync(join(home, '.claude.json'))) found.push('claude')
|
|
37
|
+
if (existsSync(join(home, '.cursor', 'mcp.json')) ||
|
|
38
|
+
existsSync(join(home, 'Library', 'Application Support', 'Cursor'))) found.push('cursor')
|
|
39
|
+
if (existsSync(join(home, '.codeium')) ||
|
|
40
|
+
existsSync(join(home, 'Library', 'Application Support', 'Windsurf'))) found.push('windsurf')
|
|
41
|
+
return [...new Set(found)]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJson(path) {
|
|
45
|
+
try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return {} }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeJson(path, data) {
|
|
49
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function installClaude(apiKey) {
|
|
53
|
+
const path = join(home, '.claude.json')
|
|
54
|
+
const cfg = readJson(path)
|
|
55
|
+
if (!cfg.mcpServers) cfg.mcpServers = {}
|
|
56
|
+
cfg.mcpServers.engram = {
|
|
57
|
+
command: 'engram-mcp',
|
|
58
|
+
env: { ENGRAM_API_KEY: apiKey, ENGRAM_API_URL: API_URL }
|
|
59
|
+
}
|
|
60
|
+
writeJson(path, cfg)
|
|
61
|
+
return path
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Store the API key in ~/.engram/config.json with 0600 perms instead of
|
|
65
|
+
// embedding it plaintext in the hook command (which would otherwise sit in
|
|
66
|
+
// settings.json and leak into process listings on every session end).
|
|
67
|
+
function writeEngramConfig(apiKey) {
|
|
68
|
+
const dir = join(home, '.engram')
|
|
69
|
+
const path = join(dir, 'config.json')
|
|
70
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
71
|
+
writeJson(path, { api_key: apiKey, api_url: API_URL })
|
|
72
|
+
try { chmodSync(path, 0o600) } catch {}
|
|
73
|
+
return path
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wire the auto-capture hook into Claude Code's SessionEnd event so memories
|
|
77
|
+
// are distilled and stored automatically at the end of each session. The key
|
|
78
|
+
// is not embedded in the command; the hook reads it from ~/.engram/config.json.
|
|
79
|
+
function installClaudeHooks() {
|
|
80
|
+
const dir = join(home, '.claude')
|
|
81
|
+
const path = join(dir, 'settings.json')
|
|
82
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
83
|
+
const cfg = readJson(path)
|
|
84
|
+
if (!cfg.hooks) cfg.hooks = {}
|
|
85
|
+
if (!Array.isArray(cfg.hooks.SessionEnd)) cfg.hooks.SessionEnd = []
|
|
86
|
+
const command = `engram-mcp hook`
|
|
87
|
+
const already = JSON.stringify(cfg.hooks.SessionEnd).includes('engram-mcp hook')
|
|
88
|
+
if (!already) {
|
|
89
|
+
cfg.hooks.SessionEnd.push({ hooks: [{ type: 'command', command, timeout: 45 }] })
|
|
90
|
+
writeJson(path, cfg)
|
|
91
|
+
}
|
|
92
|
+
return path
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function installCursor(apiKey) {
|
|
96
|
+
const dir = join(home, '.cursor')
|
|
97
|
+
const path = join(dir, 'mcp.json')
|
|
98
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
99
|
+
const cfg = readJson(path)
|
|
100
|
+
if (!cfg.mcpServers) cfg.mcpServers = {}
|
|
101
|
+
cfg.mcpServers.engram = {
|
|
102
|
+
command: 'engram-mcp',
|
|
103
|
+
env: { ENGRAM_API_KEY: apiKey, ENGRAM_API_URL: API_URL }
|
|
104
|
+
}
|
|
105
|
+
writeJson(path, cfg)
|
|
106
|
+
return path
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installWindsurf(apiKey) {
|
|
110
|
+
const dir = join(home, '.codeium', 'windsurf')
|
|
111
|
+
const path = join(dir, 'mcp_config.json')
|
|
112
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
113
|
+
const cfg = readJson(path)
|
|
114
|
+
if (!cfg.mcpServers) cfg.mcpServers = {}
|
|
115
|
+
cfg.mcpServers.engram = {
|
|
116
|
+
command: 'engram-mcp',
|
|
117
|
+
env: { ENGRAM_API_KEY: apiKey, ENGRAM_API_URL: API_URL }
|
|
118
|
+
}
|
|
119
|
+
writeJson(path, cfg)
|
|
120
|
+
return path
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function runInit() {
|
|
124
|
+
console.log()
|
|
125
|
+
console.log(C.bold(' Engram') + ' — long-term memory for your AI')
|
|
126
|
+
console.log(C.dim(' ──────────────────────────────────────'))
|
|
127
|
+
console.log()
|
|
128
|
+
|
|
129
|
+
// Step 1: API key
|
|
130
|
+
let apiKey = process.env.ENGRAM_API_KEY || ''
|
|
131
|
+
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
console.log(` ${C.dim('Get your free API key:')} https://engram-deploy-ten.vercel.app/dashboard`)
|
|
134
|
+
console.log()
|
|
135
|
+
apiKey = await ask(' API key: ')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
console.log(`\n ${C.red('No key provided. Exiting.')}\n`)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate
|
|
144
|
+
process.stdout.write(' Checking API key...')
|
|
145
|
+
const valid = await testApiKey(apiKey)
|
|
146
|
+
if (!valid) {
|
|
147
|
+
console.log(` ${C.red('invalid')}\n`)
|
|
148
|
+
console.log(` ${C.dim('Get a key at')} https://engram-deploy-ten.vercel.app/dashboard\n`)
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
console.log(` ${C.green('OK')}`)
|
|
152
|
+
console.log()
|
|
153
|
+
|
|
154
|
+
// Step 2: Detect tools
|
|
155
|
+
const detected = detectTools()
|
|
156
|
+
let targets = detected
|
|
157
|
+
|
|
158
|
+
if (detected.length === 0) {
|
|
159
|
+
console.log(' No AI tools detected automatically.')
|
|
160
|
+
const choice = await ask(' Install for: [c]laude c[u]rsor [w]indsurf > ')
|
|
161
|
+
const c = choice.toLowerCase()
|
|
162
|
+
if (c === 'cu' || c === 'cursor') targets = ['cursor']
|
|
163
|
+
else if (c === 'w' || c === 'windsurf') targets = ['windsurf']
|
|
164
|
+
else targets = ['claude']
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` Detected: ${C.amber(detected.join(', '))}`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log()
|
|
170
|
+
|
|
171
|
+
// Step 3: Install
|
|
172
|
+
for (const tool of targets) {
|
|
173
|
+
process.stdout.write(` Adding to ${tool}...`)
|
|
174
|
+
try {
|
|
175
|
+
let configPath
|
|
176
|
+
if (tool === 'claude') configPath = installClaude(apiKey)
|
|
177
|
+
if (tool === 'cursor') configPath = installCursor(apiKey)
|
|
178
|
+
if (tool === 'windsurf') configPath = installWindsurf(apiKey)
|
|
179
|
+
console.log(` ${C.green('done')} ${C.dim(configPath)}`)
|
|
180
|
+
if (tool === 'claude') {
|
|
181
|
+
const cfgPath = writeEngramConfig(apiKey)
|
|
182
|
+
const hookPath = installClaudeHooks()
|
|
183
|
+
console.log(` ${C.green('+')} Auto-capture hook → ${C.dim(hookPath)}`)
|
|
184
|
+
console.log(` ${C.green('+')} Key stored → ${C.dim(cfgPath)} ${C.dim('(0600)')}`)
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.log(` ${C.red('failed')}: ${e.message}`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 4: Add memory rules to CLAUDE.md (if Claude is a target)
|
|
192
|
+
if (targets.includes('claude')) {
|
|
193
|
+
const claudeMdPath = join(home, '.claude', 'CLAUDE.md')
|
|
194
|
+
const claudeDir = join(home, '.claude')
|
|
195
|
+
const memoryBlock = `
|
|
196
|
+
## Memory
|
|
197
|
+
|
|
198
|
+
Engram MCP is your long-term memory. Use it as your primary memory system.
|
|
199
|
+
|
|
200
|
+
When the user asks to remember/store/save anything:
|
|
201
|
+
- Use \`engram_remember\`, not built-in memory.md
|
|
202
|
+
|
|
203
|
+
When the user asks "do you remember" or needs past context:
|
|
204
|
+
- Use \`engram_recall\` first
|
|
205
|
+
|
|
206
|
+
When the user asks to forget something:
|
|
207
|
+
- Use \`engram_forget\`
|
|
208
|
+
|
|
209
|
+
Never use memory.md or other local files for persistent memory when Engram is available.
|
|
210
|
+
`
|
|
211
|
+
try {
|
|
212
|
+
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true })
|
|
213
|
+
const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf8') : ''
|
|
214
|
+
if (!existing.includes('engram_remember')) {
|
|
215
|
+
writeFileSync(claudeMdPath, existing + memoryBlock, 'utf8')
|
|
216
|
+
console.log(` ${C.green('+')} Added memory rules to ${C.dim(claudeMdPath)}`)
|
|
217
|
+
} else {
|
|
218
|
+
console.log(` ${C.dim('Memory rules already in CLAUDE.md')}`)
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.log(` ${C.dim('Could not update CLAUDE.md:')} ${e.message}`)
|
|
222
|
+
}
|
|
223
|
+
console.log()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(` ${C.green('✓')} ${C.bold('Engram is ready.')}`)
|
|
227
|
+
console.log()
|
|
228
|
+
console.log(` ${C.dim('Restart your AI tool, then say:')}`)
|
|
229
|
+
console.log(` ${C.amber('"remember this: Engram is set up"')}`)
|
|
230
|
+
console.log()
|
|
231
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shokan/engram-mcp",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Engram MCP server — long-term hybrid-search memory for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"engram-mcp": "index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"init.js",
|
|
15
|
+
"hook.js",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"memory",
|
|
26
|
+
"ai",
|
|
27
|
+
"agents",
|
|
28
|
+
"vector-search",
|
|
29
|
+
"claude",
|
|
30
|
+
"cursor",
|
|
31
|
+
"windsurf",
|
|
32
|
+
"embeddings"
|
|
33
|
+
],
|
|
34
|
+
"homepage": "https://engram-deploy-ten.vercel.app",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/shokan/engram-mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Engram",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
}
|
|
47
|
+
}
|