skills-manager-cli 2026.9.15 → 2026.10.26

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.
Files changed (61) hide show
  1. package/README.md +54 -1
  2. package/README.zh-CN.md +54 -1
  3. package/dist/cli/cost-output.js +112 -0
  4. package/dist/cli/cost-output.js.map +1 -0
  5. package/dist/cli/main.js +253 -8
  6. package/dist/cli/main.js.map +1 -1
  7. package/dist/core/ports/filesystem.js +5 -1
  8. package/dist/core/ports/filesystem.js.map +1 -1
  9. package/dist/core/ports/security-scanner.js +2 -0
  10. package/dist/core/ports/security-scanner.js.map +1 -0
  11. package/dist/core/services/cost-ledger-service.js +214 -0
  12. package/dist/core/services/cost-ledger-service.js.map +1 -0
  13. package/dist/core/services/detection-service.js +25 -40
  14. package/dist/core/services/detection-service.js.map +1 -1
  15. package/dist/core/services/distribute-service.js +58 -4
  16. package/dist/core/services/distribute-service.js.map +1 -1
  17. package/dist/core/services/doctor-service.js +13 -2
  18. package/dist/core/services/doctor-service.js.map +1 -1
  19. package/dist/core/services/frontmatter-evidence.js +99 -0
  20. package/dist/core/services/frontmatter-evidence.js.map +1 -0
  21. package/dist/core/services/frontmatter-mirror.js +151 -0
  22. package/dist/core/services/frontmatter-mirror.js.map +1 -0
  23. package/dist/core/services/get-service.js +55 -0
  24. package/dist/core/services/get-service.js.map +1 -0
  25. package/dist/core/services/index.js +26 -5
  26. package/dist/core/services/index.js.map +1 -1
  27. package/dist/core/services/init-service.js +15 -5
  28. package/dist/core/services/init-service.js.map +1 -1
  29. package/dist/core/services/install-service.js +15 -1
  30. package/dist/core/services/install-service.js.map +1 -1
  31. package/dist/core/services/migration-service.js +9 -4
  32. package/dist/core/services/migration-service.js.map +1 -1
  33. package/dist/core/services/provenance-service.js +14 -9
  34. package/dist/core/services/provenance-service.js.map +1 -1
  35. package/dist/core/services/registry-service.js +92 -7
  36. package/dist/core/services/registry-service.js.map +1 -1
  37. package/dist/core/services/security-scan.js +156 -0
  38. package/dist/core/services/security-scan.js.map +1 -0
  39. package/dist/core/services/skill-home-service.js +5 -1
  40. package/dist/core/services/skill-home-service.js.map +1 -1
  41. package/dist/core/services/sync-service.js +422 -0
  42. package/dist/core/services/sync-service.js.map +1 -0
  43. package/dist/core/services/tree-content-hash.js +38 -0
  44. package/dist/core/services/tree-content-hash.js.map +1 -0
  45. package/dist/dashboard/server/main.js +4 -0
  46. package/dist/dashboard/server/main.js.map +1 -1
  47. package/dist/dashboard-web/assets/index-Cay-QoI-.js +48 -0
  48. package/dist/dashboard-web/assets/{index-DbZ7PRHg.css → index-Upz8ZY12.css} +1 -1
  49. package/dist/dashboard-web/index.html +2 -2
  50. package/dist/infra/git-cli.js +139 -0
  51. package/dist/infra/git-cli.js.map +1 -1
  52. package/dist/infra/runtime.js +18 -1
  53. package/dist/infra/runtime.js.map +1 -1
  54. package/dist/shared/text-distance.js +31 -0
  55. package/dist/shared/text-distance.js.map +1 -0
  56. package/dist/shared/validation.js +23 -2
  57. package/dist/shared/validation.js.map +1 -1
  58. package/docs/CLI.md +88 -1
  59. package/package.json +12 -3
  60. package/skills/skills-manager/SKILL.md +124 -19
  61. package/dist/dashboard-web/assets/index-Ks0scTAG.js +0 -47
package/README.md CHANGED
@@ -1,15 +1,53 @@
1
1
  # Skills Manager
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/skills-manager-cli?color=cb3837)](https://www.npmjs.com/package/skills-manager-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/skills-manager-cli)](https://www.npmjs.com/package/skills-manager-cli)
5
+ [![GitHub stars](https://img.shields.io/github/stars/shenysun/skills-manager?style=social)](https://github.com/shenysun/skills-manager/stargazers)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/node/v/skills-manager-cli)](https://nodejs.org)
8
+
3
9
  **English** | [简体中文](README.zh-CN.md)
4
10
 
11
+
5
12
  Manage your agent skills **from the conversation**: one `npx` command installs the manager skill, and from then on you just ask your agent — install, import, distribute, update, backfill provenance. Works with any skill-loading agent (Claude Code, Codex, Cursor, …).
6
13
 
14
+ > **CLI-first, conversation-driven.** This is `skills-manager-cli` — a terminal/npm tool with an optional local web dashboard. It is not a desktop app; if you were looking for the desktop application of the same name, that is a different project ([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager)).
15
+
16
+ **What makes it different:**
17
+
18
+ - **One hub, every agent** — a single content library (`~/.skills-manager`) distributes any subset to any of the 70+ catalog agents, symlink or copy per target, with stale-copy detection and rollback.
19
+ - **Five source types** — git repos and URLs, direct download URLs (SKILL.md / zip / tar), local zip archives, Claude plugin marketplaces, and well-known discovery indexes.
20
+ - **Updates that don't cry wolf** — the update anchor is the git tree SHA of the skill's own subdirectory, so upstream commits outside the skill never light up an update.
21
+ - **The resident-cost ledger** — every distributed skill's frontmatter charges each message; `skills-manager cost` accounts it per runtime path (approximate, `≈`) with report-only recall suggestions. Undistributed = zero. See [ADR-0018](docs/adr/0018-resident-cost-ledger.md).
22
+ - **Provenance backfill** — imported skills without a source get one through lockfile-evidence adoption, agent-driven search, and your approval.
23
+ - **The manager skill** — every management action happens in agent conversation ([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md)); the CLI underneath is a plain JSON engine.
24
+
25
+
26
+ ## Why this instead of `gh skill` / `npx skills` / the desktop app?
27
+
28
+ | | **skills-manager** | `gh skill` | `npx skills` | [desktop skills-manager](https://github.com/xingkongliang/skills-manager) |
29
+ |---|---|---|---|---|
30
+ | Primary UI | **Agent conversation** (manager skill) + CLI | GitHub CLI | CLI | Desktop GUI |
31
+ | Hub model | Single `~/.skills-manager` → distribute subset | Per-agent / shared `.agents/skills` | Per install | Hub + presets |
32
+ | Agents covered | **70+** (catalog snapshot) | ~48 | Depends on target | ~54 |
33
+ | Non-GitHub sources | git / URL / zip / marketplace / well-known | GitHub (+ `--from-local`) | GitHub-centric | Marketplace + git |
34
+ | Update signal | **Skill subtree tree-SHA** (no false positives) | Frontmatter tree SHA | Lockfile / metadata | App sync |
35
+ | Context cost | Built-in **resident cost ledger** | — | — | — |
36
+ | Multi-machine | Hub git sync (`sync push/pull`) | — | — | Device-code backup |
37
+ | Install | `npx skills-manager-cli` | `gh extension` | `npx skills` | Desktop download |
38
+
39
+ Pick this when you want **one library, many agents, conversation-driven ops**, and honest update/cost signals — not another one-shot installer.
40
+
7
41
  ## Quick Start
8
42
 
9
43
  ```sh
10
44
  npx skills-manager-cli
11
45
  ```
12
46
 
47
+ ![skills-manager demo](docs/assets/demo.gif)
48
+
49
+ *`--help` and the resident context-cost ledger (`cost`) in action.*
50
+
13
51
  That is the whole setup. Bootstrap creates the skill hub (`~/.skills-manager`), installs the **manager skill** into it from the bundled copy, and mounts it (symlink) into the agents it detects on your machine — you confirm the list in the one prompt it shows.
14
52
 
15
53
  Then go back to your agent and say:
@@ -22,6 +60,20 @@ Then go back to your agent and say:
22
60
 
23
61
  Content moves into the hub, originals become symlinks (backed up first). Prefer to preview it yourself first? `skills-manager init --dry-run`.
24
62
 
63
+
64
+ ## Coming from `npx skills` or `gh skill`?
65
+
66
+ You can keep installing with those tools. To consolidate into one hub (tree-SHA updates + the cost ledger) without re-downloading:
67
+
68
+ ```sh
69
+ npx skills-manager-cli # bootstrap the hub + manager skill
70
+ skills-manager init --dry-run # preview importing existing runtime skills
71
+ # or ask your agent: Import my existing skills into skills-manager
72
+ skills-manager cost --share # privacy-safe paste card of your resident skill tax
73
+ ```
74
+
75
+ Runtime copies become hub-backed; originals are backed up first.
76
+
25
77
  ## How it works
26
78
 
27
79
  ```text
@@ -96,10 +148,11 @@ skills-manager provenance adopt # backfill lockfile evidence
96
148
  skills-manager categories set my-skill frontend # tag with a domain category
97
149
  skills-manager categories apply frontend -a claude-code # load only that domain
98
150
  skills-manager categories status # applied sets + drift
151
+ skills-manager cost # resident context-cost ledger (report-only recall suggestions)
99
152
  skills-manager archive old-skill
100
153
  ```
101
154
 
102
- Sources can be GitHub shorthand (`owner/repo`), Git URLs, GitHub tree URLs, or local paths.
155
+ Sources: GitHub shorthand (`owner/repo`), Git URLs, GitHub tree URLs, local paths, direct download URLs (single `SKILL.md` / zip / tar), local zip archives, plugin marketplaces (`.claude-plugin/marketplace.json`), and well-known discovery indexes — see the [CLI reference](docs/CLI.md).
103
156
 
104
157
  ## Optional: global install & dashboard
105
158
 
package/README.zh-CN.md CHANGED
@@ -1,15 +1,53 @@
1
1
  # Skills Manager
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/skills-manager-cli?color=cb3837)](https://www.npmjs.com/package/skills-manager-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/skills-manager-cli)](https://www.npmjs.com/package/skills-manager-cli)
5
+ [![GitHub stars](https://img.shields.io/github/stars/shenysun/skills-manager?style=social)](https://github.com/shenysun/skills-manager/stargazers)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/node/v/skills-manager-cli)](https://nodejs.org)
8
+
3
9
  [English](README.md) | **简体中文**
4
10
 
11
+
5
12
  在**对话里**管理你的 agent skills:一条 `npx` 命令装上 manager skill,之后一切都直接吩咐你的 agent——安装、导入、分发、更新、补齐来源。任何会加载 skills 的 agent 都能用(Claude Code、Codex、Cursor、…)。
6
13
 
14
+ > **CLI 优先、对话驱动。** 本项目是 `skills-manager-cli`——终端/npm 工具,附带可选的本地 web dashboard。它**不是**桌面应用;如果你找的是同名的桌面版,那是另一个项目([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager))。
15
+
16
+ **差异化在哪:**
17
+
18
+ - **一个库,所有 agent**——单一内容库(`~/.skills-manager`)把任意子集分发到 catalog 里的 70+ agent,按目标选软链接或拷贝,带副本过期检测与回滚。
19
+ - **五类源**——git 仓库与 URL、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、Claude 插件市场、well-known 发现索引。
20
+ - **更新不误报**——更新锚点是 skill 自身子目录的 git tree SHA,上游动了 skill 之外的文件不会点亮「可更新」。
21
+ - **常驻成本账本**——每个已分发 skill 的 frontmatter 都在按条消息计费;`skills-manager cost` 按运行时路径记账(近似口径,`≈` 展示)并给出只报告不执行的收回建议。未分发 = 零常驻。见 [ADR-0018](docs/adr/0018-resident-cost-ledger.md)。
22
+ - **来源可回填**——没有来源的导入 skill,经锁文件证据采纳、agent 搜索、你批准三层流程补齐 provenance。
23
+ - **manager skill**——所有管理动作都在 agent 对话里完成([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md));底层 CLI 是纯 JSON 引擎。
24
+
25
+
26
+ ## 为什么选它,而不是 `gh skill` / `npx skills` / 桌面版?
27
+
28
+ | | **skills-manager** | `gh skill` | `npx skills` | [桌面版 skills-manager](https://github.com/xingkongliang/skills-manager) |
29
+ |---|---|---|---|---|
30
+ | 主界面 | **Agent 对话**(manager skill)+ CLI | GitHub CLI | CLI | 桌面 GUI |
31
+ | 模型 | 单一 `~/.skills-manager` → 分发子集 | 按 agent / 共享 `.agents/skills` | 按次安装 | Hub + presets |
32
+ | Agent 覆盖 | **70+**(catalog 快照) | ~48 | 视目标而定 | ~54 |
33
+ | 非 GitHub 源 | git / URL / zip / marketplace / well-known | GitHub(+ `--from-local`) | 偏 GitHub | Marketplace + git |
34
+ | 更新信号 | **skill 子目录 tree-SHA**(不误报) | frontmatter tree SHA | lockfile / metadata | App 同步 |
35
+ | 上下文成本 | 内置**常驻成本账本** | — | — | — |
36
+ | 多机同步 | Hub git 化(`sync push/pull`) | — | — | 设备码备份 |
37
+ | 安装 | `npx skills-manager-cli` | `gh extension` | `npx skills` | 桌面安装包 |
38
+
39
+ 适合「**一个库、多 agent、对话驱动**,更新与成本信号要诚实」的场景——不是又一个一次性安装器。
40
+
7
41
  ## 快速开始
8
42
 
9
43
  ```sh
10
44
  npx skills-manager-cli
11
45
  ```
12
46
 
47
+ ![skills-manager demo](docs/assets/demo.gif)
48
+
49
+ *演示:`--help` 与常驻上下文成本账本(`cost`)。*
50
+
13
51
  这就是全部的安装步骤。bootstrap 会创建技能库(`~/.skills-manager`),从包内副本把 **manager skill** 种进库,并(以软链接)挂载到它检测到的 agent——展示一次多选让你确认清单。
14
52
 
15
53
  然后回到你的 agent,说:
@@ -22,6 +60,20 @@ npx skills-manager-cli
22
60
 
23
61
  内容会移入库中,原位置变成软链接(先备份)。想先自己预览?`skills-manager init --dry-run`。
24
62
 
63
+
64
+ ## 从 `npx skills` 或 `gh skill` 过来?
65
+
66
+ 可以继续用它们安装。要把已有 skill 收进同一个 hub(树 SHA 更新 + 成本账本)、不必重下:
67
+
68
+ ```sh
69
+ npx skills-manager-cli # 引导创建 hub + manager skill
70
+ skills-manager init --dry-run # 预览导入现有运行时 skill
71
+ # 或对 agent 说:把我现有的 skills 导入 skills-manager
72
+ skills-manager cost --share # 可粘贴分享的常驻 skill 税卡片(无绝对路径)
73
+ ```
74
+
75
+ 运行时副本改为由 hub 托管;原文件会先备份。
76
+
25
77
  ## 工作原理
26
78
 
27
79
  ```text
@@ -96,10 +148,11 @@ skills-manager provenance adopt # 补采锁文件证据
96
148
  skills-manager categories set my-skill 前端 # 打领域类别标签
97
149
  skills-manager categories apply 前端 -a claude-code # 只加载该领域
98
150
  skills-manager categories status # 已应用集合 + 漂移
151
+ skills-manager cost # 常驻上下文成本账本(只报告不执行的收回建议)
99
152
  skills-manager archive old-skill
100
153
  ```
101
154
 
102
- 来源(source)支持 GitHub 简写(`owner/repo`)、Git URL、GitHub tree URL 或本地路径。
155
+ 来源(source):GitHub 简写(`owner/repo`)、Git URL、GitHub tree URL、本地路径、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、插件市场(`.claude-plugin/marketplace.json`)、well-known 发现索引——详见 [CLI 参考](docs/CLI.zh-CN.md)。
103
156
 
104
157
  ## 可选:全局安装与 dashboard
105
158
 
@@ -0,0 +1,112 @@
1
+ import { formatApproxTokens } from '../core/services/cost-ledger-service.js';
2
+ /**
3
+ * Human rendering for `skills-manager cost` (spec context-cost, Q10 shape):
4
+ * path groups → per-skill lines → total + unmanaged line → the three
5
+ * suggestion layers. English per repo CLI convention; every number rides
6
+ * formatApproxTokens; every suggestion shows its verbatim command and nothing
7
+ * is ever executed.
8
+ */
9
+ export function renderCostLedger(ledger) {
10
+ const lines = [];
11
+ lines.push(`Resident context cost (${ledger.method}) — total ${formatApproxTokens(ledger.totalTokens)} tokens across ${ledger.paths.length} runtime path(s)`);
12
+ for (const group of ledger.paths) {
13
+ lines.push('');
14
+ const family = group.agents.length > 0 ? group.agents.join(', ') : 'none';
15
+ lines.push(`${group.runtimeDir} (${group.kind}, agents: ${family}) — ${formatApproxTokens(group.tokens)}`);
16
+ const width = Math.max(0, ...group.skills.map((line) => line.skill.length));
17
+ for (const line of group.skills) {
18
+ const marks = [line.archived ? '(archived)' : null, line.incomplete ? 'incomplete' : null].filter(Boolean).join(' ');
19
+ lines.push(` ${line.skill.padEnd(width)} ${formatApproxTokens(line.tokens)}${marks ? ` ${marks}` : ''}`);
20
+ }
21
+ }
22
+ lines.push('');
23
+ lines.push(`${ledger.unmanaged} unmanaged skill(s) not counted`);
24
+ if (ledger.errors.length > 0) {
25
+ lines.push('Errors (counted as 0):');
26
+ for (const error of ledger.errors)
27
+ lines.push(` ${error.skill} — ${error.runtimePath}: ${error.reason}`);
28
+ }
29
+ const suggestions = ledger.suggestions;
30
+ const empty = suggestions.archived.length === 0 && suggestions.topDescriptions.length === 0 && suggestions.scattered.length === 0;
31
+ lines.push('');
32
+ lines.push('Suggestions (report-only — nothing is executed):');
33
+ if (empty) {
34
+ lines.push(' none');
35
+ return `${lines.join('\n')}\n`;
36
+ }
37
+ if (suggestions.archived.length > 0) {
38
+ lines.push('Zero-controversy recalls (archived but still distributed):');
39
+ for (const item of suggestions.archived) {
40
+ lines.push(` ${item.skill} (archived) ${formatApproxTokens(item.tokens)} — ${item.runtimeDir}`);
41
+ lines.push(` ${item.command}`);
42
+ }
43
+ }
44
+ if (suggestions.topDescriptions.length > 0) {
45
+ const count = suggestions.topDescriptions.length;
46
+ lines.push(`Top ${count} most expensive description${count === 1 ? '' : 's'}:`);
47
+ for (const item of suggestions.topDescriptions) {
48
+ lines.push(` ${item.skill} ${formatApproxTokens(item.tokens)} — ${item.runtimeDir}`);
49
+ lines.push(` ${item.command}`);
50
+ }
51
+ }
52
+ if (suggestions.scattered.length > 0) {
53
+ lines.push('Scattered distributions (same skill on several paths — consolidate by recalling all but one):');
54
+ for (const item of suggestions.scattered) {
55
+ lines.push(` ${item.skill} on ${item.runtimeDirs.length} paths: ${item.runtimeDirs.join(', ')}`);
56
+ for (const command of item.commands)
57
+ lines.push(` ${command}`);
58
+ }
59
+ }
60
+ return `${lines.join('\n')}\n`;
61
+ }
62
+ /** The one-line resident-cost tail of a `preset apply` success (ADR-0019
63
+ * US20): ≈-prefixed char-approx tokens, the same magnitude convention as the
64
+ * ledger above. */
65
+ export function renderPresetCostLine(cost) {
66
+ return `Resident cost: ${formatApproxTokens(cost.tokens)} tokens per message (${cost.method})`;
67
+ }
68
+ /**
69
+ * Privacy-safe paste card for `skills-manager cost --share` (growth wedge):
70
+ * no absolute paths, agent-family labels only, top skills by total tokens,
71
+ * a one-line CTA + repo URL. Same ledger, thinner surface.
72
+ */
73
+ export function renderCostShare(ledger) {
74
+ const skillTotals = new Map();
75
+ for (const group of ledger.paths) {
76
+ for (const line of group.skills) {
77
+ skillTotals.set(line.skill, (skillTotals.get(line.skill) ?? 0) + line.tokens);
78
+ }
79
+ }
80
+ const skillCount = skillTotals.size;
81
+ const heaviest = [...skillTotals.entries()]
82
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
83
+ .slice(0, 3)
84
+ .map(([skill, tokens]) => `${skill} ${formatApproxTokens(tokens)}`);
85
+ const pathBits = ledger.paths.map((group) => {
86
+ const agents = group.agents.length > 0 ? group.agents.join('+') : 'none';
87
+ return `${group.kind}/${agents}`;
88
+ });
89
+ const lines = [];
90
+ lines.push(`Resident skill tax · ${ledger.method} · skills-manager`);
91
+ lines.push(`${formatApproxTokens(ledger.totalTokens)} tokens / message · ${skillCount} skill(s) · ${ledger.paths.length} path(s)`);
92
+ if (pathBits.length > 0)
93
+ lines.push(`Paths: ${pathBits.join(' · ')}`);
94
+ if (heaviest.length > 0)
95
+ lines.push(`Heaviest: ${heaviest.join(' · ')}`);
96
+ const archived = ledger.suggestions.archived.length;
97
+ const scattered = ledger.suggestions.scattered.length;
98
+ if (archived > 0 || scattered > 0) {
99
+ const watch = [];
100
+ if (archived > 0)
101
+ watch.push(`${archived} archived still distributed`);
102
+ if (scattered > 0)
103
+ watch.push(`${scattered} scattered`);
104
+ lines.push(`Watch: ${watch.join(' · ')}`);
105
+ }
106
+ if (ledger.unmanaged > 0)
107
+ lines.push(`${ledger.unmanaged} unmanaged skill(s) not counted`);
108
+ lines.push('Share: skills-manager cost --share');
109
+ lines.push('https://github.com/shenysun/skills-manager');
110
+ return `${lines.join('\n')}\n`;
111
+ }
112
+ //# sourceMappingURL=cost-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-output.js","sourceRoot":"","sources":["../../src/cli/cost-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAwC,MAAM,yCAAyC,CAAC;AAEnH;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,0BAA0B,MAAM,CAAC,MAAM,aAAa,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,MAAM,kBAAkB,CAAC,CAAC;IAC9J,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1E,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC,IAAI,aAAa,MAAM,OAAO,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3G,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5E,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACrH,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9G,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,iCAAiC,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,KAAK,MAAM,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5G,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;IACvC,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC;IAClI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;IAC/D,IAAI,KAAK,EAAE,CAAC;QACV,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACjC,CAAC;IACD,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;QACzE,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,eAAe,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACjG,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,WAAW,CAAC,eAAe,CAAC,MAAM,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,8BAA8B,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAChF,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,eAAe,EAAE,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACtF,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,+FAA+F,CAAC,CAAC;QAC5G,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YACzC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,WAAW,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClG,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;oBAEoB;AACpB,MAAM,UAAU,oBAAoB,CAAC,IAAoB;IACvD,OAAO,kBAAkB,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,wBAAwB,IAAI,CAAC,MAAM,GAAG,CAAC;AACjG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAkB;IAChD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IACD,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC;IACpC,MAAM,QAAQ,GAAG,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;SACxC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACvD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAEtE,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACzE,OAAO,GAAG,KAAK,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,wBAAwB,MAAM,CAAC,MAAM,mBAAmB,CAAC,CAAC;IACrE,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,uBAAuB,UAAU,eAAe,MAAM,CAAC,KAAK,CAAC,MAAM,UAAU,CAAC,CAAC;IACnI,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACtE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC;IACpD,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC;IACtD,IAAI,QAAQ,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,QAAQ,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,6BAA6B,CAAC,CAAC;QACvE,IAAI,SAAS,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,YAAY,CAAC,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,iCAAiC,CAAC,CAAC;IAC3F,KAAK,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACjD,KAAK,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IACzD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC"}
package/dist/cli/main.js CHANGED
@@ -13,6 +13,7 @@ import { GitHubApiClient } from '../infra/github-api-client.js';
13
13
  import { HttpDownloadClient } from '../infra/http-download-client.js';
14
14
  import { DetectionService, detectionLogPath } from '../core/services/detection-service.js';
15
15
  import { runBootstrap, managerSkillBundle } from './bootstrap.js';
16
+ import { renderCostLedger, renderCostShare, renderPresetCostLine } from './cost-output.js';
16
17
  const pkg = createRequire(import.meta.url)('../../package.json');
17
18
  const projectRoot = projectRootFromImportMeta(import.meta.url);
18
19
  function services(cmd, options = {}) {
@@ -140,6 +141,19 @@ function countErrored(s) {
140
141
  function staleOrErroredCount(s) {
141
142
  return Object.values(s.distribute.staleSummary()).reduce((total, count) => total + count, 0);
142
143
  }
144
+ /** Report-only security scan after install/update: one stderr line per skill so stdout JSON stays parseable. */
145
+ function remindSecurityScan(s, skills) {
146
+ if (skills.length === 0)
147
+ return;
148
+ for (const skill of skills) {
149
+ try {
150
+ console.error(s.security.formatSummaryLine(s.security.scanSkill(skill)));
151
+ }
152
+ catch {
153
+ /* scan must never break the install/update that already succeeded */
154
+ }
155
+ }
156
+ }
143
157
  function remindStaleTargets(s) {
144
158
  const stale = staleOrErroredCount(s);
145
159
  if (stale > 0) {
@@ -210,6 +224,18 @@ program.command('edit')
210
224
  });
211
225
  // Domain categories: a free-form multi-valued registry axis, orthogonal to the
212
226
  // frozen legacy `category` (ADR-0015). Tag edits never touch runtime dirs.
227
+ /** The applied-set column for `categories status`; a preset stamp rides beside the categories (US22), an unstamped path reads exactly as before. */
228
+ function formatAppliedSet(applied) {
229
+ if (applied === null)
230
+ return 'no filter (no category set applied)';
231
+ if ('all' in applied)
232
+ return 'no filter (--all applied)';
233
+ return `categories: ${applied.categories.join(', ')}${applied.preset ? ` (preset: ${applied.preset})` : ''}`;
234
+ }
235
+ /** Shared drift phrasing so `categories status` and `preset list` never diverge — honest, never implied healthy (US6). */
236
+ function driftNote(drift) {
237
+ return `${drift.length} skill(s) match the set but are undistributed: ${drift.join(', ')}`;
238
+ }
213
239
  const categories = program.command('categories').description('Tag skills with domain categories and inspect the hub vocabulary');
214
240
  categories.command('set')
215
241
  .description('Replace the whole category list for a skill (no categories given = clear)')
@@ -278,12 +304,9 @@ categories.command('status')
278
304
  }
279
305
  for (const item of paths) {
280
306
  const agents = item.agents.length > 0 ? item.agents.join(', ') : 'none';
281
- const set = item.applied === null
282
- ? 'no filter (no category set applied)'
283
- : 'all' in item.applied ? 'no filter (--all applied)' : `categories: ${item.applied.categories.join(', ')}`;
284
- console.log(`${item.runtimeDir} (agents: ${agents}) — ${set}`);
307
+ console.log(`${item.runtimeDir} (agents: ${agents}) — ${formatAppliedSet(item.applied)}`);
285
308
  if (item.drift.length > 0) {
286
- console.log(` drift: ${item.drift.length} skill(s) match the set but are undistributed: ${item.drift.join(', ')}`);
309
+ console.log(` drift: ${driftNote(item.drift)}`);
287
310
  const rerun = item.applied !== null && 'all' in item.applied
288
311
  ? 'skills-manager categories apply --all'
289
312
  : `skills-manager categories apply ${item.applied?.categories.join(' ')}`;
@@ -291,16 +314,80 @@ categories.command('status')
291
314
  }
292
315
  }
293
316
  });
294
- const provenance = program.command('provenance').description('Backfill provenance for source-less skills (lockfile evidence adoption, ADR-0011/0012)');
317
+ // Named presets: a saved category list — the re-appliable loading-set档位
318
+ // (ADR-0019). Storage only; switching runtime dirs is `preset apply`'s job.
319
+ const preset = program.command('preset').description('Manage named lists of domain categories (re-appliable agent loading sets, ADR-0019)');
320
+ preset.command('set')
321
+ .description('Replace the whole member list of a preset (overwrite-in-place when the name exists; categories need not exist in the hub yet)')
322
+ .argument('<name>')
323
+ .argument('<category...>')
324
+ .action((name, values, _opts, cmd) => {
325
+ const s = services(cmd);
326
+ const result = s.registry.setPreset(name, values);
327
+ s.activity.record({ action: 'cli-preset-set', summary: `set preset ${name}`, details: { preset: name, categories: values } });
328
+ print(result);
329
+ });
330
+ preset.command('list')
331
+ .description('List every preset with its member categories and mount footprint (drift on mounted paths reported as-is)')
332
+ .action((_opts, cmd) => {
333
+ const s = services(cmd);
334
+ const presets = s.registry.listPresets();
335
+ if (presets.length === 0) {
336
+ console.log('No presets yet — save one with `preset set <name> <category...>`.');
337
+ return;
338
+ }
339
+ // Mount footprint (US5/US6): which physical runtime paths currently carry
340
+ // each preset — zero mounts stay silent, and a drifted mount is reported,
341
+ // never implied healthy. Paths, not resolved-skill counts.
342
+ const statusPaths = s.distribute.categorySetStatus().paths;
343
+ for (const { name, categories } of presets) {
344
+ console.log(`${name}: ${categories.join(', ')}`);
345
+ const mounts = statusPaths.filter((item) => item.applied !== null && 'categories' in item.applied && item.applied.preset === name);
346
+ if (mounts.length === 0)
347
+ continue;
348
+ console.log(` mounted on ${mounts.length} runtime path(s): ${mounts.map((item) => item.runtimeDir).join(', ')}`);
349
+ for (const item of mounts) {
350
+ if (item.drift.length === 0)
351
+ continue;
352
+ console.log(` drift on ${item.runtimeDir}: ${driftNote(item.drift)}`);
353
+ }
354
+ }
355
+ });
356
+ preset.command('remove')
357
+ .description('Delete a preset and cascade-clear its name off every category-set record referencing it (applied categories kept, runtime untouched); reports how many paths the name was detached from')
358
+ .argument('<name>')
359
+ .action((name, _opts, cmd) => {
360
+ const s = services(cmd);
361
+ const { preset, detached } = s.distribute.removePresetCascade(name);
362
+ s.activity.record({ action: 'cli-preset-remove', summary: `removed preset ${name}, detached from ${detached} runtime path(s)`, details: { preset: name, detached } });
363
+ console.log(`Removed preset ${name} [${preset.categories.join(', ')}] — detached the name from ${detached} runtime path(s)`);
364
+ });
365
+ preset.command('apply')
366
+ .description("Rewrite the selected agents' runtime dirs to exactly the skills in the preset's categories (same strict semantics as `categories apply`: manager skill exempt, foreign untouched, uncategorized removed) and stamp the record with the preset name; success ends with the preset's resident-cost line")
367
+ .argument('<name>')
368
+ .option('-a, --agent <id...>', 'catalog agent ids (repeatable); defaults to the detected set')
369
+ .option('--json', 'machine-readable output for agents')
370
+ .action((name, opts, cmd) => {
371
+ const s = services(cmd);
372
+ const result = s.distribute.applyPreset(name, opts.agent);
373
+ const cost = s.cost.pathCost(result.paths.map((item) => item.runtimeDir));
374
+ const summary = `Applied preset ${name} [${result.categories.join(', ')}] to ${result.paths.length} runtime path(s)`;
375
+ s.activity.record({ action: 'cli-preset-apply', summary, details: { preset: name, categories: result.categories, agents: result.agents, costTokens: cost.tokens } });
376
+ if (opts.json)
377
+ return print({ ...result, cost });
378
+ console.log(summary);
379
+ console.log(renderPresetCostLine(cost));
380
+ });
381
+ const provenance = program.command('provenance').description('Backfill provenance for source-less skills (frontmatter & lockfile evidence adoption, ADR-0011/0012/0017)');
295
382
  provenance.command('adopt')
296
- .description('Adopt npx skills lockfile evidence onto legacy imported skills that have no source yet')
383
+ .description('Adopt frontmatter/lockfile evidence onto legacy imported skills that have no source yet')
297
384
  .option('--dry-run', 'print what would be adopted without touching the registry')
298
385
  .option('-s, --skill <skill...>', 'limit adoption to specific skills')
299
386
  .action((opts, cmd) => {
300
387
  const s = services(cmd);
301
388
  const result = s.provenance.adopt({ dryRun: Boolean(opts.dryRun), skills: opts.skill });
302
389
  if (!opts.dryRun)
303
- s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted lockfile evidence for ${result.adopted.map((item) => item.skill).join(', ') || 'nothing'}`, details: { adopted: result.adopted.map((item) => item.skill), skipped: result.skipped } });
390
+ s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted provenance evidence for ${result.adopted.map((item) => item.skill).join(', ') || 'nothing'}`, details: { adopted: result.adopted.map((item) => item.skill), skipped: result.skipped } });
304
391
  print(result);
305
392
  });
306
393
  provenance.command('list')
@@ -317,6 +404,108 @@ provenance.command('list')
317
404
  for (const skill of pending.locallyAuthored)
318
405
  console.log(` ${skill}`);
319
406
  });
407
+ // Hub git-ification for multi-machine sync (ADR-0020): each command thin, git
408
+ // stays the merge tool, and the operator keeps every git decision.
409
+ const sync = program.command('sync').description('Multi-machine sync via a git-ified hub (init/push/pull/status, ADR-0020)');
410
+ sync.command('init')
411
+ .description('Turn the hub into a git repository (or adopt an existing one) with the canonical .gitignore and a baseline commit; optionally attach a remote')
412
+ .option('--remote <url>', 'git remote URL to attach as origin (a local bare repo works too)')
413
+ .action((opts, cmd) => {
414
+ const s = services(cmd);
415
+ const result = s.sync.init({ remote: opts.remote });
416
+ s.activity.record({ action: 'cli-sync-init', summary: `${result.mode} hub git-ification`, details: result });
417
+ // Zero-omission report (US-4): every dimension states what happened, including "nothing".
418
+ console.log(result.mode === 'adopted' ? 'adopted: existing git repository kept (history preserved)' : 'initialized: fresh git repository created');
419
+ console.log(result.ignoreLinesAdded.length > 0
420
+ ? `.gitignore: appended ${result.ignoreLinesAdded.length} line(s): ${result.ignoreLinesAdded.join(', ')}`
421
+ : '.gitignore: nothing to append (canonical lines already present)');
422
+ console.log(result.baselineCommit ? `baseline commit: ${result.baselineCommit}` : 'baseline commit: none (tree already clean)');
423
+ if (result.remote === null)
424
+ console.log('remote origin: not set (pass --remote <url> to attach one)');
425
+ else
426
+ console.log(result.remote.attached ? `remote origin: attached ${result.remote.url}` : `remote origin: already ${result.remote.url}`);
427
+ console.log('Reminder: the hub will be pushed to this remote — confirm it contains no secrets (tokens, keys, .env files).');
428
+ });
429
+ sync.command('status')
430
+ .description('Read-only sync status: git-ification, remote, dirty files, ahead/behind (last fetch), last sync commit — zero network')
431
+ .option('--json', 'machine-readable output for agents')
432
+ .action((opts, cmd) => {
433
+ const result = services(cmd).sync.status();
434
+ if (opts.json)
435
+ return print(result);
436
+ // Read-only probe: no activity record — status itself must not dirty the hub.
437
+ console.log(`hub: ${result.home}`);
438
+ if (!result.gitified) {
439
+ console.log(`git: no — ${result.hint}`);
440
+ return;
441
+ }
442
+ console.log('git: yes');
443
+ console.log(result.remote === null
444
+ ? 'remote origin: not configured (attach one with `sync init --remote <url>`)'
445
+ : `remote origin: ${result.remote}`);
446
+ console.log(result.dirtyFiles === 0 ? 'dirty files: 0 (clean tree)' : `dirty files: ${result.dirtyFiles}`);
447
+ console.log(result.aheadBehind === null
448
+ ? 'ahead/behind: unknown (no upstream tracking ref yet — set by your first push)'
449
+ : `ahead/behind: ahead ${result.aheadBehind.ahead}, behind ${result.aheadBehind.behind} (based on last fetch — run \`git -C ${result.home} fetch\` to refresh)`);
450
+ console.log(result.lastCommit === null
451
+ ? 'last sync commit: none yet (the hub repo has no commits)'
452
+ : `last sync commit: ${result.lastCommit.sha} ${result.lastCommit.message}`);
453
+ });
454
+ sync.command('push')
455
+ .description('Stage all changes, make one summary commit, and push to the remote (no empty commits)')
456
+ .action((_opts, cmd) => {
457
+ const s = services(cmd);
458
+ const result = s.sync.push();
459
+ // Honest summary: an in-sync push sent nothing, and the log must not claim one did.
460
+ s.activity.record({
461
+ action: 'cli-sync-push',
462
+ summary: result.upToDate ? 'Sync push: already in sync, nothing sent' : `Pushed hub to ${result.remote}`,
463
+ details: { commit: result.commit?.sha ?? null, upToDate: result.upToDate },
464
+ });
465
+ if (result.upToDate) {
466
+ console.log('already in sync: no changes to commit, remote is up to date');
467
+ return;
468
+ }
469
+ console.log(result.commit === null
470
+ ? 'commit: none (working tree clean — pushed existing local commits)'
471
+ : `commit: ${result.commit.sha} ${result.commit.message}`);
472
+ console.log(`pushed to origin: ${result.remote}`);
473
+ });
474
+ sync.command('pull')
475
+ .description('Fetch and merge the remote hub (merge commits allowed; a dirty tree is refused, never stashed; user-data conflicts are yours to resolve — only the tool-generated empty registry placeholder is reconciled)')
476
+ .action((_opts, cmd) => {
477
+ const s = services(cmd);
478
+ const result = s.sync.pull();
479
+ s.activity.record({
480
+ action: 'cli-sync-pull',
481
+ summary: result.upToDate ? 'Sync pull: already up to date' : `Pulled hub from ${result.remote}`,
482
+ details: { upToDate: result.upToDate, registryPlaceholderSuperseded: result.registryPlaceholderSuperseded, ...result.stats },
483
+ });
484
+ if (result.upToDate) {
485
+ console.log('already up to date: nothing to merge from the remote');
486
+ return;
487
+ }
488
+ console.log(`pulled from origin: ${result.remote}`);
489
+ const { added, updated, removed } = result.stats;
490
+ const segments = [
491
+ ...(added.length > 0 ? [`${added.length} added (${added.join(', ')})`] : []),
492
+ ...(updated.length > 0 ? [`${updated.length} updated (${updated.join(', ')})`] : []),
493
+ ...(removed.length > 0 ? [`${removed.length} removed (${removed.join(', ')})`] : []),
494
+ ];
495
+ console.log(segments.length > 0 ? `merged skills: ${segments.join(', ')}` : 'merged skills: none changed');
496
+ console.log(result.stats.registryChanged ? 'registry: changed' : 'registry: unchanged');
497
+ if (result.registryPlaceholderSuperseded !== null) {
498
+ // Zero-omission (ticket 07): the merge clashed on registry.yaml and the
499
+ // tool settled it because the local side was provably its own init
500
+ // placeholder — say so plainly.
501
+ console.log('registry conflict: resolved by keeping the remote registry (the local side was the empty init placeholder, not user data)');
502
+ }
503
+ // Plain text, on purpose (US-14): the follow-up stays the operator's call —
504
+ // nothing is detected, distributed, or marked.
505
+ if (added.length > 0) {
506
+ console.log('Newly pulled skills are not distributed yet — run `distribute --skill <name>` when you want them available to your agents.');
507
+ }
508
+ });
320
509
  const backup = program.command('backup').description('Inspect and restore init backups (hub .backups/, 30-day retention)');
321
510
  backup.command('list')
322
511
  .description('List saved backups')
@@ -367,6 +556,50 @@ program.command('list')
367
556
  archived: Boolean(row.archived),
368
557
  })));
369
558
  });
559
+ // The reference layer (ADR-0017): a zero-retention read channel. Raw stdout
560
+ // under the `status` human-readable precedent — never JSON `print` — so agents
561
+ // can pipe the skill body; the archived notice rides stderr to keep stdout
562
+ // byte-exact. No borrow/cleanup lifecycle.
563
+ program.command('get')
564
+ .description("Print a hub skill's full SKILL.md to stdout (zero-retention read; provenance evidence rides the frontmatter)")
565
+ .argument('<skill>')
566
+ .option('--path', 'print the canonical hub directory (absolute) for read-only borrowing of sibling files, instead of the file body')
567
+ .action((skill, opts, cmd) => {
568
+ const s = services(cmd);
569
+ const target = s.get.resolve(skill);
570
+ const archivedNotice = `Note: ${skill} is archived — still readable; it only left the management plane (updates, distribution).`;
571
+ if (opts.path) {
572
+ console.log(target.dir);
573
+ console.log('Read-only borrow: do not write into this directory — the hub is canonical; changes go through skills-manager commands.');
574
+ if (target.archived)
575
+ console.log(archivedNotice);
576
+ return;
577
+ }
578
+ if (target.archived)
579
+ console.error(archivedNotice);
580
+ process.stdout.write(s.get.read(target));
581
+ });
582
+ // The resident-cost ledger (ADR-0018): a read-only account grouped by physical
583
+ // runtime path. Suggestions are report-only — each names the verbatim
584
+ // undistribute command and never runs it. No filter flags in v1: consumers
585
+ // filter the `--json` structure themselves.
586
+ program.command('cost')
587
+ .description('Show the resident context-cost ledger: per-runtime-path token cost of distributed skill frontmatter, with report-only recall suggestions')
588
+ .option('--json', 'machine-readable output for agents')
589
+ .option('--share', 'privacy-safe paste card (no absolute paths) for X / HN / chat')
590
+ .option('--top <n>', 'how many of the most expensive descriptions to list', '5')
591
+ .action((opts, cmd) => {
592
+ const s = services(cmd);
593
+ const top = Number.parseInt(opts.top, 10);
594
+ if (!Number.isInteger(top) || top < 0)
595
+ throw new Error(`--top expects a non-negative integer, got "${opts.top}"`);
596
+ if (opts.json && opts.share)
597
+ throw new Error('use either --json or --share, not both');
598
+ const ledger = s.cost.ledger(top);
599
+ if (opts.json)
600
+ return print(ledger);
601
+ process.stdout.write(opts.share ? renderCostShare(ledger) : renderCostLedger(ledger));
602
+ });
370
603
  program.command('add')
371
604
  .description('Discover from a source, then install selected skills')
372
605
  .argument('<source>', 'Git URL, GitHub owner/repo, GitHub tree URL, local path, local .zip archive, or http(s) URL of a single SKILL.md / zip / tar archive')
@@ -405,6 +638,7 @@ program.command('add')
405
638
  const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes), allowInsecureHttp, format });
406
639
  s.activity.record({ action: 'cli-add', summary: `Installed ${result.installed.join(', ')}`, details: { source, installed: result.installed } });
407
640
  print({ ...result, plan: { ...result.plan, source: redactCheckout(result.plan.source), selected: result.plan.selected.map(redactDiscovered) } });
641
+ remindSecurityScan(s, result.installed);
408
642
  remindStaleTargets(s);
409
643
  });
410
644
  program.command('update')
@@ -451,8 +685,19 @@ program.command('update')
451
685
  const result = opts.source ? s.update.updateSource(opts.source) : s.update.updateSkills(opts.skill);
452
686
  s.activity.record({ action: 'cli-update', summary: `Updated ${result.updated.join(', ')}`, details: result });
453
687
  print(result);
688
+ remindSecurityScan(s, result.updated);
454
689
  remindStaleTargets(s);
455
690
  });
691
+ const audit = program.command('audit').description('Report-only audits over hub skills (security today; more subcommands later)');
692
+ audit.command('security')
693
+ .description('Scan skill tree(s) for high-signal risks: shell exec, network calls, credential paths, secret-like strings (never blocks)')
694
+ .argument('[skills...]', 'canonical skill names; omit to scan every non-archived hub skill')
695
+ .action((skills, _opts, cmd) => {
696
+ const s = services(cmd);
697
+ const names = skills.length > 0 ? skills : s.registry.listSkills().map((skill) => skill.name);
698
+ const reports = s.security.scanSkills(names);
699
+ print(reports.length === 1 ? reports[0] : reports);
700
+ });
456
701
  const distribute = program.command('distribute')
457
702
  .description('Distribute hub skills to user or project runtime directories')
458
703
  .option('--to <kind>', 'user or project')