@yottameta/yotta-memory 0.4.0 → 0.5.0

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
@@ -15,25 +15,31 @@
15
15
  <a href="https://github.com/YottaMeta/yotta-memory"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen" /></a>
16
16
  </p>
17
17
 
18
+ > 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
19
+
18
20
  ## 核心价值
19
21
 
20
22
  多数记忆方案把「记住」做成了黑盒:数据进了数据库或云端,用户既看不到内容、也难以审计,更控制不了「哪个智能体能看到什么」。元忆换了一条路:
21
23
 
22
24
  - **记忆就是文件**:每条记忆是一个带 YAML frontmatter 的 Markdown 文件,放在用户自己的目录里。用任何编辑器都能查看、修改、删除,用 git 就能做版本管理与回滚。
23
- - **隔离由机制保证**:FACT 进公共区共享,PREF / BOUND / COMMIT 进私密区、按 agent 隔离。读取按 scope/owner 分区过滤,越界内容由 CLI 拦截、永不返回(默认静默跳过;显式跨读无授权才报错拒绝)——不依赖智能体的「自觉」。
25
+ - **隔离由机制保证**:FACT 进公共区共享,PREF / BOUND / COMMIT 进私密区、按 owner 物理分目录隔离(`private/<owner>/<type>/`)。读取按 scope/owner 分区过滤,越界内容由 CLI 拦截、永不返回(默认静默跳过;显式跨读无授权才报错拒绝);读写一律走 CLI / MCP,禁止 shell 直读写库文件——不依赖智能体的「自觉」。
24
26
  - **零依赖、即装即用**:无守护进程、无数据库、无向量库,只需要 Node.js。安装即用,数据留在本机。
27
+ - **便携记忆盘**:记忆库本身就是记忆引擎——装在硬盘或主机上随盘走,局域网内其它主机上的智能体可远程读写;本地零进程模式与局域网常驻模式可并存,插上硬盘即恢复全部记忆。
25
28
 
26
29
  ## 核心优势
27
30
 
28
31
  | 优势 | 说明 |
29
32
  |---|---|
30
33
  | **数据主权在用户** | 明文文件存储,可读、可改、可审计;git 可版本化,回滚 / 团队同步都走标准工具 |
31
- | **真正的权限边界** | 公共 / 私密分区 + scope/owner 过滤 + 授权机制(grant / identity=user / `--unsafe`),越界内容由 CLI 过滤、永不返回(默认静默;显式跨读 `--all`/`--owner <其它>` 无授权时 exit 3),不靠 AI 自律 |
34
+ | **真正的权限边界** | 公共 / 私密分区 + 按 owner 物理分目录(`private/<owner>/<type>/`)+ scope/owner 过滤 + 授权机制(grant / identity=user / `--unsafe`),越界内容由 CLI 过滤、永不返回(默认静默;显式跨读 `--all`/`--owner <其它>` 无授权时 exit 3);`--agent <其它>` 仅作身份声明、不授予跨读。读写走 CLI / MCP,禁 shell 直读写,不靠 AI 自律 |
32
35
  | **跨智能体标准** | 符合 Agent Skills 开放标准(agentskills.io);装一次,Claude Code / Codex / Cursor / OpenCode 等 78+ 智能体可共用同一份记忆 |
33
36
  | **轻量零依赖** | 无 daemon / 无数据库 / 无向量库;Node.js 自带即跑,任何机器可部署 |
34
37
  | **双级存储** | 用户级 `~/.yottamemory/`(跨项目)+ 项目级 `.yottamemory/`(随项目共享 / 交接)|
35
38
  | **检索与生命周期** | 索引 + TF 打分(中文分词友好);质量分 / 盖棺分 / 自动归档,记忆库不会无限膨胀 |
36
39
  | **生态分发** | GitHub + npm 双源同步发布;npx skills / npm / install.sh 三种方式,覆盖 17+ 类智能体目录 |
40
+ | **便携记忆盘(随盘走)** | 记忆库即引擎:装在硬盘 / 主机上,插上即恢复全部记忆;引擎主机只需装 CLI 当存放点,无需装任何 AI 智能体 |
41
+ | **局域网共享与自启** | 每智能体独立 token(Bearer + X-Agent-Id)鉴权、可吊销;`lan enable` 注册开机自启(默认登录自启,可选开机即启);MCP 工具集与 CLI 一致(8 个工具),管理动作不远程暴露 |
42
+ | **本地 / 局域网双模式** | 本地 `serve --stdio` 零进程、按需拉起(无常驻);局域网 streamable HTTP 常驻——两种模式可并存、按需选用 |
37
43
 
38
44
  ## 功能详解
39
45
 
@@ -55,16 +61,30 @@
55
61
  ### 权限与隔离
56
62
 
57
63
  - **读取三态**:公共 FACT 始终可读;自身私密始终可读;其它 agent 私密默认拒绝(不返回内容)。
64
+ - **物理隔离目录**:私密记忆按 owner 存放于 `private/<owner>/<type>/`,不同智能体的私密文件物理分离;旧版根下平铺的 `prefs/` `bounds/` `commits/` 在 `reindex` 时自动迁移。
58
65
  - **三种授权入口(满足任一即可读他人私密)**:
59
66
  1. `grants.json` 显式授权:`{"<userAgent>": ["<ownerAgent>", ...]}`;
60
67
  2. identity=user:`--agent user` / `--owner user` / 环境变量 `YOTTA_AGENT_ID=user`;
61
68
  3. 显式 `--unsafe`(用户显式授权)。
62
69
  - **默认静默、显式跨读才报错**:默认 recall 遇其它 agent 私密静默跳过(不泄露「存在 N 条私密不可见」);仅当显式跨智能体读取(`--all` / `--owner <其它>`)且无授权命中时才报错 / 警告。
70
+ - **`--agent <其它>` 不越界**:`--agent <其它agent>` 仅作身份声明 / 展示用,不授予读取他人私密;读其它智能体私密仍需 grant / identity=user / `--unsafe`。
63
71
  - **隔离定位**:scope: private 保证的是 AI 之间的语义隔离(正常 recall 不会被搜到、不会主动去读),不是文件系统级机密保护——数据主权在用户,作为所有者有权看到任何记忆文件。
72
+ - **禁止 shell 直读写**:记忆读写一律走 CLI / MCP 工具;用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改库文件会绕过 scope/owner 权限边界,读到其它智能体私密内容。
73
+
74
+ ### 智能体身份(唯一 ID + 自我档案)
75
+
76
+ 每个智能体有一个**全局唯一的 agent ID**:它是私密记忆(PREF / BOUND / COMMIT)的归属键,也是远端接入的身份声明(`X-Agent-Id`)。
77
+
78
+ - **登记(必须唯一)**:`yotta-memory iam <id>` 写入记忆库根目录 `agents.json`,**强制唯一性**——ID 已被其它主机 / 来源(含远端 token 登记)占用时拒绝,确认是同一智能体才 `--force`。
79
+ - **确认身份**:`yotta-memory whoami`(远端 MCP 工具 `agent_info`)读「当次声明身份」(本机 `YOTTA_AGENT_ID` / CLI `--agent`;远端 `X-Agent-Id`),不猜不默认。
80
+ - **自我档案(强制落盘)**:`iam` 自动写一条 PREF `subject=自我接入档案`(owner=自己),statement 为 `; ` 分隔的 key:value:`agent_id / host / memory_home / mcp_mode(stdio|http)/ engine_url(仅远端)/ token(仅远端;本机不存 token)`。开工先 `recall "自我接入档案"` 找回身份与接入信息。
81
+ - **本机免 token**:本机 CLI / stdio 直连不经网络、不校验 token;身份经该智能体 MCP 配置的 `env.YOTTA_AGENT_ID` 声明。本机多个智能体各自声明唯一 ID,互不撞。
82
+ - **私密记忆必须有 owner**:写 PREF / BOUND / COMMIT 时未声明身份会被拒绝(公共 FACT 不受影响),从机制上防止「抄别人的 ID」。
64
83
 
65
84
  ### 检索:索引 + 中文分词打分
66
85
 
67
86
  - `remember` 时自动构建 `index.json` 索引;recall 用 TF 打分排序,中文按 bigram 分词,不用额外模型。
87
+ - `index.json` 的 `tokens` 字段是中文分词的词频表(TF 打分用),**不是**访问凭证;鉴权令牌在记忆目录下 `.server/tokens.json`。
68
88
  - 支持关键词、`--type` 过滤、`--limit` 截断、项目级优先。
69
89
  - 命中展示会累加 `access_count` / `last_accessed`,为生命周期管理提供依据。
70
90
 
@@ -80,6 +100,14 @@
80
100
 
81
101
  把技能装进智能体后,SKILL.md 会自动教会智能体一套工作流:**开工 `recall` 恢复上下文 → 重要信息 `remember` 落盘 → 收工归档**。也可在对话中直接说「记住 XXX」「上次说到哪了」「记一笔」触发。
82
102
 
103
+ ### 便携记忆盘(记忆库即引擎)
104
+
105
+ - **记忆库即引擎**:`serve` 把记忆目录挂成 MCP 服务,目录随盘走;引擎主机只需装 CLI 当存放点,无需装任何 AI 智能体。
106
+ - **双模式可并存**:本地 `serve --stdio` 零进程(客户端按需拉起);局域网 streamable HTTP(默认 `0.0.0.0:8787`)+ 每智能体 token 鉴权。
107
+ - **开机自启**:`lan enable` 注册 Windows 计划任务(默认登录自启,`--onstart` 开机即启需管理员);`lan disable` 移除,`lan status` 查询。
108
+ - **安全边界**:管理动作(init / config / token / lan / serve)不进 MCP,token 不远程暴露;远程智能体只能读写记忆,不能改配置、不能管 token。
109
+ - 完整操作步骤见上文「局域网多机共享」章节与 [USER_GUIDE.md](USER_GUIDE.md)。
110
+
83
111
  ## 与其他方案对比
84
112
 
85
113
  > 以下按方案类型对比,不涉及具体产品名称。判断标准:数据主权、部署成本、权限边界、可审计性、跨智能体能力。
@@ -125,19 +153,35 @@ bash install.sh --dir /path/to/skills
125
153
  ```
126
154
  也可把整个 `yotta-memory` 文件夹复制到目标智能体的 skills 目录(常见位置见 install.sh --list)。
127
155
 
156
+ ## 升级
157
+
158
+ 两种升级方式对应两种安装方式:
159
+
160
+ **升级 CLI**(重新执行一次安装命令即可,不带版本号默认装最新):
161
+ ```bash
162
+ npm i -g @yottameta/yotta-memory
163
+ ```
164
+
165
+ **升级技能**:重新执行你当初的安装命令即可(`npx skills add YottaMeta/yotta-memory` 或 `yotta-memory-install -g`),覆盖旧版本技能文件夹。
166
+
167
+ > 升级只影响命令与技能文件,**不会动你已存的记忆**(记忆是独立于安装的文件,保留在原目录)。
168
+
128
169
  ## CLI 用法
129
170
 
171
+
130
172
  | 命令 | 作用 |
131
173
  |---|---|
132
174
  | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级 `~/.yottamemory/`;--dir 显式指定位置)|
133
175
  | `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入记忆(同 subject+statement 自动更新;--owner 标注归属)|
134
- | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
176
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;`--agent <其它>` 仅作身份声明、不授予跨读;项目级优先)|
135
177
  | `yotta-memory forget <文件>` | 删除一条记忆(按类型目录路径或文件名)|
136
178
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
137
179
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
138
180
  | `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
139
181
  | `yotta-memory config set memory_home <目录>` / `config get` | 持久记住 / 查看记忆库位置(`~/.yottamemory/config.json`)|
140
- | `yotta-memory token new --agent <id>` / `token list` / `token revoke --agent <id>` | 为智能体生成 / 列出 / 吊销访问 token(登记于记忆库 `.server/tokens.json`)|
182
+ | `yotta-memory whoami` | 查看当前智能体身份与登记状态(读 `YOTTA_AGENT_ID` / `X-Agent-Id`,不猜不默认)|
183
+ | `yotta-memory iam <id> [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一)|
184
+ | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 为智能体生成 / 列出 / 吊销访问 token(登记于记忆库 `.server/tokens.json`;同 ID 已被其它来源占用需 `--force` 覆盖,防不同智能体合流)|
141
185
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
142
186
  | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows 计划任务,默认 ONLOGON 登录自启;--onstart 开机即启需管理员)|
143
187
 
@@ -153,7 +197,7 @@ yotta-memory recall --type FACT --limit 10
153
197
 
154
198
  环境变量:
155
199
  - `YOTTA_MEMORY_HOME`:覆盖用户级记忆库目录(默认 `~/.yottamemory/`)。
156
- - `YOTTA_AGENT_ID` / `AGENT_ID`:当前 agent 标识,参与读取分区判定。
200
+ - `YOTTA_AGENT_ID` / `AGENT_ID`:当前 agent 标识(本机声明身份用,参与读取分区判定;私密记忆必须有 owner,未声明会被拒绝)。
157
201
 
158
202
  ## 智能体接入后怎么用
159
203
 
@@ -172,10 +216,11 @@ yotta-memory recall --type FACT --limit 10
172
216
  1. 初始化或接入记忆库(见 CLI 用法)。
173
217
  2. 为需要访问的每个智能体生成独立 token:
174
218
  ```bash
175
- yotta-memory token new --agent <智能体ID> # 打印一次,如 ytm_...
219
+ yotta-memory token new --agent <智能体ID> # 打印一次,如 ytm_...(同 ID 已被其它来源占用需加 --force)
176
220
  yotta-memory token list # 查看已登记智能体
177
221
  yotta-memory token revoke --agent <智能体ID> # 吊销
178
222
  ```
223
+ > 新生成的 token 即时生效,无需重启服务。
179
224
  3. 启动服务(默认监听 0.0.0.0:8787,Bearer token + X-Agent-Id 鉴权)——临时运行或注册开机自启二选一:
180
225
  ```bash
181
226
  yotta-memory serve # 临时前台运行
@@ -204,7 +249,7 @@ yotta-memory recall --type FACT --limit 10
204
249
  }
205
250
  ```
206
251
 
207
- 连接后可通过 MCP tools(remember / recall / search / forget / archive / reindex / export / import)读写记忆;管理动作(init / config / token / lan / serve)不进 MCP,token 管理不远程暴露。`X-Agent-Id` 必须与 token 登记的智能体一致;读取分区规则与 CLI 相同(FACT 公共可读,PREF / BOUND / COMMIT 私密隔离)。
252
+ 连接后可通过 MCP tools(remember / recall / search / forget / archive / reindex / export / import / agent_info)读写记忆与确认身份;管理动作(init / config / token / lan / serve)不进 MCP,token 管理不远程暴露。`X-Agent-Id` 必须与 token 登记的智能体一致;读取分区规则与 CLI 相同(FACT 公共可读,PREF / BOUND / COMMIT 私密隔离)。
208
253
 
209
254
  ### 位置持久化
210
255
 
package/SKILL.md CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆"
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  license: MIT
6
6
  ---
7
7
 
8
8
  # yotta-memory(元忆)— 有权限边界的文件式智能体记忆
9
9
 
10
10
  > 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
11
- > 版本:0.4.0 | 最后更新:2026-08-25
11
+ > 版本:0.5.0 | 最后更新:2026-08-25
12
12
 
13
13
  ## 这是什么
14
14
 
@@ -26,10 +26,42 @@ license: MIT
26
26
 
27
27
  ## 核心流程
28
28
 
29
- 1. **开工定向**:运行 `yotta-memory recall`(可带关键词)恢复上下文;项目级记忆优先,其次用户级。
29
+ 1. **开工定向**:先按「开工第一步:确认记忆位置 + 智能体身份」检测记忆库与身份,再运行 `yotta-memory recall`(可带关键词)恢复上下文;项目级记忆优先,其次用户级。
30
30
  2. **进行中落盘**:重要信息立即 `yotta-memory remember <type> <subject> <statement>`,不攒到收工。
31
31
  3. **收工归档**:写会话小结(COMMIT / 笔记),旧记录定期 `yotta-memory archive`。
32
- 4. **多智能体纪律**:FACT 写入公共区,PREF / BOUND / COMMIT 只写本智能体私密区;不读取其他智能体私密区。
32
+ 4. **多智能体纪律**:FACT 写入公共区,PREF / BOUND / COMMIT 只写本智能体私密区;不读取其他智能体私密区。**一切读写一律走 `yotta-memory` CLI / MCP 工具**——禁止用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读或改记忆库目录下的 `.md` / `index.json` / `tokens.json` / `agents.json` / `grants.json` 等文件,否则会绕过权限边界、读到别的智能体私密内容。
33
+
34
+
35
+ ### 开工第一步:确认记忆位置 + 智能体身份(每次会话先做,必做)
36
+
37
+ **A. 确认记忆库位置**(AI 不会自动知道记忆库在哪,先检测,避免「recall 读空库 / 错库」):
38
+
39
+ 1. 运行 `yotta-memory config get`。
40
+ - 输出 `memory_home: <目录>`(已显式设置)→ 直接用该位置。
41
+ - 输出 `memory_home: (未设置,默认 ~/.yottamemory)` → 🔒 征得同意后引导设置:问用户用默认还是指定目录(项目级 `<repo>/.yottamemory`、记忆盘等),确认后 AI 执行 `yotta-memory config set memory_home <目录>`,回读 `config get` 验证。
42
+ 2. **已有记忆**:目标目录已存在 `facts/` 等子目录或 `index.json` → 直接 recall;全新目录 → 按「便携记忆盘模式 §0.3」初始化。
43
+
44
+ **B. 确认本智能体唯一身份(强制,写私密记忆前必做)**:
45
+
46
+ 1. 运行 `yotta-memory whoami`(远端经 MCP 用 `agent_info`)确认「我是谁」。
47
+ - 已显示身份 + 已登记 + 有自我档案 → 用它,进入第 3 步。
48
+ - 显示「未声明身份」或「未登记」→ 进入第 2 步。
49
+ 2. **登记唯一 ID(AI 自己定义,须用户确认)**:
50
+ - AI 提议一个**全局唯一** ID(建议 `<主机名>-<角色>` 或带随机后缀,如 `win-zhiwei` / `kali-dashu`;禁止用 `dashu` / `codex` 这类易撞名)。
51
+ - 🔒 征得用户同意后执行 `yotta-memory iam <id>`:引擎**强制唯一性**(ID 已被其它主机/来源占用 → 拒绝并提示换 ID;确认是同一智能体才 `--force`),并**自动写自我档案**到本智能体私密区。
52
+ - 回读:`yotta-memory whoami` 显示「已登记 + 自我档案」。
53
+ 3. **自我档案校验**:`yotta-memory recall "自我接入档案"`(本智能体)能读回字段才算就绪:
54
+ `agent_id / host / memory_home / mcp_mode(stdio|http)/ engine_url(仅远端)/ token(仅远端;本机不存 token)`。
55
+ 4. **本机多智能体**:本机多个 AI 智能体共用引擎时,**每个都必须**在它自己的 MCP 配置里声明唯一 `YOTTA_AGENT_ID`(如 `env: { YOTTA_AGENT_ID: "<该智能体唯一ID>" }`),各自 `whoami` 各回各的、互不撞;本机走 stdio 免 token。
56
+
57
+ **C. 身份红线(强制)**:
58
+
59
+ - 智能体 ID **必须全局唯一**;**禁止**「从记忆里读到别人的 ID 就当自己的」(如看到「Kali 智能体 ID 为 dashu」就把自己当 dashu)。
60
+ - 不确定自己的 ID → 先 `whoami` / `agent_info`,再向用户确认;**禁止猜**。
61
+ - 私密记忆(PREF / BOUND / COMMIT)**必须有 owner**:未声明身份写私密会被引擎拒绝(公共 FACT 不受影响)。
62
+ - **禁止直接读写记忆库文件(硬红线)**:一切读写走 `yotta-memory` CLI / MCP 工具(`remember` / `recall` / `search` / `forget` / `archive` / `reindex` / `export` / `import` / `agent_info`);**禁止用 shell**(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` / `vim` 等)直接读、改、删 `<root>/` 下的记忆文件——否则 `--agent` / `--owner` 的越界拦截形同虚设,会读到别的智能体私密内容。`--agent <其它agent>` 只作身份声明/展示,绝不授予跨智能体私密读取(读他人私密仍需 grant / identity=user / `--unsafe`)。
63
+
64
+ > 已用 `YOTTA_MEMORY_HOME` 临时覆盖时不必改 config;本步骤是常规 CLI 直连用户级位置的引导。
33
65
 
34
66
  ## CLI 速查
35
67
 
@@ -37,19 +69,34 @@ license: MIT
37
69
  |---|---|
38
70
  | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级;--dir 显式指定位置)|
39
71
  | `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] [--unsafe]` | 检索(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
72
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;`--agent <其它>` 只作身份声明/展示,不授予跨读——读他人私密同样要授权;项目级优先)|
41
73
  | `yotta-memory forget <文件>` | 删除(按类型目录路径或文件名)|
42
74
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
43
75
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
44
76
  | `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
45
77
  | `yotta-memory config set memory_home <目录>` / `config get` | 持久记住 / 查看记忆库位置(`~/.yottamemory/config.json`)|
46
- | `yotta-memory token new --agent <id>` / `token list` / `token revoke --agent <id>` | 每智能体访问 token:生成 / 列出 / 吊销(登记 `<记忆库>/.server/tokens.json`)|
78
+ | `yotta-memory whoami` | 查看当前智能体身份与登记状态(读 `YOTTA_AGENT_ID` / `X-Agent-Id`,不猜不默认)|
79
+ | `yotta-memory iam <id> [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一)|
80
+ | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 每智能体访问 token:生成 / 列出 / 吊销(登记 `<记忆库>/.server/tokens.json`;同 ID 已被其它来源占用需 `--force` 覆盖,防不同智能体合流)|
47
81
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
48
82
  | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows 计划任务,默认 ONLOGON 登录自启;--onstart 开机即启需管理员)|
49
83
 
50
84
  ## 存储格式(摘要)
51
85
 
52
- `<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。顶层另有 `index.json`(反向索引 + TF 打分)。
86
+ 目录结构(v0.5.0 起,私密记忆按 owner 物理分目录):
87
+
88
+ ```
89
+ <root>/
90
+ ├── facts/ # FACT 事实(公共可共享)
91
+ ├── private/<owner>/<type>/ # PREF / BOUND / COMMIT,按智能体隔离
92
+ ├── .archive/ # 归档区
93
+ ├── index.json # 检索索引(tokens = 分词词频,非访问令牌)
94
+ └── agents.json # 智能体身份登记表(唯一性)
95
+ ```
96
+
97
+ 记忆文件 `<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。旧版根下平铺的 `prefs/` `bounds/` `commits/` 会在 `reindex`(或首次 recall 建索引)时按 frontmatter `owner` 自动迁移到 `private/<owner>/<type>/`。
98
+
99
+ 自我档案(本智能体身份,强制落盘):PREF,`subject=自我接入档案`,`owner=<本智能体ID>`,statement 为 `; ` 分隔的 key:value——`agent_id / host / memory_home / mcp_mode(stdio|http)/ engine_url(仅远端)/ token(仅远端;本机不存 token)`。
53
100
 
54
101
  ## 便携记忆盘模式(局域网多机共享)
55
102
 
package/USER_GUIDE.md ADDED
@@ -0,0 +1,287 @@
1
+ # 元忆(yotta-memory)用户使用手册
2
+
3
+ > 面向最终用户的操作手册:从安装、初始化,到便携记忆盘(局域网多机共享)的部署与接入。AI 智能体的自动引导流程见 `SKILL.md`。
4
+
5
+ ## 目录
6
+
7
+ 1. 这是什么
8
+ 2. 安装
9
+ 3. 本机单机使用
10
+ 4. 便携记忆盘 · 记忆引擎主机篇(Linux / Windows)
11
+ 5. 智能体接入篇(本机 / 局域网其它主机)
12
+ 6. CLI 命令速查
13
+ 7. 故障排查
14
+ 8. 安全与边界
15
+
16
+ ## 1. 这是什么
17
+
18
+ 元忆(yotta-memory)是一个文件式智能体记忆工具:每条记忆是一个 Markdown 文件,放在你自己的目录里,可以用任何编辑器查看、修改,用 git 做版本管理。FACT 记忆共享给所有智能体,PREF / BOUND / COMMIT 记忆按智能体隔离。
19
+
20
+ - 零依赖:只需要 Node.js,没有数据库、没有常驻服务(除非你开启便携记忆盘模式)。
21
+ - 记忆即文件:数据主权在你手里。
22
+
23
+ **典型使用场景:**
24
+
25
+ - **跨会话续接**:AI 开工 `recall` 恢复上次上下文,不丢记忆。
26
+ - **多智能体共享**:FACT 进公共区共享,PREF / BOUND / COMMIT 各自私密隔离。
27
+ - **交接与团队协作**:项目级 `.yottamemory` 随仓库走,交接即恢复。
28
+ - **便携记忆盘**:记忆装在固定主机上,本机与局域网其它主机共享同一份记忆(见第 4 / 5 篇)。
29
+
30
+ ## 2. 安装(CLI + 技能)
31
+
32
+ 使用元忆需要两部分:**CLI**(命令行工具,负责读写记忆)与**技能**(SKILL.md,负责教 AI 智能体怎么用)。先装 CLI,再把技能装进要用的智能体。
33
+
34
+ **CLI:任选一种方式**
35
+
36
+ | 方式 | 命令 | 适用 |
37
+ |---|---|---|
38
+ | npm 全局(推荐) | `npm i -g @yottameta/yotta-memory` | 长期使用 |
39
+ | npx 临时 | `npx -y @yottameta/yotta-memory` | 临时试用 |
40
+ | install.sh | `bash <(curl -s https://raw.githubusercontent.com/YottaMeta/yotta-memory/main/install.sh)` | 离线 / 无 npm |
41
+
42
+ 安装后验证:`yotta-memory --version` 能输出版本号即成功。
43
+
44
+ **技能(SKILL.md):装进要用的 AI 智能体**
45
+
46
+ | 方式 | 命令 | 适用 |
47
+ |---|---|---|
48
+ | npx skills(生态标准入口) | `npx skills add YottaMeta/yotta-memory` | 自动检测已装智能体 |
49
+ | 安装器(npm 全局时) | `yotta-memory-install -g` / `yotta-memory-install --agent codex` | 装进所有 / 指定智能体 |
50
+ | 手动 | 把整个 `yotta-memory` 文件夹复制到智能体的 skills 目录 | 离线 |
51
+
52
+ > CLI 与技能分工不同:CLI 让命令行能读写记忆;技能让 AI 知道「开工 recall 恢复上下文、重要信息 remember 落盘、收工归档」。只装 CLI 不装技能,AI 不会自动使用这套工作流。
53
+
54
+ **更新:** `npm i -g @yottameta/yotta-memory` 升级 CLI(不带版本号即最新版),再把技能文件夹重新装进智能体(重复执行上面的技能安装命令即可)。
55
+
56
+ **卸载:** `npm rm -g @yottameta/yotta-memory`,并从智能体的 skills 目录删除整个 `yotta-memory` 文件夹。
57
+
58
+ ## 3. 本机单机使用
59
+
60
+ ```bash
61
+ yotta-memory init # 初始化记忆库(默认 ~/.yottamemory)
62
+ yotta-memory remember FACT 项目 本周完成发布 # 记一条事实
63
+ yotta-memory recall 项目 # 检索记忆
64
+ yotta-memory config get # 查看记忆库位置
65
+ ```
66
+
67
+ - 想换记忆位置:`yotta-memory config set memory_home <目录>`,之后所有命令自动用新位置。
68
+ - 项目级记忆:在项目目录里 `yotta-memory init --project`,该项目的智能体优先读项目级记忆。
69
+
70
+ ### 记忆库位置:本机智能体如何找到记忆
71
+
72
+ 位置解析优先级:`YOTTA_MEMORY_HOME`(环境变量,临时覆盖)> `config.json#memory_home`(`config set memory_home` 持久记住)> 默认 `~/.yottamemory`。
73
+
74
+ - 本机智能体装好技能后,AI 开工执行 `yotta-memory config get` 即可看到当前生效位置,`recall` 会自动读对位置。
75
+ - 记忆库换位置后,执行一次 `yotta-memory config set memory_home <新目录>` 即可,无需改技能。
76
+
77
+ ### 手动添加 / 编辑记忆
78
+
79
+ 记忆就是文件:读写一律走 `yotta-memory` CLI / MCP 工具,**请勿用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件**——那会绕过 scope/owner 权限边界,可能读到其它智能体的私密内容。确需手改时,请把文件放对分类目录:公共事实放 `facts/`;私密的偏好 / 边界 / 承诺放 `private/<owner>/<type>/`(`<owner>` 用你登记的智能体 ID),frontmatter 至少包含 `type` / `subject` / `statement`:
80
+
81
+ > 说明:`index.json` 是引擎自动维护的检索索引,其 `tokens` 字段是中文分词的词频表(供 TF 打分),**不是**访问令牌。手动改过 `.md` 后可用 `yotta-memory reindex` 校正索引,无需手动编辑 `index.json`。
82
+
83
+ ```markdown
84
+ ---
85
+ type: FACT
86
+ subject: 项目
87
+ statement: 本周完成发布
88
+ ---
89
+ 本周完成发布
90
+ ```
91
+
92
+ 手动改过文件后运行 `yotta-memory reindex` 重建索引,`recall` 才能按索引检索到。整个记忆目录可以用 git 做版本管理与回滚。
93
+
94
+ ## 4. 便携记忆盘 · 记忆引擎主机篇
95
+
96
+ 场景:记忆放在一台主机上(Linux / Windows 均可),本机直接 CLI 读写;局域网内其它主机上的 AI 智能体经 MCP 远程接入。引擎主机只需装 CLI,不需要装任何 AI 智能体。
97
+
98
+ **本机与局域网可同时用同一个记忆库**:引擎 `serve` 运行期间,本机智能体照常用 CLI(或 stdio MCP)读写(不经网络、不需要 token),局域网其它主机的智能体同时经 MCP + token 接入——两条通道并存、互不干扰。
99
+
100
+ **第 1 步:安装 CLI**(见第 2 节)。
101
+
102
+ **第 2 步:指定记忆位置并初始化(或接入现有记忆库)**
103
+
104
+ ```bash
105
+ yotta-memory config set memory_home /srv/yotta-memory # 改成你的实际目录
106
+ yotta-memory init --dir /srv/yotta-memory # 新库:初始化(自动建 facts/private/.archive)
107
+ ```
108
+
109
+ 如果目标目录**已经是记忆库**(里面有 `facts/` 等子目录或 `index.json`,例如从旧机复制或 git 克隆来的),直接 `config set memory_home <目录>` 接入即可,**不要重复 init**。
110
+
111
+ **第 3 步:注册开机自启(可选,推荐)**
112
+
113
+ ```bash
114
+ # Windows:内置命令(计划任务)
115
+ yotta-memory lan enable # 登录后自动启动(默认)
116
+ yotta-memory lan enable --onstart # 开机即启(需管理员)
117
+ yotta-memory lan status # 查看状态
118
+ yotta-memory lan disable # 取消
119
+
120
+ # Linux:用 systemd 开机自启(lan 命令当前仅支持 Windows)
121
+ # 先查 yotta-memory 实际路径:which yotta-memory(npm 全局默认 /usr/local/bin/yotta-memory)
122
+ sudo tee /etc/systemd/system/yotta-memory.service > /dev/null <<'EOF'
123
+ [Unit]
124
+ Description=yotta-memory memory engine
125
+ After=network.target
126
+
127
+ [Service]
128
+ ExecStart=/usr/local/bin/yotta-memory serve --host 0.0.0.0 --port 8787
129
+ Restart=on-failure
130
+
131
+ [Install]
132
+ WantedBy=multi-user.target
133
+ EOF
134
+ sudo systemctl daemon-reload
135
+ sudo systemctl enable --now yotta-memory
136
+ systemctl status yotta-memory # 查看状态
137
+ ```
138
+
139
+ **第 4 步:为局域网其它主机的智能体生成访问 token**
140
+
141
+ ```bash
142
+ yotta-memory token new --agent 我的智能体ID
143
+ ```
144
+
145
+ 命令会打印一次 `ytm_...`,请妥善保管(它就是访问凭证)。多个智能体就重复执行、用不同 ID。**本机上的 AI 智能体不需要 token**(见第 5 篇)。
146
+
147
+ > token 丢失或要更换:重新执行 `token new --agent <id> --force` 会生成新 token 并覆盖旧的,旧 token 立即失效;不加 `--force` 时若该 ID 已被其它来源占用会被拒绝(每个 AI 的 ID 必须唯一);`token list` 可查看已登记哪些智能体。
148
+
149
+ **第 5 步:把连接信息交给远程智能体的用户**
150
+
151
+ - 引擎地址:`http://<本机IP>:8787/mcp`
152
+ - 该智能体的 token:`ytm_...`
153
+ - 智能体 ID(对应 X-Agent-Id 请求头)
154
+
155
+ 查本机 IP:Linux 运行 `hostname -I`(或 `ip a`);Windows 运行 `ipconfig` 找「IPv4 地址」。防火墙:Linux 若启用了 ufw,执行 `sudo ufw allow 8787/tcp`;Windows 首次监听时允许放行。否则局域网其它主机连不进来。
156
+
157
+ **第 6 步:备份与迁移**
158
+
159
+ - 备份 = 复制整个记忆目录(`facts/` `private/` `.archive/` + `index.json` + `agents.json` + `.server/`),复制到哪、哪就是记忆库;迁移同理,整个目录拷走即可。
160
+ - `export` / `import` 是把记忆导出成单个 JSON 或从 JSON 导入,适合跨工具交换或归档,不是日常备份的必需步骤。
161
+
162
+ ## 5. 智能体接入篇(本机 / 局域网其它主机)
163
+
164
+ 接入智能体分两类:引擎主机本机的智能体、局域网其它主机上的智能体。本机不需要 token、不依赖远程服务;其它主机经 MCP + token 接入。
165
+
166
+ ### 5.1 本机 AI 智能体(引擎主机本机)
167
+
168
+ **方式一(推荐,最简单):让智能体直接调用 CLI。**
169
+
170
+ ```bash
171
+ yotta-memory recall <关键词> # 智能体开工恢复上下文
172
+ yotta-memory remember FACT 主题 内容 # 智能体落盘
173
+ ```
174
+
175
+ 智能体装上技能后(`SKILL.md`)会自动学会这套工作流,无需任何 MCP 配置。
176
+
177
+ **方式二:stdio MCP(零常驻进程,智能体按需拉起 CLI)。** 在智能体 MCP 配置里加:
178
+
179
+ ```json
180
+ {
181
+ "mcpServers": {
182
+ "yotta-memory": {
183
+ "command": "yotta-memory",
184
+ "args": ["serve", "--stdio"],
185
+ "env": { "YOTTA_AGENT_ID": "<该智能体唯一ID>" }
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ 本机接入不需要 token,也不需要启动 HTTP 服务;但**必须在配置里声明唯一的 `YOTTA_AGENT_ID`**(见下),否则写私密记忆会被拒。
192
+
193
+ **本机智能体装好技能后如何获取记忆存放位置?** 按优先级:`YOTTA_MEMORY_HOME` 环境变量 > `config set memory_home` 持久化的 `~/.yottamemory/config.json` > 默认 `~/.yottamemory`。AI 开工执行 `yotta-memory config get` 查看当前生效位置;记忆库移动后执行一次 `config set memory_home <新目录>` 即可。
194
+
195
+ **给本机智能体设置唯一身份(强制)**:私密记忆(PREF / BOUND / COMMIT)按 owner 物理分目录隔离(存于 `private/<owner>/<type>/`),owner 取当前智能体的 agent ID。流程如下:
196
+
197
+ 1. 开工先 `yotta-memory whoami` 确认「我是谁」。
198
+ 2. 未登记 → 向用户确认一个**全局唯一** ID(建议 `<主机名>-<角色>`,别用 `dashu` / `codex` 这类易撞名),执行 `yotta-memory iam <id>`:引擎**强制唯一性**(被其它主机 / 来源占用会拒绝),并自动落一条「自我接入档案」PREF(owner=自己)。
199
+ 3. 本机多个 AI 智能体共用引擎时,**每个都要在它自己的 MCP 配置里声明唯一 `YOTTA_AGENT_ID`**(CLI 直连则每次带 `--agent <id>`),各自 `whoami` 各回各的、互不撞。
200
+ 4. **禁止**从记忆里读到别人的 ID 就当自己的(比如看到「Kali 智能体 ID 为 dashu」就把自己当 dashu);不确定先 `whoami` 再问用户,**禁止猜**。
201
+ 5. **不设则 owner 为空**:写私密记忆会被引擎拒绝(公共 FACT 不受影响),避免私密隔离退化。
202
+
203
+ ### 5.2 局域网其它主机的 AI 智能体
204
+
205
+ **第 1 步:向记忆引擎主机获取**:引擎 IP、端口(默认 8787)、本智能体的 token、智能体 ID。
206
+
207
+ **第 2 步:配置 MCP**(可以让 AI 按 `SKILL.md` 引导自动完成;也可以手动在你的智能体 MCP 配置里加这段):
208
+
209
+ ```json
210
+ {
211
+ "mcpServers": {
212
+ "yotta-memory": {
213
+ "url": "http://<引擎主机IP>:8787/mcp",
214
+ "headers": {
215
+ "Authorization": "Bearer <TOKEN>",
216
+ "X-Agent-Id": "<本智能体ID>"
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ **第 3 步:验证**:让 AI 调一次 `recall` / `search`,能读到记忆即连接成功。
224
+
225
+ **第 4 步:复用**:连接成功后就一直复用;token 失效(被吊销)时回到第 1 步重新获取。
226
+
227
+ **身份确认与自我档案(强制)**:先调一次 MCP 工具 `agent_info` 确认「我是谁」(读 X-Agent-Id 声明 + 登记状态)。随后用 `remember` 写一条 `subject=自我接入档案` 的 PREF(owner=自己),body 为 `; ` 分隔的 key:value——`agent_id / host / memory_home / mcp_mode: http / engine_url / token`,把接入信息存进自己的永久记忆;下次会话 `recall "自我接入档案"` 直接找回。
228
+
229
+ ## 6. CLI 命令速查
230
+
231
+ | 命令 | 作用 |
232
+ |---|---|
233
+ | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库 |
234
+ | `yotta-memory remember <类型> <主题> <内容> [--owner <id>]` | 写入记忆 |
235
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(读取分区过滤;`--agent <其它>` 仅作身份声明、不授予跨读;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`)|
236
+ | `yotta-memory forget <文件>` | 删除一条记忆 |
237
+ | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆 |
238
+ | `yotta-memory reindex` | 重建索引 |
239
+ | `yotta-memory export [--out 文件.json]` / `import <文件.json>` | 导出 / 导入 |
240
+ | `yotta-memory config set memory_home <目录>` / `config get` | 记忆库位置 |
241
+ | `yotta-memory whoami` | 查看当前智能体身份与登记状态(读 `YOTTA_AGENT_ID` / `X-Agent-Id`,不猜不默认)|
242
+ | `yotta-memory iam <id> [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一)|
243
+ | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 访问 token(同 ID 已被其它来源占用需 `--force` 覆盖)|
244
+ | `yotta-memory serve [--port 8787] [--stdio] [--no-auth]` | 启动记忆引擎(--no-auth 关闭鉴权,仅限可信内网)|
245
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理 |
246
+
247
+ 类型:`FACT`(事实,共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺),后三类按智能体物理分目录隔离(`private/<owner>/<type>/`)。
248
+
249
+ ## 7. 故障排查
250
+
251
+ **远程连不上时,按顺序检查:**
252
+
253
+ 1. 引擎在运行吗?Windows:`yotta-memory lan status`;Linux(systemd 自启时):`systemctl status yotta-memory`,或 `ps aux | grep yotta-memory`;也可直接 `yotta-memory --version`。
254
+ 2. IP 对吗?引擎主机执行 `hostname -I`(Linux)或 `ipconfig`(Windows)确认。
255
+ 3. 端口对吗?默认 8787,两端要一致。
256
+ 4. 防火墙放行了吗?Linux:`sudo ufw status`,未放行则 `sudo ufw allow 8787/tcp`;Windows:允许首次监听的入站请求。
257
+ 5. token 有效吗?记忆盘主机执行 `yotta-memory token list` 看该智能体是否登记;无效就 `token new --agent <id>` 重新生成。
258
+ 6. 网络通吗?任意一台主机执行 `curl http://<引擎IP>:8787/mcp`:
259
+ - 返回 401:服务在运行,是鉴权问题(token / 请求头不对)。
260
+ - 连接被拒 / 超时:服务没启动,或防火墙拦截。
261
+
262
+ **`lan enable` 失败提示 Access denied:** 需要管理员权限,请用管理员终端重新执行;`--onstart` 模式必须管理员。
263
+
264
+ **启动引擎报端口被占用(EADDRINUSE):** 用 `--port <其它端口>` 换端口启动,两端保持一致;或先查占用:Linux `ss -tlnp | grep 8787`,Windows `netstat -ano | findstr 8787`。
265
+
266
+ **recall 没有结果:** 关键词太细或确实没有这条记忆;先 `recall`(不带关键词)看库里的全部记忆核对。
267
+
268
+ **systemd 自启的引擎看日志:** `journalctl -u yotta-memory -n 50`。
269
+
270
+ **记错位置了?** `yotta-memory config get` 查看当前生效位置;`config set memory_home <正确目录>` 改正。
271
+
272
+ ## 8. 安全与边界
273
+
274
+ - token 等同密码:只给需要接入的智能体,别外传。
275
+ - 元忆不做加密:PREF / BOUND / COMMIT 私密记忆的隔离是「权限边界」机制(谁该读由 scope/owner 决定),属于纪律层保护,不是机密数据保护层。
276
+ - 记忆读写一律走 CLI / MCP;禁止用 shell(`Get-ChildItem` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件——否则会绕过 scope/owner 权限边界。
277
+ - 管理动作(init / config / token / lan / serve)不通过 MCP 暴露,远程只能读写记忆,不能改配置、不能管 token。
278
+ - `--no-auth` 会关闭鉴权,仅限可信内网使用。
279
+ - 数据主权在用户:所有数据都是本地明文文件,随时可看、可改、可删。
280
+
281
+ **确实需要读取其它智能体的私密记忆时(三种授权方式,满足任一即可):**
282
+
283
+ 1. 显式授权 `grants.json`:在记忆库根目录写 `{"<你的agentID>": ["<对方agentID>"]}`;
284
+ 2. identity=user:以 `--agent user` / `--owner user` / 环境变量 `YOTTA_AGENT_ID=user` 读取;
285
+ 3. 显式放行 `--unsafe`:用户明确同意时使用。
286
+
287
+ **协作纪律**:FACT 写入公共区共享;PREF / BOUND / COMMIT 只写自己的私密区;不主动读取其它智能体的私密记忆。
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // yotta-memory(元忆): 有权限边界的文件式智能体记忆 CLI(零依赖)
3
3
  // v0.4.0 新增:lan(Windows 计划任务开机自启 serve)/ init --dir(显式指定位置)/ serve --stdio(本地零进程模式)/ MCP 工具补 reindex/export/import
4
+ // v0.5.0 新增:隐私硬隔离——私密物理分目录 private/<agent_id>/{prefs,bounds,commits}/ + 关闭 --agent 越权读 + 私密写跨智能体需 --unsafe + 禁 shell 直读写(文档红线)
5
+ // v0.4.2 新增:iam/whoami 智能体身份自注册(agents.json 唯一性)+ 自我档案 PREF + 私密记忆须声明 owner
4
6
  'use strict';
5
7
 
6
8
  const fs = require('fs');
@@ -10,9 +12,12 @@ const crypto = require('crypto');
10
12
  const http = require('http');
11
13
  const child_process = require('child_process');
12
14
 
13
- const VERSION = '0.4.0';
15
+ const VERSION = '0.5.0';
14
16
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
15
17
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
18
+ const PUBLIC_DIR = 'facts';
19
+ const PRIVATE_DIR = 'private';
20
+ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
16
21
  const ARCHIVE_DIR = '.archive';
17
22
  const INDEX_FILE = 'index.json';
18
23
  const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
@@ -20,6 +25,7 @@ const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'u
20
25
  const CONFIG_FILE = 'config.json';
21
26
  const SERVER_SUBDIR = '.server';
22
27
  const TOKENS_FILE = 'tokens.json';
28
+ const AGENTS_FILE = 'agents.json';
23
29
 
24
30
  // ---- 全局配置(记忆库位置持久化,固定 ~/.yottamemory/config.json)----
25
31
  function configPath() {
@@ -57,6 +63,14 @@ function typeDir(type) {
57
63
  }
58
64
  return TYPE_DIRS[t];
59
65
  }
66
+ function privateIdDir(root, id) { return path.join(root, PRIVATE_DIR, id); }
67
+ // 布局:FACT -> <root>/facts;PREF/BOUND/COMMIT -> <root>/private/<owner>/<type>
68
+ function typeSubdir(type, owner) {
69
+ const t = String(type).toUpperCase();
70
+ if (t === 'FACT') return PUBLIC_DIR;
71
+ const id = owner || '';
72
+ return path.join(PRIVATE_DIR, id, TYPE_DIRS[t]);
73
+ }
60
74
  function defaultScope(type) {
61
75
  return PRIVATE_TYPES.indexOf(String(type).toUpperCase()) === -1 ? 'public' : 'private';
62
76
  }
@@ -123,14 +137,12 @@ function frontmatterToText(meta, body) {
123
137
  return lines.join('\n') + String(b).replace(/^\n+/, '') + '\n';
124
138
  }
125
139
  function ensureInit(root) {
126
- fs.mkdirSync(path.join(root, 'facts'), { recursive: true });
127
- fs.mkdirSync(path.join(root, 'prefs'), { recursive: true });
128
- fs.mkdirSync(path.join(root, 'bounds'), { recursive: true });
129
- fs.mkdirSync(path.join(root, 'commits'), { recursive: true });
140
+ fs.mkdirSync(path.join(root, PUBLIC_DIR), { recursive: true });
141
+ fs.mkdirSync(path.join(root, PRIVATE_DIR), { recursive: true });
130
142
  fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
131
143
  const readme = path.join(root, 'README.md');
132
144
  if (!fs.existsSync(readme)) {
133
- fs.writeFileSync(readme, '# yotta-memory(元忆)记忆库\n\n有权限边界的文件式智能体记忆存储目录。结构:facts/ prefs/ bounds/ commits/ .archive/。\n', 'utf8');
145
+ fs.writeFileSync(readme, '# yotta-memory(元忆)记忆库\n\n有权限边界的文件式智能体记忆存储目录。结构:facts/(公共 FACT) private/<agent_id>/{prefs,bounds,commits}/(各智能体私密,物理隔离) .archive/(归档)。\n', 'utf8');
134
146
  }
135
147
  }
136
148
  function nextSeq(dir) {
@@ -160,17 +172,54 @@ function saveIndex(root, entries) {
160
172
  const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
161
173
  fs.writeFileSync(indexPath(root), JSON.stringify({ version: 2, updated: today(), entries: clean }, null, 2), 'utf8');
162
174
  }
163
- function buildIndex(root) {
164
- const entries = [];
165
- for (const t of Object.keys(TYPE_DIRS)) {
166
- const dir = path.join(root, TYPE_DIRS[t]);
175
+ function collectEntryFiles(root) {
176
+ const dirs = [];
177
+ const facts = path.join(root, PUBLIC_DIR);
178
+ if (fs.existsSync(facts)) dirs.push(path.join(root, PUBLIC_DIR));
179
+ const pdir = path.join(root, PRIVATE_DIR);
180
+ if (fs.existsSync(pdir)) {
181
+ for (const id of fs.readdirSync(pdir)) {
182
+ const idDir = path.join(pdir, id);
183
+ if (!fs.statSync(idDir).isDirectory()) continue;
184
+ for (const t of PRIVATE_LEAF) {
185
+ const d = path.join(idDir, t);
186
+ if (fs.existsSync(d)) dirs.push(d);
187
+ }
188
+ }
189
+ }
190
+ const out = [];
191
+ for (const dir of dirs) {
192
+ for (const f of fs.readdirSync(dir)) {
193
+ const fp = path.join(dir, f);
194
+ if (fs.statSync(fp).isFile()) out.push(fp);
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+ // 迁移:把根下旧平铺 prefs|bounds|commits/*.md 按 frontmatter owner 迁入 private/<owner>/<type>/
200
+ function migrateLayout(root) {
201
+ let moved = 0;
202
+ for (const t of PRIVATE_LEAF) {
203
+ const dir = path.join(root, t);
167
204
  if (!fs.existsSync(dir)) continue;
168
205
  for (const f of fs.readdirSync(dir)) {
169
206
  const fp = path.join(dir, f);
170
207
  if (!fs.statSync(fp).isFile()) continue;
171
- entries.push(readEntry(fp, root));
208
+ const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
209
+ const owner = meta.owner || '';
210
+ if (!owner) continue;
211
+ const target = path.join(root, PRIVATE_DIR, owner, t, f);
212
+ fs.mkdirSync(path.dirname(target), { recursive: true });
213
+ fs.renameSync(fp, target);
214
+ moved++;
172
215
  }
173
216
  }
217
+ return moved;
218
+ }
219
+ function buildIndex(root) {
220
+ migrateLayout(root);
221
+ const entries = [];
222
+ for (const fp of collectEntryFiles(root)) entries.push(readEntry(fp, root));
174
223
  saveIndex(root, entries);
175
224
  return entries;
176
225
  }
@@ -282,14 +331,15 @@ function hasGrant(userAgent, ownerAgent) {
282
331
  return false;
283
332
  }
284
333
  // 三态读取判定:'read' | 'denied'
285
- function classifyRead(entry, agent, ownerFilter, unsafe) {
334
+ // selfAgent 为可信身份(env 声明 / 调用方上下文);--agent 仅供授权与展示,不得授予跨智能体私密读取。
335
+ function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
286
336
  if (entry.scope === 'public') return 'read';
287
337
  const owner = entry.owner || '';
288
338
  if (!owner) return 'read';
289
- if (agent && owner === agent) return 'read';
290
- if (ownerFilter && owner === ownerFilter && agent && ownerFilter === agent) return 'read';
291
- if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent && '' + agent).toLowerCase() === 'user') return 'read';
292
- if (hasGrant(agent, owner)) return 'read';
339
+ const own = selfAgent || agent;
340
+ if (own && owner === own) return 'read';
341
+ if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent || '').toLowerCase() === 'user') return 'read';
342
+ if (hasGrant(agent || selfAgent, owner)) return 'read';
293
343
  return 'denied';
294
344
  }
295
345
 
@@ -300,17 +350,26 @@ function initCore(opts) {
300
350
  return { error: false, text: '已初始化记忆库: ' + root };
301
351
  }
302
352
  function rememberCore(type, subject, statement, opts) {
353
+ opts = opts || {};
303
354
  const root = userRoot();
304
355
  ensureInit(root);
305
356
  const t = String(type).toUpperCase();
306
357
  if (!TYPE_DIRS[t]) return { error: true, text: '未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')' };
307
- const dir = path.join(root, TYPE_DIRS[t]);
308
358
  const stmt = String(statement || '').trim();
309
359
  const subj = String(subject || '').trim();
310
360
  if (!stmt) return { error: true, text: 'statement 不能为空' };
311
361
  if (!subj) return { error: true, text: 'subject 不能为空' };
312
- const owner = opts.owner || currentAgent();
362
+ const selfAgent = opts.selfAgent || currentAgent();
363
+ const owner = opts.owner || selfAgent;
313
364
  const scope = opts.scope || defaultScope(t);
365
+ if (scope === 'private' && !owner) {
366
+ return { error: true, text: '私密记忆必须声明归属智能体:请设环境变量 YOTTA_AGENT_ID(或 AGENT_ID)、传 --owner <id>,或先 yotta-memory whoami / iam 登记唯一身份。公共记忆(FACT)不受影响。' };
367
+ }
368
+ if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
369
+ return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
370
+ }
371
+ const dir = path.join(root, typeSubdir(t, owner));
372
+ fs.mkdirSync(dir, { recursive: true });
314
373
  if (fs.existsSync(dir)) {
315
374
  for (const f of fs.readdirSync(dir)) {
316
375
  const fp = path.join(dir, f);
@@ -345,17 +404,19 @@ function recallCore(query, opts) {
345
404
  const limit = opts.limit || 50;
346
405
  const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
347
406
  const q = query ? String(query).toLowerCase() : '';
348
- const agent = opts.agent || currentAgent();
407
+ const selfAgent = currentAgent();
408
+ const agent = opts.agent || selfAgent;
349
409
  const ownerFilter = opts.owner || '';
350
410
  const allSafe = !!opts.unsafe;
351
- const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
411
+ const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
412
+ const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
352
413
  const hits = [];
353
414
  let deniedCount = 0;
354
415
  for (const root of roots) {
355
416
  const entries = ensureIndex(root);
356
417
  for (const e of entries) {
357
418
  if (onlyType && e.type !== onlyType) continue;
358
- const r = classifyRead(e, agent, ownerFilter, allSafe);
419
+ const r = classifyRead(e, agent, ownerFilter, allSafe, selfAgent);
359
420
  if (r === 'denied') { deniedCount++; continue; }
360
421
  let score = 0;
361
422
  if (q) {
@@ -403,38 +464,38 @@ function recallCore(query, opts) {
403
464
  }
404
465
  return { error: false, exitCode: 0, text: lines.join('\n') };
405
466
  }
406
- function forgetCore(fileRef) {
467
+ function resolveMemoryFile(root, ref) {
468
+ const map = {};
469
+ for (const fp of collectEntryFiles(root)) {
470
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
471
+ map[rel] = fp;
472
+ const base = path.basename(rel);
473
+ if (!map[base]) map[base] = fp;
474
+ }
475
+ if (map[ref]) return { fp: map[ref], rel: relOf(root, map[ref]) };
476
+ const base = path.basename(ref.replace(/\\/g, '/'));
477
+ if (map[base]) return { fp: map[base], rel: relOf(root, map[base]) };
478
+ return null;
479
+ }
480
+ function relOf(root, fp) { return path.relative(root, fp).replace(/\\/g, '/'); }
481
+ function forgetCore(fileRef, opts) {
482
+ opts = opts || {};
483
+ const selfAgent = opts.selfAgent || currentAgent();
407
484
  const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
408
485
  const ref = String(fileRef || '').replace(/\\/g, '/');
409
- const slash = ref.indexOf('/');
410
- const dynTypeDir = slash === -1 ? '' : ref.slice(0, slash);
411
- const dynBase = slash === -1 ? ref : ref.slice(slash + 1);
412
486
  let target = null, targetRoot = null, targetRel = null;
413
487
  for (const root of roots) {
414
- if (dynTypeDir) {
415
- const dir = path.join(root, dynTypeDir);
416
- if (!fs.existsSync(dir)) continue;
417
- const cand = path.join(dir, dynBase);
418
- if (fs.existsSync(cand)) {
419
- target = cand; targetRoot = root;
420
- targetRel = dynTypeDir + '/' + path.basename(dynBase);
421
- break;
422
- }
423
- continue;
424
- }
425
- for (const t of Object.keys(TYPE_DIRS)) {
426
- const dir = path.join(root, TYPE_DIRS[t]);
427
- if (!fs.existsSync(dir)) continue;
428
- const cand = path.join(dir, dynBase);
429
- if (fs.existsSync(cand)) {
430
- target = cand; targetRoot = root;
431
- targetRel = TYPE_DIRS[t] + '/' + path.basename(dynBase);
432
- break;
433
- }
434
- }
435
- if (target) break;
488
+ const found = resolveMemoryFile(root, ref);
489
+ if (found) { target = found.fp; targetRoot = root; targetRel = found.rel; break; }
436
490
  }
437
491
  if (!target) return { error: true, text: '未找到记忆文件: ' + fileRef };
492
+ const seg = targetRel.replace(/\\/g, '/').split('/');
493
+ if (seg[0] === 'private') {
494
+ const owner = seg[1] || '';
495
+ if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
496
+ return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
497
+ }
498
+ }
438
499
  fs.unlinkSync(target);
439
500
  if (targetRoot) removeIndexEntry(targetRoot, targetRel);
440
501
  return { error: false, text: '已删除: ' + target };
@@ -447,24 +508,20 @@ function archiveCore(opts) {
447
508
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
448
509
  let moved = 0;
449
510
  const movedFiles = [];
450
- for (const t of Object.keys(TYPE_DIRS)) {
451
- const dir = path.join(root, TYPE_DIRS[t]);
452
- if (!fs.existsSync(dir)) continue;
453
- for (const f of fs.readdirSync(dir)) {
454
- const fp = path.join(dir, f);
455
- if (!fs.statSync(fp).isFile()) continue;
456
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
457
- if (meta.immutable === 'true') continue;
458
- if (!meta.created) continue;
459
- const createdTs = new Date(meta.created).getTime();
460
- if (isNaN(createdTs)) continue;
461
- if (vitality(meta) < threshold && createdTs < cutoff) {
462
- const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
463
- fs.mkdirSync(destDir, { recursive: true });
464
- fs.renameSync(fp, path.join(destDir, f));
465
- movedFiles.push(TYPE_DIRS[t] + '/' + f);
466
- moved++;
467
- }
511
+ for (const fp of collectEntryFiles(root)) {
512
+ const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
513
+ if (meta.immutable === 'true') continue;
514
+ if (!meta.created) continue;
515
+ const createdTs = new Date(meta.created).getTime();
516
+ if (isNaN(createdTs)) continue;
517
+ const t = (meta.type || 'FACT').toUpperCase();
518
+ if (vitality(meta) < threshold && createdTs < cutoff) {
519
+ const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
520
+ fs.mkdirSync(destDir, { recursive: true });
521
+ fs.renameSync(fp, path.join(destDir, path.basename(fp)));
522
+ const rel = relOf(root, fp);
523
+ movedFiles.push(rel);
524
+ moved++;
468
525
  }
469
526
  }
470
527
  if (movedFiles.length) removeIndexEntries(root, movedFiles);
@@ -505,15 +562,9 @@ function cmdReindex() {
505
562
  }
506
563
  function collectAll(root) {
507
564
  const out = [];
508
- for (const t of Object.keys(TYPE_DIRS)) {
509
- const dir = path.join(root, TYPE_DIRS[t]);
510
- if (!fs.existsSync(dir)) continue;
511
- for (const f of fs.readdirSync(dir)) {
512
- const fp = path.join(dir, f);
513
- if (!fs.statSync(fp).isFile()) continue;
514
- const e = readEntry(fp, root);
515
- out.push({ file: e.file, meta: e.meta });
516
- }
565
+ for (const fp of collectEntryFiles(root)) {
566
+ const e = readEntry(fp, root);
567
+ out.push({ file: e.file, meta: e.meta });
517
568
  }
518
569
  return out;
519
570
  }
@@ -541,7 +592,8 @@ function importCore(root, src) {
541
592
  const meta = item.meta || {};
542
593
  const t = (meta.type || 'FACT').toUpperCase();
543
594
  if (!TYPE_DIRS[t]) continue;
544
- const dir = path.join(root, TYPE_DIRS[t]);
595
+ const dir = path.join(root, typeSubdir(t, meta.owner || ''));
596
+ fs.mkdirSync(dir, { recursive: true });
545
597
  let seq = nextSeq(dir);
546
598
  let file = path.join(dir, today() + '-' + seq + '.md');
547
599
  while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
@@ -586,13 +638,18 @@ function saveTokens(root, data) {
586
638
  fs.writeFileSync(tokensPath(root), JSON.stringify(data, null, 2), 'utf8');
587
639
  try { fs.chmodSync(tokensPath(root), 0o600); } catch (e) {}
588
640
  }
589
- function cmdTokenNew(agentId) {
641
+ function cmdTokenNew(agentId, opts) {
590
642
  const root = userRoot();
591
643
  ensureInit(root);
592
644
  if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
593
645
  const data = loadTokens(root);
594
646
  data.version = 1;
595
647
  data.tokens = data.tokens || {};
648
+ const conflict = identityConflict(root, agentId);
649
+ if (conflict && !opts.force) {
650
+ console.error("错误: 智能体 ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一;确认是同一智能体后加 --force 覆盖。');
651
+ process.exit(2);
652
+ }
596
653
  const token = 'ytm_' + crypto.randomBytes(16).toString('hex');
597
654
  data.tokens[agentId] = { token: token, created: today() };
598
655
  saveTokens(root, data);
@@ -616,6 +673,106 @@ function cmdTokenRevoke(agentId) {
616
673
  else { console.log('未找到已登记智能体: ' + agentId); }
617
674
  }
618
675
 
676
+
677
+ // ---- 智能体身份登记(agents.json)+ 自我档案 ----
678
+ function agentsPath(root) { return path.join(root, AGENTS_FILE); }
679
+ function loadAgents(root) {
680
+ try { return JSON.parse(fs.readFileSync(agentsPath(root), 'utf8')) || {}; } catch (e) { return {}; }
681
+ }
682
+ function saveAgents(root, data) {
683
+ fs.mkdirSync(root, { recursive: true });
684
+ fs.writeFileSync(agentsPath(root), JSON.stringify(data, null, 2), 'utf8');
685
+ }
686
+ const SELF_PROFILE_SUBJECT = '自我接入档案';
687
+ function selfPrefsDir(root, agentId) { return path.join(root, PRIVATE_DIR, agentId, 'prefs'); }
688
+ function findSelfProfile(root, agentId) {
689
+ const dir = selfPrefsDir(root, agentId);
690
+ if (!fs.existsSync(dir)) return '';
691
+ for (const f of fs.readdirSync(dir)) {
692
+ const fp = path.join(dir, f);
693
+ if (!fs.statSync(fp).isFile()) continue;
694
+ const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
695
+ if (meta.subject === SELF_PROFILE_SUBJECT && (meta.owner || '') === agentId) return f;
696
+ }
697
+ return '';
698
+ }
699
+ function selfProfileBody(agentId, root, extra) {
700
+ const lines = [];
701
+ lines.push('agent_id: ' + agentId);
702
+ lines.push('host: ' + os.hostname());
703
+ lines.push('memory_home: ' + root);
704
+ lines.push('mcp_mode: ' + (extra.mcpMode || 'stdio'));
705
+ if (extra.engineUrl) lines.push('engine_url: ' + extra.engineUrl);
706
+ if (extra.token) lines.push('token: ' + extra.token);
707
+ return lines.join('; ');
708
+ }
709
+ function writeSelfProfile(root, agentId, extra) {
710
+ const dir = selfPrefsDir(root, agentId);
711
+ fs.mkdirSync(dir, { recursive: true });
712
+ const seq = nextSeq(dir);
713
+ const file = path.join(dir, today() + '-' + seq + '.md');
714
+ const body = selfProfileBody(agentId, root, extra || {});
715
+ const rec = {
716
+ type: 'PREF', subject: SELF_PROFILE_SUBJECT, statement: body,
717
+ confidence: 1.0, created: today(), updated: today(),
718
+ tags: ['自我档案'], immutable: false,
719
+ scope: 'private', owner: agentId, access_count: 0, last_accessed: '',
720
+ };
721
+ fs.writeFileSync(file, frontmatterToText(rec, body), 'utf8');
722
+ upsertIndexEntry(root, readEntry(file, root));
723
+ return file;
724
+ }
725
+ function identityConflict(root, agentId) {
726
+ const agents = loadAgents(root).agents || {};
727
+ const tokens = loadTokens(root).tokens || {};
728
+ const host = os.hostname();
729
+ const a = agents[agentId];
730
+ if (a && a.host && a.host !== host) return '已被智能体身份登记占用(host=' + a.host + ')';
731
+ if (tokens[agentId]) return '已被远端 token 登记占用(.server/tokens.json)';
732
+ return '';
733
+ }
734
+ function cmdIam(agentId, opts) {
735
+ const root = userRoot();
736
+ ensureInit(root);
737
+ if (!agentId) { console.error('请指定 <id>:yotta-memory iam <id> [--force]'); process.exit(2); }
738
+ const conflict = identityConflict(root, agentId);
739
+ if (conflict && !opts.force) {
740
+ console.error("错误: ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一。请换一个唯一 ID,或确认是同一智能体后加 --force 覆盖。');
741
+ process.exit(2);
742
+ }
743
+ const data = loadAgents(root);
744
+ data.version = 1;
745
+ data.agents = data.agents || {};
746
+ const host = os.hostname();
747
+ const existed = data.agents[agentId];
748
+ data.agents[agentId] = { host: host, created: (existed && existed.created) || today() };
749
+ saveAgents(root, data);
750
+ const file = writeSelfProfile(root, agentId, { mcpMode: 'stdio' });
751
+ console.log('已登记智能体身份: ' + agentId + '(host=' + host + ',' + (conflict ? '--force 覆盖' : '新建') + ')');
752
+ console.log('已写入自我档案: ' + file);
753
+ console.log('本机免 token:以后用 whoami 确认身份;远端接入需 token new --agent ' + agentId);
754
+ }
755
+ function cmdWhoami() {
756
+ const root = userRoot();
757
+ const id = currentAgent();
758
+ if (!id) {
759
+ console.log('当前未声明智能体身份(YOTTA_AGENT_ID / AGENT_ID 未设置,也未传 --agent)。');
760
+ console.log('本机:请在该智能体的 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID>,然后 yotta-memory iam <id> 登记;或 CLI 每次带 --agent <id> / --owner <id>。');
761
+ console.log('远端:X-Agent-Id 请求头 + token(token new --agent <id>)。');
762
+ return;
763
+ }
764
+ const agents = loadAgents(root).agents || {};
765
+ const tokens = loadTokens(root).tokens || {};
766
+ let reg = '未登记';
767
+ if (agents[id]) reg = 'agents.json 已登记(host=' + agents[id].host + ')';
768
+ else if (tokens[id]) reg = 'tokens.json 已登记(远端)';
769
+ const profile = findSelfProfile(root, id);
770
+ console.log('当前智能体身份: ' + id);
771
+ console.log('登记状态: ' + reg);
772
+ console.log('自我档案: ' + (profile ? profile : '未写入(请执行 yotta-memory iam ' + id + ')'));
773
+ if (!agents[id] && !tokens[id]) console.log('提示: 请先 yotta-memory iam ' + id + ' 登记唯一身份并落自我档案,再写私密记忆。');
774
+ }
775
+
619
776
  // ---- config 命令 ----
620
777
  function cmdConfigSet(key, value) {
621
778
  if (key !== 'memory_home') { console.error('未知配置项: ' + key + '(可用: memory_home)'); process.exit(2); }
@@ -643,13 +800,16 @@ function mcpTools() {
643
800
  { name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
644
801
  { name: 'export', description: '导出全部记忆到引擎主机上的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
645
802
  { name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
803
+ { name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
646
804
  ];
647
805
  }
648
806
  function callTool(name, args, ctx) {
649
807
  const agent = ctx.agent || '';
650
808
  try {
651
809
  if (name === 'remember') {
652
- const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: args.owner || agent });
810
+ const ownerArg = args.owner ? String(args.owner) : '';
811
+ if (ownerArg && ownerArg !== agent) return { text: '拒绝: MCP 写入的 owner 必须等于当前智能体身份(' + agent + '),不能写其它智能体私密区。', error: true };
812
+ const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: agent, selfAgent: agent });
653
813
  return { text: r.text, error: r.error };
654
814
  }
655
815
  if (name === 'recall' || name === 'search') {
@@ -657,7 +817,7 @@ function callTool(name, args, ctx) {
657
817
  return { text: r.text, error: r.error };
658
818
  }
659
819
  if (name === 'forget') {
660
- const r = forgetCore(String(args.file || ''));
820
+ const r = forgetCore(String(args.file || ''), { selfAgent: agent });
661
821
  return { text: r.text, error: r.error };
662
822
  }
663
823
  if (name === 'archive') {
@@ -678,6 +838,18 @@ function callTool(name, args, ctx) {
678
838
  const r = importCore(userRoot(), String(args.src || ''));
679
839
  return { text: r.text, error: r.error };
680
840
  }
841
+ if (name === 'agent_info') {
842
+ const root = userRoot();
843
+ const id = agent || '';
844
+ if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / YOTTA_AGENT_ID)。本机请在 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID> 并 iam 登记。', error: false };
845
+ const agents = loadAgents(root).agents || {};
846
+ const tokens = loadTokens(root).tokens || {};
847
+ let reg = '未登记';
848
+ if (agents[id]) reg = 'agents.json 已登记(host=' + agents[id].host + ')';
849
+ else if (tokens[id]) reg = 'tokens.json 已登记(远端)';
850
+ const profile = findSelfProfile(root, id);
851
+ return { text: '当前智能体身份: ' + id + '\n登记状态: ' + reg + '\n自我档案: ' + (profile || '未写入(本地引擎主机执行 yotta-memory iam ' + id + ')'), error: false };
852
+ }
681
853
  return { text: '未知工具: ' + name, error: true };
682
854
  } catch (e) {
683
855
  return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
@@ -856,13 +1028,15 @@ function usage() {
856
1028
  ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
857
1029
  ' yotta-memory config get 查看当前配置与生效位置',
858
1030
  ' yotta-memory remember <type> <subject> <statement> [--owner <id>] 写入记忆',
859
- ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区)',
1031
+ ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区;--agent 仅表示“以该身份检索/模拟”,跨智能体私密读取需 --unsafe / 授权)',
860
1032
  ' yotta-memory forget <文件或文件名> 删除一条记忆',
861
1033
  ' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
862
1034
  ' yotta-memory reindex 重建索引',
863
1035
  ' yotta-memory export [--out 文件.json] 导出全部记忆',
864
1036
  ' yotta-memory import <文件.json> 导入记忆',
865
- ' yotta-memory token new --agent <id> 生成/重置某智能体访问 token(登记 .server/tokens.json,打印一次)',
1037
+ ' yotta-memory whoami 查看当前智能体身份与登记状态(读 YOTTA_AGENT_ID / X-Agent-Id,不猜不默认)',
1038
+ ' yotta-memory iam <id> [--force] 登记本智能体唯一身份并自动落自我档案(agents.json;ID 必须唯一)',
1039
+ ' yotta-memory token new --agent <id> [--force] 生成/重置某智能体访问 token(同 ID 已被其它来源占用需 --force 覆盖)',
866
1040
  ' yotta-memory token list 列出已登记智能体',
867
1041
  ' yotta-memory token revoke --agent <id> 吊销某智能体 token',
868
1042
  ' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
@@ -870,7 +1044,8 @@ function usage() {
870
1044
  ' yotta-memory --version 版本',
871
1045
  '',
872
1046
  '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
873
- '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(读取分区)',
1047
+ '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)',
1048
+ '隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令',
874
1049
  '远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
875
1050
  '',
876
1051
  ].join('\n'));
@@ -892,6 +1067,7 @@ function main() {
892
1067
  else if (a === '--no-auth') opts.noAuth = true;
893
1068
  else if (a === '--stdio') opts.stdio = true;
894
1069
  else if (a === '--onstart') opts.onstart = true;
1070
+ else if (a === '--force') opts.force = true;
895
1071
  else if (valueOpts.has(a)) {
896
1072
  const v = args[++i];
897
1073
  if (a === '--type') opts.type = v;
@@ -914,6 +1090,8 @@ function main() {
914
1090
  }
915
1091
  switch (first) {
916
1092
  case 'init': cmdInit(opts); break;
1093
+ case 'whoami': cmdWhoami(); break;
1094
+ case 'iam': cmdIam(positional[0], opts); break;
917
1095
  case 'config': {
918
1096
  const sub = positional[0];
919
1097
  if (sub === 'set') cmdConfigSet(positional[1], positional[2]);
@@ -930,7 +1108,7 @@ function main() {
930
1108
  case 'import': cmdImport(positional[0]); break;
931
1109
  case 'token': {
932
1110
  const sub = positional[0];
933
- if (sub === 'new') cmdTokenNew(opts.agent);
1111
+ if (sub === 'new') cmdTokenNew(opts.agent, opts);
934
1112
  else if (sub === 'list') cmdTokenList();
935
1113
  else if (sub === 'revoke') cmdTokenRevoke(opts.agent);
936
1114
  else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
@@ -968,4 +1146,7 @@ module.exports = {
968
1146
  saveConfig: saveConfig,
969
1147
  loadTokens: loadTokens,
970
1148
  saveTokens: saveTokens,
1149
+ collectEntryFiles: collectEntryFiles,
1150
+ migrateLayout: migrateLayout,
1151
+ typeSubdir: typeSubdir,
971
1152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -15,6 +15,7 @@
15
15
  "SKILL.md",
16
16
  "LICENSE",
17
17
  "README.md",
18
+ "USER_GUIDE.md",
18
19
  "install.sh",
19
20
  "references",
20
21
  "scripts",
@@ -1,4 +1,4 @@
1
- # yotta-memory 协议规范 v0.2.9
1
+ # yotta-memory 协议规范 v0.5.0
2
2
 
3
3
  > 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
4
4
  > 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
@@ -17,15 +17,29 @@
17
17
 
18
18
  ```
19
19
  <root>/
20
- ├── facts/ # FACT 事实(公共可共享)
21
- ├── prefs/ # PREF 偏好(私密)
22
- ├── bounds/ # BOUND 边界(私密)
23
- ├── commits/ # COMMIT 承诺(私密)
24
- ├── .archive/ # 归档区(archive 命令移入)
25
- ├── index.json # 反向索引 + TF 打分
26
- └── README.md # 记忆库说明
20
+ ├── facts/ # FACT 事实(公共可共享)
21
+ ├── private/ # 私密区(按 owner 物理分目录)
22
+ │ └── <owner>/ # 每个智能体一个子目录
23
+ │ ├── prefs/ # PREF 偏好(该 owner 私密)
24
+ │ ├── bounds/ # BOUND 边界(该 owner 私密)
25
+ │ └── commits/ # COMMIT 承诺(该 owner 私密)
26
+ ├── .archive/ # 归档区(archive 命令移入)
27
+ ├── index.json # 反向索引 + TF 打分(见下方说明)
28
+ ├── agents.json # 智能体身份登记表(iam 写入,唯一性强制)
29
+ └── README.md # 记忆库说明
27
30
  ```
28
31
 
32
+ > **`index.json` 说明**:条目内的 `tokens` 字段是中文分词的「词频表」,供 recall 的 TF 打分使用,**不是**访问令牌。真正的访问令牌由 `token` 命令生成,存放在 `<root>/.server/tokens.json`,仅在局域网 `serve` 模式下用于请求鉴权。
33
+
34
+ > **目录结构(v0.5.0)**:私密记忆按 `owner` 物理分目录存放于 `private/<owner>/<type>/`。旧版根下平铺的 `prefs/` `bounds/` `commits/` 会在 `reindex`(或首次 recall 建索引)时按 frontmatter `owner` 自动迁移到 `private/<owner>/<type>/`。**AI 读写红线**:记忆读写一律走 `yotta-memory` CLI / MCP 工具;禁止用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件——否则会绕过 `scope/owner` 权限边界,读到其它智能体的私密内容。
35
+
36
+ ## 2.5 智能体身份(谁在写 / 谁在读)
37
+
38
+ - **agent ID 必须全局唯一**:`iam <id>` 写入 `agents.json`(记忆库根目录),唯一性强制——ID 已被其它主机 / 来源(含远端 token 登记)占用则拒绝,确认是同一智能体才 `--force`。
39
+ - **当次身份声明**:`whoami` / MCP `agent_info` 读「当次声明身份」——本机 `YOTTA_AGENT_ID`(stdio 由 MCP 配置 `env` 注入,CLI 用 `--agent`/环境变量);远端 `X-Agent-Id` 请求头(经 token 绑定校验)。不猜不默认。
40
+ - **自我档案**:`iam` 自动写一条 PREF `subject=自我接入档案`(owner=自己),statement 为 `; ` 分隔的 key:value——`agent_id / host / memory_home / mcp_mode(stdio|http) / engine_url(仅远端) / token(仅远端;本机不存 token)`。
41
+ - **私密记忆必须有 owner**:PREF / BOUND / COMMIT 写入时未声明身份(owner 空)直接拒绝(公共 FACT 不受影响),从机制上防止「抄别人的 ID」。
42
+
29
43
  ## 3. 文件格式
30
44
 
31
45
  每个记忆一条独立 `.md` 文件,文件名 `<YYYY-MM-DD>-<NNNN>.md`(NNNN 为当日序号):
@@ -72,12 +86,14 @@ immutable: false
72
86
  | `COMMIT` | 承诺 / 约定 | 私密 |
73
87
 
74
88
  - 写入纪律:公共事实用 FACT;涉及用户偏好 / 边界 / 承诺一律用私密类型,避免公共区泄露。
75
- - 多智能体协作:每个智能体只读自己的私密区(如 `~/.yottamemory/<agent-id>/prefs/` 可自行约定),公共 FACT 共享。
89
+ - 多智能体协作:每个智能体只读自己的私密区(如 `private/<owner>/<type>/` 可自行约定),公共 FACT 共享。
76
90
 
77
91
  ## 隔离说明
78
92
 
79
93
  > 隔离说明:scope: private 保证的是"AI 之间的语义隔离"——其他智能体在正常 recall 会话中不会被搜到、也不会主动去读本条私密记忆。但它不是文件系统级机密保护:在你自己的机器上,作为所有者你有权看到任何记忆文件,数据主权在你。本工具不承诺对抗同权限的本地进程主动读取。
80
94
 
95
+ > **`--agent` 语义**:`--agent <其它agent>` 仅作身份声明 / 展示用(以该身份检索或模拟),并不授予跨智能体私密读取——读取其它智能体私密仍需满足 grant / identity=user / `--unsafe` 任一授权。
96
+
81
97
  读分区边界(recall 三态):
82
98
 
83
99
  | 目标 | 行为 |