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.
- package/README.md +39 -0
- package/bin/cli.js +176 -0
- 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
|
+
}
|