granola-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.
package/dist/main.js ADDED
@@ -0,0 +1,1893 @@
1
+ #!/usr/bin/env node
2
+
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";
7
+ import { Command as Command20 } from "commander";
8
+
9
+ // src/commands/alias.ts
10
+ import chalk2 from "chalk";
11
+ import { Command } from "commander";
12
+
13
+ // src/lib/config.ts
14
+ import Conf from "conf";
15
+
16
+ // src/lib/alias.ts
17
+ import { parse as parseShellQuote } from "shell-quote";
18
+ var UNSAFE_ALIAS_PATTERN = /[`$]/;
19
+ function parseAliasArguments(command) {
20
+ const parsed = parseShellQuote(command);
21
+ if (parsed.length === 0) {
22
+ throw new Error("Alias command cannot be empty.");
23
+ }
24
+ const hasUnsafeToken = parsed.some((token) => typeof token !== "string");
25
+ if (hasUnsafeToken) {
26
+ throw new Error("Alias command contains unsupported shell syntax.");
27
+ }
28
+ const args = parsed;
29
+ const hasSubstitution = args.some((token) => UNSAFE_ALIAS_PATTERN.test(token));
30
+ if (hasSubstitution) {
31
+ throw new Error("Alias command contains unsupported substitution syntax.");
32
+ }
33
+ return args;
34
+ }
35
+ function isAliasCommandSafe(command) {
36
+ try {
37
+ parseAliasArguments(command);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // src/lib/debug.ts
45
+ import createDebug from "debug";
46
+ function createGranolaDebug(namespace) {
47
+ return createDebug(`granola:${namespace}`);
48
+ }
49
+ function maskToken(token) {
50
+ if (!token || token.length < 12) return "[REDACTED]";
51
+ return `${token.slice(0, 4)}...${token.slice(-4)}`;
52
+ }
53
+
54
+ // src/lib/config.ts
55
+ var debug = createGranolaDebug("lib:config");
56
+ var config = new Conf({
57
+ projectName: "granola",
58
+ defaults: {}
59
+ });
60
+ debug("config store initialized at: %s", config.path);
61
+ function getConfig() {
62
+ debug("getConfig: returning store");
63
+ return config.store;
64
+ }
65
+ function getConfigValue(key) {
66
+ const value = config.get(key);
67
+ debug("getConfigValue: %s = %O", key, value);
68
+ return value;
69
+ }
70
+ function setConfigValue(key, value) {
71
+ debug("setConfigValue: %s = %O", key, value);
72
+ config.set(key, value);
73
+ }
74
+ function resetConfig() {
75
+ debug("resetConfig: clearing all configuration");
76
+ config.clear();
77
+ }
78
+ function getAlias(name) {
79
+ const aliases = config.get("aliases") || {};
80
+ const alias = aliases[name];
81
+ debug("getAlias: %s -> %s", name, alias || "(not found)");
82
+ return alias;
83
+ }
84
+ function validateAliasCommand(command) {
85
+ return isAliasCommandSafe(command);
86
+ }
87
+ function setAlias(name, command) {
88
+ debug("setAlias: %s -> %s", name, command);
89
+ if (!validateAliasCommand(command)) {
90
+ debug("setAlias: invalid command characters");
91
+ throw new Error(
92
+ "Alias command contains invalid characters or shell syntax. Only literal arguments are allowed."
93
+ );
94
+ }
95
+ const aliases = config.get("aliases") || {};
96
+ aliases[name] = command;
97
+ config.set("aliases", aliases);
98
+ }
99
+ function deleteAlias(name) {
100
+ debug("deleteAlias: removing %s", name);
101
+ const aliases = config.get("aliases") || {};
102
+ delete aliases[name];
103
+ config.set("aliases", aliases);
104
+ }
105
+ function listAliases() {
106
+ const aliases = config.get("aliases") || {};
107
+ debug("listAliases: returning %d aliases", Object.keys(aliases).length);
108
+ return aliases;
109
+ }
110
+
111
+ // src/lib/output.ts
112
+ import { encode as toonEncode } from "@toon-format/toon";
113
+ import chalk from "chalk";
114
+ import Table from "cli-table3";
115
+ import { stringify as yamlStringify } from "yaml";
116
+ var debug2 = createGranolaDebug("lib:output");
117
+ function formatOutput(data, format) {
118
+ debug2("formatOutput: format=%s, dataType=%s", format, typeof data);
119
+ switch (format) {
120
+ case "yaml":
121
+ return yamlStringify(data);
122
+ case "toon":
123
+ return toonEncode(data);
124
+ default:
125
+ return JSON.stringify(data, null, 2);
126
+ }
127
+ }
128
+ function table(data, columns) {
129
+ debug2("table: rendering %d rows, %d columns", data.length, columns.length);
130
+ const colWidths = columns.map((c) => c.width ?? null);
131
+ const t = new Table({
132
+ head: columns.map((c) => chalk.bold(c.header)),
133
+ colWidths,
134
+ style: { head: [], border: [] },
135
+ chars: {
136
+ top: "",
137
+ "top-mid": "",
138
+ "top-left": "",
139
+ "top-right": "",
140
+ bottom: "",
141
+ "bottom-mid": "",
142
+ "bottom-left": "",
143
+ "bottom-right": "",
144
+ left: "",
145
+ "left-mid": "",
146
+ mid: "",
147
+ "mid-mid": "",
148
+ right: "",
149
+ "right-mid": "",
150
+ middle: " "
151
+ }
152
+ });
153
+ for (const row of data) {
154
+ t.push(
155
+ columns.map((c) => {
156
+ const val = row[c.key];
157
+ return c.format ? c.format(val) : String(val ?? "");
158
+ })
159
+ );
160
+ }
161
+ return t.toString();
162
+ }
163
+ function formatDate(iso) {
164
+ const d = new Date(iso);
165
+ return d.toLocaleDateString("en-US", {
166
+ month: "short",
167
+ day: "numeric",
168
+ year: "numeric"
169
+ });
170
+ }
171
+ function truncate(s, len) {
172
+ if (s.length <= len) return s;
173
+ return `${s.slice(0, len - 1)}\u2026`;
174
+ }
175
+
176
+ // src/commands/alias.ts
177
+ var debug3 = createGranolaDebug("cmd:alias");
178
+ function createAliasCommand() {
179
+ const cmd = new Command("alias").description("Create command shortcuts");
180
+ cmd.command("list").description("List aliases").option("-o, --output <format>", "Output format (json, yaml, toon)").action((opts) => {
181
+ debug3("alias list command invoked");
182
+ const aliases = listAliases();
183
+ const format = opts.output || null;
184
+ if (format) {
185
+ if (!["json", "yaml", "toon"].includes(format)) {
186
+ console.error(chalk2.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
187
+ process.exit(1);
188
+ }
189
+ console.log(formatOutput(aliases, format));
190
+ return;
191
+ }
192
+ if (Object.keys(aliases).length === 0) {
193
+ console.log(chalk2.dim("No aliases defined."));
194
+ return;
195
+ }
196
+ for (const [name, command] of Object.entries(aliases)) {
197
+ console.log(`${chalk2.bold(name)}: ${command}`);
198
+ }
199
+ });
200
+ cmd.command("set <name> <command>").description("Create alias").action((name, command) => {
201
+ debug3("alias set command invoked: %s -> %s", name, command);
202
+ setAlias(name, command);
203
+ console.log(chalk2.green(`Created alias: ${name} -> ${command}`));
204
+ });
205
+ cmd.command("delete <name>").description("Delete alias").action((name) => {
206
+ debug3("alias delete command invoked: %s", name);
207
+ const existing = getAlias(name);
208
+ if (!existing) {
209
+ console.log(chalk2.yellow(`Alias '${name}' not found`));
210
+ return;
211
+ }
212
+ deleteAlias(name);
213
+ console.log(chalk2.green(`Deleted alias: ${name}`));
214
+ });
215
+ return cmd;
216
+ }
217
+ var aliasCommand = createAliasCommand();
218
+
219
+ // src/commands/auth/index.ts
220
+ import { Command as Command5 } from "commander";
221
+
222
+ // src/commands/auth/login.ts
223
+ import chalk3 from "chalk";
224
+ import { Command as Command2 } from "commander";
225
+
226
+ // src/lib/auth.ts
227
+ import { readFile } from "fs/promises";
228
+ import { homedir, platform } from "os";
229
+ import { join } from "path";
230
+ import { deletePassword, getPassword, setPassword } from "cross-keychain";
231
+ var debug4 = createGranolaDebug("lib:auth");
232
+ var SERVICE_NAME = "com.granola.cli";
233
+ var ACCOUNT_NAME = "credentials";
234
+ var DEFAULT_CLIENT_ID = "client_GranolaMac";
235
+ async function getCredentials() {
236
+ debug4("loading credentials from keychain");
237
+ try {
238
+ const stored = await getPassword(SERVICE_NAME, ACCOUNT_NAME);
239
+ if (!stored) {
240
+ debug4("no credentials found in keychain");
241
+ return null;
242
+ }
243
+ const parsed = JSON.parse(stored);
244
+ debug4("credentials loaded, hasAccessToken: %s", Boolean(parsed.accessToken));
245
+ return {
246
+ refreshToken: parsed.refreshToken,
247
+ accessToken: parsed.accessToken || "",
248
+ clientId: parsed.clientId
249
+ };
250
+ } catch (error) {
251
+ debug4("failed to get credentials: %O", error);
252
+ return null;
253
+ }
254
+ }
255
+ async function saveCredentials(creds) {
256
+ debug4("saving credentials to keychain");
257
+ await setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(creds));
258
+ debug4("credentials saved");
259
+ }
260
+ async function deleteCredentials() {
261
+ debug4("deleting credentials from keychain");
262
+ await deletePassword(SERVICE_NAME, ACCOUNT_NAME);
263
+ debug4("credentials deleted");
264
+ }
265
+ var WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
266
+ 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
+ }
273
+ 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
+ })
282
+ });
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
+ } catch (error) {
297
+ debug4("token refresh error: %O", error);
298
+ return null;
299
+ }
300
+ }
301
+ function parseSupabaseJson(json) {
302
+ debug4("parsing supabase.json");
303
+ try {
304
+ const parsed = JSON.parse(json);
305
+ if (parsed.workos_tokens && typeof parsed.workos_tokens === "string") {
306
+ const workosTokens = JSON.parse(parsed.workos_tokens);
307
+ if (workosTokens.access_token) {
308
+ debug4("found WorkOS tokens");
309
+ return {
310
+ refreshToken: workosTokens.refresh_token || "",
311
+ accessToken: workosTokens.access_token,
312
+ clientId: workosTokens.client_id || DEFAULT_CLIENT_ID
313
+ };
314
+ }
315
+ }
316
+ if (parsed.cognito_tokens && typeof parsed.cognito_tokens === "string") {
317
+ const cognitoTokens = JSON.parse(parsed.cognito_tokens);
318
+ if (!cognitoTokens.refresh_token) return null;
319
+ debug4("found Cognito tokens");
320
+ return {
321
+ refreshToken: cognitoTokens.refresh_token,
322
+ accessToken: cognitoTokens.access_token || "",
323
+ clientId: cognitoTokens.client_id || DEFAULT_CLIENT_ID
324
+ };
325
+ }
326
+ if (!parsed.refresh_token) return null;
327
+ debug4("found legacy token format");
328
+ return {
329
+ refreshToken: parsed.refresh_token,
330
+ accessToken: parsed.access_token || "",
331
+ clientId: parsed.client_id || DEFAULT_CLIENT_ID
332
+ };
333
+ } catch (error) {
334
+ debug4("failed to parse supabase.json: %O", error);
335
+ return null;
336
+ }
337
+ }
338
+ function getDefaultSupabasePath() {
339
+ const home = homedir();
340
+ const os2 = platform();
341
+ let path;
342
+ switch (os2) {
343
+ case "darwin":
344
+ path = join(home, "Library", "Application Support", "Granola", "supabase.json");
345
+ break;
346
+ case "win32":
347
+ path = join(
348
+ process.env.APPDATA || join(home, "AppData", "Roaming"),
349
+ "Granola",
350
+ "supabase.json"
351
+ );
352
+ break;
353
+ default:
354
+ path = join(home, ".config", "granola", "supabase.json");
355
+ }
356
+ debug4("platform: %s, supabase path: %s", os2, path);
357
+ return path;
358
+ }
359
+ async function loadCredentialsFromFile() {
360
+ const path = getDefaultSupabasePath();
361
+ debug4("loading credentials from file: %s", path);
362
+ try {
363
+ const content = await readFile(path, "utf-8");
364
+ debug4("file read successful, parsing content");
365
+ return parseSupabaseJson(content);
366
+ } catch (error) {
367
+ debug4("failed to load credentials from file: %O", error);
368
+ return null;
369
+ }
370
+ }
371
+
372
+ // src/commands/auth/login.ts
373
+ var debug5 = createGranolaDebug("cmd:auth:login");
374
+ function createLoginCommand() {
375
+ return new Command2("login").description("Import credentials from Granola desktop app").action(async () => {
376
+ debug5("login command invoked");
377
+ const creds = await loadCredentialsFromFile();
378
+ if (!creds) {
379
+ const path = getDefaultSupabasePath();
380
+ debug5("login failed: could not load credentials from %s", path);
381
+ console.error(chalk3.red("Error:"), "Could not load credentials.");
382
+ console.error(`Expected file at: ${chalk3.dim(path)}`);
383
+ console.error("\nMake sure the Granola desktop app is installed and you are logged in.");
384
+ process.exit(1);
385
+ }
386
+ debug5("credentials loaded, saving to keychain");
387
+ await saveCredentials(creds);
388
+ debug5("login successful");
389
+ console.log(chalk3.green("Credentials imported successfully"));
390
+ });
391
+ }
392
+ var loginCommand = createLoginCommand();
393
+
394
+ // src/commands/auth/logout.ts
395
+ import chalk4 from "chalk";
396
+ import { Command as Command3 } from "commander";
397
+ var debug6 = createGranolaDebug("cmd:auth:logout");
398
+ function createLogoutCommand() {
399
+ return new Command3("logout").description("Logout from Granola").action(async () => {
400
+ debug6("logout command invoked");
401
+ try {
402
+ await deleteCredentials();
403
+ debug6("logout successful");
404
+ console.log(chalk4.green("Logged out successfully"));
405
+ } catch (error) {
406
+ debug6("logout failed: %O", error);
407
+ console.error(chalk4.red("Error:"), "Failed to logout.");
408
+ if (error instanceof Error) {
409
+ console.error(chalk4.dim(error.message));
410
+ }
411
+ process.exit(1);
412
+ }
413
+ });
414
+ }
415
+ var logoutCommand = createLogoutCommand();
416
+
417
+ // src/commands/auth/status.ts
418
+ import chalk5 from "chalk";
419
+ import { Command as Command4 } from "commander";
420
+ var debug7 = createGranolaDebug("cmd:auth:status");
421
+ function createStatusCommand() {
422
+ 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");
424
+ const creds = await getCredentials();
425
+ debug7("authenticated: %s", !!creds);
426
+ const format = opts.output || null;
427
+ if (format) {
428
+ if (!["json", "yaml", "toon"].includes(format)) {
429
+ console.error(chalk5.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
430
+ process.exit(1);
431
+ }
432
+ console.log(formatOutput({ authenticated: !!creds }, format));
433
+ return;
434
+ }
435
+ if (creds) {
436
+ console.log(chalk5.green("Authenticated"));
437
+ } else {
438
+ console.log(chalk5.yellow("Not authenticated"));
439
+ console.log(chalk5.dim("Run: granola auth login"));
440
+ }
441
+ });
442
+ }
443
+ var statusCommand = createStatusCommand();
444
+
445
+ // src/commands/auth/index.ts
446
+ var authCommand = new Command5("auth").description("Manage authentication").addCommand(loginCommand).addCommand(logoutCommand).addCommand(statusCommand);
447
+
448
+ // src/commands/config.ts
449
+ import chalk6 from "chalk";
450
+ import { Command as Command6 } from "commander";
451
+ var debug8 = createGranolaDebug("cmd:config");
452
+ var CONFIG_VALUE_PARSERS = {
453
+ default_workspace: (value) => value,
454
+ pager: (value) => value,
455
+ aliases: (value) => {
456
+ try {
457
+ const parsed = JSON.parse(value);
458
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
459
+ throw new Error('Aliases must be a JSON object of { "name": "command" } pairs.');
460
+ }
461
+ for (const [alias, command] of Object.entries(parsed)) {
462
+ if (typeof command !== "string") {
463
+ throw new Error(`Alias "${alias}" must map to a string command.`);
464
+ }
465
+ }
466
+ return parsed;
467
+ } catch (error) {
468
+ if (error instanceof SyntaxError) {
469
+ throw new Error('Aliases must be valid JSON (example: {"meetings":"meeting list"}).');
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ };
475
+ var CONFIG_KEYS = Object.keys(CONFIG_VALUE_PARSERS);
476
+ function isConfigKey(key) {
477
+ return CONFIG_KEYS.includes(key);
478
+ }
479
+ function createConfigCommand() {
480
+ const cmd = new Command6("config").description("Manage CLI configuration");
481
+ cmd.command("list").description("View current config").option("-o, --output <format>", "Output format (json, yaml, toon)").action((opts) => {
482
+ debug8("config list command invoked");
483
+ const config2 = getConfig();
484
+ const format = opts.output || null;
485
+ if (format) {
486
+ if (!["json", "yaml", "toon"].includes(format)) {
487
+ console.error(chalk6.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
488
+ process.exit(1);
489
+ }
490
+ console.log(formatOutput(config2, format));
491
+ return;
492
+ }
493
+ if (Object.keys(config2).length === 0) {
494
+ console.log(chalk6.dim("No configuration set."));
495
+ return;
496
+ }
497
+ for (const [key, value] of Object.entries(config2)) {
498
+ if (typeof value === "object") {
499
+ console.log(`${chalk6.bold(key)}:`);
500
+ for (const [k, v] of Object.entries(value)) {
501
+ console.log(` ${k}: ${v}`);
502
+ }
503
+ } else {
504
+ console.log(`${chalk6.bold(key)}: ${value}`);
505
+ }
506
+ }
507
+ });
508
+ 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);
510
+ const value = getConfigValue(key);
511
+ const format = opts.output || null;
512
+ if (format) {
513
+ if (!["json", "yaml", "toon"].includes(format)) {
514
+ console.error(chalk6.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
515
+ process.exit(1);
516
+ }
517
+ console.log(formatOutput({ [key]: value }, format));
518
+ return;
519
+ }
520
+ if (value === void 0) {
521
+ console.log(chalk6.dim("(not set)"));
522
+ } else {
523
+ console.log(value);
524
+ }
525
+ });
526
+ cmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
527
+ debug8("config set command invoked: %s = %s", key, value);
528
+ if (!isConfigKey(key)) {
529
+ console.error(
530
+ chalk6.red(
531
+ `Invalid config key: ${key}. Allowed keys: ${CONFIG_KEYS.map((k) => `'${k}'`).join(", ")}.`
532
+ )
533
+ );
534
+ process.exit(1);
535
+ }
536
+ const parser = CONFIG_VALUE_PARSERS[key];
537
+ let parsedValue;
538
+ try {
539
+ parsedValue = parser(value);
540
+ } catch (error) {
541
+ console.error(chalk6.red("Invalid value for config key:"), key);
542
+ if (error instanceof Error) {
543
+ console.error(chalk6.dim(error.message));
544
+ }
545
+ process.exit(1);
546
+ }
547
+ setConfigValue(key, parsedValue);
548
+ console.log(chalk6.green(`Set ${key} = ${value}`));
549
+ });
550
+ cmd.command("reset").description("Reset to defaults").action(() => {
551
+ debug8("config reset command invoked");
552
+ resetConfig();
553
+ console.log(chalk6.green("Configuration reset"));
554
+ });
555
+ return cmd;
556
+ }
557
+ var configCommand = createConfigCommand();
558
+
559
+ // src/commands/folder/index.ts
560
+ import { Command as Command9 } from "commander";
561
+
562
+ // src/commands/folder/list.ts
563
+ import chalk8 from "chalk";
564
+ import { Command as Command7 } from "commander";
565
+
566
+ // src/services/client.ts
567
+ import chalk7 from "chalk";
568
+
569
+ // src/lib/api.ts
570
+ function createApiClient(httpClient) {
571
+ async function getDocuments(options = {}) {
572
+ const body = {
573
+ include_last_viewed_panel: options.include_last_viewed_panel ?? false
574
+ };
575
+ if (options.workspace_id) body.workspace_id = options.workspace_id;
576
+ if (options.limit !== void 0) body.limit = options.limit;
577
+ if (options.offset !== void 0) body.offset = options.offset;
578
+ if (options.cursor) body.cursor = options.cursor;
579
+ return httpClient.post("/v2/get-documents", body);
580
+ }
581
+ async function getDocumentsBatch(options) {
582
+ return httpClient.post("/v1/get-documents-batch", {
583
+ document_ids: options.document_ids,
584
+ include_last_viewed_panel: options.include_last_viewed_panel ?? false
585
+ });
586
+ }
587
+ async function getDocumentMetadata(documentId) {
588
+ return httpClient.post("/v1/get-document-metadata", {
589
+ document_id: documentId
590
+ });
591
+ }
592
+ async function getDocumentTranscript(documentId) {
593
+ return httpClient.post("/v1/get-document-transcript", {
594
+ document_id: documentId
595
+ });
596
+ }
597
+ async function getDocumentLists() {
598
+ return httpClient.post("/v2/get-document-lists", {});
599
+ }
600
+ async function getDocumentList(folderId) {
601
+ const folders = await getDocumentLists();
602
+ return folders.find((f) => f.id === folderId) || null;
603
+ }
604
+ async function getWorkspaces() {
605
+ return httpClient.post("/v1/get-workspaces", {});
606
+ }
607
+ function setToken(token) {
608
+ httpClient.setToken(token);
609
+ }
610
+ return {
611
+ getDocuments,
612
+ getDocumentsBatch,
613
+ getDocumentMetadata,
614
+ getDocumentTranscript,
615
+ getDocumentLists,
616
+ getDocumentList,
617
+ getWorkspaces,
618
+ setToken
619
+ };
620
+ }
621
+
622
+ // src/lib/http.ts
623
+ import { readFileSync } from "fs";
624
+ import os from "os";
625
+ import process2 from "process";
626
+ function getPackageVersion() {
627
+ for (const path of ["../package.json", "../../package.json"]) {
628
+ try {
629
+ const pkg = JSON.parse(readFileSync(new URL(path, import.meta.url), "utf-8"));
630
+ return pkg.version;
631
+ } catch {
632
+ }
633
+ }
634
+ return "0.0.0";
635
+ }
636
+ var version = getPackageVersion();
637
+ var BASE_URL = "https://api.granola.ai";
638
+ var APP_VERSION = "7.0.0";
639
+ function buildUserAgent() {
640
+ const platform2 = process2.platform === "darwin" ? "macOS" : process2.platform;
641
+ const osRelease = os.release();
642
+ return `Granola/${APP_VERSION} granola-cli/${version} (${platform2} ${osRelease})`;
643
+ }
644
+ function getClientHeaders() {
645
+ return {
646
+ "X-App-Version": APP_VERSION,
647
+ "X-Client-Version": APP_VERSION,
648
+ "X-Client-Type": "cli",
649
+ "X-Client-Platform": process2.platform,
650
+ "X-Client-Architecture": process2.arch,
651
+ "X-Client-Id": `granola-cli-${version}`,
652
+ "User-Agent": buildUserAgent()
653
+ };
654
+ }
655
+ var RETRY_CONFIG = {
656
+ maxRetries: 3,
657
+ baseDelay: 250,
658
+ retryableStatuses: [429, 500, 502, 503, 504]
659
+ };
660
+ var ApiError = class extends Error {
661
+ constructor(message, status, body) {
662
+ super(message);
663
+ this.status = status;
664
+ this.body = body;
665
+ this.name = "ApiError";
666
+ }
667
+ };
668
+ function isRetryable(status) {
669
+ return RETRY_CONFIG.retryableStatuses.includes(status);
670
+ }
671
+ async function sleep(ms) {
672
+ return new Promise((resolve) => setTimeout(resolve, ms));
673
+ }
674
+ function createHttpClient(token) {
675
+ let currentToken = token;
676
+ async function post(endpoint, body = {}) {
677
+ let lastError = null;
678
+ const maxAttempts = RETRY_CONFIG.maxRetries + 1;
679
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
680
+ try {
681
+ const response = await fetch(`${BASE_URL}${endpoint}`, {
682
+ method: "POST",
683
+ headers: {
684
+ Authorization: `Bearer ${currentToken}`,
685
+ "Content-Type": "application/json",
686
+ ...getClientHeaders()
687
+ },
688
+ body: JSON.stringify(body)
689
+ });
690
+ if (!response.ok) {
691
+ const responseBody = await response.json().catch(() => ({}));
692
+ if (isRetryable(response.status) && attempt < maxAttempts - 1) {
693
+ const delay = RETRY_CONFIG.baseDelay * 2 ** attempt;
694
+ await sleep(delay);
695
+ continue;
696
+ }
697
+ throw new ApiError(
698
+ `HTTP ${response.status}: ${response.statusText}`,
699
+ response.status,
700
+ responseBody
701
+ );
702
+ }
703
+ return await response.json();
704
+ } catch (error) {
705
+ if (error instanceof ApiError) {
706
+ throw error;
707
+ }
708
+ lastError = error;
709
+ if (attempt < maxAttempts - 1) {
710
+ const delay = RETRY_CONFIG.baseDelay * 2 ** attempt;
711
+ await sleep(delay);
712
+ }
713
+ }
714
+ }
715
+ throw lastError;
716
+ }
717
+ function setToken(newToken) {
718
+ currentToken = newToken;
719
+ }
720
+ return { post, setToken };
721
+ }
722
+
723
+ // src/services/client.ts
724
+ var debug9 = createGranolaDebug("service:client");
725
+ var client = null;
726
+ async function getClient() {
727
+ debug9("getClient called, cached: %s", client ? "yes" : "no");
728
+ if (client) return client;
729
+ debug9("fetching credentials");
730
+ const creds = await getCredentials();
731
+ if (!creds) {
732
+ debug9("no credentials found, exiting");
733
+ console.error(chalk7.red("Error:"), "Not authenticated.");
734
+ console.error(`Run ${chalk7.cyan("granola auth login")} to authenticate.`);
735
+ process.exit(2);
736
+ }
737
+ debug9("creating API client, token: %s", maskToken(creds.accessToken));
738
+ const httpClient = createHttpClient(creds.accessToken);
739
+ client = createApiClient(httpClient);
740
+ return client;
741
+ }
742
+ function resetClient() {
743
+ debug9("client reset");
744
+ client = null;
745
+ }
746
+ function isUnauthorizedError(error) {
747
+ if (error && typeof error === "object") {
748
+ const e = error;
749
+ return e.status === 401;
750
+ }
751
+ return false;
752
+ }
753
+ async function withTokenRefresh(operation) {
754
+ try {
755
+ return await operation();
756
+ } catch (error) {
757
+ if (isUnauthorizedError(error)) {
758
+ debug9("401 detected, attempting token refresh");
759
+ const newCreds = await refreshAccessToken();
760
+ if (!newCreds) {
761
+ debug9("token refresh failed, re-throwing original error");
762
+ throw error;
763
+ }
764
+ resetClient();
765
+ debug9("retrying operation with refreshed token");
766
+ return operation();
767
+ }
768
+ throw error;
769
+ }
770
+ }
771
+
772
+ // src/services/folders.ts
773
+ var debug10 = createGranolaDebug("service:folders");
774
+ function normalizeFolder(folder) {
775
+ const documentIdsFromDocs = Array.isArray(folder.documents) ? folder.documents.map((doc) => doc?.id).filter((id) => Boolean(id)) : void 0;
776
+ const documentIds = Array.isArray(folder.document_ids) && folder.document_ids.length > 0 ? folder.document_ids : documentIdsFromDocs;
777
+ return {
778
+ id: folder.id,
779
+ name: folder.name ?? folder.title,
780
+ title: folder.title ?? folder.name ?? "Untitled",
781
+ created_at: folder.created_at,
782
+ workspace_id: folder.workspace_id,
783
+ owner_id: folder.owner_id,
784
+ document_ids: documentIds ?? [],
785
+ is_favourite: folder.is_favourite
786
+ };
787
+ }
788
+ async function list(opts = {}) {
789
+ return withTokenRefresh(async () => {
790
+ const client2 = await getClient();
791
+ const documentLists = await client2.getDocumentLists();
792
+ const folders = documentLists.map(normalizeFolder);
793
+ debug10("list fetched %d folders", folders.length);
794
+ if (opts.workspace) {
795
+ const filtered = folders.filter((folder) => folder.workspace_id === opts.workspace);
796
+ debug10("filtered to %d folders for workspace %s", filtered.length, opts.workspace);
797
+ return filtered;
798
+ }
799
+ return folders;
800
+ });
801
+ }
802
+ async function get(id) {
803
+ return withTokenRefresh(async () => {
804
+ debug10("get called for folder: %s", id);
805
+ const client2 = await getClient();
806
+ const documentLists = await client2.getDocumentLists();
807
+ const folder = documentLists.find((f) => f.id === id);
808
+ if (!folder) {
809
+ debug10("folder %s not found", id);
810
+ return null;
811
+ }
812
+ debug10("folder %s found", id);
813
+ return normalizeFolder(folder);
814
+ });
815
+ }
816
+
817
+ // src/commands/folder/list.ts
818
+ var debug11 = createGranolaDebug("cmd:folder:list");
819
+ function createListCommand() {
820
+ 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);
822
+ let data;
823
+ try {
824
+ data = await list({
825
+ workspace: opts.workspace
826
+ });
827
+ debug11("fetched %d folders", data.length);
828
+ } catch (error) {
829
+ console.error(chalk8.red("Error:"), "Failed to list folders.");
830
+ if (error instanceof Error) {
831
+ console.error(chalk8.dim(error.message));
832
+ }
833
+ process.exit(1);
834
+ }
835
+ const format = opts.output || null;
836
+ if (format) {
837
+ if (!["json", "yaml", "toon"].includes(format)) {
838
+ console.error(chalk8.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
839
+ process.exit(1);
840
+ }
841
+ console.log(formatOutput(data, format));
842
+ return;
843
+ }
844
+ if (data.length === 0) {
845
+ console.log(chalk8.dim("No folders found."));
846
+ return;
847
+ }
848
+ const rows = data.map((folder) => ({
849
+ ...folder,
850
+ display_name: folder.name || folder.title || "Unnamed"
851
+ }));
852
+ const output = table(rows, [
853
+ { key: "id", header: "ID", width: 12, format: (v) => String(v).slice(0, 8) },
854
+ { key: "display_name", header: "NAME", width: 20, format: (v) => String(v || "") },
855
+ {
856
+ key: "workspace_id",
857
+ header: "WORKSPACE",
858
+ width: 12,
859
+ format: (v) => String(v).slice(0, 8)
860
+ }
861
+ ]);
862
+ console.log(output);
863
+ });
864
+ }
865
+ var listCommand = createListCommand();
866
+
867
+ // src/commands/folder/view.ts
868
+ import chalk9 from "chalk";
869
+ import { Command as Command8 } from "commander";
870
+ var debug12 = createGranolaDebug("cmd:folder:view");
871
+ function createViewCommand() {
872
+ 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);
874
+ let folder;
875
+ try {
876
+ folder = await get(id);
877
+ } catch (error) {
878
+ console.error(chalk9.red("Error:"), "Failed to load folder.");
879
+ if (error instanceof Error) {
880
+ console.error(chalk9.dim(error.message));
881
+ }
882
+ process.exit(1);
883
+ }
884
+ if (!folder) {
885
+ console.error(chalk9.red(`Folder ${id} not found`));
886
+ process.exit(4);
887
+ }
888
+ const format = opts.output || null;
889
+ if (format) {
890
+ if (!["json", "yaml", "toon"].includes(format)) {
891
+ console.error(chalk9.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
892
+ process.exit(1);
893
+ }
894
+ console.log(formatOutput(folder, format));
895
+ return;
896
+ }
897
+ const name = folder.name || folder.title || "Unnamed";
898
+ const docCount = folder.document_ids?.length || 0;
899
+ console.log(chalk9.bold(name));
900
+ console.log(chalk9.dim(`${docCount} meetings \xB7 Workspace ${folder.workspace_id}`));
901
+ console.log();
902
+ console.log(chalk9.dim('Tip: Use "granola meeting list" to browse recent meetings.'));
903
+ });
904
+ }
905
+ var viewCommand = createViewCommand();
906
+
907
+ // src/commands/folder/index.ts
908
+ var folderCommand = new Command9("folder").description("Work with folders").addCommand(listCommand).addCommand(viewCommand);
909
+
910
+ // src/commands/meeting/index.ts
911
+ import { Command as Command16 } from "commander";
912
+
913
+ // src/commands/meeting/enhanced.ts
914
+ import chalk10 from "chalk";
915
+ import { Command as Command10 } from "commander";
916
+
917
+ // src/lib/pager.ts
918
+ import { spawn } from "child_process";
919
+ var debug13 = createGranolaDebug("lib:pager");
920
+ var ALLOWED_PAGERS = ["less", "more", "cat", "head", "tail", "bat", "most"];
921
+ var SHELL_METACHARACTERS = /[;&|`$(){}[\]<>\\!#*?]/;
922
+ function validatePagerCommand(cmd) {
923
+ debug13("validating pager command: %s", cmd);
924
+ if (SHELL_METACHARACTERS.test(cmd)) {
925
+ debug13("pager validation failed: contains shell metacharacters");
926
+ return false;
927
+ }
928
+ const [binary] = cmd.split(" ");
929
+ const binaryName = binary.split("/").pop() || "";
930
+ const valid = ALLOWED_PAGERS.includes(binaryName);
931
+ debug13("pager validation: %s (binary: %s)", valid ? "passed" : "failed", binaryName);
932
+ return valid;
933
+ }
934
+ function getPagerCommand() {
935
+ if (process.env.GRANOLA_PAGER) {
936
+ debug13("pager command: %s (source: GRANOLA_PAGER)", process.env.GRANOLA_PAGER);
937
+ return process.env.GRANOLA_PAGER;
938
+ }
939
+ if (process.env.PAGER) {
940
+ debug13("pager command: %s (source: PAGER)", process.env.PAGER);
941
+ return process.env.PAGER;
942
+ }
943
+ const configuredPager = getConfigValue("pager");
944
+ if (configuredPager) {
945
+ debug13("pager command: %s (source: config)", configuredPager);
946
+ return configuredPager;
947
+ }
948
+ debug13("pager command: less -R (source: default)");
949
+ return "less -R";
950
+ }
951
+ async function pipeToPager(content) {
952
+ debug13("pipeToPager: isTTY=%s, contentLength=%d", process.stdout.isTTY, content.length);
953
+ if (!process.stdout.isTTY) {
954
+ debug13("not a TTY, writing directly to stdout");
955
+ process.stdout.write(`${content}
956
+ `);
957
+ return;
958
+ }
959
+ const pagerCmd = getPagerCommand();
960
+ if (!validatePagerCommand(pagerCmd)) {
961
+ console.error(`Warning: Invalid pager command "${pagerCmd}". Falling back to direct output.`);
962
+ process.stdout.write(`${content}
963
+ `);
964
+ return;
965
+ }
966
+ const [cmd, ...args] = pagerCmd.split(" ");
967
+ debug13("spawning pager: %s with args: %O", cmd, args);
968
+ return new Promise((resolve) => {
969
+ let settled = false;
970
+ const finish = () => {
971
+ if (!settled) {
972
+ settled = true;
973
+ resolve();
974
+ }
975
+ };
976
+ const fallbackToStdout = (reason) => {
977
+ if (settled) return;
978
+ settled = true;
979
+ debug13("falling back to stdout: %s", reason);
980
+ console.error(
981
+ `Warning: Unable to launch pager "${pagerCmd}" (${reason}). Falling back to direct output.`
982
+ );
983
+ process.stdout.write(`${content}
984
+ `);
985
+ resolve();
986
+ };
987
+ try {
988
+ const pager = spawn(cmd, args, {
989
+ stdio: ["pipe", "inherit", "inherit"]
990
+ });
991
+ pager.stdin.write(content);
992
+ pager.stdin.end();
993
+ pager.on("close", () => {
994
+ debug13("pager closed");
995
+ finish();
996
+ });
997
+ pager.on("error", (err) => {
998
+ debug13("pager error: %O", err);
999
+ fallbackToStdout(err.message);
1000
+ });
1001
+ } catch (err) {
1002
+ debug13("failed to spawn pager: %O", err);
1003
+ fallbackToStdout(err.message);
1004
+ }
1005
+ });
1006
+ }
1007
+
1008
+ // src/lib/prosemirror.ts
1009
+ var debug14 = createGranolaDebug("lib:prosemirror");
1010
+ function toMarkdown(doc) {
1011
+ debug14("toMarkdown called with doc: %O", doc);
1012
+ if (!doc?.content) {
1013
+ debug14("No content in doc, returning empty string");
1014
+ return "";
1015
+ }
1016
+ const result = doc.content.map((n) => nodeToMd(n)).join("\n\n");
1017
+ debug14("toMarkdown result: %s", result);
1018
+ return result;
1019
+ }
1020
+ function nodeToMd(node) {
1021
+ debug14("nodeToMd processing node type: %s, node: %O", node.type, node);
1022
+ let result;
1023
+ switch (node.type) {
1024
+ case "heading": {
1025
+ const lvl = node.attrs?.level || 1;
1026
+ result = `${"#".repeat(lvl)} ${inlineToMd(node.content)}`;
1027
+ break;
1028
+ }
1029
+ case "paragraph":
1030
+ result = inlineToMd(node.content);
1031
+ break;
1032
+ case "bulletList":
1033
+ result = (node.content || []).map((li) => nodeToMd(li)).join("\n");
1034
+ break;
1035
+ case "orderedList":
1036
+ result = (node.content || []).map((li, i) => nodeToMd(li).replace(/^- /, `${i + 1}. `)).join("\n");
1037
+ break;
1038
+ case "listItem":
1039
+ result = `- ${(node.content || []).map((c) => nodeToMd(c)).join("\n ")}`;
1040
+ break;
1041
+ case "blockquote":
1042
+ result = (node.content || []).map((c) => `> ${nodeToMd(c)}`).join("\n");
1043
+ break;
1044
+ case "codeBlock": {
1045
+ const lang = node.attrs?.language || "";
1046
+ result = `\`\`\`${lang}
1047
+ ${inlineToMd(node.content)}
1048
+ \`\`\``;
1049
+ break;
1050
+ }
1051
+ case "horizontalRule":
1052
+ result = "---";
1053
+ break;
1054
+ case "text":
1055
+ result = applyMarks(node.text || "", node.marks);
1056
+ break;
1057
+ default:
1058
+ debug14("Unknown node type: %s", node.type);
1059
+ result = node.content ? node.content.map((c) => nodeToMd(c)).join("") : "";
1060
+ }
1061
+ debug14("nodeToMd result for %s: %s", node.type, result);
1062
+ return result;
1063
+ }
1064
+ function inlineToMd(content) {
1065
+ return content ? content.map((n) => nodeToMd(n)).join("") : "";
1066
+ }
1067
+ function applyMarks(text, marks) {
1068
+ if (!marks) return text;
1069
+ for (const m of marks) {
1070
+ if (m.type === "bold" || m.type === "strong") text = `**${text}**`;
1071
+ if (m.type === "italic" || m.type === "em") text = `*${text}*`;
1072
+ if (m.type === "code") text = `\`${text}\``;
1073
+ if (m.type === "strike") text = `~~${text}~~`;
1074
+ }
1075
+ return text;
1076
+ }
1077
+
1078
+ // src/services/meetings.ts
1079
+ var debug15 = createGranolaDebug("service:meetings");
1080
+ async function getFolderDocumentIds(client2, folderId) {
1081
+ debug15("fetching folder %s via getDocumentList", folderId);
1082
+ const folder = await client2.getDocumentList(folderId);
1083
+ if (!folder) {
1084
+ debug15("folder %s not found", folderId);
1085
+ return [];
1086
+ }
1087
+ const ids = folder.document_ids || folder.documents?.map((doc) => doc.id) || [];
1088
+ debug15("folder %s returned %d document ids", folderId, ids.length);
1089
+ return ids;
1090
+ }
1091
+ var DOCUMENT_BATCH_SIZE = 100;
1092
+ var NOTES_PAGE_SIZE = 50;
1093
+ var MAX_NOTES_PAGES = 100;
1094
+ async function fetchMeetingsByIds(client2, documentIds) {
1095
+ if (documentIds.length === 0) return [];
1096
+ const meetings = [];
1097
+ for (let i = 0; i < documentIds.length; i += DOCUMENT_BATCH_SIZE) {
1098
+ const chunk = documentIds.slice(i, i + DOCUMENT_BATCH_SIZE);
1099
+ const res = await client2.getDocumentsBatch({
1100
+ document_ids: chunk,
1101
+ include_last_viewed_panel: false
1102
+ });
1103
+ const docs = res?.documents || res?.docs || [];
1104
+ meetings.push(...docs);
1105
+ }
1106
+ debug15("fetched %d meetings via getDocumentsBatch", meetings.length);
1107
+ return meetings;
1108
+ }
1109
+ async function loadMeetingMetadata(client2, id) {
1110
+ try {
1111
+ const metadata = await client2.getDocumentMetadata(id);
1112
+ if (!metadata) {
1113
+ debug15("getDocumentMetadata returned null for %s", id);
1114
+ return null;
1115
+ }
1116
+ return metadata;
1117
+ } catch (err) {
1118
+ debug15("getDocumentMetadata failed for %s: %O", id, err);
1119
+ return null;
1120
+ }
1121
+ }
1122
+ async function fetchFolderMeetings(client2, folderId) {
1123
+ const ids = await getFolderDocumentIds(client2, folderId);
1124
+ if (ids.length === 0) {
1125
+ debug15("folder %s has no documents", folderId);
1126
+ return [];
1127
+ }
1128
+ return fetchMeetingsByIds(client2, ids);
1129
+ }
1130
+ async function list2(opts = {}) {
1131
+ return withTokenRefresh(async () => {
1132
+ debug15("list called with opts: %O", opts);
1133
+ const client2 = await getClient();
1134
+ const { limit = 20, offset = 0, workspace, folder } = opts;
1135
+ if (folder) {
1136
+ debug15("listing meetings for folder: %s", folder);
1137
+ const folderMeetings = await fetchFolderMeetings(client2, folder);
1138
+ debug15("fetched %d meetings for folder %s", folderMeetings.length, folder);
1139
+ let filtered = folderMeetings;
1140
+ if (workspace) {
1141
+ filtered = folderMeetings.filter((m) => m.workspace_id === workspace);
1142
+ debug15(
1143
+ "workspace filter applied for folder %s: %d meetings remain",
1144
+ folder,
1145
+ filtered.length
1146
+ );
1147
+ }
1148
+ const paginated = filtered.slice(offset, offset + limit);
1149
+ debug15("returning %d meetings from folder %s after pagination", paginated.length, folder);
1150
+ return paginated;
1151
+ }
1152
+ const res = await client2.getDocuments({
1153
+ limit,
1154
+ offset,
1155
+ include_last_viewed_panel: false
1156
+ });
1157
+ let meetings = res?.docs || [];
1158
+ debug15("fetched %d meetings", meetings.length);
1159
+ if (workspace) {
1160
+ meetings = meetings.filter((m) => m.workspace_id === workspace);
1161
+ debug15("filtered to %d meetings for workspace: %s", meetings.length, workspace);
1162
+ }
1163
+ return meetings;
1164
+ });
1165
+ }
1166
+ var RESOLVE_PAGE_SIZE = 100;
1167
+ var MAX_RESOLVE_PAGES = 100;
1168
+ async function resolveId(partialId) {
1169
+ return withTokenRefresh(async () => {
1170
+ debug15("resolving meeting id: %s", partialId);
1171
+ 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
+ }
1194
+ }
1195
+ }
1196
+ if (meetings.length < RESOLVE_PAGE_SIZE) {
1197
+ break;
1198
+ }
1199
+ offset += RESOLVE_PAGE_SIZE;
1200
+ }
1201
+ if (matches.size === 0) {
1202
+ debug15("no meeting found for id: %s", partialId);
1203
+ return null;
1204
+ }
1205
+ const match = matches.values().next().value;
1206
+ debug15("resolved meeting: %s -> %s", partialId, match);
1207
+ return match;
1208
+ });
1209
+ }
1210
+ async function get2(id) {
1211
+ return withTokenRefresh(async () => {
1212
+ debug15("getting meeting: %s", id);
1213
+ const client2 = await getClient();
1214
+ const metadata = await loadMeetingMetadata(client2, id);
1215
+ if (!metadata) {
1216
+ debug15("meeting %s: not found", id);
1217
+ return null;
1218
+ }
1219
+ debug15("meeting %s: found", id);
1220
+ return { id, ...metadata };
1221
+ });
1222
+ }
1223
+ async function findMeetingViaDocuments(client2, id, { includeLastViewedPanel }) {
1224
+ let offset = 0;
1225
+ for (let page = 0; page < MAX_NOTES_PAGES; page += 1) {
1226
+ try {
1227
+ debug15("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
1228
+ const res = await client2.getDocuments({
1229
+ limit: NOTES_PAGE_SIZE,
1230
+ offset,
1231
+ include_last_viewed_panel: includeLastViewedPanel
1232
+ });
1233
+ const meetings = res?.docs || [];
1234
+ debug15("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
1235
+ if (meetings.length === 0) break;
1236
+ const meeting = meetings.find((m) => m.id === id);
1237
+ if (meeting) {
1238
+ debug15("findMeetingViaDocuments located meeting %s on page %d", id, page);
1239
+ return meeting;
1240
+ }
1241
+ offset += NOTES_PAGE_SIZE;
1242
+ } catch (err) {
1243
+ debug15("findMeetingViaDocuments error: %O", err);
1244
+ return null;
1245
+ }
1246
+ }
1247
+ debug15("findMeetingViaDocuments did not locate meeting %s", id);
1248
+ return null;
1249
+ }
1250
+ async function getNotes(id) {
1251
+ return withTokenRefresh(async () => {
1252
+ debug15("getNotes called with id: %s", id);
1253
+ const client2 = await getClient();
1254
+ const metadata = await loadMeetingMetadata(client2, id);
1255
+ if (metadata && "notes" in metadata) {
1256
+ debug15("getNotes resolved via metadata response");
1257
+ return metadata.notes || null;
1258
+ }
1259
+ const meeting = await findMeetingViaDocuments(client2, id, {
1260
+ includeLastViewedPanel: false
1261
+ });
1262
+ if (meeting) {
1263
+ return meeting.notes || null;
1264
+ }
1265
+ return null;
1266
+ });
1267
+ }
1268
+ async function getEnhancedNotes(id) {
1269
+ return withTokenRefresh(async () => {
1270
+ debug15("getEnhancedNotes called with id: %s", id);
1271
+ const client2 = await getClient();
1272
+ const metadata = await loadMeetingMetadata(client2, id);
1273
+ if (metadata && "last_viewed_panel" in metadata) {
1274
+ debug15("getEnhancedNotes resolved via metadata response");
1275
+ return metadata.last_viewed_panel?.content || null;
1276
+ }
1277
+ const meeting = await findMeetingViaDocuments(client2, id, {
1278
+ includeLastViewedPanel: true
1279
+ });
1280
+ if (meeting) {
1281
+ return meeting.last_viewed_panel?.content || null;
1282
+ }
1283
+ return null;
1284
+ });
1285
+ }
1286
+ async function getTranscript(id) {
1287
+ return withTokenRefresh(async () => {
1288
+ debug15("getTranscript called with id: %s", id);
1289
+ const client2 = await getClient();
1290
+ try {
1291
+ const transcript = await client2.getDocumentTranscript(id);
1292
+ debug15("getTranscript got %d utterances", transcript.length);
1293
+ return transcript;
1294
+ } catch (err) {
1295
+ debug15("getTranscript error: %O", err);
1296
+ return [];
1297
+ }
1298
+ });
1299
+ }
1300
+
1301
+ // src/commands/meeting/enhanced.ts
1302
+ var debug16 = createGranolaDebug("cmd:meeting:enhanced");
1303
+ function createEnhancedCommand() {
1304
+ 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);
1306
+ const global = cmd.optsWithGlobals();
1307
+ let fullId;
1308
+ try {
1309
+ const resolved = await resolveId(id);
1310
+ if (!resolved) {
1311
+ console.error(chalk10.red(`Meeting ${id} not found`));
1312
+ process.exit(4);
1313
+ }
1314
+ fullId = resolved;
1315
+ } catch (err) {
1316
+ console.error(chalk10.red(err.message));
1317
+ process.exit(1);
1318
+ }
1319
+ let notes;
1320
+ try {
1321
+ notes = await getEnhancedNotes(fullId);
1322
+ } catch (error) {
1323
+ debug16("failed to load enhanced notes: %O", error);
1324
+ console.error(chalk10.red("Error:"), "Failed to fetch enhanced notes.");
1325
+ if (error instanceof Error) {
1326
+ console.error(chalk10.dim(error.message));
1327
+ }
1328
+ process.exit(1);
1329
+ }
1330
+ if (!notes) {
1331
+ console.error(chalk10.red(`No enhanced notes found for meeting ${id}`));
1332
+ process.exit(4);
1333
+ }
1334
+ const format = opts.output || "markdown";
1335
+ const structuredFormats = ["json", "yaml", "toon"];
1336
+ if (format !== "markdown" && !structuredFormats.includes(format)) {
1337
+ console.error(
1338
+ chalk10.red(`Invalid format: ${format}. Use 'markdown', 'json', 'yaml', or 'toon'.`)
1339
+ );
1340
+ process.exit(1);
1341
+ }
1342
+ if (structuredFormats.includes(format)) {
1343
+ console.log(formatOutput(notes, format));
1344
+ return;
1345
+ }
1346
+ const md = toMarkdown(notes);
1347
+ if (global.noPager || !process.stdout.isTTY) {
1348
+ console.log(md);
1349
+ } else {
1350
+ await pipeToPager(md);
1351
+ }
1352
+ });
1353
+ }
1354
+ var enhancedCommand = createEnhancedCommand();
1355
+
1356
+ // src/commands/meeting/export.ts
1357
+ import chalk11 from "chalk";
1358
+ import { Command as Command11 } from "commander";
1359
+
1360
+ // src/lib/toon.ts
1361
+ import { encode } from "@toon-format/toon";
1362
+ function toToon(data) {
1363
+ return encode(data);
1364
+ }
1365
+
1366
+ // src/commands/meeting/export.ts
1367
+ var debug17 = createGranolaDebug("cmd:meeting:export");
1368
+ function createExportCommand() {
1369
+ 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);
1371
+ const format = options.format;
1372
+ if (format !== "json" && format !== "toon") {
1373
+ console.error(chalk11.red(`Invalid format: ${options.format}. Use 'json' or 'toon'.`));
1374
+ process.exit(1);
1375
+ }
1376
+ let fullId;
1377
+ try {
1378
+ const resolved = await resolveId(id);
1379
+ if (!resolved) {
1380
+ console.error(chalk11.red(`Meeting ${id} not found`));
1381
+ process.exit(4);
1382
+ }
1383
+ fullId = resolved;
1384
+ } catch (err) {
1385
+ console.error(chalk11.red(err.message));
1386
+ process.exit(1);
1387
+ }
1388
+ const [meeting, notes, transcript] = await Promise.all([
1389
+ get2(fullId),
1390
+ getNotes(fullId),
1391
+ getTranscript(fullId)
1392
+ ]);
1393
+ if (!meeting) {
1394
+ console.error(chalk11.red(`Meeting ${id} not found`));
1395
+ process.exit(4);
1396
+ }
1397
+ const output = {
1398
+ id: meeting.id,
1399
+ title: meeting.title,
1400
+ created_at: meeting.created_at,
1401
+ updated_at: meeting.updated_at,
1402
+ workspace_id: meeting.workspace_id,
1403
+ people: meeting.people,
1404
+ notes_markdown: notes ? toMarkdown(notes) : null,
1405
+ notes_raw: notes,
1406
+ transcript
1407
+ };
1408
+ if (format === "toon") {
1409
+ console.log(toToon(output));
1410
+ } else {
1411
+ console.log(JSON.stringify(output, null, 2));
1412
+ }
1413
+ });
1414
+ }
1415
+ var exportCommand = createExportCommand();
1416
+
1417
+ // src/commands/meeting/list.ts
1418
+ import chalk12 from "chalk";
1419
+ import { Command as Command12 } from "commander";
1420
+ var debug18 = createGranolaDebug("cmd:meeting:list");
1421
+ function createListCommand2() {
1422
+ 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);
1424
+ const limit = Number.parseInt(opts.limit, 10);
1425
+ if (!Number.isFinite(limit) || limit < 1) {
1426
+ console.error(chalk12.red("Invalid --limit value. Please provide a positive number."));
1427
+ process.exit(1);
1428
+ }
1429
+ const configuredWorkspace = getConfigValue("default_workspace");
1430
+ const workspace = opts.workspace ?? configuredWorkspace;
1431
+ const data = await list2({
1432
+ limit,
1433
+ workspace,
1434
+ folder: opts.folder
1435
+ });
1436
+ debug18("fetched %d meetings", data.length);
1437
+ const format = opts.output || null;
1438
+ debug18("output format: %s", format || "table");
1439
+ if (format) {
1440
+ if (!["json", "yaml", "toon"].includes(format)) {
1441
+ console.error(chalk12.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
1442
+ process.exit(1);
1443
+ }
1444
+ console.log(formatOutput(data, format));
1445
+ return;
1446
+ }
1447
+ if (data.length === 0) {
1448
+ console.log(chalk12.dim("No meetings found."));
1449
+ return;
1450
+ }
1451
+ console.log(chalk12.dim(`Showing ${data.length} meetings
1452
+ `));
1453
+ const output = table(data, [
1454
+ { key: "id", header: "ID", width: 12, format: (v) => String(v).slice(0, 8) },
1455
+ { key: "title", header: "TITLE", width: 36, format: (v) => truncate(String(v), 35) },
1456
+ { key: "created_at", header: "DATE", width: 14, format: (v) => formatDate(String(v)) }
1457
+ ]);
1458
+ console.log(output);
1459
+ });
1460
+ }
1461
+ var listCommand2 = createListCommand2();
1462
+
1463
+ // src/commands/meeting/notes.ts
1464
+ import chalk13 from "chalk";
1465
+ import { Command as Command13 } from "commander";
1466
+ var debug19 = createGranolaDebug("cmd:meeting:notes");
1467
+ function createNotesCommand() {
1468
+ 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);
1470
+ const global = cmd.optsWithGlobals();
1471
+ let fullId;
1472
+ try {
1473
+ const resolved = await resolveId(id);
1474
+ if (!resolved) {
1475
+ console.error(chalk13.red(`Meeting ${id} not found`));
1476
+ process.exit(4);
1477
+ }
1478
+ fullId = resolved;
1479
+ } catch (err) {
1480
+ console.error(chalk13.red(err.message));
1481
+ process.exit(1);
1482
+ }
1483
+ let notes;
1484
+ try {
1485
+ notes = await getNotes(fullId);
1486
+ } catch (error) {
1487
+ debug19("failed to load notes: %O", error);
1488
+ console.error(chalk13.red("Error:"), "Failed to fetch notes.");
1489
+ if (error instanceof Error) {
1490
+ console.error(chalk13.dim(error.message));
1491
+ }
1492
+ process.exit(1);
1493
+ }
1494
+ if (!notes) {
1495
+ console.error(chalk13.red(`No notes found for meeting ${id}`));
1496
+ process.exit(4);
1497
+ }
1498
+ const format = opts.output || "markdown";
1499
+ const structuredFormats = ["json", "yaml", "toon"];
1500
+ if (format !== "markdown" && !structuredFormats.includes(format)) {
1501
+ console.error(
1502
+ chalk13.red(`Invalid format: ${format}. Use 'markdown', 'json', 'yaml', or 'toon'.`)
1503
+ );
1504
+ process.exit(1);
1505
+ }
1506
+ if (structuredFormats.includes(format)) {
1507
+ console.log(formatOutput(notes, format));
1508
+ return;
1509
+ }
1510
+ const md = toMarkdown(notes);
1511
+ if (global.noPager || !process.stdout.isTTY) {
1512
+ console.log(md);
1513
+ } else {
1514
+ await pipeToPager(md);
1515
+ }
1516
+ });
1517
+ }
1518
+ var notesCommand = createNotesCommand();
1519
+
1520
+ // src/commands/meeting/transcript.ts
1521
+ import chalk14 from "chalk";
1522
+ import { Command as Command14 } from "commander";
1523
+
1524
+ // src/lib/transcript.ts
1525
+ var debug20 = createGranolaDebug("lib:transcript");
1526
+ function formatTranscript(utterances, opts = {}) {
1527
+ debug20("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
1528
+ const { timestamps = false, source = "all" } = opts;
1529
+ let filtered = utterances;
1530
+ if (source !== "all") {
1531
+ filtered = utterances.filter((u) => u.source === source);
1532
+ debug20("filtered to %d utterances (source=%s)", filtered.length, source);
1533
+ }
1534
+ if (filtered.length === 0) {
1535
+ debug20("no transcript available");
1536
+ return "No transcript available.";
1537
+ }
1538
+ const lines = [];
1539
+ for (const u of filtered) {
1540
+ const speaker = u.source === "microphone" ? "You" : "Participant";
1541
+ if (timestamps) {
1542
+ const time = formatTimestamp(u.start_timestamp);
1543
+ lines.push(`[${time}] ${speaker}`);
1544
+ lines.push(u.text);
1545
+ lines.push("");
1546
+ } else {
1547
+ lines.push(`${speaker}: ${u.text}`);
1548
+ lines.push("");
1549
+ }
1550
+ }
1551
+ return lines.join("\n").trim();
1552
+ }
1553
+ function formatTimestamp(iso) {
1554
+ const d = new Date(iso);
1555
+ const h = d.getUTCHours().toString().padStart(2, "0");
1556
+ const m = d.getUTCMinutes().toString().padStart(2, "0");
1557
+ const s = d.getUTCSeconds().toString().padStart(2, "0");
1558
+ return `${h}:${m}:${s}`;
1559
+ }
1560
+
1561
+ // src/commands/meeting/transcript.ts
1562
+ var debug21 = createGranolaDebug("cmd:meeting:transcript");
1563
+ var SOURCE_OPTIONS = /* @__PURE__ */ new Set(["microphone", "system", "all"]);
1564
+ function createTranscriptCommand() {
1565
+ 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);
1567
+ const global = cmd.optsWithGlobals();
1568
+ let fullId;
1569
+ try {
1570
+ const resolved = await resolveId(id);
1571
+ if (!resolved) {
1572
+ console.error(chalk14.red(`Meeting ${id} not found`));
1573
+ process.exit(4);
1574
+ }
1575
+ fullId = resolved;
1576
+ } catch (err) {
1577
+ console.error(chalk14.red(err.message));
1578
+ process.exit(1);
1579
+ }
1580
+ const transcript = await getTranscript(fullId);
1581
+ if (transcript.length === 0) {
1582
+ console.error(chalk14.red(`No transcript found for meeting ${id}`));
1583
+ process.exit(4);
1584
+ }
1585
+ const requestedSource = opts.source || "all";
1586
+ if (!SOURCE_OPTIONS.has(requestedSource)) {
1587
+ console.error(
1588
+ chalk14.red(`Invalid source: ${requestedSource}. Use 'microphone', 'system', or 'all'.`)
1589
+ );
1590
+ process.exit(1);
1591
+ }
1592
+ const format = opts.output || "text";
1593
+ const structuredFormats = ["json", "yaml", "toon"];
1594
+ if (format !== "text" && !structuredFormats.includes(format)) {
1595
+ console.error(
1596
+ chalk14.red(`Invalid format: ${format}. Use 'text', 'json', 'yaml', or 'toon'.`)
1597
+ );
1598
+ process.exit(1);
1599
+ }
1600
+ if (structuredFormats.includes(format)) {
1601
+ console.log(formatOutput(transcript, format));
1602
+ return;
1603
+ }
1604
+ const output = formatTranscript(transcript, {
1605
+ timestamps: opts.timestamps,
1606
+ source: requestedSource
1607
+ });
1608
+ if (global.noPager || !process.stdout.isTTY) {
1609
+ console.log(output);
1610
+ } else {
1611
+ await pipeToPager(output);
1612
+ }
1613
+ });
1614
+ }
1615
+ var transcriptCommand = createTranscriptCommand();
1616
+
1617
+ // src/commands/meeting/view.ts
1618
+ import chalk15 from "chalk";
1619
+ import { Command as Command15 } from "commander";
1620
+ import open from "open";
1621
+ var debug22 = createGranolaDebug("cmd:meeting:view");
1622
+ function createViewCommand2() {
1623
+ 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);
1625
+ let fullId;
1626
+ try {
1627
+ const resolved = await resolveId(id);
1628
+ if (!resolved) {
1629
+ console.error(chalk15.red(`Meeting ${id} not found`));
1630
+ process.exit(4);
1631
+ }
1632
+ fullId = resolved;
1633
+ } catch (err) {
1634
+ console.error(chalk15.red(err.message));
1635
+ process.exit(1);
1636
+ }
1637
+ if (opts.web) {
1638
+ await open(`https://app.granola.ai/meeting/${fullId}`);
1639
+ return;
1640
+ }
1641
+ const meeting = await get2(fullId);
1642
+ if (!meeting) {
1643
+ console.error(chalk15.red(`Meeting ${id} not found`));
1644
+ process.exit(4);
1645
+ }
1646
+ const format = opts.output || null;
1647
+ if (format) {
1648
+ if (!["json", "yaml", "toon"].includes(format)) {
1649
+ console.error(chalk15.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
1650
+ process.exit(1);
1651
+ }
1652
+ console.log(formatOutput(meeting, format));
1653
+ return;
1654
+ }
1655
+ console.log(chalk15.bold(meeting.title));
1656
+ console.log(chalk15.dim(`Recorded ${formatDate(meeting.created_at)}`));
1657
+ console.log();
1658
+ console.log(`Workspace: ${meeting.workspace_id || "Personal"}`);
1659
+ if (meeting.creator?.name) {
1660
+ console.log(`Organizer: ${meeting.creator.name}`);
1661
+ }
1662
+ if (meeting.attendees?.length) {
1663
+ console.log(`Attendees: ${meeting.attendees.length} participant(s)`);
1664
+ for (const attendee of meeting.attendees) {
1665
+ const name = attendee.name || attendee.details?.person?.name?.fullName || "Unknown";
1666
+ const title = attendee.details?.employment?.title;
1667
+ const info = title ? `${name} (${title})` : name;
1668
+ console.log(chalk15.dim(` - ${info}`));
1669
+ }
1670
+ }
1671
+ console.log();
1672
+ console.log(`${chalk15.dim("View notes: ")}granola meeting notes ${id}`);
1673
+ console.log(`${chalk15.dim("View transcript: ")}granola meeting transcript ${id}`);
1674
+ });
1675
+ }
1676
+ var viewCommand2 = createViewCommand2();
1677
+
1678
+ // src/commands/meeting/index.ts
1679
+ var meetingCommand = new Command16("meeting").description("Work with meetings").addCommand(listCommand2).addCommand(viewCommand2).addCommand(notesCommand).addCommand(enhancedCommand).addCommand(transcriptCommand).addCommand(exportCommand);
1680
+
1681
+ // src/commands/workspace/index.ts
1682
+ import { Command as Command19 } from "commander";
1683
+
1684
+ // src/commands/workspace/list.ts
1685
+ import chalk16 from "chalk";
1686
+ import { Command as Command17 } from "commander";
1687
+
1688
+ // src/services/workspaces.ts
1689
+ var debug23 = createGranolaDebug("service:workspaces");
1690
+ async function list3() {
1691
+ return withTokenRefresh(async () => {
1692
+ debug23("fetching workspaces");
1693
+ const client2 = await getClient();
1694
+ const res = await client2.getWorkspaces();
1695
+ const workspacesArray = res?.workspaces || [];
1696
+ debug23("found %d workspaces", workspacesArray.length);
1697
+ return workspacesArray.map((item) => {
1698
+ const ws = item.workspace;
1699
+ return {
1700
+ id: ws.workspace_id,
1701
+ name: ws.display_name,
1702
+ created_at: ws.created_at,
1703
+ owner_id: ""
1704
+ };
1705
+ });
1706
+ });
1707
+ }
1708
+ async function resolveId2(partialId) {
1709
+ debug23("resolving workspace id: %s", partialId);
1710
+ const workspaces = await list3();
1711
+ const matches = workspaces.filter((w) => w.id.startsWith(partialId));
1712
+ if (matches.length === 0) {
1713
+ debug23("no workspace found for id: %s", partialId);
1714
+ return null;
1715
+ }
1716
+ if (matches.length > 1) {
1717
+ debug23("ambiguous id: %s matches %d workspaces", partialId, matches.length);
1718
+ throw new Error(`Ambiguous ID: ${partialId} matches ${matches.length} workspaces`);
1719
+ }
1720
+ debug23("resolved workspace: %s -> %s", partialId, matches[0].id);
1721
+ return matches[0].id;
1722
+ }
1723
+ async function get3(id) {
1724
+ debug23("getting workspace: %s", id);
1725
+ const workspaces = await list3();
1726
+ const workspace = workspaces.find((w) => w.id === id) || null;
1727
+ debug23("workspace %s: %s", id, workspace ? "found" : "not found");
1728
+ return workspace;
1729
+ }
1730
+
1731
+ // src/commands/workspace/list.ts
1732
+ var debug24 = createGranolaDebug("cmd:workspace:list");
1733
+ function createListCommand3() {
1734
+ 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");
1736
+ const data = await list3();
1737
+ debug24("fetched %d workspaces", data.length);
1738
+ const format = opts.output || null;
1739
+ if (format) {
1740
+ if (!["json", "yaml", "toon"].includes(format)) {
1741
+ console.error(chalk16.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
1742
+ process.exit(1);
1743
+ }
1744
+ console.log(formatOutput(data, format));
1745
+ return;
1746
+ }
1747
+ if (data.length === 0) {
1748
+ console.log(chalk16.dim("No workspaces found."));
1749
+ return;
1750
+ }
1751
+ const output = table(data, [
1752
+ { key: "id", header: "ID", width: 12, format: (v) => String(v).slice(0, 8) },
1753
+ { key: "name", header: "NAME", width: 20 },
1754
+ { key: "created_at", header: "CREATED", width: 14, format: (v) => formatDate(String(v)) }
1755
+ ]);
1756
+ console.log(output);
1757
+ });
1758
+ }
1759
+ var listCommand3 = createListCommand3();
1760
+
1761
+ // src/commands/workspace/view.ts
1762
+ import chalk17 from "chalk";
1763
+ import { Command as Command18 } from "commander";
1764
+ var debug25 = createGranolaDebug("cmd:workspace:view");
1765
+ function createViewCommand3() {
1766
+ 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);
1768
+ let fullId;
1769
+ try {
1770
+ const resolved = await resolveId2(id);
1771
+ if (!resolved) {
1772
+ console.error(chalk17.red(`Workspace ${id} not found`));
1773
+ process.exit(4);
1774
+ }
1775
+ fullId = resolved;
1776
+ } catch (err) {
1777
+ console.error(chalk17.red(err.message));
1778
+ process.exit(1);
1779
+ }
1780
+ const workspace = await get3(fullId);
1781
+ if (!workspace) {
1782
+ console.error(chalk17.red(`Workspace ${id} not found`));
1783
+ process.exit(4);
1784
+ }
1785
+ const format = opts.output || null;
1786
+ if (format) {
1787
+ if (!["json", "yaml", "toon"].includes(format)) {
1788
+ console.error(chalk17.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
1789
+ process.exit(1);
1790
+ }
1791
+ console.log(formatOutput(workspace, format));
1792
+ return;
1793
+ }
1794
+ console.log(chalk17.bold(workspace.name));
1795
+ console.log(chalk17.dim(`Created ${formatDate(workspace.created_at)}`));
1796
+ console.log();
1797
+ console.log(`View all meetings: granola meeting list --workspace ${id}`);
1798
+ });
1799
+ }
1800
+ var viewCommand3 = createViewCommand3();
1801
+
1802
+ // src/commands/workspace/index.ts
1803
+ var workspaceCommand = new Command19("workspace").description("Work with workspaces").addCommand(listCommand3).addCommand(viewCommand3);
1804
+
1805
+ // src/main.ts
1806
+ var debug26 = createGranolaDebug("cli");
1807
+ var debugAlias = createGranolaDebug("cli:alias");
1808
+ var debugSubcmd = createGranolaDebug("cli:subcommand");
1809
+ 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));
1812
+ var program = new Command20();
1813
+ program.name("granola").description("CLI for Granola meeting notes").version(packageJson.version).option("--no-pager", "Disable pager");
1814
+ program.addCommand(authCommand);
1815
+ program.addCommand(meetingCommand);
1816
+ program.addCommand(workspaceCommand);
1817
+ program.addCommand(folderCommand);
1818
+ program.addCommand(configCommand);
1819
+ 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
+ function expandAlias(args) {
1872
+ if (args.length < 3) return args;
1873
+ const command = args[2];
1874
+ debugAlias("checking alias for command: %s", command);
1875
+ const alias = getAlias(command);
1876
+ if (alias) {
1877
+ debugAlias("alias found: %s -> %s", command, alias);
1878
+ try {
1879
+ const aliasArgs = parseAliasArguments(alias);
1880
+ const expanded = [...args.slice(0, 2), ...aliasArgs, ...args.slice(3)];
1881
+ debugAlias("expanded args: %O", expanded.slice(2));
1882
+ return expanded;
1883
+ } catch (err) {
1884
+ debugAlias("failed to expand alias %s: %O", command, err);
1885
+ return args;
1886
+ }
1887
+ }
1888
+ return args;
1889
+ }
1890
+ var expandedArgs = expandAlias(process.argv);
1891
+ debug26("parsing with args: %O", expandedArgs.slice(2));
1892
+ program.parseAsync(expandedArgs);
1893
+ //# sourceMappingURL=main.js.map