oh-memos-mcp 3.1.1 → 3.1.2
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 +139 -26
- package/dist/handlers/memory.js +35 -5
- package/dist/handlers/search.js +11 -11
- package/dist/spreading-activation.js +14 -14
- package/dist/verbatim-source.d.ts +76 -0
- package/dist/verbatim-source.js +118 -0
- package/package.json +68 -68
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
MCP Server for **oh-memos** — Intelligent Persistent Memory for AI Assistants.
|
|
8
8
|
|
|
9
|
-
Pure Node.js. No Python required.
|
|
9
|
+
Pure Node.js. No Python required.
|
|
10
10
|
|
|
11
11
|
> **3.0:** requires Node.js 20+, serves legacy 2025-era and MCP `2026-07-28`
|
|
12
12
|
> clients from the same stdio package, and requires no data migration. Still on
|
|
@@ -19,8 +19,8 @@ Pure Node.js. No Python required. Works with `npx` out of the box.
|
|
|
19
19
|
- **Node.js 20 or newer**
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
npm i -g oh-memos-mcp # 3.x, npm latest — needs Node.js 20+
|
|
23
|
+
npm i -g oh-memos-mcp@2 # 2.x maintenance line — still runs on Node.js 18
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
oh-memos backend API must be running before connecting this MCP server:
|
|
@@ -35,50 +35,96 @@ oh-memos backend API must be running before connecting this MCP server:
|
|
|
35
35
|
|
|
36
36
|
## Quick Start
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Install once, then point the client at the installed entry point:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g oh-memos-mcp
|
|
42
|
+
npm root -g # prints the directory used in "args" below
|
|
43
|
+
```
|
|
39
44
|
|
|
40
45
|
```json
|
|
41
46
|
{
|
|
42
47
|
"mcpServers": {
|
|
43
48
|
"oh-memos": {
|
|
44
|
-
"command": "
|
|
45
|
-
"args": ["-
|
|
49
|
+
"command": "node",
|
|
50
|
+
"args": ["<npm root -g>/oh-memos-mcp/dist/index.js"],
|
|
46
51
|
"env": {
|
|
47
52
|
"MEMOS_URL": "http://localhost:18000",
|
|
48
53
|
"MEMOS_USER": "dev_user",
|
|
49
54
|
"MEMOS_DEFAULT_CUBE": "dev_cube",
|
|
50
|
-
"MEMOS_CUBES_DIR": "
|
|
55
|
+
"MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes",
|
|
56
|
+
"MEMOS_ENV_FILE": "/path/to/oh-memos/.env"
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
To upgrade, reinstall globally — the config needs no edit:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install -g oh-memos-mcp@latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then restart the client. A running MCP server is never swapped mid-session: the
|
|
70
|
+
stdio pipe is bound when the client starts, so an upgrade takes effect only after
|
|
71
|
+
the client restarts.
|
|
72
|
+
|
|
73
|
+
### Why not `npx`
|
|
74
|
+
|
|
75
|
+
`npx -y oh-memos-mcp@<version>` also works and is fine for a one-off trial, but it
|
|
76
|
+
costs two extra processes per client — `cmd`/`sh` → `npx-cli` (node) → `cmd`/`sh` →
|
|
77
|
+
server (node), versus a direct launch's two. With several MCP clients open at once
|
|
78
|
+
that overhead is measurable: on one Windows machine running seven clients, the npx
|
|
79
|
+
wrappers alone held ~360 MB. `npx` also pins the version inside every client
|
|
80
|
+
config, so upgrading means editing each one.
|
|
81
|
+
|
|
82
|
+
| | `npx -y oh-memos-mcp@x.y.z` | `node <npm root -g>/oh-memos-mcp/dist/index.js` |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| Processes per client | 4 | 2 |
|
|
85
|
+
| Upgrade | edit every config | `npm i -g oh-memos-mcp@latest` |
|
|
86
|
+
| Version source | the config | the global install |
|
|
87
|
+
|
|
88
|
+
A third option is to point `args` at a checkout's `dist/index.js`. That is the
|
|
89
|
+
lowest-overhead choice for working *on* oh-memos — `npm run build` takes effect on
|
|
90
|
+
the next client restart, with no publish — but the client then breaks if the
|
|
91
|
+
checkout moves or its build output is stale.
|
|
92
|
+
|
|
93
|
+
Or use a `.env` file in your working directory (the server auto-discovers it when
|
|
94
|
+
the working directory is the project root):
|
|
58
95
|
|
|
59
96
|
```bash
|
|
60
97
|
cp node_modules/oh-memos-mcp/.env.example .env
|
|
61
98
|
# Edit .env with your paths
|
|
62
|
-
|
|
99
|
+
node "$(npm root -g)/oh-memos-mcp/dist/index.js"
|
|
63
100
|
```
|
|
64
101
|
|
|
65
102
|
---
|
|
66
103
|
|
|
67
104
|
## Configuration Examples
|
|
68
105
|
|
|
106
|
+
The `args` path below assumes npm's **default** global prefix for each platform.
|
|
107
|
+
A custom prefix (nvm, fnm, Volta, a hand-set `prefix`) puts the package
|
|
108
|
+
elsewhere, so confirm with `npm root -g` and use what it prints:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node -e "console.log(require('path').join(process.argv[1],'oh-memos-mcp/dist/index.js'))" "$(npm root -g)"
|
|
112
|
+
```
|
|
113
|
+
|
|
69
114
|
### Linux / macOS
|
|
70
115
|
|
|
71
116
|
```json
|
|
72
117
|
{
|
|
73
118
|
"mcpServers": {
|
|
74
119
|
"oh-memos": {
|
|
75
|
-
"command": "
|
|
76
|
-
"args": ["
|
|
120
|
+
"command": "node",
|
|
121
|
+
"args": ["/usr/local/lib/node_modules/oh-memos-mcp/dist/index.js"],
|
|
77
122
|
"env": {
|
|
78
123
|
"MEMOS_URL": "http://localhost:18000",
|
|
79
124
|
"MEMOS_USER": "dev_user",
|
|
80
125
|
"MEMOS_DEFAULT_CUBE": "dev_cube",
|
|
81
|
-
"MEMOS_CUBES_DIR": "/home/user/oh-memos/data/oh-memos_cubes"
|
|
126
|
+
"MEMOS_CUBES_DIR": "/home/user/oh-memos/data/oh-memos_cubes",
|
|
127
|
+
"MEMOS_ENV_FILE": "/home/user/oh-memos/.env"
|
|
82
128
|
}
|
|
83
129
|
}
|
|
84
130
|
}
|
|
@@ -91,13 +137,14 @@ npx -y oh-memos-mcp
|
|
|
91
137
|
{
|
|
92
138
|
"mcpServers": {
|
|
93
139
|
"oh-memos": {
|
|
94
|
-
"command": "
|
|
95
|
-
"args": ["
|
|
140
|
+
"command": "node",
|
|
141
|
+
"args": ["C:/Users/you/AppData/Roaming/npm/node_modules/oh-memos-mcp/dist/index.js"],
|
|
96
142
|
"env": {
|
|
97
143
|
"MEMOS_URL": "http://localhost:18000",
|
|
98
144
|
"MEMOS_USER": "dev_user",
|
|
99
145
|
"MEMOS_DEFAULT_CUBE": "dev_cube",
|
|
100
|
-
"MEMOS_CUBES_DIR": "G:/test/oh-memos/data/oh-memos_cubes"
|
|
146
|
+
"MEMOS_CUBES_DIR": "G:/test/oh-memos/data/oh-memos_cubes",
|
|
147
|
+
"MEMOS_ENV_FILE": "G:/test/oh-memos/.env"
|
|
101
148
|
}
|
|
102
149
|
}
|
|
103
150
|
}
|
|
@@ -110,13 +157,14 @@ npx -y oh-memos-mcp
|
|
|
110
157
|
{
|
|
111
158
|
"mcpServers": {
|
|
112
159
|
"oh-memos": {
|
|
113
|
-
"command": "
|
|
114
|
-
"args": ["
|
|
160
|
+
"command": "node",
|
|
161
|
+
"args": ["/usr/lib/node_modules/oh-memos-mcp/dist/index.js"],
|
|
115
162
|
"env": {
|
|
116
163
|
"MEMOS_URL": "http://localhost:18000",
|
|
117
164
|
"MEMOS_USER": "dev_user",
|
|
118
165
|
"MEMOS_DEFAULT_CUBE": "dev_cube",
|
|
119
|
-
"MEMOS_CUBES_DIR": "/mnt/g/test/oh-memos/data/oh-memos_cubes"
|
|
166
|
+
"MEMOS_CUBES_DIR": "/mnt/g/test/oh-memos/data/oh-memos_cubes",
|
|
167
|
+
"MEMOS_ENV_FILE": "/mnt/g/test/oh-memos/.env"
|
|
120
168
|
}
|
|
121
169
|
}
|
|
122
170
|
}
|
|
@@ -137,13 +185,14 @@ be deleted without a prompt; add it only if you have decided you want that.
|
|
|
137
185
|
"mcpServers": {
|
|
138
186
|
"oh-memos": {
|
|
139
187
|
"type": "stdio",
|
|
140
|
-
"command": "
|
|
141
|
-
"args": ["-
|
|
188
|
+
"command": "node",
|
|
189
|
+
"args": ["<npm root -g>/oh-memos-mcp/dist/index.js"],
|
|
142
190
|
"env": {
|
|
143
191
|
"MEMOS_URL": "http://localhost:18000",
|
|
144
192
|
"MEMOS_USER": "dev_user",
|
|
145
193
|
"MEMOS_DEFAULT_CUBE": "dev_cube",
|
|
146
|
-
"MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes"
|
|
194
|
+
"MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes",
|
|
195
|
+
"MEMOS_ENV_FILE": "/path/to/oh-memos/.env"
|
|
147
196
|
},
|
|
148
197
|
"alwaysAllow": [
|
|
149
198
|
"memos_context_resume",
|
|
@@ -183,16 +232,80 @@ be deleted without a prompt; add it only if you have decided you want that.
|
|
|
183
232
|
| `NEO4J_PASSWORD` | No | — | Neo4j password |
|
|
184
233
|
| `MEMOS_ENV_FILE` | No | — | Explicit path to a `.env` file. Highest priority — see below |
|
|
185
234
|
|
|
235
|
+
### Mode and provider
|
|
236
|
+
|
|
237
|
+
| Variable | Required | Default | Description |
|
|
238
|
+
|----------|----------|---------|-------------|
|
|
239
|
+
| `MEMOS_MODE` | No | `full` | `lite` runs the local JSONL provider with no API, Python, Neo4j, or Qdrant. `full` uses the HTTP backend |
|
|
240
|
+
| `MEMOS_PROVIDER` | No | `local` when `MEMOS_MODE=lite`, else `api` | Storage backend. Set explicitly only to override the mode-derived default |
|
|
241
|
+
| `MEMOS_LOG_LEVEL` | No | `info` | `debug` / `info` / `warning` / `error`. Logs go to stderr so they never corrupt the stdio JSON-RPC stream |
|
|
242
|
+
|
|
243
|
+
`MEMOS_URL` is required for Full mode and unused in Lite.
|
|
244
|
+
|
|
245
|
+
### Retrieval behaviour (3.1.x)
|
|
246
|
+
|
|
247
|
+
These change what search returns. All are off or conservative by default, so
|
|
248
|
+
upgrading never silently changes ranking.
|
|
249
|
+
|
|
250
|
+
| Variable | Required | Default | Description |
|
|
251
|
+
|----------|----------|---------|-------------|
|
|
252
|
+
| `MEMOS_SPREAD_ACTIVATION` | No | `false` | One-hop graph spreading activation. When on, a search also returns memories reachable from its direct hits over `CAUSE` / `CONDITION` / `RELATE` edges, annotated ` · via CAUSE from <first 8 chars of the source id>`. Full mode only — it queries Neo4j directly, so `NEO4J_HTTP_URL`, `NEO4J_USER`, and `NEO4J_PASSWORD` must all resolve |
|
|
253
|
+
| `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier. The backend writes each memory twice — one short-term copy plus one long-term graph node with identical content — so leaving this off is what stops every memory appearing in pairs. Debugging only |
|
|
254
|
+
| `MEMOS_AUTO_CAPTURE` | No | `false` | Accept auto-captured memories. Auto-captured records are also ranked below explicit saves |
|
|
255
|
+
|
|
256
|
+
> **Spreading activation degrades silently without Neo4j credentials.** Retrieval
|
|
257
|
+
> still returns memories and logs nothing unusual — you simply get no ` via `
|
|
258
|
+
> annotations. If you enabled the switch and see no annotations, check that the
|
|
259
|
+
> Neo4j variables actually reach the server (under `npx`, that means
|
|
260
|
+
> `MEMOS_ENV_FILE`), not that the feature is missing.
|
|
261
|
+
|
|
262
|
+
Ranking that needs no configuration: per-type exponential decay (a `PROGRESS`
|
|
263
|
+
note ages out in weeks, a `DECISION` stays relevant for years), access
|
|
264
|
+
reinforcement, and near-duplicate folding with per-type thresholds. Folded
|
|
265
|
+
duplicates are reported as ` · folded N: <ids>` rather than dropped.
|
|
266
|
+
|
|
267
|
+
### Lite embeddings
|
|
268
|
+
|
|
269
|
+
| Variable | Required | Default | Description |
|
|
270
|
+
|----------|----------|---------|-------------|
|
|
271
|
+
| `MEMOS_LITE_EMBED` | No | `auto` | `off` forces lexical search. `auto` uses Ollama embeddings when reachable and falls back to lexical |
|
|
272
|
+
| `MEMOS_LITE_EMBED_URL` | No | `http://localhost:11434` | Ollama endpoint |
|
|
273
|
+
| `MEMOS_LITE_EMBED_MODEL` | No | `nomic-embed-text` | Embedding model |
|
|
274
|
+
|
|
275
|
+
### Read from `.env`, not from the client config
|
|
276
|
+
|
|
277
|
+
The server reads these when building a cube, and they are conventionally kept in
|
|
278
|
+
`.env` rather than repeated in every client config. `MOS_CHAT_MODEL` is
|
|
279
|
+
**required** — cube registration dies on it first, surfacing as a cube that
|
|
280
|
+
"registers" and immediately reports itself unregistered.
|
|
281
|
+
|
|
282
|
+
| Variable | Description |
|
|
283
|
+
|----------|-------------|
|
|
284
|
+
| `MOS_CHAT_MODEL` | Chat model for memory extraction. Required |
|
|
285
|
+
| `MOS_EMBEDDER_BACKEND` / `_PROVIDER` / `_MODEL` / `_API_BASE` | Embedder wiring |
|
|
286
|
+
| `MOS_ENABLE_REORGANIZE` | Background memory reorganization |
|
|
287
|
+
| `NEO4J_BACKEND` / `NEO4J_URI` / `NEO4J_DB_NAME` | Graph DB connection for cube construction |
|
|
288
|
+
| `NEO4J_AUTO_CREATE` / `NEO4J_USE_MULTI_DB` | Cube creation policy |
|
|
289
|
+
|
|
186
290
|
> **On locating `.env`**: without `MEMOS_ENV_FILE`, the file is found by guessing
|
|
187
291
|
> from position (working directory, two levels above the package, then dotenv's
|
|
188
|
-
> upward search). That works from a checkout
|
|
189
|
-
> where the package
|
|
190
|
-
> variable loads at all. Set `MEMOS_ENV_FILE` (or pass `--memos-env-file`)
|
|
191
|
-
> client's working directory and the install location are both outside the
|
|
192
|
-
> A path that does not exist warns on stderr rather than failing silently.
|
|
292
|
+
> upward search). That works from a checkout but **not from a global install or
|
|
293
|
+
> `npx`**, where the package sits outside the project and every candidate misses —
|
|
294
|
+
> so no variable loads at all. Set `MEMOS_ENV_FILE` (or pass `--memos-env-file`)
|
|
295
|
+
> when the client's working directory and the install location are both outside the
|
|
296
|
+
> project. A path that does not exist warns on stderr rather than failing silently.
|
|
193
297
|
>
|
|
194
298
|
> Copy `.env.example` to get started.
|
|
195
299
|
|
|
300
|
+
> **`MEMOS_ENV_FILE` wins over the client config.** It is loaded with
|
|
301
|
+
> `override: true`, so a value in that file beats the same key in the MCP client's
|
|
302
|
+
> `env` block. The positional fallbacks are the opposite — they only fill gaps and
|
|
303
|
+
> never override the launcher. This is deliberate: an explicitly named env file is
|
|
304
|
+
> meant to be authoritative, while a `.env` that merely happens to sit nearby is
|
|
305
|
+
> not. The practical consequence is that switches like
|
|
306
|
+
> `MEMOS_SHOW_WORKING_MEMORY` cannot be flipped from the client config once
|
|
307
|
+
> `.env` sets them — edit the env file instead.
|
|
308
|
+
|
|
196
309
|
---
|
|
197
310
|
|
|
198
311
|
## Tools (17)
|
package/dist/handlers/memory.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ensureCubeRegistered } from "../cube-manager.js";
|
|
|
11
11
|
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
12
12
|
import { recordAccess } from "../access-tracker.js";
|
|
13
13
|
import { filterEphemeralTier } from "../memory-tier.js";
|
|
14
|
+
import { findSiblings, renderVerbatimSections, verbatimOf, } from "../verbatim-source.js";
|
|
14
15
|
// formatMemoriesForDisplay intentionally not imported here — list output is built
|
|
15
16
|
// from the truncated allMemories in handleMemosList (avoids printing the full cube).
|
|
16
17
|
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, toFull, } from "../models.js";
|
|
@@ -223,6 +224,26 @@ export async function handleMemosList(arguments_) {
|
|
|
223
224
|
// ============================================================================
|
|
224
225
|
// memos_get
|
|
225
226
|
// ============================================================================
|
|
227
|
+
/**
|
|
228
|
+
* 取同源碎片。只在当前记忆确实带原文时才发这次请求 —— fast-path 写入不会付这个代价。
|
|
229
|
+
*
|
|
230
|
+
* 失败即返回空数组:同源列表是附加信息,取不到时 `memos_get` 仍应给出原文。
|
|
231
|
+
* 拉取量上限 200,避免大 cube 上把整库拉回来(碎片来自同一次写入,时间相邻,
|
|
232
|
+
* 后端按 updated_at 倒序返回,200 条足以覆盖)。
|
|
233
|
+
*/
|
|
234
|
+
async function fetchSiblings(cubeId, target) {
|
|
235
|
+
try {
|
|
236
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
|
|
237
|
+
if (!result.success || !result.data)
|
|
238
|
+
return [];
|
|
239
|
+
const data = result.data.data;
|
|
240
|
+
return findSiblings(target, extractMemoriesFromData(data ?? {}));
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
logger.debug(`sibling lookup failed: ${String(err)}`);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
226
247
|
export async function handleMemosGet(arguments_) {
|
|
227
248
|
const cubeId = getCubeIdFromArgs(arguments_);
|
|
228
249
|
const memoryId = String(arguments_.memory_id ?? "");
|
|
@@ -251,10 +272,11 @@ export async function handleMemosGet(arguments_) {
|
|
|
251
272
|
`**Cube**: ${full.cubeId} (local)`,
|
|
252
273
|
full.tags.length ? `**Tags**: ${full.tags.join(", ")}` : "",
|
|
253
274
|
full.createdAt ? `**Created**: ${full.createdAt}` : "",
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
// Lite 的 JSONL 不写 sources,verbatimOf 返回 null → 保持原有输出。
|
|
276
|
+
// 保留这个分支是为了后端哪天开始写该字段时自动生效。
|
|
277
|
+
...(verbatimOf(node) === null
|
|
278
|
+
? ["", "### Content", "", full.content]
|
|
279
|
+
: renderVerbatimSections(verbatimOf(node), [], full.content)),
|
|
258
280
|
]
|
|
259
281
|
.filter(Boolean)
|
|
260
282
|
.join("\n"),
|
|
@@ -272,6 +294,7 @@ export async function handleMemosGet(arguments_) {
|
|
|
272
294
|
// Full 模式同样记在本机侧车 —— 使用度是本机数据,不需要后端端点。
|
|
273
295
|
recordAccess(MEMOS_CUBES_DIR, cubeId, [memoryId]);
|
|
274
296
|
const fullMem = toFull(resultData, cubeId, MEMOS_USER);
|
|
297
|
+
const verbatim = verbatimOf(resultData);
|
|
275
298
|
const lines = [
|
|
276
299
|
"## 📝 Memory Details",
|
|
277
300
|
"",
|
|
@@ -285,7 +308,14 @@ export async function handleMemosGet(arguments_) {
|
|
|
285
308
|
lines.push(`**Tags**: ${fullMem.tags.join(", ")}`);
|
|
286
309
|
if (fullMem.createdAt)
|
|
287
310
|
lines.push(`**Created**: ${fullMem.createdAt}`);
|
|
288
|
-
|
|
311
|
+
// 有原文时原文即正文,`memory` 字段(LLM 概括)降级到下方的「抽取概括」区块。
|
|
312
|
+
// 无原文(fast-path 写入)时保持原有行为。
|
|
313
|
+
if (verbatim === null) {
|
|
314
|
+
lines.push("", "### Content", "", fullMem.content);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
lines.push(...renderVerbatimSections(verbatim, await fetchSiblings(cubeId, resultData), fullMem.content));
|
|
318
|
+
}
|
|
289
319
|
if (fullMem.background) {
|
|
290
320
|
lines.push("", "### Background", "", fullMem.background);
|
|
291
321
|
}
|
package/dist/handlers/search.js
CHANGED
|
@@ -69,17 +69,17 @@ export function buildTemporalCypher(timeWindowHours, env = process.env) {
|
|
|
69
69
|
const tierFilter = showsEphemeralTier(env)
|
|
70
70
|
? ""
|
|
71
71
|
: "AND coalesce(n.memory_type, '') <> 'WorkingMemory'";
|
|
72
|
-
return `
|
|
73
|
-
MATCH (n:Memory)
|
|
74
|
-
WHERE n.user_name = $user_name
|
|
75
|
-
AND n.status = 'activated'
|
|
76
|
-
${timeFilter}
|
|
77
|
-
${tierFilter}
|
|
78
|
-
RETURN n.id AS id, n.memory AS memory, n.key AS key,
|
|
79
|
-
n.updated_at AS updated_at, n.background AS background,
|
|
80
|
-
n.tags AS tags, n.memory_type AS memory_type
|
|
81
|
-
ORDER BY n.updated_at DESC
|
|
82
|
-
LIMIT $top_k
|
|
72
|
+
return `
|
|
73
|
+
MATCH (n:Memory)
|
|
74
|
+
WHERE n.user_name = $user_name
|
|
75
|
+
AND n.status = 'activated'
|
|
76
|
+
${timeFilter}
|
|
77
|
+
${tierFilter}
|
|
78
|
+
RETURN n.id AS id, n.memory AS memory, n.key AS key,
|
|
79
|
+
n.updated_at AS updated_at, n.background AS background,
|
|
80
|
+
n.tags AS tags, n.memory_type AS memory_type
|
|
81
|
+
ORDER BY n.updated_at DESC
|
|
82
|
+
LIMIT $top_k
|
|
83
83
|
`;
|
|
84
84
|
}
|
|
85
85
|
export async function getTemporalMemories(cubeId, topK = 10, timeWindowHours) {
|
|
@@ -32,20 +32,20 @@ export const SPREAD_FROM_KEY = "spread_from";
|
|
|
32
32
|
*/
|
|
33
33
|
export function buildSpreadCypher() {
|
|
34
34
|
const types = SPREAD_EDGE_TYPES.map((t) => `'${t}'`).join(", ");
|
|
35
|
-
return `
|
|
36
|
-
MATCH (n:Memory)-[r]-(m:Memory)
|
|
37
|
-
WHERE n.id IN $seeds
|
|
38
|
-
AND type(r) IN [${types}]
|
|
39
|
-
AND NOT m.id IN $seeds
|
|
40
|
-
AND m.user_name = $user_name
|
|
41
|
-
AND coalesce(m.status, 'activated') = 'activated'
|
|
42
|
-
AND coalesce(m.memory_type, '') <> 'WorkingMemory'
|
|
43
|
-
AND coalesce(m.type, '') <> 'reasoning'
|
|
44
|
-
WITH m, n, type(r) AS via, size([(m)--() | 1]) AS degree
|
|
45
|
-
WITH m, collect({via: via, from: n.id})[0] AS edge, degree
|
|
46
|
-
RETURN m.id AS id, m.memory AS memory, m.key AS key, m.tags AS tags,
|
|
47
|
-
m.updated_at AS updated_at, edge.via AS via, edge.from AS from,
|
|
48
|
-
degree
|
|
35
|
+
return `
|
|
36
|
+
MATCH (n:Memory)-[r]-(m:Memory)
|
|
37
|
+
WHERE n.id IN $seeds
|
|
38
|
+
AND type(r) IN [${types}]
|
|
39
|
+
AND NOT m.id IN $seeds
|
|
40
|
+
AND m.user_name = $user_name
|
|
41
|
+
AND coalesce(m.status, 'activated') = 'activated'
|
|
42
|
+
AND coalesce(m.memory_type, '') <> 'WorkingMemory'
|
|
43
|
+
AND coalesce(m.type, '') <> 'reasoning'
|
|
44
|
+
WITH m, n, type(r) AS via, size([(m)--() | 1]) AS degree
|
|
45
|
+
WITH m, collect({via: via, from: n.id})[0] AS edge, degree
|
|
46
|
+
RETURN m.id AS id, m.memory AS memory, m.key AS key, m.tags AS tags,
|
|
47
|
+
m.updated_at AS updated_at, edge.via AS via, edge.from AS from,
|
|
48
|
+
degree
|
|
49
49
|
`;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原文取回(verbatim source)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* `MOS_TYPED_SAVE_FAST=false` 时后端走 LLM 抽取:一次写入被切成多条记忆,每条的
|
|
7
|
+
* `memory` 字段是 LLM 概括(约 200-250 字),而**逐字原文保存在
|
|
8
|
+
* `metadata.sources[].content` 里**(实测:一条 877 字原文被切成 5 条,5 条的
|
|
9
|
+
* sources 内容 sha 完全相同)。
|
|
10
|
+
*
|
|
11
|
+
* 于是同一份数据有两层,用途不同:
|
|
12
|
+
*
|
|
13
|
+
* | 层 | 内容 | 服务于 |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | `memory` | LLM 概括,细粒度 | 向量化与建边 —— 图谱节点质量、扩散联想 |
|
|
16
|
+
* | `metadata.sources[].content` | 逐字原文 | `memos_get` —— 完整上下文 |
|
|
17
|
+
*
|
|
18
|
+
* 两者不冲突:抽取让图谱拿到细粒度节点(`memos_search` 的 ` via CAUSE from` 联想
|
|
19
|
+
* 依赖它),而 `memos_get` 读 sources 就不丢原文。维护者的决定是「保证图谱节点
|
|
20
|
+
* 编写分析,同时让 memos_get 拉原文」。
|
|
21
|
+
*
|
|
22
|
+
* ## 同源判定用内容指纹,不用 session_id
|
|
23
|
+
*
|
|
24
|
+
* 实测 5 条碎片的 `session_id` 相同 —— 但那是**整个 MCP 会话**的 id,一个会话里
|
|
25
|
+
* 的多次无关写入会共享它,用它分组会把无关记忆归到一起。`created_at` 也不行:
|
|
26
|
+
* 5 条各不相同(微秒级递增)。
|
|
27
|
+
*
|
|
28
|
+
* 只有 sources 内容本身是精确的分组键。
|
|
29
|
+
*
|
|
30
|
+
* ## fast-path 下本模块是无操作
|
|
31
|
+
*
|
|
32
|
+
* `MOS_TYPED_SAVE_FAST=true`(默认)时原样存储,`memory` 就是原文、`sources` 为空
|
|
33
|
+
* → `verbatimOf()` 返回 null,渲染层保持原有行为。所以这套改动对两种模式都安全。
|
|
34
|
+
*
|
|
35
|
+
* 设计讨论见 docs/design/memory-retrieval-optimization.md。
|
|
36
|
+
*/
|
|
37
|
+
import type { MemoryNode } from "./types.js";
|
|
38
|
+
/**
|
|
39
|
+
* 取出逐字原文。
|
|
40
|
+
*
|
|
41
|
+
* 没有 sources、sources 为空、或第一条没有非空 content 时返回 null —— 调用方据此
|
|
42
|
+
* 判断「这条是 fast-path 存的,无需原文层」。
|
|
43
|
+
*
|
|
44
|
+
* 多条 sources 时用第一条:LLM 抽取路径下一次写入只产生一个 source(实测
|
|
45
|
+
* `src_n=1`)。多条的情况来自 messages 数组写入,此时第一条仍是用户输入。
|
|
46
|
+
*/
|
|
47
|
+
export declare function verbatimOf(node: MemoryNode | undefined | null): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* 同源指纹。同一次写入切出的碎片共享它。
|
|
50
|
+
*
|
|
51
|
+
* 直接用原文本身,不做哈希:这是进程内的分组键,不持久化也不跨机器比较,
|
|
52
|
+
* 字符串相等就是精确判定。
|
|
53
|
+
*
|
|
54
|
+
* 初版加了 `${text.length}:` 前缀,理由是「让不同原文在第一个字符就分开」——
|
|
55
|
+
* 变异验证时发现构造不出它能防住的碰撞(去掉前缀后 `abc` 与 `abcdef` 指纹依然
|
|
56
|
+
* 不同),且 V8 的字符串相等本就先比长度。没有测试能支撑的防御代码比没有更糟,
|
|
57
|
+
* 所以删掉。
|
|
58
|
+
*
|
|
59
|
+
* 无原文返回 null —— null 不参与分组,避免把所有 fast-path 记忆归成一组。
|
|
60
|
+
*/
|
|
61
|
+
export declare function sourceFingerprint(node: MemoryNode | undefined | null): string | null;
|
|
62
|
+
/** 同源碎片的展示条目。 */
|
|
63
|
+
export interface SiblingRef {
|
|
64
|
+
id: string;
|
|
65
|
+
key: string;
|
|
66
|
+
}
|
|
67
|
+
/** 渲染原文区块与同源碎片列表,供 `memos_get` 两条路径共用。 */
|
|
68
|
+
export declare function renderVerbatimSections(verbatim: string | null, siblings: readonly SiblingRef[], summary: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* 在候选集里找与 target 同源的其他碎片。
|
|
71
|
+
*
|
|
72
|
+
* 排除 target 自身,也排除没有原文的节点。顺序按候选集原顺序,由调用方决定
|
|
73
|
+
* 候选集怎么排(后端返回顺序通常已按 created_at)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function findSiblings(target: MemoryNode | undefined | null, candidates: readonly (MemoryNode | undefined | null)[], limit?: number): SiblingRef[];
|
|
76
|
+
//# sourceMappingURL=verbatim-source.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原文取回(verbatim source)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* `MOS_TYPED_SAVE_FAST=false` 时后端走 LLM 抽取:一次写入被切成多条记忆,每条的
|
|
7
|
+
* `memory` 字段是 LLM 概括(约 200-250 字),而**逐字原文保存在
|
|
8
|
+
* `metadata.sources[].content` 里**(实测:一条 877 字原文被切成 5 条,5 条的
|
|
9
|
+
* sources 内容 sha 完全相同)。
|
|
10
|
+
*
|
|
11
|
+
* 于是同一份数据有两层,用途不同:
|
|
12
|
+
*
|
|
13
|
+
* | 层 | 内容 | 服务于 |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | `memory` | LLM 概括,细粒度 | 向量化与建边 —— 图谱节点质量、扩散联想 |
|
|
16
|
+
* | `metadata.sources[].content` | 逐字原文 | `memos_get` —— 完整上下文 |
|
|
17
|
+
*
|
|
18
|
+
* 两者不冲突:抽取让图谱拿到细粒度节点(`memos_search` 的 ` via CAUSE from` 联想
|
|
19
|
+
* 依赖它),而 `memos_get` 读 sources 就不丢原文。维护者的决定是「保证图谱节点
|
|
20
|
+
* 编写分析,同时让 memos_get 拉原文」。
|
|
21
|
+
*
|
|
22
|
+
* ## 同源判定用内容指纹,不用 session_id
|
|
23
|
+
*
|
|
24
|
+
* 实测 5 条碎片的 `session_id` 相同 —— 但那是**整个 MCP 会话**的 id,一个会话里
|
|
25
|
+
* 的多次无关写入会共享它,用它分组会把无关记忆归到一起。`created_at` 也不行:
|
|
26
|
+
* 5 条各不相同(微秒级递增)。
|
|
27
|
+
*
|
|
28
|
+
* 只有 sources 内容本身是精确的分组键。
|
|
29
|
+
*
|
|
30
|
+
* ## fast-path 下本模块是无操作
|
|
31
|
+
*
|
|
32
|
+
* `MOS_TYPED_SAVE_FAST=true`(默认)时原样存储,`memory` 就是原文、`sources` 为空
|
|
33
|
+
* → `verbatimOf()` 返回 null,渲染层保持原有行为。所以这套改动对两种模式都安全。
|
|
34
|
+
*
|
|
35
|
+
* 设计讨论见 docs/design/memory-retrieval-optimization.md。
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* 取出逐字原文。
|
|
39
|
+
*
|
|
40
|
+
* 没有 sources、sources 为空、或第一条没有非空 content 时返回 null —— 调用方据此
|
|
41
|
+
* 判断「这条是 fast-path 存的,无需原文层」。
|
|
42
|
+
*
|
|
43
|
+
* 多条 sources 时用第一条:LLM 抽取路径下一次写入只产生一个 source(实测
|
|
44
|
+
* `src_n=1`)。多条的情况来自 messages 数组写入,此时第一条仍是用户输入。
|
|
45
|
+
*/
|
|
46
|
+
export function verbatimOf(node) {
|
|
47
|
+
const sources = node?.metadata
|
|
48
|
+
?.sources;
|
|
49
|
+
if (!Array.isArray(sources) || sources.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
const first = sources[0];
|
|
52
|
+
const content = first?.content;
|
|
53
|
+
if (typeof content !== "string" || content.trim() === "")
|
|
54
|
+
return null;
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 同源指纹。同一次写入切出的碎片共享它。
|
|
59
|
+
*
|
|
60
|
+
* 直接用原文本身,不做哈希:这是进程内的分组键,不持久化也不跨机器比较,
|
|
61
|
+
* 字符串相等就是精确判定。
|
|
62
|
+
*
|
|
63
|
+
* 初版加了 `${text.length}:` 前缀,理由是「让不同原文在第一个字符就分开」——
|
|
64
|
+
* 变异验证时发现构造不出它能防住的碰撞(去掉前缀后 `abc` 与 `abcdef` 指纹依然
|
|
65
|
+
* 不同),且 V8 的字符串相等本就先比长度。没有测试能支撑的防御代码比没有更糟,
|
|
66
|
+
* 所以删掉。
|
|
67
|
+
*
|
|
68
|
+
* 无原文返回 null —— null 不参与分组,避免把所有 fast-path 记忆归成一组。
|
|
69
|
+
*/
|
|
70
|
+
export function sourceFingerprint(node) {
|
|
71
|
+
return verbatimOf(node);
|
|
72
|
+
}
|
|
73
|
+
/** 渲染原文区块与同源碎片列表,供 `memos_get` 两条路径共用。 */
|
|
74
|
+
export function renderVerbatimSections(verbatim, siblings, summary) {
|
|
75
|
+
if (verbatim === null)
|
|
76
|
+
return [];
|
|
77
|
+
const lines = ["", "### 原文 | Verbatim source", "", verbatim];
|
|
78
|
+
// 概括也一并给出:它是向量化与建边的实际输入,看得见才能判断图谱节点切得对不对。
|
|
79
|
+
if (summary.trim() !== "" && summary.trim() !== verbatim.trim()) {
|
|
80
|
+
lines.push("", "### 本节点的抽取概括 | Extracted summary (this node)", "", summary);
|
|
81
|
+
}
|
|
82
|
+
if (siblings.length > 0) {
|
|
83
|
+
lines.push("", `### 同源节点 | Sibling nodes (${siblings.length})`, "", "同一份原文被 LLM 抽取切成的其他节点。原文在每条上都完整,无需逐条取回;", "这里列出是为了看清图谱节点的切分方式。", "Other nodes the same source was split into. Each carries the full source;", "listed so the graph-node segmentation is visible.", "");
|
|
84
|
+
for (const s of siblings) {
|
|
85
|
+
lines.push(`- \`${s.id}\`${s.key ? ` — ${s.key}` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 在候选集里找与 target 同源的其他碎片。
|
|
92
|
+
*
|
|
93
|
+
* 排除 target 自身,也排除没有原文的节点。顺序按候选集原顺序,由调用方决定
|
|
94
|
+
* 候选集怎么排(后端返回顺序通常已按 created_at)。
|
|
95
|
+
*/
|
|
96
|
+
export function findSiblings(target, candidates, limit = 12) {
|
|
97
|
+
const fp = sourceFingerprint(target);
|
|
98
|
+
if (fp === null)
|
|
99
|
+
return [];
|
|
100
|
+
const targetId = String(target?.id ?? "");
|
|
101
|
+
const out = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const node of candidates) {
|
|
104
|
+
if (out.length >= limit)
|
|
105
|
+
break;
|
|
106
|
+
const id = String(node?.id ?? "");
|
|
107
|
+
if (!id || id === targetId || seen.has(id))
|
|
108
|
+
continue;
|
|
109
|
+
if (sourceFingerprint(node) !== fp)
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(id);
|
|
112
|
+
const meta = (node?.metadata ?? {});
|
|
113
|
+
const key = String(meta.key ?? "").trim();
|
|
114
|
+
out.push({ id, key });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=verbatim-source.js.map
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "oh-memos-mcp",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "MCP Server for oh-memos — Intelligent Memory Management (Node.js, no Python required)",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"oh-memos-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist/**/*.js",
|
|
12
|
-
"dist/**/*.d.ts",
|
|
13
|
-
"README.md",
|
|
14
|
-
"CHANGELOG.md",
|
|
15
|
-
".env.example"
|
|
16
|
-
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsc",
|
|
19
|
-
"dev": "tsx src/index.ts",
|
|
20
|
-
"test": "vitest run",
|
|
21
|
-
"test:protocol": "npm run build && node scripts/protocol-smoke.mjs && node scripts/protocol-v2-smoke.mjs",
|
|
22
|
-
"test:protocol:v2": "npm run build && node scripts/protocol-v2-smoke.mjs",
|
|
23
|
-
"test:lite-smoke": "npm run build && node scripts/lite-smoke-test.mjs",
|
|
24
|
-
"test:spread-smoke": "npm run build && node scripts/spread-smoke-test.mjs",
|
|
25
|
-
"test:host-env-smoke": "npm run build && node scripts/host-env-smoke.mjs",
|
|
26
|
-
"schema:semantic": "npm run build && node scripts/schema-semantic-snapshot.mjs --check",
|
|
27
|
-
"schema:semantic:freeze": "npm run build && node scripts/schema-semantic-snapshot.mjs --write",
|
|
28
|
-
"test:pack": "node scripts/pack-contract.mjs",
|
|
29
|
-
"test:watch": "vitest",
|
|
30
|
-
"schema:budget": "npm run build && node scripts/schema-budget.mjs --check",
|
|
31
|
-
"schema:budget:freeze": "npm run build && node scripts/schema-budget.mjs --write",
|
|
32
|
-
"prepublishOnly": "npm run build"
|
|
33
|
-
},
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=20.0.0"
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"mcp",
|
|
39
|
-
"memory",
|
|
40
|
-
"memos",
|
|
41
|
-
"claude",
|
|
42
|
-
"ai",
|
|
43
|
-
"knowledge-graph"
|
|
44
|
-
],
|
|
45
|
-
"author": "xigou",
|
|
46
|
-
"license": "MIT",
|
|
47
|
-
"repository": {
|
|
48
|
-
"type": "git",
|
|
49
|
-
"url": "https://github.com/lsg1103275794/oh-memos.git",
|
|
50
|
-
"directory": "mcp-server-node"
|
|
51
|
-
},
|
|
52
|
-
"bugs": {
|
|
53
|
-
"url": "https://github.com/lsg1103275794/oh-memos/issues"
|
|
54
|
-
},
|
|
55
|
-
"homepage": "https://github.com/lsg1103275794/oh-memos/tree/main/mcp-server-node",
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"@modelcontextprotocol/server": "^2.0.0",
|
|
58
|
-
"dotenv": "^16.0.0",
|
|
59
|
-
"zod": "^4.2.0"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@modelcontextprotocol/client": "^2.0.0",
|
|
63
|
-
"@types/node": "^22.0.0",
|
|
64
|
-
"tsx": "^4.23.12",
|
|
65
|
-
"typescript": "^5.5.0",
|
|
66
|
-
"vitest": "3.2.7"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "oh-memos-mcp",
|
|
3
|
+
"version": "3.1.2",
|
|
4
|
+
"description": "MCP Server for oh-memos — Intelligent Memory Management (Node.js, no Python required)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"oh-memos-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
".env.example"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:protocol": "npm run build && node scripts/protocol-smoke.mjs && node scripts/protocol-v2-smoke.mjs",
|
|
22
|
+
"test:protocol:v2": "npm run build && node scripts/protocol-v2-smoke.mjs",
|
|
23
|
+
"test:lite-smoke": "npm run build && node scripts/lite-smoke-test.mjs",
|
|
24
|
+
"test:spread-smoke": "npm run build && node scripts/spread-smoke-test.mjs",
|
|
25
|
+
"test:host-env-smoke": "npm run build && node scripts/host-env-smoke.mjs",
|
|
26
|
+
"schema:semantic": "npm run build && node scripts/schema-semantic-snapshot.mjs --check",
|
|
27
|
+
"schema:semantic:freeze": "npm run build && node scripts/schema-semantic-snapshot.mjs --write",
|
|
28
|
+
"test:pack": "node scripts/pack-contract.mjs",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"schema:budget": "npm run build && node scripts/schema-budget.mjs --check",
|
|
31
|
+
"schema:budget:freeze": "npm run build && node scripts/schema-budget.mjs --write",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"mcp",
|
|
39
|
+
"memory",
|
|
40
|
+
"memos",
|
|
41
|
+
"claude",
|
|
42
|
+
"ai",
|
|
43
|
+
"knowledge-graph"
|
|
44
|
+
],
|
|
45
|
+
"author": "xigou",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/lsg1103275794/oh-memos.git",
|
|
50
|
+
"directory": "mcp-server-node"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/lsg1103275794/oh-memos/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/lsg1103275794/oh-memos/tree/main/mcp-server-node",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
58
|
+
"dotenv": "^16.0.0",
|
|
59
|
+
"zod": "^4.2.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
63
|
+
"@types/node": "^22.0.0",
|
|
64
|
+
"tsx": "^4.23.12",
|
|
65
|
+
"typescript": "^5.5.0",
|
|
66
|
+
"vitest": "3.2.7"
|
|
67
|
+
}
|
|
68
|
+
}
|