@jacklika/dsh-memory-mcp 0.1.4-alpha.0 → 0.1.4-alpha.1
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 +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/server.mjs +54 -5
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx dsh-memory-mcp --vault /path/to/vault
|
|
|
16
16
|
|
|
17
17
|
## Surface
|
|
18
18
|
|
|
19
|
-
Tools: `wiki_read`, `wiki_search`, `wiki_write`, `wiki_graph` — same contract as `@jacklika/dsh-tool-memory-filesystem` / `-graph`, including `baseVersion` optimistic concurrency on writes. Resources: `note:///<id>` over `resources/list` / `resources/read`.
|
|
19
|
+
Tools: `wiki_read`, `wiki_search`, `wiki_write`, `wiki_graph` — same contract as `@jacklika/dsh-tool-memory-filesystem` / `-graph`, including `baseVersion` optimistic concurrency on writes, `mtime` + `modifiedExternally` on reads, and `+08:00` `created`/`updated` normalization on writes. Resources: `note:///<id>` over `resources/list` / `resources/read`.
|
|
20
20
|
|
|
21
21
|
## Client configuration
|
|
22
22
|
|
package/README.zh.md
CHANGED
|
@@ -16,7 +16,7 @@ npx dsh-memory-mcp --vault /path/to/vault
|
|
|
16
16
|
|
|
17
17
|
## 暴露面
|
|
18
18
|
|
|
19
|
-
工具:`wiki_read`、`wiki_search`、`wiki_write`、`wiki_graph` —— 与 `@jacklika/dsh-tool-memory-filesystem` / `-graph` 的契约相同,写操作带 `baseVersion`
|
|
19
|
+
工具:`wiki_read`、`wiki_search`、`wiki_write`、`wiki_graph` —— 与 `@jacklika/dsh-tool-memory-filesystem` / `-graph` 的契约相同,写操作带 `baseVersion` 乐观并发控制,读操作返回 `mtime` + `modifiedExternally`,写入归一化 `created`/`updated` 为 `+08:00`。资源:通过 `resources/list` / `resources/read` 访问 `note:///<id>`。
|
|
20
20
|
|
|
21
21
|
## 客户端配置
|
|
22
22
|
|
package/package.json
CHANGED
package/src/server.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* --vault defaults to <cwd>/.plugins/memory/ to match the Bundle default.
|
|
9
9
|
*/
|
|
10
10
|
import { createHash, randomUUID } from 'node:crypto'
|
|
11
|
-
import { mkdir, readdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
12
12
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
13
13
|
import process from 'node:process'
|
|
14
14
|
import readline from 'node:readline'
|
|
@@ -93,6 +93,45 @@ function noteVersion(text) {
|
|
|
93
93
|
return createHash('sha1').update(text).digest('hex')
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Mirrors the mtime tracking and timestamp normalization in
|
|
97
|
+
// @jacklika/dsh-tool-memory-filesystem so MCP clients see the same
|
|
98
|
+
// modifiedExternally flag and +08:00 write normalization as the dsh tools.
|
|
99
|
+
const observedMtimes = new Map()
|
|
100
|
+
const OBSERVED_MTIME_LIMIT = 256
|
|
101
|
+
|
|
102
|
+
function recordMtime(path, mtimeMs) {
|
|
103
|
+
observedMtimes.delete(path)
|
|
104
|
+
observedMtimes.set(path, mtimeMs)
|
|
105
|
+
if (observedMtimes.size > OBSERVED_MTIME_LIMIT) {
|
|
106
|
+
const oldest = observedMtimes.keys().next().value
|
|
107
|
+
if (oldest !== undefined) observedMtimes.delete(oldest)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detectExternalChange(path, mtimeMs) {
|
|
112
|
+
const seen = observedMtimes.get(path)
|
|
113
|
+
recordMtime(path, mtimeMs)
|
|
114
|
+
return seen !== undefined && seen !== mtimeMs
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeNoteTimestamps(frontmatter) {
|
|
118
|
+
for (const key of ['created', 'updated']) {
|
|
119
|
+
const value = frontmatter[key]
|
|
120
|
+
if (typeof value !== 'string') continue
|
|
121
|
+
const parsed = new Date(value)
|
|
122
|
+
if (Number.isNaN(parsed.getTime())) continue
|
|
123
|
+
const normalized = formatBeijingTime(parsed)
|
|
124
|
+
if (normalized !== value) frontmatter[key] = normalized
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeTextTimestamps(text) {
|
|
129
|
+
const { frontmatter, body } = splitFrontmatter(text)
|
|
130
|
+
if (Object.keys(frontmatter).length === 0) return text
|
|
131
|
+
normalizeNoteTimestamps(frontmatter)
|
|
132
|
+
return `---\n${yaml.dump(frontmatter).trim()}\n---\n${body}`
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
function extractLinks(text) {
|
|
97
136
|
const links = []
|
|
98
137
|
for (const m of stripCode(text).matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)) links.push(m[1].trim())
|
|
@@ -295,9 +334,10 @@ async function writeNote(id, content, mode = 'append', baseVersion) {
|
|
|
295
334
|
}
|
|
296
335
|
let finalBody
|
|
297
336
|
if (mode === 'overwrite') {
|
|
298
|
-
finalBody = content
|
|
337
|
+
finalBody = normalizeTextTimestamps(content)
|
|
299
338
|
} else {
|
|
300
339
|
const { frontmatter, body } = splitFrontmatter(existing ?? '')
|
|
340
|
+
normalizeNoteTimestamps(frontmatter)
|
|
301
341
|
const fm = Object.keys(frontmatter).length > 0
|
|
302
342
|
? `---\n${yaml.dump(frontmatter).trimEnd()}\n---\n\n`
|
|
303
343
|
: ''
|
|
@@ -311,6 +351,7 @@ async function writeNote(id, content, mode = 'append', baseVersion) {
|
|
|
311
351
|
await rm(tmp, { force: true }).catch(() => undefined)
|
|
312
352
|
throw error
|
|
313
353
|
}
|
|
354
|
+
recordMtime(absolutePath, (await stat(absolutePath)).mtimeMs)
|
|
314
355
|
return { id: vaultRelativeId(absolutePath), mode, bytes: Buffer.byteLength(finalBody, 'utf8') }
|
|
315
356
|
}
|
|
316
357
|
|
|
@@ -360,7 +401,7 @@ async function buildGraph(id, depth = 1, maxNodes = 200) {
|
|
|
360
401
|
const TOOLS = [
|
|
361
402
|
{
|
|
362
403
|
name: 'wiki_read',
|
|
363
|
-
description: 'Read one Markdown note from the wiki vault, following Obsidian-style [[link]] references up to the configured depth.',
|
|
404
|
+
description: 'Read one Markdown note from the wiki vault, following Obsidian-style [[link]] references up to the configured depth. Returns mtime and modifiedExternally alongside frontmatter, body, links, and version.',
|
|
364
405
|
inputSchema: {
|
|
365
406
|
type: 'object',
|
|
366
407
|
properties: { id: { type: 'string', description: 'Vault-relative path of the note (e.g. "concepts/RAG.md").' } },
|
|
@@ -405,8 +446,16 @@ const TOOLS = [
|
|
|
405
446
|
|
|
406
447
|
async function callTool(name, a = {}) {
|
|
407
448
|
switch (name) {
|
|
408
|
-
case 'wiki_read':
|
|
409
|
-
|
|
449
|
+
case 'wiki_read': {
|
|
450
|
+
const absolutePath = await containedPathReal(a.id)
|
|
451
|
+
const note = await readNote(absolutePath, MAX_LINK_DEPTH)
|
|
452
|
+
const info = await stat(absolutePath)
|
|
453
|
+
return {
|
|
454
|
+
...note,
|
|
455
|
+
mtime: formatBeijingTime(info.mtime),
|
|
456
|
+
modifiedExternally: detectExternalChange(absolutePath, info.mtimeMs),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
410
459
|
case 'wiki_search':
|
|
411
460
|
return searchNotes(a.query ?? '')
|
|
412
461
|
case 'wiki_write':
|