granola-cli 0.1.0 → 0.2.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/README.md +63 -7
- package/dist/main.js +584 -269
- 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(timeoutMs = LOCK_TIMEOUT_MS) {
|
|
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 < timeoutMs) {
|
|
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, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
301
|
+
const handle = await acquireLock(timeoutMs);
|
|
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) {
|
|
@@ -1075,17 +1159,90 @@ function applyMarks(text, marks) {
|
|
|
1075
1159
|
return text;
|
|
1076
1160
|
}
|
|
1077
1161
|
|
|
1162
|
+
// src/lib/filters.ts
|
|
1163
|
+
var debug16 = createGranolaDebug("lib:filters");
|
|
1164
|
+
function isSameDay(d1, d2) {
|
|
1165
|
+
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
|
|
1166
|
+
}
|
|
1167
|
+
function startOfDay(date) {
|
|
1168
|
+
const result = new Date(date);
|
|
1169
|
+
result.setHours(0, 0, 0, 0);
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
function endOfDay(date) {
|
|
1173
|
+
const result = new Date(date);
|
|
1174
|
+
result.setHours(23, 59, 59, 999);
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
function matchesSearch(meeting, query) {
|
|
1178
|
+
const normalizedQuery = query.toLowerCase();
|
|
1179
|
+
const normalizedTitle = meeting.title.toLowerCase();
|
|
1180
|
+
return normalizedTitle.includes(normalizedQuery);
|
|
1181
|
+
}
|
|
1182
|
+
function matchesAttendee(meeting, query) {
|
|
1183
|
+
const normalizedQuery = query.toLowerCase();
|
|
1184
|
+
const peopleAttendees = meeting.people?.attendees ?? [];
|
|
1185
|
+
const topLevelAttendees = meeting.attendees ?? [];
|
|
1186
|
+
const allAttendees = [...peopleAttendees, ...topLevelAttendees];
|
|
1187
|
+
return allAttendees.some((attendee) => {
|
|
1188
|
+
const name = attendee.name?.toLowerCase() ?? "";
|
|
1189
|
+
const email = attendee.email?.toLowerCase() ?? "";
|
|
1190
|
+
return name.includes(normalizedQuery) || email.includes(normalizedQuery);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function matchesDate(meeting, date) {
|
|
1194
|
+
const meetingDate = new Date(meeting.created_at);
|
|
1195
|
+
return isSameDay(meetingDate, date);
|
|
1196
|
+
}
|
|
1197
|
+
function matchesDateRange(meeting, since, until) {
|
|
1198
|
+
const meetingDate = new Date(meeting.created_at);
|
|
1199
|
+
if (since && meetingDate < startOfDay(since)) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
if (until && meetingDate > endOfDay(until)) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
function hasActiveFilters(options) {
|
|
1208
|
+
return !!(options.search || options.attendee || options.date || options.since || options.until);
|
|
1209
|
+
}
|
|
1210
|
+
function applyFilters(meetings, options) {
|
|
1211
|
+
if (!hasActiveFilters(options)) {
|
|
1212
|
+
return meetings;
|
|
1213
|
+
}
|
|
1214
|
+
debug16("applying filters: %O", options);
|
|
1215
|
+
const startCount = meetings.length;
|
|
1216
|
+
const filtered = meetings.filter((meeting) => {
|
|
1217
|
+
if (options.search && !matchesSearch(meeting, options.search)) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
if (options.attendee && !matchesAttendee(meeting, options.attendee)) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (options.date && !matchesDate(meeting, options.date)) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
if (!matchesDateRange(meeting, options.since, options.until)) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
});
|
|
1231
|
+
debug16("filtered %d -> %d meetings", startCount, filtered.length);
|
|
1232
|
+
return filtered;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1078
1235
|
// src/services/meetings.ts
|
|
1079
|
-
var
|
|
1236
|
+
var debug17 = createGranolaDebug("service:meetings");
|
|
1080
1237
|
async function getFolderDocumentIds(client2, folderId) {
|
|
1081
|
-
|
|
1238
|
+
debug17("fetching folder %s via getDocumentList", folderId);
|
|
1082
1239
|
const folder = await client2.getDocumentList(folderId);
|
|
1083
1240
|
if (!folder) {
|
|
1084
|
-
|
|
1241
|
+
debug17("folder %s not found", folderId);
|
|
1085
1242
|
return [];
|
|
1086
1243
|
}
|
|
1087
1244
|
const ids = folder.document_ids || folder.documents?.map((doc) => doc.id) || [];
|
|
1088
|
-
|
|
1245
|
+
debug17("folder %s returned %d document ids", folderId, ids.length);
|
|
1089
1246
|
return ids;
|
|
1090
1247
|
}
|
|
1091
1248
|
var DOCUMENT_BATCH_SIZE = 100;
|
|
@@ -1103,50 +1260,63 @@ async function fetchMeetingsByIds(client2, documentIds) {
|
|
|
1103
1260
|
const docs = res?.documents || res?.docs || [];
|
|
1104
1261
|
meetings.push(...docs);
|
|
1105
1262
|
}
|
|
1106
|
-
|
|
1263
|
+
debug17("fetched %d meetings via getDocumentsBatch", meetings.length);
|
|
1107
1264
|
return meetings;
|
|
1108
1265
|
}
|
|
1109
1266
|
async function loadMeetingMetadata(client2, id) {
|
|
1110
1267
|
try {
|
|
1111
1268
|
const metadata = await client2.getDocumentMetadata(id);
|
|
1112
1269
|
if (!metadata) {
|
|
1113
|
-
|
|
1270
|
+
debug17("getDocumentMetadata returned null for %s", id);
|
|
1114
1271
|
return null;
|
|
1115
1272
|
}
|
|
1116
1273
|
return metadata;
|
|
1117
1274
|
} catch (err) {
|
|
1118
|
-
|
|
1275
|
+
debug17("getDocumentMetadata failed for %s: %O", id, err);
|
|
1119
1276
|
return null;
|
|
1120
1277
|
}
|
|
1121
1278
|
}
|
|
1122
1279
|
async function fetchFolderMeetings(client2, folderId) {
|
|
1123
1280
|
const ids = await getFolderDocumentIds(client2, folderId);
|
|
1124
1281
|
if (ids.length === 0) {
|
|
1125
|
-
|
|
1282
|
+
debug17("folder %s has no documents", folderId);
|
|
1126
1283
|
return [];
|
|
1127
1284
|
}
|
|
1128
1285
|
return fetchMeetingsByIds(client2, ids);
|
|
1129
1286
|
}
|
|
1130
1287
|
async function list2(opts = {}) {
|
|
1131
1288
|
return withTokenRefresh(async () => {
|
|
1132
|
-
|
|
1289
|
+
debug17("list called with opts: %O", opts);
|
|
1133
1290
|
const client2 = await getClient();
|
|
1134
|
-
const { limit = 20, offset = 0, workspace, folder } = opts;
|
|
1291
|
+
const { limit = 20, offset = 0, workspace, folder, ...filterOpts } = opts;
|
|
1135
1292
|
if (folder) {
|
|
1136
|
-
|
|
1293
|
+
debug17("listing meetings for folder: %s", folder);
|
|
1137
1294
|
const folderMeetings = await fetchFolderMeetings(client2, folder);
|
|
1138
|
-
|
|
1295
|
+
debug17("fetched %d meetings for folder %s", folderMeetings.length, folder);
|
|
1139
1296
|
let filtered = folderMeetings;
|
|
1140
1297
|
if (workspace) {
|
|
1141
1298
|
filtered = folderMeetings.filter((m) => m.workspace_id === workspace);
|
|
1142
|
-
|
|
1299
|
+
debug17(
|
|
1143
1300
|
"workspace filter applied for folder %s: %d meetings remain",
|
|
1144
1301
|
folder,
|
|
1145
1302
|
filtered.length
|
|
1146
1303
|
);
|
|
1147
1304
|
}
|
|
1305
|
+
filtered = applyFilters(filtered, filterOpts);
|
|
1148
1306
|
const paginated = filtered.slice(offset, offset + limit);
|
|
1149
|
-
|
|
1307
|
+
debug17("returning %d meetings from folder %s after pagination", paginated.length, folder);
|
|
1308
|
+
return paginated;
|
|
1309
|
+
}
|
|
1310
|
+
if (hasActiveFilters(filterOpts)) {
|
|
1311
|
+
debug17("filters active, using cached meetings for filtering");
|
|
1312
|
+
let meetings2 = await getCachedMeetings(client2);
|
|
1313
|
+
if (workspace) {
|
|
1314
|
+
meetings2 = meetings2.filter((m) => m.workspace_id === workspace);
|
|
1315
|
+
debug17("filtered to %d meetings for workspace: %s", meetings2.length, workspace);
|
|
1316
|
+
}
|
|
1317
|
+
meetings2 = applyFilters(meetings2, filterOpts);
|
|
1318
|
+
const paginated = meetings2.slice(offset, offset + limit);
|
|
1319
|
+
debug17("returning %d meetings after filtering and pagination", paginated.length);
|
|
1150
1320
|
return paginated;
|
|
1151
1321
|
}
|
|
1152
1322
|
const res = await client2.getDocuments({
|
|
@@ -1155,68 +1325,90 @@ async function list2(opts = {}) {
|
|
|
1155
1325
|
include_last_viewed_panel: false
|
|
1156
1326
|
});
|
|
1157
1327
|
let meetings = res?.docs || [];
|
|
1158
|
-
|
|
1328
|
+
debug17("fetched %d meetings", meetings.length);
|
|
1159
1329
|
if (workspace) {
|
|
1160
1330
|
meetings = meetings.filter((m) => m.workspace_id === workspace);
|
|
1161
|
-
|
|
1331
|
+
debug17("filtered to %d meetings for workspace: %s", meetings.length, workspace);
|
|
1162
1332
|
}
|
|
1163
1333
|
return meetings;
|
|
1164
1334
|
});
|
|
1165
1335
|
}
|
|
1166
1336
|
var RESOLVE_PAGE_SIZE = 100;
|
|
1167
1337
|
var MAX_RESOLVE_PAGES = 100;
|
|
1338
|
+
var FULL_UUID_LENGTH = 36;
|
|
1339
|
+
var CACHE_TTL_MS = 6e4;
|
|
1340
|
+
var meetingsCache = null;
|
|
1341
|
+
async function getCachedMeetings(client2) {
|
|
1342
|
+
if (meetingsCache && Date.now() - meetingsCache.timestamp < CACHE_TTL_MS) {
|
|
1343
|
+
debug17("using cached meetings (%d items)", meetingsCache.meetings.length);
|
|
1344
|
+
return meetingsCache.meetings;
|
|
1345
|
+
}
|
|
1346
|
+
debug17("cache miss or expired, fetching meetings");
|
|
1347
|
+
const meetings = [];
|
|
1348
|
+
let offset = 0;
|
|
1349
|
+
for (let page = 0; page < MAX_RESOLVE_PAGES; page += 1) {
|
|
1350
|
+
const res = await client2.getDocuments({
|
|
1351
|
+
limit: RESOLVE_PAGE_SIZE,
|
|
1352
|
+
offset,
|
|
1353
|
+
include_last_viewed_panel: false
|
|
1354
|
+
});
|
|
1355
|
+
const docs = res?.docs || [];
|
|
1356
|
+
meetings.push(...docs);
|
|
1357
|
+
if (docs.length < RESOLVE_PAGE_SIZE) {
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
offset += RESOLVE_PAGE_SIZE;
|
|
1361
|
+
}
|
|
1362
|
+
meetingsCache = { meetings, timestamp: Date.now() };
|
|
1363
|
+
debug17("cached %d meetings", meetings.length);
|
|
1364
|
+
return meetings;
|
|
1365
|
+
}
|
|
1168
1366
|
async function resolveId(partialId) {
|
|
1169
1367
|
return withTokenRefresh(async () => {
|
|
1170
|
-
|
|
1368
|
+
debug17("resolving meeting id: %s (length: %d)", partialId, partialId.length);
|
|
1171
1369
|
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
|
-
}
|
|
1370
|
+
if (partialId.length >= FULL_UUID_LENGTH) {
|
|
1371
|
+
debug17("attempting direct lookup for full UUID");
|
|
1372
|
+
try {
|
|
1373
|
+
const metadata = await client2.getDocumentMetadata(partialId);
|
|
1374
|
+
if (metadata) {
|
|
1375
|
+
debug17("direct lookup successful for: %s", partialId);
|
|
1376
|
+
return partialId;
|
|
1194
1377
|
}
|
|
1378
|
+
} catch {
|
|
1379
|
+
debug17("direct lookup failed, falling back to search");
|
|
1195
1380
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1381
|
+
}
|
|
1382
|
+
const meetings = await getCachedMeetings(client2);
|
|
1383
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1384
|
+
for (const meeting of meetings) {
|
|
1385
|
+
if (meeting.id?.startsWith(partialId)) {
|
|
1386
|
+
matches.add(meeting.id);
|
|
1387
|
+
if (matches.size > 1) {
|
|
1388
|
+
debug17("ambiguous id: %s matches >1 meetings", partialId);
|
|
1389
|
+
throw new Error(`Ambiguous ID: ${partialId} matches ${matches.size} meetings`);
|
|
1390
|
+
}
|
|
1198
1391
|
}
|
|
1199
|
-
offset += RESOLVE_PAGE_SIZE;
|
|
1200
1392
|
}
|
|
1201
1393
|
if (matches.size === 0) {
|
|
1202
|
-
|
|
1394
|
+
debug17("no meeting found for id: %s", partialId);
|
|
1203
1395
|
return null;
|
|
1204
1396
|
}
|
|
1205
1397
|
const match = matches.values().next().value;
|
|
1206
|
-
|
|
1398
|
+
debug17("resolved meeting: %s -> %s", partialId, match);
|
|
1207
1399
|
return match;
|
|
1208
1400
|
});
|
|
1209
1401
|
}
|
|
1210
1402
|
async function get2(id) {
|
|
1211
1403
|
return withTokenRefresh(async () => {
|
|
1212
|
-
|
|
1404
|
+
debug17("getting meeting: %s", id);
|
|
1213
1405
|
const client2 = await getClient();
|
|
1214
1406
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1215
1407
|
if (!metadata) {
|
|
1216
|
-
|
|
1408
|
+
debug17("meeting %s: not found", id);
|
|
1217
1409
|
return null;
|
|
1218
1410
|
}
|
|
1219
|
-
|
|
1411
|
+
debug17("meeting %s: found", id);
|
|
1220
1412
|
return { id, ...metadata };
|
|
1221
1413
|
});
|
|
1222
1414
|
}
|
|
@@ -1224,36 +1416,36 @@ async function findMeetingViaDocuments(client2, id, { includeLastViewedPanel })
|
|
|
1224
1416
|
let offset = 0;
|
|
1225
1417
|
for (let page = 0; page < MAX_NOTES_PAGES; page += 1) {
|
|
1226
1418
|
try {
|
|
1227
|
-
|
|
1419
|
+
debug17("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
|
|
1228
1420
|
const res = await client2.getDocuments({
|
|
1229
1421
|
limit: NOTES_PAGE_SIZE,
|
|
1230
1422
|
offset,
|
|
1231
1423
|
include_last_viewed_panel: includeLastViewedPanel
|
|
1232
1424
|
});
|
|
1233
1425
|
const meetings = res?.docs || [];
|
|
1234
|
-
|
|
1426
|
+
debug17("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
|
|
1235
1427
|
if (meetings.length === 0) break;
|
|
1236
1428
|
const meeting = meetings.find((m) => m.id === id);
|
|
1237
1429
|
if (meeting) {
|
|
1238
|
-
|
|
1430
|
+
debug17("findMeetingViaDocuments located meeting %s on page %d", id, page);
|
|
1239
1431
|
return meeting;
|
|
1240
1432
|
}
|
|
1241
1433
|
offset += NOTES_PAGE_SIZE;
|
|
1242
1434
|
} catch (err) {
|
|
1243
|
-
|
|
1435
|
+
debug17("findMeetingViaDocuments error: %O", err);
|
|
1244
1436
|
return null;
|
|
1245
1437
|
}
|
|
1246
1438
|
}
|
|
1247
|
-
|
|
1439
|
+
debug17("findMeetingViaDocuments did not locate meeting %s", id);
|
|
1248
1440
|
return null;
|
|
1249
1441
|
}
|
|
1250
1442
|
async function getNotes(id) {
|
|
1251
1443
|
return withTokenRefresh(async () => {
|
|
1252
|
-
|
|
1444
|
+
debug17("getNotes called with id: %s", id);
|
|
1253
1445
|
const client2 = await getClient();
|
|
1254
1446
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1255
1447
|
if (metadata && "notes" in metadata) {
|
|
1256
|
-
|
|
1448
|
+
debug17("getNotes resolved via metadata response");
|
|
1257
1449
|
return metadata.notes || null;
|
|
1258
1450
|
}
|
|
1259
1451
|
const meeting = await findMeetingViaDocuments(client2, id, {
|
|
@@ -1267,11 +1459,11 @@ async function getNotes(id) {
|
|
|
1267
1459
|
}
|
|
1268
1460
|
async function getEnhancedNotes(id) {
|
|
1269
1461
|
return withTokenRefresh(async () => {
|
|
1270
|
-
|
|
1462
|
+
debug17("getEnhancedNotes called with id: %s", id);
|
|
1271
1463
|
const client2 = await getClient();
|
|
1272
1464
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1273
1465
|
if (metadata && "last_viewed_panel" in metadata) {
|
|
1274
|
-
|
|
1466
|
+
debug17("getEnhancedNotes resolved via metadata response");
|
|
1275
1467
|
return metadata.last_viewed_panel?.content || null;
|
|
1276
1468
|
}
|
|
1277
1469
|
const meeting = await findMeetingViaDocuments(client2, id, {
|
|
@@ -1285,24 +1477,24 @@ async function getEnhancedNotes(id) {
|
|
|
1285
1477
|
}
|
|
1286
1478
|
async function getTranscript(id) {
|
|
1287
1479
|
return withTokenRefresh(async () => {
|
|
1288
|
-
|
|
1480
|
+
debug17("getTranscript called with id: %s", id);
|
|
1289
1481
|
const client2 = await getClient();
|
|
1290
1482
|
try {
|
|
1291
1483
|
const transcript = await client2.getDocumentTranscript(id);
|
|
1292
|
-
|
|
1484
|
+
debug17("getTranscript got %d utterances", transcript.length);
|
|
1293
1485
|
return transcript;
|
|
1294
1486
|
} catch (err) {
|
|
1295
|
-
|
|
1487
|
+
debug17("getTranscript error: %O", err);
|
|
1296
1488
|
return [];
|
|
1297
1489
|
}
|
|
1298
1490
|
});
|
|
1299
1491
|
}
|
|
1300
1492
|
|
|
1301
1493
|
// src/commands/meeting/enhanced.ts
|
|
1302
|
-
var
|
|
1494
|
+
var debug18 = createGranolaDebug("cmd:meeting:enhanced");
|
|
1303
1495
|
function createEnhancedCommand() {
|
|
1304
1496
|
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
|
-
|
|
1497
|
+
debug18("enhanced command invoked with id: %s", id);
|
|
1306
1498
|
const global = cmd.optsWithGlobals();
|
|
1307
1499
|
let fullId;
|
|
1308
1500
|
try {
|
|
@@ -1320,7 +1512,7 @@ function createEnhancedCommand() {
|
|
|
1320
1512
|
try {
|
|
1321
1513
|
notes = await getEnhancedNotes(fullId);
|
|
1322
1514
|
} catch (error) {
|
|
1323
|
-
|
|
1515
|
+
debug18("failed to load enhanced notes: %O", error);
|
|
1324
1516
|
console.error(chalk10.red("Error:"), "Failed to fetch enhanced notes.");
|
|
1325
1517
|
if (error instanceof Error) {
|
|
1326
1518
|
console.error(chalk10.dim(error.message));
|
|
@@ -1364,10 +1556,10 @@ function toToon(data) {
|
|
|
1364
1556
|
}
|
|
1365
1557
|
|
|
1366
1558
|
// src/commands/meeting/export.ts
|
|
1367
|
-
var
|
|
1559
|
+
var debug19 = createGranolaDebug("cmd:meeting:export");
|
|
1368
1560
|
function createExportCommand() {
|
|
1369
1561
|
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
|
-
|
|
1562
|
+
debug19("export command invoked with id: %s, format: %s", id, options.format);
|
|
1371
1563
|
const format = options.format;
|
|
1372
1564
|
if (format !== "json" && format !== "toon") {
|
|
1373
1565
|
console.error(chalk11.red(`Invalid format: ${options.format}. Use 'json' or 'toon'.`));
|
|
@@ -1417,10 +1609,132 @@ var exportCommand = createExportCommand();
|
|
|
1417
1609
|
// src/commands/meeting/list.ts
|
|
1418
1610
|
import chalk12 from "chalk";
|
|
1419
1611
|
import { Command as Command12 } from "commander";
|
|
1420
|
-
|
|
1612
|
+
|
|
1613
|
+
// src/lib/date-parser.ts
|
|
1614
|
+
var debug20 = createGranolaDebug("lib:date-parser");
|
|
1615
|
+
var MONTH_NAMES = {
|
|
1616
|
+
jan: 0,
|
|
1617
|
+
january: 0,
|
|
1618
|
+
feb: 1,
|
|
1619
|
+
february: 1,
|
|
1620
|
+
mar: 2,
|
|
1621
|
+
march: 2,
|
|
1622
|
+
apr: 3,
|
|
1623
|
+
april: 3,
|
|
1624
|
+
may: 4,
|
|
1625
|
+
jun: 5,
|
|
1626
|
+
june: 5,
|
|
1627
|
+
jul: 6,
|
|
1628
|
+
july: 6,
|
|
1629
|
+
aug: 7,
|
|
1630
|
+
august: 7,
|
|
1631
|
+
sep: 8,
|
|
1632
|
+
september: 8,
|
|
1633
|
+
oct: 9,
|
|
1634
|
+
october: 9,
|
|
1635
|
+
nov: 10,
|
|
1636
|
+
november: 10,
|
|
1637
|
+
dec: 11,
|
|
1638
|
+
december: 11
|
|
1639
|
+
};
|
|
1640
|
+
function addDays(date, days) {
|
|
1641
|
+
const result = new Date(date);
|
|
1642
|
+
result.setDate(result.getDate() + days);
|
|
1643
|
+
return result;
|
|
1644
|
+
}
|
|
1645
|
+
function addMonths(date, months) {
|
|
1646
|
+
const result = new Date(date);
|
|
1647
|
+
result.setMonth(result.getMonth() + months);
|
|
1648
|
+
return result;
|
|
1649
|
+
}
|
|
1650
|
+
function startOfDay2(date) {
|
|
1651
|
+
const result = new Date(date);
|
|
1652
|
+
result.setHours(0, 0, 0, 0);
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
function parseDate(input) {
|
|
1656
|
+
const normalized = input.trim().toLowerCase();
|
|
1657
|
+
debug20("parsing date: %s", normalized);
|
|
1658
|
+
if (normalized === "today") {
|
|
1659
|
+
return startOfDay2(/* @__PURE__ */ new Date());
|
|
1660
|
+
}
|
|
1661
|
+
if (normalized === "yesterday") {
|
|
1662
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -1));
|
|
1663
|
+
}
|
|
1664
|
+
if (normalized === "tomorrow") {
|
|
1665
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), 1));
|
|
1666
|
+
}
|
|
1667
|
+
if (normalized === "last week") {
|
|
1668
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -7));
|
|
1669
|
+
}
|
|
1670
|
+
if (normalized === "last month") {
|
|
1671
|
+
return startOfDay2(addMonths(/* @__PURE__ */ new Date(), -1));
|
|
1672
|
+
}
|
|
1673
|
+
const daysAgoMatch = normalized.match(/^(\d+)\s+days?\s+ago$/);
|
|
1674
|
+
if (daysAgoMatch) {
|
|
1675
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -Number.parseInt(daysAgoMatch[1], 10)));
|
|
1676
|
+
}
|
|
1677
|
+
const weeksAgoMatch = normalized.match(/^(\d+)\s+weeks?\s+ago$/);
|
|
1678
|
+
if (weeksAgoMatch) {
|
|
1679
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -Number.parseInt(weeksAgoMatch[1], 10) * 7));
|
|
1680
|
+
}
|
|
1681
|
+
const monthsAgoMatch = normalized.match(/^(\d+)\s+months?\s+ago$/);
|
|
1682
|
+
if (monthsAgoMatch) {
|
|
1683
|
+
return startOfDay2(addMonths(/* @__PURE__ */ new Date(), -Number.parseInt(monthsAgoMatch[1], 10)));
|
|
1684
|
+
}
|
|
1685
|
+
const isoMatch = input.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
|
|
1686
|
+
if (isoMatch) {
|
|
1687
|
+
const year = Number.parseInt(isoMatch[1], 10);
|
|
1688
|
+
const month = Number.parseInt(isoMatch[2], 10) - 1;
|
|
1689
|
+
const day = Number.parseInt(isoMatch[3], 10);
|
|
1690
|
+
const date = new Date(year, month, day);
|
|
1691
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1692
|
+
return startOfDay2(date);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const monthDayMatch = normalized.match(/^([a-z]+)\s+(\d{1,2})(?:\s+(\d{4}))?$/);
|
|
1696
|
+
if (monthDayMatch) {
|
|
1697
|
+
const monthNum = MONTH_NAMES[monthDayMatch[1]];
|
|
1698
|
+
if (monthNum !== void 0) {
|
|
1699
|
+
const day = Number.parseInt(monthDayMatch[2], 10);
|
|
1700
|
+
const year = monthDayMatch[3] ? Number.parseInt(monthDayMatch[3], 10) : (/* @__PURE__ */ new Date()).getFullYear();
|
|
1701
|
+
const date = new Date(year, monthNum, day);
|
|
1702
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1703
|
+
return startOfDay2(date);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const dayMonthMatch = normalized.match(/^(\d{1,2})\s+([a-z]+)(?:\s+(\d{4}))?$/);
|
|
1708
|
+
if (dayMonthMatch) {
|
|
1709
|
+
const monthNum = MONTH_NAMES[dayMonthMatch[2]];
|
|
1710
|
+
if (monthNum !== void 0) {
|
|
1711
|
+
const day = Number.parseInt(dayMonthMatch[1], 10);
|
|
1712
|
+
const year = dayMonthMatch[3] ? Number.parseInt(dayMonthMatch[3], 10) : (/* @__PURE__ */ new Date()).getFullYear();
|
|
1713
|
+
const date = new Date(year, monthNum, day);
|
|
1714
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1715
|
+
return startOfDay2(date);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
debug20("failed to parse date: %s", input);
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
function validateDateOption(value, optionName) {
|
|
1723
|
+
const parsed = parseDate(value);
|
|
1724
|
+
if (!parsed) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
`Invalid date for ${optionName}: "${value}". Try formats like: today, yesterday, last week, 2024-01-15, "Dec 20"`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
debug20("validated %s: %s -> %s", optionName, value, parsed.toISOString());
|
|
1730
|
+
return parsed;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/commands/meeting/list.ts
|
|
1734
|
+
var debug21 = createGranolaDebug("cmd:meeting:list");
|
|
1421
1735
|
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
|
-
|
|
1736
|
+
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("-s, --search <query>", "Search in meeting titles").option("-a, --attendee <name>", "Filter by attendee name or email").option("-d, --date <date>", "Filter meetings on a specific date").option("--since <date>", "Filter meetings from date (inclusive)").option("--until <date>", "Filter meetings up to date (inclusive)").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
|
|
1737
|
+
debug21("list command invoked with opts: %O", opts);
|
|
1424
1738
|
const limit = Number.parseInt(opts.limit, 10);
|
|
1425
1739
|
if (!Number.isFinite(limit) || limit < 1) {
|
|
1426
1740
|
console.error(chalk12.red("Invalid --limit value. Please provide a positive number."));
|
|
@@ -1428,14 +1742,40 @@ function createListCommand2() {
|
|
|
1428
1742
|
}
|
|
1429
1743
|
const configuredWorkspace = getConfigValue("default_workspace");
|
|
1430
1744
|
const workspace = opts.workspace ?? configuredWorkspace;
|
|
1745
|
+
let date;
|
|
1746
|
+
let since;
|
|
1747
|
+
let until;
|
|
1748
|
+
try {
|
|
1749
|
+
if (opts.date) {
|
|
1750
|
+
date = validateDateOption(opts.date, "--date");
|
|
1751
|
+
}
|
|
1752
|
+
if (opts.since) {
|
|
1753
|
+
since = validateDateOption(opts.since, "--since");
|
|
1754
|
+
}
|
|
1755
|
+
if (opts.until) {
|
|
1756
|
+
until = validateDateOption(opts.until, "--until");
|
|
1757
|
+
}
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
console.error(chalk12.red(err.message));
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
if (since && until && since > until) {
|
|
1763
|
+
console.error(chalk12.red("--since date must be before --until date"));
|
|
1764
|
+
process.exit(1);
|
|
1765
|
+
}
|
|
1431
1766
|
const data = await list2({
|
|
1432
1767
|
limit,
|
|
1433
1768
|
workspace,
|
|
1434
|
-
folder: opts.folder
|
|
1769
|
+
folder: opts.folder,
|
|
1770
|
+
search: opts.search,
|
|
1771
|
+
attendee: opts.attendee,
|
|
1772
|
+
date,
|
|
1773
|
+
since,
|
|
1774
|
+
until
|
|
1435
1775
|
});
|
|
1436
|
-
|
|
1776
|
+
debug21("fetched %d meetings", data.length);
|
|
1437
1777
|
const format = opts.output || null;
|
|
1438
|
-
|
|
1778
|
+
debug21("output format: %s", format || "table");
|
|
1439
1779
|
if (format) {
|
|
1440
1780
|
if (!["json", "yaml", "toon"].includes(format)) {
|
|
1441
1781
|
console.error(chalk12.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
|
|
@@ -1463,10 +1803,10 @@ var listCommand2 = createListCommand2();
|
|
|
1463
1803
|
// src/commands/meeting/notes.ts
|
|
1464
1804
|
import chalk13 from "chalk";
|
|
1465
1805
|
import { Command as Command13 } from "commander";
|
|
1466
|
-
var
|
|
1806
|
+
var debug22 = createGranolaDebug("cmd:meeting:notes");
|
|
1467
1807
|
function createNotesCommand() {
|
|
1468
1808
|
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
|
-
|
|
1809
|
+
debug22("notes command invoked with id: %s", id);
|
|
1470
1810
|
const global = cmd.optsWithGlobals();
|
|
1471
1811
|
let fullId;
|
|
1472
1812
|
try {
|
|
@@ -1484,7 +1824,7 @@ function createNotesCommand() {
|
|
|
1484
1824
|
try {
|
|
1485
1825
|
notes = await getNotes(fullId);
|
|
1486
1826
|
} catch (error) {
|
|
1487
|
-
|
|
1827
|
+
debug22("failed to load notes: %O", error);
|
|
1488
1828
|
console.error(chalk13.red("Error:"), "Failed to fetch notes.");
|
|
1489
1829
|
if (error instanceof Error) {
|
|
1490
1830
|
console.error(chalk13.dim(error.message));
|
|
@@ -1522,17 +1862,17 @@ import chalk14 from "chalk";
|
|
|
1522
1862
|
import { Command as Command14 } from "commander";
|
|
1523
1863
|
|
|
1524
1864
|
// src/lib/transcript.ts
|
|
1525
|
-
var
|
|
1865
|
+
var debug23 = createGranolaDebug("lib:transcript");
|
|
1526
1866
|
function formatTranscript(utterances, opts = {}) {
|
|
1527
|
-
|
|
1867
|
+
debug23("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
|
|
1528
1868
|
const { timestamps = false, source = "all" } = opts;
|
|
1529
1869
|
let filtered = utterances;
|
|
1530
1870
|
if (source !== "all") {
|
|
1531
1871
|
filtered = utterances.filter((u) => u.source === source);
|
|
1532
|
-
|
|
1872
|
+
debug23("filtered to %d utterances (source=%s)", filtered.length, source);
|
|
1533
1873
|
}
|
|
1534
1874
|
if (filtered.length === 0) {
|
|
1535
|
-
|
|
1875
|
+
debug23("no transcript available");
|
|
1536
1876
|
return "No transcript available.";
|
|
1537
1877
|
}
|
|
1538
1878
|
const lines = [];
|
|
@@ -1559,11 +1899,11 @@ function formatTimestamp(iso) {
|
|
|
1559
1899
|
}
|
|
1560
1900
|
|
|
1561
1901
|
// src/commands/meeting/transcript.ts
|
|
1562
|
-
var
|
|
1902
|
+
var debug24 = createGranolaDebug("cmd:meeting:transcript");
|
|
1563
1903
|
var SOURCE_OPTIONS = /* @__PURE__ */ new Set(["microphone", "system", "all"]);
|
|
1564
1904
|
function createTranscriptCommand() {
|
|
1565
1905
|
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
|
-
|
|
1906
|
+
debug24("transcript command invoked with id: %s, opts: %O", id, opts);
|
|
1567
1907
|
const global = cmd.optsWithGlobals();
|
|
1568
1908
|
let fullId;
|
|
1569
1909
|
try {
|
|
@@ -1617,11 +1957,11 @@ var transcriptCommand = createTranscriptCommand();
|
|
|
1617
1957
|
// src/commands/meeting/view.ts
|
|
1618
1958
|
import chalk15 from "chalk";
|
|
1619
1959
|
import { Command as Command15 } from "commander";
|
|
1620
|
-
import
|
|
1621
|
-
var
|
|
1960
|
+
import open2 from "open";
|
|
1961
|
+
var debug25 = createGranolaDebug("cmd:meeting:view");
|
|
1622
1962
|
function createViewCommand2() {
|
|
1623
1963
|
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
|
-
|
|
1964
|
+
debug25("view command invoked with id: %s, opts: %O", id, opts);
|
|
1625
1965
|
let fullId;
|
|
1626
1966
|
try {
|
|
1627
1967
|
const resolved = await resolveId(id);
|
|
@@ -1635,7 +1975,7 @@ function createViewCommand2() {
|
|
|
1635
1975
|
process.exit(1);
|
|
1636
1976
|
}
|
|
1637
1977
|
if (opts.web) {
|
|
1638
|
-
await
|
|
1978
|
+
await open2(`https://notes.granola.ai/d/${fullId}`);
|
|
1639
1979
|
return;
|
|
1640
1980
|
}
|
|
1641
1981
|
const meeting = await get2(fullId);
|
|
@@ -1686,14 +2026,14 @@ import chalk16 from "chalk";
|
|
|
1686
2026
|
import { Command as Command17 } from "commander";
|
|
1687
2027
|
|
|
1688
2028
|
// src/services/workspaces.ts
|
|
1689
|
-
var
|
|
2029
|
+
var debug26 = createGranolaDebug("service:workspaces");
|
|
1690
2030
|
async function list3() {
|
|
1691
2031
|
return withTokenRefresh(async () => {
|
|
1692
|
-
|
|
2032
|
+
debug26("fetching workspaces");
|
|
1693
2033
|
const client2 = await getClient();
|
|
1694
2034
|
const res = await client2.getWorkspaces();
|
|
1695
2035
|
const workspacesArray = res?.workspaces || [];
|
|
1696
|
-
|
|
2036
|
+
debug26("found %d workspaces", workspacesArray.length);
|
|
1697
2037
|
return workspacesArray.map((item) => {
|
|
1698
2038
|
const ws = item.workspace;
|
|
1699
2039
|
return {
|
|
@@ -1706,35 +2046,35 @@ async function list3() {
|
|
|
1706
2046
|
});
|
|
1707
2047
|
}
|
|
1708
2048
|
async function resolveId2(partialId) {
|
|
1709
|
-
|
|
2049
|
+
debug26("resolving workspace id: %s", partialId);
|
|
1710
2050
|
const workspaces = await list3();
|
|
1711
2051
|
const matches = workspaces.filter((w) => w.id.startsWith(partialId));
|
|
1712
2052
|
if (matches.length === 0) {
|
|
1713
|
-
|
|
2053
|
+
debug26("no workspace found for id: %s", partialId);
|
|
1714
2054
|
return null;
|
|
1715
2055
|
}
|
|
1716
2056
|
if (matches.length > 1) {
|
|
1717
|
-
|
|
2057
|
+
debug26("ambiguous id: %s matches %d workspaces", partialId, matches.length);
|
|
1718
2058
|
throw new Error(`Ambiguous ID: ${partialId} matches ${matches.length} workspaces`);
|
|
1719
2059
|
}
|
|
1720
|
-
|
|
2060
|
+
debug26("resolved workspace: %s -> %s", partialId, matches[0].id);
|
|
1721
2061
|
return matches[0].id;
|
|
1722
2062
|
}
|
|
1723
2063
|
async function get3(id) {
|
|
1724
|
-
|
|
2064
|
+
debug26("getting workspace: %s", id);
|
|
1725
2065
|
const workspaces = await list3();
|
|
1726
2066
|
const workspace = workspaces.find((w) => w.id === id) || null;
|
|
1727
|
-
|
|
2067
|
+
debug26("workspace %s: %s", id, workspace ? "found" : "not found");
|
|
1728
2068
|
return workspace;
|
|
1729
2069
|
}
|
|
1730
2070
|
|
|
1731
2071
|
// src/commands/workspace/list.ts
|
|
1732
|
-
var
|
|
2072
|
+
var debug27 = createGranolaDebug("cmd:workspace:list");
|
|
1733
2073
|
function createListCommand3() {
|
|
1734
2074
|
return new Command17("list").description("List workspaces").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
|
|
1735
|
-
|
|
2075
|
+
debug27("workspace list command invoked");
|
|
1736
2076
|
const data = await list3();
|
|
1737
|
-
|
|
2077
|
+
debug27("fetched %d workspaces", data.length);
|
|
1738
2078
|
const format = opts.output || null;
|
|
1739
2079
|
if (format) {
|
|
1740
2080
|
if (!["json", "yaml", "toon"].includes(format)) {
|
|
@@ -1761,10 +2101,10 @@ var listCommand3 = createListCommand3();
|
|
|
1761
2101
|
// src/commands/workspace/view.ts
|
|
1762
2102
|
import chalk17 from "chalk";
|
|
1763
2103
|
import { Command as Command18 } from "commander";
|
|
1764
|
-
var
|
|
2104
|
+
var debug28 = createGranolaDebug("cmd:workspace:view");
|
|
1765
2105
|
function createViewCommand3() {
|
|
1766
2106
|
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
|
-
|
|
2107
|
+
debug28("workspace view command invoked with id: %s", id);
|
|
1768
2108
|
let fullId;
|
|
1769
2109
|
try {
|
|
1770
2110
|
const resolved = await resolveId2(id);
|
|
@@ -1802,13 +2142,36 @@ var viewCommand3 = createViewCommand3();
|
|
|
1802
2142
|
// src/commands/workspace/index.ts
|
|
1803
2143
|
var workspaceCommand = new Command19("workspace").description("Work with workspaces").addCommand(listCommand3).addCommand(viewCommand3);
|
|
1804
2144
|
|
|
2145
|
+
// src/lib/errors.ts
|
|
2146
|
+
import chalk18 from "chalk";
|
|
2147
|
+
function handleGlobalError(error) {
|
|
2148
|
+
if (error instanceof ApiError) {
|
|
2149
|
+
if (error.status === 401) {
|
|
2150
|
+
console.error(chalk18.red("Error:"), "Authentication required.");
|
|
2151
|
+
console.error(`Run ${chalk18.cyan("granola auth login")} to authenticate.`);
|
|
2152
|
+
return 2;
|
|
2153
|
+
}
|
|
2154
|
+
console.error(chalk18.red("Error:"), error.message);
|
|
2155
|
+
return 1;
|
|
2156
|
+
}
|
|
2157
|
+
if (error instanceof Error && error.message.includes("fetch failed")) {
|
|
2158
|
+
console.error(chalk18.red("Error:"), "Network error. Check your connection.");
|
|
2159
|
+
return 1;
|
|
2160
|
+
}
|
|
2161
|
+
if (error instanceof Error) {
|
|
2162
|
+
console.error(chalk18.red("Error:"), error.message || "An unexpected error occurred.");
|
|
2163
|
+
} else {
|
|
2164
|
+
console.error(chalk18.red("Error:"), "An unexpected error occurred.");
|
|
2165
|
+
}
|
|
2166
|
+
return 1;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1805
2169
|
// src/main.ts
|
|
1806
|
-
var
|
|
2170
|
+
var debug29 = createGranolaDebug("cli");
|
|
1807
2171
|
var debugAlias = createGranolaDebug("cli:alias");
|
|
1808
|
-
var debugSubcmd = createGranolaDebug("cli:subcommand");
|
|
1809
2172
|
var packageJson = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1810
|
-
|
|
1811
|
-
|
|
2173
|
+
debug29("granola-cli v%s starting", packageJson.version);
|
|
2174
|
+
debug29("arguments: %O", process.argv.slice(2));
|
|
1812
2175
|
var program = new Command20();
|
|
1813
2176
|
program.name("granola").description("CLI for Granola meeting notes").version(packageJson.version).option("--no-pager", "Disable pager");
|
|
1814
2177
|
program.addCommand(authCommand);
|
|
@@ -1817,57 +2180,6 @@ program.addCommand(workspaceCommand);
|
|
|
1817
2180
|
program.addCommand(folderCommand);
|
|
1818
2181
|
program.addCommand(configCommand);
|
|
1819
2182
|
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
2183
|
function expandAlias(args) {
|
|
1872
2184
|
if (args.length < 3) return args;
|
|
1873
2185
|
const command = args[2];
|
|
@@ -1888,6 +2200,9 @@ function expandAlias(args) {
|
|
|
1888
2200
|
return args;
|
|
1889
2201
|
}
|
|
1890
2202
|
var expandedArgs = expandAlias(process.argv);
|
|
1891
|
-
|
|
1892
|
-
program.parseAsync(expandedArgs)
|
|
2203
|
+
debug29("parsing with args: %O", expandedArgs.slice(2));
|
|
2204
|
+
program.parseAsync(expandedArgs).catch((error) => {
|
|
2205
|
+
const exitCode = handleGlobalError(error);
|
|
2206
|
+
process.exit(exitCode);
|
|
2207
|
+
});
|
|
1893
2208
|
//# sourceMappingURL=main.js.map
|