@lythos/skill-deck 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +2 -2
- package/src/cli.ts +5 -0
- package/src/link.ts +14 -7
- package/src/update.ts +154 -0
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ skills = ["github.com/anthropics/skills/skills/pdf"]
|
|
|
55
55
|
| Sync working set with `skill-deck.toml` | `bunx @lythos/skill-deck link` |
|
|
56
56
|
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck validate` |
|
|
57
57
|
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck add owner/repo` |
|
|
58
|
+
| Pull latest versions of declared skills | `bunx @lythos/skill-deck update` |
|
|
58
59
|
| Use a custom deck file or working dir | `bunx @lythos/skill-deck link --deck ./my-deck.toml --workdir /path/to/project` |
|
|
59
60
|
|
|
60
61
|
### Commands
|
|
@@ -64,6 +65,7 @@ skills = ["github.com/anthropics/skills/skills/pdf"]
|
|
|
64
65
|
| `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
|
|
65
66
|
| `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
|
|
66
67
|
| `add` | `<locator> [--via <backend>] [--deck <path>]` | Download skill to cold pool and append to skill-deck.toml. |
|
|
68
|
+
| `update` | `[--deck <path>]` | Pull latest versions of declared skills from upstream git repos. |
|
|
67
69
|
|
|
68
70
|
### Options
|
|
69
71
|
|
|
@@ -138,6 +140,7 @@ Different agents look for skills in different directories. `skill-deck.toml` con
|
|
|
138
140
|
|---------|-------|-----|
|
|
139
141
|
| `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck add github.com/owner/repo/skill` or clone manually into cold pool |
|
|
140
142
|
| `link` skips entries with warnings | Real files/directories exist in working set (not symlinks) | Delete the real directories in `working_set` and re-run `link`. Never create directories manually there |
|
|
143
|
+
| `update` reports "Not a git repository" | Skill was copied (not cloned) into cold pool | Re-clone with `git clone` or use `deck add` which clones by default |
|
|
141
144
|
| `link` refuses with "budget exceeded" | Declared skills > `max_cards` | Increase `max_cards` in `skill-deck.toml` or remove unused skills |
|
|
142
145
|
| `link` refuses with "unsafe working_set" | `working_set` resolves to `~` or `/` | Check `skill-deck.toml` has correct relative path (e.g. `.claude/skills/`) |
|
|
143
146
|
| Agent doesn't see skills after `link` | `working_set` path doesn't match agent's scan location | Claude Code: `.claude/skills/`; Cursor: `.cursor/skills/`; Kimi: check your platform docs. Set `working_set` correctly |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lythos/skill-deck",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Declarative skill deck governance
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Declarative skill deck governance — cold pool, working set, deny-by-default",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
7
7
|
"skill",
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { linkDeck } from './link.js'
|
|
3
3
|
import { validateDeck } from './validate.js'
|
|
4
4
|
import { addSkill } from './add.js'
|
|
5
|
+
import { updateDeck } from './update.js'
|
|
5
6
|
import { formatHelp } from './help.js'
|
|
6
7
|
|
|
7
8
|
const HELP_CONFIG = {
|
|
@@ -10,6 +11,7 @@ const HELP_CONFIG = {
|
|
|
10
11
|
commands: [
|
|
11
12
|
{ name: 'link', description: 'Sync working set with skill-deck.toml' },
|
|
12
13
|
{ name: 'add', description: 'Download skill to cold pool and add to deck', args: '<locator>' },
|
|
14
|
+
{ name: 'update', description: 'Pull latest versions of declared skills from upstream' },
|
|
13
15
|
{ name: 'validate', description: 'Validate deck configuration', args: '[deck.toml]' },
|
|
14
16
|
],
|
|
15
17
|
options: [
|
|
@@ -49,6 +51,9 @@ switch (command) {
|
|
|
49
51
|
await addSkill(locator, { via, deck: deckPath, workdir })
|
|
50
52
|
break
|
|
51
53
|
}
|
|
54
|
+
case 'update':
|
|
55
|
+
updateDeck(deckPath, workdir)
|
|
56
|
+
break
|
|
52
57
|
case 'validate':
|
|
53
58
|
validateDeck(deckPath, workdir)
|
|
54
59
|
break
|
package/src/link.ts
CHANGED
|
@@ -175,8 +175,10 @@ const deckRaw = readFileSync(DECK_PATH, "utf-8");
|
|
|
175
175
|
const deckHash = hashContent(deckRaw);
|
|
176
176
|
const deck = parseToml(deckRaw) as any;
|
|
177
177
|
|
|
178
|
-
const
|
|
179
|
-
const
|
|
178
|
+
const WORKING_SET_RAW = deck.deck?.working_set || ".claude/skills";
|
|
179
|
+
const COLD_POOL_RAW = deck.deck?.cold_pool || "~/.agents/skill-repos";
|
|
180
|
+
const WORKING_SET = expandHome(WORKING_SET_RAW, PROJECT_DIR);
|
|
181
|
+
const COLD_POOL = expandHome(COLD_POOL_RAW, PROJECT_DIR);
|
|
180
182
|
const MAX_CARDS = Number(deck.deck?.max_cards || 10);
|
|
181
183
|
|
|
182
184
|
// ── 收集声明 ────────────────────────────────────────────────
|
|
@@ -396,12 +398,17 @@ for (const item of declared) {
|
|
|
396
398
|
contentHash = hashContent(readFileSync(skillMdPath, "utf-8"));
|
|
397
399
|
} catch {}
|
|
398
400
|
|
|
401
|
+
// source: relative to cold_pool (non-transient) or project dir (transient)
|
|
402
|
+
const sourceRel = item.type === "transient"
|
|
403
|
+
? relative(PROJECT_DIR, item.sourcePath)
|
|
404
|
+
: relative(COLD_POOL, item.sourcePath);
|
|
405
|
+
|
|
399
406
|
linkedSkills.push({
|
|
400
407
|
name: item.name,
|
|
401
408
|
deck_niche: niche,
|
|
402
409
|
type: item.type,
|
|
403
|
-
source:
|
|
404
|
-
dest,
|
|
410
|
+
source: sourceRel,
|
|
411
|
+
dest: relative(PROJECT_DIR, dest),
|
|
405
412
|
content_hash: contentHash,
|
|
406
413
|
linked_at: new Date().toISOString(),
|
|
407
414
|
...(item.expires ? { expires: item.expires } : {}),
|
|
@@ -479,9 +486,9 @@ const constraints: ConstraintReport = {
|
|
|
479
486
|
const lock: SkillDeckLock = {
|
|
480
487
|
version: "1.0.0",
|
|
481
488
|
generated_at: new Date().toISOString(),
|
|
482
|
-
deck_source: { path: DECK_PATH, content_hash: deckHash },
|
|
483
|
-
working_set:
|
|
484
|
-
cold_pool:
|
|
489
|
+
deck_source: { path: relative(PROJECT_DIR, DECK_PATH), content_hash: deckHash },
|
|
490
|
+
working_set: WORKING_SET_RAW,
|
|
491
|
+
cold_pool: COLD_POOL_RAW,
|
|
485
492
|
skills: linkedSkills,
|
|
486
493
|
constraints,
|
|
487
494
|
};
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* deck-update.ts — Update declared skills from their upstream sources
|
|
4
|
+
*
|
|
5
|
+
* 读取 skill-deck.toml → 遍历声明的 skill → 对 git 来源执行 pull。
|
|
6
|
+
* 职责:让冷池跟上上游版本。
|
|
7
|
+
* 不做:下载新 skill(那是 add 的职责)、修改 deck.toml、同步 working set。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parse as parseToml } from "@iarna/toml";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { resolve, dirname, join, relative } from "node:path";
|
|
14
|
+
import { findDeckToml, expandHome, findSource } from "./link.js";
|
|
15
|
+
|
|
16
|
+
interface UpdateResult {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
status: "updated" | "up-to-date" | "skipped" | "failed" | "not-git";
|
|
20
|
+
message?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isGitRepo(dir: string): boolean {
|
|
24
|
+
return existsSync(join(dir, ".git"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function gitPull(dir: string): { status: "updated" | "up-to-date" | "failed"; message: string } {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync("git pull", {
|
|
30
|
+
cwd: dir,
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
timeout: 30000,
|
|
34
|
+
}).trim();
|
|
35
|
+
|
|
36
|
+
if (output.includes("Already up to date") || output.includes("Already up-to-date")) {
|
|
37
|
+
return { status: "up-to-date", message: output };
|
|
38
|
+
}
|
|
39
|
+
return { status: "updated", message: output };
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
const stderr = err.stderr?.toString() || err.message || "";
|
|
42
|
+
return { status: "failed", message: stderr.trim() };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function updateDeck(cliDeckPath?: string, cliWorkdir?: string): void {
|
|
47
|
+
const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
|
|
48
|
+
const DECK_PATH = cliDeck
|
|
49
|
+
? resolve(cliDeck)
|
|
50
|
+
: findDeckToml(process.cwd()) || resolve("skill-deck.toml");
|
|
51
|
+
|
|
52
|
+
if (!existsSync(DECK_PATH)) {
|
|
53
|
+
console.error(`❌ skill-deck.toml not found in ${process.cwd()}`);
|
|
54
|
+
console.error(`\nCreate one or specify a path: bunx @lythos/skill-deck link --deck /path/to/deck.toml`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const PROJECT_DIR = cliWorkdir ? resolve(cliWorkdir) : dirname(DECK_PATH);
|
|
59
|
+
const deckRaw = readFileSync(DECK_PATH, "utf-8");
|
|
60
|
+
const deck = parseToml(deckRaw) as any;
|
|
61
|
+
|
|
62
|
+
const COLD_POOL = expandHome(deck.deck?.cold_pool || "~/.agents/skill-repos", PROJECT_DIR);
|
|
63
|
+
|
|
64
|
+
// ── 收集声明 ────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const declared: { name: string; type: string }[] = [];
|
|
67
|
+
|
|
68
|
+
for (const section of ["innate", "tool", "combo"] as const) {
|
|
69
|
+
for (const name of (deck[section]?.skills || [])) {
|
|
70
|
+
if (!name || typeof name !== "string") continue;
|
|
71
|
+
declared.push({ name, type: section });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (declared.length === 0) {
|
|
76
|
+
console.log("📭 No skills declared in deck. Nothing to update.");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 执行更新 ────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const results: UpdateResult[] = [];
|
|
83
|
+
let updated = 0;
|
|
84
|
+
let upToDate = 0;
|
|
85
|
+
let skipped = 0;
|
|
86
|
+
let failed = 0;
|
|
87
|
+
|
|
88
|
+
for (const { name, type } of declared) {
|
|
89
|
+
const result = findSource(name, COLD_POOL, PROJECT_DIR);
|
|
90
|
+
|
|
91
|
+
if (result.error || !result.path) {
|
|
92
|
+
results.push({ name, path: "", status: "failed", message: result.error || "Skill not found" });
|
|
93
|
+
failed++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const path = result.path;
|
|
98
|
+
|
|
99
|
+
// localhost skills are user-managed; skip
|
|
100
|
+
const relativePath = relative(COLD_POOL, path);
|
|
101
|
+
if (relativePath.startsWith("localhost")) {
|
|
102
|
+
results.push({ name, path: relativePath, status: "skipped", message: "localhost skill — user-managed" });
|
|
103
|
+
skipped++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!isGitRepo(path)) {
|
|
108
|
+
results.push({ name, path: relativePath, status: "not-git", message: "Not a git repository" });
|
|
109
|
+
skipped++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pullResult = gitPull(path);
|
|
114
|
+
results.push({ name, path: relativePath, status: pullResult.status, message: pullResult.message });
|
|
115
|
+
|
|
116
|
+
if (pullResult.status === "updated") updated++;
|
|
117
|
+
else if (pullResult.status === "up-to-date") upToDate++;
|
|
118
|
+
else failed++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── 报告 ────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
console.log(`\n📦 Skill Update Report — ${declared.length} skill(s) checked`);
|
|
124
|
+
console.log(` Updated: ${updated} | Up-to-date: ${upToDate} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
125
|
+
console.log();
|
|
126
|
+
|
|
127
|
+
for (const r of results) {
|
|
128
|
+
const icon =
|
|
129
|
+
r.status === "updated" ? "🔄" :
|
|
130
|
+
r.status === "up-to-date" ? "✅" :
|
|
131
|
+
r.status === "skipped" ? "⏭️" :
|
|
132
|
+
r.status === "not-git" ? "📁" :
|
|
133
|
+
"❌";
|
|
134
|
+
console.log(`${icon} ${r.name}`);
|
|
135
|
+
if (r.message && r.status !== "up-to-date") {
|
|
136
|
+
const lines = r.message.split("\n").filter(l => l.trim());
|
|
137
|
+
for (const line of lines.slice(0, 3)) {
|
|
138
|
+
console.log(` ${line.trim()}`);
|
|
139
|
+
}
|
|
140
|
+
if (lines.length > 3) {
|
|
141
|
+
console.log(` ... (${lines.length - 3} more lines)`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (updated > 0) {
|
|
147
|
+
console.log(`\n💡 Run 'bunx @lythos/skill-deck link' to sync updated skills to working set.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (failed > 0) {
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|