granola-toolkit 0.40.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 +2 -0
- package/dist/cli.js +1412 -159
- 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
|
|
@@ -18,6 +18,9 @@ const granolaTransportPaths = {
|
|
|
18
18
|
authRefresh: "/auth/refresh",
|
|
19
19
|
authStatus: "/auth/status",
|
|
20
20
|
authUnlock: "/auth/unlock",
|
|
21
|
+
automationMatches: "/automation/matches",
|
|
22
|
+
automationRules: "/automation/rules",
|
|
23
|
+
automationRuns: "/automation/runs",
|
|
21
24
|
events: "/events",
|
|
22
25
|
exportJobs: "/exports/jobs",
|
|
23
26
|
exportNotes: "/exports/notes",
|
|
@@ -77,6 +80,15 @@ function granolaFoldersPath(options = {}) {
|
|
|
77
80
|
function granolaExportJobsPath(options = {}) {
|
|
78
81
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
79
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
|
+
}
|
|
80
92
|
function granolaExportJobRerunPath(id) {
|
|
81
93
|
return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
|
|
82
94
|
}
|
|
@@ -179,6 +191,23 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
179
191
|
async inspectAuth() {
|
|
180
192
|
return await this.requestJson(granolaTransportPaths.authStatus);
|
|
181
193
|
}
|
|
194
|
+
async listAutomationRules() {
|
|
195
|
+
return await this.requestJson(granolaTransportPaths.automationRules);
|
|
196
|
+
}
|
|
197
|
+
async listAutomationMatches(options = {}) {
|
|
198
|
+
const path = options.limit ? `${granolaTransportPaths.automationMatches}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.automationMatches;
|
|
199
|
+
return await this.requestJson(path);
|
|
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
|
+
}
|
|
182
211
|
async inspectSync() {
|
|
183
212
|
return cloneValue(this.#state.sync);
|
|
184
213
|
}
|
|
@@ -420,7 +449,7 @@ function asRecord(value) {
|
|
|
420
449
|
function stringValue(value) {
|
|
421
450
|
return typeof value === "string" ? value : "";
|
|
422
451
|
}
|
|
423
|
-
function stringArray(value) {
|
|
452
|
+
function stringArray$1(value) {
|
|
424
453
|
if (!Array.isArray(value)) return [];
|
|
425
454
|
return value.filter((item) => typeof item === "string");
|
|
426
455
|
}
|
|
@@ -1383,7 +1412,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1383
1412
|
}
|
|
1384
1413
|
}
|
|
1385
1414
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1386
|
-
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}`;
|
|
1387
1416
|
}
|
|
1388
1417
|
//#endregion
|
|
1389
1418
|
//#region src/tui/theme.ts
|
|
@@ -1415,6 +1444,81 @@ const granolaTuiTheme = {
|
|
|
1415
1444
|
}
|
|
1416
1445
|
};
|
|
1417
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
|
|
1418
1522
|
//#region src/tui/auth.ts
|
|
1419
1523
|
function padLine$2(text, width) {
|
|
1420
1524
|
const clipped = truncateToWidth(text, width, "");
|
|
@@ -1691,6 +1795,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1691
1795
|
#maxMeetings;
|
|
1692
1796
|
#appState;
|
|
1693
1797
|
#activePane = "meetings";
|
|
1798
|
+
#automationRuns = [];
|
|
1694
1799
|
#detailError = "";
|
|
1695
1800
|
#detailScroll = 0;
|
|
1696
1801
|
#detailToken = 0;
|
|
@@ -1723,6 +1828,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1723
1828
|
this.#unsubscribe = this.app.subscribe((event) => {
|
|
1724
1829
|
this.handleAppUpdate(event);
|
|
1725
1830
|
});
|
|
1831
|
+
await this.loadAutomationRuns();
|
|
1726
1832
|
await this.loadFolders({ setStatus: false });
|
|
1727
1833
|
await this.loadMeetings({
|
|
1728
1834
|
preferredMeetingId: this.options.initialMeetingId,
|
|
@@ -1741,6 +1847,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1741
1847
|
this.#appState = event.state;
|
|
1742
1848
|
this.#selectedFolderId = event.state.ui.selectedFolderId;
|
|
1743
1849
|
this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
|
|
1850
|
+
this.loadAutomationRuns();
|
|
1744
1851
|
if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
|
|
1745
1852
|
await this.loadFolders({ setStatus: false });
|
|
1746
1853
|
await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
|
|
@@ -1795,6 +1902,12 @@ var GranolaTuiWorkspace = class {
|
|
|
1795
1902
|
if (token === this.#folderToken) this.tui.requestRender();
|
|
1796
1903
|
}
|
|
1797
1904
|
}
|
|
1905
|
+
async loadAutomationRuns() {
|
|
1906
|
+
try {
|
|
1907
|
+
this.#automationRuns = [...(await this.app.listAutomationRuns({ limit: 20 })).runs];
|
|
1908
|
+
this.tui.requestRender();
|
|
1909
|
+
} catch {}
|
|
1910
|
+
}
|
|
1798
1911
|
async loadMeetings(options = {}) {
|
|
1799
1912
|
const token = ++this.#listToken;
|
|
1800
1913
|
this.#loadingMeetings = true;
|
|
@@ -2070,6 +2183,38 @@ var GranolaTuiWorkspace = class {
|
|
|
2070
2183
|
});
|
|
2071
2184
|
this.setStatus("Quick open");
|
|
2072
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
|
+
}
|
|
2073
2218
|
handleInput(data) {
|
|
2074
2219
|
if (matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
|
|
2075
2220
|
this.options.onExit();
|
|
@@ -2087,6 +2232,10 @@ var GranolaTuiWorkspace = class {
|
|
|
2087
2232
|
this.openAuthPanel();
|
|
2088
2233
|
return;
|
|
2089
2234
|
}
|
|
2235
|
+
if (matchesKey(data, "u")) {
|
|
2236
|
+
this.openAutomationPanel();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2090
2239
|
if (matchesKey(data, "tab")) {
|
|
2091
2240
|
this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
|
|
2092
2241
|
this.tui.requestRender();
|
|
@@ -2271,7 +2420,7 @@ var GranolaTuiWorkspace = class {
|
|
|
2271
2420
|
const bodyLines = [];
|
|
2272
2421
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
2273
2422
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
2274
|
-
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);
|
|
2275
2424
|
return [
|
|
2276
2425
|
headerTitle,
|
|
2277
2426
|
headerSummary,
|
|
@@ -2341,6 +2490,422 @@ const attachCommand = {
|
|
|
2341
2490
|
}
|
|
2342
2491
|
};
|
|
2343
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
|
|
2614
|
+
//#region src/persistence/layout.ts
|
|
2615
|
+
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2616
|
+
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
2617
|
+
}
|
|
2618
|
+
function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
2619
|
+
const targetPlatform = options.platform ?? platform();
|
|
2620
|
+
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2621
|
+
return {
|
|
2622
|
+
automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
|
|
2623
|
+
automationRulesFile: join(dataDirectory, "automation-rules.json"),
|
|
2624
|
+
automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
|
|
2625
|
+
apiKeyFile: join(dataDirectory, "api-key.txt"),
|
|
2626
|
+
dataDirectory,
|
|
2627
|
+
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2628
|
+
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2629
|
+
sessionFile: join(dataDirectory, "session.json"),
|
|
2630
|
+
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2631
|
+
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
2632
|
+
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
//#endregion
|
|
2636
|
+
//#region src/automation-matches.ts
|
|
2637
|
+
function cloneMatch(match) {
|
|
2638
|
+
return {
|
|
2639
|
+
...match,
|
|
2640
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
2641
|
+
tags: [...match.tags]
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
var FileAutomationMatchStore = class {
|
|
2645
|
+
constructor(filePath = defaultAutomationMatchesFilePath()) {
|
|
2646
|
+
this.filePath = filePath;
|
|
2647
|
+
}
|
|
2648
|
+
async appendMatches(matches) {
|
|
2649
|
+
if (matches.length === 0) return;
|
|
2650
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2651
|
+
const payload = matches.map((match) => JSON.stringify(match)).join("\n");
|
|
2652
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
2653
|
+
encoding: "utf8",
|
|
2654
|
+
mode: 384
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
async readMatches(limit = 50) {
|
|
2658
|
+
try {
|
|
2659
|
+
const matches = (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((match) => Boolean(match)).map(cloneMatch);
|
|
2660
|
+
return (limit > 0 ? matches.slice(-limit) : matches).reverse();
|
|
2661
|
+
} catch {
|
|
2662
|
+
return [];
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
function defaultAutomationMatchesFilePath() {
|
|
2667
|
+
return defaultGranolaToolkitPersistenceLayout().automationMatchesFile;
|
|
2668
|
+
}
|
|
2669
|
+
function createDefaultAutomationMatchStore(filePath) {
|
|
2670
|
+
return new FileAutomationMatchStore(filePath);
|
|
2671
|
+
}
|
|
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
|
|
2722
|
+
//#region src/automation-rules.ts
|
|
2723
|
+
function cloneRule(rule) {
|
|
2724
|
+
return {
|
|
2725
|
+
...rule,
|
|
2726
|
+
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
2727
|
+
when: {
|
|
2728
|
+
...rule.when,
|
|
2729
|
+
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
2730
|
+
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
2731
|
+
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
2732
|
+
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
2733
|
+
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
2734
|
+
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
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
|
+
}
|
|
2750
|
+
function stringArray(value) {
|
|
2751
|
+
if (!Array.isArray(value)) return;
|
|
2752
|
+
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
2753
|
+
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
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
|
+
}
|
|
2824
|
+
function parseRule(value) {
|
|
2825
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2826
|
+
const record = value;
|
|
2827
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
|
|
2828
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
2829
|
+
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
2830
|
+
if (!id || !name || !whenValue) return;
|
|
2831
|
+
return {
|
|
2832
|
+
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
2833
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
2834
|
+
id,
|
|
2835
|
+
name,
|
|
2836
|
+
when: {
|
|
2837
|
+
eventKinds: stringArray(whenValue.eventKinds),
|
|
2838
|
+
folderIds: stringArray(whenValue.folderIds),
|
|
2839
|
+
folderNames: stringArray(whenValue.folderNames),
|
|
2840
|
+
meetingIds: stringArray(whenValue.meetingIds),
|
|
2841
|
+
tags: stringArray(whenValue.tags),
|
|
2842
|
+
titleIncludes: stringArray(whenValue.titleIncludes),
|
|
2843
|
+
titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
|
|
2844
|
+
transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
var FileAutomationRuleStore = class {
|
|
2849
|
+
constructor(filePath = defaultAutomationRulesFilePath()) {
|
|
2850
|
+
this.filePath = filePath;
|
|
2851
|
+
}
|
|
2852
|
+
async readRules() {
|
|
2853
|
+
try {
|
|
2854
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
2855
|
+
return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.rules) ? parsed.rules : []).map((rule) => parseRule(rule)).filter((rule) => Boolean(rule)).map(cloneRule);
|
|
2856
|
+
} catch {
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
function defaultAutomationRulesFilePath() {
|
|
2862
|
+
return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
|
|
2863
|
+
}
|
|
2864
|
+
function includesIgnoreCase(candidate, values) {
|
|
2865
|
+
const lowerCandidate = candidate.toLowerCase();
|
|
2866
|
+
return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
|
|
2867
|
+
}
|
|
2868
|
+
function matchesRule(rule, event) {
|
|
2869
|
+
if (rule.enabled === false) return false;
|
|
2870
|
+
const { when } = rule;
|
|
2871
|
+
if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
|
|
2872
|
+
if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
|
|
2873
|
+
if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
|
|
2874
|
+
if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
|
|
2875
|
+
if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
|
|
2876
|
+
if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
|
|
2877
|
+
if (when.titleMatches) try {
|
|
2878
|
+
if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
|
|
2879
|
+
} catch {
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
|
|
2883
|
+
return true;
|
|
2884
|
+
}
|
|
2885
|
+
function matchAutomationRules(rules, events, matchedAt) {
|
|
2886
|
+
const matches = [];
|
|
2887
|
+
for (const event of events) for (const rule of rules) {
|
|
2888
|
+
if (!matchesRule(rule, event)) continue;
|
|
2889
|
+
matches.push({
|
|
2890
|
+
eventId: event.id,
|
|
2891
|
+
eventKind: event.kind,
|
|
2892
|
+
folders: event.folders.map((folder) => ({ ...folder })),
|
|
2893
|
+
id: `${event.id}:${rule.id}`,
|
|
2894
|
+
matchedAt,
|
|
2895
|
+
meetingId: event.meetingId,
|
|
2896
|
+
ruleId: rule.id,
|
|
2897
|
+
ruleName: rule.name,
|
|
2898
|
+
tags: [...event.tags],
|
|
2899
|
+
title: event.title,
|
|
2900
|
+
transcriptLoaded: event.transcriptLoaded
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
return matches;
|
|
2904
|
+
}
|
|
2905
|
+
function createDefaultAutomationRuleStore(filePath) {
|
|
2906
|
+
return new FileAutomationRuleStore(filePath);
|
|
2907
|
+
}
|
|
2908
|
+
//#endregion
|
|
2344
2909
|
//#region src/cache.ts
|
|
2345
2910
|
function parseCacheDocument(id, value) {
|
|
2346
2911
|
const record = asRecord(value);
|
|
@@ -2395,25 +2960,6 @@ function parseCacheContents(contents) {
|
|
|
2395
2960
|
};
|
|
2396
2961
|
}
|
|
2397
2962
|
//#endregion
|
|
2398
|
-
//#region src/persistence/layout.ts
|
|
2399
|
-
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2400
|
-
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
2401
|
-
}
|
|
2402
|
-
function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
2403
|
-
const targetPlatform = options.platform ?? platform();
|
|
2404
|
-
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2405
|
-
return {
|
|
2406
|
-
apiKeyFile: join(dataDirectory, "api-key.txt"),
|
|
2407
|
-
dataDirectory,
|
|
2408
|
-
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2409
|
-
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2410
|
-
sessionFile: join(dataDirectory, "session.json"),
|
|
2411
|
-
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2412
|
-
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
2413
|
-
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2414
|
-
};
|
|
2415
|
-
}
|
|
2416
|
-
//#endregion
|
|
2417
2963
|
//#region src/client/auth.ts
|
|
2418
2964
|
const execFileAsync$1 = promisify(execFile);
|
|
2419
2965
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
@@ -2911,7 +3457,7 @@ function parseDocument(value) {
|
|
|
2911
3457
|
lastViewedPanel: parseLastViewedPanel(record.last_viewed_panel),
|
|
2912
3458
|
notes: parseProseMirrorDoc(record.notes),
|
|
2913
3459
|
notesPlain: stringValue(record.notes_plain),
|
|
2914
|
-
tags: stringArray(record.tags),
|
|
3460
|
+
tags: stringArray$1(record.tags),
|
|
2915
3461
|
title: stringValue(record.title),
|
|
2916
3462
|
updatedAt: stringValue(record.updated_at)
|
|
2917
3463
|
};
|
|
@@ -3295,6 +3841,7 @@ async function loadOptionalGranolaCache(cacheFile) {
|
|
|
3295
3841
|
//#endregion
|
|
3296
3842
|
//#region src/export-scope.ts
|
|
3297
3843
|
const FOLDER_EXPORT_DIRECTORY = "_folders";
|
|
3844
|
+
const MEETING_EXPORT_DIRECTORY = "_meetings";
|
|
3298
3845
|
function allExportScope() {
|
|
3299
3846
|
return { mode: "all" };
|
|
3300
3847
|
}
|
|
@@ -3306,11 +3853,22 @@ function folderExportScope(folder) {
|
|
|
3306
3853
|
};
|
|
3307
3854
|
}
|
|
3308
3855
|
function cloneExportScope(scope) {
|
|
3309
|
-
|
|
3856
|
+
if (scope.mode === "folder" || scope.mode === "meeting") return { ...scope };
|
|
3857
|
+
return { mode: "all" };
|
|
3310
3858
|
}
|
|
3311
3859
|
function normaliseExportScope(value) {
|
|
3312
3860
|
const record = asRecord(value);
|
|
3313
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
|
+
}
|
|
3314
3872
|
if (record.mode !== "folder") return allExportScope();
|
|
3315
3873
|
const folderId = stringValue(record.folderId);
|
|
3316
3874
|
const folderName = stringValue(record.folderName) || folderId;
|
|
@@ -3321,12 +3879,22 @@ function normaliseExportScope(value) {
|
|
|
3321
3879
|
mode: "folder"
|
|
3322
3880
|
};
|
|
3323
3881
|
}
|
|
3882
|
+
function meetingExportScope(meeting) {
|
|
3883
|
+
return {
|
|
3884
|
+
meetingId: meeting.meetingId,
|
|
3885
|
+
meetingTitle: meeting.meetingTitle || meeting.meetingId,
|
|
3886
|
+
mode: "meeting"
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3324
3889
|
function renderExportScopeLabel(scope) {
|
|
3325
|
-
|
|
3890
|
+
if (scope.mode === "folder") return `folder ${scope.folderName}`;
|
|
3891
|
+
if (scope.mode === "meeting") return `meeting ${scope.meetingTitle}`;
|
|
3892
|
+
return "all meetings";
|
|
3326
3893
|
}
|
|
3327
3894
|
function resolveExportOutputDir(outputDir, scope, options = {}) {
|
|
3328
|
-
if (
|
|
3329
|
-
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"));
|
|
3330
3898
|
}
|
|
3331
3899
|
//#endregion
|
|
3332
3900
|
//#region src/export-jobs.ts
|
|
@@ -3684,6 +4252,16 @@ function normaliseMeeting(meeting) {
|
|
|
3684
4252
|
function meetingChanged(previous, next) {
|
|
3685
4253
|
return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
|
|
3686
4254
|
}
|
|
4255
|
+
function cloneFolderSummary$1(folder) {
|
|
4256
|
+
return { ...folder };
|
|
4257
|
+
}
|
|
4258
|
+
function cloneMeetingSummary$1(meeting) {
|
|
4259
|
+
return {
|
|
4260
|
+
...meeting,
|
|
4261
|
+
folders: meeting.folders.map((folder) => cloneFolderSummary$1(folder)),
|
|
4262
|
+
tags: [...meeting.tags]
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
3687
4265
|
function diffMeetingSummaries(previous, next, folderCount) {
|
|
3688
4266
|
const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
|
|
3689
4267
|
const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
|
|
@@ -3756,15 +4334,20 @@ function diffMeetingSummaries(previous, next, folderCount) {
|
|
|
3756
4334
|
}
|
|
3757
4335
|
};
|
|
3758
4336
|
}
|
|
3759
|
-
function buildSyncEvents(runId, occurredAt, changes) {
|
|
4337
|
+
function buildSyncEvents(runId, occurredAt, changes, previousMeetings, nextMeetings) {
|
|
4338
|
+
const previousById = new Map(previousMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
|
|
4339
|
+
const nextById = new Map(nextMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
|
|
3760
4340
|
return changes.map((change, index) => ({
|
|
4341
|
+
folders: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.folders.map((folder) => cloneFolderSummary$1(folder)) ?? [],
|
|
3761
4342
|
id: `${runId}:${index + 1}`,
|
|
3762
4343
|
kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
|
|
3763
4344
|
meetingId: change.meetingId,
|
|
3764
4345
|
occurredAt,
|
|
3765
4346
|
previousUpdatedAt: change.previousUpdatedAt,
|
|
3766
4347
|
runId,
|
|
4348
|
+
tags: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.tags.slice() ?? [],
|
|
3767
4349
|
title: change.title,
|
|
4350
|
+
transcriptLoaded: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.transcriptLoaded ?? false,
|
|
3768
4351
|
updatedAt: change.updatedAt
|
|
3769
4352
|
}));
|
|
3770
4353
|
}
|
|
@@ -3811,9 +4394,11 @@ function cloneMeetingSummary(meeting) {
|
|
|
3811
4394
|
function cloneState(state) {
|
|
3812
4395
|
return {
|
|
3813
4396
|
auth: { ...state.auth },
|
|
4397
|
+
automation: { ...state.automation },
|
|
3814
4398
|
cache: { ...state.cache },
|
|
3815
4399
|
config: {
|
|
3816
4400
|
...state.config,
|
|
4401
|
+
automation: state.config.automation ? { ...state.config.automation } : void 0,
|
|
3817
4402
|
notes: { ...state.config.notes },
|
|
3818
4403
|
transcripts: { ...state.config.transcripts }
|
|
3819
4404
|
},
|
|
@@ -3832,6 +4417,16 @@ function cloneState(state) {
|
|
|
3832
4417
|
function defaultState(config, auth, surface) {
|
|
3833
4418
|
return {
|
|
3834
4419
|
auth: { ...auth },
|
|
4420
|
+
automation: {
|
|
4421
|
+
loaded: false,
|
|
4422
|
+
matchCount: 0,
|
|
4423
|
+
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4424
|
+
pendingRunCount: 0,
|
|
4425
|
+
ruleCount: 0,
|
|
4426
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4427
|
+
runCount: 0,
|
|
4428
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4429
|
+
},
|
|
3835
4430
|
cache: {
|
|
3836
4431
|
configured: Boolean(config.transcripts.cacheFile),
|
|
3837
4432
|
documentCount: 0,
|
|
@@ -3873,6 +4468,9 @@ function defaultState(config, auth, surface) {
|
|
|
3873
4468
|
};
|
|
3874
4469
|
}
|
|
3875
4470
|
var GranolaApp = class {
|
|
4471
|
+
#automationActionRuns;
|
|
4472
|
+
#automationMatches;
|
|
4473
|
+
#automationRules;
|
|
3876
4474
|
#cacheData;
|
|
3877
4475
|
#cacheResolved = false;
|
|
3878
4476
|
#folders;
|
|
@@ -3886,7 +4484,26 @@ var GranolaApp = class {
|
|
|
3886
4484
|
this.config = config;
|
|
3887
4485
|
this.deps = deps;
|
|
3888
4486
|
this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
|
|
4487
|
+
this.#automationMatches = (deps.automationMatches ?? []).map((match) => ({
|
|
4488
|
+
...match,
|
|
4489
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
4490
|
+
tags: [...match.tags]
|
|
4491
|
+
}));
|
|
4492
|
+
this.#automationActionRuns = (deps.automationRuns ?? []).map((run) => this.cloneAutomationRun(run));
|
|
4493
|
+
this.#automationRules = (deps.automationRules ?? []).map((rule) => this.cloneAutomationRule(rule));
|
|
3889
4494
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
4495
|
+
this.#state.automation = {
|
|
4496
|
+
lastRunAt: this.#automationActionRuns[0]?.finishedAt ?? this.#automationActionRuns[0]?.startedAt,
|
|
4497
|
+
lastMatchedAt: this.#automationMatches[0]?.matchedAt,
|
|
4498
|
+
loaded: true,
|
|
4499
|
+
matchCount: this.#automationMatches.length,
|
|
4500
|
+
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4501
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4502
|
+
ruleCount: this.#automationRules.length,
|
|
4503
|
+
rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4504
|
+
runCount: this.#automationActionRuns.length,
|
|
4505
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
4506
|
+
};
|
|
3890
4507
|
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
3891
4508
|
this.#state.index = {
|
|
3892
4509
|
available: this.#meetingIndex.length > 0,
|
|
@@ -3959,6 +4576,93 @@ var GranolaApp = class {
|
|
|
3959
4576
|
if (!this.deps.syncStateStore) return;
|
|
3960
4577
|
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3961
4578
|
}
|
|
4579
|
+
cloneAutomationRule(rule) {
|
|
4580
|
+
return {
|
|
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
|
+
}),
|
|
4594
|
+
when: {
|
|
4595
|
+
...rule.when,
|
|
4596
|
+
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
4597
|
+
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
4598
|
+
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
4599
|
+
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
4600
|
+
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
4601
|
+
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
4602
|
+
}
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
cloneAutomationMatch(match) {
|
|
4606
|
+
return {
|
|
4607
|
+
...match,
|
|
4608
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
4609
|
+
tags: [...match.tags]
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
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);
|
|
4627
|
+
this.#state.automation = {
|
|
4628
|
+
...this.#state.automation,
|
|
4629
|
+
lastMatchedAt: latestMatch?.matchedAt ?? this.#state.automation.lastMatchedAt,
|
|
4630
|
+
lastRunAt: latestRun?.finishedAt ?? latestRun?.startedAt ?? this.#state.automation.lastRunAt,
|
|
4631
|
+
loaded: true,
|
|
4632
|
+
matchCount: this.#automationMatches.length,
|
|
4633
|
+
matchesFile: defaultAutomationMatchesFilePath(),
|
|
4634
|
+
pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
|
|
4635
|
+
ruleCount: this.#automationRules.length,
|
|
4636
|
+
rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
|
|
4637
|
+
runCount: this.#automationActionRuns.length,
|
|
4638
|
+
runsFile: defaultAutomationRunsFilePath()
|
|
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();
|
|
4646
|
+
this.emitStateUpdate();
|
|
4647
|
+
return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
|
|
4648
|
+
}
|
|
4649
|
+
async appendAutomationMatches(matches) {
|
|
4650
|
+
if (matches.length === 0) return;
|
|
4651
|
+
if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
|
|
4652
|
+
this.#automationMatches.push(...matches.map((match) => this.cloneAutomationMatch(match)));
|
|
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();
|
|
4665
|
+
}
|
|
3962
4666
|
createSyncRunId() {
|
|
3963
4667
|
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
3964
4668
|
}
|
|
@@ -4190,6 +4894,46 @@ var GranolaApp = class {
|
|
|
4190
4894
|
if (!this.deps.syncEventStore) return { events: [] };
|
|
4191
4895
|
return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
|
|
4192
4896
|
}
|
|
4897
|
+
async listAutomationRules() {
|
|
4898
|
+
const rules = await this.loadAutomationRules({ forceRefresh: true });
|
|
4899
|
+
this.setUiState({ view: "idle" });
|
|
4900
|
+
return { rules: rules.map((rule) => this.cloneAutomationRule(rule)) };
|
|
4901
|
+
}
|
|
4902
|
+
async listAutomationMatches(options = {}) {
|
|
4903
|
+
const limit = options.limit ?? 20;
|
|
4904
|
+
const matches = this.deps.automationMatchStore ? await this.deps.automationMatchStore.readMatches(limit) : this.#automationMatches.slice(-limit).reverse();
|
|
4905
|
+
this.setUiState({ view: "idle" });
|
|
4906
|
+
return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
|
|
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
|
+
}
|
|
4193
4937
|
async loginAuth(options = {}) {
|
|
4194
4938
|
const controller = this.requireAuthController();
|
|
4195
4939
|
try {
|
|
@@ -4239,6 +4983,165 @@ var GranolaApp = class {
|
|
|
4239
4983
|
throw error;
|
|
4240
4984
|
}
|
|
4241
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
|
+
}
|
|
4242
5145
|
async runSync(options) {
|
|
4243
5146
|
const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
|
|
4244
5147
|
this.#state.sync = {
|
|
@@ -4255,8 +5158,12 @@ var GranolaApp = class {
|
|
|
4255
5158
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
4256
5159
|
const completedAt = this.nowIso();
|
|
4257
5160
|
const runId = this.createSyncRunId();
|
|
4258
|
-
const events = buildSyncEvents(runId, completedAt, changes);
|
|
5161
|
+
const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
|
|
4259
5162
|
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
5163
|
+
const rules = await this.loadAutomationRules();
|
|
5164
|
+
const automationMatches = matchAutomationRules(rules, events, completedAt);
|
|
5165
|
+
await this.appendAutomationMatches(automationMatches);
|
|
5166
|
+
await this.runAutomationActions(rules, automationMatches);
|
|
4260
5167
|
this.#state.sync = {
|
|
4261
5168
|
...this.#state.sync,
|
|
4262
5169
|
eventCount: this.#state.sync.eventCount + events.length,
|
|
@@ -4428,40 +5335,46 @@ var GranolaApp = class {
|
|
|
4428
5335
|
source: "live"
|
|
4429
5336
|
};
|
|
4430
5337
|
}
|
|
4431
|
-
async
|
|
5338
|
+
async readMeetingBundleById(id, options = {}) {
|
|
4432
5339
|
const documents = await this.listDocuments();
|
|
4433
5340
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4434
5341
|
const folders = await this.loadFolders();
|
|
4435
5342
|
const document = resolveMeeting(documents, id);
|
|
4436
|
-
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
4437
|
-
this.setUiState({
|
|
4438
|
-
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
4439
|
-
selectedMeetingId: document.id,
|
|
4440
|
-
view: "meeting-detail"
|
|
4441
|
-
});
|
|
4442
5343
|
return {
|
|
4443
5344
|
cacheData,
|
|
4444
5345
|
document,
|
|
4445
|
-
meeting
|
|
5346
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4446
5347
|
};
|
|
4447
5348
|
}
|
|
4448
|
-
async
|
|
5349
|
+
async readMeetingBundleByQuery(query, options = {}) {
|
|
4449
5350
|
const documents = await this.listDocuments();
|
|
4450
5351
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
4451
5352
|
const folders = await this.loadFolders();
|
|
4452
5353
|
const document = resolveMeetingQuery(documents, query);
|
|
4453
|
-
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
4454
|
-
this.setUiState({
|
|
4455
|
-
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
4456
|
-
selectedMeetingId: document.id,
|
|
4457
|
-
view: "meeting-detail"
|
|
4458
|
-
});
|
|
4459
5354
|
return {
|
|
4460
5355
|
cacheData,
|
|
4461
5356
|
document,
|
|
4462
|
-
meeting
|
|
5357
|
+
meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
|
|
4463
5358
|
};
|
|
4464
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
|
+
}
|
|
4465
5378
|
async listExportJobs(options = {}) {
|
|
4466
5379
|
const limit = options.limit ?? 20;
|
|
4467
5380
|
const jobs = this.#state.exports.jobs.slice(0, limit).map((job) => cloneExportJob(job));
|
|
@@ -4497,7 +5410,7 @@ var GranolaApp = class {
|
|
|
4497
5410
|
await this.failExportJob(job, error);
|
|
4498
5411
|
throw error;
|
|
4499
5412
|
}
|
|
4500
|
-
this.#state.exports.notes = {
|
|
5413
|
+
if (options.trackLastRun !== false) this.#state.exports.notes = {
|
|
4501
5414
|
format: options.format,
|
|
4502
5415
|
itemCount: options.documents.length,
|
|
4503
5416
|
jobId: job.id,
|
|
@@ -4507,7 +5420,7 @@ var GranolaApp = class {
|
|
|
4507
5420
|
written
|
|
4508
5421
|
};
|
|
4509
5422
|
this.emitStateUpdate();
|
|
4510
|
-
this.setUiState({
|
|
5423
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4511
5424
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4512
5425
|
view: "notes-export"
|
|
4513
5426
|
});
|
|
@@ -4555,7 +5468,7 @@ var GranolaApp = class {
|
|
|
4555
5468
|
await this.failExportJob(job, error);
|
|
4556
5469
|
throw error;
|
|
4557
5470
|
}
|
|
4558
|
-
this.#state.exports.transcripts = {
|
|
5471
|
+
if (options.trackLastRun !== false) this.#state.exports.transcripts = {
|
|
4559
5472
|
format: options.format,
|
|
4560
5473
|
itemCount: count,
|
|
4561
5474
|
jobId: job.id,
|
|
@@ -4565,7 +5478,7 @@ var GranolaApp = class {
|
|
|
4565
5478
|
written
|
|
4566
5479
|
};
|
|
4567
5480
|
this.emitStateUpdate();
|
|
4568
|
-
this.setUiState({
|
|
5481
|
+
if (options.updateUi !== false) this.setUiState({
|
|
4569
5482
|
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
4570
5483
|
view: "transcripts-export"
|
|
4571
5484
|
});
|
|
@@ -4607,6 +5520,12 @@ var GranolaApp = class {
|
|
|
4607
5520
|
};
|
|
4608
5521
|
async function createGranolaApp(config, options = {}) {
|
|
4609
5522
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
5523
|
+
const automationMatchStore = createDefaultAutomationMatchStore();
|
|
5524
|
+
const automationMatches = await automationMatchStore.readMatches(0);
|
|
5525
|
+
const automationRunStore = createDefaultAutomationRunStore();
|
|
5526
|
+
const automationRuns = await automationRunStore.readRuns({ limit: 0 });
|
|
5527
|
+
const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
|
|
5528
|
+
const automationRules = await automationRuleStore.readRules();
|
|
4610
5529
|
const authController = createDefaultGranolaAuthController(config);
|
|
4611
5530
|
const exportJobStore = createDefaultExportJobStore();
|
|
4612
5531
|
const exportJobs = await exportJobStore.readJobs();
|
|
@@ -4618,6 +5537,12 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4618
5537
|
return new GranolaApp(config, {
|
|
4619
5538
|
auth,
|
|
4620
5539
|
authController,
|
|
5540
|
+
automationMatches,
|
|
5541
|
+
automationMatchStore,
|
|
5542
|
+
automationRunStore,
|
|
5543
|
+
automationRuns,
|
|
5544
|
+
automationRules,
|
|
5545
|
+
automationRuleStore,
|
|
4621
5546
|
cacheLoader: loadOptionalGranolaCache,
|
|
4622
5547
|
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
4623
5548
|
exportJobs,
|
|
@@ -4692,6 +5617,7 @@ async function loadConfig(options) {
|
|
|
4692
5617
|
const defaultCache = firstExistingPath(granolaCacheCandidates());
|
|
4693
5618
|
const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
|
|
4694
5619
|
return {
|
|
5620
|
+
automation: { rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile },
|
|
4695
5621
|
apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
|
|
4696
5622
|
configFileUsed: config.path,
|
|
4697
5623
|
debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
|
|
@@ -4771,6 +5697,145 @@ async function waitForShutdown(close) {
|
|
|
4771
5697
|
});
|
|
4772
5698
|
}
|
|
4773
5699
|
//#endregion
|
|
5700
|
+
//#region src/commands/automation.ts
|
|
5701
|
+
function automationHelp() {
|
|
5702
|
+
return `Granola automation
|
|
5703
|
+
|
|
5704
|
+
Usage:
|
|
5705
|
+
granola automation <rules|matches|runs|approve|reject> [options]
|
|
5706
|
+
|
|
5707
|
+
Subcommands:
|
|
5708
|
+
rules List configured automation rules
|
|
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
|
|
5713
|
+
|
|
5714
|
+
Options:
|
|
5715
|
+
--format <value> text, json, yaml (default: text)
|
|
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
|
|
5719
|
+
--rules <path> Path to automation rules JSON
|
|
5720
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
5721
|
+
--supabase <path> Path to supabase.json
|
|
5722
|
+
--debug Enable debug logging
|
|
5723
|
+
--config <path> Path to .granola.toml
|
|
5724
|
+
-h, --help Show help
|
|
5725
|
+
`;
|
|
5726
|
+
}
|
|
5727
|
+
function resolveFormat(value) {
|
|
5728
|
+
switch (value) {
|
|
5729
|
+
case void 0: return "text";
|
|
5730
|
+
case "json":
|
|
5731
|
+
case "text":
|
|
5732
|
+
case "yaml": return value;
|
|
5733
|
+
default: throw new Error("invalid automation format: expected text, json, or yaml");
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
function parseLimit$3(value) {
|
|
5737
|
+
if (value === void 0) return 20;
|
|
5738
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid automation limit: expected a positive integer");
|
|
5739
|
+
return Number(value);
|
|
5740
|
+
}
|
|
5741
|
+
function renderRules(rules, format) {
|
|
5742
|
+
if (format === "json") return toJson({ rules });
|
|
5743
|
+
if (format === "yaml") return toYaml({ rules });
|
|
5744
|
+
if (rules.length === 0) return "No automation rules configured\n";
|
|
5745
|
+
return `${["ID ENABLED EVENTS ACTIONS FILTERS", ...rules.map((rule) => {
|
|
5746
|
+
const filters = [
|
|
5747
|
+
rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
|
|
5748
|
+
rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
|
|
5749
|
+
rule.when.tags?.length ? `tags=${rule.when.tags.join(",")}` : "",
|
|
5750
|
+
rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
|
|
5751
|
+
rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
|
|
5752
|
+
].filter(Boolean).join(" ");
|
|
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 || "-"}`;
|
|
5754
|
+
})].join("\n")}\n`;
|
|
5755
|
+
}
|
|
5756
|
+
function renderMatches(matches, format) {
|
|
5757
|
+
if (format === "json") return toJson({ matches });
|
|
5758
|
+
if (format === "yaml") return toYaml({ matches });
|
|
5759
|
+
if (matches.length === 0) return "No automation matches yet\n";
|
|
5760
|
+
return `${["MATCHED AT RULE EVENT TITLE", ...matches.map((match) => {
|
|
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})`;
|
|
5762
|
+
})].join("\n")}\n`;
|
|
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
|
+
}
|
|
5782
|
+
const automationCommand = {
|
|
5783
|
+
description: "Inspect automation rules and rule matches",
|
|
5784
|
+
flags: {
|
|
5785
|
+
format: { type: "string" },
|
|
5786
|
+
help: { type: "boolean" },
|
|
5787
|
+
limit: { type: "string" },
|
|
5788
|
+
note: { type: "string" },
|
|
5789
|
+
status: { type: "string" },
|
|
5790
|
+
timeout: { type: "string" }
|
|
5791
|
+
},
|
|
5792
|
+
help: automationHelp,
|
|
5793
|
+
name: "automation",
|
|
5794
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
5795
|
+
const [action] = commandArgs;
|
|
5796
|
+
const format = resolveFormat(commandFlags.format);
|
|
5797
|
+
const config = await loadConfig({
|
|
5798
|
+
globalFlags,
|
|
5799
|
+
subcommandFlags: commandFlags
|
|
5800
|
+
});
|
|
5801
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
5802
|
+
debug(config.debug, "automationRules", config.automation?.rulesFile ?? "(default)");
|
|
5803
|
+
const app = await createGranolaApp(config);
|
|
5804
|
+
switch (action) {
|
|
5805
|
+
case "rules": {
|
|
5806
|
+
const result = await app.listAutomationRules();
|
|
5807
|
+
console.log(renderRules(result.rules, format).trimEnd());
|
|
5808
|
+
return 0;
|
|
5809
|
+
}
|
|
5810
|
+
case "matches": {
|
|
5811
|
+
const result = await app.listAutomationMatches({ limit: parseLimit$3(commandFlags.limit) });
|
|
5812
|
+
console.log(renderMatches(result.matches, format).trimEnd());
|
|
5813
|
+
return 0;
|
|
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
|
+
}
|
|
5831
|
+
case void 0:
|
|
5832
|
+
console.log(automationHelp());
|
|
5833
|
+
return 1;
|
|
5834
|
+
default: throw new Error("invalid automation command: expected rules, matches, runs, approve, or reject");
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
};
|
|
5838
|
+
//#endregion
|
|
4774
5839
|
//#region src/commands/auth.ts
|
|
4775
5840
|
function authHelp() {
|
|
4776
5841
|
return `Granola auth
|
|
@@ -6719,6 +7784,9 @@ var granolaTransportPaths = {
|
|
|
6719
7784
|
authRefresh: "/auth/refresh",
|
|
6720
7785
|
authStatus: "/auth/status",
|
|
6721
7786
|
authUnlock: "/auth/unlock",
|
|
7787
|
+
automationMatches: "/automation/matches",
|
|
7788
|
+
automationRules: "/automation/rules",
|
|
7789
|
+
automationRuns: "/automation/runs",
|
|
6722
7790
|
events: "/events",
|
|
6723
7791
|
exportJobs: "/exports/jobs",
|
|
6724
7792
|
exportNotes: "/exports/notes",
|
|
@@ -6778,6 +7846,15 @@ function granolaFoldersPath(options = {}) {
|
|
|
6778
7846
|
function granolaExportJobsPath(options = {}) {
|
|
6779
7847
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
6780
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
|
+
}
|
|
6781
7858
|
function granolaExportJobRerunPath(id) {
|
|
6782
7859
|
return \`\${granolaTransportPaths.exportJobs}/\${encodeURIComponent(id)}/rerun\`;
|
|
6783
7860
|
}
|
|
@@ -6952,6 +8029,23 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
6952
8029
|
async inspectAuth() {
|
|
6953
8030
|
return await this.requestJson(granolaTransportPaths.authStatus);
|
|
6954
8031
|
}
|
|
8032
|
+
async listAutomationRules() {
|
|
8033
|
+
return await this.requestJson(granolaTransportPaths.automationRules);
|
|
8034
|
+
}
|
|
8035
|
+
async listAutomationMatches(options = {}) {
|
|
8036
|
+
const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
|
|
8037
|
+
return await this.requestJson(path);
|
|
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
|
+
}
|
|
6955
8049
|
async inspectSync() {
|
|
6956
8050
|
return cloneValue(_classPrivateFieldGet2(_state, this).sync);
|
|
6957
8051
|
}
|
|
@@ -7170,7 +8264,7 @@ function nextWorkspaceTab(currentTab, key) {
|
|
|
7170
8264
|
//#endregion
|
|
7171
8265
|
//#region src/web-app/components.tsx
|
|
7172
8266
|
/** @jsxImportSource solid-js */
|
|
7173
|
-
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>\`);
|
|
7174
8268
|
function authModeLabel(mode) {
|
|
7175
8269
|
switch (mode) {
|
|
7176
8270
|
case "api-key": return "API key";
|
|
@@ -7387,7 +8481,7 @@ function AppStatePanel(props) {
|
|
|
7387
8481
|
return props.appState;
|
|
7388
8482
|
},
|
|
7389
8483
|
children: (appState) => (() => {
|
|
7390
|
-
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;
|
|
7391
8485
|
insert(_el$42, () => appState().ui.surface);
|
|
7392
8486
|
insert(_el$45, () => appState().ui.view);
|
|
7393
8487
|
insert(_el$48, authStatus);
|
|
@@ -7408,6 +8502,7 @@ function AppStatePanel(props) {
|
|
|
7408
8502
|
var _c$7 = memo(() => !!appState().index.loaded);
|
|
7409
8503
|
return () => _c$7() ? \`\${appState().index.meetingCount} meetings\` : appState().index.available ? "available" : "not built";
|
|
7410
8504
|
})());
|
|
8505
|
+
insert(_el$66, () => \`\${appState().automation.runCount} runs / \${appState().automation.pendingRunCount} pending\`);
|
|
7411
8506
|
return _el$39;
|
|
7412
8507
|
})()
|
|
7413
8508
|
}), null);
|
|
@@ -7422,27 +8517,27 @@ function SecurityPanel(props) {
|
|
|
7422
8517
|
return props.visible;
|
|
7423
8518
|
},
|
|
7424
8519
|
get children() {
|
|
7425
|
-
var _el$
|
|
7426
|
-
_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) => {
|
|
7427
8522
|
if (event.key === "Enter") {
|
|
7428
8523
|
event.preventDefault();
|
|
7429
8524
|
props.onUnlock();
|
|
7430
8525
|
}
|
|
7431
8526
|
};
|
|
7432
|
-
_el$
|
|
8527
|
+
_el$70.$$input = (event) => {
|
|
7433
8528
|
props.onPasswordChange(event.currentTarget.value);
|
|
7434
8529
|
};
|
|
7435
|
-
addEventListener(_el$
|
|
7436
|
-
addEventListener(_el$
|
|
7437
|
-
createRenderEffect(() => _el$
|
|
7438
|
-
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;
|
|
7439
8534
|
}
|
|
7440
8535
|
});
|
|
7441
8536
|
}
|
|
7442
8537
|
function AuthPanel(props) {
|
|
7443
8538
|
return (() => {
|
|
7444
|
-
var _el$
|
|
7445
|
-
insert(_el$
|
|
8539
|
+
var _el$74 = _tmpl$14(), _el$76 = _el$74.firstChild.nextSibling;
|
|
8540
|
+
insert(_el$76, createComponent(Show, {
|
|
7446
8541
|
get fallback() {
|
|
7447
8542
|
return _tmpl$15();
|
|
7448
8543
|
},
|
|
@@ -7450,79 +8545,79 @@ function AuthPanel(props) {
|
|
|
7450
8545
|
return props.auth;
|
|
7451
8546
|
},
|
|
7452
8547
|
children: (auth) => (() => {
|
|
7453
|
-
var _el$
|
|
7454
|
-
insert(_el$
|
|
7455
|
-
insert(_el$
|
|
7456
|
-
insert(_el$
|
|
7457
|
-
insert(_el$
|
|
7458
|
-
insert(_el$
|
|
7459
|
-
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, {
|
|
7460
8555
|
get when() {
|
|
7461
8556
|
return auth().clientId;
|
|
7462
8557
|
},
|
|
7463
8558
|
get children() {
|
|
7464
|
-
var _el$
|
|
7465
|
-
_el$
|
|
7466
|
-
insert(_el$
|
|
7467
|
-
return _el$
|
|
8559
|
+
var _el$95 = _tmpl$16();
|
|
8560
|
+
_el$95.firstChild;
|
|
8561
|
+
insert(_el$95, () => auth().clientId, null);
|
|
8562
|
+
return _el$95;
|
|
7468
8563
|
}
|
|
7469
|
-
}), _el$
|
|
7470
|
-
insert(_el$
|
|
8564
|
+
}), _el$102);
|
|
8565
|
+
insert(_el$78, createComponent(Show, {
|
|
7471
8566
|
get when() {
|
|
7472
8567
|
return auth().signInMethod;
|
|
7473
8568
|
},
|
|
7474
8569
|
get children() {
|
|
7475
|
-
var _el$
|
|
7476
|
-
_el$
|
|
7477
|
-
insert(_el$
|
|
7478
|
-
return _el$
|
|
8570
|
+
var _el$97 = _tmpl$17();
|
|
8571
|
+
_el$97.firstChild;
|
|
8572
|
+
insert(_el$97, () => auth().signInMethod, null);
|
|
8573
|
+
return _el$97;
|
|
7479
8574
|
}
|
|
7480
|
-
}), _el$
|
|
7481
|
-
insert(_el$
|
|
8575
|
+
}), _el$102);
|
|
8576
|
+
insert(_el$78, createComponent(Show, {
|
|
7482
8577
|
get when() {
|
|
7483
8578
|
return auth().supabasePath;
|
|
7484
8579
|
},
|
|
7485
8580
|
get children() {
|
|
7486
|
-
var _el$
|
|
7487
|
-
_el$
|
|
7488
|
-
insert(_el$
|
|
7489
|
-
return _el$
|
|
8581
|
+
var _el$99 = _tmpl$18();
|
|
8582
|
+
_el$99.firstChild;
|
|
8583
|
+
insert(_el$99, () => auth().supabasePath, null);
|
|
8584
|
+
return _el$99;
|
|
7490
8585
|
}
|
|
7491
|
-
}), _el$
|
|
7492
|
-
insert(_el$
|
|
8586
|
+
}), _el$102);
|
|
8587
|
+
insert(_el$78, createComponent(Show, {
|
|
7493
8588
|
get when() {
|
|
7494
8589
|
return auth().lastError;
|
|
7495
8590
|
},
|
|
7496
8591
|
get children() {
|
|
7497
|
-
var _el$
|
|
7498
|
-
insert(_el$
|
|
7499
|
-
return _el$
|
|
8592
|
+
var _el$101 = _tmpl$19();
|
|
8593
|
+
insert(_el$101, () => auth().lastError);
|
|
8594
|
+
return _el$101;
|
|
7500
8595
|
}
|
|
7501
|
-
}), _el$
|
|
7502
|
-
_el$
|
|
8596
|
+
}), _el$102);
|
|
8597
|
+
_el$104.$$input = (event) => {
|
|
7503
8598
|
props.onApiKeyDraftChange(event.currentTarget.value);
|
|
7504
8599
|
};
|
|
7505
|
-
addEventListener(_el$
|
|
7506
|
-
addEventListener(_el$
|
|
7507
|
-
addEventListener(_el$
|
|
7508
|
-
_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 = () => {
|
|
7509
8604
|
props.onSwitchMode("api-key");
|
|
7510
8605
|
};
|
|
7511
|
-
_el$
|
|
8606
|
+
_el$109.$$click = () => {
|
|
7512
8607
|
props.onSwitchMode("stored-session");
|
|
7513
8608
|
};
|
|
7514
|
-
_el$
|
|
8609
|
+
_el$110.$$click = () => {
|
|
7515
8610
|
props.onSwitchMode("supabase-file");
|
|
7516
8611
|
};
|
|
7517
|
-
addEventListener(_el$
|
|
8612
|
+
addEventListener(_el$111, "click", props.onLogout, true);
|
|
7518
8613
|
createRenderEffect((_p$) => {
|
|
7519
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;
|
|
7520
|
-
_v$ !== _p$.e && (_el$
|
|
7521
|
-
_v$2 !== _p$.t && (_el$
|
|
7522
|
-
_v$3 !== _p$.a && (_el$
|
|
7523
|
-
_v$4 !== _p$.o && (_el$
|
|
7524
|
-
_v$5 !== _p$.i && (_el$
|
|
7525
|
-
_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);
|
|
7526
8621
|
return _p$;
|
|
7527
8622
|
}, {
|
|
7528
8623
|
e: void 0,
|
|
@@ -7532,17 +8627,17 @@ function AuthPanel(props) {
|
|
|
7532
8627
|
i: void 0,
|
|
7533
8628
|
n: void 0
|
|
7534
8629
|
});
|
|
7535
|
-
createRenderEffect(() => _el$
|
|
7536
|
-
return _el$
|
|
8630
|
+
createRenderEffect(() => _el$104.value = props.apiKeyDraft);
|
|
8631
|
+
return _el$78;
|
|
7537
8632
|
})()
|
|
7538
8633
|
}));
|
|
7539
|
-
return _el$
|
|
8634
|
+
return _el$74;
|
|
7540
8635
|
})();
|
|
7541
8636
|
}
|
|
7542
8637
|
function ExportJobsPanel(props) {
|
|
7543
8638
|
return (() => {
|
|
7544
|
-
var _el$
|
|
7545
|
-
insert(_el$
|
|
8639
|
+
var _el$112 = _tmpl$21(), _el$114 = _el$112.firstChild.nextSibling;
|
|
8640
|
+
insert(_el$114, createComponent(Show, {
|
|
7546
8641
|
get when() {
|
|
7547
8642
|
return props.jobs.length > 0;
|
|
7548
8643
|
},
|
|
@@ -7555,46 +8650,127 @@ function ExportJobsPanel(props) {
|
|
|
7555
8650
|
return props.jobs.slice(0, 6);
|
|
7556
8651
|
},
|
|
7557
8652
|
children: (job) => (() => {
|
|
7558
|
-
var _el$
|
|
7559
|
-
_el$
|
|
7560
|
-
var _el$
|
|
7561
|
-
_el$
|
|
7562
|
-
var _el$
|
|
7563
|
-
insert(_el$
|
|
7564
|
-
insert(_el$
|
|
7565
|
-
insert(_el$
|
|
7566
|
-
insert(_el$
|
|
7567
|
-
insert(_el$
|
|
7568
|
-
insert(_el$
|
|
7569
|
-
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, {
|
|
7570
8665
|
get when() {
|
|
7571
8666
|
return job.error;
|
|
7572
8667
|
},
|
|
7573
8668
|
get children() {
|
|
7574
|
-
var _el$
|
|
7575
|
-
insert(_el$
|
|
7576
|
-
return _el$
|
|
8669
|
+
var _el$128 = _tmpl$23();
|
|
8670
|
+
insert(_el$128, () => job.error);
|
|
8671
|
+
return _el$128;
|
|
7577
8672
|
}
|
|
7578
|
-
}), _el$
|
|
7579
|
-
insert(_el$
|
|
8673
|
+
}), _el$129);
|
|
8674
|
+
insert(_el$129, createComponent(Show, {
|
|
7580
8675
|
get when() {
|
|
7581
8676
|
return job.status !== "running";
|
|
7582
8677
|
},
|
|
7583
8678
|
get children() {
|
|
7584
|
-
var _el$
|
|
7585
|
-
_el$
|
|
8679
|
+
var _el$130 = _tmpl$24();
|
|
8680
|
+
_el$130.$$click = () => {
|
|
7586
8681
|
props.onRerun(job.id);
|
|
7587
8682
|
};
|
|
7588
|
-
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
|
+
})()];
|
|
7589
8765
|
}
|
|
7590
8766
|
}));
|
|
7591
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7592
|
-
return _el$
|
|
8767
|
+
createRenderEffect(() => setAttribute(_el$140, "data-status", run.status));
|
|
8768
|
+
return _el$135;
|
|
7593
8769
|
})()
|
|
7594
8770
|
});
|
|
7595
8771
|
}
|
|
7596
8772
|
}));
|
|
7597
|
-
return _el$
|
|
8773
|
+
return _el$131;
|
|
7598
8774
|
})();
|
|
7599
8775
|
}
|
|
7600
8776
|
function Workspace(props) {
|
|
@@ -7604,8 +8780,8 @@ function Workspace(props) {
|
|
|
7604
8780
|
return workspaceBody(props.bundle, props.selectedMeeting, parsedTab());
|
|
7605
8781
|
};
|
|
7606
8782
|
return [(() => {
|
|
7607
|
-
var _el$
|
|
7608
|
-
insert(_el$
|
|
8783
|
+
var _el$149 = _tmpl$31(), _el$150 = _el$149.firstChild;
|
|
8784
|
+
insert(_el$149, createComponent(For, {
|
|
7609
8785
|
each: [
|
|
7610
8786
|
"notes",
|
|
7611
8787
|
"transcript",
|
|
@@ -7613,50 +8789,50 @@ function Workspace(props) {
|
|
|
7613
8789
|
"raw"
|
|
7614
8790
|
],
|
|
7615
8791
|
children: (tab) => (() => {
|
|
7616
|
-
var _el$
|
|
7617
|
-
_el$
|
|
8792
|
+
var _el$151 = _tmpl$32();
|
|
8793
|
+
_el$151.$$click = () => {
|
|
7618
8794
|
props.onSelectTab(tab);
|
|
7619
8795
|
};
|
|
7620
|
-
insert(_el$
|
|
7621
|
-
createRenderEffect(() => setAttribute(_el$
|
|
7622
|
-
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;
|
|
7623
8799
|
})()
|
|
7624
|
-
}), _el$
|
|
7625
|
-
return _el$
|
|
8800
|
+
}), _el$150);
|
|
8801
|
+
return _el$149;
|
|
7626
8802
|
})(), createComponent(Show, {
|
|
7627
8803
|
get when() {
|
|
7628
8804
|
return props.selectedMeeting;
|
|
7629
8805
|
},
|
|
7630
8806
|
get fallback() {
|
|
7631
8807
|
return (() => {
|
|
7632
|
-
var _el$
|
|
7633
|
-
insert(_el$
|
|
7634
|
-
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;
|
|
7635
8811
|
})();
|
|
7636
8812
|
},
|
|
7637
8813
|
children: (meeting) => [(() => {
|
|
7638
|
-
var _el$
|
|
7639
|
-
insert(_el$
|
|
7640
|
-
insert(_el$
|
|
7641
|
-
insert(_el$
|
|
7642
|
-
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;
|
|
7643
8819
|
})(), createComponent(Show, {
|
|
7644
8820
|
get when() {
|
|
7645
8821
|
return !props.detailError;
|
|
7646
8822
|
},
|
|
7647
8823
|
get fallback() {
|
|
7648
8824
|
return (() => {
|
|
7649
|
-
var _el$
|
|
7650
|
-
insert(_el$
|
|
7651
|
-
return _el$
|
|
8825
|
+
var _el$165 = _tmpl$33();
|
|
8826
|
+
insert(_el$165, () => props.detailError);
|
|
8827
|
+
return _el$165;
|
|
7652
8828
|
})();
|
|
7653
8829
|
},
|
|
7654
8830
|
get children() {
|
|
7655
|
-
var _el$
|
|
7656
|
-
insert(_el$
|
|
7657
|
-
insert(_el$
|
|
7658
|
-
insert(_el$
|
|
7659
|
-
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;
|
|
7660
8836
|
}
|
|
7661
8837
|
})]
|
|
7662
8838
|
})];
|
|
@@ -7687,6 +8863,7 @@ function App() {
|
|
|
7687
8863
|
const [state, setState] = createStore({
|
|
7688
8864
|
apiKeyDraft: "",
|
|
7689
8865
|
appState: null,
|
|
8866
|
+
automationRuns: [],
|
|
7690
8867
|
detailError: "",
|
|
7691
8868
|
folderError: "",
|
|
7692
8869
|
folders: [],
|
|
@@ -7755,6 +8932,14 @@ function App() {
|
|
|
7755
8932
|
setState("selectedFolderId", null);
|
|
7756
8933
|
}
|
|
7757
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
|
+
};
|
|
7758
8943
|
const loadMeeting = async (meetingId) => {
|
|
7759
8944
|
if (!client) return;
|
|
7760
8945
|
setState("selectedMeetingId", meetingId);
|
|
@@ -7808,7 +8993,11 @@ function App() {
|
|
|
7808
8993
|
forceRefresh: true,
|
|
7809
8994
|
foreground: true
|
|
7810
8995
|
});
|
|
7811
|
-
await Promise.all([
|
|
8996
|
+
await Promise.all([
|
|
8997
|
+
loadFolders(forceRefresh),
|
|
8998
|
+
loadAutomationRuns(),
|
|
8999
|
+
mergeAuthState()
|
|
9000
|
+
]);
|
|
7812
9001
|
await loadMeetings({ refresh: forceRefresh });
|
|
7813
9002
|
setState("serverLocked", false);
|
|
7814
9003
|
setStatus(forceRefresh ? "Sync complete" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
|
|
@@ -7941,6 +9130,17 @@ function App() {
|
|
|
7941
9130
|
setStatus("Rerun failed", "error");
|
|
7942
9131
|
}
|
|
7943
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
|
+
};
|
|
7944
9144
|
const unlockServer = async () => {
|
|
7945
9145
|
if (!state.serverPassword.trim()) {
|
|
7946
9146
|
setStatus("Enter the server password", "error");
|
|
@@ -7968,6 +9168,7 @@ function App() {
|
|
|
7968
9168
|
await detachClient();
|
|
7969
9169
|
setState({
|
|
7970
9170
|
appState: null,
|
|
9171
|
+
automationRuns: [],
|
|
7971
9172
|
detailError: "",
|
|
7972
9173
|
folderError: "",
|
|
7973
9174
|
folders: [],
|
|
@@ -7990,6 +9191,10 @@ function App() {
|
|
|
7990
9191
|
});
|
|
7991
9192
|
if (nextPath !== \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`) history.replaceState(null, "", nextPath);
|
|
7992
9193
|
});
|
|
9194
|
+
createEffect(() => {
|
|
9195
|
+
if (!state.appState?.automation.loaded || !client) return;
|
|
9196
|
+
loadAutomationRuns();
|
|
9197
|
+
});
|
|
7993
9198
|
onMount(() => {
|
|
7994
9199
|
const onKeyDown = (event) => {
|
|
7995
9200
|
const target = event.target;
|
|
@@ -8165,6 +9370,17 @@ function App() {
|
|
|
8165
9370
|
rerunJob(jobId);
|
|
8166
9371
|
}
|
|
8167
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);
|
|
8168
9384
|
insert(_el$3, createComponent(Workspace, {
|
|
8169
9385
|
get bundle() {
|
|
8170
9386
|
return state.selectedMeetingBundle;
|
|
@@ -8262,6 +9478,17 @@ function parseAuthMode(value) {
|
|
|
8262
9478
|
default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
|
|
8263
9479
|
}
|
|
8264
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
|
+
}
|
|
8265
9492
|
function folderIdFromBody(value) {
|
|
8266
9493
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8267
9494
|
}
|
|
@@ -8388,6 +9615,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8388
9615
|
capabilities: {
|
|
8389
9616
|
attach: true,
|
|
8390
9617
|
auth: true,
|
|
9618
|
+
automation: true,
|
|
8391
9619
|
events: true,
|
|
8392
9620
|
exports: true,
|
|
8393
9621
|
folders: true,
|
|
@@ -8508,6 +9736,28 @@ async function startGranolaServer(app, options = {}) {
|
|
|
8508
9736
|
sendJson(response, await app.inspectAuth(), { headers: originHeaders });
|
|
8509
9737
|
return;
|
|
8510
9738
|
}
|
|
9739
|
+
if (method === "GET" && path === granolaTransportPaths.automationRules) {
|
|
9740
|
+
sendJson(response, await app.listAutomationRules(), { headers: originHeaders });
|
|
9741
|
+
return;
|
|
9742
|
+
}
|
|
9743
|
+
if (method === "GET" && path === granolaTransportPaths.automationMatches) {
|
|
9744
|
+
sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
|
|
9745
|
+
return;
|
|
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
|
+
}
|
|
8511
9761
|
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
8512
9762
|
const body = await readJsonBody(request);
|
|
8513
9763
|
sendJson(response, await app.sync({
|
|
@@ -9515,6 +10765,7 @@ Options:
|
|
|
9515
10765
|
//#region src/commands/index.ts
|
|
9516
10766
|
const commands = [
|
|
9517
10767
|
attachCommand,
|
|
10768
|
+
automationCommand,
|
|
9518
10769
|
authCommand,
|
|
9519
10770
|
exportsCommand,
|
|
9520
10771
|
folderCommand,
|
|
@@ -9645,6 +10896,7 @@ Global options:
|
|
|
9645
10896
|
--api-key <token> Granola Personal API key
|
|
9646
10897
|
--config <path> Path to .granola.toml
|
|
9647
10898
|
--debug Enable debug logging
|
|
10899
|
+
--rules <path> Path to automation rules JSON
|
|
9648
10900
|
--supabase <path> Path to supabase.json
|
|
9649
10901
|
-h, --help Show help
|
|
9650
10902
|
|
|
@@ -9664,6 +10916,7 @@ async function runCli(argv) {
|
|
|
9664
10916
|
config: { type: "string" },
|
|
9665
10917
|
debug: { type: "boolean" },
|
|
9666
10918
|
help: { type: "boolean" },
|
|
10919
|
+
rules: { type: "string" },
|
|
9667
10920
|
supabase: { type: "string" }
|
|
9668
10921
|
});
|
|
9669
10922
|
if (global.values.help && !command) {
|