@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.
- package/dist/index.js +217 -21
- 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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
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;
|