@yottameta/yotta-memory 0.16.5 → 0.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## v0.16.6 (2026-09-23)
2
+
3
+ **插件 MCP 身份启动器(Agent Plugin 开箱可用)**
4
+
5
+ - 新增 `bin/plugin-mcp.js`:宿主只替换 `${PLUGIN_ROOT}`、不替换 `${AGENT_ID}` / `${AGENT_KEY_FILE}` 时,按「宿主替换值 → 环境变量 → `PLUGIN_DATA/identity.json` → 未授权可执行指引」解析身份。
6
+ - 未装配身份时不再把字面量占位符传给引擎:以未授权模式启动(公共 FACT 可读、私密 fail-closed),并输出含 `key claim` 与 `identity.json` 的可执行指引。
7
+ - `key claim <id> --plugin-data <PLUGIN_DATA>`:一条命令同时写入插件 key 与 `identity.json`;与 `--to` / `--agent-key-file` 互斥校验。
8
+ - 插件 `mcp.json` 入口由 `yotta-memory.js` 改为 `plugin-mcp.js`;宿主替换占位符的旧路径继续有效。
9
+ - 回归:`test/plugin-mcp-launcher.test.js`(身份优先级 / 无身份指引)+ `test/plugin-claim-binding.test.js`(claim → 插件身份 → 私密读取端到端);全量 `npm test` 167/167 PASS。
10
+ - 版本对齐:package.json / SKILL.md frontmatter / skill-manifest.json / CHANGELOG / 引擎 VERSION = 0.16.6。
11
+
1
12
  ## v0.16.5 (2026-09-22)
2
13
 
3
14
  **doctor --json 稳定契约补齐**
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/可回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、长期理解摘要、近期走廊、会话闭环、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤、任务相关记忆、--focus、--embedding、压缩遗忘、consolidate、周期摘要、自动合并、分类型衰减、回滚、备份、backup、防误删、doctor、事务快照
4
- version: 0.16.5
4
+ version: 0.16.6
5
5
  license: MIT
6
6
  ---
7
7
 
@@ -243,7 +243,9 @@ yotta-memory doctor --json
243
243
  - 输出 `memory_home: (未设置,默认 ~/.yottamemory)` → 🔒 征得同意后引导设置:问用户用默认还是指定目录(项目级 `<repo>/.yottamemory`、记忆盘等),确认后 AI 执行 `yotta-memory config set memory_home <目录>`,回读 `config get` 验证。
244
244
  2. **已有记忆**:目标目录已存在 `facts/` 等子目录或 `index.json` → 直接 recall;全新目录 → 按「便携记忆盘模式 §0.3」初始化。
245
245
  3. **私密区为明文(无 `keys/`;`doctor` 显示「加密: 否」)**:首次使用即主动告知风险——明文私密记忆(PREF / BOUND / COMMIT)可被同机任何能读文件的进程或用户直接看到;然后给出《明文库转加密(第一次最短路径)》。口令与迁移必须由用户本人执行,AI 只讲解并负责后续 `key claim`。用户明确拒绝加密时,记录其选择并复述明文风险,不反复打扰。
246
- 4. **私密区已加密(存在 `keys/`)**:先 `yotta-memory key list` 确认本智能体是否有 agent binding;没有 → 告知用户由用户自己执行 `yotta-memory view` → 浏览器打开平台 → 输入主口令 → 点「授权」并保存只展示一次的 `agent_key`。用户授权后服务端会写 `keys/pending/<id>.key`;AI 在新会话执行 `yotta-memory key status <id>`(需要时显式加 `--to <AI_HOME>` 或 `--agent-key-file <文件>`;默认发现规则见下),有 pending 就执行 `yotta-memory key claim <id>`,落到 `<AI_HOME>/.yotta-memory-agent-key` 后再使用 `--agent-key-file`。**升级后首次调用元忆 / 重启会话时**,若输出 `[YTM_MIGRATION_REQUIRED]`,必须主动把 marker、受影响 agent 和处理步骤转达给用户。**AI 不得代替用户执行 `migrate` / `key bind` 迁移**,只负责提醒和讲解(marker 只列仍有私密数据、未绑定的 agent;仅有 legacy cache、无迁移数据的 owner 会单独提示,不进入迁移清单;公共 FACT 不受影响)。
246
+ 4. **Agent Plugin 开箱装配(v0.16.6)**:插件 `mcp.json` 入口是 `bin/plugin-mcp.js`。宿主已替换身份占位符时优先用替换值;未替换时回退到 `YOTTA_MEMORY_AGENT_ID` + `YOTTA_MEMORY_AGENT_KEY_FILE`,再回退到 `PLUGIN_DATA/identity.json`。都没有时插件以未授权模式启动(公共 FACT 可读、私密 fail-closed)并打印绑定指引。绑定一条命令:用户在 `yotta-memory view` 授权后执行 `yotta-memory key claim <id> --plugin-data <PLUGIN_DATA>`(同时写 key 与 `identity.json`),重启会话即可。
247
+
248
+ 5. **私密区已加密(存在 `keys/`)**:先 `yotta-memory key list` 确认本智能体是否有 agent binding;没有 → 告知用户由用户自己执行 `yotta-memory view` → 浏览器打开平台 → 输入主口令 → 点「授权」并保存只展示一次的 `agent_key`。用户授权后服务端会写 `keys/pending/<id>.key`;AI 在新会话执行 `yotta-memory key status <id>`(需要时显式加 `--to <AI_HOME>` 或 `--agent-key-file <文件>`;默认发现规则见下),有 pending 就执行 `yotta-memory key claim <id>`,落到 `<AI_HOME>/.yotta-memory-agent-key` 后再使用 `--agent-key-file`。**升级后首次调用元忆 / 重启会话时**,若输出 `[YTM_MIGRATION_REQUIRED]`,必须主动把 marker、受影响 agent 和处理步骤转达给用户。**AI 不得代替用户执行 `migrate` / `key bind` 迁移**,只负责提醒和讲解(marker 只列仍有私密数据、未绑定的 agent;仅有 legacy cache、无迁移数据的 owner 会单独提示,不进入迁移清单;公共 FACT 不受影响)。
247
249
 
248
250
  **B. 确认本智能体唯一身份(强制,写私密记忆前必做)**:
249
251
 
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // yotta-memory(元忆)插件 MCP 启动器(v0.16.6)
3
+ // 宿主(Codex / Agent Plugins)只替换 ${PLUGIN_ROOT} 与注入 PLUGIN_ROOT / PLUGIN_DATA,
4
+ // 不替换 mcp.json 里的 ${AGENT_ID} / ${AGENT_KEY_FILE}。本启动器按优先级解析身份:
5
+ // 1) 宿主已替换的 --agent-id / --agent-key-file
6
+ // 2) YOTTA_MEMORY_AGENT_ID + YOTTA_MEMORY_AGENT_KEY_FILE 环境变量
7
+ // 3) PLUGIN_DATA/identity.json(agent_id / key_file;key_file 缺省 <PLUGIN_DATA>/.yotta-memory-agent-key)
8
+ // 4) 都没有 -> 以未授权模式启动,输出可执行的绑定指引(不出现字面量占位符)
9
+ // 绑定路径:用户在 yotta-memory view 授权后,AI 执行
10
+ // yotta-memory key claim <agent_id> --to <PLUGIN_DATA>
11
+ // 或写入 <PLUGIN_DATA>/identity.json。
12
+ 'use strict';
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { spawn } = require('child_process');
16
+
17
+ const AGENT_KEY_FILENAME = '.yotta-memory-agent-key';
18
+ const PLACEHOLDER_RE = /^\$\{[A-Z_]+\}$/;
19
+
20
+ function cleanValue(value) {
21
+ const v = String(value == null ? '' : value).trim();
22
+ if (!v || PLACEHOLDER_RE.test(v)) return '';
23
+ return v;
24
+ }
25
+
26
+ function readIdentityFile(pluginData) {
27
+ if (!pluginData) return null;
28
+ const file = path.join(pluginData, 'identity.json');
29
+ if (!fs.existsSync(file)) return null;
30
+ try {
31
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
32
+ const id = cleanValue(data.agent_id || data.agentId || '');
33
+ if (!id) return null;
34
+ const keyFile = cleanValue(data.key_file || data.keyFile || '') ||
35
+ path.join(pluginData, AGENT_KEY_FILENAME);
36
+ return { id: id, keyFile: keyFile, source: 'plugin-data' };
37
+ } catch (e) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function resolvePluginIdentity(opts, env) {
43
+ opts = opts || {};
44
+ env = env || process.env;
45
+ const hostId = cleanValue(opts.agentId);
46
+ const hostKey = cleanValue(opts.keyFile);
47
+ if (hostId && hostKey) {
48
+ return { id: hostId, keyFile: hostKey, source: 'host-args' };
49
+ }
50
+ const envId = cleanValue(env.YOTTA_MEMORY_AGENT_ID);
51
+ const envKey = cleanValue(env.YOTTA_MEMORY_AGENT_KEY_FILE);
52
+ if (envId && envKey) {
53
+ return { id: envId, keyFile: envKey, source: 'env' };
54
+ }
55
+ const fromData = readIdentityFile(cleanValue(env.PLUGIN_DATA));
56
+ if (fromData) return fromData;
57
+ return null;
58
+ }
59
+
60
+ function parseArgs(argv) {
61
+ const rest = [];
62
+ const opts = { agentId: '', keyFile: '' };
63
+ for (let i = 0; i < argv.length; i += 1) {
64
+ const a = argv[i];
65
+ if (a === '--agent-id') { opts.agentId = argv[++i]; continue; }
66
+ if (a === '--agent-key-file') { opts.keyFile = argv[++i]; continue; }
67
+ rest.push(a);
68
+ }
69
+ return { rest: rest, opts: opts };
70
+ }
71
+
72
+ function guidanceText(pluginData) {
73
+ const dataDir = pluginData || '<PLUGIN_DATA>';
74
+ return [
75
+ '元忆插件 MCP 尚未装配智能体身份:宿主未替换 mcp.json 中的身份占位符,且未找到环境变量或 identity.json。',
76
+ '当前以未授权模式启动:公共 FACT 可读,私密记忆(PREF / BOUND / COMMIT)fail-closed。',
77
+ '绑定步骤(用户授权后由 AI 执行):',
78
+ ' 1) 用户在 yotta-memory view 中授权本智能体;',
79
+ ' 2) AI 执行 yotta-memory key claim <agent_id> --to ' + dataDir,
80
+ ' 或写入 ' + path.join(dataDir, 'identity.json') + ':{"agent_id":"<agent_id>"}',
81
+ ' 3) 重启会话后插件即可读取私密记忆。',
82
+ ].join('\n');
83
+ }
84
+
85
+ function main() {
86
+ const parsed = parseArgs(process.argv.slice(2));
87
+ const ident = resolvePluginIdentity(parsed.opts, process.env);
88
+ const cli = path.join(__dirname, 'yotta-memory.js');
89
+ const args = [cli].concat(parsed.rest);
90
+ if (ident) {
91
+ args.push('--agent-id', ident.id, '--agent-key-file', ident.keyFile);
92
+ } else {
93
+ process.stderr.write(guidanceText(process.env.PLUGIN_DATA) + '\n');
94
+ }
95
+ const child = spawn(process.execPath, args, { stdio: 'inherit' });
96
+ child.on('exit', function (code, signal) {
97
+ if (signal) process.kill(process.pid, signal);
98
+ else process.exit(code == null ? 0 : code);
99
+ });
100
+ child.on('error', function (err) {
101
+ process.stderr.write('元忆插件 MCP 启动失败: ' + (err && err.message ? err.message : String(err)) + '\n');
102
+ process.exit(2);
103
+ });
104
+ }
105
+
106
+ module.exports = {
107
+ resolvePluginIdentity: resolvePluginIdentity,
108
+ guidanceText: guidanceText,
109
+ };
110
+
111
+ if (require.main === module) main();
@@ -25,7 +25,7 @@ const net = require('net');
25
25
  const child_process = require('child_process');
26
26
  const { AsyncLocalStorage } = require('async_hooks');
27
27
 
28
- const VERSION = '0.16.5';
28
+ const VERSION = '0.16.6';
29
29
  // @generated view-html:start
30
30
  const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\r\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\r\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\r\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\r\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\r\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\r\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\r\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\r\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\r\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\r\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\r\n</style></head><body>\r\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\r\n<div id=\"lock\" class=\"card\">\r\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\r\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\r\n <button onclick=\"unlock()\">解锁</button>\r\n <div class=\"err\" id=\"lockerr\"></div>\r\n</div>\r\n<div id=\"app\">\r\n <div class=\"card\">\r\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\r\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/&lt;agent_id&gt;.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\r\n <div id=\"owners\" style=\"margin-top:8px\"></div>\r\n </div>\r\n <div class=\"card\">\r\n <b>记忆</b>\r\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\r\n <button onclick=\"off=0;load()\">搜索</button>\r\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\r\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\r\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\r\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\r\n <div id=\"entries\" style=\"margin-top:6px\"></div>\r\n <div id=\"pager\" style=\"margin-top:10px\">\r\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\r\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\r\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\r\n </div>\r\n </div>\r\n <div class=\"card\">\r\n <b>重设口令</b><br>\r\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\r\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\r\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\r\n <button onclick=\"resetPw()\">重设</button>\r\n <span id=\"pwout\"></span>\r\n </div>\r\n</div>\r\n<script>\r\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\r\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\r\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\r\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\r\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\r\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML=esc(d.hint||'(无 owner)');return;}\r\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\r\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-a');if(!confirm('确认由你为用户授权 '+owner+' 读取其私密记忆?授权后将生成只显示一次的 agent_key,请立即保存;同时写入待领取文件供该 AI 新会话领取。AI 不应代为执行该授权操作。'))return;b.disabled=true;api('/api/authorize',{owner:owner}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'授权失败');loadOwners();return;}if(d.agentKey){showKey(d.agentKey);}loadOwners();});};});\r\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){if(!confirm('确认吊销 '+b.getAttribute('data-r')+' 的 agent_key?吊销后该智能体立即失去私密读写能力。'))return;api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\r\nfunction showKey(k){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:99';var box=document.createElement('div');box.className='card';box.style.cssText='max-width:640px;word-break:break-all';var t=document.createElement('div');t.innerHTML='<b>agent_key(只显示一次)</b>';var hint=document.createElement('div');hint.className='meta';hint.textContent='请用户立即单独保存。AI 新会话先执行 yotta-memory key status <agent_id>,有 pending 再执行 key claim <agent_id>;默认写入 AI_HOME/.yotta-memory-agent-key,需要时用 --to 或 --agent-key-file 指定。若 key 丢失,可吊销后重新授权;旧 key 会立即校验失败。';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=k;ta.style.cssText='width:100%;height:72px;margin-top:8px;font-family:monospace;font-size:12px';var close=document.createElement('button');close.textContent='我已保存,关闭';close.onclick=function(){ov.remove();};box.appendChild(t);box.appendChild(hint);box.appendChild(ta);box.appendChild(close);ov.appendChild(box);document.body.appendChild(ov);ta.focus();ta.select();}\r\n}\r\nlet off=0,PS=50;\r\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\r\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\r\nfunction nextPage(){off+=PS;load();}\r\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\r\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\r\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\r\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\r\nboot();\r\n</script></body></html>\r\n";
31
31
  // @generated view-html:end
@@ -1142,9 +1142,16 @@ function resolveAgentKeyLocation(owner, opts) {
1142
1142
  opts = opts || {};
1143
1143
  const explicitHome = String(opts.to || '').trim();
1144
1144
  const explicitFile = String(opts.agentKeyFile || '').trim();
1145
+ const explicitPluginData = String(opts.pluginData || '').trim();
1145
1146
  if (explicitHome && explicitFile) {
1146
1147
  return { ok: false, error: '--to 与 --agent-key-file 不能同时使用。' };
1147
1148
  }
1149
+ if (explicitPluginData && (explicitHome || explicitFile)) {
1150
+ return { ok: false, error: '--plugin-data 不能与 --to / --agent-key-file 同时使用。' };
1151
+ }
1152
+ if (explicitPluginData) {
1153
+ return { ok: true, file: agentKeyFilePath(explicitPluginData), source: 'option:--plugin-data' };
1154
+ }
1148
1155
  if (explicitFile) {
1149
1156
  return { ok: true, file: path.resolve(explicitFile), source: 'option:--agent-key-file' };
1150
1157
  }
@@ -4557,11 +4564,27 @@ function keyClaimCore(root, owner, opts) {
4557
4564
  return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
4558
4565
  }
4559
4566
  removePendingAgentKey(root, owner);
4567
+ let identityFile = '';
4568
+ if (opts && opts.pluginData) {
4569
+ try {
4570
+ const dataDir = path.resolve(String(opts.pluginData));
4571
+ fs.mkdirSync(dataDir, { recursive: true });
4572
+ identityFile = path.join(dataDir, 'identity.json');
4573
+ const tempIdentity = identityFile + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex');
4574
+ fs.writeFileSync(tempIdentity, JSON.stringify({ agent_id: owner }, null, 2) + '\n', { encoding: 'utf8' });
4575
+ try { fs.renameSync(tempIdentity, identityFile); }
4576
+ catch (e) { fs.rmSync(identityFile, { force: true }); fs.renameSync(tempIdentity, identityFile); }
4577
+ } catch (e) {
4578
+ identityFile = '';
4579
+ }
4580
+ }
4581
+ const identityLine = identityFile ? '\n插件身份文件: ' + identityFile : '';
4560
4582
  return {
4561
4583
  error: false,
4562
4584
  targetFile: targetFile,
4585
+ identityFile: identityFile,
4563
4586
  source: location.source,
4564
- text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\n发现规则: ' + location.source + '\npending 文件已删除。',
4587
+ text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + identityLine + '\n发现规则: ' + location.source + '\npending 文件已删除。',
4565
4588
  };
4566
4589
  }
4567
4590
 
@@ -4645,7 +4668,7 @@ function cmdKeyRevoke(id) {
4645
4668
 
4646
4669
  function cmdKeyClaim(id, opts) {
4647
4670
  const root = userRoot();
4648
- if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4671
+ if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> [--to <AI_HOME> | --plugin-data <PLUGIN_DATA> | --agent-key-file <文件>]'); process.exit(2); }
4649
4672
  const r = keyClaimCore(root, id, opts);
4650
4673
  console.log(r.text);
4651
4674
  if (r.error) process.exit(2);
@@ -4653,7 +4676,7 @@ function cmdKeyClaim(id, opts) {
4653
4676
 
4654
4677
  function cmdKeyStatus(id, opts) {
4655
4678
  const root = userRoot();
4656
- if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4679
+ if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME> | --plugin-data <PLUGIN_DATA> | --agent-key-file <文件>]'); process.exit(2); }
4657
4680
  const r = keyStatusCore(root, id, opts);
4658
4681
  console.log(r.text);
4659
4682
  if (r.error) process.exit(2);
@@ -7176,7 +7199,7 @@ function usage() {
7176
7199
  ['migrate', '把明文库迁移为密文(需主口令;空明文库同样可用;--password-stdin;--recovery-key-out <文件> 写恢复钥匙;首次迁移:echo 主口令 | yotta-memory migrate --password-stdin --recovery-key-out <文件>;迁移后授权二选一:推荐 yotta-memory view,等价 CLI 为 yotta-memory key bind <id>)'],
7177
7200
  ['view', '启动用户查看平台(--port/--host;空加密库可用恢复钥匙校验主口令;已在运行则复用 URL)'],
7178
7201
  ['reset-password', '重设主口令(忘口令用恢复钥匙)'],
7179
- ['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> [--to <AI_HOME> | --agent-key-file <文件>] / status <id> [--to <AI_HOME> | --agent-key-file <文件>] / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
7202
+ ['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> [--to <AI_HOME> | --plugin-data <PLUGIN_DATA> | --agent-key-file <文件>] / status <id> [--to <AI_HOME> | --plugin-data <PLUGIN_DATA> | --agent-key-file <文件>] / revoke <id>;bind/rotate 需主口令或恢复钥匙;--plugin-data 供 Agent Plugin 一条命令绑定插件身份)'],
7180
7203
  ['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
7181
7204
  ]],
7182
7205
  ['平台与服务', [
@@ -7206,7 +7229,7 @@ async function main() {
7206
7229
  if (!args.length) { usage(); return; }
7207
7230
  const opts = {};
7208
7231
  const positional = [];
7209
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir']);
7232
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir']);
7210
7233
  for (let i = 0; i < args.length; i++) {
7211
7234
  const a = args[i];
7212
7235
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -7280,6 +7303,7 @@ async function main() {
7280
7303
  else if (a === '--min-group') opts.minGroup = parseInt(v, 10) || 0;
7281
7304
  else if (a === '--period') opts.period = parseInt(v, 10) || 0;
7282
7305
  else if (a === '--to') opts.to = v;
7306
+ else if (a === '--plugin-data') opts.pluginData = v;
7283
7307
  else if (a === '--id') opts.id = v;
7284
7308
  else if (a === '--time') opts.time = v;
7285
7309
  else if (a === '--tools') opts.toolProfile = v;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.16.5",
3
+ "version": "0.16.6",
4
4
  "description": "Yuanyi (元忆) — boundary-aware, file-based memory for AI agents. File-based, zero-dependency, diff/rollback-able; FACT/PREF/BOUND/COMMIT types (public shared / private isolated), user-level + project-level storage; v0.12 reliability baseline (init guard, trash-based deletion, independent-volume backup/list/doctor/restore, start-of-work doctor, transactional snapshots before destructive writes), v0.10 consolidation (periodic summaries with provenance, near-duplicate auto-merge, per-type decay, batch audit + rollback), v0.9 recall quality + context focus + optional local embedding plugin, plus v0.8 semantic search, feedback loop, self-organization and distillation.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.16.5",
6
+ "version": "0.16.6",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true