@bigstrider/transcodes-cli 0.4.0 → 0.5.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 +4 -1
  2. package/dist/index.js +555 -109
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,13 +21,16 @@ Works the same on macOS, Linux, and Windows (Node ≥ 20).
21
21
 
22
22
  | Command | What it does |
23
23
  |---------|--------------|
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`). |
24
+ | `transcodes` | Opens the local dashboard (URL printed in the terminal; default port 3847, increments if busy) to paste, save, switch, label, or delete tokens (accepts `--port N` / `--no-open`). |
25
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
26
  | `transcodes tokens` | Lists all saved tokens; the active one is marked with `*`. |
27
27
  | `transcodes status` | Shows the active token source (env vs file) and its expiry. |
28
28
  | `transcodes reset` | Deletes all saved tokens. |
29
+ | `transcodes policy refresh` | Force-refreshes the org policy bundle cache (same as MCP `refresh_rules`). |
29
30
  | `transcodes help` | Shows the full command list and usage. |
30
31
 
32
+ Command descriptions are defined once in `cli/src/commands.ts` (SSOT) and shared with the dashboard CLI tab.
33
+
31
34
  ### Dashboard
32
35
 
33
36
  ```bash
package/dist/index.js CHANGED
@@ -345,7 +345,7 @@ function loadStepupConfig() {
345
345
  }
346
346
  const { token: tokenRaw } = resolveToken();
347
347
  if (!tokenRaw) {
348
- throw new Error("No Transcodes token found. Get a token from the Transcodes console (member detail page, https://app.transcodes.io), then run `transcodes login <token>` in a terminal \u2014 or set the TRANSCODES_TOKEN environment variable.");
348
+ throw new Error("No Transcodes token found. Install the CLI (`npm install -g @bigstrider/transcodes-cli`), run `transcodes` to open the dashboard, and paste a token from the Transcodes console (member detail page, https://app.transcodes.io). Non-interactive: `transcodes set <token> -l <label>`. For CI only, set the TRANSCODES_TOKEN environment variable.");
349
349
  }
350
350
  const parsed = parseMemberAccessToken(tokenRaw);
351
351
  for (const w of parsed.warnings) {
@@ -621,12 +621,11 @@ function normalizeRule(r) {
621
621
  resource: coerceRbacResource(r.resource)
622
622
  };
623
623
  }
624
- const provider = r.provider !== void 0 ? mapHostToProvider(r.provider) : void 0;
625
624
  return {
626
625
  ...r,
627
626
  type: "mcp",
628
627
  matcher: r.matcher ?? "exact",
629
- ...provider !== void 0 ? { provider } : {},
628
+ ...r.provider !== void 0 ? { provider: r.provider } : {},
630
629
  ...r.action !== void 0 ? { action: coerceRbacAction(r.action) } : {},
631
630
  ...r.resource !== void 0 ? { resource: coerceRbacResource(r.resource) } : {}
632
631
  };
@@ -641,12 +640,6 @@ function loadMergedToolRules(bundleRules = []) {
641
640
  }
642
641
  return [...merged.values()];
643
642
  }
644
- function mapHostToProvider(host) {
645
- if (!host)
646
- return void 0;
647
- const normalized = host === "claude-code" ? "claude" : host;
648
- return isGuardProvider(normalized) ? normalized : void 0;
649
- }
650
643
  var ToolRuleValidationError = class extends Error {
651
644
  };
652
645
  function detectShellCommand(name) {
@@ -5153,6 +5146,66 @@ async function updateToolRule(id, changes) {
5153
5146
  return merged;
5154
5147
  }
5155
5148
 
5149
+ // src/commands.ts
5150
+ var CLI_COMMAND_SPECS = [
5151
+ {
5152
+ usage: "transcodes",
5153
+ description: "Open the local dashboard (URL printed in terminal; default port 3847; --port N / --no-open)"
5154
+ },
5155
+ {
5156
+ usage: "transcodes set <token> -l <label>",
5157
+ description: "Validate and save a member token with a required label, then make it active"
5158
+ },
5159
+ {
5160
+ usage: "transcodes reset",
5161
+ description: "Remove all saved tokens"
5162
+ },
5163
+ {
5164
+ usage: "transcodes status",
5165
+ description: "Show the active token source and expiry"
5166
+ },
5167
+ {
5168
+ usage: "transcodes tokens",
5169
+ description: "List all saved tokens (active one marked with *)"
5170
+ },
5171
+ {
5172
+ usage: "transcodes policy refresh",
5173
+ description: "Force-refresh the org policy bundle cache now"
5174
+ },
5175
+ {
5176
+ usage: "transcodes help",
5177
+ description: "Show the full command list and how to use each one"
5178
+ }
5179
+ ];
5180
+ function escapeHtml(s) {
5181
+ return s.replace(
5182
+ /[&<>"]/g,
5183
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]
5184
+ );
5185
+ }
5186
+ function formatCliUsageFooter(configPath = transcodesConfigFile()) {
5187
+ return `The token is read by the transcodes-guard plugins/hooks with precedence:
5188
+ 1. TRANSCODES_TOKEN environment variable (overrides everything)
5189
+ 2. ${configPath}
5190
+ `;
5191
+ }
5192
+ function formatCliUsage(configPath = transcodesConfigFile()) {
5193
+ const usageLines = CLI_COMMAND_SPECS.map(
5194
+ (cmd) => ` ${cmd.usage.padEnd(34)}${cmd.description}`
5195
+ ).join("\n");
5196
+ return `transcodes \u2014 transcodes-guard token manager
5197
+
5198
+ Usage:
5199
+ ${usageLines}
5200
+
5201
+ ${formatCliUsageFooter(configPath)}`;
5202
+ }
5203
+ function renderCliCommandsHtml() {
5204
+ return CLI_COMMAND_SPECS.filter((cmd) => cmd.showInDashboard !== false).map(
5205
+ (cmd) => `<div class="cmd"><code>${escapeHtml(cmd.usage)}</code><span class="cmd-desc">${escapeHtml(cmd.description)}</span></div>`
5206
+ ).join("\n ");
5207
+ }
5208
+
5156
5209
  // src/dashboard.ts
5157
5210
  import { spawn } from "child_process";
5158
5211
  import { createHash as createHash2 } from "crypto";
@@ -5392,7 +5445,7 @@ var TRANSCODES_ADMIN_TOOLS = [
5392
5445
  {
5393
5446
  name: "update_member",
5394
5447
  title: "Update member",
5395
- description: "Update member fields (name, email, role, metadata).",
5448
+ description: "Update member profile fields (name, email, metadata). Use update_member_role to change a role.",
5396
5449
  category: "Members",
5397
5450
  access: "api",
5398
5451
  stepUpProtected: false
@@ -5465,7 +5518,7 @@ var TRANSCODES_ADMIN_TOOLS = [
5465
5518
  {
5466
5519
  name: "create_resource",
5467
5520
  title: "Create resource",
5468
- description: "Add a new RBAC resource key (defaults to deny for all roles).",
5521
+ description: "Add a new RBAC resource key (every role initialized to read=allow, write=allow+step-up).",
5469
5522
  category: "RBAC",
5470
5523
  access: "api",
5471
5524
  stepUpProtected: false
@@ -5497,7 +5550,7 @@ var TRANSCODES_ADMIN_TOOLS = [
5497
5550
  {
5498
5551
  name: "update_member_role",
5499
5552
  title: "Update member role",
5500
- description: "Change a member's assigned role.",
5553
+ description: "Change a member's assigned role (validates the role exists).",
5501
5554
  category: "RBAC",
5502
5555
  access: "api",
5503
5556
  stepUpProtected: true
@@ -5975,6 +6028,17 @@ function dashboardHtml() {
5975
6028
  object-fit: contain;
5976
6029
  background: #f4f4f6;
5977
6030
  padding: 8px;
6031
+ display: block;
6032
+ }
6033
+ .header-logo-link {
6034
+ flex-shrink: 0;
6035
+ border-radius: 14px;
6036
+ line-height: 0;
6037
+ transition: opacity 0.15s ease, box-shadow 0.15s ease;
6038
+ }
6039
+ .header-logo-link:hover {
6040
+ opacity: 0.88;
6041
+ box-shadow: 0 0 0 3px var(--accent-soft);
5978
6042
  }
5979
6043
  .header h1 {
5980
6044
  margin: 0;
@@ -5982,11 +6046,41 @@ function dashboardHtml() {
5982
6046
  font-weight: 700;
5983
6047
  letter-spacing: -0.02em;
5984
6048
  }
5985
- .header p {
5986
- margin: 5px 0 0;
6049
+ .header-title-link {
6050
+ color: inherit;
6051
+ text-decoration: none;
6052
+ transition: color 0.15s ease;
6053
+ }
6054
+ .header-title-link:hover {
6055
+ color: var(--accent);
6056
+ }
6057
+ .header-title-row {
6058
+ display: flex;
6059
+ align-items: center;
6060
+ flex-wrap: wrap;
6061
+ gap: 8px 12px;
6062
+ }
6063
+ .header-tagline {
6064
+ margin: 6px 0 0;
5987
6065
  font-size: var(--text-sm);
5988
6066
  color: var(--muted);
5989
6067
  }
6068
+ .header-status {
6069
+ display: inline-flex;
6070
+ align-items: center;
6071
+ gap: 8px;
6072
+ font-size: var(--text-2xs);
6073
+ font-weight: 600;
6074
+ color: var(--muted);
6075
+ }
6076
+ .status-dot {
6077
+ width: 8px;
6078
+ height: 8px;
6079
+ border-radius: 50%;
6080
+ background: #22c55e;
6081
+ flex-shrink: 0;
6082
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.22);
6083
+ }
5990
6084
  .tabs {
5991
6085
  display: flex;
5992
6086
  gap: 4px;
@@ -6024,7 +6118,52 @@ function dashboardHtml() {
6024
6118
  .section-sub {
6025
6119
  font-size: var(--text-base);
6026
6120
  color: var(--muted);
6027
- margin: 0 0 20px;
6121
+ margin: 0 0 16px;
6122
+ }
6123
+ .cli-map-row {
6124
+ display: flex;
6125
+ align-items: center;
6126
+ flex-wrap: wrap;
6127
+ gap: 6px;
6128
+ margin: 0 0 16px;
6129
+ font-size: var(--text-2xs);
6130
+ font-weight: 400;
6131
+ color: var(--muted);
6132
+ }
6133
+ .cli-map-row .cli-map-label {
6134
+ font-size: var(--text-sm);
6135
+ font-weight: 600;
6136
+ letter-spacing: 0.01em;
6137
+ }
6138
+ .cli-map-row .cli-map-label--title {
6139
+ font-size: var(--text-lg);
6140
+ font-weight: 700;
6141
+ color: var(--ink);
6142
+ letter-spacing: -0.01em;
6143
+ }
6144
+ .cli-map-row .cli-map-label--danger {
6145
+ font-size: var(--text-sm);
6146
+ font-weight: 700;
6147
+ color: #c0392f;
6148
+ }
6149
+ .cli-map-row .cli-map-label--ink { color: var(--ink); }
6150
+ .cli-map-row code {
6151
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
6152
+ background: var(--accent-soft);
6153
+ color: var(--accent);
6154
+ padding: 2px 8px;
6155
+ border-radius: 6px;
6156
+ font-size: var(--text-2xs);
6157
+ font-weight: 600;
6158
+ }
6159
+ .cli-map-row--list {
6160
+ margin: 26px 0 10px;
6161
+ }
6162
+ .cli-map-row--section {
6163
+ margin: 0 0 4px;
6164
+ }
6165
+ .cli-map-row--danger {
6166
+ margin: 0;
6028
6167
  }
6029
6168
  textarea {
6030
6169
  width: 100%;
@@ -6089,6 +6228,67 @@ function dashboardHtml() {
6089
6228
  color: #5a5a64;
6090
6229
  }
6091
6230
  .btn-secondary:hover:not(:disabled) { background: #ececf0; }
6231
+ .btn-inline-action {
6232
+ display: inline-flex;
6233
+ align-items: center;
6234
+ gap: 6px;
6235
+ padding: 7px 14px;
6236
+ font-size: var(--text-2xs);
6237
+ font-weight: 600;
6238
+ color: var(--accent);
6239
+ background: var(--accent-soft);
6240
+ border: none;
6241
+ border-radius: 9px;
6242
+ cursor: pointer;
6243
+ transition: background 0.15s ease;
6244
+ }
6245
+ .btn-inline-action:hover:not(:disabled) { background: #e3e1f7; }
6246
+ .btn-inline-action:disabled { opacity: 0.55; cursor: default; }
6247
+ .danger-zone {
6248
+ display: flex;
6249
+ align-items: center;
6250
+ justify-content: space-between;
6251
+ gap: 16px;
6252
+ flex-wrap: wrap;
6253
+ margin-top: 28px;
6254
+ padding: 18px 20px;
6255
+ border: 1px solid #f0d9d6;
6256
+ border-radius: 16px;
6257
+ background: #fdf6f5;
6258
+ }
6259
+ .danger-zone-desc {
6260
+ margin: 4px 0 0;
6261
+ font-size: var(--text-2xs);
6262
+ color: #8a8a94;
6263
+ line-height: 1.5;
6264
+ }
6265
+ .btn-danger {
6266
+ padding: 11px 22px;
6267
+ font-size: var(--text-sm);
6268
+ font-weight: 700;
6269
+ color: #fff;
6270
+ background: #c0392f;
6271
+ border: none;
6272
+ border-radius: 11px;
6273
+ cursor: pointer;
6274
+ transition: background 0.15s ease;
6275
+ white-space: nowrap;
6276
+ }
6277
+ .btn-danger:hover:not(:disabled) { background: #a52f26; }
6278
+ .btn-danger:disabled { opacity: 0.55; cursor: default; }
6279
+ .policy-refresh-bar {
6280
+ display: flex;
6281
+ align-items: center;
6282
+ justify-content: space-between;
6283
+ gap: 12px;
6284
+ flex-wrap: wrap;
6285
+ margin-bottom: 18px;
6286
+ padding: 14px 16px;
6287
+ border: 1px solid var(--line);
6288
+ border-radius: 14px;
6289
+ background: #fbfbfc;
6290
+ }
6291
+ .policy-refresh-bar .cli-map-row { margin: 0; flex: 1; min-width: 0; }
6092
6292
  .list-label {
6093
6293
  margin: 26px 0 10px;
6094
6294
  font-size: var(--text-sm);
@@ -6224,6 +6424,39 @@ function dashboardHtml() {
6224
6424
  .perm-chip-1 { color: #166534; border-color: #86efac; background: #f0fdf4; }
6225
6425
  .perm-chip-2 { color: #7c3aed; border-color: #d8b4fe; background: #faf5ff; }
6226
6426
  .field-k-rbac { color: #8a3ffc; font-weight: 600; }
6427
+ .field-status {
6428
+ display: flex;
6429
+ align-items: center;
6430
+ flex-wrap: wrap;
6431
+ gap: 6px 10px;
6432
+ }
6433
+ .status-chip {
6434
+ display: inline-block;
6435
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
6436
+ font-size: var(--text-xs);
6437
+ font-weight: 600;
6438
+ padding: 1px 7px;
6439
+ border-radius: 6px;
6440
+ border: 1px solid;
6441
+ text-transform: lowercase;
6442
+ line-height: inherit;
6443
+ vertical-align: baseline;
6444
+ }
6445
+ .status-chip-active {
6446
+ color: #166534;
6447
+ border-color: #86efac;
6448
+ background: #f0fdf4;
6449
+ }
6450
+ .status-chip-inactive {
6451
+ color: #9f1239;
6452
+ border-color: #fda4af;
6453
+ background: #fff1f2;
6454
+ }
6455
+ .status-hint {
6456
+ font-size: var(--text-2xs);
6457
+ color: var(--muted);
6458
+ line-height: 1.45;
6459
+ }
6227
6460
  .admin-tools-count {
6228
6461
  font-size: var(--text-sm);
6229
6462
  color: var(--muted);
@@ -6412,6 +6645,40 @@ function dashboardHtml() {
6412
6645
  flex-direction: column;
6413
6646
  gap: 10px;
6414
6647
  }
6648
+ .guide-groups {
6649
+ display: flex;
6650
+ flex-direction: column;
6651
+ gap: 22px;
6652
+ }
6653
+ .guide-group-label {
6654
+ margin: 0 0 10px;
6655
+ font-size: var(--text-sm);
6656
+ font-weight: 700;
6657
+ color: var(--ink);
6658
+ letter-spacing: 0.01em;
6659
+ }
6660
+ a.guide-group-label {
6661
+ display: inline-flex;
6662
+ align-items: center;
6663
+ gap: 6px;
6664
+ text-decoration: none;
6665
+ color: var(--ink);
6666
+ }
6667
+ a.guide-group-label:hover {
6668
+ color: var(--accent);
6669
+ }
6670
+ a.guide-group-label:hover .guide-group-link-icon {
6671
+ color: var(--accent);
6672
+ }
6673
+ .guide-group-link-icon {
6674
+ display: inline-flex;
6675
+ align-items: center;
6676
+ color: var(--muted);
6677
+ line-height: 0;
6678
+ transition: color 0.15s ease;
6679
+ }
6680
+ .guide-group--panel .guide-group-label { color: var(--accent); }
6681
+ .guide-group--agent .guide-group-label { color: #5a5a64; }
6415
6682
  .guide-step {
6416
6683
  display: flex;
6417
6684
  gap: 14px;
@@ -6455,6 +6722,15 @@ function dashboardHtml() {
6455
6722
  padding: 1px 6px;
6456
6723
  border-radius: 6px;
6457
6724
  }
6725
+ .guide-step-desc-row {
6726
+ display: flex;
6727
+ align-items: center;
6728
+ flex-wrap: wrap;
6729
+ gap: 8px 12px;
6730
+ }
6731
+ .guide-step .toast {
6732
+ margin-top: 10px;
6733
+ }
6458
6734
  .guide-help {
6459
6735
  margin: 0 0 18px;
6460
6736
  padding: 16px 18px;
@@ -6506,10 +6782,18 @@ function dashboardHtml() {
6506
6782
  <body>
6507
6783
  <div class="card">
6508
6784
  <div class="header">
6509
- <img class="avatar" src="${LOGO_DATA_URI}" alt="Transcodes" />
6785
+ <a class="header-logo-link" href="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer" aria-label="Open Transcodes console">
6786
+ <img class="avatar" src="${LOGO_DATA_URI}" alt="Transcodes" />
6787
+ </a>
6510
6788
  <div>
6511
- <h1>Transcodes</h1>
6512
- <p>CLI Dashboard</p>
6789
+ <div class="header-title-row">
6790
+ <h1><a class="header-title-link" href="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Transcodes</a> CLI Panel</h1>
6791
+ <span class="header-status">
6792
+ <span class="status-dot" aria-hidden="true"></span>
6793
+ Connected
6794
+ </span>
6795
+ </div>
6796
+ <p class="header-tagline">Manage credentials and rules from one panel \u2014 no CLI typing required</p>
6513
6797
  </div>
6514
6798
  </div>
6515
6799
  <div class="tabs">
@@ -6536,61 +6820,104 @@ function dashboardHtml() {
6536
6820
  ></mux-player>
6537
6821
  </div>
6538
6822
  <p class="list-label">Quick setup</p>
6539
- <ol class="guide-steps">
6540
- <li class="guide-step">
6541
- <span class="guide-step-num">1</span>
6542
- <div class="guide-step-body">
6543
- <p class="guide-step-title">Create a project</p>
6544
- <p class="guide-step-desc">In the Transcodes web console, create a new project for your app</p>
6545
- </div>
6546
- </li>
6547
- <li class="guide-step">
6548
- <span class="guide-step-num">2</span>
6549
- <div class="guide-step-body">
6550
- <p class="guide-step-title">Create an authentication cluster</p>
6551
- <p class="guide-step-desc">Add an authentication cluster to the project to define how members sign in and authenticate</p>
6552
- </div>
6553
- </li>
6554
- <li class="guide-step">
6555
- <span class="guide-step-num">3</span>
6556
- <div class="guide-step-body">
6557
- <p class="guide-step-title">Add a member</p>
6558
- <p class="guide-step-desc">Invite or add a member to the project so they can be issued an access token</p>
6559
- </div>
6560
- </li>
6561
- <li class="guide-step">
6562
- <span class="guide-step-num">4</span>
6563
- <div class="guide-step-body">
6564
- <p class="guide-step-title">Issue an access token</p>
6565
- <p class="guide-step-desc">Open the member detail page and issue a Member Access Token (MAT) for the agent</p>
6566
- </div>
6567
- </li>
6568
- <li class="guide-step">
6569
- <span class="guide-step-num">5</span>
6570
- <div class="guide-step-body">
6571
- <p class="guide-step-title">Add it here in the CLI dashboard</p>
6572
- <p class="guide-step-desc">Paste the token in the Tokens tab with a label (e.g. <code>transcodes-myapp-dev</code>) \u2014 the plugin reads it from <code>~/.transcodes/config.json</code></p>
6573
- </div>
6574
- </li>
6575
- <li class="guide-step">
6576
- <span class="guide-step-num">6</span>
6577
- <div class="guide-step-body">
6578
- <p class="guide-step-title">Ask your local agent to add a custom rule</p>
6579
- <p class="guide-step-desc">From your agent, request a guard rule (e.g. <code>add_tool_rule</code> / <code>add_user_pattern</code>) \u2014 it is registered as <code>inactive</code></p>
6580
- </div>
6581
- </li>
6582
- <li class="guide-step">
6583
- <span class="guide-step-num">7</span>
6584
- <div class="guide-step-body">
6585
- <p class="guide-step-title">Approve it in the console</p>
6586
- <p class="guide-step-desc">Switch the rule to <code>active</code> in the Transcodes web console \u2014 only then is it enforced</p>
6587
- </div>
6588
- </li>
6589
- </ol>
6823
+ <div class="guide-groups">
6824
+ <section class="guide-group guide-group--console">
6825
+ <a class="guide-group-label" href="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Transcodes Console<span class="guide-group-link-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span></a>
6826
+ <ol class="guide-steps">
6827
+ <li class="guide-step">
6828
+ <span class="guide-step-num">1</span>
6829
+ <div class="guide-step-body">
6830
+ <p class="guide-step-title">Create a project</p>
6831
+ <p class="guide-step-desc">Create a new project for your app</p>
6832
+ </div>
6833
+ </li>
6834
+ <li class="guide-step">
6835
+ <span class="guide-step-num">2</span>
6836
+ <div class="guide-step-body">
6837
+ <p class="guide-step-title">Create an authentication cluster</p>
6838
+ <p class="guide-step-desc">Add an authentication cluster to define how members sign in and authenticate</p>
6839
+ </div>
6840
+ </li>
6841
+ <li class="guide-step">
6842
+ <span class="guide-step-num">3</span>
6843
+ <div class="guide-step-body">
6844
+ <p class="guide-step-title">Add a member</p>
6845
+ <p class="guide-step-desc">Invite or add a member so they can be issued an access token</p>
6846
+ </div>
6847
+ </li>
6848
+ <li class="guide-step">
6849
+ <span class="guide-step-num">4</span>
6850
+ <div class="guide-step-body">
6851
+ <p class="guide-step-title">Issue an access token</p>
6852
+ <p class="guide-step-desc">Open the member detail page and issue a Member Access Token (MAT) for the agent</p>
6853
+ </div>
6854
+ </li>
6855
+ </ol>
6856
+ </section>
6857
+
6858
+ <section class="guide-group guide-group--panel">
6859
+ <p class="guide-group-label">This CLI panel</p>
6860
+ <ol class="guide-steps">
6861
+ <li class="guide-step">
6862
+ <span class="guide-step-num">5</span>
6863
+ <div class="guide-step-body">
6864
+ <p class="guide-step-title">Save the token</p>
6865
+ <p class="guide-step-desc">Paste the token in the Tokens tab with a label (e.g. <code>transcodes-myapp-dev</code>) \u2014 the plugin reads it from <code>{{HOME_DIR}}/.transcodes/config.json</code></p>
6866
+ </div>
6867
+ </li>
6868
+ </ol>
6869
+ </section>
6870
+
6871
+ <section class="guide-group guide-group--agent">
6872
+ <p class="guide-group-label">LLM agent (Claude, Cursor, Codex, Antigravity)</p>
6873
+ <ol class="guide-steps">
6874
+ <li class="guide-step">
6875
+ <span class="guide-step-num">6</span>
6876
+ <div class="guide-step-body">
6877
+ <p class="guide-step-title">Ask your local agent to add a custom rule</p>
6878
+ <p class="guide-step-desc">Use <code>add_tool_rule</code> or <code>add_user_pattern</code> \u2014 on first registration, rules are <code>inactive</code> and not applied to MCP</p>
6879
+ </div>
6880
+ </li>
6881
+ </ol>
6882
+ </section>
6883
+
6884
+ <section class="guide-group guide-group--console">
6885
+ <a class="guide-group-label" href="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Transcodes Console<span class="guide-group-link-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span></a>
6886
+ <ol class="guide-steps">
6887
+ <li class="guide-step">
6888
+ <span class="guide-step-num">7</span>
6889
+ <div class="guide-step-body">
6890
+ <p class="guide-step-title">Approve the rule</p>
6891
+ <p class="guide-step-desc">Switch the rule to <code>active</code> \u2014 only then is it enforced on MCP</p>
6892
+ </div>
6893
+ </li>
6894
+ </ol>
6895
+ </section>
6896
+
6897
+ <section class="guide-group guide-group--panel">
6898
+ <p class="guide-group-label">This CLI panel</p>
6899
+ <ol class="guide-steps">
6900
+ <li class="guide-step">
6901
+ <span class="guide-step-num">8</span>
6902
+ <div class="guide-step-body">
6903
+ <p class="guide-step-title">Refresh active rules locally</p>
6904
+ <p class="guide-step-desc guide-step-desc-row">
6905
+ Run terminal <code>transcodes policy refresh</code> or
6906
+ <button type="button" class="btn-inline-action" id="guide-policy-refresh">Refresh</button>
6907
+ </p>
6908
+ <div id="guide-toast" class="toast"></div>
6909
+ </div>
6910
+ </li>
6911
+ </ol>
6912
+ </section>
6913
+ </div>
6590
6914
  </div>
6591
6915
 
6592
6916
  <div class="panel" id="panel-tokens">
6593
- <p class="section-title">MCP Agent Token</p>
6917
+ <p class="cli-map-row cli-map-row--section">
6918
+ <span class="cli-map-label cli-map-label--title">MCP Access Token</span>
6919
+ <code>transcodes set &lt;token&gt; -l &lt;label&gt;</code>
6920
+ </p>
6594
6921
  <p class="section-sub">Paste the token from your Transcodes console member detail page</p>
6595
6922
  <textarea id="token" placeholder="eyJhbGciOi\u2026" spellcheck="false" autocomplete="off"></textarea>
6596
6923
  <input type="text" id="label" class="label-input" placeholder="Label (required) \u2014 e.g. transcodes-{project_name}-{env}" autocomplete="off" required />
@@ -6599,8 +6926,21 @@ function dashboardHtml() {
6599
6926
  <button type="button" class="btn-secondary" id="clear">Clear</button>
6600
6927
  </div>
6601
6928
  <div id="toast" class="toast"></div>
6602
- <p class="list-label">Saved tokens</p>
6929
+ <p class="cli-map-row cli-map-row--list">
6930
+ <span class="cli-map-label">Saved tokens</span>
6931
+ <code>transcodes tokens</code>
6932
+ </p>
6603
6933
  <div class="token-list" id="token-list"></div>
6934
+ <div class="danger-zone">
6935
+ <div class="danger-zone-text">
6936
+ <p class="cli-map-row cli-map-row--danger">
6937
+ <span class="cli-map-label cli-map-label--danger">Reset all tokens</span>
6938
+ <code>transcodes reset</code>
6939
+ </p>
6940
+ <p class="danger-zone-desc">Remove every saved token from this machine</p>
6941
+ </div>
6942
+ <button type="button" class="btn-danger" id="reset-all">Reset all</button>
6943
+ </div>
6604
6944
  <p class="hint">Saved to <code>{{HOME_DIR}}/.transcodes/config.json</code><br />Press Ctrl+C in the terminal to stop</p>
6605
6945
  </div>
6606
6946
 
@@ -6608,22 +6948,18 @@ function dashboardHtml() {
6608
6948
  <p class="section-title">CLI Commands</p>
6609
6949
  <p class="section-sub">Run these from your terminal \u2014 the dashboard wraps the same actions</p>
6610
6950
  <div class="cmd-list">
6611
- <div class="cmd"><code>transcodes</code><span class="cmd-desc">Open this dashboard (default, same as transcodes dashboard)</span></div>
6612
- <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>
6613
- <div class="cmd"><code>transcodes tokens</code><span class="cmd-desc">List all saved tokens (active one marked with *)</span></div>
6614
- <div class="cmd"><code>transcodes reset</code><span class="cmd-desc">Remove all saved tokens</span></div>
6615
- <div class="cmd"><code>transcodes help</code><span class="cmd-desc">Show the full command list and how to use each one</span></div>
6951
+ ${renderCliCommandsHtml()}
6616
6952
  </div>
6617
6953
  </div>
6618
6954
 
6619
6955
  <div class="panel" id="panel-rules">
6620
6956
  <p id="policy-token-warning" class="policy-token-warning" hidden>transcodes\uB97C \uB85C\uCEEC \uC5D0\uC774\uC804\uD2B8\uC5D0\uC11C \uC774\uC6A9\uD558\uAE30 \uC704\uD574\uC120 \uD1A0\uD070\uC774 \uD558\uB098 \uC774\uC0C1 \uB4F1\uB85D\uC774 \uB418\uC5B4\uC57C \uD569\uB2C8\uB2E4</p>
6621
6957
  <div class="tabs sub-tabs" role="tablist" aria-label="Rule type">
6622
- <button type="button" class="tab active" data-policy="project" role="tab">Project Rules</button>
6623
- <button type="button" class="tab" data-policy="admin" role="tab">Admin MCP</button>
6958
+ <button type="button" class="tab active" data-policy="project" role="tab">Project</button>
6959
+ <button type="button" class="tab" data-policy="admin" role="tab">Transcodes</button>
6624
6960
  </div>
6625
6961
  <div class="policy-pane active" id="policy-pane-project">
6626
- <p class="section-title">Step-up Guard Rules</p>
6962
+ <p class="section-title">Custom Project Step-up Rules</p>
6627
6963
  <p class="section-sub">When an active rule matches an agent action, Transcodes step-up authentication is triggered. Rules are registered only from your local agent; activating and deleting them is done in the Transcodes web console</p>
6628
6964
  <div class="usage">
6629
6965
  <p class="usage-title">How rules work</p>
@@ -6639,15 +6975,22 @@ function dashboardHtml() {
6639
6975
  <span class="q">\u2192 the agent registers it as <code>inactive</code>; activate it in the Transcodes web console to start enforcing</span>
6640
6976
  </div>
6641
6977
  </div>
6978
+ <div class="policy-refresh-bar">
6979
+ <p class="cli-map-row">
6980
+ <span class="cli-map-label cli-map-label--ink">Refresh Custom Project Rules</span>
6981
+ <code>transcodes policy refresh</code>
6982
+ </p>
6983
+ <button type="button" class="btn-inline-action" id="policy-refresh">Refresh</button>
6984
+ </div>
6642
6985
  <div id="rules-toast" class="toast"></div>
6643
- <p class="list-label">Your project rules</p>
6986
+ <p class="list-label">Your Project MCP Rules</p>
6644
6987
  <div class="token-list" id="project-rules-list"></div>
6645
6988
  <p class="list-label">System bash patterns (read-only)</p>
6646
6989
  <div class="token-list" id="system-patterns-list"></div>
6647
6990
  </div>
6648
6991
 
6649
6992
  <div class="policy-pane" id="policy-pane-admin">
6650
- <p class="section-title">Transcodes Admin MCP</p>
6993
+ <p class="section-title">Transcodes MCP Rules</p>
6651
6994
  <p class="section-sub">Backend API tools exposed via MCP \u2014 agents call these through the transcodes-guard plugin. Read-only reference for system tool-rules.</p>
6652
6995
  <div class="rbac-legend">
6653
6996
  <p class="rbac-legend-title">Role permission check</p>
@@ -6886,6 +7229,28 @@ function dashboardHtml() {
6886
7229
  tokenEl.focus();
6887
7230
  });
6888
7231
 
7232
+ const resetAllBtn = document.getElementById("reset-all");
7233
+ resetAllBtn.addEventListener("click", async () => {
7234
+ const count = (lastStatus.tokens || []).length;
7235
+ if (count === 0) {
7236
+ showToast("No tokens to reset", "error");
7237
+ return;
7238
+ }
7239
+ if (!confirm("Remove ALL " + count + " saved token(s)? This cannot be undone.")) return;
7240
+ resetAllBtn.disabled = true;
7241
+ try {
7242
+ const res = await fetch("/api/tokens", { method: "DELETE" });
7243
+ const data = await res.json();
7244
+ if (!res.ok) throw new Error(data.error || "Reset failed");
7245
+ showToast("All tokens removed", "success");
7246
+ refresh();
7247
+ } catch (e) {
7248
+ showToast(e.message || "Reset failed", "error");
7249
+ } finally {
7250
+ resetAllBtn.disabled = false;
7251
+ }
7252
+ });
7253
+
6889
7254
  const rulesToastEl = document.getElementById("rules-toast");
6890
7255
  const projectRulesListEl = document.getElementById("project-rules-list");
6891
7256
  const systemPatternsListEl = document.getElementById("system-patterns-list");
@@ -6915,6 +7280,41 @@ function dashboardHtml() {
6915
7280
  );
6916
7281
  }
6917
7282
 
7283
+ function ruleStatusField(status) {
7284
+ if (!status) return "";
7285
+ const chipClass =
7286
+ status === "active"
7287
+ ? "status-chip-active"
7288
+ : status === "inactive"
7289
+ ? "status-chip-inactive"
7290
+ : "";
7291
+ const chip = chipClass
7292
+ ? '<span class="status-chip ' + chipClass + '">' + esc(status) + "</span>"
7293
+ : "<code>" + esc(status) + "</code>";
7294
+ const hint =
7295
+ status === "inactive"
7296
+ ? '<span class="status-hint">Activate in the console to take effect on MCP</span>'
7297
+ : "";
7298
+ return (
7299
+ '<div class="field field-status">' +
7300
+ '<span class="k">status</span> ' +
7301
+ chip +
7302
+ hint +
7303
+ "</div>"
7304
+ );
7305
+ }
7306
+
7307
+ function ruleResourceActionField(r) {
7308
+ const resource = r.resource || "\u2014";
7309
+ const action = r.action || "\u2014";
7310
+ return (
7311
+ '<div class="field">' +
7312
+ '<span class="k">resource / action</span> ' +
7313
+ "<code>" + esc(resource) + "</code> / <code>" + esc(action) + "</code>" +
7314
+ "</div>"
7315
+ );
7316
+ }
7317
+
6918
7318
  function projectRuleRow(r) {
6919
7319
  const editing = r.id === ruleEditingId;
6920
7320
  const idField = '<div class="field"><span class="k">id</span> <code>' + esc(r.id) + '</code></div>';
@@ -6946,19 +7346,14 @@ function dashboardHtml() {
6946
7346
  const description = r.description
6947
7347
  ? '<div class="label">' + esc(r.description) + '</div>'
6948
7348
  : '';
7349
+ const status = ruleStatusField(r.status);
7350
+ const resourceAction = ruleResourceActionField(r);
6949
7351
  const matcher =
6950
7352
  '<div class="field"><span class="k">matcher</span> <code>' + esc(r.matcher || (r.type === 'bash' ? 'regex' : 'exact')) + '</code></div>';
6951
- const action =
6952
- '<div class="field"><span class="k">action</span> <code>' + esc(r.action || '\u2014') + '</code></div>';
6953
- const resource =
6954
- '<div class="field"><span class="k">resource</span> <code>' + esc(r.resource || '\u2014') + '</code></div>';
6955
- const status = r.status
6956
- ? '<div class="field"><span class="k">status</span> <code>' + esc(r.status) + '</code></div>'
6957
- : '';
6958
7353
  return (
6959
7354
  '<div class="token-row">' +
6960
7355
  '<div class="token-top">' +
6961
- '<div class="token-info">' + description + typeField + idField + labelField + matcher + action + resource + status + '</div>' +
7356
+ '<div class="token-info">' + description + status + resourceAction + typeField + idField + labelField + matcher + '</div>' +
6962
7357
  '</div>' +
6963
7358
  '<div class="token-actions">' +
6964
7359
  '<button type="button" class="btn-edit" data-edit-rule="' + esc(r.id) + '">EDIT</button>' +
@@ -6993,6 +7388,42 @@ function dashboardHtml() {
6993
7388
  renderProjectRules(await res.json());
6994
7389
  }
6995
7390
 
7391
+ const guideToastEl = document.getElementById("guide-toast");
7392
+ function showGuideToast(msg, kind) {
7393
+ guideToastEl.textContent = msg;
7394
+ guideToastEl.className = "toast show " + (kind || "success");
7395
+ setTimeout(() => guideToastEl.classList.remove("show"), 4000);
7396
+ }
7397
+
7398
+ async function runPolicyRefresh(btn, showToastFn) {
7399
+ btn.disabled = true;
7400
+ const original = btn.textContent;
7401
+ btn.textContent = "Refreshing\u2026";
7402
+ try {
7403
+ const res = await fetch("/api/policy/refresh", { method: "POST" });
7404
+ const data = await res.json();
7405
+ if (!res.ok) throw new Error(data.error || "Refresh failed");
7406
+ const detail = data.revision != null
7407
+ ? " (revision " + data.revision + ", " + data.rules + " rules)"
7408
+ : "";
7409
+ showToastFn(
7410
+ (data.outcome === "not-modified" ? "Policy already current" : "Policy bundle refreshed") + detail,
7411
+ "success");
7412
+ loadProjectRules();
7413
+ } catch (e) {
7414
+ showToastFn(e.message || "Refresh failed", "error");
7415
+ } finally {
7416
+ btn.disabled = false;
7417
+ btn.textContent = original;
7418
+ }
7419
+ }
7420
+
7421
+ const policyRefreshBtn = document.getElementById("policy-refresh");
7422
+ policyRefreshBtn.addEventListener("click", () => runPolicyRefresh(policyRefreshBtn, showRulesToast));
7423
+ document.getElementById("guide-policy-refresh").addEventListener(
7424
+ "click",
7425
+ () => runPolicyRefresh(document.getElementById("guide-policy-refresh"), showGuideToast));
7426
+
6996
7427
  function findEditingRule(id) {
6997
7428
  return (lastProjectRules.project || []).find((r) => r.id === id);
6998
7429
  }
@@ -7046,10 +7477,9 @@ function dashboardHtml() {
7046
7477
  ? '<div class="tool-badges"><span class="tool-badge rbac-gated" title="Outcome follows your role matrix: 0 block, 1 allow, 2 step-up MFA">Role permission check</span></div>'
7047
7478
  : '';
7048
7479
  const rbacFields = t.rbacGated
7049
- ? '<div class="field"><span class="k field-k-rbac">permission action</span> <code>' +
7050
- esc(t.rbacAction || 'update') + '</code></div>' +
7051
- '<div class="field"><span class="k field-k-rbac">RBAC resource</span> <code>' +
7052
- esc(t.rbacResource || 'system') + '</code></div>'
7480
+ ? '<div class="field"><span class="k field-k-rbac">Resource / Action</span> : ' +
7481
+ '<code>' + esc(t.rbacResource || 'system') + '</code> / <code>' +
7482
+ esc(t.rbacAction || 'update') + '</code></div>'
7053
7483
  : '';
7054
7484
  return (
7055
7485
  '<div class="token-row">' +
@@ -7184,6 +7614,37 @@ function listen(port) {
7184
7614
  sendJson(res, 200, { ok: true, ...buildStatus() });
7185
7615
  return;
7186
7616
  }
7617
+ if (method === "DELETE" && url === "/api/tokens") {
7618
+ clearTokenFile();
7619
+ sendJson(res, 200, { ok: true, ...buildStatus() });
7620
+ return;
7621
+ }
7622
+ if (method === "POST" && url === "/api/policy/refresh") {
7623
+ let config;
7624
+ try {
7625
+ config = loadStepupConfig();
7626
+ } catch (err) {
7627
+ sendJson(res, 400, {
7628
+ error: err instanceof Error ? err.message : String(err)
7629
+ });
7630
+ return;
7631
+ }
7632
+ const outcome = await refreshPolicyBundle(config, { force: true });
7633
+ if (outcome === "failed") {
7634
+ sendJson(res, 502, {
7635
+ error: "policy bundle refresh failed \u2014 previous cache (if any) kept"
7636
+ });
7637
+ return;
7638
+ }
7639
+ const cached = readCachedPolicyBundle(config.projectId);
7640
+ sendJson(res, 200, {
7641
+ ok: true,
7642
+ outcome,
7643
+ revision: cached?.bundle.revision,
7644
+ rules: cached?.bundle.rules.length
7645
+ });
7646
+ return;
7647
+ }
7187
7648
  if (method === "GET" && url === "/api/project-rules") {
7188
7649
  sendJson(res, 200, await fetchProjectRulesFromBackend());
7189
7650
  return;
@@ -7344,21 +7805,6 @@ async function runDashboard(options) {
7344
7805
  }
7345
7806
 
7346
7807
  // src/index.ts
7347
- var USAGE = `transcodes \u2014 transcodes-guard token manager
7348
-
7349
- Usage:
7350
- transcodes Open the dashboard at http://127.0.0.1:3847/ (add --port N or --no-open)
7351
- transcodes set <token> -l <label> Save your Transcodes member token (label required) to ${transcodesConfigFile()}
7352
- transcodes reset Remove all saved tokens
7353
- transcodes status Show the active token source and expiry
7354
- transcodes tokens List all saved tokens (active one marked with *)
7355
- transcodes policy refresh Force-refresh the org policy bundle cache now
7356
- transcodes help Show this message
7357
-
7358
- The token is read by the transcodes-guard plugins/hooks with precedence:
7359
- 1. TRANSCODES_TOKEN environment variable (overrides everything)
7360
- 2. ${transcodesConfigFile()}
7361
- `;
7362
7808
  function fail(message) {
7363
7809
  process.stderr.write(`transcodes: ${message}
7364
7810
  `);
@@ -7532,7 +7978,7 @@ function main() {
7532
7978
  case "help":
7533
7979
  case "--help":
7534
7980
  case "-h":
7535
- process.stdout.write(USAGE);
7981
+ process.stdout.write(formatCliUsage());
7536
7982
  break;
7537
7983
  case void 0:
7538
7984
  void cmdDashboard([]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigstrider/transcodes-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Transcodes CLI — manage transcodes-guard member tokens and the step-up gate (set/reset/status/tokens, enable/disable, dashboard).",
5
5
  "type": "module",
6
6
  "bin": {