@yottameta/yotta-memory 0.19.0 → 0.20.1

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/install.sh CHANGED
@@ -1,28 +1,30 @@
1
1
  #!/usr/bin/env bash
2
- # yotta-memory 多智能体安装脚本
2
+ # yotta-memory 多智能体安装脚本(YottaSkills)
3
3
  # 用法:
4
4
  # bash install.sh --agent <name> # 按智能体默认用户级目录安装
5
5
  # bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
6
6
  # bash install.sh -g # 装到全部已知智能体用户级目录
7
- # bash install.sh # 检测并安装到已存在的项目级目录
7
+ # bash install.sh # 检测并安装到已存在的项目级目录
8
8
  # bash install.sh --list # 列出智能体 -> 默认目录
9
+ # bash install.sh --dry-run # 只显示将写入的目标,不写文件
10
+ # bash install.sh --version # 显示版本
11
+ # bash install.sh --help # 显示帮助
12
+ # 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
9
13
  set -euo pipefail
10
14
 
11
15
  SKILL_NAME="yotta-memory"
12
16
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ _IS_WINDOWS=0
13
18
  case "$(uname -s)" in
14
19
  MINGW*|MSYS*)
15
20
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
21
+ _IS_WINDOWS=1
16
22
  ;;
23
+ CYGWIN*) _IS_WINDOWS=1 ;;
17
24
  esac
18
25
 
19
26
  # 智能体 -> 用户级默认目录(--agent 装到第一个)
20
27
  # .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
28
  dirs_for() {
27
29
  case "$1" in
28
30
  claude) echo ".claude/skills" ;;
@@ -60,36 +62,153 @@ resolve_user() {
60
62
  esac
61
63
  }
62
64
 
63
- install_to() {
64
- local base="$1"
65
- local dest
66
- case "$base" in
67
- ""|"/") echo "安装失败:拒绝不安全的目标目录:'$base'" >&2; return 1 ;;
65
+ skill_version() {
66
+ local pkg="$SOURCE_DIR/package.json" v=""
67
+ [ -f "$pkg" ] || return 0
68
+ if command -v awk >/dev/null 2>&1; then
69
+ v="$(awk -F'"' '{for (i=1;i<=NF;i++) if ($i=="version") {print $(i+2); exit}}' "$pkg" 2>/dev/null || true)"
70
+ fi
71
+ if [ -z "$v" ]; then
72
+ v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
73
+ fi
74
+ printf '%s' "$v"
75
+ }
76
+
77
+ usage() {
78
+ echo "$SKILL_NAME 安装脚本(YottaSkills)"
79
+ echo ""
80
+ echo "用法:"
81
+ echo " bash install.sh --agent <name> 按智能体默认用户级目录安装(推荐)"
82
+ echo " bash install.sh --dir <path> 装到指定目录(用户改了目录的智能体)"
83
+ echo " bash install.sh -g 安装到全部已知智能体用户级目录"
84
+ echo " bash install.sh 安装到检测到的项目级目录"
85
+ echo ""
86
+ echo "参数:"
87
+ echo " --agent <name> 智能体键名,见 --list"
88
+ echo " --dir <path> 自定义技能目录"
89
+ echo " -g, --global 安装到全部已知用户级目录"
90
+ echo " --list, -l 列出支持的智能体目录"
91
+ echo " --dry-run 只显示将写入的目标,不写文件"
92
+ echo " --version, -v 显示版本"
93
+ echo " --help, -h 显示帮助"
94
+ echo " --yes, -y 兼容参数(-g 不再强制要求)"
95
+ echo ""
96
+ echo "退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误"
97
+ }
98
+
99
+ resolve_path() {
100
+ # 与 SOURCE_DIR 同一风格(Windows Git Bash 用 pwd -W),供路径包含判断使用
101
+ local p="$1"
102
+ if [ "$_IS_WINDOWS" = "1" ]; then
103
+ (cd "$p" 2>/dev/null && pwd -W)
104
+ else
105
+ (cd "$p" 2>/dev/null && pwd -P)
106
+ fi
107
+ }
108
+
109
+ assert_safe_target() {
110
+ local dest="$1" src_real dest_real parent
111
+ src_real="$(resolve_path "$SOURCE_DIR")" || return 0
112
+ if [ -d "$dest" ]; then
113
+ dest_real="$(resolve_path "$dest")" || return 0
114
+ else
115
+ parent="$(dirname "$dest")"
116
+ dest_real="$(resolve_path "$parent")" || return 0
117
+ dest_real="$dest_real/$(basename "$dest")"
118
+ fi
119
+ case "$dest_real" in
120
+ "$src_real"|"$src_real"/*)
121
+ echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
122
+ return 1
123
+ ;;
68
124
  esac
125
+ return 0
126
+ }
127
+
128
+ # 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
129
+ # 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
130
+ DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules"
131
+ clean_residue() {
132
+ local dest="$1" name p
133
+ [ -d "$dest" ] || return 0
134
+ [ -f "$dest/SKILL.md" ] || return 0
135
+ for name in $DEV_SKIP; do
136
+ p="$dest/$name"
137
+ [ -L "$p" ] && continue
138
+ if [ -e "$p" ]; then
139
+ rm -rf -- "$p"
140
+ echo "已清理残留: $name"
141
+ fi
142
+ done
143
+ find "$dest" -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' \) -prune -exec rm -rf -- {} + 2>/dev/null || true
144
+ find "$dest" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete 2>/dev/null || true
145
+ }
146
+
147
+ # 复制技能本体:顶层跳过开发件与运行时不相关目录(嵌套同名保留);
148
+ # 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
149
+ copy_tree() {
150
+ local src="$1" dst="$2" top="$3" entry base
151
+ mkdir -p "$dst"
152
+ for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
153
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
154
+ base="$(basename "$entry")"
155
+ case "$base" in
156
+ __pycache__|.pytest_cache|.mypy_cache) continue ;;
157
+ esac
158
+ if [ "$top" = "1" ]; then
159
+ case " $DEV_SKIP " in
160
+ *" $base "*) continue ;;
161
+ esac
162
+ fi
163
+ if [ -d "$entry" ] && [ ! -L "$entry" ]; then
164
+ copy_tree "$entry" "$dst/$base" 0 || return 1
165
+ else
166
+ case "$base" in
167
+ *.pyc|*.pyo) continue ;;
168
+ esac
169
+ cp -RP "$entry" "$dst/$base" || return 1
170
+ fi
171
+ done
172
+ }
173
+
174
+ install_to() {
175
+ local base="$1" dry="$2" dest
176
+ dest="$base/$SKILL_NAME"
69
177
  if [ -L "$base" ]; then
70
- echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2; return 1
178
+ echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
179
+ return 1
180
+ fi
181
+ if [ -e "$base" ] && [ ! -d "$base" ]; then
182
+ echo "安装失败:目标路径已存在且不是目录:$base" >&2
183
+ return 1
71
184
  fi
72
- mkdir -p "$base"
73
- dest="$base/$SKILL_NAME"
74
185
  if [ -L "$dest" ]; then
75
- echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2; return 1
186
+ echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
187
+ return 1
76
188
  fi
77
189
  if [ -e "$dest" ] && [ ! -d "$dest" ]; then
78
- echo "安装失败:技能路径已存在且不是目录:$dest" >&2; return 1
79
- fi
80
- mkdir -p "$dest"
81
- cp -RP "$SOURCE_DIR/." "$dest/"
82
- # 只清理副本内部的开发残留(固定子路径),不做整目录删除、不跟随符号链接
83
- if [ -d "$dest/.git" ] && [ ! -L "$dest/.git" ]; then rm -rf "$dest/.git"; fi
84
- find "$dest" -type d -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
85
- find "$dest" -type f -name '*.pyc' -delete 2>/dev/null || true
190
+ echo "安装失败:技能路径已存在且不是目录:$dest" >&2
191
+ return 1
192
+ fi
193
+ assert_safe_target "$dest" || return 1
194
+ if [ "$dry" = "1" ]; then
195
+ echo "[dry-run] 将安装到 -> $dest"
196
+ return 0
197
+ fi
198
+ clean_residue "$dest"
199
+ mkdir -p "$dest" || return 1
200
+ copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
201
+ if [ ! -f "$dest/SKILL.md" ]; then
202
+ echo "安装失败:结果缺少 SKILL.md" >&2
203
+ return 1
204
+ fi
86
205
  echo "installed -> $dest"
87
206
  }
88
207
 
89
208
  list() {
90
209
  echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
210
+ local a dirs first
91
211
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
92
- local dirs first
93
212
  dirs="$(dirs_for "$a")"
94
213
  first="${dirs%% *}"
95
214
  case "$first" in
@@ -108,44 +227,101 @@ list() {
108
227
  }
109
228
 
110
229
  main() {
111
- local agent="" dir="" global=0 show_list=0
230
+ local agent="" dir="" global=0 show_list=0 show_help=0 show_version=0 dry=0
112
231
  while [ $# -gt 0 ]; do
113
232
  case "$1" in
114
- --agent) shift; agent="${1:-}" ;;
115
- --dir) shift; dir="${1:-}" ;;
116
- -g|--global) global=1 ;;
233
+ --help|-h) show_help=1 ;;
234
+ --version|-v) show_version=1 ;;
117
235
  --list|-l) show_list=1 ;;
236
+ -g|--global) global=1 ;;
237
+ --dry-run) dry=1 ;;
238
+ --yes|-y) : ;;
239
+ --agent)
240
+ shift
241
+ if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
242
+ echo "用法错误: --agent 需要一个非空名称" >&2
243
+ exit 2
244
+ fi
245
+ agent="$1"
246
+ ;;
247
+ --dir)
248
+ shift
249
+ if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
250
+ echo "用法错误: --dir 需要一个非空路径" >&2
251
+ exit 2
252
+ fi
253
+ dir="$1"
254
+ ;;
118
255
  *) echo "未知参数: $1" >&2; exit 2 ;;
119
256
  esac
120
257
  shift
121
258
  done
122
259
 
123
- if [ "$show_list" = "1" ]; then list; return; fi
124
- if [ -n "$dir" ]; then install_to "$dir"; echo "完成。"; return; fi
260
+ if [ "$show_help" = "1" ]; then usage; return 0; fi
261
+ if [ "$show_version" = "1" ]; then
262
+ local v
263
+ v="$(skill_version)"
264
+ if [ -z "$v" ]; then
265
+ echo "$SKILL_NAME 版本未知(未找到 package.json)" >&2
266
+ exit 1
267
+ fi
268
+ echo "$SKILL_NAME v$v"
269
+ return 0
270
+ fi
271
+ if [ "$show_list" = "1" ]; then list; return 0; fi
272
+
273
+ local selected=0
274
+ [ -n "$dir" ] && selected=$((selected + 1))
275
+ [ -n "$agent" ] && selected=$((selected + 1))
276
+ [ "$global" = "1" ] && selected=$((selected + 1))
277
+ if [ "$selected" -gt 1 ]; then
278
+ echo "用法错误: --dir / --agent / -g 只能选一个" >&2
279
+ exit 2
280
+ fi
281
+
282
+ if [ -n "$dir" ]; then
283
+ install_to "$dir" "$dry" || exit 1
284
+ [ "$dry" = "1" ] || echo "完成。"
285
+ return 0
286
+ fi
125
287
  if [ -n "$agent" ]; then
126
288
  local dirs first
127
289
  if ! dirs="$(dirs_for "$agent")"; then
128
- echo "未收录智能体: $agent。请用 --dir <路径> 指定技能目录。" >&2; exit 2
290
+ echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
291
+ exit 2
129
292
  fi
130
293
  first="${dirs%% *}"
131
- install_to "$(resolve_user "$first")"; echo "完成。"; return
294
+ install_to "$(resolve_user "$first")" "$dry" || exit 1
295
+ [ "$dry" = "1" ] || echo "完成。"
296
+ return 0
132
297
  fi
133
298
  if [ "$global" = "1" ]; then
134
- echo "安装到全部已知智能体用户级目录..."
135
- local dirs rel
299
+ local a dirs rel seen=" "
136
300
  for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
137
- dirs="$(dirs_for "$a")"
138
- for rel in $dirs; do install_to "$(resolve_user "$rel")"; done
301
+ dirs="$(dirs_for "$a")" || continue
302
+ for rel in $dirs; do
303
+ case "$seen" in
304
+ *" $rel "*) continue ;;
305
+ esac
306
+ seen="$seen$rel "
307
+ install_to "$(resolve_user "$rel")" "$dry" || exit 1
308
+ done
139
309
  done
140
- echo "完成。"; return
310
+ [ "$dry" = "1" ] || echo "完成。"
311
+ return 0
141
312
  fi
142
313
  local installed=0 d
143
314
  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
144
- if [ -d "$d" ]; then install_to "$d"; installed=1; fi
315
+ if [ -d "$d" ]; then
316
+ install_to "$d" "$dry" || exit 1
317
+ installed=1
318
+ fi
145
319
  done
146
320
  if [ "$installed" = "0" ]; then
147
- echo "未检测到项目级智能体目录。可用 --agent <name> / -g 装到用户级,或 --dir 指定。"
321
+ echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
322
+ exit 4
148
323
  fi
324
+ [ "$dry" = "1" ] || echo "完成。"
149
325
  }
150
326
 
151
327
  main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Yuanyi (元忆) — boundary-aware, file-based memory for AI agents. File-based, zero-dependency, diff/rollback-able; FACT/PREF/BOUND/COMMIT types (public shared / private isolated), user-level + project-level storage; v0.12 reliability baseline (init guard, trash-based deletion, independent-volume backup/list/doctor/restore, start-of-work doctor, transactional snapshots before destructive writes), v0.10 consolidation (periodic summaries with provenance, near-duplicate auto-merge, per-type decay, batch audit + rollback), v0.9 recall quality + context focus + optional local embedding plugin, plus v0.8 semantic search, feedback loop, self-organization and distillation.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,8 +1,13 @@
1
- # 扩展提供方协议 v1(元忆 · capability `memory.hook`)
1
+ # 扩展提供方协议 v1.1(元忆 · capability `memory.hook` / `context.paging`)
2
2
 
3
3
  元忆可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**,由它参与「哪些记忆进上下文」。
4
4
  未配置提供方时,`context` 的行为与输出与之前完全一致;任何失败都回落到普通记忆,不阻断命令。
5
5
 
6
+ 本版协议包含两个 capability:
7
+
8
+ - `memory.hook`:在装配前过滤候选集(`evict` / `selected`)。
9
+ - `context.paging`:在用户显式传 `--budget` 时,对已通过 `memory.hook` 的候选集做排序与分页(`drop` / `order`)。
10
+
6
11
  ## 1. 配置
7
12
 
8
13
  配置文件:`<YOTTA_PROVIDER_HOME>/provider.json`,默认 `~/.yottameta/provider.json`。
@@ -15,7 +20,7 @@
15
20
  {
16
21
  "id": "local-provider",
17
22
  "version": "0.1.0",
18
- "capabilities": ["memory.hook"],
23
+ "capabilities": ["memory.hook", "context.paging"],
19
24
  "command": ["node", "C:/path/to/provider.js"],
20
25
  "timeout_ms": 600
21
26
  }
@@ -69,7 +74,7 @@
69
74
 
70
75
  - **`evict`(推荐)**:要从上下文里驱逐的条目清单。每个 `file` 必须出现在本次 `candidates` 中;候选集外的 file 会被丢弃并记入 `dropped`。未见过的条目默认保留 —— 候选很多时也安全。
71
76
  - **`selected`(可选白名单)**:仅在 `complete: true` 且本次未 `truncated` 时接受。每个 `file` 必须 ∈ `candidates`;未列入的候选会被驱逐。
72
- - 元忆侧仍强制执行:BOUND / COMMIT 与身份画像不可驱逐;预算、去重、宽限、章节顺序由引擎决定。
77
+ - 元忆侧仍强制执行:BOUND / COMMIT 与自我接入档案不可驱逐;`## 1. 身份` 段落不受影响,画像中的其它条目随条目一并参与驱逐;预算、去重、宽限、章节顺序由引擎决定。
73
78
 
74
79
  需要授权或不可用时:
75
80
 
@@ -91,7 +96,73 @@
91
96
  `context --json` 的 `hook` 块给出 `status` / `provider_id` / `applied` / `evicted` / `dropped` / `note`;
92
97
  `context --explain` 的 trace 里追加一行 `[hook] ...`。
93
98
 
94
- ## 5. 审计与边界
99
+ ## 5. `context.paging`(v1.1 新增)
100
+
101
+ ### 5.1 调用时机
102
+
103
+ - 仅在用户显式执行 `context` 且传了 `--budget`(大于 0)时调用;不传预算不调用,状态为 `not_requested`。
104
+ - 调用顺序:`memory.hook` 先过滤,`context.paging` 再排序 / 分页。
105
+ - 候选集与 `memory.hook` 同源:权限过滤后、去掉 BOUND / COMMIT / 自我接入档案;最多 500 条。
106
+ - `context.paging` 不能覆盖引擎硬约束:身份段落、自我接入档案、BOUND / COMMIT 不参与;最终字符预算仍由元忆引擎强制。
107
+
108
+ ### 5.2 请求
109
+
110
+ ```json
111
+ {
112
+ "schema": 1,
113
+ "capability": "context.paging",
114
+ "request_id": "<uuid>",
115
+ "payload": {
116
+ "agent": "codex",
117
+ "budget": 4000,
118
+ "focus": "",
119
+ "truncated": false,
120
+ "protected": ["private/codex/prefs/2026-08-25-0001.md"],
121
+ "sections": {
122
+ "summary": ["<file>"],
123
+ "focus": ["<file>"],
124
+ "corridor": ["<file>"],
125
+ "high_value": ["<file>"]
126
+ },
127
+ "candidates": [
128
+ {
129
+ "file": "facts/2026/09/2026-09-29-0001.md",
130
+ "type": "FACT",
131
+ "subject": "示例主题",
132
+ "statement": "示例内容(最多 2000 字)",
133
+ "created": "2026-09-29",
134
+ "updated": "2026-09-29",
135
+ "chars": 128,
136
+ "baseline_section": "corridor"
137
+ }
138
+ ]
139
+ }
140
+ }
141
+ ```
142
+
143
+ - `sections` 是引擎基线的分段落位,供 provider 参考,不构成约束。
144
+ - `protected` 列出引擎硬保护、不可驱逐的文件(如自我接入档案)。
145
+
146
+ ### 5.3 响应
147
+
148
+ ```json
149
+ { "ok": true, "capability": "context.paging", "data": {
150
+ "drop": ["<file>"],
151
+ "order": ["<file>", "<file>"],
152
+ "complete": true
153
+ } }
154
+ ```
155
+
156
+ - `drop`:明确分页出去的条目,任意子集,**始终应用**;候选集外文件只记 note,不进入输出。
157
+ - `order`:期望的进入顺序,**仅在 `complete: true` 且本次未 `truncated` 时应用**;未列入的候选按引擎基线顺序补尾,不丢内容。
158
+ - 候选超过 500(`truncated=true`)时只接受 `drop`,`order` 不应用并写入 note。
159
+
160
+ ### 5.4 状态与回落
161
+
162
+ `context --json` 的 `paging` 块给出 `status` / `provider_id` / `applied` / `dropped` / `ordered` / `truncated` / `budget` / `used` / `note`;
163
+ `context --explain` 的 trace 里追加一行 `[paging] ...`。未配置 / 未声明该 capability 时为 `not_installed`;未传 `--budget` 时为 `not_requested`;授权失败 / 超时 / 非法输出 / 异常均回落普通上下文,不影响既有记忆。
164
+
165
+ ## 6. 审计与边界
95
166
 
96
167
  - 每次实际调用写一行 `<YOTTA_PROVIDER_HOME>/provider-audit.jsonl`:`ts` / `capability` / `provider_id` / `status` / `duration_ms` / `bytes_out`。
97
168
  - 审计**不记录**记忆正文、查询原文或任何 payload 内容。
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.19.0",
6
+ "version": "0.20.1",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true