granola-toolkit 0.14.0 → 0.16.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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/dist/cli.js +372 -138
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # granola-toolkit
2
2
 
3
- General toolkit to do more with Granola notes and transcripts.
3
+ Toolkit for working with Granola meetings, notes, and transcripts.
4
4
 
5
5
  ## Install
6
6
 
@@ -15,6 +15,7 @@ Without a global install:
15
15
 
16
16
  ```bash
17
17
  npx granola-toolkit --help
18
+ npx granola-toolkit meeting --help
18
19
  ```
19
20
 
20
21
  For local development:
@@ -27,7 +28,7 @@ vp install
27
28
 
28
29
  ## Run
29
30
 
30
- Installed CLI:
31
+ Installed command:
31
32
 
32
33
  ```bash
33
34
  granola --help
@@ -37,6 +38,8 @@ granola notes --help
37
38
  granola transcripts --help
38
39
  ```
39
40
 
41
+ The published package exposes both `granola` and `granola-toolkit` as executable names.
42
+
40
43
  Local build:
41
44
 
42
45
  ```bash
package/dist/cli.js CHANGED
@@ -689,96 +689,34 @@ var AuthenticatedHttpClient = class {
689
689
  };
690
690
  //#endregion
691
691
  //#region src/client/default.ts
692
- async function createDefaultGranolaApiClient(config, logger = console) {
692
+ async function inspectDefaultGranolaAuth(config) {
693
+ const storedSession = await createDefaultSessionStore().readSession();
694
+ const hasStoredSession = Boolean(storedSession?.accessToken.trim());
695
+ return {
696
+ mode: hasStoredSession ? "stored-session" : "supabase-file",
697
+ storedSessionAvailable: hasStoredSession,
698
+ supabasePath: config.supabase || void 0
699
+ };
700
+ }
701
+ async function createDefaultGranolaRuntime(config, logger = console) {
693
702
  const sessionStore = createDefaultSessionStore();
694
- const storedSession = await sessionStore.readSession();
695
- if (!storedSession && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
696
- if (!storedSession && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
697
- return new GranolaApiClient(new AuthenticatedHttpClient({
698
- logger,
699
- tokenProvider: storedSession ? new StoredSessionTokenProvider(sessionStore, { source: config.supabase && existsSync(config.supabase) ? new SupabaseFileSessionSource(config.supabase) : void 0 }) : new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore())
700
- }));
703
+ const auth = await inspectDefaultGranolaAuth(config);
704
+ const hasStoredSession = auth.storedSessionAvailable;
705
+ if (!hasStoredSession && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
706
+ if (!hasStoredSession && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
707
+ return {
708
+ auth,
709
+ client: new GranolaApiClient(new AuthenticatedHttpClient({
710
+ logger,
711
+ tokenProvider: hasStoredSession ? new StoredSessionTokenProvider(sessionStore, { source: config.supabase && existsSync(config.supabase) ? new SupabaseFileSessionSource(config.supabase) : void 0 }) : new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore())
712
+ }))
713
+ };
701
714
  }
702
715
  async function loadOptionalGranolaCache(cacheFile) {
703
716
  if (!cacheFile || !existsSync(cacheFile)) return;
704
717
  return parseCacheContents(await readFile(cacheFile, "utf8"));
705
718
  }
706
719
  //#endregion
707
- //#region src/config.ts
708
- function pickString(value) {
709
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
710
- }
711
- function pickBoolean(value) {
712
- return typeof value === "boolean" ? value : void 0;
713
- }
714
- function parseTomlScalar(rawValue) {
715
- const value = rawValue.trim();
716
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) {
717
- if (value.startsWith("\"")) try {
718
- return JSON.parse(value);
719
- } catch {
720
- return value.slice(1, -1);
721
- }
722
- return value.slice(1, -1);
723
- }
724
- if (/^(true|false)$/i.test(value)) return value.toLowerCase() === "true";
725
- if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
726
- return value;
727
- }
728
- function parseSimpleToml(contents) {
729
- const values = {};
730
- for (const rawLine of contents.split(/\r?\n/)) {
731
- const line = rawLine.trim();
732
- if (!line || line.startsWith("#")) continue;
733
- const match = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
734
- if (!match) continue;
735
- const [, key = "", rawValue = ""] = match;
736
- values[key] = parseTomlScalar(rawValue);
737
- }
738
- return values;
739
- }
740
- async function loadTomlConfig(configPath) {
741
- if (configPath) {
742
- if (!existsSync(configPath)) throw new Error(`config file not found: ${configPath}`);
743
- return {
744
- path: configPath,
745
- values: parseSimpleToml(await readUtf8(configPath))
746
- };
747
- }
748
- const candidates = [join(process.cwd(), ".granola.toml"), join(homedir(), ".granola.toml")];
749
- for (const candidate of candidates) if (existsSync(candidate)) return {
750
- path: candidate,
751
- values: parseSimpleToml(await readUtf8(candidate))
752
- };
753
- return { values: {} };
754
- }
755
- function envFlag(value) {
756
- if (value == null) return;
757
- if (/^(1|true|yes|on)$/i.test(value)) return true;
758
- if (/^(0|false|no|off)$/i.test(value)) return false;
759
- }
760
- async function loadConfig(options) {
761
- const env = options.env ?? process.env;
762
- const config = await loadTomlConfig(pickString(options.globalFlags.config));
763
- const configValues = config.values;
764
- const defaultSupabase = firstExistingPath(granolaSupabaseCandidates());
765
- const defaultCache = firstExistingPath(granolaCacheCandidates());
766
- const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
767
- return {
768
- configFileUsed: config.path,
769
- debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
770
- notes: {
771
- output: pickString(options.subcommandFlags.output) ?? pickString(env.OUTPUT) ?? pickString(configValues.output) ?? "./notes",
772
- timeoutMs: parseDuration(timeoutValue)
773
- },
774
- supabase: pickString(options.globalFlags.supabase) ?? pickString(env.SUPABASE_FILE) ?? pickString(configValues.supabase) ?? defaultSupabase,
775
- transcripts: {
776
- cacheFile: pickString(options.subcommandFlags.cache) ?? pickString(env.CACHE_FILE) ?? pickString(configValues["cache-file"]) ?? pickString(configValues.cacheFile) ?? defaultCache ?? "",
777
- output: pickString(options.subcommandFlags.output) ?? pickString(env.TRANSCRIPT_OUTPUT) ?? pickString(configValues["transcript-output"]) ?? pickString(configValues.transcriptOutput) ?? "./transcripts"
778
- }
779
- };
780
- }
781
- //#endregion
782
720
  //#region src/export-state.ts
783
721
  const EXPORT_STATE_VERSION = 1;
784
722
  function exportStatePath(outputDir, kind) {
@@ -1524,6 +1462,285 @@ function renderMeetingTranscript(document, cacheData, format = "text") {
1524
1462
  return renderTranscriptExport(transcript, format);
1525
1463
  }
1526
1464
  //#endregion
1465
+ //#region src/app/core.ts
1466
+ function transcriptCount(cacheData) {
1467
+ return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
1468
+ }
1469
+ function cloneExportState(state) {
1470
+ return state ? { ...state } : void 0;
1471
+ }
1472
+ function cloneState(state) {
1473
+ return {
1474
+ auth: { ...state.auth },
1475
+ cache: { ...state.cache },
1476
+ config: {
1477
+ ...state.config,
1478
+ notes: { ...state.config.notes },
1479
+ transcripts: { ...state.config.transcripts }
1480
+ },
1481
+ documents: { ...state.documents },
1482
+ exports: {
1483
+ notes: cloneExportState(state.exports.notes),
1484
+ transcripts: cloneExportState(state.exports.transcripts)
1485
+ },
1486
+ ui: { ...state.ui }
1487
+ };
1488
+ }
1489
+ function defaultState(config, auth, surface) {
1490
+ return {
1491
+ auth: { ...auth },
1492
+ cache: {
1493
+ configured: Boolean(config.transcripts.cacheFile),
1494
+ documentCount: 0,
1495
+ filePath: config.transcripts.cacheFile || void 0,
1496
+ loaded: false,
1497
+ transcriptCount: 0
1498
+ },
1499
+ config: {
1500
+ ...config,
1501
+ notes: { ...config.notes },
1502
+ transcripts: { ...config.transcripts }
1503
+ },
1504
+ documents: {
1505
+ count: 0,
1506
+ loaded: false
1507
+ },
1508
+ exports: {},
1509
+ ui: {
1510
+ surface,
1511
+ view: "idle"
1512
+ }
1513
+ };
1514
+ }
1515
+ var GranolaApp = class {
1516
+ #cacheData;
1517
+ #cacheResolved = false;
1518
+ #granolaClient;
1519
+ #documents;
1520
+ #state;
1521
+ constructor(config, deps, options = {}) {
1522
+ this.config = config;
1523
+ this.deps = deps;
1524
+ this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
1525
+ }
1526
+ getState() {
1527
+ return cloneState(this.#state);
1528
+ }
1529
+ setUiState(patch) {
1530
+ this.#state.ui = {
1531
+ ...this.#state.ui,
1532
+ ...patch
1533
+ };
1534
+ return this.getState();
1535
+ }
1536
+ nowIso() {
1537
+ return (this.deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
1538
+ }
1539
+ async getGranolaClient() {
1540
+ if (this.#granolaClient) return this.#granolaClient;
1541
+ if (this.deps.granolaClient) {
1542
+ this.#granolaClient = this.deps.granolaClient;
1543
+ return this.#granolaClient;
1544
+ }
1545
+ if (!this.deps.createGranolaClient) throw new Error("Granola API client is not configured");
1546
+ const runtime = await this.deps.createGranolaClient();
1547
+ this.#granolaClient = runtime.client;
1548
+ this.#state.auth = { ...runtime.auth };
1549
+ return this.#granolaClient;
1550
+ }
1551
+ missingCacheError() {
1552
+ return /* @__PURE__ */ new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
1553
+ }
1554
+ async listDocuments() {
1555
+ if (this.#documents) return this.#documents;
1556
+ const documents = await (await this.getGranolaClient()).listDocuments({ timeoutMs: this.config.notes.timeoutMs });
1557
+ this.#documents = documents;
1558
+ this.#state.documents = {
1559
+ count: documents.length,
1560
+ loaded: true,
1561
+ loadedAt: this.nowIso()
1562
+ };
1563
+ return documents;
1564
+ }
1565
+ async loadCache(options = {}) {
1566
+ if (this.#cacheResolved) {
1567
+ if (options.required && !this.#cacheData) throw this.missingCacheError();
1568
+ return this.#cacheData;
1569
+ }
1570
+ const cacheFile = this.config.transcripts.cacheFile || void 0;
1571
+ if (!cacheFile) {
1572
+ this.#cacheResolved = true;
1573
+ if (options.required) throw this.missingCacheError();
1574
+ return;
1575
+ }
1576
+ if (!existsSync(cacheFile)) throw new Error(`Granola cache file not found: ${cacheFile}`);
1577
+ const cacheData = await this.deps.cacheLoader(cacheFile);
1578
+ this.#cacheResolved = true;
1579
+ this.#cacheData = cacheData;
1580
+ this.#state.cache = {
1581
+ configured: true,
1582
+ documentCount: cacheData ? Object.keys(cacheData.documents).length : 0,
1583
+ filePath: cacheFile,
1584
+ loaded: Boolean(cacheData),
1585
+ loadedAt: cacheData ? this.nowIso() : void 0,
1586
+ transcriptCount: cacheData ? transcriptCount(cacheData) : 0
1587
+ };
1588
+ if (options.required && !cacheData) throw this.missingCacheError();
1589
+ return cacheData;
1590
+ }
1591
+ async listMeetings(options = {}) {
1592
+ const meetings = listMeetings(await this.listDocuments(), {
1593
+ cacheData: await this.loadCache(),
1594
+ limit: options.limit,
1595
+ search: options.search
1596
+ });
1597
+ this.setUiState({
1598
+ meetingSearch: options.search,
1599
+ selectedMeetingId: void 0,
1600
+ view: "meeting-list"
1601
+ });
1602
+ return meetings;
1603
+ }
1604
+ async getMeeting(id, options = {}) {
1605
+ const documents = await this.listDocuments();
1606
+ const cacheData = await this.loadCache({ required: options.requireCache });
1607
+ const document = resolveMeeting(documents, id);
1608
+ const meeting = buildMeetingRecord(document, cacheData);
1609
+ this.setUiState({
1610
+ selectedMeetingId: document.id,
1611
+ view: "meeting-detail"
1612
+ });
1613
+ return {
1614
+ cacheData,
1615
+ document,
1616
+ meeting
1617
+ };
1618
+ }
1619
+ async exportNotes(format = "markdown") {
1620
+ const documents = await this.listDocuments();
1621
+ const written = await writeNotes(documents, this.config.notes.output, format);
1622
+ this.#state.exports.notes = {
1623
+ format,
1624
+ itemCount: documents.length,
1625
+ outputDir: this.config.notes.output,
1626
+ ranAt: this.nowIso(),
1627
+ written
1628
+ };
1629
+ this.setUiState({ view: "notes-export" });
1630
+ return {
1631
+ documentCount: documents.length,
1632
+ documents,
1633
+ format,
1634
+ outputDir: this.config.notes.output,
1635
+ written
1636
+ };
1637
+ }
1638
+ async exportTranscripts(format = "text") {
1639
+ const cacheData = await this.loadCache({ required: true });
1640
+ if (!cacheData) throw this.missingCacheError();
1641
+ const written = await writeTranscripts(cacheData, this.config.transcripts.output, format);
1642
+ const count = transcriptCount(cacheData);
1643
+ this.#state.exports.transcripts = {
1644
+ format,
1645
+ itemCount: count,
1646
+ outputDir: this.config.transcripts.output,
1647
+ ranAt: this.nowIso(),
1648
+ written
1649
+ };
1650
+ this.setUiState({ view: "transcripts-export" });
1651
+ return {
1652
+ cacheData,
1653
+ format,
1654
+ outputDir: this.config.transcripts.output,
1655
+ transcriptCount: count,
1656
+ written
1657
+ };
1658
+ }
1659
+ };
1660
+ async function createGranolaApp(config, options = {}) {
1661
+ return new GranolaApp(config, {
1662
+ auth: await inspectDefaultGranolaAuth(config),
1663
+ cacheLoader: loadOptionalGranolaCache,
1664
+ createGranolaClient: async () => await createDefaultGranolaRuntime(config, options.logger),
1665
+ now: options.now
1666
+ }, { surface: options.surface });
1667
+ }
1668
+ //#endregion
1669
+ //#region src/config.ts
1670
+ function pickString(value) {
1671
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1672
+ }
1673
+ function pickBoolean(value) {
1674
+ return typeof value === "boolean" ? value : void 0;
1675
+ }
1676
+ function parseTomlScalar(rawValue) {
1677
+ const value = rawValue.trim();
1678
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) {
1679
+ if (value.startsWith("\"")) try {
1680
+ return JSON.parse(value);
1681
+ } catch {
1682
+ return value.slice(1, -1);
1683
+ }
1684
+ return value.slice(1, -1);
1685
+ }
1686
+ if (/^(true|false)$/i.test(value)) return value.toLowerCase() === "true";
1687
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
1688
+ return value;
1689
+ }
1690
+ function parseSimpleToml(contents) {
1691
+ const values = {};
1692
+ for (const rawLine of contents.split(/\r?\n/)) {
1693
+ const line = rawLine.trim();
1694
+ if (!line || line.startsWith("#")) continue;
1695
+ const match = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
1696
+ if (!match) continue;
1697
+ const [, key = "", rawValue = ""] = match;
1698
+ values[key] = parseTomlScalar(rawValue);
1699
+ }
1700
+ return values;
1701
+ }
1702
+ async function loadTomlConfig(configPath) {
1703
+ if (configPath) {
1704
+ if (!existsSync(configPath)) throw new Error(`config file not found: ${configPath}`);
1705
+ return {
1706
+ path: configPath,
1707
+ values: parseSimpleToml(await readUtf8(configPath))
1708
+ };
1709
+ }
1710
+ const candidates = [join(process.cwd(), ".granola.toml"), join(homedir(), ".granola.toml")];
1711
+ for (const candidate of candidates) if (existsSync(candidate)) return {
1712
+ path: candidate,
1713
+ values: parseSimpleToml(await readUtf8(candidate))
1714
+ };
1715
+ return { values: {} };
1716
+ }
1717
+ function envFlag(value) {
1718
+ if (value == null) return;
1719
+ if (/^(1|true|yes|on)$/i.test(value)) return true;
1720
+ if (/^(0|false|no|off)$/i.test(value)) return false;
1721
+ }
1722
+ async function loadConfig(options) {
1723
+ const env = options.env ?? process.env;
1724
+ const config = await loadTomlConfig(pickString(options.globalFlags.config));
1725
+ const configValues = config.values;
1726
+ const defaultSupabase = firstExistingPath(granolaSupabaseCandidates());
1727
+ const defaultCache = firstExistingPath(granolaCacheCandidates());
1728
+ const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
1729
+ return {
1730
+ configFileUsed: config.path,
1731
+ debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
1732
+ notes: {
1733
+ output: pickString(options.subcommandFlags.output) ?? pickString(env.OUTPUT) ?? pickString(configValues.output) ?? "./notes",
1734
+ timeoutMs: parseDuration(timeoutValue)
1735
+ },
1736
+ supabase: pickString(options.globalFlags.supabase) ?? pickString(env.SUPABASE_FILE) ?? pickString(configValues.supabase) ?? defaultSupabase,
1737
+ transcripts: {
1738
+ cacheFile: pickString(options.subcommandFlags.cache) ?? pickString(env.CACHE_FILE) ?? pickString(configValues["cache-file"]) ?? pickString(configValues.cacheFile) ?? defaultCache ?? "",
1739
+ output: pickString(options.subcommandFlags.output) ?? pickString(env.TRANSCRIPT_OUTPUT) ?? pickString(configValues["transcript-output"]) ?? pickString(configValues.transcriptOutput) ?? "./transcripts"
1740
+ }
1741
+ };
1742
+ }
1743
+ //#endregion
1527
1744
  //#region src/commands/shared.ts
1528
1745
  function debug(enabled, ...values) {
1529
1746
  if (enabled) console.error("[debug]", ...values);
@@ -1643,41 +1860,22 @@ const meetingCommand = {
1643
1860
  }
1644
1861
  }
1645
1862
  };
1646
- async function loadMeetingData(commandFlags, globalFlags, options = {}) {
1863
+ async function list(commandFlags, globalFlags) {
1864
+ const format = resolveListFormat(commandFlags.format);
1865
+ const limit = parseLimit(commandFlags.limit);
1866
+ const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
1647
1867
  const config = await loadConfig({
1648
1868
  globalFlags,
1649
1869
  subcommandFlags: commandFlags
1650
1870
  });
1651
- if (options.requireCache && !config.transcripts.cacheFile) throw new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
1652
- if (config.transcripts.cacheFile && !existsSync(config.transcripts.cacheFile)) throw new Error(`Granola cache file not found: ${config.transcripts.cacheFile}`);
1653
1871
  debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1654
1872
  debug(config.debug, "supabase", config.supabase);
1655
1873
  debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
1656
1874
  debug(config.debug, "timeoutMs", config.notes.timeoutMs);
1657
- const granolaClient = await createDefaultGranolaApiClient(config);
1658
- return {
1659
- cacheData: await loadOptionalGranolaCache(config.transcripts.cacheFile),
1660
- config,
1661
- granolaClient
1662
- };
1663
- }
1664
- async function loadResolvedMeeting(id, commandFlags, globalFlags, options = {}) {
1665
- const { cacheData, config, granolaClient } = await loadMeetingData(commandFlags, globalFlags, options);
1666
- console.log("Fetching meeting from Granola API...");
1667
- return {
1668
- cacheData,
1669
- config,
1670
- document: resolveMeeting(await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs }), id)
1671
- };
1672
- }
1673
- async function list(commandFlags, globalFlags) {
1674
- const format = resolveListFormat(commandFlags.format);
1675
- const limit = parseLimit(commandFlags.limit);
1676
- const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
1677
- const { cacheData, config, granolaClient } = await loadMeetingData(commandFlags, globalFlags);
1875
+ const app = await createGranolaApp(config);
1876
+ debug(config.debug, "authMode", app.getState().auth.mode);
1678
1877
  console.log("Fetching meetings from Granola API...");
1679
- const meetings = listMeetings(await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs }), {
1680
- cacheData,
1878
+ const meetings = await app.listMeetings({
1681
1879
  limit,
1682
1880
  search
1683
1881
  });
@@ -1686,29 +1884,71 @@ async function list(commandFlags, globalFlags) {
1686
1884
  }
1687
1885
  async function view(id, commandFlags, globalFlags) {
1688
1886
  const format = resolveViewFormat(commandFlags.format);
1689
- const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
1690
- const meeting = buildMeetingRecord(document, cacheData);
1691
- console.log(renderMeetingView(meeting, format).trimEnd());
1887
+ const config = await loadConfig({
1888
+ globalFlags,
1889
+ subcommandFlags: commandFlags
1890
+ });
1891
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1892
+ debug(config.debug, "supabase", config.supabase);
1893
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
1894
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
1895
+ const app = await createGranolaApp(config);
1896
+ debug(config.debug, "authMode", app.getState().auth.mode);
1897
+ console.log("Fetching meeting from Granola API...");
1898
+ const result = await app.getMeeting(id);
1899
+ console.log(renderMeetingView(result.meeting, format).trimEnd());
1692
1900
  return 0;
1693
1901
  }
1694
1902
  async function exportMeeting(id, commandFlags, globalFlags) {
1695
1903
  const format = resolveExportFormat(commandFlags.format);
1696
- const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
1697
- const meeting = buildMeetingRecord(document, cacheData);
1698
- console.log(renderMeetingExport(meeting, format).trimEnd());
1904
+ const config = await loadConfig({
1905
+ globalFlags,
1906
+ subcommandFlags: commandFlags
1907
+ });
1908
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1909
+ debug(config.debug, "supabase", config.supabase);
1910
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
1911
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
1912
+ const app = await createGranolaApp(config);
1913
+ debug(config.debug, "authMode", app.getState().auth.mode);
1914
+ console.log("Fetching meeting from Granola API...");
1915
+ const result = await app.getMeeting(id);
1916
+ console.log(renderMeetingExport(result.meeting, format).trimEnd());
1699
1917
  return 0;
1700
1918
  }
1701
1919
  async function notes(id, commandFlags, globalFlags) {
1702
1920
  const format = resolveNotesFormat(commandFlags.format);
1703
- const { document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
1704
- console.log(renderMeetingNotes(document, format).trimEnd());
1921
+ const config = await loadConfig({
1922
+ globalFlags,
1923
+ subcommandFlags: commandFlags
1924
+ });
1925
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1926
+ debug(config.debug, "supabase", config.supabase);
1927
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
1928
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
1929
+ const app = await createGranolaApp(config);
1930
+ debug(config.debug, "authMode", app.getState().auth.mode);
1931
+ console.log("Fetching meeting from Granola API...");
1932
+ const result = await app.getMeeting(id);
1933
+ console.log(renderMeetingNotes(result.document, format).trimEnd());
1705
1934
  return 0;
1706
1935
  }
1707
1936
  async function transcript(id, commandFlags, globalFlags) {
1708
1937
  const format = resolveTranscriptFormat$1(commandFlags.format);
1709
- const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags, { requireCache: true });
1710
- const output = renderMeetingTranscript(document, cacheData, format);
1711
- if (!output.trim()) throw new Error(`no transcript found for meeting: ${document.id}`);
1938
+ const config = await loadConfig({
1939
+ globalFlags,
1940
+ subcommandFlags: commandFlags
1941
+ });
1942
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1943
+ debug(config.debug, "supabase", config.supabase);
1944
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
1945
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
1946
+ const app = await createGranolaApp(config);
1947
+ debug(config.debug, "authMode", app.getState().auth.mode);
1948
+ console.log("Fetching meeting from Granola API...");
1949
+ const result = await app.getMeeting(id, { requireCache: true });
1950
+ const output = renderMeetingTranscript(result.document, result.cacheData, format);
1951
+ if (!output.trim()) throw new Error(`no transcript found for meeting: ${result.document.id}`);
1712
1952
  console.log(output.trimEnd());
1713
1953
  return 0;
1714
1954
  }
@@ -1751,13 +1991,11 @@ const notesCommand = {
1751
1991
  debug(config.debug, "output", config.notes.output);
1752
1992
  const format = resolveNoteFormat(commandFlags.format);
1753
1993
  debug(config.debug, "format", format);
1754
- const granolaClient = await createDefaultGranolaApiClient(config);
1755
- console.log("Fetching documents from Granola API...");
1756
- const documents = await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs });
1757
- console.log(`Exporting ${documents.length} notes to ${config.notes.output}...`);
1758
- const written = await writeNotes(documents, config.notes.output, format);
1759
- console.log("✓ Export completed successfully");
1760
- debug(config.debug, "notes written", written);
1994
+ const app = await createGranolaApp(config);
1995
+ debug(config.debug, "authMode", app.getState().auth.mode);
1996
+ const result = await app.exportNotes(format);
1997
+ console.log(`✓ Exported ${result.documentCount} notes to ${result.outputDir}`);
1998
+ debug(config.debug, "notes written", result.written);
1761
1999
  return 0;
1762
2000
  }
1763
2001
  };
@@ -1803,20 +2041,16 @@ const transcriptsCommand = {
1803
2041
  globalFlags,
1804
2042
  subcommandFlags: commandFlags
1805
2043
  });
1806
- if (!config.transcripts.cacheFile) throw new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
1807
- if (!existsSync(config.transcripts.cacheFile)) throw new Error(`Granola cache file not found: ${config.transcripts.cacheFile}`);
1808
2044
  debug(config.debug, "using config", config.configFileUsed ?? "(none)");
1809
2045
  debug(config.debug, "cacheFile", config.transcripts.cacheFile);
1810
2046
  debug(config.debug, "output", config.transcripts.output);
1811
2047
  const format = resolveTranscriptFormat(commandFlags.format);
1812
2048
  debug(config.debug, "format", format);
1813
- console.log("Reading Granola cache file...");
1814
- const cacheData = parseCacheContents(await readFile(config.transcripts.cacheFile, "utf8"));
1815
- const transcriptCount = Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
1816
- console.log(`Exporting ${transcriptCount} transcripts to ${config.transcripts.output}...`);
1817
- const written = await writeTranscripts(cacheData, config.transcripts.output, format);
1818
- console.log("✓ Export completed successfully");
1819
- debug(config.debug, "transcripts written", written);
2049
+ const app = await createGranolaApp(config);
2050
+ debug(config.debug, "authMode", app.getState().auth.mode);
2051
+ const result = await app.exportTranscripts(format);
2052
+ console.log(`✓ Exported ${result.transcriptCount} transcripts to ${result.outputDir}`);
2053
+ debug(config.debug, "transcripts written", result.written);
1820
2054
  return 0;
1821
2055
  }
1822
2056
  };
@@ -1908,9 +2142,9 @@ function splitCommand(argv) {
1908
2142
  }
1909
2143
  function rootHelp() {
1910
2144
  const commandWidth = Math.max(...commands.map((command) => command.name.length));
1911
- return `Granola CLI
2145
+ return `Granola Toolkit
1912
2146
 
1913
- Export your Granola notes and transcripts.
2147
+ Work with Granola meetings, notes, and transcripts.
1914
2148
 
1915
2149
  Usage:
1916
2150
  granola <command> [options]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.14.0",
4
- "description": "CLI toolkit for exporting and working with Granola notes and transcripts",
3
+ "version": "0.16.0",
4
+ "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "granola",
@@ -18,7 +18,8 @@
18
18
  "url": "git+https://github.com/kkarimi/granola-toolkit.git"
19
19
  },
20
20
  "bin": {
21
- "granola": "dist/cli.js"
21
+ "granola": "dist/cli.js",
22
+ "granola-toolkit": "dist/cli.js"
22
23
  },
23
24
  "files": [
24
25
  "README.md",