diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d149b09..9f020aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,9 @@ jobs: - name: HTTP MCP (auth / PKCE / lock / fail-closed) run: npm run verify:mcp:http + - name: Skill package + CLI (init/keygen/print-config/status) + run: npm run verify:mcp:skill + semantic-real: name: semantic recall (real model) runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index 074acf3..40c9ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1796,6 +1796,10 @@ "resolved": "packages/mcp", "link": true }, + "node_modules/@mebular/skill": { + "resolved": "packages/skill", + "link": true + }, "node_modules/@modelcontextprotocol/client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", @@ -7337,6 +7341,14 @@ "engines": { "node": ">=20" } + }, + "packages/skill": { + "name": "@mebular/skill", + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=20" + } } } } diff --git a/package.json b/package.json index 8adadef..2b369f2 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", - "lint": "eslint src tests tests-semantic scripts packages/mcp --ext .ts,.mjs", + "lint": "eslint src tests tests-semantic scripts packages --ext .ts,.mjs", "verify": "node scripts/verify.mjs", "verify:at-rest": "node scripts/scan-at-rest.mjs", "verify:semantic": "node scripts/verify-semantic.mjs", @@ -62,6 +62,7 @@ "bench:snapshot": "node scripts/bench-snapshot.mjs", "verify:mcp:stdio": "node scripts/verify-mcp-stdio.mjs", "verify:mcp:http": "node scripts/verify-mcp-http.mjs", + "verify:mcp:skill": "node scripts/verify-mcp-skill.mjs", "dev": "node --loader ts-node/esm src/index.ts" }, "dependencies": { diff --git a/packages/mcp/bin/mebular.mjs b/packages/mcp/bin/mebular.mjs index dc3d8e9..5ed0722 100755 --- a/packages/mcp/bin/mebular.mjs +++ b/packages/mcp/bin/mebular.mjs @@ -80,6 +80,96 @@ async function runToken(action, flags) { process.exit(2); } +async function runKeygen(flags) { + const { IdentityManager } = await import('@mebular/core'); + const out = typeof flags.out === 'string' ? flags.out : join(homeDir(), 'user-master-key.json'); + const master = await new IdentityManager().generateUserMasterKey(); + const record = { + publicKey: Buffer.from(master.publicKey).toString('base64'), + privateKeyPkcs8: await IdentityManager.exportPrivateKey(master.privateKey), + createdAt: new Date().toISOString(), + }; + await mkdir(dirname(out), { recursive: true }); + await writeFile(out, JSON.stringify(record, null, 2), 'utf-8'); + await chmod(out, 0o600); + console.log(JSON.stringify({ keyFile: out, publicKey: record.publicKey }, null, 2)); + console.error('(主私钥是信任根,请妥善保管;权限 0600)'); +} + +async function runInit(flags) { + const home = homeDir(); + const configFile = join(home, 'config.json'); + const keyFile = join(home, 'user-master-key.json'); + await mkdir(home, { recursive: true }); + + if (!existsSync(keyFile)) await runKeygen({ out: keyFile }); + + if (existsSync(configFile)) { + console.log(`配置已存在,未覆盖:${configFile}`); + } else { + const config = { + storagePath: typeof flags['storage'] === 'string' ? flags['storage'] : join(home, 'store.jsonl'), + storageAdapter: 'json', + deviceId: typeof flags['device-id'] === 'string' ? flags['device-id'] : `device-${process.env.HOSTNAME ?? 'local'}`, + encryption: { level: 'none', keyFile }, + network: { enabled: false, libp2p: { listen: [], relayServers: [], relayUnlimited: false } }, + sync: { autoSync: true }, + semantic: { enabled: false, minScore: 0.2 }, + mcp: { http: { host: '127.0.0.1', port: 7331, auth: 'none', tls: false, tokensFile: join(home, 'auth', 'tokens.json') } }, + }; + await writeFile(configFile, JSON.stringify(config, null, 2), 'utf-8'); + console.log(`已写入配置:${configFile}`); + } + console.log( + [ + '', + '下一步:', + ' node packages/mcp/bin/mebular.mjs mcp # stdio 接入(各 MCP client)', + ' node packages/mcp/bin/mebular.mjs serve # Streamable HTTP', + ' node packages/mcp/bin/mebular.mjs status # 查看状态', + ' node packages/skill/scripts/install.mjs # 安装行为层 Skill', + ].join('\n'), + ); +} + +function runPrintConfig(flags) { + const client = typeof flags.client === 'string' ? flags.client : 'generic'; + const url = typeof flags.url === 'string' ? flags.url : null; + const local = { command: 'mebular', args: ['mcp'] }; + + const snippets = { + opencode: JSON.stringify( + { $schema: 'https://opencode.ai/config.json', mcp: { mebular: url ? { type: 'remote', url } : { type: 'local', command: ['mebular', 'mcp'] } } }, + null, + 2, + ), + claude: JSON.stringify({ mcpServers: { mebular: url ? { url } : local } }, null, 2), + cursor: JSON.stringify({ mcpServers: { mebular: url ? { url } : local } }, null, 2), + generic: JSON.stringify({ mcpServers: { mebular: url ? { url } : local } }, null, 2), + dsh: url + ? `plugins:\n - name: '@deepseek-ai/dsh-mcp-client'\n config:\n serverName: mebular\n transport: streamable-http\n url: ${url}` + : "plugins:\n - name: '@deepseek-ai/dsh-mcp-client'\n config:\n serverName: mebular\n transport: stdio\n command: mebular\n args: ['mcp']", + }; + const output = snippets[client]; + if (!output) { + console.error(`未知 client:${client}(opencode/claude/cursor/dsh/generic)`); + process.exit(2); + } + console.log(output); +} + +async function runStatus() { + const { createMebular } = await import('../src/config.mjs'); + const { MemoryService } = await import('@mebular/core'); + const { app, home, storagePath } = await createMebular(); + try { + const status = await new MemoryService(app).status(); + console.log(JSON.stringify({ ...status, home, storagePath }, null, 2)); + } finally { + await app.shutdown().catch(() => undefined); + } +} + async function main() { const flags = parseFlags(argv.slice(1)); switch (command) { @@ -127,11 +217,16 @@ async function main() { process.exit(0); return; case 'init': + await runInit(flags); + return; case 'keygen': + await runKeygen(flags); + return; case 'print-config': + runPrintConfig(flags); + return; case 'status': - console.error(`mebular ${command}:尚未实现(按 G6 计划补齐)`); - process.exit(2); + await runStatus(); return; default: console.error(`未知命令:${command ?? '(空)'}`); diff --git a/packages/skill/MEMORY_POLICY.md b/packages/skill/MEMORY_POLICY.md new file mode 100644 index 0000000..a865abc --- /dev/null +++ b/packages/skill/MEMORY_POLICY.md @@ -0,0 +1,38 @@ +# Mebular 记忆使用规约(MEMORY_POLICY) + +本文件是 Agent 使用 Mebular 记忆时的行为规约;与 `SKILL.md` 配套分发。 + +## 1. 先查后写 + +写入前先用 `memory_query` / `memory_search` 查重;已有等价记忆则不重复写入,必要时用更新语义。避免记忆库被重复条目稀释。 + +## 2. 类型选择 + +| 内容 | 类型 | +|------|------| +| 稳定事实(人、物、关系、客观陈述) | `fact` | +| 用户偏好(可随时间变化的取向) | `preference` | +| 会话 / 任务 / 决策 / 报错过程 | `episode` | +| 可复用的步骤、命令、流程 | `skill` | +| 对用户或环境的观察 | `observation` | + +## 3. 时效 + +- 有有效期的记忆用 `metadata.expiresAt` 标注。 +- 过期事实不要当作现状;需要现状时重新确认。 +- 事实冲突时以更新的 `validFrom` 为准。 + +## 4. 隐私 + +- 不要写入密钥、口令、令牌、完整证件号等高敏感信息。 +- 只记录完成任务所必需的最小信息。 +- 用户明确要求删除时,尊重删除语义(墓碑)。 + +## 5. 关系 + +- 相关对象用 `metadata.relatedTo` 关联**已存在**的节点,不要制造悬空引用。 +- 需要结构化关系时优先用既有实体作为端点。 + +## 6. 无结果别编 + +召回为空就如实说明「没有相关记忆」,不要凭推测填充。宁可承认缺失,也不写入或返回未经验证的内容。 diff --git a/packages/skill/SKILL.md b/packages/skill/SKILL.md new file mode 100644 index 0000000..b1b8dc8 --- /dev/null +++ b/packages/skill/SKILL.md @@ -0,0 +1,44 @@ +--- +name: mebular-memory +description: 通过 Mebular MCP 使用跨设备、离线可用的图式长期记忆:先查后写、类型化节点、时效与关系、隐私红线。 +whenToUse: 需要记住或复用用户偏好/事实/会话/技能,跨会话或跨设备共享 Agent 记忆,或按语义/关系检索既有记忆时。 +--- + +# Mebular Memory + +Mebular 是一个本地优先的分布式图记忆层。通过 MCP 工具读写记忆,行为规约见 `MEMORY_POLICY.md`。 + +## 何时使用 + +- 用户透露稳定偏好、事实或约束,且未来会复用时。 +- 需要回顾历史会话、任务过程或既有技能时。 +- 需要按语义(同义不同词)或关系检索既有记忆时。 + +## 工具(11 个) + +| 工具 | 用途 | +|------|------| +| `memory_write` / `memory_write_batch` | 写入一/多条记忆(fact / preference / episode / observation / skill) | +| `memory_query` | 语义(配置向量索引)/关键词召回,可按类型与过滤 | +| `memory_search` | 关键词检索,可选一跳关系 | +| `memory_profile` | 用户偏好与属性画像 | +| `memory_skills` | 技能列表(按分类/关键词/标签) | +| `memory_history` | 会话历史 | +| `memory_graph` | 从某节点遍历关系图 | +| `memory_import` | 经适配器导入异构来源(kv / markdown / …) | +| `memory_status` | 设备/网络/计数/状态哈希/开关 | +| `memory_sync` | 连接对端并等待一次同步(需 network.enabled) | + +## 工作流 + +1. **先查后写**:写入前用 `memory_query`/`memory_search` 查重。 +2. **选对类型**:稳定事实→`fact`;用户偏好→`preference`;会话/任务过程→`episode`;可复用步骤→`skill`;观察→`observation`。 +3. **标注时效**:有有效期的用 `metadata.expiresAt`;过期事实不当现状。 +4. **表达关系**:`metadata.relatedTo` 精确关联已存在节点。 +5. **诚实回答**:召回为空就如实说明,不要臆造记忆。 + +完整行为规约见 `MEMORY_POLICY.md`。 + +## 接入 + +本 Skill 目录附各客户端 MCP 接入片段(见 `mcp/`);`scripts/install.mjs` 可将本 Skill 安装到常见 Skill 目录。 diff --git a/packages/skill/mcp/claude.json b/packages/skill/mcp/claude.json new file mode 100644 index 0000000..c6931bc --- /dev/null +++ b/packages/skill/mcp/claude.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "mebular": { + "command": "mebular", + "args": ["mcp"] + } + } +} diff --git a/packages/skill/mcp/cursor.json b/packages/skill/mcp/cursor.json new file mode 100644 index 0000000..c6931bc --- /dev/null +++ b/packages/skill/mcp/cursor.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "mebular": { + "command": "mebular", + "args": ["mcp"] + } + } +} diff --git a/packages/skill/mcp/dsh.cordis.yml b/packages/skill/mcp/dsh.cordis.yml new file mode 100644 index 0000000..a6a38c7 --- /dev/null +++ b/packages/skill/mcp/dsh.cordis.yml @@ -0,0 +1,13 @@ +# DeepSeek Harness(Cordis)— 只桥接 Tools(不桥接 prompts) +# 行为规约以 SKILL.md + MEMORY_POLICY.md 为主通道。 +plugins: + - name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: mebular + transport: stdio + command: mebular + args: ['mcp'] + +# 远程(Streamable HTTP)用: +# transport: streamable-http +# url: http://127.0.0.1:7331/mcp diff --git a/packages/skill/mcp/generic.json b/packages/skill/mcp/generic.json new file mode 100644 index 0000000..c6931bc --- /dev/null +++ b/packages/skill/mcp/generic.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "mebular": { + "command": "mebular", + "args": ["mcp"] + } + } +} diff --git a/packages/skill/mcp/opencode.json b/packages/skill/mcp/opencode.json new file mode 100644 index 0000000..31a5a5d --- /dev/null +++ b/packages/skill/mcp/opencode.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "mebular": { + "type": "local", + "command": ["mebular", "mcp"] + } + } +} diff --git a/packages/skill/package.json b/packages/skill/package.json new file mode 100644 index 0000000..eb1cb47 --- /dev/null +++ b/packages/skill/package.json @@ -0,0 +1,20 @@ +{ + "name": "@mebular/skill", + "version": "0.1.0", + "description": "Mebular 行为层:可移植的 SKILL.md + MEMORY_POLICY.md 与各 MCP client 接入片段", + "license": "MIT", + "private": false, + "type": "module", + "files": [ + "SKILL.md", + "MEMORY_POLICY.md", + "mcp", + "scripts" + ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/skill/scripts/install.mjs b/packages/skill/scripts/install.mjs new file mode 100644 index 0000000..216dc08 --- /dev/null +++ b/packages/skill/scripts/install.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node +// 安装 Mebular Skill 到常见 Skill 目录(G6.4)。 +// +// 用法: +// node scripts/install.mjs # 探测并安装到 cwd 的 .agents/skills、.dsh/skills(及存在的 .opencode/skills) +// node scripts/install.mjs --global # 另装到 ~/.agents/skills +// node scripts/install.mjs --target