granola-toolkit 0.27.0 → 0.28.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 +8 -0
- package/dist/cli.js +483 -368
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ granola meeting view 1234abcd
|
|
|
100
100
|
granola meeting notes 1234abcd
|
|
101
101
|
granola meeting transcript 1234abcd --format json
|
|
102
102
|
granola meeting export 1234abcd --format yaml
|
|
103
|
+
granola meeting open 1234abcd
|
|
103
104
|
granola tui
|
|
104
105
|
granola tui --meeting 1234abcd
|
|
105
106
|
```
|
|
@@ -116,6 +117,7 @@ granola attach http://127.0.0.1:4096 --meeting 1234abcd
|
|
|
116
117
|
granola attach http://127.0.0.1:4096 --password "change-me"
|
|
117
118
|
|
|
118
119
|
granola web
|
|
120
|
+
granola web --meeting 1234abcd
|
|
119
121
|
granola web --open=false --port 4096
|
|
120
122
|
granola web --network lan --password "change-me" --trusted-origins "https://trusted.example"
|
|
121
123
|
```
|
|
@@ -191,6 +193,7 @@ The focused meeting subcommands are:
|
|
|
191
193
|
|
|
192
194
|
- `meeting notes` for just the selected note output
|
|
193
195
|
- `meeting transcript` for just the selected transcript output
|
|
196
|
+
- `meeting open` to start the web workspace focused on one meeting
|
|
194
197
|
|
|
195
198
|
The machine-readable `export` command includes:
|
|
196
199
|
|
|
@@ -237,6 +240,11 @@ Server hardening now includes:
|
|
|
237
240
|
|
|
238
241
|
`web` starts the same local server as `serve`, enables the browser client at `/`, and opens that workspace in your default browser unless you pass `--open=false`.
|
|
239
242
|
|
|
243
|
+
You can deep-link into a specific meeting with either:
|
|
244
|
+
|
|
245
|
+
- `granola web --meeting <id>`
|
|
246
|
+
- `granola meeting open <id>`
|
|
247
|
+
|
|
240
248
|
The initial browser client includes:
|
|
241
249
|
|
|
242
250
|
- a searchable meeting list
|
package/dist/cli.js
CHANGED
|
@@ -3452,274 +3452,41 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
3452
3452
|
return 0;
|
|
3453
3453
|
}
|
|
3454
3454
|
//#endregion
|
|
3455
|
-
//#region src/
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
--config <path> Path to .granola.toml
|
|
3478
|
-
-h, --help Show help
|
|
3479
|
-
`;
|
|
3480
|
-
}
|
|
3481
|
-
function resolveListFormat(value) {
|
|
3482
|
-
switch (value) {
|
|
3483
|
-
case void 0: return "text";
|
|
3484
|
-
case "json":
|
|
3485
|
-
case "text":
|
|
3486
|
-
case "yaml": return value;
|
|
3487
|
-
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
function resolveViewFormat(value) {
|
|
3491
|
-
switch (value) {
|
|
3492
|
-
case void 0: return "text";
|
|
3493
|
-
case "json":
|
|
3494
|
-
case "text":
|
|
3495
|
-
case "yaml": return value;
|
|
3496
|
-
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
function resolveExportFormat(value) {
|
|
3500
|
-
switch (value) {
|
|
3501
|
-
case void 0: return "json";
|
|
3502
|
-
case "json":
|
|
3503
|
-
case "yaml": return value;
|
|
3504
|
-
default: throw new Error("invalid meeting export format: expected json or yaml");
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
function resolveNotesFormat(value) {
|
|
3508
|
-
switch (value) {
|
|
3509
|
-
case void 0: return "markdown";
|
|
3510
|
-
case "json":
|
|
3511
|
-
case "markdown":
|
|
3512
|
-
case "raw":
|
|
3513
|
-
case "yaml": return value;
|
|
3514
|
-
default: throw new Error("invalid meeting notes format: expected markdown, json, yaml, or raw");
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
function resolveTranscriptFormat$1(value) {
|
|
3518
|
-
switch (value) {
|
|
3519
|
-
case void 0: return "text";
|
|
3520
|
-
case "json":
|
|
3521
|
-
case "raw":
|
|
3522
|
-
case "text":
|
|
3523
|
-
case "yaml": return value;
|
|
3524
|
-
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
function parseLimit(value) {
|
|
3528
|
-
if (value === void 0) return 20;
|
|
3529
|
-
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
3530
|
-
const limit = Number(value);
|
|
3531
|
-
if (!Number.isInteger(limit) || limit < 1) throw new Error("invalid meeting limit: expected a positive integer");
|
|
3532
|
-
return limit;
|
|
3533
|
-
}
|
|
3534
|
-
const meetingCommand = {
|
|
3535
|
-
description: "Inspect and export individual Granola meetings",
|
|
3536
|
-
flags: {
|
|
3537
|
-
cache: { type: "string" },
|
|
3538
|
-
format: { type: "string" },
|
|
3539
|
-
help: { type: "boolean" },
|
|
3540
|
-
limit: { type: "string" },
|
|
3541
|
-
search: { type: "string" },
|
|
3542
|
-
timeout: { type: "string" }
|
|
3543
|
-
},
|
|
3544
|
-
help: meetingHelp,
|
|
3545
|
-
name: "meeting",
|
|
3546
|
-
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
3547
|
-
const [action, id] = commandArgs;
|
|
3548
|
-
switch (action) {
|
|
3549
|
-
case "list": return await list(commandFlags, globalFlags);
|
|
3550
|
-
case "view":
|
|
3551
|
-
if (!id) throw new Error("meeting view requires an id");
|
|
3552
|
-
return await view(id, commandFlags, globalFlags);
|
|
3553
|
-
case "export":
|
|
3554
|
-
if (!id) throw new Error("meeting export requires an id");
|
|
3555
|
-
return await exportMeeting(id, commandFlags, globalFlags);
|
|
3556
|
-
case "notes":
|
|
3557
|
-
if (!id) throw new Error("meeting notes requires an id");
|
|
3558
|
-
return await notes(id, commandFlags, globalFlags);
|
|
3559
|
-
case "transcript":
|
|
3560
|
-
if (!id) throw new Error("meeting transcript requires an id");
|
|
3561
|
-
return await transcript(id, commandFlags, globalFlags);
|
|
3562
|
-
case void 0:
|
|
3563
|
-
console.log(meetingHelp());
|
|
3564
|
-
return 1;
|
|
3565
|
-
default: throw new Error("invalid meeting command: expected list, view, export, notes, or transcript");
|
|
3566
|
-
}
|
|
3455
|
+
//#region src/browser.ts
|
|
3456
|
+
const execFileAsync = promisify(execFile);
|
|
3457
|
+
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
3458
|
+
const href = String(url);
|
|
3459
|
+
switch (platform) {
|
|
3460
|
+
case "darwin": return {
|
|
3461
|
+
args: [href],
|
|
3462
|
+
file: "open"
|
|
3463
|
+
};
|
|
3464
|
+
case "win32": return {
|
|
3465
|
+
args: [
|
|
3466
|
+
"/c",
|
|
3467
|
+
"start",
|
|
3468
|
+
"",
|
|
3469
|
+
href
|
|
3470
|
+
],
|
|
3471
|
+
file: "cmd"
|
|
3472
|
+
};
|
|
3473
|
+
default: return {
|
|
3474
|
+
args: [href],
|
|
3475
|
+
file: "xdg-open"
|
|
3476
|
+
};
|
|
3567
3477
|
}
|
|
3568
|
-
};
|
|
3569
|
-
async function list(commandFlags, globalFlags) {
|
|
3570
|
-
const format = resolveListFormat(commandFlags.format);
|
|
3571
|
-
const limit = parseLimit(commandFlags.limit);
|
|
3572
|
-
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
3573
|
-
const config = await loadConfig({
|
|
3574
|
-
globalFlags,
|
|
3575
|
-
subcommandFlags: commandFlags
|
|
3576
|
-
});
|
|
3577
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3578
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3579
|
-
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
3580
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3581
|
-
const app = await createGranolaApp(config);
|
|
3582
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3583
|
-
console.log("Loading meetings...");
|
|
3584
|
-
const result = await app.listMeetings({
|
|
3585
|
-
limit,
|
|
3586
|
-
search
|
|
3587
|
-
});
|
|
3588
|
-
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
3589
|
-
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
3590
|
-
return 0;
|
|
3591
|
-
}
|
|
3592
|
-
async function view(id, commandFlags, globalFlags) {
|
|
3593
|
-
const format = resolveViewFormat(commandFlags.format);
|
|
3594
|
-
const config = await loadConfig({
|
|
3595
|
-
globalFlags,
|
|
3596
|
-
subcommandFlags: commandFlags
|
|
3597
|
-
});
|
|
3598
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3599
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3600
|
-
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
3601
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3602
|
-
const app = await createGranolaApp(config);
|
|
3603
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3604
|
-
console.log("Fetching meeting from Granola API...");
|
|
3605
|
-
const result = await app.getMeeting(id);
|
|
3606
|
-
console.log(renderMeetingView(result.meeting, format).trimEnd());
|
|
3607
|
-
return 0;
|
|
3608
|
-
}
|
|
3609
|
-
async function exportMeeting(id, commandFlags, globalFlags) {
|
|
3610
|
-
const format = resolveExportFormat(commandFlags.format);
|
|
3611
|
-
const config = await loadConfig({
|
|
3612
|
-
globalFlags,
|
|
3613
|
-
subcommandFlags: commandFlags
|
|
3614
|
-
});
|
|
3615
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3616
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3617
|
-
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
3618
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3619
|
-
const app = await createGranolaApp(config);
|
|
3620
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3621
|
-
console.log("Fetching meeting from Granola API...");
|
|
3622
|
-
const result = await app.getMeeting(id);
|
|
3623
|
-
console.log(renderMeetingExport(result.meeting, format).trimEnd());
|
|
3624
|
-
return 0;
|
|
3625
3478
|
}
|
|
3626
|
-
async function
|
|
3627
|
-
const
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
});
|
|
3632
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3633
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3634
|
-
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
3635
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3636
|
-
const app = await createGranolaApp(config);
|
|
3637
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3638
|
-
console.log("Fetching meeting from Granola API...");
|
|
3639
|
-
const result = await app.getMeeting(id);
|
|
3640
|
-
console.log(renderMeetingNotes(result.document, format).trimEnd());
|
|
3641
|
-
return 0;
|
|
3642
|
-
}
|
|
3643
|
-
async function transcript(id, commandFlags, globalFlags) {
|
|
3644
|
-
const format = resolveTranscriptFormat$1(commandFlags.format);
|
|
3645
|
-
const config = await loadConfig({
|
|
3646
|
-
globalFlags,
|
|
3647
|
-
subcommandFlags: commandFlags
|
|
3648
|
-
});
|
|
3649
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3650
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3651
|
-
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
3652
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3653
|
-
const app = await createGranolaApp(config);
|
|
3654
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3655
|
-
console.log("Fetching meeting from Granola API...");
|
|
3656
|
-
const result = await app.getMeeting(id, { requireCache: true });
|
|
3657
|
-
const output = renderMeetingTranscript(result.document, result.cacheData, format);
|
|
3658
|
-
if (!output.trim()) throw new Error(`no transcript found for meeting: ${result.document.id}`);
|
|
3659
|
-
console.log(output.trimEnd());
|
|
3660
|
-
return 0;
|
|
3661
|
-
}
|
|
3662
|
-
//#endregion
|
|
3663
|
-
//#region src/commands/notes.ts
|
|
3664
|
-
function notesHelp() {
|
|
3665
|
-
return `Granola notes
|
|
3666
|
-
|
|
3667
|
-
Usage:
|
|
3668
|
-
granola notes [options]
|
|
3669
|
-
|
|
3670
|
-
Options:
|
|
3671
|
-
--format <value> Output format: markdown, json, yaml, raw (default: markdown)
|
|
3672
|
-
--output <path> Output directory for note files (default: ./notes)
|
|
3673
|
-
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
3674
|
-
--supabase <path> Path to supabase.json
|
|
3675
|
-
--debug Enable debug logging
|
|
3676
|
-
--config <path> Path to .granola.toml
|
|
3677
|
-
-h, --help Show help
|
|
3678
|
-
`;
|
|
3679
|
-
}
|
|
3680
|
-
const notesCommand = {
|
|
3681
|
-
description: "Export Granola notes",
|
|
3682
|
-
flags: {
|
|
3683
|
-
format: { type: "string" },
|
|
3684
|
-
help: { type: "boolean" },
|
|
3685
|
-
output: { type: "string" },
|
|
3686
|
-
timeout: { type: "string" }
|
|
3687
|
-
},
|
|
3688
|
-
help: notesHelp,
|
|
3689
|
-
name: "notes",
|
|
3690
|
-
async run({ commandFlags, globalFlags }) {
|
|
3691
|
-
const config = await loadConfig({
|
|
3692
|
-
globalFlags,
|
|
3693
|
-
subcommandFlags: commandFlags
|
|
3694
|
-
});
|
|
3695
|
-
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
3696
|
-
debug(config.debug, "supabase", config.supabase);
|
|
3697
|
-
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
3698
|
-
debug(config.debug, "output", config.notes.output);
|
|
3699
|
-
const format = resolveNoteFormat(commandFlags.format);
|
|
3700
|
-
debug(config.debug, "format", format);
|
|
3701
|
-
const app = await createGranolaApp(config);
|
|
3702
|
-
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
3703
|
-
const result = await app.exportNotes(format);
|
|
3704
|
-
console.log(`✓ Exported ${result.documentCount} notes to ${result.outputDir} (job ${result.job.id})`);
|
|
3705
|
-
debug(config.debug, "notes written", result.written);
|
|
3706
|
-
return 0;
|
|
3707
|
-
}
|
|
3708
|
-
};
|
|
3709
|
-
function resolveNoteFormat(value) {
|
|
3710
|
-
switch (value) {
|
|
3711
|
-
case void 0: return "markdown";
|
|
3712
|
-
case "json":
|
|
3713
|
-
case "markdown":
|
|
3714
|
-
case "raw":
|
|
3715
|
-
case "yaml": return value;
|
|
3716
|
-
default: throw new Error("invalid notes format: expected markdown, json, yaml, or raw");
|
|
3717
|
-
}
|
|
3479
|
+
async function openExternalUrl(url, options = {}) {
|
|
3480
|
+
const command = getBrowserOpenCommand(url, options.platform);
|
|
3481
|
+
await (options.run ?? (async (file, args) => {
|
|
3482
|
+
await execFileAsync(file, args);
|
|
3483
|
+
}))(command.file, command.args);
|
|
3718
3484
|
}
|
|
3719
3485
|
//#endregion
|
|
3720
3486
|
//#region src/web/client-script.ts
|
|
3721
3487
|
const granolaWebClientScript = String.raw`
|
|
3722
3488
|
const serverConfig = window.__GRANOLA_SERVER__ || { passwordRequired: false };
|
|
3489
|
+
const workspaceTabs = ["notes", "transcript", "metadata", "raw"];
|
|
3723
3490
|
|
|
3724
3491
|
const state = {
|
|
3725
3492
|
appState: null,
|
|
@@ -3764,6 +3531,40 @@ const els = {
|
|
|
3764
3531
|
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
3765
3532
|
};
|
|
3766
3533
|
|
|
3534
|
+
function parseWorkspaceTab(value) {
|
|
3535
|
+
return workspaceTabs.includes(value) ? value : "notes";
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
function startupSelection() {
|
|
3539
|
+
const params = new URLSearchParams(window.location.search);
|
|
3540
|
+
return {
|
|
3541
|
+
meetingId: params.get("meeting")?.trim() || "",
|
|
3542
|
+
workspaceTab: parseWorkspaceTab(params.get("tab")),
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function syncBrowserUrl() {
|
|
3547
|
+
const url = new URL(window.location.href);
|
|
3548
|
+
|
|
3549
|
+
if (state.selectedMeetingId) {
|
|
3550
|
+
url.searchParams.set("meeting", state.selectedMeetingId);
|
|
3551
|
+
} else {
|
|
3552
|
+
url.searchParams.delete("meeting");
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
if (state.workspaceTab !== "notes") {
|
|
3556
|
+
url.searchParams.set("tab", state.workspaceTab);
|
|
3557
|
+
} else {
|
|
3558
|
+
url.searchParams.delete("tab");
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
const nextPath = url.pathname + url.search + url.hash;
|
|
3562
|
+
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
3563
|
+
if (nextPath !== currentPath) {
|
|
3564
|
+
history.replaceState(null, "", nextPath);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3767
3568
|
function escapeHtml(value) {
|
|
3768
3569
|
return value
|
|
3769
3570
|
.replaceAll("&", "&")
|
|
@@ -3927,6 +3728,7 @@ function renderMeetingList() {
|
|
|
3927
3728
|
state.selectedMeetingId = null;
|
|
3928
3729
|
state.selectedMeeting = null;
|
|
3929
3730
|
state.selectedMeetingBundle = null;
|
|
3731
|
+
syncBrowserUrl();
|
|
3930
3732
|
const filterSummary = currentFilterSummary();
|
|
3931
3733
|
const message = filterSummary
|
|
3932
3734
|
? "No meetings match " + filterSummary + "."
|
|
@@ -3940,6 +3742,7 @@ function renderMeetingList() {
|
|
|
3940
3742
|
if (!state.selectedMeetingId || !visibleIds.has(state.selectedMeetingId)) {
|
|
3941
3743
|
state.selectedMeetingId = state.meetings[0]?.id || null;
|
|
3942
3744
|
}
|
|
3745
|
+
syncBrowserUrl();
|
|
3943
3746
|
|
|
3944
3747
|
els.list.innerHTML = state.meetings
|
|
3945
3748
|
.map((meeting) => {
|
|
@@ -4138,6 +3941,7 @@ async function loadMeetings(options = {}) {
|
|
|
4138
3941
|
|
|
4139
3942
|
async function loadMeeting(id) {
|
|
4140
3943
|
state.selectedMeetingId = id;
|
|
3944
|
+
syncBrowserUrl();
|
|
4141
3945
|
renderMeetingList();
|
|
4142
3946
|
|
|
4143
3947
|
try {
|
|
@@ -4496,6 +4300,7 @@ els.quickOpenButton.addEventListener("click", () => {
|
|
|
4496
4300
|
els.workspaceTabs.forEach((button) => {
|
|
4497
4301
|
button.addEventListener("click", () => {
|
|
4498
4302
|
state.workspaceTab = button.dataset.workspaceTab || "notes";
|
|
4303
|
+
syncBrowserUrl();
|
|
4499
4304
|
renderMeetingDetail();
|
|
4500
4305
|
});
|
|
4501
4306
|
});
|
|
@@ -4512,24 +4317,28 @@ document.addEventListener("keydown", (event) => {
|
|
|
4512
4317
|
const tabs = ["notes", "transcript", "metadata", "raw"];
|
|
4513
4318
|
if (event.key === "1") {
|
|
4514
4319
|
state.workspaceTab = "notes";
|
|
4320
|
+
syncBrowserUrl();
|
|
4515
4321
|
renderMeetingDetail();
|
|
4516
4322
|
return;
|
|
4517
4323
|
}
|
|
4518
4324
|
|
|
4519
4325
|
if (event.key === "2") {
|
|
4520
4326
|
state.workspaceTab = "transcript";
|
|
4327
|
+
syncBrowserUrl();
|
|
4521
4328
|
renderMeetingDetail();
|
|
4522
4329
|
return;
|
|
4523
4330
|
}
|
|
4524
4331
|
|
|
4525
4332
|
if (event.key === "3") {
|
|
4526
4333
|
state.workspaceTab = "metadata";
|
|
4334
|
+
syncBrowserUrl();
|
|
4527
4335
|
renderMeetingDetail();
|
|
4528
4336
|
return;
|
|
4529
4337
|
}
|
|
4530
4338
|
|
|
4531
4339
|
if (event.key === "4") {
|
|
4532
4340
|
state.workspaceTab = "raw";
|
|
4341
|
+
syncBrowserUrl();
|
|
4533
4342
|
renderMeetingDetail();
|
|
4534
4343
|
return;
|
|
4535
4344
|
}
|
|
@@ -4537,15 +4346,21 @@ document.addEventListener("keydown", (event) => {
|
|
|
4537
4346
|
const currentIndex = tabs.indexOf(state.workspaceTab);
|
|
4538
4347
|
if (event.key === "]") {
|
|
4539
4348
|
state.workspaceTab = tabs[(currentIndex + 1) % tabs.length];
|
|
4349
|
+
syncBrowserUrl();
|
|
4540
4350
|
renderMeetingDetail();
|
|
4541
4351
|
}
|
|
4542
4352
|
|
|
4543
4353
|
if (event.key === "[") {
|
|
4544
4354
|
state.workspaceTab = tabs[(currentIndex + tabs.length - 1) % tabs.length];
|
|
4355
|
+
syncBrowserUrl();
|
|
4545
4356
|
renderMeetingDetail();
|
|
4546
4357
|
}
|
|
4547
4358
|
});
|
|
4548
4359
|
|
|
4360
|
+
const initialSelection = startupSelection();
|
|
4361
|
+
state.selectedMeetingId = initialSelection.meetingId || null;
|
|
4362
|
+
state.workspaceTab = initialSelection.workspaceTab;
|
|
4363
|
+
|
|
4549
4364
|
const events = new EventSource("/events");
|
|
4550
4365
|
events.addEventListener("state.updated", (event) => {
|
|
4551
4366
|
const previousLoadedAt = state.appState?.documents?.loadedAt;
|
|
@@ -5531,35 +5346,407 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5531
5346
|
});
|
|
5532
5347
|
}
|
|
5533
5348
|
});
|
|
5534
|
-
await new Promise((resolve, reject) => {
|
|
5535
|
-
server.once("error", reject);
|
|
5536
|
-
server.listen(port, hostname, () => {
|
|
5537
|
-
server.off("error", reject);
|
|
5538
|
-
resolve();
|
|
5539
|
-
});
|
|
5349
|
+
await new Promise((resolve, reject) => {
|
|
5350
|
+
server.once("error", reject);
|
|
5351
|
+
server.listen(port, hostname, () => {
|
|
5352
|
+
server.off("error", reject);
|
|
5353
|
+
resolve();
|
|
5354
|
+
});
|
|
5355
|
+
});
|
|
5356
|
+
const address = server.address();
|
|
5357
|
+
if (!address || typeof address === "string") throw new Error("failed to resolve server address");
|
|
5358
|
+
const resolved = address;
|
|
5359
|
+
const url = new URL(`http://${hostname}:${resolved.port}`);
|
|
5360
|
+
return {
|
|
5361
|
+
app,
|
|
5362
|
+
async close() {
|
|
5363
|
+
await new Promise((resolve, reject) => {
|
|
5364
|
+
server.close((error) => {
|
|
5365
|
+
if (error) {
|
|
5366
|
+
reject(error);
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
5369
|
+
resolve();
|
|
5370
|
+
});
|
|
5371
|
+
});
|
|
5372
|
+
},
|
|
5373
|
+
hostname,
|
|
5374
|
+
port: resolved.port,
|
|
5375
|
+
server,
|
|
5376
|
+
url
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
//#endregion
|
|
5380
|
+
//#region src/web-url.ts
|
|
5381
|
+
function buildGranolaMeetingUrl(baseUrl, meetingId) {
|
|
5382
|
+
const url = new URL(baseUrl);
|
|
5383
|
+
if (meetingId.trim()) url.searchParams.set("meeting", meetingId.trim());
|
|
5384
|
+
return url;
|
|
5385
|
+
}
|
|
5386
|
+
//#endregion
|
|
5387
|
+
//#region src/commands/web-shared.ts
|
|
5388
|
+
function resolveGranolaWebWorkspaceOptions(commandFlags) {
|
|
5389
|
+
const networkMode = parseNetworkMode(commandFlags.network);
|
|
5390
|
+
const hostname = resolveServerHostname(networkMode, commandFlags.hostname);
|
|
5391
|
+
const port = parsePort(commandFlags.port);
|
|
5392
|
+
return {
|
|
5393
|
+
hostname,
|
|
5394
|
+
networkMode,
|
|
5395
|
+
openBrowser: commandFlags.open !== false,
|
|
5396
|
+
password: typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password.trim() : void 0,
|
|
5397
|
+
port,
|
|
5398
|
+
trustedOrigins: parseTrustedOrigins(commandFlags["trusted-origins"])
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
function printWebRoutes() {
|
|
5402
|
+
console.log("Routes:");
|
|
5403
|
+
console.log(" GET /");
|
|
5404
|
+
console.log(" GET /health");
|
|
5405
|
+
console.log(" POST /auth/unlock");
|
|
5406
|
+
console.log(" POST /auth/lock");
|
|
5407
|
+
console.log(" GET /auth/status");
|
|
5408
|
+
console.log(" GET /state");
|
|
5409
|
+
console.log(" GET /events");
|
|
5410
|
+
console.log(" GET /meetings");
|
|
5411
|
+
console.log(" GET /meetings/:id");
|
|
5412
|
+
console.log(" GET /exports/jobs");
|
|
5413
|
+
console.log(" POST /auth/login");
|
|
5414
|
+
console.log(" POST /auth/logout");
|
|
5415
|
+
console.log(" POST /auth/mode");
|
|
5416
|
+
console.log(" POST /auth/refresh");
|
|
5417
|
+
console.log(" POST /exports/notes");
|
|
5418
|
+
console.log(" POST /exports/jobs/:id/rerun");
|
|
5419
|
+
console.log(" POST /exports/transcripts");
|
|
5420
|
+
}
|
|
5421
|
+
async function runGranolaWebWorkspace(app, options) {
|
|
5422
|
+
const server = await startGranolaServer(app, {
|
|
5423
|
+
enableWebClient: true,
|
|
5424
|
+
hostname: options.hostname,
|
|
5425
|
+
port: options.port,
|
|
5426
|
+
security: {
|
|
5427
|
+
password: options.password,
|
|
5428
|
+
trustedOrigins: options.trustedOrigins
|
|
5429
|
+
}
|
|
5430
|
+
});
|
|
5431
|
+
const targetUrl = options.targetMeetingId ? buildGranolaMeetingUrl(server.url, options.targetMeetingId) : new URL(server.url);
|
|
5432
|
+
console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
|
|
5433
|
+
if (targetUrl.href !== server.url.href) console.log(`Focused meeting URL: ${targetUrl.href}`);
|
|
5434
|
+
console.log(`Network mode: ${options.networkMode}`);
|
|
5435
|
+
if (options.password) console.log("Server password protection: enabled");
|
|
5436
|
+
else if (options.networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
|
|
5437
|
+
if (options.trustedOrigins.length > 0) console.log(`Trusted origins: ${options.trustedOrigins.join(", ")}`);
|
|
5438
|
+
printWebRoutes();
|
|
5439
|
+
console.log(`Attach: granola attach ${server.url.href}`);
|
|
5440
|
+
if (options.password) console.log("Attach password: add --password <value>");
|
|
5441
|
+
if (options.openBrowser) try {
|
|
5442
|
+
await openExternalUrl(targetUrl);
|
|
5443
|
+
} catch (error) {
|
|
5444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5445
|
+
console.error(`failed to open browser automatically: ${message}`);
|
|
5446
|
+
console.error(`open ${targetUrl.href} manually`);
|
|
5447
|
+
}
|
|
5448
|
+
await waitForShutdown(async () => await server.close());
|
|
5449
|
+
return 0;
|
|
5450
|
+
}
|
|
5451
|
+
//#endregion
|
|
5452
|
+
//#region src/commands/meeting.ts
|
|
5453
|
+
function meetingHelp() {
|
|
5454
|
+
return `Granola meeting
|
|
5455
|
+
|
|
5456
|
+
Usage:
|
|
5457
|
+
granola meeting <list|view|export|notes|transcript|open> [options]
|
|
5458
|
+
|
|
5459
|
+
Subcommands:
|
|
5460
|
+
list List meetings from the Granola API
|
|
5461
|
+
view <id> Show a single meeting with notes and transcript text
|
|
5462
|
+
export <id> Export a single meeting as JSON or YAML
|
|
5463
|
+
notes <id> Show a single meeting's notes
|
|
5464
|
+
transcript <id> Show a single meeting's transcript
|
|
5465
|
+
open <id> Start the web workspace focused on one meeting
|
|
5466
|
+
|
|
5467
|
+
Options:
|
|
5468
|
+
--cache <path> Path to Granola cache JSON for transcript data
|
|
5469
|
+
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
5470
|
+
--network <mode> open: local or lan (default: local)
|
|
5471
|
+
--hostname <value> open: hostname to bind (overrides network default)
|
|
5472
|
+
--limit <n> Number of meetings for list (default: 20)
|
|
5473
|
+
--open[=true|false] open: launch the browser automatically (default: true)
|
|
5474
|
+
--password <value> open: optional server password
|
|
5475
|
+
--port <value> open: port to bind (default: 0 for any available port)
|
|
5476
|
+
--search <query> Filter list by title, id, or tag
|
|
5477
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5478
|
+
--trusted-origins <v> open: comma-separated extra browser origins to trust
|
|
5479
|
+
--supabase <path> Path to supabase.json
|
|
5480
|
+
--debug Enable debug logging
|
|
5481
|
+
--config <path> Path to .granola.toml
|
|
5482
|
+
-h, --help Show help
|
|
5483
|
+
`;
|
|
5484
|
+
}
|
|
5485
|
+
function resolveListFormat(value) {
|
|
5486
|
+
switch (value) {
|
|
5487
|
+
case void 0: return "text";
|
|
5488
|
+
case "json":
|
|
5489
|
+
case "text":
|
|
5490
|
+
case "yaml": return value;
|
|
5491
|
+
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function resolveViewFormat(value) {
|
|
5495
|
+
switch (value) {
|
|
5496
|
+
case void 0: return "text";
|
|
5497
|
+
case "json":
|
|
5498
|
+
case "text":
|
|
5499
|
+
case "yaml": return value;
|
|
5500
|
+
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
function resolveExportFormat(value) {
|
|
5504
|
+
switch (value) {
|
|
5505
|
+
case void 0: return "json";
|
|
5506
|
+
case "json":
|
|
5507
|
+
case "yaml": return value;
|
|
5508
|
+
default: throw new Error("invalid meeting export format: expected json or yaml");
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
function resolveNotesFormat(value) {
|
|
5512
|
+
switch (value) {
|
|
5513
|
+
case void 0: return "markdown";
|
|
5514
|
+
case "json":
|
|
5515
|
+
case "markdown":
|
|
5516
|
+
case "raw":
|
|
5517
|
+
case "yaml": return value;
|
|
5518
|
+
default: throw new Error("invalid meeting notes format: expected markdown, json, yaml, or raw");
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
function resolveTranscriptFormat$1(value) {
|
|
5522
|
+
switch (value) {
|
|
5523
|
+
case void 0: return "text";
|
|
5524
|
+
case "json":
|
|
5525
|
+
case "raw":
|
|
5526
|
+
case "text":
|
|
5527
|
+
case "yaml": return value;
|
|
5528
|
+
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
function parseLimit(value) {
|
|
5532
|
+
if (value === void 0) return 20;
|
|
5533
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
5534
|
+
const limit = Number(value);
|
|
5535
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("invalid meeting limit: expected a positive integer");
|
|
5536
|
+
return limit;
|
|
5537
|
+
}
|
|
5538
|
+
const meetingCommand = {
|
|
5539
|
+
description: "Inspect and export individual Granola meetings",
|
|
5540
|
+
flags: {
|
|
5541
|
+
cache: { type: "string" },
|
|
5542
|
+
format: { type: "string" },
|
|
5543
|
+
help: { type: "boolean" },
|
|
5544
|
+
hostname: { type: "string" },
|
|
5545
|
+
limit: { type: "string" },
|
|
5546
|
+
network: { type: "string" },
|
|
5547
|
+
open: { type: "boolean" },
|
|
5548
|
+
password: { type: "string" },
|
|
5549
|
+
port: { type: "string" },
|
|
5550
|
+
search: { type: "string" },
|
|
5551
|
+
timeout: { type: "string" },
|
|
5552
|
+
"trusted-origins": { type: "string" }
|
|
5553
|
+
},
|
|
5554
|
+
help: meetingHelp,
|
|
5555
|
+
name: "meeting",
|
|
5556
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
5557
|
+
const [action, id] = commandArgs;
|
|
5558
|
+
switch (action) {
|
|
5559
|
+
case "list": return await list(commandFlags, globalFlags);
|
|
5560
|
+
case "view":
|
|
5561
|
+
if (!id) throw new Error("meeting view requires an id");
|
|
5562
|
+
return await view(id, commandFlags, globalFlags);
|
|
5563
|
+
case "export":
|
|
5564
|
+
if (!id) throw new Error("meeting export requires an id");
|
|
5565
|
+
return await exportMeeting(id, commandFlags, globalFlags);
|
|
5566
|
+
case "notes":
|
|
5567
|
+
if (!id) throw new Error("meeting notes requires an id");
|
|
5568
|
+
return await notes(id, commandFlags, globalFlags);
|
|
5569
|
+
case "transcript":
|
|
5570
|
+
if (!id) throw new Error("meeting transcript requires an id");
|
|
5571
|
+
return await transcript(id, commandFlags, globalFlags);
|
|
5572
|
+
case "open":
|
|
5573
|
+
if (!id) throw new Error("meeting open requires an id");
|
|
5574
|
+
return await openMeeting(id, commandFlags, globalFlags);
|
|
5575
|
+
case void 0:
|
|
5576
|
+
console.log(meetingHelp());
|
|
5577
|
+
return 1;
|
|
5578
|
+
default: throw new Error("invalid meeting command: expected list, view, export, notes, transcript, or open");
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
};
|
|
5582
|
+
async function list(commandFlags, globalFlags) {
|
|
5583
|
+
const format = resolveListFormat(commandFlags.format);
|
|
5584
|
+
const limit = parseLimit(commandFlags.limit);
|
|
5585
|
+
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5586
|
+
const config = await loadConfig({
|
|
5587
|
+
globalFlags,
|
|
5588
|
+
subcommandFlags: commandFlags
|
|
5589
|
+
});
|
|
5590
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5591
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5592
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5593
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5594
|
+
const app = await createGranolaApp(config);
|
|
5595
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5596
|
+
console.log("Loading meetings...");
|
|
5597
|
+
const result = await app.listMeetings({
|
|
5598
|
+
limit,
|
|
5599
|
+
search
|
|
5540
5600
|
});
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5601
|
+
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
5602
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
5603
|
+
return 0;
|
|
5604
|
+
}
|
|
5605
|
+
async function view(id, commandFlags, globalFlags) {
|
|
5606
|
+
const format = resolveViewFormat(commandFlags.format);
|
|
5607
|
+
const config = await loadConfig({
|
|
5608
|
+
globalFlags,
|
|
5609
|
+
subcommandFlags: commandFlags
|
|
5610
|
+
});
|
|
5611
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5612
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5613
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5614
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5615
|
+
const app = await createGranolaApp(config);
|
|
5616
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5617
|
+
console.log("Fetching meeting from Granola API...");
|
|
5618
|
+
const result = await app.getMeeting(id);
|
|
5619
|
+
console.log(renderMeetingView(result.meeting, format).trimEnd());
|
|
5620
|
+
return 0;
|
|
5621
|
+
}
|
|
5622
|
+
async function exportMeeting(id, commandFlags, globalFlags) {
|
|
5623
|
+
const format = resolveExportFormat(commandFlags.format);
|
|
5624
|
+
const config = await loadConfig({
|
|
5625
|
+
globalFlags,
|
|
5626
|
+
subcommandFlags: commandFlags
|
|
5627
|
+
});
|
|
5628
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5629
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5630
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5631
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5632
|
+
const app = await createGranolaApp(config);
|
|
5633
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5634
|
+
console.log("Fetching meeting from Granola API...");
|
|
5635
|
+
const result = await app.getMeeting(id);
|
|
5636
|
+
console.log(renderMeetingExport(result.meeting, format).trimEnd());
|
|
5637
|
+
return 0;
|
|
5638
|
+
}
|
|
5639
|
+
async function notes(id, commandFlags, globalFlags) {
|
|
5640
|
+
const format = resolveNotesFormat(commandFlags.format);
|
|
5641
|
+
const config = await loadConfig({
|
|
5642
|
+
globalFlags,
|
|
5643
|
+
subcommandFlags: commandFlags
|
|
5644
|
+
});
|
|
5645
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5646
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5647
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5648
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5649
|
+
const app = await createGranolaApp(config);
|
|
5650
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5651
|
+
console.log("Fetching meeting from Granola API...");
|
|
5652
|
+
const result = await app.getMeeting(id);
|
|
5653
|
+
console.log(renderMeetingNotes(result.document, format).trimEnd());
|
|
5654
|
+
return 0;
|
|
5655
|
+
}
|
|
5656
|
+
async function transcript(id, commandFlags, globalFlags) {
|
|
5657
|
+
const format = resolveTranscriptFormat$1(commandFlags.format);
|
|
5658
|
+
const config = await loadConfig({
|
|
5659
|
+
globalFlags,
|
|
5660
|
+
subcommandFlags: commandFlags
|
|
5661
|
+
});
|
|
5662
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5663
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5664
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5665
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5666
|
+
const app = await createGranolaApp(config);
|
|
5667
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5668
|
+
console.log("Fetching meeting from Granola API...");
|
|
5669
|
+
const result = await app.getMeeting(id, { requireCache: true });
|
|
5670
|
+
const output = renderMeetingTranscript(result.document, result.cacheData, format);
|
|
5671
|
+
if (!output.trim()) throw new Error(`no transcript found for meeting: ${result.document.id}`);
|
|
5672
|
+
console.log(output.trimEnd());
|
|
5673
|
+
return 0;
|
|
5674
|
+
}
|
|
5675
|
+
async function openMeeting(id, commandFlags, globalFlags) {
|
|
5676
|
+
const config = await loadConfig({
|
|
5677
|
+
globalFlags,
|
|
5678
|
+
subcommandFlags: commandFlags
|
|
5679
|
+
});
|
|
5680
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5681
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5682
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5683
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5684
|
+
const app = await createGranolaApp(config, { surface: "web" });
|
|
5685
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5686
|
+
console.log("Resolving meeting from Granola API...");
|
|
5687
|
+
const result = await app.getMeeting(id);
|
|
5688
|
+
console.log(`Preparing web workspace for ${result.document.title || result.document.id}...`);
|
|
5689
|
+
return await runGranolaWebWorkspace(app, {
|
|
5690
|
+
...resolveGranolaWebWorkspaceOptions(commandFlags),
|
|
5691
|
+
targetMeetingId: result.document.id
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
//#endregion
|
|
5695
|
+
//#region src/commands/notes.ts
|
|
5696
|
+
function notesHelp() {
|
|
5697
|
+
return `Granola notes
|
|
5698
|
+
|
|
5699
|
+
Usage:
|
|
5700
|
+
granola notes [options]
|
|
5701
|
+
|
|
5702
|
+
Options:
|
|
5703
|
+
--format <value> Output format: markdown, json, yaml, raw (default: markdown)
|
|
5704
|
+
--output <path> Output directory for note files (default: ./notes)
|
|
5705
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5706
|
+
--supabase <path> Path to supabase.json
|
|
5707
|
+
--debug Enable debug logging
|
|
5708
|
+
--config <path> Path to .granola.toml
|
|
5709
|
+
-h, --help Show help
|
|
5710
|
+
`;
|
|
5711
|
+
}
|
|
5712
|
+
const notesCommand = {
|
|
5713
|
+
description: "Export Granola notes",
|
|
5714
|
+
flags: {
|
|
5715
|
+
format: { type: "string" },
|
|
5716
|
+
help: { type: "boolean" },
|
|
5717
|
+
output: { type: "string" },
|
|
5718
|
+
timeout: { type: "string" }
|
|
5719
|
+
},
|
|
5720
|
+
help: notesHelp,
|
|
5721
|
+
name: "notes",
|
|
5722
|
+
async run({ commandFlags, globalFlags }) {
|
|
5723
|
+
const config = await loadConfig({
|
|
5724
|
+
globalFlags,
|
|
5725
|
+
subcommandFlags: commandFlags
|
|
5726
|
+
});
|
|
5727
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5728
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5729
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5730
|
+
debug(config.debug, "output", config.notes.output);
|
|
5731
|
+
const format = resolveNoteFormat(commandFlags.format);
|
|
5732
|
+
debug(config.debug, "format", format);
|
|
5733
|
+
const app = await createGranolaApp(config);
|
|
5734
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5735
|
+
const result = await app.exportNotes(format);
|
|
5736
|
+
console.log(`✓ Exported ${result.documentCount} notes to ${result.outputDir} (job ${result.job.id})`);
|
|
5737
|
+
debug(config.debug, "notes written", result.written);
|
|
5738
|
+
return 0;
|
|
5739
|
+
}
|
|
5740
|
+
};
|
|
5741
|
+
function resolveNoteFormat(value) {
|
|
5742
|
+
switch (value) {
|
|
5743
|
+
case void 0: return "markdown";
|
|
5744
|
+
case "json":
|
|
5745
|
+
case "markdown":
|
|
5746
|
+
case "raw":
|
|
5747
|
+
case "yaml": return value;
|
|
5748
|
+
default: throw new Error("invalid notes format: expected markdown, json, yaml, or raw");
|
|
5749
|
+
}
|
|
5563
5750
|
}
|
|
5564
5751
|
//#endregion
|
|
5565
5752
|
//#region src/commands/serve.ts
|
|
@@ -5744,37 +5931,6 @@ function resolveTranscriptFormat(value) {
|
|
|
5744
5931
|
}
|
|
5745
5932
|
}
|
|
5746
5933
|
//#endregion
|
|
5747
|
-
//#region src/browser.ts
|
|
5748
|
-
const execFileAsync = promisify(execFile);
|
|
5749
|
-
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
5750
|
-
const href = String(url);
|
|
5751
|
-
switch (platform) {
|
|
5752
|
-
case "darwin": return {
|
|
5753
|
-
args: [href],
|
|
5754
|
-
file: "open"
|
|
5755
|
-
};
|
|
5756
|
-
case "win32": return {
|
|
5757
|
-
args: [
|
|
5758
|
-
"/c",
|
|
5759
|
-
"start",
|
|
5760
|
-
"",
|
|
5761
|
-
href
|
|
5762
|
-
],
|
|
5763
|
-
file: "cmd"
|
|
5764
|
-
};
|
|
5765
|
-
default: return {
|
|
5766
|
-
args: [href],
|
|
5767
|
-
file: "xdg-open"
|
|
5768
|
-
};
|
|
5769
|
-
}
|
|
5770
|
-
}
|
|
5771
|
-
async function openExternalUrl(url, options = {}) {
|
|
5772
|
-
const command = getBrowserOpenCommand(url, options.platform);
|
|
5773
|
-
await (options.run ?? (async (file, args) => {
|
|
5774
|
-
await execFileAsync(file, args);
|
|
5775
|
-
}))(command.file, command.args);
|
|
5776
|
-
}
|
|
5777
|
-
//#endregion
|
|
5778
5934
|
//#region src/commands/web.ts
|
|
5779
5935
|
function webHelp() {
|
|
5780
5936
|
return `Granola web
|
|
@@ -5783,6 +5939,7 @@ Usage:
|
|
|
5783
5939
|
granola web [options]
|
|
5784
5940
|
|
|
5785
5941
|
Options:
|
|
5942
|
+
--meeting <id> Open a specific meeting on load
|
|
5786
5943
|
--network <mode> Network mode: local or lan (default: local)
|
|
5787
5944
|
--hostname <value> Hostname to bind (overrides network default)
|
|
5788
5945
|
--port <value> Port to bind (default: 0 for any available port)
|
|
@@ -5814,6 +5971,7 @@ const commands = [
|
|
|
5814
5971
|
cache: { type: "string" },
|
|
5815
5972
|
help: { type: "boolean" },
|
|
5816
5973
|
hostname: { type: "string" },
|
|
5974
|
+
meeting: { type: "string" },
|
|
5817
5975
|
network: { type: "string" },
|
|
5818
5976
|
open: { type: "boolean" },
|
|
5819
5977
|
password: { type: "string" },
|
|
@@ -5833,55 +5991,12 @@ const commands = [
|
|
|
5833
5991
|
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5834
5992
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5835
5993
|
const app = await createGranolaApp(config, { surface: "web" });
|
|
5836
|
-
const
|
|
5837
|
-
const
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
const trustedOrigins = parseTrustedOrigins(commandFlags["trusted-origins"]);
|
|
5842
|
-
const server = await startGranolaServer(app, {
|
|
5843
|
-
enableWebClient: true,
|
|
5844
|
-
hostname,
|
|
5845
|
-
port,
|
|
5846
|
-
security: {
|
|
5847
|
-
password,
|
|
5848
|
-
trustedOrigins
|
|
5849
|
-
}
|
|
5994
|
+
const options = resolveGranolaWebWorkspaceOptions(commandFlags);
|
|
5995
|
+
const targetMeetingId = typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0;
|
|
5996
|
+
return await runGranolaWebWorkspace(app, {
|
|
5997
|
+
...options,
|
|
5998
|
+
targetMeetingId
|
|
5850
5999
|
});
|
|
5851
|
-
console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
|
|
5852
|
-
console.log(`Network mode: ${networkMode}`);
|
|
5853
|
-
if (password) console.log("Server password protection: enabled");
|
|
5854
|
-
else if (networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
|
|
5855
|
-
if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
|
|
5856
|
-
console.log("Routes:");
|
|
5857
|
-
console.log(" GET /");
|
|
5858
|
-
console.log(" GET /health");
|
|
5859
|
-
console.log(" POST /auth/unlock");
|
|
5860
|
-
console.log(" POST /auth/lock");
|
|
5861
|
-
console.log(" GET /auth/status");
|
|
5862
|
-
console.log(" GET /state");
|
|
5863
|
-
console.log(" GET /events");
|
|
5864
|
-
console.log(" GET /meetings");
|
|
5865
|
-
console.log(" GET /meetings/:id");
|
|
5866
|
-
console.log(" GET /exports/jobs");
|
|
5867
|
-
console.log(" POST /auth/login");
|
|
5868
|
-
console.log(" POST /auth/logout");
|
|
5869
|
-
console.log(" POST /auth/mode");
|
|
5870
|
-
console.log(" POST /auth/refresh");
|
|
5871
|
-
console.log(" POST /exports/notes");
|
|
5872
|
-
console.log(" POST /exports/jobs/:id/rerun");
|
|
5873
|
-
console.log(" POST /exports/transcripts");
|
|
5874
|
-
console.log(`Attach: granola attach ${server.url.href}`);
|
|
5875
|
-
if (password) console.log("Attach password: add --password <value>");
|
|
5876
|
-
if (openBrowser) try {
|
|
5877
|
-
await openExternalUrl(server.url);
|
|
5878
|
-
} catch (error) {
|
|
5879
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5880
|
-
console.error(`failed to open browser automatically: ${message}`);
|
|
5881
|
-
console.error(`open ${server.url.href} manually`);
|
|
5882
|
-
}
|
|
5883
|
-
await waitForShutdown(async () => await server.close());
|
|
5884
|
-
return 0;
|
|
5885
6000
|
}
|
|
5886
6001
|
}
|
|
5887
6002
|
];
|