@bigstrider/transcodes-cli 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +217 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -113,6 +113,31 @@ function transcodesConfigDir() {
113
113
  function transcodesConfigFile() {
114
114
  return path.join(transcodesConfigDir(), CONFIG_FILE_NAME);
115
115
  }
116
+ function readRawConfig() {
117
+ let raw;
118
+ try {
119
+ raw = readFileSync(transcodesConfigFile(), "utf8");
120
+ } catch {
121
+ return null;
122
+ }
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(raw);
126
+ } catch {
127
+ return null;
128
+ }
129
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
130
+ return null;
131
+ }
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
+ }
116
141
  function normalizeToken(v) {
117
142
  if (typeof v !== "string")
118
143
  return null;
@@ -140,22 +165,10 @@ function normalizeRecord(item) {
140
165
  return null;
141
166
  }
142
167
  function readConfig() {
143
- let raw;
144
- try {
145
- raw = readFileSync(transcodesConfigFile(), "utf8");
146
- } catch {
147
- return { token: null, tokenList: [] };
148
- }
149
- let parsed;
150
- try {
151
- parsed = JSON.parse(raw);
152
- } catch {
153
- return { token: null, tokenList: [] };
154
- }
155
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
168
+ const obj = readRawConfig();
169
+ if (!obj) {
156
170
  return { token: null, tokenList: [] };
157
171
  }
158
- const obj = parsed;
159
172
  const list = [];
160
173
  const seen = /* @__PURE__ */ new Set();
161
174
  const push = (rec) => {
@@ -183,10 +196,12 @@ function readConfig() {
183
196
  };
184
197
  }
185
198
  function writeConfig(config) {
186
- const dir = transcodesConfigDir();
187
- mkdirSync(dir, { recursive: true, mode: 448 });
188
199
  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 });
200
+ writeRawConfig({
201
+ ...readRawConfig() ?? {},
202
+ token: config.token,
203
+ token_list
204
+ });
190
205
  }
191
206
  function readTokenFromFile() {
192
207
  return readConfig().token;
@@ -239,11 +254,23 @@ function removeTokenFromFile(token) {
239
254
  writeConfig({ token: active, tokenList });
240
255
  }
241
256
  function clearTokenFile() {
257
+ const existing = readRawConfig();
242
258
  try {
259
+ if (existing && existing.enabled !== void 0) {
260
+ const { token: _token, token_list: _list, ...rest } = existing;
261
+ writeRawConfig(rest);
262
+ return;
263
+ }
243
264
  rmSync(transcodesConfigFile(), { force: true });
244
265
  } catch {
245
266
  }
246
267
  }
268
+ function isTrackerEnabled() {
269
+ return readRawConfig()?.enabled !== false;
270
+ }
271
+ function setTrackerEnabled(enabled) {
272
+ writeRawConfig({ ...readRawConfig() ?? {}, enabled });
273
+ }
247
274
  function resolveToken() {
248
275
  const envToken = process.env.TRANSCODES_TOKEN?.trim();
249
276
  if (envToken) {
@@ -468,7 +495,7 @@ function dashboardHtml() {
468
495
  gap: 12px;
469
496
  margin-top: 18px;
470
497
  }
471
- button {
498
+ .actions button {
472
499
  flex: 1;
473
500
  padding: 13px 18px;
474
501
  font-size: 15px;
@@ -478,8 +505,8 @@ function dashboardHtml() {
478
505
  cursor: pointer;
479
506
  transition: background 0.15s, opacity 0.15s, transform 0.05s;
480
507
  }
481
- button:active:not(:disabled) { transform: translateY(1px); }
482
- button:disabled { opacity: 0.5; cursor: not-allowed; }
508
+ .actions button:active:not(:disabled) { transform: translateY(1px); }
509
+ .actions button:disabled { opacity: 0.5; cursor: not-allowed; }
483
510
  .btn-primary { background: var(--accent); color: #fff; }
484
511
  .btn-primary:hover:not(:disabled) { background: #4a43d4; }
485
512
  .btn-secondary {
@@ -616,6 +643,60 @@ function dashboardHtml() {
616
643
  color: var(--muted);
617
644
  line-height: 1.5;
618
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; }
619
700
  .toast {
620
701
  margin-top: 14px;
621
702
  padding: 12px 16px;
@@ -654,6 +735,7 @@ function dashboardHtml() {
654
735
  </div>
655
736
  <div class="tabs">
656
737
  <button type="button" class="tab active" data-tab="tokens">Tokens</button>
738
+ <button type="button" class="tab" data-tab="settings">Settings</button>
657
739
  <button type="button" class="tab" data-tab="cli">CLI Commands</button>
658
740
  </div>
659
741
 
@@ -683,6 +765,21 @@ function dashboardHtml() {
683
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>
684
766
  </div>
685
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>
686
783
  </div>
687
784
  <script>
688
785
  const tokenEl = document.getElementById("token");
@@ -699,6 +796,7 @@ function dashboardHtml() {
699
796
  t.classList.toggle("active", t === tab));
700
797
  document.querySelectorAll(".panel").forEach((p) =>
701
798
  p.classList.toggle("active", p.id === "panel-" + name));
799
+ if (name === "settings") loadSettings();
702
800
  });
703
801
  });
704
802
 
@@ -882,7 +980,51 @@ function dashboardHtml() {
882
980
  tokenEl.focus();
883
981
  });
884
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
+
885
1026
  refresh();
1027
+ loadSettings();
886
1028
  </script>
887
1029
  </body>
888
1030
  </html>`;
@@ -903,6 +1045,12 @@ function listen(port) {
903
1045
  const server = createServer(async (req, res) => {
904
1046
  const url = req.url ?? "/";
905
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
+ }
906
1054
  try {
907
1055
  if (method === "GET" && (url === "/" || url === "/index.html")) {
908
1056
  res.writeHead(200, {
@@ -981,6 +1129,27 @@ function listen(port) {
981
1129
  sendJson(res, 200, { ok: true, ...buildStatus() });
982
1130
  return;
983
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
+ }
984
1153
  sendJson(res, 404, { error: "not found" });
985
1154
  } catch (err) {
986
1155
  sendJson(res, 500, {
@@ -1047,13 +1216,18 @@ Usage:
1047
1216
  transcodes Open the dashboard at http://127.0.0.1:3847/ (add --port N or --no-open)
1048
1217
  transcodes set <token> -l <label> Save your Transcodes member token (label required) to ${transcodesConfigFile()}
1049
1218
  transcodes reset Remove all saved tokens
1050
- transcodes status Show where the active token comes from
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
1051
1222
  transcodes tokens List all saved tokens (active one marked with *)
1052
1223
  transcodes help Show this message
1053
1224
 
1054
1225
  The token is read by the ai-action-tracker plugins/hooks with precedence:
1055
1226
  1. TRANSCODES_TOKEN environment variable (overrides everything)
1056
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.
1057
1231
  `;
1058
1232
  function fail(message) {
1059
1233
  process.stderr.write(`transcodes: ${message}
@@ -1116,7 +1290,23 @@ function cmdReset() {
1116
1290
  process.stdout.write(`Removed all saved tokens (${transcodesConfigFile()})
1117
1291
  `);
1118
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
+ }
1119
1305
  function cmdStatus() {
1306
+ process.stdout.write(
1307
+ `Gate: ${isTrackerEnabled() ? "enabled" : "DISABLED"}
1308
+ `
1309
+ );
1120
1310
  const { token, source } = resolveToken();
1121
1311
  if (source === "none" || !token) {
1122
1312
  process.stdout.write(
@@ -1186,6 +1376,12 @@ function main() {
1186
1376
  case "reset":
1187
1377
  cmdReset();
1188
1378
  break;
1379
+ case "enable":
1380
+ cmdSetEnabled(true);
1381
+ break;
1382
+ case "disable":
1383
+ cmdSetEnabled(false);
1384
+ break;
1189
1385
  case "status":
1190
1386
  cmdStatus();
1191
1387
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigstrider/transcodes-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Transcodes CLI — manage the ai-action-tracker member token (login/logout/status).",
5
5
  "type": "module",
6
6
  "bin": {