third-wheel 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 +39 -0
  2. package/bin/cli.js +176 -0
  3. package/package.json +17 -0
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # third-wheel
2
+
3
+ CLI for exporting Third Wheel recordings and transcripts.
4
+
5
+ ## Login
6
+
7
+ ```sh
8
+ npx third-wheel login --app-url https://your-third-wheel-app.com
9
+ ```
10
+
11
+ The command opens `/cli/login` in your browser. Create a token in the app,
12
+ paste it into the terminal, and the CLI stores it at `~/.third/config.json`.
13
+
14
+ For local development, omit `--app-url` and it defaults to
15
+ `http://localhost:3000`.
16
+
17
+ ## List Recordings
18
+
19
+ ```sh
20
+ npx third-wheel recordings
21
+ ```
22
+
23
+ This prints JSON with session ids, timestamps, speaker maps, transcripts when
24
+ available, scorecard summaries when available, recording metadata, and
25
+ authenticated audio URLs.
26
+
27
+ ## Fetch One Recording
28
+
29
+ ```sh
30
+ npx third-wheel recordings <recording-id>
31
+ ```
32
+
33
+ The response is JSON and can be piped into local coding agents or analysis
34
+ tools.
35
+
36
+ ## Configuration
37
+
38
+ The CLI reads `THIRD_WHEEL_APP_URL`, `APP_URL`, or `--app-url` to choose which
39
+ app instance to call. Saved credentials live in `~/.third/config.json`.
package/bin/cli.js ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import readline from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+
9
+ const CONFIG_DIR = path.join(homedir(), ".third");
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
11
+ const DEFAULT_APP_URL = "http://localhost:3000";
12
+
13
+ function usage() {
14
+ console.log(`Usage:
15
+ third-wheel login [--app-url <url>]
16
+ third-wheel recordings [id] [--app-url <url>]
17
+
18
+ Options:
19
+ --app-url <url> Third Wheel app URL. Defaults to saved config, THIRD_WHEEL_APP_URL, APP_URL, or ${DEFAULT_APP_URL}.
20
+ `);
21
+ }
22
+
23
+ function parseArgs(argv) {
24
+ const args = [...argv];
25
+ const command = args.shift();
26
+ const options = {};
27
+ const positional = [];
28
+
29
+ while (args.length > 0) {
30
+ const arg = args.shift();
31
+ if (arg === "--app-url") {
32
+ options.appUrl = args.shift();
33
+ } else if (arg === "--help" || arg === "-h") {
34
+ options.help = true;
35
+ } else if (arg) {
36
+ positional.push(arg);
37
+ }
38
+ }
39
+
40
+ return { command, positional, options };
41
+ }
42
+
43
+ function normalizeAppUrl(appUrl) {
44
+ return appUrl.replace(/\/+$/, "");
45
+ }
46
+
47
+ async function readConfig() {
48
+ try {
49
+ const contents = await readFile(CONFIG_FILE, "utf8");
50
+ return JSON.parse(contents);
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ async function writeConfig(config) {
57
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
58
+ await writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, {
59
+ mode: 0o600,
60
+ });
61
+ }
62
+
63
+ function resolveAppUrl(options, config = {}) {
64
+ return normalizeAppUrl(
65
+ options.appUrl ||
66
+ config.appUrl ||
67
+ process.env.THIRD_WHEEL_APP_URL ||
68
+ process.env.APP_URL ||
69
+ DEFAULT_APP_URL
70
+ );
71
+ }
72
+
73
+ function openBrowser(url) {
74
+ const platform = process.platform;
75
+ const command =
76
+ platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
77
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
78
+ const child = spawn(command, args, {
79
+ detached: true,
80
+ stdio: "ignore",
81
+ });
82
+ child.unref();
83
+ }
84
+
85
+ async function login(options) {
86
+ const config = await readConfig();
87
+ const appUrl = resolveAppUrl(options, config);
88
+ const loginUrl = `${appUrl}/cli/login`;
89
+
90
+ console.log(`Opening ${loginUrl}`);
91
+ try {
92
+ openBrowser(loginUrl);
93
+ } catch {
94
+ console.log("Could not open a browser automatically.");
95
+ }
96
+
97
+ console.log("Create a CLI token in the browser, then paste it here.");
98
+ const rl = readline.createInterface({ input, output });
99
+ const token = (await rl.question("Token: ")).trim();
100
+ rl.close();
101
+
102
+ if (!token) {
103
+ throw new Error("No token provided.");
104
+ }
105
+
106
+ await writeConfig({ ...config, appUrl, token });
107
+ console.log(`Saved credentials to ${CONFIG_FILE}`);
108
+ }
109
+
110
+ async function apiFetch(config, pathSuffix) {
111
+ if (!config.token) {
112
+ throw new Error("Not logged in. Run `third-wheel login` first.");
113
+ }
114
+
115
+ const appUrl = normalizeAppUrl(config.appUrl || DEFAULT_APP_URL);
116
+ const response = await fetch(`${appUrl}${pathSuffix}`, {
117
+ headers: {
118
+ Authorization: `Bearer ${config.token}`,
119
+ Accept: "application/json",
120
+ },
121
+ });
122
+
123
+ if (!response.ok) {
124
+ let message = `${response.status} ${response.statusText}`;
125
+ try {
126
+ const data = await response.json();
127
+ if (data?.error) message = `${message}: ${data.error}`;
128
+ } catch {
129
+ // Keep the HTTP status when the server did not return JSON.
130
+ }
131
+ throw new Error(message);
132
+ }
133
+
134
+ return response.json();
135
+ }
136
+
137
+ async function recordings(positional, options) {
138
+ const savedConfig = await readConfig();
139
+ const config = {
140
+ ...savedConfig,
141
+ appUrl: resolveAppUrl(options, savedConfig),
142
+ };
143
+ const id = positional[0];
144
+ const data = await apiFetch(
145
+ config,
146
+ id ? `/api/cli/recordings/${encodeURIComponent(id)}` : "/api/cli/recordings"
147
+ );
148
+ console.log(JSON.stringify(data, null, 2));
149
+ }
150
+
151
+ async function main() {
152
+ const { command, positional, options } = parseArgs(process.argv.slice(2));
153
+
154
+ if (options.help || !command || command === "--help" || command === "-h") {
155
+ usage();
156
+ return;
157
+ }
158
+
159
+ if (command === "login") {
160
+ await login(options);
161
+ return;
162
+ }
163
+
164
+ if (command === "recordings") {
165
+ await recordings(positional, options);
166
+ return;
167
+ }
168
+
169
+ usage();
170
+ process.exitCode = 1;
171
+ }
172
+
173
+ main().catch((error) => {
174
+ console.error(error instanceof Error ? error.message : error);
175
+ process.exitCode = 1;
176
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "third-wheel",
3
+ "version": "0.1.0",
4
+ "description": "CLI for exporting Third Wheel recordings and transcripts.",
5
+ "type": "module",
6
+ "bin": {
7
+ "third-wheel": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }