skillgauntlet 1.3.1 → 1.3.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/bin/skillgauntlet.js +24 -19
- package/lib/i18n.js +6 -0
- package/lib/models.js +35 -0
- package/lib/progress.js +48 -0
- package/package.json +1 -1
package/bin/skillgauntlet.js
CHANGED
|
@@ -13,10 +13,11 @@ import { fingerprint, cacheFileFor, readCachedQueries, writeCachedQueries } from
|
|
|
13
13
|
import { openRun, querySetHash, readRun, readAnswers, validRecords } from '../lib/runs.js';
|
|
14
14
|
import { DEPTHS, minutesFor } from '../lib/depth.js';
|
|
15
15
|
import { ask, choose, select, confirm, say, closeUi, hasTerminal, setBack, Back } from '../lib/ui.js';
|
|
16
|
-
import { hostModels, unknownModel } from '../lib/models.js';
|
|
16
|
+
import { hostModels, unknownModel, codebuddyCatalog } from '../lib/models.js';
|
|
17
17
|
import { browseDir } from '../lib/browse.js';
|
|
18
18
|
import { withTicker } from '../lib/ticker.js';
|
|
19
|
-
import { renderResult
|
|
19
|
+
import { renderResult } from '../lib/chart.js';
|
|
20
|
+
import { makeProgress } from '../lib/progress.js';
|
|
20
21
|
import { setLang, detectLang, errorText, t } from '../lib/i18n.js';
|
|
21
22
|
import { resolveBin, backend, backendFor, usesOwnDeepseek, setNativeModels, HOSTS, checkModel, draftQueries, draftQuality, runTrial, requireHost, pingHost, setHostBin, binInstalled } from '../lib/trial.js';
|
|
22
23
|
import { HOST_CLI, searchHost, installCommand, installHost } from '../lib/findhost.js';
|
|
@@ -127,6 +128,13 @@ function notListed(model, known, label) {
|
|
|
127
128
|
say(`\n${t('modelUnknown', { model, host: label, list: list.slice(0, 8).join(', ') })}`);
|
|
128
129
|
return true;
|
|
129
130
|
}
|
|
131
|
+
// Models in CodeBuddy's cached catalog that its --help does not list for this account right now.
|
|
132
|
+
function catalogOnly(host) {
|
|
133
|
+
if (host !== 'codebuddy') return [];
|
|
134
|
+
const listed = new Set(modelsOf('codebuddy', 'CodeBuddy'));
|
|
135
|
+
return codebuddyCatalog().filter((m) => !listed.has(m) && !m.startsWith('custom-local:'));
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
function modelsOf(host, label) {
|
|
131
139
|
if (!modelCache.has(host)) {
|
|
132
140
|
const list = withTicker(waitText(t('waitModels', { host: label }), { slowAfter: 600 }), () => hostModels(host, resolveBin(host)));
|
|
@@ -205,13 +213,15 @@ async function runTriggerLayer({ skillDir, skillName, depth, model, host = 'clau
|
|
|
205
213
|
queries: { schema: 'skillgauntlet/queries/1', queries: qs.queries },
|
|
206
214
|
});
|
|
207
215
|
|
|
216
|
+
const prog = progress(trials.length);
|
|
208
217
|
try {
|
|
209
218
|
const outcome = await executeTrials({
|
|
210
219
|
run,
|
|
211
220
|
trials,
|
|
212
221
|
doTrial: ({ q }) => runTrial({ bin, skillDir, skillName, query: q.query, model, maxExplore: depth.maxExplore, host }),
|
|
213
|
-
onTrial:
|
|
222
|
+
onTrial: prog,
|
|
214
223
|
});
|
|
224
|
+
prog.finish();
|
|
215
225
|
const result = finishRun(run, { model, backend: backendFor(host, model), host, runs: depth.runs, expected: trials.length }, outcome.stopped);
|
|
216
226
|
say(` ${t('runSaved', { path: run.dir })}`);
|
|
217
227
|
if (outcome.stopped) reportStopped(outcome.stopped, run.dir, model);
|
|
@@ -219,6 +229,7 @@ async function runTriggerLayer({ skillDir, skillName, depth, model, host = 'clau
|
|
|
219
229
|
else finishedRuns.push(run.dir);
|
|
220
230
|
return result && { ...result, runDir: run.dir, queries: qs.queries };
|
|
221
231
|
} catch (err) {
|
|
232
|
+
prog.finish();
|
|
222
233
|
run.close(null, { interrupted: true });
|
|
223
234
|
throw err;
|
|
224
235
|
}
|
|
@@ -250,17 +261,7 @@ function archiveProbe({ skillDir, skillName, model, probe }) {
|
|
|
250
261
|
say(` ${t('runSaved', { path: run.dir })}`);
|
|
251
262
|
}
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
return ({ row, done, total }) => {
|
|
255
|
-
// On a terminal one line is redrawn in place; the verdicts of single trials belong to the result, not the wait.
|
|
256
|
-
if (process.stdout.isTTY) {
|
|
257
|
-
process.stdout.write(`\r\x1b[2K ${t('progressLine')} ${bar(done / total, 24)} ${done}/${total}${done >= total ? '\n' : ''}`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const mark = { triggered: '✓', not_triggered: '·', other_skill: '↷' }[row.verdict] || '!';
|
|
261
|
-
say(` [${done}/${total}] ${mark} ${row.queryId}#${row.run} → ${row.verdict}`);
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
+
const progress = makeProgress;
|
|
264
265
|
|
|
265
266
|
function reportIncomplete(n, dir, model) {
|
|
266
267
|
say(`
|
|
@@ -286,12 +287,14 @@ async function cmdResume(args) {
|
|
|
286
287
|
const runMeta = JSON.parse(fs.readFileSync(path.join(full, 'meta.json'), 'utf8'));
|
|
287
288
|
const runHost = runMeta.host || 'claude-code';
|
|
288
289
|
requireHost(runHost);
|
|
290
|
+
const resumeProgress = progress();
|
|
289
291
|
const result = await resumeRun({
|
|
290
292
|
dir: full,
|
|
291
293
|
bin: resolveBin((HOSTS[runHost] || HOSTS['claude-code']).bin),
|
|
292
294
|
want: { model: flag('--model'), runs: flag('--runs'), maxExplore: flag('--max-explore'), backend: backendFor(runHost, flag('--model') ?? runMeta.model) },
|
|
293
|
-
onTrial:
|
|
295
|
+
onTrial: resumeProgress,
|
|
294
296
|
});
|
|
297
|
+
resumeProgress.finish();
|
|
295
298
|
if (result.nothing) {
|
|
296
299
|
say(t('resumeNothing'));
|
|
297
300
|
return;
|
|
@@ -320,12 +323,14 @@ async function pickInstalled() {
|
|
|
320
323
|
}
|
|
321
324
|
|
|
322
325
|
// A model for hosts that have an account default: pick it, a known name, or type one. Returns null for the default.
|
|
323
|
-
async function pickModel(known = []) {
|
|
326
|
+
async function pickModel(known = [], more = []) {
|
|
324
327
|
if (!hasTerminal()) return (await ask(t('modelOptionalQ'), '')).trim() || null;
|
|
325
328
|
const other = '\u0000other';
|
|
329
|
+
// `known` is what the account can use now; `more` is in the host's catalog but not promised to the account
|
|
326
330
|
const v = await select(t('modelQ'), [
|
|
327
331
|
{ label: t('modelDefault'), value: '' },
|
|
328
|
-
...known.map((m) => ({ label: m, value: m })),
|
|
332
|
+
...known.map((m) => ({ label: m, hint: more.length ? t('modelAccountHint') : '', value: m })),
|
|
333
|
+
...more.map((m) => ({ label: m, hint: t('modelCatalogHint'), value: m })),
|
|
329
334
|
{ label: t('modelOther'), value: other },
|
|
330
335
|
], 0);
|
|
331
336
|
if (v === other) return (await ask(t('modelOptionalQ'), '')).trim() || null;
|
|
@@ -540,8 +545,8 @@ async function interactive() {
|
|
|
540
545
|
{ label: t('depthStandard'), hint: t('depthStandardHint'), value: 'standard' },
|
|
541
546
|
], 1);
|
|
542
547
|
S.depth = DEPTHS[depthId];
|
|
543
|
-
S.model = await pickModel(modelsOf('codebuddy', 'CodeBuddy'));
|
|
544
|
-
if (notListed(S.model, modelsOf('codebuddy', 'CodeBuddy'), 'CodeBuddy')) return AGAIN;
|
|
548
|
+
S.model = await pickModel(modelsOf('codebuddy', 'CodeBuddy'), catalogOnly('codebuddy'));
|
|
549
|
+
if (notListed(S.model, [...modelsOf('codebuddy', 'CodeBuddy'), ...catalogOnly('codebuddy')], 'CodeBuddy')) return AGAIN;
|
|
545
550
|
if (usesOwnDeepseek('codebuddy', S.model) && !process.env.DEEPSEEK_API_KEY) {
|
|
546
551
|
say(`\n${t('needDeepseekKey')}`);
|
|
547
552
|
return END;
|
package/lib/i18n.js
CHANGED
|
@@ -131,6 +131,8 @@ const MESSAGES = {
|
|
|
131
131
|
'chart.otherSkill': '改用别的技能:{name}',
|
|
132
132
|
'chart.more': '…还有 {n} 题,见完整报告',
|
|
133
133
|
'chart.wrongTitle': '不该触发却触发的 {n} 次',
|
|
134
|
+
modelAccountHint: '你的账号现在可用',
|
|
135
|
+
modelCatalogHint: 'CodeBuddy 目录里有,账号不一定能用',
|
|
134
136
|
modelUnknown: '{host} 的模型清单里没有“{model}”。它现在支持:{list}。请重新选一个,或选“默认模型”。',
|
|
135
137
|
updateAvail: '有新版本 {latest}(你现在用的是 {current})。',
|
|
136
138
|
updateQ: '要更新吗?',
|
|
@@ -527,6 +529,8 @@ const MESSAGES = {
|
|
|
527
529
|
'chart.otherSkill': 'used another skill: {name}',
|
|
528
530
|
'chart.more': '…{n} more, see the full report',
|
|
529
531
|
'chart.wrongTitle': '{n} times it triggered when it should not have',
|
|
532
|
+
modelAccountHint: 'usable with your account now',
|
|
533
|
+
modelCatalogHint: 'in CodeBuddy\'s catalog; your account may not have it',
|
|
530
534
|
modelUnknown: '{host} does not list a model called "{model}". It currently supports: {list}. Pick another, or choose the default model.',
|
|
531
535
|
updateAvail: 'A newer version {latest} is available (you are on {current}).',
|
|
532
536
|
updateQ: 'Update?',
|
|
@@ -971,6 +975,8 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
|
|
|
971
975
|
'chart.otherSkill': '別のスキルを使用:{name}',
|
|
972
976
|
'chart.more': '…ほか {n} 件は詳細レポートへ',
|
|
973
977
|
'chart.wrongTitle': '発動すべきでないのに発動した {n} 回',
|
|
978
|
+
modelAccountHint: 'このアカウントで今使えます',
|
|
979
|
+
modelCatalogHint: 'CodeBuddy のカタログにあります(アカウントで使えるとは限りません)',
|
|
974
980
|
modelUnknown: '{host} のモデル一覧に「{model}」はありません。現在対応:{list}。別のものを選ぶか、既定のモデルを選んでください。',
|
|
975
981
|
updateAvail: '新しいバージョン {latest} があります(現在は {current})。',
|
|
976
982
|
updateQ: '更新しますか?',
|
package/lib/models.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Model names a host will accept, read from the host itself so the list is the user's own (it depends on the
|
|
2
2
|
// account and the custom models they set up) and never goes stale inside this package. Any failure gives [].
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import { execFileSync, execSync } from 'node:child_process';
|
|
4
7
|
import { launchSpec } from './spawn.js';
|
|
5
8
|
|
|
@@ -44,3 +47,35 @@ export function unknownModel(model, known) {
|
|
|
44
47
|
if (!model || !known?.length || known.includes(model) || /^deepseek/i.test(model)) return null;
|
|
45
48
|
return known;
|
|
46
49
|
}
|
|
50
|
+
|
|
51
|
+
// `codebuddy --help` lists only what the account is signed in for right now (it can shrink to two entries), while the
|
|
52
|
+
// model catalog CodeBuddy last fetched stays in its local cache. Every string array called "models" inside those cache
|
|
53
|
+
// files is part of that catalog; the union with the help list is what the user remembers having been offered. A name
|
|
54
|
+
// from the catalog only is not promised to work for the account, so the list marks it and a failure shows the CLI's words.
|
|
55
|
+
export function catalogFromTexts(texts) {
|
|
56
|
+
const names = new Set();
|
|
57
|
+
const walk = (node, depth) => {
|
|
58
|
+
if (!node || depth > 8) return;
|
|
59
|
+
if (Array.isArray(node)) { node.forEach((n) => walk(n, depth + 1)); return; }
|
|
60
|
+
if (typeof node !== 'object') return;
|
|
61
|
+
for (const [k, v] of Object.entries(node)) {
|
|
62
|
+
if (k === 'models' && Array.isArray(v)) v.filter((x) => typeof x === 'string' && /^[\w.:-]{2,80}$/.test(x)).forEach((x) => names.add(x));
|
|
63
|
+
else walk(v, depth + 1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
for (const text of texts) { try { walk(JSON.parse(text), 0); } catch { /* not JSON */ } }
|
|
67
|
+
return [...names];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function codebuddyCatalog(dir = path.join(os.homedir(), '.codebuddy', 'local_storage')) {
|
|
71
|
+
const texts = [];
|
|
72
|
+
try {
|
|
73
|
+
for (const f of fs.readdirSync(dir)) {
|
|
74
|
+
if (!f.endsWith('.info')) continue;
|
|
75
|
+
const file = path.join(dir, f);
|
|
76
|
+
if (fs.statSync(file).size > 2 * 1024 * 1024) continue;
|
|
77
|
+
texts.push(fs.readFileSync(file, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
} catch { /* no cache: only the help list */ }
|
|
80
|
+
return catalogFromTexts(texts);
|
|
81
|
+
}
|
package/lib/progress.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The wait for the trials. On a terminal the bar is drawn at once (0 of N) and redrawn every second with the seconds
|
|
2
|
+
// elapsed, so there is something on screen from the moment the trials start rather than after the first one finishes;
|
|
3
|
+
// a trial can take many seconds. `total` is known when a run starts; a resumed run learns it from its first trial.
|
|
4
|
+
// Without a terminal each finished trial is one line. finish() stops the redraw and ends the line.
|
|
5
|
+
import { t } from './i18n.js';
|
|
6
|
+
import { bar } from './chart.js';
|
|
7
|
+
|
|
8
|
+
export function makeProgress(total = 0, {
|
|
9
|
+
tty = Boolean(process.stdout.isTTY),
|
|
10
|
+
write = (s) => process.stdout.write(s),
|
|
11
|
+
line = (s) => process.stdout.write(`${s}\n`),
|
|
12
|
+
now = Date.now,
|
|
13
|
+
every = setInterval,
|
|
14
|
+
stopEvery = clearInterval,
|
|
15
|
+
} = {}) {
|
|
16
|
+
const started = now();
|
|
17
|
+
let finished = 0;
|
|
18
|
+
let of = total;
|
|
19
|
+
let timer = null;
|
|
20
|
+
let open = false;
|
|
21
|
+
const draw = () => {
|
|
22
|
+
open = true;
|
|
23
|
+
write(`\r\x1b[2K ${t('progressLine')} ${bar(of ? finished / of : 0, 24)} ${finished}/${of || '?'} ${t('waitElapsed', { s: Math.floor((now() - started) / 1000) })}`);
|
|
24
|
+
};
|
|
25
|
+
const stop = () => {
|
|
26
|
+
if (timer) stopEvery(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
if (tty && open) { write('\n'); open = false; }
|
|
29
|
+
};
|
|
30
|
+
if (tty && total) {
|
|
31
|
+
draw();
|
|
32
|
+
timer = every(draw, 1000);
|
|
33
|
+
}
|
|
34
|
+
const onTrial = ({ row, done, total: n }) => {
|
|
35
|
+
finished = done;
|
|
36
|
+
of = n;
|
|
37
|
+
if (tty) {
|
|
38
|
+
if (!timer) timer = every(draw, 1000);
|
|
39
|
+
draw();
|
|
40
|
+
if (done >= n) stop();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const mark = { triggered: '✓', not_triggered: '·', other_skill: '↷' }[row.verdict] || '!';
|
|
44
|
+
line(` [${done}/${n}] ${mark} ${row.queryId}#${row.run} → ${row.verdict}`);
|
|
45
|
+
};
|
|
46
|
+
onTrial.finish = stop;
|
|
47
|
+
return onTrial;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillgauntlet",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Check an agent skill before you install it: context cost, permission surface, code risk, instruction hijacking, description quality, trigger rate and gain.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|