clud-bug 0.4.1 → 0.5.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/bin/clud-bug.js +7 -1
- package/lib/skills.js +103 -2
- package/lib/update.js +2 -1
- package/package.json +1 -1
package/bin/clud-bug.js
CHANGED
|
@@ -107,7 +107,13 @@ async function runInit(args) {
|
|
|
107
107
|
log(` search terms: ${signals.searchTerms.join(', ') || '(none)'}`);
|
|
108
108
|
|
|
109
109
|
const baseline = await loadBaseline(BASELINE_DIR);
|
|
110
|
-
|
|
110
|
+
const fromAgentSkills = baseline.filter((s) => s._source === 'agent-skills').length;
|
|
111
|
+
const sourceLabel = baseline.length === 0
|
|
112
|
+
? ''
|
|
113
|
+
: fromAgentSkills === baseline.length ? ' (from thrillmot/agent-skills)'
|
|
114
|
+
: fromAgentSkills === 0 ? ' (bundled fallback)'
|
|
115
|
+
: ` (${fromAgentSkills} from agent-skills, ${baseline.length - fromAgentSkills} bundled)`;
|
|
116
|
+
log(` baseline kit: ${baseline.length} specimens${sourceLabel}`);
|
|
111
117
|
|
|
112
118
|
let curated = [];
|
|
113
119
|
let searched = [];
|
package/lib/skills.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { mkdir, writeFile, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
3
5
|
|
|
4
6
|
const API_BASE = 'https://skills.sh/api/v1';
|
|
5
7
|
const MAX_SKILLS = 8;
|
|
6
8
|
const MANIFEST_FILE = '.clud-bug.json';
|
|
7
9
|
const MANIFEST_VERSION = 1;
|
|
8
10
|
|
|
11
|
+
// Canonical home for clud-bug's baseline skills.
|
|
12
|
+
// PINNED TO A COMMIT SHA, NOT `main`. This re-couples the trust boundary
|
|
13
|
+
// to clud-bug releases: a compromised commit on agent-skills@main cannot
|
|
14
|
+
// silently land in users' Claude review skills mid-cycle. To roll new
|
|
15
|
+
// skill content, bump BASELINE_SKILLS_REF below in the same clud-bug PR
|
|
16
|
+
// that ships the corresponding bundled fallback update.
|
|
17
|
+
// See thrillmot/agent-skills — skills.sh `skills/<name>/SKILL.md` layout.
|
|
18
|
+
const BASELINE_SKILLS_REF = '977e439ec861860351239ed89dd56edcd48cbf6b';
|
|
19
|
+
const AGENT_SKILLS_BASE = process.env.CLUD_BUG_AGENT_SKILLS_BASE
|
|
20
|
+
?? `https://raw.githubusercontent.com/thrillmot/agent-skills/${BASELINE_SKILLS_REF}/skills`;
|
|
21
|
+
const SKILL_FETCH_TIMEOUT_MS = 5000;
|
|
22
|
+
const SKILL_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
23
|
+
|
|
9
24
|
export class SkillsClient {
|
|
10
25
|
constructor({ fetch = globalThis.fetch, base, userAgent = 'clud-bug' } = {}) {
|
|
11
26
|
this.fetch = fetch;
|
|
@@ -78,7 +93,34 @@ export function rankAndCap(curated, searched, baseline, cap = MAX_SKILLS) {
|
|
|
78
93
|
return out;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
// Loads the baseline skills, preferring the pinned thrillmot/agent-skills
|
|
97
|
+
// commit and falling back to the bundled npm-package copy on any fetch failure.
|
|
98
|
+
// Returns the same shape as before, plus a `_source` of either 'agent-skills'
|
|
99
|
+
// or 'bundled' so the CLI can report which path was used.
|
|
100
|
+
//
|
|
101
|
+
// Options:
|
|
102
|
+
// - fetch — injectable for tests (defaults to globalThis.fetch)
|
|
103
|
+
// - cacheDir — where to cache fetched SKILL.md files (defaults to
|
|
104
|
+
// ~/.cache/clud-bug/skills/, skipped if null)
|
|
105
|
+
export async function loadBaseline(baselineDir, opts = {}) {
|
|
106
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
107
|
+
const cacheDir = opts.cacheDir === null ? null
|
|
108
|
+
: (opts.cacheDir ?? join(homedir(), '.cache', 'clud-bug', 'skills'));
|
|
109
|
+
|
|
110
|
+
// First, enumerate the bundled baseline skills (source of truth for which
|
|
111
|
+
// names exist). Then fetch each in parallel — sequential awaits would
|
|
112
|
+
// stack timeouts (3 baselines × 5s = 15s before fallback when offline).
|
|
113
|
+
const bundled = await readBundled(baselineDir);
|
|
114
|
+
const remotes = await Promise.all(
|
|
115
|
+
bundled.map((s) => tryFetchSkill(s.name, fetchImpl, cacheDir)),
|
|
116
|
+
);
|
|
117
|
+
return bundled.map((skill, i) => remotes[i]
|
|
118
|
+
? { ...skill, content: remotes[i], _source: 'agent-skills' }
|
|
119
|
+
: { ...skill, _source: 'bundled' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reads the bundled baseline from the npm-package directory.
|
|
123
|
+
async function readBundled(baselineDir) {
|
|
82
124
|
const skills = [];
|
|
83
125
|
let entries;
|
|
84
126
|
try {
|
|
@@ -92,7 +134,7 @@ export async function loadBaseline(baselineDir) {
|
|
|
92
134
|
skills.push({
|
|
93
135
|
source: 'clud-bug-baseline',
|
|
94
136
|
name: entry.name.replace(/\.md$/, ''),
|
|
95
|
-
description: '(
|
|
137
|
+
description: '(baseline)',
|
|
96
138
|
installs: 0,
|
|
97
139
|
kind: 'baseline',
|
|
98
140
|
content,
|
|
@@ -101,6 +143,65 @@ export async function loadBaseline(baselineDir) {
|
|
|
101
143
|
return skills;
|
|
102
144
|
}
|
|
103
145
|
|
|
146
|
+
// Try to read from cache, then fall back to network. Returns the SKILL.md
|
|
147
|
+
// content string on success, null on any failure (caller falls back to bundled).
|
|
148
|
+
async function tryFetchSkill(name, fetchImpl, cacheDir) {
|
|
149
|
+
// Cache lookup first.
|
|
150
|
+
if (cacheDir) {
|
|
151
|
+
const cached = await readFromCache(cacheDir, name);
|
|
152
|
+
if (cached !== null) return cached;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Network fetch with timeout covering BOTH the connection AND the body
|
|
156
|
+
// read (clearTimeout in finally guarantees the timer doesn't keep the
|
|
157
|
+
// event loop alive for up to 5s past a failed CLI run).
|
|
158
|
+
const url = `${AGENT_SKILLS_BASE}/${encodeURIComponent(name)}/SKILL.md`;
|
|
159
|
+
const ctrl = new AbortController();
|
|
160
|
+
const timer = setTimeout(() => ctrl.abort(), SKILL_FETCH_TIMEOUT_MS);
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetchImpl(url, { signal: ctrl.signal });
|
|
163
|
+
if (!res.ok) return null;
|
|
164
|
+
const content = await res.text();
|
|
165
|
+
if (!content || !content.trim()) return null;
|
|
166
|
+
if (cacheDir) await writeToCache(cacheDir, name, content);
|
|
167
|
+
return content;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
} finally {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function readFromCache(cacheDir, name) {
|
|
176
|
+
const path = cachePath(cacheDir, name);
|
|
177
|
+
try {
|
|
178
|
+
const st = await stat(path);
|
|
179
|
+
if (Date.now() - st.mtimeMs > SKILL_CACHE_TTL_MS) return null;
|
|
180
|
+
return await readFile(path, 'utf8');
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function writeToCache(cacheDir, name, content) {
|
|
187
|
+
try {
|
|
188
|
+
await mkdir(cacheDir, { recursive: true });
|
|
189
|
+
await writeFile(cachePath(cacheDir, name), content);
|
|
190
|
+
} catch {
|
|
191
|
+
// Cache write failures are non-fatal — we already have the content.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function cachePath(cacheDir, name) {
|
|
196
|
+
// Include AGENT_SKILLS_BASE in the hash so different upstream URLs (e.g.
|
|
197
|
+
// a fork via CLUD_BUG_AGENT_SKILLS_BASE, or a different pinned SHA after
|
|
198
|
+
// a clud-bug release) get different cache entries. Otherwise switching
|
|
199
|
+
// bases would silently return the previously-cached content from a
|
|
200
|
+
// different upstream — cross-base cache poisoning.
|
|
201
|
+
const hash = createHash('sha256').update(`${AGENT_SKILLS_BASE}\n${name}`).digest('hex').slice(0, 16);
|
|
202
|
+
return join(cacheDir, `${hash}.md`);
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
export async function writeSkills(targetDir, skills, client) {
|
|
105
206
|
await mkdir(targetDir, { recursive: true });
|
|
106
207
|
const written = [];
|
package/lib/update.js
CHANGED
|
@@ -21,6 +21,7 @@ export async function runUpdate({
|
|
|
21
21
|
baselineDir,
|
|
22
22
|
ourVersion,
|
|
23
23
|
refreshRemote = false,
|
|
24
|
+
loadBaselineOpts, // forwarded to loadBaseline (e.g. for tests: { fetch, cacheDir: null })
|
|
24
25
|
} = {}) {
|
|
25
26
|
if (!cwd || !templatesDir || !baselineDir || !ourVersion) {
|
|
26
27
|
throw new Error('runUpdate requires cwd, templatesDir, baselineDir, ourVersion');
|
|
@@ -51,7 +52,7 @@ export async function runUpdate({
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// 3. Refresh baseline skills (always controlled by clud-bug).
|
|
54
|
-
const baseline = await loadBaseline(baselineDir);
|
|
55
|
+
const baseline = await loadBaseline(baselineDir, loadBaselineOpts);
|
|
55
56
|
for (const skill of baseline) {
|
|
56
57
|
const skillPath = join(skillsDir, sanitize(skill.name), 'SKILL.md');
|
|
57
58
|
await maybeWrite(skillPath, skill.content, changed, unchanged, `baseline ${skill.name}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Claude PR review with project-aware skills. CLI installs a working GitHub Actions workflow and curates skills from skills.sh.",
|
|
5
5
|
"homepage": "https://cludbug.dev",
|
|
6
6
|
"bugs": "https://github.com/thrillmot/clud-bug/issues",
|