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/README.md +6 -2
- package/dist/main.js +319 -265
- package/dist/main.js.map +1 -1
- package/package.json +10 -2
package/dist/main.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
318
|
+
debug5("loading credentials from keychain");
|
|
237
319
|
try {
|
|
238
320
|
const stored = await getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
239
321
|
if (!stored) {
|
|
240
|
-
|
|
322
|
+
debug5("no credentials found in keychain");
|
|
241
323
|
return null;
|
|
242
324
|
}
|
|
243
325
|
const parsed = JSON.parse(stored);
|
|
244
|
-
|
|
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
|
-
|
|
333
|
+
debug5("failed to get credentials: %O", error);
|
|
252
334
|
return null;
|
|
253
335
|
}
|
|
254
336
|
}
|
|
255
337
|
async function saveCredentials(creds) {
|
|
256
|
-
|
|
338
|
+
debug5("saving credentials to keychain");
|
|
257
339
|
await setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(creds));
|
|
258
|
-
|
|
340
|
+
debug5("credentials saved");
|
|
259
341
|
}
|
|
260
342
|
async function deleteCredentials() {
|
|
261
|
-
|
|
343
|
+
debug5("deleting credentials from keychain");
|
|
262
344
|
await deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
381
|
+
debug5("token refresh error: %O", error);
|
|
298
382
|
return null;
|
|
299
383
|
}
|
|
300
384
|
}
|
|
301
385
|
function parseSupabaseJson(json) {
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
418
|
+
debug5("failed to parse supabase.json: %O", error);
|
|
335
419
|
return null;
|
|
336
420
|
}
|
|
337
421
|
}
|
|
338
422
|
function getDefaultSupabasePath() {
|
|
339
|
-
const home =
|
|
423
|
+
const home = homedir2();
|
|
340
424
|
const os2 = platform();
|
|
341
425
|
let path;
|
|
342
426
|
switch (os2) {
|
|
343
427
|
case "darwin":
|
|
344
|
-
path =
|
|
428
|
+
path = join2(home, "Library", "Application Support", "Granola", "supabase.json");
|
|
345
429
|
break;
|
|
346
430
|
case "win32":
|
|
347
|
-
path =
|
|
348
|
-
process.env.APPDATA ||
|
|
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 =
|
|
438
|
+
path = join2(home, ".config", "granola", "supabase.json");
|
|
355
439
|
}
|
|
356
|
-
|
|
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
|
-
|
|
445
|
+
debug5("loading credentials from file: %s", path);
|
|
362
446
|
try {
|
|
363
447
|
const content = await readFile(path, "utf-8");
|
|
364
|
-
|
|
448
|
+
debug5("file read successful, parsing content");
|
|
365
449
|
return parseSupabaseJson(content);
|
|
366
450
|
} catch (error) {
|
|
367
|
-
|
|
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
|
|
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
|
-
|
|
460
|
+
debug6("login command invoked");
|
|
377
461
|
const creds = await loadCredentialsFromFile();
|
|
378
462
|
if (!creds) {
|
|
379
463
|
const path = getDefaultSupabasePath();
|
|
380
|
-
|
|
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
|
-
|
|
470
|
+
debug6("credentials loaded, saving to keychain");
|
|
387
471
|
await saveCredentials(creds);
|
|
388
|
-
|
|
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
|
|
481
|
+
var debug7 = createGranolaDebug("cmd:auth:logout");
|
|
398
482
|
function createLogoutCommand() {
|
|
399
483
|
return new Command3("logout").description("Logout from Granola").action(async () => {
|
|
400
|
-
|
|
484
|
+
debug7("logout command invoked");
|
|
401
485
|
try {
|
|
402
486
|
await deleteCredentials();
|
|
403
|
-
|
|
487
|
+
debug7("logout successful");
|
|
404
488
|
console.log(chalk4.green("Logged out successfully"));
|
|
405
489
|
} catch (error) {
|
|
406
|
-
|
|
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
|
|
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
|
-
|
|
507
|
+
debug8("status command invoked");
|
|
424
508
|
const creds = await getCredentials();
|
|
425
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
808
|
+
var debug10 = createGranolaDebug("service:client");
|
|
725
809
|
var client = null;
|
|
726
810
|
async function getClient() {
|
|
727
|
-
|
|
811
|
+
debug10("getClient called, cached: %s", client ? "yes" : "no");
|
|
728
812
|
if (client) return client;
|
|
729
|
-
|
|
813
|
+
debug10("fetching credentials");
|
|
730
814
|
const creds = await getCredentials();
|
|
731
815
|
if (!creds) {
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
+
debug10("401 detected, attempting token refresh");
|
|
759
843
|
const newCreds = await refreshAccessToken();
|
|
760
844
|
if (!newCreds) {
|
|
761
|
-
|
|
845
|
+
debug10("token refresh failed, re-throwing original error");
|
|
762
846
|
throw error;
|
|
763
847
|
}
|
|
764
848
|
resetClient();
|
|
765
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
893
|
+
debug11("folder %s not found", id);
|
|
810
894
|
return null;
|
|
811
895
|
}
|
|
812
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1007
|
+
debug14("validating pager command: %s", cmd);
|
|
924
1008
|
if (SHELL_METACHARACTERS.test(cmd)) {
|
|
925
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1029
|
+
debug14("pager command: %s (source: config)", configuredPager);
|
|
946
1030
|
return configuredPager;
|
|
947
1031
|
}
|
|
948
|
-
|
|
1032
|
+
debug14("pager command: less -R (source: default)");
|
|
949
1033
|
return "less -R";
|
|
950
1034
|
}
|
|
951
1035
|
async function pipeToPager(content) {
|
|
952
|
-
|
|
1036
|
+
debug14("pipeToPager: isTTY=%s, contentLength=%d", process.stdout.isTTY, content.length);
|
|
953
1037
|
if (!process.stdout.isTTY) {
|
|
954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1078
|
+
debug14("pager closed");
|
|
995
1079
|
finish();
|
|
996
1080
|
});
|
|
997
1081
|
pager.on("error", (err) => {
|
|
998
|
-
|
|
1082
|
+
debug14("pager error: %O", err);
|
|
999
1083
|
fallbackToStdout(err.message);
|
|
1000
1084
|
});
|
|
1001
1085
|
} catch (err) {
|
|
1002
|
-
|
|
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
|
|
1093
|
+
var debug15 = createGranolaDebug("lib:prosemirror");
|
|
1010
1094
|
function toMarkdown(doc) {
|
|
1011
|
-
|
|
1095
|
+
debug15("toMarkdown called with doc: %O", doc);
|
|
1012
1096
|
if (!doc?.content) {
|
|
1013
|
-
|
|
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
|
-
|
|
1101
|
+
debug15("toMarkdown result: %s", result);
|
|
1018
1102
|
return result;
|
|
1019
1103
|
}
|
|
1020
1104
|
function nodeToMd(node) {
|
|
1021
|
-
|
|
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
|
-
|
|
1142
|
+
debug15("Unknown node type: %s", node.type);
|
|
1059
1143
|
result = node.content ? node.content.map((c) => nodeToMd(c)).join("") : "";
|
|
1060
1144
|
}
|
|
1061
|
-
|
|
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
|
|
1163
|
+
var debug16 = createGranolaDebug("service:meetings");
|
|
1080
1164
|
async function getFolderDocumentIds(client2, folderId) {
|
|
1081
|
-
|
|
1165
|
+
debug16("fetching folder %s via getDocumentList", folderId);
|
|
1082
1166
|
const folder = await client2.getDocumentList(folderId);
|
|
1083
1167
|
if (!folder) {
|
|
1084
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1197
|
+
debug16("getDocumentMetadata returned null for %s", id);
|
|
1114
1198
|
return null;
|
|
1115
1199
|
}
|
|
1116
1200
|
return metadata;
|
|
1117
1201
|
} catch (err) {
|
|
1118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1220
|
+
debug16("listing meetings for folder: %s", folder);
|
|
1137
1221
|
const folderMeetings = await fetchFolderMeetings(client2, folder);
|
|
1138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1242
|
+
debug16("fetched %d meetings", meetings.length);
|
|
1159
1243
|
if (workspace) {
|
|
1160
1244
|
meetings = meetings.filter((m) => m.workspace_id === workspace);
|
|
1161
|
-
|
|
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
|
-
|
|
1282
|
+
debug16("resolving meeting id: %s (length: %d)", partialId, partialId.length);
|
|
1171
1283
|
const client2 = await getClient();
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1308
|
+
debug16("no meeting found for id: %s", partialId);
|
|
1203
1309
|
return null;
|
|
1204
1310
|
}
|
|
1205
1311
|
const match = matches.values().next().value;
|
|
1206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1322
|
+
debug16("meeting %s: not found", id);
|
|
1217
1323
|
return null;
|
|
1218
1324
|
}
|
|
1219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1349
|
+
debug16("findMeetingViaDocuments error: %O", err);
|
|
1244
1350
|
return null;
|
|
1245
1351
|
}
|
|
1246
1352
|
}
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1398
|
+
debug16("getTranscript got %d utterances", transcript.length);
|
|
1293
1399
|
return transcript;
|
|
1294
1400
|
} catch (err) {
|
|
1295
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1542
|
+
debug19("fetched %d meetings", data.length);
|
|
1437
1543
|
const format = opts.output || null;
|
|
1438
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1631
|
+
var debug21 = createGranolaDebug("lib:transcript");
|
|
1526
1632
|
function formatTranscript(utterances, opts = {}) {
|
|
1527
|
-
|
|
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
|
-
|
|
1638
|
+
debug21("filtered to %d utterances (source=%s)", filtered.length, source);
|
|
1533
1639
|
}
|
|
1534
1640
|
if (filtered.length === 0) {
|
|
1535
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1621
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
|
1795
|
+
var debug24 = createGranolaDebug("service:workspaces");
|
|
1690
1796
|
async function list3() {
|
|
1691
1797
|
return withTokenRefresh(async () => {
|
|
1692
|
-
|
|
1798
|
+
debug24("fetching workspaces");
|
|
1693
1799
|
const client2 = await getClient();
|
|
1694
1800
|
const res = await client2.getWorkspaces();
|
|
1695
1801
|
const workspacesArray = res?.workspaces || [];
|
|
1696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1819
|
+
debug24("no workspace found for id: %s", partialId);
|
|
1714
1820
|
return null;
|
|
1715
1821
|
}
|
|
1716
1822
|
if (matches.length > 1) {
|
|
1717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1830
|
+
debug24("getting workspace: %s", id);
|
|
1725
1831
|
const workspaces = await list3();
|
|
1726
1832
|
const workspace = workspaces.find((w) => w.id === id) || null;
|
|
1727
|
-
|
|
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
|
|
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
|
-
|
|
1841
|
+
debug25("workspace list command invoked");
|
|
1736
1842
|
const data = await list3();
|
|
1737
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
1945
|
+
debug27("parsing with args: %O", expandedArgs.slice(2));
|
|
1892
1946
|
program.parseAsync(expandedArgs);
|
|
1893
1947
|
//# sourceMappingURL=main.js.map
|