granola-cli 0.1.0 → 0.1.1

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/dist/main.js CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { spawn as spawn2 } from "child_process";
5
- import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
6
- import { delimiter, join as join2 } from "path";
4
+ import { readFileSync as readFileSync2 } from "fs";
7
5
  import { Command as Command20 } from "commander";
8
6
 
9
7
  // src/commands/alias.ts
@@ -225,87 +223,173 @@ import { Command as Command2 } from "commander";
225
223
 
226
224
  // src/lib/auth.ts
227
225
  import { readFile } from "fs/promises";
228
- import { homedir, platform } from "os";
229
- import { join } from "path";
226
+ import { homedir as homedir2, platform } from "os";
227
+ import { join as join2 } from "path";
230
228
  import { deletePassword, getPassword, setPassword } from "cross-keychain";
231
- var debug4 = createGranolaDebug("lib:auth");
229
+
230
+ // src/lib/lock.ts
231
+ import { mkdir, open, stat, unlink } from "fs/promises";
232
+ import { homedir, tmpdir } from "os";
233
+ import { dirname, join } from "path";
234
+ var debug4 = createGranolaDebug("lib:lock");
235
+ var LOCK_FILE_NAME = "granola-token-refresh.lock";
236
+ var LOCK_TIMEOUT_MS = 3e4;
237
+ var LOCK_RETRY_INTERVAL_MS = 100;
238
+ var LOCK_STALE_MS = 6e4;
239
+ function getLockFilePath() {
240
+ const tempDir = process.platform === "darwin" ? join(homedir(), "Library", "Caches", "granola") : tmpdir();
241
+ return join(tempDir, LOCK_FILE_NAME);
242
+ }
243
+ async function ensureLockDirectory() {
244
+ const lockPath = getLockFilePath();
245
+ const dir = dirname(lockPath);
246
+ await mkdir(dir, { recursive: true });
247
+ }
248
+ async function isLockStale(lockPath) {
249
+ try {
250
+ const stats = await stat(lockPath);
251
+ const age = Date.now() - stats.mtimeMs;
252
+ return age > LOCK_STALE_MS;
253
+ } catch {
254
+ return true;
255
+ }
256
+ }
257
+ async function acquireLock() {
258
+ const lockPath = getLockFilePath();
259
+ const startTime = Date.now();
260
+ await ensureLockDirectory();
261
+ debug4("attempting to acquire lock at %s", lockPath);
262
+ while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
263
+ try {
264
+ const handle = await open(lockPath, "wx");
265
+ debug4("lock acquired");
266
+ return { handle };
267
+ } catch (error) {
268
+ const err = error;
269
+ if (err.code === "EEXIST") {
270
+ if (await isLockStale(lockPath)) {
271
+ debug4("removing stale lock");
272
+ try {
273
+ await unlink(lockPath);
274
+ } catch {
275
+ }
276
+ continue;
277
+ }
278
+ debug4("lock held by another process, waiting...");
279
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS));
280
+ } else {
281
+ debug4("lock acquisition failed: %O", error);
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+ debug4("lock acquisition timed out");
287
+ return null;
288
+ }
289
+ async function releaseLock(lockHandle) {
290
+ const lockPath = getLockFilePath();
291
+ debug4("releasing lock");
292
+ try {
293
+ await lockHandle.handle.close();
294
+ await unlink(lockPath);
295
+ debug4("lock released");
296
+ } catch (error) {
297
+ debug4("error releasing lock: %O", error);
298
+ }
299
+ }
300
+ async function withLock(operation) {
301
+ const handle = await acquireLock();
302
+ if (handle === null) {
303
+ throw new Error("Failed to acquire token refresh lock");
304
+ }
305
+ try {
306
+ return await operation();
307
+ } finally {
308
+ await releaseLock(handle);
309
+ }
310
+ }
311
+
312
+ // src/lib/auth.ts
313
+ var debug5 = createGranolaDebug("lib:auth");
232
314
  var SERVICE_NAME = "com.granola.cli";
233
315
  var ACCOUNT_NAME = "credentials";
234
316
  var DEFAULT_CLIENT_ID = "client_GranolaMac";
235
317
  async function getCredentials() {
236
- debug4("loading credentials from keychain");
318
+ debug5("loading credentials from keychain");
237
319
  try {
238
320
  const stored = await getPassword(SERVICE_NAME, ACCOUNT_NAME);
239
321
  if (!stored) {
240
- debug4("no credentials found in keychain");
322
+ debug5("no credentials found in keychain");
241
323
  return null;
242
324
  }
243
325
  const parsed = JSON.parse(stored);
244
- debug4("credentials loaded, hasAccessToken: %s", Boolean(parsed.accessToken));
326
+ debug5("credentials loaded, hasAccessToken: %s", Boolean(parsed.accessToken));
245
327
  return {
246
328
  refreshToken: parsed.refreshToken,
247
329
  accessToken: parsed.accessToken || "",
248
330
  clientId: parsed.clientId
249
331
  };
250
332
  } catch (error) {
251
- debug4("failed to get credentials: %O", error);
333
+ debug5("failed to get credentials: %O", error);
252
334
  return null;
253
335
  }
254
336
  }
255
337
  async function saveCredentials(creds) {
256
- debug4("saving credentials to keychain");
338
+ debug5("saving credentials to keychain");
257
339
  await setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(creds));
258
- debug4("credentials saved");
340
+ debug5("credentials saved");
259
341
  }
260
342
  async function deleteCredentials() {
261
- debug4("deleting credentials from keychain");
343
+ debug5("deleting credentials from keychain");
262
344
  await deletePassword(SERVICE_NAME, ACCOUNT_NAME);
263
- debug4("credentials deleted");
345
+ debug5("credentials deleted");
264
346
  }
265
347
  var WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
266
348
  async function refreshAccessToken() {
267
- debug4("attempting token refresh");
268
- const creds = await getCredentials();
269
- if (!creds?.refreshToken || !creds?.clientId) {
270
- debug4("cannot refresh: missing refreshToken or clientId");
271
- return null;
272
- }
349
+ debug5("attempting token refresh");
273
350
  try {
274
- const response = await fetch(WORKOS_AUTH_URL, {
275
- method: "POST",
276
- headers: { "Content-Type": "application/json" },
277
- body: JSON.stringify({
278
- client_id: creds.clientId,
279
- grant_type: "refresh_token",
280
- refresh_token: creds.refreshToken
281
- })
351
+ return await withLock(async () => {
352
+ const creds = await getCredentials();
353
+ if (!creds?.refreshToken || !creds?.clientId) {
354
+ debug5("cannot refresh: missing refreshToken or clientId");
355
+ return null;
356
+ }
357
+ const response = await fetch(WORKOS_AUTH_URL, {
358
+ method: "POST",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({
361
+ client_id: creds.clientId,
362
+ grant_type: "refresh_token",
363
+ refresh_token: creds.refreshToken
364
+ })
365
+ });
366
+ if (!response.ok) {
367
+ debug5("token refresh failed: %d %s", response.status, response.statusText);
368
+ return null;
369
+ }
370
+ const data = await response.json();
371
+ const newCreds = {
372
+ refreshToken: data.refresh_token,
373
+ accessToken: data.access_token,
374
+ clientId: creds.clientId
375
+ };
376
+ await saveCredentials(newCreds);
377
+ debug5("token refresh successful, new credentials saved");
378
+ return newCreds;
282
379
  });
283
- if (!response.ok) {
284
- debug4("token refresh failed: %d %s", response.status, response.statusText);
285
- return null;
286
- }
287
- const data = await response.json();
288
- const newCreds = {
289
- refreshToken: data.refresh_token,
290
- accessToken: data.access_token,
291
- clientId: creds.clientId
292
- };
293
- await saveCredentials(newCreds);
294
- debug4("token refresh successful, new credentials saved");
295
- return newCreds;
296
380
  } catch (error) {
297
- debug4("token refresh error: %O", error);
381
+ debug5("token refresh error: %O", error);
298
382
  return null;
299
383
  }
300
384
  }
301
385
  function parseSupabaseJson(json) {
302
- debug4("parsing supabase.json");
386
+ debug5("parsing supabase.json");
303
387
  try {
304
388
  const parsed = JSON.parse(json);
305
389
  if (parsed.workos_tokens && typeof parsed.workos_tokens === "string") {
306
390
  const workosTokens = JSON.parse(parsed.workos_tokens);
307
391
  if (workosTokens.access_token) {
308
- debug4("found WorkOS tokens");
392
+ debug5("found WorkOS tokens");
309
393
  return {
310
394
  refreshToken: workosTokens.refresh_token || "",
311
395
  accessToken: workosTokens.access_token,
@@ -316,7 +400,7 @@ function parseSupabaseJson(json) {
316
400
  if (parsed.cognito_tokens && typeof parsed.cognito_tokens === "string") {
317
401
  const cognitoTokens = JSON.parse(parsed.cognito_tokens);
318
402
  if (!cognitoTokens.refresh_token) return null;
319
- debug4("found Cognito tokens");
403
+ debug5("found Cognito tokens");
320
404
  return {
321
405
  refreshToken: cognitoTokens.refresh_token,
322
406
  accessToken: cognitoTokens.access_token || "",
@@ -324,68 +408,68 @@ function parseSupabaseJson(json) {
324
408
  };
325
409
  }
326
410
  if (!parsed.refresh_token) return null;
327
- debug4("found legacy token format");
411
+ debug5("found legacy token format");
328
412
  return {
329
413
  refreshToken: parsed.refresh_token,
330
414
  accessToken: parsed.access_token || "",
331
415
  clientId: parsed.client_id || DEFAULT_CLIENT_ID
332
416
  };
333
417
  } catch (error) {
334
- debug4("failed to parse supabase.json: %O", error);
418
+ debug5("failed to parse supabase.json: %O", error);
335
419
  return null;
336
420
  }
337
421
  }
338
422
  function getDefaultSupabasePath() {
339
- const home = homedir();
423
+ const home = homedir2();
340
424
  const os2 = platform();
341
425
  let path;
342
426
  switch (os2) {
343
427
  case "darwin":
344
- path = join(home, "Library", "Application Support", "Granola", "supabase.json");
428
+ path = join2(home, "Library", "Application Support", "Granola", "supabase.json");
345
429
  break;
346
430
  case "win32":
347
- path = join(
348
- process.env.APPDATA || join(home, "AppData", "Roaming"),
431
+ path = join2(
432
+ process.env.APPDATA || join2(home, "AppData", "Roaming"),
349
433
  "Granola",
350
434
  "supabase.json"
351
435
  );
352
436
  break;
353
437
  default:
354
- path = join(home, ".config", "granola", "supabase.json");
438
+ path = join2(home, ".config", "granola", "supabase.json");
355
439
  }
356
- debug4("platform: %s, supabase path: %s", os2, path);
440
+ debug5("platform: %s, supabase path: %s", os2, path);
357
441
  return path;
358
442
  }
359
443
  async function loadCredentialsFromFile() {
360
444
  const path = getDefaultSupabasePath();
361
- debug4("loading credentials from file: %s", path);
445
+ debug5("loading credentials from file: %s", path);
362
446
  try {
363
447
  const content = await readFile(path, "utf-8");
364
- debug4("file read successful, parsing content");
448
+ debug5("file read successful, parsing content");
365
449
  return parseSupabaseJson(content);
366
450
  } catch (error) {
367
- debug4("failed to load credentials from file: %O", error);
451
+ debug5("failed to load credentials from file: %O", error);
368
452
  return null;
369
453
  }
370
454
  }
371
455
 
372
456
  // src/commands/auth/login.ts
373
- var debug5 = createGranolaDebug("cmd:auth:login");
457
+ var debug6 = createGranolaDebug("cmd:auth:login");
374
458
  function createLoginCommand() {
375
459
  return new Command2("login").description("Import credentials from Granola desktop app").action(async () => {
376
- debug5("login command invoked");
460
+ debug6("login command invoked");
377
461
  const creds = await loadCredentialsFromFile();
378
462
  if (!creds) {
379
463
  const path = getDefaultSupabasePath();
380
- debug5("login failed: could not load credentials from %s", path);
464
+ debug6("login failed: could not load credentials from %s", path);
381
465
  console.error(chalk3.red("Error:"), "Could not load credentials.");
382
466
  console.error(`Expected file at: ${chalk3.dim(path)}`);
383
467
  console.error("\nMake sure the Granola desktop app is installed and you are logged in.");
384
468
  process.exit(1);
385
469
  }
386
- debug5("credentials loaded, saving to keychain");
470
+ debug6("credentials loaded, saving to keychain");
387
471
  await saveCredentials(creds);
388
- debug5("login successful");
472
+ debug6("login successful");
389
473
  console.log(chalk3.green("Credentials imported successfully"));
390
474
  });
391
475
  }
@@ -394,16 +478,16 @@ var loginCommand = createLoginCommand();
394
478
  // src/commands/auth/logout.ts
395
479
  import chalk4 from "chalk";
396
480
  import { Command as Command3 } from "commander";
397
- var debug6 = createGranolaDebug("cmd:auth:logout");
481
+ var debug7 = createGranolaDebug("cmd:auth:logout");
398
482
  function createLogoutCommand() {
399
483
  return new Command3("logout").description("Logout from Granola").action(async () => {
400
- debug6("logout command invoked");
484
+ debug7("logout command invoked");
401
485
  try {
402
486
  await deleteCredentials();
403
- debug6("logout successful");
487
+ debug7("logout successful");
404
488
  console.log(chalk4.green("Logged out successfully"));
405
489
  } catch (error) {
406
- debug6("logout failed: %O", error);
490
+ debug7("logout failed: %O", error);
407
491
  console.error(chalk4.red("Error:"), "Failed to logout.");
408
492
  if (error instanceof Error) {
409
493
  console.error(chalk4.dim(error.message));
@@ -417,12 +501,12 @@ var logoutCommand = createLogoutCommand();
417
501
  // src/commands/auth/status.ts
418
502
  import chalk5 from "chalk";
419
503
  import { Command as Command4 } from "commander";
420
- var debug7 = createGranolaDebug("cmd:auth:status");
504
+ var debug8 = createGranolaDebug("cmd:auth:status");
421
505
  function createStatusCommand() {
422
506
  return new Command4("status").description("Check authentication status").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
423
- debug7("status command invoked");
507
+ debug8("status command invoked");
424
508
  const creds = await getCredentials();
425
- debug7("authenticated: %s", !!creds);
509
+ debug8("authenticated: %s", !!creds);
426
510
  const format = opts.output || null;
427
511
  if (format) {
428
512
  if (!["json", "yaml", "toon"].includes(format)) {
@@ -448,7 +532,7 @@ var authCommand = new Command5("auth").description("Manage authentication").addC
448
532
  // src/commands/config.ts
449
533
  import chalk6 from "chalk";
450
534
  import { Command as Command6 } from "commander";
451
- var debug8 = createGranolaDebug("cmd:config");
535
+ var debug9 = createGranolaDebug("cmd:config");
452
536
  var CONFIG_VALUE_PARSERS = {
453
537
  default_workspace: (value) => value,
454
538
  pager: (value) => value,
@@ -479,7 +563,7 @@ function isConfigKey(key) {
479
563
  function createConfigCommand() {
480
564
  const cmd = new Command6("config").description("Manage CLI configuration");
481
565
  cmd.command("list").description("View current config").option("-o, --output <format>", "Output format (json, yaml, toon)").action((opts) => {
482
- debug8("config list command invoked");
566
+ debug9("config list command invoked");
483
567
  const config2 = getConfig();
484
568
  const format = opts.output || null;
485
569
  if (format) {
@@ -506,7 +590,7 @@ function createConfigCommand() {
506
590
  }
507
591
  });
508
592
  cmd.command("get <key>").description("Get a config value").option("-o, --output <format>", "Output format (json, yaml, toon)").action((key, opts) => {
509
- debug8("config get command invoked with key: %s", key);
593
+ debug9("config get command invoked with key: %s", key);
510
594
  const value = getConfigValue(key);
511
595
  const format = opts.output || null;
512
596
  if (format) {
@@ -524,7 +608,7 @@ function createConfigCommand() {
524
608
  }
525
609
  });
526
610
  cmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
527
- debug8("config set command invoked: %s = %s", key, value);
611
+ debug9("config set command invoked: %s = %s", key, value);
528
612
  if (!isConfigKey(key)) {
529
613
  console.error(
530
614
  chalk6.red(
@@ -548,7 +632,7 @@ function createConfigCommand() {
548
632
  console.log(chalk6.green(`Set ${key} = ${value}`));
549
633
  });
550
634
  cmd.command("reset").description("Reset to defaults").action(() => {
551
- debug8("config reset command invoked");
635
+ debug9("config reset command invoked");
552
636
  resetConfig();
553
637
  console.log(chalk6.green("Configuration reset"));
554
638
  });
@@ -721,26 +805,26 @@ function createHttpClient(token) {
721
805
  }
722
806
 
723
807
  // src/services/client.ts
724
- var debug9 = createGranolaDebug("service:client");
808
+ var debug10 = createGranolaDebug("service:client");
725
809
  var client = null;
726
810
  async function getClient() {
727
- debug9("getClient called, cached: %s", client ? "yes" : "no");
811
+ debug10("getClient called, cached: %s", client ? "yes" : "no");
728
812
  if (client) return client;
729
- debug9("fetching credentials");
813
+ debug10("fetching credentials");
730
814
  const creds = await getCredentials();
731
815
  if (!creds) {
732
- debug9("no credentials found, exiting");
816
+ debug10("no credentials found, exiting");
733
817
  console.error(chalk7.red("Error:"), "Not authenticated.");
734
818
  console.error(`Run ${chalk7.cyan("granola auth login")} to authenticate.`);
735
819
  process.exit(2);
736
820
  }
737
- debug9("creating API client, token: %s", maskToken(creds.accessToken));
821
+ debug10("creating API client, token: %s", maskToken(creds.accessToken));
738
822
  const httpClient = createHttpClient(creds.accessToken);
739
823
  client = createApiClient(httpClient);
740
824
  return client;
741
825
  }
742
826
  function resetClient() {
743
- debug9("client reset");
827
+ debug10("client reset");
744
828
  client = null;
745
829
  }
746
830
  function isUnauthorizedError(error) {
@@ -755,14 +839,14 @@ async function withTokenRefresh(operation) {
755
839
  return await operation();
756
840
  } catch (error) {
757
841
  if (isUnauthorizedError(error)) {
758
- debug9("401 detected, attempting token refresh");
842
+ debug10("401 detected, attempting token refresh");
759
843
  const newCreds = await refreshAccessToken();
760
844
  if (!newCreds) {
761
- debug9("token refresh failed, re-throwing original error");
845
+ debug10("token refresh failed, re-throwing original error");
762
846
  throw error;
763
847
  }
764
848
  resetClient();
765
- debug9("retrying operation with refreshed token");
849
+ debug10("retrying operation with refreshed token");
766
850
  return operation();
767
851
  }
768
852
  throw error;
@@ -770,7 +854,7 @@ async function withTokenRefresh(operation) {
770
854
  }
771
855
 
772
856
  // src/services/folders.ts
773
- var debug10 = createGranolaDebug("service:folders");
857
+ var debug11 = createGranolaDebug("service:folders");
774
858
  function normalizeFolder(folder) {
775
859
  const documentIdsFromDocs = Array.isArray(folder.documents) ? folder.documents.map((doc) => doc?.id).filter((id) => Boolean(id)) : void 0;
776
860
  const documentIds = Array.isArray(folder.document_ids) && folder.document_ids.length > 0 ? folder.document_ids : documentIdsFromDocs;
@@ -790,10 +874,10 @@ async function list(opts = {}) {
790
874
  const client2 = await getClient();
791
875
  const documentLists = await client2.getDocumentLists();
792
876
  const folders = documentLists.map(normalizeFolder);
793
- debug10("list fetched %d folders", folders.length);
877
+ debug11("list fetched %d folders", folders.length);
794
878
  if (opts.workspace) {
795
879
  const filtered = folders.filter((folder) => folder.workspace_id === opts.workspace);
796
- debug10("filtered to %d folders for workspace %s", filtered.length, opts.workspace);
880
+ debug11("filtered to %d folders for workspace %s", filtered.length, opts.workspace);
797
881
  return filtered;
798
882
  }
799
883
  return folders;
@@ -801,30 +885,30 @@ async function list(opts = {}) {
801
885
  }
802
886
  async function get(id) {
803
887
  return withTokenRefresh(async () => {
804
- debug10("get called for folder: %s", id);
888
+ debug11("get called for folder: %s", id);
805
889
  const client2 = await getClient();
806
890
  const documentLists = await client2.getDocumentLists();
807
891
  const folder = documentLists.find((f) => f.id === id);
808
892
  if (!folder) {
809
- debug10("folder %s not found", id);
893
+ debug11("folder %s not found", id);
810
894
  return null;
811
895
  }
812
- debug10("folder %s found", id);
896
+ debug11("folder %s found", id);
813
897
  return normalizeFolder(folder);
814
898
  });
815
899
  }
816
900
 
817
901
  // src/commands/folder/list.ts
818
- var debug11 = createGranolaDebug("cmd:folder:list");
902
+ var debug12 = createGranolaDebug("cmd:folder:list");
819
903
  function createListCommand() {
820
904
  return new Command7("list").description("List folders").option("-w, --workspace <id>", "Filter by workspace").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
821
- debug11("folder list command invoked with opts: %O", opts);
905
+ debug12("folder list command invoked with opts: %O", opts);
822
906
  let data;
823
907
  try {
824
908
  data = await list({
825
909
  workspace: opts.workspace
826
910
  });
827
- debug11("fetched %d folders", data.length);
911
+ debug12("fetched %d folders", data.length);
828
912
  } catch (error) {
829
913
  console.error(chalk8.red("Error:"), "Failed to list folders.");
830
914
  if (error instanceof Error) {
@@ -867,10 +951,10 @@ var listCommand = createListCommand();
867
951
  // src/commands/folder/view.ts
868
952
  import chalk9 from "chalk";
869
953
  import { Command as Command8 } from "commander";
870
- var debug12 = createGranolaDebug("cmd:folder:view");
954
+ var debug13 = createGranolaDebug("cmd:folder:view");
871
955
  function createViewCommand() {
872
956
  return new Command8("view").description("View folder details").argument("<id>", "Folder ID").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (id, opts) => {
873
- debug12("folder view command invoked with id: %s", id);
957
+ debug13("folder view command invoked with id: %s", id);
874
958
  let folder;
875
959
  try {
876
960
  folder = await get(id);
@@ -916,42 +1000,42 @@ import { Command as Command10 } from "commander";
916
1000
 
917
1001
  // src/lib/pager.ts
918
1002
  import { spawn } from "child_process";
919
- var debug13 = createGranolaDebug("lib:pager");
1003
+ var debug14 = createGranolaDebug("lib:pager");
920
1004
  var ALLOWED_PAGERS = ["less", "more", "cat", "head", "tail", "bat", "most"];
921
1005
  var SHELL_METACHARACTERS = /[;&|`$(){}[\]<>\\!#*?]/;
922
1006
  function validatePagerCommand(cmd) {
923
- debug13("validating pager command: %s", cmd);
1007
+ debug14("validating pager command: %s", cmd);
924
1008
  if (SHELL_METACHARACTERS.test(cmd)) {
925
- debug13("pager validation failed: contains shell metacharacters");
1009
+ debug14("pager validation failed: contains shell metacharacters");
926
1010
  return false;
927
1011
  }
928
1012
  const [binary] = cmd.split(" ");
929
1013
  const binaryName = binary.split("/").pop() || "";
930
1014
  const valid = ALLOWED_PAGERS.includes(binaryName);
931
- debug13("pager validation: %s (binary: %s)", valid ? "passed" : "failed", binaryName);
1015
+ debug14("pager validation: %s (binary: %s)", valid ? "passed" : "failed", binaryName);
932
1016
  return valid;
933
1017
  }
934
1018
  function getPagerCommand() {
935
1019
  if (process.env.GRANOLA_PAGER) {
936
- debug13("pager command: %s (source: GRANOLA_PAGER)", process.env.GRANOLA_PAGER);
1020
+ debug14("pager command: %s (source: GRANOLA_PAGER)", process.env.GRANOLA_PAGER);
937
1021
  return process.env.GRANOLA_PAGER;
938
1022
  }
939
1023
  if (process.env.PAGER) {
940
- debug13("pager command: %s (source: PAGER)", process.env.PAGER);
1024
+ debug14("pager command: %s (source: PAGER)", process.env.PAGER);
941
1025
  return process.env.PAGER;
942
1026
  }
943
1027
  const configuredPager = getConfigValue("pager");
944
1028
  if (configuredPager) {
945
- debug13("pager command: %s (source: config)", configuredPager);
1029
+ debug14("pager command: %s (source: config)", configuredPager);
946
1030
  return configuredPager;
947
1031
  }
948
- debug13("pager command: less -R (source: default)");
1032
+ debug14("pager command: less -R (source: default)");
949
1033
  return "less -R";
950
1034
  }
951
1035
  async function pipeToPager(content) {
952
- debug13("pipeToPager: isTTY=%s, contentLength=%d", process.stdout.isTTY, content.length);
1036
+ debug14("pipeToPager: isTTY=%s, contentLength=%d", process.stdout.isTTY, content.length);
953
1037
  if (!process.stdout.isTTY) {
954
- debug13("not a TTY, writing directly to stdout");
1038
+ debug14("not a TTY, writing directly to stdout");
955
1039
  process.stdout.write(`${content}
956
1040
  `);
957
1041
  return;
@@ -964,7 +1048,7 @@ async function pipeToPager(content) {
964
1048
  return;
965
1049
  }
966
1050
  const [cmd, ...args] = pagerCmd.split(" ");
967
- debug13("spawning pager: %s with args: %O", cmd, args);
1051
+ debug14("spawning pager: %s with args: %O", cmd, args);
968
1052
  return new Promise((resolve) => {
969
1053
  let settled = false;
970
1054
  const finish = () => {
@@ -976,7 +1060,7 @@ async function pipeToPager(content) {
976
1060
  const fallbackToStdout = (reason) => {
977
1061
  if (settled) return;
978
1062
  settled = true;
979
- debug13("falling back to stdout: %s", reason);
1063
+ debug14("falling back to stdout: %s", reason);
980
1064
  console.error(
981
1065
  `Warning: Unable to launch pager "${pagerCmd}" (${reason}). Falling back to direct output.`
982
1066
  );
@@ -991,34 +1075,34 @@ async function pipeToPager(content) {
991
1075
  pager.stdin.write(content);
992
1076
  pager.stdin.end();
993
1077
  pager.on("close", () => {
994
- debug13("pager closed");
1078
+ debug14("pager closed");
995
1079
  finish();
996
1080
  });
997
1081
  pager.on("error", (err) => {
998
- debug13("pager error: %O", err);
1082
+ debug14("pager error: %O", err);
999
1083
  fallbackToStdout(err.message);
1000
1084
  });
1001
1085
  } catch (err) {
1002
- debug13("failed to spawn pager: %O", err);
1086
+ debug14("failed to spawn pager: %O", err);
1003
1087
  fallbackToStdout(err.message);
1004
1088
  }
1005
1089
  });
1006
1090
  }
1007
1091
 
1008
1092
  // src/lib/prosemirror.ts
1009
- var debug14 = createGranolaDebug("lib:prosemirror");
1093
+ var debug15 = createGranolaDebug("lib:prosemirror");
1010
1094
  function toMarkdown(doc) {
1011
- debug14("toMarkdown called with doc: %O", doc);
1095
+ debug15("toMarkdown called with doc: %O", doc);
1012
1096
  if (!doc?.content) {
1013
- debug14("No content in doc, returning empty string");
1097
+ debug15("No content in doc, returning empty string");
1014
1098
  return "";
1015
1099
  }
1016
1100
  const result = doc.content.map((n) => nodeToMd(n)).join("\n\n");
1017
- debug14("toMarkdown result: %s", result);
1101
+ debug15("toMarkdown result: %s", result);
1018
1102
  return result;
1019
1103
  }
1020
1104
  function nodeToMd(node) {
1021
- debug14("nodeToMd processing node type: %s, node: %O", node.type, node);
1105
+ debug15("nodeToMd processing node type: %s, node: %O", node.type, node);
1022
1106
  let result;
1023
1107
  switch (node.type) {
1024
1108
  case "heading": {
@@ -1055,10 +1139,10 @@ ${inlineToMd(node.content)}
1055
1139
  result = applyMarks(node.text || "", node.marks);
1056
1140
  break;
1057
1141
  default:
1058
- debug14("Unknown node type: %s", node.type);
1142
+ debug15("Unknown node type: %s", node.type);
1059
1143
  result = node.content ? node.content.map((c) => nodeToMd(c)).join("") : "";
1060
1144
  }
1061
- debug14("nodeToMd result for %s: %s", node.type, result);
1145
+ debug15("nodeToMd result for %s: %s", node.type, result);
1062
1146
  return result;
1063
1147
  }
1064
1148
  function inlineToMd(content) {
@@ -1076,16 +1160,16 @@ function applyMarks(text, marks) {
1076
1160
  }
1077
1161
 
1078
1162
  // src/services/meetings.ts
1079
- var debug15 = createGranolaDebug("service:meetings");
1163
+ var debug16 = createGranolaDebug("service:meetings");
1080
1164
  async function getFolderDocumentIds(client2, folderId) {
1081
- debug15("fetching folder %s via getDocumentList", folderId);
1165
+ debug16("fetching folder %s via getDocumentList", folderId);
1082
1166
  const folder = await client2.getDocumentList(folderId);
1083
1167
  if (!folder) {
1084
- debug15("folder %s not found", folderId);
1168
+ debug16("folder %s not found", folderId);
1085
1169
  return [];
1086
1170
  }
1087
1171
  const ids = folder.document_ids || folder.documents?.map((doc) => doc.id) || [];
1088
- debug15("folder %s returned %d document ids", folderId, ids.length);
1172
+ debug16("folder %s returned %d document ids", folderId, ids.length);
1089
1173
  return ids;
1090
1174
  }
1091
1175
  var DOCUMENT_BATCH_SIZE = 100;
@@ -1103,50 +1187,50 @@ async function fetchMeetingsByIds(client2, documentIds) {
1103
1187
  const docs = res?.documents || res?.docs || [];
1104
1188
  meetings.push(...docs);
1105
1189
  }
1106
- debug15("fetched %d meetings via getDocumentsBatch", meetings.length);
1190
+ debug16("fetched %d meetings via getDocumentsBatch", meetings.length);
1107
1191
  return meetings;
1108
1192
  }
1109
1193
  async function loadMeetingMetadata(client2, id) {
1110
1194
  try {
1111
1195
  const metadata = await client2.getDocumentMetadata(id);
1112
1196
  if (!metadata) {
1113
- debug15("getDocumentMetadata returned null for %s", id);
1197
+ debug16("getDocumentMetadata returned null for %s", id);
1114
1198
  return null;
1115
1199
  }
1116
1200
  return metadata;
1117
1201
  } catch (err) {
1118
- debug15("getDocumentMetadata failed for %s: %O", id, err);
1202
+ debug16("getDocumentMetadata failed for %s: %O", id, err);
1119
1203
  return null;
1120
1204
  }
1121
1205
  }
1122
1206
  async function fetchFolderMeetings(client2, folderId) {
1123
1207
  const ids = await getFolderDocumentIds(client2, folderId);
1124
1208
  if (ids.length === 0) {
1125
- debug15("folder %s has no documents", folderId);
1209
+ debug16("folder %s has no documents", folderId);
1126
1210
  return [];
1127
1211
  }
1128
1212
  return fetchMeetingsByIds(client2, ids);
1129
1213
  }
1130
1214
  async function list2(opts = {}) {
1131
1215
  return withTokenRefresh(async () => {
1132
- debug15("list called with opts: %O", opts);
1216
+ debug16("list called with opts: %O", opts);
1133
1217
  const client2 = await getClient();
1134
1218
  const { limit = 20, offset = 0, workspace, folder } = opts;
1135
1219
  if (folder) {
1136
- debug15("listing meetings for folder: %s", folder);
1220
+ debug16("listing meetings for folder: %s", folder);
1137
1221
  const folderMeetings = await fetchFolderMeetings(client2, folder);
1138
- debug15("fetched %d meetings for folder %s", folderMeetings.length, folder);
1222
+ debug16("fetched %d meetings for folder %s", folderMeetings.length, folder);
1139
1223
  let filtered = folderMeetings;
1140
1224
  if (workspace) {
1141
1225
  filtered = folderMeetings.filter((m) => m.workspace_id === workspace);
1142
- debug15(
1226
+ debug16(
1143
1227
  "workspace filter applied for folder %s: %d meetings remain",
1144
1228
  folder,
1145
1229
  filtered.length
1146
1230
  );
1147
1231
  }
1148
1232
  const paginated = filtered.slice(offset, offset + limit);
1149
- debug15("returning %d meetings from folder %s after pagination", paginated.length, folder);
1233
+ debug16("returning %d meetings from folder %s after pagination", paginated.length, folder);
1150
1234
  return paginated;
1151
1235
  }
1152
1236
  const res = await client2.getDocuments({
@@ -1155,68 +1239,90 @@ async function list2(opts = {}) {
1155
1239
  include_last_viewed_panel: false
1156
1240
  });
1157
1241
  let meetings = res?.docs || [];
1158
- debug15("fetched %d meetings", meetings.length);
1242
+ debug16("fetched %d meetings", meetings.length);
1159
1243
  if (workspace) {
1160
1244
  meetings = meetings.filter((m) => m.workspace_id === workspace);
1161
- debug15("filtered to %d meetings for workspace: %s", meetings.length, workspace);
1245
+ debug16("filtered to %d meetings for workspace: %s", meetings.length, workspace);
1162
1246
  }
1163
1247
  return meetings;
1164
1248
  });
1165
1249
  }
1166
1250
  var RESOLVE_PAGE_SIZE = 100;
1167
1251
  var MAX_RESOLVE_PAGES = 100;
1252
+ var FULL_UUID_LENGTH = 36;
1253
+ var CACHE_TTL_MS = 6e4;
1254
+ var meetingsCache = null;
1255
+ async function getCachedMeetings(client2) {
1256
+ if (meetingsCache && Date.now() - meetingsCache.timestamp < CACHE_TTL_MS) {
1257
+ debug16("using cached meetings (%d items)", meetingsCache.meetings.length);
1258
+ return meetingsCache.meetings;
1259
+ }
1260
+ debug16("cache miss or expired, fetching meetings");
1261
+ const meetings = [];
1262
+ let offset = 0;
1263
+ for (let page = 0; page < MAX_RESOLVE_PAGES; page += 1) {
1264
+ const res = await client2.getDocuments({
1265
+ limit: RESOLVE_PAGE_SIZE,
1266
+ offset,
1267
+ include_last_viewed_panel: false
1268
+ });
1269
+ const docs = res?.docs || [];
1270
+ meetings.push(...docs);
1271
+ if (docs.length < RESOLVE_PAGE_SIZE) {
1272
+ break;
1273
+ }
1274
+ offset += RESOLVE_PAGE_SIZE;
1275
+ }
1276
+ meetingsCache = { meetings, timestamp: Date.now() };
1277
+ debug16("cached %d meetings", meetings.length);
1278
+ return meetings;
1279
+ }
1168
1280
  async function resolveId(partialId) {
1169
1281
  return withTokenRefresh(async () => {
1170
- debug15("resolving meeting id: %s", partialId);
1282
+ debug16("resolving meeting id: %s (length: %d)", partialId, partialId.length);
1171
1283
  const client2 = await getClient();
1172
- const matches = /* @__PURE__ */ new Set();
1173
- let offset = 0;
1174
- for (let page = 0; page < MAX_RESOLVE_PAGES; page += 1) {
1175
- const res = await client2.getDocuments({
1176
- limit: RESOLVE_PAGE_SIZE,
1177
- offset,
1178
- include_last_viewed_panel: false
1179
- });
1180
- const meetings = res?.docs || [];
1181
- debug15(
1182
- "resolveId page %d (offset %d) returned %d meetings",
1183
- page + 1,
1184
- offset,
1185
- meetings.length
1186
- );
1187
- for (const meeting of meetings) {
1188
- if (meeting.id?.startsWith(partialId)) {
1189
- matches.add(meeting.id);
1190
- if (matches.size > 1) {
1191
- debug15("ambiguous id: %s matches >1 meetings", partialId);
1192
- throw new Error(`Ambiguous ID: ${partialId} matches ${matches.size} meetings`);
1193
- }
1284
+ if (partialId.length >= FULL_UUID_LENGTH) {
1285
+ debug16("attempting direct lookup for full UUID");
1286
+ try {
1287
+ const metadata = await client2.getDocumentMetadata(partialId);
1288
+ if (metadata) {
1289
+ debug16("direct lookup successful for: %s", partialId);
1290
+ return partialId;
1194
1291
  }
1292
+ } catch {
1293
+ debug16("direct lookup failed, falling back to search");
1195
1294
  }
1196
- if (meetings.length < RESOLVE_PAGE_SIZE) {
1197
- break;
1295
+ }
1296
+ const meetings = await getCachedMeetings(client2);
1297
+ const matches = /* @__PURE__ */ new Set();
1298
+ for (const meeting of meetings) {
1299
+ if (meeting.id?.startsWith(partialId)) {
1300
+ matches.add(meeting.id);
1301
+ if (matches.size > 1) {
1302
+ debug16("ambiguous id: %s matches >1 meetings", partialId);
1303
+ throw new Error(`Ambiguous ID: ${partialId} matches ${matches.size} meetings`);
1304
+ }
1198
1305
  }
1199
- offset += RESOLVE_PAGE_SIZE;
1200
1306
  }
1201
1307
  if (matches.size === 0) {
1202
- debug15("no meeting found for id: %s", partialId);
1308
+ debug16("no meeting found for id: %s", partialId);
1203
1309
  return null;
1204
1310
  }
1205
1311
  const match = matches.values().next().value;
1206
- debug15("resolved meeting: %s -> %s", partialId, match);
1312
+ debug16("resolved meeting: %s -> %s", partialId, match);
1207
1313
  return match;
1208
1314
  });
1209
1315
  }
1210
1316
  async function get2(id) {
1211
1317
  return withTokenRefresh(async () => {
1212
- debug15("getting meeting: %s", id);
1318
+ debug16("getting meeting: %s", id);
1213
1319
  const client2 = await getClient();
1214
1320
  const metadata = await loadMeetingMetadata(client2, id);
1215
1321
  if (!metadata) {
1216
- debug15("meeting %s: not found", id);
1322
+ debug16("meeting %s: not found", id);
1217
1323
  return null;
1218
1324
  }
1219
- debug15("meeting %s: found", id);
1325
+ debug16("meeting %s: found", id);
1220
1326
  return { id, ...metadata };
1221
1327
  });
1222
1328
  }
@@ -1224,36 +1330,36 @@ async function findMeetingViaDocuments(client2, id, { includeLastViewedPanel })
1224
1330
  let offset = 0;
1225
1331
  for (let page = 0; page < MAX_NOTES_PAGES; page += 1) {
1226
1332
  try {
1227
- debug15("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
1333
+ debug16("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
1228
1334
  const res = await client2.getDocuments({
1229
1335
  limit: NOTES_PAGE_SIZE,
1230
1336
  offset,
1231
1337
  include_last_viewed_panel: includeLastViewedPanel
1232
1338
  });
1233
1339
  const meetings = res?.docs || [];
1234
- debug15("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
1340
+ debug16("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
1235
1341
  if (meetings.length === 0) break;
1236
1342
  const meeting = meetings.find((m) => m.id === id);
1237
1343
  if (meeting) {
1238
- debug15("findMeetingViaDocuments located meeting %s on page %d", id, page);
1344
+ debug16("findMeetingViaDocuments located meeting %s on page %d", id, page);
1239
1345
  return meeting;
1240
1346
  }
1241
1347
  offset += NOTES_PAGE_SIZE;
1242
1348
  } catch (err) {
1243
- debug15("findMeetingViaDocuments error: %O", err);
1349
+ debug16("findMeetingViaDocuments error: %O", err);
1244
1350
  return null;
1245
1351
  }
1246
1352
  }
1247
- debug15("findMeetingViaDocuments did not locate meeting %s", id);
1353
+ debug16("findMeetingViaDocuments did not locate meeting %s", id);
1248
1354
  return null;
1249
1355
  }
1250
1356
  async function getNotes(id) {
1251
1357
  return withTokenRefresh(async () => {
1252
- debug15("getNotes called with id: %s", id);
1358
+ debug16("getNotes called with id: %s", id);
1253
1359
  const client2 = await getClient();
1254
1360
  const metadata = await loadMeetingMetadata(client2, id);
1255
1361
  if (metadata && "notes" in metadata) {
1256
- debug15("getNotes resolved via metadata response");
1362
+ debug16("getNotes resolved via metadata response");
1257
1363
  return metadata.notes || null;
1258
1364
  }
1259
1365
  const meeting = await findMeetingViaDocuments(client2, id, {
@@ -1267,11 +1373,11 @@ async function getNotes(id) {
1267
1373
  }
1268
1374
  async function getEnhancedNotes(id) {
1269
1375
  return withTokenRefresh(async () => {
1270
- debug15("getEnhancedNotes called with id: %s", id);
1376
+ debug16("getEnhancedNotes called with id: %s", id);
1271
1377
  const client2 = await getClient();
1272
1378
  const metadata = await loadMeetingMetadata(client2, id);
1273
1379
  if (metadata && "last_viewed_panel" in metadata) {
1274
- debug15("getEnhancedNotes resolved via metadata response");
1380
+ debug16("getEnhancedNotes resolved via metadata response");
1275
1381
  return metadata.last_viewed_panel?.content || null;
1276
1382
  }
1277
1383
  const meeting = await findMeetingViaDocuments(client2, id, {
@@ -1285,24 +1391,24 @@ async function getEnhancedNotes(id) {
1285
1391
  }
1286
1392
  async function getTranscript(id) {
1287
1393
  return withTokenRefresh(async () => {
1288
- debug15("getTranscript called with id: %s", id);
1394
+ debug16("getTranscript called with id: %s", id);
1289
1395
  const client2 = await getClient();
1290
1396
  try {
1291
1397
  const transcript = await client2.getDocumentTranscript(id);
1292
- debug15("getTranscript got %d utterances", transcript.length);
1398
+ debug16("getTranscript got %d utterances", transcript.length);
1293
1399
  return transcript;
1294
1400
  } catch (err) {
1295
- debug15("getTranscript error: %O", err);
1401
+ debug16("getTranscript error: %O", err);
1296
1402
  return [];
1297
1403
  }
1298
1404
  });
1299
1405
  }
1300
1406
 
1301
1407
  // src/commands/meeting/enhanced.ts
1302
- var debug16 = createGranolaDebug("cmd:meeting:enhanced");
1408
+ var debug17 = createGranolaDebug("cmd:meeting:enhanced");
1303
1409
  function createEnhancedCommand() {
1304
1410
  return new Command10("enhanced").description("View AI-enhanced meeting notes").argument("<id>", "Meeting ID").option("-o, --output <format>", "Output format (markdown, json, yaml, toon)", "markdown").action(async (id, opts, cmd) => {
1305
- debug16("enhanced command invoked with id: %s", id);
1411
+ debug17("enhanced command invoked with id: %s", id);
1306
1412
  const global = cmd.optsWithGlobals();
1307
1413
  let fullId;
1308
1414
  try {
@@ -1320,7 +1426,7 @@ function createEnhancedCommand() {
1320
1426
  try {
1321
1427
  notes = await getEnhancedNotes(fullId);
1322
1428
  } catch (error) {
1323
- debug16("failed to load enhanced notes: %O", error);
1429
+ debug17("failed to load enhanced notes: %O", error);
1324
1430
  console.error(chalk10.red("Error:"), "Failed to fetch enhanced notes.");
1325
1431
  if (error instanceof Error) {
1326
1432
  console.error(chalk10.dim(error.message));
@@ -1364,10 +1470,10 @@ function toToon(data) {
1364
1470
  }
1365
1471
 
1366
1472
  // src/commands/meeting/export.ts
1367
- var debug17 = createGranolaDebug("cmd:meeting:export");
1473
+ var debug18 = createGranolaDebug("cmd:meeting:export");
1368
1474
  function createExportCommand() {
1369
1475
  return new Command11("export").description("Export meeting data").argument("<id>", "Meeting ID").option("-f, --format <format>", "Output format (json, toon)", "json").action(async (id, options) => {
1370
- debug17("export command invoked with id: %s, format: %s", id, options.format);
1476
+ debug18("export command invoked with id: %s, format: %s", id, options.format);
1371
1477
  const format = options.format;
1372
1478
  if (format !== "json" && format !== "toon") {
1373
1479
  console.error(chalk11.red(`Invalid format: ${options.format}. Use 'json' or 'toon'.`));
@@ -1417,10 +1523,10 @@ var exportCommand = createExportCommand();
1417
1523
  // src/commands/meeting/list.ts
1418
1524
  import chalk12 from "chalk";
1419
1525
  import { Command as Command12 } from "commander";
1420
- var debug18 = createGranolaDebug("cmd:meeting:list");
1526
+ var debug19 = createGranolaDebug("cmd:meeting:list");
1421
1527
  function createListCommand2() {
1422
1528
  return new Command12("list").description("List meetings").option("-l, --limit <n>", "Number of meetings", "20").option("-w, --workspace <id>", "Filter by workspace").option("-f, --folder <id>", "Filter by folder").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
1423
- debug18("list command invoked with opts: %O", opts);
1529
+ debug19("list command invoked with opts: %O", opts);
1424
1530
  const limit = Number.parseInt(opts.limit, 10);
1425
1531
  if (!Number.isFinite(limit) || limit < 1) {
1426
1532
  console.error(chalk12.red("Invalid --limit value. Please provide a positive number."));
@@ -1433,9 +1539,9 @@ function createListCommand2() {
1433
1539
  workspace,
1434
1540
  folder: opts.folder
1435
1541
  });
1436
- debug18("fetched %d meetings", data.length);
1542
+ debug19("fetched %d meetings", data.length);
1437
1543
  const format = opts.output || null;
1438
- debug18("output format: %s", format || "table");
1544
+ debug19("output format: %s", format || "table");
1439
1545
  if (format) {
1440
1546
  if (!["json", "yaml", "toon"].includes(format)) {
1441
1547
  console.error(chalk12.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
@@ -1463,10 +1569,10 @@ var listCommand2 = createListCommand2();
1463
1569
  // src/commands/meeting/notes.ts
1464
1570
  import chalk13 from "chalk";
1465
1571
  import { Command as Command13 } from "commander";
1466
- var debug19 = createGranolaDebug("cmd:meeting:notes");
1572
+ var debug20 = createGranolaDebug("cmd:meeting:notes");
1467
1573
  function createNotesCommand() {
1468
1574
  return new Command13("notes").description("View meeting notes").argument("<id>", "Meeting ID").option("-o, --output <format>", "Output format (markdown, json, yaml, toon)", "markdown").action(async (id, opts, cmd) => {
1469
- debug19("notes command invoked with id: %s", id);
1575
+ debug20("notes command invoked with id: %s", id);
1470
1576
  const global = cmd.optsWithGlobals();
1471
1577
  let fullId;
1472
1578
  try {
@@ -1484,7 +1590,7 @@ function createNotesCommand() {
1484
1590
  try {
1485
1591
  notes = await getNotes(fullId);
1486
1592
  } catch (error) {
1487
- debug19("failed to load notes: %O", error);
1593
+ debug20("failed to load notes: %O", error);
1488
1594
  console.error(chalk13.red("Error:"), "Failed to fetch notes.");
1489
1595
  if (error instanceof Error) {
1490
1596
  console.error(chalk13.dim(error.message));
@@ -1522,17 +1628,17 @@ import chalk14 from "chalk";
1522
1628
  import { Command as Command14 } from "commander";
1523
1629
 
1524
1630
  // src/lib/transcript.ts
1525
- var debug20 = createGranolaDebug("lib:transcript");
1631
+ var debug21 = createGranolaDebug("lib:transcript");
1526
1632
  function formatTranscript(utterances, opts = {}) {
1527
- debug20("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
1633
+ debug21("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
1528
1634
  const { timestamps = false, source = "all" } = opts;
1529
1635
  let filtered = utterances;
1530
1636
  if (source !== "all") {
1531
1637
  filtered = utterances.filter((u) => u.source === source);
1532
- debug20("filtered to %d utterances (source=%s)", filtered.length, source);
1638
+ debug21("filtered to %d utterances (source=%s)", filtered.length, source);
1533
1639
  }
1534
1640
  if (filtered.length === 0) {
1535
- debug20("no transcript available");
1641
+ debug21("no transcript available");
1536
1642
  return "No transcript available.";
1537
1643
  }
1538
1644
  const lines = [];
@@ -1559,11 +1665,11 @@ function formatTimestamp(iso) {
1559
1665
  }
1560
1666
 
1561
1667
  // src/commands/meeting/transcript.ts
1562
- var debug21 = createGranolaDebug("cmd:meeting:transcript");
1668
+ var debug22 = createGranolaDebug("cmd:meeting:transcript");
1563
1669
  var SOURCE_OPTIONS = /* @__PURE__ */ new Set(["microphone", "system", "all"]);
1564
1670
  function createTranscriptCommand() {
1565
1671
  return new Command14("transcript").description("View meeting transcript").argument("<id>", "Meeting ID").option("-t, --timestamps", "Include timestamps").option("-s, --source <type>", "Filter: microphone, system, all", "all").option("-o, --output <format>", "Output format (text, json, yaml, toon)", "text").action(async (id, opts, cmd) => {
1566
- debug21("transcript command invoked with id: %s, opts: %O", id, opts);
1672
+ debug22("transcript command invoked with id: %s, opts: %O", id, opts);
1567
1673
  const global = cmd.optsWithGlobals();
1568
1674
  let fullId;
1569
1675
  try {
@@ -1617,11 +1723,11 @@ var transcriptCommand = createTranscriptCommand();
1617
1723
  // src/commands/meeting/view.ts
1618
1724
  import chalk15 from "chalk";
1619
1725
  import { Command as Command15 } from "commander";
1620
- import open from "open";
1621
- var debug22 = createGranolaDebug("cmd:meeting:view");
1726
+ import open2 from "open";
1727
+ var debug23 = createGranolaDebug("cmd:meeting:view");
1622
1728
  function createViewCommand2() {
1623
1729
  return new Command15("view").description("View meeting details").argument("<id>", "Meeting ID").option("--web", "Open in browser").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (id, opts) => {
1624
- debug22("view command invoked with id: %s, opts: %O", id, opts);
1730
+ debug23("view command invoked with id: %s, opts: %O", id, opts);
1625
1731
  let fullId;
1626
1732
  try {
1627
1733
  const resolved = await resolveId(id);
@@ -1635,7 +1741,7 @@ function createViewCommand2() {
1635
1741
  process.exit(1);
1636
1742
  }
1637
1743
  if (opts.web) {
1638
- await open(`https://app.granola.ai/meeting/${fullId}`);
1744
+ await open2(`https://app.granola.ai/meeting/${fullId}`);
1639
1745
  return;
1640
1746
  }
1641
1747
  const meeting = await get2(fullId);
@@ -1686,14 +1792,14 @@ import chalk16 from "chalk";
1686
1792
  import { Command as Command17 } from "commander";
1687
1793
 
1688
1794
  // src/services/workspaces.ts
1689
- var debug23 = createGranolaDebug("service:workspaces");
1795
+ var debug24 = createGranolaDebug("service:workspaces");
1690
1796
  async function list3() {
1691
1797
  return withTokenRefresh(async () => {
1692
- debug23("fetching workspaces");
1798
+ debug24("fetching workspaces");
1693
1799
  const client2 = await getClient();
1694
1800
  const res = await client2.getWorkspaces();
1695
1801
  const workspacesArray = res?.workspaces || [];
1696
- debug23("found %d workspaces", workspacesArray.length);
1802
+ debug24("found %d workspaces", workspacesArray.length);
1697
1803
  return workspacesArray.map((item) => {
1698
1804
  const ws = item.workspace;
1699
1805
  return {
@@ -1706,35 +1812,35 @@ async function list3() {
1706
1812
  });
1707
1813
  }
1708
1814
  async function resolveId2(partialId) {
1709
- debug23("resolving workspace id: %s", partialId);
1815
+ debug24("resolving workspace id: %s", partialId);
1710
1816
  const workspaces = await list3();
1711
1817
  const matches = workspaces.filter((w) => w.id.startsWith(partialId));
1712
1818
  if (matches.length === 0) {
1713
- debug23("no workspace found for id: %s", partialId);
1819
+ debug24("no workspace found for id: %s", partialId);
1714
1820
  return null;
1715
1821
  }
1716
1822
  if (matches.length > 1) {
1717
- debug23("ambiguous id: %s matches %d workspaces", partialId, matches.length);
1823
+ debug24("ambiguous id: %s matches %d workspaces", partialId, matches.length);
1718
1824
  throw new Error(`Ambiguous ID: ${partialId} matches ${matches.length} workspaces`);
1719
1825
  }
1720
- debug23("resolved workspace: %s -> %s", partialId, matches[0].id);
1826
+ debug24("resolved workspace: %s -> %s", partialId, matches[0].id);
1721
1827
  return matches[0].id;
1722
1828
  }
1723
1829
  async function get3(id) {
1724
- debug23("getting workspace: %s", id);
1830
+ debug24("getting workspace: %s", id);
1725
1831
  const workspaces = await list3();
1726
1832
  const workspace = workspaces.find((w) => w.id === id) || null;
1727
- debug23("workspace %s: %s", id, workspace ? "found" : "not found");
1833
+ debug24("workspace %s: %s", id, workspace ? "found" : "not found");
1728
1834
  return workspace;
1729
1835
  }
1730
1836
 
1731
1837
  // src/commands/workspace/list.ts
1732
- var debug24 = createGranolaDebug("cmd:workspace:list");
1838
+ var debug25 = createGranolaDebug("cmd:workspace:list");
1733
1839
  function createListCommand3() {
1734
1840
  return new Command17("list").description("List workspaces").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
1735
- debug24("workspace list command invoked");
1841
+ debug25("workspace list command invoked");
1736
1842
  const data = await list3();
1737
- debug24("fetched %d workspaces", data.length);
1843
+ debug25("fetched %d workspaces", data.length);
1738
1844
  const format = opts.output || null;
1739
1845
  if (format) {
1740
1846
  if (!["json", "yaml", "toon"].includes(format)) {
@@ -1761,10 +1867,10 @@ var listCommand3 = createListCommand3();
1761
1867
  // src/commands/workspace/view.ts
1762
1868
  import chalk17 from "chalk";
1763
1869
  import { Command as Command18 } from "commander";
1764
- var debug25 = createGranolaDebug("cmd:workspace:view");
1870
+ var debug26 = createGranolaDebug("cmd:workspace:view");
1765
1871
  function createViewCommand3() {
1766
1872
  return new Command18("view").description("View workspace details").argument("<id>", "Workspace ID").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (id, opts) => {
1767
- debug25("workspace view command invoked with id: %s", id);
1873
+ debug26("workspace view command invoked with id: %s", id);
1768
1874
  let fullId;
1769
1875
  try {
1770
1876
  const resolved = await resolveId2(id);
@@ -1803,12 +1909,11 @@ var viewCommand3 = createViewCommand3();
1803
1909
  var workspaceCommand = new Command19("workspace").description("Work with workspaces").addCommand(listCommand3).addCommand(viewCommand3);
1804
1910
 
1805
1911
  // src/main.ts
1806
- var debug26 = createGranolaDebug("cli");
1912
+ var debug27 = createGranolaDebug("cli");
1807
1913
  var debugAlias = createGranolaDebug("cli:alias");
1808
- var debugSubcmd = createGranolaDebug("cli:subcommand");
1809
1914
  var packageJson = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
1810
- debug26("granola-cli v%s starting", packageJson.version);
1811
- debug26("arguments: %O", process.argv.slice(2));
1915
+ debug27("granola-cli v%s starting", packageJson.version);
1916
+ debug27("arguments: %O", process.argv.slice(2));
1812
1917
  var program = new Command20();
1813
1918
  program.name("granola").description("CLI for Granola meeting notes").version(packageJson.version).option("--no-pager", "Disable pager");
1814
1919
  program.addCommand(authCommand);
@@ -1817,57 +1922,6 @@ program.addCommand(workspaceCommand);
1817
1922
  program.addCommand(folderCommand);
1818
1923
  program.addCommand(configCommand);
1819
1924
  program.addCommand(aliasCommand);
1820
- function discoverExternalSubcommands() {
1821
- const subcommands = /* @__PURE__ */ new Map();
1822
- const pathDirs = (process.env.PATH || "").split(delimiter);
1823
- debugSubcmd("scanning PATH directories: %d dirs", pathDirs.length);
1824
- for (const dir of pathDirs) {
1825
- if (!existsSync(dir)) continue;
1826
- try {
1827
- const entries = readdirSync(dir);
1828
- for (const entry of entries) {
1829
- if (!entry.startsWith("granola-")) continue;
1830
- const fullPath = join2(dir, entry);
1831
- try {
1832
- const stat = statSync(fullPath);
1833
- if (stat.isFile()) {
1834
- const subcommandName = entry.replace(/^granola-/, "").replace(/\.(exe|cmd|bat)$/i, "");
1835
- if (!subcommands.has(subcommandName)) {
1836
- debugSubcmd("found external subcommand: %s at %s", subcommandName, fullPath);
1837
- subcommands.set(subcommandName, fullPath);
1838
- }
1839
- }
1840
- } catch {
1841
- }
1842
- }
1843
- } catch {
1844
- }
1845
- }
1846
- debugSubcmd("discovered %d external subcommands", subcommands.size);
1847
- return subcommands;
1848
- }
1849
- var externalSubcommands = discoverExternalSubcommands();
1850
- for (const [name, execPath] of externalSubcommands) {
1851
- if (program.commands.some((cmd) => cmd.name() === name)) continue;
1852
- const externalCmd = new Command20(name).description(`[external] ${name}`).allowUnknownOption().allowExcessArguments().action((...args) => {
1853
- const cmdArgs = args.slice(0, -1);
1854
- debugSubcmd("executing external command: %s with args: %O", execPath, cmdArgs);
1855
- const child = spawn2(execPath, cmdArgs, {
1856
- stdio: "inherit",
1857
- shell: process.platform === "win32"
1858
- });
1859
- child.on("close", (code) => {
1860
- debugSubcmd("external command exited with code: %d", code);
1861
- process.exit(code ?? 0);
1862
- });
1863
- child.on("error", (err) => {
1864
- debugSubcmd("external command error: %O", err);
1865
- console.error(`Failed to run external command: ${err.message}`);
1866
- process.exit(1);
1867
- });
1868
- });
1869
- program.addCommand(externalCmd);
1870
- }
1871
1925
  function expandAlias(args) {
1872
1926
  if (args.length < 3) return args;
1873
1927
  const command = args[2];
@@ -1888,6 +1942,6 @@ function expandAlias(args) {
1888
1942
  return args;
1889
1943
  }
1890
1944
  var expandedArgs = expandAlias(process.argv);
1891
- debug26("parsing with args: %O", expandedArgs.slice(2));
1945
+ debug27("parsing with args: %O", expandedArgs.slice(2));
1892
1946
  program.parseAsync(expandedArgs);
1893
1947
  //# sourceMappingURL=main.js.map