@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 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` 乐观并发控制。资源:通过 `resources/list` / `resources/read` 访问 `note:///<id>`。
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jacklika/dsh-memory-mcp",
3
3
  "description": "MCP stdio server exposing the memory vault to any MCP client — one process owns all writes.",
4
- "version": "0.1.4-alpha.0",
4
+ "version": "0.1.4-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
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
- return readNote(await containedPathReal(a.id), MAX_LINK_DEPTH)
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':