granola-toolkit 0.27.0 → 0.29.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 +10 -0
- package/dist/cli.js +754 -411
- 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
|
|
@@ -272,6 +280,7 @@ The initial terminal workspace includes:
|
|
|
272
280
|
|
|
273
281
|
- a meeting list pane with keyboard navigation
|
|
274
282
|
- a detail pane with notes, transcript, metadata, and raw views
|
|
283
|
+
- an auth session overlay for import, refresh, source switching, and sign-out
|
|
275
284
|
- a footer with app state and key hints
|
|
276
285
|
- a quick-open overlay for jumping by title, id, or tag
|
|
277
286
|
|
|
@@ -279,6 +288,7 @@ The main keyboard controls are:
|
|
|
279
288
|
|
|
280
289
|
- `j` / `k` or arrow keys to move between meetings
|
|
281
290
|
- `/` or `Ctrl+P` to open quick open
|
|
291
|
+
- `a` to open auth session actions
|
|
282
292
|
- `1`-`4` to switch detail tabs
|
|
283
293
|
- `PageUp` / `PageDown` to scroll the detail pane
|
|
284
294
|
- `r` to refresh from live Granola data
|
package/dist/cli.js
CHANGED
|
@@ -1305,6 +1305,151 @@ const granolaTuiTheme = {
|
|
|
1305
1305
|
}
|
|
1306
1306
|
};
|
|
1307
1307
|
//#endregion
|
|
1308
|
+
//#region src/tui/auth.ts
|
|
1309
|
+
function padLine$2(text, width) {
|
|
1310
|
+
const clipped = truncateToWidth(text, width, "");
|
|
1311
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
1312
|
+
}
|
|
1313
|
+
function frameLine$1(text, width) {
|
|
1314
|
+
return `| ${padLine$2(text, Math.max(1, width - 4))} |`;
|
|
1315
|
+
}
|
|
1316
|
+
function actionDisabledReason(auth, actionId) {
|
|
1317
|
+
switch (actionId) {
|
|
1318
|
+
case "login": return auth.supabaseAvailable ? "" : "supabase.json unavailable";
|
|
1319
|
+
case "refresh":
|
|
1320
|
+
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1321
|
+
return auth.refreshAvailable ? "" : "refresh unavailable";
|
|
1322
|
+
case "use-stored":
|
|
1323
|
+
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1324
|
+
return auth.mode === "stored-session" ? "already active" : "";
|
|
1325
|
+
case "use-supabase":
|
|
1326
|
+
if (!auth.supabaseAvailable) return "supabase.json unavailable";
|
|
1327
|
+
return auth.mode === "supabase-file" ? "already active" : "";
|
|
1328
|
+
case "logout": return auth.storedSessionAvailable ? "" : "stored session missing";
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function buildGranolaTuiAuthActions(auth) {
|
|
1332
|
+
return [
|
|
1333
|
+
{
|
|
1334
|
+
description: "Import the Granola desktop session from supabase.json",
|
|
1335
|
+
id: "login",
|
|
1336
|
+
key: "1",
|
|
1337
|
+
label: "Import desktop session"
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
description: "Refresh the stored Granola session",
|
|
1341
|
+
id: "refresh",
|
|
1342
|
+
key: "2",
|
|
1343
|
+
label: "Refresh stored session"
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
description: "Switch the active auth source to the stored session",
|
|
1347
|
+
id: "use-stored",
|
|
1348
|
+
key: "3",
|
|
1349
|
+
label: "Use stored session"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
description: "Switch the active auth source to supabase.json",
|
|
1353
|
+
id: "use-supabase",
|
|
1354
|
+
key: "4",
|
|
1355
|
+
label: "Use supabase.json"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
description: "Delete the stored session and fall back to supabase.json",
|
|
1359
|
+
id: "logout",
|
|
1360
|
+
key: "5",
|
|
1361
|
+
label: "Sign out"
|
|
1362
|
+
}
|
|
1363
|
+
].map((action) => {
|
|
1364
|
+
const disabledReason = actionDisabledReason(auth, action.id);
|
|
1365
|
+
return {
|
|
1366
|
+
...action,
|
|
1367
|
+
disabled: disabledReason.length > 0,
|
|
1368
|
+
disabledReason: disabledReason || void 0
|
|
1369
|
+
};
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
function renderGranolaTuiAuthState(auth) {
|
|
1373
|
+
const lines = [
|
|
1374
|
+
`Active source: ${auth.mode === "stored-session" ? "Stored session" : "supabase.json"}`,
|
|
1375
|
+
`Stored session: ${auth.storedSessionAvailable ? "available" : "missing"}`,
|
|
1376
|
+
`supabase.json: ${auth.supabaseAvailable ? "available" : "missing"}`,
|
|
1377
|
+
`Refresh: ${auth.refreshAvailable ? "available" : "missing"}`
|
|
1378
|
+
];
|
|
1379
|
+
if (auth.clientId) lines.push(`Client ID: ${auth.clientId}`);
|
|
1380
|
+
if (auth.signInMethod) lines.push(`Sign-in method: ${auth.signInMethod}`);
|
|
1381
|
+
if (auth.supabasePath) lines.push(`supabase path: ${auth.supabasePath}`);
|
|
1382
|
+
if (auth.lastError) lines.push(`Last error: ${auth.lastError}`);
|
|
1383
|
+
return lines.join("\n");
|
|
1384
|
+
}
|
|
1385
|
+
function nextEnabledIndex(actions, startIndex, delta) {
|
|
1386
|
+
if (actions.length === 0) return -1;
|
|
1387
|
+
for (let attempts = 0; attempts < actions.length; attempts += 1) {
|
|
1388
|
+
const nextIndex = (startIndex + delta * (attempts + 1) + actions.length) % actions.length;
|
|
1389
|
+
if (!actions[nextIndex]?.disabled) return nextIndex;
|
|
1390
|
+
}
|
|
1391
|
+
return Math.max(0, Math.min(actions.length - 1, startIndex));
|
|
1392
|
+
}
|
|
1393
|
+
var GranolaTuiAuthOverlay = class {
|
|
1394
|
+
focused = false;
|
|
1395
|
+
#actions;
|
|
1396
|
+
#selectedIndex;
|
|
1397
|
+
constructor(options) {
|
|
1398
|
+
this.options = options;
|
|
1399
|
+
this.#actions = buildGranolaTuiAuthActions(this.options.auth);
|
|
1400
|
+
const firstEnabledIndex = this.#actions.findIndex((action) => !action.disabled);
|
|
1401
|
+
this.#selectedIndex = firstEnabledIndex >= 0 ? firstEnabledIndex : 0;
|
|
1402
|
+
}
|
|
1403
|
+
invalidate() {}
|
|
1404
|
+
async runAction(actionId) {
|
|
1405
|
+
const action = this.#actions.find((candidate) => candidate.id === actionId);
|
|
1406
|
+
if (!action || action.disabled) return;
|
|
1407
|
+
await this.options.onRun(action.id);
|
|
1408
|
+
}
|
|
1409
|
+
handleInput(data) {
|
|
1410
|
+
if (matchesKey(data, "esc")) {
|
|
1411
|
+
this.options.onCancel();
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (matchesKey(data, "up")) {
|
|
1415
|
+
this.#selectedIndex = nextEnabledIndex(this.#actions, this.#selectedIndex, -1);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (matchesKey(data, "down")) {
|
|
1419
|
+
this.#selectedIndex = nextEnabledIndex(this.#actions, this.#selectedIndex, 1);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const selected = this.#actions[this.#selectedIndex];
|
|
1423
|
+
if (matchesKey(data, "enter")) {
|
|
1424
|
+
if (selected && !selected.disabled) this.runAction(selected.id);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const hotkeyAction = this.#actions.find((action) => action.key === data);
|
|
1428
|
+
if (hotkeyAction && !hotkeyAction.disabled) this.runAction(hotkeyAction.id);
|
|
1429
|
+
}
|
|
1430
|
+
render(width) {
|
|
1431
|
+
const lines = [];
|
|
1432
|
+
const bodyWidth = Math.max(48, width);
|
|
1433
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1434
|
+
lines.push(frameLine$1(granolaTuiTheme.strong("Auth Session"), bodyWidth));
|
|
1435
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1436
|
+
for (const detailLine of renderGranolaTuiAuthState(this.options.auth).split("\n")) lines.push(frameLine$1(detailLine, bodyWidth));
|
|
1437
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1438
|
+
for (const action of this.#actions) {
|
|
1439
|
+
const selected = this.#actions[this.#selectedIndex]?.id === action.id;
|
|
1440
|
+
const label = `${action.key}. ${action.label}`;
|
|
1441
|
+
const titleLine = action.disabled ? granolaTuiTheme.dim(label) : selected ? granolaTuiTheme.selected(label) : label;
|
|
1442
|
+
lines.push(frameLine$1(titleLine, bodyWidth));
|
|
1443
|
+
const detail = action.disabled ? granolaTuiTheme.warning(action.disabledReason ?? "Unavailable") : granolaTuiTheme.dim(action.description);
|
|
1444
|
+
for (const wrapped of wrapTextWithAnsi(detail, Math.max(1, bodyWidth - 6))) lines.push(frameLine$1(` ${wrapped}`, bodyWidth));
|
|
1445
|
+
}
|
|
1446
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1447
|
+
lines.push(frameLine$1(granolaTuiTheme.dim("Enter to run, Esc to cancel, arrows to move"), bodyWidth));
|
|
1448
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1449
|
+
return lines;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
//#endregion
|
|
1308
1453
|
//#region src/tui/palette.ts
|
|
1309
1454
|
function padLine$1(text, width) {
|
|
1310
1455
|
const clipped = truncateToWidth(text, width, "");
|
|
@@ -1597,6 +1742,85 @@ var GranolaTuiWorkspace = class {
|
|
|
1597
1742
|
this.#detailScroll = 0;
|
|
1598
1743
|
this.tui.requestRender();
|
|
1599
1744
|
}
|
|
1745
|
+
async reloadAfterAuthChange() {
|
|
1746
|
+
const preferredMeetingId = this.#selectedMeeting?.document.id ?? this.#selectedMeetingId;
|
|
1747
|
+
try {
|
|
1748
|
+
await this.loadMeetings({
|
|
1749
|
+
forceRefresh: true,
|
|
1750
|
+
preferredMeetingId,
|
|
1751
|
+
setStatus: false
|
|
1752
|
+
});
|
|
1753
|
+
if (this.#selectedMeetingId) {
|
|
1754
|
+
await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
this.#selectedMeeting = void 0;
|
|
1758
|
+
this.#detailError = "";
|
|
1759
|
+
this.#detailScroll = 0;
|
|
1760
|
+
this.tui.requestRender();
|
|
1761
|
+
} catch {}
|
|
1762
|
+
}
|
|
1763
|
+
async runAuthAction(actionId) {
|
|
1764
|
+
let successMessage = "";
|
|
1765
|
+
try {
|
|
1766
|
+
switch (actionId) {
|
|
1767
|
+
case "login":
|
|
1768
|
+
this.setStatus("Importing desktop session…");
|
|
1769
|
+
await this.app.loginAuth();
|
|
1770
|
+
successMessage = "Stored session imported";
|
|
1771
|
+
break;
|
|
1772
|
+
case "refresh":
|
|
1773
|
+
this.setStatus("Refreshing stored session…");
|
|
1774
|
+
await this.app.refreshAuth();
|
|
1775
|
+
successMessage = "Stored session refreshed";
|
|
1776
|
+
break;
|
|
1777
|
+
case "use-stored":
|
|
1778
|
+
this.setStatus("Switching to stored session…");
|
|
1779
|
+
await this.app.switchAuthMode("stored-session");
|
|
1780
|
+
successMessage = "Using stored session";
|
|
1781
|
+
break;
|
|
1782
|
+
case "use-supabase":
|
|
1783
|
+
this.setStatus("Switching to supabase.json…");
|
|
1784
|
+
await this.app.switchAuthMode("supabase-file");
|
|
1785
|
+
successMessage = "Using supabase.json";
|
|
1786
|
+
break;
|
|
1787
|
+
case "logout":
|
|
1788
|
+
this.setStatus("Signing out…");
|
|
1789
|
+
await this.app.logoutAuth();
|
|
1790
|
+
successMessage = "Stored session removed";
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
await this.reloadAfterAuthChange();
|
|
1794
|
+
this.setStatus(successMessage);
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1797
|
+
this.setStatus(message, "error");
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
openAuthPanel(auth = this.#appState.auth) {
|
|
1801
|
+
if (this.#overlay) return;
|
|
1802
|
+
const closeOverlay = () => {
|
|
1803
|
+
this.#overlay?.hide();
|
|
1804
|
+
this.#overlay = void 0;
|
|
1805
|
+
this.tui.setFocus(this);
|
|
1806
|
+
this.tui.requestRender();
|
|
1807
|
+
};
|
|
1808
|
+
const overlay = new GranolaTuiAuthOverlay({
|
|
1809
|
+
auth,
|
|
1810
|
+
onCancel: closeOverlay,
|
|
1811
|
+
onRun: async (actionId) => {
|
|
1812
|
+
closeOverlay();
|
|
1813
|
+
await this.runAuthAction(actionId);
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
this.#overlay = this.tui.showOverlay(overlay, {
|
|
1817
|
+
anchor: "center",
|
|
1818
|
+
maxHeight: "70%",
|
|
1819
|
+
minWidth: 52,
|
|
1820
|
+
width: "72%"
|
|
1821
|
+
});
|
|
1822
|
+
this.setStatus("Auth session");
|
|
1823
|
+
}
|
|
1600
1824
|
openQuickOpen() {
|
|
1601
1825
|
if (this.#overlay) return;
|
|
1602
1826
|
const closeOverlay = () => {
|
|
@@ -1641,6 +1865,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1641
1865
|
this.openQuickOpen();
|
|
1642
1866
|
return;
|
|
1643
1867
|
}
|
|
1868
|
+
if (matchesKey(data, "a")) {
|
|
1869
|
+
this.openAuthPanel();
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1644
1872
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
1645
1873
|
this.moveSelection(-1);
|
|
1646
1874
|
return;
|
|
@@ -1779,7 +2007,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1779
2007
|
const bodyLines = [];
|
|
1780
2008
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
1781
2009
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
1782
|
-
const footerHints = padLine(granolaTuiTheme.dim("/ quick open r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2010
|
+
const footerHints = padLine(granolaTuiTheme.dim("/ quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
1783
2011
|
return [
|
|
1784
2012
|
headerTitle,
|
|
1785
2013
|
headerSummary,
|
|
@@ -3452,337 +3680,138 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
3452
3680
|
return 0;
|
|
3453
3681
|
}
|
|
3454
3682
|
//#endregion
|
|
3455
|
-
//#region src/
|
|
3456
|
-
|
|
3457
|
-
|
|
3683
|
+
//#region src/browser.ts
|
|
3684
|
+
const execFileAsync = promisify(execFile);
|
|
3685
|
+
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
3686
|
+
const href = String(url);
|
|
3687
|
+
switch (platform) {
|
|
3688
|
+
case "darwin": return {
|
|
3689
|
+
args: [href],
|
|
3690
|
+
file: "open"
|
|
3691
|
+
};
|
|
3692
|
+
case "win32": return {
|
|
3693
|
+
args: [
|
|
3694
|
+
"/c",
|
|
3695
|
+
"start",
|
|
3696
|
+
"",
|
|
3697
|
+
href
|
|
3698
|
+
],
|
|
3699
|
+
file: "cmd"
|
|
3700
|
+
};
|
|
3701
|
+
default: return {
|
|
3702
|
+
args: [href],
|
|
3703
|
+
file: "xdg-open"
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
async function openExternalUrl(url, options = {}) {
|
|
3708
|
+
const command = getBrowserOpenCommand(url, options.platform);
|
|
3709
|
+
await (options.run ?? (async (file, args) => {
|
|
3710
|
+
await execFileAsync(file, args);
|
|
3711
|
+
}))(command.file, command.args);
|
|
3712
|
+
}
|
|
3713
|
+
//#endregion
|
|
3714
|
+
//#region src/web/client-script.ts
|
|
3715
|
+
const granolaWebClientScript = String.raw`
|
|
3716
|
+
const serverConfig = window.__GRANOLA_SERVER__ || { passwordRequired: false };
|
|
3717
|
+
const workspaceTabs = ["notes", "transcript", "metadata", "raw"];
|
|
3458
3718
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3719
|
+
const state = {
|
|
3720
|
+
appState: null,
|
|
3721
|
+
detailError: "",
|
|
3722
|
+
listError: "",
|
|
3723
|
+
meetings: [],
|
|
3724
|
+
quickOpen: "",
|
|
3725
|
+
search: "",
|
|
3726
|
+
selectedMeeting: null,
|
|
3727
|
+
selectedMeetingBundle: null,
|
|
3728
|
+
selectedMeetingId: null,
|
|
3729
|
+
meetingSource: "live",
|
|
3730
|
+
serverLocked: Boolean(serverConfig.passwordRequired),
|
|
3731
|
+
sort: "updated-desc",
|
|
3732
|
+
updatedFrom: "",
|
|
3733
|
+
updatedTo: "",
|
|
3734
|
+
workspaceTab: "notes",
|
|
3735
|
+
};
|
|
3461
3736
|
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3737
|
+
const els = {
|
|
3738
|
+
appState: document.querySelector("[data-app-state]"),
|
|
3739
|
+
authPanel: document.querySelector("[data-auth-panel]"),
|
|
3740
|
+
detailBody: document.querySelector("[data-detail-body]"),
|
|
3741
|
+
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
3742
|
+
empty: document.querySelector("[data-empty]"),
|
|
3743
|
+
jobsList: document.querySelector("[data-jobs-list]"),
|
|
3744
|
+
list: document.querySelector("[data-meeting-list]"),
|
|
3745
|
+
noteButton: document.querySelector("[data-export-notes]"),
|
|
3746
|
+
quickOpen: document.querySelector("[data-quick-open]"),
|
|
3747
|
+
quickOpenButton: document.querySelector("[data-quick-open-button]"),
|
|
3748
|
+
refreshButton: document.querySelector("[data-refresh]"),
|
|
3749
|
+
search: document.querySelector("[data-search]"),
|
|
3750
|
+
securityPanel: document.querySelector("[data-security-panel]"),
|
|
3751
|
+
serverPassword: document.querySelector("[data-server-password]"),
|
|
3752
|
+
lockServerButton: document.querySelector("[data-lock-server]"),
|
|
3753
|
+
sort: document.querySelector("[data-sort]"),
|
|
3754
|
+
stateBadge: document.querySelector("[data-state-badge]"),
|
|
3755
|
+
transcriptButton: document.querySelector("[data-export-transcripts]"),
|
|
3756
|
+
unlockServerButton: document.querySelector("[data-unlock-server]"),
|
|
3757
|
+
updatedFrom: document.querySelector("[data-updated-from]"),
|
|
3758
|
+
updatedTo: document.querySelector("[data-updated-to]"),
|
|
3759
|
+
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
3760
|
+
};
|
|
3468
3761
|
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
3472
|
-
--limit <n> Number of meetings for list (default: 20)
|
|
3473
|
-
--search <query> Filter list by title, id, or tag
|
|
3474
|
-
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
3475
|
-
--supabase <path> Path to supabase.json
|
|
3476
|
-
--debug Enable debug logging
|
|
3477
|
-
--config <path> Path to .granola.toml
|
|
3478
|
-
-h, --help Show help
|
|
3479
|
-
`;
|
|
3762
|
+
function parseWorkspaceTab(value) {
|
|
3763
|
+
return workspaceTabs.includes(value) ? value : "notes";
|
|
3480
3764
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
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
|
-
}
|
|
3765
|
+
|
|
3766
|
+
function startupSelection() {
|
|
3767
|
+
const params = new URLSearchParams(window.location.search);
|
|
3768
|
+
return {
|
|
3769
|
+
meetingId: params.get("meeting")?.trim() || "",
|
|
3770
|
+
workspaceTab: parseWorkspaceTab(params.get("tab")),
|
|
3771
|
+
};
|
|
3498
3772
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3773
|
+
|
|
3774
|
+
function syncBrowserUrl() {
|
|
3775
|
+
const url = new URL(window.location.href);
|
|
3776
|
+
|
|
3777
|
+
if (state.selectedMeetingId) {
|
|
3778
|
+
url.searchParams.set("meeting", state.selectedMeetingId);
|
|
3779
|
+
} else {
|
|
3780
|
+
url.searchParams.delete("meeting");
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
if (state.workspaceTab !== "notes") {
|
|
3784
|
+
url.searchParams.set("tab", state.workspaceTab);
|
|
3785
|
+
} else {
|
|
3786
|
+
url.searchParams.delete("tab");
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
const nextPath = url.pathname + url.search + url.hash;
|
|
3790
|
+
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
3791
|
+
if (nextPath !== currentPath) {
|
|
3792
|
+
history.replaceState(null, "", nextPath);
|
|
3793
|
+
}
|
|
3506
3794
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
default: throw new Error("invalid meeting notes format: expected markdown, json, yaml, or raw");
|
|
3515
|
-
}
|
|
3795
|
+
|
|
3796
|
+
function escapeHtml(value) {
|
|
3797
|
+
return value
|
|
3798
|
+
.replaceAll("&", "&")
|
|
3799
|
+
.replaceAll("<", "<")
|
|
3800
|
+
.replaceAll(">", ">")
|
|
3801
|
+
.replaceAll('"', """);
|
|
3516
3802
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
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
|
-
}
|
|
3803
|
+
|
|
3804
|
+
function setStatus(label, tone = "idle") {
|
|
3805
|
+
els.stateBadge.textContent = label;
|
|
3806
|
+
els.stateBadge.dataset.tone = tone;
|
|
3526
3807
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
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
|
-
}
|
|
3567
|
-
}
|
|
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
|
-
}
|
|
3626
|
-
async function notes(id, commandFlags, globalFlags) {
|
|
3627
|
-
const format = resolveNotesFormat(commandFlags.format);
|
|
3628
|
-
const config = await loadConfig({
|
|
3629
|
-
globalFlags,
|
|
3630
|
-
subcommandFlags: commandFlags
|
|
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
|
-
}
|
|
3718
|
-
}
|
|
3719
|
-
//#endregion
|
|
3720
|
-
//#region src/web/client-script.ts
|
|
3721
|
-
const granolaWebClientScript = String.raw`
|
|
3722
|
-
const serverConfig = window.__GRANOLA_SERVER__ || { passwordRequired: false };
|
|
3723
|
-
|
|
3724
|
-
const state = {
|
|
3725
|
-
appState: null,
|
|
3726
|
-
detailError: "",
|
|
3727
|
-
listError: "",
|
|
3728
|
-
meetings: [],
|
|
3729
|
-
quickOpen: "",
|
|
3730
|
-
search: "",
|
|
3731
|
-
selectedMeeting: null,
|
|
3732
|
-
selectedMeetingBundle: null,
|
|
3733
|
-
selectedMeetingId: null,
|
|
3734
|
-
meetingSource: "live",
|
|
3735
|
-
serverLocked: Boolean(serverConfig.passwordRequired),
|
|
3736
|
-
sort: "updated-desc",
|
|
3737
|
-
updatedFrom: "",
|
|
3738
|
-
updatedTo: "",
|
|
3739
|
-
workspaceTab: "notes",
|
|
3740
|
-
};
|
|
3741
|
-
|
|
3742
|
-
const els = {
|
|
3743
|
-
appState: document.querySelector("[data-app-state]"),
|
|
3744
|
-
authPanel: document.querySelector("[data-auth-panel]"),
|
|
3745
|
-
detailBody: document.querySelector("[data-detail-body]"),
|
|
3746
|
-
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
3747
|
-
empty: document.querySelector("[data-empty]"),
|
|
3748
|
-
jobsList: document.querySelector("[data-jobs-list]"),
|
|
3749
|
-
list: document.querySelector("[data-meeting-list]"),
|
|
3750
|
-
noteButton: document.querySelector("[data-export-notes]"),
|
|
3751
|
-
quickOpen: document.querySelector("[data-quick-open]"),
|
|
3752
|
-
quickOpenButton: document.querySelector("[data-quick-open-button]"),
|
|
3753
|
-
refreshButton: document.querySelector("[data-refresh]"),
|
|
3754
|
-
search: document.querySelector("[data-search]"),
|
|
3755
|
-
securityPanel: document.querySelector("[data-security-panel]"),
|
|
3756
|
-
serverPassword: document.querySelector("[data-server-password]"),
|
|
3757
|
-
lockServerButton: document.querySelector("[data-lock-server]"),
|
|
3758
|
-
sort: document.querySelector("[data-sort]"),
|
|
3759
|
-
stateBadge: document.querySelector("[data-state-badge]"),
|
|
3760
|
-
transcriptButton: document.querySelector("[data-export-transcripts]"),
|
|
3761
|
-
unlockServerButton: document.querySelector("[data-unlock-server]"),
|
|
3762
|
-
updatedFrom: document.querySelector("[data-updated-from]"),
|
|
3763
|
-
updatedTo: document.querySelector("[data-updated-to]"),
|
|
3764
|
-
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
3765
|
-
};
|
|
3766
|
-
|
|
3767
|
-
function escapeHtml(value) {
|
|
3768
|
-
return value
|
|
3769
|
-
.replaceAll("&", "&")
|
|
3770
|
-
.replaceAll("<", "<")
|
|
3771
|
-
.replaceAll(">", ">")
|
|
3772
|
-
.replaceAll('"', """);
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
function setStatus(label, tone = "idle") {
|
|
3776
|
-
els.stateBadge.textContent = label;
|
|
3777
|
-
els.stateBadge.dataset.tone = tone;
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
function syncFilterInputs() {
|
|
3781
|
-
els.quickOpen.value = state.quickOpen;
|
|
3782
|
-
els.search.value = state.search;
|
|
3783
|
-
els.sort.value = state.sort;
|
|
3784
|
-
els.updatedFrom.value = state.updatedFrom;
|
|
3785
|
-
els.updatedTo.value = state.updatedTo;
|
|
3808
|
+
|
|
3809
|
+
function syncFilterInputs() {
|
|
3810
|
+
els.quickOpen.value = state.quickOpen;
|
|
3811
|
+
els.search.value = state.search;
|
|
3812
|
+
els.sort.value = state.sort;
|
|
3813
|
+
els.updatedFrom.value = state.updatedFrom;
|
|
3814
|
+
els.updatedTo.value = state.updatedTo;
|
|
3786
3815
|
}
|
|
3787
3816
|
|
|
3788
3817
|
function currentFilterSummary() {
|
|
@@ -3927,6 +3956,7 @@ function renderMeetingList() {
|
|
|
3927
3956
|
state.selectedMeetingId = null;
|
|
3928
3957
|
state.selectedMeeting = null;
|
|
3929
3958
|
state.selectedMeetingBundle = null;
|
|
3959
|
+
syncBrowserUrl();
|
|
3930
3960
|
const filterSummary = currentFilterSummary();
|
|
3931
3961
|
const message = filterSummary
|
|
3932
3962
|
? "No meetings match " + filterSummary + "."
|
|
@@ -3940,6 +3970,7 @@ function renderMeetingList() {
|
|
|
3940
3970
|
if (!state.selectedMeetingId || !visibleIds.has(state.selectedMeetingId)) {
|
|
3941
3971
|
state.selectedMeetingId = state.meetings[0]?.id || null;
|
|
3942
3972
|
}
|
|
3973
|
+
syncBrowserUrl();
|
|
3943
3974
|
|
|
3944
3975
|
els.list.innerHTML = state.meetings
|
|
3945
3976
|
.map((meeting) => {
|
|
@@ -4138,6 +4169,7 @@ async function loadMeetings(options = {}) {
|
|
|
4138
4169
|
|
|
4139
4170
|
async function loadMeeting(id) {
|
|
4140
4171
|
state.selectedMeetingId = id;
|
|
4172
|
+
syncBrowserUrl();
|
|
4141
4173
|
renderMeetingList();
|
|
4142
4174
|
|
|
4143
4175
|
try {
|
|
@@ -4496,6 +4528,7 @@ els.quickOpenButton.addEventListener("click", () => {
|
|
|
4496
4528
|
els.workspaceTabs.forEach((button) => {
|
|
4497
4529
|
button.addEventListener("click", () => {
|
|
4498
4530
|
state.workspaceTab = button.dataset.workspaceTab || "notes";
|
|
4531
|
+
syncBrowserUrl();
|
|
4499
4532
|
renderMeetingDetail();
|
|
4500
4533
|
});
|
|
4501
4534
|
});
|
|
@@ -4512,24 +4545,28 @@ document.addEventListener("keydown", (event) => {
|
|
|
4512
4545
|
const tabs = ["notes", "transcript", "metadata", "raw"];
|
|
4513
4546
|
if (event.key === "1") {
|
|
4514
4547
|
state.workspaceTab = "notes";
|
|
4548
|
+
syncBrowserUrl();
|
|
4515
4549
|
renderMeetingDetail();
|
|
4516
4550
|
return;
|
|
4517
4551
|
}
|
|
4518
4552
|
|
|
4519
4553
|
if (event.key === "2") {
|
|
4520
4554
|
state.workspaceTab = "transcript";
|
|
4555
|
+
syncBrowserUrl();
|
|
4521
4556
|
renderMeetingDetail();
|
|
4522
4557
|
return;
|
|
4523
4558
|
}
|
|
4524
4559
|
|
|
4525
4560
|
if (event.key === "3") {
|
|
4526
4561
|
state.workspaceTab = "metadata";
|
|
4562
|
+
syncBrowserUrl();
|
|
4527
4563
|
renderMeetingDetail();
|
|
4528
4564
|
return;
|
|
4529
4565
|
}
|
|
4530
4566
|
|
|
4531
4567
|
if (event.key === "4") {
|
|
4532
4568
|
state.workspaceTab = "raw";
|
|
4569
|
+
syncBrowserUrl();
|
|
4533
4570
|
renderMeetingDetail();
|
|
4534
4571
|
return;
|
|
4535
4572
|
}
|
|
@@ -4537,15 +4574,21 @@ document.addEventListener("keydown", (event) => {
|
|
|
4537
4574
|
const currentIndex = tabs.indexOf(state.workspaceTab);
|
|
4538
4575
|
if (event.key === "]") {
|
|
4539
4576
|
state.workspaceTab = tabs[(currentIndex + 1) % tabs.length];
|
|
4577
|
+
syncBrowserUrl();
|
|
4540
4578
|
renderMeetingDetail();
|
|
4541
4579
|
}
|
|
4542
4580
|
|
|
4543
4581
|
if (event.key === "[") {
|
|
4544
4582
|
state.workspaceTab = tabs[(currentIndex + tabs.length - 1) % tabs.length];
|
|
4583
|
+
syncBrowserUrl();
|
|
4545
4584
|
renderMeetingDetail();
|
|
4546
4585
|
}
|
|
4547
4586
|
});
|
|
4548
4587
|
|
|
4588
|
+
const initialSelection = startupSelection();
|
|
4589
|
+
state.selectedMeetingId = initialSelection.meetingId || null;
|
|
4590
|
+
state.workspaceTab = initialSelection.workspaceTab;
|
|
4591
|
+
|
|
4549
4592
|
const events = new EventSource("/events");
|
|
4550
4593
|
events.addEventListener("state.updated", (event) => {
|
|
4551
4594
|
const previousLoadedAt = state.appState?.documents?.loadedAt;
|
|
@@ -5562,14 +5605,386 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5562
5605
|
};
|
|
5563
5606
|
}
|
|
5564
5607
|
//#endregion
|
|
5565
|
-
//#region src/
|
|
5566
|
-
function
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5608
|
+
//#region src/web-url.ts
|
|
5609
|
+
function buildGranolaMeetingUrl(baseUrl, meetingId) {
|
|
5610
|
+
const url = new URL(baseUrl);
|
|
5611
|
+
if (meetingId.trim()) url.searchParams.set("meeting", meetingId.trim());
|
|
5612
|
+
return url;
|
|
5613
|
+
}
|
|
5614
|
+
//#endregion
|
|
5615
|
+
//#region src/commands/web-shared.ts
|
|
5616
|
+
function resolveGranolaWebWorkspaceOptions(commandFlags) {
|
|
5617
|
+
const networkMode = parseNetworkMode(commandFlags.network);
|
|
5618
|
+
const hostname = resolveServerHostname(networkMode, commandFlags.hostname);
|
|
5619
|
+
const port = parsePort(commandFlags.port);
|
|
5620
|
+
return {
|
|
5621
|
+
hostname,
|
|
5622
|
+
networkMode,
|
|
5623
|
+
openBrowser: commandFlags.open !== false,
|
|
5624
|
+
password: typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password.trim() : void 0,
|
|
5625
|
+
port,
|
|
5626
|
+
trustedOrigins: parseTrustedOrigins(commandFlags["trusted-origins"])
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
function printWebRoutes() {
|
|
5630
|
+
console.log("Routes:");
|
|
5631
|
+
console.log(" GET /");
|
|
5632
|
+
console.log(" GET /health");
|
|
5633
|
+
console.log(" POST /auth/unlock");
|
|
5634
|
+
console.log(" POST /auth/lock");
|
|
5635
|
+
console.log(" GET /auth/status");
|
|
5636
|
+
console.log(" GET /state");
|
|
5637
|
+
console.log(" GET /events");
|
|
5638
|
+
console.log(" GET /meetings");
|
|
5639
|
+
console.log(" GET /meetings/:id");
|
|
5640
|
+
console.log(" GET /exports/jobs");
|
|
5641
|
+
console.log(" POST /auth/login");
|
|
5642
|
+
console.log(" POST /auth/logout");
|
|
5643
|
+
console.log(" POST /auth/mode");
|
|
5644
|
+
console.log(" POST /auth/refresh");
|
|
5645
|
+
console.log(" POST /exports/notes");
|
|
5646
|
+
console.log(" POST /exports/jobs/:id/rerun");
|
|
5647
|
+
console.log(" POST /exports/transcripts");
|
|
5648
|
+
}
|
|
5649
|
+
async function runGranolaWebWorkspace(app, options) {
|
|
5650
|
+
const server = await startGranolaServer(app, {
|
|
5651
|
+
enableWebClient: true,
|
|
5652
|
+
hostname: options.hostname,
|
|
5653
|
+
port: options.port,
|
|
5654
|
+
security: {
|
|
5655
|
+
password: options.password,
|
|
5656
|
+
trustedOrigins: options.trustedOrigins
|
|
5657
|
+
}
|
|
5658
|
+
});
|
|
5659
|
+
const targetUrl = options.targetMeetingId ? buildGranolaMeetingUrl(server.url, options.targetMeetingId) : new URL(server.url);
|
|
5660
|
+
console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
|
|
5661
|
+
if (targetUrl.href !== server.url.href) console.log(`Focused meeting URL: ${targetUrl.href}`);
|
|
5662
|
+
console.log(`Network mode: ${options.networkMode}`);
|
|
5663
|
+
if (options.password) console.log("Server password protection: enabled");
|
|
5664
|
+
else if (options.networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
|
|
5665
|
+
if (options.trustedOrigins.length > 0) console.log(`Trusted origins: ${options.trustedOrigins.join(", ")}`);
|
|
5666
|
+
printWebRoutes();
|
|
5667
|
+
console.log(`Attach: granola attach ${server.url.href}`);
|
|
5668
|
+
if (options.password) console.log("Attach password: add --password <value>");
|
|
5669
|
+
if (options.openBrowser) try {
|
|
5670
|
+
await openExternalUrl(targetUrl);
|
|
5671
|
+
} catch (error) {
|
|
5672
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5673
|
+
console.error(`failed to open browser automatically: ${message}`);
|
|
5674
|
+
console.error(`open ${targetUrl.href} manually`);
|
|
5675
|
+
}
|
|
5676
|
+
await waitForShutdown(async () => await server.close());
|
|
5677
|
+
return 0;
|
|
5678
|
+
}
|
|
5679
|
+
//#endregion
|
|
5680
|
+
//#region src/commands/meeting.ts
|
|
5681
|
+
function meetingHelp() {
|
|
5682
|
+
return `Granola meeting
|
|
5683
|
+
|
|
5684
|
+
Usage:
|
|
5685
|
+
granola meeting <list|view|export|notes|transcript|open> [options]
|
|
5686
|
+
|
|
5687
|
+
Subcommands:
|
|
5688
|
+
list List meetings from the Granola API
|
|
5689
|
+
view <id> Show a single meeting with notes and transcript text
|
|
5690
|
+
export <id> Export a single meeting as JSON or YAML
|
|
5691
|
+
notes <id> Show a single meeting's notes
|
|
5692
|
+
transcript <id> Show a single meeting's transcript
|
|
5693
|
+
open <id> Start the web workspace focused on one meeting
|
|
5694
|
+
|
|
5695
|
+
Options:
|
|
5696
|
+
--cache <path> Path to Granola cache JSON for transcript data
|
|
5697
|
+
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
5698
|
+
--network <mode> open: local or lan (default: local)
|
|
5699
|
+
--hostname <value> open: hostname to bind (overrides network default)
|
|
5700
|
+
--limit <n> Number of meetings for list (default: 20)
|
|
5701
|
+
--open[=true|false] open: launch the browser automatically (default: true)
|
|
5702
|
+
--password <value> open: optional server password
|
|
5703
|
+
--port <value> open: port to bind (default: 0 for any available port)
|
|
5704
|
+
--search <query> Filter list by title, id, or tag
|
|
5705
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5706
|
+
--trusted-origins <v> open: comma-separated extra browser origins to trust
|
|
5707
|
+
--supabase <path> Path to supabase.json
|
|
5708
|
+
--debug Enable debug logging
|
|
5709
|
+
--config <path> Path to .granola.toml
|
|
5710
|
+
-h, --help Show help
|
|
5711
|
+
`;
|
|
5712
|
+
}
|
|
5713
|
+
function resolveListFormat(value) {
|
|
5714
|
+
switch (value) {
|
|
5715
|
+
case void 0: return "text";
|
|
5716
|
+
case "json":
|
|
5717
|
+
case "text":
|
|
5718
|
+
case "yaml": return value;
|
|
5719
|
+
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
function resolveViewFormat(value) {
|
|
5723
|
+
switch (value) {
|
|
5724
|
+
case void 0: return "text";
|
|
5725
|
+
case "json":
|
|
5726
|
+
case "text":
|
|
5727
|
+
case "yaml": return value;
|
|
5728
|
+
default: throw new Error("invalid meeting format: expected text, json, or yaml");
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
function resolveExportFormat(value) {
|
|
5732
|
+
switch (value) {
|
|
5733
|
+
case void 0: return "json";
|
|
5734
|
+
case "json":
|
|
5735
|
+
case "yaml": return value;
|
|
5736
|
+
default: throw new Error("invalid meeting export format: expected json or yaml");
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
function resolveNotesFormat(value) {
|
|
5740
|
+
switch (value) {
|
|
5741
|
+
case void 0: return "markdown";
|
|
5742
|
+
case "json":
|
|
5743
|
+
case "markdown":
|
|
5744
|
+
case "raw":
|
|
5745
|
+
case "yaml": return value;
|
|
5746
|
+
default: throw new Error("invalid meeting notes format: expected markdown, json, yaml, or raw");
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
function resolveTranscriptFormat$1(value) {
|
|
5750
|
+
switch (value) {
|
|
5751
|
+
case void 0: return "text";
|
|
5752
|
+
case "json":
|
|
5753
|
+
case "raw":
|
|
5754
|
+
case "text":
|
|
5755
|
+
case "yaml": return value;
|
|
5756
|
+
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
function parseLimit(value) {
|
|
5760
|
+
if (value === void 0) return 20;
|
|
5761
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
5762
|
+
const limit = Number(value);
|
|
5763
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("invalid meeting limit: expected a positive integer");
|
|
5764
|
+
return limit;
|
|
5765
|
+
}
|
|
5766
|
+
const meetingCommand = {
|
|
5767
|
+
description: "Inspect and export individual Granola meetings",
|
|
5768
|
+
flags: {
|
|
5769
|
+
cache: { type: "string" },
|
|
5770
|
+
format: { type: "string" },
|
|
5771
|
+
help: { type: "boolean" },
|
|
5772
|
+
hostname: { type: "string" },
|
|
5773
|
+
limit: { type: "string" },
|
|
5774
|
+
network: { type: "string" },
|
|
5775
|
+
open: { type: "boolean" },
|
|
5776
|
+
password: { type: "string" },
|
|
5777
|
+
port: { type: "string" },
|
|
5778
|
+
search: { type: "string" },
|
|
5779
|
+
timeout: { type: "string" },
|
|
5780
|
+
"trusted-origins": { type: "string" }
|
|
5781
|
+
},
|
|
5782
|
+
help: meetingHelp,
|
|
5783
|
+
name: "meeting",
|
|
5784
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
5785
|
+
const [action, id] = commandArgs;
|
|
5786
|
+
switch (action) {
|
|
5787
|
+
case "list": return await list(commandFlags, globalFlags);
|
|
5788
|
+
case "view":
|
|
5789
|
+
if (!id) throw new Error("meeting view requires an id");
|
|
5790
|
+
return await view(id, commandFlags, globalFlags);
|
|
5791
|
+
case "export":
|
|
5792
|
+
if (!id) throw new Error("meeting export requires an id");
|
|
5793
|
+
return await exportMeeting(id, commandFlags, globalFlags);
|
|
5794
|
+
case "notes":
|
|
5795
|
+
if (!id) throw new Error("meeting notes requires an id");
|
|
5796
|
+
return await notes(id, commandFlags, globalFlags);
|
|
5797
|
+
case "transcript":
|
|
5798
|
+
if (!id) throw new Error("meeting transcript requires an id");
|
|
5799
|
+
return await transcript(id, commandFlags, globalFlags);
|
|
5800
|
+
case "open":
|
|
5801
|
+
if (!id) throw new Error("meeting open requires an id");
|
|
5802
|
+
return await openMeeting(id, commandFlags, globalFlags);
|
|
5803
|
+
case void 0:
|
|
5804
|
+
console.log(meetingHelp());
|
|
5805
|
+
return 1;
|
|
5806
|
+
default: throw new Error("invalid meeting command: expected list, view, export, notes, transcript, or open");
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
};
|
|
5810
|
+
async function list(commandFlags, globalFlags) {
|
|
5811
|
+
const format = resolveListFormat(commandFlags.format);
|
|
5812
|
+
const limit = parseLimit(commandFlags.limit);
|
|
5813
|
+
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5814
|
+
const config = await loadConfig({
|
|
5815
|
+
globalFlags,
|
|
5816
|
+
subcommandFlags: commandFlags
|
|
5817
|
+
});
|
|
5818
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5819
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5820
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5821
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5822
|
+
const app = await createGranolaApp(config);
|
|
5823
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5824
|
+
console.log("Loading meetings...");
|
|
5825
|
+
const result = await app.listMeetings({
|
|
5826
|
+
limit,
|
|
5827
|
+
search
|
|
5828
|
+
});
|
|
5829
|
+
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
5830
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
5831
|
+
return 0;
|
|
5832
|
+
}
|
|
5833
|
+
async function view(id, commandFlags, globalFlags) {
|
|
5834
|
+
const format = resolveViewFormat(commandFlags.format);
|
|
5835
|
+
const config = await loadConfig({
|
|
5836
|
+
globalFlags,
|
|
5837
|
+
subcommandFlags: commandFlags
|
|
5838
|
+
});
|
|
5839
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5840
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5841
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5842
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5843
|
+
const app = await createGranolaApp(config);
|
|
5844
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5845
|
+
console.log("Fetching meeting from Granola API...");
|
|
5846
|
+
const result = await app.getMeeting(id);
|
|
5847
|
+
console.log(renderMeetingView(result.meeting, format).trimEnd());
|
|
5848
|
+
return 0;
|
|
5849
|
+
}
|
|
5850
|
+
async function exportMeeting(id, commandFlags, globalFlags) {
|
|
5851
|
+
const format = resolveExportFormat(commandFlags.format);
|
|
5852
|
+
const config = await loadConfig({
|
|
5853
|
+
globalFlags,
|
|
5854
|
+
subcommandFlags: commandFlags
|
|
5855
|
+
});
|
|
5856
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5857
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5858
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5859
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5860
|
+
const app = await createGranolaApp(config);
|
|
5861
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5862
|
+
console.log("Fetching meeting from Granola API...");
|
|
5863
|
+
const result = await app.getMeeting(id);
|
|
5864
|
+
console.log(renderMeetingExport(result.meeting, format).trimEnd());
|
|
5865
|
+
return 0;
|
|
5866
|
+
}
|
|
5867
|
+
async function notes(id, commandFlags, globalFlags) {
|
|
5868
|
+
const format = resolveNotesFormat(commandFlags.format);
|
|
5869
|
+
const config = await loadConfig({
|
|
5870
|
+
globalFlags,
|
|
5871
|
+
subcommandFlags: commandFlags
|
|
5872
|
+
});
|
|
5873
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5874
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5875
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5876
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5877
|
+
const app = await createGranolaApp(config);
|
|
5878
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5879
|
+
console.log("Fetching meeting from Granola API...");
|
|
5880
|
+
const result = await app.getMeeting(id);
|
|
5881
|
+
console.log(renderMeetingNotes(result.document, format).trimEnd());
|
|
5882
|
+
return 0;
|
|
5883
|
+
}
|
|
5884
|
+
async function transcript(id, commandFlags, globalFlags) {
|
|
5885
|
+
const format = resolveTranscriptFormat$1(commandFlags.format);
|
|
5886
|
+
const config = await loadConfig({
|
|
5887
|
+
globalFlags,
|
|
5888
|
+
subcommandFlags: commandFlags
|
|
5889
|
+
});
|
|
5890
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5891
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5892
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5893
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5894
|
+
const app = await createGranolaApp(config);
|
|
5895
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5896
|
+
console.log("Fetching meeting from Granola API...");
|
|
5897
|
+
const result = await app.getMeeting(id, { requireCache: true });
|
|
5898
|
+
const output = renderMeetingTranscript(result.document, result.cacheData, format);
|
|
5899
|
+
if (!output.trim()) throw new Error(`no transcript found for meeting: ${result.document.id}`);
|
|
5900
|
+
console.log(output.trimEnd());
|
|
5901
|
+
return 0;
|
|
5902
|
+
}
|
|
5903
|
+
async function openMeeting(id, commandFlags, globalFlags) {
|
|
5904
|
+
const config = await loadConfig({
|
|
5905
|
+
globalFlags,
|
|
5906
|
+
subcommandFlags: commandFlags
|
|
5907
|
+
});
|
|
5908
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5909
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5910
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5911
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5912
|
+
const app = await createGranolaApp(config, { surface: "web" });
|
|
5913
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5914
|
+
console.log("Resolving meeting from Granola API...");
|
|
5915
|
+
const result = await app.getMeeting(id);
|
|
5916
|
+
console.log(`Preparing web workspace for ${result.document.title || result.document.id}...`);
|
|
5917
|
+
return await runGranolaWebWorkspace(app, {
|
|
5918
|
+
...resolveGranolaWebWorkspaceOptions(commandFlags),
|
|
5919
|
+
targetMeetingId: result.document.id
|
|
5920
|
+
});
|
|
5921
|
+
}
|
|
5922
|
+
//#endregion
|
|
5923
|
+
//#region src/commands/notes.ts
|
|
5924
|
+
function notesHelp() {
|
|
5925
|
+
return `Granola notes
|
|
5926
|
+
|
|
5927
|
+
Usage:
|
|
5928
|
+
granola notes [options]
|
|
5929
|
+
|
|
5930
|
+
Options:
|
|
5931
|
+
--format <value> Output format: markdown, json, yaml, raw (default: markdown)
|
|
5932
|
+
--output <path> Output directory for note files (default: ./notes)
|
|
5933
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5934
|
+
--supabase <path> Path to supabase.json
|
|
5935
|
+
--debug Enable debug logging
|
|
5936
|
+
--config <path> Path to .granola.toml
|
|
5937
|
+
-h, --help Show help
|
|
5938
|
+
`;
|
|
5939
|
+
}
|
|
5940
|
+
const notesCommand = {
|
|
5941
|
+
description: "Export Granola notes",
|
|
5942
|
+
flags: {
|
|
5943
|
+
format: { type: "string" },
|
|
5944
|
+
help: { type: "boolean" },
|
|
5945
|
+
output: { type: "string" },
|
|
5946
|
+
timeout: { type: "string" }
|
|
5947
|
+
},
|
|
5948
|
+
help: notesHelp,
|
|
5949
|
+
name: "notes",
|
|
5950
|
+
async run({ commandFlags, globalFlags }) {
|
|
5951
|
+
const config = await loadConfig({
|
|
5952
|
+
globalFlags,
|
|
5953
|
+
subcommandFlags: commandFlags
|
|
5954
|
+
});
|
|
5955
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5956
|
+
debug(config.debug, "supabase", config.supabase);
|
|
5957
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5958
|
+
debug(config.debug, "output", config.notes.output);
|
|
5959
|
+
const format = resolveNoteFormat(commandFlags.format);
|
|
5960
|
+
debug(config.debug, "format", format);
|
|
5961
|
+
const app = await createGranolaApp(config);
|
|
5962
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5963
|
+
const result = await app.exportNotes(format);
|
|
5964
|
+
console.log(`✓ Exported ${result.documentCount} notes to ${result.outputDir} (job ${result.job.id})`);
|
|
5965
|
+
debug(config.debug, "notes written", result.written);
|
|
5966
|
+
return 0;
|
|
5967
|
+
}
|
|
5968
|
+
};
|
|
5969
|
+
function resolveNoteFormat(value) {
|
|
5970
|
+
switch (value) {
|
|
5971
|
+
case void 0: return "markdown";
|
|
5972
|
+
case "json":
|
|
5973
|
+
case "markdown":
|
|
5974
|
+
case "raw":
|
|
5975
|
+
case "yaml": return value;
|
|
5976
|
+
default: throw new Error("invalid notes format: expected markdown, json, yaml, or raw");
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
//#endregion
|
|
5980
|
+
//#region src/commands/serve.ts
|
|
5981
|
+
function serveHelp() {
|
|
5982
|
+
return `Granola serve
|
|
5983
|
+
|
|
5984
|
+
Usage:
|
|
5985
|
+
granola serve [options]
|
|
5986
|
+
|
|
5987
|
+
Options:
|
|
5573
5988
|
--network <mode> Network mode: local or lan (default: local)
|
|
5574
5989
|
--hostname <value> Hostname to bind (overrides network default)
|
|
5575
5990
|
--port <value> Port to bind (default: 0 for any available port)
|
|
@@ -5744,37 +6159,6 @@ function resolveTranscriptFormat(value) {
|
|
|
5744
6159
|
}
|
|
5745
6160
|
}
|
|
5746
6161
|
//#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
6162
|
//#region src/commands/web.ts
|
|
5779
6163
|
function webHelp() {
|
|
5780
6164
|
return `Granola web
|
|
@@ -5783,6 +6167,7 @@ Usage:
|
|
|
5783
6167
|
granola web [options]
|
|
5784
6168
|
|
|
5785
6169
|
Options:
|
|
6170
|
+
--meeting <id> Open a specific meeting on load
|
|
5786
6171
|
--network <mode> Network mode: local or lan (default: local)
|
|
5787
6172
|
--hostname <value> Hostname to bind (overrides network default)
|
|
5788
6173
|
--port <value> Port to bind (default: 0 for any available port)
|
|
@@ -5814,6 +6199,7 @@ const commands = [
|
|
|
5814
6199
|
cache: { type: "string" },
|
|
5815
6200
|
help: { type: "boolean" },
|
|
5816
6201
|
hostname: { type: "string" },
|
|
6202
|
+
meeting: { type: "string" },
|
|
5817
6203
|
network: { type: "string" },
|
|
5818
6204
|
open: { type: "boolean" },
|
|
5819
6205
|
password: { type: "string" },
|
|
@@ -5833,55 +6219,12 @@ const commands = [
|
|
|
5833
6219
|
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
5834
6220
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
5835
6221
|
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
|
-
}
|
|
6222
|
+
const options = resolveGranolaWebWorkspaceOptions(commandFlags);
|
|
6223
|
+
const targetMeetingId = typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0;
|
|
6224
|
+
return await runGranolaWebWorkspace(app, {
|
|
6225
|
+
...options,
|
|
6226
|
+
targetMeetingId
|
|
5850
6227
|
});
|
|
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
6228
|
}
|
|
5886
6229
|
}
|
|
5887
6230
|
];
|