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