@yottameta/yotta-memory 0.6.2 → 0.6.4

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
@@ -43,7 +43,7 @@
43
43
  | **越用越懂(v0.6.0)** | `profile` 画像聚合(零推断)+ `context` 开工上下文包(身份 / 画像 / 近期记忆 / 边界 / 承诺)+ SKILL「记忆守则」规则层,记忆随使用成长 |
44
44
  | **生态分发** | GitHub + npm 双源同步发布;npx skills / npm / install.sh 三种方式,覆盖 17+ 类智能体目录 |
45
45
  | **便携记忆盘(随盘走)** | 记忆库即引擎:装在硬盘 / 主机上,插上即恢复全部记忆;引擎主机只需装 CLI 当存放点,无需装任何 AI 智能体 |
46
- | **局域网共享与自启** | 每智能体独立 token(Bearer + X-Agent-Id)鉴权、可吊销;`lan enable` 注册开机自启(Windows:优先计划任务;非管理员自动降级用户级 Startup 静默自启);MCP 工具集与 CLI 一致(8 个工具),管理动作不远程暴露 |
46
+ | **局域网共享与自启** | 每智能体独立 token(Bearer + X-Agent-Id)鉴权、可吊销;`lan enable` 注册开机自启(Windows:优先计划任务,非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot);MCP 工具集与 CLI 一致(8 个工具),管理动作不远程暴露 |
47
47
  | **本地 / 局域网双模式** | 本地 `serve --stdio` 零进程、按需拉起(无常驻);局域网 streamable HTTP 常驻——两种模式可并存、按需选用 |
48
48
 
49
49
  ## 功能详解
@@ -115,7 +115,7 @@
115
115
 
116
116
  - **记忆库即引擎**:`serve` 把记忆目录挂成 MCP 服务,目录随盘走;引擎主机只需装 CLI 当存放点,无需装任何 AI 智能体。
117
117
  - **双模式可并存**:本地 `serve --stdio` 零进程(客户端按需拉起);局域网 streamable HTTP(默认 `0.0.0.0:8787`)+ 每智能体 token 鉴权。
118
- - **开机自启**:`lan enable` 注册 Windows 开机自启——优先计划任务(默认登录自启,`--onstart` 开机即启需管理员);非管理员(计划任务被拒)自动降级为**用户级 Startup 静默自启**(免管理员);`lan disable` 移除,`lan status` 查询。
118
+ - **开机自启**:`lan enable` 注册开机自启——Windows 优先计划任务(默认登录自启,`--onstart` 开机即启需管理员),非管理员自动降级为**用户级 Startup 静默自启**(免管理员,启动脚本内联启动命令、被清理也会在开机时自动重建,v0.6.3 起不再弹 80070002);Linux 优先 **systemd 用户单元**(`systemctl --user`,登录自启;`--onstart` 附加 `loginctl enable-linger` 开机即启),systemd 不可用时自动降级**用户 crontab @reboot**(v0.6.4);`lan disable` 移除,`lan status` 查询。
119
119
  - **安全边界**:管理动作(init / config / token / lan / serve)不进 MCP,token 不远程暴露;远程智能体只能读写记忆,不能改配置、不能管 token。
120
120
  - 完整操作步骤见上文「局域网多机共享」章节与 [USER_GUIDE.md](USER_GUIDE.md)。
121
121
 
@@ -196,7 +196,7 @@ npm i -g @yottameta/yotta-memory
196
196
  | `yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一;可选扩展显示名 / 用户 / 关系)|
197
197
  | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 为智能体生成 / 列出 / 吊销访问 token(登记于记忆库 `.server/tokens.json`;同 ID 已被其它来源占用需 `--force` 覆盖,防不同智能体合流)|
198
198
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
199
- | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务,默认 ONLOGON 登录自启、--onstart 开机即启需管理员;非管理员自动降级用户级 Startup 静默自启)|
199
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务,默认 ONLOGON、--onstart 开机即启需管理员,非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)|
200
200
 
201
201
  类型:`FACT`(事实,公共共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺)。
202
202
 
@@ -239,7 +239,7 @@ yotta-memory recall --type FACT --limit 10
239
239
  3. 启动服务(默认监听 0.0.0.0:8787,Bearer token + X-Agent-Id 鉴权)——临时运行或注册开机自启二选一:
240
240
  ```bash
241
241
  yotta-memory serve # 临时前台运行
242
- yotta-memory lan enable # 注册开机自启(Windows:计划任务;非管理员自动降级用户级 Startup 静默自启)
242
+ yotta-memory lan enable # 注册开机自启(Windows:计划任务/用户级 Startup;Linux:systemd 用户单元/用户 crontab)
243
243
  yotta-memory lan status # 查看自启状态
244
244
  ```
245
245
  > `lan enable --onstart` 改为开机即启(需管理员);非管理员时 `lan enable` 自动改用用户级 Startup 静默自启(无需管理员);`lan disable` 移除自启。
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、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、记忆守则、profile、context、越用越懂"
4
- version: 0.6.2
4
+ version: 0.6.4
5
5
  license: MIT
6
6
  ---
7
7
 
8
8
  # yotta-memory(元忆)— 有权限边界的文件式智能体记忆
9
9
 
10
10
  > 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
11
- > 版本:0.6.2 | 最后更新:2026-08-25
11
+ > 版本:0.6.4 | 最后更新:2026-08-26
12
12
 
13
13
  ## 这是什么
14
14
 
@@ -158,7 +158,7 @@ license: MIT
158
158
  | `yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一;可选扩展显示名 / 用户 / 关系)|
159
159
  | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 每智能体访问 token:生成 / 列出 / 吊销(登记 `<记忆库>/.server/tokens.json`;同 ID 已被其它来源占用需 `--force` 覆盖,防不同智能体合流)|
160
160
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id 鉴权)|
161
- | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务,默认 ONLOGON 登录自启、--onstart 开机即启需管理员;非管理员自动降级用户级 Startup 静默自启)|
161
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务,默认 ONLOGON、--onstart 开机即启需管理员,非管理员自动降级用户级 Startup 静默自启,v0.6.3 起 VBS 自愈不弹 80070002;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)|
162
162
 
163
163
  ## 存储格式(摘要)
164
164
 
package/USER_GUIDE.md CHANGED
@@ -124,31 +124,23 @@ yotta-memory init --dir /srv/yotta-memory # 新库:初始化(
124
124
  **第 3 步:注册开机自启(可选,推荐)**
125
125
 
126
126
  ```bash
127
- # Windows:内置命令(优先计划任务;非管理员自动降级用户级 Startup 静默自启)
127
+ # Windows:内置命令(优先计划任务;非管理员自动降级用户级 Startup 静默自启;
128
+ # v0.6.3 起启动脚本自愈——启动文件被清理也会在开机时自动重建,无需手动处理)
128
129
  yotta-memory lan enable # 登录后自动启动(默认)
129
130
  yotta-memory lan enable --onstart # 开机即启(需管理员)
130
131
  yotta-memory lan status # 查看状态
131
132
  yotta-memory lan disable # 取消
132
133
 
133
- # Linux:用 systemd 开机自启(lan 命令当前仅支持 Windows)
134
- # 先查 yotta-memory 实际路径:which yotta-memory(npm 全局默认 /usr/local/bin/yotta-memory)
135
- sudo tee /etc/systemd/system/yotta-memory.service > /dev/null <<'EOF'
136
- [Unit]
137
- Description=yotta-memory memory engine
138
- After=network.target
139
-
140
- [Service]
141
- ExecStart=/usr/local/bin/yotta-memory serve --host 0.0.0.0 --port 8787
142
- Restart=on-failure
143
-
144
- [Install]
145
- WantedBy=multi-user.target
146
- EOF
147
- sudo systemctl daemon-reload
148
- sudo systemctl enable --now yotta-memory
149
- systemctl status yotta-memory # 查看状态
134
+ # Linux:内置命令(v0.6.4 起)——优先 systemd 用户单元(登录自启);
135
+ # systemd 不可用时自动降级用户 crontab @reboot(开机自启,免管理员)
136
+ yotta-memory lan enable # 登录后自动启动(默认)
137
+ yotta-memory lan enable --onstart # 开机即启(无需登录,需系统支持 loginctl)
138
+ yotta-memory lan status # 查看状态
139
+ yotta-memory lan disable # 取消
150
140
  ```
151
141
 
142
+ > 说明:`lan enable` 默认「登录后自启」;如需系统级(整机、开机即启)服务,可手动创建 `/etc/systemd/system/yotta-memory.service`(ExecStart 指向 `which yotta-memory` 实际路径)并 `sudo systemctl enable --now yotta-memory`;桌面环境也可在 `~/.config/autostart/` 放置 `yotta-memory.desktop` 实现图形登录自启。
143
+
152
144
  **第 4 步:为局域网其它主机的智能体生成访问 token**
153
145
 
154
146
  ```bash
@@ -257,7 +249,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
257
249
  | `yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一;可选扩展显示名 / 用户 / 关系)|
258
250
  | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 访问 token(同 ID 已被其它来源占用需 `--force` 覆盖)|
259
251
  | `yotta-memory serve [--port 8787] [--stdio] [--no-auth]` | 启动记忆引擎(--no-auth 关闭鉴权,仅限可信内网)|
260
- | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务;非管理员自动降级用户级 Startup 静默自启)|
252
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务/用户级 Startup 静默自启;Linux:systemd 用户单元/用户 crontab @reboot)|
261
253
 
262
254
  类型:`FACT`(事实,共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺),后三类按智能体物理分目录隔离(`private/<owner>/<type>/`)。
263
255
 
@@ -265,7 +257,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
265
257
 
266
258
  **远程连不上时,按顺序检查:**
267
259
 
268
- 1. 引擎在运行吗?Windows:`yotta-memory lan status`;Linux(systemd 自启时):`systemctl status yotta-memory`,或 `ps aux | grep yotta-memory`;也可直接 `yotta-memory --version`。
260
+ 1. 引擎在运行吗?`yotta-memory lan status`(Windows / Linux 通用);Linux 也可 `systemctl --user status yotta-memory-serve.service`(systemd 自启时)或 `ps aux | grep yotta-memory`;也可直接 `yotta-memory --version`。
269
261
  2. IP 对吗?引擎主机执行 `hostname -I`(Linux)或 `ipconfig`(Windows)确认。
270
262
  3. 端口对吗?默认 8787,两端要一致。
271
263
  4. 防火墙放行了吗?Linux:`sudo ufw status`,未放行则 `sudo ufw allow 8787/tcp`;Windows:允许首次监听的入站请求。
@@ -280,7 +272,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
280
272
 
281
273
  **recall 没有结果:** 关键词太细或确实没有这条记忆;先 `recall`(不带关键词)看库里的全部记忆核对。
282
274
 
283
- **systemd 自启的引擎看日志:** `journalctl -u yotta-memory -n 50`。
275
+ **systemd 自启的引擎看日志:** `journalctl --user -u yotta-memory-serve.service -n 50`(系统级手动服务为 `journalctl -u yotta-memory -n 50`)。
284
276
 
285
277
  **记错位置了?** `yotta-memory config get` 查看当前生效位置;`config set memory_home <正确目录>` 改正。
286
278
 
@@ -7,6 +7,8 @@
7
7
  // v0.5.4 新增:lan enable 在非管理员(schtasks Access denied)时自动降级为用户级 Startup 静默自启(VBS sh.Run 窗口0 + autostart.cmd,node 路径 process.execPath 自动探测);lan disable/status 同时管理计划任务与 Startup 双机制
8
8
  // v0.6.0 新增:灵魂盘核心——profile(用户画像聚合,零推断)/ context(开工上下文包)/ iam 扩展(--name/--user/--relationship)/ remember --verify(写后回读)与 --no-hint(关闭类型启发式提示)+ SKILL「记忆守则」
9
9
  // v0.6.1 新增:灵魂盘机制精髓补齐——context --budget(token 预算,近记忆按剩余预算放行)/ context 内嵌「多智能体接入铁律」段 / remember --source/--weight(来源 + 重要性权重,去重 weight 取 max)/ 近期记忆排序融合 importance(confidence×recency+updated+weight+immutable)
10
+ // v0.6.3 修复:lan 开机自启 VBS 自愈——VBS 内联 autostart.cmd 内容,启动时自动重建 .cmd(根治 80070002:wscript 找不到被引用启动文件)
11
+ // v0.6.4 新增:lan 命令扩展 Linux——systemd 用户单元(systemctl --user enable/start,登录自启;--onstart 附加 loginctl enable-linger 开机即启)/ systemd 不可用时自动降级用户 crontab @reboot;lanPlatform 测试钩子(YOTTA_LAN_PLATFORM)
10
12
  // v0.6.2 修复:remember --verify 写后回读改为直查索引 + 权限判定 + 召回匹配性(不再依赖 recall top-N 排序,消除泛化 subject 下偶发误报「回读未命中」)
11
13
  'use strict';
12
14
 
@@ -17,7 +19,7 @@ const crypto = require('crypto');
17
19
  const http = require('http');
18
20
  const child_process = require('child_process');
19
21
 
20
- const VERSION = '0.6.2';
22
+ const VERSION = '0.6.4';
21
23
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
22
24
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
23
25
  const PUBLIC_DIR = 'facts';
@@ -1267,11 +1269,17 @@ function cmdServeStdio() {
1267
1269
  process.stdin.on('end', function () { process.exit(0); });
1268
1270
  }
1269
1271
 
1270
- // ---- lan 命令(Windows 开机自启管理 serve)----
1271
- // 双机制:① schtasks 计划任务(默认,需相应权限);② 非管理员(Access denied)自动降级为
1272
- // 用户级 Startup 静默自启(VBS sh.Run <autostart.cmd>, 0, False + autostart.cmd;node 路径 process.execPath 自动探测,0.5.4)
1272
+ // ---- lan command (autostart management for serve) ----
1273
+ // Windows dual mechanism: (1) schtasks scheduled task (default, needs permission); (2) non-admin
1274
+ // (Access denied) auto-fallback to user-level Startup silent autostart (VBS sh.Run <autostart.cmd>,
1275
+ // 0, False + autostart.cmd; node path process.execPath auto-detected, 0.5.4)
1276
+ // Linux (v0.6.4): (1) systemd user unit (systemctl --user enable/start, login autostart; --onstart
1277
+ // additionally runs loginctl enable-linger for boot autostart); (2) auto-fallback to user crontab
1278
+ // @reboot when no systemd user session is available.
1273
1279
  const LAN_TASK_NAME = 'YottaMemoryServe';
1274
1280
  const LAN_GEN_MARKER = 'Generated by yotta-memory lan enable';
1281
+ const LAN_UNIT_NAME = 'yotta-memory-serve.service';
1282
+ const LAN_CRONTAB_MARKER = '#YTM_LAN:yotta-memory-serve';
1275
1283
  function lanTaskRunCmd(opts) {
1276
1284
  const host = opts.host || '0.0.0.0';
1277
1285
  const port = opts.port || 8787;
@@ -1279,6 +1287,33 @@ function lanTaskRunCmd(opts) {
1279
1287
  const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
1280
1288
  return q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port;
1281
1289
  }
1290
+
1291
+ function lanServeArgs(opts) {
1292
+ return ['serve', '--host', String(opts.host || '0.0.0.0'), '--port', String(opts.port || 8787)];
1293
+ }
1294
+ // generic spawn wrapper: YOTTA_LAN_*_BIN may point to a .js stub (testing/advanced use;
1295
+ // on Windows run it with the current node interpreter automatically)
1296
+ function lanSpawn(bin, args, spawnOpts) {
1297
+ if (process.platform === 'win32' && /\.js$/i.test(String(bin))) {
1298
+ return child_process.spawnSync(process.execPath, [bin].concat(args), spawnOpts);
1299
+ }
1300
+ return child_process.spawnSync(bin, args, spawnOpts);
1301
+ }
1302
+ // platform override for testing/advanced use (YOTTA_LAN_PLATFORM=linux/win32/...)
1303
+ function lanPlatform() { return process.env.YOTTA_LAN_PLATFORM || process.platform; }
1304
+ // sh-style single-quote quoting (crontab line is executed via /bin/sh -c)
1305
+ function shQuote(s) {
1306
+ const p = String(s);
1307
+ if (!/[\s'"\\$\`]/.test(p)) return p;
1308
+ return "'" + p.replace(/'/g, "'\\''") + "'";
1309
+ }
1310
+ // systemd ExecStart quoting (systemd's own parse rules: double quotes group, \" escapes a quote,
1311
+ // \\ escapes a backslash)
1312
+ function systemdEscapeArg(s) {
1313
+ const p = String(s);
1314
+ if (!/[\s"\\$;]/.test(p)) return p;
1315
+ return '"' + p.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
1316
+ }
1282
1317
  function lanStartupDir() {
1283
1318
  // 测试/高级用途可用 YOTTA_LAN_STARTUP_DIR 覆盖(默认用户级 Startup 目录,免管理员)
1284
1319
  if (process.env.YOTTA_LAN_STARTUP_DIR) return process.env.YOTTA_LAN_STARTUP_DIR;
@@ -1300,23 +1335,51 @@ function lanAutostartCmdContent(opts) {
1300
1335
  const host = opts.host || '0.0.0.0';
1301
1336
  const port = opts.port || 8787;
1302
1337
  const q = (p) => '"' + String(p).replace(/"/g, '""') + '"';
1303
- // 生成文件用纯 ASCII(避免 cmd/VBS 按系统代码页读取中文注释乱码)
1338
+ // 生成文件用纯 ASCII(避免 cmd/VBS 按系统代码页读取中文注释乱码;VBS 自愈内联同一内容)
1304
1339
  return '@echo off\r\n'
1305
1340
  + 'chcp 65001 >nul\r\n'
1306
- + 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable\r\n'
1341
+ + 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
1307
1342
  + q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
1308
1343
  }
1309
- function lanVbsContent() {
1310
- return "' " + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable\r\n'
1311
- + 'Set sh = CreateObject("WScript.Shell")\r\n'
1312
- + 'sh.Run "' + String(lanAutostartCmdPath()).replace(/"/g, '""') + '", 0, False\r\n';
1344
+ function lanVbsContent(opts) {
1345
+ // v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
1346
+ // 根治 80070002(wscript 找不到被引用的启动文件)。VBS 写 UTF-16LE,wscript 按 Unicode 读取。
1347
+ const cmdContent = lanAutostartCmdContent(opts || {});
1348
+ // VBS 字符串字面量不能含原始换行:把 CRLF 编码为 Chr(13) & Chr(10) & 拼接,
1349
+ // 生成的 VBS 保持单行合法(否则 wscript 报语法错误,.cmd 重建失败)。
1350
+ const vbsCmdLiteral = cmdContent
1351
+ .replace(/\r\n/g, '\n')
1352
+ .replace(/\n/g, '\n')
1353
+ .split('\n')
1354
+ .map(function (seg) {
1355
+ // 每段都带引号(VBS 字符串字面量),换行用 Chr(13) & Chr(10) 拼接
1356
+ return '"' + seg.replace(/"/g, '""') + '"';
1357
+ })
1358
+ .join(' & Chr(13) & Chr(10) & ');
1359
+ const vbsCmdPath = String(lanAutostartCmdPath()).replace(/"/g, '""');
1360
+ const lines = [
1361
+ "' " + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable',
1362
+ "' v0.6.3 self-heal: always rebuild autostart.cmd from embedded content, then run it (fix 80070002)",
1363
+ 'Set fso = CreateObject("Scripting.FileSystemObject")',
1364
+ 'Set sh = CreateObject("WScript.Shell")',
1365
+ 'cmdPath = "' + vbsCmdPath + '"',
1366
+ 'On Error Resume Next',
1367
+ 'Set dir = fso.GetParentFolderName(cmdPath)',
1368
+ 'If Not fso.FolderExists(dir) Then fso.CreateFolder(dir)',
1369
+ 'Set f = fso.CreateTextFile(cmdPath, True)',
1370
+ 'f.Write ' + vbsCmdLiteral,
1371
+ 'f.Close',
1372
+ 'sh.Run cmdPath, 0, False',
1373
+ '',
1374
+ ];
1375
+ return lines.join('\r\n') + '\r\n';
1313
1376
  }
1314
1377
  function lanInstallStartup(opts) {
1315
1378
  fs.mkdirSync(lanAutostartDir(), { recursive: true });
1316
1379
  fs.writeFileSync(lanAutostartCmdPath(), lanAutostartCmdContent(opts), 'utf8');
1317
1380
  fs.mkdirSync(lanStartupDir(), { recursive: true });
1318
1381
  // VBS 用 UTF-16LE+BOM:wscript 按 Unicode 读取,中文路径不乱码(纯 ANSI 读取会按系统代码页误读)
1319
- fs.writeFileSync(lanVbsPath(), '\ufeff' + lanVbsContent(), 'utf16le');
1382
+ fs.writeFileSync(lanVbsPath(), '\ufeff' + lanVbsContent(opts), 'utf16le');
1320
1383
  }
1321
1384
  function lanRemoveStartupFiles() {
1322
1385
  // 只删除带产品标记的生成文件,绝不误删用户自己的 Startup 文件
@@ -1341,7 +1404,191 @@ function isSchtasksAccessDenied(e) {
1341
1404
  const msg = String((e && e.stderr) || (e && e.message) || e);
1342
1405
  return /access\s+is\s+denied|access\s+denied|拒绝访问/i.test(msg);
1343
1406
  }
1344
- function cmdLanEnable(opts) {
1407
+
1408
+ // ---- lan Linux (systemd user unit / user crontab @reboot) ----
1409
+ function lanLinuxSystemdUserDir() {
1410
+ // override with YOTTA_LAN_SYSTEMD_USER_DIR for testing/advanced use (default ~/.config/systemd/user)
1411
+ if (process.env.YOTTA_LAN_SYSTEMD_USER_DIR) return process.env.YOTTA_LAN_SYSTEMD_USER_DIR;
1412
+ const cfg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
1413
+ return path.join(cfg, 'systemd', 'user');
1414
+ }
1415
+ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNIT_NAME); }
1416
+ function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
1417
+ function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
1418
+ function lanLinuxExecStart(opts) {
1419
+ return [systemdEscapeArg(process.execPath), systemdEscapeArg(__filename)].concat(lanServeArgs(opts)).join(' ');
1420
+ }
1421
+ function lanLinuxUnitContent(opts) {
1422
+ // keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
1423
+ return [
1424
+ '# ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)',
1425
+ '',
1426
+ '[Unit]',
1427
+ 'Description=yotta-memory memory engine (lan autostart)',
1428
+ '',
1429
+ '[Service]',
1430
+ 'Type=simple',
1431
+ 'ExecStart=' + lanLinuxExecStart(opts),
1432
+ 'Restart=on-failure',
1433
+ 'RestartSec=3',
1434
+ '',
1435
+ '[Install]',
1436
+ 'WantedBy=default.target',
1437
+ '',
1438
+ ].join('\n');
1439
+ }
1440
+ function lanCrontabBin() { return process.env.YOTTA_LAN_CRONTAB_BIN || 'crontab'; }
1441
+ function lanCrontabLine(opts) {
1442
+ const parts = ['@reboot', shQuote(process.execPath), shQuote(__filename)].concat(lanServeArgs(opts));
1443
+ return parts.join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
1444
+ }
1445
+ function lanCrontabRead() {
1446
+ try {
1447
+ const r = lanSpawn(lanCrontabBin(), ['-l'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
1448
+ if (r.status === 0) return String(r.stdout || '').split(/\r?\n/).filter(function (l) { return l !== ''; });
1449
+ } catch (e) { /* crontab unavailable -> treat as empty */ }
1450
+ return [];
1451
+ }
1452
+ function lanCrontabWrite(lines) {
1453
+ const content = lines.join('\n') + (lines.length ? '\n' : '');
1454
+ const r = lanSpawn(lanCrontabBin(), ['-'], { input: content, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
1455
+ if (r.error) throw r.error;
1456
+ if (r.status !== 0) throw new Error(String(r.stderr || r.stdout || '').toString().split(/\r?\n/)[0]);
1457
+ }
1458
+ function lanCrontabHasOurLine(lines) {
1459
+ return lines.some(function (l) { return l.indexOf(LAN_CRONTAB_MARKER) !== -1; });
1460
+ }
1461
+ function lanCrontabWithoutOurLine(lines) {
1462
+ return lines.filter(function (l) { return l.indexOf(LAN_CRONTAB_MARKER) === -1; });
1463
+ }
1464
+ function lanLinuxHasSystemd() {
1465
+ try {
1466
+ const r = lanSpawn(lanLinuxSystemctlBin(), ['--user', 'show-environment'], { stdio: ['ignore', 'ignore', 'ignore'] });
1467
+ return r.status === 0;
1468
+ } catch (e) { return false; }
1469
+ }
1470
+ function lanLinuxSystemctl(args) {
1471
+ const r = lanSpawn(lanLinuxSystemctlBin(), ['--user'].concat(args), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
1472
+ if (r.error) throw r.error;
1473
+ return { status: r.status, out: String(r.stdout || ''), err: String(r.stderr || '') };
1474
+ }
1475
+ function lanLinuxInstallSystemd(opts) {
1476
+ const dir = lanLinuxSystemdUserDir();
1477
+ fs.mkdirSync(dir, { recursive: true });
1478
+ const unit = lanLinuxUnitPath();
1479
+ fs.writeFileSync(unit, lanLinuxUnitContent(opts), 'utf8');
1480
+ lanLinuxSystemctl(['daemon-reload']);
1481
+ const en = lanLinuxSystemctl(['enable', LAN_UNIT_NAME]);
1482
+ if (en.status !== 0) throw new Error('systemctl --user enable 失败: ' + en.err.split(/\r?\n/)[0]);
1483
+ const st = lanLinuxSystemctl(['start', LAN_UNIT_NAME]);
1484
+ if (st.status !== 0) {
1485
+ console.error('启动服务失败(已注册,稍后可执行 systemctl --user start yotta-memory-serve.service): ' + st.err.split(/\r?\n/)[0]);
1486
+ }
1487
+ if (opts.onstart) {
1488
+ try {
1489
+ const lr = lanSpawn(lanLinuxLoginctlBin(), ['enable-linger'], { stdio: ['ignore', 'pipe', 'pipe'] });
1490
+ if (lr.status === 0) console.log('已启用 lingering(开机即启,无需登录)');
1491
+ else console.error('提示: 开机即启需 loginctl enable-linger,当前未生效(' + String(lr.stderr || '').split(/\r?\n/)[0] + ')');
1492
+ } catch (e) {
1493
+ console.error('提示: 开机即启需 loginctl enable-linger,当前无 loginctl(' + String((e && e.message) || e).split(/\r?\n/)[0] + ')');
1494
+ }
1495
+ }
1496
+ return unit;
1497
+ }
1498
+ function lanLinuxRemoveSystemd() {
1499
+ // only remove our own marker unit file, never a user's own systemd unit
1500
+ const unit = lanLinuxUnitPath();
1501
+ if (!fs.existsSync(unit) || !lanFileHasMarker(unit)) return false;
1502
+ try { lanLinuxSystemctl(['disable', LAN_UNIT_NAME]); } catch (e) {}
1503
+ try { lanLinuxSystemctl(['stop', LAN_UNIT_NAME]); } catch (e) {}
1504
+ try { fs.unlinkSync(unit); } catch (e) {}
1505
+ try { lanLinuxSystemctl(['daemon-reload']); } catch (e) {}
1506
+ return true;
1507
+ }
1508
+ function lanLinuxInstallCrontab(opts) {
1509
+ const lines = lanCrontabWithoutOurLine(lanCrontabRead());
1510
+ lines.push(lanCrontabLine(opts));
1511
+ lanCrontabWrite(lines);
1512
+ return lanCrontabLine(opts);
1513
+ }
1514
+ function lanLinuxRemoveCrontab() {
1515
+ const cur = lanCrontabRead();
1516
+ const lines = lanCrontabWithoutOurLine(cur);
1517
+ if (lines.length === cur.length) return false;
1518
+ lanCrontabWrite(lines);
1519
+ return true;
1520
+ }
1521
+ function cmdLanLinuxEnable(opts) {
1522
+ if (lanLinuxHasSystemd()) {
1523
+ try {
1524
+ const unit = lanLinuxInstallSystemd(opts);
1525
+ console.log('已注册开机自启: systemd 用户单元 ' + LAN_UNIT_NAME);
1526
+ console.log('单元文件: ' + unit);
1527
+ console.log('备注: 登录后自动启动(systemctl --user);如需现在运行请执行 yotta-memory serve');
1528
+ if (!opts.onstart) console.log('提示: 如需开机即启(无需登录),请用 --onstart 重新执行 yotta-memory lan enable');
1529
+ return;
1530
+ } catch (e) {
1531
+ const msg = String((e && e.message) || e).split(/\r?\n/)[0];
1532
+ console.error('systemd 注册失败: ' + msg);
1533
+ console.log('自动改用用户 crontab @reboot 自启...');
1534
+ }
1535
+ } else {
1536
+ console.log('未检测到 systemd 用户会话,改用用户 crontab @reboot 自启...');
1537
+ }
1538
+ try {
1539
+ const line = lanLinuxInstallCrontab(opts);
1540
+ console.log('已启用用户 crontab @reboot 自启');
1541
+ console.log('启动命令: ' + line);
1542
+ console.log('日志: ' + lanLogPath(opts));
1543
+ console.log('备注: 开机时自动启动;如需现在运行请执行 yotta-memory serve');
1544
+ } catch (e2) {
1545
+ console.error('crontab 注册失败: ' + String((e2 && e2.message) || e2).split(/\r?\n/)[0]);
1546
+ console.error('请手动配置开机自启(如 ~/.config/autostart/*.desktop 或系统服务),或直接运行 yotta-memory serve');
1547
+ process.exit(1);
1548
+ }
1549
+ }
1550
+ function cmdLanLinuxDisable() {
1551
+ let removedAny = false;
1552
+ let sawError = false;
1553
+ try {
1554
+ if (lanLinuxRemoveSystemd()) { console.log('已移除 systemd 用户单元 ' + LAN_UNIT_NAME); removedAny = true; }
1555
+ } catch (e) { console.error('移除 systemd 单元失败: ' + String((e && e.message) || e).split(/\r?\n/)[0]); sawError = true; }
1556
+ try {
1557
+ if (lanLinuxRemoveCrontab()) { console.log('已移除 crontab @reboot 自启'); removedAny = true; }
1558
+ } catch (e) { console.error('移除 crontab 自启失败: ' + String((e && e.message) || e).split(/\r?\n/)[0]); sawError = true; }
1559
+ if (!removedAny && !sawError) console.log('未启用: 未发现任何自启配置(systemd 单元与 crontab 均不存在)');
1560
+ if (sawError) process.exit(1);
1561
+ }
1562
+ function cmdLanLinuxStatus() {
1563
+ let any = false;
1564
+ const unit = lanLinuxUnitPath();
1565
+ const unitExists = fs.existsSync(unit) && lanFileHasMarker(unit);
1566
+ if (unitExists) {
1567
+ any = true;
1568
+ console.log('systemd 用户单元 ' + LAN_UNIT_NAME + ': 已注册');
1569
+ try {
1570
+ const en = lanLinuxSystemctl(['is-enabled', LAN_UNIT_NAME]);
1571
+ if (en.status === 0) console.log(' 启用状态: ' + String(en.out).trim());
1572
+ } catch (e) {}
1573
+ try {
1574
+ const ac = lanLinuxSystemctl(['is-active', LAN_UNIT_NAME]);
1575
+ if (ac.status === 0) console.log(' 运行状态: ' + String(ac.out).trim());
1576
+ } catch (e) {}
1577
+ console.log(' 单元文件: ' + unit);
1578
+ } else {
1579
+ console.log('systemd 用户单元 ' + LAN_UNIT_NAME + ': 未注册');
1580
+ }
1581
+ const lines = lanCrontabRead();
1582
+ if (lanCrontabHasOurLine(lines)) {
1583
+ any = true;
1584
+ console.log('crontab @reboot: 已启用');
1585
+ for (const l of lines) if (l.indexOf(LAN_CRONTAB_MARKER) !== -1) console.log(' ' + l.trim());
1586
+ } else {
1587
+ console.log('crontab @reboot: 未启用');
1588
+ }
1589
+ if (!any) console.log('未启用任何开机自启(可用 yotta-memory lan enable 注册)');
1590
+ }
1591
+ function cmdLanWinEnable(opts) {
1345
1592
  if (process.platform !== 'win32') {
1346
1593
  console.error('lan 命令当前仅支持 Windows(计划任务 / Startup 自启);本机平台: ' + process.platform);
1347
1594
  process.exit(2);
@@ -1375,7 +1622,7 @@ function cmdLanEnable(opts) {
1375
1622
  return;
1376
1623
  }
1377
1624
  }
1378
- function cmdLanDisable() {
1625
+ function cmdLanWinDisable() {
1379
1626
  if (process.platform !== 'win32') {
1380
1627
  console.error('lan 命令当前仅支持 Windows(计划任务 / Startup 自启);本机平台: ' + process.platform);
1381
1628
  process.exit(2);
@@ -1400,7 +1647,7 @@ function cmdLanDisable() {
1400
1647
  if (!removedAny && !sawError) console.log('未启用: 未发现任何自启配置(计划任务与 Startup 文件均不存在)');
1401
1648
  if (sawError) process.exit(1);
1402
1649
  }
1403
- function cmdLanStatus() {
1650
+ function cmdLanWinStatus() {
1404
1651
  if (process.platform !== 'win32') {
1405
1652
  console.log('lan 命令当前仅支持 Windows(计划任务 / Startup 自启);本机平台: ' + process.platform);
1406
1653
  return;
@@ -1431,6 +1678,27 @@ function cmdLanStatus() {
1431
1678
  }
1432
1679
  if (!taskFound && !vbsOk && !cmdOk) console.log('未启用任何开机自启(可用 yotta-memory lan enable 注册)');
1433
1680
  }
1681
+
1682
+ function cmdLanEnable(opts) {
1683
+ const p = lanPlatform();
1684
+ if (p === 'win32') return cmdLanWinEnable(opts);
1685
+ if (p === 'linux') return cmdLanLinuxEnable(opts);
1686
+ console.error('lan 命令当前仅支持 Windows / Linux;本机平台: ' + p);
1687
+ process.exit(2);
1688
+ }
1689
+ function cmdLanDisable() {
1690
+ const p = lanPlatform();
1691
+ if (p === 'win32') return cmdLanWinDisable();
1692
+ if (p === 'linux') return cmdLanLinuxDisable();
1693
+ console.error('lan 命令当前仅支持 Windows / Linux;本机平台: ' + p);
1694
+ process.exit(2);
1695
+ }
1696
+ function cmdLanStatus() {
1697
+ const p = lanPlatform();
1698
+ if (p === 'win32') return cmdLanWinStatus();
1699
+ if (p === 'linux') return cmdLanLinuxStatus();
1700
+ console.log('lan 命令当前仅支持 Windows / Linux;本机平台: ' + p);
1701
+ }
1434
1702
  // ---- usage / main ----
1435
1703
  function usage() {
1436
1704
  console.log([
@@ -1455,7 +1723,7 @@ function usage() {
1455
1723
  ' yotta-memory token list 列出已登记智能体',
1456
1724
  ' yotta-memory token revoke --agent <id> 吊销某智能体 token',
1457
1725
  ' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
1458
- ' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows 计划任务,默认 ONLOGON;非管理员自动降级用户级 Startup 静默自启)',
1726
+ ' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows:计划任务,默认 ONLOGON,非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)',
1459
1727
  ' yotta-memory --version 版本',
1460
1728
  '',
1461
1729
  '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
@@ -1591,4 +1859,34 @@ module.exports = {
1591
1859
  lanRemoveStartupFiles: lanRemoveStartupFiles,
1592
1860
  lanFileHasMarker: lanFileHasMarker,
1593
1861
  isSchtasksAccessDenied: isSchtasksAccessDenied,
1862
+ lanServeArgs: lanServeArgs,
1863
+ lanSpawn: lanSpawn,
1864
+ lanPlatform: lanPlatform,
1865
+ shQuote: shQuote,
1866
+ systemdEscapeArg: systemdEscapeArg,
1867
+ lanLinuxSystemdUserDir: lanLinuxSystemdUserDir,
1868
+ lanLinuxUnitPath: lanLinuxUnitPath,
1869
+ lanLinuxSystemctlBin: lanLinuxSystemctlBin,
1870
+ lanLinuxLoginctlBin: lanLinuxLoginctlBin,
1871
+ lanLinuxExecStart: lanLinuxExecStart,
1872
+ lanLinuxUnitContent: lanLinuxUnitContent,
1873
+ lanCrontabBin: lanCrontabBin,
1874
+ lanCrontabLine: lanCrontabLine,
1875
+ lanCrontabRead: lanCrontabRead,
1876
+ lanCrontabWrite: lanCrontabWrite,
1877
+ lanCrontabHasOurLine: lanCrontabHasOurLine,
1878
+ lanCrontabWithoutOurLine: lanCrontabWithoutOurLine,
1879
+ lanLinuxHasSystemd: lanLinuxHasSystemd,
1880
+ lanLinuxSystemctl: lanLinuxSystemctl,
1881
+ lanLinuxInstallSystemd: lanLinuxInstallSystemd,
1882
+ lanLinuxRemoveSystemd: lanLinuxRemoveSystemd,
1883
+ lanLinuxInstallCrontab: lanLinuxInstallCrontab,
1884
+ lanLinuxRemoveCrontab: lanLinuxRemoveCrontab,
1885
+ cmdLanWinEnable: cmdLanWinEnable,
1886
+ cmdLanWinDisable: cmdLanWinDisable,
1887
+ cmdLanWinStatus: cmdLanWinStatus,
1888
+ cmdLanLinuxEnable: cmdLanLinuxEnable,
1889
+ cmdLanLinuxDisable: cmdLanLinuxDisable,
1890
+ cmdLanLinuxStatus: cmdLanLinuxStatus,
1891
+
1594
1892
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- # yotta-memory 协议规范 v0.6.2
1
+ # yotta-memory 协议规范 v0.6.4
2
2
 
3
3
  > 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
4
4
  > 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。