@sx4im/skillcheck 0.2.1 → 0.2.3

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.
@@ -0,0 +1,393 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Skillcheck — Connect &amp; Start</title>
7
+ <style>
8
+ :root {
9
+ --blue: #0a64ff;
10
+ --deep: #00227a;
11
+ --ink: #0b1220;
12
+ --muted: #5b6b7a;
13
+ --line: #d8e0f0;
14
+ --bg: #f5f8ff;
15
+ --card: #ffffff;
16
+ --ok: #138a36;
17
+ --bad: #c0233b;
18
+ --code: #0b1020;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0;
23
+ background: var(--bg);
24
+ color: var(--ink);
25
+ font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
26
+ }
27
+ a { color: var(--blue); }
28
+ .wrap { max-width: 860px; margin: 0 auto; padding: 28px 20px 80px; }
29
+ header { text-align: center; margin-bottom: 8px; }
30
+ .brand {
31
+ font-weight: 800; letter-spacing: -0.5px; font-size: 30px; color: var(--deep);
32
+ }
33
+ .brand span { color: var(--blue); }
34
+ .tag { color: var(--muted); margin-top: 2px; }
35
+ .badge {
36
+ display: inline-flex; align-items: center; gap: 7px; margin-top: 14px;
37
+ padding: 5px 12px; border-radius: 999px; font-size: 13px; font-weight: 600;
38
+ border: 1px solid var(--line); background: #fff;
39
+ }
40
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: #b8c2d8; }
41
+ .badge.connected .dot { background: var(--ok); }
42
+ .badge.connected { color: var(--ok); border-color: #bfe6c8; }
43
+ .badge.failed .dot { background: var(--bad); }
44
+ .badge.failed { color: var(--bad); border-color: #f0c4cc; }
45
+ .card {
46
+ background: var(--card); border: 1px solid var(--line); border-radius: 14px;
47
+ padding: 20px 22px; margin-top: 18px; box-shadow: 0 1px 2px rgba(10,40,120,0.04);
48
+ }
49
+ .step { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }
50
+ .step .n {
51
+ width: 26px; height: 26px; border-radius: 50%; background: var(--deep); color: #fff;
52
+ display: grid; place-items: center; font-size: 14px; font-weight: 700; flex: none;
53
+ }
54
+ .step h2 { font-size: 17px; margin: 0; }
55
+ label { display: block; font-weight: 600; font-size: 13px; margin: 12px 0 5px; }
56
+ .hint { color: var(--muted); font-weight: 400; }
57
+ input[type=text], input[type=password], textarea {
58
+ width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px;
59
+ font: inherit; background: #fbfcff; color: var(--ink);
60
+ }
61
+ input:focus, textarea:focus { outline: 2px solid var(--blue); border-color: var(--blue); }
62
+ textarea { resize: vertical; min-height: 90px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
63
+ .row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
64
+ button {
65
+ font: inherit; font-weight: 600; cursor: pointer; border-radius: 9px; padding: 10px 16px;
66
+ border: 1px solid var(--blue); background: var(--blue); color: #fff;
67
+ }
68
+ button.ghost { background: #fff; color: var(--blue); }
69
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
70
+ .status { margin-top: 12px; font-size: 13px; min-height: 18px; }
71
+ .status.ok { color: var(--ok); }
72
+ .status.bad { color: var(--bad); }
73
+ pre {
74
+ background: var(--code); color: #e7eefc; border-radius: 9px; padding: 12px 14px; margin: 8px 0 0;
75
+ overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
76
+ }
77
+ .cmd { position: relative; }
78
+ .cmd button.copy {
79
+ position: absolute; top: 8px; right: 8px; padding: 4px 10px; font-size: 12px;
80
+ background: #1b2540; border-color: #2c3a63; color: #cdd8f5;
81
+ }
82
+ .cmd .lbl { font-size: 12px; color: var(--muted); margin: 14px 0 0; font-weight: 600; }
83
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
84
+ @media (max-width: 620px) { .grid2 { grid-template-columns: 1fr; } }
85
+ .out h3 { font-size: 13px; margin: 0 0 6px; }
86
+ .out .body {
87
+ white-space: pre-wrap; background: #fbfcff; border: 1px solid var(--line); border-radius: 9px;
88
+ padding: 11px 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; min-height: 60px;
89
+ }
90
+ .meta { color: var(--muted); font-size: 12px; margin-top: 6px; }
91
+ .note { color: var(--muted); font-size: 13px; margin-top: 10px; }
92
+ .pill { display: inline-block; background: #eef3ff; color: var(--deep); border-radius: 6px; padding: 1px 7px; font-size: 12px; font-weight: 600; }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <div class="wrap">
97
+ <header>
98
+ <div class="brand">Skill<span>check</span></div>
99
+ <div class="tag">Drop a skill file. Get a verdict.</div>
100
+ <div id="badge" class="badge"><span class="dot"></span><span id="badgeText">Not connected</span></div>
101
+ </header>
102
+
103
+ <!-- Step 1: Connect -->
104
+ <section class="card">
105
+ <div class="step"><div class="n">1</div><h2>Connect your Skillcheck API URL</h2></div>
106
+ <label for="apiUrl">Skillcheck API URL <span class="hint">— paste the URL your workspace gave you</span></label>
107
+ <input id="apiUrl" type="text" placeholder="https://api.yourdomain.com/v1" autocomplete="off" spellcheck="false" />
108
+ <label for="token">Token <span class="hint">— optional; leave blank for open/anonymous proxies</span></label>
109
+ <input id="token" type="password" placeholder="sk_live_…" autocomplete="off" spellcheck="false" />
110
+ <div class="row">
111
+ <button id="saveBtn">Save &amp; connect</button>
112
+ <button id="testBtn" class="ghost">Test connection</button>
113
+ <button id="clearBtn" class="ghost">Clear</button>
114
+ </div>
115
+ <div id="connectStatus" class="status"></div>
116
+ </section>
117
+
118
+ <!-- Step 2: Start using -->
119
+ <section class="card">
120
+ <div class="step"><div class="n">2</div><h2>Start using skillcheck</h2></div>
121
+ <p class="note">Install once, point the CLI at your URL, then check any <span class="pill">SKILL.md</span> <span class="pill">AGENTS.md</span> <span class="pill">CLAUDE.md</span> <span class="pill">.cursorrules</span> or a folder containing one.</p>
122
+ <div id="commands"></div>
123
+ </section>
124
+
125
+ <!-- Step 3: Live preview -->
126
+ <section class="card">
127
+ <div class="step"><div class="n">3</div><h2>Quick check in the browser <span class="hint" style="font-weight:400">— optional preview</span></h2></div>
128
+ <p class="note">Runs your task once <strong>with</strong> and once <strong>without</strong> the skill so you can see the contrast and confirm the connection works. This is a single ungraded trial — for the real verdict (N&nbsp;tasks&nbsp;×&nbsp;K&nbsp;trials, blind grading, bootstrap CI) run <code>skillcheck check</code> in the CLI.</p>
129
+ <label for="skill">Skill instructions</label>
130
+ <textarea id="skill" placeholder="Paste the body of your SKILL.md here…"></textarea>
131
+ <label for="task">Task prompt</label>
132
+ <textarea id="task" placeholder="A concrete task the skill should help with…" style="min-height:64px"></textarea>
133
+ <div class="row">
134
+ <button id="runBtn">Run with vs without skill</button>
135
+ <span id="runStatus" class="status" style="align-self:center"></span>
136
+ </div>
137
+ <div class="grid2" style="margin-top:14px">
138
+ <div class="out">
139
+ <h3>With skill</h3>
140
+ <div id="withBody" class="body"></div>
141
+ <div id="withMeta" class="meta"></div>
142
+ </div>
143
+ <div class="out">
144
+ <h3>Without skill</h3>
145
+ <div id="noBody" class="body"></div>
146
+ <div id="noMeta" class="meta"></div>
147
+ </div>
148
+ </div>
149
+ <div id="overhead" class="note"></div>
150
+ </section>
151
+
152
+ <p class="note" style="text-align:center">
153
+ Settings are stored only in this browser (localStorage). Your token is never sent anywhere except your Skillcheck API URL.
154
+ </p>
155
+ </div>
156
+
157
+ <script>
158
+ var DEFAULT_MODEL = 'minimaxai/minimax-m2.7';
159
+ var KEY_URL = 'skillcheck.apiUrl';
160
+ var KEY_TOKEN = 'skillcheck.token';
161
+
162
+ var $ = function (id) { return document.getElementById(id); };
163
+
164
+ // Mirror of the CLI's config.normalizeApiUrl.
165
+ function normalizeApiUrl(value) {
166
+ var trimmed = (value || '').trim().replace(/\/+$/, '');
167
+ if (!trimmed) throw new Error('API URL cannot be empty.');
168
+ var url = new URL(trimmed);
169
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
170
+ throw new Error('API URL must start with https:// or http://');
171
+ }
172
+ if (url.pathname === '/' || url.pathname === '') url.pathname = '/v1';
173
+ return url.toString().replace(/\/+$/, '');
174
+ }
175
+
176
+ function savedUrl() { return localStorage.getItem(KEY_URL) || ''; }
177
+ function savedToken() { return localStorage.getItem(KEY_TOKEN) || ''; }
178
+
179
+ function setBadge(state, text) {
180
+ var b = $('badge');
181
+ b.className = 'badge' + (state ? ' ' + state : '');
182
+ $('badgeText').textContent = text;
183
+ }
184
+
185
+ function shellQuote(value) {
186
+ return "'" + String(value).replace(/'/g, "'\\''") + "'";
187
+ }
188
+
189
+ function renderCommands() {
190
+ var url = savedUrl();
191
+ var token = savedToken();
192
+ var shown = url || 'https://api.yourdomain.com/v1';
193
+ var blocks = [];
194
+ blocks.push({ lbl: 'Install (once)', cmd: 'npm install -g @sx4im/skillcheck' });
195
+
196
+ var envCmd = 'export SKILLCHECK_API_URL=' + shellQuote(shown);
197
+ if (token) envCmd += '\nexport SKILLCHECK_TOKEN=' + shellQuote(token);
198
+ blocks.push({ lbl: 'Point the CLI at your URL', cmd: envCmd });
199
+
200
+ blocks.push({ lbl: 'Or set it interactively', cmd: 'skillcheck setup\n# paste when prompted: ' + shown });
201
+ blocks.push({ lbl: 'Run your first check', cmd: 'skillcheck check path/to/SKILL.md' });
202
+
203
+ var html = '';
204
+ for (var i = 0; i < blocks.length; i++) {
205
+ var id = 'cmd' + i;
206
+ html +=
207
+ '<p class="lbl">' + blocks[i].lbl + '</p>' +
208
+ '<div class="cmd"><pre id="' + id + '">' + escapeHtml(blocks[i].cmd) + '</pre>' +
209
+ '<button class="copy" data-target="' + id + '">Copy</button></div>';
210
+ }
211
+ $('commands').innerHTML = html;
212
+ var btns = document.querySelectorAll('.copy');
213
+ for (var j = 0; j < btns.length; j++) {
214
+ btns[j].addEventListener('click', function () {
215
+ var el = $(this.getAttribute('data-target'));
216
+ var self = this;
217
+ copyText(el.textContent, function () {
218
+ var prev = self.textContent; self.textContent = 'Copied';
219
+ setTimeout(function () { self.textContent = prev; }, 1200);
220
+ });
221
+ });
222
+ }
223
+ }
224
+
225
+ function escapeHtml(s) {
226
+ return String(s).replace(/[&<>]/g, function (c) {
227
+ return c === '&' ? '&amp;' : c === '<' ? '&lt;' : '&gt;';
228
+ });
229
+ }
230
+
231
+ function copyText(text, done) {
232
+ if (navigator.clipboard && navigator.clipboard.writeText) {
233
+ navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text, done); });
234
+ } else {
235
+ fallbackCopy(text, done);
236
+ }
237
+ }
238
+ function fallbackCopy(text, done) {
239
+ var ta = document.createElement('textarea');
240
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
241
+ document.body.appendChild(ta); ta.select();
242
+ try { document.execCommand('copy'); } catch (e) {}
243
+ document.body.removeChild(ta); if (done) done();
244
+ }
245
+
246
+ function save() {
247
+ var status = $('connectStatus');
248
+ try {
249
+ var url = normalizeApiUrl($('apiUrl').value);
250
+ localStorage.setItem(KEY_URL, url);
251
+ var token = $('token').value.trim();
252
+ if (token) localStorage.setItem(KEY_TOKEN, token); else localStorage.removeItem(KEY_TOKEN);
253
+ $('apiUrl').value = url;
254
+ status.className = 'status ok';
255
+ status.textContent = 'Saved ' + url + '. Now test the connection or jump to step 2.';
256
+ setBadge('', 'Saved — not tested');
257
+ renderCommands();
258
+ } catch (e) {
259
+ status.className = 'status bad';
260
+ status.textContent = e.message;
261
+ }
262
+ }
263
+
264
+ function healthUrl(apiUrl) {
265
+ return new URL(apiUrl).origin + '/health';
266
+ }
267
+
268
+ function authHeaders() {
269
+ var h = { 'content-type': 'application/json' };
270
+ var token = savedToken();
271
+ if (token) h['authorization'] = 'Bearer ' + token;
272
+ return h;
273
+ }
274
+
275
+ function testConnection() {
276
+ var status = $('connectStatus');
277
+ var url = savedUrl();
278
+ if (!url) { save(); url = savedUrl(); }
279
+ if (!url) return;
280
+ status.className = 'status';
281
+ status.textContent = 'Testing ' + healthUrl(url) + ' …';
282
+ $('testBtn').disabled = true;
283
+ fetch(healthUrl(url), { method: 'GET', headers: authHeaders() })
284
+ .then(function (r) {
285
+ if (r.ok) {
286
+ setBadge('connected', 'Connected');
287
+ status.className = 'status ok';
288
+ status.textContent = 'Connected. ' + url + ' is reachable.';
289
+ } else {
290
+ setBadge('failed', 'Reachable, status ' + r.status);
291
+ status.className = 'status bad';
292
+ status.textContent = 'Server answered with HTTP ' + r.status + '. The URL is saved; if /health is not exposed, try a real check in step 3.';
293
+ }
294
+ })
295
+ .catch(function () {
296
+ setBadge('', 'Saved — not verified');
297
+ status.className = 'status';
298
+ status.textContent = 'Could not reach /health from the browser (often CORS or no /health route). Your URL is saved — verify from the CLI: skillcheck check path/to/SKILL.md';
299
+ })
300
+ .then(function () { $('testBtn').disabled = false; });
301
+ }
302
+
303
+ function clearAll() {
304
+ localStorage.removeItem(KEY_URL);
305
+ localStorage.removeItem(KEY_TOKEN);
306
+ $('apiUrl').value = '';
307
+ $('token').value = '';
308
+ $('connectStatus').className = 'status';
309
+ $('connectStatus').textContent = 'Cleared.';
310
+ setBadge('', 'Not connected');
311
+ renderCommands();
312
+ }
313
+
314
+ function extractContent(data) {
315
+ var msg = data && data.choices && data.choices[0] && data.choices[0].message;
316
+ if (!msg) return '';
317
+ return msg.content || msg.reasoning_content || msg.refusal || '';
318
+ }
319
+
320
+ function callModel(messages) {
321
+ var url = savedUrl();
322
+ if (!url) throw new Error('Save your API URL first (step 1).');
323
+ return fetch(url + '/chat/completions', {
324
+ method: 'POST',
325
+ headers: authHeaders(),
326
+ body: JSON.stringify({
327
+ model: DEFAULT_MODEL,
328
+ messages: messages,
329
+ temperature: 0.7,
330
+ max_tokens: 1200,
331
+ stream: false
332
+ })
333
+ }).then(function (r) {
334
+ return r.text().then(function (text) {
335
+ if (!r.ok) throw new Error('HTTP ' + r.status + ': ' + text.slice(0, 200));
336
+ var data;
337
+ try { data = JSON.parse(text); } catch (e) { throw new Error('Non-JSON response: ' + text.slice(0, 200)); }
338
+ return { content: extractContent(data), usage: data.usage || {} };
339
+ });
340
+ });
341
+ }
342
+
343
+ function runPreview() {
344
+ var skill = $('skill').value.trim();
345
+ var task = $('task').value.trim();
346
+ var status = $('runStatus');
347
+ if (!savedUrl()) { status.className = 'status bad'; status.textContent = 'Save your API URL first.'; return; }
348
+ if (!skill || !task) { status.className = 'status bad'; status.textContent = 'Add both a skill and a task.'; return; }
349
+
350
+ var withMessages = [
351
+ { role: 'system', content: 'You are completing an evaluation task. Apply the following skill instructions when relevant.\n\n' + skill },
352
+ { role: 'user', content: task }
353
+ ];
354
+ var noMessages = [{ role: 'user', content: task }];
355
+
356
+ $('runBtn').disabled = true;
357
+ status.className = 'status'; status.textContent = 'Running both arms…';
358
+ $('withBody').textContent = ''; $('noBody').textContent = '';
359
+ $('withMeta').textContent = ''; $('noMeta').textContent = ''; $('overhead').textContent = '';
360
+
361
+ Promise.all([callModel(withMessages), callModel(noMessages)])
362
+ .then(function (res) {
363
+ var w = res[0], n = res[1];
364
+ $('withBody').textContent = w.content || '(empty)';
365
+ $('noBody').textContent = n.content || '(empty)';
366
+ $('withMeta').textContent = 'prompt ' + (w.usage.prompt_tokens || 0) + ' · completion ' + (w.usage.completion_tokens || 0) + ' tokens';
367
+ $('noMeta').textContent = 'prompt ' + (n.usage.prompt_tokens || 0) + ' · completion ' + (n.usage.completion_tokens || 0) + ' tokens';
368
+ var overhead = (w.usage.prompt_tokens || 0) - (n.usage.prompt_tokens || 0);
369
+ $('overhead').textContent = 'Token cost of injecting the skill: ' + (overhead > 0 ? '+' + overhead : overhead) + ' prompt tokens.';
370
+ status.className = 'status ok'; status.textContent = 'Done.';
371
+ })
372
+ .catch(function (e) {
373
+ status.className = 'status bad';
374
+ status.textContent = e.message + (/Failed to fetch/i.test(e.message) ? ' (likely CORS — run the check from the CLI instead).' : '');
375
+ })
376
+ .then(function () { $('runBtn').disabled = false; });
377
+ }
378
+
379
+ function load() {
380
+ $('apiUrl').value = savedUrl();
381
+ $('token').value = savedToken();
382
+ renderCommands();
383
+ if (savedUrl()) setBadge('', 'Saved — not tested');
384
+ }
385
+
386
+ $('saveBtn').addEventListener('click', save);
387
+ $('testBtn').addEventListener('click', testConnection);
388
+ $('clearBtn').addEventListener('click', clearAll);
389
+ $('runBtn').addEventListener('click', runPreview);
390
+ load();
391
+ </script>
392
+ </body>
393
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sx4im/skillcheck",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Measure whether agent skills improve task performance.",
5
5
  "type": "module",
6
6
  "license": "MIT",