@michengai/dsh-skills-manager 0.1.31 → 0.1.32

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 CHANGED
@@ -6,6 +6,13 @@ The five most recent published versions are listed below.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.32 — 2026-08-31
10
+
11
+ - Unified every per-Skill enable/disable action—user DSH, shared Agents, Codex, Claude, Gemini, OpenCode, project DSH, and project Agents—under manager-local tri-state policy; toggles never rewrite a source Skill file.
12
+ - Added explicit enable overrides for source-disabled or invalid invocation fields, enforced by user DSH rank 399 and project rank 99/199 policy candidates; legacy state files migrate without losing existing disabled entries.
13
+ - Replaced ambiguous **Loaded/Discovered** UI and API counters with **Enabled/Disabled** invocation-policy terminology; this no longer implies that a Skill body has already been fetched into a Session.
14
+ - Expanded i18n contract tests to cover every literal client key, every host error/warning/diagnostic code, placeholder parity, and removal of legacy loading terminology in both languages.
15
+
9
16
  ## 0.1.31 — 2026-08-30
10
17
 
11
18
  - Added project Skill discovery for active Session workspaces, covering project-root `.dsh/skills` and `.agents/skills` with stable workspace-specific identities and official rank order.
@@ -6,6 +6,13 @@
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## 0.1.32 — 2026-08-31
10
+
11
+ - 将用户 DSH、公共 Agent、Codex、Claude、Gemini、OpenCode、项目 DSH 与项目 Agent 的逐 Skill 启停全部统一为 manager 本地三态策略;启停不再改写任何来源 Skill 文件。
12
+ - 新增对来源原本停用或调用字段非法 Skill 的显式启用覆盖,通过用户 DSH rank 399、项目 rank 99/199 策略候选执行;旧版状态文件可无损迁移已有停用项。
13
+ - 将容易误解的“已加载/已发现”界面与 API 统计统一为“已启用/已停用”调用策略语义,不再暗示 Skill 正文已经进入某个 Session。
14
+ - 扩展 i18n 契约测试,覆盖所有客户端字面量 key、宿主错误/警告/诊断代码、占位符一致性及中英文旧加载术语清理。
15
+
9
16
  ## 0.1.31 — 2026-08-30
10
17
 
11
18
  - 新增活动 Session 工作区的项目 Skill 发现,覆盖项目根 `.dsh/skills` 与 `.agents/skills`,使用稳定的 workspace 专属身份并遵循官方优先级。
package/README.md CHANGED
@@ -23,16 +23,16 @@
23
23
  ## Features
24
24
 
25
25
  - Discover and load user-level skills from `.agents`, Codex, Claude, Gemini, and OpenCode into DSH.
26
- - Discover project-level `.dsh/skills` and `.agents/skills` from active Session workspaces, grouped by project; project DSH skills can be enabled, disabled, created, moved to Trash, and restored while project Agent skills stay read-only.
27
- - Persist external source and skill toggles under `$DSH_HOME/skills-manager/state.json` without rewriting shared source files.
28
- - Inspect Markdown bodies, frontmatter, load state, duplicate shadowing, and format diagnostics in a source-first UI.
26
+ - Discover project-level `.dsh/skills` and `.agents/skills` from active Session workspaces, grouped by project; every valid Skill can be enabled or disabled through non-mutating local policy.
27
+ - Persist every per-Skill toggle under `$DSH_HOME/skills-manager/state.json` without rewriting any source Skill file.
28
+ - Inspect Markdown bodies, frontmatter, invocation state, duplicate shadowing, and format diagnostics in a source-first UI.
29
29
  - Create a user- or project-level DSH skill from Settings; conversational creation remains user-level and requires approval.
30
30
  - Move user- and project-level DSH skills to recoverable Trash, then restore them to the original source or permanently delete them.
31
31
  - Import `.zip` archives, skill folders, or a single `SKILL.md` safely into `$DSH_HOME/skills`.
32
32
 
33
33
  ## Screenshots
34
34
 
35
- Browse by source or search in **Settings → Skills**. External Agent sources are loaded through a manager-owned provider while their files stay read-only:
35
+ Browse by source or search in **Settings → Skills**. External Agent sources are made available through manager policy while their files stay read-only:
36
36
 
37
37
  ![Skills Manager source-first settings page](assets/screenshots/skills-manager-v2-preview.png)
38
38
 
@@ -137,12 +137,12 @@ Open **Settings → Skills**, then use the panel as follows:
137
137
  | --- | --- | --- |
138
138
  | Search or filter | Narrow by source, name, or description. | All sources |
139
139
  | Inspect details | Review body, frontmatter, path, format diagnostics, and duplicate shadowing. | All sources |
140
- | Enable or disable | User and project DSH skills update their own invocation policy; external user skills update manager-local state only. | User-level sources and active project DSH skills |
140
+ | Enable or disable | Update manager-local invocation policy without modifying the source Skill file. | All valid Skills in user and active-project sources |
141
141
  | Create or import | Choose a user or active project DSH destination when creating in Settings; imports remain user-level. | `$DSH_HOME/skills`, active `<project>/.dsh/skills` for creation |
142
142
  | Create from conversation | Let an Agent call `create_skill`; DSH asks for approval before writing. | `$DSH_HOME/skills` |
143
143
  | Delete and recover | Move to Trash, restore to the original source, or permanently delete in a second step. | User and active-project DSH skills |
144
144
 
145
- > Toggling a shared source never changes its files; only user or project DSH skills can move to Trash.
145
+ > Toggling never changes a source Skill file; only user or project DSH skills can move to Trash.
146
146
 
147
147
  Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
148
148
 
@@ -150,19 +150,19 @@ Escape closes only the frontmost upload or confirmation dialog and leaves Settin
150
150
 
151
151
  | Directory | View/load | Enable or disable | Create/import | Delete |
152
152
  | --- | --- | --- | --- | --- |
153
- | `$DSH_HOME\skills` | Yes | Updates local invocation policy | Yes | Moves to Trash |
153
+ | `$DSH_HOME\skills` | Yes | Manager state only | Yes | Moves to Trash |
154
154
  | `$DSH_AGENTS_HOME\skills` | Yes | Manager state only | No | No |
155
155
  | `~/.codex/skills`, `~/.claude/skills`, `~/.gemini/skills`, `~/.config/opencode/skills` | Yes | Manager state only | No | No |
156
- | `<project>/.dsh/skills` | Yes, for active Session workspaces | Updates the Skill's invocation policy; DSH's scoped provider remains responsible for loading | Create in Settings | Moves to Trash and restores to the original project |
157
- | `<project>/.agents/skills` | Yes, for active Session workspaces | No; managed by DSH's scoped filesystem provider | No | No |
156
+ | `<project>/.dsh/skills` | Yes, for active Session workspaces | Manager state only | Create in Settings | Moves to Trash and restores to the original project |
157
+ | `<project>/.agents/skills` | Yes, for active Session workspaces | Manager state only | No | No |
158
158
 
159
159
  - Enable, disable, and delete accept only one ordinary skill-name path segment.
160
160
  - Project roots are derived only from active Session `cwd` values; client requests carry an opaque source key and cannot nominate an arbitrary workspace path.
161
- - Project sources follow DSH's nearest-`.git` root convention and rank order (`project-dsh` 100 before `project-agents` 200). The manager re-scans for each state/detail request and does not register a duplicate project provider; writes are limited to invocation-policy toggles and create/Trash/restore under the active project's `.dsh/skills` root.
161
+ - Project sources follow DSH's nearest-`.git` root convention and rank order (`project-dsh` 100 before `project-agents` 200). The manager re-scans for each state/detail request. Explicit project policy is enforced by workspace-scoped rank 99/199 overlays; user DSH policy uses rank 399. With no override, DSH's official provider remains the owner. Toggle writes are limited to manager state; project file writes occur only for explicit create/Trash/restore actions under `.dsh/skills`.
162
162
  - Trash falls back to copy-then-hide when a project and `$DSH_HOME` are on different volumes; restore uses the same guarded cross-volume path in reverse.
163
163
  - Project Trash entries retain their original opaque source identity. Restore is allowed only while that original project is still represented by an active Session workspace; the client cannot nominate a replacement path.
164
164
  - Project writes reject linked `.dsh` or `.dsh/skills` directories so a repository cannot redirect creation, deletion, or restore outside its own project root.
165
- - Project rows say **Discovered**, not **Loaded**: only DSH's Session-scoped catalog can establish model visibility. Use Refresh after IDE, Git, or shell changes; the official provider remains responsible for model-catalog watching and invalidation.
165
+ - Rows and summaries say **Enabled/Disabled**, not **Loaded**: these labels describe invocation policy, while full Skill bodies are loaded on demand by DSH. Use Refresh after IDE, Git, or shell changes; the official provider remains responsible for project catalog watching and invalidation.
166
166
  - Empty project roots stay out of the main source list to reduce noise, but remain selectable in Create Skill so the first project Skill can still be created. Project DSH supports per-Skill toggles only, not a source-wide switch.
167
167
  - Replacements copy to a temporary sibling path first and keep the original until that succeeds.
168
168
  - Every endpoint, including GET `/state`, accepts only a loopback `Host` or a canonical `host[:port]` that the DSH Web runtime already trusts through its LAN bind and `--trusted-host`; unknown hosts still receive 403.
package/README.zh-CN.md CHANGED
@@ -23,9 +23,9 @@
23
23
  ## 功能概览
24
24
 
25
25
  - 自动发现并把 `.agents`、Codex、Claude、Gemini 和 OpenCode 的用户级技能真正加载进 DSH。
26
- - 从活动 Session 工作区发现项目级 `.dsh/skills` 与 `.agents/skills` 并按项目分组;项目 DSH Skill 可启停、创建、移入回收站和恢复,项目 Agent Skill 保持只读。
27
- - 外部来源启停只写入 `$DSH_HOME\skills-manager\state.json`,绝不改写共享源文件。
28
- - 按来源折叠、搜索和筛选,并查看技能正文、frontmatter、加载状态、重名遮蔽与格式诊断。
26
+ - 从活动 Session 工作区发现项目级 `.dsh/skills` 与 `.agents/skills` 并按项目分组;所有有效 Skill 都可通过不改源文件的本地策略启停。
27
+ - 所有逐 Skill 启停都只写入 `$DSH_HOME\skills-manager\state.json`,绝不改写任何来源 Skill 文件。
28
+ - 按来源折叠、搜索和筛选,并查看技能正文、frontmatter、调用状态、重名遮蔽与格式诊断。
29
29
  - 在设置页创建用户级或项目级 DSH Skill;对话创建仍为用户级,并在写入前请求确认。
30
30
  - 用户级和项目级 DSH Skill 删除后先进入回收站,可恢复到原来源或二次永久删除。
31
31
  - 支持把 `.zip`、包含 `SKILL.md` 的技能文件夹或单个 `SKILL.md` 安全导入 `$DSH_HOME\skills`。
@@ -137,12 +137,12 @@ dsh --profile web --dump-config
137
137
  | --- | --- | --- |
138
138
  | 搜索或筛选 | 按来源、名称或简介收窄列表。 | 全部来源 |
139
139
  | 查看详情与诊断 | 查看正文、frontmatter、源文件路径、格式问题和重名遮蔽。 | 全部来源 |
140
- | 启用或停用 | 用户级与项目级 DSH Skill 更新自身调用策略;外部用户技能只更新 manager 本地状态。 | 用户级来源与活动项目 DSH Skill |
140
+ | 启用或停用 | 只更新 manager 本地调用策略,不修改来源 Skill 文件。 | 全部用户级来源与活动项目来源中的有效 Skill |
141
141
  | 创建或导入 | 在设置页创建时选择用户 DSH 或活动项目 DSH;导入仍为用户级。 | `$DSH_HOME\skills`,创建可选活动 `<project>/.dsh/skills` |
142
142
  | 从对话创建 | 让 Agent 调用 `create_skill`;写入前由 DSH 审批界面确认。 | `$DSH_HOME\skills` |
143
143
  | 删除与恢复 | 删除先进入回收站;可恢复到原来源或永久二次删除。 | 用户级与活动项目级 DSH Skill |
144
144
 
145
- > 共享来源的启停不会修改源文件;只有用户级或项目级 DSH Skill 可以移入回收站。
145
+ > 任何来源的启停都不会修改 Skill 文件;只有用户级或项目级 DSH Skill 可以移入回收站。
146
146
 
147
147
  按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
148
148
 
@@ -150,19 +150,19 @@ dsh --profile web --dump-config
150
150
 
151
151
  | 目录 | 查看/加载 | 启用或停用 | 创建/导入 | 删除 |
152
152
  | --- | --- | --- | --- | --- |
153
- | `$DSH_HOME\skills` | 支持 | 改写本地调用策略 | 支持 | 进入回收站 |
153
+ | `$DSH_HOME\skills` | 支持 | 仅写 manager 状态 | 支持 | 进入回收站 |
154
154
  | `$DSH_AGENTS_HOME\skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
155
155
  | `~/.codex/skills`、`~/.claude/skills`、`~/.gemini/skills`、`~/.config/opencode/skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
156
- | `<project>/.dsh/skills` | 支持活动 Session 工作区 | 改写 Skill 调用策略;仍由 DSH 作用域 provider 负责加载 | 设置页支持创建 | 进入回收站并恢复到原项目 |
157
- | `<project>/.agents/skills` | 支持活动 Session 工作区 | 不支持;由 DSH 作用域 filesystem provider 管理 | 不支持 | 不支持 |
156
+ | `<project>/.dsh/skills` | 支持活动 Session 工作区 | 仅写 manager 状态 | 设置页支持创建 | 进入回收站并恢复到原项目 |
157
+ | `<project>/.agents/skills` | 支持活动 Session 工作区 | 仅写 manager 状态 | 不支持 | 不支持 |
158
158
 
159
159
  - 启用、停用和删除只接受单个普通技能名称,目录穿越名称会被拒绝。
160
160
  - 项目根只从活动 Session 的 `cwd` 推导;客户端只提交不透明来源 key,不能指定任意工作区路径。
161
- - 项目来源遵循 DSH 最近 `.git` 项目根和固定优先级(`project-dsh` 100、`project-agents` 200)。管理器每次读取状态或详情时重新扫描且不重复注册项目 provider;写入仅限活动项目 `.dsh/skills` 下的调用策略启停、创建、回收与恢复。
161
+ - 项目来源遵循 DSH 最近 `.git` 项目根和固定优先级(`project-dsh` 100、`project-agents` 200)。管理器每次读取状态或详情时重新扫描;显式项目策略通过 workspace 作用域 rank 99/199 覆盖候选执行,用户 DSH 策略使用 rank 399;没有覆盖时仍由 DSH 官方 provider 负责。启停只写 manager 状态,项目文件写入仅发生在用户明确执行 `.dsh/skills` 创建、回收或恢复时。
162
162
  - 当项目与 `$DSH_HOME` 位于不同磁盘时,回收站会降级为“复制后在源盘原子隐藏”;恢复使用反向的同一安全流程。
163
163
  - 项目回收站条目保存原始不透明来源身份。仅当原项目仍由活动 Session 工作区提供时才允许恢复;客户端不能指定替代路径。
164
164
  - 项目写入会拒绝链接形式的 `.dsh` 或 `.dsh/skills` 目录,避免仓库把创建、删除或恢复重定向到项目根之外。
165
- - 项目条目标记为“已发现”而不是“已加载”:只有 DSH 的 Session 作用域目录才能证明模型可见性。IDE、Git 或 shell 改动后可点击“刷新”;模型目录 watcher 与 invalidation 仍由官方 provider 负责。
165
+ - 列表和摘要统一使用“已启用/已停用”,不再使用“已加载”:这里描述的是调用策略,完整 Skill 正文仍由 DSH 按需加载。IDE、Git 或 shell 改动后可点击“刷新”;项目 catalog 的 watcher 与 invalidation 仍由官方 provider 负责。
166
166
  - 没有 Skill 的项目根不会出现在主来源列表中,但仍保留在“创建技能”的目标选择中,确保可以创建第一个项目 Skill。项目 DSH 只提供逐 Skill 启停,不提供整个项目来源总开关。
167
167
  - 覆盖前先复制到同目录临时路径;复制成功前不会改动现有技能。
168
168
  - 全部接口(含 GET `/state`)只接受 loopback `Host`,或 DSH Web runtime 已通过 LAN 绑定和 `--trusted-host` 明确信任的 `host[:port]`;未知 Host 继续返回 403。
package/lib/client.js CHANGED
@@ -12,20 +12,20 @@ window.__ModuleLoader__.load({
12
12
  var DICT = {
13
13
  zh: {
14
14
  "title": "技能", "desc": "统一加载和管理本机 Agent Skills。",
15
- "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.repair.enable": "修复并启用",
16
- "status.enabled": "已加载", "status.discovered": "已发现", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已加载", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
17
- "summary.total": "{count} 个技能", "summary.loaded": "{count} 个已在 DSH 加载", "summary.disabled": "{count} 个已停用", "summary.issues": "{count} 个诊断项", "summary.group": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "DSH 加载状态",
15
+ "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用",
16
+ "status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
17
+ "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
18
18
  "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
19
19
  "empty.search": "没有匹配的技能。", "empty.source": "该来源目录不存在或暂时没有技能。", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
20
- "source.external.note": "通过 Skills Manager 只读加载,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站。",
20
+ "source.external.note": "通过 Skills Manager 接入,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站;启停只更新管理器状态。",
21
21
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
22
22
  "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话中的 create_skill 仍创建用户级 DSH Skill;项目 Skill 可在这里选择活动项目后创建。",
23
- "import.title": "导入技能", "upload.drop.title": "拖拽技能到此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md。", "select.folder.invalid": "所选文件夹中没有找到 SKILL.md。", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
24
- "trash.title": "回收站", "trash.count": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
23
+ "import.title": "导入技能", "upload.drop.title": "拖拽技能到此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md。", "select.folder.invalid": "所选文件夹中没有找到 SKILL.md。", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
24
+ "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
25
25
  "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从当前技能来源移入回收站,之后可以恢复到原位置。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
26
26
  "result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "error.action": "操作失败:{error}",
27
- "warning.state.invalid": "技能管理器状态文件不可读;外部技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理: {path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
28
- "error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}",
27
+ "warning.state.invalid": "技能管理器状态文件不可读;所有技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
28
+ "error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}", "error.skill.notLoadable": "技能结构不完整,无法{action}: {name}",
29
29
  "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
30
30
  "error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中", "error.import.emptySource": "目录下未找到任何 skill 条目: {path}", "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
31
31
  "error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
@@ -39,20 +39,20 @@ window.__ModuleLoader__.load({
39
39
  },
40
40
  en: {
41
41
  "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.",
42
- "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.repair.enable": "Repair & enable",
43
- "status.enabled": "Loaded", "status.discovered": "Discovered", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Loaded", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
44
- "summary.total": "{count} skills", "summary.loaded": "{count} loaded in DSH", "summary.disabled": "{count} disabled", "summary.issues": "{count} diagnostics", "summary.group": "{count} skills", "table.skill": "Skill name and description", "table.status": "DSH load status",
42
+ "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable",
43
+ "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
44
+ "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
45
45
  "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
46
46
  "empty.search": "No matching skills.", "empty.source": "This source does not exist or has no skills yet.", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
47
- "source.external.note": "Loaded read-only through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash.",
47
+ "source.external.note": "Managed through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash; toggles update manager state only.",
48
48
  "detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
49
49
  "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "The conversational create_skill tool still creates a user-level DSH Skill; choose an active project here for a project Skill.",
50
- "import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .zip, a skill folder, or one SKILL.md", "upload.selected": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
51
- "trash.title": "Trash", "trash.count": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
50
+ "import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
51
+ "trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
52
52
  "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of its current skill source and can be restored to the same location later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
53
53
  "result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "error.action": "Action failed: {error}",
54
- "warning.state.invalid": "The manager state file could not be read; external skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
55
- "error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}",
54
+ "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
55
+ "error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}", "error.skill.notLoadable": "Skill structure is incomplete, cannot {action}: {name}",
56
56
  "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Skill sources containing symbolic links are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
57
57
  "error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory", "error.import.emptySource": "No skill entries found in the directory: {path}", "error.import.invalidName": "Cannot generate a valid kebab-case name (original: {name})", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
58
58
  "error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
@@ -72,7 +72,7 @@ window.__ModuleLoader__.load({
72
72
  .dssm-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dssm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dssm-stat:last-child{border-right:0}.dssm-stat strong{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dssm-filters{display:flex;gap:9px}.dssm-search{flex:1}.dssm-source-filter{width:210px;flex:none}
73
73
  .dssm-control,.dssm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dssm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dssm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dssm-select{position:relative}.dssm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dssm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dssm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dssm-option:hover,.dssm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
74
74
  .dssm-sources{display:flex;flex-direction:column;gap:9px}.dssm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dssm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dssm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dssm-source-head:hover,.dssm-row:hover,.dssm-trash-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dssm-source-title{font-size:14px;font-weight:650}.dssm-count{color:var(--dsw-alias-label-tertiary);font-size:12px}.dssm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dssm-source-actions{display:flex;align-items:center;gap:9px;margin-left:8px}.dssm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dssm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
75
- .dssm-table-head,.dssm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dssm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-main{min-width:0}.dssm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dssm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dssm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dssm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-loaded{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dssm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dssm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dssm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
75
+ .dssm-table-head,.dssm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dssm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-main{min-width:0}.dssm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dssm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dssm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dssm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dssm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dssm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dssm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
76
76
  .dssm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dssm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dssm-switch-on{background:var(--dsw-alias-state-success-primary)}.dssm-switch-on:after{transform:translateX(14px)}.dssm-switch:disabled{opacity:.45;cursor:default}.dssm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dssm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dssm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dssm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-warning{border-color:#d49245;color:#d49245}.dssm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
77
77
  .dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dssm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-wide{width:min(720px,100%)!important}.dssm-modal-import{width:min(480px,100%)!important;padding:24px}.dssm-modal-head{display:flex;align-items:flex-start;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dssm-form,.dssm-field,.dssm-detail-section{display:flex;flex-direction:column}.dssm-form{gap:12px}.dssm-field{gap:6px}.dssm-label,.dssm-detail-title{font-size:12px}.dssm-label{color:var(--dsw-alias-label-secondary)}.dssm-detail-title{font-weight:650}.dssm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}.dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease}.dssm-dropzone:hover,.dssm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dssm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:620}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-upload-choices{display:flex;align-items:center;gap:7px}.dssm-upload-link,.dssm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dssm-upload-link{color:var(--dsw-alias-label-secondary)}.dssm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dssm-upload-link:disabled{opacity:.45;cursor:default}.dssm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dssm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dssm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dssm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dssm-upload-requirements{padding:1px 1px 0}.dssm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dssm-detail-section{gap:7px}.dssm-detail-path,.dssm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dssm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dssm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dssm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-trash-item{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-trash-item:last-child{border-bottom:0}.dssm-trash-main{min-width:0;flex:1}
78
78
  @container(max-width:780px){.dssm-table-head{display:none}.dssm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dssm-row>.dssm-tags,.dssm-row>.dssm-status{grid-column:1}.dssm-row-actions{grid-column:2;grid-row:1 / span 3}.dssm-path{display:none}}@media(max-width:760px){.dssm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.dssm-head{flex-direction:column}.dssm-actions{width:100%;margin-left:0}.dssm-actions .dssm-btn{flex:1}.dssm-filters{flex-direction:column}.dssm-source-filter{width:100%}.dssm-summary{grid-template-columns:1fr}}
@@ -89,7 +89,8 @@ window.__ModuleLoader__.load({
89
89
  }
90
90
  function parseApiResponse(response) { return response.json().catch(function () { var error = new Error("non-json response"); error.code = "error.proto.nonJson"; error.params = { status: response.status }; throw error; }).then(function (payload) { if (!response.ok || payload.ok === false) throw payload; return payload.data; }); }
91
91
  function callApi(path, options) { return fetch("/api/dsh-skills-manager" + path, options).then(parseApiResponse); }
92
- function isSkillEnabled(skill) { if (skill.loaded !== undefined) return skill.loaded === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
92
+ function isSkillEnabled(skill) { if (skill.enabled !== undefined) return skill.enabled === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
93
+ function countKey(key, count) { return key + (Number(count) === 1 ? ".one" : ".other"); }
93
94
  function rootDisplayName(t, root) { var base = translateOrFallback(t, "root." + (root.localeKey || root.kind || root.key), root.label); return root.projectName ? base + " · " + root.projectName : base; }
94
95
  function summarizeImportResult(t, data) {
95
96
  var importedItems = data && data.imported || [];
@@ -176,24 +177,25 @@ window.__ModuleLoader__.load({
176
177
  function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
177
178
  function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
178
179
  function submitImport() { if (!upload) return; buildUploadPayload(upload).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
179
- var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, loaded: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
180
+ var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
180
181
  var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "dsh", label: t("root.dsh") });
181
182
  function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
182
183
  function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
183
184
  var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: rootDisplayName(t, root), count: root.count == null ? root.skills.length : root.count }) }; }));
184
185
 
185
186
  function renderSkill(root, skill) {
186
- var enabled = isSkillEnabled(skill), repairable = root.mutable && skill.hasFrontmatter && skill.invocationPolicyValid === false && !skill.shadowedBy, key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false || !skill.invocationPolicyValid ? "status.invalid" : root.scope === "project" && enabled ? "status.discovered" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dssm-shadowed" : enabled ? "dssm-loaded" : "dssm-disabled";
187
- return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" }, repairable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/enable", { root: root.key, name: skill.name }); } }, t("btn.repair.enable")) : root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
187
+ var enabled = isSkillEnabled(skill), key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dssm-shadowed" : enabled ? "dssm-enabled" : "dssm-disabled";
188
+ return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" }, root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
188
189
  }
189
190
  function renderRoot(root) {
190
191
  if (activeSource && activeSource !== root.key) return null;
191
192
  var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
192
- return h("section", { key: root.key, className: "dssm-source" }, h("div", { className: "dssm-source-head" }, h("button", { type: "button", className: "dssm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dssm-source-title" }, displayName), h("span", { className: "dssm-count" }, t("summary.group", { count: root.count == null ? root.skills.length : root.count })), h("span", { className: "dssm-tag " + (root.scope === "project" ? "dssm-tag-on" : root.key === "dsh" ? "" : root.enabled ? "dssm-tag-on" : "dssm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dssm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dssm-path", title: root.path }, root.path)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dssm-source-actions" }, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dssm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dssm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dssm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
193
+ var rootCount = root.count == null ? root.skills.length : root.count;
194
+ return h("section", { key: root.key, className: "dssm-source" }, h("div", { className: "dssm-source-head" }, h("button", { type: "button", className: "dssm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dssm-source-title" }, displayName), h("span", { className: "dssm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dssm-tag " + (root.scope === "project" ? "dssm-tag-on" : root.key === "dsh" ? "" : root.enabled ? "dssm-tag-on" : "dssm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dssm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dssm-path", title: root.path }, root.path)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dssm-source-actions" }, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dssm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dssm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dssm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
193
195
  }
194
196
 
195
- var summary = data.summary || { total: 0, loaded: 0, disabled: 0, issues: 0 };
196
- var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.loaded, "summary.loaded"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(item[1], { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
197
+ var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
198
+ var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
197
199
 
198
200
  if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-form" }, h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dssm-field" }, h("span", { className: "dssm-label" }, t(field[1])), h("input", { className: "dssm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.body")), h("textarea", { className: "dssm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dssm-help" }, t("create.chat.note"))), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
199
201
  if (modal === "import") content.push(h(Modal, { key: "import", className: "dssm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
@@ -204,12 +206,12 @@ window.__ModuleLoader__.load({
204
206
  h("span", { className: "dssm-dropzone-copy" }, t("upload.drop.copy")),
205
207
  h("div", { className: "dssm-upload-choices" }, h("button", { type: "button", className: "dssm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(folderInputRef); } }, t("btn.folder.pick")), h("span", { className: "dssm-upload-divider" }, "/"), h("button", { type: "button", className: "dssm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(importInputRef); } }, t("btn.file.pick")))
206
208
  ),
207
- upload ? h("div", { className: "dssm-file", title: upload.name }, h("span", { className: "dssm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dssm-file-name" }, upload.name), h("span", { className: "dssm-file-meta" }, t("upload.selected", { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dssm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
209
+ upload ? h("div", { className: "dssm-file", title: upload.name }, h("span", { className: "dssm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dssm-file-name" }, upload.name), h("span", { className: "dssm-file-meta" }, t(countKey("upload.selected", upload.count), { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dssm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
208
210
  result ? h("div", { className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error"), role: "alert" }, result.text) : null,
209
211
  h("div", { className: "dssm-upload-requirements" }, h("div", { className: "dssm-label" }, t("upload.requirements")), h("ul", null, h("li", null, t("upload.requirement.skill")), h("li", null, t("upload.requirement.frontmatter")), h("li", null, t("upload.requirement.copy")))),
210
212
  h("div", { className: "dssm-modal-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dssm-btn", disabled: busy || !upload, onClick: submitImport }, busy ? t("upload.importing") : t("btn.import.now")))
211
213
  )); if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dssm-detail-path" }, detail.path), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dssm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dssm-note" }, t("detail.noIssues"))), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.body")), h("pre", { className: "dssm-code" }, detail.body || "")), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.frontmatter")), h("pre", { className: "dssm-code" }, JSON.stringify(detail.frontmatter, null, 2)))) : h("div", { className: "dssm-empty" }, t("loading"))));
212
- if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t("trash.count", { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
214
+ if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t(countKey("trash.count", data.trash.length), { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
213
215
  if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dssm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.trash")))));
214
216
  if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal("trash"); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
215
217
  return h("section", { className: "dssm-section" }, content);
@@ -217,7 +219,7 @@ window.__ModuleLoader__.load({
217
219
 
218
220
  var inject = ["slots", "locale"];
219
221
  function apply(ctx) { ctx.effect(function () { return ctx.locale.register(NS, DICT); }); ctx.slots.inject("settings.section", function () { return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, icon: "skill", locale: NS }, SkillManagerSection); }); }
220
- module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.rootDisplayName = rootDisplayName; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.visibleSkillRoots = visibleSkillRoots; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
222
+ module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.countKey = countKey; module.exports.rootDisplayName = rootDisplayName; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.visibleSkillRoots = visibleSkillRoots; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
221
223
  return module.exports;
222
224
  }
223
225
  });
package/lib/core.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // 覆盖 DSH 用户级与活动 Session 项目级技能根:
4
4
  // - 用户根目录:~/.dsh/skills 与常见 Agent 用户目录
5
- // - 项目根目录:<project>/.dsh/skills(可启停、创建、回收)、<project>/.agents/skills(只读展示)
5
+ // - 项目根目录:<project>/.dsh/skills(可启停、创建、回收)、<project>/.agents/skills(本地策略启停、源文件只读)
6
6
  // - 条目形态:<root>/<name>/SKILL.md(bundle)或 <root>/<name>.md(flat),只扫一层
7
7
  // - 前端展示 name、description 与启停状态,不做格式检查或自动修复
8
8
  //
@@ -18,6 +18,8 @@ import { createHash, randomUUID } from "node:crypto";
18
18
  import { unzipSync } from "fflate";
19
19
 
20
20
  const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+ const PROJECT_ROOT_KEY_RE = /^project-(?:dsh|agents):[a-f0-9]{16}$/;
22
+ const USER_DSH_POLICY_RANK = 399;
21
23
  const WINDOWS_DEVICE_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
22
24
  const MAX_SOURCE_DEPTH = 64;
23
25
  const MAX_ENTRY_NAME_LENGTH = 128;
@@ -143,7 +145,7 @@ export async function projectRoots(projectCwds = [], diagnostics) {
143
145
  };
144
146
  const candidates = [
145
147
  { ...common, key: `project-dsh:${id}`, kind: "project-dsh", localeKey: "projectDsh", path: join(project.root, ".dsh", "skills"), label: "Project DSH", rank: 100, mutable: true, toggleable: true },
146
- { ...common, key: `project-agents:${id}`, kind: "project-agents", localeKey: "projectAgents", path: join(project.root, ".agents", "skills"), label: "Project Agent", rank: 200 },
148
+ { ...common, key: `project-agents:${id}`, kind: "project-agents", localeKey: "projectAgents", path: join(project.root, ".agents", "skills"), label: "Project Agent", rank: 200, toggleable: true },
147
149
  ];
148
150
  for (const candidate of candidates) {
149
151
  if (await projectSourceSafe(candidate)) roots.push(candidate);
@@ -376,8 +378,7 @@ function stripBom(text) {
376
378
  }
377
379
 
378
380
  /** 解析 SKILL.md 的 frontmatter。返回 { fields, map, body },map 保留键序。
379
- * 只识别顶层 key: value;缩进嵌套字段(如 metadata.source)不进入 map。
380
- * 写入走 updateInvocationPolicy 的文本级替换,因此嵌套块会被原样保留。 */
381
+ * 只识别顶层 key: value;缩进嵌套字段(如 metadata.source)不进入 map。 */
381
382
  export function parseSkillDoc(text) {
382
383
  const src = stripBom(String(text));
383
384
  const lines = src.split(/\r?\n/);
@@ -443,20 +444,6 @@ export function unquote(v) {
443
444
  return s;
444
445
  }
445
446
 
446
- /** 仅更新调用策略行,避免重写并破坏其他 YAML frontmatter。
447
- * 启用时删除这两行以恢复缺省可调用,不还原这两行的原始文本或行内注释。 */
448
- function updateInvocationPolicy(text, enabled) {
449
- const source = String(text);
450
- const newline = source.includes("\r\n") ? "\r\n" : "\n";
451
- const lines = source.split(/\r?\n/);
452
- if (lines[0]?.trim() !== "---") return null;
453
- const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
454
- if (end < 0) return null;
455
- const fields = lines.slice(1, end).filter((line) => !/^(disable-model-invocation|user-invocable)\s*:/.test(line));
456
- if (!enabled) fields.push("disable-model-invocation: true", "user-invocable: false");
457
- return ["---", ...fields, "---", ...lines.slice(end + 1)].join(newline);
458
- }
459
-
460
447
  /** 同目录临时文件加 rename,避免写入中断时截断原 SKILL.md。 */
461
448
  /** Windows 上杀毒软件或索引器可能短暂占用目录;只重试明确可恢复的 rename 错误。 */
462
449
  export async function renameWithRetry(source, destination, options = {}) {
@@ -584,7 +571,8 @@ function entryOf(name, kind, docPath, doc) {
584
571
  userInvocable: !userDisabled,
585
572
  invocationPolicyValid,
586
573
  hasFrontmatter: doc.hasFrontmatter,
587
- loadable: doc.hasFrontmatter && KEBAB_RE.test(declaredName) && description !== "" && invocationPolicyValid,
574
+ // 调用策略值异常可以由 manager 本地策略覆盖;结构本身合法即可加载。
575
+ loadable: doc.hasFrontmatter && KEBAB_RE.test(declaredName) && description !== "",
588
576
  diagnostics,
589
577
  };
590
578
  }
@@ -631,12 +619,17 @@ export async function scanEntries(root) {
631
619
  function defaultManagerState() {
632
620
  const sources = Object.create(null);
633
621
  const disabledSkills = Object.create(null);
622
+ const enabledSkills = Object.create(null);
634
623
  for (const root of userRoots()) {
635
- if (root.key === "dsh") continue;
636
- sources[root.key] = true;
624
+ if (root.key !== "dsh") sources[root.key] = true;
637
625
  disabledSkills[root.key] = [];
626
+ enabledSkills[root.key] = [];
638
627
  }
639
- return { version: 1, sources, disabledSkills };
628
+ return { version: 1, sources, disabledSkills, enabledSkills };
629
+ }
630
+
631
+ function validStateSkillName(name) {
632
+ return typeof name === "string" && KEBAB_RE.test(name) && entryPath(resolveDshHome(), name) !== null;
640
633
  }
641
634
 
642
635
  /** 状态文件已存在但不可用时一律关闭外部来源,避免损坏配置重新暴露技能。 */
@@ -646,16 +639,31 @@ function failClosedManagerState() {
646
639
  return state;
647
640
  }
648
641
 
642
+ function validPolicyLists(value, allowMissing = false) {
643
+ if (value === undefined && allowMissing) return true;
644
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
645
+ for (const [key, list] of Object.entries(value)) {
646
+ if (!userRoots().some((root) => root.key === key) && !PROJECT_ROOT_KEY_RE.test(key)) continue;
647
+ if (!Array.isArray(list) || list.some((name) => !validStateSkillName(name))) return false;
648
+ }
649
+ return true;
650
+ }
651
+
649
652
  function validManagerStateDocument(value) {
650
653
  if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== 1) return false;
651
654
  if (!value.sources || typeof value.sources !== "object" || Array.isArray(value.sources)) return false;
652
- if (!value.disabledSkills || typeof value.disabledSkills !== "object" || Array.isArray(value.disabledSkills)) return false;
655
+ if (!validPolicyLists(value.disabledSkills) || !validPolicyLists(value.enabledSkills, true)) return false;
653
656
  for (const root of userRoots()) {
654
657
  if (root.key === "dsh") continue;
655
658
  if (typeof value.sources[root.key] !== "boolean") return false;
656
659
  const list = value.disabledSkills[root.key];
657
660
  if (!Array.isArray(list) || list.some((name) => typeof name !== "string" || entryPath(root.path, name) === null)) return false;
658
661
  }
662
+ const enabledSkills = value.enabledSkills || {};
663
+ for (const key of new Set([...Object.keys(value.disabledSkills), ...Object.keys(enabledSkills)])) {
664
+ const disabled = new Set(value.disabledSkills[key] || []);
665
+ if ((enabledSkills[key] || []).some((name) => disabled.has(name))) return false;
666
+ }
659
667
  return true;
660
668
  }
661
669
 
@@ -663,10 +671,20 @@ function normalizeManagerState(value) {
663
671
  const normalized = defaultManagerState();
664
672
  if (!value || typeof value !== "object" || Array.isArray(value)) return normalized;
665
673
  for (const root of userRoots()) {
666
- if (root.key === "dsh") continue;
667
- if (value.sources && typeof value.sources[root.key] === "boolean") normalized.sources[root.key] = value.sources[root.key];
674
+ if (root.key !== "dsh" && value.sources && typeof value.sources[root.key] === "boolean") normalized.sources[root.key] = value.sources[root.key];
668
675
  const list = value.disabledSkills && value.disabledSkills[root.key];
669
676
  if (Array.isArray(list)) normalized.disabledSkills[root.key] = [...new Set(list.filter((name) => typeof name === "string" && entryPath(root.path, name) !== null))].sort();
677
+ const enabled = value.enabledSkills && value.enabledSkills[root.key];
678
+ if (Array.isArray(enabled)) normalized.enabledSkills[root.key] = [...new Set(enabled.filter((name) => typeof name === "string" && entryPath(root.path, name) !== null))].sort();
679
+ }
680
+ for (const field of ["disabledSkills", "enabledSkills"]) {
681
+ const source = value[field];
682
+ if (source && typeof source === "object" && !Array.isArray(source)) {
683
+ for (const [key, list] of Object.entries(source)) {
684
+ if (!PROJECT_ROOT_KEY_RE.test(key) || !Array.isArray(list)) continue;
685
+ normalized[field][key] = [...new Set(list.filter(validStateSkillName))].sort();
686
+ }
687
+ }
670
688
  }
671
689
  return normalized;
672
690
  }
@@ -682,7 +700,7 @@ export async function readManagerState() {
682
700
  return {
683
701
  state: failClosedManagerState(),
684
702
  writable: false,
685
- warning: { code: "warning.state.invalid", params: { path: managerStatePath() }, error: `技能管理器状态文件不可读,外部技能已安全停用且状态写入已锁定: ${managerStatePath()}` },
703
+ warning: { code: "warning.state.invalid", params: { path: managerStatePath() }, error: `技能管理器状态文件不可读,所有技能已安全停用且状态写入已锁定: ${managerStatePath()}` },
686
704
  };
687
705
  }
688
706
  }
@@ -692,8 +710,24 @@ async function writeManagerState(value) {
692
710
  await writeFileAtomically(managerStatePath(), `${JSON.stringify(normalizeManagerState(value), null, 2)}\n`);
693
711
  }
694
712
 
695
- function managerSkillEnabled(policy, rootKey, name) {
696
- return policy.sources[rootKey] !== false && !(policy.disabledSkills[rootKey] || []).includes(name);
713
+ function managerSkillOverride(policy, rootKey, name) {
714
+ if ((policy.enabledSkills[rootKey] || []).includes(name)) return true;
715
+ if ((policy.disabledSkills[rootKey] || []).includes(name)) return false;
716
+ return undefined;
717
+ }
718
+
719
+ function effectiveSkillPolicy(policyResult, root, entry) {
720
+ const override = managerSkillOverride(policyResult.state, root.key, entry.name);
721
+ const sourceEnabled = root.key === "dsh" || root.scope === "project" || policyResult.state.sources[root.key] !== false;
722
+ if (policyResult.writable === false || !sourceEnabled || override === false) {
723
+ return { override, sourceEnabled, modelInvocable: false, userInvocable: false, enabled: false };
724
+ }
725
+ if (override === true) {
726
+ return { override, sourceEnabled, modelInvocable: true, userInvocable: true, enabled: true };
727
+ }
728
+ const modelInvocable = entry.invocationPolicyValid && entry.modelInvocable;
729
+ const userInvocable = entry.invocationPolicyValid && entry.userInvocable;
730
+ return { override, sourceEnabled, modelInvocable, userInvocable, enabled: modelInvocable && userInvocable };
697
731
  }
698
732
 
699
733
  function invalidManagerStateWrite() {
@@ -716,38 +750,62 @@ export async function setSourceEnabled(key, enabled, log) {
716
750
  return { root: root.key, enabled: enabled === true };
717
751
  }
718
752
 
719
- async function setReadOnlySkillEnabled(root, name, enabled, log) {
720
- const resolved = await resolveEntry(root.path, name);
753
+ async function checkedPolicyRootDefinition(root) {
754
+ const definition = rootDefinition(root);
755
+ if (!definition || !definition.toggleable) return null;
756
+ if (definition.scope !== "project") {
757
+ const canonical = rootByKey(definition.key);
758
+ return canonical && resolve(canonical.path) === resolve(definition.path) ? canonical : null;
759
+ }
760
+ const validProjectRoot = (definition.kind === "project-dsh" || definition.kind === "project-agents")
761
+ && PROJECT_ROOT_KEY_RE.test(definition.key)
762
+ && typeof definition.projectRoot === "string"
763
+ && isAbsolute(definition.projectRoot)
764
+ && definition.key === `${definition.kind}:${projectIdentity(definition.projectRoot)}`
765
+ && resolve(definition.path) === resolve(join(definition.projectRoot, definition.kind === "project-dsh" ? ".dsh" : ".agents", "skills"));
766
+ if (!validProjectRoot) return null;
767
+ if (!(await projectSourceSafe(definition))) {
768
+ return { ok: false, error: `项目技能目录不安全,拒绝写入状态: ${definition.path}`, code: "error.root.unsafe", params: { path: definition.path } };
769
+ }
770
+ return definition;
771
+ }
772
+
773
+ async function setPolicySkillEnabled(root, name, enabled, log) {
774
+ const definition = await checkedPolicyRootDefinition(root);
775
+ if (definition && definition.ok === false) return definition;
776
+ if (!definition) return readonlyError("toggle");
777
+ const resolved = await resolveEntry(definition.path, name);
721
778
  if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
779
+ const summary = entryOf(name, resolved.kind, resolved.docPath, parseSkillDoc(await fs.readFile(resolved.docPath, "utf8")));
780
+ if (!summary.hasFrontmatter) {
781
+ return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
782
+ }
783
+ if (!summary.loadable) {
784
+ return { ok: false, error: `技能结构不完整,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.notLoadable", params: { name, action: enabled ? "enable" : "disable" } };
785
+ }
722
786
  const current = await readManagerState();
723
787
  if (current.writable === false) return invalidManagerStateWrite();
724
- const disabled = new Set(current.state.disabledSkills[root.key] || []);
725
- if (enabled) disabled.delete(name);
726
- else disabled.add(name);
727
- current.state.disabledSkills[root.key] = [...disabled].sort();
788
+ const disabled = new Set(current.state.disabledSkills[definition.key] || []);
789
+ const explicitlyEnabled = new Set(current.state.enabledSkills[definition.key] || []);
790
+ if (enabled) {
791
+ disabled.delete(name);
792
+ explicitlyEnabled.add(name);
793
+ } else {
794
+ explicitlyEnabled.delete(name);
795
+ disabled.add(name);
796
+ }
797
+ current.state.disabledSkills[definition.key] = [...disabled].sort();
798
+ current.state.enabledSkills[definition.key] = [...explicitlyEnabled].sort();
728
799
  await writeManagerState(current.state);
729
- if (log) log(enabled ? "external-enable" : "external-disable", `${enabled ? "启用" : "停用"} ${root.key}/${name}(源文件只读)`);
730
- return { root: root.key, name, enabled: enabled === true };
800
+ if (log) log(enabled ? "policy-enable" : "policy-disable", `${enabled ? "启用" : "停用"} ${definition.key}/${name}(本地策略,源文件不变)`);
801
+ return { root: definition.key, name, enabled: enabled === true };
731
802
  }
732
803
 
733
804
  // ── 启用 / 停用(同时控制模型与 / 手动调用,非破坏)──────────────────────────
734
805
 
735
806
  /** enabled=true 恢复模型与 / 手动调用;false 同时停用两种调用入口。 */
736
807
  export async function setSkillEnabled(root, name, enabled, log) {
737
- let definition = rootDefinition(root);
738
- if (!definition || !definition.toggleable) return readonlyError("toggle");
739
- if (definition.key !== "dsh" && definition.kind !== "project-dsh") return setReadOnlySkillEnabled(definition, name, enabled, log);
740
- definition = await checkedWritableRootDefinition(definition);
741
- if (definition && definition.ok === false) return definition;
742
- if (!definition) return readonlyError("toggle");
743
- const resolved = await resolveEntry(definition.path, name);
744
- if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
745
- const source = await fs.readFile(resolved.docPath, "utf8");
746
- const updated = updateInvocationPolicy(source, enabled);
747
- if (updated === null) return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
748
- await writeFileAtomically(resolved.docPath, updated);
749
- if (log) log(enabled ? "enable" : "disable", `${enabled ? "启用" : "停用"} ${resolved.docPath}`);
750
- return { root: definition.key, name, enabled };
808
+ return setPolicySkillEnabled(root, name, enabled, log);
751
809
  }
752
810
 
753
811
  async function safeExistingEntryPaths(root, name) {
@@ -1410,37 +1468,67 @@ export async function skillDetail(key, name, options = {}) {
1410
1468
  }
1411
1469
 
1412
1470
  /** 生成 manager provider 候选:保留禁用候选以阻止低优先级重名副本意外激活。 */
1413
- export async function listProviderCandidates() {
1414
- const policy = (await readManagerState()).state;
1471
+ export async function listProviderCandidates(options = {}) {
1472
+ const policyResult = await readManagerState();
1415
1473
  const candidates = [];
1416
1474
  for (const root of userRoots()) {
1417
- if (root.key === "dsh") continue;
1418
1475
  const scanned = await scanEntries(root.path);
1419
1476
  for (const entry of scanned.entries) {
1420
1477
  if (!entry.loadable) continue;
1421
- const managerEnabled = managerSkillEnabled(policy, root.key, entry.name);
1478
+ const policy = effectiveSkillPolicy(policyResult, root, entry);
1479
+ const needsOverlay = root.key !== "dsh" || policy.override !== undefined || !entry.invocationPolicyValid || policyResult.writable === false;
1480
+ if (!needsOverlay) continue;
1422
1481
  candidates.push({
1423
1482
  name: entry.declaredName,
1424
1483
  description: entry.description,
1425
- invocation: managerEnabled ? { modelInvocable: entry.modelInvocable, userInvocable: entry.userInvocable } : { modelInvocable: false, userInvocable: false },
1484
+ invocation: { modelInvocable: policy.modelInvocable, userInvocable: policy.userInvocable },
1426
1485
  provider: "dsh-skills-manager-external",
1427
- source: `agent-${root.key}`,
1428
- rank: root.rank,
1486
+ source: root.key === "dsh" ? "user-dsh" : `agent-${root.key}`,
1487
+ rank: root.key === "dsh" ? USER_DSH_POLICY_RANK : root.rank,
1429
1488
  locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath },
1430
1489
  resourceBase: { kind: "directory", path: entry.kind === "bundle" ? dirname(entry.docPath) : root.path },
1431
1490
  path: entry.docPath,
1432
- metadata: { dshSkillsManager: { root: root.key, readOnly: true } },
1491
+ metadata: { dshSkillsManager: { root: root.key, readOnly: !root.mutable, sourceReadOnly: !root.mutable, policyOnly: root.key === "dsh" } },
1433
1492
  });
1434
1493
  }
1435
1494
  }
1495
+ const cwd = options && typeof options.cwd === "string" ? options.cwd : undefined;
1496
+ if (cwd) {
1497
+ const roots = await projectRoots([cwd]);
1498
+ for (const root of roots) {
1499
+ const scanned = await scanEntries(root.path);
1500
+ for (const entry of scanned.entries) {
1501
+ if (!entry.loadable) continue;
1502
+ const policy = effectiveSkillPolicy(policyResult, root, entry);
1503
+ const needsOverlay = policy.override !== undefined || !entry.invocationPolicyValid || policyResult.writable === false;
1504
+ // 没有本地覆盖时继续由 DSH 官方作用域 provider 负责加载。
1505
+ if (!needsOverlay) continue;
1506
+ candidates.push({
1507
+ name: entry.declaredName,
1508
+ description: entry.description,
1509
+ invocation: { modelInvocable: policy.modelInvocable, userInvocable: policy.userInvocable },
1510
+ provider: "dsh-skills-manager-external",
1511
+ source: root.kind,
1512
+ rank: root.rank - 1,
1513
+ locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath },
1514
+ resourceBase: { kind: "directory", path: entry.kind === "bundle" ? dirname(entry.docPath) : root.path },
1515
+ path: entry.docPath,
1516
+ metadata: { dshSkillsManager: { root: root.key, readOnly: !root.mutable, sourceReadOnly: !root.mutable, policyOnly: true } },
1517
+ });
1518
+ }
1519
+ }
1520
+ }
1436
1521
  return candidates;
1437
1522
  }
1438
1523
 
1439
- export async function getProviderSkill(candidate) {
1524
+ export async function getProviderSkill(candidate, options = {}) {
1440
1525
  const locator = candidate && candidate.locator;
1441
1526
  if (!locator || typeof locator.path !== "string" || typeof locator.rootKey !== "string") return undefined;
1442
- const root = rootByKey(locator.rootKey);
1443
- if (!root || root.key === "dsh") return undefined;
1527
+ let root = rootByKey(locator.rootKey);
1528
+ if (!root && PROJECT_ROOT_KEY_RE.test(locator.rootKey) && typeof options.cwd === "string") {
1529
+ root = (await projectRoots([options.cwd])).find((item) => item.key === locator.rootKey);
1530
+ }
1531
+ if (!root) return undefined;
1444
1532
  const entry = await resolveEntry(root.path, String(locator.entryName || ""));
1445
1533
  if (!entry || resolve(entry.docPath) !== resolve(locator.path)) return undefined;
1446
1534
  const doc = parseSkillDoc(await fs.readFile(entry.docPath, "utf8"));
@@ -1478,8 +1566,7 @@ function markWinners(items, options = {}) {
1478
1566
  }
1479
1567
  for (const [canonicalName, winner] of winners) {
1480
1568
  if (options.markWinner !== false) winner.view.winner = true;
1481
- if (options.setLoaded === false) winner.view.discovered = true;
1482
- else winner.view.loaded = winner.managerEnabled && winner.entry.modelInvocable && winner.entry.userInvocable;
1569
+ winner.view.enabled = winner.policy.enabled;
1483
1570
  winner.view.canonicalName = canonicalName;
1484
1571
  }
1485
1572
  return winners;
@@ -1500,7 +1587,8 @@ export async function state(options = {}) {
1500
1587
  if (root.scope === "project" && !exists && root.kind !== "project-dsh") continue;
1501
1588
  const skills = [];
1502
1589
  for (const e of entries) {
1503
- const managerEnabled = root.scope === "project" || root.key === "dsh" ? true : managerSkillEnabled(policyResult.state, root.key, e.name);
1590
+ const policy = effectiveSkillPolicy(policyResult, root, e);
1591
+ const managerEnabled = policyResult.writable !== false && policy.sourceEnabled && policy.override !== false;
1504
1592
  skills.push({
1505
1593
  name: e.name,
1506
1594
  declaredName: e.declaredName,
@@ -1512,10 +1600,13 @@ export async function state(options = {}) {
1512
1600
  hasFrontmatter: e.hasFrontmatter,
1513
1601
  loadable: e.loadable,
1514
1602
  managerEnabled,
1603
+ managerOverride: policy.override === undefined ? null : policy.override,
1604
+ effectiveModelInvocable: policy.modelInvocable,
1605
+ effectiveUserInvocable: policy.userInvocable,
1515
1606
  diagnostics: e.diagnostics,
1516
1607
  path: e.docPath,
1517
1608
  });
1518
- all.push({ root, entry: e, managerEnabled, view: skills[skills.length - 1] });
1609
+ all.push({ root, entry: e, managerEnabled, policy, view: skills[skills.length - 1] });
1519
1610
  }
1520
1611
  result.roots.push({
1521
1612
  key: root.key,
@@ -1530,7 +1621,7 @@ export async function state(options = {}) {
1530
1621
  scope: root.scope || "user",
1531
1622
  ...(root.projectRoot ? { projectRoot: root.projectRoot, projectName: root.projectName, workspaceCwds: root.workspaceCwds } : {}),
1532
1623
  exists,
1533
- enabled: root.scope === "project" || root.key === "dsh" ? true : policyResult.state.sources[root.key] !== false,
1624
+ enabled: policyResult.writable !== false && (root.scope === "project" || root.key === "dsh" || policyResult.state.sources[root.key] !== false),
1534
1625
  skills,
1535
1626
  });
1536
1627
  }
@@ -1545,7 +1636,7 @@ export async function state(options = {}) {
1545
1636
  for (const projectItems of projectGroups.values()) {
1546
1637
  // 用户级条目参与该 workspace 的优先级判定,但不把其全局视图标记为“被某项目覆盖”。
1547
1638
  const userCopies = userItems.map((item) => ({ ...item, view: { ...item.view } }));
1548
- markWinners([...projectItems, ...userCopies], { setLoaded: false });
1639
+ markWinners([...projectItems, ...userCopies]);
1549
1640
  }
1550
1641
  const seenProjects = new Set();
1551
1642
  for (const root of result.roots.filter((item) => item.scope === "project")) {
@@ -1555,8 +1646,8 @@ export async function state(options = {}) {
1555
1646
  }
1556
1647
  result.summary = {
1557
1648
  total: all.length,
1558
- loaded: all.filter((item) => item.view.loaded === true).length,
1559
- disabled: all.filter((item) => item.entry.loadable && (!item.managerEnabled || !item.entry.modelInvocable || !item.entry.userInvocable)).length,
1649
+ enabled: all.filter((item) => item.view.enabled === true).length,
1650
+ disabled: all.filter((item) => item.entry.loadable && item.policy.enabled === false).length,
1560
1651
  issues: all.reduce((count, item) => count + item.entry.diagnostics.length + (item.view.shadowedBy ? 1 : 0), 0),
1561
1652
  };
1562
1653
  return result;
package/lib/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // - host 路由仅依赖 node: 内置与本地 core.js;ZIP 解压封装在 core
3
3
  // - 路由:状态、启停、详情、创建、导入、回收站
4
4
  // - 全部接口先校验 DSH Web 信任的 Host;写接口再校验请求标记与 JSON
5
- // - $DSH_HOME\skills 可创建、导入、回收、启停;用户级外部 Agent 根只读,由 manager 状态控制加载
6
- // - 活动 Session 的项目 .dsh/skills 可启停、创建、回收与恢复;项目 .agents/skills 保持只读
5
+ // - 所有来源的启停只写 manager 状态,不改原始 Skill 文件
6
+ // - $DSH_HOME\skills 与活动项目 .dsh/skills 可创建、回收与恢复;Agent 来源文件保持只读
7
7
 
8
8
  import { appendFile, rename, rm, stat } from "node:fs/promises";
9
9
  import {
@@ -268,8 +268,8 @@ function externalSkillProvider(control, invalidators) {
268
268
  }
269
269
  return {
270
270
  name: "dsh-skills-manager-external",
271
- list: async () => listProviderCandidates(),
272
- get: async (candidate) => getProviderSkill(candidate),
271
+ list: async (options) => listProviderCandidates(options),
272
+ get: async (candidate, options) => getProviderSkill(candidate, options),
273
273
  };
274
274
  }
275
275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "NPM-installable DSH Web plugin for safely loading and managing skills across DSH and common local Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {