oh-memos-mcp 3.1.6 → 3.1.7
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/CHANGELOG.md +66 -0
- package/dist/canvas-format.d.ts +24 -1
- package/dist/canvas-format.js +30 -2
- package/dist/handlers/canvas.js +4 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,72 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [3.1.7] - 2026-08-27
|
|
10
|
+
|
|
11
|
+
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
|
12
|
+
|
|
13
|
+
### 🏷️ `open` 报出的画布名,`list` 找不到
|
|
14
|
+
|
|
15
|
+
goal 足够长时,`actionOpen` 报出的名字比磁盘上真实存在的多一个字符:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
open reports: 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mcp (61)
|
|
19
|
+
list shows : 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mc (60)
|
|
20
|
+
on disk : ...-live-mc.mmd
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`actionOpen` 先 `slugify(goal)` 拿到最多 `SLUG_MAX`(60)字符,再拼 `${prefix}-`,
|
|
24
|
+
总长可达 64。`saveCanvas` → `canvasPath` 在落盘路上又 slugify 一次,砍回 60。
|
|
25
|
+
**前缀那 4 个字符的预算从来没被算进去。**
|
|
26
|
+
|
|
27
|
+
`canvasPath` 里那处「slug 与原名不同就拒绝」的检查本该拦住它,但它只在名字
|
|
28
|
+
「像路径」(含 `/`、`\` 或 `..`)时抛错 —— 截断不像路径,于是静默放过。
|
|
29
|
+
|
|
30
|
+
影响是**报错的句柄,不是损坏的数据**:前缀仍让每个画布互不相同,
|
|
31
|
+
两个前 60 字符相同的长 goal 得到 `000-`/`001-` 两个独立文件,各自节点都在;
|
|
32
|
+
`update`/`delete` 传 `open` 报的长名字也照样能用,因为 `canvasPath` 的截断是幂等的。
|
|
33
|
+
坏的只有一件事:调用方拿到的名字,`list` 永远不会显示,磁盘上也不存在。
|
|
34
|
+
|
|
35
|
+
修法是把名字组装收进 `canvas-format.ts` 的新函数 `canvasName(prefix, goal)` ——
|
|
36
|
+
让它紧挨着自己必须遵守的那个上限,而不是让 handler 去做预算算术:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export function canvasName(prefix: string, goal: string): string {
|
|
40
|
+
const head = `${prefix}-`;
|
|
41
|
+
const slug = slugify(goal, SLUG_MAX - head.length);
|
|
42
|
+
return slug ? `${head}${slug}` : `${head}task`;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`slugify` 因此多一个可选的 `maxLength`,**向内钳制到 `SLUG_MAX`** ——
|
|
47
|
+
调用方能收紧上限,不能放宽它,否则「每个画布文件名都装得进 `SLUG_MAX`」
|
|
48
|
+
这个不变量就成了各调用点的自选项。截断后仍走一次尾部连字符清理,
|
|
49
|
+
所以 `000-abc-.mmd` 这种名字不会出现。
|
|
50
|
+
|
|
51
|
+
### 🧪 测试
|
|
52
|
+
|
|
53
|
+
canvas-format 单测 41 → 50,canvas e2e 31 → 36 项。
|
|
54
|
+
|
|
55
|
+
核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
|
|
56
|
+
跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
|
|
57
|
+
长度只是症状 —— `canvasPath` 会再 slugify 一次,任何不是不动点的名字都是
|
|
58
|
+
「告诉了调用方、但文件系统从没见过」的名字。
|
|
59
|
+
|
|
60
|
+
e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须等于
|
|
61
|
+
`list` 显示的名字、等于磁盘上存在的文件。反向验证还原实现后 3 项变红
|
|
62
|
+
(长度 64、磁盘上无此名、`list` 显示另一个名字)。
|
|
63
|
+
|
|
64
|
+
另两项(`reported name still routes to its canvas` / `... to delete`)
|
|
65
|
+
在修复前后都通过 —— `canvasPath` 截断幂等使然。**保留但改名如实**:
|
|
66
|
+
它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
|
|
67
|
+
这条回归会被抓到」,不是这次的不变量。
|
|
68
|
+
|
|
69
|
+
门禁:vitest 520 passed、tsc、canvas e2e 36 项、pack 契约 104 文件、
|
|
70
|
+
schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
|
|
71
|
+
lite smoke、host-env smoke、spread smoke。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
9
75
|
## [3.1.6] - 2026-08-26
|
|
10
76
|
|
|
11
77
|
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
package/dist/canvas-format.d.ts
CHANGED
|
@@ -45,8 +45,31 @@ export declare const SUMMARY_MAX = 120;
|
|
|
45
45
|
*
|
|
46
46
|
* Returns "" when nothing usable remains (a purely CJK title, for instance);
|
|
47
47
|
* callers must have a fallback and must not write a file named "".
|
|
48
|
+
*
|
|
49
|
+
* `maxLength` narrows the cap for a caller that must leave room for a prefix.
|
|
50
|
+
* It is clamped to `SLUG_MAX`, so no caller can widen the invariant that every
|
|
51
|
+
* canvas filename fits in `SLUG_MAX` characters.
|
|
52
|
+
*/
|
|
53
|
+
export declare function slugify(input: string, maxLength?: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Compose a canvas filename from its prefix and the task goal.
|
|
56
|
+
*
|
|
57
|
+
* The prefix has to be part of the length budget, and it was not: `actionOpen`
|
|
58
|
+
* used to slug the goal to `SLUG_MAX` and then prepend `${prefix}-`, producing a
|
|
59
|
+
* name up to `SLUG_MAX + 4` characters. `canvasPath` slugs again on the way to
|
|
60
|
+
* disk and cut it back to `SLUG_MAX`, so the name reported by `open` was not the
|
|
61
|
+
* name `list` would show, and not the file that existed. Nothing was overwritten
|
|
62
|
+
* — the numeric prefix still made every canvas distinct — but the handle handed
|
|
63
|
+
* back to the caller was wrong.
|
|
64
|
+
*
|
|
65
|
+
* So the composition lives here, next to the cap it has to respect: slug the
|
|
66
|
+
* goal into what is left after the prefix, and the result is already a fixed
|
|
67
|
+
* point of `slugify`.
|
|
68
|
+
*
|
|
69
|
+
* A goal with no ASCII (a purely CJK title) slugs to "", so fall back to
|
|
70
|
+
* `<prefix>-task` rather than writing a file named after the prefix alone.
|
|
48
71
|
*/
|
|
49
|
-
export declare function
|
|
72
|
+
export declare function canvasName(prefix: string, goal: string): string;
|
|
50
73
|
/**
|
|
51
74
|
* Make text safe to sit inside a Mermaid `["..."]` label.
|
|
52
75
|
*
|
package/dist/canvas-format.js
CHANGED
|
@@ -36,12 +36,40 @@ const SLUG_MAX = 60;
|
|
|
36
36
|
*
|
|
37
37
|
* Returns "" when nothing usable remains (a purely CJK title, for instance);
|
|
38
38
|
* callers must have a fallback and must not write a file named "".
|
|
39
|
+
*
|
|
40
|
+
* `maxLength` narrows the cap for a caller that must leave room for a prefix.
|
|
41
|
+
* It is clamped to `SLUG_MAX`, so no caller can widen the invariant that every
|
|
42
|
+
* canvas filename fits in `SLUG_MAX` characters.
|
|
39
43
|
*/
|
|
40
|
-
export function slugify(input) {
|
|
44
|
+
export function slugify(input, maxLength = SLUG_MAX) {
|
|
45
|
+
const cap = Math.max(0, Math.min(maxLength, SLUG_MAX));
|
|
41
46
|
const lowered = String(input ?? "").toLowerCase();
|
|
42
47
|
const kept = lowered.replace(/[^a-z0-9]+/g, "-");
|
|
43
48
|
const collapsed = kept.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
44
|
-
return collapsed.slice(0,
|
|
49
|
+
return collapsed.slice(0, cap).replace(/-+$/g, "");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compose a canvas filename from its prefix and the task goal.
|
|
53
|
+
*
|
|
54
|
+
* The prefix has to be part of the length budget, and it was not: `actionOpen`
|
|
55
|
+
* used to slug the goal to `SLUG_MAX` and then prepend `${prefix}-`, producing a
|
|
56
|
+
* name up to `SLUG_MAX + 4` characters. `canvasPath` slugs again on the way to
|
|
57
|
+
* disk and cut it back to `SLUG_MAX`, so the name reported by `open` was not the
|
|
58
|
+
* name `list` would show, and not the file that existed. Nothing was overwritten
|
|
59
|
+
* — the numeric prefix still made every canvas distinct — but the handle handed
|
|
60
|
+
* back to the caller was wrong.
|
|
61
|
+
*
|
|
62
|
+
* So the composition lives here, next to the cap it has to respect: slug the
|
|
63
|
+
* goal into what is left after the prefix, and the result is already a fixed
|
|
64
|
+
* point of `slugify`.
|
|
65
|
+
*
|
|
66
|
+
* A goal with no ASCII (a purely CJK title) slugs to "", so fall back to
|
|
67
|
+
* `<prefix>-task` rather than writing a file named after the prefix alone.
|
|
68
|
+
*/
|
|
69
|
+
export function canvasName(prefix, goal) {
|
|
70
|
+
const head = `${prefix}-`;
|
|
71
|
+
const slug = slugify(goal, SLUG_MAX - head.length);
|
|
72
|
+
return slug ? `${head}${slug}` : `${head}task`;
|
|
45
73
|
}
|
|
46
74
|
// ============================================================================
|
|
47
75
|
// escapeLabel
|
package/dist/handlers/canvas.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* compaction, and anchors that make a summary checkable.
|
|
28
28
|
*/
|
|
29
29
|
import { MEMOS_CUBES_DIR } from "../config.js";
|
|
30
|
-
import { NODE_STATUSES, allocateNodeId, countByStatus, formatCanvasHeadline,
|
|
30
|
+
import { NODE_STATUSES, allocateNodeId, canvasName, countByStatus, formatCanvasHeadline, truncateSummary, } from "../canvas-format.js";
|
|
31
31
|
import { CanvasPathError, deleteCanvas, listCanvases, loadCanvas, nextPrefix, saveCanvas, } from "../canvas-store.js";
|
|
32
32
|
import { ERR_PARAM_INVALID, ERR_PARAM_MISSING, ERR_OPERATION_FAILED, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
33
33
|
const STATUS_LIST = NODE_STATUSES.join(", ");
|
|
@@ -118,10 +118,9 @@ function actionOpen(cubeId, args) {
|
|
|
118
118
|
if (prefix === null) {
|
|
119
119
|
return errorResponse(`Cube '${cubeId}' already holds 1000 canvases (000-999)`, ERR_OPERATION_FAILED, ["Remove finished canvas files from the cube's canvas/ directory"]);
|
|
120
120
|
}
|
|
121
|
-
// The
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
const name = goalSlug ? `${prefix}-${goalSlug}` : `${prefix}-task`;
|
|
121
|
+
// The prefix is part of the filename budget, so composing the name is
|
|
122
|
+
// `canvasName`'s job — see the note there on why doing it inline was wrong.
|
|
123
|
+
const name = canvasName(prefix, goal);
|
|
125
124
|
const now = new Date().toISOString();
|
|
126
125
|
const canvas = {
|
|
127
126
|
prefix,
|