Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
99 changes: 97 additions & 2 deletions packages/mcp/bin/mebular.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 ?? '(空)'}`);
Expand Down
38 changes: 38 additions & 0 deletions packages/skill/MEMORY_POLICY.md
Original file line number Diff line number Diff line change
@@ -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. 无结果别编

召回为空就如实说明「没有相关记忆」,不要凭推测填充。宁可承认缺失,也不写入或返回未经验证的内容。
44 changes: 44 additions & 0 deletions packages/skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 目录。
8 changes: 8 additions & 0 deletions packages/skill/mcp/claude.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mebular": {
"command": "mebular",
"args": ["mcp"]
}
}
}
8 changes: 8 additions & 0 deletions packages/skill/mcp/cursor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mebular": {
"command": "mebular",
"args": ["mcp"]
}
}
}
13 changes: 13 additions & 0 deletions packages/skill/mcp/dsh.cordis.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions packages/skill/mcp/generic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mebular": {
"command": "mebular",
"args": ["mcp"]
}
}
}
9 changes: 9 additions & 0 deletions packages/skill/mcp/opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"mebular": {
"type": "local",
"command": ["mebular", "mcp"]
}
}
}
20 changes: 20 additions & 0 deletions packages/skill/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
61 changes: 61 additions & 0 deletions packages/skill/scripts/install.mjs
Original file line number Diff line number Diff line change
@@ -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 <dir> # 只装到指定目录(<dir>/mebular-memory,测试/自定义用)

import { existsSync } from 'node:fs';
import { cp, mkdir } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
const SKILL_NAME = 'mebular-memory';

function parseFlags(argv) {
const flags = {};
for (let i = 0; i < argv.length; i++) {
const token = argv[i];
if (token.startsWith('--')) {
const key = token.slice(2);
const next = argv[i + 1];
if (next === undefined || next.startsWith('--')) flags[key] = true;
else {
flags[key] = next;
i++;
}
}
}
return flags;
}

const flags = parseFlags(process.argv.slice(2));
const cwd = process.cwd();
const targets = [];
if (typeof flags.target === 'string') {
targets.push(flags.target);
} else {
targets.push(join(cwd, '.agents', 'skills'));
targets.push(join(cwd, '.dsh', 'skills'));
if (existsSync(join(cwd, '.opencode'))) targets.push(join(cwd, '.opencode', 'skills'));
if (flags.global === true) targets.push(join(homedir(), '.agents', 'skills'));
}

const installed = [];
for (const target of targets) {
const dest = join(target, SKILL_NAME);
await mkdir(dest, { recursive: true });
await cp(join(pkgRoot, 'SKILL.md'), join(dest, 'SKILL.md'));
await cp(join(pkgRoot, 'MEMORY_POLICY.md'), join(dest, 'MEMORY_POLICY.md'));
installed.push(dest);
console.log(`installed ${dest}`);
}

if (installed.length === 0) {
console.error('没有可安装的目标目录');
process.exit(1);
}
console.log(JSON.stringify({ skill: SKILL_NAME, installed }, null, 2));
Loading
Loading