granola-toolkit 0.28.0 → 0.30.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 +13 -0
- package/dist/cli.js +374 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,6 +208,7 @@ The machine-readable `export` command includes:
|
|
|
208
208
|
The initial server API includes:
|
|
209
209
|
|
|
210
210
|
- `GET /health`
|
|
211
|
+
- `GET /server/info`
|
|
211
212
|
- `POST /auth/unlock` for password-protected servers
|
|
212
213
|
- `POST /auth/lock` to clear the browser/API unlock cookie
|
|
213
214
|
- `GET /auth/status`
|
|
@@ -272,6 +273,16 @@ Use it when you want:
|
|
|
272
273
|
|
|
273
274
|
The attach flow uses the existing local HTTP API plus `GET /events` for live state updates.
|
|
274
275
|
|
|
276
|
+
### Runtime Boundaries
|
|
277
|
+
|
|
278
|
+
The toolkit now keeps its local persistence and transport contracts explicit:
|
|
279
|
+
|
|
280
|
+
- one shared local data directory for export jobs, meeting index data, and any file-backed session state
|
|
281
|
+
- one versioned local HTTP transport contract, exposed by `GET /server/info`
|
|
282
|
+
- one remote client handshake that validates the transport protocol before attaching
|
|
283
|
+
|
|
284
|
+
That keeps the current single-package repo simple, while making a future split into separate server/client packages or remote-hosted clients much less invasive.
|
|
285
|
+
|
|
275
286
|
### TUI
|
|
276
287
|
|
|
277
288
|
`tui` starts a full-screen terminal workspace on the shared app core, without requiring the local server or browser client. Use `attach` when you want the same workspace against an existing shared server instance instead.
|
|
@@ -280,6 +291,7 @@ The initial terminal workspace includes:
|
|
|
280
291
|
|
|
281
292
|
- a meeting list pane with keyboard navigation
|
|
282
293
|
- a detail pane with notes, transcript, metadata, and raw views
|
|
294
|
+
- an auth session overlay for import, refresh, source switching, and sign-out
|
|
283
295
|
- a footer with app state and key hints
|
|
284
296
|
- a quick-open overlay for jumping by title, id, or tag
|
|
285
297
|
|
|
@@ -287,6 +299,7 @@ The main keyboard controls are:
|
|
|
287
299
|
|
|
288
300
|
- `j` / `k` or arrow keys to move between meetings
|
|
289
301
|
- `/` or `Ctrl+P` to open quick open
|
|
302
|
+
- `a` to open auth session actions
|
|
290
303
|
- `1`-`4` to switch detail tabs
|
|
291
304
|
- `PageUp` / `PageDown` to scroll the detail pane
|
|
292
305
|
- `r` to refresh from live Granola data
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,60 @@ import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
|
+
//#region src/transport.ts
|
|
13
|
+
const granolaTransportPaths = {
|
|
14
|
+
authLock: "/auth/lock",
|
|
15
|
+
authLogin: "/auth/login",
|
|
16
|
+
authLogout: "/auth/logout",
|
|
17
|
+
authMode: "/auth/mode",
|
|
18
|
+
authRefresh: "/auth/refresh",
|
|
19
|
+
authStatus: "/auth/status",
|
|
20
|
+
authUnlock: "/auth/unlock",
|
|
21
|
+
events: "/events",
|
|
22
|
+
exportJobs: "/exports/jobs",
|
|
23
|
+
exportNotes: "/exports/notes",
|
|
24
|
+
exportTranscripts: "/exports/transcripts",
|
|
25
|
+
health: "/health",
|
|
26
|
+
meetingResolve: "/meetings/resolve",
|
|
27
|
+
meetings: "/meetings",
|
|
28
|
+
root: "/",
|
|
29
|
+
serverInfo: "/server/info",
|
|
30
|
+
state: "/state"
|
|
31
|
+
};
|
|
32
|
+
function appendSearchParams(path, params) {
|
|
33
|
+
const url = new URL(path, "http://localhost");
|
|
34
|
+
for (const [key, value] of Object.entries(params)) {
|
|
35
|
+
if (value === void 0 || value === false || value === "") continue;
|
|
36
|
+
url.searchParams.set(key, String(value));
|
|
37
|
+
}
|
|
38
|
+
return `${url.pathname}${url.search}`;
|
|
39
|
+
}
|
|
40
|
+
function granolaMeetingPath(id) {
|
|
41
|
+
return `${granolaTransportPaths.meetings}/${encodeURIComponent(id)}`;
|
|
42
|
+
}
|
|
43
|
+
function granolaMeetingResolvePath(query, options = {}) {
|
|
44
|
+
return appendSearchParams(granolaTransportPaths.meetingResolve, {
|
|
45
|
+
includeTranscript: options.includeTranscript ? "true" : void 0,
|
|
46
|
+
q: query
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function granolaMeetingsPath(options = {}) {
|
|
50
|
+
return appendSearchParams(granolaTransportPaths.meetings, {
|
|
51
|
+
limit: options.limit,
|
|
52
|
+
refresh: options.forceRefresh ? "true" : void 0,
|
|
53
|
+
search: options.search,
|
|
54
|
+
sort: options.sort,
|
|
55
|
+
updatedFrom: options.updatedFrom,
|
|
56
|
+
updatedTo: options.updatedTo
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function granolaExportJobsPath(options = {}) {
|
|
60
|
+
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
61
|
+
}
|
|
62
|
+
function granolaExportJobRerunPath(id) {
|
|
63
|
+
return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
12
66
|
//#region src/server/client.ts
|
|
13
67
|
function cloneValue(value) {
|
|
14
68
|
return structuredClone(value);
|
|
@@ -24,14 +78,6 @@ function normaliseServerUrl(serverUrl) {
|
|
|
24
78
|
parsed.hash = "";
|
|
25
79
|
return parsed;
|
|
26
80
|
}
|
|
27
|
-
function appendSearchParams(path, params) {
|
|
28
|
-
const url = new URL(path, "http://localhost");
|
|
29
|
-
for (const [key, value] of Object.entries(params)) {
|
|
30
|
-
if (value === void 0 || value === false || value === "") continue;
|
|
31
|
-
url.searchParams.set(key, String(value));
|
|
32
|
-
}
|
|
33
|
-
return `${url.pathname}${url.search}`;
|
|
34
|
-
}
|
|
35
81
|
function mergeHeaders(...values) {
|
|
36
82
|
const headers = new Headers();
|
|
37
83
|
for (const value of values) {
|
|
@@ -66,22 +112,32 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
66
112
|
#fetchImpl;
|
|
67
113
|
#password;
|
|
68
114
|
#reconnectDelayMs;
|
|
115
|
+
info;
|
|
69
116
|
#streamAbortController;
|
|
70
|
-
constructor(url, initialState, options = {}) {
|
|
117
|
+
constructor(info, url, initialState, options = {}) {
|
|
71
118
|
this.url = url;
|
|
72
119
|
this.#fetchImpl = options.fetchImpl ?? fetch;
|
|
120
|
+
this.info = cloneValue(info);
|
|
73
121
|
this.#password = options.password?.trim() || void 0;
|
|
74
122
|
this.#reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
|
|
75
123
|
this.#state = cloneValue(initialState);
|
|
76
124
|
}
|
|
77
125
|
static async connect(serverUrl, options = {}) {
|
|
78
126
|
const url = normaliseServerUrl(serverUrl);
|
|
79
|
-
const
|
|
127
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
128
|
+
const infoResponse = await fetchImpl(new URL(granolaTransportPaths.serverInfo, url), { headers: mergeHeaders({
|
|
129
|
+
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
130
|
+
accept: "application/json"
|
|
131
|
+
}) });
|
|
132
|
+
if (!infoResponse.ok) throw await responseError(infoResponse);
|
|
133
|
+
const info = await infoResponse.json();
|
|
134
|
+
if (info.protocolVersion !== 1) throw new Error(`unsupported Granola transport protocol: expected 1, got ${info.protocolVersion}`);
|
|
135
|
+
const response = await fetchImpl(new URL(granolaTransportPaths.state, url), { headers: mergeHeaders({
|
|
80
136
|
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
81
137
|
accept: "application/json"
|
|
82
138
|
}) });
|
|
83
139
|
if (!response.ok) throw await responseError(response);
|
|
84
|
-
const client = new GranolaServerClient(url, await response.json(), options);
|
|
140
|
+
const client = new GranolaServerClient(info, url, await response.json(), options);
|
|
85
141
|
client.startEvents();
|
|
86
142
|
return client;
|
|
87
143
|
}
|
|
@@ -103,66 +159,56 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
103
159
|
};
|
|
104
160
|
}
|
|
105
161
|
async inspectAuth() {
|
|
106
|
-
return await this.requestJson(
|
|
162
|
+
return await this.requestJson(granolaTransportPaths.authStatus);
|
|
107
163
|
}
|
|
108
164
|
async loginAuth(options = {}) {
|
|
109
|
-
return await this.requestJson(
|
|
165
|
+
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
110
166
|
body: JSON.stringify(options),
|
|
111
167
|
headers: { "content-type": "application/json" },
|
|
112
168
|
method: "POST"
|
|
113
169
|
});
|
|
114
170
|
}
|
|
115
171
|
async logoutAuth() {
|
|
116
|
-
return await this.requestJson(
|
|
172
|
+
return await this.requestJson(granolaTransportPaths.authLogout, { method: "POST" });
|
|
117
173
|
}
|
|
118
174
|
async refreshAuth() {
|
|
119
|
-
return await this.requestJson(
|
|
175
|
+
return await this.requestJson(granolaTransportPaths.authRefresh, { method: "POST" });
|
|
120
176
|
}
|
|
121
177
|
async switchAuthMode(mode) {
|
|
122
|
-
return await this.requestJson(
|
|
178
|
+
return await this.requestJson(granolaTransportPaths.authMode, {
|
|
123
179
|
body: JSON.stringify({ mode }),
|
|
124
180
|
headers: { "content-type": "application/json" },
|
|
125
181
|
method: "POST"
|
|
126
182
|
});
|
|
127
183
|
}
|
|
128
184
|
async listMeetings(options = {}) {
|
|
129
|
-
return await this.requestJson(
|
|
130
|
-
limit: options.limit,
|
|
131
|
-
refresh: options.forceRefresh ? "true" : void 0,
|
|
132
|
-
search: options.search,
|
|
133
|
-
sort: options.sort,
|
|
134
|
-
updatedFrom: options.updatedFrom,
|
|
135
|
-
updatedTo: options.updatedTo
|
|
136
|
-
}));
|
|
185
|
+
return await this.requestJson(granolaMeetingsPath(options));
|
|
137
186
|
}
|
|
138
187
|
async getMeeting(id, options = {}) {
|
|
139
|
-
return await this.requestJson(
|
|
188
|
+
return await this.requestJson(`${granolaMeetingPath(id)}${options.requireCache ? "?includeTranscript=true" : ""}`);
|
|
140
189
|
}
|
|
141
190
|
async findMeeting(query, options = {}) {
|
|
142
|
-
return await this.requestJson(
|
|
143
|
-
includeTranscript: options.requireCache ? "true" : void 0,
|
|
144
|
-
q: query
|
|
145
|
-
}));
|
|
191
|
+
return await this.requestJson(granolaMeetingResolvePath(query, { includeTranscript: options.requireCache }));
|
|
146
192
|
}
|
|
147
193
|
async listExportJobs(options = {}) {
|
|
148
|
-
return await this.requestJson(
|
|
194
|
+
return await this.requestJson(granolaExportJobsPath(options));
|
|
149
195
|
}
|
|
150
196
|
async exportNotes(format = "markdown") {
|
|
151
|
-
return await this.requestJson(
|
|
197
|
+
return await this.requestJson(granolaTransportPaths.exportNotes, {
|
|
152
198
|
body: JSON.stringify({ format }),
|
|
153
199
|
headers: { "content-type": "application/json" },
|
|
154
200
|
method: "POST"
|
|
155
201
|
});
|
|
156
202
|
}
|
|
157
203
|
async exportTranscripts(format = "text") {
|
|
158
|
-
return await this.requestJson(
|
|
204
|
+
return await this.requestJson(granolaTransportPaths.exportTranscripts, {
|
|
159
205
|
body: JSON.stringify({ format }),
|
|
160
206
|
headers: { "content-type": "application/json" },
|
|
161
207
|
method: "POST"
|
|
162
208
|
});
|
|
163
209
|
}
|
|
164
210
|
async rerunExportJob(id) {
|
|
165
|
-
return await this.requestJson(
|
|
211
|
+
return await this.requestJson(granolaExportJobRerunPath(id), { method: "POST" });
|
|
166
212
|
}
|
|
167
213
|
async request(path, init = {}) {
|
|
168
214
|
const response = await this.#fetchImpl(new URL(path, this.url), {
|
|
@@ -192,7 +238,7 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
192
238
|
const controller = new AbortController();
|
|
193
239
|
this.#streamAbortController = controller;
|
|
194
240
|
try {
|
|
195
|
-
const response = await this.request(
|
|
241
|
+
const response = await this.request(granolaTransportPaths.events, {
|
|
196
242
|
headers: { accept: "text/event-stream" },
|
|
197
243
|
signal: controller.signal
|
|
198
244
|
});
|
|
@@ -1305,6 +1351,151 @@ const granolaTuiTheme = {
|
|
|
1305
1351
|
}
|
|
1306
1352
|
};
|
|
1307
1353
|
//#endregion
|
|
1354
|
+
//#region src/tui/auth.ts
|
|
1355
|
+
function padLine$2(text, width) {
|
|
1356
|
+
const clipped = truncateToWidth(text, width, "");
|
|
1357
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
1358
|
+
}
|
|
1359
|
+
function frameLine$1(text, width) {
|
|
1360
|
+
return `| ${padLine$2(text, Math.max(1, width - 4))} |`;
|
|
1361
|
+
}
|
|
1362
|
+
function actionDisabledReason(auth, actionId) {
|
|
1363
|
+
switch (actionId) {
|
|
1364
|
+
case "login": return auth.supabaseAvailable ? "" : "supabase.json unavailable";
|
|
1365
|
+
case "refresh":
|
|
1366
|
+
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1367
|
+
return auth.refreshAvailable ? "" : "refresh unavailable";
|
|
1368
|
+
case "use-stored":
|
|
1369
|
+
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1370
|
+
return auth.mode === "stored-session" ? "already active" : "";
|
|
1371
|
+
case "use-supabase":
|
|
1372
|
+
if (!auth.supabaseAvailable) return "supabase.json unavailable";
|
|
1373
|
+
return auth.mode === "supabase-file" ? "already active" : "";
|
|
1374
|
+
case "logout": return auth.storedSessionAvailable ? "" : "stored session missing";
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function buildGranolaTuiAuthActions(auth) {
|
|
1378
|
+
return [
|
|
1379
|
+
{
|
|
1380
|
+
description: "Import the Granola desktop session from supabase.json",
|
|
1381
|
+
id: "login",
|
|
1382
|
+
key: "1",
|
|
1383
|
+
label: "Import desktop session"
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
description: "Refresh the stored Granola session",
|
|
1387
|
+
id: "refresh",
|
|
1388
|
+
key: "2",
|
|
1389
|
+
label: "Refresh stored session"
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
description: "Switch the active auth source to the stored session",
|
|
1393
|
+
id: "use-stored",
|
|
1394
|
+
key: "3",
|
|
1395
|
+
label: "Use stored session"
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
description: "Switch the active auth source to supabase.json",
|
|
1399
|
+
id: "use-supabase",
|
|
1400
|
+
key: "4",
|
|
1401
|
+
label: "Use supabase.json"
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
description: "Delete the stored session and fall back to supabase.json",
|
|
1405
|
+
id: "logout",
|
|
1406
|
+
key: "5",
|
|
1407
|
+
label: "Sign out"
|
|
1408
|
+
}
|
|
1409
|
+
].map((action) => {
|
|
1410
|
+
const disabledReason = actionDisabledReason(auth, action.id);
|
|
1411
|
+
return {
|
|
1412
|
+
...action,
|
|
1413
|
+
disabled: disabledReason.length > 0,
|
|
1414
|
+
disabledReason: disabledReason || void 0
|
|
1415
|
+
};
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
function renderGranolaTuiAuthState(auth) {
|
|
1419
|
+
const lines = [
|
|
1420
|
+
`Active source: ${auth.mode === "stored-session" ? "Stored session" : "supabase.json"}`,
|
|
1421
|
+
`Stored session: ${auth.storedSessionAvailable ? "available" : "missing"}`,
|
|
1422
|
+
`supabase.json: ${auth.supabaseAvailable ? "available" : "missing"}`,
|
|
1423
|
+
`Refresh: ${auth.refreshAvailable ? "available" : "missing"}`
|
|
1424
|
+
];
|
|
1425
|
+
if (auth.clientId) lines.push(`Client ID: ${auth.clientId}`);
|
|
1426
|
+
if (auth.signInMethod) lines.push(`Sign-in method: ${auth.signInMethod}`);
|
|
1427
|
+
if (auth.supabasePath) lines.push(`supabase path: ${auth.supabasePath}`);
|
|
1428
|
+
if (auth.lastError) lines.push(`Last error: ${auth.lastError}`);
|
|
1429
|
+
return lines.join("\n");
|
|
1430
|
+
}
|
|
1431
|
+
function nextEnabledIndex(actions, startIndex, delta) {
|
|
1432
|
+
if (actions.length === 0) return -1;
|
|
1433
|
+
for (let attempts = 0; attempts < actions.length; attempts += 1) {
|
|
1434
|
+
const nextIndex = (startIndex + delta * (attempts + 1) + actions.length) % actions.length;
|
|
1435
|
+
if (!actions[nextIndex]?.disabled) return nextIndex;
|
|
1436
|
+
}
|
|
1437
|
+
return Math.max(0, Math.min(actions.length - 1, startIndex));
|
|
1438
|
+
}
|
|
1439
|
+
var GranolaTuiAuthOverlay = class {
|
|
1440
|
+
focused = false;
|
|
1441
|
+
#actions;
|
|
1442
|
+
#selectedIndex;
|
|
1443
|
+
constructor(options) {
|
|
1444
|
+
this.options = options;
|
|
1445
|
+
this.#actions = buildGranolaTuiAuthActions(this.options.auth);
|
|
1446
|
+
const firstEnabledIndex = this.#actions.findIndex((action) => !action.disabled);
|
|
1447
|
+
this.#selectedIndex = firstEnabledIndex >= 0 ? firstEnabledIndex : 0;
|
|
1448
|
+
}
|
|
1449
|
+
invalidate() {}
|
|
1450
|
+
async runAction(actionId) {
|
|
1451
|
+
const action = this.#actions.find((candidate) => candidate.id === actionId);
|
|
1452
|
+
if (!action || action.disabled) return;
|
|
1453
|
+
await this.options.onRun(action.id);
|
|
1454
|
+
}
|
|
1455
|
+
handleInput(data) {
|
|
1456
|
+
if (matchesKey(data, "esc")) {
|
|
1457
|
+
this.options.onCancel();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (matchesKey(data, "up")) {
|
|
1461
|
+
this.#selectedIndex = nextEnabledIndex(this.#actions, this.#selectedIndex, -1);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (matchesKey(data, "down")) {
|
|
1465
|
+
this.#selectedIndex = nextEnabledIndex(this.#actions, this.#selectedIndex, 1);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const selected = this.#actions[this.#selectedIndex];
|
|
1469
|
+
if (matchesKey(data, "enter")) {
|
|
1470
|
+
if (selected && !selected.disabled) this.runAction(selected.id);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const hotkeyAction = this.#actions.find((action) => action.key === data);
|
|
1474
|
+
if (hotkeyAction && !hotkeyAction.disabled) this.runAction(hotkeyAction.id);
|
|
1475
|
+
}
|
|
1476
|
+
render(width) {
|
|
1477
|
+
const lines = [];
|
|
1478
|
+
const bodyWidth = Math.max(48, width);
|
|
1479
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1480
|
+
lines.push(frameLine$1(granolaTuiTheme.strong("Auth Session"), bodyWidth));
|
|
1481
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1482
|
+
for (const detailLine of renderGranolaTuiAuthState(this.options.auth).split("\n")) lines.push(frameLine$1(detailLine, bodyWidth));
|
|
1483
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1484
|
+
for (const action of this.#actions) {
|
|
1485
|
+
const selected = this.#actions[this.#selectedIndex]?.id === action.id;
|
|
1486
|
+
const label = `${action.key}. ${action.label}`;
|
|
1487
|
+
const titleLine = action.disabled ? granolaTuiTheme.dim(label) : selected ? granolaTuiTheme.selected(label) : label;
|
|
1488
|
+
lines.push(frameLine$1(titleLine, bodyWidth));
|
|
1489
|
+
const detail = action.disabled ? granolaTuiTheme.warning(action.disabledReason ?? "Unavailable") : granolaTuiTheme.dim(action.description);
|
|
1490
|
+
for (const wrapped of wrapTextWithAnsi(detail, Math.max(1, bodyWidth - 6))) lines.push(frameLine$1(` ${wrapped}`, bodyWidth));
|
|
1491
|
+
}
|
|
1492
|
+
lines.push(frameLine$1("", bodyWidth));
|
|
1493
|
+
lines.push(frameLine$1(granolaTuiTheme.dim("Enter to run, Esc to cancel, arrows to move"), bodyWidth));
|
|
1494
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1495
|
+
return lines;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
//#endregion
|
|
1308
1499
|
//#region src/tui/palette.ts
|
|
1309
1500
|
function padLine$1(text, width) {
|
|
1310
1501
|
const clipped = truncateToWidth(text, width, "");
|
|
@@ -1597,6 +1788,85 @@ var GranolaTuiWorkspace = class {
|
|
|
1597
1788
|
this.#detailScroll = 0;
|
|
1598
1789
|
this.tui.requestRender();
|
|
1599
1790
|
}
|
|
1791
|
+
async reloadAfterAuthChange() {
|
|
1792
|
+
const preferredMeetingId = this.#selectedMeeting?.document.id ?? this.#selectedMeetingId;
|
|
1793
|
+
try {
|
|
1794
|
+
await this.loadMeetings({
|
|
1795
|
+
forceRefresh: true,
|
|
1796
|
+
preferredMeetingId,
|
|
1797
|
+
setStatus: false
|
|
1798
|
+
});
|
|
1799
|
+
if (this.#selectedMeetingId) {
|
|
1800
|
+
await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
this.#selectedMeeting = void 0;
|
|
1804
|
+
this.#detailError = "";
|
|
1805
|
+
this.#detailScroll = 0;
|
|
1806
|
+
this.tui.requestRender();
|
|
1807
|
+
} catch {}
|
|
1808
|
+
}
|
|
1809
|
+
async runAuthAction(actionId) {
|
|
1810
|
+
let successMessage = "";
|
|
1811
|
+
try {
|
|
1812
|
+
switch (actionId) {
|
|
1813
|
+
case "login":
|
|
1814
|
+
this.setStatus("Importing desktop session…");
|
|
1815
|
+
await this.app.loginAuth();
|
|
1816
|
+
successMessage = "Stored session imported";
|
|
1817
|
+
break;
|
|
1818
|
+
case "refresh":
|
|
1819
|
+
this.setStatus("Refreshing stored session…");
|
|
1820
|
+
await this.app.refreshAuth();
|
|
1821
|
+
successMessage = "Stored session refreshed";
|
|
1822
|
+
break;
|
|
1823
|
+
case "use-stored":
|
|
1824
|
+
this.setStatus("Switching to stored session…");
|
|
1825
|
+
await this.app.switchAuthMode("stored-session");
|
|
1826
|
+
successMessage = "Using stored session";
|
|
1827
|
+
break;
|
|
1828
|
+
case "use-supabase":
|
|
1829
|
+
this.setStatus("Switching to supabase.json…");
|
|
1830
|
+
await this.app.switchAuthMode("supabase-file");
|
|
1831
|
+
successMessage = "Using supabase.json";
|
|
1832
|
+
break;
|
|
1833
|
+
case "logout":
|
|
1834
|
+
this.setStatus("Signing out…");
|
|
1835
|
+
await this.app.logoutAuth();
|
|
1836
|
+
successMessage = "Stored session removed";
|
|
1837
|
+
break;
|
|
1838
|
+
}
|
|
1839
|
+
await this.reloadAfterAuthChange();
|
|
1840
|
+
this.setStatus(successMessage);
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1843
|
+
this.setStatus(message, "error");
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
openAuthPanel(auth = this.#appState.auth) {
|
|
1847
|
+
if (this.#overlay) return;
|
|
1848
|
+
const closeOverlay = () => {
|
|
1849
|
+
this.#overlay?.hide();
|
|
1850
|
+
this.#overlay = void 0;
|
|
1851
|
+
this.tui.setFocus(this);
|
|
1852
|
+
this.tui.requestRender();
|
|
1853
|
+
};
|
|
1854
|
+
const overlay = new GranolaTuiAuthOverlay({
|
|
1855
|
+
auth,
|
|
1856
|
+
onCancel: closeOverlay,
|
|
1857
|
+
onRun: async (actionId) => {
|
|
1858
|
+
closeOverlay();
|
|
1859
|
+
await this.runAuthAction(actionId);
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
this.#overlay = this.tui.showOverlay(overlay, {
|
|
1863
|
+
anchor: "center",
|
|
1864
|
+
maxHeight: "70%",
|
|
1865
|
+
minWidth: 52,
|
|
1866
|
+
width: "72%"
|
|
1867
|
+
});
|
|
1868
|
+
this.setStatus("Auth session");
|
|
1869
|
+
}
|
|
1600
1870
|
openQuickOpen() {
|
|
1601
1871
|
if (this.#overlay) return;
|
|
1602
1872
|
const closeOverlay = () => {
|
|
@@ -1641,6 +1911,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1641
1911
|
this.openQuickOpen();
|
|
1642
1912
|
return;
|
|
1643
1913
|
}
|
|
1914
|
+
if (matchesKey(data, "a")) {
|
|
1915
|
+
this.openAuthPanel();
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1644
1918
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
1645
1919
|
this.moveSelection(-1);
|
|
1646
1920
|
return;
|
|
@@ -1779,7 +2053,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1779
2053
|
const bodyLines = [];
|
|
1780
2054
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
1781
2055
|
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);
|
|
2056
|
+
const footerHints = padLine(granolaTuiTheme.dim("/ quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
1783
2057
|
return [
|
|
1784
2058
|
headerTitle,
|
|
1785
2059
|
headerSummary,
|
|
@@ -1903,6 +2177,22 @@ function parseCacheContents(contents) {
|
|
|
1903
2177
|
};
|
|
1904
2178
|
}
|
|
1905
2179
|
//#endregion
|
|
2180
|
+
//#region src/persistence/layout.ts
|
|
2181
|
+
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2182
|
+
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
2183
|
+
}
|
|
2184
|
+
function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
2185
|
+
const targetPlatform = options.platform ?? platform();
|
|
2186
|
+
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2187
|
+
return {
|
|
2188
|
+
dataDirectory,
|
|
2189
|
+
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2190
|
+
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2191
|
+
sessionFile: join(dataDirectory, "session.json"),
|
|
2192
|
+
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file"
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
//#endregion
|
|
1906
2196
|
//#region src/client/auth.ts
|
|
1907
2197
|
const execFileAsync$1 = promisify(execFile);
|
|
1908
2198
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
@@ -2123,11 +2413,10 @@ async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
|
2123
2413
|
};
|
|
2124
2414
|
}
|
|
2125
2415
|
function defaultSessionFilePath() {
|
|
2126
|
-
|
|
2127
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "session.json") : join(home, ".config", "granola-toolkit", "session.json");
|
|
2416
|
+
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
2128
2417
|
}
|
|
2129
2418
|
function createDefaultSessionStore() {
|
|
2130
|
-
return
|
|
2419
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
2131
2420
|
}
|
|
2132
2421
|
//#endregion
|
|
2133
2422
|
//#region src/client/default-auth.ts
|
|
@@ -2508,8 +2797,7 @@ function createExportJobId(kind) {
|
|
|
2508
2797
|
return `${kind}-${randomUUID()}`;
|
|
2509
2798
|
}
|
|
2510
2799
|
function defaultExportJobsFilePath() {
|
|
2511
|
-
|
|
2512
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "export-jobs.json") : join(home, ".config", "granola-toolkit", "export-jobs.json");
|
|
2800
|
+
return defaultGranolaToolkitPersistenceLayout().exportJobsFile;
|
|
2513
2801
|
}
|
|
2514
2802
|
var FileExportJobStore = class {
|
|
2515
2803
|
constructor(filePath = defaultExportJobsFilePath()) {
|
|
@@ -2573,8 +2861,7 @@ var FileMeetingIndexStore = class {
|
|
|
2573
2861
|
}
|
|
2574
2862
|
};
|
|
2575
2863
|
function defaultMeetingIndexFilePath() {
|
|
2576
|
-
|
|
2577
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "meeting-index.json") : join(home, ".config", "granola-toolkit", "meeting-index.json");
|
|
2864
|
+
return defaultGranolaToolkitPersistenceLayout().meetingIndexFile;
|
|
2578
2865
|
}
|
|
2579
2866
|
function createDefaultMeetingIndexStore() {
|
|
2580
2867
|
return new FileMeetingIndexStore();
|
|
@@ -5127,7 +5414,7 @@ function isPasswordAuthenticated(request, password) {
|
|
|
5127
5414
|
return parseCookies(request)[PASSWORD_COOKIE_NAME] === password;
|
|
5128
5415
|
}
|
|
5129
5416
|
function publicRoute(path, enableWebClient) {
|
|
5130
|
-
return path ===
|
|
5417
|
+
return path === granolaTransportPaths.health || path === granolaTransportPaths.serverInfo || path === granolaTransportPaths.authUnlock || enableWebClient && path === granolaTransportPaths.root;
|
|
5131
5418
|
}
|
|
5132
5419
|
async function startGranolaServer(app, options = {}) {
|
|
5133
5420
|
const enableWebClient = options.enableWebClient ?? false;
|
|
@@ -5137,6 +5424,24 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5137
5424
|
password: options.security?.password?.trim() || void 0,
|
|
5138
5425
|
trustedOrigins: (options.security?.trustedOrigins ?? []).map((origin) => origin.trim()).filter(Boolean)
|
|
5139
5426
|
};
|
|
5427
|
+
const serverInfo = {
|
|
5428
|
+
capabilities: {
|
|
5429
|
+
attach: true,
|
|
5430
|
+
auth: true,
|
|
5431
|
+
events: true,
|
|
5432
|
+
exports: true,
|
|
5433
|
+
meetingOpen: true,
|
|
5434
|
+
webClient: enableWebClient
|
|
5435
|
+
},
|
|
5436
|
+
persistence: {
|
|
5437
|
+
exportJobs: true,
|
|
5438
|
+
meetingIndex: true,
|
|
5439
|
+
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
5440
|
+
},
|
|
5441
|
+
product: "granola-toolkit",
|
|
5442
|
+
protocolVersion: 1,
|
|
5443
|
+
transport: "local-http"
|
|
5444
|
+
};
|
|
5140
5445
|
const server = createServer(async (request, response) => {
|
|
5141
5446
|
const method = request.method ?? "GET";
|
|
5142
5447
|
const url = new URL(request.url ?? "/", `http://${hostname}`);
|
|
@@ -5164,11 +5469,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5164
5469
|
sendNoContent(response, 204, originHeaders);
|
|
5165
5470
|
return;
|
|
5166
5471
|
}
|
|
5167
|
-
if (method === "GET" && path ===
|
|
5472
|
+
if (method === "GET" && path === granolaTransportPaths.root && enableWebClient) {
|
|
5168
5473
|
sendHtml(response, renderGranolaWebPage({ serverPasswordRequired: Boolean(security.password) }), 200, originHeaders);
|
|
5169
5474
|
return;
|
|
5170
5475
|
}
|
|
5171
|
-
if (method === "GET" && path ===
|
|
5476
|
+
if (method === "GET" && path === granolaTransportPaths.health) {
|
|
5172
5477
|
sendJson(response, {
|
|
5173
5478
|
ok: true,
|
|
5174
5479
|
service: "granola-toolkit",
|
|
@@ -5176,7 +5481,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5176
5481
|
}, { headers: originHeaders });
|
|
5177
5482
|
return;
|
|
5178
5483
|
}
|
|
5179
|
-
if (method === "
|
|
5484
|
+
if (method === "GET" && path === granolaTransportPaths.serverInfo) {
|
|
5485
|
+
sendJson(response, serverInfo, { headers: originHeaders });
|
|
5486
|
+
return;
|
|
5487
|
+
}
|
|
5488
|
+
if (method === "POST" && path === granolaTransportPaths.authUnlock) {
|
|
5180
5489
|
if (!security.password) {
|
|
5181
5490
|
sendJson(response, {
|
|
5182
5491
|
ok: true,
|
|
@@ -5215,22 +5524,22 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5215
5524
|
});
|
|
5216
5525
|
return;
|
|
5217
5526
|
}
|
|
5218
|
-
if (method === "GET" && path ===
|
|
5527
|
+
if (method === "GET" && path === granolaTransportPaths.state) {
|
|
5219
5528
|
sendJson(response, app.getState(), { headers: originHeaders });
|
|
5220
5529
|
return;
|
|
5221
5530
|
}
|
|
5222
|
-
if (method === "GET" && path ===
|
|
5531
|
+
if (method === "GET" && path === granolaTransportPaths.authStatus) {
|
|
5223
5532
|
sendJson(response, await app.inspectAuth(), { headers: originHeaders });
|
|
5224
5533
|
return;
|
|
5225
5534
|
}
|
|
5226
|
-
if (method === "POST" && path ===
|
|
5535
|
+
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
5227
5536
|
sendJson(response, { ok: true }, { headers: {
|
|
5228
5537
|
...originHeaders,
|
|
5229
5538
|
"set-cookie": clearPasswordCookieHeader()
|
|
5230
5539
|
} });
|
|
5231
5540
|
return;
|
|
5232
5541
|
}
|
|
5233
|
-
if (method === "GET" && path ===
|
|
5542
|
+
if (method === "GET" && path === granolaTransportPaths.events) {
|
|
5234
5543
|
response.writeHead(200, {
|
|
5235
5544
|
"cache-control": "no-cache, no-transform",
|
|
5236
5545
|
connection: "keep-alive",
|
|
@@ -5251,7 +5560,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5251
5560
|
});
|
|
5252
5561
|
return;
|
|
5253
5562
|
}
|
|
5254
|
-
if (method === "GET" && path ===
|
|
5563
|
+
if (method === "GET" && path === granolaTransportPaths.meetings) {
|
|
5255
5564
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5256
5565
|
const refresh = url.searchParams.get("refresh") === "true";
|
|
5257
5566
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
@@ -5277,38 +5586,38 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5277
5586
|
}, { headers: originHeaders });
|
|
5278
5587
|
return;
|
|
5279
5588
|
}
|
|
5280
|
-
if (method === "GET" && path ===
|
|
5589
|
+
if (method === "GET" && path === granolaTransportPaths.meetingResolve) {
|
|
5281
5590
|
const query = url.searchParams.get("q")?.trim();
|
|
5282
5591
|
if (!query) throw new Error("meeting query is required");
|
|
5283
5592
|
sendJson(response, await app.findMeeting(query, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5284
5593
|
return;
|
|
5285
5594
|
}
|
|
5286
|
-
if (method === "GET" && path.startsWith(
|
|
5287
|
-
const id = decodeURIComponent(path.slice(
|
|
5595
|
+
if (method === "GET" && path.startsWith(`${granolaTransportPaths.meetings}/`) && path !== granolaTransportPaths.meetingResolve) {
|
|
5596
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.meetings}/`.length));
|
|
5288
5597
|
if (!id) throw new Error("meeting id is required");
|
|
5289
5598
|
sendJson(response, await app.getMeeting(id, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5290
5599
|
return;
|
|
5291
5600
|
}
|
|
5292
|
-
if (method === "POST" && path ===
|
|
5601
|
+
if (method === "POST" && path === granolaTransportPaths.authLogin) {
|
|
5293
5602
|
const body = await readJsonBody(request);
|
|
5294
5603
|
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
5295
5604
|
sendJson(response, await app.loginAuth({ supabasePath }), { headers: originHeaders });
|
|
5296
5605
|
return;
|
|
5297
5606
|
}
|
|
5298
|
-
if (method === "POST" && path ===
|
|
5607
|
+
if (method === "POST" && path === granolaTransportPaths.authLogout) {
|
|
5299
5608
|
sendJson(response, await app.logoutAuth(), { headers: originHeaders });
|
|
5300
5609
|
return;
|
|
5301
5610
|
}
|
|
5302
|
-
if (method === "POST" && path ===
|
|
5611
|
+
if (method === "POST" && path === granolaTransportPaths.authRefresh) {
|
|
5303
5612
|
sendJson(response, await app.refreshAuth(), { headers: originHeaders });
|
|
5304
5613
|
return;
|
|
5305
5614
|
}
|
|
5306
|
-
if (method === "POST" && path ===
|
|
5615
|
+
if (method === "POST" && path === granolaTransportPaths.authMode) {
|
|
5307
5616
|
const body = await readJsonBody(request);
|
|
5308
5617
|
sendJson(response, await app.switchAuthMode(parseAuthMode(body.mode)), { headers: originHeaders });
|
|
5309
5618
|
return;
|
|
5310
5619
|
}
|
|
5311
|
-
if (method === "POST" && path ===
|
|
5620
|
+
if (method === "POST" && path === granolaTransportPaths.exportNotes) {
|
|
5312
5621
|
const body = await readJsonBody(request);
|
|
5313
5622
|
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), {
|
|
5314
5623
|
headers: originHeaders,
|
|
@@ -5316,13 +5625,13 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5316
5625
|
});
|
|
5317
5626
|
return;
|
|
5318
5627
|
}
|
|
5319
|
-
if (method === "GET" && path ===
|
|
5628
|
+
if (method === "GET" && path === granolaTransportPaths.exportJobs) {
|
|
5320
5629
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5321
5630
|
sendJson(response, await app.listExportJobs({ limit }), { headers: originHeaders });
|
|
5322
5631
|
return;
|
|
5323
5632
|
}
|
|
5324
|
-
if (method === "POST" && path.startsWith(
|
|
5325
|
-
const id = decodeURIComponent(path.slice(
|
|
5633
|
+
if (method === "POST" && path.startsWith(`${granolaTransportPaths.exportJobs}/`) && path.endsWith("/rerun")) {
|
|
5634
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.exportJobs}/`.length, -6));
|
|
5326
5635
|
if (!id) throw new Error("export job id is required");
|
|
5327
5636
|
sendJson(response, await app.rerunExportJob(id), {
|
|
5328
5637
|
headers: originHeaders,
|
|
@@ -5330,7 +5639,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5330
5639
|
});
|
|
5331
5640
|
return;
|
|
5332
5641
|
}
|
|
5333
|
-
if (method === "POST" && path ===
|
|
5642
|
+
if (method === "POST" && path === granolaTransportPaths.exportTranscripts) {
|
|
5334
5643
|
const body = await readJsonBody(request);
|
|
5335
5644
|
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format)), {
|
|
5336
5645
|
headers: originHeaders,
|
|
@@ -5402,6 +5711,7 @@ function printWebRoutes() {
|
|
|
5402
5711
|
console.log("Routes:");
|
|
5403
5712
|
console.log(" GET /");
|
|
5404
5713
|
console.log(" GET /health");
|
|
5714
|
+
console.log(" GET /server/info");
|
|
5405
5715
|
console.log(" POST /auth/unlock");
|
|
5406
5716
|
console.log(" POST /auth/lock");
|
|
5407
5717
|
console.log(" GET /auth/status");
|
|
@@ -5814,6 +6124,7 @@ const serveCommand = {
|
|
|
5814
6124
|
if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
|
|
5815
6125
|
console.log("Endpoints:");
|
|
5816
6126
|
console.log(" GET /health");
|
|
6127
|
+
console.log(" GET /server/info");
|
|
5817
6128
|
console.log(" POST /auth/unlock");
|
|
5818
6129
|
console.log(" POST /auth/lock");
|
|
5819
6130
|
console.log(" GET /auth/status");
|