@bigstrider/transcodes-cli 0.1.0 → 0.3.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/README.md +21 -6
- package/dist/index.js +1180 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@ The plugins and their hooks authenticate to the Transcodes backend with a member
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# no install needed
|
|
11
|
-
npx @bigstrider/transcodes-cli
|
|
10
|
+
# no install needed — opens the dashboard
|
|
11
|
+
npx @bigstrider/transcodes-cli
|
|
12
12
|
|
|
13
13
|
# or global
|
|
14
14
|
npm install -g @bigstrider/transcodes-cli
|
|
15
|
-
transcodes
|
|
15
|
+
transcodes
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Works the same on macOS, Linux, and Windows (Node ≥ 20).
|
|
@@ -21,10 +21,25 @@ Works the same on macOS, Linux, and Windows (Node ≥ 20).
|
|
|
21
21
|
|
|
22
22
|
| Command | What it does |
|
|
23
23
|
|---------|--------------|
|
|
24
|
-
| `transcodes
|
|
25
|
-
| `transcodes
|
|
24
|
+
| `transcodes` | Opens the dashboard at `http://127.0.0.1:3847/` to paste, save, switch, label, or delete tokens (accepts `--port N` / `--no-open`). |
|
|
25
|
+
| `transcodes set <token> -l <label>` | Validates the JWT and saves it (label required) to `~/.transcodes/config.json` (dir `0700`, file `0600`), making it active. |
|
|
26
|
+
| `transcodes tokens` | Lists all saved tokens; the active one is marked with `*`. |
|
|
26
27
|
| `transcodes status` | Shows the active token source (env vs file) and its expiry. |
|
|
27
|
-
| `transcodes
|
|
28
|
+
| `transcodes reset` | Deletes all saved tokens. |
|
|
29
|
+
| `transcodes help` | Shows the full command list and usage. |
|
|
30
|
+
|
|
31
|
+
### Dashboard
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @bigstrider/transcodes-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Starts a small localhost server (127.0.0.1 only), opens your browser, and lets you save, switch, rename, or delete tokens without pasting them on the command line. Multiple tokens are kept in `~/.transcodes/config.json` under `token_list`, each with a label; the active one is stored as `token`.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
|
|
41
|
+
- `--port N` — bind to a specific port (default `3847`; increments if busy)
|
|
42
|
+
- `--no-open` — do not open the browser automatically
|
|
28
43
|
|
|
29
44
|
## Token precedence
|
|
30
45
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/dashboard.ts
|
|
4
|
+
import {
|
|
5
|
+
createServer
|
|
6
|
+
} from "http";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
3
10
|
// ../stepup-core/dist/jwt.js
|
|
4
11
|
var REQUIRED_AUDIENCE = "transcodes-mcp";
|
|
5
12
|
function isPlainObject(v) {
|
|
@@ -106,7 +113,7 @@ function transcodesConfigDir() {
|
|
|
106
113
|
function transcodesConfigFile() {
|
|
107
114
|
return path.join(transcodesConfigDir(), CONFIG_FILE_NAME);
|
|
108
115
|
}
|
|
109
|
-
function
|
|
116
|
+
function readRawConfig() {
|
|
110
117
|
let raw;
|
|
111
118
|
try {
|
|
112
119
|
raw = readFileSync(transcodesConfigFile(), "utf8");
|
|
@@ -122,29 +129,148 @@ function readTokenFromFile() {
|
|
|
122
129
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
123
130
|
return null;
|
|
124
131
|
}
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
function writeRawConfig(config) {
|
|
135
|
+
const dir = transcodesConfigDir();
|
|
136
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
137
|
+
writeFileSync(transcodesConfigFile(), JSON.stringify(config), {
|
|
138
|
+
mode: 384
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function normalizeToken(v) {
|
|
142
|
+
if (typeof v !== "string")
|
|
127
143
|
return null;
|
|
128
|
-
const trimmed =
|
|
144
|
+
const trimmed = v.trim();
|
|
145
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
146
|
+
}
|
|
147
|
+
function normalizeLabel(v) {
|
|
148
|
+
if (typeof v !== "string")
|
|
149
|
+
return null;
|
|
150
|
+
const trimmed = v.trim();
|
|
129
151
|
return trimmed.length > 0 ? trimmed : null;
|
|
130
152
|
}
|
|
131
|
-
function
|
|
153
|
+
function normalizeRecord(item) {
|
|
154
|
+
if (typeof item === "string") {
|
|
155
|
+
const token = normalizeToken(item);
|
|
156
|
+
return token ? { token, label: null } : null;
|
|
157
|
+
}
|
|
158
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
159
|
+
const obj = item;
|
|
160
|
+
const token = normalizeToken(obj.token);
|
|
161
|
+
if (!token)
|
|
162
|
+
return null;
|
|
163
|
+
return { token, label: normalizeLabel(obj.label) };
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function readConfig() {
|
|
168
|
+
const obj = readRawConfig();
|
|
169
|
+
if (!obj) {
|
|
170
|
+
return { token: null, tokenList: [] };
|
|
171
|
+
}
|
|
172
|
+
const list = [];
|
|
173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
174
|
+
const push = (rec) => {
|
|
175
|
+
if (!rec)
|
|
176
|
+
return;
|
|
177
|
+
const existing = list.find((r) => r.token === rec.token);
|
|
178
|
+
if (existing) {
|
|
179
|
+
if (!existing.label && rec.label)
|
|
180
|
+
existing.label = rec.label;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
seen.add(rec.token);
|
|
184
|
+
list.push(rec);
|
|
185
|
+
};
|
|
186
|
+
if (Array.isArray(obj.token_list)) {
|
|
187
|
+
for (const item of obj.token_list)
|
|
188
|
+
push(normalizeRecord(item));
|
|
189
|
+
}
|
|
190
|
+
const active = normalizeToken(obj.token);
|
|
191
|
+
if (active)
|
|
192
|
+
push({ token: active, label: null });
|
|
193
|
+
return {
|
|
194
|
+
token: active ?? (list.length > 0 ? list[0].token : null),
|
|
195
|
+
tokenList: list
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function writeConfig(config) {
|
|
199
|
+
const token_list = config.tokenList.map((r) => r.label ? { token: r.token, label: r.label } : { token: r.token });
|
|
200
|
+
writeRawConfig({
|
|
201
|
+
...readRawConfig() ?? {},
|
|
202
|
+
token: config.token,
|
|
203
|
+
token_list
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function readTokenFromFile() {
|
|
207
|
+
return readConfig().token;
|
|
208
|
+
}
|
|
209
|
+
function readTokenList() {
|
|
210
|
+
return readConfig().tokenList.map((r) => r.token);
|
|
211
|
+
}
|
|
212
|
+
function readTokenRecords() {
|
|
213
|
+
return readConfig().tokenList;
|
|
214
|
+
}
|
|
215
|
+
function writeTokenToFile(token, label) {
|
|
132
216
|
const trimmed = token.trim();
|
|
133
217
|
if (!trimmed) {
|
|
134
218
|
throw new Error("token is empty");
|
|
135
219
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
220
|
+
const nextLabel = normalizeLabel(label);
|
|
221
|
+
const current = readConfig();
|
|
222
|
+
const existing = current.tokenList.find((r) => r.token === trimmed);
|
|
223
|
+
if (!existing && !nextLabel) {
|
|
224
|
+
throw new Error("label is required");
|
|
225
|
+
}
|
|
226
|
+
const tokenList = existing ? current.tokenList.map((r) => r.token === trimmed ? { token: trimmed, label: nextLabel ?? r.label } : r) : [...current.tokenList, { token: trimmed, label: nextLabel }];
|
|
227
|
+
writeConfig({ token: trimmed, tokenList });
|
|
228
|
+
}
|
|
229
|
+
function setActiveToken(token) {
|
|
230
|
+
writeTokenToFile(token);
|
|
231
|
+
}
|
|
232
|
+
function setTokenLabel(token, label) {
|
|
233
|
+
const trimmed = token.trim();
|
|
234
|
+
const nextLabel = normalizeLabel(label);
|
|
235
|
+
if (!nextLabel) {
|
|
236
|
+
throw new Error("label is required");
|
|
237
|
+
}
|
|
238
|
+
const current = readConfig();
|
|
239
|
+
if (!current.tokenList.some((r) => r.token === trimmed)) {
|
|
240
|
+
throw new Error("token not found");
|
|
241
|
+
}
|
|
242
|
+
const tokenList = current.tokenList.map((r) => r.token === trimmed ? { token: r.token, label: nextLabel } : r);
|
|
243
|
+
writeConfig({ token: current.token, tokenList });
|
|
244
|
+
}
|
|
245
|
+
function removeTokenFromFile(token) {
|
|
246
|
+
const trimmed = token.trim();
|
|
247
|
+
const current = readConfig();
|
|
248
|
+
const tokenList = current.tokenList.filter((r) => r.token !== trimmed);
|
|
249
|
+
if (tokenList.length === 0) {
|
|
250
|
+
clearTokenFile();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const active = current.token && tokenList.some((r) => r.token === current.token) ? current.token : tokenList[0].token;
|
|
254
|
+
writeConfig({ token: active, tokenList });
|
|
141
255
|
}
|
|
142
256
|
function clearTokenFile() {
|
|
257
|
+
const existing = readRawConfig();
|
|
143
258
|
try {
|
|
259
|
+
if (existing && existing.enabled !== void 0) {
|
|
260
|
+
const { token: _token, token_list: _list, ...rest } = existing;
|
|
261
|
+
writeRawConfig(rest);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
144
264
|
rmSync(transcodesConfigFile(), { force: true });
|
|
145
265
|
} catch {
|
|
146
266
|
}
|
|
147
267
|
}
|
|
268
|
+
function isTrackerEnabled() {
|
|
269
|
+
return readRawConfig()?.enabled !== false;
|
|
270
|
+
}
|
|
271
|
+
function setTrackerEnabled(enabled) {
|
|
272
|
+
writeRawConfig({ ...readRawConfig() ?? {}, enabled });
|
|
273
|
+
}
|
|
148
274
|
function resolveToken() {
|
|
149
275
|
const envToken = process.env.TRANSCODES_TOKEN?.trim();
|
|
150
276
|
if (envToken) {
|
|
@@ -157,18 +283,951 @@ function resolveToken() {
|
|
|
157
283
|
return { token: null, source: "none" };
|
|
158
284
|
}
|
|
159
285
|
|
|
286
|
+
// src/logo.ts
|
|
287
|
+
var LOGO_DATA_URI = "data:image/png;base64,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";
|
|
288
|
+
|
|
289
|
+
// src/dashboard.ts
|
|
290
|
+
var DEFAULT_PORT = 3847;
|
|
291
|
+
var HOST = "127.0.0.1";
|
|
292
|
+
function fingerprint(token) {
|
|
293
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
294
|
+
}
|
|
295
|
+
function readJsonBody(req) {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const chunks = [];
|
|
298
|
+
req.on("data", (c) => chunks.push(c));
|
|
299
|
+
req.on("end", () => {
|
|
300
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
301
|
+
if (!raw.trim()) {
|
|
302
|
+
resolve({});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
resolve(JSON.parse(raw));
|
|
307
|
+
} catch {
|
|
308
|
+
reject(new Error("invalid JSON body"));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
req.on("error", reject);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function buildStatus() {
|
|
315
|
+
const configPath = transcodesConfigFile();
|
|
316
|
+
const records = readTokenRecords();
|
|
317
|
+
const active = readTokenFromFile();
|
|
318
|
+
const envOverridesFile = Boolean(
|
|
319
|
+
process.env.TRANSCODES_TOKEN?.trim() && active
|
|
320
|
+
);
|
|
321
|
+
const tokens = records.map(({ token, label }) => {
|
|
322
|
+
const entry = {
|
|
323
|
+
id: fingerprint(token),
|
|
324
|
+
active: token === active
|
|
325
|
+
};
|
|
326
|
+
if (label) entry.label = label;
|
|
327
|
+
try {
|
|
328
|
+
const parsed = parseMemberAccessToken(token);
|
|
329
|
+
entry.projectId = parsed.claims.projectId;
|
|
330
|
+
entry.organizationId = parsed.claims.organizationId;
|
|
331
|
+
entry.expiresAt = new Date(parsed.claims.exp * 1e3).toISOString();
|
|
332
|
+
if (parsed.warnings.length > 0) entry.warnings = [...parsed.warnings];
|
|
333
|
+
} catch (err) {
|
|
334
|
+
entry.warnings = [err instanceof Error ? err.message : String(err)];
|
|
335
|
+
}
|
|
336
|
+
return entry;
|
|
337
|
+
});
|
|
338
|
+
return { configPath, envOverridesFile, tokens };
|
|
339
|
+
}
|
|
340
|
+
function tokenById(id) {
|
|
341
|
+
return readTokenList().find((t) => fingerprint(t) === id);
|
|
342
|
+
}
|
|
343
|
+
function sendJson(res, status, body) {
|
|
344
|
+
res.writeHead(status, {
|
|
345
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
346
|
+
"Cache-Control": "no-store"
|
|
347
|
+
});
|
|
348
|
+
res.end(JSON.stringify(body));
|
|
349
|
+
}
|
|
350
|
+
function dashboardHtml() {
|
|
351
|
+
return `<!DOCTYPE html>
|
|
352
|
+
<html lang="en">
|
|
353
|
+
<head>
|
|
354
|
+
<meta charset="utf-8" />
|
|
355
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
356
|
+
<title>Transcodes \u2014 Token</title>
|
|
357
|
+
<style>
|
|
358
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
359
|
+
:root {
|
|
360
|
+
--bg: #f4f4f6;
|
|
361
|
+
--card: #ffffff;
|
|
362
|
+
--line: #ececf0;
|
|
363
|
+
--ink: #16161a;
|
|
364
|
+
--muted: #8a8a94;
|
|
365
|
+
--accent: #5b54e6;
|
|
366
|
+
--accent-soft: #eeedfb;
|
|
367
|
+
}
|
|
368
|
+
body {
|
|
369
|
+
margin: 0;
|
|
370
|
+
min-height: 100vh;
|
|
371
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
372
|
+
background: var(--bg);
|
|
373
|
+
color: var(--ink);
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: center;
|
|
377
|
+
padding: 32px;
|
|
378
|
+
-webkit-font-smoothing: antialiased;
|
|
379
|
+
}
|
|
380
|
+
.card {
|
|
381
|
+
width: 100%;
|
|
382
|
+
max-width: 460px;
|
|
383
|
+
background: var(--card);
|
|
384
|
+
border-radius: 24px;
|
|
385
|
+
padding: 34px;
|
|
386
|
+
box-shadow: 0 1px 2px rgba(16, 16, 26, 0.04), 0 12px 40px rgba(16, 16, 26, 0.06);
|
|
387
|
+
}
|
|
388
|
+
.header {
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 16px;
|
|
392
|
+
padding-bottom: 24px;
|
|
393
|
+
border-bottom: 1px solid var(--line);
|
|
394
|
+
}
|
|
395
|
+
.avatar {
|
|
396
|
+
width: 56px;
|
|
397
|
+
height: 56px;
|
|
398
|
+
border-radius: 14px;
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
object-fit: contain;
|
|
401
|
+
background: #f4f4f6;
|
|
402
|
+
padding: 8px;
|
|
403
|
+
}
|
|
404
|
+
.header h1 {
|
|
405
|
+
margin: 0;
|
|
406
|
+
font-size: 21px;
|
|
407
|
+
font-weight: 700;
|
|
408
|
+
letter-spacing: -0.02em;
|
|
409
|
+
}
|
|
410
|
+
.header p {
|
|
411
|
+
margin: 5px 0 0;
|
|
412
|
+
font-size: 14px;
|
|
413
|
+
color: var(--muted);
|
|
414
|
+
}
|
|
415
|
+
.tabs {
|
|
416
|
+
display: flex;
|
|
417
|
+
gap: 4px;
|
|
418
|
+
margin-top: 22px;
|
|
419
|
+
padding: 4px;
|
|
420
|
+
background: #f4f4f6;
|
|
421
|
+
border-radius: 13px;
|
|
422
|
+
}
|
|
423
|
+
.tab {
|
|
424
|
+
flex: 1;
|
|
425
|
+
padding: 9px 12px;
|
|
426
|
+
font-size: 13.5px;
|
|
427
|
+
font-weight: 600;
|
|
428
|
+
color: var(--muted);
|
|
429
|
+
background: transparent;
|
|
430
|
+
border: none;
|
|
431
|
+
border-radius: 9px;
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
transition: background 0.15s, color 0.15s;
|
|
434
|
+
}
|
|
435
|
+
.tab:hover { color: var(--ink); }
|
|
436
|
+
.tab.active {
|
|
437
|
+
background: #fff;
|
|
438
|
+
color: var(--ink);
|
|
439
|
+
box-shadow: 0 1px 2px rgba(16, 16, 26, 0.08);
|
|
440
|
+
}
|
|
441
|
+
.panel { display: none; padding-top: 26px; }
|
|
442
|
+
.panel.active { display: block; }
|
|
443
|
+
.section-title {
|
|
444
|
+
font-size: 17px;
|
|
445
|
+
font-weight: 700;
|
|
446
|
+
margin: 0 0 4px;
|
|
447
|
+
letter-spacing: -0.01em;
|
|
448
|
+
}
|
|
449
|
+
.section-sub {
|
|
450
|
+
font-size: 14px;
|
|
451
|
+
color: var(--muted);
|
|
452
|
+
margin: 0 0 20px;
|
|
453
|
+
}
|
|
454
|
+
textarea {
|
|
455
|
+
width: 100%;
|
|
456
|
+
min-height: 92px;
|
|
457
|
+
padding: 14px 16px;
|
|
458
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
459
|
+
font-size: 12.5px;
|
|
460
|
+
line-height: 1.5;
|
|
461
|
+
color: var(--ink);
|
|
462
|
+
background: #fbfbfc;
|
|
463
|
+
border: 1px solid var(--line);
|
|
464
|
+
border-radius: 14px;
|
|
465
|
+
resize: vertical;
|
|
466
|
+
outline: none;
|
|
467
|
+
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
|
468
|
+
}
|
|
469
|
+
textarea:focus {
|
|
470
|
+
background: #fff;
|
|
471
|
+
border-color: var(--accent);
|
|
472
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
473
|
+
}
|
|
474
|
+
textarea::placeholder { color: #b9b9c2; }
|
|
475
|
+
.label-input {
|
|
476
|
+
width: 100%;
|
|
477
|
+
margin-top: 12px;
|
|
478
|
+
padding: 12px 16px;
|
|
479
|
+
font-size: 13.5px;
|
|
480
|
+
color: var(--ink);
|
|
481
|
+
background: #fbfbfc;
|
|
482
|
+
border: 1px solid var(--line);
|
|
483
|
+
border-radius: 14px;
|
|
484
|
+
outline: none;
|
|
485
|
+
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
|
486
|
+
}
|
|
487
|
+
.label-input:focus {
|
|
488
|
+
background: #fff;
|
|
489
|
+
border-color: var(--accent);
|
|
490
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
491
|
+
}
|
|
492
|
+
.label-input::placeholder { color: #b9b9c2; }
|
|
493
|
+
.actions {
|
|
494
|
+
display: flex;
|
|
495
|
+
gap: 12px;
|
|
496
|
+
margin-top: 18px;
|
|
497
|
+
}
|
|
498
|
+
.actions button {
|
|
499
|
+
flex: 1;
|
|
500
|
+
padding: 13px 18px;
|
|
501
|
+
font-size: 15px;
|
|
502
|
+
font-weight: 600;
|
|
503
|
+
border-radius: 14px;
|
|
504
|
+
border: none;
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
transition: background 0.15s, opacity 0.15s, transform 0.05s;
|
|
507
|
+
}
|
|
508
|
+
.actions button:active:not(:disabled) { transform: translateY(1px); }
|
|
509
|
+
.actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
510
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
511
|
+
.btn-primary:hover:not(:disabled) { background: #4a43d4; }
|
|
512
|
+
.btn-secondary {
|
|
513
|
+
background: #f4f4f6;
|
|
514
|
+
color: #5a5a64;
|
|
515
|
+
}
|
|
516
|
+
.btn-secondary:hover:not(:disabled) { background: #ececf0; }
|
|
517
|
+
.list-label {
|
|
518
|
+
margin: 26px 0 10px;
|
|
519
|
+
font-size: 13px;
|
|
520
|
+
font-weight: 600;
|
|
521
|
+
color: var(--muted);
|
|
522
|
+
letter-spacing: 0.01em;
|
|
523
|
+
}
|
|
524
|
+
.token-list { display: flex; flex-direction: column; gap: 10px; }
|
|
525
|
+
.token-empty {
|
|
526
|
+
padding: 16px 18px;
|
|
527
|
+
background: #fbfbfc;
|
|
528
|
+
border: 1px dashed var(--line);
|
|
529
|
+
border-radius: 16px;
|
|
530
|
+
font-size: 13.5px;
|
|
531
|
+
color: var(--muted);
|
|
532
|
+
text-align: center;
|
|
533
|
+
}
|
|
534
|
+
.token-row {
|
|
535
|
+
display: flex;
|
|
536
|
+
flex-direction: column;
|
|
537
|
+
gap: 14px;
|
|
538
|
+
padding: 16px;
|
|
539
|
+
background: #fbfbfc;
|
|
540
|
+
border: 1px solid var(--line);
|
|
541
|
+
border-radius: 16px;
|
|
542
|
+
transition: border-color 0.15s, background 0.15s;
|
|
543
|
+
}
|
|
544
|
+
.token-row.active {
|
|
545
|
+
border-color: var(--accent);
|
|
546
|
+
background: var(--accent-soft);
|
|
547
|
+
}
|
|
548
|
+
.token-top { display: flex; align-items: center; gap: 14px; }
|
|
549
|
+
.radio {
|
|
550
|
+
width: 18px;
|
|
551
|
+
height: 18px;
|
|
552
|
+
border-radius: 50%;
|
|
553
|
+
border: 2px solid #d0d0d8;
|
|
554
|
+
flex-shrink: 0;
|
|
555
|
+
position: relative;
|
|
556
|
+
transition: border-color 0.15s;
|
|
557
|
+
}
|
|
558
|
+
.token-row.active .radio { border-color: var(--accent); }
|
|
559
|
+
.token-row.active .radio::after {
|
|
560
|
+
content: "";
|
|
561
|
+
position: absolute;
|
|
562
|
+
inset: 3px;
|
|
563
|
+
border-radius: 50%;
|
|
564
|
+
background: var(--accent);
|
|
565
|
+
}
|
|
566
|
+
.token-info { flex: 1; min-width: 0; line-height: 1.45; }
|
|
567
|
+
.token-info .label {
|
|
568
|
+
font-size: 14.5px;
|
|
569
|
+
font-weight: 700;
|
|
570
|
+
color: var(--ink);
|
|
571
|
+
margin-bottom: 4px;
|
|
572
|
+
}
|
|
573
|
+
.token-info .field { font-size: 13px; color: #4a4a52; }
|
|
574
|
+
.token-info .field .k { color: var(--muted); }
|
|
575
|
+
.token-info .field code {
|
|
576
|
+
font-size: 12px;
|
|
577
|
+
color: var(--ink);
|
|
578
|
+
background: #fff;
|
|
579
|
+
border: 1px solid var(--line);
|
|
580
|
+
padding: 1px 7px;
|
|
581
|
+
border-radius: 6px;
|
|
582
|
+
}
|
|
583
|
+
.token-row.active .token-info .field code { background: #fff; }
|
|
584
|
+
.token-info .warn { font-size: 12px; color: #c0392f; margin-top: 2px; }
|
|
585
|
+
.token-actions {
|
|
586
|
+
display: flex;
|
|
587
|
+
gap: 8px;
|
|
588
|
+
padding-top: 12px;
|
|
589
|
+
border-top: 1px solid var(--line);
|
|
590
|
+
}
|
|
591
|
+
.token-row.active .token-actions { border-top-color: rgba(91, 84, 230, 0.18); }
|
|
592
|
+
.token-actions button {
|
|
593
|
+
flex: 1;
|
|
594
|
+
padding: 9px 12px;
|
|
595
|
+
font-size: 12px;
|
|
596
|
+
font-weight: 600;
|
|
597
|
+
letter-spacing: 0.03em;
|
|
598
|
+
border-radius: 10px;
|
|
599
|
+
border: 1px solid var(--line);
|
|
600
|
+
background: #fff;
|
|
601
|
+
cursor: pointer;
|
|
602
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
|
|
603
|
+
}
|
|
604
|
+
.btn-set { color: var(--accent); }
|
|
605
|
+
.btn-set:hover:not(:disabled) { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
606
|
+
.btn-set:disabled { opacity: 0.45; cursor: default; }
|
|
607
|
+
.btn-del { color: #c0392f; }
|
|
608
|
+
.btn-del:hover { background: #c0392f; color: #fff; border-color: #c0392f; }
|
|
609
|
+
.btn-edit, .btn-cancel { color: #5a5a64; }
|
|
610
|
+
.btn-edit:hover, .btn-cancel:hover { background: #ececf0; color: var(--ink); border-color: #dcdce2; }
|
|
611
|
+
.label-edit {
|
|
612
|
+
width: 100%;
|
|
613
|
+
padding: 9px 12px;
|
|
614
|
+
font-size: 13.5px;
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
color: var(--ink);
|
|
617
|
+
background: #fff;
|
|
618
|
+
border: 1px solid var(--accent);
|
|
619
|
+
border-radius: 9px;
|
|
620
|
+
outline: none;
|
|
621
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
622
|
+
}
|
|
623
|
+
.cmd-list { display: flex; flex-direction: column; gap: 10px; }
|
|
624
|
+
.cmd {
|
|
625
|
+
padding: 14px 16px;
|
|
626
|
+
background: #fbfbfc;
|
|
627
|
+
border: 1px solid var(--line);
|
|
628
|
+
border-radius: 14px;
|
|
629
|
+
}
|
|
630
|
+
.cmd code {
|
|
631
|
+
display: inline-block;
|
|
632
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
633
|
+
font-size: 12.5px;
|
|
634
|
+
color: var(--accent);
|
|
635
|
+
background: var(--accent-soft);
|
|
636
|
+
padding: 3px 9px;
|
|
637
|
+
border-radius: 7px;
|
|
638
|
+
}
|
|
639
|
+
.cmd .cmd-desc {
|
|
640
|
+
display: block;
|
|
641
|
+
margin-top: 8px;
|
|
642
|
+
font-size: 13px;
|
|
643
|
+
color: var(--muted);
|
|
644
|
+
line-height: 1.5;
|
|
645
|
+
}
|
|
646
|
+
.settings-list { display: flex; flex-direction: column; gap: 10px; }
|
|
647
|
+
.setting-row {
|
|
648
|
+
display: flex;
|
|
649
|
+
align-items: flex-start;
|
|
650
|
+
justify-content: space-between;
|
|
651
|
+
gap: 16px;
|
|
652
|
+
padding: 14px 16px;
|
|
653
|
+
background: #fbfbfc;
|
|
654
|
+
border: 1px solid var(--line);
|
|
655
|
+
border-radius: 14px;
|
|
656
|
+
}
|
|
657
|
+
.setting-info { flex: 1; min-width: 0; }
|
|
658
|
+
.setting-label {
|
|
659
|
+
display: block;
|
|
660
|
+
font-size: 14.5px;
|
|
661
|
+
font-weight: 600;
|
|
662
|
+
color: var(--ink);
|
|
663
|
+
letter-spacing: -0.01em;
|
|
664
|
+
}
|
|
665
|
+
.setting-desc {
|
|
666
|
+
margin: 8px 0 0;
|
|
667
|
+
font-size: 13px;
|
|
668
|
+
color: var(--muted);
|
|
669
|
+
line-height: 1.5;
|
|
670
|
+
}
|
|
671
|
+
.toggle {
|
|
672
|
+
position: relative;
|
|
673
|
+
width: 44px;
|
|
674
|
+
height: 26px;
|
|
675
|
+
flex: none;
|
|
676
|
+
flex-shrink: 0;
|
|
677
|
+
margin-top: 2px;
|
|
678
|
+
border: none;
|
|
679
|
+
border-radius: 999px;
|
|
680
|
+
background: #d8d8de;
|
|
681
|
+
cursor: pointer;
|
|
682
|
+
padding: 0;
|
|
683
|
+
transition: background 0.2s;
|
|
684
|
+
}
|
|
685
|
+
.toggle::after {
|
|
686
|
+
content: "";
|
|
687
|
+
position: absolute;
|
|
688
|
+
top: 3px;
|
|
689
|
+
left: 3px;
|
|
690
|
+
width: 20px;
|
|
691
|
+
height: 20px;
|
|
692
|
+
border-radius: 50%;
|
|
693
|
+
background: #fff;
|
|
694
|
+
box-shadow: 0 1px 3px rgba(16, 16, 26, 0.18);
|
|
695
|
+
transition: transform 0.2s;
|
|
696
|
+
}
|
|
697
|
+
.toggle.on { background: var(--accent); }
|
|
698
|
+
.toggle.on::after { transform: translateX(18px); }
|
|
699
|
+
.toggle:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
700
|
+
.toast {
|
|
701
|
+
margin-top: 14px;
|
|
702
|
+
padding: 12px 16px;
|
|
703
|
+
border-radius: 12px;
|
|
704
|
+
font-size: 13.5px;
|
|
705
|
+
font-weight: 500;
|
|
706
|
+
display: none;
|
|
707
|
+
}
|
|
708
|
+
.toast.show { display: block; }
|
|
709
|
+
.toast.success { background: #effaf2; color: #1a7f45; }
|
|
710
|
+
.toast.error { background: #fdf0f0; color: #c0392f; }
|
|
711
|
+
.hint {
|
|
712
|
+
margin: 18px 0 0;
|
|
713
|
+
font-size: 12.5px;
|
|
714
|
+
color: #b9b9c2;
|
|
715
|
+
text-align: center;
|
|
716
|
+
line-height: 1.6;
|
|
717
|
+
}
|
|
718
|
+
.hint code {
|
|
719
|
+
font-size: 11.5px;
|
|
720
|
+
background: #f4f4f6;
|
|
721
|
+
padding: 2px 7px;
|
|
722
|
+
border-radius: 6px;
|
|
723
|
+
color: #8a8a94;
|
|
724
|
+
}
|
|
725
|
+
</style>
|
|
726
|
+
</head>
|
|
727
|
+
<body>
|
|
728
|
+
<div class="card">
|
|
729
|
+
<div class="header">
|
|
730
|
+
<img class="avatar" src="${LOGO_DATA_URI}" alt="Transcodes" />
|
|
731
|
+
<div>
|
|
732
|
+
<h1>Transcodes</h1>
|
|
733
|
+
<p>CLI Dashboard</p>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
<div class="tabs">
|
|
737
|
+
<button type="button" class="tab active" data-tab="tokens">Tokens</button>
|
|
738
|
+
<button type="button" class="tab" data-tab="settings">Settings</button>
|
|
739
|
+
<button type="button" class="tab" data-tab="cli">CLI Commands</button>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
<div class="panel active" id="panel-tokens">
|
|
743
|
+
<p class="section-title">MCP Agent Token</p>
|
|
744
|
+
<p class="section-sub">Paste the token from your Transcodes console member detail page</p>
|
|
745
|
+
<textarea id="token" placeholder="eyJhbGciOi\u2026" spellcheck="false" autocomplete="off"></textarea>
|
|
746
|
+
<input type="text" id="label" class="label-input" placeholder="Label (required) \u2014 e.g. transcodes-{project_name}-{env}" autocomplete="off" required />
|
|
747
|
+
<div class="actions">
|
|
748
|
+
<button type="button" class="btn-primary" id="save">Save</button>
|
|
749
|
+
<button type="button" class="btn-secondary" id="clear">Clear</button>
|
|
750
|
+
</div>
|
|
751
|
+
<div id="toast" class="toast"></div>
|
|
752
|
+
<p class="list-label">Saved tokens</p>
|
|
753
|
+
<div class="token-list" id="token-list"></div>
|
|
754
|
+
<p class="hint">Saved to <code>{{HOME_DIR}}/.transcodes/config.json</code><br />Press Ctrl+C in the terminal to stop</p>
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
<div class="panel" id="panel-cli">
|
|
758
|
+
<p class="section-title">CLI Commands</p>
|
|
759
|
+
<p class="section-sub">Run these from your terminal \u2014 the dashboard wraps the same actions</p>
|
|
760
|
+
<div class="cmd-list">
|
|
761
|
+
<div class="cmd"><code>transcodes</code><span class="cmd-desc">Open this dashboard (default, same as transcodes dashboard)</span></div>
|
|
762
|
+
<div class="cmd"><code>transcodes set <token> -l <label></code><span class="cmd-desc">Validate and save a token with a label, then make it active</span></div>
|
|
763
|
+
<div class="cmd"><code>transcodes tokens</code><span class="cmd-desc">List all saved tokens (active one marked with *)</span></div>
|
|
764
|
+
<div class="cmd"><code>transcodes reset</code><span class="cmd-desc">Remove all saved tokens</span></div>
|
|
765
|
+
<div class="cmd"><code>transcodes help</code><span class="cmd-desc">Show the full command list and how to use each one</span></div>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div class="panel" id="panel-settings">
|
|
770
|
+
<p class="section-title">Settings</p>
|
|
771
|
+
<p class="section-sub">Manage your local MCP Agent settings</p>
|
|
772
|
+
<div class="settings-list">
|
|
773
|
+
<div class="setting-row">
|
|
774
|
+
<div class="setting-info">
|
|
775
|
+
<span class="setting-label">Step-up Authentication</span>
|
|
776
|
+
<p class="setting-desc">When off, all features except Transcodes essentials are skipped</p>
|
|
777
|
+
</div>
|
|
778
|
+
<button type="button" class="toggle" id="stepup-toggle" aria-label="Step-up Authentication"></button>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
<div id="settings-toast" class="toast"></div>
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
<script>
|
|
785
|
+
const tokenEl = document.getElementById("token");
|
|
786
|
+
const labelEl = document.getElementById("label");
|
|
787
|
+
const toastEl = document.getElementById("toast");
|
|
788
|
+
const listEl = document.getElementById("token-list");
|
|
789
|
+
const saveBtn = document.getElementById("save");
|
|
790
|
+
const clearBtn = document.getElementById("clear");
|
|
791
|
+
|
|
792
|
+
document.querySelectorAll(".tab").forEach((tab) => {
|
|
793
|
+
tab.addEventListener("click", () => {
|
|
794
|
+
const name = tab.getAttribute("data-tab");
|
|
795
|
+
document.querySelectorAll(".tab").forEach((t) =>
|
|
796
|
+
t.classList.toggle("active", t === tab));
|
|
797
|
+
document.querySelectorAll(".panel").forEach((p) =>
|
|
798
|
+
p.classList.toggle("active", p.id === "panel-" + name));
|
|
799
|
+
if (name === "settings") loadSettings();
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
function showToast(msg, kind) {
|
|
804
|
+
toastEl.textContent = msg;
|
|
805
|
+
toastEl.className = "toast show " + (kind || "success");
|
|
806
|
+
setTimeout(() => toastEl.classList.remove("show"), 4000);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function esc(s) {
|
|
810
|
+
return String(s).replace(/[&<>"]/g, (c) =>
|
|
811
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let lastStatus = { tokens: [] };
|
|
815
|
+
let editingId = null;
|
|
816
|
+
|
|
817
|
+
function renderTokens(s) {
|
|
818
|
+
if (!s.tokens || s.tokens.length === 0) {
|
|
819
|
+
listEl.innerHTML = '<div class="token-empty">No tokens saved yet \u2014 paste one above and press Save</div>';
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
listEl.innerHTML = s.tokens.map((t) => {
|
|
824
|
+
const editing = t.id === editingId;
|
|
825
|
+
const project = t.projectId
|
|
826
|
+
? '<div class="field"><span class="k">Project Id</span> <code>' + esc(t.projectId) + '</code></div>'
|
|
827
|
+
: '';
|
|
828
|
+
const org = t.organizationId
|
|
829
|
+
? '<div class="field"><span class="k">Organization Id</span> <code>' + esc(t.organizationId) + '</code></div>'
|
|
830
|
+
: '';
|
|
831
|
+
const warn = t.warnings && t.warnings.length
|
|
832
|
+
? '<div class="warn">' + esc(t.warnings.join("; ")) + '</div>'
|
|
833
|
+
: '';
|
|
834
|
+
const labelBlock = editing
|
|
835
|
+
? '<input type="text" class="label-edit" data-edit-input="' + t.id + '" value="' + esc(t.label || "") + '" placeholder="Label" />'
|
|
836
|
+
: (t.label ? '<div class="label">' + esc(t.label) + '</div>' : '');
|
|
837
|
+
const actions = editing
|
|
838
|
+
? '<button type="button" class="btn-set" data-save-label="' + t.id + '">SAVE</button>' +
|
|
839
|
+
'<button type="button" class="btn-cancel" data-cancel-edit="1">CANCEL</button>'
|
|
840
|
+
: '<button type="button" class="btn-edit" data-edit="' + t.id + '">EDIT</button>' +
|
|
841
|
+
'<button type="button" class="btn-set" data-set="' + t.id + '"' + (t.active ? " disabled" : "") + '>' +
|
|
842
|
+
(t.active ? "DEFAULT" : "SET DEFAULT") +
|
|
843
|
+
'</button>' +
|
|
844
|
+
'<button type="button" class="btn-del" data-del="' + t.id + '">DELETE</button>';
|
|
845
|
+
return (
|
|
846
|
+
'<div class="token-row' + (t.active ? " active" : "") + '" data-id="' + t.id + '">' +
|
|
847
|
+
'<div class="token-top">' +
|
|
848
|
+
'<span class="radio"></span>' +
|
|
849
|
+
'<div class="token-info">' + labelBlock + org + project + warn + '</div>' +
|
|
850
|
+
'</div>' +
|
|
851
|
+
'<div class="token-actions">' + actions + '</div>' +
|
|
852
|
+
'</div>'
|
|
853
|
+
);
|
|
854
|
+
}).join("");
|
|
855
|
+
|
|
856
|
+
if (editingId) {
|
|
857
|
+
const el = listEl.querySelector('[data-edit-input="' + editingId + '"]');
|
|
858
|
+
if (el) { el.focus(); el.select(); }
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function refresh() {
|
|
863
|
+
const res = await fetch("/api/status");
|
|
864
|
+
lastStatus = await res.json();
|
|
865
|
+
renderTokens(lastStatus);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function saveLabel(id) {
|
|
869
|
+
const input = listEl.querySelector('[data-edit-input="' + id + '"]');
|
|
870
|
+
const label = input ? input.value.trim() : "";
|
|
871
|
+
if (!label) {
|
|
872
|
+
showToast("Label cannot be empty", "error");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
const res = await fetch("/api/label", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
headers: { "Content-Type": "application/json" },
|
|
879
|
+
body: JSON.stringify({ id, label }),
|
|
880
|
+
});
|
|
881
|
+
const data = await res.json();
|
|
882
|
+
if (!res.ok) throw new Error(data.error || "Rename failed");
|
|
883
|
+
editingId = null;
|
|
884
|
+
showToast("Label updated", "success");
|
|
885
|
+
refresh();
|
|
886
|
+
} catch (e) {
|
|
887
|
+
showToast(e.message || "Rename failed", "error");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function setDefault(id) {
|
|
892
|
+
try {
|
|
893
|
+
const res = await fetch("/api/select", {
|
|
894
|
+
method: "POST",
|
|
895
|
+
headers: { "Content-Type": "application/json" },
|
|
896
|
+
body: JSON.stringify({ id }),
|
|
897
|
+
});
|
|
898
|
+
const data = await res.json();
|
|
899
|
+
if (!res.ok) throw new Error(data.error || "Set default failed");
|
|
900
|
+
showToast("Default token updated", "success");
|
|
901
|
+
refresh();
|
|
902
|
+
} catch (e) {
|
|
903
|
+
showToast(e.message || "Set default failed", "error");
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function removeToken(id) {
|
|
908
|
+
if (!confirm("Delete this token from the saved list?")) return;
|
|
909
|
+
try {
|
|
910
|
+
const res = await fetch("/api/token", {
|
|
911
|
+
method: "DELETE",
|
|
912
|
+
headers: { "Content-Type": "application/json" },
|
|
913
|
+
body: JSON.stringify({ id }),
|
|
914
|
+
});
|
|
915
|
+
const data = await res.json();
|
|
916
|
+
if (!res.ok) throw new Error(data.error || "Delete failed");
|
|
917
|
+
showToast("Token deleted", "success");
|
|
918
|
+
refresh();
|
|
919
|
+
} catch (e) {
|
|
920
|
+
showToast(e.message || "Delete failed", "error");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
listEl.addEventListener("click", (e) => {
|
|
925
|
+
const editId = e.target.getAttribute("data-edit");
|
|
926
|
+
if (editId) { editingId = editId; renderTokens(lastStatus); return; }
|
|
927
|
+
const saveId = e.target.getAttribute("data-save-label");
|
|
928
|
+
if (saveId) { saveLabel(saveId); return; }
|
|
929
|
+
if (e.target.getAttribute("data-cancel-edit")) {
|
|
930
|
+
editingId = null; renderTokens(lastStatus); return;
|
|
931
|
+
}
|
|
932
|
+
const setId = e.target.getAttribute("data-set");
|
|
933
|
+
if (setId) { setDefault(setId); return; }
|
|
934
|
+
const delId = e.target.getAttribute("data-del");
|
|
935
|
+
if (delId) { removeToken(delId); return; }
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
listEl.addEventListener("keydown", (e) => {
|
|
939
|
+
const input = e.target.closest(".label-edit");
|
|
940
|
+
if (!input) return;
|
|
941
|
+
if (e.key === "Enter") { e.preventDefault(); saveLabel(editingId); }
|
|
942
|
+
else if (e.key === "Escape") { editingId = null; renderTokens(lastStatus); }
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
saveBtn.addEventListener("click", async () => {
|
|
946
|
+
const token = tokenEl.value.trim();
|
|
947
|
+
const label = labelEl.value.trim();
|
|
948
|
+
if (!token) {
|
|
949
|
+
showToast("Paste a token first", "error");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (!label) {
|
|
953
|
+
showToast("Add a label first", "error");
|
|
954
|
+
labelEl.focus();
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
saveBtn.disabled = true;
|
|
958
|
+
try {
|
|
959
|
+
const res = await fetch("/api/token", {
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: { "Content-Type": "application/json" },
|
|
962
|
+
body: JSON.stringify({ token, label }),
|
|
963
|
+
});
|
|
964
|
+
const data = await res.json();
|
|
965
|
+
if (!res.ok) throw new Error(data.error || "Save failed");
|
|
966
|
+
tokenEl.value = "";
|
|
967
|
+
labelEl.value = "";
|
|
968
|
+
showToast("Token saved", "success");
|
|
969
|
+
refresh();
|
|
970
|
+
} catch (e) {
|
|
971
|
+
showToast(e.message || "Save failed", "error");
|
|
972
|
+
} finally {
|
|
973
|
+
saveBtn.disabled = false;
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
clearBtn.addEventListener("click", () => {
|
|
978
|
+
tokenEl.value = "";
|
|
979
|
+
labelEl.value = "";
|
|
980
|
+
tokenEl.focus();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const stepupToggleEl = document.getElementById("stepup-toggle");
|
|
984
|
+
const settingsToastEl = document.getElementById("settings-toast");
|
|
985
|
+
|
|
986
|
+
function showSettingsToast(msg, kind) {
|
|
987
|
+
settingsToastEl.textContent = msg;
|
|
988
|
+
settingsToastEl.className = "toast show " + (kind || "success");
|
|
989
|
+
setTimeout(() => settingsToastEl.classList.remove("show"), 4000);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function renderStepupToggle(enabled) {
|
|
993
|
+
stepupToggleEl.classList.toggle("on", enabled);
|
|
994
|
+
stepupToggleEl.setAttribute("aria-checked", enabled ? "true" : "false");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function loadSettings() {
|
|
998
|
+
const res = await fetch("/api/settings");
|
|
999
|
+
const s = await res.json();
|
|
1000
|
+
renderStepupToggle(s.enabled !== false);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
stepupToggleEl.addEventListener("click", async () => {
|
|
1004
|
+
const next = !stepupToggleEl.classList.contains("on");
|
|
1005
|
+
stepupToggleEl.disabled = true;
|
|
1006
|
+
try {
|
|
1007
|
+
const res = await fetch("/api/settings", {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: { "Content-Type": "application/json" },
|
|
1010
|
+
body: JSON.stringify({ enabled: next }),
|
|
1011
|
+
});
|
|
1012
|
+
const data = await res.json();
|
|
1013
|
+
if (!res.ok) throw new Error(data.error || "Save failed");
|
|
1014
|
+
renderStepupToggle(next);
|
|
1015
|
+
showSettingsToast(
|
|
1016
|
+
next ? "Step-up Authentication enabled" : "Step-up Authentication disabled",
|
|
1017
|
+
"success"
|
|
1018
|
+
);
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
showSettingsToast(e.message || "Save failed", "error");
|
|
1021
|
+
} finally {
|
|
1022
|
+
stepupToggleEl.disabled = false;
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
refresh();
|
|
1027
|
+
loadSettings();
|
|
1028
|
+
</script>
|
|
1029
|
+
</body>
|
|
1030
|
+
</html>`;
|
|
1031
|
+
}
|
|
1032
|
+
function openBrowser(url) {
|
|
1033
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1034
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1035
|
+
try {
|
|
1036
|
+
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
1037
|
+
child.on("error", () => {
|
|
1038
|
+
});
|
|
1039
|
+
child.unref();
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function listen(port) {
|
|
1044
|
+
return new Promise((resolve, reject) => {
|
|
1045
|
+
const server = createServer(async (req, res) => {
|
|
1046
|
+
const url = req.url ?? "/";
|
|
1047
|
+
const method = req.method ?? "GET";
|
|
1048
|
+
const hostName = (req.headers.host ?? "").split(":")[0];
|
|
1049
|
+
if (hostName !== "127.0.0.1" && hostName !== "localhost") {
|
|
1050
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
1051
|
+
res.end("forbidden host");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
if (method === "GET" && (url === "/" || url === "/index.html")) {
|
|
1056
|
+
res.writeHead(200, {
|
|
1057
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1058
|
+
"Cache-Control": "no-store"
|
|
1059
|
+
});
|
|
1060
|
+
res.end(dashboardHtml());
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (method === "GET" && url === "/api/status") {
|
|
1064
|
+
sendJson(res, 200, buildStatus());
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (method === "POST" && url === "/api/token") {
|
|
1068
|
+
const body = await readJsonBody(req);
|
|
1069
|
+
const token = typeof body.token === "string" ? body.token.trim() : "";
|
|
1070
|
+
const label = typeof body.label === "string" ? body.label.trim() : "";
|
|
1071
|
+
if (!token) {
|
|
1072
|
+
sendJson(res, 400, { error: "token is required" });
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (!label) {
|
|
1076
|
+
sendJson(res, 400, { error: "label is required" });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
parseMemberAccessToken(token);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
sendJson(res, 400, {
|
|
1083
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
writeTokenToFile(token, label);
|
|
1088
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (method === "POST" && url === "/api/label") {
|
|
1092
|
+
const body = await readJsonBody(req);
|
|
1093
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1094
|
+
const label = typeof body.label === "string" ? body.label.trim() : "";
|
|
1095
|
+
const token = id ? tokenById(id) : void 0;
|
|
1096
|
+
if (!token) {
|
|
1097
|
+
sendJson(res, 404, { error: "token not found" });
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (!label) {
|
|
1101
|
+
sendJson(res, 400, { error: "label is required" });
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
setTokenLabel(token, label);
|
|
1105
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (method === "POST" && url === "/api/select") {
|
|
1109
|
+
const body = await readJsonBody(req);
|
|
1110
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1111
|
+
const token = id ? tokenById(id) : void 0;
|
|
1112
|
+
if (!token) {
|
|
1113
|
+
sendJson(res, 404, { error: "token not found" });
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
setActiveToken(token);
|
|
1117
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (method === "DELETE" && url === "/api/token") {
|
|
1121
|
+
const body = await readJsonBody(req);
|
|
1122
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1123
|
+
const token = id ? tokenById(id) : void 0;
|
|
1124
|
+
if (!token) {
|
|
1125
|
+
sendJson(res, 404, { error: "token not found" });
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
removeTokenFromFile(token);
|
|
1129
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (method === "GET" && url === "/api/settings") {
|
|
1133
|
+
sendJson(res, 200, {
|
|
1134
|
+
enabled: isTrackerEnabled()
|
|
1135
|
+
});
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (method === "POST" && url === "/api/settings") {
|
|
1139
|
+
const body = await readJsonBody(req);
|
|
1140
|
+
if (typeof body.enabled !== "boolean") {
|
|
1141
|
+
sendJson(res, 400, {
|
|
1142
|
+
error: "enabled must be a boolean"
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
setTrackerEnabled(body.enabled);
|
|
1147
|
+
sendJson(res, 200, {
|
|
1148
|
+
ok: true,
|
|
1149
|
+
enabled: isTrackerEnabled()
|
|
1150
|
+
});
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
sendJson(res, 404, { error: "not found" });
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
sendJson(res, 500, {
|
|
1156
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
server.on("error", reject);
|
|
1161
|
+
server.listen(port, HOST, () => resolve(server));
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
async function runDashboard(options) {
|
|
1165
|
+
const preferred = options.port ?? DEFAULT_PORT;
|
|
1166
|
+
let server;
|
|
1167
|
+
let port = preferred;
|
|
1168
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
1169
|
+
try {
|
|
1170
|
+
server = await listen(port);
|
|
1171
|
+
break;
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
const code = err.code;
|
|
1174
|
+
if (code === "EADDRINUSE") {
|
|
1175
|
+
port += 1;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
throw err;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (!server) {
|
|
1182
|
+
const last = preferred + 9;
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`could not find a free port in ${preferred}-${last} (all in use).
|
|
1185
|
+
A previous dashboard is probably still running.
|
|
1186
|
+
Tip: if you stopped one with Ctrl+Z it is only suspended (still alive) \u2014 use Ctrl+C to stop it.
|
|
1187
|
+
Free the ports and retry:
|
|
1188
|
+
macOS/Linux: lsof -ti tcp:${preferred}-${last} | xargs kill -9
|
|
1189
|
+
any platform: npx kill-port ${preferred} ${preferred + 1} # repeat per port
|
|
1190
|
+
Or choose another port: transcodes --port <N>`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
const url = `http://${HOST}:${port}/`;
|
|
1194
|
+
process.stdout.write(
|
|
1195
|
+
`Transcodes dashboard running at ${url}
|
|
1196
|
+
Config file: ${transcodesConfigFile()}
|
|
1197
|
+
Press Ctrl+C to stop
|
|
1198
|
+
`
|
|
1199
|
+
);
|
|
1200
|
+
if (options.open !== false) {
|
|
1201
|
+
openBrowser(url);
|
|
1202
|
+
}
|
|
1203
|
+
await new Promise((resolve) => {
|
|
1204
|
+
const onSignal = () => {
|
|
1205
|
+
server.close(() => resolve());
|
|
1206
|
+
};
|
|
1207
|
+
process.on("SIGINT", onSignal);
|
|
1208
|
+
process.on("SIGTERM", onSignal);
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
160
1212
|
// src/index.ts
|
|
161
1213
|
var USAGE = `transcodes \u2014 ai-action-tracker token manager
|
|
162
1214
|
|
|
163
1215
|
Usage:
|
|
164
|
-
transcodes
|
|
165
|
-
transcodes
|
|
166
|
-
transcodes
|
|
167
|
-
transcodes
|
|
1216
|
+
transcodes Open the dashboard at http://127.0.0.1:3847/ (add --port N or --no-open)
|
|
1217
|
+
transcodes set <token> -l <label> Save your Transcodes member token (label required) to ${transcodesConfigFile()}
|
|
1218
|
+
transcodes reset Remove all saved tokens
|
|
1219
|
+
transcodes enable Turn the ai-action-tracker step-up gate ON
|
|
1220
|
+
transcodes disable Turn the gate OFF (stops blocking Bash + MCP across all hosts)
|
|
1221
|
+
transcodes status Show the active token source, expiry, and gate state
|
|
1222
|
+
transcodes tokens List all saved tokens (active one marked with *)
|
|
1223
|
+
transcodes help Show this message
|
|
168
1224
|
|
|
169
1225
|
The token is read by the ai-action-tracker plugins/hooks with precedence:
|
|
170
1226
|
1. TRANSCODES_TOKEN environment variable (overrides everything)
|
|
171
1227
|
2. ${transcodesConfigFile()}
|
|
1228
|
+
|
|
1229
|
+
enable/disable flips the \`enabled\` flag in the same file; it takes effect on
|
|
1230
|
+
the next hook invocation (no restart needed) and applies to every host.
|
|
172
1231
|
`;
|
|
173
1232
|
function fail(message) {
|
|
174
1233
|
process.stderr.write(`transcodes: ${message}
|
|
@@ -185,11 +1244,27 @@ function expiryLine(token) {
|
|
|
185
1244
|
return `unable to decode token: ${err instanceof Error ? err.message : String(err)}`;
|
|
186
1245
|
}
|
|
187
1246
|
}
|
|
188
|
-
function
|
|
1247
|
+
function cmdSet(args) {
|
|
1248
|
+
let token;
|
|
1249
|
+
let label;
|
|
1250
|
+
for (let i = 0; i < args.length; i++) {
|
|
1251
|
+
const arg = args[i];
|
|
1252
|
+
if (arg === "-l" || arg === "--label") {
|
|
1253
|
+
label = args[++i];
|
|
1254
|
+
} else if (token === void 0) {
|
|
1255
|
+
token = arg;
|
|
1256
|
+
} else {
|
|
1257
|
+
fail(`unexpected argument "${arg}". Usage: transcodes set <token> -l <label>`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
189
1260
|
if (!token || !token.trim()) {
|
|
190
|
-
fail("missing token. Usage: transcodes
|
|
1261
|
+
fail("missing token. Usage: transcodes set <token> -l <label>");
|
|
1262
|
+
}
|
|
1263
|
+
if (!label || !label.trim()) {
|
|
1264
|
+
fail("missing label. Usage: transcodes set <token> -l <label>");
|
|
191
1265
|
}
|
|
192
1266
|
const trimmed = token.trim();
|
|
1267
|
+
const trimmedLabel = label.trim();
|
|
193
1268
|
try {
|
|
194
1269
|
parseMemberAccessToken(trimmed);
|
|
195
1270
|
} catch (err) {
|
|
@@ -198,7 +1273,7 @@ function cmdLogin(token) {
|
|
|
198
1273
|
);
|
|
199
1274
|
}
|
|
200
1275
|
try {
|
|
201
|
-
writeTokenToFile(trimmed);
|
|
1276
|
+
writeTokenToFile(trimmed, trimmedLabel);
|
|
202
1277
|
} catch (err) {
|
|
203
1278
|
fail(
|
|
204
1279
|
`could not write token file: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -206,20 +1281,36 @@ function cmdLogin(token) {
|
|
|
206
1281
|
}
|
|
207
1282
|
process.stdout.write(
|
|
208
1283
|
`Saved to ${transcodesConfigFile()}
|
|
209
|
-
${expiryLine(trimmed)}
|
|
1284
|
+
label=${trimmedLabel} ${expiryLine(trimmed)}
|
|
210
1285
|
`
|
|
211
1286
|
);
|
|
212
1287
|
}
|
|
213
|
-
function
|
|
1288
|
+
function cmdReset() {
|
|
214
1289
|
clearTokenFile();
|
|
215
|
-
process.stdout.write(`Removed ${transcodesConfigFile()}
|
|
1290
|
+
process.stdout.write(`Removed all saved tokens (${transcodesConfigFile()})
|
|
216
1291
|
`);
|
|
217
1292
|
}
|
|
1293
|
+
function cmdSetEnabled(enabled) {
|
|
1294
|
+
try {
|
|
1295
|
+
setTrackerEnabled(enabled);
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
fail(
|
|
1298
|
+
`could not update gate state: ${err instanceof Error ? err.message : String(err)}`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
process.stdout.write(
|
|
1302
|
+
enabled ? "ai-action-tracker gate ENABLED \u2014 danger commands require step-up MFA again.\n" : "ai-action-tracker gate DISABLED \u2014 Bash + MCP tool calls pass without step-up until `transcodes enable`.\n"
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
218
1305
|
function cmdStatus() {
|
|
1306
|
+
process.stdout.write(
|
|
1307
|
+
`Gate: ${isTrackerEnabled() ? "enabled" : "DISABLED"}
|
|
1308
|
+
`
|
|
1309
|
+
);
|
|
219
1310
|
const { token, source } = resolveToken();
|
|
220
1311
|
if (source === "none" || !token) {
|
|
221
1312
|
process.stdout.write(
|
|
222
|
-
"No token configured. Run `transcodes
|
|
1313
|
+
"No token configured. Run `transcodes set <token> -l <label>` or `transcodes` to set one.\n"
|
|
223
1314
|
);
|
|
224
1315
|
return;
|
|
225
1316
|
}
|
|
@@ -228,25 +1319,88 @@ function cmdStatus() {
|
|
|
228
1319
|
${expiryLine(token)}
|
|
229
1320
|
`);
|
|
230
1321
|
}
|
|
1322
|
+
function cmdTokens() {
|
|
1323
|
+
const records = readTokenRecords();
|
|
1324
|
+
if (records.length === 0) {
|
|
1325
|
+
process.stdout.write(
|
|
1326
|
+
"No tokens saved. Run `transcodes set <token> -l <label>` or `transcodes` to add one.\n"
|
|
1327
|
+
);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const active = readTokenFromFile();
|
|
1331
|
+
process.stdout.write(`Saved tokens (${transcodesConfigFile()}):
|
|
1332
|
+
`);
|
|
1333
|
+
for (const { token, label } of records) {
|
|
1334
|
+
const marker = token === active ? "*" : " ";
|
|
1335
|
+
process.stdout.write(` ${marker} ${label ?? "(no label)"}
|
|
1336
|
+
`);
|
|
1337
|
+
process.stdout.write(` ${expiryLine(token)}
|
|
1338
|
+
`);
|
|
1339
|
+
}
|
|
1340
|
+
const envToken = process.env.TRANSCODES_TOKEN?.trim();
|
|
1341
|
+
if (envToken) {
|
|
1342
|
+
process.stdout.write(
|
|
1343
|
+
"\nNote: TRANSCODES_TOKEN is set and overrides the active selection above.\n"
|
|
1344
|
+
);
|
|
1345
|
+
} else {
|
|
1346
|
+
process.stdout.write("\n* = active token used by the plugins/hooks.\n");
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function cmdDashboard(args) {
|
|
1350
|
+
let port;
|
|
1351
|
+
let open = true;
|
|
1352
|
+
for (let i = 0; i < args.length; i++) {
|
|
1353
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
1354
|
+
port = Number(args[++i]);
|
|
1355
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1356
|
+
fail("--port must be an integer between 1 and 65535");
|
|
1357
|
+
}
|
|
1358
|
+
} else if (args[i] === "--no-open") {
|
|
1359
|
+
open = false;
|
|
1360
|
+
} else {
|
|
1361
|
+
fail(`unknown flag "${args[i]}". Usage: transcodes [--port N] [--no-open]`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
await runDashboard({ port, open });
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
231
1370
|
function main() {
|
|
232
1371
|
const [command, ...rest] = process.argv.slice(2);
|
|
233
1372
|
switch (command) {
|
|
234
|
-
case "
|
|
235
|
-
|
|
1373
|
+
case "set":
|
|
1374
|
+
cmdSet(rest);
|
|
1375
|
+
break;
|
|
1376
|
+
case "reset":
|
|
1377
|
+
cmdReset();
|
|
1378
|
+
break;
|
|
1379
|
+
case "enable":
|
|
1380
|
+
cmdSetEnabled(true);
|
|
236
1381
|
break;
|
|
237
|
-
case "
|
|
238
|
-
|
|
1382
|
+
case "disable":
|
|
1383
|
+
cmdSetEnabled(false);
|
|
239
1384
|
break;
|
|
240
1385
|
case "status":
|
|
241
1386
|
cmdStatus();
|
|
242
1387
|
break;
|
|
1388
|
+
case "tokens":
|
|
1389
|
+
cmdTokens();
|
|
1390
|
+
break;
|
|
243
1391
|
case "help":
|
|
244
1392
|
case "--help":
|
|
245
1393
|
case "-h":
|
|
246
|
-
case void 0:
|
|
247
1394
|
process.stdout.write(USAGE);
|
|
248
1395
|
break;
|
|
1396
|
+
case void 0:
|
|
1397
|
+
void cmdDashboard([]);
|
|
1398
|
+
break;
|
|
249
1399
|
default:
|
|
1400
|
+
if (command.startsWith("-")) {
|
|
1401
|
+
void cmdDashboard([command, ...rest]);
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
250
1404
|
fail(`unknown command "${command}". Run \`transcodes help\`.`);
|
|
251
1405
|
}
|
|
252
1406
|
}
|