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.
@@ -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, bar } from '../lib/chart.js';
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: progress(),
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
- function progress() {
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: progress(),
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
+ }
@@ -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.1",
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": {