@plaud-ai/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 (2) hide show
  1. package/dist/index.js +542 -0
  2. package/package.json +26 -0
package/dist/index.js ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import "dotenv/config";
5
+ import { Command as Command9 } from "commander";
6
+
7
+ // src/commands/login.ts
8
+ import { Command } from "commander";
9
+ import { createServer } from "http";
10
+ import open from "open";
11
+ import chalk from "chalk";
12
+ import ora from "ora";
13
+
14
+ // ../shared/dist/oauth.js
15
+ import { randomBytes, createHash } from "crypto";
16
+
17
+ // ../shared/dist/token-store.js
18
+ import { readFile, writeFile, mkdir, rm } from "fs/promises";
19
+ import { join } from "path";
20
+ import { homedir } from "os";
21
+ var TokenStore = class {
22
+ configDir;
23
+ tokenPath;
24
+ constructor() {
25
+ this.configDir = join(homedir(), ".plaud");
26
+ this.tokenPath = join(this.configDir, "tokens.json");
27
+ }
28
+ async save(tokenSet) {
29
+ await mkdir(this.configDir, { recursive: true });
30
+ await writeFile(this.tokenPath, JSON.stringify(tokenSet, null, 2), "utf-8");
31
+ }
32
+ async load() {
33
+ try {
34
+ const data = await readFile(this.tokenPath, "utf-8");
35
+ return JSON.parse(data);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ async clear() {
41
+ try {
42
+ await rm(this.tokenPath);
43
+ } catch {
44
+ }
45
+ }
46
+ };
47
+
48
+ // ../shared/dist/oauth.js
49
+ var DEFAULT_AUTHORIZATION_URL = "https://app.plaud.ai/platform/oauth";
50
+ var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
51
+ var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
52
+ function generateCodeVerifier() {
53
+ return randomBytes(32).toString("base64url");
54
+ }
55
+ function generateCodeChallenge(verifier) {
56
+ return createHash("sha256").update(verifier).digest("base64url");
57
+ }
58
+ var OAuth = class {
59
+ config;
60
+ tokenStore;
61
+ authorizationUrl;
62
+ tokenUrl;
63
+ refreshUrl;
64
+ constructor(config) {
65
+ this.config = config;
66
+ this.tokenStore = new TokenStore();
67
+ this.authorizationUrl = config.authorizationUrl ?? DEFAULT_AUTHORIZATION_URL;
68
+ this.tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
69
+ this.refreshUrl = config.refreshUrl ?? DEFAULT_REFRESH_URL;
70
+ }
71
+ createAuthorizationRequest() {
72
+ const codeVerifier = generateCodeVerifier();
73
+ const codeChallenge = generateCodeChallenge(codeVerifier);
74
+ const params = new URLSearchParams({
75
+ client_id: this.config.clientId,
76
+ redirect_uri: this.config.redirectUri,
77
+ response_type: "code",
78
+ code_challenge: codeChallenge,
79
+ code_challenge_method: "S256"
80
+ });
81
+ return {
82
+ url: `${this.authorizationUrl}?${params.toString()}`,
83
+ codeVerifier,
84
+ state: ""
85
+ };
86
+ }
87
+ /**
88
+ * @deprecated Use createAuthorizationRequest() for PKCE flow
89
+ */
90
+ getAuthorizationUrl() {
91
+ return this.createAuthorizationRequest().url;
92
+ }
93
+ async exchangeCode(code, codeVerifier) {
94
+ const basicAuth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
95
+ const body = {
96
+ code,
97
+ redirect_uri: this.config.redirectUri
98
+ };
99
+ if (codeVerifier) {
100
+ body.code_verifier = codeVerifier;
101
+ }
102
+ const res = await fetch(this.tokenUrl, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/x-www-form-urlencoded",
106
+ Accept: "application/json",
107
+ Authorization: `Basic ${basicAuth}`,
108
+ ...this.config.extraHeaders
109
+ },
110
+ body: new URLSearchParams(body)
111
+ });
112
+ if (!res.ok) {
113
+ throw new Error(`Token exchange failed: ${res.status} ${await res.text()}`);
114
+ }
115
+ const data = await res.json();
116
+ const tokenSet = {
117
+ access_token: data.access_token,
118
+ refresh_token: data.refresh_token,
119
+ token_type: data.token_type ?? "Bearer",
120
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
121
+ };
122
+ await this.tokenStore.save(tokenSet);
123
+ return tokenSet;
124
+ }
125
+ async getAccessToken() {
126
+ const tokenSet = await this.tokenStore.load();
127
+ if (!tokenSet)
128
+ return null;
129
+ if (tokenSet.expires_at && Date.now() > tokenSet.expires_at - 6e4) {
130
+ if (tokenSet.refresh_token) {
131
+ const refreshed = await this.refresh(tokenSet.refresh_token);
132
+ return refreshed.access_token;
133
+ }
134
+ return null;
135
+ }
136
+ return tokenSet.access_token;
137
+ }
138
+ async refresh(refreshToken) {
139
+ const res = await fetch(this.refreshUrl, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/x-www-form-urlencoded",
143
+ Accept: "application/json",
144
+ ...this.config.extraHeaders
145
+ },
146
+ body: new URLSearchParams({
147
+ refresh_token: refreshToken
148
+ })
149
+ });
150
+ if (!res.ok) {
151
+ const body = await res.text();
152
+ throw new Error(`Token refresh failed: ${res.status} ${body}`);
153
+ }
154
+ const data = await res.json();
155
+ const tokenSet = {
156
+ access_token: data.access_token,
157
+ refresh_token: data.refresh_token ?? refreshToken,
158
+ token_type: data.token_type ?? "Bearer",
159
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
160
+ };
161
+ await this.tokenStore.save(tokenSet);
162
+ return tokenSet;
163
+ }
164
+ async logout() {
165
+ await this.tokenStore.clear();
166
+ }
167
+ };
168
+
169
+ // ../shared/dist/client.js
170
+ var DEFAULT_API_BASE = "https://platform.plaud.ai/developer/api";
171
+ var PlaudClient = class {
172
+ oauth;
173
+ apiBase;
174
+ extraHeaders;
175
+ constructor(config) {
176
+ this.oauth = new OAuth(config);
177
+ this.apiBase = config.apiBase ?? DEFAULT_API_BASE;
178
+ this.extraHeaders = config.extraHeaders ?? {};
179
+ }
180
+ get auth() {
181
+ return this.oauth;
182
+ }
183
+ async request(path, init) {
184
+ const token = await this.oauth.getAccessToken();
185
+ if (!token) {
186
+ throw new Error("Not authenticated. Please login first.");
187
+ }
188
+ const url = `${this.apiBase}${path}`;
189
+ const method = init?.method ?? "GET";
190
+ const headers = {
191
+ Authorization: `Bearer ${token}`,
192
+ Accept: "application/json",
193
+ ...this.extraHeaders,
194
+ ...init?.headers
195
+ };
196
+ const res = await fetch(url, { ...init, headers });
197
+ if (!res.ok) {
198
+ const body = await res.text();
199
+ if (res.status === 422) {
200
+ try {
201
+ const parsed = JSON.parse(body);
202
+ const messages = parsed.detail.map((d) => `${d.loc.at(-1)}: ${d.msg}`).join("; ");
203
+ throw new Error(messages);
204
+ } catch (e) {
205
+ if (e instanceof SyntaxError)
206
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
207
+ throw e;
208
+ }
209
+ }
210
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
211
+ }
212
+ const json = await res.json();
213
+ return json;
214
+ }
215
+ async getCurrentUser() {
216
+ return this.request("/open/third-party/users/current");
217
+ }
218
+ async revokeCurrentUser() {
219
+ await this.request("/open/third-party/users/current/revoke", {
220
+ method: "POST"
221
+ });
222
+ }
223
+ async listFiles(page = 1, pageSize = 20) {
224
+ return this.request(`/open/third-party/files/?page=${page}&page_size=${pageSize}`);
225
+ }
226
+ async getFile(fileId) {
227
+ return this.request(`/open/third-party/files/${fileId}`);
228
+ }
229
+ };
230
+
231
+ // src/config.ts
232
+ function buildExtraHeaders() {
233
+ const headers = {};
234
+ if (process.env.PLAUD_ENV) headers["x-pld-env"] = process.env.PLAUD_ENV;
235
+ if (process.env.PLAUD_REGION) headers["x-pld-region"] = process.env.PLAUD_REGION;
236
+ return headers;
237
+ }
238
+ var CONFIG = {
239
+ clientId: process.env.PLAUD_CLIENT_ID ?? "client_f9e0b214-c11f-434b-8b95-c4497d1feb81",
240
+ clientSecret: process.env.PLAUD_CLIENT_SECRET ?? "",
241
+ redirectUri: "http://localhost:8199/auth/callback",
242
+ apiBase: process.env.PLAUD_API_BASE,
243
+ authorizationUrl: process.env.PLAUD_AUTH_URL,
244
+ tokenUrl: process.env.PLAUD_TOKEN_URL,
245
+ refreshUrl: process.env.PLAUD_REFRESH_URL,
246
+ extraHeaders: buildExtraHeaders()
247
+ };
248
+ var client = null;
249
+ function getClient() {
250
+ if (!client) {
251
+ client = new PlaudClient(CONFIG);
252
+ }
253
+ return client;
254
+ }
255
+
256
+ // src/commands/login.ts
257
+ var CORS_HEADERS = {
258
+ "Access-Control-Allow-Origin": "*",
259
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
260
+ "Access-Control-Allow-Headers": "*"
261
+ };
262
+ var loginCommand = new Command("login").description("Authenticate with Plaud via OAuth").action(async () => {
263
+ const client2 = getClient();
264
+ const token = await client2.auth.getAccessToken();
265
+ if (token) {
266
+ console.log(chalk.yellow("Already logged in. Run `plaud logout` first to switch accounts."));
267
+ return;
268
+ }
269
+ const { url, codeVerifier } = client2.auth.createAuthorizationRequest();
270
+ const spinner = ora("Waiting for browser authentication...").start();
271
+ const server = createServer(async (req, res) => {
272
+ if (req.method === "OPTIONS") {
273
+ res.writeHead(204, CORS_HEADERS);
274
+ res.end();
275
+ return;
276
+ }
277
+ const reqUrl = new URL(req.url, `http://localhost:8199`);
278
+ if (reqUrl.pathname !== "/auth/callback") {
279
+ res.writeHead(404, CORS_HEADERS);
280
+ res.end();
281
+ return;
282
+ }
283
+ const code = reqUrl.searchParams.get("code");
284
+ if (!code) {
285
+ res.writeHead(400, CORS_HEADERS);
286
+ res.end("Invalid callback: missing code");
287
+ spinner.fail("Authentication failed: missing authorization code");
288
+ server.close();
289
+ return;
290
+ }
291
+ try {
292
+ await client2.auth.exchangeCode(code, codeVerifier);
293
+ res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
294
+ res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
295
+ spinner.succeed("Logged in successfully!");
296
+ } catch (err) {
297
+ res.writeHead(500, CORS_HEADERS);
298
+ res.end("Token exchange failed");
299
+ spinner.fail(`Authentication failed: ${err}`);
300
+ } finally {
301
+ server.closeAllConnections();
302
+ server.close(() => process.exit(0));
303
+ }
304
+ });
305
+ server.listen(8199, () => {
306
+ console.log(chalk.blue(`
307
+ Opening browser for authentication...
308
+ `));
309
+ open(url);
310
+ });
311
+ });
312
+
313
+ // src/commands/logout.ts
314
+ import { Command as Command2 } from "commander";
315
+ import chalk2 from "chalk";
316
+ var logoutCommand = new Command2("logout").description("Log out and revoke authorization").action(async () => {
317
+ const client2 = getClient();
318
+ const token = await client2.auth.getAccessToken();
319
+ if (!token) {
320
+ console.log(chalk2.yellow("Not logged in."));
321
+ return;
322
+ }
323
+ try {
324
+ await client2.revokeCurrentUser();
325
+ } catch {
326
+ }
327
+ await client2.auth.logout();
328
+ console.log(chalk2.green("Logged out and revoked authorization."));
329
+ });
330
+
331
+ // src/commands/me.ts
332
+ import { Command as Command3 } from "commander";
333
+ import chalk3 from "chalk";
334
+ import ora2 from "ora";
335
+ var meCommand = new Command3("me").description("Show current authenticated user info").action(async () => {
336
+ const client2 = getClient();
337
+ const spinner = ora2("Fetching user info...").start();
338
+ try {
339
+ const user = await client2.getCurrentUser();
340
+ spinner.stop();
341
+ console.log(chalk3.bold("\nUser Info:\n"));
342
+ for (const [key, value] of Object.entries(user)) {
343
+ console.log(` ${chalk3.cyan(key)}: ${value}`);
344
+ }
345
+ console.log();
346
+ } catch (err) {
347
+ spinner.fail(`Failed to fetch user info: ${err}`);
348
+ process.exit(1);
349
+ }
350
+ });
351
+
352
+ // src/commands/list-files.ts
353
+ import { Command as Command4 } from "commander";
354
+ import chalk4 from "chalk";
355
+ import ora3 from "ora";
356
+ var listFilesCommand = new Command4("files").description("List your Plaud recordings").option("-p, --page <number>", "Page number", "1").option("-s, --page-size <number>", "Page size", "20").action(async (opts) => {
357
+ const page = parseInt(opts.page);
358
+ const pageSize = parseInt(opts.pageSize);
359
+ if (isNaN(page) || page < 1 || page > 1e3) {
360
+ console.error(chalk4.red("Error: --page must be a number between 1 and 1000"));
361
+ process.exit(1);
362
+ }
363
+ if (isNaN(pageSize) || pageSize < 10 || pageSize > 100) {
364
+ console.error(chalk4.red("Error: --page-size must be a number between 10 and 100"));
365
+ process.exit(1);
366
+ }
367
+ const client2 = getClient();
368
+ const spinner = ora3("Fetching files...").start();
369
+ try {
370
+ const result = await client2.listFiles(page, pageSize);
371
+ spinner.stop();
372
+ console.log(chalk4.bold(`
373
+ Files on this page: ${result.data.length}
374
+ `));
375
+ for (const file of result.data) {
376
+ const date = new Date(file.created_at).toLocaleDateString();
377
+ const duration = file.duration ? `${Math.round(file.duration / 60)}min` : "";
378
+ console.log(` ${chalk4.cyan(file.id)} ${file.name} ${chalk4.gray(date)} ${chalk4.gray(duration)}`);
379
+ }
380
+ console.log(
381
+ chalk4.gray(`
382
+ Page ${result.page}`)
383
+ );
384
+ } catch (err) {
385
+ spinner.fail(`Failed to fetch files: ${err}`);
386
+ process.exit(1);
387
+ }
388
+ });
389
+
390
+ // src/commands/get-file.ts
391
+ import { Command as Command5 } from "commander";
392
+ import chalk5 from "chalk";
393
+ import ora4 from "ora";
394
+ function formatDuration(ms) {
395
+ const totalSeconds = Math.floor(ms / 1e3);
396
+ const minutes = Math.floor(totalSeconds / 60);
397
+ const seconds = totalSeconds % 60;
398
+ return minutes > 0 ? `${minutes}min ${seconds}sec` : `${seconds}sec`;
399
+ }
400
+ var getFileCommand = new Command5("file").description("Get details of a specific Plaud recording").argument("<file_id>", "The file ID to retrieve").action(async (fileId) => {
401
+ const client2 = getClient();
402
+ const spinner = ora4("Fetching file...").start();
403
+ try {
404
+ const file = await client2.getFile(fileId);
405
+ spinner.stop();
406
+ const sourceList = file.source_list ?? [];
407
+ const noteList = file.note_list ?? [];
408
+ const hasTranscript = sourceList.some((s) => s.data_type === "transaction");
409
+ const hasSummary = noteList.some((n) => n.data_type === "auto_sum_note");
410
+ console.log(chalk5.bold("\nFile Details:\n"));
411
+ console.log(` ${chalk5.cyan("id")}: ${file.id}`);
412
+ console.log(` ${chalk5.cyan("name")}: ${file.name}`);
413
+ console.log(` ${chalk5.cyan("created_at")}: ${file.created_at}`);
414
+ console.log(` ${chalk5.cyan("start_at")}: ${file.start_at ?? "-"}`);
415
+ console.log(` ${chalk5.cyan("duration")}: ${file.duration ? formatDuration(file.duration) : "-"}`);
416
+ console.log(` ${chalk5.cyan("serial_number")}: ${file.serial_number ?? "-"}`);
417
+ console.log(` ${chalk5.cyan("audio")}: ${file.presigned_url ? chalk5.green("available") : chalk5.gray("unavailable")}`);
418
+ console.log(` ${chalk5.cyan("transcript")}: ${hasTranscript ? chalk5.green("available") : chalk5.gray("unavailable")}`);
419
+ console.log(` ${chalk5.cyan("summary")}: ${hasSummary ? chalk5.green("available") : chalk5.gray("unavailable")}`);
420
+ console.log();
421
+ } catch (err) {
422
+ spinner.fail(`Failed to fetch file: ${err}`);
423
+ process.exit(1);
424
+ }
425
+ });
426
+
427
+ // src/commands/audio.ts
428
+ import { Command as Command6 } from "commander";
429
+ import chalk6 from "chalk";
430
+ import ora5 from "ora";
431
+ var audioCommand = new Command6("audio").description("Get the audio download URL for a Plaud recording").argument("<file_id>", "The file ID to retrieve audio for").action(async (fileId) => {
432
+ const client2 = getClient();
433
+ const spinner = ora5("Fetching audio URL...").start();
434
+ try {
435
+ const file = await client2.getFile(fileId);
436
+ spinner.stop();
437
+ if (!file.presigned_url) {
438
+ console.log(chalk6.yellow("Audio not available for this recording."));
439
+ return;
440
+ }
441
+ console.log(chalk6.bold("\nAudio Download URL:\n"));
442
+ console.log(file.presigned_url);
443
+ console.log(chalk6.gray("\nNote: This URL expires in 24 hours."));
444
+ console.log();
445
+ } catch (err) {
446
+ spinner.fail(`Failed to fetch audio URL: ${err}`);
447
+ process.exit(1);
448
+ }
449
+ });
450
+
451
+ // src/commands/transcript.ts
452
+ import { Command as Command7 } from "commander";
453
+ import chalk7 from "chalk";
454
+ import ora6 from "ora";
455
+ import { writeFile as writeFile2 } from "fs/promises";
456
+ function formatTime(ms) {
457
+ const totalSeconds = Math.floor(ms / 1e3);
458
+ const minutes = Math.floor(totalSeconds / 60);
459
+ const seconds = totalSeconds % 60;
460
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
461
+ }
462
+ var transcriptCommand = new Command7("transcript").description("Get the transcript for a Plaud recording").argument("<file_id>", "The file ID to retrieve transcript for").option("-o, --output <file>", "Save transcript to a file").action(async (fileId, opts) => {
463
+ const client2 = getClient();
464
+ const spinner = ora6("Fetching transcript...").start();
465
+ try {
466
+ const file = await client2.getFile(fileId);
467
+ spinner.stop();
468
+ const sourceList = file.source_list ?? [];
469
+ const source = sourceList.find((s) => s.data_type === "transaction");
470
+ if (!source) {
471
+ console.log(chalk7.yellow("Transcript not available for this recording."));
472
+ return;
473
+ }
474
+ const segments = JSON.parse(source.data_content);
475
+ const lines = segments.map((seg) => {
476
+ const time = `[${formatTime(seg.start_time)} - ${formatTime(seg.end_time)}]`;
477
+ const speaker = seg.speaker ? `${seg.speaker}: ` : "";
478
+ return `${time} ${speaker}${seg.content}`;
479
+ });
480
+ const output = lines.join("\n");
481
+ if (opts.output) {
482
+ await writeFile2(opts.output, output, "utf-8");
483
+ console.log(chalk7.green(`Transcript saved to ${opts.output}`));
484
+ } else {
485
+ console.log(chalk7.bold(`
486
+ Transcript: ${file.name}
487
+ `));
488
+ console.log(output);
489
+ console.log();
490
+ }
491
+ } catch (err) {
492
+ spinner.fail(`Failed to fetch transcript: ${err}`);
493
+ process.exit(1);
494
+ }
495
+ });
496
+
497
+ // src/commands/summary.ts
498
+ import { Command as Command8 } from "commander";
499
+ import chalk8 from "chalk";
500
+ import ora7 from "ora";
501
+ import { writeFile as writeFile3 } from "fs/promises";
502
+ var summaryCommand = new Command8("summary").description("Get the AI summary for a Plaud recording").argument("<file_id>", "The file ID to retrieve summary for").option("-o, --output <file>", "Save summary to a file").action(async (fileId, opts) => {
503
+ const client2 = getClient();
504
+ const spinner = ora7("Fetching summary...").start();
505
+ try {
506
+ const file = await client2.getFile(fileId);
507
+ spinner.stop();
508
+ const noteList = file.note_list ?? [];
509
+ const note = noteList.find((n) => n.data_type === "auto_sum_note");
510
+ if (!note || !note.data_content) {
511
+ console.log(chalk8.yellow("Summary not available for this recording."));
512
+ return;
513
+ }
514
+ const content = note.data_content;
515
+ if (opts.output) {
516
+ await writeFile3(opts.output, content, "utf-8");
517
+ console.log(chalk8.green(`Summary saved to ${opts.output}`));
518
+ } else {
519
+ console.log(chalk8.bold(`
520
+ Summary: ${file.name}
521
+ `));
522
+ console.log(content);
523
+ console.log();
524
+ }
525
+ } catch (err) {
526
+ spinner.fail(`Failed to fetch summary: ${err}`);
527
+ process.exit(1);
528
+ }
529
+ });
530
+
531
+ // src/index.ts
532
+ var program = new Command9();
533
+ program.name("plaud").description("Plaud CLI - manage your Plaud recordings").version("0.1.0");
534
+ program.addCommand(loginCommand);
535
+ program.addCommand(logoutCommand);
536
+ program.addCommand(meCommand);
537
+ program.addCommand(listFilesCommand);
538
+ program.addCommand(getFileCommand);
539
+ program.addCommand(audioCommand);
540
+ program.addCommand(transcriptCommand);
541
+ program.addCommand(summaryCommand);
542
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@plaud-ai/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "plaud": "dist/index.js"
7
+ },
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsup --watch",
12
+ "clean": "rm -rf dist"
13
+ },
14
+ "dependencies": {
15
+ "chalk": "^5.4.0",
16
+ "commander": "^13.0.0",
17
+ "dotenv": "^17.3.1",
18
+ "open": "^10.2.0",
19
+ "ora": "^8.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@plaud-ai/shared": "workspace:*",
23
+ "@types/node": "^25.5.0",
24
+ "typescript": "^5.7.0"
25
+ }
26
+ }