granola-toolkit 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.
Files changed (3) hide show
  1. package/README.md +8 -59
  2. package/dist/cli.js +170 -153
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -146,17 +146,9 @@ Supported environment variables:
146
146
  - `CACHE_FILE`
147
147
  - `TRANSCRIPT_OUTPUT`
148
148
 
149
- ## What Changed In The Port
149
+ ## Development Checks
150
150
 
151
- This port deliberately preserves the Go repo's architecture, but it also fixes a few obvious rough edges instead of copying them blindly:
152
-
153
- - deterministic export ordering, so duplicate-title suffixes are stable across runs
154
- - shared filename sanitisation between notes and transcripts
155
- - cross-platform default path discovery for both `supabase.json` and cache files
156
- - HTML fallback for note export is converted into readable Markdown-ish text instead of being dumped raw
157
- - transcript timestamps preserve the original clock time instead of being normalised to UTC
158
-
159
- ## Verify
151
+ Before pushing changes, run:
160
152
 
161
153
  ```bash
162
154
  vp check
@@ -165,54 +157,11 @@ vp pack
165
157
  npm pack --dry-run
166
158
  ```
167
159
 
168
- `vp build` is for web apps. This repo publishes a CLI bundle, so the correct build step here is `vp pack`.
169
-
170
- ## Publishing
171
-
172
- Any push to `main` with a package version that is not already on npm becomes a publish candidate automatically. The workflow verifies the build, checks whether `package.json` contains an unpublished version, and then pauses in the `production` environment until someone approves the deployment review in GitHub.
173
-
174
- That means you can use either flow:
175
-
176
- - merge a PR that already includes the version bump
177
- - run the local release helper on `main`
178
-
179
- Local release helper:
180
-
181
- ```bash
182
- npm run release
183
- ```
184
-
185
- That script:
186
-
187
- 1. verifies the git working tree is clean
188
- 2. verifies you are on `main`
189
- 3. bumps the package version with `npm version --no-git-tag-version`
190
- 4. commits and pushes the release commit
191
- 5. lets the push-to-`main` workflow create a publish candidate automatically
192
-
193
- You can also choose the bump type explicitly:
194
-
195
- ```bash
196
- npm run release patch
197
- npm run release minor
198
- npm run release major
199
- ```
200
-
201
- The GitHub Actions release job then:
202
-
203
- - installs dependencies with Vite+ via `setup-vp`
204
- - runs `vp check`, `vp test`, `vp pack`, and `npm pack --dry-run`
205
- - checks npm first and skips the publish job if that exact version already exists
206
- - waits for approval on the `production` environment before npm credentials are exposed
207
- - publishes to npm using `NPM_TOKEN`
208
- - tags the published version as `v<version>`
209
-
210
- ### GitHub Setup
211
-
212
- To get the review dialog you showed in the screenshots, configure this once in GitHub:
160
+ What those do:
213
161
 
214
- 1. create a `production` environment in repository Settings -> Environments
215
- 2. add required reviewers to that environment
216
- 3. add `NPM_TOKEN` as an environment secret on `production`
162
+ - `vp check`: formatting, linting, and type checks
163
+ - `vp test`: unit tests
164
+ - `vp pack`: builds the CLI bundle into `dist/cli.js`
165
+ - `npm pack --dry-run`: shows the exact npm package contents without publishing
217
166
 
218
- After that, merges to `main` that contain a new unpublished version will stop at "Review deployments". Approving that deployment is what allows the npm publish step to run.
167
+ `vp build` is for web apps. This repo is a CLI package, so the build step here is `vp pack`.
package/dist/cli.js CHANGED
@@ -274,60 +274,6 @@ async function fetchDocuments(options) {
274
274
  return documents;
275
275
  }
276
276
  //#endregion
277
- //#region src/cache.ts
278
- function parseCacheDocument(id, value) {
279
- const record = asRecord(value);
280
- if (!record) return;
281
- return {
282
- createdAt: stringValue(record.created_at),
283
- id,
284
- title: stringValue(record.title),
285
- updatedAt: stringValue(record.updated_at)
286
- };
287
- }
288
- function parseTranscriptSegments(value) {
289
- if (!Array.isArray(value)) return;
290
- return value.flatMap((segment) => {
291
- const record = asRecord(segment);
292
- if (!record) return [];
293
- return [{
294
- documentId: stringValue(record.document_id),
295
- endTimestamp: stringValue(record.end_timestamp),
296
- id: stringValue(record.id),
297
- isFinal: Boolean(record.is_final),
298
- source: stringValue(record.source),
299
- startTimestamp: stringValue(record.start_timestamp),
300
- text: stringValue(record.text)
301
- }];
302
- });
303
- }
304
- function parseCacheContents(contents) {
305
- const outer = parseJsonString(contents);
306
- if (!outer) throw new Error("failed to parse cache JSON");
307
- const rawCache = outer.cache;
308
- let cachePayload;
309
- if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
310
- else cachePayload = asRecord(rawCache);
311
- const state = cachePayload ? asRecord(cachePayload.state) : void 0;
312
- if (!state) throw new Error("failed to parse cache state");
313
- const rawDocuments = asRecord(state.documents) ?? {};
314
- const rawTranscripts = asRecord(state.transcripts) ?? {};
315
- const documents = {};
316
- for (const [id, rawDocument] of Object.entries(rawDocuments)) {
317
- const document = parseCacheDocument(id, rawDocument);
318
- if (document) documents[id] = document;
319
- }
320
- const transcripts = {};
321
- for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
322
- const segments = parseTranscriptSegments(rawTranscript);
323
- if (segments) transcripts[id] = segments;
324
- }
325
- return {
326
- documents,
327
- transcripts
328
- };
329
- }
330
- //#endregion
331
277
  //#region src/config.ts
332
278
  function pickString(value) {
333
279
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -529,6 +475,113 @@ async function writeNotes(documents, outputDir) {
529
475
  return written;
530
476
  }
531
477
  //#endregion
478
+ //#region src/commands/shared.ts
479
+ function debug(enabled, ...values) {
480
+ if (enabled) console.error("[debug]", ...values);
481
+ }
482
+ //#endregion
483
+ //#region src/commands/notes.ts
484
+ function notesHelp() {
485
+ return `Granola notes
486
+
487
+ Usage:
488
+ granola notes [options]
489
+
490
+ Options:
491
+ --output <path> Output directory for Markdown files (default: ./notes)
492
+ --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
493
+ --supabase <path> Path to supabase.json
494
+ --debug Enable debug logging
495
+ --config <path> Path to .granola.toml
496
+ -h, --help Show help
497
+ `;
498
+ }
499
+ const notesCommand = {
500
+ description: "Export Granola notes to Markdown",
501
+ flags: {
502
+ help: { type: "boolean" },
503
+ output: { type: "string" },
504
+ timeout: { type: "string" }
505
+ },
506
+ help: notesHelp,
507
+ name: "notes",
508
+ async run({ commandFlags, globalFlags }) {
509
+ const config = await loadConfig({
510
+ globalFlags,
511
+ subcommandFlags: commandFlags
512
+ });
513
+ if (!config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
514
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
515
+ debug(config.debug, "supabase", config.supabase);
516
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
517
+ debug(config.debug, "output", config.notes.output);
518
+ console.log("Fetching documents from Granola API...");
519
+ const documents = await fetchDocuments({
520
+ supabaseContents: await readFile(config.supabase, "utf8"),
521
+ timeoutMs: config.notes.timeoutMs
522
+ });
523
+ console.log(`Exporting ${documents.length} notes to ${config.notes.output}...`);
524
+ const written = await writeNotes(documents, config.notes.output);
525
+ console.log("✓ Export completed successfully");
526
+ debug(config.debug, "notes written", written);
527
+ return 0;
528
+ }
529
+ };
530
+ //#endregion
531
+ //#region src/cache.ts
532
+ function parseCacheDocument(id, value) {
533
+ const record = asRecord(value);
534
+ if (!record) return;
535
+ return {
536
+ createdAt: stringValue(record.created_at),
537
+ id,
538
+ title: stringValue(record.title),
539
+ updatedAt: stringValue(record.updated_at)
540
+ };
541
+ }
542
+ function parseTranscriptSegments(value) {
543
+ if (!Array.isArray(value)) return;
544
+ return value.flatMap((segment) => {
545
+ const record = asRecord(segment);
546
+ if (!record) return [];
547
+ return [{
548
+ documentId: stringValue(record.document_id),
549
+ endTimestamp: stringValue(record.end_timestamp),
550
+ id: stringValue(record.id),
551
+ isFinal: Boolean(record.is_final),
552
+ source: stringValue(record.source),
553
+ startTimestamp: stringValue(record.start_timestamp),
554
+ text: stringValue(record.text)
555
+ }];
556
+ });
557
+ }
558
+ function parseCacheContents(contents) {
559
+ const outer = parseJsonString(contents);
560
+ if (!outer) throw new Error("failed to parse cache JSON");
561
+ const rawCache = outer.cache;
562
+ let cachePayload;
563
+ if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
564
+ else cachePayload = asRecord(rawCache);
565
+ const state = cachePayload ? asRecord(cachePayload.state) : void 0;
566
+ if (!state) throw new Error("failed to parse cache state");
567
+ const rawDocuments = asRecord(state.documents) ?? {};
568
+ const rawTranscripts = asRecord(state.transcripts) ?? {};
569
+ const documents = {};
570
+ for (const [id, rawDocument] of Object.entries(rawDocuments)) {
571
+ const document = parseCacheDocument(id, rawDocument);
572
+ if (document) documents[id] = document;
573
+ }
574
+ const transcripts = {};
575
+ for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
576
+ const segments = parseTranscriptSegments(rawTranscript);
577
+ if (segments) transcripts[id] = segments;
578
+ }
579
+ return {
580
+ documents,
581
+ transcripts
582
+ };
583
+ }
584
+ //#endregion
532
585
  //#region src/transcripts.ts
533
586
  function formatTranscript(document, segments) {
534
587
  if (segments.length === 0) return "";
@@ -576,7 +629,54 @@ async function writeTranscripts(cacheData, outputDir) {
576
629
  return written;
577
630
  }
578
631
  //#endregion
579
- //#region src/cli.ts
632
+ //#region src/commands/transcripts.ts
633
+ function transcriptsHelp() {
634
+ return `Granola transcripts
635
+
636
+ Usage:
637
+ granola transcripts [options]
638
+
639
+ Options:
640
+ --cache <path> Path to Granola cache JSON
641
+ --output <path> Output directory for transcript files (default: ./transcripts)
642
+ --debug Enable debug logging
643
+ --config <path> Path to .granola.toml
644
+ -h, --help Show help
645
+ `;
646
+ }
647
+ //#endregion
648
+ //#region src/commands/index.ts
649
+ const commands = [notesCommand, {
650
+ description: "Export Granola transcripts to text files",
651
+ flags: {
652
+ cache: { type: "string" },
653
+ help: { type: "boolean" },
654
+ output: { type: "string" }
655
+ },
656
+ help: transcriptsHelp,
657
+ name: "transcripts",
658
+ async run({ commandFlags, globalFlags }) {
659
+ const config = await loadConfig({
660
+ globalFlags,
661
+ subcommandFlags: commandFlags
662
+ });
663
+ if (!config.transcripts.cacheFile) throw new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
664
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
665
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile);
666
+ debug(config.debug, "output", config.transcripts.output);
667
+ console.log("Reading Granola cache file...");
668
+ const cacheData = parseCacheContents(await readFile(config.transcripts.cacheFile, "utf8"));
669
+ const transcriptCount = Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
670
+ console.log(`Exporting ${transcriptCount} transcripts to ${config.transcripts.output}...`);
671
+ const written = await writeTranscripts(cacheData, config.transcripts.output);
672
+ console.log("✓ Export completed successfully");
673
+ debug(config.debug, "transcripts written", written);
674
+ return 0;
675
+ }
676
+ }];
677
+ const commandMap = new Map(commands.map((command) => [command.name, command]));
678
+ //#endregion
679
+ //#region src/flags.ts
580
680
  function parseBooleanValue(value) {
581
681
  if (/^(true|1|yes|on)$/i.test(value)) return true;
582
682
  if (/^(false|0|no|off)$/i.test(value)) return false;
@@ -624,13 +724,15 @@ function parseFlags(args, spec) {
624
724
  values
625
725
  };
626
726
  }
727
+ //#endregion
728
+ //#region src/cli.ts
627
729
  function splitCommand(argv) {
628
- const commands = new Set(["notes", "transcripts"]);
629
730
  const rest = [];
630
731
  let command;
631
732
  for (const token of argv) {
632
- if (!command && !token.startsWith("-") && commands.has(token)) {
633
- command = token;
733
+ const candidate = !token.startsWith("-") ? commandMap.get(token) : void 0;
734
+ if (!command && candidate) {
735
+ command = candidate;
634
736
  continue;
635
737
  }
636
738
  rest.push(token);
@@ -641,6 +743,7 @@ function splitCommand(argv) {
641
743
  };
642
744
  }
643
745
  function rootHelp() {
746
+ const commandWidth = Math.max(...commands.map((command) => command.name.length));
644
747
  return `Granola CLI
645
748
 
646
749
  Export your Granola notes and transcripts.
@@ -649,8 +752,7 @@ Usage:
649
752
  granola <command> [options]
650
753
 
651
754
  Commands:
652
- notes Export Granola notes to Markdown
653
- transcripts Export Granola transcripts to text files
755
+ ${commands.map((command) => ` ${command.name.padEnd(commandWidth)} ${command.description}`).join("\n")}
654
756
 
655
757
  Global options:
656
758
  --config <path> Path to .granola.toml
@@ -663,38 +765,6 @@ Examples:
663
765
  granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
664
766
  `;
665
767
  }
666
- function notesHelp() {
667
- return `Granola notes
668
-
669
- Usage:
670
- granola notes [options]
671
-
672
- Options:
673
- --output <path> Output directory for Markdown files (default: ./notes)
674
- --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
675
- --supabase <path> Path to supabase.json
676
- --debug Enable debug logging
677
- --config <path> Path to .granola.toml
678
- -h, --help Show help
679
- `;
680
- }
681
- function transcriptsHelp() {
682
- return `Granola transcripts
683
-
684
- Usage:
685
- granola transcripts [options]
686
-
687
- Options:
688
- --cache <path> Path to Granola cache JSON
689
- --output <path> Output directory for transcript files (default: ./transcripts)
690
- --debug Enable debug logging
691
- --config <path> Path to .granola.toml
692
- -h, --help Show help
693
- `;
694
- }
695
- function debug(enabled, ...values) {
696
- if (enabled) console.error("[debug]", ...values);
697
- }
698
768
  async function runCli(argv) {
699
769
  try {
700
770
  const { command, rest } = splitCommand(argv);
@@ -712,68 +782,15 @@ async function runCli(argv) {
712
782
  console.log(rootHelp());
713
783
  return 1;
714
784
  }
715
- switch (command) {
716
- case "notes": {
717
- const subcommand = parseFlags(global.rest, {
718
- help: { type: "boolean" },
719
- output: { type: "string" },
720
- timeout: { type: "string" }
721
- });
722
- if (subcommand.values.help || global.values.help) {
723
- console.log(notesHelp());
724
- return 0;
725
- }
726
- const config = await loadConfig({
727
- globalFlags: global.values,
728
- subcommandFlags: subcommand.values
729
- });
730
- if (!config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
731
- debug(config.debug, "using config", config.configFileUsed ?? "(none)");
732
- debug(config.debug, "supabase", config.supabase);
733
- debug(config.debug, "timeoutMs", config.notes.timeoutMs);
734
- debug(config.debug, "output", config.notes.output);
735
- console.log("Fetching documents from Granola API...");
736
- const documents = await fetchDocuments({
737
- supabaseContents: await readFile(config.supabase, "utf8"),
738
- timeoutMs: config.notes.timeoutMs
739
- });
740
- console.log(`Exporting ${documents.length} notes to ${config.notes.output}...`);
741
- const written = await writeNotes(documents, config.notes.output);
742
- console.log("✓ Export completed successfully");
743
- debug(config.debug, "notes written", written);
744
- return 0;
745
- }
746
- case "transcripts": {
747
- const subcommand = parseFlags(global.rest, {
748
- cache: { type: "string" },
749
- help: { type: "boolean" },
750
- output: { type: "string" }
751
- });
752
- if (subcommand.values.help || global.values.help) {
753
- console.log(transcriptsHelp());
754
- return 0;
755
- }
756
- const config = await loadConfig({
757
- globalFlags: global.values,
758
- subcommandFlags: subcommand.values
759
- });
760
- if (!config.transcripts.cacheFile) throw new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
761
- debug(config.debug, "using config", config.configFileUsed ?? "(none)");
762
- debug(config.debug, "cacheFile", config.transcripts.cacheFile);
763
- debug(config.debug, "output", config.transcripts.output);
764
- console.log("Reading Granola cache file...");
765
- const cacheData = parseCacheContents(await readFile(config.transcripts.cacheFile, "utf8"));
766
- const transcriptCount = Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
767
- console.log(`Exporting ${transcriptCount} transcripts to ${config.transcripts.output}...`);
768
- const written = await writeTranscripts(cacheData, config.transcripts.output);
769
- console.log("✓ Export completed successfully");
770
- debug(config.debug, "transcripts written", written);
771
- return 0;
772
- }
773
- default:
774
- console.log(rootHelp());
775
- return 1;
785
+ const subcommand = parseFlags(global.rest, command.flags);
786
+ if (subcommand.values.help || global.values.help) {
787
+ console.log(command.help());
788
+ return 0;
776
789
  }
790
+ return await command.run({
791
+ commandFlags: subcommand.values,
792
+ globalFlags: global.values
793
+ });
777
794
  } catch (error) {
778
795
  const message = error instanceof Error ? error.message : String(error);
779
796
  console.error(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI toolkit for exporting and working with Granola notes and transcripts",
5
5
  "keywords": [
6
6
  "cli",
@@ -15,7 +15,7 @@
15
15
  "author": "Nima Karimi",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git@github.com:kkarimi/granola-toolkit.git"
18
+ "url": "git+https://github.com/kkarimi/granola-toolkit.git"
19
19
  },
20
20
  "bin": {
21
21
  "granola": "dist/cli.js"