@sx4im/skillcheck 0.2.6 → 0.2.7
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 +88 -111
- package/dist/src/cli.js +2 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/corpus.js +2 -1
- package/dist/src/corpus.js.map +1 -1
- package/dist/src/eval.d.ts +1 -0
- package/dist/src/eval.js +8 -1
- package/dist/src/eval.js.map +1 -1
- package/dist/src/score.d.ts +1 -0
- package/dist/src/score.js +2 -0
- package/dist/src/score.js.map +1 -1
- package/dist/src/ui.d.ts +1 -0
- package/dist/src/ui.js +56 -17
- package/dist/src/ui.js.map +1 -1
- package/package.json +1 -3
- package/docs/skillcheck-cloud-build-plan.md +0 -644
- package/docs/skillcheck-cloud.md +0 -111
- package/examples/dashboard/README.md +0 -15
- package/examples/dashboard/index.html +0 -393
- package/examples/nvidia-proxy/README.md +0 -19
- package/examples/nvidia-proxy/server.mjs +0 -96
package/docs/skillcheck-cloud.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Skillcheck Cloud setup
|
|
2
|
-
|
|
3
|
-
Use this when you want users to install the CLI and run checks without configuring model-provider secrets locally.
|
|
4
|
-
|
|
5
|
-
## Architecture
|
|
6
|
-
|
|
7
|
-
```text
|
|
8
|
-
skillcheck CLI
|
|
9
|
-
-> Skillcheck Cloud API
|
|
10
|
-
-> model provider
|
|
11
|
-
|
|
12
|
-
Dashboard
|
|
13
|
-
-> user signs up
|
|
14
|
-
-> user creates a Skillcheck token
|
|
15
|
-
-> token is stored hashed in your database
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
The CLI only needs:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
skillcheck setup
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
The setup wizard asks for:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
https://api.yourdomain.com/v1
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
For token-gated private beta, users can additionally set:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
export SKILLCHECK_TOKEN=sk_live_...
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
If you want public free trials, the proxy can allow anonymous requests with strict rate limits and no token.
|
|
37
|
-
|
|
38
|
-
## API contract
|
|
39
|
-
|
|
40
|
-
The CLI expects an OpenAI-compatible endpoint:
|
|
41
|
-
|
|
42
|
-
```http
|
|
43
|
-
POST /v1/chat/completions
|
|
44
|
-
Authorization: Bearer <skillcheck-token>
|
|
45
|
-
Content-Type: application/json
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Response should match OpenAI chat completions enough for the `openai` Node SDK:
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"id": "chatcmpl_...",
|
|
53
|
-
"object": "chat.completion",
|
|
54
|
-
"created": 1780000000,
|
|
55
|
-
"model": "your-model",
|
|
56
|
-
"choices": [
|
|
57
|
-
{
|
|
58
|
-
"index": 0,
|
|
59
|
-
"message": {
|
|
60
|
-
"role": "assistant",
|
|
61
|
-
"content": "..."
|
|
62
|
-
},
|
|
63
|
-
"finish_reason": "stop"
|
|
64
|
-
}
|
|
65
|
-
],
|
|
66
|
-
"usage": {
|
|
67
|
-
"prompt_tokens": 1,
|
|
68
|
-
"completion_tokens": 1,
|
|
69
|
-
"total_tokens": 2
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Minimal proxy
|
|
75
|
-
|
|
76
|
-
The repo includes a tiny Node proxy in `examples/nvidia-proxy/`. It is useful for testing the `SKILLCHECK_API_URL` flow before building the dashboard.
|
|
77
|
-
|
|
78
|
-
Run it on a server:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
export NVIDIA_API_KEY=...
|
|
82
|
-
export SKILLCHECK_PROXY_TOKEN=dev-token
|
|
83
|
-
node examples/nvidia-proxy/server.mjs
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Point the CLI at it:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
export SKILLCHECK_API_URL=https://your-proxy.example.com/v1
|
|
90
|
-
export SKILLCHECK_TOKEN=dev-token
|
|
91
|
-
skillcheck check path/to/SKILL.md
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Dashboard requirements
|
|
95
|
-
|
|
96
|
-
- `users`: id, email, password/session provider, created_at.
|
|
97
|
-
- `tokens`: id, user_id, token_hash, prefix, created_at, last_used_at, revoked_at.
|
|
98
|
-
- `usage_events`: user_id, token_id, request_id, model, prompt_tokens, completion_tokens, created_at.
|
|
99
|
-
- Rate limit by token and IP.
|
|
100
|
-
- Store model-provider secrets only on the server.
|
|
101
|
-
- Never expose upstream provider secrets to the browser or CLI.
|
|
102
|
-
|
|
103
|
-
## First production path
|
|
104
|
-
|
|
105
|
-
1. Deploy the proxy API at `https://api.yourdomain.com/v1`.
|
|
106
|
-
2. Add dashboard auth with GitHub or email login.
|
|
107
|
-
3. Add “Create token” in the dashboard and show the token once.
|
|
108
|
-
4. Hash tokens before storing them.
|
|
109
|
-
5. Verify `Authorization: Bearer <token>` in the proxy.
|
|
110
|
-
6. Add rate limits and usage logging.
|
|
111
|
-
7. Ship the CLI with docs telling users to set `SKILLCHECK_API_URL` and `SKILLCHECK_TOKEN`.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Skillcheck dashboard
|
|
2
|
-
|
|
3
|
-
A single static file (`index.html`) where a user pastes their Skillcheck API URL and gets the commands to start using the CLI, a connection test, and an in-browser "with vs without skill" preview.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# open it directly
|
|
7
|
-
xdg-open index.html # macOS: open index.html · Windows: start index.html
|
|
8
|
-
|
|
9
|
-
# or serve it
|
|
10
|
-
npx --yes serve .
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Nothing to build or configure — the API URL and optional token are entered at runtime and stored in the browser's localStorage.
|
|
14
|
-
|
|
15
|
-
The live preview calls `POST <apiUrl>/chat/completions`, so the API must allow the page's origin via CORS. The bundled `../nvidia-proxy` already returns `access-control-allow-origin: *`, so the preview works even from `file://`. See [`../../dashboard.md`](../../dashboard.md) for the full write-up and [`../../docs/skillcheck-cloud.md`](../../docs/skillcheck-cloud.md) for the API contract.
|
|
@@ -1,393 +0,0 @@
|
|
|
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 & 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 & 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 tasks × K 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 === '&' ? '&' : c === '<' ? '<' : '>';
|
|
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>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# skillcheck NVIDIA proxy
|
|
2
|
-
|
|
3
|
-
This is the safe way to let CLI users run `skillcheck` without seeing your NVIDIA key.
|
|
4
|
-
|
|
5
|
-
Run the proxy on a server:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
export NVIDIA_API_KEY=...
|
|
9
|
-
node examples/nvidia-proxy/server.mjs
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Point the CLI at the proxy:
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
export SKILLCHECK_API_URL=https://your-proxy.example.com/v1
|
|
16
|
-
skillcheck check path/to/SKILL.md
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Do not publish `NVIDIA_API_KEY` inside the npm package. If the proxy is public, put it behind rate limiting, quotas, or authentication before sharing it broadly.
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createServer } from 'node:http';
|
|
3
|
-
|
|
4
|
-
const port = Number(process.env.PORT || 8787);
|
|
5
|
-
const nvidiaApiKey = process.env.NVIDIA_API_KEY?.trim();
|
|
6
|
-
const nvidiaBaseUrl = process.env.NVIDIA_BASE_URL?.trim() || 'https://integrate.api.nvidia.com/v1';
|
|
7
|
-
const proxyToken = process.env.SKILLCHECK_PROXY_TOKEN?.trim();
|
|
8
|
-
const maxBodyBytes = Number(process.env.MAX_BODY_BYTES || 5_000_000);
|
|
9
|
-
|
|
10
|
-
if (!nvidiaApiKey) {
|
|
11
|
-
throw new Error('Missing NVIDIA_API_KEY. Set it on the server, never inside the npm CLI.');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function writeJson(res, status, body) {
|
|
15
|
-
res.writeHead(status, {
|
|
16
|
-
'access-control-allow-headers': 'authorization, content-type',
|
|
17
|
-
'access-control-allow-methods': 'POST, OPTIONS',
|
|
18
|
-
'access-control-allow-origin': '*',
|
|
19
|
-
'content-type': 'application/json'
|
|
20
|
-
});
|
|
21
|
-
res.end(`${JSON.stringify(body)}\n`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readBody(req) {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const chunks = [];
|
|
27
|
-
let size = 0;
|
|
28
|
-
req.on('data', (chunk) => {
|
|
29
|
-
size += chunk.length;
|
|
30
|
-
if (size > maxBodyBytes) {
|
|
31
|
-
reject(new Error('Request body is too large'));
|
|
32
|
-
req.destroy();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
chunks.push(chunk);
|
|
36
|
-
});
|
|
37
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
38
|
-
req.on('error', reject);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isAuthorized(req) {
|
|
43
|
-
if (!proxyToken) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return req.headers.authorization === `Bearer ${proxyToken}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const server = createServer(async (req, res) => {
|
|
50
|
-
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
51
|
-
|
|
52
|
-
if (req.method === 'OPTIONS') {
|
|
53
|
-
writeJson(res, 204, {});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (req.method === 'GET' && url.pathname === '/health') {
|
|
58
|
-
writeJson(res, 200, { ok: true });
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (req.method !== 'POST' || url.pathname !== '/v1/chat/completions') {
|
|
63
|
-
writeJson(res, 404, { error: 'Not found' });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!isAuthorized(req)) {
|
|
68
|
-
writeJson(res, 401, { error: 'Unauthorized' });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const body = await readBody(req);
|
|
74
|
-
const upstream = await fetch(`${nvidiaBaseUrl}/chat/completions`, {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: {
|
|
77
|
-
authorization: `Bearer ${nvidiaApiKey}`,
|
|
78
|
-
'content-type': 'application/json'
|
|
79
|
-
},
|
|
80
|
-
body
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
res.writeHead(upstream.status, {
|
|
84
|
-
'access-control-allow-origin': '*',
|
|
85
|
-
'content-type': upstream.headers.get('content-type') || 'application/json'
|
|
86
|
-
});
|
|
87
|
-
res.end(await upstream.text());
|
|
88
|
-
} catch (error) {
|
|
89
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
-
writeJson(res, 500, { error: message });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
server.listen(port, () => {
|
|
95
|
-
console.log(`skillcheck NVIDIA proxy listening on http://localhost:${port}`);
|
|
96
|
-
});
|