@yottameta/yotta-memory 0.23.0 → 0.23.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/CHANGELOG.md +10 -0
- package/README.md +11 -0
- package/README.zh-CN.md +9 -0
- package/SKILL.md +1 -1
- package/bin/install.js +85 -16
- package/bin/yotta-memory.js +1 -1
- package/install.sh +42 -2
- package/package.json +1 -1
- package/skill-manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## v0.23.2 (2026-11-01)
|
|
2
|
+
|
|
3
|
+
- 安装器 containment:目标根 / 嵌套链接 fail-closed;受管 Hub 链接解析真实目标;精简安装器同口径。
|
|
4
|
+
|
|
5
|
+
## v0.23.1 (2026-10-25)
|
|
6
|
+
|
|
7
|
+
- **ClawHub 英文门面(无引擎变更)**:发布副本改为英文优先的双语 `summary` / `description`(英文在前、中文关键词保留在后)并补英文 tags,上线后 ClawHub 技能卡摘要由纯中文变为英文优先;构建与校验由独立发布副本流程覆盖,中文检索优化字段只增不减。
|
|
8
|
+
- **README 使用案例**:英文 / 中文 README 首屏新增「使用案例」三条(跨会话恢复上下文 / 决策当场落盘 / 收工留锚点 + 压缩审计)。
|
|
9
|
+
- 兼容性:命令、参数、输出与记忆库格式全部不变;本次仅展示层文案与发布元数据。
|
|
10
|
+
|
|
1
11
|
## v0.23.0 (2026-10-20)
|
|
2
12
|
- **上下文包 COMMIT 段保留策略(根治上下文包无预算膨胀)**:开工上下文包第 6 段默认只展示最近 20 条 COMMIT,`weight ≥ 1.5` 或带 `pinned` / `evergreen` 标签的条目不受上限影响、始终展示;其余折叠为一行提示(不删除,`recall --type COMMIT` 仍可检索)。新配置键 `context_commits_limit`(0 = 全部展示,回旧行为);BOUND 段保持全量(底线保证不变)。
|
|
3
13
|
- **归档门槛与效用地板对齐(根治「库只长不瘦」)**:`maintain_archived_utility` 默认 0.35 → **0.55**(记忆默认 `confidence=1.0`,效用分地板约 0.42~0.47,旧门槛永远打不到);按分类型半衰形成退役阶梯:COMMIT 约 6 个月 / PREF 约 13 个月 / FACT 约 17 个月(无再访问时,且需超 180 天龄;BOUND / immutable / pinned / evergreen 豁免不变)。`context` 的「归档提醒」升级为可执行:显示超龄条数与已达门槛条数,并给出 `maintain` 预览 / `--apply` 命令。
|
package/README.md
CHANGED
|
@@ -21,7 +21,18 @@
|
|
|
21
21
|
<a href="https://github.com/YottaMeta/yotta-memory"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen" /></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
## Usage cases
|
|
25
|
+
|
|
26
|
+
**1. Stop losing context between sessions.** At the start of work the agent runs `yotta-memory context` and gets one package: identity, long-term summaries, a recent corridor, boundaries and commitments. There is no need to re-explain the project every session.
|
|
27
|
+
|
|
28
|
+
**2. Remember a decision the moment it is made.** When the user says "remember this", the agent runs `yotta-memory remember <type> <subject> <statement> --verify` and reads the entry back. Fact types go to the shared FACT zone; preference, boundary and commitment entries stay in that agent's own isolated zone.
|
|
29
|
+
|
|
30
|
+
**3. Wrap up without hand-carrying state.** Before the session ends the agent writes a COMMIT anchor; `maintain` / `consolidate` keep the store small over time, and `context --audit` checks whether anything important was dropped when the host compacted the conversation.
|
|
31
|
+
|
|
32
|
+
## Docs and release notes
|
|
33
|
+
|
|
24
34
|
> 📖 The user-facing operations manual lives in [USER_GUIDE.md](USER_GUIDE.md).
|
|
35
|
+
> 🆕 **v0.23.2 (ClawHub storefront)**: the ClawHub listing now ships an English-first bilingual summary and description (a dedicated `clawhub-dist/yotta-memory` publish copy), and the README gained the three usage cases above. Commands, options, output and the memory store format are unchanged.
|
|
25
36
|
> 🆕 **v0.18.1 (distillation provenance + absolute dates + consolidation markers + rule-promotion suggestions + write discipline)**: `distill` now emits typed extraction lists (event / lesson / todo / growth / boundary-rule / other) with per-item `[source: file#L<a>-L<b>]` anchors, measured quality metrics and a skipped/boundary list, plus structured `--json`; `consolidate` rewrites relative dates ("yesterday / last week / this month / this year") into absolute dates based on each entry's `created` date and appends a consolidation marker to archived copies (`--undo` strips it); `maintain --rules` prints read-only "repeat pitfalls → promote to a rule" suggestions (default threshold 3, never writes BOUND automatically); the Memory Doctrine gained a 6-level conflict authority order and three write-discipline rules.
|
|
26
37
|
|
|
27
38
|
> 🆕 **v0.18.0 (hit tracking + capacity water level + consolidate gate + context audit)**: `recall` / `explain` / `feedback --useful` / `context` record local hit metadata (`hit_days`, retained 90 days by default; `hit_queries` stores only an 8-hex query fingerprint, up to 12 slots, never the query text) inside the memory file, so it travels with encryption / backup / export. Disable globally with `config set usage_enabled false` or per call with `--no-usage`; read-only commands and cross-owner private entries never write hits. New read-only `maintain --capacity [--json]` reports water level, 30/90-day activity, LRU/LFU eviction candidates (30-day cooldown; immutable / BOUND / `evergreen` / `pinned` exempt) and promotion suggestions (≥3 hits across ≥3 distinct queries; commands only, nothing is changed automatically). `archive` respects cooldown and evergreen tags, with `--force` to override those two exemptions. `consolidate` now defaults to `--propose` (structured report + `--json`); `--apply` requires an interactive confirmation token, or explicit `--yes` in non-interactive runs, and shows a one-time data-lifecycle notice. `doctor` scale checks gained `scale_info_*` thresholds and per-metric `ok / info / warning` levels (`doctor.ok` still only follows critical issues). New `context --audit [--from <file|->] [--json] [--gate N]` checks whether compressed-away content was persisted, printing missing items and `remember` suggestions without writing anything. `archive --dry-run` now truly previews (no file moves, no snapshot, no audit), a no-candidate run no longer takes a whole-store snapshot, and `archive --json` emits a structured report. On the MCP side this release adds read-only / preview inputs only (`archive.dryRun`, `maintain.capacity`, `context.audit` with inline `auditText`, and a propose-only `consolidate` tool), while `archive --force` and `consolidate --apply / --undo` stay CLI-only. Previous v0.17.4: `rename` removed flat/layered same-sequence collisions (cross-layout fail-closed, destructive guard, audit).
|
package/README.zh-CN.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
24
|
> 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
|
|
25
|
+
> 🆕 **v0.23.2(ClawHub 门面)**:ClawHub 上的技能卡改为英文优先的双语摘要与描述(新增 `clawhub-dist/yotta-memory` 专属发布副本),README 首屏新增上面三条使用案例;命令、参数、输出与记忆库格式全部不变。
|
|
25
26
|
> 🆕 **v0.20.0(上下文分页 + 画像驱逐修复)**:`memory.hook` 的 PREF 驱逐空转已修复——hook / paging 决策生效时,画像段落按实时可读条目渲染并套用同一套过滤;自我接入档案不进候选、不可驱逐。新增 `context.paging`:仅在显式 `--budget` 时调用,hook 先过滤、paging 再排序与分页;`drop` 始终安全应用,`order` 仅在 `complete:true` 且未截断时应用。`context --json` 新增 `paging` 块,`--explain` 增加 `[paging]` 轨迹。配置与协议见 `references/provider-protocol.md`。
|
|
26
27
|
> 🆕 **v0.18.1(蒸馏溯源 + 日期绝对化 + 巩固标记 + 规则晋升建议 + 写入纪律)**:`distill` 新增分类型提取清单(事件 / 教训 / 待办 / 成长 / 规则边界 / 其他)+ 逐条 `[溯源: 文件#L<a>-L<b>]` + 实测质量指标 + 跳过与边界,`--json` 输出结构化报告;`consolidate` 摘要按条目 `created` 把「昨天 / 上周 / 本月 / 今年」写成绝对日期,归档副本写入巩固标记(`--undo` 剥离还原);`maintain --rules` 只读给出「重复踩坑 → 建议升级为规则」(默认阈值 3,不自动写 BOUND);记忆守则新增冲突权威顺序 6 层与写入纪律 3 条。
|
|
27
28
|
|
|
@@ -60,6 +61,14 @@
|
|
|
60
61
|
|
|
61
62
|
> 🆕 **v0.8.1**:`view` 用户查看平台改为服务端分页(记忆多了一次只渲染当前页,页码上下页);`recall` 增加候选预过滤(语义检索前先粗筛候选集,命中集不变,记忆量大时不再逐条全量语义 / 模糊 / 编辑距离);公共 `index.json` 超过 5000 条时按年份分片(`index-<year>.json`),避免单文件膨胀。
|
|
62
63
|
|
|
64
|
+
## 使用案例
|
|
65
|
+
|
|
66
|
+
**1. 不再丢上下文。** 开工时智能体跑 `yotta-memory context`,一次拿到身份、长期摘要、近期走廊、边界与承诺;不用每个会话重新解释项目。
|
|
67
|
+
|
|
68
|
+
**2. 决策当场落盘。** 用户说「记住」时,智能体执行 `yotta-memory remember <类型> <主题> <内容> --verify` 并回读;FACT 进公共区,偏好 / 边界 / 承诺只进本智能体私密区。
|
|
69
|
+
|
|
70
|
+
**3. 收工不留半截状态。** 会话结束前写一条 COMMIT 锚点;`maintain` / `consolidate` 让记忆库长期不膨胀,`context --audit` 核对上下文被宿主压缩时是否有重要内容没落盘。
|
|
71
|
+
|
|
63
72
|
## 核心价值
|
|
64
73
|
|
|
65
74
|
多数记忆方案把「记住」做成了黑盒:数据进了数据库或云端,用户看得到内容却改不动、难以审计,更控制不了「哪个智能体能看到什么」。元忆换了一条路——把记忆还原成看得见、管得着的文件(公共 FACT 明文可审计,私密区机制级加密、用户可口令解锁):
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-memory
|
|
3
3
|
description: 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/可回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、长期理解摘要、近期走廊、会话闭环、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤、任务相关记忆、--focus、--embedding、压缩遗忘、consolidate、周期摘要、自动合并、分类型衰减、回滚、备份、backup、防误删、doctor、事务快照、基线探针、恢复演练、记忆库安全扫描、scan、命中打点、容量水位、--capacity、--propose、上下文压缩审计、context --audit
|
|
4
|
-
version: 0.23.
|
|
4
|
+
version: 0.23.2
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
package/bin/install.js
CHANGED
|
@@ -108,7 +108,12 @@ function skillVersion() {
|
|
|
108
108
|
const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
109
109
|
return data.version || null;
|
|
110
110
|
} catch (_) {
|
|
111
|
-
|
|
111
|
+
try {
|
|
112
|
+
const version = fs.readFileSync(path.join(PKG_ROOT, 'VERSION'), 'utf8').trim();
|
|
113
|
+
return version || null;
|
|
114
|
+
} catch (_) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
|
|
@@ -178,6 +183,63 @@ function assertSafeTarget(target) {
|
|
|
178
183
|
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
|
|
179
184
|
throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
|
|
180
185
|
}
|
|
186
|
+
if (isDirectLink(target) && !isManagedHubTarget(target)) {
|
|
187
|
+
throw new TargetError('目标目录是符号链接或 junction,拒绝跟随: ' + target);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isDirectLink(target) {
|
|
192
|
+
let stat = null;
|
|
193
|
+
try { stat = fs.lstatSync(target); } catch (_) { return false; }
|
|
194
|
+
if (stat.isSymbolicLink()) return true;
|
|
195
|
+
try {
|
|
196
|
+
const real = fs.realpathSync(target);
|
|
197
|
+
const parentReal = fs.realpathSync(path.dirname(target));
|
|
198
|
+
const norm = (value) => process.platform === 'win32'
|
|
199
|
+
? path.resolve(value).toLowerCase()
|
|
200
|
+
: path.resolve(value);
|
|
201
|
+
return norm(real) !== norm(path.join(parentReal, path.basename(target)));
|
|
202
|
+
} catch (_) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isManagedHubTarget(target) {
|
|
208
|
+
if (!isDirectLink(target)) return false;
|
|
209
|
+
try {
|
|
210
|
+
const real = fs.realpathSync(target);
|
|
211
|
+
if (path.basename(real) !== SKILL_NAME) return false;
|
|
212
|
+
const parent = path.dirname(real);
|
|
213
|
+
return fs.existsSync(path.join(parent, '.yotta-hub.json'))
|
|
214
|
+
|| fs.existsSync(path.join(parent, '.yotta-links.json'));
|
|
215
|
+
} catch (_) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeLink(target) {
|
|
221
|
+
try {
|
|
222
|
+
fs.unlinkSync(target);
|
|
223
|
+
} catch (_) {
|
|
224
|
+
fs.rmdirSync(target);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function safeRemove(target) {
|
|
229
|
+
let stat = null;
|
|
230
|
+
try { stat = fs.lstatSync(target); } catch (_) { return; }
|
|
231
|
+
if (isDirectLink(target)) {
|
|
232
|
+
removeLink(target);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (stat.isDirectory()) {
|
|
236
|
+
for (const name of fs.readdirSync(target)) {
|
|
237
|
+
safeRemove(path.join(target, name));
|
|
238
|
+
}
|
|
239
|
+
fs.rmdirSync(target);
|
|
240
|
+
} else {
|
|
241
|
+
fs.unlinkSync(target);
|
|
242
|
+
}
|
|
181
243
|
}
|
|
182
244
|
|
|
183
245
|
function shouldSkipCache(name, isFile) {
|
|
@@ -195,9 +257,18 @@ function copyDir(src, dst, topLevel) {
|
|
|
195
257
|
const s = path.join(src, entry.name);
|
|
196
258
|
const d = path.join(dst, entry.name);
|
|
197
259
|
if (entry.isDirectory()) {
|
|
260
|
+
if (isDirectLink(d)) {
|
|
261
|
+
throw new TargetError('目标目录包含符号链接或 junction,拒绝写入: ' + d);
|
|
262
|
+
}
|
|
198
263
|
fs.mkdirSync(d, { recursive: true });
|
|
264
|
+
if (isDirectLink(d)) {
|
|
265
|
+
throw new TargetError('目标目录包含符号链接或 junction,拒绝写入: ' + d);
|
|
266
|
+
}
|
|
199
267
|
copyDir(s, d, false);
|
|
200
268
|
} else if (entry.isFile()) {
|
|
269
|
+
if (isDirectLink(d)) {
|
|
270
|
+
throw new TargetError('目标文件是符号链接,拒绝写入: ' + d);
|
|
271
|
+
}
|
|
201
272
|
fs.copyFileSync(s, d);
|
|
202
273
|
}
|
|
203
274
|
}
|
|
@@ -208,16 +279,18 @@ function copyDir(src, dst, topLevel) {
|
|
|
208
279
|
// 不整目录删除、不跟随符号链接。
|
|
209
280
|
function cleanResidue(target) {
|
|
210
281
|
const removed = [];
|
|
282
|
+
const workTarget = isManagedHubTarget(target) ? fs.realpathSync(target) : target;
|
|
211
283
|
let stat = null;
|
|
212
|
-
try { stat = fs.
|
|
284
|
+
try { stat = fs.lstatSync(workTarget); } catch (_) { return removed; }
|
|
213
285
|
if (!stat.isDirectory()) return removed;
|
|
214
|
-
if (
|
|
286
|
+
if (isDirectLink(workTarget)) return removed;
|
|
287
|
+
if (!fs.existsSync(path.join(workTarget, 'SKILL.md'))) return removed;
|
|
215
288
|
for (const name of TOP_SKIP) {
|
|
216
|
-
const p = path.join(
|
|
289
|
+
const p = path.join(workTarget, name);
|
|
217
290
|
let entry = null;
|
|
218
291
|
try { entry = fs.lstatSync(p); } catch (_) { continue; }
|
|
219
|
-
if (entry.isSymbolicLink()) continue;
|
|
220
|
-
|
|
292
|
+
if (entry.isSymbolicLink() || isDirectLink(p)) continue;
|
|
293
|
+
safeRemove(p);
|
|
221
294
|
removed.push(name);
|
|
222
295
|
}
|
|
223
296
|
try {
|
|
@@ -233,10 +306,10 @@ function cleanResidue(target) {
|
|
|
233
306
|
const walk = (dir) => {
|
|
234
307
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
235
308
|
const p = path.join(dir, entry.name);
|
|
236
|
-
if (entry.isSymbolicLink()) continue;
|
|
309
|
+
if (entry.isSymbolicLink() || isDirectLink(p)) continue;
|
|
237
310
|
if (entry.isDirectory()) {
|
|
238
311
|
if (CACHE_DIRS.has(entry.name)) {
|
|
239
|
-
|
|
312
|
+
safeRemove(p);
|
|
240
313
|
removed.push(path.relative(target, p));
|
|
241
314
|
} else {
|
|
242
315
|
walk(p);
|
|
@@ -247,7 +320,7 @@ function cleanResidue(target) {
|
|
|
247
320
|
}
|
|
248
321
|
}
|
|
249
322
|
};
|
|
250
|
-
walk(
|
|
323
|
+
walk(workTarget);
|
|
251
324
|
return removed;
|
|
252
325
|
}
|
|
253
326
|
|
|
@@ -255,13 +328,6 @@ function installTo(dest, opts) {
|
|
|
255
328
|
if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
|
|
256
329
|
const target = path.resolve(dest, SKILL_NAME);
|
|
257
330
|
assertSafeTarget(target);
|
|
258
|
-
// fail-closed:目标基目录 / 技能目录是符号链接(含 Windows junction)时拒绝跟随;
|
|
259
|
-
// 与 install.sh 同口径,避免安装器改写 Hub 链接指向的真源(含 runtimePayload)。
|
|
260
|
-
for (const p of [path.resolve(dest), target]) {
|
|
261
|
-
let st = null;
|
|
262
|
-
try { st = fs.lstatSync(p); } catch (_) { continue; }
|
|
263
|
-
if (st.isSymbolicLink()) throw new TargetError('目标路径是符号链接,拒绝跟随:' + p);
|
|
264
|
-
}
|
|
265
331
|
if (opts.dryRun) {
|
|
266
332
|
console.log('[dry-run] 将安装到 -> ' + target);
|
|
267
333
|
return target;
|
|
@@ -269,6 +335,9 @@ function installTo(dest, opts) {
|
|
|
269
335
|
try {
|
|
270
336
|
for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
|
|
271
337
|
fs.mkdirSync(target, { recursive: true });
|
|
338
|
+
if (isDirectLink(target) && !isManagedHubTarget(target)) {
|
|
339
|
+
throw new TargetError('目标目录在安装期间变成了链接,拒绝写入: ' + target);
|
|
340
|
+
}
|
|
272
341
|
copyDir(PKG_ROOT, target, true);
|
|
273
342
|
if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
|
|
274
343
|
throw new InstallError('安装结果缺少 SKILL.md');
|
package/bin/yotta-memory.js
CHANGED
|
@@ -25,7 +25,7 @@ const net = require('net');
|
|
|
25
25
|
const child_process = require('child_process');
|
|
26
26
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
27
27
|
|
|
28
|
-
const VERSION = '0.23.
|
|
28
|
+
const VERSION = '0.23.2';
|
|
29
29
|
const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from']);
|
|
30
30
|
const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--propose', '--audit', '--capacity', '--rules', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--no-usage', '--yes', '--keep-memories', '--keep-identity']);
|
|
31
31
|
|
package/install.sh
CHANGED
|
@@ -125,6 +125,25 @@ assert_safe_target() {
|
|
|
125
125
|
return 0
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
is_managed_hub_link() {
|
|
129
|
+
local p="$1" real parent
|
|
130
|
+
[ -L "$p" ] || return 1
|
|
131
|
+
real="$(resolve_path "$p")" || return 1
|
|
132
|
+
[ "$(basename "$real")" = "$SKILL_NAME" ] || return 1
|
|
133
|
+
parent="$(dirname "$real")"
|
|
134
|
+
[ -f "$parent/.yotta-hub.json" ] || [ -f "$parent/.yotta-links.json" ]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assert_no_nested_links() {
|
|
138
|
+
local dest="$1"
|
|
139
|
+
[ -d "$dest" ] || return 0
|
|
140
|
+
if find "$dest" -mindepth 1 -type l -print -quit 2>/dev/null | grep -q .; then
|
|
141
|
+
echo "安装失败:目标目录包含符号链接,拒绝写入:$dest" >&2
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
return 0
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
# 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
|
|
129
148
|
# 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
|
|
130
149
|
DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules test-fixtures"
|
|
@@ -154,7 +173,15 @@ clean_residue() {
|
|
|
154
173
|
# 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
|
|
155
174
|
copy_tree() {
|
|
156
175
|
local src="$1" dst="$2" top="$3" entry base
|
|
157
|
-
|
|
176
|
+
if [ "$top" != "1" ] && [ -L "$dst" ]; then
|
|
177
|
+
echo "安装失败:目标目录包含符号链接,拒绝写入:$dst" >&2
|
|
178
|
+
return 1
|
|
179
|
+
fi
|
|
180
|
+
mkdir -p "$dst" || return 1
|
|
181
|
+
if [ "$top" != "1" ] && [ -L "$dst" ]; then
|
|
182
|
+
echo "安装失败:目标目录包含符号链接,拒绝写入:$dst" >&2
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
158
185
|
for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
|
|
159
186
|
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
160
187
|
base="$(basename "$entry")"
|
|
@@ -170,11 +197,19 @@ copy_tree() {
|
|
|
170
197
|
esac
|
|
171
198
|
fi
|
|
172
199
|
if [ -d "$entry" ] && [ ! -L "$entry" ]; then
|
|
200
|
+
if [ -L "$dst/$base" ]; then
|
|
201
|
+
echo "安装失败:目标目录包含符号链接,拒绝写入:$dst/$base" >&2
|
|
202
|
+
return 1
|
|
203
|
+
fi
|
|
173
204
|
copy_tree "$entry" "$dst/$base" 0 || return 1
|
|
174
205
|
else
|
|
175
206
|
case "$base" in
|
|
176
207
|
*.pyc|*.pyo) continue ;;
|
|
177
208
|
esac
|
|
209
|
+
if [ -L "$dst/$base" ]; then
|
|
210
|
+
echo "安装失败:目标文件是符号链接,拒绝写入:$dst/$base" >&2
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
178
213
|
cp -RP "$entry" "$dst/$base" || return 1
|
|
179
214
|
fi
|
|
180
215
|
done
|
|
@@ -191,7 +226,7 @@ install_to() {
|
|
|
191
226
|
echo "安装失败:目标路径已存在且不是目录:$base" >&2
|
|
192
227
|
return 1
|
|
193
228
|
fi
|
|
194
|
-
if [ -L "$dest" ]; then
|
|
229
|
+
if [ -L "$dest" ] && ! is_managed_hub_link "$dest"; then
|
|
195
230
|
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
196
231
|
return 1
|
|
197
232
|
fi
|
|
@@ -200,12 +235,17 @@ install_to() {
|
|
|
200
235
|
return 1
|
|
201
236
|
fi
|
|
202
237
|
assert_safe_target "$dest" || return 1
|
|
238
|
+
assert_no_nested_links "$dest" || return 1
|
|
203
239
|
if [ "$dry" = "1" ]; then
|
|
204
240
|
echo "[dry-run] 将安装到 -> $dest"
|
|
205
241
|
return 0
|
|
206
242
|
fi
|
|
207
243
|
clean_residue "$dest"
|
|
208
244
|
mkdir -p "$dest" || return 1
|
|
245
|
+
if [ -L "$dest" ] && ! is_managed_hub_link "$dest"; then
|
|
246
|
+
echo "安装失败:技能目录在安装期间变成了链接,拒绝写入:$dest" >&2
|
|
247
|
+
return 1
|
|
248
|
+
fi
|
|
209
249
|
copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
|
|
210
250
|
if [ ! -f "$dest/SKILL.md" ]; then
|
|
211
251
|
echo "安装失败:结果缺少 SKILL.md" >&2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-memory",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
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": [
|