granola-toolkit 0.41.0 → 0.43.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 +2 -0
- package/dist/cli.js +1293 -178
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
8
8
|
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
9
|
-
import { execFile } from "node:child_process";
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
12
|
//#region src/transport.ts
|
|
@@ -20,6 +20,7 @@ const granolaTransportPaths = {
|
|
|
20
20
|
authUnlock: "/auth/unlock",
|
|
21
21
|
automationMatches: "/automation/matches",
|
|
22
22
|
automationRules: "/automation/rules",
|
|
23
|
+
automationRuns: "/automation/runs",
|
|
23
24
|
events: "/events",
|
|
24
25
|
exportJobs: "/exports/jobs",
|
|
25
26
|
exportNotes: "/exports/notes",
|
|
@@ -79,6 +80,15 @@ function granolaFoldersPath(options = {}) {
|
|
|
79
80
|
function granolaExportJobsPath(options = {}) {
|
|
80
81
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
81
82
|
}
|
|
83
|
+
function granolaAutomationRunsPath(options = {}) {
|
|
84
|
+
return appendSearchParams(granolaTransportPaths.automationRuns, {
|
|
85
|
+
limit: options.limit,
|
|
86
|
+
status: options.status
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function granolaAutomationRunDecisionPath(id, decision) {
|
|
90
|
+
return `${granolaTransportPaths.automationRuns}/${encodeURIComponent(id)}/${decision}`;
|
|
91
|
+
}
|
|
82
92
|
function granolaExportJobRerunPath(id) {
|
|
83
93
|
return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
|
|
84
94
|
}
|
|
@@ -188,6 +198,16 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
188
198
|
const path = options.limit ? `${granolaTransportPaths.automationMatches}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.automationMatches;
|
|
189
199
|
return await this.requestJson(path);
|
|
190
200
|
}
|
|
201
|
+
async listAutomationRuns(options = {}) {
|
|
202
|
+
return await this.requestJson(granolaAutomationRunsPath(options));
|
|
203
|
+
}
|
|
204
|
+
async resolveAutomationRun(id, decision, options = {}) {
|
|
205
|
+
return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
|
|
206
|
+
body: JSON.stringify(options),
|
|
207
|
+
headers: { "content-type": "application/json" },
|
|
208
|
+
method: "POST"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
191
211
|
async inspectSync() {
|
|
192
212
|
return cloneValue(this.#state.sync);
|
|
193
213
|
}
|
|
@@ -1392,7 +1412,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1392
1412
|
}
|
|
1393
1413
|
}
|
|
1394
1414
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1395
|
-
return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
|
|
1415
|
+
return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | ${state.automation.pendingRunCount ? `${state.automation.pendingRunCount} pending` : state.automation.runCount ? `${state.automation.runCount} runs` : "automation idle"} | list ${meetingSource}`;
|
|
1396
1416
|
}
|
|
1397
1417
|
//#endregion
|
|
1398
1418
|
//#region src/tui/theme.ts
|
|
@@ -1424,6 +1444,81 @@ const granolaTuiTheme = {
|
|
|
1424
1444
|
}
|
|
1425
1445
|
};
|
|
1426
1446
|
//#endregion
|
|
1447
|
+
//#region src/tui/automation.ts
|
|
1448
|
+
function padLine$3(text, width) {
|
|
1449
|
+
const clipped = truncateToWidth(text, width, "");
|
|
1450
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
1451
|
+
}
|
|
1452
|
+
function frameLine$2(text, width) {
|
|
1453
|
+
return `| ${padLine$3(text, Math.max(1, width - 4))} |`;
|
|
1454
|
+
}
|
|
1455
|
+
function wrapDetails(text, width) {
|
|
1456
|
+
return wrapTextWithAnsi(text, Math.max(1, width - 4));
|
|
1457
|
+
}
|
|
1458
|
+
function statusLabel(run) {
|
|
1459
|
+
switch (run.status) {
|
|
1460
|
+
case "completed": return granolaTuiTheme.info(run.status);
|
|
1461
|
+
case "failed": return granolaTuiTheme.error(run.status);
|
|
1462
|
+
case "pending": return granolaTuiTheme.warning(run.status);
|
|
1463
|
+
default: return granolaTuiTheme.dim(run.status);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
var GranolaTuiAutomationOverlay = class {
|
|
1467
|
+
focused = false;
|
|
1468
|
+
#selectedIndex = 0;
|
|
1469
|
+
constructor(options) {
|
|
1470
|
+
this.options = options;
|
|
1471
|
+
}
|
|
1472
|
+
invalidate() {}
|
|
1473
|
+
get selected() {
|
|
1474
|
+
return this.options.runs[this.#selectedIndex];
|
|
1475
|
+
}
|
|
1476
|
+
handleInput(data) {
|
|
1477
|
+
if (matchesKey(data, "esc")) {
|
|
1478
|
+
this.options.onCancel();
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (matchesKey(data, "up")) {
|
|
1482
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (matchesKey(data, "down")) {
|
|
1486
|
+
this.#selectedIndex = Math.min(this.options.runs.length - 1, this.#selectedIndex + 1);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (matchesKey(data, "enter") || matchesKey(data, "a")) {
|
|
1490
|
+
if (this.selected?.status === "pending") this.options.onApprove(this.selected.id);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (matchesKey(data, "r")) {
|
|
1494
|
+
if (this.selected?.status === "pending") this.options.onReject(this.selected.id);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
render(width) {
|
|
1498
|
+
const bodyWidth = Math.max(56, width);
|
|
1499
|
+
const innerWidth = Math.max(1, bodyWidth - 4);
|
|
1500
|
+
const maxRuns = 6;
|
|
1501
|
+
const lines = [];
|
|
1502
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1503
|
+
lines.push(frameLine$2(granolaTuiTheme.strong("Automation Runs"), bodyWidth));
|
|
1504
|
+
lines.push(frameLine$2("Pending runs can be approved with Enter/a or rejected with r.", bodyWidth));
|
|
1505
|
+
lines.push(frameLine$2("", bodyWidth));
|
|
1506
|
+
if (this.options.runs.length === 0) lines.push(frameLine$2(granolaTuiTheme.dim("No automation runs yet."), bodyWidth));
|
|
1507
|
+
else for (const [index, run] of this.options.runs.slice(0, maxRuns).entries()) {
|
|
1508
|
+
const selected = index === this.#selectedIndex;
|
|
1509
|
+
const title = `${selected ? ">" : " "} ${run.actionName} · ${statusLabel(run)}`;
|
|
1510
|
+
lines.push(frameLine$2(selected ? granolaTuiTheme.selected(title) : title, bodyWidth));
|
|
1511
|
+
lines.push(frameLine$2(` ${run.ruleName} · ${run.title}`, bodyWidth));
|
|
1512
|
+
const details = run.prompt || run.result || run.error || run.eventKind;
|
|
1513
|
+
for (const line of wrapDetails(` ${details}`, innerWidth).slice(0, 2)) lines.push(frameLine$2(line, bodyWidth));
|
|
1514
|
+
lines.push(frameLine$2("", bodyWidth));
|
|
1515
|
+
}
|
|
1516
|
+
lines.push(frameLine$2(granolaTuiTheme.dim("Esc close"), bodyWidth));
|
|
1517
|
+
lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
|
|
1518
|
+
return lines;
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
//#endregion
|
|
1427
1522
|
//#region src/tui/auth.ts
|
|
1428
1523
|
function padLine$2(text, width) {
|
|
1429
1524
|
const clipped = truncateToWidth(text, width, "");
|
|
@@ -1700,6 +1795,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1700
1795
|
#maxMeetings;
|
|
1701
1796
|
#appState;
|
|
1702
1797
|
#activePane = "meetings";
|
|
1798
|
+
#automationRuns = [];
|
|
1703
1799
|
#detailError = "";
|
|
1704
1800
|
#detailScroll = 0;
|
|
1705
1801
|
#detailToken = 0;
|
|
@@ -1732,6 +1828,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1732
1828
|
this.#unsubscribe = this.app.subscribe((event) => {
|
|
1733
1829
|
this.handleAppUpdate(event);
|
|
1734
1830
|
});
|
|
1831
|
+
await this.loadAutomationRuns();
|
|
1735
1832
|
await this.loadFolders({ setStatus: false });
|
|
1736
1833
|
await this.loadMeetings({
|
|
1737
1834
|
preferredMeetingId: this.options.initialMeetingId,
|
|
@@ -1750,6 +1847,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1750
1847
|
this.#appState = event.state;
|
|
1751
1848
|
this.#selectedFolderId = event.state.ui.selectedFolderId;
|
|
1752
1849
|
this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
|
|
1850
|
+
this.loadAutomationRuns();
|
|
1753
1851
|
if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
|
|
1754
1852
|
await this.loadFolders({ setStatus: false });
|
|
1755
1853
|
await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
|
|
@@ -1804,6 +1902,12 @@ var GranolaTuiWorkspace = class {
|
|
|
1804
1902
|
if (token === this.#folderToken) this.tui.requestRender();
|
|
1805
1903
|
}
|
|
1806
1904
|
}
|
|
1905
|
+
async loadAutomationRuns() {
|
|
1906
|
+
try {
|
|
1907
|
+
this.#automationRuns = [...(await this.app.listAutomationRuns({ limit: 20 })).runs];
|
|
1908
|
+
this.tui.requestRender();
|
|
1909
|
+
} catch {}
|
|
1910
|
+
}
|
|
1807
1911
|
async loadMeetings(options = {}) {
|
|
1808
1912
|
const token = ++this.#listToken;
|
|
1809
1913
|
this.#loadingMeetings = true;
|
|
@@ -2079,6 +2183,38 @@ var GranolaTuiWorkspace = class {
|
|
|
2079
2183
|
});
|
|
2080
2184
|
this.setStatus("Quick open");
|
|
2081
2185
|
}
|
|
2186
|
+
openAutomationPanel() {
|
|
2187
|
+
if (this.#overlay) return;
|
|
2188
|
+
const closeOverlay = () => {
|
|
2189
|
+
this.#overlay?.hide();
|
|
2190
|
+
this.#overlay = void 0;
|
|
2191
|
+
this.tui.setFocus(this);
|
|
2192
|
+
this.tui.requestRender();
|
|
2193
|
+
};
|
|
2194
|
+
const overlay = new GranolaTuiAutomationOverlay({
|
|
2195
|
+
onApprove: async (id) => {
|
|
2196
|
+
closeOverlay();
|
|
2197
|
+
await this.app.resolveAutomationRun(id, "approve");
|
|
2198
|
+
await this.loadAutomationRuns();
|
|
2199
|
+
this.setStatus("Automation approved");
|
|
2200
|
+
},
|
|
2201
|
+
onCancel: closeOverlay,
|
|
2202
|
+
onReject: async (id) => {
|
|
2203
|
+
closeOverlay();
|
|
2204
|
+
await this.app.resolveAutomationRun(id, "reject");
|
|
2205
|
+
await this.loadAutomationRuns();
|
|
2206
|
+
this.setStatus("Automation rejected");
|
|
2207
|
+
},
|
|
2208
|
+
runs: this.#automationRuns
|
|
2209
|
+
});
|
|
2210
|
+
this.#overlay = this.tui.showOverlay(overlay, {
|
|
2211
|
+
anchor: "center",
|
|
2212
|
+
maxHeight: "70%",
|
|
2213
|
+
minWidth: 56,
|
|
2214
|
+
width: "76%"
|
|
2215
|
+
});
|
|
2216
|
+
this.setStatus("Automation runs");
|
|
2217
|
+
}
|
|
2082
2218
|
handleInput(data) {
|
|
2083
2219
|
if (matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
|
|
2084
2220
|
this.options.onExit();
|
|
@@ -2096,6 +2232,10 @@ var GranolaTuiWorkspace = class {
|
|
|
2096
2232
|
this.openAuthPanel();
|
|
2097
2233
|
return;
|
|
2098
2234
|
}
|
|
2235
|
+
if (matchesKey(data, "u")) {
|
|
2236
|
+
this.openAutomationPanel();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2099
2239
|
if (matchesKey(data, "tab")) {
|
|
2100
2240
|
this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
|
|
2101
2241
|
this.tui.requestRender();
|
|
@@ -2280,7 +2420,7 @@ var GranolaTuiWorkspace = class {
|
|
|
2280
2420
|
const bodyLines = [];
|
|
2281
2421
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
2282
2422
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
2283
|
-
const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2423
|
+
const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth u automation r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2284
2424
|
return [
|
|
2285
2425
|
headerTitle,
|
|
2286
2426
|
headerSummary,
|
|
@@ -2350,6 +2490,127 @@ const attachCommand = {
|
|
|
2350
2490
|
}
|
|
2351
2491
|
};
|
|
2352
2492
|
//#endregion
|
|
2493
|
+
//#region src/automation-actions.ts
|
|
2494
|
+
function cloneAction$1(action) {
|
|
2495
|
+
switch (action.kind) {
|
|
2496
|
+
case "ask-user": return { ...action };
|
|
2497
|
+
case "command": return {
|
|
2498
|
+
...action,
|
|
2499
|
+
args: action.args ? [...action.args] : void 0,
|
|
2500
|
+
env: action.env ? { ...action.env } : void 0
|
|
2501
|
+
};
|
|
2502
|
+
case "export-notes":
|
|
2503
|
+
case "export-transcript": return { ...action };
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function automationActionName(action) {
|
|
2507
|
+
return action.name || action.id;
|
|
2508
|
+
}
|
|
2509
|
+
function buildAutomationActionRunId(match, actionId) {
|
|
2510
|
+
return `${match.id}:${actionId}`;
|
|
2511
|
+
}
|
|
2512
|
+
function enabledAutomationActions(rule) {
|
|
2513
|
+
return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
|
|
2514
|
+
}
|
|
2515
|
+
function baseRun(match, rule, action, startedAt) {
|
|
2516
|
+
return {
|
|
2517
|
+
actionId: action.id,
|
|
2518
|
+
actionKind: action.kind,
|
|
2519
|
+
actionName: automationActionName(action),
|
|
2520
|
+
eventId: match.eventId,
|
|
2521
|
+
eventKind: match.eventKind,
|
|
2522
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
2523
|
+
id: buildAutomationActionRunId(match, action.id),
|
|
2524
|
+
matchedAt: match.matchedAt,
|
|
2525
|
+
meetingId: match.meetingId,
|
|
2526
|
+
ruleId: rule.id,
|
|
2527
|
+
ruleName: rule.name,
|
|
2528
|
+
startedAt,
|
|
2529
|
+
status: "completed",
|
|
2530
|
+
tags: [...match.tags],
|
|
2531
|
+
title: match.title,
|
|
2532
|
+
transcriptLoaded: match.transcriptLoaded
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
function completedRun(run, finishedAt, patch = {}) {
|
|
2536
|
+
return {
|
|
2537
|
+
...run,
|
|
2538
|
+
...patch,
|
|
2539
|
+
finishedAt,
|
|
2540
|
+
status: "completed"
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
function failedRun(run, finishedAt, error) {
|
|
2544
|
+
return {
|
|
2545
|
+
...run,
|
|
2546
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2547
|
+
finishedAt,
|
|
2548
|
+
status: "failed"
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
function skippedRun(run, finishedAt, reason) {
|
|
2552
|
+
return {
|
|
2553
|
+
...run,
|
|
2554
|
+
finishedAt,
|
|
2555
|
+
result: reason,
|
|
2556
|
+
status: "skipped"
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
async function executeAutomationAction(match, rule, action, handlers) {
|
|
2560
|
+
const run = baseRun(match, rule, action, handlers.nowIso());
|
|
2561
|
+
switch (action.kind) {
|
|
2562
|
+
case "ask-user": return {
|
|
2563
|
+
...run,
|
|
2564
|
+
meta: action.details ? { details: action.details } : void 0,
|
|
2565
|
+
prompt: action.prompt,
|
|
2566
|
+
result: "Pending user decision",
|
|
2567
|
+
status: "pending"
|
|
2568
|
+
};
|
|
2569
|
+
case "command": try {
|
|
2570
|
+
const result = await handlers.runCommand(match, rule, action);
|
|
2571
|
+
return completedRun(run, handlers.nowIso(), {
|
|
2572
|
+
meta: {
|
|
2573
|
+
command: result.command,
|
|
2574
|
+
cwd: result.cwd
|
|
2575
|
+
},
|
|
2576
|
+
result: result.output
|
|
2577
|
+
});
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
2580
|
+
}
|
|
2581
|
+
case "export-notes": try {
|
|
2582
|
+
const result = await handlers.exportNotes(match, action);
|
|
2583
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
|
|
2584
|
+
return completedRun(run, handlers.nowIso(), {
|
|
2585
|
+
meta: {
|
|
2586
|
+
format: result.format,
|
|
2587
|
+
outputDir: result.outputDir,
|
|
2588
|
+
scope: result.scope,
|
|
2589
|
+
written: result.written
|
|
2590
|
+
},
|
|
2591
|
+
result: `Exported notes to ${result.outputDir}`
|
|
2592
|
+
});
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
2595
|
+
}
|
|
2596
|
+
case "export-transcript": try {
|
|
2597
|
+
const result = await handlers.exportTranscripts(match, action);
|
|
2598
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
|
|
2599
|
+
return completedRun(run, handlers.nowIso(), {
|
|
2600
|
+
meta: {
|
|
2601
|
+
format: result.format,
|
|
2602
|
+
outputDir: result.outputDir,
|
|
2603
|
+
scope: result.scope,
|
|
2604
|
+
written: result.written
|
|
2605
|
+
},
|
|
2606
|
+
result: `Exported transcript to ${result.outputDir}`
|
|
2607
|
+
});
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
//#endregion
|
|
2353
2614
|
//#region src/persistence/layout.ts
|
|
2354
2615
|
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2355
2616
|
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
@@ -2360,10 +2621,12 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2360
2621
|
return {
|
|
2361
2622
|
automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
|
|
2362
2623
|
automationRulesFile: join(dataDirectory, "automation-rules.json"),
|
|
2624
|
+
automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
|
|
2363
2625
|
apiKeyFile: join(dataDirectory, "api-key.txt"),
|
|
2364
2626
|
dataDirectory,
|
|
2365
2627
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2366
2628
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2629
|
+
searchIndexFile: join(dataDirectory, "search-index.json"),
|
|
2367
2630
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2368
2631
|
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2369
2632
|
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
@@ -2408,10 +2671,60 @@ function createDefaultAutomationMatchStore(filePath) {
|
|
|
2408
2671
|
return new FileAutomationMatchStore(filePath);
|
|
2409
2672
|
}
|
|
2410
2673
|
//#endregion
|
|
2674
|
+
//#region src/automation-runs.ts
|
|
2675
|
+
function cloneRun(run) {
|
|
2676
|
+
return {
|
|
2677
|
+
...run,
|
|
2678
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
2679
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
2680
|
+
tags: [...run.tags]
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
function sortRuns(runs) {
|
|
2684
|
+
return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
2685
|
+
}
|
|
2686
|
+
function mergeRuns(runs) {
|
|
2687
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2688
|
+
for (const run of runs) byId.set(run.id, cloneRun(run));
|
|
2689
|
+
return sortRuns([...byId.values()]);
|
|
2690
|
+
}
|
|
2691
|
+
var FileAutomationRunStore = class {
|
|
2692
|
+
constructor(filePath = defaultAutomationRunsFilePath()) {
|
|
2693
|
+
this.filePath = filePath;
|
|
2694
|
+
}
|
|
2695
|
+
async appendRuns(runs) {
|
|
2696
|
+
if (runs.length === 0) return;
|
|
2697
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2698
|
+
const payload = runs.map((run) => JSON.stringify(run)).join("\n");
|
|
2699
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
2700
|
+
encoding: "utf8",
|
|
2701
|
+
mode: 384
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
async readRun(id) {
|
|
2705
|
+
return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
|
|
2706
|
+
}
|
|
2707
|
+
async readRuns(options = {}) {
|
|
2708
|
+
try {
|
|
2709
|
+
const runs = mergeRuns((await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((run) => Boolean(run))).filter((run) => options.status ? run.status === options.status : true);
|
|
2710
|
+
return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
|
|
2711
|
+
} catch {
|
|
2712
|
+
return [];
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
function defaultAutomationRunsFilePath() {
|
|
2717
|
+
return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
|
|
2718
|
+
}
|
|
2719
|
+
function createDefaultAutomationRunStore(filePath) {
|
|
2720
|
+
return new FileAutomationRunStore(filePath);
|
|
2721
|
+
}
|
|
2722
|
+
//#endregion
|
|
2411
2723
|
//#region src/automation-rules.ts
|
|
2412
2724
|
function cloneRule(rule) {
|
|
2413
2725
|
return {
|
|
2414
2726
|
...rule,
|
|
2727
|
+
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
2415
2728
|
when: {
|
|
2416
2729
|
...rule.when,
|
|
2417
2730
|
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
@@ -2423,11 +2736,92 @@ function cloneRule(rule) {
|
|
|
2423
2736
|
}
|
|
2424
2737
|
};
|
|
2425
2738
|
}
|
|
2739
|
+
function cloneAction(action) {
|
|
2740
|
+
switch (action.kind) {
|
|
2741
|
+
case "ask-user": return { ...action };
|
|
2742
|
+
case "command": return {
|
|
2743
|
+
...action,
|
|
2744
|
+
args: action.args ? [...action.args] : void 0,
|
|
2745
|
+
env: action.env ? { ...action.env } : void 0
|
|
2746
|
+
};
|
|
2747
|
+
case "export-notes":
|
|
2748
|
+
case "export-transcript": return { ...action };
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2426
2751
|
function stringArray(value) {
|
|
2427
2752
|
if (!Array.isArray(value)) return;
|
|
2428
2753
|
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
2429
2754
|
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
2430
2755
|
}
|
|
2756
|
+
function stringRecord(value) {
|
|
2757
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2758
|
+
const entries = Object.entries(value).filter(([key, item]) => {
|
|
2759
|
+
return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
|
|
2760
|
+
});
|
|
2761
|
+
if (entries.length === 0) return;
|
|
2762
|
+
return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
|
|
2763
|
+
}
|
|
2764
|
+
function parseAction(value, index) {
|
|
2765
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2766
|
+
const record = value;
|
|
2767
|
+
const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
|
|
2768
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
|
|
2769
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
2770
|
+
const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
|
|
2771
|
+
switch (kind) {
|
|
2772
|
+
case "ask-user": {
|
|
2773
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
2774
|
+
if (!id || !prompt) return;
|
|
2775
|
+
return {
|
|
2776
|
+
details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
|
|
2777
|
+
enabled,
|
|
2778
|
+
id,
|
|
2779
|
+
kind,
|
|
2780
|
+
name,
|
|
2781
|
+
prompt
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
case "command": {
|
|
2785
|
+
const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
|
|
2786
|
+
if (!id || !command) return;
|
|
2787
|
+
return {
|
|
2788
|
+
args: stringArray(record.args),
|
|
2789
|
+
command,
|
|
2790
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
2791
|
+
enabled,
|
|
2792
|
+
env: stringRecord(record.env),
|
|
2793
|
+
id,
|
|
2794
|
+
kind,
|
|
2795
|
+
name,
|
|
2796
|
+
stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
|
|
2797
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
case "export-notes":
|
|
2801
|
+
if (!id) return;
|
|
2802
|
+
return {
|
|
2803
|
+
enabled,
|
|
2804
|
+
format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
|
|
2805
|
+
id,
|
|
2806
|
+
kind,
|
|
2807
|
+
name,
|
|
2808
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2809
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2810
|
+
};
|
|
2811
|
+
case "export-transcript":
|
|
2812
|
+
if (!id) return;
|
|
2813
|
+
return {
|
|
2814
|
+
enabled,
|
|
2815
|
+
format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
|
|
2816
|
+
id,
|
|
2817
|
+
kind,
|
|
2818
|
+
name,
|
|
2819
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2820
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2821
|
+
};
|
|
2822
|
+
default: return;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2431
2825
|
function parseRule(value) {
|
|
2432
2826
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2433
2827
|
const record = value;
|
|
@@ -2436,6 +2830,7 @@ function parseRule(value) {
|
|
|
2436
2830
|
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
2437
2831
|
if (!id || !name || !whenValue) return;
|
|
2438
2832
|
return {
|
|
2833
|
+
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
2439
2834
|
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
2440
2835
|
id,
|
|
2441
2836
|
name,
|
|
@@ -3447,6 +3842,7 @@ async function loadOptionalGranolaCache(cacheFile) {
|
|
|
3447
3842
|
//#endregion
|
|
3448
3843
|
//#region src/export-scope.ts
|
|
3449
3844
|
const FOLDER_EXPORT_DIRECTORY = "_folders";
|
|
3845
|
+
const MEETING_EXPORT_DIRECTORY = "_meetings";
|
|
3450
3846
|
function allExportScope() {
|
|
3451
3847
|
return { mode: "all" };
|
|
3452
3848
|
}
|
|
@@ -3458,11 +3854,22 @@ function folderExportScope(folder) {
|
|
|
3458
3854
|
};
|
|
3459
3855
|
}
|
|
3460
3856
|
function cloneExportScope(scope) {
|
|
3461
|
-
|
|
3857
|
+
if (scope.mode === "folder" || scope.mode === "meeting") return { ...scope };
|
|
3858
|
+
return { mode: "all" };
|
|
3462
3859
|
}
|
|
3463
3860
|
function normaliseExportScope(value) {
|
|
3464
3861
|
const record = asRecord(value);
|
|
3465
3862
|
if (!record) return allExportScope();
|
|
3863
|
+
if (record.mode === "meeting") {
|
|
3864
|
+
const meetingId = stringValue(record.meetingId);
|
|
3865
|
+
const meetingTitle = stringValue(record.meetingTitle) || meetingId;
|
|
3866
|
+
if (!meetingId) return allExportScope();
|
|
3867
|
+
return {
|
|
3868
|
+
meetingId,
|
|
3869
|
+
meetingTitle,
|
|
3870
|
+
mode: "meeting"
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3466
3873
|
if (record.mode !== "folder") return allExportScope();
|
|
3467
3874
|
const folderId = stringValue(record.folderId);
|
|
3468
3875
|
const folderName = stringValue(record.folderName) || folderId;
|
|
@@ -3473,12 +3880,22 @@ function normaliseExportScope(value) {
|
|
|
3473
3880
|
mode: "folder"
|
|
3474
3881
|
};
|
|
3475
3882
|
}
|
|
3883
|
+
function meetingExportScope(meeting) {
|
|
3884
|
+
return {
|
|
3885
|
+
meetingId: meeting.meetingId,
|
|
3886
|
+
meetingTitle: meeting.meetingTitle || meeting.meetingId,
|
|
3887
|
+
mode: "meeting"
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3476
3890
|
function renderExportScopeLabel(scope) {
|
|
3477
|
-
|
|
3891
|
+
if (scope.mode === "folder") return `folder ${scope.folderName}`;
|
|
3892
|
+
if (scope.mode === "meeting") return `meeting ${scope.meetingTitle}`;
|
|
3893
|
+
return "all meetings";
|
|
3478
3894
|
}
|
|
3479
3895
|
function resolveExportOutputDir(outputDir, scope, options = {}) {
|
|
3480
|
-
if (
|
|
3481
|
-
return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3896
|
+
if (options.scopedDirectory === false || scope.mode === "all") return outputDir;
|
|
3897
|
+
if (scope.mode === "folder") return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3898
|
+
return join(outputDir, MEETING_EXPORT_DIRECTORY, sanitiseFilename(scope.meetingId, "meeting"));
|
|
3482
3899
|
}
|
|
3483
3900
|
//#endregion
|
|
3484
3901
|
//#region src/export-jobs.ts
|
|
@@ -3936,6 +4353,128 @@ function buildSyncEvents(runId, occurredAt, changes, previousMeetings, nextMeeti
|
|
|
3936
4353
|
}));
|
|
3937
4354
|
}
|
|
3938
4355
|
//#endregion
|
|
4356
|
+
//#region src/search-index.ts
|
|
4357
|
+
const SEARCH_INDEX_VERSION = 1;
|
|
4358
|
+
function cloneEntry(entry) {
|
|
4359
|
+
return {
|
|
4360
|
+
...entry,
|
|
4361
|
+
folderIds: [...entry.folderIds],
|
|
4362
|
+
folderNames: [...entry.folderNames],
|
|
4363
|
+
tags: [...entry.tags]
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
function noteText(document) {
|
|
4367
|
+
const notes = document.notesPlain.trim();
|
|
4368
|
+
if (notes) return notes;
|
|
4369
|
+
const panel = document.lastViewedPanel?.originalContent?.trim();
|
|
4370
|
+
if (panel) return panel;
|
|
4371
|
+
return document.content.trim();
|
|
4372
|
+
}
|
|
4373
|
+
function transcriptText(documentId, cacheData) {
|
|
4374
|
+
return (cacheData?.transcripts[documentId] ?? []).filter((segment) => segment.isFinal).map((segment) => segment.text.trim()).filter(Boolean).join("\n");
|
|
4375
|
+
}
|
|
4376
|
+
function buildSearchIndex(documents, options = {}) {
|
|
4377
|
+
return documents.map((document) => {
|
|
4378
|
+
const folders = options.foldersByDocumentId?.get(document.id) ?? [];
|
|
4379
|
+
const transcript = transcriptText(document.id, options.cacheData);
|
|
4380
|
+
return {
|
|
4381
|
+
createdAt: document.createdAt,
|
|
4382
|
+
folderIds: folders.map((folder) => folder.id),
|
|
4383
|
+
folderNames: folders.map((folder) => folder.name || folder.id),
|
|
4384
|
+
id: document.id,
|
|
4385
|
+
noteText: noteText(document),
|
|
4386
|
+
tags: [...document.tags],
|
|
4387
|
+
title: document.title,
|
|
4388
|
+
transcriptLoaded: transcript.length > 0,
|
|
4389
|
+
transcriptText: transcript,
|
|
4390
|
+
updatedAt: document.updatedAt
|
|
4391
|
+
};
|
|
4392
|
+
}).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
4393
|
+
}
|
|
4394
|
+
function searchFieldScore(value, term) {
|
|
4395
|
+
const lower = value.toLowerCase();
|
|
4396
|
+
if (!lower || !term) return 0;
|
|
4397
|
+
if (lower === term) return 8;
|
|
4398
|
+
if (lower.startsWith(term)) return 5;
|
|
4399
|
+
if (lower.includes(term)) return 3;
|
|
4400
|
+
return 0;
|
|
4401
|
+
}
|
|
4402
|
+
function combinedText(entry) {
|
|
4403
|
+
return [
|
|
4404
|
+
entry.id,
|
|
4405
|
+
entry.title,
|
|
4406
|
+
...entry.tags,
|
|
4407
|
+
...entry.folderNames,
|
|
4408
|
+
entry.noteText,
|
|
4409
|
+
entry.transcriptText
|
|
4410
|
+
].join("\n").toLowerCase();
|
|
4411
|
+
}
|
|
4412
|
+
function searchEntryScore(entry, term) {
|
|
4413
|
+
const scoredFields = [
|
|
4414
|
+
searchFieldScore(entry.id, term) * 5,
|
|
4415
|
+
searchFieldScore(entry.title, term) * 8,
|
|
4416
|
+
...entry.tags.map((tag) => searchFieldScore(tag, term) * 6),
|
|
4417
|
+
...entry.folderNames.map((folderName) => searchFieldScore(folderName, term) * 4)
|
|
4418
|
+
].filter((score) => score > 0);
|
|
4419
|
+
if (scoredFields.length > 0) return Math.max(...scoredFields);
|
|
4420
|
+
if (combinedText(entry).includes(term)) return 1;
|
|
4421
|
+
}
|
|
4422
|
+
function searchSearchIndex(entries, query) {
|
|
4423
|
+
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
4424
|
+
if (terms.length === 0) return [];
|
|
4425
|
+
return entries.map((entry) => {
|
|
4426
|
+
let score = 0;
|
|
4427
|
+
for (const term of terms) {
|
|
4428
|
+
const termScore = searchEntryScore(entry, term);
|
|
4429
|
+
if (termScore == null) return;
|
|
4430
|
+
score += termScore;
|
|
4431
|
+
}
|
|
4432
|
+
return {
|
|
4433
|
+
id: entry.id,
|
|
4434
|
+
score,
|
|
4435
|
+
updatedAt: entry.updatedAt
|
|
4436
|
+
};
|
|
4437
|
+
}).filter((entry) => Boolean(entry)).sort((left, right) => right.score - left.score || right.updatedAt.localeCompare(left.updatedAt) || left.id.localeCompare(right.id)).map(({ id, score }) => ({
|
|
4438
|
+
id,
|
|
4439
|
+
score
|
|
4440
|
+
}));
|
|
4441
|
+
}
|
|
4442
|
+
var FileSearchIndexStore = class {
|
|
4443
|
+
constructor(filePath = defaultSearchIndexFilePath()) {
|
|
4444
|
+
this.filePath = filePath;
|
|
4445
|
+
}
|
|
4446
|
+
async readIndex() {
|
|
4447
|
+
try {
|
|
4448
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
4449
|
+
if (!parsed || parsed.version !== SEARCH_INDEX_VERSION || !Array.isArray(parsed.entries)) return [];
|
|
4450
|
+
return parsed.entries.map(cloneEntry);
|
|
4451
|
+
} catch {
|
|
4452
|
+
return [];
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
async writeIndex(entries) {
|
|
4456
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
4457
|
+
const payload = {
|
|
4458
|
+
entries: entries.map(cloneEntry),
|
|
4459
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4460
|
+
version: SEARCH_INDEX_VERSION
|
|
4461
|
+
};
|
|
4462
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
4463
|
+
encoding: "utf8",
|
|
4464
|
+
mode: 384
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
function defaultSearchIndexFilePath() {
|
|
4469
|
+
return defaultGranolaToolkitPersistenceLayout().searchIndexFile;
|
|
4470
|
+
}
|
|
4471
|
+
function createDefaultSearchIndexStore() {
|
|
4472
|
+
return new FileSearchIndexStore();
|
|
4473
|
+
}
|
|
4474
|
+
function meetingIdsFromSearchResults(results) {
|
|
4475
|
+
return results.map((result) => result.id);
|
|
4476
|
+
}
|
|
4477
|
+
//#endregion
|
|
3939
4478
|
//#region src/app/core.ts
|
|
3940
4479
|
function transcriptCount(cacheData) {
|
|
3941
4480
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -4005,8 +4544,11 @@ function defaultState(config, auth, surface) {
|
|
|
4005
4544
|
loaded: false,
|
|
4006
4545
|
matchCount: 0,
|
|
4007
4546
|
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4547
|
+
pendingRunCount: 0,
|
|
4008
4548
|
ruleCount: 0,
|
|
4009
|
-
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4549
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4550
|
+
runCount: 0,
|
|
4551
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4010
4552
|
},
|
|
4011
4553
|
cache: {
|
|
4012
4554
|
configured: Boolean(config.transcripts.cacheFile),
|
|
@@ -4049,6 +4591,7 @@ function defaultState(config, auth, surface) {
|
|
|
4049
4591
|
};
|
|
4050
4592
|
}
|
|
4051
4593
|
var GranolaApp = class {
|
|
4594
|
+
#automationActionRuns;
|
|
4052
4595
|
#automationMatches;
|
|
4053
4596
|
#automationRules;
|
|
4054
4597
|
#cacheData;
|
|
@@ -4057,6 +4600,7 @@ var GranolaApp = class {
|
|
|
4057
4600
|
#granolaClient;
|
|
4058
4601
|
#documents;
|
|
4059
4602
|
#meetingIndex;
|
|
4603
|
+
#searchIndex;
|
|
4060
4604
|
#listeners = /* @__PURE__ */ new Set();
|
|
4061
4605
|
#refreshingMeetingIndex;
|
|
4062
4606
|
#state;
|
|
@@ -4069,28 +4613,28 @@ var GranolaApp = class {
|
|
|
4069
4613
|
folders: match.folders.map((folder) => ({ ...folder })),
|
|
4070
4614
|
tags: [...match.tags]
|
|
4071
4615
|
}));
|
|
4072
|
-
this.#
|
|
4073
|
-
|
|
4074
|
-
when: {
|
|
4075
|
-
...rule.when,
|
|
4076
|
-
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
4077
|
-
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
4078
|
-
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
4079
|
-
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
4080
|
-
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
4081
|
-
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
4082
|
-
}
|
|
4083
|
-
}));
|
|
4616
|
+
this.#automationActionRuns = (deps.automationRuns ?? []).map((run) => this.cloneAutomationRun(run));
|
|
4617
|
+
this.#automationRules = (deps.automationRules ?? []).map((rule) => this.cloneAutomationRule(rule));
|
|
4084
4618
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
4085
4619
|
this.#state.automation = {
|
|
4620
|
+
lastRunAt: this.#automationActionRuns[0]?.finishedAt ?? this.#automationActionRuns[0]?.startedAt,
|
|
4086
4621
|
lastMatchedAt: this.#automationMatches[0]?.matchedAt,
|
|
4087
4622
|
loaded: true,
|
|
4088
4623
|
matchCount: this.#automationMatches.length,
|
|
4089
4624
|
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4625
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4090
4626
|
ruleCount: this.#automationRules.length,
|
|
4091
|
-
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4627
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4628
|
+
runCount: this.#automationActionRuns.length,
|
|
4629
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4092
4630
|
};
|
|
4093
4631
|
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
4632
|
+
this.#searchIndex = (deps.searchIndex ?? []).map((entry) => ({
|
|
4633
|
+
...entry,
|
|
4634
|
+
folderIds: [...entry.folderIds],
|
|
4635
|
+
folderNames: [...entry.folderNames],
|
|
4636
|
+
tags: [...entry.tags]
|
|
4637
|
+
}));
|
|
4094
4638
|
this.#state.index = {
|
|
4095
4639
|
available: this.#meetingIndex.length > 0,
|
|
4096
4640
|
filePath: defaultMeetingIndexFilePath(),
|
|
@@ -4165,6 +4709,18 @@ var GranolaApp = class {
|
|
|
4165
4709
|
cloneAutomationRule(rule) {
|
|
4166
4710
|
return {
|
|
4167
4711
|
...rule,
|
|
4712
|
+
actions: rule.actions?.map((action) => {
|
|
4713
|
+
switch (action.kind) {
|
|
4714
|
+
case "ask-user": return { ...action };
|
|
4715
|
+
case "command": return {
|
|
4716
|
+
...action,
|
|
4717
|
+
args: action.args ? [...action.args] : void 0,
|
|
4718
|
+
env: action.env ? { ...action.env } : void 0
|
|
4719
|
+
};
|
|
4720
|
+
case "export-notes":
|
|
4721
|
+
case "export-transcript": return { ...action };
|
|
4722
|
+
}
|
|
4723
|
+
}),
|
|
4168
4724
|
when: {
|
|
4169
4725
|
...rule.when,
|
|
4170
4726
|
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
@@ -4183,16 +4739,40 @@ var GranolaApp = class {
|
|
|
4183
4739
|
tags: [...match.tags]
|
|
4184
4740
|
};
|
|
4185
4741
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4742
|
+
cloneAutomationRun(run) {
|
|
4743
|
+
return {
|
|
4744
|
+
...run,
|
|
4745
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
4746
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
4747
|
+
tags: [...run.tags]
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
refreshAutomationState() {
|
|
4751
|
+
const latestMatch = this.#automationMatches.reduce((current, candidate) => !current || candidate.matchedAt.localeCompare(current.matchedAt) > 0 ? candidate : current, void 0);
|
|
4752
|
+
const latestRun = this.#automationActionRuns.reduce((current, candidate) => {
|
|
4753
|
+
const candidateTime = candidate.finishedAt ?? candidate.startedAt;
|
|
4754
|
+
const currentTime = current ? current.finishedAt ?? current.startedAt : void 0;
|
|
4755
|
+
return !currentTime || candidateTime.localeCompare(currentTime) > 0 ? candidate : current;
|
|
4756
|
+
}, void 0);
|
|
4190
4757
|
this.#state.automation = {
|
|
4191
4758
|
...this.#state.automation,
|
|
4759
|
+
lastMatchedAt: latestMatch?.matchedAt ?? this.#state.automation.lastMatchedAt,
|
|
4760
|
+
lastRunAt: latestRun?.finishedAt ?? latestRun?.startedAt ?? this.#state.automation.lastRunAt,
|
|
4192
4761
|
loaded: true,
|
|
4762
|
+
matchCount: this.#automationMatches.length,
|
|
4763
|
+
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4764
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4193
4765
|
ruleCount: this.#automationRules.length,
|
|
4194
|
-
rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4766
|
+
rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4767
|
+
runCount: this.#automationActionRuns.length,
|
|
4768
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4195
4769
|
};
|
|
4770
|
+
}
|
|
4771
|
+
async loadAutomationRules(options = {}) {
|
|
4772
|
+
if (this.#automationRules.length > 0 && !options.forceRefresh) return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
|
|
4773
|
+
if (!this.deps.automationRuleStore) return [];
|
|
4774
|
+
this.#automationRules = (await this.deps.automationRuleStore.readRules()).map((rule) => this.cloneAutomationRule(rule));
|
|
4775
|
+
this.refreshAutomationState();
|
|
4196
4776
|
this.emitStateUpdate();
|
|
4197
4777
|
return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
|
|
4198
4778
|
}
|
|
@@ -4200,12 +4780,18 @@ var GranolaApp = class {
|
|
|
4200
4780
|
if (matches.length === 0) return;
|
|
4201
4781
|
if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
|
|
4202
4782
|
this.#automationMatches.push(...matches.map((match) => this.cloneAutomationMatch(match)));
|
|
4203
|
-
this
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4783
|
+
this.refreshAutomationState();
|
|
4784
|
+
}
|
|
4785
|
+
async appendAutomationRuns(runs) {
|
|
4786
|
+
if (runs.length === 0) return;
|
|
4787
|
+
if (this.deps.automationRunStore) await this.deps.automationRunStore.appendRuns(runs);
|
|
4788
|
+
for (const run of runs) {
|
|
4789
|
+
const index = this.#automationActionRuns.findIndex((candidate) => candidate.id === run.id);
|
|
4790
|
+
if (index >= 0) this.#automationActionRuns[index] = this.cloneAutomationRun(run);
|
|
4791
|
+
else this.#automationActionRuns.push(this.cloneAutomationRun(run));
|
|
4792
|
+
}
|
|
4793
|
+
this.#automationActionRuns.sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
4794
|
+
this.refreshAutomationState();
|
|
4209
4795
|
}
|
|
4210
4796
|
createSyncRunId() {
|
|
4211
4797
|
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
@@ -4232,6 +4818,15 @@ var GranolaApp = class {
|
|
|
4232
4818
|
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
4233
4819
|
this.emitStateUpdate();
|
|
4234
4820
|
}
|
|
4821
|
+
async persistSearchIndex(entries) {
|
|
4822
|
+
this.#searchIndex = entries.map((entry) => ({
|
|
4823
|
+
...entry,
|
|
4824
|
+
folderIds: [...entry.folderIds],
|
|
4825
|
+
folderNames: [...entry.folderNames],
|
|
4826
|
+
tags: [...entry.tags]
|
|
4827
|
+
}));
|
|
4828
|
+
if (this.deps.searchIndexStore) await this.deps.searchIndexStore.writeIndex(this.#searchIndex);
|
|
4829
|
+
}
|
|
4235
4830
|
async liveMeetingSnapshot(options = {}) {
|
|
4236
4831
|
const cacheData = await this.loadCache({ forceRefresh: options.forceRefresh });
|
|
4237
4832
|
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
@@ -4449,6 +5044,35 @@ var GranolaApp = class {
|
|
|
4449
5044
|
this.setUiState({ view: "idle" });
|
|
4450
5045
|
return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
|
|
4451
5046
|
}
|
|
5047
|
+
async listAutomationRuns(options = {}) {
|
|
5048
|
+
const limit = options.limit ?? 20;
|
|
5049
|
+
const runs = this.deps.automationRunStore ? await this.deps.automationRunStore.readRuns({
|
|
5050
|
+
limit,
|
|
5051
|
+
status: options.status
|
|
5052
|
+
}) : this.#automationActionRuns.filter((run) => options.status ? run.status === options.status : true).slice(0, limit);
|
|
5053
|
+
this.setUiState({ view: "idle" });
|
|
5054
|
+
return { runs: runs.map((run) => this.cloneAutomationRun(run)) };
|
|
5055
|
+
}
|
|
5056
|
+
async resolveAutomationRun(id, decision, options = {}) {
|
|
5057
|
+
const current = (this.deps.automationRunStore ? await this.deps.automationRunStore.readRun(id) : void 0) ?? this.#automationActionRuns.find((run) => run.id === id);
|
|
5058
|
+
if (!current) throw new Error(`automation run not found: ${id}`);
|
|
5059
|
+
if (current.status !== "pending") throw new Error(`automation run is not pending: ${id}`);
|
|
5060
|
+
const finishedAt = this.nowIso();
|
|
5061
|
+
const resolved = {
|
|
5062
|
+
...this.cloneAutomationRun(current),
|
|
5063
|
+
finishedAt,
|
|
5064
|
+
meta: {
|
|
5065
|
+
...current.meta ? structuredClone(current.meta) : {},
|
|
5066
|
+
decision,
|
|
5067
|
+
note: options.note?.trim() || void 0
|
|
5068
|
+
},
|
|
5069
|
+
result: decision === "approve" ? options.note?.trim() || "Approved by user" : options.note?.trim() || "Rejected by user",
|
|
5070
|
+
status: decision === "approve" ? "completed" : "skipped"
|
|
5071
|
+
};
|
|
5072
|
+
await this.appendAutomationRuns([resolved]);
|
|
5073
|
+
this.emitStateUpdate();
|
|
5074
|
+
return this.cloneAutomationRun(resolved);
|
|
5075
|
+
}
|
|
4452
5076
|
async loginAuth(options = {}) {
|
|
4453
5077
|
const controller = this.requireAuthController();
|
|
4454
5078
|
try {
|
|
@@ -4498,6 +5122,165 @@ var GranolaApp = class {
|
|
|
4498
5122
|
throw error;
|
|
4499
5123
|
}
|
|
4500
5124
|
}
|
|
5125
|
+
async maybeReadMeetingBundleById(id, options = {}) {
|
|
5126
|
+
try {
|
|
5127
|
+
return await this.readMeetingBundleById(id, options);
|
|
5128
|
+
} catch {
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
async runAutomationNotesAction(match, action) {
|
|
5133
|
+
const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
|
|
5134
|
+
if (!bundle) return;
|
|
5135
|
+
const scope = meetingExportScope({
|
|
5136
|
+
meetingId: bundle.document.id,
|
|
5137
|
+
meetingTitle: bundle.meeting.meeting.title || bundle.document.id
|
|
5138
|
+
});
|
|
5139
|
+
const result = await this.runNotesExport({
|
|
5140
|
+
documents: [bundle.document],
|
|
5141
|
+
format: action.format ?? "markdown",
|
|
5142
|
+
outputDir: resolveExportOutputDir(action.outputDir ?? this.config.notes.output, scope, { scopedDirectory: action.scopedOutput }),
|
|
5143
|
+
scope,
|
|
5144
|
+
trackLastRun: false,
|
|
5145
|
+
updateUi: false
|
|
5146
|
+
});
|
|
5147
|
+
return {
|
|
5148
|
+
format: result.format,
|
|
5149
|
+
outputDir: result.outputDir,
|
|
5150
|
+
scope: result.scope,
|
|
5151
|
+
written: result.written
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
5154
|
+
async runAutomationTranscriptAction(match, action) {
|
|
5155
|
+
const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
|
|
5156
|
+
if (!bundle?.cacheData) return;
|
|
5157
|
+
const cacheDocument = bundle.cacheData.documents[bundle.document.id];
|
|
5158
|
+
const transcriptSegments = bundle.cacheData.transcripts[bundle.document.id];
|
|
5159
|
+
if (!cacheDocument || !transcriptSegments || transcriptSegments.length === 0) return;
|
|
5160
|
+
const scope = meetingExportScope({
|
|
5161
|
+
meetingId: bundle.document.id,
|
|
5162
|
+
meetingTitle: bundle.meeting.meeting.title || bundle.document.id
|
|
5163
|
+
});
|
|
5164
|
+
const result = await this.runTranscriptsExport({
|
|
5165
|
+
cacheData: {
|
|
5166
|
+
documents: { [bundle.document.id]: cacheDocument },
|
|
5167
|
+
transcripts: { [bundle.document.id]: transcriptSegments }
|
|
5168
|
+
},
|
|
5169
|
+
format: action.format ?? "text",
|
|
5170
|
+
outputDir: resolveExportOutputDir(action.outputDir ?? this.config.transcripts.output, scope, { scopedDirectory: action.scopedOutput }),
|
|
5171
|
+
scope,
|
|
5172
|
+
trackLastRun: false,
|
|
5173
|
+
updateUi: false
|
|
5174
|
+
});
|
|
5175
|
+
return {
|
|
5176
|
+
format: result.format,
|
|
5177
|
+
outputDir: result.outputDir,
|
|
5178
|
+
scope: result.scope,
|
|
5179
|
+
written: result.written
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
async runAutomationCommand(match, rule, action) {
|
|
5183
|
+
const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
|
|
5184
|
+
const cwd = action.cwd ? resolve(action.cwd) : process.cwd();
|
|
5185
|
+
const payload = JSON.stringify({
|
|
5186
|
+
action: {
|
|
5187
|
+
id: action.id,
|
|
5188
|
+
kind: "command",
|
|
5189
|
+
name: automationActionName(action)
|
|
5190
|
+
},
|
|
5191
|
+
authMode: this.#state.auth.mode,
|
|
5192
|
+
generatedAt: this.nowIso(),
|
|
5193
|
+
match: this.cloneAutomationMatch(match),
|
|
5194
|
+
meeting: bundle ? {
|
|
5195
|
+
document: bundle.document,
|
|
5196
|
+
meeting: bundle.meeting
|
|
5197
|
+
} : void 0,
|
|
5198
|
+
rule: {
|
|
5199
|
+
id: rule.id,
|
|
5200
|
+
name: rule.name
|
|
5201
|
+
}
|
|
5202
|
+
}, null, 2);
|
|
5203
|
+
return await new Promise((resolve, reject) => {
|
|
5204
|
+
const child = spawn(action.command, action.args ?? [], {
|
|
5205
|
+
cwd,
|
|
5206
|
+
env: {
|
|
5207
|
+
...process.env,
|
|
5208
|
+
...action.env,
|
|
5209
|
+
GRANOLA_ACTION_KIND: "command",
|
|
5210
|
+
GRANOLA_EVENT_ID: match.eventId,
|
|
5211
|
+
GRANOLA_EVENT_KIND: match.eventKind,
|
|
5212
|
+
GRANOLA_MATCH_ID: match.id,
|
|
5213
|
+
GRANOLA_MEETING_ID: match.meetingId,
|
|
5214
|
+
GRANOLA_RULE_ID: rule.id
|
|
5215
|
+
},
|
|
5216
|
+
stdio: [
|
|
5217
|
+
"pipe",
|
|
5218
|
+
"pipe",
|
|
5219
|
+
"pipe"
|
|
5220
|
+
]
|
|
5221
|
+
});
|
|
5222
|
+
const stdoutChunks = [];
|
|
5223
|
+
const stderrChunks = [];
|
|
5224
|
+
let timedOut = false;
|
|
5225
|
+
const timeoutMs = action.timeoutMs ?? this.config.notes.timeoutMs;
|
|
5226
|
+
const timeout = setTimeout(() => {
|
|
5227
|
+
timedOut = true;
|
|
5228
|
+
child.kill("SIGTERM");
|
|
5229
|
+
}, timeoutMs);
|
|
5230
|
+
child.stdout.on("data", (chunk) => {
|
|
5231
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5232
|
+
});
|
|
5233
|
+
child.stderr.on("data", (chunk) => {
|
|
5234
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5235
|
+
});
|
|
5236
|
+
child.on("error", (error) => {
|
|
5237
|
+
clearTimeout(timeout);
|
|
5238
|
+
reject(error);
|
|
5239
|
+
});
|
|
5240
|
+
child.on("close", (code) => {
|
|
5241
|
+
clearTimeout(timeout);
|
|
5242
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
5243
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
5244
|
+
if (timedOut) {
|
|
5245
|
+
reject(/* @__PURE__ */ new Error(`automation command timed out after ${timeoutMs}ms`));
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
5248
|
+
if (code !== 0) {
|
|
5249
|
+
reject(new Error(stderr || stdout || `automation command exited with status ${String(code)}`));
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
resolve({
|
|
5253
|
+
command: [action.command, ...action.args ?? []].join(" "),
|
|
5254
|
+
cwd,
|
|
5255
|
+
output: stdout || stderr || void 0
|
|
5256
|
+
});
|
|
5257
|
+
});
|
|
5258
|
+
if (action.stdin !== "none") child.stdin.write(payload);
|
|
5259
|
+
child.stdin.end();
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
async runAutomationActions(rules, matches) {
|
|
5263
|
+
const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
|
|
5264
|
+
const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
|
|
5265
|
+
const runs = [];
|
|
5266
|
+
for (const match of matches) {
|
|
5267
|
+
const rule = rulesById.get(match.ruleId);
|
|
5268
|
+
if (!rule) continue;
|
|
5269
|
+
for (const action of enabledAutomationActions(rule)) {
|
|
5270
|
+
const runId = buildAutomationActionRunId(match, action.id);
|
|
5271
|
+
if (existingRunIds.has(runId)) continue;
|
|
5272
|
+
existingRunIds.add(runId);
|
|
5273
|
+
runs.push(await executeAutomationAction(match, rule, action, {
|
|
5274
|
+
exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
|
|
5275
|
+
exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
|
|
5276
|
+
nowIso: () => this.nowIso(),
|
|
5277
|
+
runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
|
|
5278
|
+
}));
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
await this.appendAutomationRuns(runs);
|
|
5282
|
+
return runs.map((run) => this.cloneAutomationRun(run));
|
|
5283
|
+
}
|
|
4501
5284
|
async runSync(options) {
|
|
4502
5285
|
const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
|
|
4503
5286
|
this.#state.sync = {
|
|
@@ -4511,13 +5294,19 @@ var GranolaApp = class {
|
|
|
4511
5294
|
try {
|
|
4512
5295
|
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
4513
5296
|
await this.persistMeetingIndex(snapshot.meetings);
|
|
5297
|
+
await this.persistSearchIndex(buildSearchIndex(snapshot.documents, {
|
|
5298
|
+
cacheData: snapshot.cacheData,
|
|
5299
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(snapshot.folders)
|
|
5300
|
+
}));
|
|
4514
5301
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
4515
5302
|
const completedAt = this.nowIso();
|
|
4516
5303
|
const runId = this.createSyncRunId();
|
|
4517
5304
|
const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
|
|
4518
5305
|
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
4519
|
-
const
|
|
5306
|
+
const rules = await this.loadAutomationRules();
|
|
5307
|
+
const automationMatches = matchAutomationRules(rules, events, completedAt);
|
|
4520
5308
|
await this.appendAutomationMatches(automationMatches);
|
|
5309
|
+
await this.runAutomationActions(rules, automationMatches);
|
|
4521
5310
|
this.#state.sync = {
|
|
4522
5311
|
...this.#state.sync,
|
|
4523
5312
|
eventCount: this.#state.sync.eventCount + events.length,
|
|
@@ -4639,10 +5428,34 @@ var GranolaApp = class {
|
|
|
4639
5428
|
const summary = resolveFolderQuery((await this.loadFolders({ required: true }) ?? []).map((folder) => buildFolderSummary(folder)), query);
|
|
4640
5429
|
return await this.getFolder(summary.id);
|
|
4641
5430
|
}
|
|
5431
|
+
indexedMeetingsForSearch(options) {
|
|
5432
|
+
const rankedIds = meetingIdsFromSearchResults(searchSearchIndex(this.#searchIndex, options.search));
|
|
5433
|
+
const rankById = new Map(rankedIds.map((id, index) => [id, index]));
|
|
5434
|
+
return filterMeetingSummaries([...this.#meetingIndex.filter((meeting) => rankById.has(meeting.id))].sort((left, right) => {
|
|
5435
|
+
const leftRank = rankById.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
5436
|
+
const rightRank = rankById.get(right.id) ?? Number.MAX_SAFE_INTEGER;
|
|
5437
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
5438
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
5439
|
+
}), {
|
|
5440
|
+
folderId: options.folderId,
|
|
5441
|
+
limit: options.limit,
|
|
5442
|
+
sort: options.sort,
|
|
5443
|
+
updatedFrom: options.updatedFrom,
|
|
5444
|
+
updatedTo: options.updatedTo
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
4642
5447
|
async listMeetings(options = {}) {
|
|
4643
5448
|
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
4644
|
-
|
|
4645
|
-
|
|
5449
|
+
const canUseSearchIndex = Boolean(options.search?.trim()) && !options.forceRefresh && this.#searchIndex.length > 0;
|
|
5450
|
+
if (!options.forceRefresh && preferIndex && this.#meetingIndex.length > 0 && (canUseSearchIndex || !this.#documents)) {
|
|
5451
|
+
const meetings = canUseSearchIndex ? this.indexedMeetingsForSearch({
|
|
5452
|
+
folderId: options.folderId,
|
|
5453
|
+
limit: options.limit,
|
|
5454
|
+
search: options.search,
|
|
5455
|
+
sort: options.sort,
|
|
5456
|
+
updatedFrom: options.updatedFrom,
|
|
5457
|
+
updatedTo: options.updatedTo
|
|
5458
|
+
}) : filterMeetingSummaries(this.#meetingIndex, options);
|
|
4646
5459
|
this.setUiState({
|
|
4647
5460
|
folderSearch: void 0,
|
|
4648
5461
|
meetingListSource: "index",
|
|
@@ -4689,40 +5502,53 @@ var GranolaApp = class {
|
|
|
4689
5502
|
source: "live"
|
|
4690
5503
|
};
|
|
4691
5504
|
}
|
|
4692
|
-
async
|
|
5505
|
+
async readMeetingBundleById(id, options = {}) {
|
|
4693
5506
|
const documents = await this.listDocuments();
|
|
4694
5507
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4695
5508
|
const folders = await this.loadFolders();
|
|
4696
5509
|
const document = resolveMeeting(documents, id);
|
|
4697
|
-
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
4698
|
-
this.setUiState({
|
|
4699
|
-
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
4700
|
-
selectedMeetingId: document.id,
|
|
4701
|
-
view: "meeting-detail"
|
|
4702
|
-
});
|
|
4703
5510
|
return {
|
|
4704
5511
|
cacheData,
|
|
4705
5512
|
document,
|
|
4706
|
-
meeting
|
|
5513
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4707
5514
|
};
|
|
4708
5515
|
}
|
|
4709
|
-
async
|
|
5516
|
+
async readMeetingBundleByQuery(query, options = {}) {
|
|
4710
5517
|
const documents = await this.listDocuments();
|
|
4711
5518
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4712
5519
|
const folders = await this.loadFolders();
|
|
4713
5520
|
const document = resolveMeetingQuery(documents, query);
|
|
4714
|
-
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
4715
|
-
this.setUiState({
|
|
4716
|
-
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
4717
|
-
selectedMeetingId: document.id,
|
|
4718
|
-
view: "meeting-detail"
|
|
4719
|
-
});
|
|
4720
5521
|
return {
|
|
4721
5522
|
cacheData,
|
|
4722
5523
|
document,
|
|
4723
|
-
meeting
|
|
5524
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4724
5525
|
};
|
|
4725
5526
|
}
|
|
5527
|
+
async getMeeting(id, options = {}) {
|
|
5528
|
+
const bundle = await this.readMeetingBundleById(id, options);
|
|
5529
|
+
this.setUiState({
|
|
5530
|
+
selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
|
|
5531
|
+
selectedMeetingId: bundle.document.id,
|
|
5532
|
+
view: "meeting-detail"
|
|
5533
|
+
});
|
|
5534
|
+
return bundle;
|
|
5535
|
+
}
|
|
5536
|
+
async findMeeting(query, options = {}) {
|
|
5537
|
+
let bundle;
|
|
5538
|
+
try {
|
|
5539
|
+
bundle = await this.readMeetingBundleByQuery(query, options);
|
|
5540
|
+
} catch (error) {
|
|
5541
|
+
const fallbackId = meetingIdsFromSearchResults(searchSearchIndex(this.#searchIndex, query))[0];
|
|
5542
|
+
if (!fallbackId) throw error;
|
|
5543
|
+
bundle = await this.readMeetingBundleById(fallbackId, options);
|
|
5544
|
+
}
|
|
5545
|
+
this.setUiState({
|
|
5546
|
+
selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
|
|
5547
|
+
selectedMeetingId: bundle.document.id,
|
|
5548
|
+
view: "meeting-detail"
|
|
5549
|
+
});
|
|
5550
|
+
return bundle;
|
|
5551
|
+
}
|
|
4726
5552
|
async listExportJobs(options = {}) {
|
|
4727
5553
|
const limit = options.limit ?? 20;
|
|
4728
5554
|
const jobs = this.#state.exports.jobs.slice(0, limit).map((job) => cloneExportJob(job));
|
|
@@ -4758,7 +5584,7 @@ var GranolaApp = class {
|
|
|
4758
5584
|
await this.failExportJob(job, error);
|
|
4759
5585
|
throw error;
|
|
4760
5586
|
}
|
|
4761
|
-
this.#state.exports.notes = {
|
|
5587
|
+
if (options.trackLastRun !== false) this.#state.exports.notes = {
|
|
4762
5588
|
format: options.format,
|
|
4763
5589
|
itemCount: options.documents.length,
|
|
4764
5590
|
jobId: job.id,
|
|
@@ -4768,7 +5594,7 @@ var GranolaApp = class {
|
|
|
4768
5594
|
written
|
|
4769
5595
|
};
|
|
4770
5596
|
this.emitStateUpdate();
|
|
4771
|
-
this.setUiState({
|
|
5597
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4772
5598
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4773
5599
|
view: "notes-export"
|
|
4774
5600
|
});
|
|
@@ -4816,7 +5642,7 @@ var GranolaApp = class {
|
|
|
4816
5642
|
await this.failExportJob(job, error);
|
|
4817
5643
|
throw error;
|
|
4818
5644
|
}
|
|
4819
|
-
this.#state.exports.transcripts = {
|
|
5645
|
+
if (options.trackLastRun !== false) this.#state.exports.transcripts = {
|
|
4820
5646
|
format: options.format,
|
|
4821
5647
|
itemCount: count,
|
|
4822
5648
|
jobId: job.id,
|
|
@@ -4826,7 +5652,7 @@ var GranolaApp = class {
|
|
|
4826
5652
|
written
|
|
4827
5653
|
};
|
|
4828
5654
|
this.emitStateUpdate();
|
|
4829
|
-
this.setUiState({
|
|
5655
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4830
5656
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4831
5657
|
view: "transcripts-export"
|
|
4832
5658
|
});
|
|
@@ -4870,6 +5696,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4870
5696
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
4871
5697
|
const automationMatchStore = createDefaultAutomationMatchStore();
|
|
4872
5698
|
const automationMatches = await automationMatchStore.readMatches(0);
|
|
5699
|
+
const automationRunStore = createDefaultAutomationRunStore();
|
|
5700
|
+
const automationRuns = await automationRunStore.readRuns({ limit: 0 });
|
|
4873
5701
|
const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
|
|
4874
5702
|
const automationRules = await automationRuleStore.readRules();
|
|
4875
5703
|
const authController = createDefaultGranolaAuthController(config);
|
|
@@ -4877,6 +5705,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4877
5705
|
const exportJobs = await exportJobStore.readJobs();
|
|
4878
5706
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4879
5707
|
const meetingIndex = await meetingIndexStore.readIndex();
|
|
5708
|
+
const searchIndexStore = createDefaultSearchIndexStore();
|
|
5709
|
+
const searchIndex = await searchIndexStore.readIndex();
|
|
4880
5710
|
const syncEventStore = createDefaultSyncEventStore();
|
|
4881
5711
|
const syncStateStore = createDefaultSyncStateStore();
|
|
4882
5712
|
const syncState = await syncStateStore.readState();
|
|
@@ -4885,6 +5715,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4885
5715
|
authController,
|
|
4886
5716
|
automationMatches,
|
|
4887
5717
|
automationMatchStore,
|
|
5718
|
+
automationRunStore,
|
|
5719
|
+
automationRuns,
|
|
4888
5720
|
automationRules,
|
|
4889
5721
|
automationRuleStore,
|
|
4890
5722
|
cacheLoader: loadOptionalGranolaCache,
|
|
@@ -4894,6 +5726,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4894
5726
|
meetingIndex,
|
|
4895
5727
|
meetingIndexStore,
|
|
4896
5728
|
now: options.now,
|
|
5729
|
+
searchIndex,
|
|
5730
|
+
searchIndexStore,
|
|
4897
5731
|
syncEventStore,
|
|
4898
5732
|
syncState,
|
|
4899
5733
|
syncStateStore
|
|
@@ -5046,15 +5880,20 @@ function automationHelp() {
|
|
|
5046
5880
|
return `Granola automation
|
|
5047
5881
|
|
|
5048
5882
|
Usage:
|
|
5049
|
-
granola automation <rules|matches> [options]
|
|
5883
|
+
granola automation <rules|matches|runs|approve|reject> [options]
|
|
5050
5884
|
|
|
5051
5885
|
Subcommands:
|
|
5052
5886
|
rules List configured automation rules
|
|
5053
5887
|
matches Show recent rule matches from sync events
|
|
5888
|
+
runs Show recent automation action runs
|
|
5889
|
+
approve <id> Approve a pending ask-user action run
|
|
5890
|
+
reject <id> Reject a pending ask-user action run
|
|
5054
5891
|
|
|
5055
5892
|
Options:
|
|
5056
5893
|
--format <value> text, json, yaml (default: text)
|
|
5057
5894
|
--limit <n> Number of matches to show (default: 20)
|
|
5895
|
+
--status <value> completed, failed, pending, skipped
|
|
5896
|
+
--note <text> Note to store with approve/reject decisions
|
|
5058
5897
|
--rules <path> Path to automation rules JSON
|
|
5059
5898
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5060
5899
|
--supabase <path> Path to supabase.json
|
|
@@ -5063,7 +5902,7 @@ Options:
|
|
|
5063
5902
|
-h, --help Show help
|
|
5064
5903
|
`;
|
|
5065
5904
|
}
|
|
5066
|
-
function resolveFormat(value) {
|
|
5905
|
+
function resolveFormat$1(value) {
|
|
5067
5906
|
switch (value) {
|
|
5068
5907
|
case void 0: return "text";
|
|
5069
5908
|
case "json":
|
|
@@ -5072,7 +5911,7 @@ function resolveFormat(value) {
|
|
|
5072
5911
|
default: throw new Error("invalid automation format: expected text, json, or yaml");
|
|
5073
5912
|
}
|
|
5074
5913
|
}
|
|
5075
|
-
function parseLimit$
|
|
5914
|
+
function parseLimit$4(value) {
|
|
5076
5915
|
if (value === void 0) return 20;
|
|
5077
5916
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid automation limit: expected a positive integer");
|
|
5078
5917
|
return Number(value);
|
|
@@ -5081,7 +5920,7 @@ function renderRules(rules, format) {
|
|
|
5081
5920
|
if (format === "json") return toJson({ rules });
|
|
5082
5921
|
if (format === "yaml") return toYaml({ rules });
|
|
5083
5922
|
if (rules.length === 0) return "No automation rules configured\n";
|
|
5084
|
-
return `${["ID ENABLED EVENTS FILTERS", ...rules.map((rule) => {
|
|
5923
|
+
return `${["ID ENABLED EVENTS ACTIONS FILTERS", ...rules.map((rule) => {
|
|
5085
5924
|
const filters = [
|
|
5086
5925
|
rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
|
|
5087
5926
|
rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
|
|
@@ -5089,7 +5928,7 @@ function renderRules(rules, format) {
|
|
|
5089
5928
|
rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
|
|
5090
5929
|
rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
|
|
5091
5930
|
].filter(Boolean).join(" ");
|
|
5092
|
-
return `${rule.id.padEnd(23).slice(0, 23)} ${(rule.enabled === false ? "no" : "yes").padEnd(8)} ${(rule.when.eventKinds?.join(",") || "any").padEnd(22).slice(0, 22)} ${filters || "-"}`;
|
|
5931
|
+
return `${rule.id.padEnd(23).slice(0, 23)} ${(rule.enabled === false ? "no" : "yes").padEnd(8)} ${(rule.when.eventKinds?.join(",") || "any").padEnd(22).slice(0, 22)} ${String(rule.actions?.length ?? 0).padEnd(8)} ${filters || "-"}`;
|
|
5093
5932
|
})].join("\n")}\n`;
|
|
5094
5933
|
}
|
|
5095
5934
|
function renderMatches(matches, format) {
|
|
@@ -5100,19 +5939,39 @@ function renderMatches(matches, format) {
|
|
|
5100
5939
|
return `${match.matchedAt.slice(0, 19).padEnd(21)} ${match.ruleName.padEnd(23).slice(0, 23)} ${match.eventKind.padEnd(18).slice(0, 18)} ${match.title} (${match.meetingId})`;
|
|
5101
5940
|
})].join("\n")}\n`;
|
|
5102
5941
|
}
|
|
5942
|
+
function parseRunStatus(value) {
|
|
5943
|
+
switch (value) {
|
|
5944
|
+
case void 0: return;
|
|
5945
|
+
case "completed":
|
|
5946
|
+
case "failed":
|
|
5947
|
+
case "pending":
|
|
5948
|
+
case "skipped": return value;
|
|
5949
|
+
default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
function renderRuns(runs, format) {
|
|
5953
|
+
if (format === "json") return toJson({ runs });
|
|
5954
|
+
if (format === "yaml") return toYaml({ runs });
|
|
5955
|
+
if (runs.length === 0) return "No automation runs yet\n";
|
|
5956
|
+
return `${["STARTED AT STATUS ACTION RULE TITLE", ...runs.map((run) => {
|
|
5957
|
+
return `${run.startedAt.slice(0, 19).padEnd(21)} ${run.status.padEnd(11).slice(0, 11)} ${run.actionName.padEnd(23).slice(0, 23)} ${run.ruleName.padEnd(23).slice(0, 23)} ${[run.title, run.result || run.error].filter(Boolean).join(" - ")}`;
|
|
5958
|
+
})].join("\n")}\n`;
|
|
5959
|
+
}
|
|
5103
5960
|
const automationCommand = {
|
|
5104
5961
|
description: "Inspect automation rules and rule matches",
|
|
5105
5962
|
flags: {
|
|
5106
5963
|
format: { type: "string" },
|
|
5107
5964
|
help: { type: "boolean" },
|
|
5108
5965
|
limit: { type: "string" },
|
|
5966
|
+
note: { type: "string" },
|
|
5967
|
+
status: { type: "string" },
|
|
5109
5968
|
timeout: { type: "string" }
|
|
5110
5969
|
},
|
|
5111
5970
|
help: automationHelp,
|
|
5112
5971
|
name: "automation",
|
|
5113
5972
|
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
5114
5973
|
const [action] = commandArgs;
|
|
5115
|
-
const format = resolveFormat(commandFlags.format);
|
|
5974
|
+
const format = resolveFormat$1(commandFlags.format);
|
|
5116
5975
|
const config = await loadConfig({
|
|
5117
5976
|
globalFlags,
|
|
5118
5977
|
subcommandFlags: commandFlags
|
|
@@ -5127,14 +5986,30 @@ const automationCommand = {
|
|
|
5127
5986
|
return 0;
|
|
5128
5987
|
}
|
|
5129
5988
|
case "matches": {
|
|
5130
|
-
const result = await app.listAutomationMatches({ limit: parseLimit$
|
|
5989
|
+
const result = await app.listAutomationMatches({ limit: parseLimit$4(commandFlags.limit) });
|
|
5131
5990
|
console.log(renderMatches(result.matches, format).trimEnd());
|
|
5132
5991
|
return 0;
|
|
5133
5992
|
}
|
|
5993
|
+
case "runs": {
|
|
5994
|
+
const result = await app.listAutomationRuns({
|
|
5995
|
+
limit: parseLimit$4(commandFlags.limit),
|
|
5996
|
+
status: parseRunStatus(commandFlags.status)
|
|
5997
|
+
});
|
|
5998
|
+
console.log(renderRuns(result.runs, format).trimEnd());
|
|
5999
|
+
return 0;
|
|
6000
|
+
}
|
|
6001
|
+
case "approve":
|
|
6002
|
+
case "reject": {
|
|
6003
|
+
const id = commandArgs[1]?.trim();
|
|
6004
|
+
if (!id) throw new Error(`missing automation run id for ${action}`);
|
|
6005
|
+
const run = await app.resolveAutomationRun(id, action, { note: typeof commandFlags.note === "string" ? commandFlags.note : void 0 });
|
|
6006
|
+
console.log(`${action === "approve" ? "Approved" : "Rejected"} ${run.actionName} for ${run.title} (${run.id})`);
|
|
6007
|
+
return 0;
|
|
6008
|
+
}
|
|
5134
6009
|
case void 0:
|
|
5135
6010
|
console.log(automationHelp());
|
|
5136
6011
|
return 1;
|
|
5137
|
-
default: throw new Error("invalid automation command: expected rules or
|
|
6012
|
+
default: throw new Error("invalid automation command: expected rules, matches, runs, approve, or reject");
|
|
5138
6013
|
}
|
|
5139
6014
|
}
|
|
5140
6015
|
};
|
|
@@ -5279,7 +6154,7 @@ function resolveListFormat$1(value) {
|
|
|
5279
6154
|
default: throw new Error("invalid exports format: expected text, json, or yaml");
|
|
5280
6155
|
}
|
|
5281
6156
|
}
|
|
5282
|
-
function parseLimit$
|
|
6157
|
+
function parseLimit$3(value) {
|
|
5283
6158
|
if (value === void 0) return 20;
|
|
5284
6159
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid exports limit: expected a positive integer");
|
|
5285
6160
|
const limit = Number(value);
|
|
@@ -5321,7 +6196,7 @@ const exportsCommand = {
|
|
|
5321
6196
|
};
|
|
5322
6197
|
async function list$2(commandFlags, globalFlags) {
|
|
5323
6198
|
const format = resolveListFormat$1(commandFlags.format);
|
|
5324
|
-
const limit = parseLimit$
|
|
6199
|
+
const limit = parseLimit$3(commandFlags.limit);
|
|
5325
6200
|
const config = await loadConfig({
|
|
5326
6201
|
globalFlags,
|
|
5327
6202
|
subcommandFlags: commandFlags
|
|
@@ -5382,7 +6257,7 @@ function resolveFolderListFormat(value) {
|
|
|
5382
6257
|
function resolveFolderDetailFormat(value) {
|
|
5383
6258
|
return resolveFolderListFormat(value);
|
|
5384
6259
|
}
|
|
5385
|
-
function parseLimit$
|
|
6260
|
+
function parseLimit$2(value) {
|
|
5386
6261
|
if (value === void 0) return 20;
|
|
5387
6262
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid folder limit: expected a positive integer");
|
|
5388
6263
|
const limit = Number(value);
|
|
@@ -5416,7 +6291,7 @@ const folderCommand = {
|
|
|
5416
6291
|
};
|
|
5417
6292
|
async function list$1(commandFlags, globalFlags) {
|
|
5418
6293
|
const format = resolveFolderListFormat(commandFlags.format);
|
|
5419
|
-
const limit = parseLimit$
|
|
6294
|
+
const limit = parseLimit$2(commandFlags.limit);
|
|
5420
6295
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5421
6296
|
const config = await loadConfig({
|
|
5422
6297
|
globalFlags,
|
|
@@ -7089,6 +7964,7 @@ var granolaTransportPaths = {
|
|
|
7089
7964
|
authUnlock: "/auth/unlock",
|
|
7090
7965
|
automationMatches: "/automation/matches",
|
|
7091
7966
|
automationRules: "/automation/rules",
|
|
7967
|
+
automationRuns: "/automation/runs",
|
|
7092
7968
|
events: "/events",
|
|
7093
7969
|
exportJobs: "/exports/jobs",
|
|
7094
7970
|
exportNotes: "/exports/notes",
|
|
@@ -7148,6 +8024,15 @@ function granolaFoldersPath(options = {}) {
|
|
|
7148
8024
|
function granolaExportJobsPath(options = {}) {
|
|
7149
8025
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
7150
8026
|
}
|
|
8027
|
+
function granolaAutomationRunsPath(options = {}) {
|
|
8028
|
+
return appendSearchParams(granolaTransportPaths.automationRuns, {
|
|
8029
|
+
limit: options.limit,
|
|
8030
|
+
status: options.status
|
|
8031
|
+
});
|
|
8032
|
+
}
|
|
8033
|
+
function granolaAutomationRunDecisionPath(id, decision) {
|
|
8034
|
+
return \`\${granolaTransportPaths.automationRuns}/\${encodeURIComponent(id)}/\${decision}\`;
|
|
8035
|
+
}
|
|
7151
8036
|
function granolaExportJobRerunPath(id) {
|
|
7152
8037
|
return \`\${granolaTransportPaths.exportJobs}/\${encodeURIComponent(id)}/rerun\`;
|
|
7153
8038
|
}
|
|
@@ -7329,6 +8214,16 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
7329
8214
|
const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
|
|
7330
8215
|
return await this.requestJson(path);
|
|
7331
8216
|
}
|
|
8217
|
+
async listAutomationRuns(options = {}) {
|
|
8218
|
+
return await this.requestJson(granolaAutomationRunsPath(options));
|
|
8219
|
+
}
|
|
8220
|
+
async resolveAutomationRun(id, decision, options = {}) {
|
|
8221
|
+
return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
|
|
8222
|
+
body: JSON.stringify(options),
|
|
8223
|
+
headers: { "content-type": "application/json" },
|
|
8224
|
+
method: "POST"
|
|
8225
|
+
});
|
|
8226
|
+
}
|
|
7332
8227
|
async inspectSync() {
|
|
7333
8228
|
return cloneValue(_classPrivateFieldGet2(_state, this).sync);
|
|
7334
8229
|
}
|
|
@@ -7547,7 +8442,7 @@ function nextWorkspaceTab(currentTab, key) {
|
|
|
7547
8442
|
//#endregion
|
|
7548
8443
|
//#region src/web-app/components.tsx
|
|
7549
8444
|
/** @jsxImportSource solid-js */
|
|
7550
|
-
var _tmpl$$1 = /* @__PURE__ */ template(\`<section class=hero><h1>Granola Toolkit</h1><p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p><input class=search placeholder="Search meetings, ids, or tags"><div class="field-row field-row--inline"><label><span class=field-label>Sort</span><select class=select><option value=updated-desc>Newest first</option><option value=updated-asc>Oldest first</option><option value=title-asc>Title A-Z</option><option value=title-desc>Title Z-A</option></select></label><label><span class=field-label>Updated From</span><input class=field-input type=date></label></div><label class=field-row><span class=field-label>Updated To</span><input class=field-input type=date>\`), _tmpl$2 = /* @__PURE__ */ template(\`<section class=toolbar><div><p>Meetings are loaded from the shared server state so this view can stay aligned with the terminal UI and sync loop.</p></div><div class=toolbar-form><input class=field-input placeholder="Quick open by id or title"><button class="button button--secondary"type=button>Open\`), _tmpl$3 = /* @__PURE__ */ template(\`<div class="folder-empty folder-empty--error">\`), _tmpl$4 = /* @__PURE__ */ template(\`<section class=folder-panel><div class=folder-panel__head><h2>Folders</h2><p>Pick a folder to scope the meeting browser, or stay on All meetings.</p></div><div class=folder-list>\`), _tmpl$5 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title>All meetings</span><span class=folder-row__meta>Browse the full meeting list.\`), _tmpl$6 = /* @__PURE__ */ template(\`<div class=folder-empty>No folders found.\`), _tmpl$7 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title></span><span class=folder-row__meta>\`), _tmpl$8 = /* @__PURE__ */ template(\`<div class="meeting-empty meeting-empty--error">\`), _tmpl$9 = /* @__PURE__ */ template(\`<section class=meeting-list>\`), _tmpl$0 = /* @__PURE__ */ template(\`<div class=meeting-empty>\`), _tmpl$1 = /* @__PURE__ */ template(\`<button class=meeting-row type=button><span class=meeting-row__title></span><span class=meeting-row__meta></span><span class=meeting-row__meta>\`), _tmpl$10 = /* @__PURE__ */ template(\`<section class=detail-head><div><h2>Meeting Workspace</h2></div><div class=state-badge>\`), _tmpl$11 = /* @__PURE__ */ template(\`<p>Waiting for server state…\`), _tmpl$12 = /* @__PURE__ */ template(\`<div class=status-grid><div><span class=status-label>Surface</span><strong></strong></div><div><span class=status-label>View</span><strong></strong></div><div><span class=status-label>Auth</span><strong></strong></div><div><span class=status-label>Sync</span><strong></strong></div><div><span class=status-label>Documents</span><strong></strong></div><div><span class=status-label>Folders</span><strong></strong></div><div><span class=status-label>Cache</span><strong></strong></div><div><span class=status-label>Index</span><strong>\`), _tmpl$13 = /* @__PURE__ */ template(\`<section class=security-panel><div class=security-panel__head><h3>Server Access</h3><p>This server is locked with a password. Unlock it to load meetings and live state.</p></div><div class=security-panel__body><input class=field-input placeholder="Server password"type=password><div class=toolbar-actions><button class="button button--primary"type=button>Unlock</button><button class="button button--secondary"type=button>Lock\`), _tmpl$14 = /* @__PURE__ */ template(\`<section class=auth-panel><div class=auth-panel__head><h3>Auth Session</h3><p>Inspect, refresh, and switch between API key, stored session, and <code>supabase.json</code>.</p></div><div class=auth-panel__body>\`), _tmpl$15 = /* @__PURE__ */ template(\`<div class=auth-card><div class=auth-card__meta>Auth state unavailable.\`), _tmpl$16 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Client ID: \`), _tmpl$17 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Sign-in method: \`), _tmpl$18 = /* @__PURE__ */ template(\`<div class=auth-card__meta>supabase path: \`), _tmpl$19 = /* @__PURE__ */ template(\`<div class="auth-card__meta auth-card__error">\`), _tmpl$20 = /* @__PURE__ */ template(\`<div class=auth-card><div class=status-grid><div><span class=status-label>Active</span><strong></strong></div><div><span class=status-label>API key</span><strong></strong></div><div><span class=status-label>Stored</span><strong></strong></div><div><span class=status-label>supabase.json</span><strong></strong></div><div><span class=status-label>Refresh</span><strong></strong></div></div><div class=auth-card__meta>Store a Granola Personal API key here or use <code>granola auth login --api-key <token></code>.</div><div class=auth-card__actions><input class=input placeholder=grn_... type=password><button class="button button--secondary"type=button>Save API key</button><button class="button button--secondary"type=button>Import desktop session</button><button class="button button--secondary"type=button>Refresh stored session</button><button class="button button--secondary"type=button>Use API key</button><button class="button button--secondary"type=button>Use stored session</button><button class="button button--secondary"type=button>Use supabase.json</button><button class="button button--secondary"type=button>Sign out\`), _tmpl$21 = /* @__PURE__ */ template(\`<section class=jobs-panel><div class=jobs-panel__head><h3>Recent Export Jobs</h3><p>Tracked across CLI and web runs.</p></div><div class=jobs-list>\`), _tmpl$22 = /* @__PURE__ */ template(\`<div class=job-empty>No export jobs yet.\`), _tmpl$23 = /* @__PURE__ */ template(\`<div class=job-card__meta>\`), _tmpl$24 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Rerun\`), _tmpl$25 = /* @__PURE__ */ template(\`<article class=job-card><div class=job-card__head><div><div class=job-card__title> export</div><div class=job-card__meta></div></div><div class=job-card__status></div></div><div class=job-card__meta></div><div class=job-card__meta>Started: </div><div class=job-card__meta>Output: </div><div class=job-card__actions>\`), _tmpl$26 = /* @__PURE__ */ template(\`<nav class=workspace-tabs><span class=workspace-hint>1-4 switch tabs, [ and ] cycle\`), _tmpl$
|
|
8445
|
+
var _tmpl$$1 = /* @__PURE__ */ template(\`<section class=hero><h1>Granola Toolkit</h1><p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p><input class=search placeholder="Search meetings, ids, or tags"><div class="field-row field-row--inline"><label><span class=field-label>Sort</span><select class=select><option value=updated-desc>Newest first</option><option value=updated-asc>Oldest first</option><option value=title-asc>Title A-Z</option><option value=title-desc>Title Z-A</option></select></label><label><span class=field-label>Updated From</span><input class=field-input type=date></label></div><label class=field-row><span class=field-label>Updated To</span><input class=field-input type=date>\`), _tmpl$2 = /* @__PURE__ */ template(\`<section class=toolbar><div><p>Meetings are loaded from the shared server state so this view can stay aligned with the terminal UI and sync loop.</p></div><div class=toolbar-form><input class=field-input placeholder="Quick open by id or title"><button class="button button--secondary"type=button>Open\`), _tmpl$3 = /* @__PURE__ */ template(\`<div class="folder-empty folder-empty--error">\`), _tmpl$4 = /* @__PURE__ */ template(\`<section class=folder-panel><div class=folder-panel__head><h2>Folders</h2><p>Pick a folder to scope the meeting browser, or stay on All meetings.</p></div><div class=folder-list>\`), _tmpl$5 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title>All meetings</span><span class=folder-row__meta>Browse the full meeting list.\`), _tmpl$6 = /* @__PURE__ */ template(\`<div class=folder-empty>No folders found.\`), _tmpl$7 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title></span><span class=folder-row__meta>\`), _tmpl$8 = /* @__PURE__ */ template(\`<div class="meeting-empty meeting-empty--error">\`), _tmpl$9 = /* @__PURE__ */ template(\`<section class=meeting-list>\`), _tmpl$0 = /* @__PURE__ */ template(\`<div class=meeting-empty>\`), _tmpl$1 = /* @__PURE__ */ template(\`<button class=meeting-row type=button><span class=meeting-row__title></span><span class=meeting-row__meta></span><span class=meeting-row__meta>\`), _tmpl$10 = /* @__PURE__ */ template(\`<section class=detail-head><div><h2>Meeting Workspace</h2></div><div class=state-badge>\`), _tmpl$11 = /* @__PURE__ */ template(\`<p>Waiting for server state…\`), _tmpl$12 = /* @__PURE__ */ template(\`<div class=status-grid><div><span class=status-label>Surface</span><strong></strong></div><div><span class=status-label>View</span><strong></strong></div><div><span class=status-label>Auth</span><strong></strong></div><div><span class=status-label>Sync</span><strong></strong></div><div><span class=status-label>Documents</span><strong></strong></div><div><span class=status-label>Folders</span><strong></strong></div><div><span class=status-label>Cache</span><strong></strong></div><div><span class=status-label>Index</span><strong></strong></div><div><span class=status-label>Automation</span><strong>\`), _tmpl$13 = /* @__PURE__ */ template(\`<section class=security-panel><div class=security-panel__head><h3>Server Access</h3><p>This server is locked with a password. Unlock it to load meetings and live state.</p></div><div class=security-panel__body><input class=field-input placeholder="Server password"type=password><div class=toolbar-actions><button class="button button--primary"type=button>Unlock</button><button class="button button--secondary"type=button>Lock\`), _tmpl$14 = /* @__PURE__ */ template(\`<section class=auth-panel><div class=auth-panel__head><h3>Auth Session</h3><p>Inspect, refresh, and switch between API key, stored session, and <code>supabase.json</code>.</p></div><div class=auth-panel__body>\`), _tmpl$15 = /* @__PURE__ */ template(\`<div class=auth-card><div class=auth-card__meta>Auth state unavailable.\`), _tmpl$16 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Client ID: \`), _tmpl$17 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Sign-in method: \`), _tmpl$18 = /* @__PURE__ */ template(\`<div class=auth-card__meta>supabase path: \`), _tmpl$19 = /* @__PURE__ */ template(\`<div class="auth-card__meta auth-card__error">\`), _tmpl$20 = /* @__PURE__ */ template(\`<div class=auth-card><div class=status-grid><div><span class=status-label>Active</span><strong></strong></div><div><span class=status-label>API key</span><strong></strong></div><div><span class=status-label>Stored</span><strong></strong></div><div><span class=status-label>supabase.json</span><strong></strong></div><div><span class=status-label>Refresh</span><strong></strong></div></div><div class=auth-card__meta>Store a Granola Personal API key here or use <code>granola auth login --api-key <token></code>.</div><div class=auth-card__actions><input class=input placeholder=grn_... type=password><button class="button button--secondary"type=button>Save API key</button><button class="button button--secondary"type=button>Import desktop session</button><button class="button button--secondary"type=button>Refresh stored session</button><button class="button button--secondary"type=button>Use API key</button><button class="button button--secondary"type=button>Use stored session</button><button class="button button--secondary"type=button>Use supabase.json</button><button class="button button--secondary"type=button>Sign out\`), _tmpl$21 = /* @__PURE__ */ template(\`<section class=jobs-panel><div class=jobs-panel__head><h3>Recent Export Jobs</h3><p>Tracked across CLI and web runs.</p></div><div class=jobs-list>\`), _tmpl$22 = /* @__PURE__ */ template(\`<div class=job-empty>No export jobs yet.\`), _tmpl$23 = /* @__PURE__ */ template(\`<div class=job-card__meta>\`), _tmpl$24 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Rerun\`), _tmpl$25 = /* @__PURE__ */ template(\`<article class=job-card><div class=job-card__head><div><div class=job-card__title> export</div><div class=job-card__meta></div></div><div class=job-card__status></div></div><div class=job-card__meta></div><div class=job-card__meta>Started: </div><div class=job-card__meta>Output: </div><div class=job-card__actions>\`), _tmpl$26 = /* @__PURE__ */ template(\`<section class=jobs-panel><div class=jobs-panel__head><h3>Automation Runs</h3><p>Recent action runs triggered by durable sync events.</p></div><div class=jobs-list>\`), _tmpl$27 = /* @__PURE__ */ template(\`<div class=job-empty>No automation runs yet.\`), _tmpl$28 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Approve\`), _tmpl$29 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Reject\`), _tmpl$30 = /* @__PURE__ */ template(\`<article class=job-card><div class=job-card__head><div><div class=job-card__title></div><div class=job-card__meta></div></div><div class=job-card__status></div></div><div class=job-card__meta></div><div class=job-card__meta></div><div class=job-card__actions>\`), _tmpl$31 = /* @__PURE__ */ template(\`<nav class=workspace-tabs><span class=workspace-hint>1-4 switch tabs, [ and ] cycle\`), _tmpl$32 = /* @__PURE__ */ template(\`<button class=workspace-tab type=button>\`), _tmpl$33 = /* @__PURE__ */ template(\`<div class=empty>\`), _tmpl$34 = /* @__PURE__ */ template(\`<div class=detail-meta><div class=detail-chip></div><div class=detail-chip></div><div class=detail-chip>\`), _tmpl$35 = /* @__PURE__ */ template(\`<div class=detail-body><div class=workspace-grid><aside class="detail-section workspace-sidebar"><h2>Meeting Metadata</h2><pre class=detail-pre></pre></aside><section class="detail-section workspace-main"><h2></h2><pre class=detail-pre>\`);
|
|
7551
8446
|
function authModeLabel(mode) {
|
|
7552
8447
|
switch (mode) {
|
|
7553
8448
|
case "api-key": return "API key";
|
|
@@ -7764,7 +8659,7 @@ function AppStatePanel(props) {
|
|
|
7764
8659
|
return props.appState;
|
|
7765
8660
|
},
|
|
7766
8661
|
children: (appState) => (() => {
|
|
7767
|
-
var _el$39 = _tmpl$12(), _el$40 = _el$39.firstChild, _el$42 = _el$40.firstChild.nextSibling, _el$43 = _el$40.nextSibling, _el$45 = _el$43.firstChild.nextSibling, _el$46 = _el$43.nextSibling, _el$48 = _el$46.firstChild.nextSibling, _el$49 = _el$46.nextSibling, _el$51 = _el$49.firstChild.nextSibling, _el$52 = _el$49.nextSibling, _el$54 = _el$52.firstChild.nextSibling, _el$55 = _el$52.nextSibling, _el$57 = _el$55.firstChild.nextSibling, _el$58 = _el$55.nextSibling, _el$60 = _el$58.firstChild.nextSibling, _el$
|
|
8662
|
+
var _el$39 = _tmpl$12(), _el$40 = _el$39.firstChild, _el$42 = _el$40.firstChild.nextSibling, _el$43 = _el$40.nextSibling, _el$45 = _el$43.firstChild.nextSibling, _el$46 = _el$43.nextSibling, _el$48 = _el$46.firstChild.nextSibling, _el$49 = _el$46.nextSibling, _el$51 = _el$49.firstChild.nextSibling, _el$52 = _el$49.nextSibling, _el$54 = _el$52.firstChild.nextSibling, _el$55 = _el$52.nextSibling, _el$57 = _el$55.firstChild.nextSibling, _el$58 = _el$55.nextSibling, _el$60 = _el$58.firstChild.nextSibling, _el$61 = _el$58.nextSibling, _el$63 = _el$61.firstChild.nextSibling, _el$66 = _el$61.nextSibling.firstChild.nextSibling;
|
|
7768
8663
|
insert(_el$42, () => appState().ui.surface);
|
|
7769
8664
|
insert(_el$45, () => appState().ui.view);
|
|
7770
8665
|
insert(_el$48, authStatus);
|
|
@@ -7785,6 +8680,7 @@ function AppStatePanel(props) {
|
|
|
7785
8680
|
var _c$7 = memo(() => !!appState().index.loaded);
|
|
7786
8681
|
return () => _c$7() ? \`\${appState().index.meetingCount} meetings\` : appState().index.available ? "available" : "not built";
|
|
7787
8682
|
})());
|
|
8683
|
+
insert(_el$66, () => \`\${appState().automation.runCount} runs / \${appState().automation.pendingRunCount} pending\`);
|
|
7788
8684
|
return _el$39;
|
|
7789
8685
|
})()
|
|
7790
8686
|
}), null);
|
|
@@ -7799,27 +8695,27 @@ function SecurityPanel(props) {
|
|
|
7799
8695
|
return props.visible;
|
|
7800
8696
|
},
|
|
7801
8697
|
get children() {
|
|
7802
|
-
var _el$
|
|
7803
|
-
_el$
|
|
8698
|
+
var _el$67 = _tmpl$13(), _el$70 = _el$67.firstChild.nextSibling.firstChild, _el$72 = _el$70.nextSibling.firstChild, _el$73 = _el$72.nextSibling;
|
|
8699
|
+
_el$70.$$keydown = (event) => {
|
|
7804
8700
|
if (event.key === "Enter") {
|
|
7805
8701
|
event.preventDefault();
|
|
7806
8702
|
props.onUnlock();
|
|
7807
8703
|
}
|
|
7808
8704
|
};
|
|
7809
|
-
_el$
|
|
8705
|
+
_el$70.$$input = (event) => {
|
|
7810
8706
|
props.onPasswordChange(event.currentTarget.value);
|
|
7811
8707
|
};
|
|
7812
|
-
addEventListener(_el$
|
|
7813
|
-
addEventListener(_el$
|
|
7814
|
-
createRenderEffect(() => _el$
|
|
7815
|
-
return _el$
|
|
8708
|
+
addEventListener(_el$72, "click", props.onUnlock, true);
|
|
8709
|
+
addEventListener(_el$73, "click", props.onLock, true);
|
|
8710
|
+
createRenderEffect(() => _el$70.value = props.password);
|
|
8711
|
+
return _el$67;
|
|
7816
8712
|
}
|
|
7817
8713
|
});
|
|
7818
8714
|
}
|
|
7819
8715
|
function AuthPanel(props) {
|
|
7820
8716
|
return (() => {
|
|
7821
|
-
var _el$
|
|
7822
|
-
insert(_el$
|
|
8717
|
+
var _el$74 = _tmpl$14(), _el$76 = _el$74.firstChild.nextSibling;
|
|
8718
|
+
insert(_el$76, createComponent(Show, {
|
|
7823
8719
|
get fallback() {
|
|
7824
8720
|
return _tmpl$15();
|
|
7825
8721
|
},
|
|
@@ -7827,79 +8723,79 @@ function AuthPanel(props) {
|
|
|
7827
8723
|
return props.auth;
|
|
7828
8724
|
},
|
|
7829
8725
|
children: (auth) => (() => {
|
|
7830
|
-
var _el$
|
|
7831
|
-
insert(_el$
|
|
7832
|
-
insert(_el$
|
|
7833
|
-
insert(_el$
|
|
7834
|
-
insert(_el$
|
|
7835
|
-
insert(_el$
|
|
7836
|
-
insert(_el$
|
|
8726
|
+
var _el$78 = _tmpl$20(), _el$79 = _el$78.firstChild, _el$80 = _el$79.firstChild, _el$82 = _el$80.firstChild.nextSibling, _el$83 = _el$80.nextSibling, _el$85 = _el$83.firstChild.nextSibling, _el$86 = _el$83.nextSibling, _el$88 = _el$86.firstChild.nextSibling, _el$89 = _el$86.nextSibling, _el$91 = _el$89.firstChild.nextSibling, _el$94 = _el$89.nextSibling.firstChild.nextSibling, _el$102 = _el$79.nextSibling, _el$104 = _el$102.nextSibling.firstChild, _el$105 = _el$104.nextSibling, _el$106 = _el$105.nextSibling, _el$107 = _el$106.nextSibling, _el$108 = _el$107.nextSibling, _el$109 = _el$108.nextSibling, _el$110 = _el$109.nextSibling, _el$111 = _el$110.nextSibling;
|
|
8727
|
+
insert(_el$82, () => authModeLabel(auth().mode));
|
|
8728
|
+
insert(_el$85, () => auth().apiKeyAvailable ? "available" : "missing");
|
|
8729
|
+
insert(_el$88, () => auth().storedSessionAvailable ? "available" : "missing");
|
|
8730
|
+
insert(_el$91, () => auth().supabaseAvailable ? "available" : "missing");
|
|
8731
|
+
insert(_el$94, () => auth().refreshAvailable ? "available" : "missing");
|
|
8732
|
+
insert(_el$78, createComponent(Show, {
|
|
7837
8733
|
get when() {
|
|
7838
8734
|
return auth().clientId;
|
|
7839
8735
|
},
|
|
7840
8736
|
get children() {
|
|
7841
|
-
var _el$
|
|
7842
|
-
_el$
|
|
7843
|
-
insert(_el$
|
|
7844
|
-
return _el$
|
|
8737
|
+
var _el$95 = _tmpl$16();
|
|
8738
|
+
_el$95.firstChild;
|
|
8739
|
+
insert(_el$95, () => auth().clientId, null);
|
|
8740
|
+
return _el$95;
|
|
7845
8741
|
}
|
|
7846
|
-
}), _el$
|
|
7847
|
-
insert(_el$
|
|
8742
|
+
}), _el$102);
|
|
8743
|
+
insert(_el$78, createComponent(Show, {
|
|
7848
8744
|
get when() {
|
|
7849
8745
|
return auth().signInMethod;
|
|
7850
8746
|
},
|
|
7851
8747
|
get children() {
|
|
7852
|
-
var _el$
|
|
7853
|
-
_el$
|
|
7854
|
-
insert(_el$
|
|
7855
|
-
return _el$
|
|
8748
|
+
var _el$97 = _tmpl$17();
|
|
8749
|
+
_el$97.firstChild;
|
|
8750
|
+
insert(_el$97, () => auth().signInMethod, null);
|
|
8751
|
+
return _el$97;
|
|
7856
8752
|
}
|
|
7857
|
-
}), _el$
|
|
7858
|
-
insert(_el$
|
|
8753
|
+
}), _el$102);
|
|
8754
|
+
insert(_el$78, createComponent(Show, {
|
|
7859
8755
|
get when() {
|
|
7860
8756
|
return auth().supabasePath;
|
|
7861
8757
|
},
|
|
7862
8758
|
get children() {
|
|
7863
|
-
var _el$
|
|
7864
|
-
_el$
|
|
7865
|
-
insert(_el$
|
|
7866
|
-
return _el$
|
|
8759
|
+
var _el$99 = _tmpl$18();
|
|
8760
|
+
_el$99.firstChild;
|
|
8761
|
+
insert(_el$99, () => auth().supabasePath, null);
|
|
8762
|
+
return _el$99;
|
|
7867
8763
|
}
|
|
7868
|
-
}), _el$
|
|
7869
|
-
insert(_el$
|
|
8764
|
+
}), _el$102);
|
|
8765
|
+
insert(_el$78, createComponent(Show, {
|
|
7870
8766
|
get when() {
|
|
7871
8767
|
return auth().lastError;
|
|
7872
8768
|
},
|
|
7873
8769
|
get children() {
|
|
7874
|
-
var _el$
|
|
7875
|
-
insert(_el$
|
|
7876
|
-
return _el$
|
|
8770
|
+
var _el$101 = _tmpl$19();
|
|
8771
|
+
insert(_el$101, () => auth().lastError);
|
|
8772
|
+
return _el$101;
|
|
7877
8773
|
}
|
|
7878
|
-
}), _el$
|
|
7879
|
-
_el$
|
|
8774
|
+
}), _el$102);
|
|
8775
|
+
_el$104.$$input = (event) => {
|
|
7880
8776
|
props.onApiKeyDraftChange(event.currentTarget.value);
|
|
7881
8777
|
};
|
|
7882
|
-
addEventListener(_el$
|
|
7883
|
-
addEventListener(_el$
|
|
7884
|
-
addEventListener(_el$
|
|
7885
|
-
_el$
|
|
8778
|
+
addEventListener(_el$105, "click", props.onSaveApiKey, true);
|
|
8779
|
+
addEventListener(_el$106, "click", props.onImportDesktopSession, true);
|
|
8780
|
+
addEventListener(_el$107, "click", props.onRefresh, true);
|
|
8781
|
+
_el$108.$$click = () => {
|
|
7886
8782
|
props.onSwitchMode("api-key");
|
|
7887
8783
|
};
|
|
7888
|
-
_el$
|
|
8784
|
+
_el$109.$$click = () => {
|
|
7889
8785
|
props.onSwitchMode("stored-session");
|
|
7890
8786
|
};
|
|
7891
|
-
_el$
|
|
8787
|
+
_el$110.$$click = () => {
|
|
7892
8788
|
props.onSwitchMode("supabase-file");
|
|
7893
8789
|
};
|
|
7894
|
-
addEventListener(_el$
|
|
8790
|
+
addEventListener(_el$111, "click", props.onLogout, true);
|
|
7895
8791
|
createRenderEffect((_p$) => {
|
|
7896
8792
|
var _v$ = !auth().supabaseAvailable, _v$2 = !auth().storedSessionAvailable || !auth().refreshAvailable, _v$3 = !auth().apiKeyAvailable || auth().mode === "api-key", _v$4 = !auth().storedSessionAvailable || auth().mode === "stored-session", _v$5 = !auth().supabaseAvailable || auth().mode === "supabase-file", _v$6 = !auth().apiKeyAvailable && !auth().storedSessionAvailable;
|
|
7897
|
-
_v$ !== _p$.e && (_el$
|
|
7898
|
-
_v$2 !== _p$.t && (_el$
|
|
7899
|
-
_v$3 !== _p$.a && (_el$
|
|
7900
|
-
_v$4 !== _p$.o && (_el$
|
|
7901
|
-
_v$5 !== _p$.i && (_el$
|
|
7902
|
-
_v$6 !== _p$.n && (_el$
|
|
8793
|
+
_v$ !== _p$.e && (_el$106.disabled = _p$.e = _v$);
|
|
8794
|
+
_v$2 !== _p$.t && (_el$107.disabled = _p$.t = _v$2);
|
|
8795
|
+
_v$3 !== _p$.a && (_el$108.disabled = _p$.a = _v$3);
|
|
8796
|
+
_v$4 !== _p$.o && (_el$109.disabled = _p$.o = _v$4);
|
|
8797
|
+
_v$5 !== _p$.i && (_el$110.disabled = _p$.i = _v$5);
|
|
8798
|
+
_v$6 !== _p$.n && (_el$111.disabled = _p$.n = _v$6);
|
|
7903
8799
|
return _p$;
|
|
7904
8800
|
}, {
|
|
7905
8801
|
e: void 0,
|
|
@@ -7909,17 +8805,17 @@ function AuthPanel(props) {
|
|
|
7909
8805
|
i: void 0,
|
|
7910
8806
|
n: void 0
|
|
7911
8807
|
});
|
|
7912
|
-
createRenderEffect(() => _el$
|
|
7913
|
-
return _el$
|
|
8808
|
+
createRenderEffect(() => _el$104.value = props.apiKeyDraft);
|
|
8809
|
+
return _el$78;
|
|
7914
8810
|
})()
|
|
7915
8811
|
}));
|
|
7916
|
-
return _el$
|
|
8812
|
+
return _el$74;
|
|
7917
8813
|
})();
|
|
7918
8814
|
}
|
|
7919
8815
|
function ExportJobsPanel(props) {
|
|
7920
8816
|
return (() => {
|
|
7921
|
-
var _el$
|
|
7922
|
-
insert(_el$
|
|
8817
|
+
var _el$112 = _tmpl$21(), _el$114 = _el$112.firstChild.nextSibling;
|
|
8818
|
+
insert(_el$114, createComponent(Show, {
|
|
7923
8819
|
get when() {
|
|
7924
8820
|
return props.jobs.length > 0;
|
|
7925
8821
|
},
|
|
@@ -7932,46 +8828,127 @@ function ExportJobsPanel(props) {
|
|
|
7932
8828
|
return props.jobs.slice(0, 6);
|
|
7933
8829
|
},
|
|
7934
8830
|
children: (job) => (() => {
|
|
7935
|
-
var _el$
|
|
7936
|
-
_el$
|
|
7937
|
-
var _el$
|
|
7938
|
-
_el$
|
|
7939
|
-
var _el$
|
|
7940
|
-
insert(_el$
|
|
7941
|
-
insert(_el$
|
|
7942
|
-
insert(_el$
|
|
7943
|
-
insert(_el$
|
|
7944
|
-
insert(_el$
|
|
7945
|
-
insert(_el$
|
|
7946
|
-
insert(_el$
|
|
8831
|
+
var _el$116 = _tmpl$25(), _el$117 = _el$116.firstChild, _el$118 = _el$117.firstChild, _el$119 = _el$118.firstChild, _el$120 = _el$119.firstChild, _el$121 = _el$119.nextSibling, _el$122 = _el$118.nextSibling, _el$123 = _el$117.nextSibling, _el$124 = _el$123.nextSibling;
|
|
8832
|
+
_el$124.firstChild;
|
|
8833
|
+
var _el$126 = _el$124.nextSibling;
|
|
8834
|
+
_el$126.firstChild;
|
|
8835
|
+
var _el$129 = _el$126.nextSibling;
|
|
8836
|
+
insert(_el$119, () => job.kind, _el$120);
|
|
8837
|
+
insert(_el$121, () => job.id);
|
|
8838
|
+
insert(_el$122, () => job.status);
|
|
8839
|
+
insert(_el$123, () => \`Format: \${job.format} • \${scopeLabel(job.scope)} • \${job.itemCount > 0 ? \`\${job.completedCount}/\${job.itemCount} items\` : "0 items"} • Written: \${job.written}\`);
|
|
8840
|
+
insert(_el$124, () => job.startedAt.slice(0, 19), null);
|
|
8841
|
+
insert(_el$126, () => job.outputDir, null);
|
|
8842
|
+
insert(_el$116, createComponent(Show, {
|
|
7947
8843
|
get when() {
|
|
7948
8844
|
return job.error;
|
|
7949
8845
|
},
|
|
7950
8846
|
get children() {
|
|
7951
|
-
var _el$
|
|
7952
|
-
insert(_el$
|
|
7953
|
-
return _el$
|
|
8847
|
+
var _el$128 = _tmpl$23();
|
|
8848
|
+
insert(_el$128, () => job.error);
|
|
8849
|
+
return _el$128;
|
|
7954
8850
|
}
|
|
7955
|
-
}), _el$
|
|
7956
|
-
insert(_el$
|
|
8851
|
+
}), _el$129);
|
|
8852
|
+
insert(_el$129, createComponent(Show, {
|
|
7957
8853
|
get when() {
|
|
7958
8854
|
return job.status !== "running";
|
|
7959
8855
|
},
|
|
7960
8856
|
get children() {
|
|
7961
|
-
var _el$
|
|
7962
|
-
_el$
|
|
8857
|
+
var _el$130 = _tmpl$24();
|
|
8858
|
+
_el$130.$$click = () => {
|
|
7963
8859
|
props.onRerun(job.id);
|
|
7964
8860
|
};
|
|
7965
|
-
return _el$
|
|
8861
|
+
return _el$130;
|
|
7966
8862
|
}
|
|
7967
8863
|
}));
|
|
7968
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7969
|
-
return _el$
|
|
8864
|
+
createRenderEffect(() => setAttribute(_el$122, "data-status", job.status));
|
|
8865
|
+
return _el$116;
|
|
7970
8866
|
})()
|
|
7971
8867
|
});
|
|
7972
8868
|
}
|
|
7973
8869
|
}));
|
|
7974
|
-
return _el$
|
|
8870
|
+
return _el$112;
|
|
8871
|
+
})();
|
|
8872
|
+
}
|
|
8873
|
+
function AutomationRunsPanel(props) {
|
|
8874
|
+
return (() => {
|
|
8875
|
+
var _el$131 = _tmpl$26(), _el$133 = _el$131.firstChild.nextSibling;
|
|
8876
|
+
insert(_el$133, createComponent(Show, {
|
|
8877
|
+
get when() {
|
|
8878
|
+
return props.runs.length > 0;
|
|
8879
|
+
},
|
|
8880
|
+
get fallback() {
|
|
8881
|
+
return _tmpl$27();
|
|
8882
|
+
},
|
|
8883
|
+
get children() {
|
|
8884
|
+
return createComponent(For, {
|
|
8885
|
+
get each() {
|
|
8886
|
+
return props.runs.slice(0, 6);
|
|
8887
|
+
},
|
|
8888
|
+
children: (run) => (() => {
|
|
8889
|
+
var _el$135 = _tmpl$30(), _el$136 = _el$135.firstChild, _el$137 = _el$136.firstChild, _el$138 = _el$137.firstChild, _el$139 = _el$138.nextSibling, _el$140 = _el$137.nextSibling, _el$141 = _el$136.nextSibling, _el$142 = _el$141.nextSibling, _el$146 = _el$142.nextSibling;
|
|
8890
|
+
insert(_el$138, () => run.actionName);
|
|
8891
|
+
insert(_el$139, () => \`\${run.ruleName} • \${run.id}\`);
|
|
8892
|
+
insert(_el$140, () => run.status);
|
|
8893
|
+
insert(_el$141, () => \`\${run.title} • \${run.eventKind}\`);
|
|
8894
|
+
insert(_el$142, () => \`Started: \${run.startedAt.slice(0, 19)}\`);
|
|
8895
|
+
insert(_el$135, createComponent(Show, {
|
|
8896
|
+
get when() {
|
|
8897
|
+
return run.prompt;
|
|
8898
|
+
},
|
|
8899
|
+
get children() {
|
|
8900
|
+
var _el$143 = _tmpl$23();
|
|
8901
|
+
insert(_el$143, () => run.prompt);
|
|
8902
|
+
return _el$143;
|
|
8903
|
+
}
|
|
8904
|
+
}), _el$146);
|
|
8905
|
+
insert(_el$135, createComponent(Show, {
|
|
8906
|
+
get when() {
|
|
8907
|
+
return run.result;
|
|
8908
|
+
},
|
|
8909
|
+
get children() {
|
|
8910
|
+
var _el$144 = _tmpl$23();
|
|
8911
|
+
insert(_el$144, () => run.result);
|
|
8912
|
+
return _el$144;
|
|
8913
|
+
}
|
|
8914
|
+
}), _el$146);
|
|
8915
|
+
insert(_el$135, createComponent(Show, {
|
|
8916
|
+
get when() {
|
|
8917
|
+
return run.error;
|
|
8918
|
+
},
|
|
8919
|
+
get children() {
|
|
8920
|
+
var _el$145 = _tmpl$23();
|
|
8921
|
+
insert(_el$145, () => run.error);
|
|
8922
|
+
return _el$145;
|
|
8923
|
+
}
|
|
8924
|
+
}), _el$146);
|
|
8925
|
+
insert(_el$146, createComponent(Show, {
|
|
8926
|
+
get when() {
|
|
8927
|
+
return run.status === "pending";
|
|
8928
|
+
},
|
|
8929
|
+
get children() {
|
|
8930
|
+
return [(() => {
|
|
8931
|
+
var _el$147 = _tmpl$28();
|
|
8932
|
+
_el$147.$$click = () => {
|
|
8933
|
+
props.onApprove(run.id);
|
|
8934
|
+
};
|
|
8935
|
+
return _el$147;
|
|
8936
|
+
})(), (() => {
|
|
8937
|
+
var _el$148 = _tmpl$29();
|
|
8938
|
+
_el$148.$$click = () => {
|
|
8939
|
+
props.onReject(run.id);
|
|
8940
|
+
};
|
|
8941
|
+
return _el$148;
|
|
8942
|
+
})()];
|
|
8943
|
+
}
|
|
8944
|
+
}));
|
|
8945
|
+
createRenderEffect(() => setAttribute(_el$140, "data-status", run.status));
|
|
8946
|
+
return _el$135;
|
|
8947
|
+
})()
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
}));
|
|
8951
|
+
return _el$131;
|
|
7975
8952
|
})();
|
|
7976
8953
|
}
|
|
7977
8954
|
function Workspace(props) {
|
|
@@ -7981,8 +8958,8 @@ function Workspace(props) {
|
|
|
7981
8958
|
return workspaceBody(props.bundle, props.selectedMeeting, parsedTab());
|
|
7982
8959
|
};
|
|
7983
8960
|
return [(() => {
|
|
7984
|
-
var _el$
|
|
7985
|
-
insert(_el$
|
|
8961
|
+
var _el$149 = _tmpl$31(), _el$150 = _el$149.firstChild;
|
|
8962
|
+
insert(_el$149, createComponent(For, {
|
|
7986
8963
|
each: [
|
|
7987
8964
|
"notes",
|
|
7988
8965
|
"transcript",
|
|
@@ -7990,50 +8967,50 @@ function Workspace(props) {
|
|
|
7990
8967
|
"raw"
|
|
7991
8968
|
],
|
|
7992
8969
|
children: (tab) => (() => {
|
|
7993
|
-
var _el$
|
|
7994
|
-
_el$
|
|
8970
|
+
var _el$151 = _tmpl$32();
|
|
8971
|
+
_el$151.$$click = () => {
|
|
7995
8972
|
props.onSelectTab(tab);
|
|
7996
8973
|
};
|
|
7997
|
-
insert(_el$
|
|
7998
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7999
|
-
return _el$
|
|
8974
|
+
insert(_el$151, tab === "notes" ? "Notes" : tab === "transcript" ? "Transcript" : tab === "metadata" ? "Metadata" : "Raw");
|
|
8975
|
+
createRenderEffect(() => setAttribute(_el$151, "data-selected", parsedTab() === tab ? "true" : void 0));
|
|
8976
|
+
return _el$151;
|
|
8000
8977
|
})()
|
|
8001
|
-
}), _el$
|
|
8002
|
-
return _el$
|
|
8978
|
+
}), _el$150);
|
|
8979
|
+
return _el$149;
|
|
8003
8980
|
})(), createComponent(Show, {
|
|
8004
8981
|
get when() {
|
|
8005
8982
|
return props.selectedMeeting;
|
|
8006
8983
|
},
|
|
8007
8984
|
get fallback() {
|
|
8008
8985
|
return (() => {
|
|
8009
|
-
var _el$
|
|
8010
|
-
insert(_el$
|
|
8011
|
-
return _el$
|
|
8986
|
+
var _el$152 = _tmpl$33();
|
|
8987
|
+
insert(_el$152, () => props.detailError || "Select a meeting to inspect its notes and transcript.");
|
|
8988
|
+
return _el$152;
|
|
8012
8989
|
})();
|
|
8013
8990
|
},
|
|
8014
8991
|
children: (meeting) => [(() => {
|
|
8015
|
-
var _el$
|
|
8016
|
-
insert(_el$
|
|
8017
|
-
insert(_el$
|
|
8018
|
-
insert(_el$
|
|
8019
|
-
return _el$
|
|
8992
|
+
var _el$153 = _tmpl$34(), _el$154 = _el$153.firstChild, _el$155 = _el$154.nextSibling, _el$156 = _el$155.nextSibling;
|
|
8993
|
+
insert(_el$154, () => \`ID: \${meeting().meeting.id}\`);
|
|
8994
|
+
insert(_el$155, () => \`Source: \${meeting().meeting.noteContentSource}\`);
|
|
8995
|
+
insert(_el$156, () => \`Transcript: \${meeting().meeting.transcriptSegmentCount} segments\`);
|
|
8996
|
+
return _el$153;
|
|
8020
8997
|
})(), createComponent(Show, {
|
|
8021
8998
|
get when() {
|
|
8022
8999
|
return !props.detailError;
|
|
8023
9000
|
},
|
|
8024
9001
|
get fallback() {
|
|
8025
9002
|
return (() => {
|
|
8026
|
-
var _el$
|
|
8027
|
-
insert(_el$
|
|
8028
|
-
return _el$
|
|
9003
|
+
var _el$165 = _tmpl$33();
|
|
9004
|
+
insert(_el$165, () => props.detailError);
|
|
9005
|
+
return _el$165;
|
|
8029
9006
|
})();
|
|
8030
9007
|
},
|
|
8031
9008
|
get children() {
|
|
8032
|
-
var _el$
|
|
8033
|
-
insert(_el$
|
|
8034
|
-
insert(_el$
|
|
8035
|
-
insert(_el$
|
|
8036
|
-
return _el$
|
|
9009
|
+
var _el$157 = _tmpl$35(), _el$159 = _el$157.firstChild.firstChild, _el$161 = _el$159.firstChild.nextSibling, _el$163 = _el$159.nextSibling.firstChild, _el$164 = _el$163.nextSibling;
|
|
9010
|
+
insert(_el$161, () => metadataLines(meeting()));
|
|
9011
|
+
insert(_el$163, () => details()?.title);
|
|
9012
|
+
insert(_el$164, () => details()?.body);
|
|
9013
|
+
return _el$157;
|
|
8037
9014
|
}
|
|
8038
9015
|
})]
|
|
8039
9016
|
})];
|
|
@@ -8064,6 +9041,7 @@ function App() {
|
|
|
8064
9041
|
const [state, setState] = createStore({
|
|
8065
9042
|
apiKeyDraft: "",
|
|
8066
9043
|
appState: null,
|
|
9044
|
+
automationRuns: [],
|
|
8067
9045
|
detailError: "",
|
|
8068
9046
|
folderError: "",
|
|
8069
9047
|
folders: [],
|
|
@@ -8132,6 +9110,14 @@ function App() {
|
|
|
8132
9110
|
setState("selectedFolderId", null);
|
|
8133
9111
|
}
|
|
8134
9112
|
};
|
|
9113
|
+
const loadAutomationRuns = async () => {
|
|
9114
|
+
if (!client) return;
|
|
9115
|
+
try {
|
|
9116
|
+
setState("automationRuns", (await client.listAutomationRuns({ limit: 20 })).runs);
|
|
9117
|
+
} catch (error) {
|
|
9118
|
+
setState("detailError", error instanceof Error ? error.message : String(error));
|
|
9119
|
+
}
|
|
9120
|
+
};
|
|
8135
9121
|
const loadMeeting = async (meetingId) => {
|
|
8136
9122
|
if (!client) return;
|
|
8137
9123
|
setState("selectedMeetingId", meetingId);
|
|
@@ -8185,7 +9171,11 @@ function App() {
|
|
|
8185
9171
|
forceRefresh: true,
|
|
8186
9172
|
foreground: true
|
|
8187
9173
|
});
|
|
8188
|
-
await Promise.all([
|
|
9174
|
+
await Promise.all([
|
|
9175
|
+
loadFolders(forceRefresh),
|
|
9176
|
+
loadAutomationRuns(),
|
|
9177
|
+
mergeAuthState()
|
|
9178
|
+
]);
|
|
8189
9179
|
await loadMeetings({ refresh: forceRefresh });
|
|
8190
9180
|
setState("serverLocked", false);
|
|
8191
9181
|
setStatus(forceRefresh ? "Sync complete" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
|
|
@@ -8318,6 +9308,17 @@ function App() {
|
|
|
8318
9308
|
setStatus("Rerun failed", "error");
|
|
8319
9309
|
}
|
|
8320
9310
|
};
|
|
9311
|
+
const resolveAutomationRun = async (id, decision) => {
|
|
9312
|
+
if (!client) return;
|
|
9313
|
+
setStatus(decision === "approve" ? "Approving automation…" : "Rejecting automation…", "busy");
|
|
9314
|
+
try {
|
|
9315
|
+
await client.resolveAutomationRun(id, decision);
|
|
9316
|
+
await refreshAll();
|
|
9317
|
+
} catch (error) {
|
|
9318
|
+
setState("detailError", error instanceof Error ? error.message : String(error));
|
|
9319
|
+
setStatus("Automation decision failed", "error");
|
|
9320
|
+
}
|
|
9321
|
+
};
|
|
8321
9322
|
const unlockServer = async () => {
|
|
8322
9323
|
if (!state.serverPassword.trim()) {
|
|
8323
9324
|
setStatus("Enter the server password", "error");
|
|
@@ -8345,6 +9346,7 @@ function App() {
|
|
|
8345
9346
|
await detachClient();
|
|
8346
9347
|
setState({
|
|
8347
9348
|
appState: null,
|
|
9349
|
+
automationRuns: [],
|
|
8348
9350
|
detailError: "",
|
|
8349
9351
|
folderError: "",
|
|
8350
9352
|
folders: [],
|
|
@@ -8367,6 +9369,10 @@ function App() {
|
|
|
8367
9369
|
});
|
|
8368
9370
|
if (nextPath !== \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`) history.replaceState(null, "", nextPath);
|
|
8369
9371
|
});
|
|
9372
|
+
createEffect(() => {
|
|
9373
|
+
if (!state.appState?.automation.loaded || !client) return;
|
|
9374
|
+
loadAutomationRuns();
|
|
9375
|
+
});
|
|
8370
9376
|
onMount(() => {
|
|
8371
9377
|
const onKeyDown = (event) => {
|
|
8372
9378
|
const target = event.target;
|
|
@@ -8542,6 +9548,17 @@ function App() {
|
|
|
8542
9548
|
rerunJob(jobId);
|
|
8543
9549
|
}
|
|
8544
9550
|
}), null);
|
|
9551
|
+
insert(_el$3, createComponent(AutomationRunsPanel, {
|
|
9552
|
+
onApprove: (runId) => {
|
|
9553
|
+
resolveAutomationRun(runId, "approve");
|
|
9554
|
+
},
|
|
9555
|
+
onReject: (runId) => {
|
|
9556
|
+
resolveAutomationRun(runId, "reject");
|
|
9557
|
+
},
|
|
9558
|
+
get runs() {
|
|
9559
|
+
return state.automationRuns;
|
|
9560
|
+
}
|
|
9561
|
+
}), null);
|
|
8545
9562
|
insert(_el$3, createComponent(Workspace, {
|
|
8546
9563
|
get bundle() {
|
|
8547
9564
|
return state.selectedMeetingBundle;
|
|
@@ -8639,6 +9656,17 @@ function parseAuthMode(value) {
|
|
|
8639
9656
|
default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
|
|
8640
9657
|
}
|
|
8641
9658
|
}
|
|
9659
|
+
function parseAutomationRunStatus(value) {
|
|
9660
|
+
switch (value) {
|
|
9661
|
+
case null:
|
|
9662
|
+
case "": return;
|
|
9663
|
+
case "completed":
|
|
9664
|
+
case "failed":
|
|
9665
|
+
case "pending":
|
|
9666
|
+
case "skipped": return value;
|
|
9667
|
+
default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
|
|
9668
|
+
}
|
|
9669
|
+
}
|
|
8642
9670
|
function folderIdFromBody(value) {
|
|
8643
9671
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8644
9672
|
}
|
|
@@ -8765,6 +9793,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8765
9793
|
capabilities: {
|
|
8766
9794
|
attach: true,
|
|
8767
9795
|
auth: true,
|
|
9796
|
+
automation: true,
|
|
8768
9797
|
events: true,
|
|
8769
9798
|
exports: true,
|
|
8770
9799
|
folders: true,
|
|
@@ -8893,6 +9922,20 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8893
9922
|
sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
|
|
8894
9923
|
return;
|
|
8895
9924
|
}
|
|
9925
|
+
if (method === "GET" && path === granolaTransportPaths.automationRuns) {
|
|
9926
|
+
sendJson(response, await app.listAutomationRuns({
|
|
9927
|
+
limit: parseInteger(url.searchParams.get("limit")),
|
|
9928
|
+
status: parseAutomationRunStatus(url.searchParams.get("status"))
|
|
9929
|
+
}), { headers: originHeaders });
|
|
9930
|
+
return;
|
|
9931
|
+
}
|
|
9932
|
+
if (method === "POST" && (path.endsWith("/approve") || path.endsWith("/reject")) && path.startsWith(`${granolaTransportPaths.automationRuns}/`)) {
|
|
9933
|
+
const decision = path.endsWith("/approve") ? "approve" : "reject";
|
|
9934
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.automationRuns}/`.length, -`/${decision}`.length));
|
|
9935
|
+
const body = await readJsonBody(request);
|
|
9936
|
+
sendJson(response, await app.resolveAutomationRun(id, decision, { note: typeof body.note === "string" ? body.note : void 0 }), { headers: originHeaders });
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
8896
9939
|
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
8897
9940
|
const body = await readJsonBody(request);
|
|
8898
9941
|
sendJson(response, await app.sync({
|
|
@@ -9317,7 +10360,7 @@ function resolveTranscriptFormat$1(value) {
|
|
|
9317
10360
|
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
9318
10361
|
}
|
|
9319
10362
|
}
|
|
9320
|
-
function parseLimit(value) {
|
|
10363
|
+
function parseLimit$1(value) {
|
|
9321
10364
|
if (value === void 0) return 20;
|
|
9322
10365
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
9323
10366
|
const limit = Number(value);
|
|
@@ -9371,7 +10414,7 @@ const meetingCommand = {
|
|
|
9371
10414
|
};
|
|
9372
10415
|
async function list(commandFlags, globalFlags) {
|
|
9373
10416
|
const format = resolveListFormat(commandFlags.format);
|
|
9374
|
-
const limit = parseLimit(commandFlags.limit);
|
|
10417
|
+
const limit = parseLimit$1(commandFlags.limit);
|
|
9375
10418
|
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
9376
10419
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
9377
10420
|
const config = await loadConfig({
|
|
@@ -9552,6 +10595,77 @@ function resolveNoteFormat(value) {
|
|
|
9552
10595
|
}
|
|
9553
10596
|
}
|
|
9554
10597
|
//#endregion
|
|
10598
|
+
//#region src/commands/search.ts
|
|
10599
|
+
function searchHelp() {
|
|
10600
|
+
return `Granola search
|
|
10601
|
+
|
|
10602
|
+
Usage:
|
|
10603
|
+
granola search <query> [options]
|
|
10604
|
+
|
|
10605
|
+
Options:
|
|
10606
|
+
--folder <query> Filter search results to one folder id or name
|
|
10607
|
+
--format <value> text, json, yaml (default: text)
|
|
10608
|
+
--limit <n> Number of meetings to show (default: 20)
|
|
10609
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
10610
|
+
--supabase <path> Path to supabase.json
|
|
10611
|
+
--debug Enable debug logging
|
|
10612
|
+
--config <path> Path to .granola.toml
|
|
10613
|
+
-h, --help Show help
|
|
10614
|
+
`;
|
|
10615
|
+
}
|
|
10616
|
+
function resolveFormat(value) {
|
|
10617
|
+
switch (value) {
|
|
10618
|
+
case void 0: return "text";
|
|
10619
|
+
case "json":
|
|
10620
|
+
case "text":
|
|
10621
|
+
case "yaml": return value;
|
|
10622
|
+
default: throw new Error("invalid search format: expected text, json, or yaml");
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
function parseLimit(value) {
|
|
10626
|
+
if (value === void 0) return 20;
|
|
10627
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid search limit: expected a positive integer");
|
|
10628
|
+
return Number(value);
|
|
10629
|
+
}
|
|
10630
|
+
const searchCommand = {
|
|
10631
|
+
description: "Search meetings across titles, notes, transcripts, folders, and tags",
|
|
10632
|
+
flags: {
|
|
10633
|
+
folder: { type: "string" },
|
|
10634
|
+
format: { type: "string" },
|
|
10635
|
+
help: { type: "boolean" },
|
|
10636
|
+
limit: { type: "string" },
|
|
10637
|
+
timeout: { type: "string" }
|
|
10638
|
+
},
|
|
10639
|
+
help: searchHelp,
|
|
10640
|
+
name: "search",
|
|
10641
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
10642
|
+
const query = commandArgs.join(" ").trim();
|
|
10643
|
+
if (!query) {
|
|
10644
|
+
console.log(searchHelp());
|
|
10645
|
+
return 1;
|
|
10646
|
+
}
|
|
10647
|
+
const format = resolveFormat(commandFlags.format);
|
|
10648
|
+
const limit = parseLimit(commandFlags.limit);
|
|
10649
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
10650
|
+
const config = await loadConfig({
|
|
10651
|
+
globalFlags,
|
|
10652
|
+
subcommandFlags: commandFlags
|
|
10653
|
+
});
|
|
10654
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
10655
|
+
const app = await createGranolaApp(config);
|
|
10656
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
10657
|
+
const result = await app.listMeetings({
|
|
10658
|
+
folderId: folder?.id,
|
|
10659
|
+
limit,
|
|
10660
|
+
preferIndex: true,
|
|
10661
|
+
search: query
|
|
10662
|
+
});
|
|
10663
|
+
console.log(result.source === "index" ? "Searched the local index" : "Search index unavailable, fell back to live meeting metadata");
|
|
10664
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
10665
|
+
return 0;
|
|
10666
|
+
}
|
|
10667
|
+
};
|
|
10668
|
+
//#endregion
|
|
9555
10669
|
//#region src/commands/serve.ts
|
|
9556
10670
|
function serveHelp() {
|
|
9557
10671
|
return `Granola serve
|
|
@@ -9906,6 +11020,7 @@ const commands = [
|
|
|
9906
11020
|
folderCommand,
|
|
9907
11021
|
meetingCommand,
|
|
9908
11022
|
notesCommand,
|
|
11023
|
+
searchCommand,
|
|
9909
11024
|
serveCommand,
|
|
9910
11025
|
syncCommand,
|
|
9911
11026
|
tuiCommand,
|