openviking-claude-code 0.1.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/README.md +164 -0
- package/bin/setup.cjs +238 -0
- package/hooks/auto-capture.cjs +201 -0
- package/hooks/auto-recall.cjs +192 -0
- package/hooks/mcp-server.cjs +314 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# claude-openviking
|
|
2
|
+
|
|
3
|
+
**OpenViking long-term memory integration for Claude Code.**
|
|
4
|
+
|
|
5
|
+
> Give Claude Code persistent memory across sessions — auto-recall past context, auto-capture new learnings.
|
|
6
|
+
|
|
7
|
+
[English](#english) | [中文](#中文)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<a name="english"></a>
|
|
12
|
+
|
|
13
|
+
## English
|
|
14
|
+
|
|
15
|
+
### What is this?
|
|
16
|
+
|
|
17
|
+
`claude-openviking` connects Claude Code to an [OpenViking](https://github.com/nicepkg/openviking) server for long-term memory. Once installed:
|
|
18
|
+
|
|
19
|
+
- **Auto-recall**: Every message you send triggers a memory search — relevant past context is injected automatically
|
|
20
|
+
- **Auto-capture**: Every Claude response is silently sent to OpenViking for memory extraction
|
|
21
|
+
- **MCP tools**: `memory_recall`, `memory_store`, `memory_forget` available for manual use
|
|
22
|
+
|
|
23
|
+
### Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx claude-openviking
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You'll be asked for your OpenViking server URL and API key. That's it.
|
|
30
|
+
|
|
31
|
+
### Non-Interactive
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx claude-openviking --url http://your-server:1933 --key your-api-key
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### What Gets Installed
|
|
38
|
+
|
|
39
|
+
| Component | Location | Purpose |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `auto-recall.cjs` | `~/.claude/hooks/openviking/` | Search memories on every `UserPromptSubmit` |
|
|
42
|
+
| `auto-capture.cjs` | `~/.claude/hooks/openviking/` | Capture conversation on every `Stop` (async) |
|
|
43
|
+
| `mcp-server.cjs` | `~/.claude/hooks/openviking/` | MCP stdio server for memory tools |
|
|
44
|
+
| Hook config | `~/.claude/settings.json` | Registers hooks with Claude Code |
|
|
45
|
+
| MCP config | `.mcp.json` (current directory) | Registers MCP server |
|
|
46
|
+
|
|
47
|
+
### How It Works
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
You send a message
|
|
51
|
+
↓
|
|
52
|
+
auto-recall hook fires → searches OpenViking → injects relevant memories
|
|
53
|
+
↓
|
|
54
|
+
Claude sees your message + memories → responds
|
|
55
|
+
↓
|
|
56
|
+
auto-capture hook fires (async) → sends conversation to OpenViking
|
|
57
|
+
↓
|
|
58
|
+
OpenViking extracts and stores memories for future recall
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Configuration
|
|
62
|
+
|
|
63
|
+
All settings use environment variables (with defaults from setup):
|
|
64
|
+
|
|
65
|
+
| Variable | Description |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `OPENVIKING_BASE_URL` | OpenViking server URL |
|
|
68
|
+
| `OPENVIKING_API_KEY` | API key for authentication |
|
|
69
|
+
| `OPENVIKING_AGENT_ID` | Agent identifier (default: `claude-code`) |
|
|
70
|
+
|
|
71
|
+
### MCP Tools
|
|
72
|
+
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `memory_recall` | Search memories by query |
|
|
76
|
+
| `memory_store` | Store text as memory |
|
|
77
|
+
| `memory_forget` | Delete a memory by URI or search query |
|
|
78
|
+
|
|
79
|
+
### Uninstall
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx claude-openviking --uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Removes hooks and settings entries. MCP config (`.mcp.json`) left for manual cleanup.
|
|
86
|
+
|
|
87
|
+
### Author
|
|
88
|
+
|
|
89
|
+
**Bill Zhao** — [LinkedIn](https://www.linkedin.com/in/billzhaodi/) | [GitHub](https://github.com/billzhao9)
|
|
90
|
+
|
|
91
|
+
### Credits
|
|
92
|
+
|
|
93
|
+
- [OpenViking](https://github.com/nicepkg/openviking) — Long-term memory backend
|
|
94
|
+
- [Claude Code](https://claude.com/claude-code) — The AI coding assistant
|
|
95
|
+
|
|
96
|
+
### License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<a name="中文"></a>
|
|
103
|
+
|
|
104
|
+
## 中文
|
|
105
|
+
|
|
106
|
+
### 这是什么?
|
|
107
|
+
|
|
108
|
+
`claude-openviking` 为 Claude Code 接入 [OpenViking](https://github.com/nicepkg/openviking) 长期记忆服务。安装后:
|
|
109
|
+
|
|
110
|
+
- **自动召回**:每条消息自动搜索记忆,注入相关上下文
|
|
111
|
+
- **自动捕获**:每次回复后静默发送对话到 OpenViking 提取记忆
|
|
112
|
+
- **MCP 工具**:`memory_recall`、`memory_store`、`memory_forget` 可手动调用
|
|
113
|
+
|
|
114
|
+
### 快速安装
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx claude-openviking
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
输入 OpenViking 服务器地址和 API key 即可。
|
|
121
|
+
|
|
122
|
+
### 非交互安装
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx claude-openviking --url http://your-server:1933 --key your-api-key
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 安装了什么
|
|
129
|
+
|
|
130
|
+
| 组件 | 位置 | 功能 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `auto-recall.cjs` | `~/.claude/hooks/openviking/` | 每条消息自动搜索记忆 |
|
|
133
|
+
| `auto-capture.cjs` | `~/.claude/hooks/openviking/` | 每次回复后异步捕获对话 |
|
|
134
|
+
| `mcp-server.cjs` | `~/.claude/hooks/openviking/` | MCP 工具服务 |
|
|
135
|
+
| Hook 配置 | `~/.claude/settings.json` | 注册 hooks |
|
|
136
|
+
| MCP 配置 | `.mcp.json`(当前目录) | 注册 MCP server |
|
|
137
|
+
|
|
138
|
+
### 工作流程
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
你发一条消息
|
|
142
|
+
↓
|
|
143
|
+
auto-recall 触发 → 搜索 OpenViking → 注入相关记忆
|
|
144
|
+
↓
|
|
145
|
+
Claude 看到你的消息 + 记忆 → 回复
|
|
146
|
+
↓
|
|
147
|
+
auto-capture 触发(异步)→ 对话发到 OpenViking
|
|
148
|
+
↓
|
|
149
|
+
OpenViking 提取并存储记忆,供未来召回
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 卸载
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx claude-openviking --uninstall
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 作者
|
|
159
|
+
|
|
160
|
+
**Bill Zhao** — [LinkedIn](https://www.linkedin.com/in/billzhaodi/) | [GitHub](https://github.com/billzhao9)
|
|
161
|
+
|
|
162
|
+
### 协议
|
|
163
|
+
|
|
164
|
+
MIT
|
package/bin/setup.cjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* claude-openviking setup script
|
|
4
|
+
*
|
|
5
|
+
* Installs hooks + MCP server config for Claude Code.
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx claude-openviking # interactive setup
|
|
8
|
+
* npx claude-openviking --url http://... --key xxx # non-interactive
|
|
9
|
+
* npx claude-openviking --uninstall # remove
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
const readline = require("readline");
|
|
16
|
+
|
|
17
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
18
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, "hooks", "openviking");
|
|
19
|
+
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
20
|
+
const PKG_HOOKS_DIR = path.join(__dirname, "..", "hooks");
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const isUninstall = args.includes("--uninstall");
|
|
24
|
+
const urlArg = args[args.indexOf("--url") + 1];
|
|
25
|
+
const keyArg = args[args.indexOf("--key") + 1];
|
|
26
|
+
const agentArg = args[args.indexOf("--agent") + 1];
|
|
27
|
+
|
|
28
|
+
async function ask(question) {
|
|
29
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
30
|
+
return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeJson(filePath, data) {
|
|
42
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Uninstall ---
|
|
46
|
+
|
|
47
|
+
function uninstall() {
|
|
48
|
+
console.log("🗑️ Removing claude-openviking...\n");
|
|
49
|
+
|
|
50
|
+
// Remove hooks dir
|
|
51
|
+
if (fs.existsSync(HOOKS_DIR)) {
|
|
52
|
+
fs.rmSync(HOOKS_DIR, { recursive: true });
|
|
53
|
+
console.log(" ✅ Removed hooks directory");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove hook entries from settings.json
|
|
57
|
+
const settings = readJson(SETTINGS_PATH);
|
|
58
|
+
if (settings?.hooks) {
|
|
59
|
+
let changed = false;
|
|
60
|
+
|
|
61
|
+
for (const event of ["UserPromptSubmit", "Stop"]) {
|
|
62
|
+
if (Array.isArray(settings.hooks[event])) {
|
|
63
|
+
const before = settings.hooks[event].length;
|
|
64
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
65
|
+
(entry) => !JSON.stringify(entry).includes("openviking")
|
|
66
|
+
);
|
|
67
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
68
|
+
if (settings.hooks[event]?.length !== before) changed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (changed) {
|
|
73
|
+
writeJson(SETTINGS_PATH, settings);
|
|
74
|
+
console.log(" ✅ Removed hooks from settings.json");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log("\n✅ Uninstalled. MCP server config (.mcp.json) left untouched — remove manually if needed.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Install ---
|
|
83
|
+
|
|
84
|
+
async function install() {
|
|
85
|
+
console.log("🦞 claude-openviking setup\n");
|
|
86
|
+
console.log("This will install OpenViking memory integration for Claude Code:\n");
|
|
87
|
+
console.log(" • Auto-recall: search memories before every message");
|
|
88
|
+
console.log(" • Auto-capture: save conversation to OpenViking after every reply");
|
|
89
|
+
console.log(" • MCP tools: memory_recall, memory_store, memory_forget\n");
|
|
90
|
+
|
|
91
|
+
// Get config
|
|
92
|
+
let baseUrl = urlArg;
|
|
93
|
+
let apiKey = keyArg;
|
|
94
|
+
let agentId = agentArg || "claude-code";
|
|
95
|
+
|
|
96
|
+
if (!baseUrl) {
|
|
97
|
+
baseUrl = await ask("OpenViking server URL (e.g. http://127.0.0.1:1933): ");
|
|
98
|
+
}
|
|
99
|
+
if (!baseUrl) {
|
|
100
|
+
console.log("❌ URL is required. Aborting.");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
apiKey = await ask("OpenViking API key (press Enter to skip): ");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!agentArg) {
|
|
109
|
+
const ans = await ask(`Agent ID [${agentId}]: `);
|
|
110
|
+
if (ans) agentId = ans;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log("\n📦 Installing...\n");
|
|
114
|
+
|
|
115
|
+
// 1. Copy hooks
|
|
116
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
117
|
+
for (const file of ["auto-recall.cjs", "auto-capture.cjs", "mcp-server.cjs"]) {
|
|
118
|
+
const src = path.join(PKG_HOOKS_DIR, file);
|
|
119
|
+
const dst = path.join(HOOKS_DIR, file);
|
|
120
|
+
fs.copyFileSync(src, dst);
|
|
121
|
+
console.log(` ✅ Copied ${file}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Patch hooks files with user's config
|
|
125
|
+
for (const file of ["auto-recall.cjs", "auto-capture.cjs", "mcp-server.cjs"]) {
|
|
126
|
+
const filePath = path.join(HOOKS_DIR, file);
|
|
127
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
128
|
+
// Replace default values
|
|
129
|
+
content = content.replace(
|
|
130
|
+
/const OPENVIKING_BASE_URL = process\.env\.OPENVIKING_BASE_URL \|\| "[^"]*"/,
|
|
131
|
+
`const OPENVIKING_BASE_URL = process.env.OPENVIKING_BASE_URL || ${JSON.stringify(baseUrl)}`
|
|
132
|
+
);
|
|
133
|
+
content = content.replace(
|
|
134
|
+
/const OPENVIKING_API_KEY = process\.env\.OPENVIKING_API_KEY \|\| "[^"]*"/,
|
|
135
|
+
`const OPENVIKING_API_KEY = process.env.OPENVIKING_API_KEY || ${JSON.stringify(apiKey || "")}`
|
|
136
|
+
);
|
|
137
|
+
content = content.replace(
|
|
138
|
+
/const OPENVIKING_AGENT_ID = process\.env\.OPENVIKING_AGENT_ID \|\| "[^"]*"/,
|
|
139
|
+
`const OPENVIKING_AGENT_ID = process.env.OPENVIKING_AGENT_ID || ${JSON.stringify(agentId)}`
|
|
140
|
+
);
|
|
141
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
142
|
+
}
|
|
143
|
+
console.log(" ✅ Configured OpenViking connection");
|
|
144
|
+
|
|
145
|
+
// 3. Update settings.json (add hooks)
|
|
146
|
+
const settings = readJson(SETTINGS_PATH) || {};
|
|
147
|
+
settings.hooks = settings.hooks || {};
|
|
148
|
+
|
|
149
|
+
// UserPromptSubmit hook
|
|
150
|
+
const recallHook = {
|
|
151
|
+
matcher: "",
|
|
152
|
+
hooks: [{
|
|
153
|
+
type: "command",
|
|
154
|
+
command: `node "${path.join(HOOKS_DIR, "auto-recall.cjs")}"`,
|
|
155
|
+
timeout: 6000,
|
|
156
|
+
statusMessage: "Recalling OpenViking memories..."
|
|
157
|
+
}]
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Stop hook
|
|
161
|
+
const captureHook = {
|
|
162
|
+
matcher: "",
|
|
163
|
+
hooks: [{
|
|
164
|
+
type: "command",
|
|
165
|
+
command: `node "${path.join(HOOKS_DIR, "auto-capture.cjs")}"`,
|
|
166
|
+
timeout: 12000,
|
|
167
|
+
async: true,
|
|
168
|
+
statusMessage: "Capturing memories to OpenViking..."
|
|
169
|
+
}]
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Add if not already present
|
|
173
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit || [];
|
|
174
|
+
if (!JSON.stringify(settings.hooks.UserPromptSubmit).includes("openviking")) {
|
|
175
|
+
settings.hooks.UserPromptSubmit.push(recallHook);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
179
|
+
if (!JSON.stringify(settings.hooks.Stop).includes("openviking")) {
|
|
180
|
+
settings.hooks.Stop.push(captureHook);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
writeJson(SETTINGS_PATH, settings);
|
|
184
|
+
console.log(" ✅ Added hooks to settings.json");
|
|
185
|
+
|
|
186
|
+
// 4. Create .mcp.json in cwd (user can move it)
|
|
187
|
+
const mcpConfig = {
|
|
188
|
+
mcpServers: {
|
|
189
|
+
openviking: {
|
|
190
|
+
command: "node",
|
|
191
|
+
args: [path.join(HOOKS_DIR, "mcp-server.cjs")],
|
|
192
|
+
env: {
|
|
193
|
+
OPENVIKING_BASE_URL: baseUrl,
|
|
194
|
+
OPENVIKING_API_KEY: apiKey || "",
|
|
195
|
+
OPENVIKING_AGENT_ID: agentId,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const mcpPath = path.join(process.cwd(), ".mcp.json");
|
|
202
|
+
if (!fs.existsSync(mcpPath)) {
|
|
203
|
+
writeJson(mcpPath, mcpConfig);
|
|
204
|
+
console.log(` ✅ Created ${mcpPath}`);
|
|
205
|
+
} else {
|
|
206
|
+
const existing = readJson(mcpPath) || {};
|
|
207
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
208
|
+
if (!existing.mcpServers.openviking) {
|
|
209
|
+
existing.mcpServers.openviking = mcpConfig.mcpServers.openviking;
|
|
210
|
+
writeJson(mcpPath, existing);
|
|
211
|
+
console.log(` ✅ Added openviking to existing ${mcpPath}`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(` ⏭️ openviking already in ${mcpPath} — skipped`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`
|
|
218
|
+
✅ Setup complete!
|
|
219
|
+
|
|
220
|
+
Restart Claude Code to activate. You'll see:
|
|
221
|
+
• "[OpenViking Auto-Recall]" context on every message
|
|
222
|
+
• Conversations auto-saved to OpenViking
|
|
223
|
+
• memory_recall / memory_store / memory_forget tools available
|
|
224
|
+
|
|
225
|
+
To uninstall: npx claude-openviking --uninstall
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Main ---
|
|
230
|
+
|
|
231
|
+
if (isUninstall) {
|
|
232
|
+
uninstall();
|
|
233
|
+
} else {
|
|
234
|
+
install().catch((err) => {
|
|
235
|
+
console.error("Setup failed:", err.message);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenViking Auto-Capture Hook for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Runs on Stop: reads the latest conversation turn from the transcript,
|
|
6
|
+
* sends it to OpenViking for memory extraction.
|
|
7
|
+
* Mirrors the afterTurn auto-capture behavior of the OpenClaw plugin.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const readline = require("readline");
|
|
12
|
+
|
|
13
|
+
const OPENVIKING_BASE_URL = process.env.OPENVIKING_BASE_URL || "http://69.5.7.190:1933";
|
|
14
|
+
const OPENVIKING_API_KEY = process.env.OPENVIKING_API_KEY || "fb56a300af1a93e580aa2ef16952cb7663fc3963f1f1f0ec26dfec4f0900f790";
|
|
15
|
+
const OPENVIKING_AGENT_ID = process.env.OPENVIKING_AGENT_ID || "claude-code";
|
|
16
|
+
const TIMEOUT_MS = 10000;
|
|
17
|
+
const CAPTURE_MAX_LENGTH = 24000;
|
|
18
|
+
const MIN_CAPTURE_LENGTH = 10;
|
|
19
|
+
|
|
20
|
+
// Skip patterns
|
|
21
|
+
const COMMAND_RE = /^\/[a-z0-9_-]{1,64}\b/i;
|
|
22
|
+
const NON_CONTENT_RE = /^[\p{P}\p{S}\s]+$/u;
|
|
23
|
+
const RELEVANT_MEMORIES_RE = /<relevant-memories>[\s\S]*?<\/relevant-memories>/gi;
|
|
24
|
+
const METADATA_BLOCK_RE = /(?:^|\n)\s*(?:Conversation info|Conversation metadata)\s*(?:\([^)]+\))?\s*:\s*```[\s\S]*?```/gi;
|
|
25
|
+
const QUESTION_ONLY_RE = /^[??\s]*$/;
|
|
26
|
+
|
|
27
|
+
// Memory trigger keywords for keyword mode (we use semantic mode by default)
|
|
28
|
+
const CJK_RE = /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
29
|
+
|
|
30
|
+
function sanitize(text) {
|
|
31
|
+
return text
|
|
32
|
+
.replace(RELEVANT_MEMORIES_RE, " ")
|
|
33
|
+
.replace(METADATA_BLOCK_RE, " ")
|
|
34
|
+
.replace(/\u0000/g, "")
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shouldCapture(text) {
|
|
40
|
+
if (!text || text.length < MIN_CAPTURE_LENGTH) return false;
|
|
41
|
+
if (text.length > CAPTURE_MAX_LENGTH) return false;
|
|
42
|
+
if (COMMAND_RE.test(text)) return false;
|
|
43
|
+
if (NON_CONTENT_RE.test(text)) return false;
|
|
44
|
+
if (QUESTION_ONLY_RE.test(text)) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ovRequest(path, options = {}) {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${OPENVIKING_BASE_URL}${path}`, {
|
|
53
|
+
...options,
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"X-API-Key": OPENVIKING_API_KEY,
|
|
57
|
+
"X-OpenViking-Agent": OPENVIKING_AGENT_ID,
|
|
58
|
+
...(options.headers || {}),
|
|
59
|
+
},
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
const payload = await res.json().catch(() => ({}));
|
|
63
|
+
return payload.result ?? payload;
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read the last N lines from a JSONL transcript file to extract
|
|
71
|
+
* the most recent user + assistant messages.
|
|
72
|
+
*/
|
|
73
|
+
async function readRecentMessages(transcriptPath, maxLines = 50) {
|
|
74
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return [];
|
|
75
|
+
|
|
76
|
+
const messages = [];
|
|
77
|
+
const lines = [];
|
|
78
|
+
|
|
79
|
+
// Read last maxLines from file
|
|
80
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: "utf8" });
|
|
81
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
82
|
+
|
|
83
|
+
for await (const line of rl) {
|
|
84
|
+
lines.push(line);
|
|
85
|
+
if (lines.length > maxLines * 2) {
|
|
86
|
+
lines.splice(0, lines.length - maxLines);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse JSONL entries and extract user/assistant messages
|
|
91
|
+
for (const line of lines.slice(-maxLines)) {
|
|
92
|
+
if (!line.trim()) continue;
|
|
93
|
+
try {
|
|
94
|
+
const entry = JSON.parse(line);
|
|
95
|
+
// Claude Code transcript format: look for message entries
|
|
96
|
+
if (entry.type === "human" || entry.type === "user") {
|
|
97
|
+
const text = extractText(entry);
|
|
98
|
+
if (text) messages.push({ role: "user", text });
|
|
99
|
+
} else if (entry.type === "assistant") {
|
|
100
|
+
const text = extractText(entry);
|
|
101
|
+
if (text) messages.push({ role: "assistant", text });
|
|
102
|
+
} else if (entry.role === "user" || entry.role === "human") {
|
|
103
|
+
const text = extractText(entry);
|
|
104
|
+
if (text) messages.push({ role: "user", text });
|
|
105
|
+
} else if (entry.role === "assistant") {
|
|
106
|
+
const text = extractText(entry);
|
|
107
|
+
if (text) messages.push({ role: "assistant", text });
|
|
108
|
+
}
|
|
109
|
+
// Also handle message wrapper format
|
|
110
|
+
if (entry.message) {
|
|
111
|
+
const msg = entry.message;
|
|
112
|
+
const role = msg.role === "human" ? "user" : msg.role;
|
|
113
|
+
if (role === "user" || role === "assistant") {
|
|
114
|
+
const text = extractText(msg);
|
|
115
|
+
if (text) messages.push({ role, text });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// skip unparseable lines
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return messages;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractText(obj) {
|
|
127
|
+
if (typeof obj.content === "string") return obj.content.trim();
|
|
128
|
+
if (typeof obj.text === "string") return obj.text.trim();
|
|
129
|
+
if (Array.isArray(obj.content)) {
|
|
130
|
+
const parts = [];
|
|
131
|
+
for (const block of obj.content) {
|
|
132
|
+
if (typeof block === "string") parts.push(block);
|
|
133
|
+
else if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
134
|
+
}
|
|
135
|
+
return parts.join("\n").trim();
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main() {
|
|
141
|
+
let input = "";
|
|
142
|
+
for await (const chunk of process.stdin) {
|
|
143
|
+
input += chunk;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let data;
|
|
147
|
+
try {
|
|
148
|
+
data = JSON.parse(input);
|
|
149
|
+
} catch {
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const transcriptPath = data.transcript_path;
|
|
154
|
+
const sessionId = data.session_id || `claude-code-${Date.now()}`;
|
|
155
|
+
|
|
156
|
+
// Use session_id as the OV session ID for 1:1 mapping (like OpenClaw plugin)
|
|
157
|
+
const ovSessionId = `cc-${sessionId}`;
|
|
158
|
+
|
|
159
|
+
// Read recent messages from transcript
|
|
160
|
+
const messages = await readRecentMessages(transcriptPath);
|
|
161
|
+
if (messages.length === 0) {
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Take the last few messages (current turn)
|
|
166
|
+
const recentMessages = messages.slice(-6);
|
|
167
|
+
|
|
168
|
+
// Build turn text
|
|
169
|
+
const turnTexts = recentMessages.map((m) => `[${m.role}]: ${m.text}`);
|
|
170
|
+
const turnText = turnTexts.join("\n");
|
|
171
|
+
const sanitized = sanitize(turnText);
|
|
172
|
+
|
|
173
|
+
if (!shouldCapture(sanitized)) {
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Truncate if needed
|
|
178
|
+
const captureText = sanitized.length > CAPTURE_MAX_LENGTH
|
|
179
|
+
? sanitized.slice(0, CAPTURE_MAX_LENGTH)
|
|
180
|
+
: sanitized;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Send to OpenViking session
|
|
184
|
+
await ovRequest(`/api/v1/sessions/${encodeURIComponent(ovSessionId)}/messages`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: JSON.stringify({ role: "user", content: captureText }),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Commit session to extract memories
|
|
190
|
+
await ovRequest(`/api/v1/sessions/${encodeURIComponent(ovSessionId)}/commit?wait=false`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: JSON.stringify({}),
|
|
193
|
+
});
|
|
194
|
+
} catch {
|
|
195
|
+
// Silently fail - don't block the user
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenViking Auto-Recall Hook for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Runs on UserPromptSubmit: takes the user's message, searches OpenViking
|
|
6
|
+
* for relevant memories, and injects them as additional context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const OPENVIKING_BASE_URL = process.env.OPENVIKING_BASE_URL || "http://69.5.7.190:1933";
|
|
10
|
+
const OPENVIKING_API_KEY = process.env.OPENVIKING_API_KEY || "fb56a300af1a93e580aa2ef16952cb7663fc3963f1f1f0ec26dfec4f0900f790";
|
|
11
|
+
const OPENVIKING_AGENT_ID = process.env.OPENVIKING_AGENT_ID || "claude-code";
|
|
12
|
+
const RECALL_LIMIT = 6;
|
|
13
|
+
const RECALL_SCORE_THRESHOLD = 0.15;
|
|
14
|
+
const RECALL_MAX_CONTENT_CHARS = 500;
|
|
15
|
+
const RECALL_TOKEN_BUDGET = 2000;
|
|
16
|
+
const TIMEOUT_MS = 5000;
|
|
17
|
+
const MIN_QUERY_LENGTH = 5;
|
|
18
|
+
|
|
19
|
+
// Skip patterns (commands, very short, pure punctuation)
|
|
20
|
+
const COMMAND_RE = /^\/[a-z0-9_-]{1,64}\b/i;
|
|
21
|
+
const NON_CONTENT_RE = /^[\p{P}\p{S}\s]+$/u;
|
|
22
|
+
|
|
23
|
+
function clampScore(v) {
|
|
24
|
+
if (typeof v !== "number" || Number.isNaN(v)) return 0;
|
|
25
|
+
return Math.max(0, Math.min(1, v));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function estimateTokens(text) {
|
|
29
|
+
return Math.ceil((text || "").length / 4);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ovFind(query, targetUri) {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${OPENVIKING_BASE_URL}/api/v1/search/find`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"X-API-Key": OPENVIKING_API_KEY,
|
|
41
|
+
"X-OpenViking-Agent": OPENVIKING_AGENT_ID,
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
query,
|
|
45
|
+
...(targetUri ? { target_uri: targetUri } : {}),
|
|
46
|
+
limit: Math.max(RECALL_LIMIT * 4, 20),
|
|
47
|
+
score_threshold: 0,
|
|
48
|
+
}),
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
const payload = await res.json().catch(() => ({}));
|
|
52
|
+
if (!res.ok) return [];
|
|
53
|
+
return (payload.result?.memories || payload.memories || []);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ovRead(uri) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(
|
|
66
|
+
`${OPENVIKING_BASE_URL}/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
|
|
67
|
+
{
|
|
68
|
+
headers: {
|
|
69
|
+
"X-API-Key": OPENVIKING_API_KEY,
|
|
70
|
+
"X-OpenViking-Agent": OPENVIKING_AGENT_ID,
|
|
71
|
+
},
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
const payload = await res.json().catch(() => ({}));
|
|
76
|
+
if (!res.ok) return null;
|
|
77
|
+
const result = payload.result ?? payload;
|
|
78
|
+
return typeof result === "string" ? result.trim() : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
// Read stdin
|
|
88
|
+
let input = "";
|
|
89
|
+
for await (const chunk of process.stdin) {
|
|
90
|
+
input += chunk;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let prompt;
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(input);
|
|
96
|
+
prompt = data.prompt || "";
|
|
97
|
+
} catch {
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Clean up the prompt text
|
|
102
|
+
const queryText = prompt
|
|
103
|
+
.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>/gi, " ")
|
|
104
|
+
.replace(/\s+/g, " ")
|
|
105
|
+
.trim();
|
|
106
|
+
|
|
107
|
+
// Skip if too short, is a command, or is pure punctuation
|
|
108
|
+
if (
|
|
109
|
+
queryText.length < MIN_QUERY_LENGTH ||
|
|
110
|
+
COMMAND_RE.test(queryText) ||
|
|
111
|
+
NON_CONTENT_RE.test(queryText)
|
|
112
|
+
) {
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Search ALL memory spaces (no target_uri filter)
|
|
117
|
+
const allMems = await ovFind(queryText, null);
|
|
118
|
+
|
|
119
|
+
// Deduplicate
|
|
120
|
+
const uriSet = new Set();
|
|
121
|
+
const unique = allMems.filter((m) => {
|
|
122
|
+
if (uriSet.has(m.uri)) return false;
|
|
123
|
+
uriSet.add(m.uri);
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Filter: leaf only, score threshold, dedup by abstract
|
|
128
|
+
const sorted = unique
|
|
129
|
+
.filter((m) => m.level === 2 && clampScore(m.score) >= RECALL_SCORE_THRESHOLD)
|
|
130
|
+
.sort((a, b) => clampScore(b.score) - clampScore(a.score));
|
|
131
|
+
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const memories = [];
|
|
134
|
+
for (const item of sorted) {
|
|
135
|
+
const key = (item.abstract || item.overview || "").trim().toLowerCase() || item.uri;
|
|
136
|
+
if (seen.has(key)) continue;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
memories.push(item);
|
|
139
|
+
if (memories.length >= RECALL_LIMIT) break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (memories.length === 0) {
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build memory lines with token budget
|
|
147
|
+
let budgetRemaining = RECALL_TOKEN_BUDGET;
|
|
148
|
+
const lines = [];
|
|
149
|
+
for (const item of memories) {
|
|
150
|
+
if (budgetRemaining <= 0) break;
|
|
151
|
+
|
|
152
|
+
let content = item.abstract?.trim() || item.uri;
|
|
153
|
+
// Try to read full content for leaf memories
|
|
154
|
+
if (item.level === 2) {
|
|
155
|
+
const full = await ovRead(item.uri);
|
|
156
|
+
if (full) content = full;
|
|
157
|
+
}
|
|
158
|
+
if (content.length > RECALL_MAX_CONTENT_CHARS) {
|
|
159
|
+
content = content.slice(0, RECALL_MAX_CONTENT_CHARS) + "...";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const line = `- [${item.category || "memory"}] ${content}`;
|
|
163
|
+
const lineTokens = estimateTokens(line);
|
|
164
|
+
|
|
165
|
+
// First line always included even if over budget
|
|
166
|
+
if (lineTokens > budgetRemaining && lines.length > 0) break;
|
|
167
|
+
|
|
168
|
+
lines.push(line);
|
|
169
|
+
budgetRemaining -= lineTokens;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (lines.length === 0) {
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const memoryContext =
|
|
177
|
+
"[OpenViking Auto-Recall] The following long-term memories may be relevant to this conversation:\n" +
|
|
178
|
+
lines.join("\n");
|
|
179
|
+
|
|
180
|
+
// Output as additionalContext JSON
|
|
181
|
+
const output = {
|
|
182
|
+
hookSpecificOutput: {
|
|
183
|
+
hookEventName: "UserPromptSubmit",
|
|
184
|
+
additionalContext: memoryContext,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
process.stdout.write(JSON.stringify(output));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenViking MCP Server for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Provides memory_recall, memory_store, memory_forget tools
|
|
6
|
+
* that connect to the same OpenViking backend used by the OpenClaw plugin.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: MCP stdio (JSON-RPC 2.0 over stdin/stdout)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const OPENVIKING_BASE_URL = process.env.OPENVIKING_BASE_URL || "http://69.5.7.190:1933";
|
|
12
|
+
const OPENVIKING_API_KEY = process.env.OPENVIKING_API_KEY || "fb56a300af1a93e580aa2ef16952cb7663fc3963f1f1f0ec26dfec4f0900f790";
|
|
13
|
+
const OPENVIKING_AGENT_ID = process.env.OPENVIKING_AGENT_ID || "claude-code";
|
|
14
|
+
const OPENVIKING_TIMEOUT_MS = 15000;
|
|
15
|
+
const RECALL_LIMIT = 6;
|
|
16
|
+
const RECALL_SCORE_THRESHOLD = 0.15;
|
|
17
|
+
const RECALL_MAX_CONTENT_CHARS = 500;
|
|
18
|
+
|
|
19
|
+
// --- HTTP client ---
|
|
20
|
+
|
|
21
|
+
async function ovRequest(path, options = {}, agentId) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), OPENVIKING_TIMEOUT_MS);
|
|
24
|
+
try {
|
|
25
|
+
const headers = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"X-API-Key": OPENVIKING_API_KEY,
|
|
28
|
+
"X-OpenViking-Agent": agentId || OPENVIKING_AGENT_ID,
|
|
29
|
+
};
|
|
30
|
+
const res = await fetch(`${OPENVIKING_BASE_URL}${path}`, {
|
|
31
|
+
...options,
|
|
32
|
+
headers: { ...headers, ...(options.headers || {}) },
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
const payload = await res.json().catch(() => ({}));
|
|
36
|
+
if (!res.ok || payload.status === "error") {
|
|
37
|
+
const code = payload.error?.code ? ` [${payload.error.code}]` : "";
|
|
38
|
+
const message = payload.error?.message || `HTTP ${res.status}`;
|
|
39
|
+
throw new Error(`OpenViking${code}: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
return payload.result ?? payload;
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Memory helpers ---
|
|
48
|
+
|
|
49
|
+
function clampScore(v) {
|
|
50
|
+
if (typeof v !== "number" || Number.isNaN(v)) return 0;
|
|
51
|
+
return Math.max(0, Math.min(1, v));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dedupeMemories(items, limit, scoreThreshold) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const result = [];
|
|
57
|
+
const sorted = [...items].sort((a, b) => clampScore(b.score) - clampScore(a.score));
|
|
58
|
+
for (const item of sorted) {
|
|
59
|
+
if (clampScore(item.score) < scoreThreshold) continue;
|
|
60
|
+
if (item.level !== 2) continue;
|
|
61
|
+
const key = (item.abstract || item.overview || "").trim().toLowerCase() || item.uri;
|
|
62
|
+
if (seen.has(key)) continue;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
result.push(item);
|
|
65
|
+
if (result.length >= limit) break;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const MEMORY_URI_RE = /^viking:\/\/(user|agent)\/(?:[^/]+\/)?memories(?:\/|$)/;
|
|
71
|
+
function isMemoryUri(uri) {
|
|
72
|
+
return MEMORY_URI_RE.test(uri);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Tool implementations ---
|
|
76
|
+
|
|
77
|
+
async function memoryRecall(params) {
|
|
78
|
+
const { query } = params;
|
|
79
|
+
const limit = params.limit || RECALL_LIMIT;
|
|
80
|
+
const scoreThreshold = params.scoreThreshold ?? RECALL_SCORE_THRESHOLD;
|
|
81
|
+
const targetUri = params.targetUri;
|
|
82
|
+
const requestLimit = Math.max(limit * 4, 20);
|
|
83
|
+
|
|
84
|
+
let allMemories;
|
|
85
|
+
if (targetUri) {
|
|
86
|
+
const result = await ovRequest("/api/v1/search/find", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({ query, target_uri: targetUri, limit: requestLimit, score_threshold: 0 }),
|
|
89
|
+
});
|
|
90
|
+
allMemories = result.memories || [];
|
|
91
|
+
} else {
|
|
92
|
+
// Search ALL spaces (no target_uri) to avoid space-resolution mismatch
|
|
93
|
+
const result = await ovRequest("/api/v1/search/find", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify({ query, limit: requestLimit, score_threshold: 0 }),
|
|
96
|
+
});
|
|
97
|
+
allMemories = result.memories || [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const memories = dedupeMemories(allMemories, limit, scoreThreshold);
|
|
101
|
+
if (memories.length === 0) {
|
|
102
|
+
return { content: [{ type: "text", text: "No relevant OpenViking memories found." }] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fetch full content for each memory
|
|
106
|
+
const lines = [];
|
|
107
|
+
for (const item of memories) {
|
|
108
|
+
let content = item.abstract?.trim() || item.uri;
|
|
109
|
+
if (item.level === 2) {
|
|
110
|
+
try {
|
|
111
|
+
const full = await ovRequest(`/api/v1/content/read?uri=${encodeURIComponent(item.uri)}`);
|
|
112
|
+
if (typeof full === "string" && full.trim()) {
|
|
113
|
+
content = full.trim();
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
if (content.length > RECALL_MAX_CONTENT_CHARS) {
|
|
118
|
+
content = content.slice(0, RECALL_MAX_CONTENT_CHARS) + "...";
|
|
119
|
+
}
|
|
120
|
+
const score = (clampScore(item.score) * 100).toFixed(0);
|
|
121
|
+
lines.push(`- [${item.category || "memory"}] ${content} (${score}%)`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: `Found ${memories.length} memories:\n\n${lines.join("\n")}` }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function memoryStore(params) {
|
|
130
|
+
const { text, sessionId } = params;
|
|
131
|
+
const role = params.role || "user";
|
|
132
|
+
const sid = sessionId || `claude-code-${Date.now()}`;
|
|
133
|
+
|
|
134
|
+
await ovRequest(`/api/v1/sessions/${encodeURIComponent(sid)}/messages`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({ role, content: text }),
|
|
137
|
+
});
|
|
138
|
+
const commitResult = await ovRequest(`/api/v1/sessions/${encodeURIComponent(sid)}/commit?wait=true`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: JSON.stringify({}),
|
|
141
|
+
});
|
|
142
|
+
const count = commitResult.memories_extracted || 0;
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `Stored in OpenViking session ${sid}, extracted ${count} memories.` }],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function memoryForget(params) {
|
|
149
|
+
const { uri, query } = params;
|
|
150
|
+
|
|
151
|
+
if (uri) {
|
|
152
|
+
if (!isMemoryUri(uri)) {
|
|
153
|
+
return { content: [{ type: "text", text: `Refusing to delete non-memory URI: ${uri}` }] };
|
|
154
|
+
}
|
|
155
|
+
await ovRequest(`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=false`, { method: "DELETE" });
|
|
156
|
+
return { content: [{ type: "text", text: `Forgotten: ${uri}` }] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!query) {
|
|
160
|
+
return { content: [{ type: "text", text: "Provide uri or query." }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = await ovRequest("/api/v1/search/find", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: JSON.stringify({ query, limit: 20, score_threshold: 0 }),
|
|
166
|
+
});
|
|
167
|
+
const candidates = dedupeMemories(result.memories || [], 20, RECALL_SCORE_THRESHOLD)
|
|
168
|
+
.filter((m) => isMemoryUri(m.uri));
|
|
169
|
+
|
|
170
|
+
if (candidates.length === 0) {
|
|
171
|
+
return { content: [{ type: "text", text: "No matching memories found." }] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const top = candidates[0];
|
|
175
|
+
if (candidates.length === 1 && clampScore(top.score) >= 0.85) {
|
|
176
|
+
await ovRequest(`/api/v1/fs?uri=${encodeURIComponent(top.uri)}&recursive=false`, { method: "DELETE" });
|
|
177
|
+
return { content: [{ type: "text", text: `Forgotten: ${top.uri}` }] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const list = candidates
|
|
181
|
+
.map((item) => `- ${item.uri} (${(clampScore(item.score) * 100).toFixed(0)}%)`)
|
|
182
|
+
.join("\n");
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: `Found ${candidates.length} candidates. Specify uri:\n${list}` }],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- MCP Protocol ---
|
|
189
|
+
|
|
190
|
+
const TOOLS = [
|
|
191
|
+
{
|
|
192
|
+
name: "memory_recall",
|
|
193
|
+
description: "Search long-term memories from OpenViking. Use when you need past user preferences, facts, decisions, or context from previous conversations.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
query: { type: "string", description: "Search query" },
|
|
198
|
+
limit: { type: "number", description: "Max results (default: 6)" },
|
|
199
|
+
scoreThreshold: { type: "number", description: "Minimum score 0-1 (default: 0.15)" },
|
|
200
|
+
targetUri: { type: "string", description: "Search scope URI (e.g. viking://user/memories)" },
|
|
201
|
+
},
|
|
202
|
+
required: ["query"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "memory_store",
|
|
207
|
+
description: "Store information in OpenViking long-term memory. Use to save important facts, preferences, decisions, or context for future recall.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
text: { type: "string", description: "Information to store" },
|
|
212
|
+
role: { type: "string", description: "Session role (default: user)" },
|
|
213
|
+
sessionId: { type: "string", description: "Session ID (auto-generated if omitted)" },
|
|
214
|
+
},
|
|
215
|
+
required: ["text"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "memory_forget",
|
|
220
|
+
description: "Delete a memory by URI, or search then delete when a strong match is found.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
uri: { type: "string", description: "Exact memory URI to delete" },
|
|
225
|
+
query: { type: "string", description: "Search query to find memory" },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const TOOL_HANDLERS = {
|
|
232
|
+
memory_recall: memoryRecall,
|
|
233
|
+
memory_store: memoryStore,
|
|
234
|
+
memory_forget: memoryForget,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function makeResponse(id, result) {
|
|
238
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function makeError(id, code, message) {
|
|
242
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function handleMessage(msg) {
|
|
246
|
+
const { id, method, params } = msg;
|
|
247
|
+
|
|
248
|
+
switch (method) {
|
|
249
|
+
case "initialize":
|
|
250
|
+
return makeResponse(id, {
|
|
251
|
+
protocolVersion: "2024-11-05",
|
|
252
|
+
capabilities: { tools: {} },
|
|
253
|
+
serverInfo: { name: "openviking", version: "1.0.0" },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
case "notifications/initialized":
|
|
257
|
+
return null; // no response needed
|
|
258
|
+
|
|
259
|
+
case "tools/list":
|
|
260
|
+
return makeResponse(id, { tools: TOOLS });
|
|
261
|
+
|
|
262
|
+
case "tools/call": {
|
|
263
|
+
const toolName = params?.name;
|
|
264
|
+
const handler = TOOL_HANDLERS[toolName];
|
|
265
|
+
if (!handler) {
|
|
266
|
+
return makeError(id, -32601, `Unknown tool: ${toolName}`);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = await handler(params.arguments || {});
|
|
270
|
+
return makeResponse(id, result);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return makeResponse(id, {
|
|
273
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
274
|
+
isError: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case "ping":
|
|
280
|
+
return makeResponse(id, {});
|
|
281
|
+
|
|
282
|
+
default:
|
|
283
|
+
if (id != null) {
|
|
284
|
+
return makeError(id, -32601, `Method not found: ${method}`);
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- stdio transport ---
|
|
291
|
+
|
|
292
|
+
let buffer = "";
|
|
293
|
+
|
|
294
|
+
process.stdin.setEncoding("utf8");
|
|
295
|
+
process.stdin.on("data", async (chunk) => {
|
|
296
|
+
buffer += chunk;
|
|
297
|
+
let newlineIdx;
|
|
298
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
299
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
300
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
301
|
+
if (!line) continue;
|
|
302
|
+
try {
|
|
303
|
+
const msg = JSON.parse(line);
|
|
304
|
+
const response = await handleMessage(msg);
|
|
305
|
+
if (response) {
|
|
306
|
+
process.stdout.write(response + "\n");
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
process.stderr.write(`MCP parse error: ${err.message}\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
process.stdin.on("end", () => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openviking-claude-code",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenViking long-term memory integration for Claude Code — auto-recall, auto-capture, and MCP tools",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Bill Zhao",
|
|
7
|
+
"email": "zhaodibill@gmail.com",
|
|
8
|
+
"url": "https://www.linkedin.com/in/billzhaodi/"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bin": {
|
|
12
|
+
"openviking-claude-code": "bin/setup.cjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"hooks/*.cjs",
|
|
16
|
+
"bin/*.cjs",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": ["claude-code", "openviking", "memory", "mcp", "auto-recall", "long-term-memory"],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/billzhao9/claude-openviking"
|
|
23
|
+
}
|
|
24
|
+
}
|