granola-toolkit 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/cli.js +166 -47
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ Export notes:
|
|
|
79
79
|
```bash
|
|
80
80
|
granola auth login
|
|
81
81
|
granola notes
|
|
82
|
+
granola notes --folder Team
|
|
82
83
|
|
|
83
84
|
node dist/cli.js notes --supabase "$HOME/Library/Application Support/Granola/supabase.json"
|
|
84
85
|
node dist/cli.js notes --format json --output ./notes-json
|
|
@@ -91,6 +92,7 @@ Export transcripts:
|
|
|
91
92
|
```bash
|
|
92
93
|
node dist/cli.js transcripts --cache "$HOME/Library/Application Support/Granola/cache-v3.json"
|
|
93
94
|
node dist/cli.js transcripts --format yaml --output ./transcripts-yaml
|
|
95
|
+
granola transcripts --folder Team
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
Inspect individual meetings:
|
|
@@ -143,6 +145,8 @@ The flow is:
|
|
|
143
145
|
6. render that export as Markdown, JSON, YAML, or raw JSON
|
|
144
146
|
7. write one file per document into the output directory
|
|
145
147
|
|
|
148
|
+
When you pass `--folder <id|name>`, the export is filtered to that folder and, by default, written into a stable per-folder subdirectory under the notes output root.
|
|
149
|
+
|
|
146
150
|
Content is chosen in this order:
|
|
147
151
|
|
|
148
152
|
1. `notes`
|
|
@@ -169,6 +173,8 @@ The flow is:
|
|
|
169
173
|
5. render each export as text, JSON, YAML, or raw JSON
|
|
170
174
|
6. write one file per document into the output directory
|
|
171
175
|
|
|
176
|
+
When you pass `--folder <id|name>`, the export is filtered to that folder and, by default, written into a stable per-folder subdirectory under the transcripts output root.
|
|
177
|
+
|
|
172
178
|
Speaker labels are currently normalised to:
|
|
173
179
|
|
|
174
180
|
- `You` for `microphone`
|
|
@@ -223,6 +229,8 @@ The current CLI surface includes:
|
|
|
223
229
|
- `folder list`
|
|
224
230
|
- `folder view <id|name>`
|
|
225
231
|
- `meeting list --folder <id|name>`
|
|
232
|
+
- `notes --folder <id|name>`
|
|
233
|
+
- `transcripts --folder <id|name>`
|
|
226
234
|
|
|
227
235
|
### Server
|
|
228
236
|
|
|
@@ -250,9 +258,9 @@ The initial server API includes:
|
|
|
250
258
|
- `POST /auth/logout`
|
|
251
259
|
- `POST /auth/mode`
|
|
252
260
|
- `POST /auth/refresh`
|
|
253
|
-
- `POST /exports/notes`
|
|
261
|
+
- `POST /exports/notes` with optional `folderId`
|
|
254
262
|
- `POST /exports/jobs/:id/rerun`
|
|
255
|
-
- `POST /exports/transcripts`
|
|
263
|
+
- `POST /exports/transcripts` with optional `folderId`
|
|
256
264
|
|
|
257
265
|
This is the shared runtime for `granola web` and `granola attach`.
|
|
258
266
|
|
|
@@ -287,6 +295,7 @@ The initial browser client includes:
|
|
|
287
295
|
- app-state status from the shared core
|
|
288
296
|
- an auth session panel for login, refresh, source switching, and sign-out
|
|
289
297
|
- note and transcript export actions backed by the same local API
|
|
298
|
+
- folder-scoped export actions that follow the currently selected folder
|
|
290
299
|
- a recent export-jobs panel with rerun actions
|
|
291
300
|
- stronger empty and error states for list/detail failures
|
|
292
301
|
- a server-access panel that can unlock or lock a password-protected local server
|
|
@@ -354,6 +363,7 @@ The web client uses the index as a fast path and upgrades to live data automatic
|
|
|
354
363
|
Exports are now tracked as jobs with:
|
|
355
364
|
|
|
356
365
|
- persistent local history across CLI and web runs
|
|
366
|
+
- explicit scope metadata for all-meetings and folder-scoped runs
|
|
357
367
|
- running, completed, and failed status
|
|
358
368
|
- per-export progress counters
|
|
359
369
|
- rerun support from `granola exports rerun <job-id>` or the web client
|
|
@@ -439,6 +449,7 @@ Before pushing changes, run:
|
|
|
439
449
|
```bash
|
|
440
450
|
vp check
|
|
441
451
|
vp test
|
|
452
|
+
npm run coverage
|
|
442
453
|
vp pack
|
|
443
454
|
npm pack --dry-run
|
|
444
455
|
```
|
|
@@ -447,6 +458,7 @@ What those do:
|
|
|
447
458
|
|
|
448
459
|
- `vp check`: formatting, linting, and type checks
|
|
449
460
|
- `vp test`: unit tests
|
|
461
|
+
- `npm run coverage`: unit tests plus a local coverage report in `coverage/coverage-summary.json`
|
|
450
462
|
- `vp pack`: builds the CLI bundle into `dist/cli.js`
|
|
451
463
|
- `npm pack --dry-run`: shows the exact npm package contents without publishing
|
|
452
464
|
|
package/dist/cli.js
CHANGED
|
@@ -218,16 +218,22 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
218
218
|
async listExportJobs(options = {}) {
|
|
219
219
|
return await this.requestJson(granolaExportJobsPath(options));
|
|
220
220
|
}
|
|
221
|
-
async exportNotes(format = "markdown") {
|
|
221
|
+
async exportNotes(format = "markdown", options = {}) {
|
|
222
222
|
return await this.requestJson(granolaTransportPaths.exportNotes, {
|
|
223
|
-
body: JSON.stringify({
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
folderId: options.folderId,
|
|
225
|
+
format
|
|
226
|
+
}),
|
|
224
227
|
headers: { "content-type": "application/json" },
|
|
225
228
|
method: "POST"
|
|
226
229
|
});
|
|
227
230
|
}
|
|
228
|
-
async exportTranscripts(format = "text") {
|
|
231
|
+
async exportTranscripts(format = "text", options = {}) {
|
|
229
232
|
return await this.requestJson(granolaTransportPaths.exportTranscripts, {
|
|
230
|
-
body: JSON.stringify({
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
folderId: options.folderId,
|
|
235
|
+
format
|
|
236
|
+
}),
|
|
231
237
|
headers: { "content-type": "application/json" },
|
|
232
238
|
method: "POST"
|
|
233
239
|
});
|
|
@@ -2974,6 +2980,42 @@ async function loadOptionalGranolaCache(cacheFile) {
|
|
|
2974
2980
|
return parseCacheContents(await readFile(cacheFile, "utf8"));
|
|
2975
2981
|
}
|
|
2976
2982
|
//#endregion
|
|
2983
|
+
//#region src/export-scope.ts
|
|
2984
|
+
const FOLDER_EXPORT_DIRECTORY = "_folders";
|
|
2985
|
+
function allExportScope() {
|
|
2986
|
+
return { mode: "all" };
|
|
2987
|
+
}
|
|
2988
|
+
function folderExportScope(folder) {
|
|
2989
|
+
return {
|
|
2990
|
+
folderId: folder.id,
|
|
2991
|
+
folderName: folder.name || folder.id,
|
|
2992
|
+
mode: "folder"
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
function cloneExportScope(scope) {
|
|
2996
|
+
return scope.mode === "folder" ? { ...scope } : { mode: "all" };
|
|
2997
|
+
}
|
|
2998
|
+
function normaliseExportScope(value) {
|
|
2999
|
+
const record = asRecord(value);
|
|
3000
|
+
if (!record) return allExportScope();
|
|
3001
|
+
if (record.mode !== "folder") return allExportScope();
|
|
3002
|
+
const folderId = stringValue(record.folderId);
|
|
3003
|
+
const folderName = stringValue(record.folderName) || folderId;
|
|
3004
|
+
if (!folderId) return allExportScope();
|
|
3005
|
+
return {
|
|
3006
|
+
folderId,
|
|
3007
|
+
folderName,
|
|
3008
|
+
mode: "folder"
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
function renderExportScopeLabel(scope) {
|
|
3012
|
+
return scope.mode === "folder" ? `folder ${scope.folderName}` : "all meetings";
|
|
3013
|
+
}
|
|
3014
|
+
function resolveExportOutputDir(outputDir, scope, options = {}) {
|
|
3015
|
+
if (scope.mode !== "folder" || options.scopedDirectory === false) return outputDir;
|
|
3016
|
+
return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3017
|
+
}
|
|
3018
|
+
//#endregion
|
|
2977
3019
|
//#region src/export-jobs.ts
|
|
2978
3020
|
const EXPORT_JOBS_VERSION = 1;
|
|
2979
3021
|
const MAX_EXPORT_JOBS = 100;
|
|
@@ -2999,6 +3041,7 @@ function normaliseJob(value) {
|
|
|
2999
3041
|
itemCount,
|
|
3000
3042
|
kind,
|
|
3001
3043
|
outputDir,
|
|
3044
|
+
scope: normaliseExportScope(record.scope),
|
|
3002
3045
|
startedAt,
|
|
3003
3046
|
status,
|
|
3004
3047
|
written
|
|
@@ -3209,10 +3252,16 @@ function transcriptCount(cacheData) {
|
|
|
3209
3252
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
3210
3253
|
}
|
|
3211
3254
|
function cloneExportState(state) {
|
|
3212
|
-
return state ? {
|
|
3255
|
+
return state ? {
|
|
3256
|
+
...state,
|
|
3257
|
+
scope: cloneExportScope(state.scope)
|
|
3258
|
+
} : void 0;
|
|
3213
3259
|
}
|
|
3214
3260
|
function cloneExportJob(job) {
|
|
3215
|
-
return {
|
|
3261
|
+
return {
|
|
3262
|
+
...job,
|
|
3263
|
+
scope: cloneExportScope(job.scope)
|
|
3264
|
+
};
|
|
3216
3265
|
}
|
|
3217
3266
|
function cloneFolderSummary(folder) {
|
|
3218
3267
|
return { ...folder };
|
|
@@ -3463,7 +3512,7 @@ var GranolaApp = class {
|
|
|
3463
3512
|
this.emitStateUpdate();
|
|
3464
3513
|
return cloneExportJob(job);
|
|
3465
3514
|
}
|
|
3466
|
-
async startExportJob(kind, format, itemCount, outputDir) {
|
|
3515
|
+
async startExportJob(kind, format, itemCount, outputDir, scope) {
|
|
3467
3516
|
return await this.updateExportJob({
|
|
3468
3517
|
completedCount: 0,
|
|
3469
3518
|
format,
|
|
@@ -3471,6 +3520,7 @@ var GranolaApp = class {
|
|
|
3471
3520
|
itemCount,
|
|
3472
3521
|
kind,
|
|
3473
3522
|
outputDir,
|
|
3523
|
+
scope: cloneExportScope(scope),
|
|
3474
3524
|
startedAt: this.nowIso(),
|
|
3475
3525
|
status: "running",
|
|
3476
3526
|
written: 0
|
|
@@ -3740,25 +3790,29 @@ var GranolaApp = class {
|
|
|
3740
3790
|
this.setUiState({ view: "exports-history" });
|
|
3741
3791
|
return { jobs };
|
|
3742
3792
|
}
|
|
3743
|
-
async exportNotes(format = "markdown") {
|
|
3793
|
+
async exportNotes(format = "markdown", options = {}) {
|
|
3794
|
+
const documents = await this.listDocuments();
|
|
3795
|
+
const exportContext = await this.resolveExportContext(options.folderId);
|
|
3796
|
+
const filteredDocuments = exportContext.documentIds ? documents.filter((document) => exportContext.documentIds.has(document.id)) : documents;
|
|
3744
3797
|
return await this.runNotesExport({
|
|
3798
|
+
documents: filteredDocuments,
|
|
3745
3799
|
format,
|
|
3746
|
-
outputDir: this.config.notes.output
|
|
3800
|
+
outputDir: resolveExportOutputDir(options.outputDir ?? this.config.notes.output, exportContext.scope, { scopedDirectory: options.scopedOutput }),
|
|
3801
|
+
scope: exportContext.scope
|
|
3747
3802
|
});
|
|
3748
3803
|
}
|
|
3749
3804
|
async runNotesExport(options) {
|
|
3750
|
-
|
|
3751
|
-
let job = await this.startExportJob("notes", options.format, documents.length, options.outputDir);
|
|
3805
|
+
let job = await this.startExportJob("notes", options.format, options.documents.length, options.outputDir, options.scope);
|
|
3752
3806
|
let written = 0;
|
|
3753
3807
|
try {
|
|
3754
|
-
written = await writeNotes(documents, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3808
|
+
written = await writeNotes(options.documents, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3755
3809
|
job = await this.setExportJobProgress(job, {
|
|
3756
3810
|
completedCount: progress.completed,
|
|
3757
3811
|
written: progress.written
|
|
3758
3812
|
});
|
|
3759
3813
|
} });
|
|
3760
3814
|
job = await this.completeExportJob(job, {
|
|
3761
|
-
completedCount: documents.length,
|
|
3815
|
+
completedCount: options.documents.length,
|
|
3762
3816
|
written
|
|
3763
3817
|
});
|
|
3764
3818
|
} catch (error) {
|
|
@@ -3767,37 +3821,49 @@ var GranolaApp = class {
|
|
|
3767
3821
|
}
|
|
3768
3822
|
this.#state.exports.notes = {
|
|
3769
3823
|
format: options.format,
|
|
3770
|
-
itemCount: documents.length,
|
|
3824
|
+
itemCount: options.documents.length,
|
|
3771
3825
|
jobId: job.id,
|
|
3772
3826
|
outputDir: options.outputDir,
|
|
3773
3827
|
ranAt: this.nowIso(),
|
|
3828
|
+
scope: cloneExportScope(options.scope),
|
|
3774
3829
|
written
|
|
3775
3830
|
};
|
|
3776
3831
|
this.emitStateUpdate();
|
|
3777
|
-
this.setUiState({
|
|
3832
|
+
this.setUiState({
|
|
3833
|
+
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
3834
|
+
view: "notes-export"
|
|
3835
|
+
});
|
|
3778
3836
|
return {
|
|
3779
|
-
documentCount: documents.length,
|
|
3780
|
-
documents,
|
|
3837
|
+
documentCount: options.documents.length,
|
|
3838
|
+
documents: options.documents,
|
|
3781
3839
|
format: options.format,
|
|
3782
3840
|
job,
|
|
3783
3841
|
outputDir: options.outputDir,
|
|
3842
|
+
scope: cloneExportScope(options.scope),
|
|
3784
3843
|
written
|
|
3785
3844
|
};
|
|
3786
3845
|
}
|
|
3787
|
-
async exportTranscripts(format = "text") {
|
|
3846
|
+
async exportTranscripts(format = "text", options = {}) {
|
|
3847
|
+
const cacheData = await this.loadCache({ required: true });
|
|
3848
|
+
if (!cacheData) throw this.missingCacheError();
|
|
3849
|
+
const exportContext = await this.resolveExportContext(options.folderId);
|
|
3850
|
+
const scopedCacheData = exportContext.documentIds ? {
|
|
3851
|
+
documents: Object.fromEntries(Object.entries(cacheData.documents).filter(([id]) => exportContext.documentIds.has(id))),
|
|
3852
|
+
transcripts: Object.fromEntries(Object.entries(cacheData.transcripts).filter(([id]) => exportContext.documentIds.has(id)))
|
|
3853
|
+
} : cacheData;
|
|
3788
3854
|
return await this.runTranscriptsExport({
|
|
3855
|
+
cacheData: scopedCacheData,
|
|
3789
3856
|
format,
|
|
3790
|
-
outputDir: this.config.transcripts.output
|
|
3857
|
+
outputDir: resolveExportOutputDir(options.outputDir ?? this.config.transcripts.output, exportContext.scope, { scopedDirectory: options.scopedOutput }),
|
|
3858
|
+
scope: exportContext.scope
|
|
3791
3859
|
});
|
|
3792
3860
|
}
|
|
3793
3861
|
async runTranscriptsExport(options) {
|
|
3794
|
-
const
|
|
3795
|
-
|
|
3796
|
-
const count = transcriptCount(cacheData);
|
|
3797
|
-
let job = await this.startExportJob("transcripts", options.format, count, options.outputDir);
|
|
3862
|
+
const count = transcriptCount(options.cacheData);
|
|
3863
|
+
let job = await this.startExportJob("transcripts", options.format, count, options.outputDir, options.scope);
|
|
3798
3864
|
let written = 0;
|
|
3799
3865
|
try {
|
|
3800
|
-
written = await writeTranscripts(cacheData, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3866
|
+
written = await writeTranscripts(options.cacheData, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3801
3867
|
job = await this.setExportJobProgress(job, {
|
|
3802
3868
|
completedCount: progress.completed,
|
|
3803
3869
|
written: progress.written
|
|
@@ -3817,15 +3883,20 @@ var GranolaApp = class {
|
|
|
3817
3883
|
jobId: job.id,
|
|
3818
3884
|
outputDir: options.outputDir,
|
|
3819
3885
|
ranAt: this.nowIso(),
|
|
3886
|
+
scope: cloneExportScope(options.scope),
|
|
3820
3887
|
written
|
|
3821
3888
|
};
|
|
3822
3889
|
this.emitStateUpdate();
|
|
3823
|
-
this.setUiState({
|
|
3890
|
+
this.setUiState({
|
|
3891
|
+
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
3892
|
+
view: "transcripts-export"
|
|
3893
|
+
});
|
|
3824
3894
|
return {
|
|
3825
|
-
cacheData,
|
|
3895
|
+
cacheData: options.cacheData,
|
|
3826
3896
|
format: options.format,
|
|
3827
3897
|
job,
|
|
3828
3898
|
outputDir: options.outputDir,
|
|
3899
|
+
scope: cloneExportScope(options.scope),
|
|
3829
3900
|
transcriptCount: count,
|
|
3830
3901
|
written
|
|
3831
3902
|
};
|
|
@@ -3833,15 +3904,28 @@ var GranolaApp = class {
|
|
|
3833
3904
|
async rerunExportJob(id) {
|
|
3834
3905
|
const job = this.#state.exports.jobs.find((candidate) => candidate.id === id);
|
|
3835
3906
|
if (!job) throw new Error(`export job not found: ${id}`);
|
|
3836
|
-
if (job.kind === "notes") return await this.
|
|
3837
|
-
|
|
3838
|
-
outputDir: job.outputDir
|
|
3907
|
+
if (job.kind === "notes") return await this.exportNotes(job.format, {
|
|
3908
|
+
folderId: job.scope.mode === "folder" ? job.scope.folderId : void 0,
|
|
3909
|
+
outputDir: job.outputDir,
|
|
3910
|
+
scopedOutput: false
|
|
3839
3911
|
});
|
|
3840
|
-
return await this.
|
|
3841
|
-
|
|
3842
|
-
outputDir: job.outputDir
|
|
3912
|
+
return await this.exportTranscripts(job.format, {
|
|
3913
|
+
folderId: job.scope.mode === "folder" ? job.scope.folderId : void 0,
|
|
3914
|
+
outputDir: job.outputDir,
|
|
3915
|
+
scopedOutput: false
|
|
3843
3916
|
});
|
|
3844
3917
|
}
|
|
3918
|
+
async resolveExportContext(folderId) {
|
|
3919
|
+
if (!folderId) return { scope: allExportScope() };
|
|
3920
|
+
const folders = await this.loadFolders({ required: true });
|
|
3921
|
+
const summary = resolveFolder((folders ?? []).map((folder) => buildFolderSummary(folder)), folderId);
|
|
3922
|
+
const rawFolder = (folders ?? []).find((candidate) => candidate.id === summary.id);
|
|
3923
|
+
if (!rawFolder) throw new Error(`folder not found: ${folderId}`);
|
|
3924
|
+
return {
|
|
3925
|
+
documentIds: new Set(rawFolder.documentIds),
|
|
3926
|
+
scope: folderExportScope(summary)
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3845
3929
|
};
|
|
3846
3930
|
async function createGranolaApp(config, options = {}) {
|
|
3847
3931
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
@@ -4132,8 +4216,8 @@ function renderExportJobs(jobs, format) {
|
|
|
4132
4216
|
if (format === "json") return toJson({ jobs });
|
|
4133
4217
|
if (format === "yaml") return toYaml({ jobs });
|
|
4134
4218
|
if (jobs.length === 0) return "No export jobs\n";
|
|
4135
|
-
return `${["ID KIND STATUS FORMAT ITEMS WRITTEN STARTED", ...jobs.map((job) => {
|
|
4136
|
-
return `${job.id.padEnd(28).slice(0, 28)} ${job.kind.padEnd(12)} ${job.status.padEnd(11)} ${job.format.padEnd(11)} ${String(job.itemCount).padEnd(7)} ${String(job.written).padEnd(8)} ${job.startedAt.slice(0, 19)}`;
|
|
4219
|
+
return `${["ID KIND STATUS FORMAT SCOPE ITEMS WRITTEN STARTED", ...jobs.map((job) => {
|
|
4220
|
+
return `${job.id.padEnd(28).slice(0, 28)} ${job.kind.padEnd(12)} ${job.status.padEnd(11)} ${job.format.padEnd(11)} ${renderExportScopeLabel(job.scope).padEnd(20).slice(0, 20)} ${String(job.itemCount).padEnd(7)} ${String(job.written).padEnd(8)} ${job.startedAt.slice(0, 19)}`;
|
|
4137
4221
|
})].join("\n")}\n`;
|
|
4138
4222
|
}
|
|
4139
4223
|
const exportsCommand = {
|
|
@@ -4183,10 +4267,10 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
4183
4267
|
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
4184
4268
|
const result = await (await createGranolaApp(config)).rerunExportJob(id);
|
|
4185
4269
|
if ("documentCount" in result) {
|
|
4186
|
-
console.log(`✓ Reran notes export job ${result.job.id} to ${result.outputDir} (${result.written}/${result.documentCount} written)`);
|
|
4270
|
+
console.log(`✓ Reran notes export job ${result.job.id} from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (${result.written}/${result.documentCount} written)`);
|
|
4187
4271
|
return 0;
|
|
4188
4272
|
}
|
|
4189
|
-
console.log(`✓ Reran transcripts export job ${result.job.id} to ${result.outputDir} (${result.written}/${result.transcriptCount} written)`);
|
|
4273
|
+
console.log(`✓ Reran transcripts export job ${result.job.id} from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (${result.written}/${result.transcriptCount} written)`);
|
|
4190
4274
|
return 0;
|
|
4191
4275
|
}
|
|
4192
4276
|
//#endregion
|
|
@@ -4426,6 +4510,12 @@ function escapeHtml(value) {
|
|
|
4426
4510
|
.replaceAll('"', """);
|
|
4427
4511
|
}
|
|
4428
4512
|
|
|
4513
|
+
function exportScopeLabel(scope) {
|
|
4514
|
+
return scope && scope.mode === "folder"
|
|
4515
|
+
? "Folder: " + (scope.folderName || scope.folderId)
|
|
4516
|
+
: "Scope: All meetings";
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4429
4519
|
function setStatus(label, tone = "idle") {
|
|
4430
4520
|
els.stateBadge.textContent = label;
|
|
4431
4521
|
els.stateBadge.dataset.tone = tone;
|
|
@@ -4764,8 +4854,9 @@ function renderExportJobs() {
|
|
|
4764
4854
|
"</div>",
|
|
4765
4855
|
'<div class="job-card__status" data-status="' + escapeHtml(job.status) + '">' + escapeHtml(job.status) + "</div>",
|
|
4766
4856
|
"</div>",
|
|
4767
|
-
'<div class="job-card__meta">Format: ' + escapeHtml(job.format) + " • " + escapeHtml(progress) + " • Written: " + escapeHtml(String(job.written)) + "</div>",
|
|
4857
|
+
'<div class="job-card__meta">Format: ' + escapeHtml(job.format) + " • " + escapeHtml(exportScopeLabel(job.scope)) + " • " + escapeHtml(progress) + " • Written: " + escapeHtml(String(job.written)) + "</div>",
|
|
4768
4858
|
'<div class="job-card__meta">Started: ' + escapeHtml(job.startedAt.slice(0, 19)) + "</div>",
|
|
4859
|
+
'<div class="job-card__meta">Output: ' + escapeHtml(job.outputDir) + "</div>",
|
|
4769
4860
|
error,
|
|
4770
4861
|
'<div class="job-card__actions">' + rerunButton + "</div>",
|
|
4771
4862
|
"</article>",
|
|
@@ -4965,9 +5056,12 @@ async function syncAuthState() {
|
|
|
4965
5056
|
}
|
|
4966
5057
|
|
|
4967
5058
|
async function exportNotes() {
|
|
4968
|
-
setStatus("Exporting notes…", "busy");
|
|
5059
|
+
setStatus(state.selectedFolderId ? "Exporting folder notes…" : "Exporting notes…", "busy");
|
|
4969
5060
|
await fetchJson("/exports/notes", {
|
|
4970
|
-
body: JSON.stringify({
|
|
5061
|
+
body: JSON.stringify({
|
|
5062
|
+
folderId: state.selectedFolderId || undefined,
|
|
5063
|
+
format: "markdown",
|
|
5064
|
+
}),
|
|
4971
5065
|
headers: { "content-type": "application/json" },
|
|
4972
5066
|
method: "POST",
|
|
4973
5067
|
});
|
|
@@ -4975,9 +5069,15 @@ async function exportNotes() {
|
|
|
4975
5069
|
}
|
|
4976
5070
|
|
|
4977
5071
|
async function exportTranscripts() {
|
|
4978
|
-
setStatus(
|
|
5072
|
+
setStatus(
|
|
5073
|
+
state.selectedFolderId ? "Exporting folder transcripts…" : "Exporting transcripts…",
|
|
5074
|
+
"busy",
|
|
5075
|
+
);
|
|
4979
5076
|
await fetchJson("/exports/transcripts", {
|
|
4980
|
-
body: JSON.stringify({
|
|
5077
|
+
body: JSON.stringify({
|
|
5078
|
+
folderId: state.selectedFolderId || undefined,
|
|
5079
|
+
format: "text",
|
|
5080
|
+
}),
|
|
4981
5081
|
headers: { "content-type": "application/json" },
|
|
4982
5082
|
method: "POST",
|
|
4983
5083
|
});
|
|
@@ -6059,6 +6159,9 @@ function parseAuthMode(value) {
|
|
|
6059
6159
|
default: throw new Error("invalid auth mode: expected stored-session or supabase-file");
|
|
6060
6160
|
}
|
|
6061
6161
|
}
|
|
6162
|
+
function folderIdFromBody(value) {
|
|
6163
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
6164
|
+
}
|
|
6062
6165
|
function sendJson(response, body, init = {}) {
|
|
6063
6166
|
const payload = `${JSON.stringify(body, null, 2)}\n`;
|
|
6064
6167
|
response.writeHead(init.status ?? 200, {
|
|
@@ -6404,7 +6507,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6404
6507
|
}
|
|
6405
6508
|
if (method === "POST" && path === granolaTransportPaths.exportNotes) {
|
|
6406
6509
|
const body = await readJsonBody(request);
|
|
6407
|
-
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), {
|
|
6510
|
+
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format), { folderId: folderIdFromBody(body.folderId) }), {
|
|
6408
6511
|
headers: originHeaders,
|
|
6409
6512
|
status: 202
|
|
6410
6513
|
});
|
|
@@ -6426,7 +6529,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6426
6529
|
}
|
|
6427
6530
|
if (method === "POST" && path === granolaTransportPaths.exportTranscripts) {
|
|
6428
6531
|
const body = await readJsonBody(request);
|
|
6429
|
-
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format)), {
|
|
6532
|
+
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format), { folderId: folderIdFromBody(body.folderId) }), {
|
|
6430
6533
|
headers: originHeaders,
|
|
6431
6534
|
status: 202
|
|
6432
6535
|
});
|
|
@@ -6806,6 +6909,7 @@ Usage:
|
|
|
6806
6909
|
granola notes [options]
|
|
6807
6910
|
|
|
6808
6911
|
Options:
|
|
6912
|
+
--folder <query> Export only meetings inside one folder id or name
|
|
6809
6913
|
--format <value> Output format: markdown, json, yaml, raw (default: markdown)
|
|
6810
6914
|
--output <path> Output directory for note files (default: ./notes)
|
|
6811
6915
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
@@ -6818,6 +6922,7 @@ Options:
|
|
|
6818
6922
|
const notesCommand = {
|
|
6819
6923
|
description: "Export Granola notes",
|
|
6820
6924
|
flags: {
|
|
6925
|
+
folder: { type: "string" },
|
|
6821
6926
|
format: { type: "string" },
|
|
6822
6927
|
help: { type: "boolean" },
|
|
6823
6928
|
output: { type: "string" },
|
|
@@ -6838,8 +6943,14 @@ const notesCommand = {
|
|
|
6838
6943
|
debug(config.debug, "format", format);
|
|
6839
6944
|
const app = await createGranolaApp(config);
|
|
6840
6945
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
6841
|
-
const
|
|
6842
|
-
|
|
6946
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
6947
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
6948
|
+
debug(config.debug, "folder", folder?.id ?? "(all)");
|
|
6949
|
+
const result = await app.exportNotes(format, {
|
|
6950
|
+
folderId: folder?.id,
|
|
6951
|
+
scopedOutput: typeof commandFlags.output !== "string"
|
|
6952
|
+
});
|
|
6953
|
+
console.log(`✓ Exported ${result.documentCount} notes from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (job ${result.job.id})`);
|
|
6843
6954
|
debug(config.debug, "notes written", result.written);
|
|
6844
6955
|
return 0;
|
|
6845
6956
|
}
|
|
@@ -6992,6 +7103,7 @@ Usage:
|
|
|
6992
7103
|
|
|
6993
7104
|
Options:
|
|
6994
7105
|
--cache <path> Path to Granola cache JSON
|
|
7106
|
+
--folder <query> Export only meetings inside one folder id or name
|
|
6995
7107
|
--format <value> Output format: text, json, yaml, raw (default: text)
|
|
6996
7108
|
--output <path> Output directory for transcript files (default: ./transcripts)
|
|
6997
7109
|
--debug Enable debug logging
|
|
@@ -7003,6 +7115,7 @@ const transcriptsCommand = {
|
|
|
7003
7115
|
description: "Export Granola transcripts",
|
|
7004
7116
|
flags: {
|
|
7005
7117
|
cache: { type: "string" },
|
|
7118
|
+
folder: { type: "string" },
|
|
7006
7119
|
format: { type: "string" },
|
|
7007
7120
|
help: { type: "boolean" },
|
|
7008
7121
|
output: { type: "string" }
|
|
@@ -7021,8 +7134,14 @@ const transcriptsCommand = {
|
|
|
7021
7134
|
debug(config.debug, "format", format);
|
|
7022
7135
|
const app = await createGranolaApp(config);
|
|
7023
7136
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7024
|
-
const
|
|
7025
|
-
|
|
7137
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
7138
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
7139
|
+
debug(config.debug, "folder", folder?.id ?? "(all)");
|
|
7140
|
+
const result = await app.exportTranscripts(format, {
|
|
7141
|
+
folderId: folder?.id,
|
|
7142
|
+
scopedOutput: typeof commandFlags.output !== "string"
|
|
7143
|
+
});
|
|
7144
|
+
console.log(`✓ Exported ${result.transcriptCount} transcripts from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (job ${result.job.id})`);
|
|
7026
7145
|
debug(config.debug, "transcripts written", result.written);
|
|
7027
7146
|
return 0;
|
|
7028
7147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granola-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "vp pack",
|
|
36
36
|
"check": "vp check",
|
|
37
|
+
"coverage": "vp test --coverage",
|
|
37
38
|
"dev": "vp pack --watch",
|
|
38
39
|
"fmt": "vp fmt",
|
|
39
40
|
"lint": "vp lint",
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/node": "^25.5.2",
|
|
61
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
60
62
|
"typescript": "^5.9.3",
|
|
61
63
|
"vite-plus": "0.1.15"
|
|
62
64
|
},
|