granola-toolkit 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +1031 -166
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
6
6
  import { existsSync } from "node:fs";
7
7
  import { homedir, platform } from "node:os";
8
8
  import { NodeHtmlMarkdown } from "node-html-markdown";
9
- import { execFile } from "node:child_process";
9
+ import { execFile, spawn } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  import { createServer } from "node:http";
12
12
  //#region src/transport.ts
@@ -20,6 +20,7 @@ const granolaTransportPaths = {
20
20
  authUnlock: "/auth/unlock",
21
21
  automationMatches: "/automation/matches",
22
22
  automationRules: "/automation/rules",
23
+ automationRuns: "/automation/runs",
23
24
  events: "/events",
24
25
  exportJobs: "/exports/jobs",
25
26
  exportNotes: "/exports/notes",
@@ -79,6 +80,15 @@ function granolaFoldersPath(options = {}) {
79
80
  function granolaExportJobsPath(options = {}) {
80
81
  return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
81
82
  }
83
+ function granolaAutomationRunsPath(options = {}) {
84
+ return appendSearchParams(granolaTransportPaths.automationRuns, {
85
+ limit: options.limit,
86
+ status: options.status
87
+ });
88
+ }
89
+ function granolaAutomationRunDecisionPath(id, decision) {
90
+ return `${granolaTransportPaths.automationRuns}/${encodeURIComponent(id)}/${decision}`;
91
+ }
82
92
  function granolaExportJobRerunPath(id) {
83
93
  return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
84
94
  }
@@ -188,6 +198,16 @@ var GranolaServerClient = class GranolaServerClient {
188
198
  const path = options.limit ? `${granolaTransportPaths.automationMatches}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.automationMatches;
189
199
  return await this.requestJson(path);
190
200
  }
201
+ async listAutomationRuns(options = {}) {
202
+ return await this.requestJson(granolaAutomationRunsPath(options));
203
+ }
204
+ async resolveAutomationRun(id, decision, options = {}) {
205
+ return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
206
+ body: JSON.stringify(options),
207
+ headers: { "content-type": "application/json" },
208
+ method: "POST"
209
+ });
210
+ }
191
211
  async inspectSync() {
192
212
  return cloneValue(this.#state.sync);
193
213
  }
@@ -1392,7 +1412,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
1392
1412
  }
1393
1413
  }
1394
1414
  function buildGranolaTuiSummary(state, meetingSource) {
1395
- return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
1415
+ return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | ${state.automation.pendingRunCount ? `${state.automation.pendingRunCount} pending` : state.automation.runCount ? `${state.automation.runCount} runs` : "automation idle"} | list ${meetingSource}`;
1396
1416
  }
1397
1417
  //#endregion
1398
1418
  //#region src/tui/theme.ts
@@ -1424,6 +1444,81 @@ const granolaTuiTheme = {
1424
1444
  }
1425
1445
  };
1426
1446
  //#endregion
1447
+ //#region src/tui/automation.ts
1448
+ function padLine$3(text, width) {
1449
+ const clipped = truncateToWidth(text, width, "");
1450
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
1451
+ }
1452
+ function frameLine$2(text, width) {
1453
+ return `| ${padLine$3(text, Math.max(1, width - 4))} |`;
1454
+ }
1455
+ function wrapDetails(text, width) {
1456
+ return wrapTextWithAnsi(text, Math.max(1, width - 4));
1457
+ }
1458
+ function statusLabel(run) {
1459
+ switch (run.status) {
1460
+ case "completed": return granolaTuiTheme.info(run.status);
1461
+ case "failed": return granolaTuiTheme.error(run.status);
1462
+ case "pending": return granolaTuiTheme.warning(run.status);
1463
+ default: return granolaTuiTheme.dim(run.status);
1464
+ }
1465
+ }
1466
+ var GranolaTuiAutomationOverlay = class {
1467
+ focused = false;
1468
+ #selectedIndex = 0;
1469
+ constructor(options) {
1470
+ this.options = options;
1471
+ }
1472
+ invalidate() {}
1473
+ get selected() {
1474
+ return this.options.runs[this.#selectedIndex];
1475
+ }
1476
+ handleInput(data) {
1477
+ if (matchesKey(data, "esc")) {
1478
+ this.options.onCancel();
1479
+ return;
1480
+ }
1481
+ if (matchesKey(data, "up")) {
1482
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
1483
+ return;
1484
+ }
1485
+ if (matchesKey(data, "down")) {
1486
+ this.#selectedIndex = Math.min(this.options.runs.length - 1, this.#selectedIndex + 1);
1487
+ return;
1488
+ }
1489
+ if (matchesKey(data, "enter") || matchesKey(data, "a")) {
1490
+ if (this.selected?.status === "pending") this.options.onApprove(this.selected.id);
1491
+ return;
1492
+ }
1493
+ if (matchesKey(data, "r")) {
1494
+ if (this.selected?.status === "pending") this.options.onReject(this.selected.id);
1495
+ }
1496
+ }
1497
+ render(width) {
1498
+ const bodyWidth = Math.max(56, width);
1499
+ const innerWidth = Math.max(1, bodyWidth - 4);
1500
+ const maxRuns = 6;
1501
+ const lines = [];
1502
+ lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
1503
+ lines.push(frameLine$2(granolaTuiTheme.strong("Automation Runs"), bodyWidth));
1504
+ lines.push(frameLine$2("Pending runs can be approved with Enter/a or rejected with r.", bodyWidth));
1505
+ lines.push(frameLine$2("", bodyWidth));
1506
+ if (this.options.runs.length === 0) lines.push(frameLine$2(granolaTuiTheme.dim("No automation runs yet."), bodyWidth));
1507
+ else for (const [index, run] of this.options.runs.slice(0, maxRuns).entries()) {
1508
+ const selected = index === this.#selectedIndex;
1509
+ const title = `${selected ? ">" : " "} ${run.actionName} · ${statusLabel(run)}`;
1510
+ lines.push(frameLine$2(selected ? granolaTuiTheme.selected(title) : title, bodyWidth));
1511
+ lines.push(frameLine$2(` ${run.ruleName} · ${run.title}`, bodyWidth));
1512
+ const details = run.prompt || run.result || run.error || run.eventKind;
1513
+ for (const line of wrapDetails(` ${details}`, innerWidth).slice(0, 2)) lines.push(frameLine$2(line, bodyWidth));
1514
+ lines.push(frameLine$2("", bodyWidth));
1515
+ }
1516
+ lines.push(frameLine$2(granolaTuiTheme.dim("Esc close"), bodyWidth));
1517
+ lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
1518
+ return lines;
1519
+ }
1520
+ };
1521
+ //#endregion
1427
1522
  //#region src/tui/auth.ts
1428
1523
  function padLine$2(text, width) {
1429
1524
  const clipped = truncateToWidth(text, width, "");
@@ -1700,6 +1795,7 @@ var GranolaTuiWorkspace = class {
1700
1795
  #maxMeetings;
1701
1796
  #appState;
1702
1797
  #activePane = "meetings";
1798
+ #automationRuns = [];
1703
1799
  #detailError = "";
1704
1800
  #detailScroll = 0;
1705
1801
  #detailToken = 0;
@@ -1732,6 +1828,7 @@ var GranolaTuiWorkspace = class {
1732
1828
  this.#unsubscribe = this.app.subscribe((event) => {
1733
1829
  this.handleAppUpdate(event);
1734
1830
  });
1831
+ await this.loadAutomationRuns();
1735
1832
  await this.loadFolders({ setStatus: false });
1736
1833
  await this.loadMeetings({
1737
1834
  preferredMeetingId: this.options.initialMeetingId,
@@ -1750,6 +1847,7 @@ var GranolaTuiWorkspace = class {
1750
1847
  this.#appState = event.state;
1751
1848
  this.#selectedFolderId = event.state.ui.selectedFolderId;
1752
1849
  this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
1850
+ this.loadAutomationRuns();
1753
1851
  if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
1754
1852
  await this.loadFolders({ setStatus: false });
1755
1853
  await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
@@ -1804,6 +1902,12 @@ var GranolaTuiWorkspace = class {
1804
1902
  if (token === this.#folderToken) this.tui.requestRender();
1805
1903
  }
1806
1904
  }
1905
+ async loadAutomationRuns() {
1906
+ try {
1907
+ this.#automationRuns = [...(await this.app.listAutomationRuns({ limit: 20 })).runs];
1908
+ this.tui.requestRender();
1909
+ } catch {}
1910
+ }
1807
1911
  async loadMeetings(options = {}) {
1808
1912
  const token = ++this.#listToken;
1809
1913
  this.#loadingMeetings = true;
@@ -2079,6 +2183,38 @@ var GranolaTuiWorkspace = class {
2079
2183
  });
2080
2184
  this.setStatus("Quick open");
2081
2185
  }
2186
+ openAutomationPanel() {
2187
+ if (this.#overlay) return;
2188
+ const closeOverlay = () => {
2189
+ this.#overlay?.hide();
2190
+ this.#overlay = void 0;
2191
+ this.tui.setFocus(this);
2192
+ this.tui.requestRender();
2193
+ };
2194
+ const overlay = new GranolaTuiAutomationOverlay({
2195
+ onApprove: async (id) => {
2196
+ closeOverlay();
2197
+ await this.app.resolveAutomationRun(id, "approve");
2198
+ await this.loadAutomationRuns();
2199
+ this.setStatus("Automation approved");
2200
+ },
2201
+ onCancel: closeOverlay,
2202
+ onReject: async (id) => {
2203
+ closeOverlay();
2204
+ await this.app.resolveAutomationRun(id, "reject");
2205
+ await this.loadAutomationRuns();
2206
+ this.setStatus("Automation rejected");
2207
+ },
2208
+ runs: this.#automationRuns
2209
+ });
2210
+ this.#overlay = this.tui.showOverlay(overlay, {
2211
+ anchor: "center",
2212
+ maxHeight: "70%",
2213
+ minWidth: 56,
2214
+ width: "76%"
2215
+ });
2216
+ this.setStatus("Automation runs");
2217
+ }
2082
2218
  handleInput(data) {
2083
2219
  if (matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
2084
2220
  this.options.onExit();
@@ -2096,6 +2232,10 @@ var GranolaTuiWorkspace = class {
2096
2232
  this.openAuthPanel();
2097
2233
  return;
2098
2234
  }
2235
+ if (matchesKey(data, "u")) {
2236
+ this.openAutomationPanel();
2237
+ return;
2238
+ }
2099
2239
  if (matchesKey(data, "tab")) {
2100
2240
  this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
2101
2241
  this.tui.requestRender();
@@ -2280,7 +2420,7 @@ var GranolaTuiWorkspace = class {
2280
2420
  const bodyLines = [];
2281
2421
  for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
2282
2422
  const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
2283
- const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2423
+ const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth u automation r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2284
2424
  return [
2285
2425
  headerTitle,
2286
2426
  headerSummary,
@@ -2350,6 +2490,127 @@ const attachCommand = {
2350
2490
  }
2351
2491
  };
2352
2492
  //#endregion
2493
+ //#region src/automation-actions.ts
2494
+ function cloneAction$1(action) {
2495
+ switch (action.kind) {
2496
+ case "ask-user": return { ...action };
2497
+ case "command": return {
2498
+ ...action,
2499
+ args: action.args ? [...action.args] : void 0,
2500
+ env: action.env ? { ...action.env } : void 0
2501
+ };
2502
+ case "export-notes":
2503
+ case "export-transcript": return { ...action };
2504
+ }
2505
+ }
2506
+ function automationActionName(action) {
2507
+ return action.name || action.id;
2508
+ }
2509
+ function buildAutomationActionRunId(match, actionId) {
2510
+ return `${match.id}:${actionId}`;
2511
+ }
2512
+ function enabledAutomationActions(rule) {
2513
+ return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
2514
+ }
2515
+ function baseRun(match, rule, action, startedAt) {
2516
+ return {
2517
+ actionId: action.id,
2518
+ actionKind: action.kind,
2519
+ actionName: automationActionName(action),
2520
+ eventId: match.eventId,
2521
+ eventKind: match.eventKind,
2522
+ folders: match.folders.map((folder) => ({ ...folder })),
2523
+ id: buildAutomationActionRunId(match, action.id),
2524
+ matchedAt: match.matchedAt,
2525
+ meetingId: match.meetingId,
2526
+ ruleId: rule.id,
2527
+ ruleName: rule.name,
2528
+ startedAt,
2529
+ status: "completed",
2530
+ tags: [...match.tags],
2531
+ title: match.title,
2532
+ transcriptLoaded: match.transcriptLoaded
2533
+ };
2534
+ }
2535
+ function completedRun(run, finishedAt, patch = {}) {
2536
+ return {
2537
+ ...run,
2538
+ ...patch,
2539
+ finishedAt,
2540
+ status: "completed"
2541
+ };
2542
+ }
2543
+ function failedRun(run, finishedAt, error) {
2544
+ return {
2545
+ ...run,
2546
+ error: error instanceof Error ? error.message : String(error),
2547
+ finishedAt,
2548
+ status: "failed"
2549
+ };
2550
+ }
2551
+ function skippedRun(run, finishedAt, reason) {
2552
+ return {
2553
+ ...run,
2554
+ finishedAt,
2555
+ result: reason,
2556
+ status: "skipped"
2557
+ };
2558
+ }
2559
+ async function executeAutomationAction(match, rule, action, handlers) {
2560
+ const run = baseRun(match, rule, action, handlers.nowIso());
2561
+ switch (action.kind) {
2562
+ case "ask-user": return {
2563
+ ...run,
2564
+ meta: action.details ? { details: action.details } : void 0,
2565
+ prompt: action.prompt,
2566
+ result: "Pending user decision",
2567
+ status: "pending"
2568
+ };
2569
+ case "command": try {
2570
+ const result = await handlers.runCommand(match, rule, action);
2571
+ return completedRun(run, handlers.nowIso(), {
2572
+ meta: {
2573
+ command: result.command,
2574
+ cwd: result.cwd
2575
+ },
2576
+ result: result.output
2577
+ });
2578
+ } catch (error) {
2579
+ return failedRun(run, handlers.nowIso(), error);
2580
+ }
2581
+ case "export-notes": try {
2582
+ const result = await handlers.exportNotes(match, action);
2583
+ if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
2584
+ return completedRun(run, handlers.nowIso(), {
2585
+ meta: {
2586
+ format: result.format,
2587
+ outputDir: result.outputDir,
2588
+ scope: result.scope,
2589
+ written: result.written
2590
+ },
2591
+ result: `Exported notes to ${result.outputDir}`
2592
+ });
2593
+ } catch (error) {
2594
+ return failedRun(run, handlers.nowIso(), error);
2595
+ }
2596
+ case "export-transcript": try {
2597
+ const result = await handlers.exportTranscripts(match, action);
2598
+ if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
2599
+ return completedRun(run, handlers.nowIso(), {
2600
+ meta: {
2601
+ format: result.format,
2602
+ outputDir: result.outputDir,
2603
+ scope: result.scope,
2604
+ written: result.written
2605
+ },
2606
+ result: `Exported transcript to ${result.outputDir}`
2607
+ });
2608
+ } catch (error) {
2609
+ return failedRun(run, handlers.nowIso(), error);
2610
+ }
2611
+ }
2612
+ }
2613
+ //#endregion
2353
2614
  //#region src/persistence/layout.ts
2354
2615
  function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
2355
2616
  return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
@@ -2360,6 +2621,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2360
2621
  return {
2361
2622
  automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
2362
2623
  automationRulesFile: join(dataDirectory, "automation-rules.json"),
2624
+ automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
2363
2625
  apiKeyFile: join(dataDirectory, "api-key.txt"),
2364
2626
  dataDirectory,
2365
2627
  exportJobsFile: join(dataDirectory, "export-jobs.json"),
@@ -2408,10 +2670,60 @@ function createDefaultAutomationMatchStore(filePath) {
2408
2670
  return new FileAutomationMatchStore(filePath);
2409
2671
  }
2410
2672
  //#endregion
2673
+ //#region src/automation-runs.ts
2674
+ function cloneRun(run) {
2675
+ return {
2676
+ ...run,
2677
+ folders: run.folders.map((folder) => ({ ...folder })),
2678
+ meta: run.meta ? structuredClone(run.meta) : void 0,
2679
+ tags: [...run.tags]
2680
+ };
2681
+ }
2682
+ function sortRuns(runs) {
2683
+ return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
2684
+ }
2685
+ function mergeRuns(runs) {
2686
+ const byId = /* @__PURE__ */ new Map();
2687
+ for (const run of runs) byId.set(run.id, cloneRun(run));
2688
+ return sortRuns([...byId.values()]);
2689
+ }
2690
+ var FileAutomationRunStore = class {
2691
+ constructor(filePath = defaultAutomationRunsFilePath()) {
2692
+ this.filePath = filePath;
2693
+ }
2694
+ async appendRuns(runs) {
2695
+ if (runs.length === 0) return;
2696
+ await mkdir(dirname(this.filePath), { recursive: true });
2697
+ const payload = runs.map((run) => JSON.stringify(run)).join("\n");
2698
+ await appendFile(this.filePath, `${payload}\n`, {
2699
+ encoding: "utf8",
2700
+ mode: 384
2701
+ });
2702
+ }
2703
+ async readRun(id) {
2704
+ return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
2705
+ }
2706
+ async readRuns(options = {}) {
2707
+ try {
2708
+ const runs = mergeRuns((await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((run) => Boolean(run))).filter((run) => options.status ? run.status === options.status : true);
2709
+ return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
2710
+ } catch {
2711
+ return [];
2712
+ }
2713
+ }
2714
+ };
2715
+ function defaultAutomationRunsFilePath() {
2716
+ return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
2717
+ }
2718
+ function createDefaultAutomationRunStore(filePath) {
2719
+ return new FileAutomationRunStore(filePath);
2720
+ }
2721
+ //#endregion
2411
2722
  //#region src/automation-rules.ts
2412
2723
  function cloneRule(rule) {
2413
2724
  return {
2414
2725
  ...rule,
2726
+ actions: rule.actions?.map((action) => cloneAction(action)),
2415
2727
  when: {
2416
2728
  ...rule.when,
2417
2729
  eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
@@ -2423,11 +2735,92 @@ function cloneRule(rule) {
2423
2735
  }
2424
2736
  };
2425
2737
  }
2738
+ function cloneAction(action) {
2739
+ switch (action.kind) {
2740
+ case "ask-user": return { ...action };
2741
+ case "command": return {
2742
+ ...action,
2743
+ args: action.args ? [...action.args] : void 0,
2744
+ env: action.env ? { ...action.env } : void 0
2745
+ };
2746
+ case "export-notes":
2747
+ case "export-transcript": return { ...action };
2748
+ }
2749
+ }
2426
2750
  function stringArray(value) {
2427
2751
  if (!Array.isArray(value)) return;
2428
2752
  const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
2429
2753
  return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
2430
2754
  }
2755
+ function stringRecord(value) {
2756
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2757
+ const entries = Object.entries(value).filter(([key, item]) => {
2758
+ return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
2759
+ });
2760
+ if (entries.length === 0) return;
2761
+ return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
2762
+ }
2763
+ function parseAction(value, index) {
2764
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2765
+ const record = value;
2766
+ const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
2767
+ const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
2768
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
2769
+ const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
2770
+ switch (kind) {
2771
+ case "ask-user": {
2772
+ const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
2773
+ if (!id || !prompt) return;
2774
+ return {
2775
+ details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
2776
+ enabled,
2777
+ id,
2778
+ kind,
2779
+ name,
2780
+ prompt
2781
+ };
2782
+ }
2783
+ case "command": {
2784
+ const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
2785
+ if (!id || !command) return;
2786
+ return {
2787
+ args: stringArray(record.args),
2788
+ command,
2789
+ cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
2790
+ enabled,
2791
+ env: stringRecord(record.env),
2792
+ id,
2793
+ kind,
2794
+ name,
2795
+ stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
2796
+ timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
2797
+ };
2798
+ }
2799
+ case "export-notes":
2800
+ if (!id) return;
2801
+ return {
2802
+ enabled,
2803
+ format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
2804
+ id,
2805
+ kind,
2806
+ name,
2807
+ outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
2808
+ scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
2809
+ };
2810
+ case "export-transcript":
2811
+ if (!id) return;
2812
+ return {
2813
+ enabled,
2814
+ format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
2815
+ id,
2816
+ kind,
2817
+ name,
2818
+ outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
2819
+ scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
2820
+ };
2821
+ default: return;
2822
+ }
2823
+ }
2431
2824
  function parseRule(value) {
2432
2825
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
2433
2826
  const record = value;
@@ -2436,6 +2829,7 @@ function parseRule(value) {
2436
2829
  const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
2437
2830
  if (!id || !name || !whenValue) return;
2438
2831
  return {
2832
+ actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
2439
2833
  enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
2440
2834
  id,
2441
2835
  name,
@@ -3447,6 +3841,7 @@ async function loadOptionalGranolaCache(cacheFile) {
3447
3841
  //#endregion
3448
3842
  //#region src/export-scope.ts
3449
3843
  const FOLDER_EXPORT_DIRECTORY = "_folders";
3844
+ const MEETING_EXPORT_DIRECTORY = "_meetings";
3450
3845
  function allExportScope() {
3451
3846
  return { mode: "all" };
3452
3847
  }
@@ -3458,11 +3853,22 @@ function folderExportScope(folder) {
3458
3853
  };
3459
3854
  }
3460
3855
  function cloneExportScope(scope) {
3461
- return scope.mode === "folder" ? { ...scope } : { mode: "all" };
3856
+ if (scope.mode === "folder" || scope.mode === "meeting") return { ...scope };
3857
+ return { mode: "all" };
3462
3858
  }
3463
3859
  function normaliseExportScope(value) {
3464
3860
  const record = asRecord(value);
3465
3861
  if (!record) return allExportScope();
3862
+ if (record.mode === "meeting") {
3863
+ const meetingId = stringValue(record.meetingId);
3864
+ const meetingTitle = stringValue(record.meetingTitle) || meetingId;
3865
+ if (!meetingId) return allExportScope();
3866
+ return {
3867
+ meetingId,
3868
+ meetingTitle,
3869
+ mode: "meeting"
3870
+ };
3871
+ }
3466
3872
  if (record.mode !== "folder") return allExportScope();
3467
3873
  const folderId = stringValue(record.folderId);
3468
3874
  const folderName = stringValue(record.folderName) || folderId;
@@ -3473,12 +3879,22 @@ function normaliseExportScope(value) {
3473
3879
  mode: "folder"
3474
3880
  };
3475
3881
  }
3882
+ function meetingExportScope(meeting) {
3883
+ return {
3884
+ meetingId: meeting.meetingId,
3885
+ meetingTitle: meeting.meetingTitle || meeting.meetingId,
3886
+ mode: "meeting"
3887
+ };
3888
+ }
3476
3889
  function renderExportScopeLabel(scope) {
3477
- return scope.mode === "folder" ? `folder ${scope.folderName}` : "all meetings";
3890
+ if (scope.mode === "folder") return `folder ${scope.folderName}`;
3891
+ if (scope.mode === "meeting") return `meeting ${scope.meetingTitle}`;
3892
+ return "all meetings";
3478
3893
  }
3479
3894
  function resolveExportOutputDir(outputDir, scope, options = {}) {
3480
- if (scope.mode !== "folder" || options.scopedDirectory === false) return outputDir;
3481
- return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
3895
+ if (options.scopedDirectory === false || scope.mode === "all") return outputDir;
3896
+ if (scope.mode === "folder") return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
3897
+ return join(outputDir, MEETING_EXPORT_DIRECTORY, sanitiseFilename(scope.meetingId, "meeting"));
3482
3898
  }
3483
3899
  //#endregion
3484
3900
  //#region src/export-jobs.ts
@@ -4005,8 +4421,11 @@ function defaultState(config, auth, surface) {
4005
4421
  loaded: false,
4006
4422
  matchCount: 0,
4007
4423
  matchesFile: defaultAutomationMatchesFilePath(),
4424
+ pendingRunCount: 0,
4008
4425
  ruleCount: 0,
4009
- rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4426
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4427
+ runCount: 0,
4428
+ runsFile: defaultAutomationRunsFilePath()
4010
4429
  },
4011
4430
  cache: {
4012
4431
  configured: Boolean(config.transcripts.cacheFile),
@@ -4049,6 +4468,7 @@ function defaultState(config, auth, surface) {
4049
4468
  };
4050
4469
  }
4051
4470
  var GranolaApp = class {
4471
+ #automationActionRuns;
4052
4472
  #automationMatches;
4053
4473
  #automationRules;
4054
4474
  #cacheData;
@@ -4069,26 +4489,20 @@ var GranolaApp = class {
4069
4489
  folders: match.folders.map((folder) => ({ ...folder })),
4070
4490
  tags: [...match.tags]
4071
4491
  }));
4072
- this.#automationRules = (deps.automationRules ?? []).map((rule) => ({
4073
- ...rule,
4074
- when: {
4075
- ...rule.when,
4076
- eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
4077
- folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
4078
- folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
4079
- meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
4080
- tags: rule.when.tags ? [...rule.when.tags] : void 0,
4081
- titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
4082
- }
4083
- }));
4492
+ this.#automationActionRuns = (deps.automationRuns ?? []).map((run) => this.cloneAutomationRun(run));
4493
+ this.#automationRules = (deps.automationRules ?? []).map((rule) => this.cloneAutomationRule(rule));
4084
4494
  this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
4085
4495
  this.#state.automation = {
4496
+ lastRunAt: this.#automationActionRuns[0]?.finishedAt ?? this.#automationActionRuns[0]?.startedAt,
4086
4497
  lastMatchedAt: this.#automationMatches[0]?.matchedAt,
4087
4498
  loaded: true,
4088
4499
  matchCount: this.#automationMatches.length,
4089
4500
  matchesFile: defaultAutomationMatchesFilePath(),
4501
+ pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
4090
4502
  ruleCount: this.#automationRules.length,
4091
- rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4503
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4504
+ runCount: this.#automationActionRuns.length,
4505
+ runsFile: defaultAutomationRunsFilePath()
4092
4506
  };
4093
4507
  this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
4094
4508
  this.#state.index = {
@@ -4165,6 +4579,18 @@ var GranolaApp = class {
4165
4579
  cloneAutomationRule(rule) {
4166
4580
  return {
4167
4581
  ...rule,
4582
+ actions: rule.actions?.map((action) => {
4583
+ switch (action.kind) {
4584
+ case "ask-user": return { ...action };
4585
+ case "command": return {
4586
+ ...action,
4587
+ args: action.args ? [...action.args] : void 0,
4588
+ env: action.env ? { ...action.env } : void 0
4589
+ };
4590
+ case "export-notes":
4591
+ case "export-transcript": return { ...action };
4592
+ }
4593
+ }),
4168
4594
  when: {
4169
4595
  ...rule.when,
4170
4596
  eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
@@ -4183,16 +4609,40 @@ var GranolaApp = class {
4183
4609
  tags: [...match.tags]
4184
4610
  };
4185
4611
  }
4186
- async loadAutomationRules(options = {}) {
4187
- if (this.#automationRules.length > 0 && !options.forceRefresh) return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4188
- if (!this.deps.automationRuleStore) return [];
4189
- this.#automationRules = (await this.deps.automationRuleStore.readRules()).map((rule) => this.cloneAutomationRule(rule));
4612
+ cloneAutomationRun(run) {
4613
+ return {
4614
+ ...run,
4615
+ folders: run.folders.map((folder) => ({ ...folder })),
4616
+ meta: run.meta ? structuredClone(run.meta) : void 0,
4617
+ tags: [...run.tags]
4618
+ };
4619
+ }
4620
+ refreshAutomationState() {
4621
+ const latestMatch = this.#automationMatches.reduce((current, candidate) => !current || candidate.matchedAt.localeCompare(current.matchedAt) > 0 ? candidate : current, void 0);
4622
+ const latestRun = this.#automationActionRuns.reduce((current, candidate) => {
4623
+ const candidateTime = candidate.finishedAt ?? candidate.startedAt;
4624
+ const currentTime = current ? current.finishedAt ?? current.startedAt : void 0;
4625
+ return !currentTime || candidateTime.localeCompare(currentTime) > 0 ? candidate : current;
4626
+ }, void 0);
4190
4627
  this.#state.automation = {
4191
4628
  ...this.#state.automation,
4629
+ lastMatchedAt: latestMatch?.matchedAt ?? this.#state.automation.lastMatchedAt,
4630
+ lastRunAt: latestRun?.finishedAt ?? latestRun?.startedAt ?? this.#state.automation.lastRunAt,
4192
4631
  loaded: true,
4632
+ matchCount: this.#automationMatches.length,
4633
+ matchesFile: defaultAutomationMatchesFilePath(),
4634
+ pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
4193
4635
  ruleCount: this.#automationRules.length,
4194
- rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4636
+ rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4637
+ runCount: this.#automationActionRuns.length,
4638
+ runsFile: defaultAutomationRunsFilePath()
4195
4639
  };
4640
+ }
4641
+ async loadAutomationRules(options = {}) {
4642
+ if (this.#automationRules.length > 0 && !options.forceRefresh) return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4643
+ if (!this.deps.automationRuleStore) return [];
4644
+ this.#automationRules = (await this.deps.automationRuleStore.readRules()).map((rule) => this.cloneAutomationRule(rule));
4645
+ this.refreshAutomationState();
4196
4646
  this.emitStateUpdate();
4197
4647
  return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4198
4648
  }
@@ -4200,12 +4650,18 @@ var GranolaApp = class {
4200
4650
  if (matches.length === 0) return;
4201
4651
  if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
4202
4652
  this.#automationMatches.push(...matches.map((match) => this.cloneAutomationMatch(match)));
4203
- this.#state.automation = {
4204
- ...this.#state.automation,
4205
- lastMatchedAt: matches.at(-1)?.matchedAt ?? this.#state.automation.lastMatchedAt,
4206
- loaded: true,
4207
- matchCount: this.#automationMatches.length
4208
- };
4653
+ this.refreshAutomationState();
4654
+ }
4655
+ async appendAutomationRuns(runs) {
4656
+ if (runs.length === 0) return;
4657
+ if (this.deps.automationRunStore) await this.deps.automationRunStore.appendRuns(runs);
4658
+ for (const run of runs) {
4659
+ const index = this.#automationActionRuns.findIndex((candidate) => candidate.id === run.id);
4660
+ if (index >= 0) this.#automationActionRuns[index] = this.cloneAutomationRun(run);
4661
+ else this.#automationActionRuns.push(this.cloneAutomationRun(run));
4662
+ }
4663
+ this.#automationActionRuns.sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
4664
+ this.refreshAutomationState();
4209
4665
  }
4210
4666
  createSyncRunId() {
4211
4667
  return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
@@ -4449,6 +4905,35 @@ var GranolaApp = class {
4449
4905
  this.setUiState({ view: "idle" });
4450
4906
  return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
4451
4907
  }
4908
+ async listAutomationRuns(options = {}) {
4909
+ const limit = options.limit ?? 20;
4910
+ const runs = this.deps.automationRunStore ? await this.deps.automationRunStore.readRuns({
4911
+ limit,
4912
+ status: options.status
4913
+ }) : this.#automationActionRuns.filter((run) => options.status ? run.status === options.status : true).slice(0, limit);
4914
+ this.setUiState({ view: "idle" });
4915
+ return { runs: runs.map((run) => this.cloneAutomationRun(run)) };
4916
+ }
4917
+ async resolveAutomationRun(id, decision, options = {}) {
4918
+ const current = (this.deps.automationRunStore ? await this.deps.automationRunStore.readRun(id) : void 0) ?? this.#automationActionRuns.find((run) => run.id === id);
4919
+ if (!current) throw new Error(`automation run not found: ${id}`);
4920
+ if (current.status !== "pending") throw new Error(`automation run is not pending: ${id}`);
4921
+ const finishedAt = this.nowIso();
4922
+ const resolved = {
4923
+ ...this.cloneAutomationRun(current),
4924
+ finishedAt,
4925
+ meta: {
4926
+ ...current.meta ? structuredClone(current.meta) : {},
4927
+ decision,
4928
+ note: options.note?.trim() || void 0
4929
+ },
4930
+ result: decision === "approve" ? options.note?.trim() || "Approved by user" : options.note?.trim() || "Rejected by user",
4931
+ status: decision === "approve" ? "completed" : "skipped"
4932
+ };
4933
+ await this.appendAutomationRuns([resolved]);
4934
+ this.emitStateUpdate();
4935
+ return this.cloneAutomationRun(resolved);
4936
+ }
4452
4937
  async loginAuth(options = {}) {
4453
4938
  const controller = this.requireAuthController();
4454
4939
  try {
@@ -4498,6 +4983,165 @@ var GranolaApp = class {
4498
4983
  throw error;
4499
4984
  }
4500
4985
  }
4986
+ async maybeReadMeetingBundleById(id, options = {}) {
4987
+ try {
4988
+ return await this.readMeetingBundleById(id, options);
4989
+ } catch {
4990
+ return;
4991
+ }
4992
+ }
4993
+ async runAutomationNotesAction(match, action) {
4994
+ const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
4995
+ if (!bundle) return;
4996
+ const scope = meetingExportScope({
4997
+ meetingId: bundle.document.id,
4998
+ meetingTitle: bundle.meeting.meeting.title || bundle.document.id
4999
+ });
5000
+ const result = await this.runNotesExport({
5001
+ documents: [bundle.document],
5002
+ format: action.format ?? "markdown",
5003
+ outputDir: resolveExportOutputDir(action.outputDir ?? this.config.notes.output, scope, { scopedDirectory: action.scopedOutput }),
5004
+ scope,
5005
+ trackLastRun: false,
5006
+ updateUi: false
5007
+ });
5008
+ return {
5009
+ format: result.format,
5010
+ outputDir: result.outputDir,
5011
+ scope: result.scope,
5012
+ written: result.written
5013
+ };
5014
+ }
5015
+ async runAutomationTranscriptAction(match, action) {
5016
+ const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
5017
+ if (!bundle?.cacheData) return;
5018
+ const cacheDocument = bundle.cacheData.documents[bundle.document.id];
5019
+ const transcriptSegments = bundle.cacheData.transcripts[bundle.document.id];
5020
+ if (!cacheDocument || !transcriptSegments || transcriptSegments.length === 0) return;
5021
+ const scope = meetingExportScope({
5022
+ meetingId: bundle.document.id,
5023
+ meetingTitle: bundle.meeting.meeting.title || bundle.document.id
5024
+ });
5025
+ const result = await this.runTranscriptsExport({
5026
+ cacheData: {
5027
+ documents: { [bundle.document.id]: cacheDocument },
5028
+ transcripts: { [bundle.document.id]: transcriptSegments }
5029
+ },
5030
+ format: action.format ?? "text",
5031
+ outputDir: resolveExportOutputDir(action.outputDir ?? this.config.transcripts.output, scope, { scopedDirectory: action.scopedOutput }),
5032
+ scope,
5033
+ trackLastRun: false,
5034
+ updateUi: false
5035
+ });
5036
+ return {
5037
+ format: result.format,
5038
+ outputDir: result.outputDir,
5039
+ scope: result.scope,
5040
+ written: result.written
5041
+ };
5042
+ }
5043
+ async runAutomationCommand(match, rule, action) {
5044
+ const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
5045
+ const cwd = action.cwd ? resolve(action.cwd) : process.cwd();
5046
+ const payload = JSON.stringify({
5047
+ action: {
5048
+ id: action.id,
5049
+ kind: "command",
5050
+ name: automationActionName(action)
5051
+ },
5052
+ authMode: this.#state.auth.mode,
5053
+ generatedAt: this.nowIso(),
5054
+ match: this.cloneAutomationMatch(match),
5055
+ meeting: bundle ? {
5056
+ document: bundle.document,
5057
+ meeting: bundle.meeting
5058
+ } : void 0,
5059
+ rule: {
5060
+ id: rule.id,
5061
+ name: rule.name
5062
+ }
5063
+ }, null, 2);
5064
+ return await new Promise((resolve, reject) => {
5065
+ const child = spawn(action.command, action.args ?? [], {
5066
+ cwd,
5067
+ env: {
5068
+ ...process.env,
5069
+ ...action.env,
5070
+ GRANOLA_ACTION_KIND: "command",
5071
+ GRANOLA_EVENT_ID: match.eventId,
5072
+ GRANOLA_EVENT_KIND: match.eventKind,
5073
+ GRANOLA_MATCH_ID: match.id,
5074
+ GRANOLA_MEETING_ID: match.meetingId,
5075
+ GRANOLA_RULE_ID: rule.id
5076
+ },
5077
+ stdio: [
5078
+ "pipe",
5079
+ "pipe",
5080
+ "pipe"
5081
+ ]
5082
+ });
5083
+ const stdoutChunks = [];
5084
+ const stderrChunks = [];
5085
+ let timedOut = false;
5086
+ const timeoutMs = action.timeoutMs ?? this.config.notes.timeoutMs;
5087
+ const timeout = setTimeout(() => {
5088
+ timedOut = true;
5089
+ child.kill("SIGTERM");
5090
+ }, timeoutMs);
5091
+ child.stdout.on("data", (chunk) => {
5092
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5093
+ });
5094
+ child.stderr.on("data", (chunk) => {
5095
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5096
+ });
5097
+ child.on("error", (error) => {
5098
+ clearTimeout(timeout);
5099
+ reject(error);
5100
+ });
5101
+ child.on("close", (code) => {
5102
+ clearTimeout(timeout);
5103
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
5104
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
5105
+ if (timedOut) {
5106
+ reject(/* @__PURE__ */ new Error(`automation command timed out after ${timeoutMs}ms`));
5107
+ return;
5108
+ }
5109
+ if (code !== 0) {
5110
+ reject(new Error(stderr || stdout || `automation command exited with status ${String(code)}`));
5111
+ return;
5112
+ }
5113
+ resolve({
5114
+ command: [action.command, ...action.args ?? []].join(" "),
5115
+ cwd,
5116
+ output: stdout || stderr || void 0
5117
+ });
5118
+ });
5119
+ if (action.stdin !== "none") child.stdin.write(payload);
5120
+ child.stdin.end();
5121
+ });
5122
+ }
5123
+ async runAutomationActions(rules, matches) {
5124
+ const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
5125
+ const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
5126
+ const runs = [];
5127
+ for (const match of matches) {
5128
+ const rule = rulesById.get(match.ruleId);
5129
+ if (!rule) continue;
5130
+ for (const action of enabledAutomationActions(rule)) {
5131
+ const runId = buildAutomationActionRunId(match, action.id);
5132
+ if (existingRunIds.has(runId)) continue;
5133
+ existingRunIds.add(runId);
5134
+ runs.push(await executeAutomationAction(match, rule, action, {
5135
+ exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
5136
+ exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
5137
+ nowIso: () => this.nowIso(),
5138
+ runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
5139
+ }));
5140
+ }
5141
+ }
5142
+ await this.appendAutomationRuns(runs);
5143
+ return runs.map((run) => this.cloneAutomationRun(run));
5144
+ }
4501
5145
  async runSync(options) {
4502
5146
  const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
4503
5147
  this.#state.sync = {
@@ -4516,8 +5160,10 @@ var GranolaApp = class {
4516
5160
  const runId = this.createSyncRunId();
4517
5161
  const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
4518
5162
  if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
4519
- const automationMatches = matchAutomationRules(await this.loadAutomationRules(), events, completedAt);
5163
+ const rules = await this.loadAutomationRules();
5164
+ const automationMatches = matchAutomationRules(rules, events, completedAt);
4520
5165
  await this.appendAutomationMatches(automationMatches);
5166
+ await this.runAutomationActions(rules, automationMatches);
4521
5167
  this.#state.sync = {
4522
5168
  ...this.#state.sync,
4523
5169
  eventCount: this.#state.sync.eventCount + events.length,
@@ -4689,40 +5335,46 @@ var GranolaApp = class {
4689
5335
  source: "live"
4690
5336
  };
4691
5337
  }
4692
- async getMeeting(id, options = {}) {
5338
+ async readMeetingBundleById(id, options = {}) {
4693
5339
  const documents = await this.listDocuments();
4694
5340
  const cacheData = await this.loadCache({ required: options.requireCache });
4695
5341
  const folders = await this.loadFolders();
4696
5342
  const document = resolveMeeting(documents, id);
4697
- const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
4698
- this.setUiState({
4699
- selectedFolderId: meeting.meeting.folders[0]?.id,
4700
- selectedMeetingId: document.id,
4701
- view: "meeting-detail"
4702
- });
4703
5343
  return {
4704
5344
  cacheData,
4705
5345
  document,
4706
- meeting
5346
+ meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
4707
5347
  };
4708
5348
  }
4709
- async findMeeting(query, options = {}) {
5349
+ async readMeetingBundleByQuery(query, options = {}) {
4710
5350
  const documents = await this.listDocuments();
4711
5351
  const cacheData = await this.loadCache({ required: options.requireCache });
4712
5352
  const folders = await this.loadFolders();
4713
5353
  const document = resolveMeetingQuery(documents, query);
4714
- const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
4715
- this.setUiState({
4716
- selectedFolderId: meeting.meeting.folders[0]?.id,
4717
- selectedMeetingId: document.id,
4718
- view: "meeting-detail"
4719
- });
4720
5354
  return {
4721
5355
  cacheData,
4722
5356
  document,
4723
- meeting
5357
+ meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
4724
5358
  };
4725
5359
  }
5360
+ async getMeeting(id, options = {}) {
5361
+ const bundle = await this.readMeetingBundleById(id, options);
5362
+ this.setUiState({
5363
+ selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
5364
+ selectedMeetingId: bundle.document.id,
5365
+ view: "meeting-detail"
5366
+ });
5367
+ return bundle;
5368
+ }
5369
+ async findMeeting(query, options = {}) {
5370
+ const bundle = await this.readMeetingBundleByQuery(query, options);
5371
+ this.setUiState({
5372
+ selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
5373
+ selectedMeetingId: bundle.document.id,
5374
+ view: "meeting-detail"
5375
+ });
5376
+ return bundle;
5377
+ }
4726
5378
  async listExportJobs(options = {}) {
4727
5379
  const limit = options.limit ?? 20;
4728
5380
  const jobs = this.#state.exports.jobs.slice(0, limit).map((job) => cloneExportJob(job));
@@ -4758,7 +5410,7 @@ var GranolaApp = class {
4758
5410
  await this.failExportJob(job, error);
4759
5411
  throw error;
4760
5412
  }
4761
- this.#state.exports.notes = {
5413
+ if (options.trackLastRun !== false) this.#state.exports.notes = {
4762
5414
  format: options.format,
4763
5415
  itemCount: options.documents.length,
4764
5416
  jobId: job.id,
@@ -4768,7 +5420,7 @@ var GranolaApp = class {
4768
5420
  written
4769
5421
  };
4770
5422
  this.emitStateUpdate();
4771
- this.setUiState({
5423
+ if (options.updateUi !== false) this.setUiState({
4772
5424
  selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
4773
5425
  view: "notes-export"
4774
5426
  });
@@ -4816,7 +5468,7 @@ var GranolaApp = class {
4816
5468
  await this.failExportJob(job, error);
4817
5469
  throw error;
4818
5470
  }
4819
- this.#state.exports.transcripts = {
5471
+ if (options.trackLastRun !== false) this.#state.exports.transcripts = {
4820
5472
  format: options.format,
4821
5473
  itemCount: count,
4822
5474
  jobId: job.id,
@@ -4826,7 +5478,7 @@ var GranolaApp = class {
4826
5478
  written
4827
5479
  };
4828
5480
  this.emitStateUpdate();
4829
- this.setUiState({
5481
+ if (options.updateUi !== false) this.setUiState({
4830
5482
  selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
4831
5483
  view: "transcripts-export"
4832
5484
  });
@@ -4870,6 +5522,8 @@ async function createGranolaApp(config, options = {}) {
4870
5522
  const auth = await inspectDefaultGranolaAuth(config);
4871
5523
  const automationMatchStore = createDefaultAutomationMatchStore();
4872
5524
  const automationMatches = await automationMatchStore.readMatches(0);
5525
+ const automationRunStore = createDefaultAutomationRunStore();
5526
+ const automationRuns = await automationRunStore.readRuns({ limit: 0 });
4873
5527
  const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
4874
5528
  const automationRules = await automationRuleStore.readRules();
4875
5529
  const authController = createDefaultGranolaAuthController(config);
@@ -4885,6 +5539,8 @@ async function createGranolaApp(config, options = {}) {
4885
5539
  authController,
4886
5540
  automationMatches,
4887
5541
  automationMatchStore,
5542
+ automationRunStore,
5543
+ automationRuns,
4888
5544
  automationRules,
4889
5545
  automationRuleStore,
4890
5546
  cacheLoader: loadOptionalGranolaCache,
@@ -5046,15 +5702,20 @@ function automationHelp() {
5046
5702
  return `Granola automation
5047
5703
 
5048
5704
  Usage:
5049
- granola automation <rules|matches> [options]
5705
+ granola automation <rules|matches|runs|approve|reject> [options]
5050
5706
 
5051
5707
  Subcommands:
5052
5708
  rules List configured automation rules
5053
5709
  matches Show recent rule matches from sync events
5710
+ runs Show recent automation action runs
5711
+ approve <id> Approve a pending ask-user action run
5712
+ reject <id> Reject a pending ask-user action run
5054
5713
 
5055
5714
  Options:
5056
5715
  --format <value> text, json, yaml (default: text)
5057
5716
  --limit <n> Number of matches to show (default: 20)
5717
+ --status <value> completed, failed, pending, skipped
5718
+ --note <text> Note to store with approve/reject decisions
5058
5719
  --rules <path> Path to automation rules JSON
5059
5720
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
5060
5721
  --supabase <path> Path to supabase.json
@@ -5081,7 +5742,7 @@ function renderRules(rules, format) {
5081
5742
  if (format === "json") return toJson({ rules });
5082
5743
  if (format === "yaml") return toYaml({ rules });
5083
5744
  if (rules.length === 0) return "No automation rules configured\n";
5084
- return `${["ID ENABLED EVENTS FILTERS", ...rules.map((rule) => {
5745
+ return `${["ID ENABLED EVENTS ACTIONS FILTERS", ...rules.map((rule) => {
5085
5746
  const filters = [
5086
5747
  rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
5087
5748
  rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
@@ -5089,7 +5750,7 @@ function renderRules(rules, format) {
5089
5750
  rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
5090
5751
  rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
5091
5752
  ].filter(Boolean).join(" ");
5092
- return `${rule.id.padEnd(23).slice(0, 23)} ${(rule.enabled === false ? "no" : "yes").padEnd(8)} ${(rule.when.eventKinds?.join(",") || "any").padEnd(22).slice(0, 22)} ${filters || "-"}`;
5753
+ return `${rule.id.padEnd(23).slice(0, 23)} ${(rule.enabled === false ? "no" : "yes").padEnd(8)} ${(rule.when.eventKinds?.join(",") || "any").padEnd(22).slice(0, 22)} ${String(rule.actions?.length ?? 0).padEnd(8)} ${filters || "-"}`;
5093
5754
  })].join("\n")}\n`;
5094
5755
  }
5095
5756
  function renderMatches(matches, format) {
@@ -5100,12 +5761,32 @@ function renderMatches(matches, format) {
5100
5761
  return `${match.matchedAt.slice(0, 19).padEnd(21)} ${match.ruleName.padEnd(23).slice(0, 23)} ${match.eventKind.padEnd(18).slice(0, 18)} ${match.title} (${match.meetingId})`;
5101
5762
  })].join("\n")}\n`;
5102
5763
  }
5764
+ function parseRunStatus(value) {
5765
+ switch (value) {
5766
+ case void 0: return;
5767
+ case "completed":
5768
+ case "failed":
5769
+ case "pending":
5770
+ case "skipped": return value;
5771
+ default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
5772
+ }
5773
+ }
5774
+ function renderRuns(runs, format) {
5775
+ if (format === "json") return toJson({ runs });
5776
+ if (format === "yaml") return toYaml({ runs });
5777
+ if (runs.length === 0) return "No automation runs yet\n";
5778
+ return `${["STARTED AT STATUS ACTION RULE TITLE", ...runs.map((run) => {
5779
+ return `${run.startedAt.slice(0, 19).padEnd(21)} ${run.status.padEnd(11).slice(0, 11)} ${run.actionName.padEnd(23).slice(0, 23)} ${run.ruleName.padEnd(23).slice(0, 23)} ${[run.title, run.result || run.error].filter(Boolean).join(" - ")}`;
5780
+ })].join("\n")}\n`;
5781
+ }
5103
5782
  const automationCommand = {
5104
5783
  description: "Inspect automation rules and rule matches",
5105
5784
  flags: {
5106
5785
  format: { type: "string" },
5107
5786
  help: { type: "boolean" },
5108
5787
  limit: { type: "string" },
5788
+ note: { type: "string" },
5789
+ status: { type: "string" },
5109
5790
  timeout: { type: "string" }
5110
5791
  },
5111
5792
  help: automationHelp,
@@ -5131,10 +5812,26 @@ const automationCommand = {
5131
5812
  console.log(renderMatches(result.matches, format).trimEnd());
5132
5813
  return 0;
5133
5814
  }
5815
+ case "runs": {
5816
+ const result = await app.listAutomationRuns({
5817
+ limit: parseLimit$3(commandFlags.limit),
5818
+ status: parseRunStatus(commandFlags.status)
5819
+ });
5820
+ console.log(renderRuns(result.runs, format).trimEnd());
5821
+ return 0;
5822
+ }
5823
+ case "approve":
5824
+ case "reject": {
5825
+ const id = commandArgs[1]?.trim();
5826
+ if (!id) throw new Error(`missing automation run id for ${action}`);
5827
+ const run = await app.resolveAutomationRun(id, action, { note: typeof commandFlags.note === "string" ? commandFlags.note : void 0 });
5828
+ console.log(`${action === "approve" ? "Approved" : "Rejected"} ${run.actionName} for ${run.title} (${run.id})`);
5829
+ return 0;
5830
+ }
5134
5831
  case void 0:
5135
5832
  console.log(automationHelp());
5136
5833
  return 1;
5137
- default: throw new Error("invalid automation command: expected rules or matches");
5834
+ default: throw new Error("invalid automation command: expected rules, matches, runs, approve, or reject");
5138
5835
  }
5139
5836
  }
5140
5837
  };
@@ -7089,6 +7786,7 @@ var granolaTransportPaths = {
7089
7786
  authUnlock: "/auth/unlock",
7090
7787
  automationMatches: "/automation/matches",
7091
7788
  automationRules: "/automation/rules",
7789
+ automationRuns: "/automation/runs",
7092
7790
  events: "/events",
7093
7791
  exportJobs: "/exports/jobs",
7094
7792
  exportNotes: "/exports/notes",
@@ -7148,6 +7846,15 @@ function granolaFoldersPath(options = {}) {
7148
7846
  function granolaExportJobsPath(options = {}) {
7149
7847
  return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
7150
7848
  }
7849
+ function granolaAutomationRunsPath(options = {}) {
7850
+ return appendSearchParams(granolaTransportPaths.automationRuns, {
7851
+ limit: options.limit,
7852
+ status: options.status
7853
+ });
7854
+ }
7855
+ function granolaAutomationRunDecisionPath(id, decision) {
7856
+ return \`\${granolaTransportPaths.automationRuns}/\${encodeURIComponent(id)}/\${decision}\`;
7857
+ }
7151
7858
  function granolaExportJobRerunPath(id) {
7152
7859
  return \`\${granolaTransportPaths.exportJobs}/\${encodeURIComponent(id)}/rerun\`;
7153
7860
  }
@@ -7329,6 +8036,16 @@ var GranolaServerClient = class GranolaServerClient {
7329
8036
  const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
7330
8037
  return await this.requestJson(path);
7331
8038
  }
8039
+ async listAutomationRuns(options = {}) {
8040
+ return await this.requestJson(granolaAutomationRunsPath(options));
8041
+ }
8042
+ async resolveAutomationRun(id, decision, options = {}) {
8043
+ return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
8044
+ body: JSON.stringify(options),
8045
+ headers: { "content-type": "application/json" },
8046
+ method: "POST"
8047
+ });
8048
+ }
7332
8049
  async inspectSync() {
7333
8050
  return cloneValue(_classPrivateFieldGet2(_state, this).sync);
7334
8051
  }
@@ -7547,7 +8264,7 @@ function nextWorkspaceTab(currentTab, key) {
7547
8264
  //#endregion
7548
8265
  //#region src/web-app/components.tsx
7549
8266
  /** @jsxImportSource solid-js */
7550
- var _tmpl$$1 = /* @__PURE__ */ template(\`<section class=hero><h1>Granola Toolkit</h1><p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p><input class=search placeholder="Search meetings, ids, or tags"><div class="field-row field-row--inline"><label><span class=field-label>Sort</span><select class=select><option value=updated-desc>Newest first</option><option value=updated-asc>Oldest first</option><option value=title-asc>Title A-Z</option><option value=title-desc>Title Z-A</option></select></label><label><span class=field-label>Updated From</span><input class=field-input type=date></label></div><label class=field-row><span class=field-label>Updated To</span><input class=field-input type=date>\`), _tmpl$2 = /* @__PURE__ */ template(\`<section class=toolbar><div><p>Meetings are loaded from the shared server state so this view can stay aligned with the terminal UI and sync loop.</p></div><div class=toolbar-form><input class=field-input placeholder="Quick open by id or title"><button class="button button--secondary"type=button>Open\`), _tmpl$3 = /* @__PURE__ */ template(\`<div class="folder-empty folder-empty--error">\`), _tmpl$4 = /* @__PURE__ */ template(\`<section class=folder-panel><div class=folder-panel__head><h2>Folders</h2><p>Pick a folder to scope the meeting browser, or stay on All meetings.</p></div><div class=folder-list>\`), _tmpl$5 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title>All meetings</span><span class=folder-row__meta>Browse the full meeting list.\`), _tmpl$6 = /* @__PURE__ */ template(\`<div class=folder-empty>No folders found.\`), _tmpl$7 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title></span><span class=folder-row__meta>\`), _tmpl$8 = /* @__PURE__ */ template(\`<div class="meeting-empty meeting-empty--error">\`), _tmpl$9 = /* @__PURE__ */ template(\`<section class=meeting-list>\`), _tmpl$0 = /* @__PURE__ */ template(\`<div class=meeting-empty>\`), _tmpl$1 = /* @__PURE__ */ template(\`<button class=meeting-row type=button><span class=meeting-row__title></span><span class=meeting-row__meta></span><span class=meeting-row__meta>\`), _tmpl$10 = /* @__PURE__ */ template(\`<section class=detail-head><div><h2>Meeting Workspace</h2></div><div class=state-badge>\`), _tmpl$11 = /* @__PURE__ */ template(\`<p>Waiting for server state…\`), _tmpl$12 = /* @__PURE__ */ template(\`<div class=status-grid><div><span class=status-label>Surface</span><strong></strong></div><div><span class=status-label>View</span><strong></strong></div><div><span class=status-label>Auth</span><strong></strong></div><div><span class=status-label>Sync</span><strong></strong></div><div><span class=status-label>Documents</span><strong></strong></div><div><span class=status-label>Folders</span><strong></strong></div><div><span class=status-label>Cache</span><strong></strong></div><div><span class=status-label>Index</span><strong>\`), _tmpl$13 = /* @__PURE__ */ template(\`<section class=security-panel><div class=security-panel__head><h3>Server Access</h3><p>This server is locked with a password. Unlock it to load meetings and live state.</p></div><div class=security-panel__body><input class=field-input placeholder="Server password"type=password><div class=toolbar-actions><button class="button button--primary"type=button>Unlock</button><button class="button button--secondary"type=button>Lock\`), _tmpl$14 = /* @__PURE__ */ template(\`<section class=auth-panel><div class=auth-panel__head><h3>Auth Session</h3><p>Inspect, refresh, and switch between API key, stored session, and <code>supabase.json</code>.</p></div><div class=auth-panel__body>\`), _tmpl$15 = /* @__PURE__ */ template(\`<div class=auth-card><div class=auth-card__meta>Auth state unavailable.\`), _tmpl$16 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Client ID: \`), _tmpl$17 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Sign-in method: \`), _tmpl$18 = /* @__PURE__ */ template(\`<div class=auth-card__meta>supabase path: \`), _tmpl$19 = /* @__PURE__ */ template(\`<div class="auth-card__meta auth-card__error">\`), _tmpl$20 = /* @__PURE__ */ template(\`<div class=auth-card><div class=status-grid><div><span class=status-label>Active</span><strong></strong></div><div><span class=status-label>API key</span><strong></strong></div><div><span class=status-label>Stored</span><strong></strong></div><div><span class=status-label>supabase.json</span><strong></strong></div><div><span class=status-label>Refresh</span><strong></strong></div></div><div class=auth-card__meta>Store a Granola Personal API key here or use <code>granola auth login --api-key &lt;token&gt;</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$27 = /* @__PURE__ */ template(\`<button class=workspace-tab type=button>\`), _tmpl$28 = /* @__PURE__ */ template(\`<div class=empty>\`), _tmpl$29 = /* @__PURE__ */ template(\`<div class=detail-meta><div class=detail-chip></div><div class=detail-chip></div><div class=detail-chip>\`), _tmpl$30 = /* @__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>\`);
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 &lt;token&gt;</code>.</div><div class=auth-card__actions><input class=input placeholder=grn_... type=password><button class="button button--secondary"type=button>Save API key</button><button class="button button--secondary"type=button>Import desktop session</button><button class="button button--secondary"type=button>Refresh stored session</button><button class="button button--secondary"type=button>Use API key</button><button class="button button--secondary"type=button>Use stored session</button><button class="button button--secondary"type=button>Use supabase.json</button><button class="button button--secondary"type=button>Sign out\`), _tmpl$21 = /* @__PURE__ */ template(\`<section class=jobs-panel><div class=jobs-panel__head><h3>Recent Export Jobs</h3><p>Tracked across CLI and web runs.</p></div><div class=jobs-list>\`), _tmpl$22 = /* @__PURE__ */ template(\`<div class=job-empty>No export jobs yet.\`), _tmpl$23 = /* @__PURE__ */ template(\`<div class=job-card__meta>\`), _tmpl$24 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Rerun\`), _tmpl$25 = /* @__PURE__ */ template(\`<article class=job-card><div class=job-card__head><div><div class=job-card__title> export</div><div class=job-card__meta></div></div><div class=job-card__status></div></div><div class=job-card__meta></div><div class=job-card__meta>Started: </div><div class=job-card__meta>Output: </div><div class=job-card__actions>\`), _tmpl$26 = /* @__PURE__ */ template(\`<section class=jobs-panel><div class=jobs-panel__head><h3>Automation Runs</h3><p>Recent action runs triggered by durable sync events.</p></div><div class=jobs-list>\`), _tmpl$27 = /* @__PURE__ */ template(\`<div class=job-empty>No automation runs yet.\`), _tmpl$28 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Approve\`), _tmpl$29 = /* @__PURE__ */ template(\`<button class="button button--secondary"type=button>Reject\`), _tmpl$30 = /* @__PURE__ */ template(\`<article class=job-card><div class=job-card__head><div><div class=job-card__title></div><div class=job-card__meta></div></div><div class=job-card__status></div></div><div class=job-card__meta></div><div class=job-card__meta></div><div class=job-card__actions>\`), _tmpl$31 = /* @__PURE__ */ template(\`<nav class=workspace-tabs><span class=workspace-hint>1-4 switch tabs, [ and ] cycle\`), _tmpl$32 = /* @__PURE__ */ template(\`<button class=workspace-tab type=button>\`), _tmpl$33 = /* @__PURE__ */ template(\`<div class=empty>\`), _tmpl$34 = /* @__PURE__ */ template(\`<div class=detail-meta><div class=detail-chip></div><div class=detail-chip></div><div class=detail-chip>\`), _tmpl$35 = /* @__PURE__ */ template(\`<div class=detail-body><div class=workspace-grid><aside class="detail-section workspace-sidebar"><h2>Meeting Metadata</h2><pre class=detail-pre></pre></aside><section class="detail-section workspace-main"><h2></h2><pre class=detail-pre>\`);
7551
8268
  function authModeLabel(mode) {
7552
8269
  switch (mode) {
7553
8270
  case "api-key": return "API key";
@@ -7764,7 +8481,7 @@ function AppStatePanel(props) {
7764
8481
  return props.appState;
7765
8482
  },
7766
8483
  children: (appState) => (() => {
7767
- var _el$39 = _tmpl$12(), _el$40 = _el$39.firstChild, _el$42 = _el$40.firstChild.nextSibling, _el$43 = _el$40.nextSibling, _el$45 = _el$43.firstChild.nextSibling, _el$46 = _el$43.nextSibling, _el$48 = _el$46.firstChild.nextSibling, _el$49 = _el$46.nextSibling, _el$51 = _el$49.firstChild.nextSibling, _el$52 = _el$49.nextSibling, _el$54 = _el$52.firstChild.nextSibling, _el$55 = _el$52.nextSibling, _el$57 = _el$55.firstChild.nextSibling, _el$58 = _el$55.nextSibling, _el$60 = _el$58.firstChild.nextSibling, _el$63 = _el$58.nextSibling.firstChild.nextSibling;
8484
+ var _el$39 = _tmpl$12(), _el$40 = _el$39.firstChild, _el$42 = _el$40.firstChild.nextSibling, _el$43 = _el$40.nextSibling, _el$45 = _el$43.firstChild.nextSibling, _el$46 = _el$43.nextSibling, _el$48 = _el$46.firstChild.nextSibling, _el$49 = _el$46.nextSibling, _el$51 = _el$49.firstChild.nextSibling, _el$52 = _el$49.nextSibling, _el$54 = _el$52.firstChild.nextSibling, _el$55 = _el$52.nextSibling, _el$57 = _el$55.firstChild.nextSibling, _el$58 = _el$55.nextSibling, _el$60 = _el$58.firstChild.nextSibling, _el$61 = _el$58.nextSibling, _el$63 = _el$61.firstChild.nextSibling, _el$66 = _el$61.nextSibling.firstChild.nextSibling;
7768
8485
  insert(_el$42, () => appState().ui.surface);
7769
8486
  insert(_el$45, () => appState().ui.view);
7770
8487
  insert(_el$48, authStatus);
@@ -7785,6 +8502,7 @@ function AppStatePanel(props) {
7785
8502
  var _c$7 = memo(() => !!appState().index.loaded);
7786
8503
  return () => _c$7() ? \`\${appState().index.meetingCount} meetings\` : appState().index.available ? "available" : "not built";
7787
8504
  })());
8505
+ insert(_el$66, () => \`\${appState().automation.runCount} runs / \${appState().automation.pendingRunCount} pending\`);
7788
8506
  return _el$39;
7789
8507
  })()
7790
8508
  }), null);
@@ -7799,27 +8517,27 @@ function SecurityPanel(props) {
7799
8517
  return props.visible;
7800
8518
  },
7801
8519
  get children() {
7802
- var _el$64 = _tmpl$13(), _el$67 = _el$64.firstChild.nextSibling.firstChild, _el$69 = _el$67.nextSibling.firstChild, _el$70 = _el$69.nextSibling;
7803
- _el$67.$$keydown = (event) => {
8520
+ var _el$67 = _tmpl$13(), _el$70 = _el$67.firstChild.nextSibling.firstChild, _el$72 = _el$70.nextSibling.firstChild, _el$73 = _el$72.nextSibling;
8521
+ _el$70.$$keydown = (event) => {
7804
8522
  if (event.key === "Enter") {
7805
8523
  event.preventDefault();
7806
8524
  props.onUnlock();
7807
8525
  }
7808
8526
  };
7809
- _el$67.$$input = (event) => {
8527
+ _el$70.$$input = (event) => {
7810
8528
  props.onPasswordChange(event.currentTarget.value);
7811
8529
  };
7812
- addEventListener(_el$69, "click", props.onUnlock, true);
7813
- addEventListener(_el$70, "click", props.onLock, true);
7814
- createRenderEffect(() => _el$67.value = props.password);
7815
- return _el$64;
8530
+ addEventListener(_el$72, "click", props.onUnlock, true);
8531
+ addEventListener(_el$73, "click", props.onLock, true);
8532
+ createRenderEffect(() => _el$70.value = props.password);
8533
+ return _el$67;
7816
8534
  }
7817
8535
  });
7818
8536
  }
7819
8537
  function AuthPanel(props) {
7820
8538
  return (() => {
7821
- var _el$71 = _tmpl$14(), _el$73 = _el$71.firstChild.nextSibling;
7822
- insert(_el$73, createComponent(Show, {
8539
+ var _el$74 = _tmpl$14(), _el$76 = _el$74.firstChild.nextSibling;
8540
+ insert(_el$76, createComponent(Show, {
7823
8541
  get fallback() {
7824
8542
  return _tmpl$15();
7825
8543
  },
@@ -7827,79 +8545,79 @@ function AuthPanel(props) {
7827
8545
  return props.auth;
7828
8546
  },
7829
8547
  children: (auth) => (() => {
7830
- var _el$75 = _tmpl$20(), _el$76 = _el$75.firstChild, _el$77 = _el$76.firstChild, _el$79 = _el$77.firstChild.nextSibling, _el$80 = _el$77.nextSibling, _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$91 = _el$86.nextSibling.firstChild.nextSibling, _el$99 = _el$76.nextSibling, _el$101 = _el$99.nextSibling.firstChild, _el$102 = _el$101.nextSibling, _el$103 = _el$102.nextSibling, _el$104 = _el$103.nextSibling, _el$105 = _el$104.nextSibling, _el$106 = _el$105.nextSibling, _el$107 = _el$106.nextSibling, _el$108 = _el$107.nextSibling;
7831
- insert(_el$79, () => authModeLabel(auth().mode));
7832
- insert(_el$82, () => auth().apiKeyAvailable ? "available" : "missing");
7833
- insert(_el$85, () => auth().storedSessionAvailable ? "available" : "missing");
7834
- insert(_el$88, () => auth().supabaseAvailable ? "available" : "missing");
7835
- insert(_el$91, () => auth().refreshAvailable ? "available" : "missing");
7836
- insert(_el$75, createComponent(Show, {
8548
+ var _el$78 = _tmpl$20(), _el$79 = _el$78.firstChild, _el$80 = _el$79.firstChild, _el$82 = _el$80.firstChild.nextSibling, _el$83 = _el$80.nextSibling, _el$85 = _el$83.firstChild.nextSibling, _el$86 = _el$83.nextSibling, _el$88 = _el$86.firstChild.nextSibling, _el$89 = _el$86.nextSibling, _el$91 = _el$89.firstChild.nextSibling, _el$94 = _el$89.nextSibling.firstChild.nextSibling, _el$102 = _el$79.nextSibling, _el$104 = _el$102.nextSibling.firstChild, _el$105 = _el$104.nextSibling, _el$106 = _el$105.nextSibling, _el$107 = _el$106.nextSibling, _el$108 = _el$107.nextSibling, _el$109 = _el$108.nextSibling, _el$110 = _el$109.nextSibling, _el$111 = _el$110.nextSibling;
8549
+ insert(_el$82, () => authModeLabel(auth().mode));
8550
+ insert(_el$85, () => auth().apiKeyAvailable ? "available" : "missing");
8551
+ insert(_el$88, () => auth().storedSessionAvailable ? "available" : "missing");
8552
+ insert(_el$91, () => auth().supabaseAvailable ? "available" : "missing");
8553
+ insert(_el$94, () => auth().refreshAvailable ? "available" : "missing");
8554
+ insert(_el$78, createComponent(Show, {
7837
8555
  get when() {
7838
8556
  return auth().clientId;
7839
8557
  },
7840
8558
  get children() {
7841
- var _el$92 = _tmpl$16();
7842
- _el$92.firstChild;
7843
- insert(_el$92, () => auth().clientId, null);
7844
- return _el$92;
8559
+ var _el$95 = _tmpl$16();
8560
+ _el$95.firstChild;
8561
+ insert(_el$95, () => auth().clientId, null);
8562
+ return _el$95;
7845
8563
  }
7846
- }), _el$99);
7847
- insert(_el$75, createComponent(Show, {
8564
+ }), _el$102);
8565
+ insert(_el$78, createComponent(Show, {
7848
8566
  get when() {
7849
8567
  return auth().signInMethod;
7850
8568
  },
7851
8569
  get children() {
7852
- var _el$94 = _tmpl$17();
7853
- _el$94.firstChild;
7854
- insert(_el$94, () => auth().signInMethod, null);
7855
- return _el$94;
8570
+ var _el$97 = _tmpl$17();
8571
+ _el$97.firstChild;
8572
+ insert(_el$97, () => auth().signInMethod, null);
8573
+ return _el$97;
7856
8574
  }
7857
- }), _el$99);
7858
- insert(_el$75, createComponent(Show, {
8575
+ }), _el$102);
8576
+ insert(_el$78, createComponent(Show, {
7859
8577
  get when() {
7860
8578
  return auth().supabasePath;
7861
8579
  },
7862
8580
  get children() {
7863
- var _el$96 = _tmpl$18();
7864
- _el$96.firstChild;
7865
- insert(_el$96, () => auth().supabasePath, null);
7866
- return _el$96;
8581
+ var _el$99 = _tmpl$18();
8582
+ _el$99.firstChild;
8583
+ insert(_el$99, () => auth().supabasePath, null);
8584
+ return _el$99;
7867
8585
  }
7868
- }), _el$99);
7869
- insert(_el$75, createComponent(Show, {
8586
+ }), _el$102);
8587
+ insert(_el$78, createComponent(Show, {
7870
8588
  get when() {
7871
8589
  return auth().lastError;
7872
8590
  },
7873
8591
  get children() {
7874
- var _el$98 = _tmpl$19();
7875
- insert(_el$98, () => auth().lastError);
7876
- return _el$98;
8592
+ var _el$101 = _tmpl$19();
8593
+ insert(_el$101, () => auth().lastError);
8594
+ return _el$101;
7877
8595
  }
7878
- }), _el$99);
7879
- _el$101.$$input = (event) => {
8596
+ }), _el$102);
8597
+ _el$104.$$input = (event) => {
7880
8598
  props.onApiKeyDraftChange(event.currentTarget.value);
7881
8599
  };
7882
- addEventListener(_el$102, "click", props.onSaveApiKey, true);
7883
- addEventListener(_el$103, "click", props.onImportDesktopSession, true);
7884
- addEventListener(_el$104, "click", props.onRefresh, true);
7885
- _el$105.$$click = () => {
8600
+ addEventListener(_el$105, "click", props.onSaveApiKey, true);
8601
+ addEventListener(_el$106, "click", props.onImportDesktopSession, true);
8602
+ addEventListener(_el$107, "click", props.onRefresh, true);
8603
+ _el$108.$$click = () => {
7886
8604
  props.onSwitchMode("api-key");
7887
8605
  };
7888
- _el$106.$$click = () => {
8606
+ _el$109.$$click = () => {
7889
8607
  props.onSwitchMode("stored-session");
7890
8608
  };
7891
- _el$107.$$click = () => {
8609
+ _el$110.$$click = () => {
7892
8610
  props.onSwitchMode("supabase-file");
7893
8611
  };
7894
- addEventListener(_el$108, "click", props.onLogout, true);
8612
+ addEventListener(_el$111, "click", props.onLogout, true);
7895
8613
  createRenderEffect((_p$) => {
7896
8614
  var _v$ = !auth().supabaseAvailable, _v$2 = !auth().storedSessionAvailable || !auth().refreshAvailable, _v$3 = !auth().apiKeyAvailable || auth().mode === "api-key", _v$4 = !auth().storedSessionAvailable || auth().mode === "stored-session", _v$5 = !auth().supabaseAvailable || auth().mode === "supabase-file", _v$6 = !auth().apiKeyAvailable && !auth().storedSessionAvailable;
7897
- _v$ !== _p$.e && (_el$103.disabled = _p$.e = _v$);
7898
- _v$2 !== _p$.t && (_el$104.disabled = _p$.t = _v$2);
7899
- _v$3 !== _p$.a && (_el$105.disabled = _p$.a = _v$3);
7900
- _v$4 !== _p$.o && (_el$106.disabled = _p$.o = _v$4);
7901
- _v$5 !== _p$.i && (_el$107.disabled = _p$.i = _v$5);
7902
- _v$6 !== _p$.n && (_el$108.disabled = _p$.n = _v$6);
8615
+ _v$ !== _p$.e && (_el$106.disabled = _p$.e = _v$);
8616
+ _v$2 !== _p$.t && (_el$107.disabled = _p$.t = _v$2);
8617
+ _v$3 !== _p$.a && (_el$108.disabled = _p$.a = _v$3);
8618
+ _v$4 !== _p$.o && (_el$109.disabled = _p$.o = _v$4);
8619
+ _v$5 !== _p$.i && (_el$110.disabled = _p$.i = _v$5);
8620
+ _v$6 !== _p$.n && (_el$111.disabled = _p$.n = _v$6);
7903
8621
  return _p$;
7904
8622
  }, {
7905
8623
  e: void 0,
@@ -7909,17 +8627,17 @@ function AuthPanel(props) {
7909
8627
  i: void 0,
7910
8628
  n: void 0
7911
8629
  });
7912
- createRenderEffect(() => _el$101.value = props.apiKeyDraft);
7913
- return _el$75;
8630
+ createRenderEffect(() => _el$104.value = props.apiKeyDraft);
8631
+ return _el$78;
7914
8632
  })()
7915
8633
  }));
7916
- return _el$71;
8634
+ return _el$74;
7917
8635
  })();
7918
8636
  }
7919
8637
  function ExportJobsPanel(props) {
7920
8638
  return (() => {
7921
- var _el$109 = _tmpl$21(), _el$111 = _el$109.firstChild.nextSibling;
7922
- insert(_el$111, createComponent(Show, {
8639
+ var _el$112 = _tmpl$21(), _el$114 = _el$112.firstChild.nextSibling;
8640
+ insert(_el$114, createComponent(Show, {
7923
8641
  get when() {
7924
8642
  return props.jobs.length > 0;
7925
8643
  },
@@ -7932,46 +8650,127 @@ function ExportJobsPanel(props) {
7932
8650
  return props.jobs.slice(0, 6);
7933
8651
  },
7934
8652
  children: (job) => (() => {
7935
- var _el$113 = _tmpl$25(), _el$114 = _el$113.firstChild, _el$115 = _el$114.firstChild, _el$116 = _el$115.firstChild, _el$117 = _el$116.firstChild, _el$118 = _el$116.nextSibling, _el$119 = _el$115.nextSibling, _el$120 = _el$114.nextSibling, _el$121 = _el$120.nextSibling;
7936
- _el$121.firstChild;
7937
- var _el$123 = _el$121.nextSibling;
7938
- _el$123.firstChild;
7939
- var _el$126 = _el$123.nextSibling;
7940
- insert(_el$116, () => job.kind, _el$117);
7941
- insert(_el$118, () => job.id);
7942
- insert(_el$119, () => job.status);
7943
- insert(_el$120, () => \`Format: \${job.format} • \${scopeLabel(job.scope)} • \${job.itemCount > 0 ? \`\${job.completedCount}/\${job.itemCount} items\` : "0 items"} • Written: \${job.written}\`);
7944
- insert(_el$121, () => job.startedAt.slice(0, 19), null);
7945
- insert(_el$123, () => job.outputDir, null);
7946
- insert(_el$113, createComponent(Show, {
8653
+ var _el$116 = _tmpl$25(), _el$117 = _el$116.firstChild, _el$118 = _el$117.firstChild, _el$119 = _el$118.firstChild, _el$120 = _el$119.firstChild, _el$121 = _el$119.nextSibling, _el$122 = _el$118.nextSibling, _el$123 = _el$117.nextSibling, _el$124 = _el$123.nextSibling;
8654
+ _el$124.firstChild;
8655
+ var _el$126 = _el$124.nextSibling;
8656
+ _el$126.firstChild;
8657
+ var _el$129 = _el$126.nextSibling;
8658
+ insert(_el$119, () => job.kind, _el$120);
8659
+ insert(_el$121, () => job.id);
8660
+ insert(_el$122, () => job.status);
8661
+ insert(_el$123, () => \`Format: \${job.format} • \${scopeLabel(job.scope)} • \${job.itemCount > 0 ? \`\${job.completedCount}/\${job.itemCount} items\` : "0 items"} • Written: \${job.written}\`);
8662
+ insert(_el$124, () => job.startedAt.slice(0, 19), null);
8663
+ insert(_el$126, () => job.outputDir, null);
8664
+ insert(_el$116, createComponent(Show, {
7947
8665
  get when() {
7948
8666
  return job.error;
7949
8667
  },
7950
8668
  get children() {
7951
- var _el$125 = _tmpl$23();
7952
- insert(_el$125, () => job.error);
7953
- return _el$125;
8669
+ var _el$128 = _tmpl$23();
8670
+ insert(_el$128, () => job.error);
8671
+ return _el$128;
7954
8672
  }
7955
- }), _el$126);
7956
- insert(_el$126, createComponent(Show, {
8673
+ }), _el$129);
8674
+ insert(_el$129, createComponent(Show, {
7957
8675
  get when() {
7958
8676
  return job.status !== "running";
7959
8677
  },
7960
8678
  get children() {
7961
- var _el$127 = _tmpl$24();
7962
- _el$127.$$click = () => {
8679
+ var _el$130 = _tmpl$24();
8680
+ _el$130.$$click = () => {
7963
8681
  props.onRerun(job.id);
7964
8682
  };
7965
- return _el$127;
8683
+ return _el$130;
8684
+ }
8685
+ }));
8686
+ createRenderEffect(() => setAttribute(_el$122, "data-status", job.status));
8687
+ return _el$116;
8688
+ })()
8689
+ });
8690
+ }
8691
+ }));
8692
+ return _el$112;
8693
+ })();
8694
+ }
8695
+ function AutomationRunsPanel(props) {
8696
+ return (() => {
8697
+ var _el$131 = _tmpl$26(), _el$133 = _el$131.firstChild.nextSibling;
8698
+ insert(_el$133, createComponent(Show, {
8699
+ get when() {
8700
+ return props.runs.length > 0;
8701
+ },
8702
+ get fallback() {
8703
+ return _tmpl$27();
8704
+ },
8705
+ get children() {
8706
+ return createComponent(For, {
8707
+ get each() {
8708
+ return props.runs.slice(0, 6);
8709
+ },
8710
+ children: (run) => (() => {
8711
+ var _el$135 = _tmpl$30(), _el$136 = _el$135.firstChild, _el$137 = _el$136.firstChild, _el$138 = _el$137.firstChild, _el$139 = _el$138.nextSibling, _el$140 = _el$137.nextSibling, _el$141 = _el$136.nextSibling, _el$142 = _el$141.nextSibling, _el$146 = _el$142.nextSibling;
8712
+ insert(_el$138, () => run.actionName);
8713
+ insert(_el$139, () => \`\${run.ruleName} • \${run.id}\`);
8714
+ insert(_el$140, () => run.status);
8715
+ insert(_el$141, () => \`\${run.title} • \${run.eventKind}\`);
8716
+ insert(_el$142, () => \`Started: \${run.startedAt.slice(0, 19)}\`);
8717
+ insert(_el$135, createComponent(Show, {
8718
+ get when() {
8719
+ return run.prompt;
8720
+ },
8721
+ get children() {
8722
+ var _el$143 = _tmpl$23();
8723
+ insert(_el$143, () => run.prompt);
8724
+ return _el$143;
8725
+ }
8726
+ }), _el$146);
8727
+ insert(_el$135, createComponent(Show, {
8728
+ get when() {
8729
+ return run.result;
8730
+ },
8731
+ get children() {
8732
+ var _el$144 = _tmpl$23();
8733
+ insert(_el$144, () => run.result);
8734
+ return _el$144;
8735
+ }
8736
+ }), _el$146);
8737
+ insert(_el$135, createComponent(Show, {
8738
+ get when() {
8739
+ return run.error;
8740
+ },
8741
+ get children() {
8742
+ var _el$145 = _tmpl$23();
8743
+ insert(_el$145, () => run.error);
8744
+ return _el$145;
8745
+ }
8746
+ }), _el$146);
8747
+ insert(_el$146, createComponent(Show, {
8748
+ get when() {
8749
+ return run.status === "pending";
8750
+ },
8751
+ get children() {
8752
+ return [(() => {
8753
+ var _el$147 = _tmpl$28();
8754
+ _el$147.$$click = () => {
8755
+ props.onApprove(run.id);
8756
+ };
8757
+ return _el$147;
8758
+ })(), (() => {
8759
+ var _el$148 = _tmpl$29();
8760
+ _el$148.$$click = () => {
8761
+ props.onReject(run.id);
8762
+ };
8763
+ return _el$148;
8764
+ })()];
7966
8765
  }
7967
8766
  }));
7968
- createRenderEffect(() => setAttribute(_el$119, "data-status", job.status));
7969
- return _el$113;
8767
+ createRenderEffect(() => setAttribute(_el$140, "data-status", run.status));
8768
+ return _el$135;
7970
8769
  })()
7971
8770
  });
7972
8771
  }
7973
8772
  }));
7974
- return _el$109;
8773
+ return _el$131;
7975
8774
  })();
7976
8775
  }
7977
8776
  function Workspace(props) {
@@ -7981,8 +8780,8 @@ function Workspace(props) {
7981
8780
  return workspaceBody(props.bundle, props.selectedMeeting, parsedTab());
7982
8781
  };
7983
8782
  return [(() => {
7984
- var _el$128 = _tmpl$26(), _el$129 = _el$128.firstChild;
7985
- insert(_el$128, createComponent(For, {
8783
+ var _el$149 = _tmpl$31(), _el$150 = _el$149.firstChild;
8784
+ insert(_el$149, createComponent(For, {
7986
8785
  each: [
7987
8786
  "notes",
7988
8787
  "transcript",
@@ -7990,50 +8789,50 @@ function Workspace(props) {
7990
8789
  "raw"
7991
8790
  ],
7992
8791
  children: (tab) => (() => {
7993
- var _el$130 = _tmpl$27();
7994
- _el$130.$$click = () => {
8792
+ var _el$151 = _tmpl$32();
8793
+ _el$151.$$click = () => {
7995
8794
  props.onSelectTab(tab);
7996
8795
  };
7997
- insert(_el$130, tab === "notes" ? "Notes" : tab === "transcript" ? "Transcript" : tab === "metadata" ? "Metadata" : "Raw");
7998
- createRenderEffect(() => setAttribute(_el$130, "data-selected", parsedTab() === tab ? "true" : void 0));
7999
- return _el$130;
8796
+ insert(_el$151, tab === "notes" ? "Notes" : tab === "transcript" ? "Transcript" : tab === "metadata" ? "Metadata" : "Raw");
8797
+ createRenderEffect(() => setAttribute(_el$151, "data-selected", parsedTab() === tab ? "true" : void 0));
8798
+ return _el$151;
8000
8799
  })()
8001
- }), _el$129);
8002
- return _el$128;
8800
+ }), _el$150);
8801
+ return _el$149;
8003
8802
  })(), createComponent(Show, {
8004
8803
  get when() {
8005
8804
  return props.selectedMeeting;
8006
8805
  },
8007
8806
  get fallback() {
8008
8807
  return (() => {
8009
- var _el$131 = _tmpl$28();
8010
- insert(_el$131, () => props.detailError || "Select a meeting to inspect its notes and transcript.");
8011
- return _el$131;
8808
+ var _el$152 = _tmpl$33();
8809
+ insert(_el$152, () => props.detailError || "Select a meeting to inspect its notes and transcript.");
8810
+ return _el$152;
8012
8811
  })();
8013
8812
  },
8014
8813
  children: (meeting) => [(() => {
8015
- var _el$132 = _tmpl$29(), _el$133 = _el$132.firstChild, _el$134 = _el$133.nextSibling, _el$135 = _el$134.nextSibling;
8016
- insert(_el$133, () => \`ID: \${meeting().meeting.id}\`);
8017
- insert(_el$134, () => \`Source: \${meeting().meeting.noteContentSource}\`);
8018
- insert(_el$135, () => \`Transcript: \${meeting().meeting.transcriptSegmentCount} segments\`);
8019
- return _el$132;
8814
+ var _el$153 = _tmpl$34(), _el$154 = _el$153.firstChild, _el$155 = _el$154.nextSibling, _el$156 = _el$155.nextSibling;
8815
+ insert(_el$154, () => \`ID: \${meeting().meeting.id}\`);
8816
+ insert(_el$155, () => \`Source: \${meeting().meeting.noteContentSource}\`);
8817
+ insert(_el$156, () => \`Transcript: \${meeting().meeting.transcriptSegmentCount} segments\`);
8818
+ return _el$153;
8020
8819
  })(), createComponent(Show, {
8021
8820
  get when() {
8022
8821
  return !props.detailError;
8023
8822
  },
8024
8823
  get fallback() {
8025
8824
  return (() => {
8026
- var _el$144 = _tmpl$28();
8027
- insert(_el$144, () => props.detailError);
8028
- return _el$144;
8825
+ var _el$165 = _tmpl$33();
8826
+ insert(_el$165, () => props.detailError);
8827
+ return _el$165;
8029
8828
  })();
8030
8829
  },
8031
8830
  get children() {
8032
- var _el$136 = _tmpl$30(), _el$138 = _el$136.firstChild.firstChild, _el$140 = _el$138.firstChild.nextSibling, _el$142 = _el$138.nextSibling.firstChild, _el$143 = _el$142.nextSibling;
8033
- insert(_el$140, () => metadataLines(meeting()));
8034
- insert(_el$142, () => details()?.title);
8035
- insert(_el$143, () => details()?.body);
8036
- return _el$136;
8831
+ var _el$157 = _tmpl$35(), _el$159 = _el$157.firstChild.firstChild, _el$161 = _el$159.firstChild.nextSibling, _el$163 = _el$159.nextSibling.firstChild, _el$164 = _el$163.nextSibling;
8832
+ insert(_el$161, () => metadataLines(meeting()));
8833
+ insert(_el$163, () => details()?.title);
8834
+ insert(_el$164, () => details()?.body);
8835
+ return _el$157;
8037
8836
  }
8038
8837
  })]
8039
8838
  })];
@@ -8064,6 +8863,7 @@ function App() {
8064
8863
  const [state, setState] = createStore({
8065
8864
  apiKeyDraft: "",
8066
8865
  appState: null,
8866
+ automationRuns: [],
8067
8867
  detailError: "",
8068
8868
  folderError: "",
8069
8869
  folders: [],
@@ -8132,6 +8932,14 @@ function App() {
8132
8932
  setState("selectedFolderId", null);
8133
8933
  }
8134
8934
  };
8935
+ const loadAutomationRuns = async () => {
8936
+ if (!client) return;
8937
+ try {
8938
+ setState("automationRuns", (await client.listAutomationRuns({ limit: 20 })).runs);
8939
+ } catch (error) {
8940
+ setState("detailError", error instanceof Error ? error.message : String(error));
8941
+ }
8942
+ };
8135
8943
  const loadMeeting = async (meetingId) => {
8136
8944
  if (!client) return;
8137
8945
  setState("selectedMeetingId", meetingId);
@@ -8185,7 +8993,11 @@ function App() {
8185
8993
  forceRefresh: true,
8186
8994
  foreground: true
8187
8995
  });
8188
- await Promise.all([loadFolders(forceRefresh), mergeAuthState()]);
8996
+ await Promise.all([
8997
+ loadFolders(forceRefresh),
8998
+ loadAutomationRuns(),
8999
+ mergeAuthState()
9000
+ ]);
8189
9001
  await loadMeetings({ refresh: forceRefresh });
8190
9002
  setState("serverLocked", false);
8191
9003
  setStatus(forceRefresh ? "Sync complete" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
@@ -8318,6 +9130,17 @@ function App() {
8318
9130
  setStatus("Rerun failed", "error");
8319
9131
  }
8320
9132
  };
9133
+ const resolveAutomationRun = async (id, decision) => {
9134
+ if (!client) return;
9135
+ setStatus(decision === "approve" ? "Approving automation…" : "Rejecting automation…", "busy");
9136
+ try {
9137
+ await client.resolveAutomationRun(id, decision);
9138
+ await refreshAll();
9139
+ } catch (error) {
9140
+ setState("detailError", error instanceof Error ? error.message : String(error));
9141
+ setStatus("Automation decision failed", "error");
9142
+ }
9143
+ };
8321
9144
  const unlockServer = async () => {
8322
9145
  if (!state.serverPassword.trim()) {
8323
9146
  setStatus("Enter the server password", "error");
@@ -8345,6 +9168,7 @@ function App() {
8345
9168
  await detachClient();
8346
9169
  setState({
8347
9170
  appState: null,
9171
+ automationRuns: [],
8348
9172
  detailError: "",
8349
9173
  folderError: "",
8350
9174
  folders: [],
@@ -8367,6 +9191,10 @@ function App() {
8367
9191
  });
8368
9192
  if (nextPath !== \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`) history.replaceState(null, "", nextPath);
8369
9193
  });
9194
+ createEffect(() => {
9195
+ if (!state.appState?.automation.loaded || !client) return;
9196
+ loadAutomationRuns();
9197
+ });
8370
9198
  onMount(() => {
8371
9199
  const onKeyDown = (event) => {
8372
9200
  const target = event.target;
@@ -8542,6 +9370,17 @@ function App() {
8542
9370
  rerunJob(jobId);
8543
9371
  }
8544
9372
  }), null);
9373
+ insert(_el$3, createComponent(AutomationRunsPanel, {
9374
+ onApprove: (runId) => {
9375
+ resolveAutomationRun(runId, "approve");
9376
+ },
9377
+ onReject: (runId) => {
9378
+ resolveAutomationRun(runId, "reject");
9379
+ },
9380
+ get runs() {
9381
+ return state.automationRuns;
9382
+ }
9383
+ }), null);
8545
9384
  insert(_el$3, createComponent(Workspace, {
8546
9385
  get bundle() {
8547
9386
  return state.selectedMeetingBundle;
@@ -8639,6 +9478,17 @@ function parseAuthMode(value) {
8639
9478
  default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
8640
9479
  }
8641
9480
  }
9481
+ function parseAutomationRunStatus(value) {
9482
+ switch (value) {
9483
+ case null:
9484
+ case "": return;
9485
+ case "completed":
9486
+ case "failed":
9487
+ case "pending":
9488
+ case "skipped": return value;
9489
+ default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
9490
+ }
9491
+ }
8642
9492
  function folderIdFromBody(value) {
8643
9493
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
8644
9494
  }
@@ -8765,6 +9615,7 @@ async function startGranolaServer(app, options = {}) {
8765
9615
  capabilities: {
8766
9616
  attach: true,
8767
9617
  auth: true,
9618
+ automation: true,
8768
9619
  events: true,
8769
9620
  exports: true,
8770
9621
  folders: true,
@@ -8893,6 +9744,20 @@ async function startGranolaServer(app, options = {}) {
8893
9744
  sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
8894
9745
  return;
8895
9746
  }
9747
+ if (method === "GET" && path === granolaTransportPaths.automationRuns) {
9748
+ sendJson(response, await app.listAutomationRuns({
9749
+ limit: parseInteger(url.searchParams.get("limit")),
9750
+ status: parseAutomationRunStatus(url.searchParams.get("status"))
9751
+ }), { headers: originHeaders });
9752
+ return;
9753
+ }
9754
+ if (method === "POST" && (path.endsWith("/approve") || path.endsWith("/reject")) && path.startsWith(`${granolaTransportPaths.automationRuns}/`)) {
9755
+ const decision = path.endsWith("/approve") ? "approve" : "reject";
9756
+ const id = decodeURIComponent(path.slice(`${granolaTransportPaths.automationRuns}/`.length, -`/${decision}`.length));
9757
+ const body = await readJsonBody(request);
9758
+ sendJson(response, await app.resolveAutomationRun(id, decision, { note: typeof body.note === "string" ? body.note : void 0 }), { headers: originHeaders });
9759
+ return;
9760
+ }
8896
9761
  if (method === "POST" && path === granolaTransportPaths.syncRun) {
8897
9762
  const body = await readJsonBody(request);
8898
9763
  sendJson(response, await app.sync({