skills-manager-cli 2026.9.11 → 2026.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/README.zh-CN.md +13 -1
- package/dist/cli/cost-output.js +68 -0
- package/dist/cli/cost-output.js.map +1 -0
- package/dist/cli/insecure-http-confirm.js +20 -0
- package/dist/cli/insecure-http-confirm.js.map +1 -0
- package/dist/cli/main.js +256 -19
- package/dist/cli/main.js.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model/index.js.map +1 -1
- package/dist/core/ports/filesystem.js +5 -1
- package/dist/core/ports/filesystem.js.map +1 -1
- package/dist/core/ports/http-download.js +33 -0
- package/dist/core/ports/http-download.js.map +1 -0
- package/dist/core/services/archive-extract.js +212 -0
- package/dist/core/services/archive-extract.js.map +1 -0
- package/dist/core/services/archive-safety.js +137 -0
- package/dist/core/services/archive-safety.js.map +1 -0
- package/dist/core/services/cost-ledger-service.js +214 -0
- package/dist/core/services/cost-ledger-service.js.map +1 -0
- package/dist/core/services/detection-service.js +118 -29
- package/dist/core/services/detection-service.js.map +1 -1
- package/dist/core/services/distribute-service.js +58 -4
- package/dist/core/services/distribute-service.js.map +1 -1
- package/dist/core/services/doctor-service.js +13 -2
- package/dist/core/services/doctor-service.js.map +1 -1
- package/dist/core/services/frontmatter-evidence.js +99 -0
- package/dist/core/services/frontmatter-evidence.js.map +1 -0
- package/dist/core/services/frontmatter-mirror.js +151 -0
- package/dist/core/services/frontmatter-mirror.js.map +1 -0
- package/dist/core/services/get-service.js +55 -0
- package/dist/core/services/get-service.js.map +1 -0
- package/dist/core/services/index.js +26 -6
- package/dist/core/services/index.js.map +1 -1
- package/dist/core/services/init-service.js +15 -5
- package/dist/core/services/init-service.js.map +1 -1
- package/dist/core/services/install-service.js +62 -9
- package/dist/core/services/install-service.js.map +1 -1
- package/dist/core/services/marketplace-manifest.js +40 -0
- package/dist/core/services/marketplace-manifest.js.map +1 -0
- package/dist/core/services/migration-service.js +9 -4
- package/dist/core/services/migration-service.js.map +1 -1
- package/dist/core/services/provenance-service.js +14 -9
- package/dist/core/services/provenance-service.js.map +1 -1
- package/dist/core/services/registry-service.js +92 -7
- package/dist/core/services/registry-service.js.map +1 -1
- package/dist/core/services/skill-home-service.js +5 -1
- package/dist/core/services/skill-home-service.js.map +1 -1
- package/dist/core/services/source-service.js +358 -47
- package/dist/core/services/source-service.js.map +1 -1
- package/dist/core/services/sync-service.js +422 -0
- package/dist/core/services/sync-service.js.map +1 -0
- package/dist/core/services/tar-extract.js +275 -0
- package/dist/core/services/tar-extract.js.map +1 -0
- package/dist/core/services/tree-content-hash.js +38 -0
- package/dist/core/services/tree-content-hash.js.map +1 -0
- package/dist/core/services/update-service.js +14 -1
- package/dist/core/services/update-service.js.map +1 -1
- package/dist/core/services/url-payload.js +92 -0
- package/dist/core/services/url-payload.js.map +1 -0
- package/dist/core/services/wellknown-index.js +100 -0
- package/dist/core/services/wellknown-index.js.map +1 -0
- package/dist/dashboard/server/main.js +31 -3
- package/dist/dashboard/server/main.js.map +1 -1
- package/dist/dashboard-web/assets/index-Cay-QoI-.js +48 -0
- package/dist/dashboard-web/assets/{index-DbZ7PRHg.css → index-Upz8ZY12.css} +1 -1
- package/dist/dashboard-web/index.html +2 -2
- package/dist/infra/fs-skill-home.js +10 -0
- package/dist/infra/fs-skill-home.js.map +1 -1
- package/dist/infra/git-cli.js +139 -0
- package/dist/infra/git-cli.js.map +1 -1
- package/dist/infra/http-download-client.js +194 -0
- package/dist/infra/http-download-client.js.map +1 -0
- package/dist/infra/index.js +1 -0
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/runtime.js +19 -1
- package/dist/infra/runtime.js.map +1 -1
- package/dist/shared/redact.js +1 -1
- package/dist/shared/redact.js.map +1 -1
- package/dist/shared/text-distance.js +31 -0
- package/dist/shared/text-distance.js.map +1 -0
- package/dist/shared/validation.js +23 -2
- package/dist/shared/validation.js.map +1 -1
- package/docs/CLI.md +69 -1
- package/package.json +12 -3
- package/skills/skills-manager/SKILL.md +111 -19
- package/dist/dashboard-web/assets/index-DSQV4D7B.js +0 -47
package/README.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
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
6
|
|
|
7
|
+
> **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)).
|
|
8
|
+
|
|
9
|
+
**What makes it different:**
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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).
|
|
15
|
+
- **Provenance backfill** — imported skills without a source get one through lockfile-evidence adoption, agent-driven search, and your approval.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
7
18
|
## Quick Start
|
|
8
19
|
|
|
9
20
|
```sh
|
|
@@ -96,10 +107,11 @@ skills-manager provenance adopt # backfill lockfile evidence
|
|
|
96
107
|
skills-manager categories set my-skill frontend # tag with a domain category
|
|
97
108
|
skills-manager categories apply frontend -a claude-code # load only that domain
|
|
98
109
|
skills-manager categories status # applied sets + drift
|
|
110
|
+
skills-manager cost # resident context-cost ledger (report-only recall suggestions)
|
|
99
111
|
skills-manager archive old-skill
|
|
100
112
|
```
|
|
101
113
|
|
|
102
|
-
Sources
|
|
114
|
+
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
115
|
|
|
104
116
|
## Optional: global install & dashboard
|
|
105
117
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
在**对话里**管理你的 agent skills:一条 `npx` 命令装上 manager skill,之后一切都直接吩咐你的 agent——安装、导入、分发、更新、补齐来源。任何会加载 skills 的 agent 都能用(Claude Code、Codex、Cursor、…)。
|
|
6
6
|
|
|
7
|
+
> **CLI 优先、对话驱动。** 本项目是 `skills-manager-cli`——终端/npm 工具,附带可选的本地 web dashboard。它**不是**桌面应用;如果你找的是同名的桌面版,那是另一个项目([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager))。
|
|
8
|
+
|
|
9
|
+
**差异化在哪:**
|
|
10
|
+
|
|
11
|
+
- **一个库,所有 agent**——单一内容库(`~/.skills-manager`)把任意子集分发到 catalog 里的 70+ agent,按目标选软链接或拷贝,带副本过期检测与回滚。
|
|
12
|
+
- **五类源**——git 仓库与 URL、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、Claude 插件市场、well-known 发现索引。
|
|
13
|
+
- **更新不误报**——更新锚点是 skill 自身子目录的 git tree SHA,上游动了 skill 之外的文件不会点亮「可更新」。
|
|
14
|
+
- **常驻成本账本**——每个已分发 skill 的 frontmatter 都在按条消息计费;`skills-manager cost` 按运行时路径记账(近似口径,`≈` 展示)并给出只报告不执行的收回建议。未分发 = 零常驻。见 [ADR-0018](docs/adr/0018-resident-cost-ledger.md)。
|
|
15
|
+
- **来源可回填**——没有来源的导入 skill,经锁文件证据采纳、agent 搜索、你批准三层流程补齐 provenance。
|
|
16
|
+
- **manager skill**——所有管理动作都在 agent 对话里完成([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md));底层 CLI 是纯 JSON 引擎。
|
|
17
|
+
|
|
7
18
|
## 快速开始
|
|
8
19
|
|
|
9
20
|
```sh
|
|
@@ -96,10 +107,11 @@ skills-manager provenance adopt # 补采锁文件证据
|
|
|
96
107
|
skills-manager categories set my-skill 前端 # 打领域类别标签
|
|
97
108
|
skills-manager categories apply 前端 -a claude-code # 只加载该领域
|
|
98
109
|
skills-manager categories status # 已应用集合 + 漂移
|
|
110
|
+
skills-manager cost # 常驻上下文成本账本(只报告不执行的收回建议)
|
|
99
111
|
skills-manager archive old-skill
|
|
100
112
|
```
|
|
101
113
|
|
|
102
|
-
来源(source
|
|
114
|
+
来源(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
115
|
|
|
104
116
|
## 可选:全局安装与 dashboard
|
|
105
117
|
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
//# sourceMappingURL=cost-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-output.js","sourceRoot":"","sources":["../../src/cli/cost-output.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
/**
|
|
3
|
+
* The plain-http confirmation (spec source-formats, US-12): an http:// URL
|
|
4
|
+
* source downloads only when the user says so — `--yes` on the command line
|
|
5
|
+
* or an interactive y/N. A non-interactive session (agent context, ADR-0014)
|
|
6
|
+
* declines: the core's insecure_http_unconfirmed error then names the remedy.
|
|
7
|
+
*/
|
|
8
|
+
export async function confirmInsecureHttp(url) {
|
|
9
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
10
|
+
return false;
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
try {
|
|
13
|
+
const answer = (await rl.question(`Download ${url} over unencrypted http? [y/N] `)).trim().toLowerCase();
|
|
14
|
+
return answer === 'y' || answer === 'yes';
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
rl.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=insecure-http-confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insecure-http-confirm.js","sourceRoot":"","sources":["../../src/cli/insecure-http-confirm.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,wBAAwB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAW;IACnD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAChE,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,GAAG,gCAAgC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzG,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC;IAC5C,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|
package/dist/cli/main.js
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { createRuntimeServices, projectRootFromImportMeta } from '../infra/runtime.js';
|
|
5
|
-
import { normalizeGitSourceUrl } from '../core/services/source-service.js';
|
|
5
|
+
import { isInsecureHttpSource, normalizeGitSourceUrl } from '../core/services/source-service.js';
|
|
6
|
+
import { isUpdatableSourceType } from '../core/services/update-service.js';
|
|
7
|
+
import { parseFormatFlag } from '../core/services/url-payload.js';
|
|
8
|
+
import { confirmInsecureHttp } from './insecure-http-confirm.js';
|
|
6
9
|
import { SkillsManagerError } from '../shared/errors.js';
|
|
7
10
|
import { redactCheckout, redactDiscovered } from '../shared/redact.js';
|
|
8
11
|
import { NodeFileSystem } from '../infra/fs-skill-home.js';
|
|
9
12
|
import { GitHubApiClient } from '../infra/github-api-client.js';
|
|
13
|
+
import { HttpDownloadClient } from '../infra/http-download-client.js';
|
|
10
14
|
import { DetectionService, detectionLogPath } from '../core/services/detection-service.js';
|
|
11
15
|
import { runBootstrap, managerSkillBundle } from './bootstrap.js';
|
|
16
|
+
import { renderCostLedger, renderPresetCostLine } from './cost-output.js';
|
|
12
17
|
const pkg = createRequire(import.meta.url)('../../package.json');
|
|
13
18
|
const projectRoot = projectRootFromImportMeta(import.meta.url);
|
|
14
19
|
function services(cmd, options = {}) {
|
|
@@ -206,6 +211,18 @@ program.command('edit')
|
|
|
206
211
|
});
|
|
207
212
|
// Domain categories: a free-form multi-valued registry axis, orthogonal to the
|
|
208
213
|
// frozen legacy `category` (ADR-0015). Tag edits never touch runtime dirs.
|
|
214
|
+
/** The applied-set column for `categories status`; a preset stamp rides beside the categories (US22), an unstamped path reads exactly as before. */
|
|
215
|
+
function formatAppliedSet(applied) {
|
|
216
|
+
if (applied === null)
|
|
217
|
+
return 'no filter (no category set applied)';
|
|
218
|
+
if ('all' in applied)
|
|
219
|
+
return 'no filter (--all applied)';
|
|
220
|
+
return `categories: ${applied.categories.join(', ')}${applied.preset ? ` (preset: ${applied.preset})` : ''}`;
|
|
221
|
+
}
|
|
222
|
+
/** Shared drift phrasing so `categories status` and `preset list` never diverge — honest, never implied healthy (US6). */
|
|
223
|
+
function driftNote(drift) {
|
|
224
|
+
return `${drift.length} skill(s) match the set but are undistributed: ${drift.join(', ')}`;
|
|
225
|
+
}
|
|
209
226
|
const categories = program.command('categories').description('Tag skills with domain categories and inspect the hub vocabulary');
|
|
210
227
|
categories.command('set')
|
|
211
228
|
.description('Replace the whole category list for a skill (no categories given = clear)')
|
|
@@ -274,12 +291,9 @@ categories.command('status')
|
|
|
274
291
|
}
|
|
275
292
|
for (const item of paths) {
|
|
276
293
|
const agents = item.agents.length > 0 ? item.agents.join(', ') : 'none';
|
|
277
|
-
|
|
278
|
-
? 'no filter (no category set applied)'
|
|
279
|
-
: 'all' in item.applied ? 'no filter (--all applied)' : `categories: ${item.applied.categories.join(', ')}`;
|
|
280
|
-
console.log(`${item.runtimeDir} (agents: ${agents}) — ${set}`);
|
|
294
|
+
console.log(`${item.runtimeDir} (agents: ${agents}) — ${formatAppliedSet(item.applied)}`);
|
|
281
295
|
if (item.drift.length > 0) {
|
|
282
|
-
console.log(` drift: ${
|
|
296
|
+
console.log(` drift: ${driftNote(item.drift)}`);
|
|
283
297
|
const rerun = item.applied !== null && 'all' in item.applied
|
|
284
298
|
? 'skills-manager categories apply --all'
|
|
285
299
|
: `skills-manager categories apply ${item.applied?.categories.join(' ')}`;
|
|
@@ -287,16 +301,80 @@ categories.command('status')
|
|
|
287
301
|
}
|
|
288
302
|
}
|
|
289
303
|
});
|
|
290
|
-
|
|
304
|
+
// Named presets: a saved category list — the re-appliable loading-set档位
|
|
305
|
+
// (ADR-0019). Storage only; switching runtime dirs is `preset apply`'s job.
|
|
306
|
+
const preset = program.command('preset').description('Manage named lists of domain categories (re-appliable agent loading sets, ADR-0019)');
|
|
307
|
+
preset.command('set')
|
|
308
|
+
.description('Replace the whole member list of a preset (overwrite-in-place when the name exists; categories need not exist in the hub yet)')
|
|
309
|
+
.argument('<name>')
|
|
310
|
+
.argument('<category...>')
|
|
311
|
+
.action((name, values, _opts, cmd) => {
|
|
312
|
+
const s = services(cmd);
|
|
313
|
+
const result = s.registry.setPreset(name, values);
|
|
314
|
+
s.activity.record({ action: 'cli-preset-set', summary: `set preset ${name}`, details: { preset: name, categories: values } });
|
|
315
|
+
print(result);
|
|
316
|
+
});
|
|
317
|
+
preset.command('list')
|
|
318
|
+
.description('List every preset with its member categories and mount footprint (drift on mounted paths reported as-is)')
|
|
319
|
+
.action((_opts, cmd) => {
|
|
320
|
+
const s = services(cmd);
|
|
321
|
+
const presets = s.registry.listPresets();
|
|
322
|
+
if (presets.length === 0) {
|
|
323
|
+
console.log('No presets yet — save one with `preset set <name> <category...>`.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Mount footprint (US5/US6): which physical runtime paths currently carry
|
|
327
|
+
// each preset — zero mounts stay silent, and a drifted mount is reported,
|
|
328
|
+
// never implied healthy. Paths, not resolved-skill counts.
|
|
329
|
+
const statusPaths = s.distribute.categorySetStatus().paths;
|
|
330
|
+
for (const { name, categories } of presets) {
|
|
331
|
+
console.log(`${name}: ${categories.join(', ')}`);
|
|
332
|
+
const mounts = statusPaths.filter((item) => item.applied !== null && 'categories' in item.applied && item.applied.preset === name);
|
|
333
|
+
if (mounts.length === 0)
|
|
334
|
+
continue;
|
|
335
|
+
console.log(` mounted on ${mounts.length} runtime path(s): ${mounts.map((item) => item.runtimeDir).join(', ')}`);
|
|
336
|
+
for (const item of mounts) {
|
|
337
|
+
if (item.drift.length === 0)
|
|
338
|
+
continue;
|
|
339
|
+
console.log(` drift on ${item.runtimeDir}: ${driftNote(item.drift)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
preset.command('remove')
|
|
344
|
+
.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')
|
|
345
|
+
.argument('<name>')
|
|
346
|
+
.action((name, _opts, cmd) => {
|
|
347
|
+
const s = services(cmd);
|
|
348
|
+
const { preset, detached } = s.distribute.removePresetCascade(name);
|
|
349
|
+
s.activity.record({ action: 'cli-preset-remove', summary: `removed preset ${name}, detached from ${detached} runtime path(s)`, details: { preset: name, detached } });
|
|
350
|
+
console.log(`Removed preset ${name} [${preset.categories.join(', ')}] — detached the name from ${detached} runtime path(s)`);
|
|
351
|
+
});
|
|
352
|
+
preset.command('apply')
|
|
353
|
+
.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")
|
|
354
|
+
.argument('<name>')
|
|
355
|
+
.option('-a, --agent <id...>', 'catalog agent ids (repeatable); defaults to the detected set')
|
|
356
|
+
.option('--json', 'machine-readable output for agents')
|
|
357
|
+
.action((name, opts, cmd) => {
|
|
358
|
+
const s = services(cmd);
|
|
359
|
+
const result = s.distribute.applyPreset(name, opts.agent);
|
|
360
|
+
const cost = s.cost.pathCost(result.paths.map((item) => item.runtimeDir));
|
|
361
|
+
const summary = `Applied preset ${name} [${result.categories.join(', ')}] to ${result.paths.length} runtime path(s)`;
|
|
362
|
+
s.activity.record({ action: 'cli-preset-apply', summary, details: { preset: name, categories: result.categories, agents: result.agents, costTokens: cost.tokens } });
|
|
363
|
+
if (opts.json)
|
|
364
|
+
return print({ ...result, cost });
|
|
365
|
+
console.log(summary);
|
|
366
|
+
console.log(renderPresetCostLine(cost));
|
|
367
|
+
});
|
|
368
|
+
const provenance = program.command('provenance').description('Backfill provenance for source-less skills (frontmatter & lockfile evidence adoption, ADR-0011/0012/0017)');
|
|
291
369
|
provenance.command('adopt')
|
|
292
|
-
.description('Adopt
|
|
370
|
+
.description('Adopt frontmatter/lockfile evidence onto legacy imported skills that have no source yet')
|
|
293
371
|
.option('--dry-run', 'print what would be adopted without touching the registry')
|
|
294
372
|
.option('-s, --skill <skill...>', 'limit adoption to specific skills')
|
|
295
373
|
.action((opts, cmd) => {
|
|
296
374
|
const s = services(cmd);
|
|
297
375
|
const result = s.provenance.adopt({ dryRun: Boolean(opts.dryRun), skills: opts.skill });
|
|
298
376
|
if (!opts.dryRun)
|
|
299
|
-
s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted
|
|
377
|
+
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 } });
|
|
300
378
|
print(result);
|
|
301
379
|
});
|
|
302
380
|
provenance.command('list')
|
|
@@ -313,6 +391,108 @@ provenance.command('list')
|
|
|
313
391
|
for (const skill of pending.locallyAuthored)
|
|
314
392
|
console.log(` ${skill}`);
|
|
315
393
|
});
|
|
394
|
+
// Hub git-ification for multi-machine sync (ADR-0020): each command thin, git
|
|
395
|
+
// stays the merge tool, and the operator keeps every git decision.
|
|
396
|
+
const sync = program.command('sync').description('Multi-machine sync via a git-ified hub (init/push/pull/status, ADR-0020)');
|
|
397
|
+
sync.command('init')
|
|
398
|
+
.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')
|
|
399
|
+
.option('--remote <url>', 'git remote URL to attach as origin (a local bare repo works too)')
|
|
400
|
+
.action((opts, cmd) => {
|
|
401
|
+
const s = services(cmd);
|
|
402
|
+
const result = s.sync.init({ remote: opts.remote });
|
|
403
|
+
s.activity.record({ action: 'cli-sync-init', summary: `${result.mode} hub git-ification`, details: result });
|
|
404
|
+
// Zero-omission report (US-4): every dimension states what happened, including "nothing".
|
|
405
|
+
console.log(result.mode === 'adopted' ? 'adopted: existing git repository kept (history preserved)' : 'initialized: fresh git repository created');
|
|
406
|
+
console.log(result.ignoreLinesAdded.length > 0
|
|
407
|
+
? `.gitignore: appended ${result.ignoreLinesAdded.length} line(s): ${result.ignoreLinesAdded.join(', ')}`
|
|
408
|
+
: '.gitignore: nothing to append (canonical lines already present)');
|
|
409
|
+
console.log(result.baselineCommit ? `baseline commit: ${result.baselineCommit}` : 'baseline commit: none (tree already clean)');
|
|
410
|
+
if (result.remote === null)
|
|
411
|
+
console.log('remote origin: not set (pass --remote <url> to attach one)');
|
|
412
|
+
else
|
|
413
|
+
console.log(result.remote.attached ? `remote origin: attached ${result.remote.url}` : `remote origin: already ${result.remote.url}`);
|
|
414
|
+
console.log('Reminder: the hub will be pushed to this remote — confirm it contains no secrets (tokens, keys, .env files).');
|
|
415
|
+
});
|
|
416
|
+
sync.command('status')
|
|
417
|
+
.description('Read-only sync status: git-ification, remote, dirty files, ahead/behind (last fetch), last sync commit — zero network')
|
|
418
|
+
.option('--json', 'machine-readable output for agents')
|
|
419
|
+
.action((opts, cmd) => {
|
|
420
|
+
const result = services(cmd).sync.status();
|
|
421
|
+
if (opts.json)
|
|
422
|
+
return print(result);
|
|
423
|
+
// Read-only probe: no activity record — status itself must not dirty the hub.
|
|
424
|
+
console.log(`hub: ${result.home}`);
|
|
425
|
+
if (!result.gitified) {
|
|
426
|
+
console.log(`git: no — ${result.hint}`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
console.log('git: yes');
|
|
430
|
+
console.log(result.remote === null
|
|
431
|
+
? 'remote origin: not configured (attach one with `sync init --remote <url>`)'
|
|
432
|
+
: `remote origin: ${result.remote}`);
|
|
433
|
+
console.log(result.dirtyFiles === 0 ? 'dirty files: 0 (clean tree)' : `dirty files: ${result.dirtyFiles}`);
|
|
434
|
+
console.log(result.aheadBehind === null
|
|
435
|
+
? 'ahead/behind: unknown (no upstream tracking ref yet — set by your first push)'
|
|
436
|
+
: `ahead/behind: ahead ${result.aheadBehind.ahead}, behind ${result.aheadBehind.behind} (based on last fetch — run \`git -C ${result.home} fetch\` to refresh)`);
|
|
437
|
+
console.log(result.lastCommit === null
|
|
438
|
+
? 'last sync commit: none yet (the hub repo has no commits)'
|
|
439
|
+
: `last sync commit: ${result.lastCommit.sha} ${result.lastCommit.message}`);
|
|
440
|
+
});
|
|
441
|
+
sync.command('push')
|
|
442
|
+
.description('Stage all changes, make one summary commit, and push to the remote (no empty commits)')
|
|
443
|
+
.action((_opts, cmd) => {
|
|
444
|
+
const s = services(cmd);
|
|
445
|
+
const result = s.sync.push();
|
|
446
|
+
// Honest summary: an in-sync push sent nothing, and the log must not claim one did.
|
|
447
|
+
s.activity.record({
|
|
448
|
+
action: 'cli-sync-push',
|
|
449
|
+
summary: result.upToDate ? 'Sync push: already in sync, nothing sent' : `Pushed hub to ${result.remote}`,
|
|
450
|
+
details: { commit: result.commit?.sha ?? null, upToDate: result.upToDate },
|
|
451
|
+
});
|
|
452
|
+
if (result.upToDate) {
|
|
453
|
+
console.log('already in sync: no changes to commit, remote is up to date');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
console.log(result.commit === null
|
|
457
|
+
? 'commit: none (working tree clean — pushed existing local commits)'
|
|
458
|
+
: `commit: ${result.commit.sha} ${result.commit.message}`);
|
|
459
|
+
console.log(`pushed to origin: ${result.remote}`);
|
|
460
|
+
});
|
|
461
|
+
sync.command('pull')
|
|
462
|
+
.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)')
|
|
463
|
+
.action((_opts, cmd) => {
|
|
464
|
+
const s = services(cmd);
|
|
465
|
+
const result = s.sync.pull();
|
|
466
|
+
s.activity.record({
|
|
467
|
+
action: 'cli-sync-pull',
|
|
468
|
+
summary: result.upToDate ? 'Sync pull: already up to date' : `Pulled hub from ${result.remote}`,
|
|
469
|
+
details: { upToDate: result.upToDate, registryPlaceholderSuperseded: result.registryPlaceholderSuperseded, ...result.stats },
|
|
470
|
+
});
|
|
471
|
+
if (result.upToDate) {
|
|
472
|
+
console.log('already up to date: nothing to merge from the remote');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
console.log(`pulled from origin: ${result.remote}`);
|
|
476
|
+
const { added, updated, removed } = result.stats;
|
|
477
|
+
const segments = [
|
|
478
|
+
...(added.length > 0 ? [`${added.length} added (${added.join(', ')})`] : []),
|
|
479
|
+
...(updated.length > 0 ? [`${updated.length} updated (${updated.join(', ')})`] : []),
|
|
480
|
+
...(removed.length > 0 ? [`${removed.length} removed (${removed.join(', ')})`] : []),
|
|
481
|
+
];
|
|
482
|
+
console.log(segments.length > 0 ? `merged skills: ${segments.join(', ')}` : 'merged skills: none changed');
|
|
483
|
+
console.log(result.stats.registryChanged ? 'registry: changed' : 'registry: unchanged');
|
|
484
|
+
if (result.registryPlaceholderSuperseded !== null) {
|
|
485
|
+
// Zero-omission (ticket 07): the merge clashed on registry.yaml and the
|
|
486
|
+
// tool settled it because the local side was provably its own init
|
|
487
|
+
// placeholder — say so plainly.
|
|
488
|
+
console.log('registry conflict: resolved by keeping the remote registry (the local side was the empty init placeholder, not user data)');
|
|
489
|
+
}
|
|
490
|
+
// Plain text, on purpose (US-14): the follow-up stays the operator's call —
|
|
491
|
+
// nothing is detected, distributed, or marked.
|
|
492
|
+
if (added.length > 0) {
|
|
493
|
+
console.log('Newly pulled skills are not distributed yet — run `distribute --skill <name>` when you want them available to your agents.');
|
|
494
|
+
}
|
|
495
|
+
});
|
|
316
496
|
const backup = program.command('backup').description('Inspect and restore init backups (hub .backups/, 30-day retention)');
|
|
317
497
|
backup.command('list')
|
|
318
498
|
.description('List saved backups')
|
|
@@ -359,30 +539,87 @@ program.command('list')
|
|
|
359
539
|
name: row.name,
|
|
360
540
|
title: row.title,
|
|
361
541
|
category: row.category,
|
|
362
|
-
updatable: Boolean(row.source?.url && row.source?.subpath),
|
|
542
|
+
updatable: Boolean(row.source?.url && row.source?.subpath && isUpdatableSourceType(row.source.type)),
|
|
363
543
|
archived: Boolean(row.archived),
|
|
364
544
|
})));
|
|
365
545
|
});
|
|
546
|
+
// The reference layer (ADR-0017): a zero-retention read channel. Raw stdout
|
|
547
|
+
// under the `status` human-readable precedent — never JSON `print` — so agents
|
|
548
|
+
// can pipe the skill body; the archived notice rides stderr to keep stdout
|
|
549
|
+
// byte-exact. No borrow/cleanup lifecycle.
|
|
550
|
+
program.command('get')
|
|
551
|
+
.description("Print a hub skill's full SKILL.md to stdout (zero-retention read; provenance evidence rides the frontmatter)")
|
|
552
|
+
.argument('<skill>')
|
|
553
|
+
.option('--path', 'print the canonical hub directory (absolute) for read-only borrowing of sibling files, instead of the file body')
|
|
554
|
+
.action((skill, opts, cmd) => {
|
|
555
|
+
const s = services(cmd);
|
|
556
|
+
const target = s.get.resolve(skill);
|
|
557
|
+
const archivedNotice = `Note: ${skill} is archived — still readable; it only left the management plane (updates, distribution).`;
|
|
558
|
+
if (opts.path) {
|
|
559
|
+
console.log(target.dir);
|
|
560
|
+
console.log('Read-only borrow: do not write into this directory — the hub is canonical; changes go through skills-manager commands.');
|
|
561
|
+
if (target.archived)
|
|
562
|
+
console.log(archivedNotice);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (target.archived)
|
|
566
|
+
console.error(archivedNotice);
|
|
567
|
+
process.stdout.write(s.get.read(target));
|
|
568
|
+
});
|
|
569
|
+
// The resident-cost ledger (ADR-0018): a read-only account grouped by physical
|
|
570
|
+
// runtime path. Suggestions are report-only — each names the verbatim
|
|
571
|
+
// undistribute command and never runs it. No filter flags in v1: consumers
|
|
572
|
+
// filter the `--json` structure themselves.
|
|
573
|
+
program.command('cost')
|
|
574
|
+
.description('Show the resident context-cost ledger: per-runtime-path token cost of distributed skill frontmatter, with report-only recall suggestions')
|
|
575
|
+
.option('--json', 'machine-readable output for agents')
|
|
576
|
+
.option('--top <n>', 'how many of the most expensive descriptions to list', '5')
|
|
577
|
+
.action((opts, cmd) => {
|
|
578
|
+
const s = services(cmd);
|
|
579
|
+
const top = Number.parseInt(opts.top, 10);
|
|
580
|
+
if (!Number.isInteger(top) || top < 0)
|
|
581
|
+
throw new Error(`--top expects a non-negative integer, got "${opts.top}"`);
|
|
582
|
+
const ledger = s.cost.ledger(top);
|
|
583
|
+
if (opts.json)
|
|
584
|
+
return print(ledger);
|
|
585
|
+
process.stdout.write(renderCostLedger(ledger));
|
|
586
|
+
});
|
|
366
587
|
program.command('add')
|
|
367
588
|
.description('Discover from a source, then install selected skills')
|
|
368
|
-
.argument('<source>', 'Git URL, GitHub owner/repo, GitHub tree URL,
|
|
589
|
+
.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')
|
|
369
590
|
.option('--list', 'only list discovered skills')
|
|
370
591
|
.option('--all', 'install all discovered skills')
|
|
371
592
|
.option('-s, --skill <skill...>', 'skill name or source subpath to install')
|
|
372
|
-
.option('-
|
|
373
|
-
.
|
|
593
|
+
.option('-f, --format <format>', 'how to interpret a url payload: md, zip, or tar (tar covers tar/tar.gz/tgz) — the escape hatch when extension and Content-Type identify nothing')
|
|
594
|
+
.option('-y, --yes', 'overwrite existing skills without prompting; also confirms plain-http URL downloads')
|
|
595
|
+
.action(async (source, opts, cmd) => {
|
|
374
596
|
const s = services(cmd);
|
|
597
|
+
const format = parseFormatFlag(opts.format);
|
|
598
|
+
// A plain-http url source downloads only with explicit confirmation
|
|
599
|
+
// (US-12): --yes on the command line or an interactive y/N — https and
|
|
600
|
+
// every other source kind needs none.
|
|
601
|
+
const allowInsecureHttp = isInsecureHttpSource(s.source.normalize(source))
|
|
602
|
+
? Boolean(opts.yes) || await confirmInsecureHttp(source)
|
|
603
|
+
: false;
|
|
375
604
|
if (opts.list) {
|
|
376
605
|
return s.source.withCheckout(source, undefined, (checkout) => {
|
|
377
606
|
const discovered = s.source.discover(checkout);
|
|
378
|
-
|
|
379
|
-
|
|
607
|
+
// Marketplace sources present the two-level plugin → skills structure
|
|
608
|
+
// (US-15/17): every manifest plugin shows either its skills or why it
|
|
609
|
+
// cannot be consumed. Ordinary sources have no second level.
|
|
610
|
+
const view = s.source.marketplaceView(checkout, discovered);
|
|
611
|
+
return print({
|
|
612
|
+
source: redactCheckout(checkout),
|
|
613
|
+
...(view ? { plugins: view.plugins } : {}),
|
|
614
|
+
discovered: discovered.map(redactDiscovered),
|
|
615
|
+
});
|
|
616
|
+
}, { allowInsecureHttp, format });
|
|
380
617
|
}
|
|
381
618
|
if (!opts.all && (opts.skill || []).length === 0)
|
|
382
619
|
throw new Error('Use --all or --skill <name-or-subpath> to choose skills in this non-interactive CLI.');
|
|
383
620
|
// Empty selectors mean "everything discovered" — the --all semantics.
|
|
384
621
|
const selectors = opts.all ? [] : (opts.skill || []);
|
|
385
|
-
const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes) });
|
|
622
|
+
const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes), allowInsecureHttp, format });
|
|
386
623
|
s.activity.record({ action: 'cli-add', summary: `Installed ${result.installed.join(', ')}`, details: { source, installed: result.installed } });
|
|
387
624
|
print({ ...result, plan: { ...result.plan, source: redactCheckout(result.plan.source), selected: result.plan.selected.map(redactDiscovered) } });
|
|
388
625
|
remindStaleTargets(s);
|
|
@@ -396,7 +633,7 @@ program.command('update')
|
|
|
396
633
|
.action(async (opts, cmd) => {
|
|
397
634
|
const s = services(cmd);
|
|
398
635
|
if (opts.check) {
|
|
399
|
-
const detection = new DetectionService({ githubApi: new GitHubApiClient(), fs: new NodeFileSystem() });
|
|
636
|
+
const detection = new DetectionService({ githubApi: new GitHubApiClient(), http: new HttpDownloadClient(), fs: new NodeFileSystem() });
|
|
400
637
|
const listed = s.registry.listSkills({ includeArchived: false });
|
|
401
638
|
const outcomes = await detection.detect(s, listed);
|
|
402
639
|
const detectionLog = detectionLogPath(s.resolution.root);
|
|
@@ -532,8 +769,8 @@ program.command('migrate-consumers')
|
|
|
532
769
|
print(result);
|
|
533
770
|
});
|
|
534
771
|
program.command('rebuild-collections').description('Regenerate category collections').action((_opts, cmd) => { services(cmd).views.rebuildCollections(); print('collections rebuilt'); });
|
|
535
|
-
program.command('archive').description('Archive canonical skills without permanent deletion').argument('<skills...>').action((skills, cmd) => print(services(cmd).archive.archiveSkills(skills)));
|
|
536
|
-
program.command('adopt').description('Adopt a real directory from a generated view into canonical skills').argument('<view>').argument('<skill>').argument('[alsoConsumers...]').action((view, skill, alsoConsumers, cmd) => print(services(cmd).adopt.adopt(view, skill, alsoConsumers || [])));
|
|
772
|
+
program.command('archive').description('Archive canonical skills without permanent deletion').argument('<skills...>').action((skills, _opts, cmd) => print(services(cmd).archive.archiveSkills(skills)));
|
|
773
|
+
program.command('adopt').description('Adopt a real directory from a generated view into canonical skills').argument('<view>').argument('<skill>').argument('[alsoConsumers...]').action((view, skill, alsoConsumers, _opts, cmd) => print(services(cmd).adopt.adopt(view, skill, alsoConsumers || [])));
|
|
537
774
|
program.parseAsync(process.argv).catch((error) => {
|
|
538
775
|
console.error(error instanceof Error ? error.message : String(error));
|
|
539
776
|
process.exitCode = 1;
|