@lythos/skill-deck 0.5.0 → 0.6.0
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 +39 -14
- package/package.json +1 -1
- package/src/add.ts +16 -1
- package/src/link.ts +80 -17
package/README.md
CHANGED
|
@@ -49,8 +49,9 @@ skills = ["report-generation-combo"]
|
|
|
49
49
|
|
|
50
50
|
| Situation | Command |
|
|
51
51
|
|-----------|---------|
|
|
52
|
-
| Sync
|
|
52
|
+
| Sync working set with `skill-deck.toml` | `bunx @lythos/skill-deck link` |
|
|
53
53
|
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck validate` |
|
|
54
|
+
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck add owner/repo` |
|
|
54
55
|
| Use a custom deck file or working dir | `bunx @lythos/skill-deck link --deck ./my-deck.toml --workdir /path/to/project` |
|
|
55
56
|
|
|
56
57
|
### Commands
|
|
@@ -59,6 +60,7 @@ skills = ["report-generation-combo"]
|
|
|
59
60
|
|---------|------|-------------|
|
|
60
61
|
| `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
|
|
61
62
|
| `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
|
|
63
|
+
| `add` | `<locator> [--via <backend>] [--deck <path>]` | Download skill to cold pool and append to skill-deck.toml. |
|
|
62
64
|
|
|
63
65
|
### Options
|
|
64
66
|
|
|
@@ -66,6 +68,11 @@ skills = ["report-generation-combo"]
|
|
|
66
68
|
|------|-------------|---------|
|
|
67
69
|
| `--deck <path>` | Path to skill-deck.toml | Find upward from cwd |
|
|
68
70
|
| `--workdir <dir>` | Working directory | cwd |
|
|
71
|
+
| `--via <backend>` | Download backend for `add`: `git` or `skills.sh` | `git` |
|
|
72
|
+
|
|
73
|
+
### Safety guards
|
|
74
|
+
|
|
75
|
+
`link` refuses to operate if `working_set` resolves to your home directory or root (`/`). It also only removes **symlinks** from the working set — real files or directories are skipped with a warning.
|
|
69
76
|
|
|
70
77
|
### Exit codes
|
|
71
78
|
|
|
@@ -106,25 +113,43 @@ bunx @lythos/skill-deck link
|
|
|
106
113
|
|---------|-----------|
|
|
107
114
|
| **Cold Pool** | All downloaded skills (`~/.agents/skill-repos/`). Agent cannot see here. |
|
|
108
115
|
| **skill-deck.toml** | Declares desired state: "this project uses these skills." |
|
|
109
|
-
| **`deck link`** | Reconciler. Makes
|
|
110
|
-
| **Working Set** | `.claude/skills/` —
|
|
116
|
+
| **`deck link`** | Reconciler. Makes the working set match the declaration. |
|
|
117
|
+
| **Working Set** | Symlinks only. Default: `.claude/skills/` — where agents scan for skills. |
|
|
111
118
|
| **deny-by-default** | Undeclared skills are physically absent from the working set. |
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
### Agent skill scan locations
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
The agent-visible **Skill** layer documentation is here:
|
|
117
|
-
[packages/lythoskill-deck/skill/SKILL.md](../../packages/lythoskill-deck/skill/SKILL.md)
|
|
122
|
+
Different agents look for skills in different directories. `skill-deck.toml` configures the working set to match your agent:
|
|
118
123
|
|
|
119
|
-
|
|
124
|
+
| Agent | Default skills directory |
|
|
125
|
+
|-------|--------------------------|
|
|
126
|
+
| Claude Code | `.claude/skills/` |
|
|
127
|
+
| Cursor | `.cursor/skills/` |
|
|
128
|
+
| Generic / custom | Configure `working_set` in `skill-deck.toml` |
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
> **If you are an agent**: verify where your platform scans for skills, then set `working_set` to that path before running `deck link`.
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
### Troubleshooting
|
|
133
|
+
|
|
134
|
+
| Symptom | Cause | Fix |
|
|
135
|
+
|---------|-------|-----|
|
|
136
|
+
| `❌ 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 |
|
|
137
|
+
| `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 |
|
|
138
|
+
| `link` refuses with "budget exceeded" | Declared skills > `max_cards` | Increase `max_cards` in `skill-deck.toml` or remove unused skills |
|
|
139
|
+
| `link` refuses with "unsafe working_set" | `working_set` resolves to `~` or `/` | Check `skill-deck.toml` has correct relative path (e.g. `.claude/skills/`) |
|
|
140
|
+
| 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 |
|
|
141
|
+
| Broken symlinks in working set | Skill moved or deleted from cold pool | Re-run `link` — it recreates symlinks automatically |
|
|
142
|
+
| `deck add` fails with 404 | Locator format wrong or repo doesn't exist | Format: `github.com/owner/repo/skill-name` (path to skill directory inside repo) |
|
|
143
|
+
| `skill-deck.toml not found` | Running `link` outside project tree | Run from project root, or use `--deck ./path/to/skill-deck.toml` |
|
|
144
|
+
|
|
145
|
+
## More Documentation
|
|
146
|
+
|
|
147
|
+
- **Skill layer** (agent-facing instructions):
|
|
148
|
+
[`packages/lythoskill-deck/skill/SKILL.md`](https://github.com/lythos-labs/lythoskill/blob/main/packages/lythoskill-deck/skill/SKILL.md)
|
|
149
|
+
- **Full project README** (ecosystem overview, cold pool setup):
|
|
150
|
+
[`README.md`](https://github.com/lythos-labs/lythoskill#readme)
|
|
151
|
+
- **Architecture** (thin-skill pattern, three-layer separation):
|
|
152
|
+
[`AGENTS.md`](https://github.com/lythos-labs/lythoskill/blob/main/AGENTS.md)
|
|
128
153
|
|
|
129
154
|
## License
|
|
130
155
|
|
package/package.json
CHANGED
package/src/add.ts
CHANGED
|
@@ -105,6 +105,11 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
105
105
|
process.exit(1)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
if (!existsSync(coldPool)) {
|
|
109
|
+
console.log(`📁 Creating cold pool: ${coldPool}`)
|
|
110
|
+
mkdirSync(coldPool, { recursive: true })
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lythoskill-deck-add-'))
|
|
109
114
|
const tmpRepo = join(tmpDir, 'repo')
|
|
110
115
|
|
|
@@ -113,13 +118,23 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
113
118
|
const skillsShLocator = `${parsed.owner}/${parsed.repo}`
|
|
114
119
|
console.log(`📦 Downloading via skills.sh: ${skillsShLocator}`)
|
|
115
120
|
execSync(`npx skills add ${skillsShLocator} -g`, { cwd: tmpDir, stdio: 'inherit' })
|
|
116
|
-
console.error(`⚠️ skills.sh backend: manual cold-pool placement may be needed`)
|
|
117
121
|
} else {
|
|
118
122
|
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
119
123
|
console.log(`📦 Cloning: ${gitUrl}`)
|
|
120
124
|
execSync(`git clone --depth 1 ${gitUrl} ${tmpRepo}`, { stdio: 'inherit' })
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
if (!existsSync(tmpRepo)) {
|
|
128
|
+
if (backend === 'skills.sh' || backend === 'vercel') {
|
|
129
|
+
console.error(`❌ skills.sh backend installs globally, not to cold pool.`)
|
|
130
|
+
console.error(` Please manually place the skill at: ${targetDir}`)
|
|
131
|
+
console.error(` Or use: deck add ${locator} --via git`)
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
console.error(`❌ Download failed: expected output not found at ${tmpRepo}`)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
mkdirSync(dirname(targetDir), { recursive: true })
|
|
124
139
|
renameSync(tmpRepo, targetDir)
|
|
125
140
|
|
package/src/link.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
15
15
|
symlinkSync, lstatSync, rmSync, writeFileSync,
|
|
16
16
|
} from "fs";
|
|
17
|
-
import { resolve, dirname, join } from "path";
|
|
17
|
+
import { resolve, dirname, join, basename, relative } from "path";
|
|
18
18
|
import { homedir } from "os";
|
|
19
19
|
import {
|
|
20
20
|
SkillDeckLockSchema,
|
|
@@ -152,7 +152,7 @@ for (const section of ["innate", "tool", "combo"] as const) {
|
|
|
152
152
|
if (!name || typeof name !== "string") continue;
|
|
153
153
|
const src = findSource(name, COLD_POOL, PROJECT_DIR);
|
|
154
154
|
if (!src) {
|
|
155
|
-
errors.push(`
|
|
155
|
+
errors.push(`Skill not found: ${name}`);
|
|
156
156
|
continue;
|
|
157
157
|
}
|
|
158
158
|
declared.push({ name, type: section, sourcePath: src });
|
|
@@ -165,37 +165,100 @@ for (const [key, value] of Object.entries(deck.transient || {})) {
|
|
|
165
165
|
if (!t?.path) continue;
|
|
166
166
|
const src = resolve(PROJECT_DIR, t.path);
|
|
167
167
|
if (!existsSync(src)) {
|
|
168
|
-
errors.push(`
|
|
168
|
+
errors.push(`Transient path does not exist: ${key} → ${src}`);
|
|
169
169
|
continue;
|
|
170
170
|
}
|
|
171
171
|
declared.push({ name: key, type: "transient", sourcePath: src, expires: t.expires });
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
if (errors.length > 0) {
|
|
175
|
-
for (const e of errors)
|
|
175
|
+
for (const e of errors) {
|
|
176
|
+
console.error(`❌ ${e}`);
|
|
177
|
+
// 智能引导:如果 skill 在工作集中以真实目录存在,提示移到冷池
|
|
178
|
+
const match = e.match(/^Skill not found: (.+)$/);
|
|
179
|
+
if (match) {
|
|
180
|
+
const skillName = match[1];
|
|
181
|
+
const wsEntry = join(WORKING_SET, skillName);
|
|
182
|
+
if (existsSync(wsEntry)) {
|
|
183
|
+
const st = lstatSync(wsEntry);
|
|
184
|
+
if (st.isDirectory() && !st.isSymbolicLink()) {
|
|
185
|
+
console.error(` → Found a real directory at ${relative(PROJECT_DIR, wsEntry)}`);
|
|
186
|
+
const cpRel = relative(PROJECT_DIR, COLD_POOL);
|
|
187
|
+
const cpHint = cpRel === "" ? `skills/${skillName}` : `${cpRel}/${skillName}`;
|
|
188
|
+
console.error(` Move it to your cold pool (${cpHint}) and retry.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
176
193
|
// 继续执行已找到的 skill,不因个别缺失中断全部
|
|
194
|
+
|
|
195
|
+
// 引导:如果 cold pool 为空,给出更明确的指引
|
|
196
|
+
const hasSkills = existsSync(COLD_POOL) && readdirSync(COLD_POOL).filter(e => !e.startsWith('.')).length > 0;
|
|
197
|
+
if (!hasSkills) {
|
|
198
|
+
console.error(`\n💡 Cold pool is empty. To add skills:`);
|
|
199
|
+
console.error(` bunx @lythos/skill-deck add github.com/owner/repo/skill`);
|
|
200
|
+
console.error(` # or manually: git clone <repo> ~/.agents/skill-repos/github.com/owner/repo`);
|
|
201
|
+
} else {
|
|
202
|
+
console.error(`\n💡 To install missing skills:`);
|
|
203
|
+
console.error(` bunx @lythos/skill-deck add github.com/owner/repo/skill`);
|
|
204
|
+
}
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
// ── 预算检查(硬约束,链接前检查)──────────────────────────
|
|
180
208
|
|
|
181
209
|
if (declared.length > MAX_CARDS) {
|
|
182
|
-
console.error(`❌
|
|
183
|
-
console.error(`
|
|
210
|
+
console.error(`❌ Budget exceeded: declared ${declared.length}, max ${MAX_CARDS}`);
|
|
211
|
+
console.error(` Reduce declarations in skill-deck.toml or increase max_cards`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── 工作目录安全 guard ──────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
const resolvedWorkingSet = resolve(WORKING_SET);
|
|
218
|
+
const resolvedHome = resolve(homedir());
|
|
219
|
+
const resolvedCwd = resolve(process.cwd());
|
|
220
|
+
const resolvedColdPool = resolve(COLD_POOL);
|
|
221
|
+
|
|
222
|
+
if (resolvedWorkingSet === resolvedHome || resolvedWorkingSet === "/") {
|
|
223
|
+
console.error(`❌ Refusing operation: working_set resolves to home or root directory (${resolvedWorkingSet})`);
|
|
224
|
+
console.error(` Check working_set in skill-deck.toml`);
|
|
184
225
|
process.exit(1);
|
|
185
226
|
}
|
|
186
227
|
|
|
228
|
+
const relWs = relative(resolvedColdPool, resolvedWorkingSet);
|
|
229
|
+
if (
|
|
230
|
+
resolvedWorkingSet.startsWith(resolvedColdPool + "/") &&
|
|
231
|
+
!relWs.split("/").some(p => p.startsWith("."))
|
|
232
|
+
) {
|
|
233
|
+
console.warn(`⚠️ working_set is inside cold_pool and not hidden — may be picked up by cold-pool scans`);
|
|
234
|
+
console.warn(` working_set: ${resolvedWorkingSet}`);
|
|
235
|
+
console.warn(` cold_pool: ${resolvedColdPool}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
187
238
|
// ── 收束 working set ────────────────────────────────────────
|
|
188
239
|
|
|
189
240
|
mkdirSync(WORKING_SET, { recursive: true });
|
|
190
241
|
|
|
191
|
-
//
|
|
242
|
+
// 清理未声明的条目(只删 symlink,防呆)
|
|
192
243
|
const declaredNames = new Set(declared.map(d => d.name.split("/")[0]));
|
|
193
244
|
try {
|
|
194
245
|
for (const entry of readdirSync(WORKING_SET)) {
|
|
195
246
|
if (entry.startsWith("_")) continue;
|
|
196
247
|
if (!declaredNames.has(entry)) {
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
const entryPath = join(WORKING_SET, entry);
|
|
249
|
+
try {
|
|
250
|
+
const st = lstatSync(entryPath);
|
|
251
|
+
if (!st.isSymbolicLink()) {
|
|
252
|
+
console.warn(`⚠️ Skipping non-symlink entry: ${entry}`);
|
|
253
|
+
console.warn(` → ${entry} is a real directory, not a symlink. Deck only manages symlinks.`);
|
|
254
|
+
const cpRel2 = relative(PROJECT_DIR, COLD_POOL);
|
|
255
|
+
const cpHint2 = cpRel2 === "" ? `skills/${entry}` : `${cpRel2}/${entry}`;
|
|
256
|
+
console.warn(` Move it to your cold pool (${cpHint2}) and run link again.`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
} catch { continue; }
|
|
260
|
+
rmSync(entryPath, { recursive: true, force: true });
|
|
261
|
+
console.log(` 🗑️ Removed: ${entry}`);
|
|
199
262
|
}
|
|
200
263
|
}
|
|
201
264
|
} catch {}
|
|
@@ -216,7 +279,7 @@ for (const item of declared) {
|
|
|
216
279
|
mkdirSync(dirname(dest), { recursive: true });
|
|
217
280
|
symlinkSync(item.sourcePath, dest);
|
|
218
281
|
} catch (err: any) {
|
|
219
|
-
console.error(`❌
|
|
282
|
+
console.error(`❌ Link failed: ${item.name}: ${err.message}`);
|
|
220
283
|
continue;
|
|
221
284
|
}
|
|
222
285
|
|
|
@@ -260,9 +323,9 @@ for (const s of linkedSkills) {
|
|
|
260
323
|
const days = Math.ceil((exp - now) / 86400000);
|
|
261
324
|
transientWarnings.push({ name: s.name, expires: s.expires, days_remaining: days });
|
|
262
325
|
if (days <= 0) {
|
|
263
|
-
console.warn(`⚠️
|
|
326
|
+
console.warn(`⚠️ Expired: ${s.name} (expires ${s.expires}) — evaluate if still needed`);
|
|
264
327
|
} else if (days <= 14) {
|
|
265
|
-
console.warn(`⏰
|
|
328
|
+
console.warn(`⏰ Expiring soon: ${s.name} (${days} days remaining)`);
|
|
266
329
|
}
|
|
267
330
|
}
|
|
268
331
|
|
|
@@ -282,7 +345,7 @@ const dirOverlaps: { dir: string; skills: string[] }[] = [];
|
|
|
282
345
|
for (const [dir, owners] of dirOwners) {
|
|
283
346
|
if (owners.length > 1) {
|
|
284
347
|
dirOverlaps.push({ dir, skills: owners });
|
|
285
|
-
console.warn(`⚠️
|
|
348
|
+
console.warn(`⚠️ Directory overlap: ${dir} ← ${owners.join(", ")}`);
|
|
286
349
|
}
|
|
287
350
|
}
|
|
288
351
|
|
|
@@ -297,7 +360,7 @@ for (let i = 0; i < allDirs.length; i++) {
|
|
|
297
360
|
const cross = parentOwners.filter(o => !childOwners.includes(o));
|
|
298
361
|
if (cross.length > 0) {
|
|
299
362
|
const msg = `${allDirs[i]} (${parentOwners.join(",")}) 包含 ${allDirs[j]} (${childOwners.join(",")})`;
|
|
300
|
-
console.warn(`⚠️
|
|
363
|
+
console.warn(`⚠️ Directory containment: ${msg}`);
|
|
301
364
|
dirOverlaps.push({ dir: `${allDirs[i]} ⊃ ${allDirs[j]}`, skills: [...new Set([...parentOwners, ...childOwners])] });
|
|
302
365
|
}
|
|
303
366
|
}
|
|
@@ -326,7 +389,7 @@ const lock: SkillDeckLock = {
|
|
|
326
389
|
|
|
327
390
|
const parsed = SkillDeckLockSchema.safeParse(lock);
|
|
328
391
|
if (!parsed.success) {
|
|
329
|
-
console.error("❌ Lock schema
|
|
392
|
+
console.error("❌ Lock schema validation failed:", JSON.stringify(parsed.error.format(), null, 2));
|
|
330
393
|
process.exit(1);
|
|
331
394
|
}
|
|
332
395
|
|
|
@@ -336,10 +399,10 @@ writeFileSync(LOCK_PATH, JSON.stringify(parsed.data, null, 2) + "\n");
|
|
|
336
399
|
// ── 报告 ────────────────────────────────────────────────────
|
|
337
400
|
|
|
338
401
|
console.log("");
|
|
339
|
-
console.log(`✅
|
|
402
|
+
console.log(`✅ Sync complete: ${linkedSkills.length}/${MAX_CARDS} skills`);
|
|
340
403
|
console.log(` lock: ${LOCK_PATH}`);
|
|
341
404
|
if (dirOverlaps.length > 0) {
|
|
342
|
-
console.log(` ⚠️ ${dirOverlaps.length}
|
|
405
|
+
console.log(` ⚠️ ${dirOverlaps.length} directory overlap(s) (see warnings above)`);
|
|
343
406
|
}
|
|
344
407
|
}
|
|
345
408
|
|