@bigstrider/transcodes-cli 0.1.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 +40 -0
  2. package/dist/index.js +253 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @bigstrider/transcodes-cli
2
+
3
+ Token manager for the **ai-action-tracker** plugins (Claude Code / Codex / Cursor / Antigravity).
4
+
5
+ The plugins and their hooks authenticate to the Transcodes backend with a member MCP JWT. This CLI is the safe way to store that token: you paste it into your terminal, **never into the agent chat** (which would leak it into the transcript).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # no install needed
11
+ npx @bigstrider/transcodes-cli login <token>
12
+
13
+ # or global
14
+ npm install -g @bigstrider/transcodes-cli
15
+ transcodes login <token>
16
+ ```
17
+
18
+ Works the same on macOS, Linux, and Windows (Node ≥ 20).
19
+
20
+ ## Commands
21
+
22
+ | Command | What it does |
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. |
26
+ | `transcodes status` | Shows the active token source (env vs file) and its expiry. |
27
+ | `transcodes help` | Usage. |
28
+
29
+ ## Token precedence
30
+
31
+ The plugins resolve the token in this order (see `@ai-action-tracker/stepup-core` `resolveToken()`):
32
+
33
+ 1. `TRANSCODES_TOKEN` environment variable — overrides everything (CI / power users)
34
+ 2. `~/.transcodes/config.json` — written by this CLI
35
+ 3. none → the hook fail-safes (blocks danger commands, cannot start step-up)
36
+
37
+ ## Notes
38
+
39
+ - **Windows security**: the `0600` mode is a POSIX concept and is largely ignored on Windows. The file still lives under your user profile (`C:\Users\<you>\.transcodes\`) and is user-scoped by default. A hardware-backed OS keychain is tracked in `docs/prd/0005-token-auth-device-flow.md`.
40
+ - The token never passes through the agent chat — this CLI writes the file directly.
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../stepup-core/dist/jwt.js
4
+ var REQUIRED_AUDIENCE = "transcodes-mcp";
5
+ function isPlainObject(v) {
6
+ return typeof v === "object" && v !== null && !Array.isArray(v);
7
+ }
8
+ function tryDecodeBase64UrlJson(segment) {
9
+ if (!segment)
10
+ return void 0;
11
+ try {
12
+ const json = Buffer.from(segment, "base64url").toString("utf8");
13
+ const parsed = JSON.parse(json);
14
+ return isPlainObject(parsed) ? parsed : void 0;
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
19
+ function readString(rec, key) {
20
+ const v = rec[key];
21
+ if (typeof v !== "string")
22
+ return void 0;
23
+ const t = v.trim();
24
+ return t || void 0;
25
+ }
26
+ function readNumericDate(rec, key) {
27
+ const v = rec[key];
28
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : Number.NaN;
29
+ return Number.isFinite(n) ? Math.floor(n) : void 0;
30
+ }
31
+ function readAudience(rec) {
32
+ const aud = rec["aud"];
33
+ if (typeof aud === "string") {
34
+ const t = aud.trim();
35
+ return t ? [t] : void 0;
36
+ }
37
+ if (Array.isArray(aud)) {
38
+ const list = aud.filter((x) => typeof x === "string").map((x) => x.trim()).filter((x) => x.length > 0);
39
+ return list.length > 0 ? list : void 0;
40
+ }
41
+ return void 0;
42
+ }
43
+ function parseMemberAccessToken(rawToken) {
44
+ if (typeof rawToken !== "string") {
45
+ throw new Error("token must be a string");
46
+ }
47
+ const raw = rawToken.trim();
48
+ if (!raw) {
49
+ throw new Error("token is empty");
50
+ }
51
+ const warnings = [];
52
+ const parts = raw.split(".");
53
+ if (parts.length !== 3 || parts.some((p) => !p)) {
54
+ warnings.push(`token does not look like a JWT (expected 3 non-empty segments, got ${parts.length})`);
55
+ }
56
+ const payloadSegment = parts.length === 3 ? parts[1] : raw;
57
+ const payload = tryDecodeBase64UrlJson(payloadSegment);
58
+ if (!payload) {
59
+ throw new Error("token payload could not be decoded as base64url JSON object");
60
+ }
61
+ const organizationId = readString(payload, "oid");
62
+ const projectId = readString(payload, "pid");
63
+ const memberId = readString(payload, "mid");
64
+ if (!organizationId || !projectId || !memberId) {
65
+ throw new Error("token payload must include oid, pid, and mid claims");
66
+ }
67
+ const aud = readAudience(payload);
68
+ if (!aud) {
69
+ warnings.push("aud claim is missing");
70
+ } else if (!aud.includes(REQUIRED_AUDIENCE)) {
71
+ warnings.push(`aud does not include "${REQUIRED_AUDIENCE}" (got ${JSON.stringify(aud)})`);
72
+ }
73
+ const exp = readNumericDate(payload, "exp");
74
+ if (exp === void 0) {
75
+ throw new Error("token must include exp claim (NumericDate, integer seconds)");
76
+ }
77
+ const nowSec = Math.floor(Date.now() / 1e3);
78
+ if (nowSec >= exp) {
79
+ throw new Error("token has expired");
80
+ }
81
+ return {
82
+ raw,
83
+ claims: {
84
+ organizationId,
85
+ projectId,
86
+ memberId,
87
+ aud,
88
+ exp,
89
+ iss: readString(payload, "iss"),
90
+ jti: readString(payload, "jti"),
91
+ iat: readNumericDate(payload, "iat")
92
+ },
93
+ warnings
94
+ };
95
+ }
96
+
97
+ // ../stepup-core/dist/token-store.js
98
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
99
+ import os from "os";
100
+ import path from "path";
101
+ var CONFIG_DIR_NAME = ".transcodes";
102
+ var CONFIG_FILE_NAME = "config.json";
103
+ function transcodesConfigDir() {
104
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
105
+ }
106
+ function transcodesConfigFile() {
107
+ return path.join(transcodesConfigDir(), CONFIG_FILE_NAME);
108
+ }
109
+ function readTokenFromFile() {
110
+ let raw;
111
+ try {
112
+ raw = readFileSync(transcodesConfigFile(), "utf8");
113
+ } catch {
114
+ return null;
115
+ }
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(raw);
119
+ } catch {
120
+ return null;
121
+ }
122
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
123
+ return null;
124
+ }
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;
130
+ }
131
+ function writeTokenToFile(token) {
132
+ const trimmed = token.trim();
133
+ if (!trimmed) {
134
+ throw new Error("token is empty");
135
+ }
136
+ const dir = transcodesConfigDir();
137
+ mkdirSync(dir, { recursive: true, mode: 448 });
138
+ writeFileSync(transcodesConfigFile(), JSON.stringify({ token: trimmed }), {
139
+ mode: 384
140
+ });
141
+ }
142
+ function clearTokenFile() {
143
+ try {
144
+ rmSync(transcodesConfigFile(), { force: true });
145
+ } catch {
146
+ }
147
+ }
148
+ function resolveToken() {
149
+ const envToken = process.env.TRANSCODES_TOKEN?.trim();
150
+ if (envToken) {
151
+ return { token: envToken, source: "env" };
152
+ }
153
+ const fileToken = readTokenFromFile();
154
+ if (fileToken) {
155
+ return { token: fileToken, source: "file" };
156
+ }
157
+ return { token: null, source: "none" };
158
+ }
159
+
160
+ // src/index.ts
161
+ var USAGE = `transcodes \u2014 ai-action-tracker token manager
162
+
163
+ 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
168
+
169
+ The token is read by the ai-action-tracker plugins/hooks with precedence:
170
+ 1. TRANSCODES_TOKEN environment variable (overrides everything)
171
+ 2. ${transcodesConfigFile()}
172
+ `;
173
+ function fail(message) {
174
+ process.stderr.write(`transcodes: ${message}
175
+ `);
176
+ process.exit(1);
177
+ }
178
+ function expiryLine(token) {
179
+ try {
180
+ const parsed = parseMemberAccessToken(token);
181
+ const exp = new Date(parsed.claims.exp * 1e3).toISOString();
182
+ const warn = parsed.warnings.length > 0 ? ` (warnings: ${parsed.warnings.join("; ")})` : "";
183
+ return `member=${parsed.claims.memberId} project=${parsed.claims.projectId} expires=${exp}${warn}`;
184
+ } catch (err) {
185
+ return `unable to decode token: ${err instanceof Error ? err.message : String(err)}`;
186
+ }
187
+ }
188
+ function cmdLogin(token) {
189
+ if (!token || !token.trim()) {
190
+ fail("missing token. Usage: transcodes login <token>");
191
+ }
192
+ const trimmed = token.trim();
193
+ try {
194
+ parseMemberAccessToken(trimmed);
195
+ } catch (err) {
196
+ fail(
197
+ `token rejected: ${err instanceof Error ? err.message : String(err)}`
198
+ );
199
+ }
200
+ try {
201
+ writeTokenToFile(trimmed);
202
+ } catch (err) {
203
+ fail(
204
+ `could not write token file: ${err instanceof Error ? err.message : String(err)}`
205
+ );
206
+ }
207
+ process.stdout.write(
208
+ `Saved to ${transcodesConfigFile()}
209
+ ${expiryLine(trimmed)}
210
+ `
211
+ );
212
+ }
213
+ function cmdLogout() {
214
+ clearTokenFile();
215
+ process.stdout.write(`Removed ${transcodesConfigFile()}
216
+ `);
217
+ }
218
+ function cmdStatus() {
219
+ const { token, source } = resolveToken();
220
+ if (source === "none" || !token) {
221
+ process.stdout.write(
222
+ "No token configured. Run `transcodes login <token>` to set one.\n"
223
+ );
224
+ return;
225
+ }
226
+ const where = source === "env" ? "TRANSCODES_TOKEN environment variable" : transcodesConfigFile();
227
+ process.stdout.write(`Active token source: ${where}
228
+ ${expiryLine(token)}
229
+ `);
230
+ }
231
+ function main() {
232
+ const [command, ...rest] = process.argv.slice(2);
233
+ switch (command) {
234
+ case "login":
235
+ cmdLogin(rest[0]);
236
+ break;
237
+ case "logout":
238
+ cmdLogout();
239
+ break;
240
+ case "status":
241
+ cmdStatus();
242
+ break;
243
+ case "help":
244
+ case "--help":
245
+ case "-h":
246
+ case void 0:
247
+ process.stdout.write(USAGE);
248
+ break;
249
+ default:
250
+ fail(`unknown command "${command}". Run \`transcodes help\`.`);
251
+ }
252
+ }
253
+ main();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@bigstrider/transcodes-cli",
3
+ "version": "0.1.0",
4
+ "description": "Transcodes CLI — manage the ai-action-tracker member token (login/logout/status).",
5
+ "type": "module",
6
+ "bin": {
7
+ "transcodes": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup && chmod 755 dist/index.js",
15
+ "typecheck": "tsc --noEmit",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "devDependencies": {
19
+ "@ai-action-tracker/stepup-core": "*",
20
+ "@types/node": "^22.0.0",
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.5.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }