@yottameta/yotta-memory 0.1.0 → 0.2.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 +9 -7
- package/SKILL.md +11 -10
- package/bin/yotta-memory.js +568 -330
- package/package.json +2 -2
- package/references/protocol.md +10 -4
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
# yotta-memory
|
|
1
|
+
# 元忆(yotta-memory)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff / 回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离——谁该读、谁不该读,由机制而非 AI 自觉决定。
|
|
4
4
|
|
|
5
5
|
## 这是什么
|
|
6
6
|
|
|
7
7
|
- **文件式记忆标准**:记忆 = Markdown + YAML frontmatter 文件,git 可版本化,任何智能体可读,数据主权在用户本地。
|
|
8
8
|
- **零依赖**:无 daemon / 无数据库 / 无向量库,Node.js 自带即可运行。
|
|
9
9
|
- **类型体系**:FACT(事实,公共共享)/ PREF(偏好)/ BOUND(边界)/ COMMIT(承诺)——后三者私密、per-agent 隔离。
|
|
10
|
-
- **双级存储**:用户级
|
|
10
|
+
- **双级存储**:用户级 ~/.yottamemory/ + 项目级 .yottamemory/。
|
|
11
|
+
- **有权限边界**:FACT 进公共区共享;PREF / BOUND / COMMIT 进私密区、按 agent 隔离;读取按 scope/owner 分区,默认只返当前 agent 可读项。
|
|
11
12
|
|
|
12
13
|
适用场景:跨会话续测、个人偏好与边界沉淀、多智能体协作、项目交接、收工归档。
|
|
13
14
|
|
|
@@ -45,10 +46,11 @@ bash install.sh --dir /path/to/skills
|
|
|
45
46
|
| 命令 | 作用 |
|
|
46
47
|
|---|---|
|
|
47
48
|
| `yotta-memory init [--project]` | 初始化记忆库(默认用户级 `~/.yottamemory/`)|
|
|
48
|
-
| `yotta-memory remember <type> <subject> <statement
|
|
49
|
-
| `yotta-memory recall [关键词] [--type T] [--limit N]` |
|
|
50
|
-
| `yotta-memory forget <文件>` |
|
|
51
|
-
| `yotta-memory archive [--days 180]` |
|
|
49
|
+
| `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入记忆(同 subject+statement 自动更新;--owner 标注归属)|
|
|
50
|
+
| `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all]` | 检索记忆(索引+TF 打分,读取分区过滤;项目级优先)|
|
|
51
|
+
| `yotta-memory forget <文件>` | 删除一条记忆(按类型目录路径或文件名)|
|
|
52
|
+
| `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
|
|
53
|
+
| `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
|
|
52
54
|
| `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
|
|
53
55
|
|
|
54
56
|
类型:`FACT`(事实,公共共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺)。
|
package/SKILL.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-memory
|
|
3
|
-
description: "
|
|
4
|
-
version: 0.1
|
|
3
|
+
description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档"
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# yotta-memory
|
|
8
|
+
# yotta-memory(元忆)— 有权限边界的文件式智能体记忆
|
|
9
9
|
|
|
10
|
-
>
|
|
11
|
-
> 版本:0.1
|
|
10
|
+
> 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
|
|
11
|
+
> 版本:0.2.1 | 最后更新:2026-08-23
|
|
12
12
|
|
|
13
13
|
## 这是什么
|
|
14
14
|
|
|
@@ -36,15 +36,16 @@ license: MIT
|
|
|
36
36
|
| 命令 | 作用 |
|
|
37
37
|
|---|---|
|
|
38
38
|
| `yotta-memory init [--project]` | 初始化记忆库(默认用户级)|
|
|
39
|
-
| `yotta-memory remember <type> <subject> <statement
|
|
40
|
-
| `yotta-memory recall [关键词] [--type T] [--limit N]` |
|
|
41
|
-
| `yotta-memory forget <文件>` |
|
|
42
|
-
| `yotta-memory archive [--days 180]` |
|
|
39
|
+
| `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入(同 subject+statement 自动更新;--owner 标注归属,默认取 agent id)|
|
|
40
|
+
| `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all]` | 检索(索引+TF 打分,读取分区过滤;项目级优先)|
|
|
41
|
+
| `yotta-memory forget <文件>` | 删除(按类型目录路径或文件名)|
|
|
42
|
+
| `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
|
|
43
|
+
| `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
|
|
43
44
|
| `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
|
|
44
45
|
|
|
45
46
|
## 存储格式(摘要)
|
|
46
47
|
|
|
47
|
-
`<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable
|
|
48
|
+
`<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。顶层另有 `index.json`(反向索引 + TF 打分)。
|
|
48
49
|
|
|
49
50
|
## 渐进披露
|
|
50
51
|
|
package/bin/yotta-memory.js
CHANGED
|
@@ -1,331 +1,569 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// yotta-memory
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// yotta-memory(元忆): 有权限边界的文件式智能体记忆 CLI(零依赖)
|
|
3
|
+
// v0.2 增强:① index.json 索引+TF 打分 ② 质量分+盖棺分(access_count/last_accessed)③ 读取分区(scope/owner)
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const VERSION = '0.2.1';
|
|
11
|
+
const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
|
|
12
|
+
const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
|
|
13
|
+
const ARCHIVE_DIR = '.archive';
|
|
14
|
+
const INDEX_FILE = 'index.json';
|
|
15
|
+
const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
|
|
16
|
+
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'access_count', 'last_accessed'];
|
|
17
|
+
|
|
18
|
+
function userRoot() {
|
|
19
|
+
return process.env.YOTTA_MEMORY_HOME || path.join(os.homedir(), '.yottamemory');
|
|
20
|
+
}
|
|
21
|
+
function projectRoot() {
|
|
22
|
+
return path.join(process.cwd(), '.yottamemory');
|
|
23
|
+
}
|
|
24
|
+
function today() {
|
|
25
|
+
const d = new Date();
|
|
26
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
27
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
28
|
+
}
|
|
29
|
+
function currentAgent() {
|
|
30
|
+
return process.env.YOTTA_AGENT_ID || process.env.AGENT_ID || '';
|
|
31
|
+
}
|
|
32
|
+
function typeDir(type) {
|
|
33
|
+
const t = String(type).toUpperCase();
|
|
34
|
+
if (!TYPE_DIRS[t]) {
|
|
35
|
+
console.error('未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
return TYPE_DIRS[t];
|
|
39
|
+
}
|
|
40
|
+
function defaultScope(type) {
|
|
41
|
+
return PRIVATE_TYPES.indexOf(String(type).toUpperCase()) === -1 ? 'public' : 'private';
|
|
42
|
+
}
|
|
43
|
+
function parseFrontmatter(text) {
|
|
44
|
+
const m = text.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
45
|
+
if (!m) return { meta: {}, body: text };
|
|
46
|
+
const meta = {};
|
|
47
|
+
for (const line of m[1].split('\n')) {
|
|
48
|
+
const i = line.indexOf(':');
|
|
49
|
+
if (i === -1) continue;
|
|
50
|
+
let k = line.slice(0, i).trim();
|
|
51
|
+
let v = line.slice(i + 1).trim();
|
|
52
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
53
|
+
v = v.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
meta[k] = v;
|
|
56
|
+
}
|
|
57
|
+
return { meta: meta, body: text.slice(m[0][0].length + m[0].length) };
|
|
58
|
+
}
|
|
59
|
+
function escapeYaml(v) {
|
|
60
|
+
return String(v).replace(/\n/g, ' ').replace(/"/g, '\\"');
|
|
61
|
+
}
|
|
62
|
+
function parseTags(v) {
|
|
63
|
+
if (Array.isArray(v)) return v.map(String);
|
|
64
|
+
if (typeof v === 'string') {
|
|
65
|
+
let s = v.trim();
|
|
66
|
+
if (s.startsWith('[') && s.endsWith(']')) s = s.slice(1, -1);
|
|
67
|
+
return s.split(',').map(function (x) { return x.trim(); }).filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
function tokenize(s) {
|
|
72
|
+
s = String(s || '').toLowerCase();
|
|
73
|
+
const toks = [];
|
|
74
|
+
const latin = s.match(/[a-z0-9_]+/g);
|
|
75
|
+
if (latin) for (const t of latin) toks.push(t);
|
|
76
|
+
const han = s.match(/[\u4e00-\u9fa5]+/g);
|
|
77
|
+
if (han) {
|
|
78
|
+
for (const seg of han) {
|
|
79
|
+
if (seg.length === 1) toks.push(seg);
|
|
80
|
+
else for (let i = 0; i < seg.length - 1; i++) toks.push(seg.slice(i, i + 2));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return toks;
|
|
84
|
+
}
|
|
85
|
+
function buildTokens(subject, statement, tags) {
|
|
86
|
+
const toks = tokenize(subject + ' ' + statement + ' ' + tags.join(' '));
|
|
87
|
+
const m = {};
|
|
88
|
+
for (const t of toks) m[t] = (m[t] || 0) + 1;
|
|
89
|
+
return m;
|
|
90
|
+
}
|
|
91
|
+
function frontmatterToText(meta, body) {
|
|
92
|
+
const lines = ['---'];
|
|
93
|
+
const ordered = FIELD_ORDER.filter(function (k) { return meta[k] !== undefined && meta[k] !== null && meta[k] !== ''; });
|
|
94
|
+
const extra = Object.keys(meta).filter(function (k) { return FIELD_ORDER.indexOf(k) === -1 && meta[k] !== undefined && meta[k] !== null && meta[k] !== ''; });
|
|
95
|
+
for (const k of ordered.concat(extra)) {
|
|
96
|
+
const v = meta[k];
|
|
97
|
+
if (Array.isArray(v)) lines.push(k + ': ' + JSON.stringify(v));
|
|
98
|
+
else if (typeof v === 'number') lines.push(k + ': ' + v);
|
|
99
|
+
else lines.push(k + ': ' + escapeYaml(v));
|
|
100
|
+
}
|
|
101
|
+
lines.push('---', '');
|
|
102
|
+
const b = body === undefined || body === null ? '' : body;
|
|
103
|
+
return lines.join('\n') + String(b).replace(/^\n+/, '') + '\n';
|
|
104
|
+
}
|
|
105
|
+
function ensureInit(root) {
|
|
106
|
+
fs.mkdirSync(path.join(root, 'facts'), { recursive: true });
|
|
107
|
+
fs.mkdirSync(path.join(root, 'prefs'), { recursive: true });
|
|
108
|
+
fs.mkdirSync(path.join(root, 'bounds'), { recursive: true });
|
|
109
|
+
fs.mkdirSync(path.join(root, 'commits'), { recursive: true });
|
|
110
|
+
fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
|
|
111
|
+
const readme = path.join(root, 'README.md');
|
|
112
|
+
if (!fs.existsSync(readme)) {
|
|
113
|
+
fs.writeFileSync(readme, '# yotta-memory(元忆)记忆库\n\n有权限边界的文件式智能体记忆存储目录。结构:facts/ prefs/ bounds/ commits/ .archive/。\n', 'utf8');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function nextSeq(dir) {
|
|
117
|
+
let max = 0;
|
|
118
|
+
if (fs.existsSync(dir)) {
|
|
119
|
+
for (const f of fs.readdirSync(dir)) {
|
|
120
|
+
const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md$/);
|
|
121
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return String(max + 1).padStart(4, '0');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- 索引(index.json)----
|
|
128
|
+
function indexPath(root) { return path.join(root, INDEX_FILE); }
|
|
129
|
+
function loadIndex(root) {
|
|
130
|
+
const p = indexPath(root);
|
|
131
|
+
if (!fs.existsSync(p)) return null;
|
|
132
|
+
try {
|
|
133
|
+
const d = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
134
|
+
if (d && Array.isArray(d.entries)) return d.entries;
|
|
135
|
+
} catch (e) { /* ignore */ }
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function getIndex(root) { return loadIndex(root) || []; }
|
|
139
|
+
function saveIndex(root, entries) {
|
|
140
|
+
const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
|
|
141
|
+
fs.writeFileSync(indexPath(root), JSON.stringify({ version: 2, updated: today(), entries: clean }, null, 2), 'utf8');
|
|
142
|
+
}
|
|
143
|
+
function buildIndex(root) {
|
|
144
|
+
const entries = [];
|
|
145
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
146
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
147
|
+
if (!fs.existsSync(dir)) continue;
|
|
148
|
+
for (const f of fs.readdirSync(dir)) {
|
|
149
|
+
const fp = path.join(dir, f);
|
|
150
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
151
|
+
entries.push(readEntry(fp, root));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
saveIndex(root, entries);
|
|
155
|
+
return entries;
|
|
156
|
+
}
|
|
157
|
+
function ensureIndex(root) {
|
|
158
|
+
const idx = loadIndex(root);
|
|
159
|
+
if (idx) return idx;
|
|
160
|
+
return buildIndex(root);
|
|
161
|
+
}
|
|
162
|
+
function upsertIndexEntry(root, entry) {
|
|
163
|
+
const idx = getIndex(root);
|
|
164
|
+
const i = idx.findIndex(function (e) { return e.file === entry.file; });
|
|
165
|
+
if (i >= 0) idx[i] = entry; else idx.push(entry);
|
|
166
|
+
saveIndex(root, idx);
|
|
167
|
+
}
|
|
168
|
+
function removeIndexEntry(root, file) {
|
|
169
|
+
const idx = getIndex(root);
|
|
170
|
+
const i = idx.findIndex(function (e) { return e.file === file; });
|
|
171
|
+
if (i >= 0) { idx.splice(i, 1); saveIndex(root, idx); }
|
|
172
|
+
}
|
|
173
|
+
function removeIndexEntries(root, files) {
|
|
174
|
+
const set = new Set(files);
|
|
175
|
+
const idx = getIndex(root);
|
|
176
|
+
const next = idx.filter(function (e) { return !set.has(e.file); });
|
|
177
|
+
if (next.length !== idx.length) saveIndex(root, next);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- 读取 / 写入记忆文件 ----
|
|
181
|
+
function readEntry(fp, root) {
|
|
182
|
+
const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
|
|
183
|
+
const meta = parsed.meta;
|
|
184
|
+
const type = (meta.type || 'FACT').toUpperCase();
|
|
185
|
+
const subject = meta.subject || '';
|
|
186
|
+
const statement = meta.statement || '';
|
|
187
|
+
const tags = parseTags(meta.tags);
|
|
188
|
+
return {
|
|
189
|
+
file: path.relative(root, fp).replace(/\\/g, '/'),
|
|
190
|
+
type: type,
|
|
191
|
+
scope: meta.scope || defaultScope(type),
|
|
192
|
+
owner: meta.owner || '',
|
|
193
|
+
subject: subject,
|
|
194
|
+
statement: statement,
|
|
195
|
+
tags: tags,
|
|
196
|
+
confidence: parseFloat(meta.confidence || 1.0),
|
|
197
|
+
created: meta.created || '',
|
|
198
|
+
updated: meta.updated || '',
|
|
199
|
+
access_count: parseInt(meta.access_count || '0', 10) || 0,
|
|
200
|
+
last_accessed: meta.last_accessed || '',
|
|
201
|
+
immutable: meta.immutable === 'true',
|
|
202
|
+
tokens: buildTokens(subject, statement, tags),
|
|
203
|
+
meta: meta,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function rewriteFrontmatter(fp, patch) {
|
|
207
|
+
const txt = fs.readFileSync(fp, 'utf8');
|
|
208
|
+
const parsed = parseFrontmatter(txt);
|
|
209
|
+
const meta = Object.assign({}, parsed.meta);
|
|
210
|
+
for (const k of Object.keys(patch)) meta[k] = patch[k];
|
|
211
|
+
fs.writeFileSync(fp, frontmatterToText(meta, parsed.body), 'utf8');
|
|
212
|
+
}
|
|
213
|
+
function bumpReadMeta(root, relFiles) {
|
|
214
|
+
const now = today();
|
|
215
|
+
for (const rel of relFiles) {
|
|
216
|
+
const fp = path.join(root, rel);
|
|
217
|
+
if (!fs.existsSync(fp)) continue;
|
|
218
|
+
const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
|
|
219
|
+
const meta = parsed.meta;
|
|
220
|
+
if (meta.immutable === 'true') continue;
|
|
221
|
+
const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
|
|
222
|
+
rewriteFrontmatter(fp, { access_count: acc, last_accessed: now });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function touchIndex(root, relFiles) {
|
|
226
|
+
const set = new Set(relFiles);
|
|
227
|
+
const idx = getIndex(root);
|
|
228
|
+
let dirty = false;
|
|
229
|
+
const now = today();
|
|
230
|
+
for (const e of idx) {
|
|
231
|
+
if (set.has(e.file)) { e.access_count = (parseInt(e.access_count, 10) || 0) + 1; e.last_accessed = now; dirty = true; }
|
|
232
|
+
}
|
|
233
|
+
if (dirty) saveIndex(root, idx);
|
|
234
|
+
}
|
|
235
|
+
function daysBetween(a, b) {
|
|
236
|
+
const da = new Date(a);
|
|
237
|
+
const db = new Date(b);
|
|
238
|
+
if (isNaN(da.getTime()) || isNaN(db.getTime())) return 99999;
|
|
239
|
+
return Math.round((db.getTime() - da.getTime()) / (24 * 60 * 60 * 1000));
|
|
240
|
+
}
|
|
241
|
+
function vitality(meta) {
|
|
242
|
+
const conf = parseFloat(meta.confidence || 1.0);
|
|
243
|
+
const acc = parseInt(meta.access_count || '0', 10) || 0;
|
|
244
|
+
const last = meta.last_accessed || '';
|
|
245
|
+
const accScore = Math.min(acc, 10) / 10;
|
|
246
|
+
let recency = 0;
|
|
247
|
+
if (last) { const d = daysBetween(last, today()); recency = Math.max(0, 1 - Math.floor(d / 30) * 0.2); }
|
|
248
|
+
return 0.4 * conf + 0.3 * accScore + 0.3 * recency;
|
|
249
|
+
}
|
|
250
|
+
function isReadable(entry, agent, ownerFilter, all) {
|
|
251
|
+
if (entry.scope === 'public') return true;
|
|
252
|
+
if (all) return true;
|
|
253
|
+
const owner = entry.owner || '';
|
|
254
|
+
if (!owner) return true;
|
|
255
|
+
if (ownerFilter && owner === ownerFilter) return true;
|
|
256
|
+
if (agent && owner === agent) return true;
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---- 命令 ----
|
|
261
|
+
function cmdInit(opts) {
|
|
262
|
+
const root = opts.project ? projectRoot() : userRoot();
|
|
263
|
+
ensureInit(root);
|
|
264
|
+
console.log('已初始化记忆库: ' + root);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cmdRemember(type, subject, statement, opts) {
|
|
268
|
+
const root = userRoot();
|
|
269
|
+
ensureInit(root);
|
|
270
|
+
const t = String(type).toUpperCase();
|
|
271
|
+
const dir = path.join(root, typeDir(t));
|
|
272
|
+
const stmt = String(statement || '').trim();
|
|
273
|
+
const subj = String(subject || '').trim();
|
|
274
|
+
if (!stmt) { console.error('statement 不能为空'); process.exit(2); }
|
|
275
|
+
if (!subj) { console.error('subject 不能为空'); process.exit(2); }
|
|
276
|
+
const owner = opts.owner || currentAgent();
|
|
277
|
+
const scope = opts.scope || defaultScope(t);
|
|
278
|
+
if (fs.existsSync(dir)) {
|
|
279
|
+
for (const f of fs.readdirSync(dir)) {
|
|
280
|
+
const fp = path.join(dir, f);
|
|
281
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
282
|
+
const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
|
|
283
|
+
const meta = parsed.meta;
|
|
284
|
+
if ((meta.type || '').toUpperCase() === t && meta.subject === subj && meta.statement === stmt) {
|
|
285
|
+
const patch = { updated: today() };
|
|
286
|
+
if (owner && !meta.owner) patch.owner = owner;
|
|
287
|
+
if (scope && !meta.scope) patch.scope = scope;
|
|
288
|
+
rewriteFrontmatter(fp, patch);
|
|
289
|
+
upsertIndexEntry(root, readEntry(fp, root));
|
|
290
|
+
console.log('已更新: ' + fp);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const seq = nextSeq(dir);
|
|
296
|
+
const file = path.join(dir, today() + '-' + seq + '.md');
|
|
297
|
+
const rec = {
|
|
298
|
+
type: t, subject: subj, statement: stmt,
|
|
299
|
+
confidence: 1.0, created: today(), updated: today(),
|
|
300
|
+
tags: [], immutable: false,
|
|
301
|
+
scope: scope, owner: owner, access_count: 0, last_accessed: '',
|
|
302
|
+
};
|
|
303
|
+
fs.writeFileSync(file, frontmatterToText(rec, stmt), 'utf8');
|
|
304
|
+
upsertIndexEntry(root, readEntry(file, root));
|
|
305
|
+
console.log('已记录: ' + file);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function cmdRecall(query, opts) {
|
|
309
|
+
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
310
|
+
if (!roots.length) {
|
|
311
|
+
console.log('记忆库不存在,请先运行: yotta-memory init');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const limit = opts.limit || 50;
|
|
315
|
+
const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
|
|
316
|
+
const q = query ? String(query).toLowerCase() : '';
|
|
317
|
+
const agent = opts.agent || currentAgent();
|
|
318
|
+
const ownerFilter = opts.owner || '';
|
|
319
|
+
const all = !!opts.all;
|
|
320
|
+
const hits = [];
|
|
321
|
+
for (const root of roots) {
|
|
322
|
+
const entries = ensureIndex(root);
|
|
323
|
+
for (const e of entries) {
|
|
324
|
+
if (onlyType && e.type !== onlyType) continue;
|
|
325
|
+
if (!isReadable(e, agent, ownerFilter, all)) continue;
|
|
326
|
+
let score = 0;
|
|
327
|
+
if (q) {
|
|
328
|
+
const qtoks = tokenize(q);
|
|
329
|
+
for (const tt of qtoks) { if (e.tokens && e.tokens[tt]) score += e.tokens[tt]; }
|
|
330
|
+
if (score === 0) {
|
|
331
|
+
const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase();
|
|
332
|
+
if (hay.indexOf(q) !== -1) score = 1;
|
|
333
|
+
}
|
|
334
|
+
if (score === 0) continue;
|
|
335
|
+
} else {
|
|
336
|
+
score = 1;
|
|
337
|
+
}
|
|
338
|
+
hits.push({ entry: e, score: score, root: root });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
hits.sort(function (a, b) {
|
|
342
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
343
|
+
const pa = a.root === projectRoot() ? 0 : 1;
|
|
344
|
+
const pb = b.root === projectRoot() ? 0 : 1;
|
|
345
|
+
if (pa !== pb) return pa - pb;
|
|
346
|
+
return String(b.entry.created).localeCompare(String(a.entry.created));
|
|
347
|
+
});
|
|
348
|
+
const shown = hits.slice(0, limit);
|
|
349
|
+
if (!shown.length) { console.log('无匹配记忆。'); return; }
|
|
350
|
+
const touchRel = shown.filter(function (h) { return !h.entry.immutable; }).map(function (h) { return h.entry.file; });
|
|
351
|
+
if (touchRel.length) {
|
|
352
|
+
for (const root of roots) {
|
|
353
|
+
bumpReadMeta(root, touchRel);
|
|
354
|
+
touchIndex(root, touchRel);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
console.log('共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):');
|
|
358
|
+
for (const h of shown) {
|
|
359
|
+
console.log('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
|
|
360
|
+
console.log(' ' + path.join(h.root, h.entry.file));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function cmdForget(fileRef) {
|
|
365
|
+
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
366
|
+
const ref = String(fileRef || '').replace(/\\/g, '/');
|
|
367
|
+
const slash = ref.indexOf('/');
|
|
368
|
+
const dynTypeDir = slash === -1 ? '' : ref.slice(0, slash);
|
|
369
|
+
const dynBase = slash === -1 ? ref : ref.slice(slash + 1);
|
|
370
|
+
let target = null, targetRoot = null, targetRel = null;
|
|
371
|
+
for (const root of roots) {
|
|
372
|
+
if (dynTypeDir) {
|
|
373
|
+
const dir = path.join(root, dynTypeDir);
|
|
374
|
+
if (!fs.existsSync(dir)) continue;
|
|
375
|
+
const cand = path.join(dir, dynBase);
|
|
376
|
+
if (fs.existsSync(cand)) {
|
|
377
|
+
target = cand; targetRoot = root;
|
|
378
|
+
targetRel = dynTypeDir + '/' + path.basename(dynBase);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
384
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
385
|
+
if (!fs.existsSync(dir)) continue;
|
|
386
|
+
const cand = path.join(dir, dynBase);
|
|
387
|
+
if (fs.existsSync(cand)) {
|
|
388
|
+
target = cand; targetRoot = root;
|
|
389
|
+
targetRel = TYPE_DIRS[t] + '/' + path.basename(dynBase);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (target) break;
|
|
394
|
+
}
|
|
395
|
+
if (!target) { console.error('未找到记忆文件: ' + fileRef); process.exit(2); }
|
|
396
|
+
fs.unlinkSync(target);
|
|
397
|
+
if (targetRoot) removeIndexEntry(targetRoot, targetRel);
|
|
398
|
+
console.log('已删除: ' + target);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function cmdArchive(opts) {
|
|
402
|
+
const days = opts.days || 180;
|
|
403
|
+
const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : 0.4;
|
|
404
|
+
const root = userRoot();
|
|
405
|
+
if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
|
|
406
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
407
|
+
let moved = 0;
|
|
408
|
+
const movedFiles = [];
|
|
409
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
410
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
411
|
+
if (!fs.existsSync(dir)) continue;
|
|
412
|
+
for (const f of fs.readdirSync(dir)) {
|
|
413
|
+
const fp = path.join(dir, f);
|
|
414
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
415
|
+
const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
|
|
416
|
+
if (meta.immutable === 'true') continue;
|
|
417
|
+
if (!meta.created) continue;
|
|
418
|
+
const createdTs = new Date(meta.created).getTime();
|
|
419
|
+
if (isNaN(createdTs)) continue;
|
|
420
|
+
if (vitality(meta) < threshold && createdTs < cutoff) {
|
|
421
|
+
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
|
|
422
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
423
|
+
fs.renameSync(fp, path.join(destDir, f));
|
|
424
|
+
movedFiles.push(TYPE_DIRS[t] + '/' + f);
|
|
425
|
+
moved++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (movedFiles.length) removeIndexEntries(root, movedFiles);
|
|
430
|
+
console.log('已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function cmdReindex() {
|
|
434
|
+
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
435
|
+
if (!roots.length) { console.log('记忆库不存在。'); return; }
|
|
436
|
+
for (const root of roots) {
|
|
437
|
+
const n = buildIndex(root).length;
|
|
438
|
+
console.log('已重建索引 ' + root + '(' + n + ' 条)');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function collectAll(root) {
|
|
443
|
+
const out = [];
|
|
444
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
445
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
446
|
+
if (!fs.existsSync(dir)) continue;
|
|
447
|
+
for (const f of fs.readdirSync(dir)) {
|
|
448
|
+
const fp = path.join(dir, f);
|
|
449
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
450
|
+
const e = readEntry(fp, root);
|
|
451
|
+
out.push({ file: e.file, meta: e.meta });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function cmdExport(outPath) {
|
|
458
|
+
const root = userRoot();
|
|
459
|
+
if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
|
|
460
|
+
const data = { exported: today(), version: VERSION, memories: collectAll(root) };
|
|
461
|
+
const target = outPath || 'yottamemory-export-' + today() + '.json';
|
|
462
|
+
fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
|
|
463
|
+
console.log('已导出 ' + data.memories.length + ' 条记忆 -> ' + target);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function cmdImport(src) {
|
|
467
|
+
if (!src || !fs.existsSync(src)) { console.error('请提供 JSON 文件路径'); process.exit(2); }
|
|
468
|
+
const data = JSON.parse(fs.readFileSync(src, 'utf8'));
|
|
469
|
+
if (!data.memories || !Array.isArray(data.memories)) { console.error('JSON 格式不正确(缺少 memories 数组)'); process.exit(2); }
|
|
470
|
+
const root = userRoot();
|
|
471
|
+
ensureInit(root);
|
|
472
|
+
let n = 0;
|
|
473
|
+
for (const item of data.memories) {
|
|
474
|
+
const meta = item.meta || {};
|
|
475
|
+
const t = (meta.type || 'FACT').toUpperCase();
|
|
476
|
+
if (!TYPE_DIRS[t]) continue;
|
|
477
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
478
|
+
let seq = nextSeq(dir);
|
|
479
|
+
let file = path.join(dir, today() + '-' + seq + '.md');
|
|
480
|
+
while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
|
|
481
|
+
const rec = {
|
|
482
|
+
type: t,
|
|
483
|
+
subject: meta.subject || '',
|
|
484
|
+
statement: meta.statement || '',
|
|
485
|
+
confidence: parseFloat(meta.confidence || 1.0),
|
|
486
|
+
created: meta.created || today(),
|
|
487
|
+
updated: meta.updated || today(),
|
|
488
|
+
tags: parseTags(meta.tags),
|
|
489
|
+
immutable: meta.immutable === true || meta.immutable === 'true',
|
|
490
|
+
scope: meta.scope || defaultScope(t),
|
|
491
|
+
owner: meta.owner || '',
|
|
492
|
+
access_count: parseInt(meta.access_count || '0', 10) || 0,
|
|
493
|
+
last_accessed: meta.last_accessed || '',
|
|
494
|
+
};
|
|
495
|
+
fs.writeFileSync(file, frontmatterToText(rec, rec.statement), 'utf8');
|
|
496
|
+
upsertIndexEntry(root, readEntry(file, root));
|
|
497
|
+
n++;
|
|
498
|
+
}
|
|
499
|
+
console.log('已导入 ' + n + ' 条记忆 -> ' + root);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function usage() {
|
|
503
|
+
console.log([
|
|
504
|
+
'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
|
|
505
|
+
'',
|
|
506
|
+
'用法:',
|
|
507
|
+
' yotta-memory init [--project] 初始化记忆库(默认用户级 ~/.yottamemory)',
|
|
508
|
+
' yotta-memory remember <type> <subject> <statement> [--owner <id>] 写入记忆',
|
|
509
|
+
' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] 检索(索引+TF,读取分区)',
|
|
510
|
+
' yotta-memory forget <文件或文件名> 删除一条记忆',
|
|
511
|
+
' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
|
|
512
|
+
' yotta-memory reindex 重建索引(手动改文件后用)',
|
|
513
|
+
' yotta-memory export [--out 文件.json] 导出全部记忆',
|
|
514
|
+
' yotta-memory import <文件.json> 导入记忆',
|
|
515
|
+
' yotta-memory --version 版本',
|
|
516
|
+
'',
|
|
517
|
+
'类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
|
|
518
|
+
'环境变量: YOTTA_MEMORY_HOME 覆盖用户级目录; YOTTA_AGENT_ID/AGENT_ID 作为当前 agent 标识(读取分区)',
|
|
519
|
+
'',
|
|
520
|
+
].join('\n'));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function main() {
|
|
524
|
+
const args = process.argv.slice(2);
|
|
525
|
+
if (!args.length) { usage(); return; }
|
|
526
|
+
const first = args[0];
|
|
527
|
+
if (first === '--version' || first === '-v') { console.log(VERSION); return; }
|
|
528
|
+
if (first === '--help' || first === '-h') { usage(); return; }
|
|
529
|
+
const opts = {};
|
|
530
|
+
const positional = [];
|
|
531
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope']);
|
|
532
|
+
for (let i = 1; i < args.length; i++) {
|
|
533
|
+
const a = args[i];
|
|
534
|
+
if (a === '--project') opts.project = true;
|
|
535
|
+
else if (a === '--all') opts.all = true;
|
|
536
|
+
else if (valueOpts.has(a)) {
|
|
537
|
+
const v = args[++i];
|
|
538
|
+
if (a === '--type') opts.type = v;
|
|
539
|
+
else if (a === '--limit') opts.limit = parseInt(v, 10) || 50;
|
|
540
|
+
else if (a === '--days') opts.days = parseInt(v, 10) || 180;
|
|
541
|
+
else if (a === '--threshold') opts.threshold = parseFloat(v);
|
|
542
|
+
else if (a === '--out') opts.out = v;
|
|
543
|
+
else if (a === '--owner') opts.owner = v;
|
|
544
|
+
else if (a === '--agent') opts.agent = v;
|
|
545
|
+
else if (a === '--scope') opts.scope = v;
|
|
546
|
+
} else if (a.startsWith('--')) {
|
|
547
|
+
console.error('未知选项: ' + a);
|
|
548
|
+
process.exit(2);
|
|
549
|
+
} else {
|
|
550
|
+
positional.push(a);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
switch (first) {
|
|
554
|
+
case 'init': cmdInit(opts); break;
|
|
555
|
+
case 'remember': cmdRemember(positional[0], positional[1], positional[2], opts); break;
|
|
556
|
+
case 'recall': cmdRecall(positional[0] || null, opts); break;
|
|
557
|
+
case 'forget': cmdForget(positional[0]); break;
|
|
558
|
+
case 'archive': cmdArchive(opts); break;
|
|
559
|
+
case 'reindex': cmdReindex(); break;
|
|
560
|
+
case 'export': cmdExport(opts.out); break;
|
|
561
|
+
case 'import': cmdImport(positional[0]); break;
|
|
562
|
+
default:
|
|
563
|
+
console.error('未知命令: ' + first);
|
|
564
|
+
usage();
|
|
565
|
+
process.exit(2);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
331
569
|
main();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-memory",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent-skills",
|
package/references/protocol.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# yotta-memory 协议规范 v0.
|
|
1
|
+
# yotta-memory 协议规范 v0.2.0
|
|
2
2
|
|
|
3
3
|
> 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
|
|
4
4
|
> 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
├── bounds/ # BOUND 边界(私密)
|
|
23
23
|
├── commits/ # COMMIT 承诺(私密)
|
|
24
24
|
├── .archive/ # 归档区(archive 命令移入)
|
|
25
|
+
├── index.json # 反向索引 + TF 打分
|
|
25
26
|
└── README.md # 记忆库说明
|
|
26
27
|
```
|
|
27
28
|
|
|
@@ -56,6 +57,10 @@ immutable: false
|
|
|
56
57
|
| `updated` | 是 | 最近更新日期 |
|
|
57
58
|
| `tags` | 否 | 标签数组 |
|
|
58
59
|
| `immutable` | 否 | `true` 时 archive 不移动(用户级硬事实)|
|
|
60
|
+
| `scope` | 否 | `public` / `private`,默认按类型(FACT=public,其余 private)|
|
|
61
|
+
| `owner` | 否 | 归属 agent id,默认空;private+owner 非空仅供该 agent 读取 |
|
|
62
|
+
| `access_count` | 否 | 命中次数,recall 命中展示集时 +1 |
|
|
63
|
+
| `last_accessed` | 否 | 最近访问日期 `YYYY-MM-DD` |
|
|
59
64
|
|
|
60
65
|
## 4. 类型体系
|
|
61
66
|
|
|
@@ -74,10 +79,11 @@ immutable: false
|
|
|
74
79
|
| 命令 | 行为 |
|
|
75
80
|
|---|---|
|
|
76
81
|
| `init [--project]` | 创建目录结构;默认用户级,`--project` 建项目级 |
|
|
77
|
-
| `remember <type> <subject> <statement
|
|
78
|
-
| `recall [关键词] [--type T] [--limit N]` |
|
|
82
|
+
| `remember <type> <subject> <statement> [--owner <id>]` | 写入;同 subject+statement 已存在则只更新 `updated`;`--owner` 标注归属 |
|
|
83
|
+
| `recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all]` | 索引+TF 打分匹配;读取分区过滤;项目级优先;默认 50 条 |
|
|
79
84
|
| `forget <文件>` | 删除(按路径或文件名)|
|
|
80
|
-
| `archive [--days 180]` |
|
|
85
|
+
| `archive [--days 180] [--threshold 0.4]` | 按盖棺分+年龄移入 `.archive/`(`vitality < threshold` 且超过 N 天)|
|
|
86
|
+
| `reindex` | 全量扫描重建 `index.json`(手动改 .md 后校正)|
|
|
81
87
|
| `export [--out f.json]` | 导出全部记忆为 JSON |
|
|
82
88
|
| `import <f.json>` | 从 JSON 导入(幂等)|
|
|
83
89
|
|