@yottameta/yotta-memory 0.2.10 → 0.4.2
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 +116 -14
- package/SKILL.md +153 -7
- package/USER_GUIDE.md +286 -0
- package/assets/banner.png +0 -0
- package/bin/yotta-memory.js +560 -71
- package/package.json +3 -2
- package/references/protocol.md +11 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// yotta-memory(元忆): 有权限边界的文件式智能体记忆 CLI(零依赖)
|
|
3
|
-
// v0.
|
|
3
|
+
// v0.4.0 新增:lan(Windows 计划任务开机自启 serve)/ init --dir(显式指定位置)/ serve --stdio(本地零进程模式)/ MCP 工具补 reindex/export/import
|
|
4
|
+
// v0.4.2 新增:iam/whoami 智能体身份自注册(agents.json 唯一性)+ 自我档案 PREF + 私密记忆须声明 owner
|
|
4
5
|
'use strict';
|
|
5
6
|
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
const os = require('os');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const child_process = require('child_process');
|
|
9
13
|
|
|
10
|
-
const VERSION = '0.2
|
|
14
|
+
const VERSION = '0.4.2';
|
|
11
15
|
const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
|
|
12
16
|
const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
|
|
13
17
|
const ARCHIVE_DIR = '.archive';
|
|
14
18
|
const INDEX_FILE = 'index.json';
|
|
15
19
|
const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
|
|
16
20
|
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'access_count', 'last_accessed'];
|
|
21
|
+
const CONFIG_FILE = 'config.json';
|
|
22
|
+
const SERVER_SUBDIR = '.server';
|
|
23
|
+
const TOKENS_FILE = 'tokens.json';
|
|
24
|
+
const AGENTS_FILE = 'agents.json';
|
|
17
25
|
|
|
26
|
+
// ---- 全局配置(记忆库位置持久化,固定 ~/.yottamemory/config.json)----
|
|
27
|
+
function configPath() {
|
|
28
|
+
return path.join(os.homedir(), '.yottamemory', CONFIG_FILE);
|
|
29
|
+
}
|
|
30
|
+
function loadConfig() {
|
|
31
|
+
try { return JSON.parse(fs.readFileSync(configPath(), 'utf8')) || {}; } catch (e) { return {}; }
|
|
32
|
+
}
|
|
33
|
+
function saveConfig(cfg) {
|
|
34
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
35
|
+
fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2), 'utf8');
|
|
36
|
+
}
|
|
18
37
|
function userRoot() {
|
|
19
|
-
|
|
38
|
+
if (process.env.YOTTA_MEMORY_HOME) return process.env.YOTTA_MEMORY_HOME;
|
|
39
|
+
const cfg = loadConfig();
|
|
40
|
+
if (cfg.memory_home) return cfg.memory_home;
|
|
41
|
+
return path.join(os.homedir(), '.yottamemory');
|
|
20
42
|
}
|
|
21
43
|
function projectRoot() {
|
|
22
44
|
return path.join(process.cwd(), '.yottamemory');
|
|
@@ -249,11 +271,7 @@ function vitality(meta) {
|
|
|
249
271
|
}
|
|
250
272
|
function loadGrants(root) {
|
|
251
273
|
const fp = path.join(root, 'grants.json');
|
|
252
|
-
try {
|
|
253
|
-
return JSON.parse(fs.readFileSync(fp, 'utf8')) || {};
|
|
254
|
-
} catch (e) {
|
|
255
|
-
return {};
|
|
256
|
-
}
|
|
274
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')) || {}; } catch (e) { return {}; }
|
|
257
275
|
}
|
|
258
276
|
function hasGrant(userAgent, ownerAgent) {
|
|
259
277
|
if (!userAgent || !ownerAgent) return false;
|
|
@@ -269,35 +287,35 @@ function hasGrant(userAgent, ownerAgent) {
|
|
|
269
287
|
function classifyRead(entry, agent, ownerFilter, unsafe) {
|
|
270
288
|
if (entry.scope === 'public') return 'read';
|
|
271
289
|
const owner = entry.owner || '';
|
|
272
|
-
// 无归属的 private:所有 agent 视为自身私密(默认行为保留)
|
|
273
290
|
if (!owner) return 'read';
|
|
274
|
-
// 归属等于当前 agent 或(显式 --owner 等于当前 agent):自身授权
|
|
275
291
|
if (agent && owner === agent) return 'read';
|
|
276
292
|
if (ownerFilter && owner === ownerFilter && agent && ownerFilter === agent) return 'read';
|
|
277
|
-
// 越界读:需要 --unsafe / identity=user / grant 授权,否则拒绝
|
|
278
293
|
if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent && '' + agent).toLowerCase() === 'user') return 'read';
|
|
279
294
|
if (hasGrant(agent, owner)) return 'read';
|
|
280
295
|
return 'denied';
|
|
281
296
|
}
|
|
282
297
|
|
|
283
|
-
// ---- 命令
|
|
284
|
-
function
|
|
285
|
-
const root = opts.project ? projectRoot() : userRoot();
|
|
298
|
+
// ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
|
|
299
|
+
function initCore(opts) {
|
|
300
|
+
const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
|
|
286
301
|
ensureInit(root);
|
|
287
|
-
|
|
302
|
+
return { error: false, text: '已初始化记忆库: ' + root };
|
|
288
303
|
}
|
|
289
|
-
|
|
290
|
-
function cmdRemember(type, subject, statement, opts) {
|
|
304
|
+
function rememberCore(type, subject, statement, opts) {
|
|
291
305
|
const root = userRoot();
|
|
292
306
|
ensureInit(root);
|
|
293
307
|
const t = String(type).toUpperCase();
|
|
294
|
-
|
|
308
|
+
if (!TYPE_DIRS[t]) return { error: true, text: '未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')' };
|
|
309
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
295
310
|
const stmt = String(statement || '').trim();
|
|
296
311
|
const subj = String(subject || '').trim();
|
|
297
|
-
if (!stmt) {
|
|
298
|
-
if (!subj) {
|
|
312
|
+
if (!stmt) return { error: true, text: 'statement 不能为空' };
|
|
313
|
+
if (!subj) return { error: true, text: 'subject 不能为空' };
|
|
299
314
|
const owner = opts.owner || currentAgent();
|
|
300
315
|
const scope = opts.scope || defaultScope(t);
|
|
316
|
+
if (scope === 'private' && !owner) {
|
|
317
|
+
return { error: true, text: '私密记忆必须声明归属智能体:请设环境变量 YOTTA_AGENT_ID(或 AGENT_ID)、传 --owner <id>,或先 yotta-memory whoami / iam 登记唯一身份。公共记忆(FACT)不受影响。' };
|
|
318
|
+
}
|
|
301
319
|
if (fs.existsSync(dir)) {
|
|
302
320
|
for (const f of fs.readdirSync(dir)) {
|
|
303
321
|
const fp = path.join(dir, f);
|
|
@@ -310,8 +328,7 @@ function cmdRemember(type, subject, statement, opts) {
|
|
|
310
328
|
if (scope && !meta.scope) patch.scope = scope;
|
|
311
329
|
rewriteFrontmatter(fp, patch);
|
|
312
330
|
upsertIndexEntry(root, readEntry(fp, root));
|
|
313
|
-
|
|
314
|
-
return;
|
|
331
|
+
return { error: false, text: '已更新: ' + fp };
|
|
315
332
|
}
|
|
316
333
|
}
|
|
317
334
|
}
|
|
@@ -325,22 +342,17 @@ function cmdRemember(type, subject, statement, opts) {
|
|
|
325
342
|
};
|
|
326
343
|
fs.writeFileSync(file, frontmatterToText(rec, stmt), 'utf8');
|
|
327
344
|
upsertIndexEntry(root, readEntry(file, root));
|
|
328
|
-
|
|
345
|
+
return { error: false, text: '已记录: ' + file };
|
|
329
346
|
}
|
|
330
|
-
|
|
331
|
-
function cmdRecall(query, opts) {
|
|
347
|
+
function recallCore(query, opts) {
|
|
332
348
|
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
333
|
-
if (!roots.length) {
|
|
334
|
-
console.log('记忆库不存在,请先运行: yotta-memory init');
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
349
|
+
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
337
350
|
const limit = opts.limit || 50;
|
|
338
351
|
const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
|
|
339
352
|
const q = query ? String(query).toLowerCase() : '';
|
|
340
353
|
const agent = opts.agent || currentAgent();
|
|
341
354
|
const ownerFilter = opts.owner || '';
|
|
342
355
|
const allSafe = !!opts.unsafe;
|
|
343
|
-
// 是否显式发起跨智能体读取(--all 或 --owner <其它agent>):只有此时越界读才报错/警告,默认 recall 保持原有静默跳过
|
|
344
356
|
const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
|
|
345
357
|
const hits = [];
|
|
346
358
|
let deniedCount = 0;
|
|
@@ -375,12 +387,9 @@ function cmdRecall(query, opts) {
|
|
|
375
387
|
const shown = hits.slice(0, limit);
|
|
376
388
|
if (!shown.length) {
|
|
377
389
|
if (deniedCount > 0 && explicitCross) {
|
|
378
|
-
|
|
379
|
-
console.log('如需读取其它智能体私密记忆,请加 --unsafe(用户显式授权)或 --owner user。');
|
|
380
|
-
process.exit(3);
|
|
390
|
+
return { error: false, exitCode: 3, text: '检测到 ' + deniedCount + ' 条越界访问已被拒绝。\n如需读取其它智能体私密记忆,请加 --unsafe(用户显式授权)或 --owner user。' };
|
|
381
391
|
}
|
|
382
|
-
|
|
383
|
-
return;
|
|
392
|
+
return { error: false, exitCode: 0, text: '无匹配记忆。' };
|
|
384
393
|
}
|
|
385
394
|
const touchRel = shown.filter(function (h) { return !h.entry.immutable; }).map(function (h) { return h.entry.file; });
|
|
386
395
|
if (touchRel.length) {
|
|
@@ -389,17 +398,17 @@ function cmdRecall(query, opts) {
|
|
|
389
398
|
touchIndex(root, touchRel);
|
|
390
399
|
}
|
|
391
400
|
}
|
|
392
|
-
|
|
401
|
+
const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
|
|
393
402
|
for (const h of shown) {
|
|
394
|
-
|
|
395
|
-
|
|
403
|
+
lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
|
|
404
|
+
lines.push(' ' + path.join(h.root, h.entry.file));
|
|
396
405
|
}
|
|
397
406
|
if (deniedCount > 0 && explicitCross) {
|
|
398
|
-
|
|
407
|
+
lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
|
|
399
408
|
}
|
|
409
|
+
return { error: false, exitCode: 0, text: lines.join('\n') };
|
|
400
410
|
}
|
|
401
|
-
|
|
402
|
-
function cmdForget(fileRef) {
|
|
411
|
+
function forgetCore(fileRef) {
|
|
403
412
|
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
404
413
|
const ref = String(fileRef || '').replace(/\\/g, '/');
|
|
405
414
|
const slash = ref.indexOf('/');
|
|
@@ -430,17 +439,16 @@ function cmdForget(fileRef) {
|
|
|
430
439
|
}
|
|
431
440
|
if (target) break;
|
|
432
441
|
}
|
|
433
|
-
if (!target) {
|
|
442
|
+
if (!target) return { error: true, text: '未找到记忆文件: ' + fileRef };
|
|
434
443
|
fs.unlinkSync(target);
|
|
435
444
|
if (targetRoot) removeIndexEntry(targetRoot, targetRel);
|
|
436
|
-
|
|
445
|
+
return { error: false, text: '已删除: ' + target };
|
|
437
446
|
}
|
|
438
|
-
|
|
439
|
-
function cmdArchive(opts) {
|
|
447
|
+
function archiveCore(opts) {
|
|
440
448
|
const days = opts.days || 180;
|
|
441
449
|
const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : 0.4;
|
|
442
450
|
const root = userRoot();
|
|
443
|
-
if (!fs.existsSync(root)) {
|
|
451
|
+
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
444
452
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
445
453
|
let moved = 0;
|
|
446
454
|
const movedFiles = [];
|
|
@@ -465,9 +473,33 @@ function cmdArchive(opts) {
|
|
|
465
473
|
}
|
|
466
474
|
}
|
|
467
475
|
if (movedFiles.length) removeIndexEntries(root, movedFiles);
|
|
468
|
-
|
|
476
|
+
return { error: false, text: '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) };
|
|
469
477
|
}
|
|
470
478
|
|
|
479
|
+
// ---- 命令包装(CLI 入口)----
|
|
480
|
+
function cmdInit(opts) {
|
|
481
|
+
const r = initCore(opts);
|
|
482
|
+
console.log(r.text);
|
|
483
|
+
}
|
|
484
|
+
function cmdRemember(type, subject, statement, opts) {
|
|
485
|
+
const r = rememberCore(type, subject, statement, opts);
|
|
486
|
+
console.log(r.text);
|
|
487
|
+
if (r.error) process.exit(2);
|
|
488
|
+
}
|
|
489
|
+
function cmdRecall(query, opts) {
|
|
490
|
+
const r = recallCore(query, opts);
|
|
491
|
+
console.log(r.text);
|
|
492
|
+
if (r.exitCode) process.exit(r.exitCode);
|
|
493
|
+
}
|
|
494
|
+
function cmdForget(fileRef) {
|
|
495
|
+
const r = forgetCore(fileRef);
|
|
496
|
+
console.log(r.text);
|
|
497
|
+
if (r.error) process.exit(2);
|
|
498
|
+
}
|
|
499
|
+
function cmdArchive(opts) {
|
|
500
|
+
const r = archiveCore(opts);
|
|
501
|
+
console.log(r.text);
|
|
502
|
+
}
|
|
471
503
|
function cmdReindex() {
|
|
472
504
|
const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
|
|
473
505
|
if (!roots.length) { console.log('记忆库不存在。'); return; }
|
|
@@ -476,7 +508,6 @@ function cmdReindex() {
|
|
|
476
508
|
console.log('已重建索引 ' + root + '(' + n + ' 条)');
|
|
477
509
|
}
|
|
478
510
|
}
|
|
479
|
-
|
|
480
511
|
function collectAll(root) {
|
|
481
512
|
const out = [];
|
|
482
513
|
for (const t of Object.keys(TYPE_DIRS)) {
|
|
@@ -491,23 +522,26 @@ function collectAll(root) {
|
|
|
491
522
|
}
|
|
492
523
|
return out;
|
|
493
524
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
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';
|
|
525
|
+
function exportCore(root, outPath) {
|
|
526
|
+
const data = { format: 'yottamemory', version: 1, exported: today(), memories: collectAll(root) };
|
|
527
|
+
const target = outPath || path.join(root, 'yottamemory-export-' + today() + '.json');
|
|
500
528
|
fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
|
|
501
|
-
|
|
529
|
+
return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target };
|
|
502
530
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
531
|
+
function importCore(root, src) {
|
|
532
|
+
if (!src) return { error: true, text: '请提供 JSON 文件路径' };
|
|
533
|
+
let fp = String(src);
|
|
534
|
+
if (!fs.existsSync(fp)) {
|
|
535
|
+
const alt = path.resolve(root, fp);
|
|
536
|
+
if (fs.existsSync(alt)) fp = alt;
|
|
537
|
+
}
|
|
538
|
+
if (!fs.existsSync(fp)) return { error: true, text: 'JSON 文件不存在: ' + src };
|
|
539
|
+
let data;
|
|
540
|
+
try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
541
|
+
catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
|
|
542
|
+
if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
|
|
509
543
|
ensureInit(root);
|
|
510
|
-
let
|
|
544
|
+
let m = 0;
|
|
511
545
|
for (const item of data.memories) {
|
|
512
546
|
const meta = item.meta || {};
|
|
513
547
|
const t = (meta.type || 'FACT').toUpperCase();
|
|
@@ -532,32 +566,438 @@ function cmdImport(src) {
|
|
|
532
566
|
};
|
|
533
567
|
fs.writeFileSync(file, frontmatterToText(rec, rec.statement), 'utf8');
|
|
534
568
|
upsertIndexEntry(root, readEntry(file, root));
|
|
535
|
-
|
|
569
|
+
m++;
|
|
570
|
+
}
|
|
571
|
+
return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root };
|
|
572
|
+
}
|
|
573
|
+
function cmdExport(outPath) {
|
|
574
|
+
const r = exportCore(userRoot(), outPath || 'yottamemory-export-' + today() + '.json');
|
|
575
|
+
console.log(r.text);
|
|
576
|
+
}
|
|
577
|
+
function cmdImport(src) {
|
|
578
|
+
const r = importCore(userRoot(), src);
|
|
579
|
+
console.log(r.text);
|
|
580
|
+
if (r.error) process.exit(2);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ---- token 管理(每智能体一个,登记 <记忆库>/.server/tokens.json)----
|
|
584
|
+
function serverDir(root) { return path.join(root, SERVER_SUBDIR); }
|
|
585
|
+
function tokensPath(root) { return path.join(serverDir(root), TOKENS_FILE); }
|
|
586
|
+
function loadTokens(root) {
|
|
587
|
+
try { return JSON.parse(fs.readFileSync(tokensPath(root), 'utf8')) || {}; } catch (e) { return {}; }
|
|
588
|
+
}
|
|
589
|
+
function saveTokens(root, data) {
|
|
590
|
+
fs.mkdirSync(serverDir(root), { recursive: true });
|
|
591
|
+
fs.writeFileSync(tokensPath(root), JSON.stringify(data, null, 2), 'utf8');
|
|
592
|
+
try { fs.chmodSync(tokensPath(root), 0o600); } catch (e) {}
|
|
593
|
+
}
|
|
594
|
+
function cmdTokenNew(agentId, opts) {
|
|
595
|
+
const root = userRoot();
|
|
596
|
+
ensureInit(root);
|
|
597
|
+
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
598
|
+
const data = loadTokens(root);
|
|
599
|
+
data.version = 1;
|
|
600
|
+
data.tokens = data.tokens || {};
|
|
601
|
+
const conflict = identityConflict(root, agentId);
|
|
602
|
+
if (conflict && !opts.force) {
|
|
603
|
+
console.error("错误: 智能体 ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一;确认是同一智能体后加 --force 覆盖。');
|
|
604
|
+
process.exit(2);
|
|
536
605
|
}
|
|
537
|
-
|
|
606
|
+
const token = 'ytm_' + crypto.randomBytes(16).toString('hex');
|
|
607
|
+
data.tokens[agentId] = { token: token, created: today() };
|
|
608
|
+
saveTokens(root, data);
|
|
609
|
+
console.log(token);
|
|
610
|
+
}
|
|
611
|
+
function cmdTokenList() {
|
|
612
|
+
const root = userRoot();
|
|
613
|
+
const data = loadTokens(root);
|
|
614
|
+
const map = data.tokens || {};
|
|
615
|
+
const ids = Object.keys(map);
|
|
616
|
+
if (!ids.length) { console.log('暂无已登记 token(yotta-memory token new --agent <id> 生成)'); return; }
|
|
617
|
+
console.log('已登记智能体:');
|
|
618
|
+
for (const id of ids) console.log(' ' + id + ' (创建于 ' + map[id].created + ')');
|
|
619
|
+
}
|
|
620
|
+
function cmdTokenRevoke(agentId) {
|
|
621
|
+
const root = userRoot();
|
|
622
|
+
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
623
|
+
const data = loadTokens(root);
|
|
624
|
+
data.tokens = data.tokens || {};
|
|
625
|
+
if (data.tokens[agentId]) { delete data.tokens[agentId]; saveTokens(root, data); console.log('已吊销: ' + agentId); }
|
|
626
|
+
else { console.log('未找到已登记智能体: ' + agentId); }
|
|
538
627
|
}
|
|
539
628
|
|
|
629
|
+
|
|
630
|
+
// ---- 智能体身份登记(agents.json)+ 自我档案 ----
|
|
631
|
+
function agentsPath(root) { return path.join(root, AGENTS_FILE); }
|
|
632
|
+
function loadAgents(root) {
|
|
633
|
+
try { return JSON.parse(fs.readFileSync(agentsPath(root), 'utf8')) || {}; } catch (e) { return {}; }
|
|
634
|
+
}
|
|
635
|
+
function saveAgents(root, data) {
|
|
636
|
+
fs.mkdirSync(root, { recursive: true });
|
|
637
|
+
fs.writeFileSync(agentsPath(root), JSON.stringify(data, null, 2), 'utf8');
|
|
638
|
+
}
|
|
639
|
+
const SELF_PROFILE_SUBJECT = '自我接入档案';
|
|
640
|
+
function findSelfProfile(root, agentId) {
|
|
641
|
+
const dir = path.join(root, TYPE_DIRS.PREF);
|
|
642
|
+
if (!fs.existsSync(dir)) return '';
|
|
643
|
+
for (const f of fs.readdirSync(dir)) {
|
|
644
|
+
const fp = path.join(dir, f);
|
|
645
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
646
|
+
const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
|
|
647
|
+
if (meta.subject === SELF_PROFILE_SUBJECT && (meta.owner || '') === agentId) return f;
|
|
648
|
+
}
|
|
649
|
+
return '';
|
|
650
|
+
}
|
|
651
|
+
function selfProfileBody(agentId, root, extra) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push('agent_id: ' + agentId);
|
|
654
|
+
lines.push('host: ' + os.hostname());
|
|
655
|
+
lines.push('memory_home: ' + root);
|
|
656
|
+
lines.push('mcp_mode: ' + (extra.mcpMode || 'stdio'));
|
|
657
|
+
if (extra.engineUrl) lines.push('engine_url: ' + extra.engineUrl);
|
|
658
|
+
if (extra.token) lines.push('token: ' + extra.token);
|
|
659
|
+
return lines.join('; ');
|
|
660
|
+
}
|
|
661
|
+
function writeSelfProfile(root, agentId, extra) {
|
|
662
|
+
const dir = path.join(root, TYPE_DIRS.PREF);
|
|
663
|
+
const seq = nextSeq(dir);
|
|
664
|
+
const file = path.join(dir, today() + '-' + seq + '.md');
|
|
665
|
+
const body = selfProfileBody(agentId, root, extra || {});
|
|
666
|
+
const rec = {
|
|
667
|
+
type: 'PREF', subject: SELF_PROFILE_SUBJECT, statement: body,
|
|
668
|
+
confidence: 1.0, created: today(), updated: today(),
|
|
669
|
+
tags: ['自我档案'], immutable: false,
|
|
670
|
+
scope: 'private', owner: agentId, access_count: 0, last_accessed: '',
|
|
671
|
+
};
|
|
672
|
+
fs.writeFileSync(file, frontmatterToText(rec, body), 'utf8');
|
|
673
|
+
upsertIndexEntry(root, readEntry(file, root));
|
|
674
|
+
return file;
|
|
675
|
+
}
|
|
676
|
+
function identityConflict(root, agentId) {
|
|
677
|
+
const agents = loadAgents(root).agents || {};
|
|
678
|
+
const tokens = loadTokens(root).tokens || {};
|
|
679
|
+
const host = os.hostname();
|
|
680
|
+
const a = agents[agentId];
|
|
681
|
+
if (a && a.host && a.host !== host) return '已被智能体身份登记占用(host=' + a.host + ')';
|
|
682
|
+
if (tokens[agentId]) return '已被远端 token 登记占用(.server/tokens.json)';
|
|
683
|
+
return '';
|
|
684
|
+
}
|
|
685
|
+
function cmdIam(agentId, opts) {
|
|
686
|
+
const root = userRoot();
|
|
687
|
+
ensureInit(root);
|
|
688
|
+
if (!agentId) { console.error('请指定 <id>:yotta-memory iam <id> [--force]'); process.exit(2); }
|
|
689
|
+
const conflict = identityConflict(root, agentId);
|
|
690
|
+
if (conflict && !opts.force) {
|
|
691
|
+
console.error("错误: ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一。请换一个唯一 ID,或确认是同一智能体后加 --force 覆盖。');
|
|
692
|
+
process.exit(2);
|
|
693
|
+
}
|
|
694
|
+
const data = loadAgents(root);
|
|
695
|
+
data.version = 1;
|
|
696
|
+
data.agents = data.agents || {};
|
|
697
|
+
const host = os.hostname();
|
|
698
|
+
const existed = data.agents[agentId];
|
|
699
|
+
data.agents[agentId] = { host: host, created: (existed && existed.created) || today() };
|
|
700
|
+
saveAgents(root, data);
|
|
701
|
+
const file = writeSelfProfile(root, agentId, { mcpMode: 'stdio' });
|
|
702
|
+
console.log('已登记智能体身份: ' + agentId + '(host=' + host + ',' + (conflict ? '--force 覆盖' : '新建') + ')');
|
|
703
|
+
console.log('已写入自我档案: ' + file);
|
|
704
|
+
console.log('本机免 token:以后用 whoami 确认身份;远端接入需 token new --agent ' + agentId);
|
|
705
|
+
}
|
|
706
|
+
function cmdWhoami() {
|
|
707
|
+
const root = userRoot();
|
|
708
|
+
const id = currentAgent();
|
|
709
|
+
if (!id) {
|
|
710
|
+
console.log('当前未声明智能体身份(YOTTA_AGENT_ID / AGENT_ID 未设置,也未传 --agent)。');
|
|
711
|
+
console.log('本机:请在该智能体的 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID>,然后 yotta-memory iam <id> 登记;或 CLI 每次带 --agent <id> / --owner <id>。');
|
|
712
|
+
console.log('远端:X-Agent-Id 请求头 + token(token new --agent <id>)。');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const agents = loadAgents(root).agents || {};
|
|
716
|
+
const tokens = loadTokens(root).tokens || {};
|
|
717
|
+
let reg = '未登记';
|
|
718
|
+
if (agents[id]) reg = 'agents.json 已登记(host=' + agents[id].host + ')';
|
|
719
|
+
else if (tokens[id]) reg = 'tokens.json 已登记(远端)';
|
|
720
|
+
const profile = findSelfProfile(root, id);
|
|
721
|
+
console.log('当前智能体身份: ' + id);
|
|
722
|
+
console.log('登记状态: ' + reg);
|
|
723
|
+
console.log('自我档案: ' + (profile ? profile : '未写入(请执行 yotta-memory iam ' + id + ')'));
|
|
724
|
+
if (!agents[id] && !tokens[id]) console.log('提示: 请先 yotta-memory iam ' + id + ' 登记唯一身份并落自我档案,再写私密记忆。');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ---- config 命令 ----
|
|
728
|
+
function cmdConfigSet(key, value) {
|
|
729
|
+
if (key !== 'memory_home') { console.error('未知配置项: ' + key + '(可用: memory_home)'); process.exit(2); }
|
|
730
|
+
if (!value) { console.error('缺少值: config set memory_home <目录>'); process.exit(2); }
|
|
731
|
+
const cfg = loadConfig();
|
|
732
|
+
cfg.memory_home = value;
|
|
733
|
+
saveConfig(cfg);
|
|
734
|
+
console.log('已记住记忆库位置: ' + value);
|
|
735
|
+
}
|
|
736
|
+
function cmdConfigGet() {
|
|
737
|
+
const cfg = loadConfig();
|
|
738
|
+
console.log('memory_home: ' + (cfg.memory_home || '(未设置,默认 ~/.yottamemory)'));
|
|
739
|
+
console.log('当前生效用户级位置: ' + userRoot());
|
|
740
|
+
if (cfg.serve && Object.keys(cfg.serve).length) console.log('serve: ' + JSON.stringify(cfg.serve));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
|
|
744
|
+
function mcpTools() {
|
|
745
|
+
return [
|
|
746
|
+
{ 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'] } },
|
|
747
|
+
{ name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20)。只返回当前智能体可读记忆', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' } } } },
|
|
748
|
+
{ name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20)', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' } } } },
|
|
749
|
+
{ name: 'forget', description: '删除一条记忆。file 为记忆文件路径(如 facts/2026-08-24-0001.md 或文件名)', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
750
|
+
{ name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' } } } },
|
|
751
|
+
{ name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
|
|
752
|
+
{ name: 'export', description: '导出全部记忆到引擎主机上的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
|
|
753
|
+
{ name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
754
|
+
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
function callTool(name, args, ctx) {
|
|
758
|
+
const agent = ctx.agent || '';
|
|
759
|
+
try {
|
|
760
|
+
if (name === 'remember') {
|
|
761
|
+
const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: args.owner || agent });
|
|
762
|
+
return { text: r.text, error: r.error };
|
|
763
|
+
}
|
|
764
|
+
if (name === 'recall' || name === 'search') {
|
|
765
|
+
const r = recallCore(args.query ? String(args.query) : null, { limit: args.limit || 20, type: args.type ? String(args.type) : null, agent: agent });
|
|
766
|
+
return { text: r.text, error: r.error };
|
|
767
|
+
}
|
|
768
|
+
if (name === 'forget') {
|
|
769
|
+
const r = forgetCore(String(args.file || ''));
|
|
770
|
+
return { text: r.text, error: r.error };
|
|
771
|
+
}
|
|
772
|
+
if (name === 'archive') {
|
|
773
|
+
const r = archiveCore({ days: args.days, threshold: args.threshold });
|
|
774
|
+
return { text: r.text, error: r.error };
|
|
775
|
+
}
|
|
776
|
+
if (name === 'reindex') {
|
|
777
|
+
const root = userRoot();
|
|
778
|
+
if (!fs.existsSync(root)) return { text: '记忆库不存在。', error: false };
|
|
779
|
+
const cnt = buildIndex(root).length;
|
|
780
|
+
return { text: '已重建索引 ' + root + '(' + cnt + ' 条)', error: false };
|
|
781
|
+
}
|
|
782
|
+
if (name === 'export') {
|
|
783
|
+
const r = exportCore(userRoot(), args.out ? String(args.out) : null);
|
|
784
|
+
return { text: r.text, error: r.error };
|
|
785
|
+
}
|
|
786
|
+
if (name === 'import') {
|
|
787
|
+
const r = importCore(userRoot(), String(args.src || ''));
|
|
788
|
+
return { text: r.text, error: r.error };
|
|
789
|
+
}
|
|
790
|
+
if (name === 'agent_info') {
|
|
791
|
+
const root = userRoot();
|
|
792
|
+
const id = agent || '';
|
|
793
|
+
if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / YOTTA_AGENT_ID)。本机请在 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID> 并 iam 登记。', error: false };
|
|
794
|
+
const agents = loadAgents(root).agents || {};
|
|
795
|
+
const tokens = loadTokens(root).tokens || {};
|
|
796
|
+
let reg = '未登记';
|
|
797
|
+
if (agents[id]) reg = 'agents.json 已登记(host=' + agents[id].host + ')';
|
|
798
|
+
else if (tokens[id]) reg = 'tokens.json 已登记(远端)';
|
|
799
|
+
const profile = findSelfProfile(root, id);
|
|
800
|
+
return { text: '当前智能体身份: ' + id + '\n登记状态: ' + reg + '\n自我档案: ' + (profile || '未写入(本地引擎主机执行 yotta-memory iam ' + id + ')'), error: false };
|
|
801
|
+
}
|
|
802
|
+
return { text: '未知工具: ' + name, error: true };
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function handleMessage(msg, ctx) {
|
|
808
|
+
if (!msg || msg.jsonrpc !== '2.0') return { jsonrpc: '2.0', id: msg && msg.id, error: { code: -32600, message: 'invalid request' } };
|
|
809
|
+
const id = msg.id;
|
|
810
|
+
if (id === undefined || id === null) return null;
|
|
811
|
+
const method = msg.method || '';
|
|
812
|
+
const params = msg.params || {};
|
|
813
|
+
if (method === 'initialize') {
|
|
814
|
+
return { jsonrpc: '2.0', id: id, result: { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'yotta-memory', version: VERSION } } };
|
|
815
|
+
}
|
|
816
|
+
if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
|
|
817
|
+
if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools() } };
|
|
818
|
+
if (method === 'tools/call') {
|
|
819
|
+
const out = callTool(params.name || '', params.arguments || {}, ctx);
|
|
820
|
+
return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
|
|
821
|
+
}
|
|
822
|
+
return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
|
|
823
|
+
}
|
|
824
|
+
function cmdServe(opts) {
|
|
825
|
+
if (opts.stdio) { cmdServeStdio(); return; }
|
|
826
|
+
const host = opts.host || '0.0.0.0';
|
|
827
|
+
const port = opts.port || 8787;
|
|
828
|
+
const noAuth = !!opts.noAuth;
|
|
829
|
+
const root = userRoot();
|
|
830
|
+
ensureInit(root);
|
|
831
|
+
function authorize(req) {
|
|
832
|
+
if (noAuth) return { agent: String(req.headers['x-agent-id'] || '') };
|
|
833
|
+
const auth = req.headers['authorization'] || '';
|
|
834
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth);
|
|
835
|
+
if (!m) return null;
|
|
836
|
+
const token = m[1].trim();
|
|
837
|
+
const agent = String(req.headers['x-agent-id'] || '');
|
|
838
|
+
const tokenMap = (loadTokens(root).tokens) || {};
|
|
839
|
+
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent };
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const server = http.createServer(function (req, res) {
|
|
843
|
+
let pathname = '/';
|
|
844
|
+
try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
|
|
845
|
+
if (pathname !== '/mcp') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
|
|
846
|
+
const auth = authorize(req);
|
|
847
|
+
if (!auth) {
|
|
848
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
849
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id' } }));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (req.method === 'GET') {
|
|
853
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
854
|
+
res.write('event: endpoint\ndata: /mcp\n\n');
|
|
855
|
+
const iv = setInterval(function () { res.write(': keep-alive\n\n'); }, 15000);
|
|
856
|
+
req.on('close', function () { clearInterval(iv); });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (req.method === 'POST') {
|
|
860
|
+
let body = '';
|
|
861
|
+
req.on('data', function (c) { body += c; });
|
|
862
|
+
req.on('end', function () {
|
|
863
|
+
let msg;
|
|
864
|
+
try { msg = JSON.parse(body); } catch (e) {
|
|
865
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
866
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'parse error' } }));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const resp = handleMessage(msg, auth);
|
|
870
|
+
if (resp === null) { res.writeHead(204); res.end(); return; }
|
|
871
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
872
|
+
res.end(JSON.stringify(resp));
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('method not allowed');
|
|
877
|
+
});
|
|
878
|
+
server.listen(port, host, function () {
|
|
879
|
+
console.log('yotta-memory 记忆引擎已启动(v' + VERSION + ')');
|
|
880
|
+
console.log('URL: http://' + host + ':' + port + '/mcp');
|
|
881
|
+
console.log('记忆库: ' + root);
|
|
882
|
+
if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
|
|
883
|
+
else console.log('鉴权: Bearer token + X-Agent-Id(yotta-memory token new --agent <id> 生成)');
|
|
884
|
+
console.log('按 Ctrl+C 停止');
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ---- stdio 本地零进程模式(照灵魂盘:客户端按需拉起 CLI)----
|
|
889
|
+
function cmdServeStdio() {
|
|
890
|
+
const root = userRoot();
|
|
891
|
+
ensureInit(root);
|
|
892
|
+
const ctx = { agent: currentAgent() };
|
|
893
|
+
let buf = '';
|
|
894
|
+
process.stdin.setEncoding('utf8');
|
|
895
|
+
process.stdin.on('data', function (chunk) {
|
|
896
|
+
buf += chunk;
|
|
897
|
+
let idx;
|
|
898
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
899
|
+
const line = buf.slice(0, idx).trim();
|
|
900
|
+
buf = buf.slice(idx + 1);
|
|
901
|
+
if (!line) continue;
|
|
902
|
+
let msg;
|
|
903
|
+
try { msg = JSON.parse(line); } catch (e) { continue; }
|
|
904
|
+
const resp = handleMessage(msg, ctx);
|
|
905
|
+
if (resp !== null) process.stdout.write(JSON.stringify(resp) + '\n');
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
process.stdin.on('end', function () { process.exit(0); });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ---- lan 命令(Windows 计划任务管理开机自启 serve)----
|
|
912
|
+
const LAN_TASK_NAME = 'YottaMemoryServe';
|
|
913
|
+
function lanTaskRunCmd(opts) {
|
|
914
|
+
const host = opts.host || '0.0.0.0';
|
|
915
|
+
const port = opts.port || 8787;
|
|
916
|
+
return '"' + process.execPath + '" "' + __filename + '" serve --host ' + host + ' --port ' + port;
|
|
917
|
+
}
|
|
918
|
+
function cmdLanEnable(opts) {
|
|
919
|
+
if (process.platform !== 'win32') {
|
|
920
|
+
console.error('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
|
|
921
|
+
process.exit(2);
|
|
922
|
+
}
|
|
923
|
+
const trigger = opts.onstart ? 'onstart' : 'onlogon';
|
|
924
|
+
const tr = lanTaskRunCmd(opts);
|
|
925
|
+
try {
|
|
926
|
+
child_process.execFileSync('schtasks', ['/create', '/tn', LAN_TASK_NAME, '/tr', tr, '/sc', trigger, '/f'], { stdio: 'inherit' });
|
|
927
|
+
} catch (e) {
|
|
928
|
+
console.error('注册计划任务失败(ONSTART 需要管理员权限;ONLOGON 一般不需要;如仍失败请用管理员终端执行): ' + String((e && e.stderr) || (e && e.message) || e).split(/\r?\n/)[0]);
|
|
929
|
+
process.exit(1);
|
|
930
|
+
}
|
|
931
|
+
console.log('已注册开机自启: 计划任务 ' + LAN_TASK_NAME + '(触发器 ' + trigger + ')');
|
|
932
|
+
console.log('运行命令: ' + tr);
|
|
933
|
+
}
|
|
934
|
+
function cmdLanDisable() {
|
|
935
|
+
if (process.platform !== 'win32') {
|
|
936
|
+
console.error('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
|
|
937
|
+
process.exit(2);
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
child_process.execFileSync('schtasks', ['/delete', '/tn', LAN_TASK_NAME, '/f'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
941
|
+
} catch (e) {
|
|
942
|
+
const msg = String((e && e.stderr) || (e && e.message) || e);
|
|
943
|
+
if (msg.indexOf('cannot find the path') !== -1 || msg.indexOf('没有找到') !== -1) {
|
|
944
|
+
console.log('未启用: 计划任务 ' + LAN_TASK_NAME + ' 不存在,无需移除');
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
console.error('移除计划任务失败: ' + msg.split(/\r?\n/)[0]);
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
console.log('已移除开机自启计划任务 ' + LAN_TASK_NAME);
|
|
951
|
+
}
|
|
952
|
+
function cmdLanStatus() {
|
|
953
|
+
if (process.platform !== 'win32') {
|
|
954
|
+
console.log('lan 命令当前仅支持 Windows(计划任务);本机平台: ' + process.platform);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
const out = child_process.execFileSync('schtasks', ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST', '/v'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
959
|
+
console.log('已启用: 计划任务 ' + LAN_TASK_NAME + ' 存在');
|
|
960
|
+
const re = /^(状态|Status)\s*:\s*(.+)$/;
|
|
961
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
962
|
+
const m = re.exec(line.trim());
|
|
963
|
+
if (m) console.log(m[1] + ': ' + m[2]);
|
|
964
|
+
}
|
|
965
|
+
} catch (e) {
|
|
966
|
+
console.log('未启用: 计划任务 ' + LAN_TASK_NAME + ' 不存在(可用 yotta-memory lan enable 注册)');
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ---- usage / main ----
|
|
540
971
|
function usage() {
|
|
541
972
|
console.log([
|
|
542
973
|
'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
|
|
543
974
|
'',
|
|
544
975
|
'用法:',
|
|
545
|
-
' yotta-memory init [--project]
|
|
976
|
+
' yotta-memory init [--project] [--dir <目录>] 初始化记忆库(默认用户级;--dir 显式指定位置)',
|
|
977
|
+
' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
|
|
978
|
+
' yotta-memory config get 查看当前配置与生效位置',
|
|
546
979
|
' yotta-memory remember <type> <subject> <statement> [--owner <id>] 写入记忆',
|
|
547
|
-
' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF
|
|
980
|
+
' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区)',
|
|
548
981
|
' yotta-memory forget <文件或文件名> 删除一条记忆',
|
|
549
982
|
' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
|
|
550
|
-
' yotta-memory reindex
|
|
983
|
+
' yotta-memory reindex 重建索引',
|
|
551
984
|
' yotta-memory export [--out 文件.json] 导出全部记忆',
|
|
552
985
|
' yotta-memory import <文件.json> 导入记忆',
|
|
986
|
+
' yotta-memory whoami 查看当前智能体身份与登记状态(读 YOTTA_AGENT_ID / X-Agent-Id,不猜不默认)',
|
|
987
|
+
' yotta-memory iam <id> [--force] 登记本智能体唯一身份并自动落自我档案(agents.json;ID 必须唯一)',
|
|
988
|
+
' yotta-memory token new --agent <id> [--force] 生成/重置某智能体访问 token(同 ID 已被其它来源占用需 --force 覆盖)',
|
|
989
|
+
' yotta-memory token list 列出已登记智能体',
|
|
990
|
+
' yotta-memory token revoke --agent <id> 吊销某智能体 token',
|
|
991
|
+
' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
|
|
992
|
+
' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows 计划任务,默认 ONLOGON)',
|
|
553
993
|
' yotta-memory --version 版本',
|
|
554
994
|
'',
|
|
555
995
|
'类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
|
|
556
|
-
'环境变量: YOTTA_MEMORY_HOME
|
|
996
|
+
'环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)',
|
|
997
|
+
'远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
|
|
557
998
|
'',
|
|
558
999
|
].join('\n'));
|
|
559
1000
|
}
|
|
560
|
-
|
|
561
1001
|
function main() {
|
|
562
1002
|
const args = process.argv.slice(2);
|
|
563
1003
|
if (!args.length) { usage(); return; }
|
|
@@ -566,12 +1006,16 @@ function main() {
|
|
|
566
1006
|
if (first === '--help' || first === '-h') { usage(); return; }
|
|
567
1007
|
const opts = {};
|
|
568
1008
|
const positional = [];
|
|
569
|
-
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope']);
|
|
1009
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir']);
|
|
570
1010
|
for (let i = 1; i < args.length; i++) {
|
|
571
1011
|
const a = args[i];
|
|
572
1012
|
if (a === '--project') opts.project = true;
|
|
573
1013
|
else if (a === '--all') opts.all = true;
|
|
574
1014
|
else if (a === '--unsafe') opts.unsafe = true;
|
|
1015
|
+
else if (a === '--no-auth') opts.noAuth = true;
|
|
1016
|
+
else if (a === '--stdio') opts.stdio = true;
|
|
1017
|
+
else if (a === '--onstart') opts.onstart = true;
|
|
1018
|
+
else if (a === '--force') opts.force = true;
|
|
575
1019
|
else if (valueOpts.has(a)) {
|
|
576
1020
|
const v = args[++i];
|
|
577
1021
|
if (a === '--type') opts.type = v;
|
|
@@ -582,6 +1026,9 @@ function main() {
|
|
|
582
1026
|
else if (a === '--owner') opts.owner = v;
|
|
583
1027
|
else if (a === '--agent') opts.agent = v;
|
|
584
1028
|
else if (a === '--scope') opts.scope = v;
|
|
1029
|
+
else if (a === '--host') opts.host = v;
|
|
1030
|
+
else if (a === '--port') opts.port = parseInt(v, 10) || 8787;
|
|
1031
|
+
else if (a === '--dir') opts.dir = v;
|
|
585
1032
|
} else if (a.startsWith('--')) {
|
|
586
1033
|
console.error('未知选项: ' + a);
|
|
587
1034
|
process.exit(2);
|
|
@@ -591,6 +1038,15 @@ function main() {
|
|
|
591
1038
|
}
|
|
592
1039
|
switch (first) {
|
|
593
1040
|
case 'init': cmdInit(opts); break;
|
|
1041
|
+
case 'whoami': cmdWhoami(); break;
|
|
1042
|
+
case 'iam': cmdIam(positional[0], opts); break;
|
|
1043
|
+
case 'config': {
|
|
1044
|
+
const sub = positional[0];
|
|
1045
|
+
if (sub === 'set') cmdConfigSet(positional[1], positional[2]);
|
|
1046
|
+
else if (sub === 'get') cmdConfigGet();
|
|
1047
|
+
else { console.error('config 子命令: set memory_home <目录> / get'); process.exit(2); }
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
594
1050
|
case 'remember': cmdRemember(positional[0], positional[1], positional[2], opts); break;
|
|
595
1051
|
case 'recall': cmdRecall(positional[0] || null, opts); break;
|
|
596
1052
|
case 'forget': cmdForget(positional[0]); break;
|
|
@@ -598,6 +1054,23 @@ function main() {
|
|
|
598
1054
|
case 'reindex': cmdReindex(); break;
|
|
599
1055
|
case 'export': cmdExport(opts.out); break;
|
|
600
1056
|
case 'import': cmdImport(positional[0]); break;
|
|
1057
|
+
case 'token': {
|
|
1058
|
+
const sub = positional[0];
|
|
1059
|
+
if (sub === 'new') cmdTokenNew(opts.agent, opts);
|
|
1060
|
+
else if (sub === 'list') cmdTokenList();
|
|
1061
|
+
else if (sub === 'revoke') cmdTokenRevoke(opts.agent);
|
|
1062
|
+
else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
case 'serve': cmdServe(opts); break;
|
|
1066
|
+
case 'lan': {
|
|
1067
|
+
const sub = positional[0];
|
|
1068
|
+
if (sub === 'enable') cmdLanEnable(opts);
|
|
1069
|
+
else if (sub === 'disable') cmdLanDisable();
|
|
1070
|
+
else if (sub === 'status') cmdLanStatus();
|
|
1071
|
+
else { console.error('lan 子命令: enable [--onstart] / disable / status'); process.exit(2); }
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
601
1074
|
default:
|
|
602
1075
|
console.error('未知命令: ' + first);
|
|
603
1076
|
usage();
|
|
@@ -605,4 +1078,20 @@ function main() {
|
|
|
605
1078
|
}
|
|
606
1079
|
}
|
|
607
1080
|
|
|
608
|
-
main();
|
|
1081
|
+
if (require.main === module) main();
|
|
1082
|
+
module.exports = {
|
|
1083
|
+
VERSION: VERSION,
|
|
1084
|
+
userRoot: userRoot,
|
|
1085
|
+
projectRoot: projectRoot,
|
|
1086
|
+
rememberCore: rememberCore,
|
|
1087
|
+
recallCore: recallCore,
|
|
1088
|
+
forgetCore: forgetCore,
|
|
1089
|
+
archiveCore: archiveCore,
|
|
1090
|
+
mcpTools: mcpTools,
|
|
1091
|
+
callTool: callTool,
|
|
1092
|
+
handleMessage: handleMessage,
|
|
1093
|
+
loadConfig: loadConfig,
|
|
1094
|
+
saveConfig: saveConfig,
|
|
1095
|
+
loadTokens: loadTokens,
|
|
1096
|
+
saveTokens: saveTokens,
|
|
1097
|
+
};
|