@yottameta/yotta-memory 0.1.0
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/LICENSE +21 -0
- package/README.md +74 -0
- package/SKILL.md +51 -0
- package/bin/install.js +163 -0
- package/bin/yotta-memory.js +331 -0
- package/install.sh +132 -0
- package/package.json +35 -0
- package/references/protocol.md +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YottaMeta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# yotta-memory
|
|
2
|
+
|
|
3
|
+
> 文件式跨智能体记忆协议与零依赖读写工具:让任何 AI 智能体活过会话。
|
|
4
|
+
|
|
5
|
+
## 这是什么
|
|
6
|
+
|
|
7
|
+
- **文件式记忆标准**:记忆 = Markdown + YAML frontmatter 文件,git 可版本化,任何智能体可读,数据主权在用户本地。
|
|
8
|
+
- **零依赖**:无 daemon / 无数据库 / 无向量库,Node.js 自带即可运行。
|
|
9
|
+
- **类型体系**:FACT(事实,公共共享)/ PREF(偏好)/ BOUND(边界)/ COMMIT(承诺)——后三者私密、per-agent 隔离。
|
|
10
|
+
- **双级存储**:用户级 `~/.yottamemory/` + 项目级 `.yottamemory/`。
|
|
11
|
+
|
|
12
|
+
适用场景:跨会话续测、个人偏好与边界沉淀、多智能体协作、项目交接、收工归档。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
三种方式任选其一,技能文件统一从 **npm** 获取(GitHub 无代理时较慢,npm 可配国内镜像加速)。
|
|
17
|
+
|
|
18
|
+
### 方式一:npx skills(推荐,生态标准入口)
|
|
19
|
+
```bash
|
|
20
|
+
npx skills add YottaMeta/yotta-memory
|
|
21
|
+
```
|
|
22
|
+
> 自动安装到已检测到的智能体(Claude Code / Codex / Cursor / OpenCode 等 78+ 智能体)。
|
|
23
|
+
|
|
24
|
+
### 方式二:npm 直接安装(CLI + 技能)
|
|
25
|
+
```bash
|
|
26
|
+
# 国内加速(可选):npm config set registry https://registry.npmmirror.com
|
|
27
|
+
npm install -g @yottameta/yotta-memory
|
|
28
|
+
yotta-memory init # 初始化记忆库
|
|
29
|
+
# 若要把技能文件装进某个智能体的 skills 目录:
|
|
30
|
+
npx -y @yottameta/yotta-memory-install -g
|
|
31
|
+
npx -y @yottameta/yotta-memory-install --agent codex
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 方式三:install.sh / 手动复制
|
|
35
|
+
获取技能文件夹后(`npm pack` 解包或 `git clone`),进入技能文件夹:
|
|
36
|
+
```bash
|
|
37
|
+
bash install.sh -g # 用户级;bash install.sh --list 查看全部目录
|
|
38
|
+
bash install.sh --agent codex # 指定智能体
|
|
39
|
+
bash install.sh --dir /path/to/skills
|
|
40
|
+
```
|
|
41
|
+
也可把整个 `yotta-memory` 文件夹复制到目标智能体的 skills 目录(常见位置见 install.sh --list)。
|
|
42
|
+
|
|
43
|
+
## CLI 用法
|
|
44
|
+
|
|
45
|
+
| 命令 | 作用 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `yotta-memory init [--project]` | 初始化记忆库(默认用户级 `~/.yottamemory/`)|
|
|
48
|
+
| `yotta-memory remember <type> <subject> <statement>` | 写入记忆(同 subject+statement 自动更新)|
|
|
49
|
+
| `yotta-memory recall [关键词] [--type T] [--limit N]` | 检索记忆(项目级优先)|
|
|
50
|
+
| `yotta-memory forget <文件>` | 删除一条记忆 |
|
|
51
|
+
| `yotta-memory archive [--days 180]` | 归档旧记忆(immutable 除外)|
|
|
52
|
+
| `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
|
|
53
|
+
|
|
54
|
+
类型:`FACT`(事实,公共共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺)。
|
|
55
|
+
|
|
56
|
+
示例:
|
|
57
|
+
```bash
|
|
58
|
+
yotta-memory init
|
|
59
|
+
yotta-memory remember PREF 用户 偏好短回复,不要用表情
|
|
60
|
+
yotta-memory recall 偏好
|
|
61
|
+
yotta-memory recall --type FACT --limit 10
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 智能体接入后怎么用
|
|
65
|
+
|
|
66
|
+
把技能装进智能体后,SKILL.md 会自动教会智能体:开工 `recall` 恢复上下文 → 重要信息 `remember` 落盘 → 收工归档。也可在对话中直接说「记住 XXX」「上次说到哪了」。
|
|
67
|
+
|
|
68
|
+
## 开发与校验
|
|
69
|
+
|
|
70
|
+
本项目内运行:`python tools/validate-skill.py yotta-memory`。
|
|
71
|
+
|
|
72
|
+
## 许可证
|
|
73
|
+
|
|
74
|
+
MIT © YottaMeta —— 详见 [LICENSE](./LICENSE)。
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yotta-memory
|
|
3
|
+
description: "文件式跨智能体记忆协议与零依赖读写工具。让任何 AI 智能体活过会话:开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档"
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
license: MIT
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# yotta-memory — 文件式跨智能体记忆
|
|
9
|
+
|
|
10
|
+
> 一句话:定义记忆协议 + 提供零依赖读写 CLI,让任何 AI 智能体活过会话。
|
|
11
|
+
> 版本:0.1.0 | 最后更新:2026-08-23
|
|
12
|
+
|
|
13
|
+
## 这是什么
|
|
14
|
+
|
|
15
|
+
- **文件式记忆标准**:记忆 = Markdown + YAML frontmatter 文件,git 可版本化,任何智能体可读,数据主权在用户本地。
|
|
16
|
+
- **零依赖**:无 daemon / 无数据库 / 无向量库,Node.js 自带即可运行。
|
|
17
|
+
- **类型体系**:FACT(事实,公共共享)/ PREF(偏好,私密)/ BOUND(边界,私密)/ COMMIT(承诺,私密)。
|
|
18
|
+
- **双级存储**:用户级 `~/.yottamemory/`(跨项目)+ 项目级 `.yottamemory/`(随项目共享)。
|
|
19
|
+
|
|
20
|
+
## 何时使用(触发)
|
|
21
|
+
|
|
22
|
+
- 用户说「记住」「别忘了」「记一笔」等保存类指令。
|
|
23
|
+
- 会话开始需要恢复上下文(跨会话 / 跨项目 / 续测)。
|
|
24
|
+
- 收工时需要留交接与归档。
|
|
25
|
+
- 多智能体协作时,公共事实进 FACT,个人偏好 / 边界 / 承诺进各自私密区。
|
|
26
|
+
|
|
27
|
+
## 核心流程
|
|
28
|
+
|
|
29
|
+
1. **开工定向**:运行 `yotta-memory recall`(可带关键词)恢复上下文;项目级记忆优先,其次用户级。
|
|
30
|
+
2. **进行中落盘**:重要信息立即 `yotta-memory remember <type> <subject> <statement>`,不攒到收工。
|
|
31
|
+
3. **收工归档**:写会话小结(COMMIT / 笔记),旧记录定期 `yotta-memory archive`。
|
|
32
|
+
4. **多智能体纪律**:FACT 写入公共区,PREF / BOUND / COMMIT 只写本智能体私密区;不读取其他智能体私密区。
|
|
33
|
+
|
|
34
|
+
## CLI 速查
|
|
35
|
+
|
|
36
|
+
| 命令 | 作用 |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `yotta-memory init [--project]` | 初始化记忆库(默认用户级)|
|
|
39
|
+
| `yotta-memory remember <type> <subject> <statement>` | 写入(同 subject+statement 自动更新)|
|
|
40
|
+
| `yotta-memory recall [关键词] [--type T] [--limit N]` | 检索(项目级优先)|
|
|
41
|
+
| `yotta-memory forget <文件>` | 删除 |
|
|
42
|
+
| `yotta-memory archive [--days 180]` | 归档旧记忆(immutable 除外)|
|
|
43
|
+
| `yotta-memory export [--out f.json]` / `import <f.json>` | 导出 / 导入 |
|
|
44
|
+
|
|
45
|
+
## 存储格式(摘要)
|
|
46
|
+
|
|
47
|
+
`<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable`;正文为记忆内容。
|
|
48
|
+
|
|
49
|
+
## 渐进披露
|
|
50
|
+
|
|
51
|
+
- 协议细节、目录结构与类型规则见 `references/protocol.md`,需要时读取,不要每次全读。
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yotta-memory 跨平台安装器(YottaSkills)
|
|
4
|
+
* 用法:
|
|
5
|
+
* npx -y @yottameta/yotta-memory --agent <name> # 按智能体默认用户级目录安装(推荐)
|
|
6
|
+
* npx -y @yottameta/yotta-memory --dir PATH # 装到指定目录(用户改了目录的智能体)
|
|
7
|
+
* npx -y @yottameta/yotta-memory -g # 安装到全部已知智能体用户级目录
|
|
8
|
+
* npx -y @yottameta/yotta-memory # 安装到检测到的项目级目录
|
|
9
|
+
* npx -y @yottameta/yotta-memory --list # 列出智能体 -> 默认目录
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
const SKILL_NAME = 'yotta-memory';
|
|
17
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
// 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
|
|
20
|
+
// 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
|
|
21
|
+
// Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
|
|
22
|
+
const AGENT_DIRS = {
|
|
23
|
+
claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
|
|
24
|
+
cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
|
|
25
|
+
codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
|
|
26
|
+
gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
|
|
27
|
+
goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
|
|
28
|
+
amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
|
|
29
|
+
opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
|
|
30
|
+
windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
|
|
31
|
+
workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
|
|
32
|
+
kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
|
|
33
|
+
trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
|
|
34
|
+
'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
|
|
35
|
+
qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
|
|
36
|
+
comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
|
|
37
|
+
codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
|
|
38
|
+
kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
|
|
39
|
+
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
|
|
43
|
+
function codexUserDir() {
|
|
44
|
+
const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
45
|
+
return path.join(base, 'skills');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
|
|
49
|
+
function opencodeUserDir() {
|
|
50
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
51
|
+
return path.join(base, 'opencode', 'skills');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveUserDir(rel) {
|
|
55
|
+
if (rel === '.codex/skills') return codexUserDir();
|
|
56
|
+
if (rel === '.config/opencode/skills') return opencodeUserDir();
|
|
57
|
+
return path.join(os.homedir(), rel);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installTo(dest) {
|
|
61
|
+
const target = path.join(dest, SKILL_NAME);
|
|
62
|
+
fs.mkdirSync(target, { recursive: true });
|
|
63
|
+
copyDir(PKG_ROOT, target, new Set(['package.json', 'bin', 'node_modules', '.git']));
|
|
64
|
+
console.log('installed -> ' + target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyDir(src, dst, skip) {
|
|
68
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
69
|
+
if (skip.has(entry.name)) continue;
|
|
70
|
+
const s = path.join(src, entry.name);
|
|
71
|
+
const d = path.join(dst, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
fs.mkdirSync(d, { recursive: true });
|
|
74
|
+
copyDir(s, d, skip);
|
|
75
|
+
} else if (entry.isFile()) {
|
|
76
|
+
fs.copyFileSync(s, d);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function displayDir(rel) {
|
|
82
|
+
if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
|
|
83
|
+
return '~/' + rel;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function main() {
|
|
87
|
+
const args = process.argv.slice(2);
|
|
88
|
+
const isGlobal = args.includes('-g') || args.includes('--global');
|
|
89
|
+
const list = args.includes('--list') || args.includes('-l');
|
|
90
|
+
let explicitDir = null;
|
|
91
|
+
const di = args.indexOf('--dir');
|
|
92
|
+
if (di !== -1 && args[di + 1]) explicitDir = args[di + 1];
|
|
93
|
+
let agent = null;
|
|
94
|
+
const ai = args.indexOf('--agent');
|
|
95
|
+
if (ai !== -1 && args[ai + 1]) agent = String(args[ai + 1]).toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (list) {
|
|
98
|
+
console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
|
|
99
|
+
for (const [key, v] of Object.entries(AGENT_DIRS)) {
|
|
100
|
+
const resolved = v.dirs.map(displayDir);
|
|
101
|
+
console.log(' ' + key.padEnd(10) + v.label.padEnd(18) + resolved.join('、'));
|
|
102
|
+
}
|
|
103
|
+
console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
|
|
104
|
+
console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (explicitDir) { installTo(explicitDir); return; }
|
|
109
|
+
|
|
110
|
+
if (agent) {
|
|
111
|
+
const info = AGENT_DIRS[agent];
|
|
112
|
+
if (!info) {
|
|
113
|
+
console.log('未收录智能体: ' + agent + '。请用 --dir <路径> 指定技能目录。');
|
|
114
|
+
console.log('可用: ' + Object.keys(AGENT_DIRS).join(', '));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
installTo(resolveUserDir(info.dirs[0]));
|
|
118
|
+
console.log('完成。');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isGlobal) {
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const v of Object.values(AGENT_DIRS)) {
|
|
125
|
+
for (const d of v.dirs) {
|
|
126
|
+
if (seen.has(d)) continue;
|
|
127
|
+
seen.add(d);
|
|
128
|
+
installTo(resolveUserDir(d));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
console.log('完成。');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const PROJECT_DIRS = [
|
|
136
|
+
'.claude/skills',
|
|
137
|
+
'.cursor/skills',
|
|
138
|
+
'.codex/skills',
|
|
139
|
+
'.config/goose/skills',
|
|
140
|
+
'.config/agents/skills',
|
|
141
|
+
'.opencode/skills',
|
|
142
|
+
'.codeium/windsurf/skills',
|
|
143
|
+
'.workbuddy/skills',
|
|
144
|
+
'.kiro/skills',
|
|
145
|
+
'.traecli/skills',
|
|
146
|
+
'.gemini/skills',
|
|
147
|
+
'.trae-cn/skills',
|
|
148
|
+
'.qwen/skills',
|
|
149
|
+
'.comate/skills',
|
|
150
|
+
'.codebuddy/skills',
|
|
151
|
+
'.kimi/skills',
|
|
152
|
+
'.agents/skills',
|
|
153
|
+
];
|
|
154
|
+
let installedAny = false;
|
|
155
|
+
for (const d of PROJECT_DIRS) {
|
|
156
|
+
if (fs.existsSync(d)) { installTo(d); installedAny = true; }
|
|
157
|
+
}
|
|
158
|
+
if (!installedAny) {
|
|
159
|
+
console.log('未检测到项目级智能体目录。可手动复制,或用 --agent <name> / -g 装到用户级。');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
main();
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// yotta-memory: 文件式跨智能体记忆 CLI(零依赖)
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
|
|
11
|
+
const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
|
|
12
|
+
const ARCHIVE_DIR = '.archive';
|
|
13
|
+
|
|
14
|
+
function userRoot() {
|
|
15
|
+
return process.env.YOTTA_MEMORY_HOME || path.join(os.homedir(), '.yottamemory');
|
|
16
|
+
}
|
|
17
|
+
function projectRoot() {
|
|
18
|
+
return path.join(process.cwd(), '.yottamemory');
|
|
19
|
+
}
|
|
20
|
+
function today() {
|
|
21
|
+
const d = new Date();
|
|
22
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
23
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
24
|
+
}
|
|
25
|
+
function typeDir(type) {
|
|
26
|
+
const t = String(type).toUpperCase();
|
|
27
|
+
if (!TYPE_DIRS[t]) {
|
|
28
|
+
console.error('未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
return TYPE_DIRS[t];
|
|
32
|
+
}
|
|
33
|
+
function parseFrontmatter(text) {
|
|
34
|
+
const m = text.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
35
|
+
if (!m) return { meta: {}, body: text };
|
|
36
|
+
const meta = {};
|
|
37
|
+
for (const line of m[1].split('\n')) {
|
|
38
|
+
const i = line.indexOf(':');
|
|
39
|
+
if (i === -1) continue;
|
|
40
|
+
let k = line.slice(0, i).trim();
|
|
41
|
+
let v = line.slice(i + 1).trim();
|
|
42
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
43
|
+
v = v.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
meta[k] = v;
|
|
46
|
+
}
|
|
47
|
+
return { meta: meta, body: text.slice(m[0][0].length + m[0].length) };
|
|
48
|
+
}
|
|
49
|
+
function escapeYaml(v) {
|
|
50
|
+
return String(v).replace(/\n/g, ' ').replace(/"/g, '\\"');
|
|
51
|
+
}
|
|
52
|
+
function ensureInit(root) {
|
|
53
|
+
fs.mkdirSync(path.join(root, 'facts'), { recursive: true });
|
|
54
|
+
fs.mkdirSync(path.join(root, 'prefs'), { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.join(root, 'bounds'), { recursive: true });
|
|
56
|
+
fs.mkdirSync(path.join(root, 'commits'), { recursive: true });
|
|
57
|
+
fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
|
|
58
|
+
const readme = path.join(root, 'README.md');
|
|
59
|
+
if (!fs.existsSync(readme)) {
|
|
60
|
+
fs.writeFileSync(readme, '# yotta-memory 记忆库\n\n文件式跨智能体记忆协议存储目录。结构:facts/ prefs/ bounds/ commits/ .archive/。\n', 'utf8');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function nextSeq(dir) {
|
|
64
|
+
let max = 0;
|
|
65
|
+
if (fs.existsSync(dir)) {
|
|
66
|
+
for (const f of fs.readdirSync(dir)) {
|
|
67
|
+
const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md$/);
|
|
68
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return String(max + 1).padStart(4, '0');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cmdInit(opts) {
|
|
75
|
+
const root = opts.project ? projectRoot() : userRoot();
|
|
76
|
+
ensureInit(root);
|
|
77
|
+
console.log('已初始化记忆库: ' + root);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cmdRemember(type, subject, statement) {
|
|
81
|
+
const root = userRoot();
|
|
82
|
+
ensureInit(root);
|
|
83
|
+
const t = String(type).toUpperCase();
|
|
84
|
+
const dir = path.join(root, typeDir(t));
|
|
85
|
+
const stmt = String(statement || '').trim();
|
|
86
|
+
const subj = String(subject || '').trim();
|
|
87
|
+
if (!stmt) { console.error('statement 不能为空'); process.exit(2); }
|
|
88
|
+
if (!subj) { console.error('subject 不能为空'); process.exit(2); }
|
|
89
|
+
if (fs.existsSync(dir)) {
|
|
90
|
+
for (const f of fs.readdirSync(dir)) {
|
|
91
|
+
const fp = path.join(dir, f);
|
|
92
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
93
|
+
const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
|
|
94
|
+
const meta = parsed.meta;
|
|
95
|
+
if ((meta.type || '').toUpperCase() === t && meta.subject === subj && meta.statement === stmt) {
|
|
96
|
+
const txt = fs.readFileSync(fp, 'utf8');
|
|
97
|
+
fs.writeFileSync(fp, txt.replace(/^updated:.*$/m, 'updated: ' + today()), 'utf8');
|
|
98
|
+
console.log('已更新: ' + fp);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const seq = nextSeq(dir);
|
|
104
|
+
const file = path.join(dir, today() + '-' + seq + '.md');
|
|
105
|
+
const content = [
|
|
106
|
+
'---',
|
|
107
|
+
'type: ' + t,
|
|
108
|
+
'subject: "' + escapeYaml(subj) + '"',
|
|
109
|
+
'statement: "' + escapeYaml(stmt) + '"',
|
|
110
|
+
'confidence: 1.0',
|
|
111
|
+
'created: ' + today(),
|
|
112
|
+
'updated: ' + today(),
|
|
113
|
+
'tags: []',
|
|
114
|
+
'immutable: false',
|
|
115
|
+
'---',
|
|
116
|
+
'',
|
|
117
|
+
stmt,
|
|
118
|
+
'',
|
|
119
|
+
].join('\n');
|
|
120
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
121
|
+
console.log('已记录: ' + file);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cmdRecall(query, opts) {
|
|
125
|
+
const roots = [projectRoot(), userRoot()].filter((r) => fs.existsSync(r));
|
|
126
|
+
if (!roots.length) {
|
|
127
|
+
console.log('记忆库不存在,请先运行: yotta-memory init');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const limit = opts.limit || 50;
|
|
131
|
+
const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
|
|
132
|
+
const q = query ? String(query).toLowerCase() : null;
|
|
133
|
+
const hits = [];
|
|
134
|
+
for (const root of roots) {
|
|
135
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
136
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
137
|
+
if (!fs.existsSync(dir)) continue;
|
|
138
|
+
for (const f of fs.readdirSync(dir)) {
|
|
139
|
+
const fp = path.join(dir, f);
|
|
140
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
141
|
+
const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
|
|
142
|
+
if (onlyType && (meta.type || '').toUpperCase() !== onlyType) continue;
|
|
143
|
+
if (q) {
|
|
144
|
+
const hay = ((meta.subject || '') + ' ' + (meta.statement || '') + ' ' + (meta.tags || '')).toLowerCase();
|
|
145
|
+
if (hay.indexOf(q) === -1) continue;
|
|
146
|
+
}
|
|
147
|
+
hits.push({ type: meta.type || t, subject: meta.subject || '', statement: meta.statement || '', file: fp, created: meta.created || '' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
hits.sort(function (a, b) {
|
|
152
|
+
const pa = a.file.indexOf(projectRoot()) === 0 ? 0 : 1;
|
|
153
|
+
const pb = b.file.indexOf(projectRoot()) === 0 ? 0 : 1;
|
|
154
|
+
if (pa !== pb) return pa - pb;
|
|
155
|
+
return String(b.created).localeCompare(String(a.created));
|
|
156
|
+
});
|
|
157
|
+
const shown = hits.slice(0, limit);
|
|
158
|
+
if (!shown.length) { console.log('无匹配记忆。'); return; }
|
|
159
|
+
console.log('共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):');
|
|
160
|
+
for (const h of shown) {
|
|
161
|
+
console.log('[' + h.type + '] ' + h.subject + ': ' + h.statement);
|
|
162
|
+
console.log(' ' + h.file);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cmdForget(fileRef) {
|
|
167
|
+
const roots = [projectRoot(), userRoot()].filter((r) => fs.existsSync(r));
|
|
168
|
+
let target = null;
|
|
169
|
+
for (const root of roots) {
|
|
170
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
171
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
172
|
+
if (!fs.existsSync(dir)) continue;
|
|
173
|
+
const cand = path.join(dir, fileRef);
|
|
174
|
+
if (fs.existsSync(cand)) { target = cand; break; }
|
|
175
|
+
if (fs.existsSync(path.join(dir, path.basename(fileRef)))) { target = path.join(dir, path.basename(fileRef)); break; }
|
|
176
|
+
}
|
|
177
|
+
if (target) break;
|
|
178
|
+
}
|
|
179
|
+
if (!target) { console.error('未找到记忆文件: ' + fileRef); process.exit(2); }
|
|
180
|
+
fs.unlinkSync(target);
|
|
181
|
+
console.log('已删除: ' + target);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cmdArchive(opts) {
|
|
185
|
+
const days = opts.days || 180;
|
|
186
|
+
const root = userRoot();
|
|
187
|
+
if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
|
|
188
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
189
|
+
let moved = 0;
|
|
190
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
191
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
192
|
+
if (!fs.existsSync(dir)) continue;
|
|
193
|
+
for (const f of fs.readdirSync(dir)) {
|
|
194
|
+
const fp = path.join(dir, f);
|
|
195
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
196
|
+
const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
|
|
197
|
+
if (meta.immutable === 'true') continue;
|
|
198
|
+
if (!meta.created) continue;
|
|
199
|
+
const ts = new Date(meta.created).getTime();
|
|
200
|
+
if (isNaN(ts)) continue;
|
|
201
|
+
if (ts < cutoff) {
|
|
202
|
+
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
|
|
203
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
204
|
+
fs.renameSync(fp, path.join(destDir, f));
|
|
205
|
+
moved++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
console.log('已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function collectAll(root) {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const t of Object.keys(TYPE_DIRS)) {
|
|
215
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
216
|
+
if (!fs.existsSync(dir)) continue;
|
|
217
|
+
for (const f of fs.readdirSync(dir)) {
|
|
218
|
+
const fp = path.join(dir, f);
|
|
219
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
220
|
+
out.push({ file: path.relative(root, fp), meta: parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function cmdExport(outPath) {
|
|
227
|
+
const root = userRoot();
|
|
228
|
+
if (!fs.existsSync(root)) { console.log('记忆库不存在。'); return; }
|
|
229
|
+
const data = { exported: today(), version: VERSION, memories: collectAll(root) };
|
|
230
|
+
const target = outPath || 'yottamemory-export-' + today() + '.json';
|
|
231
|
+
fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
|
|
232
|
+
console.log('已导出 ' + data.memories.length + ' 条记忆 -> ' + target);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cmdImport(src) {
|
|
236
|
+
if (!src || !fs.existsSync(src)) { console.error('请提供 JSON 文件路径'); process.exit(2); }
|
|
237
|
+
const data = JSON.parse(fs.readFileSync(src, 'utf8'));
|
|
238
|
+
if (!data.memories || !Array.isArray(data.memories)) { console.error('JSON 格式不正确(缺少 memories 数组)'); process.exit(2); }
|
|
239
|
+
const root = userRoot();
|
|
240
|
+
ensureInit(root);
|
|
241
|
+
let n = 0;
|
|
242
|
+
for (const item of data.memories) {
|
|
243
|
+
const meta = item.meta || {};
|
|
244
|
+
const t = (meta.type || 'FACT').toUpperCase();
|
|
245
|
+
if (!TYPE_DIRS[t]) continue;
|
|
246
|
+
const dir = path.join(root, TYPE_DIRS[t]);
|
|
247
|
+
let seq = nextSeq(dir);
|
|
248
|
+
let file = path.join(dir, today() + '-' + seq + '.md');
|
|
249
|
+
while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
|
|
250
|
+
const content = [
|
|
251
|
+
'---',
|
|
252
|
+
'type: ' + t,
|
|
253
|
+
'subject: "' + escapeYaml(meta.subject || '') + '"',
|
|
254
|
+
'statement: "' + escapeYaml(meta.statement || '') + '"',
|
|
255
|
+
'confidence: ' + (meta.confidence || 1.0),
|
|
256
|
+
'created: ' + (meta.created || today()),
|
|
257
|
+
'updated: ' + (meta.updated || today()),
|
|
258
|
+
'tags: ' + (meta.tags || '[]'),
|
|
259
|
+
'immutable: ' + (meta.immutable || 'false'),
|
|
260
|
+
'---',
|
|
261
|
+
'',
|
|
262
|
+
meta.statement || '',
|
|
263
|
+
'',
|
|
264
|
+
].join('\n');
|
|
265
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
266
|
+
n++;
|
|
267
|
+
}
|
|
268
|
+
console.log('已导入 ' + n + ' 条记忆 -> ' + root);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function usage() {
|
|
272
|
+
console.log([
|
|
273
|
+
'yotta-memory v' + VERSION + ' — 文件式跨智能体记忆',
|
|
274
|
+
'',
|
|
275
|
+
'用法:',
|
|
276
|
+
' yotta-memory init [--project] 初始化记忆库(默认用户级 ~/.yottamemory)',
|
|
277
|
+
' yotta-memory remember <type> <subject> <statement> 写入记忆',
|
|
278
|
+
' yotta-memory recall [关键词] [--type T] [--limit N] 检索记忆(项目级优先)',
|
|
279
|
+
' yotta-memory forget <文件或文件名> 删除一条记忆',
|
|
280
|
+
' yotta-memory archive [--days 180] 归档超过 N 天且非 immutable 的旧记忆',
|
|
281
|
+
' yotta-memory export [--out 文件.json] 导出全部记忆',
|
|
282
|
+
' yotta-memory import <文件.json> 导入记忆',
|
|
283
|
+
' yotta-memory --version 版本',
|
|
284
|
+
'',
|
|
285
|
+
'类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
|
|
286
|
+
'环境变量: YOTTA_MEMORY_HOME 可覆盖用户级记忆库目录',
|
|
287
|
+
'',
|
|
288
|
+
].join('\n'));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function main() {
|
|
292
|
+
const args = process.argv.slice(2);
|
|
293
|
+
if (!args.length) { usage(); return; }
|
|
294
|
+
const first = args[0];
|
|
295
|
+
if (first === '--version' || first === '-v') { console.log(VERSION); return; }
|
|
296
|
+
if (first === '--help' || first === '-h') { usage(); return; }
|
|
297
|
+
const opts = {};
|
|
298
|
+
const positional = [];
|
|
299
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out']);
|
|
300
|
+
for (let i = 1; i < args.length; i++) {
|
|
301
|
+
const a = args[i];
|
|
302
|
+
if (a === '--project') opts.project = true;
|
|
303
|
+
else if (valueOpts.has(a)) {
|
|
304
|
+
const v = args[++i];
|
|
305
|
+
if (a === '--type') opts.type = v;
|
|
306
|
+
else if (a === '--limit') opts.limit = parseInt(v, 10) || 50;
|
|
307
|
+
else if (a === '--days') opts.days = parseInt(v, 10) || 180;
|
|
308
|
+
else if (a === '--out') opts.out = v;
|
|
309
|
+
} else if (a.startsWith('--')) {
|
|
310
|
+
console.error('未知选项: ' + a);
|
|
311
|
+
process.exit(2);
|
|
312
|
+
} else {
|
|
313
|
+
positional.push(a);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
switch (first) {
|
|
317
|
+
case 'init': cmdInit(opts); break;
|
|
318
|
+
case 'remember': cmdRemember(positional[0], positional[1], positional[2]); break;
|
|
319
|
+
case 'recall': cmdRecall(positional[0] || null, opts); break;
|
|
320
|
+
case 'forget': cmdForget(positional[0]); break;
|
|
321
|
+
case 'archive': cmdArchive(opts); break;
|
|
322
|
+
case 'export': cmdExport(opts.out); break;
|
|
323
|
+
case 'import': cmdImport(positional[0]); break;
|
|
324
|
+
default:
|
|
325
|
+
console.error('未知命令: ' + first);
|
|
326
|
+
usage();
|
|
327
|
+
process.exit(2);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
main();
|
package/install.sh
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# yotta-memory 多智能体安装脚本(YottaSkills 模板)
|
|
3
|
+
# 用法:
|
|
4
|
+
# bash install.sh --agent <name> # 按智能体默认用户级目录安装
|
|
5
|
+
# bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
|
|
6
|
+
# bash install.sh -g # 装到全部已知智能体用户级目录
|
|
7
|
+
# bash install.sh # 检测并安装到已存在的项目级目录
|
|
8
|
+
# bash install.sh --list # 列出智能体 -> 默认目录
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SKILL_NAME="yotta-memory"
|
|
12
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
case "$(uname -s)" in
|
|
14
|
+
MINGW*|MSYS*)
|
|
15
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
# 智能体 -> 用户级默认目录(--agent 装到第一个)
|
|
20
|
+
# .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
|
|
21
|
+
# 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
|
|
22
|
+
_IS_WINDOWS=0
|
|
23
|
+
case "$(uname -s)" in
|
|
24
|
+
MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
|
|
25
|
+
esac
|
|
26
|
+
dirs_for() {
|
|
27
|
+
case "$1" in
|
|
28
|
+
claude) echo ".claude/skills" ;;
|
|
29
|
+
cursor) echo ".cursor/skills .agents/skills" ;;
|
|
30
|
+
codex) echo "__CODEX__" ;;
|
|
31
|
+
gemini) echo ".gemini/skills .agents/skills" ;;
|
|
32
|
+
goose) echo ".config/goose/skills .agents/skills" ;;
|
|
33
|
+
amp) echo ".config/agents/skills .agents/skills" ;;
|
|
34
|
+
opencode) echo "__OPENCODE__" ;;
|
|
35
|
+
windsurf) echo ".codeium/windsurf/skills" ;;
|
|
36
|
+
workbuddy) echo ".workbuddy/skills" ;;
|
|
37
|
+
kiro) echo ".kiro/skills" ;;
|
|
38
|
+
trae) echo ".traecli/skills" ;;
|
|
39
|
+
trae-cn) echo ".trae-cn/skills" ;;
|
|
40
|
+
qwen) echo ".qwen/skills" ;;
|
|
41
|
+
comate) echo ".comate/skills" ;;
|
|
42
|
+
codebuddy) echo ".codebuddy/skills" ;;
|
|
43
|
+
kimi) echo ".kimi/skills" ;;
|
|
44
|
+
agents) echo ".agents/skills" ;;
|
|
45
|
+
*) return 1 ;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
codex_dir() {
|
|
50
|
+
if [ -n "${CODEX_HOME:-}" ]; then printf '%s' "$CODEX_HOME/skills"; else printf '%s' "$HOME/.codex/skills"; fi
|
|
51
|
+
}
|
|
52
|
+
opencode_dir() {
|
|
53
|
+
if [ -n "${XDG_CONFIG_HOME:-}" ]; then printf '%s' "$XDG_CONFIG_HOME/opencode/skills"; else printf '%s' "$HOME/.config/opencode/skills"; fi
|
|
54
|
+
}
|
|
55
|
+
resolve_user() {
|
|
56
|
+
case "$1" in
|
|
57
|
+
__CODEX__) codex_dir ;;
|
|
58
|
+
__OPENCODE__) opencode_dir ;;
|
|
59
|
+
*) printf '%s' "$HOME/$1" ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
install_to() {
|
|
64
|
+
mkdir -p "$1/$SKILL_NAME"
|
|
65
|
+
cp -r "$SOURCE_DIR/." "$1/$SKILL_NAME/"
|
|
66
|
+
rm -rf "$1/$SKILL_NAME/.git"
|
|
67
|
+
echo "installed -> $1/$SKILL_NAME"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
list() {
|
|
71
|
+
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
72
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
73
|
+
local dirs first
|
|
74
|
+
dirs="$(dirs_for "$a")"
|
|
75
|
+
first="${dirs%% *}"
|
|
76
|
+
case "$first" in
|
|
77
|
+
__CODEX__) first=".codex/skills" ;;
|
|
78
|
+
__OPENCODE__) first=".config/opencode/skills" ;;
|
|
79
|
+
esac
|
|
80
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
81
|
+
first="%USERPROFILE%\\${first//\//\\}"
|
|
82
|
+
else
|
|
83
|
+
first="~/$first"
|
|
84
|
+
fi
|
|
85
|
+
printf ' %-10s %s\n' "$a" "$first"
|
|
86
|
+
done
|
|
87
|
+
echo '说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。'
|
|
88
|
+
echo '改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main() {
|
|
92
|
+
local agent="" dir="" global=0 show_list=0
|
|
93
|
+
while [ $# -gt 0 ]; do
|
|
94
|
+
case "$1" in
|
|
95
|
+
--agent) shift; agent="${1:-}" ;;
|
|
96
|
+
--dir) shift; dir="${1:-}" ;;
|
|
97
|
+
-g|--global) global=1 ;;
|
|
98
|
+
--list|-l) show_list=1 ;;
|
|
99
|
+
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
100
|
+
esac
|
|
101
|
+
shift
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
if [ "$show_list" = "1" ]; then list; return; fi
|
|
105
|
+
if [ -n "$dir" ]; then install_to "$dir"; echo "完成。"; return; fi
|
|
106
|
+
if [ -n "$agent" ]; then
|
|
107
|
+
local dirs first
|
|
108
|
+
if ! dirs="$(dirs_for "$agent")"; then
|
|
109
|
+
echo "未收录智能体: $agent。请用 --dir <路径> 指定技能目录。" >&2; exit 2
|
|
110
|
+
fi
|
|
111
|
+
first="${dirs%% *}"
|
|
112
|
+
install_to "$(resolve_user "$first")"; echo "完成。"; return
|
|
113
|
+
fi
|
|
114
|
+
if [ "$global" = "1" ]; then
|
|
115
|
+
echo "安装到全部已知智能体用户级目录..."
|
|
116
|
+
local dirs rel
|
|
117
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
118
|
+
dirs="$(dirs_for "$a")"
|
|
119
|
+
for rel in $dirs; do install_to "$(resolve_user "$rel")"; done
|
|
120
|
+
done
|
|
121
|
+
echo "完成。"; return
|
|
122
|
+
fi
|
|
123
|
+
local installed=0 d
|
|
124
|
+
for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
|
|
125
|
+
if [ -d "$d" ]; then install_to "$d"; installed=1; fi
|
|
126
|
+
done
|
|
127
|
+
if [ "$installed" = "0" ]; then
|
|
128
|
+
echo "未检测到项目级智能体目录。可用 --agent <name> / -g 装到用户级,或 --dir 指定。"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yottameta/yotta-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "文件式跨智能体记忆协议与零依赖读写工具:让任何 AI 智能体活过会话(FACT/PREF/BOUND/COMMIT 四分类,用户级+项目级双级存储)。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skills",
|
|
8
|
+
"yotta-memory",
|
|
9
|
+
"memory",
|
|
10
|
+
"persistent-memory",
|
|
11
|
+
"cross-agent",
|
|
12
|
+
"session-continuity"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"SKILL.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md",
|
|
18
|
+
"install.sh",
|
|
19
|
+
"references",
|
|
20
|
+
"scripts",
|
|
21
|
+
"assets",
|
|
22
|
+
"bin"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/YottaMeta/yotta-memory.git"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"yotta-memory": "bin/yotta-memory.js",
|
|
33
|
+
"yotta-memory-install": "bin/install.js"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# yotta-memory 协议规范 v0.1.0
|
|
2
|
+
|
|
3
|
+
> 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
|
|
4
|
+
> 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
|
|
5
|
+
|
|
6
|
+
## 1. 存储位置
|
|
7
|
+
|
|
8
|
+
| 级别 | 默认路径 | 覆盖方式 | 用途 |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| 用户级 | `~/.yottamemory/` | 环境变量 `YOTTA_MEMORY_HOME` | 跨项目个人记忆 |
|
|
11
|
+
| 项目级 | `<repo>/.yottamemory/` | `yotta-memory init --project` | 随项目提交,团队共享 |
|
|
12
|
+
|
|
13
|
+
- recall 时项目级优先,其次用户级;写入默认用户级(`remember` 不指定级别)。
|
|
14
|
+
- 项目级记忆随 git 提交,适合团队共享的 FACT 与项目级 BOUND。
|
|
15
|
+
|
|
16
|
+
## 2. 目录结构
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
<root>/
|
|
20
|
+
├── facts/ # FACT 事实(公共可共享)
|
|
21
|
+
├── prefs/ # PREF 偏好(私密)
|
|
22
|
+
├── bounds/ # BOUND 边界(私密)
|
|
23
|
+
├── commits/ # COMMIT 承诺(私密)
|
|
24
|
+
├── .archive/ # 归档区(archive 命令移入)
|
|
25
|
+
└── README.md # 记忆库说明
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 3. 文件格式
|
|
29
|
+
|
|
30
|
+
每个记忆一条独立 `.md` 文件,文件名 `<YYYY-MM-DD>-<NNNN>.md`(NNNN 为当日序号):
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
---
|
|
34
|
+
type: FACT
|
|
35
|
+
subject: "用户"
|
|
36
|
+
statement: "用户偏好短回复"
|
|
37
|
+
confidence: 1.0
|
|
38
|
+
created: 2026-08-23
|
|
39
|
+
updated: 2026-08-23
|
|
40
|
+
tags: []
|
|
41
|
+
immutable: false
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
用户偏好短回复
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### frontmatter 字段
|
|
48
|
+
|
|
49
|
+
| 字段 | 必填 | 说明 |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `type` | 是 | `FACT` / `PREF` / `BOUND` / `COMMIT` |
|
|
52
|
+
| `subject` | 是 | 记忆主体(人 / 项目 / 系统)|
|
|
53
|
+
| `statement` | 是 | 记忆内容 |
|
|
54
|
+
| `confidence` | 否 | 置信度 0-1,默认 1.0 |
|
|
55
|
+
| `created` | 是 | 创建日期 `YYYY-MM-DD` |
|
|
56
|
+
| `updated` | 是 | 最近更新日期 |
|
|
57
|
+
| `tags` | 否 | 标签数组 |
|
|
58
|
+
| `immutable` | 否 | `true` 时 archive 不移动(用户级硬事实)|
|
|
59
|
+
|
|
60
|
+
## 4. 类型体系
|
|
61
|
+
|
|
62
|
+
| 类型 | 含义 | 可见性 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `FACT` | 客观事实 / 知识 / 经验 | 公共,可共享给所有智能体 |
|
|
65
|
+
| `PREF` | 用户偏好 / 习惯 | 私密,per-agent 隔离 |
|
|
66
|
+
| `BOUND` | 边界 / 规则 / 底线 | 私密,不可违反 |
|
|
67
|
+
| `COMMIT` | 承诺 / 约定 | 私密 |
|
|
68
|
+
|
|
69
|
+
- 写入纪律:公共事实用 FACT;涉及用户偏好 / 边界 / 承诺一律用私密类型,避免公共区泄露。
|
|
70
|
+
- 多智能体协作:每个智能体只读自己的私密区(如 `~/.yottamemory/<agent-id>/prefs/` 可自行约定),公共 FACT 共享。
|
|
71
|
+
|
|
72
|
+
## 5. CLI 命令参考
|
|
73
|
+
|
|
74
|
+
| 命令 | 行为 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `init [--project]` | 创建目录结构;默认用户级,`--project` 建项目级 |
|
|
77
|
+
| `remember <type> <subject> <statement>` | 写入;同 subject+statement 已存在则只更新 `updated` |
|
|
78
|
+
| `recall [关键词] [--type T] [--limit N]` | 全文匹配 subject/statement/tags;项目级优先;默认 50 条 |
|
|
79
|
+
| `forget <文件>` | 删除(按路径或文件名)|
|
|
80
|
+
| `archive [--days 180]` | 将超过 N 天且非 immutable 的记录移入 `.archive/` |
|
|
81
|
+
| `export [--out f.json]` | 导出全部记忆为 JSON |
|
|
82
|
+
| `import <f.json>` | 从 JSON 导入(幂等)|
|
|
83
|
+
|
|
84
|
+
### 去重与更新
|
|
85
|
+
|
|
86
|
+
- remember 时若同 `type + subject + statement` 已存在,仅刷新 `updated`,不产生重复文件。
|
|
87
|
+
- 修改内容:先 `recall` 定位文件,再 `forget` + 重新 `remember`,或直接编辑文件。
|
|
88
|
+
|
|
89
|
+
## 6. 与其他系统互操作
|
|
90
|
+
|
|
91
|
+
- **git**:整个记忆库可纳入版本控制,回滚 / 审计 / 团队同步。
|
|
92
|
+
- **灵魂盘(Soul Framework)**:类型体系同源(FACT/PREF/BOUND/COMMIT);`export` 出的 JSON 可作迁移中间格式。
|
|
93
|
+
- **Agent Skills 标准**:本技能 SKILL.md 符合 agentskills.io 开放标准,支持 npx skills 安装。
|