granola-toolkit 0.41.0 → 0.42.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 +1 -0
- package/dist/cli.js +1031 -166
- 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,6 +2621,7 @@ 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"),
|
|
@@ -2408,10 +2670,60 @@ function createDefaultAutomationMatchStore(filePath) {
|
|
|
2408
2670
|
return new FileAutomationMatchStore(filePath);
|
|
2409
2671
|
}
|
|
2410
2672
|
//#endregion
|
|
2673
|
+
//#region src/automation-runs.ts
|
|
2674
|
+
function cloneRun(run) {
|
|
2675
|
+
return {
|
|
2676
|
+
...run,
|
|
2677
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
2678
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
2679
|
+
tags: [...run.tags]
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
function sortRuns(runs) {
|
|
2683
|
+
return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
2684
|
+
}
|
|
2685
|
+
function mergeRuns(runs) {
|
|
2686
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2687
|
+
for (const run of runs) byId.set(run.id, cloneRun(run));
|
|
2688
|
+
return sortRuns([...byId.values()]);
|
|
2689
|
+
}
|
|
2690
|
+
var FileAutomationRunStore = class {
|
|
2691
|
+
constructor(filePath = defaultAutomationRunsFilePath()) {
|
|
2692
|
+
this.filePath = filePath;
|
|
2693
|
+
}
|
|
2694
|
+
async appendRuns(runs) {
|
|
2695
|
+
if (runs.length === 0) return;
|
|
2696
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2697
|
+
const payload = runs.map((run) => JSON.stringify(run)).join("\n");
|
|
2698
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
2699
|
+
encoding: "utf8",
|
|
2700
|
+
mode: 384
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
async readRun(id) {
|
|
2704
|
+
return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
|
|
2705
|
+
}
|
|
2706
|
+
async readRuns(options = {}) {
|
|
2707
|
+
try {
|
|
2708
|
+
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);
|
|
2709
|
+
return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
|
|
2710
|
+
} catch {
|
|
2711
|
+
return [];
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
function defaultAutomationRunsFilePath() {
|
|
2716
|
+
return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
|
|
2717
|
+
}
|
|
2718
|
+
function createDefaultAutomationRunStore(filePath) {
|
|
2719
|
+
return new FileAutomationRunStore(filePath);
|
|
2720
|
+
}
|
|
2721
|
+
//#endregion
|
|
2411
2722
|
//#region src/automation-rules.ts
|
|
2412
2723
|
function cloneRule(rule) {
|
|
2413
2724
|
return {
|
|
2414
2725
|
...rule,
|
|
2726
|
+
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
2415
2727
|
when: {
|
|
2416
2728
|
...rule.when,
|
|
2417
2729
|
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
@@ -2423,11 +2735,92 @@ function cloneRule(rule) {
|
|
|
2423
2735
|
}
|
|
2424
2736
|
};
|
|
2425
2737
|
}
|
|
2738
|
+
function cloneAction(action) {
|
|
2739
|
+
switch (action.kind) {
|
|
2740
|
+
case "ask-user": return { ...action };
|
|
2741
|
+
case "command": return {
|
|
2742
|
+
...action,
|
|
2743
|
+
args: action.args ? [...action.args] : void 0,
|
|
2744
|
+
env: action.env ? { ...action.env } : void 0
|
|
2745
|
+
};
|
|
2746
|
+
case "export-notes":
|
|
2747
|
+
case "export-transcript": return { ...action };
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2426
2750
|
function stringArray(value) {
|
|
2427
2751
|
if (!Array.isArray(value)) return;
|
|
2428
2752
|
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
2429
2753
|
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
2430
2754
|
}
|
|
2755
|
+
function stringRecord(value) {
|
|
2756
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2757
|
+
const entries = Object.entries(value).filter(([key, item]) => {
|
|
2758
|
+
return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
|
|
2759
|
+
});
|
|
2760
|
+
if (entries.length === 0) return;
|
|
2761
|
+
return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
|
|
2762
|
+
}
|
|
2763
|
+
function parseAction(value, index) {
|
|
2764
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2765
|
+
const record = value;
|
|
2766
|
+
const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
|
|
2767
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
|
|
2768
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
2769
|
+
const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
|
|
2770
|
+
switch (kind) {
|
|
2771
|
+
case "ask-user": {
|
|
2772
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
2773
|
+
if (!id || !prompt) return;
|
|
2774
|
+
return {
|
|
2775
|
+
details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
|
|
2776
|
+
enabled,
|
|
2777
|
+
id,
|
|
2778
|
+
kind,
|
|
2779
|
+
name,
|
|
2780
|
+
prompt
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
case "command": {
|
|
2784
|
+
const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
|
|
2785
|
+
if (!id || !command) return;
|
|
2786
|
+
return {
|
|
2787
|
+
args: stringArray(record.args),
|
|
2788
|
+
command,
|
|
2789
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
2790
|
+
enabled,
|
|
2791
|
+
env: stringRecord(record.env),
|
|
2792
|
+
id,
|
|
2793
|
+
kind,
|
|
2794
|
+
name,
|
|
2795
|
+
stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
|
|
2796
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
case "export-notes":
|
|
2800
|
+
if (!id) return;
|
|
2801
|
+
return {
|
|
2802
|
+
enabled,
|
|
2803
|
+
format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
|
|
2804
|
+
id,
|
|
2805
|
+
kind,
|
|
2806
|
+
name,
|
|
2807
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2808
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2809
|
+
};
|
|
2810
|
+
case "export-transcript":
|
|
2811
|
+
if (!id) return;
|
|
2812
|
+
return {
|
|
2813
|
+
enabled,
|
|
2814
|
+
format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
|
|
2815
|
+
id,
|
|
2816
|
+
kind,
|
|
2817
|
+
name,
|
|
2818
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2819
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2820
|
+
};
|
|
2821
|
+
default: return;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2431
2824
|
function parseRule(value) {
|
|
2432
2825
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2433
2826
|
const record = value;
|
|
@@ -2436,6 +2829,7 @@ function parseRule(value) {
|
|
|
2436
2829
|
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
2437
2830
|
if (!id || !name || !whenValue) return;
|
|
2438
2831
|
return {
|
|
2832
|
+
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
2439
2833
|
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
2440
2834
|
id,
|
|
2441
2835
|
name,
|
|
@@ -3447,6 +3841,7 @@ async function loadOptionalGranolaCache(cacheFile) {
|
|
|
3447
3841
|
//#endregion
|
|
3448
3842
|
//#region src/export-scope.ts
|
|
3449
3843
|
const FOLDER_EXPORT_DIRECTORY = "_folders";
|
|
3844
|
+
const MEETING_EXPORT_DIRECTORY = "_meetings";
|
|
3450
3845
|
function allExportScope() {
|
|
3451
3846
|
return { mode: "all" };
|
|
3452
3847
|
}
|
|
@@ -3458,11 +3853,22 @@ function folderExportScope(folder) {
|
|
|
3458
3853
|
};
|
|
3459
3854
|
}
|
|
3460
3855
|
function cloneExportScope(scope) {
|
|
3461
|
-
|
|
3856
|
+
if (scope.mode === "folder" || scope.mode === "meeting") return { ...scope };
|
|
3857
|
+
return { mode: "all" };
|
|
3462
3858
|
}
|
|
3463
3859
|
function normaliseExportScope(value) {
|
|
3464
3860
|
const record = asRecord(value);
|
|
3465
3861
|
if (!record) return allExportScope();
|
|
3862
|
+
if (record.mode === "meeting") {
|
|
3863
|
+
const meetingId = stringValue(record.meetingId);
|
|
3864
|
+
const meetingTitle = stringValue(record.meetingTitle) || meetingId;
|
|
3865
|
+
if (!meetingId) return allExportScope();
|
|
3866
|
+
return {
|
|
3867
|
+
meetingId,
|
|
3868
|
+
meetingTitle,
|
|
3869
|
+
mode: "meeting"
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3466
3872
|
if (record.mode !== "folder") return allExportScope();
|
|
3467
3873
|
const folderId = stringValue(record.folderId);
|
|
3468
3874
|
const folderName = stringValue(record.folderName) || folderId;
|
|
@@ -3473,12 +3879,22 @@ function normaliseExportScope(value) {
|
|
|
3473
3879
|
mode: "folder"
|
|
3474
3880
|
};
|
|
3475
3881
|
}
|
|
3882
|
+
function meetingExportScope(meeting) {
|
|
3883
|
+
return {
|
|
3884
|
+
meetingId: meeting.meetingId,
|
|
3885
|
+
meetingTitle: meeting.meetingTitle || meeting.meetingId,
|
|
3886
|
+
mode: "meeting"
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3476
3889
|
function renderExportScopeLabel(scope) {
|
|
3477
|
-
|
|
3890
|
+
if (scope.mode === "folder") return `folder ${scope.folderName}`;
|
|
3891
|
+
if (scope.mode === "meeting") return `meeting ${scope.meetingTitle}`;
|
|
3892
|
+
return "all meetings";
|
|
3478
3893
|
}
|
|
3479
3894
|
function resolveExportOutputDir(outputDir, scope, options = {}) {
|
|
3480
|
-
if (
|
|
3481
|
-
return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3895
|
+
if (options.scopedDirectory === false || scope.mode === "all") return outputDir;
|
|
3896
|
+
if (scope.mode === "folder") return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3897
|
+
return join(outputDir, MEETING_EXPORT_DIRECTORY, sanitiseFilename(scope.meetingId, "meeting"));
|
|
3482
3898
|
}
|
|
3483
3899
|
//#endregion
|
|
3484
3900
|
//#region src/export-jobs.ts
|
|
@@ -4005,8 +4421,11 @@ function defaultState(config, auth, surface) {
|
|
|
4005
4421
|
loaded: false,
|
|
4006
4422
|
matchCount: 0,
|
|
4007
4423
|
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4424
|
+
pendingRunCount: 0,
|
|
4008
4425
|
ruleCount: 0,
|
|
4009
|
-
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4426
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4427
|
+
runCount: 0,
|
|
4428
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4010
4429
|
},
|
|
4011
4430
|
cache: {
|
|
4012
4431
|
configured: Boolean(config.transcripts.cacheFile),
|
|
@@ -4049,6 +4468,7 @@ function defaultState(config, auth, surface) {
|
|
|
4049
4468
|
};
|
|
4050
4469
|
}
|
|
4051
4470
|
var GranolaApp = class {
|
|
4471
|
+
#automationActionRuns;
|
|
4052
4472
|
#automationMatches;
|
|
4053
4473
|
#automationRules;
|
|
4054
4474
|
#cacheData;
|
|
@@ -4069,26 +4489,20 @@ var GranolaApp = class {
|
|
|
4069
4489
|
folders: match.folders.map((folder) => ({ ...folder })),
|
|
4070
4490
|
tags: [...match.tags]
|
|
4071
4491
|
}));
|
|
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
|
-
}));
|
|
4492
|
+
this.#automationActionRuns = (deps.automationRuns ?? []).map((run) => this.cloneAutomationRun(run));
|
|
4493
|
+
this.#automationRules = (deps.automationRules ?? []).map((rule) => this.cloneAutomationRule(rule));
|
|
4084
4494
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
4085
4495
|
this.#state.automation = {
|
|
4496
|
+
lastRunAt: this.#automationActionRuns[0]?.finishedAt ?? this.#automationActionRuns[0]?.startedAt,
|
|
4086
4497
|
lastMatchedAt: this.#automationMatches[0]?.matchedAt,
|
|
4087
4498
|
loaded: true,
|
|
4088
4499
|
matchCount: this.#automationMatches.length,
|
|
4089
4500
|
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4501
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4090
4502
|
ruleCount: this.#automationRules.length,
|
|
4091
|
-
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4503
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4504
|
+
runCount: this.#automationActionRuns.length,
|
|
4505
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4092
4506
|
};
|
|
4093
4507
|
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
4094
4508
|
this.#state.index = {
|
|
@@ -4165,6 +4579,18 @@ var GranolaApp = class {
|
|
|
4165
4579
|
cloneAutomationRule(rule) {
|
|
4166
4580
|
return {
|
|
4167
4581
|
...rule,
|
|
4582
|
+
actions: rule.actions?.map((action) => {
|
|
4583
|
+
switch (action.kind) {
|
|
4584
|
+
case "ask-user": return { ...action };
|
|
4585
|
+
case "command": return {
|
|
4586
|
+
...action,
|
|
4587
|
+
args: action.args ? [...action.args] : void 0,
|
|
4588
|
+
env: action.env ? { ...action.env } : void 0
|
|
4589
|
+
};
|
|
4590
|
+
case "export-notes":
|
|
4591
|
+
case "export-transcript": return { ...action };
|
|
4592
|
+
}
|
|
4593
|
+
}),
|
|
4168
4594
|
when: {
|
|
4169
4595
|
...rule.when,
|
|
4170
4596
|
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
@@ -4183,16 +4609,40 @@ var GranolaApp = class {
|
|
|
4183
4609
|
tags: [...match.tags]
|
|
4184
4610
|
};
|
|
4185
4611
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4612
|
+
cloneAutomationRun(run) {
|
|
4613
|
+
return {
|
|
4614
|
+
...run,
|
|
4615
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
4616
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
4617
|
+
tags: [...run.tags]
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
refreshAutomationState() {
|
|
4621
|
+
const latestMatch = this.#automationMatches.reduce((current, candidate) => !current || candidate.matchedAt.localeCompare(current.matchedAt) > 0 ? candidate : current, void 0);
|
|
4622
|
+
const latestRun = this.#automationActionRuns.reduce((current, candidate) => {
|
|
4623
|
+
const candidateTime = candidate.finishedAt ?? candidate.startedAt;
|
|
4624
|
+
const currentTime = current ? current.finishedAt ?? current.startedAt : void 0;
|
|
4625
|
+
return !currentTime || candidateTime.localeCompare(currentTime) > 0 ? candidate : current;
|
|
4626
|
+
}, void 0);
|
|
4190
4627
|
this.#state.automation = {
|
|
4191
4628
|
...this.#state.automation,
|
|
4629
|
+
lastMatchedAt: latestMatch?.matchedAt ?? this.#state.automation.lastMatchedAt,
|
|
4630
|
+
lastRunAt: latestRun?.finishedAt ?? latestRun?.startedAt ?? this.#state.automation.lastRunAt,
|
|
4192
4631
|
loaded: true,
|
|
4632
|
+
matchCount: this.#automationMatches.length,
|
|
4633
|
+
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4634
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4193
4635
|
ruleCount: this.#automationRules.length,
|
|
4194
|
-
rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
|
|
4636
|
+
rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4637
|
+
runCount: this.#automationActionRuns.length,
|
|
4638
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4195
4639
|
};
|
|
4640
|
+
}
|
|
4641
|
+
async loadAutomationRules(options = {}) {
|
|
4642
|
+
if (this.#automationRules.length > 0 && !options.forceRefresh) return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
|
|
4643
|
+
if (!this.deps.automationRuleStore) return [];
|
|
4644
|
+
this.#automationRules = (await this.deps.automationRuleStore.readRules()).map((rule) => this.cloneAutomationRule(rule));
|
|
4645
|
+
this.refreshAutomationState();
|
|
4196
4646
|
this.emitStateUpdate();
|
|
4197
4647
|
return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
|
|
4198
4648
|
}
|
|
@@ -4200,12 +4650,18 @@ var GranolaApp = class {
|
|
|
4200
4650
|
if (matches.length === 0) return;
|
|
4201
4651
|
if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
|
|
4202
4652
|
this.#automationMatches.push(...matches.map((match) => this.cloneAutomationMatch(match)));
|
|
4203
|
-
this
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4653
|
+
this.refreshAutomationState();
|
|
4654
|
+
}
|
|
4655
|
+
async appendAutomationRuns(runs) {
|
|
4656
|
+
if (runs.length === 0) return;
|
|
4657
|
+
if (this.deps.automationRunStore) await this.deps.automationRunStore.appendRuns(runs);
|
|
4658
|
+
for (const run of runs) {
|
|
4659
|
+
const index = this.#automationActionRuns.findIndex((candidate) => candidate.id === run.id);
|
|
4660
|
+
if (index >= 0) this.#automationActionRuns[index] = this.cloneAutomationRun(run);
|
|
4661
|
+
else this.#automationActionRuns.push(this.cloneAutomationRun(run));
|
|
4662
|
+
}
|
|
4663
|
+
this.#automationActionRuns.sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
4664
|
+
this.refreshAutomationState();
|
|
4209
4665
|
}
|
|
4210
4666
|
createSyncRunId() {
|
|
4211
4667
|
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
@@ -4449,6 +4905,35 @@ var GranolaApp = class {
|
|
|
4449
4905
|
this.setUiState({ view: "idle" });
|
|
4450
4906
|
return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
|
|
4451
4907
|
}
|
|
4908
|
+
async listAutomationRuns(options = {}) {
|
|
4909
|
+
const limit = options.limit ?? 20;
|
|
4910
|
+
const runs = this.deps.automationRunStore ? await this.deps.automationRunStore.readRuns({
|
|
4911
|
+
limit,
|
|
4912
|
+
status: options.status
|
|
4913
|
+
}) : this.#automationActionRuns.filter((run) => options.status ? run.status === options.status : true).slice(0, limit);
|
|
4914
|
+
this.setUiState({ view: "idle" });
|
|
4915
|
+
return { runs: runs.map((run) => this.cloneAutomationRun(run)) };
|
|
4916
|
+
}
|
|
4917
|
+
async resolveAutomationRun(id, decision, options = {}) {
|
|
4918
|
+
const current = (this.deps.automationRunStore ? await this.deps.automationRunStore.readRun(id) : void 0) ?? this.#automationActionRuns.find((run) => run.id === id);
|
|
4919
|
+
if (!current) throw new Error(`automation run not found: ${id}`);
|
|
4920
|
+
if (current.status !== "pending") throw new Error(`automation run is not pending: ${id}`);
|
|
4921
|
+
const finishedAt = this.nowIso();
|
|
4922
|
+
const resolved = {
|
|
4923
|
+
...this.cloneAutomationRun(current),
|
|
4924
|
+
finishedAt,
|
|
4925
|
+
meta: {
|
|
4926
|
+
...current.meta ? structuredClone(current.meta) : {},
|
|
4927
|
+
decision,
|
|
4928
|
+
note: options.note?.trim() || void 0
|
|
4929
|
+
},
|
|
4930
|
+
result: decision === "approve" ? options.note?.trim() || "Approved by user" : options.note?.trim() || "Rejected by user",
|
|
4931
|
+
status: decision === "approve" ? "completed" : "skipped"
|
|
4932
|
+
};
|
|
4933
|
+
await this.appendAutomationRuns([resolved]);
|
|
4934
|
+
this.emitStateUpdate();
|
|
4935
|
+
return this.cloneAutomationRun(resolved);
|
|
4936
|
+
}
|
|
4452
4937
|
async loginAuth(options = {}) {
|
|
4453
4938
|
const controller = this.requireAuthController();
|
|
4454
4939
|
try {
|
|
@@ -4498,6 +4983,165 @@ var GranolaApp = class {
|
|
|
4498
4983
|
throw error;
|
|
4499
4984
|
}
|
|
4500
4985
|
}
|
|
4986
|
+
async maybeReadMeetingBundleById(id, options = {}) {
|
|
4987
|
+
try {
|
|
4988
|
+
return await this.readMeetingBundleById(id, options);
|
|
4989
|
+
} catch {
|
|
4990
|
+
return;
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async runAutomationNotesAction(match, action) {
|
|
4994
|
+
const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
|
|
4995
|
+
if (!bundle) return;
|
|
4996
|
+
const scope = meetingExportScope({
|
|
4997
|
+
meetingId: bundle.document.id,
|
|
4998
|
+
meetingTitle: bundle.meeting.meeting.title || bundle.document.id
|
|
4999
|
+
});
|
|
5000
|
+
const result = await this.runNotesExport({
|
|
5001
|
+
documents: [bundle.document],
|
|
5002
|
+
format: action.format ?? "markdown",
|
|
5003
|
+
outputDir: resolveExportOutputDir(action.outputDir ?? this.config.notes.output, scope, { scopedDirectory: action.scopedOutput }),
|
|
5004
|
+
scope,
|
|
5005
|
+
trackLastRun: false,
|
|
5006
|
+
updateUi: false
|
|
5007
|
+
});
|
|
5008
|
+
return {
|
|
5009
|
+
format: result.format,
|
|
5010
|
+
outputDir: result.outputDir,
|
|
5011
|
+
scope: result.scope,
|
|
5012
|
+
written: result.written
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
5015
|
+
async runAutomationTranscriptAction(match, action) {
|
|
5016
|
+
const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
|
|
5017
|
+
if (!bundle?.cacheData) return;
|
|
5018
|
+
const cacheDocument = bundle.cacheData.documents[bundle.document.id];
|
|
5019
|
+
const transcriptSegments = bundle.cacheData.transcripts[bundle.document.id];
|
|
5020
|
+
if (!cacheDocument || !transcriptSegments || transcriptSegments.length === 0) return;
|
|
5021
|
+
const scope = meetingExportScope({
|
|
5022
|
+
meetingId: bundle.document.id,
|
|
5023
|
+
meetingTitle: bundle.meeting.meeting.title || bundle.document.id
|
|
5024
|
+
});
|
|
5025
|
+
const result = await this.runTranscriptsExport({
|
|
5026
|
+
cacheData: {
|
|
5027
|
+
documents: { [bundle.document.id]: cacheDocument },
|
|
5028
|
+
transcripts: { [bundle.document.id]: transcriptSegments }
|
|
5029
|
+
},
|
|
5030
|
+
format: action.format ?? "text",
|
|
5031
|
+
outputDir: resolveExportOutputDir(action.outputDir ?? this.config.transcripts.output, scope, { scopedDirectory: action.scopedOutput }),
|
|
5032
|
+
scope,
|
|
5033
|
+
trackLastRun: false,
|
|
5034
|
+
updateUi: false
|
|
5035
|
+
});
|
|
5036
|
+
return {
|
|
5037
|
+
format: result.format,
|
|
5038
|
+
outputDir: result.outputDir,
|
|
5039
|
+
scope: result.scope,
|
|
5040
|
+
written: result.written
|
|
5041
|
+
};
|
|
5042
|
+
}
|
|
5043
|
+
async runAutomationCommand(match, rule, action) {
|
|
5044
|
+
const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
|
|
5045
|
+
const cwd = action.cwd ? resolve(action.cwd) : process.cwd();
|
|
5046
|
+
const payload = JSON.stringify({
|
|
5047
|
+
action: {
|
|
5048
|
+
id: action.id,
|
|
5049
|
+
kind: "command",
|
|
5050
|
+
name: automationActionName(action)
|
|
5051
|
+
},
|
|
5052
|
+
authMode: this.#state.auth.mode,
|
|
5053
|
+
generatedAt: this.nowIso(),
|
|
5054
|
+
match: this.cloneAutomationMatch(match),
|
|
5055
|
+
meeting: bundle ? {
|
|
5056
|
+
document: bundle.document,
|
|
5057
|
+
meeting: bundle.meeting
|
|
5058
|
+
} : void 0,
|
|
5059
|
+
rule: {
|
|
5060
|
+
id: rule.id,
|
|
5061
|
+
name: rule.name
|
|
5062
|
+
}
|
|
5063
|
+
}, null, 2);
|
|
5064
|
+
return await new Promise((resolve, reject) => {
|
|
5065
|
+
const child = spawn(action.command, action.args ?? [], {
|
|
5066
|
+
cwd,
|
|
5067
|
+
env: {
|
|
5068
|
+
...process.env,
|
|
5069
|
+
...action.env,
|
|
5070
|
+
GRANOLA_ACTION_KIND: "command",
|
|
5071
|
+
GRANOLA_EVENT_ID: match.eventId,
|
|
5072
|
+
GRANOLA_EVENT_KIND: match.eventKind,
|
|
5073
|
+
GRANOLA_MATCH_ID: match.id,
|
|
5074
|
+
GRANOLA_MEETING_ID: match.meetingId,
|
|
5075
|
+
GRANOLA_RULE_ID: rule.id
|
|
5076
|
+
},
|
|
5077
|
+
stdio: [
|
|
5078
|
+
"pipe",
|
|
5079
|
+
"pipe",
|
|
5080
|
+
"pipe"
|
|
5081
|
+
]
|
|
5082
|
+
});
|
|
5083
|
+
const stdoutChunks = [];
|
|
5084
|
+
const stderrChunks = [];
|
|
5085
|
+
let timedOut = false;
|
|
5086
|
+
const timeoutMs = action.timeoutMs ?? this.config.notes.timeoutMs;
|
|
5087
|
+
const timeout = setTimeout(() => {
|
|
5088
|
+
timedOut = true;
|
|
5089
|
+
child.kill("SIGTERM");
|
|
5090
|
+
}, timeoutMs);
|
|
5091
|
+
child.stdout.on("data", (chunk) => {
|
|
5092
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5093
|
+
});
|
|
5094
|
+
child.stderr.on("data", (chunk) => {
|
|
5095
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5096
|
+
});
|
|
5097
|
+
child.on("error", (error) => {
|
|
5098
|
+
clearTimeout(timeout);
|
|
5099
|
+
reject(error);
|
|
5100
|
+
});
|
|
5101
|
+
child.on("close", (code) => {
|
|
5102
|
+
clearTimeout(timeout);
|
|
5103
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
5104
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
5105
|
+
if (timedOut) {
|
|
5106
|
+
reject(/* @__PURE__ */ new Error(`automation command timed out after ${timeoutMs}ms`));
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
if (code !== 0) {
|
|
5110
|
+
reject(new Error(stderr || stdout || `automation command exited with status ${String(code)}`));
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
resolve({
|
|
5114
|
+
command: [action.command, ...action.args ?? []].join(" "),
|
|
5115
|
+
cwd,
|
|
5116
|
+
output: stdout || stderr || void 0
|
|
5117
|
+
});
|
|
5118
|
+
});
|
|
5119
|
+
if (action.stdin !== "none") child.stdin.write(payload);
|
|
5120
|
+
child.stdin.end();
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
async runAutomationActions(rules, matches) {
|
|
5124
|
+
const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
|
|
5125
|
+
const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
|
|
5126
|
+
const runs = [];
|
|
5127
|
+
for (const match of matches) {
|
|
5128
|
+
const rule = rulesById.get(match.ruleId);
|
|
5129
|
+
if (!rule) continue;
|
|
5130
|
+
for (const action of enabledAutomationActions(rule)) {
|
|
5131
|
+
const runId = buildAutomationActionRunId(match, action.id);
|
|
5132
|
+
if (existingRunIds.has(runId)) continue;
|
|
5133
|
+
existingRunIds.add(runId);
|
|
5134
|
+
runs.push(await executeAutomationAction(match, rule, action, {
|
|
5135
|
+
exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
|
|
5136
|
+
exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
|
|
5137
|
+
nowIso: () => this.nowIso(),
|
|
5138
|
+
runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
|
|
5139
|
+
}));
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
await this.appendAutomationRuns(runs);
|
|
5143
|
+
return runs.map((run) => this.cloneAutomationRun(run));
|
|
5144
|
+
}
|
|
4501
5145
|
async runSync(options) {
|
|
4502
5146
|
const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
|
|
4503
5147
|
this.#state.sync = {
|
|
@@ -4516,8 +5160,10 @@ var GranolaApp = class {
|
|
|
4516
5160
|
const runId = this.createSyncRunId();
|
|
4517
5161
|
const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
|
|
4518
5162
|
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
4519
|
-
const
|
|
5163
|
+
const rules = await this.loadAutomationRules();
|
|
5164
|
+
const automationMatches = matchAutomationRules(rules, events, completedAt);
|
|
4520
5165
|
await this.appendAutomationMatches(automationMatches);
|
|
5166
|
+
await this.runAutomationActions(rules, automationMatches);
|
|
4521
5167
|
this.#state.sync = {
|
|
4522
5168
|
...this.#state.sync,
|
|
4523
5169
|
eventCount: this.#state.sync.eventCount + events.length,
|
|
@@ -4689,40 +5335,46 @@ var GranolaApp = class {
|
|
|
4689
5335
|
source: "live"
|
|
4690
5336
|
};
|
|
4691
5337
|
}
|
|
4692
|
-
async
|
|
5338
|
+
async readMeetingBundleById(id, options = {}) {
|
|
4693
5339
|
const documents = await this.listDocuments();
|
|
4694
5340
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4695
5341
|
const folders = await this.loadFolders();
|
|
4696
5342
|
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
5343
|
return {
|
|
4704
5344
|
cacheData,
|
|
4705
5345
|
document,
|
|
4706
|
-
meeting
|
|
5346
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4707
5347
|
};
|
|
4708
5348
|
}
|
|
4709
|
-
async
|
|
5349
|
+
async readMeetingBundleByQuery(query, options = {}) {
|
|
4710
5350
|
const documents = await this.listDocuments();
|
|
4711
5351
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4712
5352
|
const folders = await this.loadFolders();
|
|
4713
5353
|
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
5354
|
return {
|
|
4721
5355
|
cacheData,
|
|
4722
5356
|
document,
|
|
4723
|
-
meeting
|
|
5357
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4724
5358
|
};
|
|
4725
5359
|
}
|
|
5360
|
+
async getMeeting(id, options = {}) {
|
|
5361
|
+
const bundle = await this.readMeetingBundleById(id, options);
|
|
5362
|
+
this.setUiState({
|
|
5363
|
+
selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
|
|
5364
|
+
selectedMeetingId: bundle.document.id,
|
|
5365
|
+
view: "meeting-detail"
|
|
5366
|
+
});
|
|
5367
|
+
return bundle;
|
|
5368
|
+
}
|
|
5369
|
+
async findMeeting(query, options = {}) {
|
|
5370
|
+
const bundle = await this.readMeetingBundleByQuery(query, options);
|
|
5371
|
+
this.setUiState({
|
|
5372
|
+
selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
|
|
5373
|
+
selectedMeetingId: bundle.document.id,
|
|
5374
|
+
view: "meeting-detail"
|
|
5375
|
+
});
|
|
5376
|
+
return bundle;
|
|
5377
|
+
}
|
|
4726
5378
|
async listExportJobs(options = {}) {
|
|
4727
5379
|
const limit = options.limit ?? 20;
|
|
4728
5380
|
const jobs = this.#state.exports.jobs.slice(0, limit).map((job) => cloneExportJob(job));
|
|
@@ -4758,7 +5410,7 @@ var GranolaApp = class {
|
|
|
4758
5410
|
await this.failExportJob(job, error);
|
|
4759
5411
|
throw error;
|
|
4760
5412
|
}
|
|
4761
|
-
this.#state.exports.notes = {
|
|
5413
|
+
if (options.trackLastRun !== false) this.#state.exports.notes = {
|
|
4762
5414
|
format: options.format,
|
|
4763
5415
|
itemCount: options.documents.length,
|
|
4764
5416
|
jobId: job.id,
|
|
@@ -4768,7 +5420,7 @@ var GranolaApp = class {
|
|
|
4768
5420
|
written
|
|
4769
5421
|
};
|
|
4770
5422
|
this.emitStateUpdate();
|
|
4771
|
-
this.setUiState({
|
|
5423
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4772
5424
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4773
5425
|
view: "notes-export"
|
|
4774
5426
|
});
|
|
@@ -4816,7 +5468,7 @@ var GranolaApp = class {
|
|
|
4816
5468
|
await this.failExportJob(job, error);
|
|
4817
5469
|
throw error;
|
|
4818
5470
|
}
|
|
4819
|
-
this.#state.exports.transcripts = {
|
|
5471
|
+
if (options.trackLastRun !== false) this.#state.exports.transcripts = {
|
|
4820
5472
|
format: options.format,
|
|
4821
5473
|
itemCount: count,
|
|
4822
5474
|
jobId: job.id,
|
|
@@ -4826,7 +5478,7 @@ var GranolaApp = class {
|
|
|
4826
5478
|
written
|
|
4827
5479
|
};
|
|
4828
5480
|
this.emitStateUpdate();
|
|
4829
|
-
this.setUiState({
|
|
5481
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4830
5482
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4831
5483
|
view: "transcripts-export"
|
|
4832
5484
|
});
|
|
@@ -4870,6 +5522,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4870
5522
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
4871
5523
|
const automationMatchStore = createDefaultAutomationMatchStore();
|
|
4872
5524
|
const automationMatches = await automationMatchStore.readMatches(0);
|
|
5525
|
+
const automationRunStore = createDefaultAutomationRunStore();
|
|
5526
|
+
const automationRuns = await automationRunStore.readRuns({ limit: 0 });
|
|
4873
5527
|
const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
|
|
4874
5528
|
const automationRules = await automationRuleStore.readRules();
|
|
4875
5529
|
const authController = createDefaultGranolaAuthController(config);
|
|
@@ -4885,6 +5539,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4885
5539
|
authController,
|
|
4886
5540
|
automationMatches,
|
|
4887
5541
|
automationMatchStore,
|
|
5542
|
+
automationRunStore,
|
|
5543
|
+
automationRuns,
|
|
4888
5544
|
automationRules,
|
|
4889
5545
|
automationRuleStore,
|
|
4890
5546
|
cacheLoader: loadOptionalGranolaCache,
|
|
@@ -5046,15 +5702,20 @@ function automationHelp() {
|
|
|
5046
5702
|
return `Granola automation
|
|
5047
5703
|
|
|
5048
5704
|
Usage:
|
|
5049
|
-
granola automation <rules|matches> [options]
|
|
5705
|
+
granola automation <rules|matches|runs|approve|reject> [options]
|
|
5050
5706
|
|
|
5051
5707
|
Subcommands:
|
|
5052
5708
|
rules List configured automation rules
|
|
5053
5709
|
matches Show recent rule matches from sync events
|
|
5710
|
+
runs Show recent automation action runs
|
|
5711
|
+
approve <id> Approve a pending ask-user action run
|
|
5712
|
+
reject <id> Reject a pending ask-user action run
|
|
5054
5713
|
|
|
5055
5714
|
Options:
|
|
5056
5715
|
--format <value> text, json, yaml (default: text)
|
|
5057
5716
|
--limit <n> Number of matches to show (default: 20)
|
|
5717
|
+
--status <value> completed, failed, pending, skipped
|
|
5718
|
+
--note <text> Note to store with approve/reject decisions
|
|
5058
5719
|
--rules <path> Path to automation rules JSON
|
|
5059
5720
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5060
5721
|
--supabase <path> Path to supabase.json
|
|
@@ -5081,7 +5742,7 @@ function renderRules(rules, format) {
|
|
|
5081
5742
|
if (format === "json") return toJson({ rules });
|
|
5082
5743
|
if (format === "yaml") return toYaml({ rules });
|
|
5083
5744
|
if (rules.length === 0) return "No automation rules configured\n";
|
|
5084
|
-
return `${["ID ENABLED EVENTS FILTERS", ...rules.map((rule) => {
|
|
5745
|
+
return `${["ID ENABLED EVENTS ACTIONS FILTERS", ...rules.map((rule) => {
|
|
5085
5746
|
const filters = [
|
|
5086
5747
|
rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
|
|
5087
5748
|
rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
|
|
@@ -5089,7 +5750,7 @@ function renderRules(rules, format) {
|
|
|
5089
5750
|
rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
|
|
5090
5751
|
rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
|
|
5091
5752
|
].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 || "-"}`;
|
|
5753
|
+
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
5754
|
})].join("\n")}\n`;
|
|
5094
5755
|
}
|
|
5095
5756
|
function renderMatches(matches, format) {
|
|
@@ -5100,12 +5761,32 @@ function renderMatches(matches, format) {
|
|
|
5100
5761
|
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
5762
|
})].join("\n")}\n`;
|
|
5102
5763
|
}
|
|
5764
|
+
function parseRunStatus(value) {
|
|
5765
|
+
switch (value) {
|
|
5766
|
+
case void 0: return;
|
|
5767
|
+
case "completed":
|
|
5768
|
+
case "failed":
|
|
5769
|
+
case "pending":
|
|
5770
|
+
case "skipped": return value;
|
|
5771
|
+
default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
function renderRuns(runs, format) {
|
|
5775
|
+
if (format === "json") return toJson({ runs });
|
|
5776
|
+
if (format === "yaml") return toYaml({ runs });
|
|
5777
|
+
if (runs.length === 0) return "No automation runs yet\n";
|
|
5778
|
+
return `${["STARTED AT STATUS ACTION RULE TITLE", ...runs.map((run) => {
|
|
5779
|
+
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(" - ")}`;
|
|
5780
|
+
})].join("\n")}\n`;
|
|
5781
|
+
}
|
|
5103
5782
|
const automationCommand = {
|
|
5104
5783
|
description: "Inspect automation rules and rule matches",
|
|
5105
5784
|
flags: {
|
|
5106
5785
|
format: { type: "string" },
|
|
5107
5786
|
help: { type: "boolean" },
|
|
5108
5787
|
limit: { type: "string" },
|
|
5788
|
+
note: { type: "string" },
|
|
5789
|
+
status: { type: "string" },
|
|
5109
5790
|
timeout: { type: "string" }
|
|
5110
5791
|
},
|
|
5111
5792
|
help: automationHelp,
|
|
@@ -5131,10 +5812,26 @@ const automationCommand = {
|
|
|
5131
5812
|
console.log(renderMatches(result.matches, format).trimEnd());
|
|
5132
5813
|
return 0;
|
|
5133
5814
|
}
|
|
5815
|
+
case "runs": {
|
|
5816
|
+
const result = await app.listAutomationRuns({
|
|
5817
|
+
limit: parseLimit$3(commandFlags.limit),
|
|
5818
|
+
status: parseRunStatus(commandFlags.status)
|
|
5819
|
+
});
|
|
5820
|
+
console.log(renderRuns(result.runs, format).trimEnd());
|
|
5821
|
+
return 0;
|
|
5822
|
+
}
|
|
5823
|
+
case "approve":
|
|
5824
|
+
case "reject": {
|
|
5825
|
+
const id = commandArgs[1]?.trim();
|
|
5826
|
+
if (!id) throw new Error(`missing automation run id for ${action}`);
|
|
5827
|
+
const run = await app.resolveAutomationRun(id, action, { note: typeof commandFlags.note === "string" ? commandFlags.note : void 0 });
|
|
5828
|
+
console.log(`${action === "approve" ? "Approved" : "Rejected"} ${run.actionName} for ${run.title} (${run.id})`);
|
|
5829
|
+
return 0;
|
|
5830
|
+
}
|
|
5134
5831
|
case void 0:
|
|
5135
5832
|
console.log(automationHelp());
|
|
5136
5833
|
return 1;
|
|
5137
|
-
default: throw new Error("invalid automation command: expected rules or
|
|
5834
|
+
default: throw new Error("invalid automation command: expected rules, matches, runs, approve, or reject");
|
|
5138
5835
|
}
|
|
5139
5836
|
}
|
|
5140
5837
|
};
|
|
@@ -7089,6 +7786,7 @@ var granolaTransportPaths = {
|
|
|
7089
7786
|
authUnlock: "/auth/unlock",
|
|
7090
7787
|
automationMatches: "/automation/matches",
|
|
7091
7788
|
automationRules: "/automation/rules",
|
|
7789
|
+
automationRuns: "/automation/runs",
|
|
7092
7790
|
events: "/events",
|
|
7093
7791
|
exportJobs: "/exports/jobs",
|
|
7094
7792
|
exportNotes: "/exports/notes",
|
|
@@ -7148,6 +7846,15 @@ function granolaFoldersPath(options = {}) {
|
|
|
7148
7846
|
function granolaExportJobsPath(options = {}) {
|
|
7149
7847
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
7150
7848
|
}
|
|
7849
|
+
function granolaAutomationRunsPath(options = {}) {
|
|
7850
|
+
return appendSearchParams(granolaTransportPaths.automationRuns, {
|
|
7851
|
+
limit: options.limit,
|
|
7852
|
+
status: options.status
|
|
7853
|
+
});
|
|
7854
|
+
}
|
|
7855
|
+
function granolaAutomationRunDecisionPath(id, decision) {
|
|
7856
|
+
return \`\${granolaTransportPaths.automationRuns}/\${encodeURIComponent(id)}/\${decision}\`;
|
|
7857
|
+
}
|
|
7151
7858
|
function granolaExportJobRerunPath(id) {
|
|
7152
7859
|
return \`\${granolaTransportPaths.exportJobs}/\${encodeURIComponent(id)}/rerun\`;
|
|
7153
7860
|
}
|
|
@@ -7329,6 +8036,16 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
7329
8036
|
const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
|
|
7330
8037
|
return await this.requestJson(path);
|
|
7331
8038
|
}
|
|
8039
|
+
async listAutomationRuns(options = {}) {
|
|
8040
|
+
return await this.requestJson(granolaAutomationRunsPath(options));
|
|
8041
|
+
}
|
|
8042
|
+
async resolveAutomationRun(id, decision, options = {}) {
|
|
8043
|
+
return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
|
|
8044
|
+
body: JSON.stringify(options),
|
|
8045
|
+
headers: { "content-type": "application/json" },
|
|
8046
|
+
method: "POST"
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
7332
8049
|
async inspectSync() {
|
|
7333
8050
|
return cloneValue(_classPrivateFieldGet2(_state, this).sync);
|
|
7334
8051
|
}
|
|
@@ -7547,7 +8264,7 @@ function nextWorkspaceTab(currentTab, key) {
|
|
|
7547
8264
|
//#endregion
|
|
7548
8265
|
//#region src/web-app/components.tsx
|
|
7549
8266
|
/** @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$
|
|
8267
|
+
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
8268
|
function authModeLabel(mode) {
|
|
7552
8269
|
switch (mode) {
|
|
7553
8270
|
case "api-key": return "API key";
|
|
@@ -7764,7 +8481,7 @@ function AppStatePanel(props) {
|
|
|
7764
8481
|
return props.appState;
|
|
7765
8482
|
},
|
|
7766
8483
|
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$
|
|
8484
|
+
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
8485
|
insert(_el$42, () => appState().ui.surface);
|
|
7769
8486
|
insert(_el$45, () => appState().ui.view);
|
|
7770
8487
|
insert(_el$48, authStatus);
|
|
@@ -7785,6 +8502,7 @@ function AppStatePanel(props) {
|
|
|
7785
8502
|
var _c$7 = memo(() => !!appState().index.loaded);
|
|
7786
8503
|
return () => _c$7() ? \`\${appState().index.meetingCount} meetings\` : appState().index.available ? "available" : "not built";
|
|
7787
8504
|
})());
|
|
8505
|
+
insert(_el$66, () => \`\${appState().automation.runCount} runs / \${appState().automation.pendingRunCount} pending\`);
|
|
7788
8506
|
return _el$39;
|
|
7789
8507
|
})()
|
|
7790
8508
|
}), null);
|
|
@@ -7799,27 +8517,27 @@ function SecurityPanel(props) {
|
|
|
7799
8517
|
return props.visible;
|
|
7800
8518
|
},
|
|
7801
8519
|
get children() {
|
|
7802
|
-
var _el$
|
|
7803
|
-
_el$
|
|
8520
|
+
var _el$67 = _tmpl$13(), _el$70 = _el$67.firstChild.nextSibling.firstChild, _el$72 = _el$70.nextSibling.firstChild, _el$73 = _el$72.nextSibling;
|
|
8521
|
+
_el$70.$$keydown = (event) => {
|
|
7804
8522
|
if (event.key === "Enter") {
|
|
7805
8523
|
event.preventDefault();
|
|
7806
8524
|
props.onUnlock();
|
|
7807
8525
|
}
|
|
7808
8526
|
};
|
|
7809
|
-
_el$
|
|
8527
|
+
_el$70.$$input = (event) => {
|
|
7810
8528
|
props.onPasswordChange(event.currentTarget.value);
|
|
7811
8529
|
};
|
|
7812
|
-
addEventListener(_el$
|
|
7813
|
-
addEventListener(_el$
|
|
7814
|
-
createRenderEffect(() => _el$
|
|
7815
|
-
return _el$
|
|
8530
|
+
addEventListener(_el$72, "click", props.onUnlock, true);
|
|
8531
|
+
addEventListener(_el$73, "click", props.onLock, true);
|
|
8532
|
+
createRenderEffect(() => _el$70.value = props.password);
|
|
8533
|
+
return _el$67;
|
|
7816
8534
|
}
|
|
7817
8535
|
});
|
|
7818
8536
|
}
|
|
7819
8537
|
function AuthPanel(props) {
|
|
7820
8538
|
return (() => {
|
|
7821
|
-
var _el$
|
|
7822
|
-
insert(_el$
|
|
8539
|
+
var _el$74 = _tmpl$14(), _el$76 = _el$74.firstChild.nextSibling;
|
|
8540
|
+
insert(_el$76, createComponent(Show, {
|
|
7823
8541
|
get fallback() {
|
|
7824
8542
|
return _tmpl$15();
|
|
7825
8543
|
},
|
|
@@ -7827,79 +8545,79 @@ function AuthPanel(props) {
|
|
|
7827
8545
|
return props.auth;
|
|
7828
8546
|
},
|
|
7829
8547
|
children: (auth) => (() => {
|
|
7830
|
-
var _el$
|
|
7831
|
-
insert(_el$
|
|
7832
|
-
insert(_el$
|
|
7833
|
-
insert(_el$
|
|
7834
|
-
insert(_el$
|
|
7835
|
-
insert(_el$
|
|
7836
|
-
insert(_el$
|
|
8548
|
+
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;
|
|
8549
|
+
insert(_el$82, () => authModeLabel(auth().mode));
|
|
8550
|
+
insert(_el$85, () => auth().apiKeyAvailable ? "available" : "missing");
|
|
8551
|
+
insert(_el$88, () => auth().storedSessionAvailable ? "available" : "missing");
|
|
8552
|
+
insert(_el$91, () => auth().supabaseAvailable ? "available" : "missing");
|
|
8553
|
+
insert(_el$94, () => auth().refreshAvailable ? "available" : "missing");
|
|
8554
|
+
insert(_el$78, createComponent(Show, {
|
|
7837
8555
|
get when() {
|
|
7838
8556
|
return auth().clientId;
|
|
7839
8557
|
},
|
|
7840
8558
|
get children() {
|
|
7841
|
-
var _el$
|
|
7842
|
-
_el$
|
|
7843
|
-
insert(_el$
|
|
7844
|
-
return _el$
|
|
8559
|
+
var _el$95 = _tmpl$16();
|
|
8560
|
+
_el$95.firstChild;
|
|
8561
|
+
insert(_el$95, () => auth().clientId, null);
|
|
8562
|
+
return _el$95;
|
|
7845
8563
|
}
|
|
7846
|
-
}), _el$
|
|
7847
|
-
insert(_el$
|
|
8564
|
+
}), _el$102);
|
|
8565
|
+
insert(_el$78, createComponent(Show, {
|
|
7848
8566
|
get when() {
|
|
7849
8567
|
return auth().signInMethod;
|
|
7850
8568
|
},
|
|
7851
8569
|
get children() {
|
|
7852
|
-
var _el$
|
|
7853
|
-
_el$
|
|
7854
|
-
insert(_el$
|
|
7855
|
-
return _el$
|
|
8570
|
+
var _el$97 = _tmpl$17();
|
|
8571
|
+
_el$97.firstChild;
|
|
8572
|
+
insert(_el$97, () => auth().signInMethod, null);
|
|
8573
|
+
return _el$97;
|
|
7856
8574
|
}
|
|
7857
|
-
}), _el$
|
|
7858
|
-
insert(_el$
|
|
8575
|
+
}), _el$102);
|
|
8576
|
+
insert(_el$78, createComponent(Show, {
|
|
7859
8577
|
get when() {
|
|
7860
8578
|
return auth().supabasePath;
|
|
7861
8579
|
},
|
|
7862
8580
|
get children() {
|
|
7863
|
-
var _el$
|
|
7864
|
-
_el$
|
|
7865
|
-
insert(_el$
|
|
7866
|
-
return _el$
|
|
8581
|
+
var _el$99 = _tmpl$18();
|
|
8582
|
+
_el$99.firstChild;
|
|
8583
|
+
insert(_el$99, () => auth().supabasePath, null);
|
|
8584
|
+
return _el$99;
|
|
7867
8585
|
}
|
|
7868
|
-
}), _el$
|
|
7869
|
-
insert(_el$
|
|
8586
|
+
}), _el$102);
|
|
8587
|
+
insert(_el$78, createComponent(Show, {
|
|
7870
8588
|
get when() {
|
|
7871
8589
|
return auth().lastError;
|
|
7872
8590
|
},
|
|
7873
8591
|
get children() {
|
|
7874
|
-
var _el$
|
|
7875
|
-
insert(_el$
|
|
7876
|
-
return _el$
|
|
8592
|
+
var _el$101 = _tmpl$19();
|
|
8593
|
+
insert(_el$101, () => auth().lastError);
|
|
8594
|
+
return _el$101;
|
|
7877
8595
|
}
|
|
7878
|
-
}), _el$
|
|
7879
|
-
_el$
|
|
8596
|
+
}), _el$102);
|
|
8597
|
+
_el$104.$$input = (event) => {
|
|
7880
8598
|
props.onApiKeyDraftChange(event.currentTarget.value);
|
|
7881
8599
|
};
|
|
7882
|
-
addEventListener(_el$
|
|
7883
|
-
addEventListener(_el$
|
|
7884
|
-
addEventListener(_el$
|
|
7885
|
-
_el$
|
|
8600
|
+
addEventListener(_el$105, "click", props.onSaveApiKey, true);
|
|
8601
|
+
addEventListener(_el$106, "click", props.onImportDesktopSession, true);
|
|
8602
|
+
addEventListener(_el$107, "click", props.onRefresh, true);
|
|
8603
|
+
_el$108.$$click = () => {
|
|
7886
8604
|
props.onSwitchMode("api-key");
|
|
7887
8605
|
};
|
|
7888
|
-
_el$
|
|
8606
|
+
_el$109.$$click = () => {
|
|
7889
8607
|
props.onSwitchMode("stored-session");
|
|
7890
8608
|
};
|
|
7891
|
-
_el$
|
|
8609
|
+
_el$110.$$click = () => {
|
|
7892
8610
|
props.onSwitchMode("supabase-file");
|
|
7893
8611
|
};
|
|
7894
|
-
addEventListener(_el$
|
|
8612
|
+
addEventListener(_el$111, "click", props.onLogout, true);
|
|
7895
8613
|
createRenderEffect((_p$) => {
|
|
7896
8614
|
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$
|
|
8615
|
+
_v$ !== _p$.e && (_el$106.disabled = _p$.e = _v$);
|
|
8616
|
+
_v$2 !== _p$.t && (_el$107.disabled = _p$.t = _v$2);
|
|
8617
|
+
_v$3 !== _p$.a && (_el$108.disabled = _p$.a = _v$3);
|
|
8618
|
+
_v$4 !== _p$.o && (_el$109.disabled = _p$.o = _v$4);
|
|
8619
|
+
_v$5 !== _p$.i && (_el$110.disabled = _p$.i = _v$5);
|
|
8620
|
+
_v$6 !== _p$.n && (_el$111.disabled = _p$.n = _v$6);
|
|
7903
8621
|
return _p$;
|
|
7904
8622
|
}, {
|
|
7905
8623
|
e: void 0,
|
|
@@ -7909,17 +8627,17 @@ function AuthPanel(props) {
|
|
|
7909
8627
|
i: void 0,
|
|
7910
8628
|
n: void 0
|
|
7911
8629
|
});
|
|
7912
|
-
createRenderEffect(() => _el$
|
|
7913
|
-
return _el$
|
|
8630
|
+
createRenderEffect(() => _el$104.value = props.apiKeyDraft);
|
|
8631
|
+
return _el$78;
|
|
7914
8632
|
})()
|
|
7915
8633
|
}));
|
|
7916
|
-
return _el$
|
|
8634
|
+
return _el$74;
|
|
7917
8635
|
})();
|
|
7918
8636
|
}
|
|
7919
8637
|
function ExportJobsPanel(props) {
|
|
7920
8638
|
return (() => {
|
|
7921
|
-
var _el$
|
|
7922
|
-
insert(_el$
|
|
8639
|
+
var _el$112 = _tmpl$21(), _el$114 = _el$112.firstChild.nextSibling;
|
|
8640
|
+
insert(_el$114, createComponent(Show, {
|
|
7923
8641
|
get when() {
|
|
7924
8642
|
return props.jobs.length > 0;
|
|
7925
8643
|
},
|
|
@@ -7932,46 +8650,127 @@ function ExportJobsPanel(props) {
|
|
|
7932
8650
|
return props.jobs.slice(0, 6);
|
|
7933
8651
|
},
|
|
7934
8652
|
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$
|
|
8653
|
+
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;
|
|
8654
|
+
_el$124.firstChild;
|
|
8655
|
+
var _el$126 = _el$124.nextSibling;
|
|
8656
|
+
_el$126.firstChild;
|
|
8657
|
+
var _el$129 = _el$126.nextSibling;
|
|
8658
|
+
insert(_el$119, () => job.kind, _el$120);
|
|
8659
|
+
insert(_el$121, () => job.id);
|
|
8660
|
+
insert(_el$122, () => job.status);
|
|
8661
|
+
insert(_el$123, () => \`Format: \${job.format} • \${scopeLabel(job.scope)} • \${job.itemCount > 0 ? \`\${job.completedCount}/\${job.itemCount} items\` : "0 items"} • Written: \${job.written}\`);
|
|
8662
|
+
insert(_el$124, () => job.startedAt.slice(0, 19), null);
|
|
8663
|
+
insert(_el$126, () => job.outputDir, null);
|
|
8664
|
+
insert(_el$116, createComponent(Show, {
|
|
7947
8665
|
get when() {
|
|
7948
8666
|
return job.error;
|
|
7949
8667
|
},
|
|
7950
8668
|
get children() {
|
|
7951
|
-
var _el$
|
|
7952
|
-
insert(_el$
|
|
7953
|
-
return _el$
|
|
8669
|
+
var _el$128 = _tmpl$23();
|
|
8670
|
+
insert(_el$128, () => job.error);
|
|
8671
|
+
return _el$128;
|
|
7954
8672
|
}
|
|
7955
|
-
}), _el$
|
|
7956
|
-
insert(_el$
|
|
8673
|
+
}), _el$129);
|
|
8674
|
+
insert(_el$129, createComponent(Show, {
|
|
7957
8675
|
get when() {
|
|
7958
8676
|
return job.status !== "running";
|
|
7959
8677
|
},
|
|
7960
8678
|
get children() {
|
|
7961
|
-
var _el$
|
|
7962
|
-
_el$
|
|
8679
|
+
var _el$130 = _tmpl$24();
|
|
8680
|
+
_el$130.$$click = () => {
|
|
7963
8681
|
props.onRerun(job.id);
|
|
7964
8682
|
};
|
|
7965
|
-
return _el$
|
|
8683
|
+
return _el$130;
|
|
8684
|
+
}
|
|
8685
|
+
}));
|
|
8686
|
+
createRenderEffect(() => setAttribute(_el$122, "data-status", job.status));
|
|
8687
|
+
return _el$116;
|
|
8688
|
+
})()
|
|
8689
|
+
});
|
|
8690
|
+
}
|
|
8691
|
+
}));
|
|
8692
|
+
return _el$112;
|
|
8693
|
+
})();
|
|
8694
|
+
}
|
|
8695
|
+
function AutomationRunsPanel(props) {
|
|
8696
|
+
return (() => {
|
|
8697
|
+
var _el$131 = _tmpl$26(), _el$133 = _el$131.firstChild.nextSibling;
|
|
8698
|
+
insert(_el$133, createComponent(Show, {
|
|
8699
|
+
get when() {
|
|
8700
|
+
return props.runs.length > 0;
|
|
8701
|
+
},
|
|
8702
|
+
get fallback() {
|
|
8703
|
+
return _tmpl$27();
|
|
8704
|
+
},
|
|
8705
|
+
get children() {
|
|
8706
|
+
return createComponent(For, {
|
|
8707
|
+
get each() {
|
|
8708
|
+
return props.runs.slice(0, 6);
|
|
8709
|
+
},
|
|
8710
|
+
children: (run) => (() => {
|
|
8711
|
+
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;
|
|
8712
|
+
insert(_el$138, () => run.actionName);
|
|
8713
|
+
insert(_el$139, () => \`\${run.ruleName} • \${run.id}\`);
|
|
8714
|
+
insert(_el$140, () => run.status);
|
|
8715
|
+
insert(_el$141, () => \`\${run.title} • \${run.eventKind}\`);
|
|
8716
|
+
insert(_el$142, () => \`Started: \${run.startedAt.slice(0, 19)}\`);
|
|
8717
|
+
insert(_el$135, createComponent(Show, {
|
|
8718
|
+
get when() {
|
|
8719
|
+
return run.prompt;
|
|
8720
|
+
},
|
|
8721
|
+
get children() {
|
|
8722
|
+
var _el$143 = _tmpl$23();
|
|
8723
|
+
insert(_el$143, () => run.prompt);
|
|
8724
|
+
return _el$143;
|
|
8725
|
+
}
|
|
8726
|
+
}), _el$146);
|
|
8727
|
+
insert(_el$135, createComponent(Show, {
|
|
8728
|
+
get when() {
|
|
8729
|
+
return run.result;
|
|
8730
|
+
},
|
|
8731
|
+
get children() {
|
|
8732
|
+
var _el$144 = _tmpl$23();
|
|
8733
|
+
insert(_el$144, () => run.result);
|
|
8734
|
+
return _el$144;
|
|
8735
|
+
}
|
|
8736
|
+
}), _el$146);
|
|
8737
|
+
insert(_el$135, createComponent(Show, {
|
|
8738
|
+
get when() {
|
|
8739
|
+
return run.error;
|
|
8740
|
+
},
|
|
8741
|
+
get children() {
|
|
8742
|
+
var _el$145 = _tmpl$23();
|
|
8743
|
+
insert(_el$145, () => run.error);
|
|
8744
|
+
return _el$145;
|
|
8745
|
+
}
|
|
8746
|
+
}), _el$146);
|
|
8747
|
+
insert(_el$146, createComponent(Show, {
|
|
8748
|
+
get when() {
|
|
8749
|
+
return run.status === "pending";
|
|
8750
|
+
},
|
|
8751
|
+
get children() {
|
|
8752
|
+
return [(() => {
|
|
8753
|
+
var _el$147 = _tmpl$28();
|
|
8754
|
+
_el$147.$$click = () => {
|
|
8755
|
+
props.onApprove(run.id);
|
|
8756
|
+
};
|
|
8757
|
+
return _el$147;
|
|
8758
|
+
})(), (() => {
|
|
8759
|
+
var _el$148 = _tmpl$29();
|
|
8760
|
+
_el$148.$$click = () => {
|
|
8761
|
+
props.onReject(run.id);
|
|
8762
|
+
};
|
|
8763
|
+
return _el$148;
|
|
8764
|
+
})()];
|
|
7966
8765
|
}
|
|
7967
8766
|
}));
|
|
7968
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7969
|
-
return _el$
|
|
8767
|
+
createRenderEffect(() => setAttribute(_el$140, "data-status", run.status));
|
|
8768
|
+
return _el$135;
|
|
7970
8769
|
})()
|
|
7971
8770
|
});
|
|
7972
8771
|
}
|
|
7973
8772
|
}));
|
|
7974
|
-
return _el$
|
|
8773
|
+
return _el$131;
|
|
7975
8774
|
})();
|
|
7976
8775
|
}
|
|
7977
8776
|
function Workspace(props) {
|
|
@@ -7981,8 +8780,8 @@ function Workspace(props) {
|
|
|
7981
8780
|
return workspaceBody(props.bundle, props.selectedMeeting, parsedTab());
|
|
7982
8781
|
};
|
|
7983
8782
|
return [(() => {
|
|
7984
|
-
var _el$
|
|
7985
|
-
insert(_el$
|
|
8783
|
+
var _el$149 = _tmpl$31(), _el$150 = _el$149.firstChild;
|
|
8784
|
+
insert(_el$149, createComponent(For, {
|
|
7986
8785
|
each: [
|
|
7987
8786
|
"notes",
|
|
7988
8787
|
"transcript",
|
|
@@ -7990,50 +8789,50 @@ function Workspace(props) {
|
|
|
7990
8789
|
"raw"
|
|
7991
8790
|
],
|
|
7992
8791
|
children: (tab) => (() => {
|
|
7993
|
-
var _el$
|
|
7994
|
-
_el$
|
|
8792
|
+
var _el$151 = _tmpl$32();
|
|
8793
|
+
_el$151.$$click = () => {
|
|
7995
8794
|
props.onSelectTab(tab);
|
|
7996
8795
|
};
|
|
7997
|
-
insert(_el$
|
|
7998
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7999
|
-
return _el$
|
|
8796
|
+
insert(_el$151, tab === "notes" ? "Notes" : tab === "transcript" ? "Transcript" : tab === "metadata" ? "Metadata" : "Raw");
|
|
8797
|
+
createRenderEffect(() => setAttribute(_el$151, "data-selected", parsedTab() === tab ? "true" : void 0));
|
|
8798
|
+
return _el$151;
|
|
8000
8799
|
})()
|
|
8001
|
-
}), _el$
|
|
8002
|
-
return _el$
|
|
8800
|
+
}), _el$150);
|
|
8801
|
+
return _el$149;
|
|
8003
8802
|
})(), createComponent(Show, {
|
|
8004
8803
|
get when() {
|
|
8005
8804
|
return props.selectedMeeting;
|
|
8006
8805
|
},
|
|
8007
8806
|
get fallback() {
|
|
8008
8807
|
return (() => {
|
|
8009
|
-
var _el$
|
|
8010
|
-
insert(_el$
|
|
8011
|
-
return _el$
|
|
8808
|
+
var _el$152 = _tmpl$33();
|
|
8809
|
+
insert(_el$152, () => props.detailError || "Select a meeting to inspect its notes and transcript.");
|
|
8810
|
+
return _el$152;
|
|
8012
8811
|
})();
|
|
8013
8812
|
},
|
|
8014
8813
|
children: (meeting) => [(() => {
|
|
8015
|
-
var _el$
|
|
8016
|
-
insert(_el$
|
|
8017
|
-
insert(_el$
|
|
8018
|
-
insert(_el$
|
|
8019
|
-
return _el$
|
|
8814
|
+
var _el$153 = _tmpl$34(), _el$154 = _el$153.firstChild, _el$155 = _el$154.nextSibling, _el$156 = _el$155.nextSibling;
|
|
8815
|
+
insert(_el$154, () => \`ID: \${meeting().meeting.id}\`);
|
|
8816
|
+
insert(_el$155, () => \`Source: \${meeting().meeting.noteContentSource}\`);
|
|
8817
|
+
insert(_el$156, () => \`Transcript: \${meeting().meeting.transcriptSegmentCount} segments\`);
|
|
8818
|
+
return _el$153;
|
|
8020
8819
|
})(), createComponent(Show, {
|
|
8021
8820
|
get when() {
|
|
8022
8821
|
return !props.detailError;
|
|
8023
8822
|
},
|
|
8024
8823
|
get fallback() {
|
|
8025
8824
|
return (() => {
|
|
8026
|
-
var _el$
|
|
8027
|
-
insert(_el$
|
|
8028
|
-
return _el$
|
|
8825
|
+
var _el$165 = _tmpl$33();
|
|
8826
|
+
insert(_el$165, () => props.detailError);
|
|
8827
|
+
return _el$165;
|
|
8029
8828
|
})();
|
|
8030
8829
|
},
|
|
8031
8830
|
get children() {
|
|
8032
|
-
var _el$
|
|
8033
|
-
insert(_el$
|
|
8034
|
-
insert(_el$
|
|
8035
|
-
insert(_el$
|
|
8036
|
-
return _el$
|
|
8831
|
+
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;
|
|
8832
|
+
insert(_el$161, () => metadataLines(meeting()));
|
|
8833
|
+
insert(_el$163, () => details()?.title);
|
|
8834
|
+
insert(_el$164, () => details()?.body);
|
|
8835
|
+
return _el$157;
|
|
8037
8836
|
}
|
|
8038
8837
|
})]
|
|
8039
8838
|
})];
|
|
@@ -8064,6 +8863,7 @@ function App() {
|
|
|
8064
8863
|
const [state, setState] = createStore({
|
|
8065
8864
|
apiKeyDraft: "",
|
|
8066
8865
|
appState: null,
|
|
8866
|
+
automationRuns: [],
|
|
8067
8867
|
detailError: "",
|
|
8068
8868
|
folderError: "",
|
|
8069
8869
|
folders: [],
|
|
@@ -8132,6 +8932,14 @@ function App() {
|
|
|
8132
8932
|
setState("selectedFolderId", null);
|
|
8133
8933
|
}
|
|
8134
8934
|
};
|
|
8935
|
+
const loadAutomationRuns = async () => {
|
|
8936
|
+
if (!client) return;
|
|
8937
|
+
try {
|
|
8938
|
+
setState("automationRuns", (await client.listAutomationRuns({ limit: 20 })).runs);
|
|
8939
|
+
} catch (error) {
|
|
8940
|
+
setState("detailError", error instanceof Error ? error.message : String(error));
|
|
8941
|
+
}
|
|
8942
|
+
};
|
|
8135
8943
|
const loadMeeting = async (meetingId) => {
|
|
8136
8944
|
if (!client) return;
|
|
8137
8945
|
setState("selectedMeetingId", meetingId);
|
|
@@ -8185,7 +8993,11 @@ function App() {
|
|
|
8185
8993
|
forceRefresh: true,
|
|
8186
8994
|
foreground: true
|
|
8187
8995
|
});
|
|
8188
|
-
await Promise.all([
|
|
8996
|
+
await Promise.all([
|
|
8997
|
+
loadFolders(forceRefresh),
|
|
8998
|
+
loadAutomationRuns(),
|
|
8999
|
+
mergeAuthState()
|
|
9000
|
+
]);
|
|
8189
9001
|
await loadMeetings({ refresh: forceRefresh });
|
|
8190
9002
|
setState("serverLocked", false);
|
|
8191
9003
|
setStatus(forceRefresh ? "Sync complete" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
|
|
@@ -8318,6 +9130,17 @@ function App() {
|
|
|
8318
9130
|
setStatus("Rerun failed", "error");
|
|
8319
9131
|
}
|
|
8320
9132
|
};
|
|
9133
|
+
const resolveAutomationRun = async (id, decision) => {
|
|
9134
|
+
if (!client) return;
|
|
9135
|
+
setStatus(decision === "approve" ? "Approving automation…" : "Rejecting automation…", "busy");
|
|
9136
|
+
try {
|
|
9137
|
+
await client.resolveAutomationRun(id, decision);
|
|
9138
|
+
await refreshAll();
|
|
9139
|
+
} catch (error) {
|
|
9140
|
+
setState("detailError", error instanceof Error ? error.message : String(error));
|
|
9141
|
+
setStatus("Automation decision failed", "error");
|
|
9142
|
+
}
|
|
9143
|
+
};
|
|
8321
9144
|
const unlockServer = async () => {
|
|
8322
9145
|
if (!state.serverPassword.trim()) {
|
|
8323
9146
|
setStatus("Enter the server password", "error");
|
|
@@ -8345,6 +9168,7 @@ function App() {
|
|
|
8345
9168
|
await detachClient();
|
|
8346
9169
|
setState({
|
|
8347
9170
|
appState: null,
|
|
9171
|
+
automationRuns: [],
|
|
8348
9172
|
detailError: "",
|
|
8349
9173
|
folderError: "",
|
|
8350
9174
|
folders: [],
|
|
@@ -8367,6 +9191,10 @@ function App() {
|
|
|
8367
9191
|
});
|
|
8368
9192
|
if (nextPath !== \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`) history.replaceState(null, "", nextPath);
|
|
8369
9193
|
});
|
|
9194
|
+
createEffect(() => {
|
|
9195
|
+
if (!state.appState?.automation.loaded || !client) return;
|
|
9196
|
+
loadAutomationRuns();
|
|
9197
|
+
});
|
|
8370
9198
|
onMount(() => {
|
|
8371
9199
|
const onKeyDown = (event) => {
|
|
8372
9200
|
const target = event.target;
|
|
@@ -8542,6 +9370,17 @@ function App() {
|
|
|
8542
9370
|
rerunJob(jobId);
|
|
8543
9371
|
}
|
|
8544
9372
|
}), null);
|
|
9373
|
+
insert(_el$3, createComponent(AutomationRunsPanel, {
|
|
9374
|
+
onApprove: (runId) => {
|
|
9375
|
+
resolveAutomationRun(runId, "approve");
|
|
9376
|
+
},
|
|
9377
|
+
onReject: (runId) => {
|
|
9378
|
+
resolveAutomationRun(runId, "reject");
|
|
9379
|
+
},
|
|
9380
|
+
get runs() {
|
|
9381
|
+
return state.automationRuns;
|
|
9382
|
+
}
|
|
9383
|
+
}), null);
|
|
8545
9384
|
insert(_el$3, createComponent(Workspace, {
|
|
8546
9385
|
get bundle() {
|
|
8547
9386
|
return state.selectedMeetingBundle;
|
|
@@ -8639,6 +9478,17 @@ function parseAuthMode(value) {
|
|
|
8639
9478
|
default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
|
|
8640
9479
|
}
|
|
8641
9480
|
}
|
|
9481
|
+
function parseAutomationRunStatus(value) {
|
|
9482
|
+
switch (value) {
|
|
9483
|
+
case null:
|
|
9484
|
+
case "": return;
|
|
9485
|
+
case "completed":
|
|
9486
|
+
case "failed":
|
|
9487
|
+
case "pending":
|
|
9488
|
+
case "skipped": return value;
|
|
9489
|
+
default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
|
|
9490
|
+
}
|
|
9491
|
+
}
|
|
8642
9492
|
function folderIdFromBody(value) {
|
|
8643
9493
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8644
9494
|
}
|
|
@@ -8765,6 +9615,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8765
9615
|
capabilities: {
|
|
8766
9616
|
attach: true,
|
|
8767
9617
|
auth: true,
|
|
9618
|
+
automation: true,
|
|
8768
9619
|
events: true,
|
|
8769
9620
|
exports: true,
|
|
8770
9621
|
folders: true,
|
|
@@ -8893,6 +9744,20 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8893
9744
|
sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
|
|
8894
9745
|
return;
|
|
8895
9746
|
}
|
|
9747
|
+
if (method === "GET" && path === granolaTransportPaths.automationRuns) {
|
|
9748
|
+
sendJson(response, await app.listAutomationRuns({
|
|
9749
|
+
limit: parseInteger(url.searchParams.get("limit")),
|
|
9750
|
+
status: parseAutomationRunStatus(url.searchParams.get("status"))
|
|
9751
|
+
}), { headers: originHeaders });
|
|
9752
|
+
return;
|
|
9753
|
+
}
|
|
9754
|
+
if (method === "POST" && (path.endsWith("/approve") || path.endsWith("/reject")) && path.startsWith(`${granolaTransportPaths.automationRuns}/`)) {
|
|
9755
|
+
const decision = path.endsWith("/approve") ? "approve" : "reject";
|
|
9756
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.automationRuns}/`.length, -`/${decision}`.length));
|
|
9757
|
+
const body = await readJsonBody(request);
|
|
9758
|
+
sendJson(response, await app.resolveAutomationRun(id, decision, { note: typeof body.note === "string" ? body.note : void 0 }), { headers: originHeaders });
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
8896
9761
|
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
8897
9762
|
const body = await readJsonBody(request);
|
|
8898
9763
|
sendJson(response, await app.sync({
|