@plaud-ai/cli 0.2.0 → 0.2.4

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 +204 -56
  2. package/package.json +8 -12
package/dist/index.js CHANGED
@@ -6,17 +6,11 @@ import { Command as Command13 } from "commander";
6
6
 
7
7
  // src/commands/login.ts
8
8
  import { Command } from "commander";
9
- import { createServer } from "http";
9
+ import { createServer as createServer2 } from "net";
10
10
  import open from "open";
11
11
  import chalk2 from "chalk";
12
12
  import ora from "ora";
13
13
 
14
- // src/config.ts
15
- import { readFileSync, existsSync } from "fs";
16
- import { homedir as homedir2 } from "os";
17
- import { join as join2 } from "path";
18
- import { parse } from "yaml";
19
-
20
14
  // ../shared/dist/oauth.js
21
15
  import { randomBytes, createHash } from "crypto";
22
16
 
@@ -248,7 +242,128 @@ var PlaudClient = class {
248
242
  }
249
243
  };
250
244
 
245
+ // ../shared/dist/oauth-callback-server.js
246
+ import { createServer } from "http";
247
+ var SUCCESS_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization successful!</h1><p>You can close this tab.</p></body></html>';
248
+ var NEUTRAL_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Continue authorization in the original window.</h1><p>This page can be closed.</p></body></html>';
249
+ function errorHtml(message) {
250
+ const escaped = message.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
251
+ return '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization failed</h1><pre style="white-space:pre-wrap;">' + escaped + "</pre></body></html>";
252
+ }
253
+ var CORS_HEADERS = {
254
+ "Access-Control-Allow-Origin": "*",
255
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
256
+ "Access-Control-Allow-Headers": "*"
257
+ };
258
+ function runOAuthCallback(opts) {
259
+ const { port, expectedState, exchangeCode, timeoutMs = 12e4, onListening, postSuccessDelayMs = 1500 } = opts;
260
+ return new Promise((resolve) => {
261
+ let settled = false;
262
+ let exchangeStarted = false;
263
+ let exchangeSucceeded = false;
264
+ let timeoutId = null;
265
+ let closeTimeoutId = null;
266
+ const server = createServer((req, res) => {
267
+ if (req.method === "OPTIONS") {
268
+ res.writeHead(204, CORS_HEADERS);
269
+ res.end();
270
+ return;
271
+ }
272
+ const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
273
+ if (reqUrl.pathname !== "/auth/callback") {
274
+ res.writeHead(404, CORS_HEADERS);
275
+ res.end();
276
+ return;
277
+ }
278
+ const params = reqUrl.searchParams;
279
+ const error = params.get("error");
280
+ const state = params.get("state");
281
+ const code = params.get("code");
282
+ if (error) {
283
+ const desc = params.get("error_description") ?? error;
284
+ res.writeHead(400, { "Content-Type": "text/html", ...CORS_HEADERS });
285
+ res.end(errorHtml(`Authorization denied: ${desc}`));
286
+ finalize({ status: "denied", error: new Error(desc) });
287
+ return;
288
+ }
289
+ if (!state || state !== expectedState) {
290
+ respondNeutral(res);
291
+ return;
292
+ }
293
+ if (exchangeSucceeded) {
294
+ respondSuccess(res);
295
+ return;
296
+ }
297
+ if (!code) {
298
+ respondNeutral(res);
299
+ return;
300
+ }
301
+ if (exchangeStarted) {
302
+ respondNeutral(res);
303
+ return;
304
+ }
305
+ exchangeStarted = true;
306
+ exchangeCode(code).then(() => {
307
+ exchangeSucceeded = true;
308
+ respondSuccess(res);
309
+ finalize({ status: "success" });
310
+ }, (err) => {
311
+ const e = err instanceof Error ? err : new Error(String(err));
312
+ res.writeHead(500, { "Content-Type": "text/html", ...CORS_HEADERS });
313
+ res.end(errorHtml(e.message));
314
+ finalize({ status: "exchange-failed", error: e });
315
+ });
316
+ });
317
+ server.on("error", (err) => {
318
+ if (settled)
319
+ return;
320
+ const message = err.code === "EADDRINUSE" ? `port ${port} is in use \u2014 another \`plaud login\` may still be running. Wait a few seconds and retry.` : `callback server error: ${err.message}`;
321
+ finalize({ status: "listen-failed", error: new Error(message) }, true);
322
+ });
323
+ timeoutId = setTimeout(() => {
324
+ finalize({ status: "timeout" }, true);
325
+ }, timeoutMs);
326
+ server.listen(port, () => {
327
+ onListening?.();
328
+ });
329
+ function respondSuccess(res) {
330
+ res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
331
+ res.end(SUCCESS_HTML);
332
+ }
333
+ function respondNeutral(res) {
334
+ res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
335
+ res.end(NEUTRAL_HTML);
336
+ }
337
+ function finalize(result, immediate = false) {
338
+ if (settled)
339
+ return;
340
+ settled = true;
341
+ if (timeoutId) {
342
+ clearTimeout(timeoutId);
343
+ timeoutId = null;
344
+ }
345
+ const close = () => {
346
+ try {
347
+ server.closeAllConnections?.();
348
+ } catch {
349
+ }
350
+ server.close(() => resolve(result));
351
+ };
352
+ if (immediate || result.status !== "success") {
353
+ close();
354
+ } else {
355
+ closeTimeoutId = setTimeout(close, postSuccessDelayMs);
356
+ closeTimeoutId.unref?.();
357
+ }
358
+ }
359
+ });
360
+ }
361
+
251
362
  // src/config.ts
363
+ import { readFileSync, existsSync } from "fs";
364
+ import { homedir as homedir2 } from "os";
365
+ import { join as join2 } from "path";
366
+ import { parse } from "yaml";
252
367
  function loadCliConfig() {
253
368
  const configPath = join2(homedir2(), ".plaud", "cli.yaml");
254
369
  if (!existsSync(configPath)) return {};
@@ -316,66 +431,99 @@ function isTimeoutError(err) {
316
431
  }
317
432
 
318
433
  // src/commands/login.ts
319
- var CORS_HEADERS = {
320
- "Access-Control-Allow-Origin": "*",
321
- "Access-Control-Allow-Methods": "GET, OPTIONS",
322
- "Access-Control-Allow-Headers": "*"
323
- };
434
+ var CALLBACK_PORT = 8199;
435
+ var LOGIN_TIMEOUT_MS = 12e4;
436
+ function probeCallbackPort(port) {
437
+ return new Promise((resolve) => {
438
+ const probe = createServer2();
439
+ probe.once("error", (err) => resolve(err));
440
+ probe.listen(port, "127.0.0.1", () => {
441
+ probe.close(() => resolve(null));
442
+ });
443
+ });
444
+ }
324
445
  var loginCommand = new Command("login").description("Authenticate with Plaud via OAuth").action(async () => {
325
446
  const client2 = getClient();
326
447
  try {
327
448
  const token = await client2.auth.getAccessToken();
328
449
  if (token) {
329
- console.log(chalk2.yellow("Already logged in. Run `plaud logout` first to switch accounts."));
330
- return;
450
+ try {
451
+ await client2.getCurrentUser();
452
+ console.log(chalk2.yellow("Already logged in. Run `plaud logout` first to switch accounts."));
453
+ return;
454
+ } catch (err) {
455
+ if (isAuthError(err)) {
456
+ console.log(chalk2.yellow("Existing credentials are no longer valid. Starting fresh login..."));
457
+ await client2.auth.logout();
458
+ } else if (isNetworkError(err)) {
459
+ printError("UNREACHABLE", "Cannot reach Plaud servers to verify login state. Check your network.", err);
460
+ process.exit(ExitCode.UNREACHABLE);
461
+ } else if (isTimeoutError(err)) {
462
+ printError("TIMEOUT", "Login state check timed out.");
463
+ process.exit(ExitCode.TIMEOUT);
464
+ } else {
465
+ printError("FETCH_FAILED", "Failed to verify login state.", err);
466
+ process.exit(ExitCode.ERROR);
467
+ }
468
+ }
331
469
  }
332
470
  } catch {
333
471
  await client2.auth.logout();
334
472
  }
473
+ const portError = await probeCallbackPort(CALLBACK_PORT);
474
+ if (portError) {
475
+ if (portError.code === "EADDRINUSE") {
476
+ printError(
477
+ "PORT_IN_USE",
478
+ `OAuth callback port ${CALLBACK_PORT} is already in use. Another Plaud process is likely holding it (e.g. \`plaud-mcp http\` or another \`plaud login\`). The OAuth redirect_uri is fixed to localhost:${CALLBACK_PORT}, so login cannot proceed until the port is free.`
479
+ );
480
+ console.error(chalk2.gray(` Find the process: lsof -nP -iTCP:${CALLBACK_PORT} -sTCP:LISTEN`));
481
+ console.error(chalk2.gray(` Then stop it and retry \`plaud login\`.`));
482
+ } else {
483
+ printError("PORT_PROBE_FAILED", `Could not bind callback port ${CALLBACK_PORT}.`, portError);
484
+ }
485
+ process.exit(ExitCode.ERROR);
486
+ }
335
487
  const { url, codeVerifier, state } = client2.auth.createAuthorizationRequest();
336
488
  const spinner = ora("Waiting for browser authentication...").start();
337
- const server = createServer(async (req, res) => {
338
- if (req.method === "OPTIONS") {
339
- res.writeHead(204, CORS_HEADERS);
340
- res.end();
341
- return;
342
- }
343
- const reqUrl = new URL(req.url, `http://localhost:8199`);
344
- if (reqUrl.pathname !== "/auth/callback") {
345
- res.writeHead(404, CORS_HEADERS);
346
- res.end();
347
- return;
348
- }
349
- const code = reqUrl.searchParams.get("code");
350
- if (!code) {
351
- res.writeHead(400, CORS_HEADERS);
352
- res.end("Invalid callback: missing code");
353
- spinner.stop();
354
- printError("AUTH_FAILED", "Authentication failed: missing authorization code");
355
- server.close(() => process.exit(ExitCode.AUTH_FAILED));
356
- return;
357
- }
358
- try {
489
+ const result = await runOAuthCallback({
490
+ port: CALLBACK_PORT,
491
+ expectedState: state,
492
+ timeoutMs: LOGIN_TIMEOUT_MS,
493
+ exchangeCode: async (code) => {
359
494
  await client2.auth.exchangeCode(code, codeVerifier, state);
360
- res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
361
- res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
362
- spinner.succeed("Logged in successfully!");
363
- } catch (err) {
364
- res.writeHead(500, CORS_HEADERS);
365
- res.end("Token exchange failed");
366
- spinner.stop();
367
- printError("AUTH_FAILED", "Authentication failed.", err);
368
- } finally {
369
- server.closeAllConnections();
370
- server.close(() => process.exit(0));
371
- }
372
- });
373
- server.listen(8199, () => {
374
- console.log(chalk2.blue(`
495
+ },
496
+ onListening: () => {
497
+ console.log(chalk2.blue(`
375
498
  Opening browser for authentication...
376
499
  `));
377
- open(url);
500
+ open(url).catch(() => {
501
+ console.log(chalk2.yellow(`Could not open browser. Open this URL manually:
502
+ ${url}`));
503
+ });
504
+ }
378
505
  });
506
+ switch (result.status) {
507
+ case "success":
508
+ spinner.succeed("Logged in successfully!");
509
+ process.exit(ExitCode.OK);
510
+ case "timeout":
511
+ spinner.stop();
512
+ printError("AUTH_FAILED", "Authentication timed out after 2 minutes.");
513
+ process.exit(ExitCode.TIMEOUT);
514
+ case "denied":
515
+ spinner.stop();
516
+ printError("AUTH_FAILED", "Authentication was denied.", result.error);
517
+ process.exit(ExitCode.AUTH_FAILED);
518
+ case "exchange-failed":
519
+ spinner.stop();
520
+ printError("AUTH_FAILED", "Authentication failed.", result.error);
521
+ process.exit(ExitCode.AUTH_FAILED);
522
+ case "listen-failed":
523
+ spinner.stop();
524
+ printError("AUTH_FAILED", "Could not start callback server.", result.error);
525
+ process.exit(ExitCode.ERROR);
526
+ }
379
527
  });
380
528
 
381
529
  // src/commands/logout.ts
@@ -744,7 +892,7 @@ function isNewer(current, latest) {
744
892
  return false;
745
893
  }
746
894
  var updateCommand = new Command9("update").description("Check npm for the latest Plaud CLI and print the upgrade command").action(async () => {
747
- const current = "0.2.0";
895
+ const current = "0.2.4";
748
896
  const spinner = ora8("Checking npm for latest version...").start();
749
897
  const latest = await fetchLatestVersion();
750
898
  spinner.stop();
@@ -798,10 +946,10 @@ async function checkForUpdate(current) {
798
946
  return isNewer(current, latest) ? latest : null;
799
947
  }
800
948
  var versionCommand = new Command10("version").description("Show CLI version information").action(async () => {
801
- const current = "0.2.0";
949
+ const current = "0.2.4";
802
950
  console.log(`plaud ${current}`);
803
- if ("9ac224a") console.log(`commit ${"9ac224a"}`);
804
- if ("2026-04-24T08:28:52.747Z") console.log(`built ${"2026-04-24T08:28:52.747Z"}`);
951
+ if ("4b84277") console.log(`commit ${"4b84277"}`);
952
+ if ("2026-05-07T11:34:37.562Z") console.log(`built ${"2026-05-07T11:34:37.562Z"}`);
805
953
  if (current === "unknown") return;
806
954
  const newer = await checkForUpdate(current);
807
955
  if (newer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "plaud": "dist/index.js"
@@ -8,9 +8,10 @@
8
8
  "files": [
9
9
  "dist"
10
10
  ],
11
- "publishConfig": {
12
- "registry": "https://registry.npmjs.org/",
13
- "access": "public"
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "clean": "rm -rf dist"
14
15
  },
15
16
  "dependencies": {
16
17
  "@inquirer/prompts": "^7.2.0",
@@ -22,13 +23,8 @@
22
23
  "yaml": "^2.8.3"
23
24
  },
24
25
  "devDependencies": {
26
+ "@plaud-ai/shared": "workspace:*",
25
27
  "@types/node": "^25.5.0",
26
- "typescript": "^5.7.0",
27
- "@plaud-ai/shared": "0.1.0"
28
- },
29
- "scripts": {
30
- "build": "tsup",
31
- "dev": "tsup --watch",
32
- "clean": "rm -rf dist"
28
+ "typescript": "^5.7.0"
33
29
  }
34
- }
30
+ }