dsh-continual-evolve 0.15.4 → 0.15.6
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/README.md +14 -10
- package/README.zh.md +14 -10
- package/client/client.js +229 -16
- package/lib/card-routes.d.ts +2 -0
- package/lib/card-routes.js +74 -1
- package/lib/index.d.ts +4 -2
- package/lib/index.js +7 -2
- package/lib/memory-snapshot.d.ts +59 -1
- package/lib/memory-snapshot.js +126 -5
- package/lib/skill-rules.d.ts +79 -0
- package/lib/skill-rules.js +166 -0
- package/lib/skill-tools.d.ts +9 -0
- package/lib/skill-tools.js +205 -0
- package/lib/store.d.ts +112 -6
- package/lib/store.js +589 -6
- package/lib/token-usage.d.ts +30 -0
- package/lib/token-usage.js +58 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
|
+
[]()
|
|
12
|
+
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
15
|
A workspace-memory plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`): every memory lives in one central SQLite database (`~/.dsh/evolve/memory.db`, rows partitioned by workspace), the index is injected at session start, and the plugin's code is the literal **sole writer** — the model proposes, code gates and lands.
|
|
@@ -21,10 +21,10 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
|
|
|
21
21
|
## How it works
|
|
22
22
|
|
|
23
23
|
1. **Session-start injection** — the plugin registers a single system-prompt section: the workspace's memory index as a store query (hooks only, whole-line truncated over budget, ordered feedback > user > reference), the central-library path, and a three-line guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
|
|
24
|
-
2. **
|
|
25
|
-
3. **Proposal-based extraction** — the only background automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
|
|
26
|
-
4. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted). The read-only card surfaces quarantine anomalies.
|
|
27
|
-
5. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), and
|
|
24
|
+
2. **Four tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. `skill_write` (create/update/delete in the project, plus an explicit move-to-global) sediments reusable procedures through the same discipline (kebab ids, secret screening, 64KB cap, no duplicate ids or bodies across scopes); every applied skill is materialized by the plugin as `<workspace>/.agents/skills/<id>/SKILL.md` — the database is the source of truth, the files are a read-only projection patrol restores on drift. `skill_read` fetches skill bodies by id and searches project plus global skills (the project shadows a same-named global); each body served by id increments that skill's usage counter (use count + last used), which the card shows as pruning evidence. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
|
|
25
|
+
3. **Proposal-based extraction** — the only background automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for. Per-run token usage is normalized through `token-usage.ts`, and the card shows a one-line review audit (applied/rejected/skipped/failed counts plus token totals) from the same ledger.
|
|
26
|
+
4. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies.
|
|
27
|
+
5. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, a review-audit line, click-to-preview bodies (5 MiB cap), patrol warnings, and a skills section listing the workspace's project skills plus visible global skills (global rows badged, project shadowing global) with the same preview, each row showing its consumption count. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
|
|
28
28
|
|
|
29
29
|
Memory types follow the ZCode taxonomy: `user` (profile and environment facts), `feedback` (corrected or confirmed practices — bodies must carry **Why:** and **How to apply:** lines), `reference` (resource pointers). The extraction boundary is person- and environment-scoped knowledge only: decisions and trade-offs are repo-scoped by nature and belong to the repo's ADR route, never to memory.
|
|
30
30
|
|
|
@@ -44,7 +44,7 @@ After installing or updating, restart the DSH profile you actually use (`dsh web
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
-
No commands. On the first session after restart the index is injected automatically. Just tell the model "remember …" / "forget …" — it lands through `memory_write` and the gates. Memories written mid-session become visible in the next session (the section is frozen to protect the prompt cache). To see the whole store, open the plugin's page in the official plugin manager: the read-only card shows each workspace's entries, search, and patrol anomalies. The store requires Node ≥ 22.5 (`node:sqlite`, part of the desktop host's EnsureNode runtime); without it the plugin degrades to a no-op with a console warning.
|
|
47
|
+
No commands. On the first session after restart the index is injected automatically. Just tell the model "remember …" / "forget …" — it lands through `memory_write` and the gates. To sediment a reusable procedure, tell the model to save it as a skill — it lands through `skill_write` in the current project and materializes as a `SKILL.md` the `skill` tool discovers after restart; say "use it everywhere" and the model moves it to the global scope (the project copy archives). Memories and skills written mid-session become visible in the next session (the section is frozen to protect the prompt cache). To see the whole store, open the plugin's page in the official plugin manager: the read-only card shows each workspace's entries and skills, search, and patrol anomalies. The store requires Node ≥ 22.5 (`node:sqlite`, part of the desktop host's EnsureNode runtime); without it the plugin degrades to a no-op with a console warning.
|
|
48
48
|
|
|
49
49
|
## Configuration
|
|
50
50
|
|
|
@@ -71,7 +71,7 @@ Profile patch example:
|
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
pnpm install && pnpm build # deps + clean build -> lib/
|
|
74
|
-
pnpm test # vitest (
|
|
74
|
+
pnpm test # vitest (273 tests)
|
|
75
75
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
76
76
|
pnpm lint # oxlint src test client
|
|
77
77
|
pnpm check:pack # published-artifact consistency (also runs on prepack)
|
|
@@ -88,6 +88,8 @@ Layout:
|
|
|
88
88
|
│ ├── memory-section.ts # injection: store query / truncate / session freeze
|
|
89
89
|
│ ├── memory-guide.ts # the main-session guide (three lines)
|
|
90
90
|
│ ├── memory-tools.ts # memory_write + memory_read (host-shaped definitions)
|
|
91
|
+
│ ├── skill-rules.ts # skill gates: field checks, SKILL.md rendering, managed paths
|
|
92
|
+
│ ├── skill-tools.ts # skill_write + skill_read (project shadowing global)
|
|
91
93
|
│ ├── extraction.ts # runner + per-session scheduler (turn-level, no idle timer)
|
|
92
94
|
│ ├── extraction-surface.ts # trajectory slicing: cursor, eligibility, serialization
|
|
93
95
|
│ ├── extraction-prompt.ts # the extraction prompt + answer parsing
|
|
@@ -98,7 +100,7 @@ Layout:
|
|
|
98
100
|
├── client/
|
|
99
101
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
100
102
|
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
101
|
-
├── test/ # vitest suite (
|
|
103
|
+
├── test/ # vitest suite (20 files)
|
|
102
104
|
├── lib/ # build output (tsc)
|
|
103
105
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
|
104
106
|
└── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
|
|
@@ -111,3 +113,5 @@ Layout:
|
|
|
111
113
|
## License
|
|
112
114
|
|
|
113
115
|
MIT. Independent project — not affiliated with DeepSeek.
|
|
116
|
+
|
|
117
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
package/README.zh.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
|
+
[]()
|
|
12
|
+
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
15
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的工作区记忆插件:全部记忆住在一个中央 SQLite 库里(`~/.dsh/evolve/memory.db`,按工作区路径行分区),会话开局注入索引,插件的代码是字面意义的**唯一写者**——模型提议,代码门禁后落盘。
|
|
@@ -21,10 +21,10 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
|
|
|
21
21
|
## 工作原理
|
|
22
22
|
|
|
23
23
|
1. **开局注入** —— 插件只注册一个 system-prompt section:注入本工作区的记忆索引(查库结果,只含钩子,超预算按整行截断,feedback > user > reference 排序)、中央库路径、三行指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
|
|
24
|
-
2.
|
|
25
|
-
3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write`
|
|
26
|
-
4. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before
|
|
27
|
-
5. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode
|
|
24
|
+
2. **四个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的记住/忘掉:每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。`skill_write`(项目内 create/update/delete + 显式搬到全局)沉淀可复用的操作规程,走同一套纪律(短横线 id、secret 筛查、64KB 上限、跨范围禁重名重文);每条落地的技能由插件代码物化为 `<workspace>/.agents/skills/<id>/SKILL.md`——库是唯一真相,文件是只读投影,漂移由巡检按库还原。`skill_read` 按 id 取技能正文、检索项目与全局技能(同名时项目遮蔽全局);按 id 取到的每份正文计一次取用(取用次数 + 上次取用,卡片展示,作为剪枝证据)。宿主没有工具服务时降级为告诉用户措辞,注入不受影响。
|
|
25
|
+
3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。每次调用的 token 用量经 `token-usage.ts` 归一汇总,卡片给出一行评审审计(各状态计数 + token 总量)。
|
|
26
|
+
4. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除;被隔离技能的物化文件一并撤下,使其不再被加载)。只读卡片显形巡检异常。
|
|
27
|
+
5. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、评审审计行、点击行内展开正文预览(5 MiB 上限)、巡检异常警告,以及技能区(本工作区技能 + 可见的全局技能,全局行打标记,同名时项目遮蔽全局),预览同记忆,每行显示取用次数。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
|
|
28
28
|
|
|
29
29
|
记忆类型沿 ZCode 分类法:`user`(画像与环境事实)、`feedback`(被纠正/确认的做法——正文必带 **Why:** 与 **How to apply:**)、`reference`(资源指针)。提取边界只收"跟人与环境走的知识":决策与取舍天然属于仓库,走仓库的 ADR 路线,不进记忆库。
|
|
30
30
|
|
|
@@ -44,7 +44,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
44
44
|
|
|
45
45
|
## 使用
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
无需命令。重启后第一场会话索引自动注入开场;对模型直接说记住/忘掉,经 `memory_write` 过门禁落盘。想沉淀可复用的操作规程,直接让模型记为技能——经 `skill_write` 落本项目并物化为 `SKILL.md`,重启后 `skill` 工具可发现;说到处都要用则由模型手动搬到全局(项目侧归档)。会话中途写入的记忆下个会话生效(会话内冻结守 prompt cache)。想看记忆库全貌,打开官方插件管理里本插件的页面:只读卡片展示各工作区的记忆条目、搜索与巡检异常。中央库需要 Node ≥ 22.5(`node:sqlite`,桌面宿主的 EnsureNode 运行时自带);运行时过旧时插件整体降级为无操作并打控制台告警。
|
|
48
48
|
|
|
49
49
|
## 配置
|
|
50
50
|
|
|
@@ -71,7 +71,7 @@ profile patch 示例:
|
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
|
|
74
|
-
pnpm test # vitest(
|
|
74
|
+
pnpm test # vitest(273 例)
|
|
75
75
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
76
76
|
pnpm lint # oxlint src test client
|
|
77
77
|
pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
@@ -88,6 +88,8 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
88
88
|
│ ├── memory-section.ts # 注入:查库 / 截断 / 会话冻结
|
|
89
89
|
│ ├── memory-guide.ts # 主会话指南(三行)
|
|
90
90
|
│ ├── memory-tools.ts # memory_write + memory_read(宿主形状定义)
|
|
91
|
+
│ ├── skill-rules.ts # 技能门禁:字段校验、SKILL.md 渲染、受管路径
|
|
92
|
+
│ ├── skill-tools.ts # skill_write + skill_read(项目遮蔽全局)
|
|
91
93
|
│ ├── extraction.ts # 运行器 + 按会话分槽的调度器(轮级、无空闲计时器)
|
|
92
94
|
│ ├── extraction-surface.ts # 轨迹切片:游标、资格判定、序列化
|
|
93
95
|
│ ├── extraction-prompt.ts # 提取 prompt + 应答解析
|
|
@@ -98,7 +100,7 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
98
100
|
├── client/
|
|
99
101
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
100
102
|
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
101
|
-
├── test/ # vitest 测试(
|
|
103
|
+
├── test/ # vitest 测试(20 个文件)
|
|
102
104
|
├── lib/ # 构建产物(tsc)
|
|
103
105
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
|
104
106
|
└── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
|
|
@@ -111,3 +113,5 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
111
113
|
## License
|
|
112
114
|
|
|
113
115
|
MIT。独立项目——与 DeepSeek 无关联。
|
|
116
|
+
|
|
117
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
package/client/client.js
CHANGED
|
@@ -97,6 +97,13 @@ window.__ModuleLoader__.load({
|
|
|
97
97
|
readFailed: "记忆库读取失败:",
|
|
98
98
|
stats: "{files} 条记忆",
|
|
99
99
|
filesHeading: "文件",
|
|
100
|
+
skillsHeading: "技能",
|
|
101
|
+
skillStats: "{files} 个技能",
|
|
102
|
+
skillsEmpty: "该工作区还没有技能——明确告诉模型沉淀为技能即可,重启后生效。",
|
|
103
|
+
skillScopeGlobal: "全局",
|
|
104
|
+
skillPreviewDeleted: "该技能已被删除或搬家,请刷新列表。",
|
|
105
|
+
skillUsed: "用过 {count} 次",
|
|
106
|
+
reviewStats: "评审:应用 {applied} · 拒绝 {rejected} · 跳过 {skipped} · 失败 {failed} · token {tokens}",
|
|
100
107
|
scopeLabel: "选择工作区",
|
|
101
108
|
searchPlaceholder: "搜索记忆…",
|
|
102
109
|
searchEmpty: "没有匹配的记忆文件。",
|
|
@@ -124,6 +131,13 @@ window.__ModuleLoader__.load({
|
|
|
124
131
|
readFailed: "Failed to read the memory store: ",
|
|
125
132
|
stats: "{files} memories",
|
|
126
133
|
filesHeading: "Files",
|
|
134
|
+
skillsHeading: "Skills",
|
|
135
|
+
skillStats: "{files} skills",
|
|
136
|
+
skillsEmpty: "No skills in this workspace yet — ask the model to save one as a skill, then restart.",
|
|
137
|
+
skillScopeGlobal: "global",
|
|
138
|
+
skillPreviewDeleted: "This skill was deleted or moved — refresh the list.",
|
|
139
|
+
skillUsed: "used {count} times",
|
|
140
|
+
reviewStats: "Reviews: {applied} applied · {rejected} rejected · {skipped} skipped · {failed} failed · {tokens} tokens",
|
|
127
141
|
scopeLabel: "Choose workspace",
|
|
128
142
|
searchPlaceholder: "Search memory files…",
|
|
129
143
|
searchEmpty: "No matching memory files.",
|
|
@@ -182,6 +196,22 @@ window.__ModuleLoader__.load({
|
|
|
182
196
|
}).format(date);
|
|
183
197
|
}
|
|
184
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Compact token totals for the review line: 0 / 999 / 12.4k / 3.1M.
|
|
201
|
+
*/
|
|
202
|
+
function formatTokens(total) {
|
|
203
|
+
if (typeof total !== "number" || !isFinite(total) || total <= 0) {
|
|
204
|
+
return "0";
|
|
205
|
+
}
|
|
206
|
+
if (total >= 1000000) {
|
|
207
|
+
return (total / 1000000).toFixed(1) + "M";
|
|
208
|
+
}
|
|
209
|
+
if (total >= 1000) {
|
|
210
|
+
return (total / 1000).toFixed(1) + "k";
|
|
211
|
+
}
|
|
212
|
+
return String(Math.round(total));
|
|
213
|
+
}
|
|
214
|
+
|
|
185
215
|
/**
|
|
186
216
|
* Assemble the card's view model: the workspace catalogue, then one
|
|
187
217
|
* snapshot per workspace. Network or API failure per workspace degrades
|
|
@@ -205,20 +235,34 @@ window.__ModuleLoader__.load({
|
|
|
205
235
|
typeof workspace.label === "string" && workspace.label.trim().length > 0
|
|
206
236
|
? workspace.label
|
|
207
237
|
: workspaceLabel(root);
|
|
238
|
+
var snapshot = null;
|
|
239
|
+
var snapshotError = null;
|
|
240
|
+
var skillSnapshot = null;
|
|
241
|
+
var skillError = null;
|
|
208
242
|
try {
|
|
209
243
|
var snapshotResponse = await fetchImpl(API_ROOT + "/memory?root=" + encodeURIComponent(root));
|
|
210
244
|
if (!snapshotResponse.ok) {
|
|
211
|
-
|
|
245
|
+
snapshotError = "HTTP " + snapshotResponse.status;
|
|
246
|
+
} else {
|
|
247
|
+
snapshot = await snapshotResponse.json();
|
|
212
248
|
}
|
|
213
|
-
return { root: root, label: label, snapshot: await snapshotResponse.json(), error: null };
|
|
214
249
|
} catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
250
|
+
snapshotError = error instanceof Error ? error.message : String(error);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
var skillResponse = await fetchImpl(API_ROOT + "/skills?root=" + encodeURIComponent(root));
|
|
254
|
+
if (!skillResponse.ok) {
|
|
255
|
+
skillError = "HTTP " + skillResponse.status;
|
|
256
|
+
} else {
|
|
257
|
+
skillSnapshot = await skillResponse.json();
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
skillError = error instanceof Error ? error.message : String(error);
|
|
261
|
+
}
|
|
262
|
+
if (snapshotError !== null && skillError !== null) {
|
|
263
|
+
return { root: root, label: label, snapshot: null, error: snapshotError, skillSnapshot: null, skillError: skillError };
|
|
221
264
|
}
|
|
265
|
+
return { root: root, label: label, snapshot: snapshot, error: snapshotError, skillSnapshot: skillSnapshot, skillError: skillError };
|
|
222
266
|
}),
|
|
223
267
|
);
|
|
224
268
|
return {
|
|
@@ -267,6 +311,42 @@ window.__ModuleLoader__.load({
|
|
|
267
311
|
}
|
|
268
312
|
}
|
|
269
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Fetch one skill's body for the preview. Outcomes mirror the
|
|
316
|
+
* endpoint's status mapping, same as memory files.
|
|
317
|
+
*
|
|
318
|
+
* @param {typeof fetch} fetchImpl - injectable for tests.
|
|
319
|
+
* @param {string} root - known workspace root.
|
|
320
|
+
* @param {string} id - the skill id visible in that workspace (project shadowing global).
|
|
321
|
+
* @returns {Promise<{status: "ready"|"deleted"|"tooLarge"|"failed", content: string, message: string}>}
|
|
322
|
+
*/
|
|
323
|
+
async function loadSkillContent(fetchImpl, root, id) {
|
|
324
|
+
var url = API_ROOT + "/skills/file?root=" + encodeURIComponent(root) + "&id=" + encodeURIComponent(id);
|
|
325
|
+
try {
|
|
326
|
+
var response = await fetchImpl(url);
|
|
327
|
+
if (response.ok) {
|
|
328
|
+
var body = await response.json();
|
|
329
|
+
return {
|
|
330
|
+
status: "ready",
|
|
331
|
+
content: typeof body.content === "string" ? body.content : "",
|
|
332
|
+
message: "",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (response.status === 404) {
|
|
336
|
+
return { status: "deleted", content: "", message: "" };
|
|
337
|
+
}
|
|
338
|
+
if (response.status === 413) {
|
|
339
|
+
return { status: "tooLarge", content: "", message: "" };
|
|
340
|
+
}
|
|
341
|
+
return { status: "failed", content: "", message: "HTTP " + response.status };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return {
|
|
344
|
+
status: "failed",
|
|
345
|
+
content: "",
|
|
346
|
+
message: error instanceof Error ? error.message : String(error),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
270
350
|
/** Display label for a workspace: its directory name (ZCode shows a project name, not a path). */
|
|
271
351
|
function workspaceLabel(root) {
|
|
272
352
|
var normalized = String(root).replace(/[\\/]+$/, "");
|
|
@@ -376,6 +456,7 @@ window.__ModuleLoader__.load({
|
|
|
376
456
|
var preview = props.preview;
|
|
377
457
|
var onToggle = props.onToggle;
|
|
378
458
|
var UI = props.UI;
|
|
459
|
+
var deletedKey = typeof props.deletedKey === "string" ? props.deletedKey : "previewDeleted";
|
|
379
460
|
var rowChildren = [];
|
|
380
461
|
if (UI !== null && UI !== undefined && UI.FileIcon !== undefined && UI.FileIcon !== null) {
|
|
381
462
|
// ZCode's memory rows open with a category tile; the host classifies
|
|
@@ -389,10 +470,18 @@ window.__ModuleLoader__.load({
|
|
|
389
470
|
);
|
|
390
471
|
}
|
|
391
472
|
rowChildren.push(createElement("span", { key: "name", className: "dce-file-name" }, file.title));
|
|
473
|
+
if (typeof props.badge === "string" && props.badge !== "") {
|
|
474
|
+
rowChildren.push(createElement("span", { key: "badge", className: "dce-file-time" }, props.badge));
|
|
475
|
+
}
|
|
392
476
|
var time = formatRelative(file.updatedAt, now, t);
|
|
393
477
|
if (time !== "") {
|
|
394
478
|
rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
|
|
395
479
|
}
|
|
480
|
+
// Skill rows carry V2 usage counters; memory rows have no such
|
|
481
|
+
// field and skip this span. Zero means never consumed.
|
|
482
|
+
if (typeof file.useCount === "number" && file.useCount > 0) {
|
|
483
|
+
rowChildren.push(createElement("span", { key: "usage", className: "dce-file-time" }, t("skillUsed", { count: file.useCount })));
|
|
484
|
+
}
|
|
396
485
|
var children = [
|
|
397
486
|
createElement(
|
|
398
487
|
"button",
|
|
@@ -415,7 +504,7 @@ window.__ModuleLoader__.load({
|
|
|
415
504
|
} else if (preview.status === "ready") {
|
|
416
505
|
body = createElement("pre", { className: "dce-pre" }, preview.content);
|
|
417
506
|
} else if (preview.status === "deleted") {
|
|
418
|
-
body = createElement("div", { className: "dce-hint" }, t(
|
|
507
|
+
body = createElement("div", { className: "dce-hint" }, t(deletedKey));
|
|
419
508
|
} else if (preview.status === "tooLarge") {
|
|
420
509
|
body = createElement("div", { className: "dce-hint" }, t("previewTooLarge"));
|
|
421
510
|
} else {
|
|
@@ -620,6 +709,9 @@ window.__ModuleLoader__.load({
|
|
|
620
709
|
var onRefresh = props.onRefresh;
|
|
621
710
|
var query = props.query;
|
|
622
711
|
var onQueryChange = props.onQueryChange;
|
|
712
|
+
var expandedSkill = props.expandedSkill;
|
|
713
|
+
var skillPreview = props.skillPreview;
|
|
714
|
+
var onToggleSkill = props.onToggleSkill;
|
|
623
715
|
var scope = createElement(WorkspaceScopeMenu, {
|
|
624
716
|
key: "scope",
|
|
625
717
|
workspaces: entry.workspaces.map(function (workspace) {
|
|
@@ -671,6 +763,29 @@ window.__ModuleLoader__.load({
|
|
|
671
763
|
t("stats", { files: snapshot.fileCount }),
|
|
672
764
|
),
|
|
673
765
|
];
|
|
766
|
+
// Review-loop audit rides the memory snapshot: one compact line
|
|
767
|
+
// when the ledger has any judgments, nothing when it is quiet.
|
|
768
|
+
var review = snapshot.review;
|
|
769
|
+
if (
|
|
770
|
+
review !== null &&
|
|
771
|
+
review !== undefined &&
|
|
772
|
+
(review.applied + review.rejected + review.skipped + review.failed) > 0
|
|
773
|
+
) {
|
|
774
|
+
var reviewTokens = review.tokens === null || review.tokens === undefined ? 0 : review.tokens.total;
|
|
775
|
+
parts.push(
|
|
776
|
+
createElement(
|
|
777
|
+
"div",
|
|
778
|
+
{ key: "review", className: "dce-hint" },
|
|
779
|
+
t("reviewStats", {
|
|
780
|
+
applied: review.applied,
|
|
781
|
+
rejected: review.rejected,
|
|
782
|
+
skipped: review.skipped,
|
|
783
|
+
failed: review.failed,
|
|
784
|
+
tokens: formatTokens(reviewTokens),
|
|
785
|
+
}),
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
674
789
|
if (snapshot.quarantined.length > 0) {
|
|
675
790
|
parts.push(
|
|
676
791
|
createElement("div", { key: "quarantined", className: "dce-warn" }, t("quarantined") + snapshot.quarantined.map(function (entry) { return entry.id; }).join("、")),
|
|
@@ -729,7 +844,77 @@ window.__ModuleLoader__.load({
|
|
|
729
844
|
}
|
|
730
845
|
body = parts;
|
|
731
846
|
}
|
|
732
|
-
|
|
847
|
+
var skillsBody = (function () {
|
|
848
|
+
var skillData = active.skillSnapshot;
|
|
849
|
+
if ((skillData === null || skillData === undefined) && active.skillError !== null && active.skillError !== undefined) {
|
|
850
|
+
return createElement("div", { key: "skills", className: "dce-error" }, t("readError") + active.skillError);
|
|
851
|
+
}
|
|
852
|
+
if (skillData === null || skillData === undefined || !skillData.exists) {
|
|
853
|
+
return createElement("div", { key: "skills", className: "dce-hint" }, t("skillsEmpty"));
|
|
854
|
+
}
|
|
855
|
+
var rows = Array.isArray(skillData.files) ? skillData.files : [];
|
|
856
|
+
var skillQuery = String(query).trim().toLocaleLowerCase();
|
|
857
|
+
var skillFiles = rows.filter(function (file) {
|
|
858
|
+
if (skillQuery.length === 0) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
return [file.id, file.title, file.description, file.scope]
|
|
862
|
+
.filter(function (value) {
|
|
863
|
+
return typeof value === "string";
|
|
864
|
+
})
|
|
865
|
+
.some(function (value) {
|
|
866
|
+
return value.toLocaleLowerCase().includes(skillQuery);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
var skillParts = [
|
|
870
|
+
createElement(
|
|
871
|
+
"div",
|
|
872
|
+
{ key: "stats", className: "dce-stats" },
|
|
873
|
+
t("skillStats", { files: skillData.fileCount }),
|
|
874
|
+
),
|
|
875
|
+
];
|
|
876
|
+
if (Array.isArray(skillData.quarantined) && skillData.quarantined.length > 0) {
|
|
877
|
+
skillParts.push(
|
|
878
|
+
createElement("div", { key: "quarantined", className: "dce-warn" }, t("quarantined") + skillData.quarantined.map(function (entry) { return entry.id; }).join("、")),
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
skillParts.push(
|
|
882
|
+
createElement(
|
|
883
|
+
"div",
|
|
884
|
+
{ key: "head", className: "dce-files-head" },
|
|
885
|
+
createElement("span", { className: "dce-files-title" }, t("skillsHeading")),
|
|
886
|
+
createElement("span", { className: "dce-files-count" }, String(skillFiles.length)),
|
|
887
|
+
),
|
|
888
|
+
);
|
|
889
|
+
if (skillFiles.length === 0) {
|
|
890
|
+
skillParts.push(createElement("div", { key: "none", className: "dce-empty" }, t("skillsEmpty")));
|
|
891
|
+
} else {
|
|
892
|
+
skillParts.push(
|
|
893
|
+
createElement(
|
|
894
|
+
"ul",
|
|
895
|
+
{ key: "files", className: "dce-list" },
|
|
896
|
+
skillFiles.map(function (file) {
|
|
897
|
+
return createElement(FileRow, {
|
|
898
|
+
key: file.id,
|
|
899
|
+
file: file,
|
|
900
|
+
now: now,
|
|
901
|
+
t: t,
|
|
902
|
+
UI: UI,
|
|
903
|
+
badge: file.scope === "global" ? t("skillScopeGlobal") : "",
|
|
904
|
+
deletedKey: "skillPreviewDeleted",
|
|
905
|
+
expanded: expandedSkill === file.id,
|
|
906
|
+
preview: skillPreview,
|
|
907
|
+
onToggle: function () {
|
|
908
|
+
onToggleSkill(file.id);
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}),
|
|
912
|
+
),
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return createElement("div", { key: "skills", className: "dce-workspace" }, skillParts);
|
|
916
|
+
})();
|
|
917
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, scope, body, skillsBody);
|
|
733
918
|
}
|
|
734
919
|
|
|
735
920
|
/**
|
|
@@ -749,12 +934,15 @@ window.__ModuleLoader__.load({
|
|
|
749
934
|
var setModel = state[1];
|
|
750
935
|
var reloadState = useState(0);
|
|
751
936
|
var reload = reloadState[1];
|
|
752
|
-
var selectionState = useState({ root: null, query: "", expandedFile: null });
|
|
937
|
+
var selectionState = useState({ root: null, query: "", expandedFile: null, expandedSkill: null });
|
|
753
938
|
var selection = selectionState[0];
|
|
754
939
|
var setSelection = selectionState[1];
|
|
755
940
|
var previewState = useState({ file: null, status: "loading", content: "", message: "" });
|
|
756
941
|
var preview = previewState[0];
|
|
757
942
|
var setPreview = previewState[1];
|
|
943
|
+
var skillPreviewState = useState({ file: null, status: "loading", content: "", message: "" });
|
|
944
|
+
var skillPreview = skillPreviewState[0];
|
|
945
|
+
var setSkillPreview = skillPreviewState[1];
|
|
758
946
|
var nowState = useState(Date.now());
|
|
759
947
|
var now = nowState[0];
|
|
760
948
|
var setNow = nowState[1];
|
|
@@ -786,9 +974,10 @@ window.__ModuleLoader__.load({
|
|
|
786
974
|
var refresh = function () {
|
|
787
975
|
setModel({ status: "loading", model: null, message: "" });
|
|
788
976
|
setSelection(function (current) {
|
|
789
|
-
return { root: current.root, query: "", expandedFile: null };
|
|
977
|
+
return { root: current.root, query: "", expandedFile: null, expandedSkill: null };
|
|
790
978
|
});
|
|
791
979
|
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
980
|
+
setSkillPreview({ file: null, status: "loading", content: "", message: "" });
|
|
792
981
|
reload(function (n) {
|
|
793
982
|
return n + 1;
|
|
794
983
|
});
|
|
@@ -846,8 +1035,9 @@ window.__ModuleLoader__.load({
|
|
|
846
1035
|
entry: model.model,
|
|
847
1036
|
selected: selected,
|
|
848
1037
|
onSelect: function (root) {
|
|
849
|
-
setSelection({ root: root, query: "", expandedFile: null });
|
|
1038
|
+
setSelection({ root: root, query: "", expandedFile: null, expandedSkill: null });
|
|
850
1039
|
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
1040
|
+
setSkillPreview({ file: null, status: "loading", content: "", message: "" });
|
|
851
1041
|
},
|
|
852
1042
|
onRefresh: refresh,
|
|
853
1043
|
t: t,
|
|
@@ -855,15 +1045,37 @@ window.__ModuleLoader__.load({
|
|
|
855
1045
|
now: now,
|
|
856
1046
|
expandedFile: selection.expandedFile,
|
|
857
1047
|
preview: preview,
|
|
1048
|
+
expandedSkill: selection.expandedSkill,
|
|
1049
|
+
skillPreview: skillPreview,
|
|
1050
|
+
onToggleSkill: function (file) {
|
|
1051
|
+
if (selection.expandedSkill === file) {
|
|
1052
|
+
setSelection(function (current) {
|
|
1053
|
+
return { root: current.root, query: current.query, expandedFile: current.expandedFile, expandedSkill: null };
|
|
1054
|
+
});
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
setSelection(function (current) {
|
|
1058
|
+
return { root: current.root, query: current.query, expandedFile: current.expandedFile, expandedSkill: file };
|
|
1059
|
+
});
|
|
1060
|
+
setSkillPreview({ file: file, status: "loading", content: "", message: "" });
|
|
1061
|
+
loadSkillContent(fetch, selected, file).then(function (result) {
|
|
1062
|
+
setSkillPreview(function (current) {
|
|
1063
|
+
if (current.file !== file) {
|
|
1064
|
+
return current;
|
|
1065
|
+
}
|
|
1066
|
+
return { file: file, status: result.status, content: result.content, message: result.message };
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
},
|
|
858
1070
|
onToggleFile: function (file) {
|
|
859
1071
|
if (selection.expandedFile === file) {
|
|
860
1072
|
setSelection(function (current) {
|
|
861
|
-
return { root: current.root, query: current.query, expandedFile: null };
|
|
1073
|
+
return { root: current.root, query: current.query, expandedFile: null, expandedSkill: current.expandedSkill };
|
|
862
1074
|
});
|
|
863
1075
|
return;
|
|
864
1076
|
}
|
|
865
1077
|
setSelection(function (current) {
|
|
866
|
-
return { root: current.root, query: current.query, expandedFile: file };
|
|
1078
|
+
return { root: current.root, query: current.query, expandedFile: file, expandedSkill: current.expandedSkill };
|
|
867
1079
|
});
|
|
868
1080
|
setPreview({ file: file, status: "loading", content: "", message: "" });
|
|
869
1081
|
loadFileContent(fetch, selected, file).then(function (result) {
|
|
@@ -878,7 +1090,7 @@ window.__ModuleLoader__.load({
|
|
|
878
1090
|
query: selection.query,
|
|
879
1091
|
onQueryChange: function (value) {
|
|
880
1092
|
setSelection(function (current) {
|
|
881
|
-
return { root: current.root, query: value, expandedFile: current.expandedFile };
|
|
1093
|
+
return { root: current.root, query: value, expandedFile: current.expandedFile, expandedSkill: current.expandedSkill };
|
|
882
1094
|
});
|
|
883
1095
|
},
|
|
884
1096
|
});
|
|
@@ -901,6 +1113,7 @@ window.__ModuleLoader__.load({
|
|
|
901
1113
|
exports.inject = ["slots", "locale", "theme"];
|
|
902
1114
|
exports.loadCardModel = loadCardModel;
|
|
903
1115
|
exports.loadFileContent = loadFileContent;
|
|
1116
|
+
exports.loadSkillContent = loadSkillContent;
|
|
904
1117
|
exports.apply = function apply(ctx) {
|
|
905
1118
|
var tRaw = null;
|
|
906
1119
|
try {
|
package/lib/card-routes.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { MemoryStore } from "./store.js";
|
|
|
3
3
|
export declare const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
4
4
|
export declare const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
5
5
|
export declare const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
6
|
+
export declare const CARD_API_SKILLS_PATH = "/dsh-continual-evolve/api/v1/skills";
|
|
7
|
+
export declare const CARD_API_SKILL_FILE_PATH = "/dsh-continual-evolve/api/v1/skills/file";
|
|
6
8
|
/** Minimal request shape the handlers touch (Node IncomingMessage subset). */
|
|
7
9
|
export interface CardRequest {
|
|
8
10
|
method?: string;
|
package/lib/card-routes.js
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
* so the API cannot probe the filesystem; the content endpoint takes an id,
|
|
12
12
|
* never a path. Handlers are synchronous and never throw past the response.
|
|
13
13
|
*/
|
|
14
|
-
import { memoryEntryContent, listCardWorkspaces, memorySnapshot } from "./memory-snapshot.js";
|
|
14
|
+
import { memoryEntryContent, listCardWorkspaces, memorySnapshot, skillEntryContent, skillSnapshot } from "./memory-snapshot.js";
|
|
15
|
+
import { skillUsagePartition } from "./skill-rules.js";
|
|
15
16
|
import { canonicalPath } from "./workspace-hint.js";
|
|
16
17
|
/** URL prefix of the card API (kind-exact routes registered under it). */
|
|
17
18
|
export const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
18
19
|
export const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
19
20
|
export const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
21
|
+
export const CARD_API_SKILLS_PATH = "/dsh-continual-evolve/api/v1/skills";
|
|
22
|
+
export const CARD_API_SKILL_FILE_PATH = "/dsh-continual-evolve/api/v1/skills/file";
|
|
20
23
|
function sendJson(response, status, body) {
|
|
21
24
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
22
25
|
response.end(JSON.stringify(body));
|
|
@@ -116,6 +119,76 @@ export function mountCardRoutes(webServer, store, options = {}) {
|
|
|
116
119
|
});
|
|
117
120
|
},
|
|
118
121
|
}));
|
|
122
|
+
disposers.push(webServer.register({
|
|
123
|
+
kind: "exact",
|
|
124
|
+
path: CARD_API_SKILLS_PATH,
|
|
125
|
+
handler(request, response) {
|
|
126
|
+
if (!isGet(request)) {
|
|
127
|
+
response.writeHead(405, { allow: "GET" });
|
|
128
|
+
response.end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const root = rootParamOf(request);
|
|
132
|
+
if (root === null) {
|
|
133
|
+
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!isKnownRoot(store, root)) {
|
|
137
|
+
sendJson(response, 404, { error: "unknown workspace root" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
sendJson(response, 200, skillSnapshot(store, root));
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
disposers.push(webServer.register({
|
|
144
|
+
kind: "exact",
|
|
145
|
+
path: CARD_API_SKILL_FILE_PATH,
|
|
146
|
+
handler(request, response) {
|
|
147
|
+
if (!isGet(request)) {
|
|
148
|
+
response.writeHead(405, { allow: "GET" });
|
|
149
|
+
response.end();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const root = rootParamOf(request);
|
|
153
|
+
if (root === null) {
|
|
154
|
+
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const id = idParamOf(request);
|
|
158
|
+
if (id === null) {
|
|
159
|
+
sendJson(response, 400, { error: "id query parameter is required" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!isKnownRoot(store, root)) {
|
|
163
|
+
// Same fence as the snapshot route.
|
|
164
|
+
sendJson(response, 404, { error: "unknown workspace root" });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const content = skillEntryContent(store, root, id);
|
|
168
|
+
if (!content.ok) {
|
|
169
|
+
if (content.reason === "too-large") {
|
|
170
|
+
sendJson(response, 413, { error: "skill body exceeds the 5 MiB preview limit" });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
sendJson(response, 404, { error: "skill not found" });
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// V2 usage loop: a served preview is one real consumption.
|
|
178
|
+
try {
|
|
179
|
+
store.recordSkillUse(skillUsagePartition(content.scope, root), content.id, content.scope);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Accounting never fails the preview it rides on.
|
|
183
|
+
}
|
|
184
|
+
sendJson(response, 200, {
|
|
185
|
+
id: content.id,
|
|
186
|
+
scope: content.scope,
|
|
187
|
+
content: content.content,
|
|
188
|
+
updatedAtMs: content.updatedAtMs,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
119
192
|
return () => {
|
|
120
193
|
for (const disposer of disposers) {
|
|
121
194
|
if (typeof disposer === "function") {
|