@yottameta/yotta-memory 0.2.9 → 0.4.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
@@ -1,6 +1,19 @@
1
- # 元忆(yotta-memory)
1
+ <p align="center">
2
+ <img src="assets/banner.png" alt="yotta-memory banner" width="100%" />
3
+ </p>
2
4
 
3
- > 元忆 —— 有权限边界的文件式智能体记忆。让任何 AI 智能体活过会话:开工 `recall` 恢复上下文、重要信息 `remember` 落盘、收工归档。文件式、零依赖、可 diff / 回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离——谁该读、谁不该读,由机制而非 AI 自觉决定。
5
+ <h1 align="center">元忆(yotta-memory)</h1>
6
+
7
+ <p align="center">元忆 —— 有权限边界的文件式智能体记忆。让任何 AI 智能体活过会话:开工 <code>recall</code> 恢复上下文、重要信息 <code>remember</code> 落盘、收工归档。文件式、零依赖、可 diff / 回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离——谁该读、谁不该读,由机制而非 AI 自觉决定。</p>
8
+
9
+ <p align="center">
10
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue" /></a>
11
+ <a href="https://agentskills.io/"><img alt="Standard: agentskills.io" src="https://img.shields.io/badge/standard-agentskills.io-orange" /></a>
12
+ <a href="https://www.npmjs.com/package/@yottameta/yotta-memory"><img alt="npm package" src="https://img.shields.io/npm/v/@yottameta/yotta-memory" /></a>
13
+ <a href="https://github.com/YottaMeta/yotta-memory"><img alt="GitHub stars" src="https://img.shields.io/github/stars/YottaMeta/yotta-memory" /></a>
14
+ <a href="https://github.com/YottaMeta/yotta-memory/commits/main"><img alt="last commit" src="https://img.shields.io/github/last-commit/YottaMeta/yotta-memory" /></a>
15
+ <a href="https://github.com/YottaMeta/yotta-memory"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen" /></a>
16
+ </p>
4
17
 
5
18
  ## 核心价值
6
19
 
@@ -116,13 +129,17 @@ bash install.sh --dir /path/to/skills
116
129
 
117
130
  | 命令 | 作用 |
118
131
  |---|---|
119
- | `yotta-memory init [--project]` | 初始化记忆库(默认用户级 `~/.yottamemory/`)|
132
+ | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级 `~/.yottamemory/`;--dir 显式指定位置)|
120
133
  | `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入记忆(同 subject+statement 自动更新;--owner 标注归属)|
121
134
  | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
122
135
  | `yotta-memory forget <文件>` | 删除一条记忆(按类型目录路径或文件名)|
123
136
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
124
137
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
125
138
  | `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
139
+ | `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`)|
141
+ | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
142
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows 计划任务,默认 ONLOGON 登录自启;--onstart 开机即启需管理员)|
126
143
 
127
144
  类型:`FACT`(事实,公共共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺)。
128
145
 
@@ -142,6 +159,64 @@ yotta-memory recall --type FACT --limit 10
142
159
 
143
160
  把技能装进智能体后,SKILL.md 会自动教会智能体:开工 `recall` 恢复上下文 → 重要信息 `remember` 落盘 → 收工归档。也可在对话中直接说「记住 XXX」「上次说到哪了」。
144
161
 
162
+ ## 局域网多机共享(便携记忆盘模式)
163
+
164
+ 记忆库可以装在任何主机或硬盘上(= 记忆引擎),供局域网内其它主机上的智能体远程接入:
165
+
166
+ - **本机直连**:CLI 直接读写,无需 token;
167
+ - **远程接入**:引擎主机运行 `yotta-memory serve` 常驻(或 `lan enable` 注册开机自启),远程智能体通过 MCP 以 `url + token` 连接。
168
+ - **本地零进程**:本机 MCP 客户端可用 `serve --stdio` 按需拉起 CLI(无常驻进程)。
169
+
170
+ ### 引擎侧(记忆所在主机)
171
+
172
+ 1. 初始化或接入记忆库(见 CLI 用法)。
173
+ 2. 为需要访问的每个智能体生成独立 token:
174
+ ```bash
175
+ yotta-memory token new --agent <智能体ID> # 打印一次,如 ytm_...
176
+ yotta-memory token list # 查看已登记智能体
177
+ yotta-memory token revoke --agent <智能体ID> # 吊销
178
+ ```
179
+ 3. 启动服务(默认监听 0.0.0.0:8787,Bearer token + X-Agent-Id 鉴权)——临时运行或注册开机自启二选一:
180
+ ```bash
181
+ yotta-memory serve # 临时前台运行
182
+ yotta-memory lan enable # 注册开机自启(Windows 计划任务,登录自启)
183
+ yotta-memory lan status # 查看自启状态
184
+ ```
185
+ > `lan enable --onstart` 改为开机即启(需管理员);`lan disable` 移除自启。
186
+
187
+ > 首次监听 0.0.0.0 时,Windows / 系统防火墙可能询问是否放行,需允许放行,否则局域网内其它主机无法访问;`--no-auth` 会关闭鉴权,仅限可信内网使用。
188
+
189
+ ### 客户端侧(远程智能体)
190
+
191
+ 在智能体 MCP 配置中登记连接(`url` + 两个请求头):
192
+
193
+ ```json
194
+ {
195
+ "mcpServers": {
196
+ "yotta-memory": {
197
+ "url": "http://<引擎主机IP>:8787/mcp",
198
+ "headers": {
199
+ "Authorization": "Bearer <TOKEN>",
200
+ "X-Agent-Id": "<本智能体ID>"
201
+ }
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
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 私密隔离)。
208
+
209
+ ### 位置持久化
210
+
211
+ CLI 会持久记住记忆库位置(`~/.yottamemory/config.json`),同一主机上的智能体此后开工 `recall` 自动使用正确位置,无需每次指定:
212
+
213
+ ```bash
214
+ yotta-memory config set memory_home <记忆库目录>
215
+ yotta-memory config get
216
+ ```
217
+
218
+ 位置解析优先级:`YOTTA_MEMORY_HOME`(临时覆盖)> `config.json#memory_home`(持久)> 默认 `~/.yottamemory`。把记忆库放在硬盘上、插到任意主机后执行一次 `config set memory_home`(或用 `yotta-memory init --dir <目录>` 一步初始化到指定位置),即可在该主机恢复全部记忆。
219
+
145
220
  ## 开发与校验
146
221
 
147
222
  本项目内运行:`python tools/validate-skill.py yotta-memory`。
package/SKILL.md CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: yotta-memory
3
- description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档"
4
- version: 0.2.9
3
+ description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆"
4
+ version: 0.4.0
5
5
  license: MIT
6
6
  ---
7
7
 
8
8
  # yotta-memory(元忆)— 有权限边界的文件式智能体记忆
9
9
 
10
10
  > 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
11
- > 版本:0.2.9 | 最后更新:2026-08-23
11
+ > 版本:0.4.0 | 最后更新:2026-08-25
12
12
 
13
13
  ## 这是什么
14
14
 
@@ -35,18 +35,129 @@ license: MIT
35
35
 
36
36
  | 命令 | 作用 |
37
37
  |---|---|
38
- | `yotta-memory init [--project]` | 初始化记忆库(默认用户级)|
38
+ | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级;--dir 显式指定位置)|
39
39
  | `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入(同 subject+statement 自动更新;--owner 标注归属,默认取 agent id)|
40
40
  | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
41
41
  | `yotta-memory forget <文件>` | 删除(按类型目录路径或文件名)|
42
42
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
43
43
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
44
44
  | `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
45
+ | `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`)|
47
+ | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
48
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows 计划任务,默认 ONLOGON 登录自启;--onstart 开机即启需管理员)|
45
49
 
46
50
  ## 存储格式(摘要)
47
51
 
48
52
  `<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。顶层另有 `index.json`(反向索引 + TF 打分)。
49
53
 
54
+ ## 便携记忆盘模式(局域网多机共享)
55
+
56
+ 记忆库可装在任何主机或硬盘上(= 记忆引擎):本机 AI 用 CLI 直连,局域网内其它主机上的 AI 经 MCP 远程接入。全程 AI 自主执行;关键动作(🔒)执行前一句话说明「做什么 + 为什么 + 影响」并征得用户同意,用户只需回答「可以 / 好 / 允许」;除复制粘贴 token 外,用户不做任何手工操作。
57
+
58
+ ### 4.0 前置:CLI 自装 + 记忆位置准备 + 持久记住
59
+
60
+ **步骤 0.1 检查 CLI 是否已装**
61
+ ```bash
62
+ yotta-memory --version # 或 command -v yotta-memory / where yotta-memory
63
+ ```
64
+ 已装 → 跳过。未装 → 🔒 **征得同意后**自行安装(三选一,AI 判断):
65
+ | 方式 | 命令 | 适用 |
66
+ |---|---|---|
67
+ | npx 临时 | `npx -y @yottameta/yotta-memory` | 任何有 Node 的机器 |
68
+ | npm 全局 | `npm i -g @yottameta/yotta-memory` | 长期使用 |
69
+ | install.sh | `bash <(curl -s https://raw.githubusercontent.com/YottaMeta/yotta-memory/main/install.sh)` | 离线 / 国内 / 无 npm |
70
+ 装后验证:`yotta-memory --version` 出版本即就绪。
71
+
72
+ **步骤 0.2 确定记忆存储位置**
73
+ | 位置 | 目录 | 适用 |
74
+ |---|---|---|
75
+ | 用户级(默认) | `~/.yottamemory` | 个人跨项目记忆 |
76
+ | 项目级 | `<repo>/.yottamemory` | 随项目提交共享 |
77
+ | 便携记忆盘 | 硬盘上目录(如 `D:\memory` / 挂载点) | 记忆盘 / 局域网共享 |
78
+
79
+ **步骤 0.3 接入现有 vs 初始化新库(关键判断)**
80
+ 1. 检查目标目录是否已是记忆库:存在 `facts/` 等子目录或 `index.json`。
81
+ 2. 已是 → 🔒 **征得同意后**直接接入(不重复初始化):设位置后 `recall` 验证能读到旧记忆。
82
+ 3. 不是 → 🔒 **征得同意后**初始化新库:`yotta-memory init --dir <目录>` 或 `YOTTA_MEMORY_HOME=<目录> yotta-memory init`(项目级用 `init --project`)。
83
+ 4. 验证:`recall` 跑通。
84
+
85
+ **步骤 0.4 持久记住位置(重要)**
86
+ - 🔒 **征得同意后**执行:`yotta-memory config set memory_home <目录>`(项目级可跳过)。
87
+ - 作用:写入 `~/.yottamemory/config.json`,此后该机器 AI 开工 `recall` 自动用对位置。
88
+ - 验证:`yotta-memory config get` 显示 memory_home 与预期一致。
89
+
90
+ > 记忆盘场景:硬盘插上 → AI 检查盘上目录是否有数据 → 有则接入 + config 记住 → 插盘即恢复,机器记住位置。
91
+
92
+ **步骤 0.5 启动记忆引擎(仅引擎主机,供远程接入)**
93
+ - 本机若作引擎:🔒 **征得同意后**启动服务——临时运行 `yotta-memory serve`(默认 `0.0.0.0:8787`,Bearer token + X-Agent-Id 鉴权;`--no-auth` 仅限可信内网),或注册开机自启 `yotta-memory lan enable`(Windows 计划任务,默认登录自启;`--onstart` 开机即启需管理员)。
94
+ - 本地零进程模式:本机 AI 也可用 `serve --stdio` 由 MCP 客户端按需拉起 CLI(无常驻进程,照灵魂盘)。
95
+ - 远程客户端接入前,先确认引擎主机 serve 已运行(`lan status` 可查)。
96
+
97
+ ### 4.1 触发
98
+ 用户提及「记忆盘 / 记忆引擎 / 共享记忆 / 局域网记忆」,或开工 `recall` 发现需要访问远程记忆库时进入本流程。
99
+
100
+ ### 4.2 第一步:检查是否已配置(避免重复询问)
101
+ 1. `YOTTA_MEMORY_HOME` 或 `config get` 显示 memory_home 已指向本机可读目录 → 「本机直连」,直接用 CLI。
102
+ 2. 当前智能体 MCP 配置已有 `yotta-memory` server → 「远程已配置」,直接用 MCP tools。
103
+ 3. 都没有 → 进入 4.3。
104
+
105
+ ### 4.3 第二步:向用户确认引擎位置(AI 提问,用户回答)
106
+ 1. **记忆引擎在本机,还是局域网内其他主机?**(本机 / 远程)
107
+ 2. 若**远程**:引擎主机 **IP**(或主机名)?**端口**?(默认 8787)
108
+ 3. 本机 → 4.4;远程 → 4.5。
109
+
110
+ ### 4.4 本机直连
111
+ 确认记忆库目录(`config get` / `YOTTA_MEMORY_HOME` / 默认 `~/.yottamemory`)→ 直接 CLI 读写,**不配置 MCP、不需要 token**。
112
+
113
+ ### 4.5 远程连接:AI 引导用户获取 token(用户只做复制粘贴)
114
+ 1. AI 告知需要为本智能体申请访问 token。
115
+ 2. AI 引导用户在**引擎主机**执行:`yotta-memory token new --agent <本智能体ID>`(引擎主机没装 → 按 4.0 先装;或请引擎主机上的 AI 代执行)。
116
+ 3. 命令打印 token(`ytm_...`),只打印一次,请用户妥善保管。
117
+ 4. AI 请用户复制 token 发给 AI。
118
+ 5. 用户发来 → AI 继续 4.6。
119
+
120
+ > 用户不会操作时:AI 逐步引导(开终端 → 粘贴命令 → 回车 → 复制输出),直到成功。**除复制粘贴外用户不做别的**。
121
+
122
+ ### 4.6 配置 MCP(AI 自己完成,🔒 需同意)
123
+ 1. 🔒 说明将把 yotta-memory 写入本智能体 MCP 配置并请用户同意;
124
+ 2. 定位当前智能体 MCP 配置文件(见 4.7);
125
+ 3. 添加 server(JSON 见 4.8);
126
+ 4. 按当前智能体机制重载 MCP(必要时请用户重启会话);
127
+ 5. 用 MCP tools 读写记忆。
128
+
129
+ > MCP 工具集与 CLI 一致:remember / recall / search / forget / archive / reindex / export / import;管理动作(init / config / token / lan / serve)不进 MCP,token 管理不远程暴露。
130
+
131
+ ### 4.7 MCP 配置位置表
132
+ | 智能体 | 常见 MCP 配置位置 |
133
+ |---|---|
134
+ | Claude Code | 项目 `.mcp.json` 或用户级 `~/.claude.json` |
135
+ | Codex | `~/.codex/config.toml`(`[mcp_servers]`) |
136
+ | Cursor | 项目 `.cursor/mcp.json` 或用户级 |
137
+ | 其它(Trae / Qwen / Comate / Kimi 等) | 各自 MCP 配置 |
138
+
139
+ ### 4.8 通用 MCP server 配置片段
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "yotta-memory": {
144
+ "url": "http://<IP>:8787/mcp",
145
+ "headers": {
146
+ "Authorization": "Bearer <TOKEN>",
147
+ "X-Agent-Id": "<本智能体ID>"
148
+ }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### 4.9 验证连接(循环兜底)
155
+ - 🔒 连接远程引擎前已获同意(4.5 / 4.6)→ 调一次 `recall` / `search` 确认能读到记忆 → 成功。
156
+ - 失败:查 IP / 端口 / token 完整性 / 防火墙 / token 吊销;仍失败回 4.3。
157
+
158
+ ### 4.10 复用
159
+ - 成功后优先复用现有连接;失败(token 吊销等)再回 4.3。
160
+
50
161
  ## 渐进披露
51
162
 
52
- - 协议细节、目录结构与类型规则见 `references/protocol.md`,需要时读取,不要每次全读。
163
+ - 协议细节、目录结构与类型规则见 `references/protocol.md`,需要时读取,不要每次全读。
Binary file
@@ -1,22 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  // yotta-memory(元忆): 有权限边界的文件式智能体记忆 CLI(零依赖)
3
- // v0.2 增强:① index.json 索引+TF 打分 ② 质量分+盖棺分(access_count/last_accessed)③ 读取分区(scope/owner)
3
+ // v0.4.0 新增:lan(Windows 计划任务开机自启 serve)/ init --dir(显式指定位置)/ serve --stdio(本地零进程模式)/ MCP 工具补 reindex/export/import
4
4
  'use strict';
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
+ const crypto = require('crypto');
10
+ const http = require('http');
11
+ const child_process = require('child_process');
9
12
 
10
- const VERSION = '0.2.9';
13
+ const VERSION = '0.4.0';
11
14
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
12
15
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
13
16
  const ARCHIVE_DIR = '.archive';
14
17
  const INDEX_FILE = 'index.json';
15
18
  const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
16
19
  const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'access_count', 'last_accessed'];
20
+ const CONFIG_FILE = 'config.json';
21
+ const SERVER_SUBDIR = '.server';
22
+ const TOKENS_FILE = 'tokens.json';
17
23
 
24
+ // ---- 全局配置(记忆库位置持久化,固定 ~/.yottamemory/config.json)----
25
+ function configPath() {
26
+ return path.join(os.homedir(), '.yottamemory', CONFIG_FILE);
27
+ }
28
+ function loadConfig() {
29
+ try { return JSON.parse(fs.readFileSync(configPath(), 'utf8')) || {}; } catch (e) { return {}; }
30
+ }
31
+ function saveConfig(cfg) {
32
+ fs.mkdirSync(path.dirname(configPath()), { recursive: true });
33
+ fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2), 'utf8');
34
+ }
18
35
  function userRoot() {
19
- return process.env.YOTTA_MEMORY_HOME || path.join(os.homedir(), '.yottamemory');
36
+ if (process.env.YOTTA_MEMORY_HOME) return process.env.YOTTA_MEMORY_HOME;
37
+ const cfg = loadConfig();
38
+ if (cfg.memory_home) return cfg.memory_home;
39
+ return path.join(os.homedir(), '.yottamemory');
20
40
  }
21
41
  function projectRoot() {
22
42
  return path.join(process.cwd(), '.yottamemory');
@@ -249,11 +269,7 @@ function vitality(meta) {
249
269
  }
250
270
  function loadGrants(root) {
251
271
  const fp = path.join(root, 'grants.json');
252
- try {
253
- return JSON.parse(fs.readFileSync(fp, 'utf8')) || {};
254
- } catch (e) {
255
- return {};
256
- }
272
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')) || {}; } catch (e) { return {}; }
257
273
  }
258
274
  function hasGrant(userAgent, ownerAgent) {
259
275
  if (!userAgent || !ownerAgent) return false;
@@ -269,33 +285,30 @@ function hasGrant(userAgent, ownerAgent) {
269
285
  function classifyRead(entry, agent, ownerFilter, unsafe) {
270
286
  if (entry.scope === 'public') return 'read';
271
287
  const owner = entry.owner || '';
272
- // 无归属的 private:所有 agent 视为自身私密(默认行为保留)
273
288
  if (!owner) return 'read';
274
- // 归属等于当前 agent 或(显式 --owner 等于当前 agent):自身授权
275
289
  if (agent && owner === agent) return 'read';
276
290
  if (ownerFilter && owner === ownerFilter && agent && ownerFilter === agent) return 'read';
277
- // 越界读:需要 --unsafe / identity=user / grant 授权,否则拒绝
278
291
  if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent && '' + agent).toLowerCase() === 'user') return 'read';
279
292
  if (hasGrant(agent, owner)) return 'read';
280
293
  return 'denied';
281
294
  }
282
295
 
283
- // ---- 命令 ----
284
- function cmdInit(opts) {
285
- const root = opts.project ? projectRoot() : userRoot();
296
+ // ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
297
+ function initCore(opts) {
298
+ const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
286
299
  ensureInit(root);
287
- console.log('已初始化记忆库: ' + root);
300
+ return { error: false, text: '已初始化记忆库: ' + root };
288
301
  }
289
-
290
- function cmdRemember(type, subject, statement, opts) {
302
+ function rememberCore(type, subject, statement, opts) {
291
303
  const root = userRoot();
292
304
  ensureInit(root);
293
305
  const t = String(type).toUpperCase();
294
- const dir = path.join(root, typeDir(t));
306
+ if (!TYPE_DIRS[t]) return { error: true, text: '未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')' };
307
+ const dir = path.join(root, TYPE_DIRS[t]);
295
308
  const stmt = String(statement || '').trim();
296
309
  const subj = String(subject || '').trim();
297
- if (!stmt) { console.error('statement 不能为空'); process.exit(2); }
298
- if (!subj) { console.error('subject 不能为空'); process.exit(2); }
310
+ if (!stmt) return { error: true, text: 'statement 不能为空' };
311
+ if (!subj) return { error: true, text: 'subject 不能为空' };
299
312
  const owner = opts.owner || currentAgent();
300
313
  const scope = opts.scope || defaultScope(t);
301
314
  if (fs.existsSync(dir)) {
@@ -310,8 +323,7 @@ function cmdRemember(type, subject, statement, opts) {
310
323
  if (scope && !meta.scope) patch.scope = scope;
311
324
  rewriteFrontmatter(fp, patch);
312
325
  upsertIndexEntry(root, readEntry(fp, root));
313
- console.log('已更新: ' + fp);
314
- return;
326
+ return { error: false, text: '已更新: ' + fp };
315
327
  }
316
328
  }
317
329
  }
@@ -325,22 +337,17 @@ function cmdRemember(type, subject, statement, opts) {
325
337
  };
326
338
  fs.writeFileSync(file, frontmatterToText(rec, stmt), 'utf8');
327
339
  upsertIndexEntry(root, readEntry(file, root));
328
- console.log('已记录: ' + file);
340
+ return { error: false, text: '已记录: ' + file };
329
341
  }
330
-
331
- function cmdRecall(query, opts) {
342
+ function recallCore(query, opts) {
332
343
  const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
333
- if (!roots.length) {
334
- console.log('记忆库不存在,请先运行: yotta-memory init');
335
- return;
336
- }
344
+ if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
337
345
  const limit = opts.limit || 50;
338
346
  const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
339
347
  const q = query ? String(query).toLowerCase() : '';
340
348
  const agent = opts.agent || currentAgent();
341
349
  const ownerFilter = opts.owner || '';
342
350
  const allSafe = !!opts.unsafe;
343
- // 是否显式发起跨智能体读取(--all 或 --owner <其它agent>):只有此时越界读才报错/警告,默认 recall 保持原有静默跳过
344
351
  const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
345
352
  const hits = [];
346
353
  let deniedCount = 0;
@@ -375,12 +382,9 @@ function cmdRecall(query, opts) {
375
382
  const shown = hits.slice(0, limit);
376
383
  if (!shown.length) {
377
384
  if (deniedCount > 0 && explicitCross) {
378
- console.log('检测到 ' + deniedCount + ' 条越界访问已被拒绝。');
379
- console.log('如需读取其它智能体私密记忆,请加 --unsafe(用户显式授权)或 --owner user。');
380
- process.exit(3);
385
+ return { error: false, exitCode: 3, text: '检测到 ' + deniedCount + ' 条越界访问已被拒绝。\n如需读取其它智能体私密记忆,请加 --unsafe(用户显式授权)或 --owner user。' };
381
386
  }
382
- console.log('无匹配记忆。');
383
- return;
387
+ return { error: false, exitCode: 0, text: '无匹配记忆。' };
384
388
  }
385
389
  const touchRel = shown.filter(function (h) { return !h.entry.immutable; }).map(function (h) { return h.entry.file; });
386
390
  if (touchRel.length) {
@@ -389,17 +393,17 @@ function cmdRecall(query, opts) {
389
393
  touchIndex(root, touchRel);
390
394
  }
391
395
  }
392
- console.log('共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):');
396
+ const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
393
397
  for (const h of shown) {
394
- console.log('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
395
- console.log(' ' + path.join(h.root, h.entry.file));
398
+ lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
399
+ lines.push(' ' + path.join(h.root, h.entry.file));
396
400
  }
397
401
  if (deniedCount > 0 && explicitCross) {
398
- console.log('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
402
+ lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
399
403
  }
404
+ return { error: false, exitCode: 0, text: lines.join('\n') };
400
405
  }
401
-
402
- function cmdForget(fileRef) {
406
+ function forgetCore(fileRef) {
403
407
  const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
404
408
  const ref = String(fileRef || '').replace(/\\/g, '/');
405
409
  const slash = ref.indexOf('/');
@@ -430,17 +434,16 @@ function cmdForget(fileRef) {
430
434
  }
431
435
  if (target) break;
432
436
  }
433
- if (!target) { console.error('未找到记忆文件: ' + fileRef); process.exit(2); }
437
+ if (!target) return { error: true, text: '未找到记忆文件: ' + fileRef };
434
438
  fs.unlinkSync(target);
435
439
  if (targetRoot) removeIndexEntry(targetRoot, targetRel);
436
- console.log('已删除: ' + target);
440
+ return { error: false, text: '已删除: ' + target };
437
441
  }
438
-
439
- function cmdArchive(opts) {
442
+ function archiveCore(opts) {
440
443
  const days = opts.days || 180;
441
444
  const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : 0.4;
442
445
  const root = userRoot();
443
- if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
446
+ if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
444
447
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
445
448
  let moved = 0;
446
449
  const movedFiles = [];
@@ -465,9 +468,33 @@ function cmdArchive(opts) {
465
468
  }
466
469
  }
467
470
  if (movedFiles.length) removeIndexEntries(root, movedFiles);
468
- console.log('已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR));
471
+ return { error: false, text: '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) };
469
472
  }
470
473
 
474
+ // ---- 命令包装(CLI 入口)----
475
+ function cmdInit(opts) {
476
+ const r = initCore(opts);
477
+ console.log(r.text);
478
+ }
479
+ function cmdRemember(type, subject, statement, opts) {
480
+ const r = rememberCore(type, subject, statement, opts);
481
+ console.log(r.text);
482
+ if (r.error) process.exit(2);
483
+ }
484
+ function cmdRecall(query, opts) {
485
+ const r = recallCore(query, opts);
486
+ console.log(r.text);
487
+ if (r.exitCode) process.exit(r.exitCode);
488
+ }
489
+ function cmdForget(fileRef) {
490
+ const r = forgetCore(fileRef);
491
+ console.log(r.text);
492
+ if (r.error) process.exit(2);
493
+ }
494
+ function cmdArchive(opts) {
495
+ const r = archiveCore(opts);
496
+ console.log(r.text);
497
+ }
471
498
  function cmdReindex() {
472
499
  const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
473
500
  if (!roots.length) { console.log('记忆库不存在。'); return; }
@@ -476,7 +503,6 @@ function cmdReindex() {
476
503
  console.log('已重建索引 ' + root + '(' + n + ' 条)');
477
504
  }
478
505
  }
479
-
480
506
  function collectAll(root) {
481
507
  const out = [];
482
508
  for (const t of Object.keys(TYPE_DIRS)) {
@@ -491,23 +517,26 @@ function collectAll(root) {
491
517
  }
492
518
  return out;
493
519
  }
494
-
495
- function cmdExport(outPath) {
496
- const root = userRoot();
497
- if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
498
- const data = { exported: today(), version: VERSION, memories: collectAll(root) };
499
- const target = outPath || 'yottamemory-export-' + today() + '.json';
520
+ function exportCore(root, outPath) {
521
+ const data = { format: 'yottamemory', version: 1, exported: today(), memories: collectAll(root) };
522
+ const target = outPath || path.join(root, 'yottamemory-export-' + today() + '.json');
500
523
  fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
501
- console.log('已导出 ' + data.memories.length + ' 条记忆 -> ' + target);
502
- }
503
-
504
- function cmdImport(src) {
505
- if (!src || !fs.existsSync(src)) { console.error('请提供 JSON 文件路径'); process.exit(2); }
506
- const data = JSON.parse(fs.readFileSync(src, 'utf8'));
507
- if (!data.memories || !Array.isArray(data.memories)) { console.error('JSON 格式不正确(缺少 memories 数组)'); process.exit(2); }
508
- const root = userRoot();
524
+ return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target };
525
+ }
526
+ function importCore(root, src) {
527
+ if (!src) return { error: true, text: '请提供 JSON 文件路径' };
528
+ let fp = String(src);
529
+ if (!fs.existsSync(fp)) {
530
+ const alt = path.resolve(root, fp);
531
+ if (fs.existsSync(alt)) fp = alt;
532
+ }
533
+ if (!fs.existsSync(fp)) return { error: true, text: 'JSON 文件不存在: ' + src };
534
+ let data;
535
+ try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); }
536
+ catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
537
+ if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
509
538
  ensureInit(root);
510
- let n = 0;
539
+ let m = 0;
511
540
  for (const item of data.memories) {
512
541
  const meta = item.meta || {};
513
542
  const t = (meta.type || 'FACT').toUpperCase();
@@ -532,32 +561,320 @@ function cmdImport(src) {
532
561
  };
533
562
  fs.writeFileSync(file, frontmatterToText(rec, rec.statement), 'utf8');
534
563
  upsertIndexEntry(root, readEntry(file, root));
535
- n++;
564
+ m++;
536
565
  }
537
- console.log('已导入 ' + n + ' 条记忆 -> ' + root);
566
+ return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root };
567
+ }
568
+ function cmdExport(outPath) {
569
+ const r = exportCore(userRoot(), outPath || 'yottamemory-export-' + today() + '.json');
570
+ console.log(r.text);
571
+ }
572
+ function cmdImport(src) {
573
+ const r = importCore(userRoot(), src);
574
+ console.log(r.text);
575
+ if (r.error) process.exit(2);
576
+ }
577
+
578
+ // ---- token 管理(每智能体一个,登记 <记忆库>/.server/tokens.json)----
579
+ function serverDir(root) { return path.join(root, SERVER_SUBDIR); }
580
+ function tokensPath(root) { return path.join(serverDir(root), TOKENS_FILE); }
581
+ function loadTokens(root) {
582
+ try { return JSON.parse(fs.readFileSync(tokensPath(root), 'utf8')) || {}; } catch (e) { return {}; }
583
+ }
584
+ function saveTokens(root, data) {
585
+ fs.mkdirSync(serverDir(root), { recursive: true });
586
+ fs.writeFileSync(tokensPath(root), JSON.stringify(data, null, 2), 'utf8');
587
+ try { fs.chmodSync(tokensPath(root), 0o600); } catch (e) {}
588
+ }
589
+ function cmdTokenNew(agentId) {
590
+ const root = userRoot();
591
+ ensureInit(root);
592
+ if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
593
+ const data = loadTokens(root);
594
+ data.version = 1;
595
+ data.tokens = data.tokens || {};
596
+ const token = 'ytm_' + crypto.randomBytes(16).toString('hex');
597
+ data.tokens[agentId] = { token: token, created: today() };
598
+ saveTokens(root, data);
599
+ console.log(token);
600
+ }
601
+ function cmdTokenList() {
602
+ const root = userRoot();
603
+ const data = loadTokens(root);
604
+ const map = data.tokens || {};
605
+ const ids = Object.keys(map);
606
+ if (!ids.length) { console.log('暂无已登记 token(yotta-memory token new --agent <id> 生成)'); return; }
607
+ console.log('已登记智能体:');
608
+ for (const id of ids) console.log(' ' + id + ' (创建于 ' + map[id].created + ')');
609
+ }
610
+ function cmdTokenRevoke(agentId) {
611
+ const root = userRoot();
612
+ if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
613
+ const data = loadTokens(root);
614
+ data.tokens = data.tokens || {};
615
+ if (data.tokens[agentId]) { delete data.tokens[agentId]; saveTokens(root, data); console.log('已吊销: ' + agentId); }
616
+ else { console.log('未找到已登记智能体: ' + agentId); }
538
617
  }
539
618
 
619
+ // ---- config 命令 ----
620
+ function cmdConfigSet(key, value) {
621
+ if (key !== 'memory_home') { console.error('未知配置项: ' + key + '(可用: memory_home)'); process.exit(2); }
622
+ if (!value) { console.error('缺少值: config set memory_home <目录>'); process.exit(2); }
623
+ const cfg = loadConfig();
624
+ cfg.memory_home = value;
625
+ saveConfig(cfg);
626
+ console.log('已记住记忆库位置: ' + value);
627
+ }
628
+ function cmdConfigGet() {
629
+ const cfg = loadConfig();
630
+ console.log('memory_home: ' + (cfg.memory_home || '(未设置,默认 ~/.yottamemory)'));
631
+ console.log('当前生效用户级位置: ' + userRoot());
632
+ if (cfg.serve && Object.keys(cfg.serve).length) console.log('serve: ' + JSON.stringify(cfg.serve));
633
+ }
634
+
635
+ // ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
636
+ function mcpTools() {
637
+ return [
638
+ { name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
639
+ { name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20)。只返回当前智能体可读记忆', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' } } } },
640
+ { name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20)', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' } } } },
641
+ { name: 'forget', description: '删除一条记忆。file 为记忆文件路径(如 facts/2026-08-24-0001.md 或文件名)', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
642
+ { name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' } } } },
643
+ { name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
644
+ { name: 'export', description: '导出全部记忆到引擎主机上的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
645
+ { name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
646
+ ];
647
+ }
648
+ function callTool(name, args, ctx) {
649
+ const agent = ctx.agent || '';
650
+ try {
651
+ if (name === 'remember') {
652
+ const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: args.owner || agent });
653
+ return { text: r.text, error: r.error };
654
+ }
655
+ if (name === 'recall' || name === 'search') {
656
+ const r = recallCore(args.query ? String(args.query) : null, { limit: args.limit || 20, type: args.type ? String(args.type) : null, agent: agent });
657
+ return { text: r.text, error: r.error };
658
+ }
659
+ if (name === 'forget') {
660
+ const r = forgetCore(String(args.file || ''));
661
+ return { text: r.text, error: r.error };
662
+ }
663
+ if (name === 'archive') {
664
+ const r = archiveCore({ days: args.days, threshold: args.threshold });
665
+ return { text: r.text, error: r.error };
666
+ }
667
+ if (name === 'reindex') {
668
+ const root = userRoot();
669
+ if (!fs.existsSync(root)) return { text: '记忆库不存在。', error: false };
670
+ const cnt = buildIndex(root).length;
671
+ return { text: '已重建索引 ' + root + '(' + cnt + ' 条)', error: false };
672
+ }
673
+ if (name === 'export') {
674
+ const r = exportCore(userRoot(), args.out ? String(args.out) : null);
675
+ return { text: r.text, error: r.error };
676
+ }
677
+ if (name === 'import') {
678
+ const r = importCore(userRoot(), String(args.src || ''));
679
+ return { text: r.text, error: r.error };
680
+ }
681
+ return { text: '未知工具: ' + name, error: true };
682
+ } catch (e) {
683
+ return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
684
+ }
685
+ }
686
+ function handleMessage(msg, ctx) {
687
+ if (!msg || msg.jsonrpc !== '2.0') return { jsonrpc: '2.0', id: msg && msg.id, error: { code: -32600, message: 'invalid request' } };
688
+ const id = msg.id;
689
+ if (id === undefined || id === null) return null;
690
+ const method = msg.method || '';
691
+ const params = msg.params || {};
692
+ if (method === 'initialize') {
693
+ return { jsonrpc: '2.0', id: id, result: { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'yotta-memory', version: VERSION } } };
694
+ }
695
+ if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
696
+ if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools() } };
697
+ if (method === 'tools/call') {
698
+ const out = callTool(params.name || '', params.arguments || {}, ctx);
699
+ return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
700
+ }
701
+ return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
702
+ }
703
+ function cmdServe(opts) {
704
+ if (opts.stdio) { cmdServeStdio(); return; }
705
+ const host = opts.host || '0.0.0.0';
706
+ const port = opts.port || 8787;
707
+ const noAuth = !!opts.noAuth;
708
+ const root = userRoot();
709
+ ensureInit(root);
710
+ function authorize(req) {
711
+ if (noAuth) return { agent: String(req.headers['x-agent-id'] || '') };
712
+ const auth = req.headers['authorization'] || '';
713
+ const m = /^Bearer\s+(.+)$/i.exec(auth);
714
+ if (!m) return null;
715
+ const token = m[1].trim();
716
+ const agent = String(req.headers['x-agent-id'] || '');
717
+ const tokenMap = (loadTokens(root).tokens) || {};
718
+ if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent };
719
+ return null;
720
+ }
721
+ const server = http.createServer(function (req, res) {
722
+ let pathname = '/';
723
+ try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
724
+ if (pathname !== '/mcp') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
725
+ const auth = authorize(req);
726
+ if (!auth) {
727
+ res.writeHead(401, { 'Content-Type': 'application/json' });
728
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id' } }));
729
+ return;
730
+ }
731
+ if (req.method === 'GET') {
732
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
733
+ res.write('event: endpoint\ndata: /mcp\n\n');
734
+ const iv = setInterval(function () { res.write(': keep-alive\n\n'); }, 15000);
735
+ req.on('close', function () { clearInterval(iv); });
736
+ return;
737
+ }
738
+ if (req.method === 'POST') {
739
+ let body = '';
740
+ req.on('data', function (c) { body += c; });
741
+ req.on('end', function () {
742
+ let msg;
743
+ try { msg = JSON.parse(body); } catch (e) {
744
+ res.writeHead(400, { 'Content-Type': 'application/json' });
745
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'parse error' } }));
746
+ return;
747
+ }
748
+ const resp = handleMessage(msg, auth);
749
+ if (resp === null) { res.writeHead(204); res.end(); return; }
750
+ res.writeHead(200, { 'Content-Type': 'application/json' });
751
+ res.end(JSON.stringify(resp));
752
+ });
753
+ return;
754
+ }
755
+ res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('method not allowed');
756
+ });
757
+ server.listen(port, host, function () {
758
+ console.log('yotta-memory 记忆引擎已启动(v' + VERSION + ')');
759
+ console.log('URL: http://' + host + ':' + port + '/mcp');
760
+ console.log('记忆库: ' + root);
761
+ if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
762
+ else console.log('鉴权: Bearer token + X-Agent-Id(yotta-memory token new --agent <id> 生成)');
763
+ console.log('按 Ctrl+C 停止');
764
+ });
765
+ }
766
+
767
+ // ---- stdio 本地零进程模式(照灵魂盘:客户端按需拉起 CLI)----
768
+ function cmdServeStdio() {
769
+ const root = userRoot();
770
+ ensureInit(root);
771
+ const ctx = { agent: currentAgent() };
772
+ let buf = '';
773
+ process.stdin.setEncoding('utf8');
774
+ process.stdin.on('data', function (chunk) {
775
+ buf += chunk;
776
+ let idx;
777
+ while ((idx = buf.indexOf('\n')) !== -1) {
778
+ const line = buf.slice(0, idx).trim();
779
+ buf = buf.slice(idx + 1);
780
+ if (!line) continue;
781
+ let msg;
782
+ try { msg = JSON.parse(line); } catch (e) { continue; }
783
+ const resp = handleMessage(msg, ctx);
784
+ if (resp !== null) process.stdout.write(JSON.stringify(resp) + '\n');
785
+ }
786
+ });
787
+ process.stdin.on('end', function () { process.exit(0); });
788
+ }
789
+
790
+ // ---- lan 命令(Windows 计划任务管理开机自启 serve)----
791
+ const LAN_TASK_NAME = 'YottaMemoryServe';
792
+ function lanTaskRunCmd(opts) {
793
+ const host = opts.host || '0.0.0.0';
794
+ const port = opts.port || 8787;
795
+ return '"' + process.execPath + '" "' + __filename + '" serve --host ' + host + ' --port ' + port;
796
+ }
797
+ function cmdLanEnable(opts) {
798
+ if (process.platform !== 'win32') {
799
+ console.error('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
800
+ process.exit(2);
801
+ }
802
+ const trigger = opts.onstart ? 'onstart' : 'onlogon';
803
+ const tr = lanTaskRunCmd(opts);
804
+ try {
805
+ child_process.execFileSync('schtasks', ['/create', '/tn', LAN_TASK_NAME, '/tr', tr, '/sc', trigger, '/f'], { stdio: 'inherit' });
806
+ } catch (e) {
807
+ console.error('注册计划任务失败(ONSTART 需要管理员权限;ONLOGON 一般不需要;如仍失败请用管理员终端执行): ' + String((e && e.stderr) || (e && e.message) || e).split(/\r?\n/)[0]);
808
+ process.exit(1);
809
+ }
810
+ console.log('已注册开机自启: 计划任务 ' + LAN_TASK_NAME + '(触发器 ' + trigger + ')');
811
+ console.log('运行命令: ' + tr);
812
+ }
813
+ function cmdLanDisable() {
814
+ if (process.platform !== 'win32') {
815
+ console.error('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
816
+ process.exit(2);
817
+ }
818
+ try {
819
+ child_process.execFileSync('schtasks', ['/delete', '/tn', LAN_TASK_NAME, '/f'], { stdio: ['ignore', 'pipe', 'ignore'] });
820
+ } catch (e) {
821
+ const msg = String((e && e.stderr) || (e && e.message) || e);
822
+ if (msg.indexOf('cannot find the path') !== -1 || msg.indexOf('没有找到') !== -1) {
823
+ console.log('未启用: 计划任务 ' + LAN_TASK_NAME + ' 不存在,无需移除');
824
+ return;
825
+ }
826
+ console.error('移除计划任务失败: ' + msg.split(/\r?\n/)[0]);
827
+ process.exit(1);
828
+ }
829
+ console.log('已移除开机自启计划任务 ' + LAN_TASK_NAME);
830
+ }
831
+ function cmdLanStatus() {
832
+ if (process.platform !== 'win32') {
833
+ console.log('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
834
+ return;
835
+ }
836
+ try {
837
+ const out = child_process.execFileSync('schtasks', ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST', '/v'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
838
+ console.log('已启用: 计划任务 ' + LAN_TASK_NAME + ' 存在');
839
+ const re = /^(状态|Status)\s*:\s*(.+)$/;
840
+ for (const line of String(out).split(/\r?\n/)) {
841
+ const m = re.exec(line.trim());
842
+ if (m) console.log(m[1] + ': ' + m[2]);
843
+ }
844
+ } catch (e) {
845
+ console.log('未启用: 计划任务 ' + LAN_TASK_NAME + ' 不存在(可用 yotta-memory lan enable 注册)');
846
+ }
847
+ }
848
+
849
+ // ---- usage / main ----
540
850
  function usage() {
541
851
  console.log([
542
852
  'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
543
853
  '',
544
854
  '用法:',
545
- ' yotta-memory init [--project] 初始化记忆库(默认用户级 ~/.yottamemory)',
855
+ ' yotta-memory init [--project] [--dir <目录>] 初始化记忆库(默认用户级;--dir 显式指定位置)',
856
+ ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
857
+ ' yotta-memory config get 查看当前配置与生效位置',
546
858
  ' yotta-memory remember <type> <subject> <statement> [--owner <id>] 写入记忆',
547
- ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区;--unsafe 允许越界读其它智能体私密)',
859
+ ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区)',
548
860
  ' yotta-memory forget <文件或文件名> 删除一条记忆',
549
861
  ' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
550
- ' yotta-memory reindex 重建索引(手动改文件后用)',
862
+ ' yotta-memory reindex 重建索引',
551
863
  ' yotta-memory export [--out 文件.json] 导出全部记忆',
552
864
  ' yotta-memory import <文件.json> 导入记忆',
865
+ ' yotta-memory token new --agent <id> 生成/重置某智能体访问 token(登记 .server/tokens.json,打印一次)',
866
+ ' yotta-memory token list 列出已登记智能体',
867
+ ' yotta-memory token revoke --agent <id> 吊销某智能体 token',
868
+ ' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
869
+ ' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows 计划任务,默认 ONLOGON)',
553
870
  ' yotta-memory --version 版本',
554
871
  '',
555
872
  '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
556
- '环境变量: YOTTA_MEMORY_HOME 覆盖用户级目录; YOTTA_AGENT_ID/AGENT_ID 作为当前 agent 标识(读取分区)',
873
+ '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(读取分区)',
874
+ '远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
557
875
  '',
558
876
  ].join('\n'));
559
877
  }
560
-
561
878
  function main() {
562
879
  const args = process.argv.slice(2);
563
880
  if (!args.length) { usage(); return; }
@@ -566,12 +883,15 @@ function main() {
566
883
  if (first === '--help' || first === '-h') { usage(); return; }
567
884
  const opts = {};
568
885
  const positional = [];
569
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope']);
886
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir']);
570
887
  for (let i = 1; i < args.length; i++) {
571
888
  const a = args[i];
572
889
  if (a === '--project') opts.project = true;
573
890
  else if (a === '--all') opts.all = true;
574
891
  else if (a === '--unsafe') opts.unsafe = true;
892
+ else if (a === '--no-auth') opts.noAuth = true;
893
+ else if (a === '--stdio') opts.stdio = true;
894
+ else if (a === '--onstart') opts.onstart = true;
575
895
  else if (valueOpts.has(a)) {
576
896
  const v = args[++i];
577
897
  if (a === '--type') opts.type = v;
@@ -582,6 +902,9 @@ function main() {
582
902
  else if (a === '--owner') opts.owner = v;
583
903
  else if (a === '--agent') opts.agent = v;
584
904
  else if (a === '--scope') opts.scope = v;
905
+ else if (a === '--host') opts.host = v;
906
+ else if (a === '--port') opts.port = parseInt(v, 10) || 8787;
907
+ else if (a === '--dir') opts.dir = v;
585
908
  } else if (a.startsWith('--')) {
586
909
  console.error('未知选项: ' + a);
587
910
  process.exit(2);
@@ -591,6 +914,13 @@ function main() {
591
914
  }
592
915
  switch (first) {
593
916
  case 'init': cmdInit(opts); break;
917
+ case 'config': {
918
+ const sub = positional[0];
919
+ if (sub === 'set') cmdConfigSet(positional[1], positional[2]);
920
+ else if (sub === 'get') cmdConfigGet();
921
+ else { console.error('config 子命令: set memory_home <目录> / get'); process.exit(2); }
922
+ break;
923
+ }
594
924
  case 'remember': cmdRemember(positional[0], positional[1], positional[2], opts); break;
595
925
  case 'recall': cmdRecall(positional[0] || null, opts); break;
596
926
  case 'forget': cmdForget(positional[0]); break;
@@ -598,6 +928,23 @@ function main() {
598
928
  case 'reindex': cmdReindex(); break;
599
929
  case 'export': cmdExport(opts.out); break;
600
930
  case 'import': cmdImport(positional[0]); break;
931
+ case 'token': {
932
+ const sub = positional[0];
933
+ if (sub === 'new') cmdTokenNew(opts.agent);
934
+ else if (sub === 'list') cmdTokenList();
935
+ else if (sub === 'revoke') cmdTokenRevoke(opts.agent);
936
+ else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
937
+ break;
938
+ }
939
+ case 'serve': cmdServe(opts); break;
940
+ case 'lan': {
941
+ const sub = positional[0];
942
+ if (sub === 'enable') cmdLanEnable(opts);
943
+ else if (sub === 'disable') cmdLanDisable();
944
+ else if (sub === 'status') cmdLanStatus();
945
+ else { console.error('lan 子命令: enable [--onstart] / disable / status'); process.exit(2); }
946
+ break;
947
+ }
601
948
  default:
602
949
  console.error('未知命令: ' + first);
603
950
  usage();
@@ -605,4 +952,20 @@ function main() {
605
952
  }
606
953
  }
607
954
 
608
- main();
955
+ if (require.main === module) main();
956
+ module.exports = {
957
+ VERSION: VERSION,
958
+ userRoot: userRoot,
959
+ projectRoot: projectRoot,
960
+ rememberCore: rememberCore,
961
+ recallCore: recallCore,
962
+ forgetCore: forgetCore,
963
+ archiveCore: archiveCore,
964
+ mcpTools: mcpTools,
965
+ callTool: callTool,
966
+ handleMessage: handleMessage,
967
+ loadConfig: loadConfig,
968
+ saveConfig: saveConfig,
969
+ loadTokens: loadTokens,
970
+ saveTokens: saveTokens,
971
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.2.9",
3
+ "version": "0.4.0",
4
4
  "description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -32,4 +32,4 @@
32
32
  "yotta-memory": "bin/yotta-memory.js",
33
33
  "yotta-memory-install": "bin/install.js"
34
34
  }
35
- }
35
+ }