granola-toolkit 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +1412 -159
  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
@@ -18,6 +18,9 @@ const granolaTransportPaths = {
18
18
  authRefresh: "/auth/refresh",
19
19
  authStatus: "/auth/status",
20
20
  authUnlock: "/auth/unlock",
21
+ automationMatches: "/automation/matches",
22
+ automationRules: "/automation/rules",
23
+ automationRuns: "/automation/runs",
21
24
  events: "/events",
22
25
  exportJobs: "/exports/jobs",
23
26
  exportNotes: "/exports/notes",
@@ -77,6 +80,15 @@ function granolaFoldersPath(options = {}) {
77
80
  function granolaExportJobsPath(options = {}) {
78
81
  return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
79
82
  }
83
+ function granolaAutomationRunsPath(options = {}) {
84
+ return appendSearchParams(granolaTransportPaths.automationRuns, {
85
+ limit: options.limit,
86
+ status: options.status
87
+ });
88
+ }
89
+ function granolaAutomationRunDecisionPath(id, decision) {
90
+ return `${granolaTransportPaths.automationRuns}/${encodeURIComponent(id)}/${decision}`;
91
+ }
80
92
  function granolaExportJobRerunPath(id) {
81
93
  return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
82
94
  }
@@ -179,6 +191,23 @@ var GranolaServerClient = class GranolaServerClient {
179
191
  async inspectAuth() {
180
192
  return await this.requestJson(granolaTransportPaths.authStatus);
181
193
  }
194
+ async listAutomationRules() {
195
+ return await this.requestJson(granolaTransportPaths.automationRules);
196
+ }
197
+ async listAutomationMatches(options = {}) {
198
+ const path = options.limit ? `${granolaTransportPaths.automationMatches}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.automationMatches;
199
+ return await this.requestJson(path);
200
+ }
201
+ async listAutomationRuns(options = {}) {
202
+ return await this.requestJson(granolaAutomationRunsPath(options));
203
+ }
204
+ async resolveAutomationRun(id, decision, options = {}) {
205
+ return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
206
+ body: JSON.stringify(options),
207
+ headers: { "content-type": "application/json" },
208
+ method: "POST"
209
+ });
210
+ }
182
211
  async inspectSync() {
183
212
  return cloneValue(this.#state.sync);
184
213
  }
@@ -420,7 +449,7 @@ function asRecord(value) {
420
449
  function stringValue(value) {
421
450
  return typeof value === "string" ? value : "";
422
451
  }
423
- function stringArray(value) {
452
+ function stringArray$1(value) {
424
453
  if (!Array.isArray(value)) return [];
425
454
  return value.filter((item) => typeof item === "string");
426
455
  }
@@ -1383,7 +1412,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
1383
1412
  }
1384
1413
  }
1385
1414
  function buildGranolaTuiSummary(state, meetingSource) {
1386
- return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
1415
+ return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | ${state.automation.pendingRunCount ? `${state.automation.pendingRunCount} pending` : state.automation.runCount ? `${state.automation.runCount} runs` : "automation idle"} | list ${meetingSource}`;
1387
1416
  }
1388
1417
  //#endregion
1389
1418
  //#region src/tui/theme.ts
@@ -1415,6 +1444,81 @@ const granolaTuiTheme = {
1415
1444
  }
1416
1445
  };
1417
1446
  //#endregion
1447
+ //#region src/tui/automation.ts
1448
+ function padLine$3(text, width) {
1449
+ const clipped = truncateToWidth(text, width, "");
1450
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
1451
+ }
1452
+ function frameLine$2(text, width) {
1453
+ return `| ${padLine$3(text, Math.max(1, width - 4))} |`;
1454
+ }
1455
+ function wrapDetails(text, width) {
1456
+ return wrapTextWithAnsi(text, Math.max(1, width - 4));
1457
+ }
1458
+ function statusLabel(run) {
1459
+ switch (run.status) {
1460
+ case "completed": return granolaTuiTheme.info(run.status);
1461
+ case "failed": return granolaTuiTheme.error(run.status);
1462
+ case "pending": return granolaTuiTheme.warning(run.status);
1463
+ default: return granolaTuiTheme.dim(run.status);
1464
+ }
1465
+ }
1466
+ var GranolaTuiAutomationOverlay = class {
1467
+ focused = false;
1468
+ #selectedIndex = 0;
1469
+ constructor(options) {
1470
+ this.options = options;
1471
+ }
1472
+ invalidate() {}
1473
+ get selected() {
1474
+ return this.options.runs[this.#selectedIndex];
1475
+ }
1476
+ handleInput(data) {
1477
+ if (matchesKey(data, "esc")) {
1478
+ this.options.onCancel();
1479
+ return;
1480
+ }
1481
+ if (matchesKey(data, "up")) {
1482
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
1483
+ return;
1484
+ }
1485
+ if (matchesKey(data, "down")) {
1486
+ this.#selectedIndex = Math.min(this.options.runs.length - 1, this.#selectedIndex + 1);
1487
+ return;
1488
+ }
1489
+ if (matchesKey(data, "enter") || matchesKey(data, "a")) {
1490
+ if (this.selected?.status === "pending") this.options.onApprove(this.selected.id);
1491
+ return;
1492
+ }
1493
+ if (matchesKey(data, "r")) {
1494
+ if (this.selected?.status === "pending") this.options.onReject(this.selected.id);
1495
+ }
1496
+ }
1497
+ render(width) {
1498
+ const bodyWidth = Math.max(56, width);
1499
+ const innerWidth = Math.max(1, bodyWidth - 4);
1500
+ const maxRuns = 6;
1501
+ const lines = [];
1502
+ lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
1503
+ lines.push(frameLine$2(granolaTuiTheme.strong("Automation Runs"), bodyWidth));
1504
+ lines.push(frameLine$2("Pending runs can be approved with Enter/a or rejected with r.", bodyWidth));
1505
+ lines.push(frameLine$2("", bodyWidth));
1506
+ if (this.options.runs.length === 0) lines.push(frameLine$2(granolaTuiTheme.dim("No automation runs yet."), bodyWidth));
1507
+ else for (const [index, run] of this.options.runs.slice(0, maxRuns).entries()) {
1508
+ const selected = index === this.#selectedIndex;
1509
+ const title = `${selected ? ">" : " "} ${run.actionName} · ${statusLabel(run)}`;
1510
+ lines.push(frameLine$2(selected ? granolaTuiTheme.selected(title) : title, bodyWidth));
1511
+ lines.push(frameLine$2(` ${run.ruleName} · ${run.title}`, bodyWidth));
1512
+ const details = run.prompt || run.result || run.error || run.eventKind;
1513
+ for (const line of wrapDetails(` ${details}`, innerWidth).slice(0, 2)) lines.push(frameLine$2(line, bodyWidth));
1514
+ lines.push(frameLine$2("", bodyWidth));
1515
+ }
1516
+ lines.push(frameLine$2(granolaTuiTheme.dim("Esc close"), bodyWidth));
1517
+ lines.push(`+${"-".repeat(bodyWidth - 2)}+`);
1518
+ return lines;
1519
+ }
1520
+ };
1521
+ //#endregion
1418
1522
  //#region src/tui/auth.ts
1419
1523
  function padLine$2(text, width) {
1420
1524
  const clipped = truncateToWidth(text, width, "");
@@ -1691,6 +1795,7 @@ var GranolaTuiWorkspace = class {
1691
1795
  #maxMeetings;
1692
1796
  #appState;
1693
1797
  #activePane = "meetings";
1798
+ #automationRuns = [];
1694
1799
  #detailError = "";
1695
1800
  #detailScroll = 0;
1696
1801
  #detailToken = 0;
@@ -1723,6 +1828,7 @@ var GranolaTuiWorkspace = class {
1723
1828
  this.#unsubscribe = this.app.subscribe((event) => {
1724
1829
  this.handleAppUpdate(event);
1725
1830
  });
1831
+ await this.loadAutomationRuns();
1726
1832
  await this.loadFolders({ setStatus: false });
1727
1833
  await this.loadMeetings({
1728
1834
  preferredMeetingId: this.options.initialMeetingId,
@@ -1741,6 +1847,7 @@ var GranolaTuiWorkspace = class {
1741
1847
  this.#appState = event.state;
1742
1848
  this.#selectedFolderId = event.state.ui.selectedFolderId;
1743
1849
  this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
1850
+ this.loadAutomationRuns();
1744
1851
  if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
1745
1852
  await this.loadFolders({ setStatus: false });
1746
1853
  await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
@@ -1795,6 +1902,12 @@ var GranolaTuiWorkspace = class {
1795
1902
  if (token === this.#folderToken) this.tui.requestRender();
1796
1903
  }
1797
1904
  }
1905
+ async loadAutomationRuns() {
1906
+ try {
1907
+ this.#automationRuns = [...(await this.app.listAutomationRuns({ limit: 20 })).runs];
1908
+ this.tui.requestRender();
1909
+ } catch {}
1910
+ }
1798
1911
  async loadMeetings(options = {}) {
1799
1912
  const token = ++this.#listToken;
1800
1913
  this.#loadingMeetings = true;
@@ -2070,6 +2183,38 @@ var GranolaTuiWorkspace = class {
2070
2183
  });
2071
2184
  this.setStatus("Quick open");
2072
2185
  }
2186
+ openAutomationPanel() {
2187
+ if (this.#overlay) return;
2188
+ const closeOverlay = () => {
2189
+ this.#overlay?.hide();
2190
+ this.#overlay = void 0;
2191
+ this.tui.setFocus(this);
2192
+ this.tui.requestRender();
2193
+ };
2194
+ const overlay = new GranolaTuiAutomationOverlay({
2195
+ onApprove: async (id) => {
2196
+ closeOverlay();
2197
+ await this.app.resolveAutomationRun(id, "approve");
2198
+ await this.loadAutomationRuns();
2199
+ this.setStatus("Automation approved");
2200
+ },
2201
+ onCancel: closeOverlay,
2202
+ onReject: async (id) => {
2203
+ closeOverlay();
2204
+ await this.app.resolveAutomationRun(id, "reject");
2205
+ await this.loadAutomationRuns();
2206
+ this.setStatus("Automation rejected");
2207
+ },
2208
+ runs: this.#automationRuns
2209
+ });
2210
+ this.#overlay = this.tui.showOverlay(overlay, {
2211
+ anchor: "center",
2212
+ maxHeight: "70%",
2213
+ minWidth: 56,
2214
+ width: "76%"
2215
+ });
2216
+ this.setStatus("Automation runs");
2217
+ }
2073
2218
  handleInput(data) {
2074
2219
  if (matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
2075
2220
  this.options.onExit();
@@ -2087,6 +2232,10 @@ var GranolaTuiWorkspace = class {
2087
2232
  this.openAuthPanel();
2088
2233
  return;
2089
2234
  }
2235
+ if (matchesKey(data, "u")) {
2236
+ this.openAutomationPanel();
2237
+ return;
2238
+ }
2090
2239
  if (matchesKey(data, "tab")) {
2091
2240
  this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
2092
2241
  this.tui.requestRender();
@@ -2271,7 +2420,7 @@ var GranolaTuiWorkspace = class {
2271
2420
  const bodyLines = [];
2272
2421
  for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
2273
2422
  const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
2274
- const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2423
+ const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth u automation r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2275
2424
  return [
2276
2425
  headerTitle,
2277
2426
  headerSummary,
@@ -2341,6 +2490,422 @@ const attachCommand = {
2341
2490
  }
2342
2491
  };
2343
2492
  //#endregion
2493
+ //#region src/automation-actions.ts
2494
+ function cloneAction$1(action) {
2495
+ switch (action.kind) {
2496
+ case "ask-user": return { ...action };
2497
+ case "command": return {
2498
+ ...action,
2499
+ args: action.args ? [...action.args] : void 0,
2500
+ env: action.env ? { ...action.env } : void 0
2501
+ };
2502
+ case "export-notes":
2503
+ case "export-transcript": return { ...action };
2504
+ }
2505
+ }
2506
+ function automationActionName(action) {
2507
+ return action.name || action.id;
2508
+ }
2509
+ function buildAutomationActionRunId(match, actionId) {
2510
+ return `${match.id}:${actionId}`;
2511
+ }
2512
+ function enabledAutomationActions(rule) {
2513
+ return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
2514
+ }
2515
+ function baseRun(match, rule, action, startedAt) {
2516
+ return {
2517
+ actionId: action.id,
2518
+ actionKind: action.kind,
2519
+ actionName: automationActionName(action),
2520
+ eventId: match.eventId,
2521
+ eventKind: match.eventKind,
2522
+ folders: match.folders.map((folder) => ({ ...folder })),
2523
+ id: buildAutomationActionRunId(match, action.id),
2524
+ matchedAt: match.matchedAt,
2525
+ meetingId: match.meetingId,
2526
+ ruleId: rule.id,
2527
+ ruleName: rule.name,
2528
+ startedAt,
2529
+ status: "completed",
2530
+ tags: [...match.tags],
2531
+ title: match.title,
2532
+ transcriptLoaded: match.transcriptLoaded
2533
+ };
2534
+ }
2535
+ function completedRun(run, finishedAt, patch = {}) {
2536
+ return {
2537
+ ...run,
2538
+ ...patch,
2539
+ finishedAt,
2540
+ status: "completed"
2541
+ };
2542
+ }
2543
+ function failedRun(run, finishedAt, error) {
2544
+ return {
2545
+ ...run,
2546
+ error: error instanceof Error ? error.message : String(error),
2547
+ finishedAt,
2548
+ status: "failed"
2549
+ };
2550
+ }
2551
+ function skippedRun(run, finishedAt, reason) {
2552
+ return {
2553
+ ...run,
2554
+ finishedAt,
2555
+ result: reason,
2556
+ status: "skipped"
2557
+ };
2558
+ }
2559
+ async function executeAutomationAction(match, rule, action, handlers) {
2560
+ const run = baseRun(match, rule, action, handlers.nowIso());
2561
+ switch (action.kind) {
2562
+ case "ask-user": return {
2563
+ ...run,
2564
+ meta: action.details ? { details: action.details } : void 0,
2565
+ prompt: action.prompt,
2566
+ result: "Pending user decision",
2567
+ status: "pending"
2568
+ };
2569
+ case "command": try {
2570
+ const result = await handlers.runCommand(match, rule, action);
2571
+ return completedRun(run, handlers.nowIso(), {
2572
+ meta: {
2573
+ command: result.command,
2574
+ cwd: result.cwd
2575
+ },
2576
+ result: result.output
2577
+ });
2578
+ } catch (error) {
2579
+ return failedRun(run, handlers.nowIso(), error);
2580
+ }
2581
+ case "export-notes": try {
2582
+ const result = await handlers.exportNotes(match, action);
2583
+ if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
2584
+ return completedRun(run, handlers.nowIso(), {
2585
+ meta: {
2586
+ format: result.format,
2587
+ outputDir: result.outputDir,
2588
+ scope: result.scope,
2589
+ written: result.written
2590
+ },
2591
+ result: `Exported notes to ${result.outputDir}`
2592
+ });
2593
+ } catch (error) {
2594
+ return failedRun(run, handlers.nowIso(), error);
2595
+ }
2596
+ case "export-transcript": try {
2597
+ const result = await handlers.exportTranscripts(match, action);
2598
+ if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
2599
+ return completedRun(run, handlers.nowIso(), {
2600
+ meta: {
2601
+ format: result.format,
2602
+ outputDir: result.outputDir,
2603
+ scope: result.scope,
2604
+ written: result.written
2605
+ },
2606
+ result: `Exported transcript to ${result.outputDir}`
2607
+ });
2608
+ } catch (error) {
2609
+ return failedRun(run, handlers.nowIso(), error);
2610
+ }
2611
+ }
2612
+ }
2613
+ //#endregion
2614
+ //#region src/persistence/layout.ts
2615
+ function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
2616
+ return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
2617
+ }
2618
+ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2619
+ const targetPlatform = options.platform ?? platform();
2620
+ const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
2621
+ return {
2622
+ automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
2623
+ automationRulesFile: join(dataDirectory, "automation-rules.json"),
2624
+ automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
2625
+ apiKeyFile: join(dataDirectory, "api-key.txt"),
2626
+ dataDirectory,
2627
+ exportJobsFile: join(dataDirectory, "export-jobs.json"),
2628
+ meetingIndexFile: join(dataDirectory, "meeting-index.json"),
2629
+ sessionFile: join(dataDirectory, "session.json"),
2630
+ sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
2631
+ syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
2632
+ syncStateFile: join(dataDirectory, "sync-state.json")
2633
+ };
2634
+ }
2635
+ //#endregion
2636
+ //#region src/automation-matches.ts
2637
+ function cloneMatch(match) {
2638
+ return {
2639
+ ...match,
2640
+ folders: match.folders.map((folder) => ({ ...folder })),
2641
+ tags: [...match.tags]
2642
+ };
2643
+ }
2644
+ var FileAutomationMatchStore = class {
2645
+ constructor(filePath = defaultAutomationMatchesFilePath()) {
2646
+ this.filePath = filePath;
2647
+ }
2648
+ async appendMatches(matches) {
2649
+ if (matches.length === 0) return;
2650
+ await mkdir(dirname(this.filePath), { recursive: true });
2651
+ const payload = matches.map((match) => JSON.stringify(match)).join("\n");
2652
+ await appendFile(this.filePath, `${payload}\n`, {
2653
+ encoding: "utf8",
2654
+ mode: 384
2655
+ });
2656
+ }
2657
+ async readMatches(limit = 50) {
2658
+ try {
2659
+ const matches = (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((match) => Boolean(match)).map(cloneMatch);
2660
+ return (limit > 0 ? matches.slice(-limit) : matches).reverse();
2661
+ } catch {
2662
+ return [];
2663
+ }
2664
+ }
2665
+ };
2666
+ function defaultAutomationMatchesFilePath() {
2667
+ return defaultGranolaToolkitPersistenceLayout().automationMatchesFile;
2668
+ }
2669
+ function createDefaultAutomationMatchStore(filePath) {
2670
+ return new FileAutomationMatchStore(filePath);
2671
+ }
2672
+ //#endregion
2673
+ //#region src/automation-runs.ts
2674
+ function cloneRun(run) {
2675
+ return {
2676
+ ...run,
2677
+ folders: run.folders.map((folder) => ({ ...folder })),
2678
+ meta: run.meta ? structuredClone(run.meta) : void 0,
2679
+ tags: [...run.tags]
2680
+ };
2681
+ }
2682
+ function sortRuns(runs) {
2683
+ return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
2684
+ }
2685
+ function mergeRuns(runs) {
2686
+ const byId = /* @__PURE__ */ new Map();
2687
+ for (const run of runs) byId.set(run.id, cloneRun(run));
2688
+ return sortRuns([...byId.values()]);
2689
+ }
2690
+ var FileAutomationRunStore = class {
2691
+ constructor(filePath = defaultAutomationRunsFilePath()) {
2692
+ this.filePath = filePath;
2693
+ }
2694
+ async appendRuns(runs) {
2695
+ if (runs.length === 0) return;
2696
+ await mkdir(dirname(this.filePath), { recursive: true });
2697
+ const payload = runs.map((run) => JSON.stringify(run)).join("\n");
2698
+ await appendFile(this.filePath, `${payload}\n`, {
2699
+ encoding: "utf8",
2700
+ mode: 384
2701
+ });
2702
+ }
2703
+ async readRun(id) {
2704
+ return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
2705
+ }
2706
+ async readRuns(options = {}) {
2707
+ try {
2708
+ const runs = mergeRuns((await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((run) => Boolean(run))).filter((run) => options.status ? run.status === options.status : true);
2709
+ return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
2710
+ } catch {
2711
+ return [];
2712
+ }
2713
+ }
2714
+ };
2715
+ function defaultAutomationRunsFilePath() {
2716
+ return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
2717
+ }
2718
+ function createDefaultAutomationRunStore(filePath) {
2719
+ return new FileAutomationRunStore(filePath);
2720
+ }
2721
+ //#endregion
2722
+ //#region src/automation-rules.ts
2723
+ function cloneRule(rule) {
2724
+ return {
2725
+ ...rule,
2726
+ actions: rule.actions?.map((action) => cloneAction(action)),
2727
+ when: {
2728
+ ...rule.when,
2729
+ eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
2730
+ folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
2731
+ folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
2732
+ meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
2733
+ tags: rule.when.tags ? [...rule.when.tags] : void 0,
2734
+ titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
2735
+ }
2736
+ };
2737
+ }
2738
+ function cloneAction(action) {
2739
+ switch (action.kind) {
2740
+ case "ask-user": return { ...action };
2741
+ case "command": return {
2742
+ ...action,
2743
+ args: action.args ? [...action.args] : void 0,
2744
+ env: action.env ? { ...action.env } : void 0
2745
+ };
2746
+ case "export-notes":
2747
+ case "export-transcript": return { ...action };
2748
+ }
2749
+ }
2750
+ function stringArray(value) {
2751
+ if (!Array.isArray(value)) return;
2752
+ const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
2753
+ return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
2754
+ }
2755
+ function stringRecord(value) {
2756
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2757
+ const entries = Object.entries(value).filter(([key, item]) => {
2758
+ return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
2759
+ });
2760
+ if (entries.length === 0) return;
2761
+ return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
2762
+ }
2763
+ function parseAction(value, index) {
2764
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2765
+ const record = value;
2766
+ const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
2767
+ const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
2768
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
2769
+ const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
2770
+ switch (kind) {
2771
+ case "ask-user": {
2772
+ const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
2773
+ if (!id || !prompt) return;
2774
+ return {
2775
+ details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
2776
+ enabled,
2777
+ id,
2778
+ kind,
2779
+ name,
2780
+ prompt
2781
+ };
2782
+ }
2783
+ case "command": {
2784
+ const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
2785
+ if (!id || !command) return;
2786
+ return {
2787
+ args: stringArray(record.args),
2788
+ command,
2789
+ cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
2790
+ enabled,
2791
+ env: stringRecord(record.env),
2792
+ id,
2793
+ kind,
2794
+ name,
2795
+ stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
2796
+ timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
2797
+ };
2798
+ }
2799
+ case "export-notes":
2800
+ if (!id) return;
2801
+ return {
2802
+ enabled,
2803
+ format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
2804
+ id,
2805
+ kind,
2806
+ name,
2807
+ outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
2808
+ scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
2809
+ };
2810
+ case "export-transcript":
2811
+ if (!id) return;
2812
+ return {
2813
+ enabled,
2814
+ format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
2815
+ id,
2816
+ kind,
2817
+ name,
2818
+ outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
2819
+ scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
2820
+ };
2821
+ default: return;
2822
+ }
2823
+ }
2824
+ function parseRule(value) {
2825
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2826
+ const record = value;
2827
+ const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
2828
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
2829
+ const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
2830
+ if (!id || !name || !whenValue) return;
2831
+ return {
2832
+ actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
2833
+ enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
2834
+ id,
2835
+ name,
2836
+ when: {
2837
+ eventKinds: stringArray(whenValue.eventKinds),
2838
+ folderIds: stringArray(whenValue.folderIds),
2839
+ folderNames: stringArray(whenValue.folderNames),
2840
+ meetingIds: stringArray(whenValue.meetingIds),
2841
+ tags: stringArray(whenValue.tags),
2842
+ titleIncludes: stringArray(whenValue.titleIncludes),
2843
+ titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
2844
+ transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
2845
+ }
2846
+ };
2847
+ }
2848
+ var FileAutomationRuleStore = class {
2849
+ constructor(filePath = defaultAutomationRulesFilePath()) {
2850
+ this.filePath = filePath;
2851
+ }
2852
+ async readRules() {
2853
+ try {
2854
+ const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
2855
+ return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.rules) ? parsed.rules : []).map((rule) => parseRule(rule)).filter((rule) => Boolean(rule)).map(cloneRule);
2856
+ } catch {
2857
+ return [];
2858
+ }
2859
+ }
2860
+ };
2861
+ function defaultAutomationRulesFilePath() {
2862
+ return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
2863
+ }
2864
+ function includesIgnoreCase(candidate, values) {
2865
+ const lowerCandidate = candidate.toLowerCase();
2866
+ return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
2867
+ }
2868
+ function matchesRule(rule, event) {
2869
+ if (rule.enabled === false) return false;
2870
+ const { when } = rule;
2871
+ if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
2872
+ if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
2873
+ if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
2874
+ if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
2875
+ if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
2876
+ if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
2877
+ if (when.titleMatches) try {
2878
+ if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
2879
+ } catch {
2880
+ return false;
2881
+ }
2882
+ if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
2883
+ return true;
2884
+ }
2885
+ function matchAutomationRules(rules, events, matchedAt) {
2886
+ const matches = [];
2887
+ for (const event of events) for (const rule of rules) {
2888
+ if (!matchesRule(rule, event)) continue;
2889
+ matches.push({
2890
+ eventId: event.id,
2891
+ eventKind: event.kind,
2892
+ folders: event.folders.map((folder) => ({ ...folder })),
2893
+ id: `${event.id}:${rule.id}`,
2894
+ matchedAt,
2895
+ meetingId: event.meetingId,
2896
+ ruleId: rule.id,
2897
+ ruleName: rule.name,
2898
+ tags: [...event.tags],
2899
+ title: event.title,
2900
+ transcriptLoaded: event.transcriptLoaded
2901
+ });
2902
+ }
2903
+ return matches;
2904
+ }
2905
+ function createDefaultAutomationRuleStore(filePath) {
2906
+ return new FileAutomationRuleStore(filePath);
2907
+ }
2908
+ //#endregion
2344
2909
  //#region src/cache.ts
2345
2910
  function parseCacheDocument(id, value) {
2346
2911
  const record = asRecord(value);
@@ -2395,25 +2960,6 @@ function parseCacheContents(contents) {
2395
2960
  };
2396
2961
  }
2397
2962
  //#endregion
2398
- //#region src/persistence/layout.ts
2399
- function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
2400
- return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
2401
- }
2402
- function defaultGranolaToolkitPersistenceLayout(options = {}) {
2403
- const targetPlatform = options.platform ?? platform();
2404
- const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
2405
- return {
2406
- apiKeyFile: join(dataDirectory, "api-key.txt"),
2407
- dataDirectory,
2408
- exportJobsFile: join(dataDirectory, "export-jobs.json"),
2409
- meetingIndexFile: join(dataDirectory, "meeting-index.json"),
2410
- sessionFile: join(dataDirectory, "session.json"),
2411
- sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
2412
- syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
2413
- syncStateFile: join(dataDirectory, "sync-state.json")
2414
- };
2415
- }
2416
- //#endregion
2417
2963
  //#region src/client/auth.ts
2418
2964
  const execFileAsync$1 = promisify(execFile);
2419
2965
  const DEFAULT_CLIENT_ID = "client_GranolaMac";
@@ -2911,7 +3457,7 @@ function parseDocument(value) {
2911
3457
  lastViewedPanel: parseLastViewedPanel(record.last_viewed_panel),
2912
3458
  notes: parseProseMirrorDoc(record.notes),
2913
3459
  notesPlain: stringValue(record.notes_plain),
2914
- tags: stringArray(record.tags),
3460
+ tags: stringArray$1(record.tags),
2915
3461
  title: stringValue(record.title),
2916
3462
  updatedAt: stringValue(record.updated_at)
2917
3463
  };
@@ -3295,6 +3841,7 @@ async function loadOptionalGranolaCache(cacheFile) {
3295
3841
  //#endregion
3296
3842
  //#region src/export-scope.ts
3297
3843
  const FOLDER_EXPORT_DIRECTORY = "_folders";
3844
+ const MEETING_EXPORT_DIRECTORY = "_meetings";
3298
3845
  function allExportScope() {
3299
3846
  return { mode: "all" };
3300
3847
  }
@@ -3306,11 +3853,22 @@ function folderExportScope(folder) {
3306
3853
  };
3307
3854
  }
3308
3855
  function cloneExportScope(scope) {
3309
- return scope.mode === "folder" ? { ...scope } : { mode: "all" };
3856
+ if (scope.mode === "folder" || scope.mode === "meeting") return { ...scope };
3857
+ return { mode: "all" };
3310
3858
  }
3311
3859
  function normaliseExportScope(value) {
3312
3860
  const record = asRecord(value);
3313
3861
  if (!record) return allExportScope();
3862
+ if (record.mode === "meeting") {
3863
+ const meetingId = stringValue(record.meetingId);
3864
+ const meetingTitle = stringValue(record.meetingTitle) || meetingId;
3865
+ if (!meetingId) return allExportScope();
3866
+ return {
3867
+ meetingId,
3868
+ meetingTitle,
3869
+ mode: "meeting"
3870
+ };
3871
+ }
3314
3872
  if (record.mode !== "folder") return allExportScope();
3315
3873
  const folderId = stringValue(record.folderId);
3316
3874
  const folderName = stringValue(record.folderName) || folderId;
@@ -3321,12 +3879,22 @@ function normaliseExportScope(value) {
3321
3879
  mode: "folder"
3322
3880
  };
3323
3881
  }
3882
+ function meetingExportScope(meeting) {
3883
+ return {
3884
+ meetingId: meeting.meetingId,
3885
+ meetingTitle: meeting.meetingTitle || meeting.meetingId,
3886
+ mode: "meeting"
3887
+ };
3888
+ }
3324
3889
  function renderExportScopeLabel(scope) {
3325
- 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";
3326
3893
  }
3327
3894
  function resolveExportOutputDir(outputDir, scope, options = {}) {
3328
- if (scope.mode !== "folder" || options.scopedDirectory === false) return outputDir;
3329
- return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
3895
+ if (options.scopedDirectory === false || scope.mode === "all") return outputDir;
3896
+ if (scope.mode === "folder") return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
3897
+ return join(outputDir, MEETING_EXPORT_DIRECTORY, sanitiseFilename(scope.meetingId, "meeting"));
3330
3898
  }
3331
3899
  //#endregion
3332
3900
  //#region src/export-jobs.ts
@@ -3684,6 +4252,16 @@ function normaliseMeeting(meeting) {
3684
4252
  function meetingChanged(previous, next) {
3685
4253
  return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
3686
4254
  }
4255
+ function cloneFolderSummary$1(folder) {
4256
+ return { ...folder };
4257
+ }
4258
+ function cloneMeetingSummary$1(meeting) {
4259
+ return {
4260
+ ...meeting,
4261
+ folders: meeting.folders.map((folder) => cloneFolderSummary$1(folder)),
4262
+ tags: [...meeting.tags]
4263
+ };
4264
+ }
3687
4265
  function diffMeetingSummaries(previous, next, folderCount) {
3688
4266
  const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
3689
4267
  const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
@@ -3756,15 +4334,20 @@ function diffMeetingSummaries(previous, next, folderCount) {
3756
4334
  }
3757
4335
  };
3758
4336
  }
3759
- function buildSyncEvents(runId, occurredAt, changes) {
4337
+ function buildSyncEvents(runId, occurredAt, changes, previousMeetings, nextMeetings) {
4338
+ const previousById = new Map(previousMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
4339
+ const nextById = new Map(nextMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
3760
4340
  return changes.map((change, index) => ({
4341
+ folders: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.folders.map((folder) => cloneFolderSummary$1(folder)) ?? [],
3761
4342
  id: `${runId}:${index + 1}`,
3762
4343
  kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
3763
4344
  meetingId: change.meetingId,
3764
4345
  occurredAt,
3765
4346
  previousUpdatedAt: change.previousUpdatedAt,
3766
4347
  runId,
4348
+ tags: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.tags.slice() ?? [],
3767
4349
  title: change.title,
4350
+ transcriptLoaded: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.transcriptLoaded ?? false,
3768
4351
  updatedAt: change.updatedAt
3769
4352
  }));
3770
4353
  }
@@ -3811,9 +4394,11 @@ function cloneMeetingSummary(meeting) {
3811
4394
  function cloneState(state) {
3812
4395
  return {
3813
4396
  auth: { ...state.auth },
4397
+ automation: { ...state.automation },
3814
4398
  cache: { ...state.cache },
3815
4399
  config: {
3816
4400
  ...state.config,
4401
+ automation: state.config.automation ? { ...state.config.automation } : void 0,
3817
4402
  notes: { ...state.config.notes },
3818
4403
  transcripts: { ...state.config.transcripts }
3819
4404
  },
@@ -3832,6 +4417,16 @@ function cloneState(state) {
3832
4417
  function defaultState(config, auth, surface) {
3833
4418
  return {
3834
4419
  auth: { ...auth },
4420
+ automation: {
4421
+ loaded: false,
4422
+ matchCount: 0,
4423
+ matchesFile: defaultAutomationMatchesFilePath(),
4424
+ pendingRunCount: 0,
4425
+ ruleCount: 0,
4426
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4427
+ runCount: 0,
4428
+ runsFile: defaultAutomationRunsFilePath()
4429
+ },
3835
4430
  cache: {
3836
4431
  configured: Boolean(config.transcripts.cacheFile),
3837
4432
  documentCount: 0,
@@ -3873,6 +4468,9 @@ function defaultState(config, auth, surface) {
3873
4468
  };
3874
4469
  }
3875
4470
  var GranolaApp = class {
4471
+ #automationActionRuns;
4472
+ #automationMatches;
4473
+ #automationRules;
3876
4474
  #cacheData;
3877
4475
  #cacheResolved = false;
3878
4476
  #folders;
@@ -3886,7 +4484,26 @@ var GranolaApp = class {
3886
4484
  this.config = config;
3887
4485
  this.deps = deps;
3888
4486
  this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
4487
+ this.#automationMatches = (deps.automationMatches ?? []).map((match) => ({
4488
+ ...match,
4489
+ folders: match.folders.map((folder) => ({ ...folder })),
4490
+ tags: [...match.tags]
4491
+ }));
4492
+ this.#automationActionRuns = (deps.automationRuns ?? []).map((run) => this.cloneAutomationRun(run));
4493
+ this.#automationRules = (deps.automationRules ?? []).map((rule) => this.cloneAutomationRule(rule));
3889
4494
  this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
4495
+ this.#state.automation = {
4496
+ lastRunAt: this.#automationActionRuns[0]?.finishedAt ?? this.#automationActionRuns[0]?.startedAt,
4497
+ lastMatchedAt: this.#automationMatches[0]?.matchedAt,
4498
+ loaded: true,
4499
+ matchCount: this.#automationMatches.length,
4500
+ matchesFile: defaultAutomationMatchesFilePath(),
4501
+ pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
4502
+ ruleCount: this.#automationRules.length,
4503
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4504
+ runCount: this.#automationActionRuns.length,
4505
+ runsFile: defaultAutomationRunsFilePath()
4506
+ };
3890
4507
  this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
3891
4508
  this.#state.index = {
3892
4509
  available: this.#meetingIndex.length > 0,
@@ -3959,6 +4576,93 @@ var GranolaApp = class {
3959
4576
  if (!this.deps.syncStateStore) return;
3960
4577
  await this.deps.syncStateStore.writeState(this.#state.sync);
3961
4578
  }
4579
+ cloneAutomationRule(rule) {
4580
+ return {
4581
+ ...rule,
4582
+ actions: rule.actions?.map((action) => {
4583
+ switch (action.kind) {
4584
+ case "ask-user": return { ...action };
4585
+ case "command": return {
4586
+ ...action,
4587
+ args: action.args ? [...action.args] : void 0,
4588
+ env: action.env ? { ...action.env } : void 0
4589
+ };
4590
+ case "export-notes":
4591
+ case "export-transcript": return { ...action };
4592
+ }
4593
+ }),
4594
+ when: {
4595
+ ...rule.when,
4596
+ eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
4597
+ folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
4598
+ folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
4599
+ meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
4600
+ tags: rule.when.tags ? [...rule.when.tags] : void 0,
4601
+ titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
4602
+ }
4603
+ };
4604
+ }
4605
+ cloneAutomationMatch(match) {
4606
+ return {
4607
+ ...match,
4608
+ folders: match.folders.map((folder) => ({ ...folder })),
4609
+ tags: [...match.tags]
4610
+ };
4611
+ }
4612
+ cloneAutomationRun(run) {
4613
+ return {
4614
+ ...run,
4615
+ folders: run.folders.map((folder) => ({ ...folder })),
4616
+ meta: run.meta ? structuredClone(run.meta) : void 0,
4617
+ tags: [...run.tags]
4618
+ };
4619
+ }
4620
+ refreshAutomationState() {
4621
+ const latestMatch = this.#automationMatches.reduce((current, candidate) => !current || candidate.matchedAt.localeCompare(current.matchedAt) > 0 ? candidate : current, void 0);
4622
+ const latestRun = this.#automationActionRuns.reduce((current, candidate) => {
4623
+ const candidateTime = candidate.finishedAt ?? candidate.startedAt;
4624
+ const currentTime = current ? current.finishedAt ?? current.startedAt : void 0;
4625
+ return !currentTime || candidateTime.localeCompare(currentTime) > 0 ? candidate : current;
4626
+ }, void 0);
4627
+ this.#state.automation = {
4628
+ ...this.#state.automation,
4629
+ lastMatchedAt: latestMatch?.matchedAt ?? this.#state.automation.lastMatchedAt,
4630
+ lastRunAt: latestRun?.finishedAt ?? latestRun?.startedAt ?? this.#state.automation.lastRunAt,
4631
+ loaded: true,
4632
+ matchCount: this.#automationMatches.length,
4633
+ matchesFile: defaultAutomationMatchesFilePath(),
4634
+ pendingRunCount: this.#automationActionRuns.filter((run) => run.status === "pending").length,
4635
+ ruleCount: this.#automationRules.length,
4636
+ rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath(),
4637
+ runCount: this.#automationActionRuns.length,
4638
+ runsFile: defaultAutomationRunsFilePath()
4639
+ };
4640
+ }
4641
+ async loadAutomationRules(options = {}) {
4642
+ if (this.#automationRules.length > 0 && !options.forceRefresh) return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4643
+ if (!this.deps.automationRuleStore) return [];
4644
+ this.#automationRules = (await this.deps.automationRuleStore.readRules()).map((rule) => this.cloneAutomationRule(rule));
4645
+ this.refreshAutomationState();
4646
+ this.emitStateUpdate();
4647
+ return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4648
+ }
4649
+ async appendAutomationMatches(matches) {
4650
+ if (matches.length === 0) return;
4651
+ if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
4652
+ this.#automationMatches.push(...matches.map((match) => this.cloneAutomationMatch(match)));
4653
+ this.refreshAutomationState();
4654
+ }
4655
+ async appendAutomationRuns(runs) {
4656
+ if (runs.length === 0) return;
4657
+ if (this.deps.automationRunStore) await this.deps.automationRunStore.appendRuns(runs);
4658
+ for (const run of runs) {
4659
+ const index = this.#automationActionRuns.findIndex((candidate) => candidate.id === run.id);
4660
+ if (index >= 0) this.#automationActionRuns[index] = this.cloneAutomationRun(run);
4661
+ else this.#automationActionRuns.push(this.cloneAutomationRun(run));
4662
+ }
4663
+ this.#automationActionRuns.sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
4664
+ this.refreshAutomationState();
4665
+ }
3962
4666
  createSyncRunId() {
3963
4667
  return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
3964
4668
  }
@@ -4190,6 +4894,46 @@ var GranolaApp = class {
4190
4894
  if (!this.deps.syncEventStore) return { events: [] };
4191
4895
  return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
4192
4896
  }
4897
+ async listAutomationRules() {
4898
+ const rules = await this.loadAutomationRules({ forceRefresh: true });
4899
+ this.setUiState({ view: "idle" });
4900
+ return { rules: rules.map((rule) => this.cloneAutomationRule(rule)) };
4901
+ }
4902
+ async listAutomationMatches(options = {}) {
4903
+ const limit = options.limit ?? 20;
4904
+ const matches = this.deps.automationMatchStore ? await this.deps.automationMatchStore.readMatches(limit) : this.#automationMatches.slice(-limit).reverse();
4905
+ this.setUiState({ view: "idle" });
4906
+ return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
4907
+ }
4908
+ async listAutomationRuns(options = {}) {
4909
+ const limit = options.limit ?? 20;
4910
+ const runs = this.deps.automationRunStore ? await this.deps.automationRunStore.readRuns({
4911
+ limit,
4912
+ status: options.status
4913
+ }) : this.#automationActionRuns.filter((run) => options.status ? run.status === options.status : true).slice(0, limit);
4914
+ this.setUiState({ view: "idle" });
4915
+ return { runs: runs.map((run) => this.cloneAutomationRun(run)) };
4916
+ }
4917
+ async resolveAutomationRun(id, decision, options = {}) {
4918
+ const current = (this.deps.automationRunStore ? await this.deps.automationRunStore.readRun(id) : void 0) ?? this.#automationActionRuns.find((run) => run.id === id);
4919
+ if (!current) throw new Error(`automation run not found: ${id}`);
4920
+ if (current.status !== "pending") throw new Error(`automation run is not pending: ${id}`);
4921
+ const finishedAt = this.nowIso();
4922
+ const resolved = {
4923
+ ...this.cloneAutomationRun(current),
4924
+ finishedAt,
4925
+ meta: {
4926
+ ...current.meta ? structuredClone(current.meta) : {},
4927
+ decision,
4928
+ note: options.note?.trim() || void 0
4929
+ },
4930
+ result: decision === "approve" ? options.note?.trim() || "Approved by user" : options.note?.trim() || "Rejected by user",
4931
+ status: decision === "approve" ? "completed" : "skipped"
4932
+ };
4933
+ await this.appendAutomationRuns([resolved]);
4934
+ this.emitStateUpdate();
4935
+ return this.cloneAutomationRun(resolved);
4936
+ }
4193
4937
  async loginAuth(options = {}) {
4194
4938
  const controller = this.requireAuthController();
4195
4939
  try {
@@ -4239,6 +4983,165 @@ var GranolaApp = class {
4239
4983
  throw error;
4240
4984
  }
4241
4985
  }
4986
+ async maybeReadMeetingBundleById(id, options = {}) {
4987
+ try {
4988
+ return await this.readMeetingBundleById(id, options);
4989
+ } catch {
4990
+ return;
4991
+ }
4992
+ }
4993
+ async runAutomationNotesAction(match, action) {
4994
+ const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
4995
+ if (!bundle) return;
4996
+ const scope = meetingExportScope({
4997
+ meetingId: bundle.document.id,
4998
+ meetingTitle: bundle.meeting.meeting.title || bundle.document.id
4999
+ });
5000
+ const result = await this.runNotesExport({
5001
+ documents: [bundle.document],
5002
+ format: action.format ?? "markdown",
5003
+ outputDir: resolveExportOutputDir(action.outputDir ?? this.config.notes.output, scope, { scopedDirectory: action.scopedOutput }),
5004
+ scope,
5005
+ trackLastRun: false,
5006
+ updateUi: false
5007
+ });
5008
+ return {
5009
+ format: result.format,
5010
+ outputDir: result.outputDir,
5011
+ scope: result.scope,
5012
+ written: result.written
5013
+ };
5014
+ }
5015
+ async runAutomationTranscriptAction(match, action) {
5016
+ const bundle = await this.maybeReadMeetingBundleById(match.meetingId);
5017
+ if (!bundle?.cacheData) return;
5018
+ const cacheDocument = bundle.cacheData.documents[bundle.document.id];
5019
+ const transcriptSegments = bundle.cacheData.transcripts[bundle.document.id];
5020
+ if (!cacheDocument || !transcriptSegments || transcriptSegments.length === 0) return;
5021
+ const scope = meetingExportScope({
5022
+ meetingId: bundle.document.id,
5023
+ meetingTitle: bundle.meeting.meeting.title || bundle.document.id
5024
+ });
5025
+ const result = await this.runTranscriptsExport({
5026
+ cacheData: {
5027
+ documents: { [bundle.document.id]: cacheDocument },
5028
+ transcripts: { [bundle.document.id]: transcriptSegments }
5029
+ },
5030
+ format: action.format ?? "text",
5031
+ outputDir: resolveExportOutputDir(action.outputDir ?? this.config.transcripts.output, scope, { scopedDirectory: action.scopedOutput }),
5032
+ scope,
5033
+ trackLastRun: false,
5034
+ updateUi: false
5035
+ });
5036
+ return {
5037
+ format: result.format,
5038
+ outputDir: result.outputDir,
5039
+ scope: result.scope,
5040
+ written: result.written
5041
+ };
5042
+ }
5043
+ async runAutomationCommand(match, rule, action) {
5044
+ const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
5045
+ const cwd = action.cwd ? resolve(action.cwd) : process.cwd();
5046
+ const payload = JSON.stringify({
5047
+ action: {
5048
+ id: action.id,
5049
+ kind: "command",
5050
+ name: automationActionName(action)
5051
+ },
5052
+ authMode: this.#state.auth.mode,
5053
+ generatedAt: this.nowIso(),
5054
+ match: this.cloneAutomationMatch(match),
5055
+ meeting: bundle ? {
5056
+ document: bundle.document,
5057
+ meeting: bundle.meeting
5058
+ } : void 0,
5059
+ rule: {
5060
+ id: rule.id,
5061
+ name: rule.name
5062
+ }
5063
+ }, null, 2);
5064
+ return await new Promise((resolve, reject) => {
5065
+ const child = spawn(action.command, action.args ?? [], {
5066
+ cwd,
5067
+ env: {
5068
+ ...process.env,
5069
+ ...action.env,
5070
+ GRANOLA_ACTION_KIND: "command",
5071
+ GRANOLA_EVENT_ID: match.eventId,
5072
+ GRANOLA_EVENT_KIND: match.eventKind,
5073
+ GRANOLA_MATCH_ID: match.id,
5074
+ GRANOLA_MEETING_ID: match.meetingId,
5075
+ GRANOLA_RULE_ID: rule.id
5076
+ },
5077
+ stdio: [
5078
+ "pipe",
5079
+ "pipe",
5080
+ "pipe"
5081
+ ]
5082
+ });
5083
+ const stdoutChunks = [];
5084
+ const stderrChunks = [];
5085
+ let timedOut = false;
5086
+ const timeoutMs = action.timeoutMs ?? this.config.notes.timeoutMs;
5087
+ const timeout = setTimeout(() => {
5088
+ timedOut = true;
5089
+ child.kill("SIGTERM");
5090
+ }, timeoutMs);
5091
+ child.stdout.on("data", (chunk) => {
5092
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5093
+ });
5094
+ child.stderr.on("data", (chunk) => {
5095
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5096
+ });
5097
+ child.on("error", (error) => {
5098
+ clearTimeout(timeout);
5099
+ reject(error);
5100
+ });
5101
+ child.on("close", (code) => {
5102
+ clearTimeout(timeout);
5103
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
5104
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
5105
+ if (timedOut) {
5106
+ reject(/* @__PURE__ */ new Error(`automation command timed out after ${timeoutMs}ms`));
5107
+ return;
5108
+ }
5109
+ if (code !== 0) {
5110
+ reject(new Error(stderr || stdout || `automation command exited with status ${String(code)}`));
5111
+ return;
5112
+ }
5113
+ resolve({
5114
+ command: [action.command, ...action.args ?? []].join(" "),
5115
+ cwd,
5116
+ output: stdout || stderr || void 0
5117
+ });
5118
+ });
5119
+ if (action.stdin !== "none") child.stdin.write(payload);
5120
+ child.stdin.end();
5121
+ });
5122
+ }
5123
+ async runAutomationActions(rules, matches) {
5124
+ const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
5125
+ const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
5126
+ const runs = [];
5127
+ for (const match of matches) {
5128
+ const rule = rulesById.get(match.ruleId);
5129
+ if (!rule) continue;
5130
+ for (const action of enabledAutomationActions(rule)) {
5131
+ const runId = buildAutomationActionRunId(match, action.id);
5132
+ if (existingRunIds.has(runId)) continue;
5133
+ existingRunIds.add(runId);
5134
+ runs.push(await executeAutomationAction(match, rule, action, {
5135
+ exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
5136
+ exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
5137
+ nowIso: () => this.nowIso(),
5138
+ runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
5139
+ }));
5140
+ }
5141
+ }
5142
+ await this.appendAutomationRuns(runs);
5143
+ return runs.map((run) => this.cloneAutomationRun(run));
5144
+ }
4242
5145
  async runSync(options) {
4243
5146
  const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
4244
5147
  this.#state.sync = {
@@ -4255,8 +5158,12 @@ var GranolaApp = class {
4255
5158
  const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
4256
5159
  const completedAt = this.nowIso();
4257
5160
  const runId = this.createSyncRunId();
4258
- const events = buildSyncEvents(runId, completedAt, changes);
5161
+ const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
4259
5162
  if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
5163
+ const rules = await this.loadAutomationRules();
5164
+ const automationMatches = matchAutomationRules(rules, events, completedAt);
5165
+ await this.appendAutomationMatches(automationMatches);
5166
+ await this.runAutomationActions(rules, automationMatches);
4260
5167
  this.#state.sync = {
4261
5168
  ...this.#state.sync,
4262
5169
  eventCount: this.#state.sync.eventCount + events.length,
@@ -4428,40 +5335,46 @@ var GranolaApp = class {
4428
5335
  source: "live"
4429
5336
  };
4430
5337
  }
4431
- async getMeeting(id, options = {}) {
5338
+ async readMeetingBundleById(id, options = {}) {
4432
5339
  const documents = await this.listDocuments();
4433
5340
  const cacheData = await this.loadCache({ required: options.requireCache });
4434
5341
  const folders = await this.loadFolders();
4435
5342
  const document = resolveMeeting(documents, id);
4436
- const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
4437
- this.setUiState({
4438
- selectedFolderId: meeting.meeting.folders[0]?.id,
4439
- selectedMeetingId: document.id,
4440
- view: "meeting-detail"
4441
- });
4442
5343
  return {
4443
5344
  cacheData,
4444
5345
  document,
4445
- meeting
5346
+ meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
4446
5347
  };
4447
5348
  }
4448
- async findMeeting(query, options = {}) {
5349
+ async readMeetingBundleByQuery(query, options = {}) {
4449
5350
  const documents = await this.listDocuments();
4450
5351
  const cacheData = await this.loadCache({ required: options.requireCache });
4451
5352
  const folders = await this.loadFolders();
4452
5353
  const document = resolveMeetingQuery(documents, query);
4453
- const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
4454
- this.setUiState({
4455
- selectedFolderId: meeting.meeting.folders[0]?.id,
4456
- selectedMeetingId: document.id,
4457
- view: "meeting-detail"
4458
- });
4459
5354
  return {
4460
5355
  cacheData,
4461
5356
  document,
4462
- meeting
5357
+ meeting: buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id))
4463
5358
  };
4464
5359
  }
5360
+ async getMeeting(id, options = {}) {
5361
+ const bundle = await this.readMeetingBundleById(id, options);
5362
+ this.setUiState({
5363
+ selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
5364
+ selectedMeetingId: bundle.document.id,
5365
+ view: "meeting-detail"
5366
+ });
5367
+ return bundle;
5368
+ }
5369
+ async findMeeting(query, options = {}) {
5370
+ const bundle = await this.readMeetingBundleByQuery(query, options);
5371
+ this.setUiState({
5372
+ selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
5373
+ selectedMeetingId: bundle.document.id,
5374
+ view: "meeting-detail"
5375
+ });
5376
+ return bundle;
5377
+ }
4465
5378
  async listExportJobs(options = {}) {
4466
5379
  const limit = options.limit ?? 20;
4467
5380
  const jobs = this.#state.exports.jobs.slice(0, limit).map((job) => cloneExportJob(job));
@@ -4497,7 +5410,7 @@ var GranolaApp = class {
4497
5410
  await this.failExportJob(job, error);
4498
5411
  throw error;
4499
5412
  }
4500
- this.#state.exports.notes = {
5413
+ if (options.trackLastRun !== false) this.#state.exports.notes = {
4501
5414
  format: options.format,
4502
5415
  itemCount: options.documents.length,
4503
5416
  jobId: job.id,
@@ -4507,7 +5420,7 @@ var GranolaApp = class {
4507
5420
  written
4508
5421
  };
4509
5422
  this.emitStateUpdate();
4510
- this.setUiState({
5423
+ if (options.updateUi !== false) this.setUiState({
4511
5424
  selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
4512
5425
  view: "notes-export"
4513
5426
  });
@@ -4555,7 +5468,7 @@ var GranolaApp = class {
4555
5468
  await this.failExportJob(job, error);
4556
5469
  throw error;
4557
5470
  }
4558
- this.#state.exports.transcripts = {
5471
+ if (options.trackLastRun !== false) this.#state.exports.transcripts = {
4559
5472
  format: options.format,
4560
5473
  itemCount: count,
4561
5474
  jobId: job.id,
@@ -4565,7 +5478,7 @@ var GranolaApp = class {
4565
5478
  written
4566
5479
  };
4567
5480
  this.emitStateUpdate();
4568
- this.setUiState({
5481
+ if (options.updateUi !== false) this.setUiState({
4569
5482
  selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
4570
5483
  view: "transcripts-export"
4571
5484
  });
@@ -4607,6 +5520,12 @@ var GranolaApp = class {
4607
5520
  };
4608
5521
  async function createGranolaApp(config, options = {}) {
4609
5522
  const auth = await inspectDefaultGranolaAuth(config);
5523
+ const automationMatchStore = createDefaultAutomationMatchStore();
5524
+ const automationMatches = await automationMatchStore.readMatches(0);
5525
+ const automationRunStore = createDefaultAutomationRunStore();
5526
+ const automationRuns = await automationRunStore.readRuns({ limit: 0 });
5527
+ const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
5528
+ const automationRules = await automationRuleStore.readRules();
4610
5529
  const authController = createDefaultGranolaAuthController(config);
4611
5530
  const exportJobStore = createDefaultExportJobStore();
4612
5531
  const exportJobs = await exportJobStore.readJobs();
@@ -4618,6 +5537,12 @@ async function createGranolaApp(config, options = {}) {
4618
5537
  return new GranolaApp(config, {
4619
5538
  auth,
4620
5539
  authController,
5540
+ automationMatches,
5541
+ automationMatchStore,
5542
+ automationRunStore,
5543
+ automationRuns,
5544
+ automationRules,
5545
+ automationRuleStore,
4621
5546
  cacheLoader: loadOptionalGranolaCache,
4622
5547
  createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
4623
5548
  exportJobs,
@@ -4692,6 +5617,7 @@ async function loadConfig(options) {
4692
5617
  const defaultCache = firstExistingPath(granolaCacheCandidates());
4693
5618
  const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
4694
5619
  return {
5620
+ automation: { rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile },
4695
5621
  apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
4696
5622
  configFileUsed: config.path,
4697
5623
  debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
@@ -4771,6 +5697,145 @@ async function waitForShutdown(close) {
4771
5697
  });
4772
5698
  }
4773
5699
  //#endregion
5700
+ //#region src/commands/automation.ts
5701
+ function automationHelp() {
5702
+ return `Granola automation
5703
+
5704
+ Usage:
5705
+ granola automation <rules|matches|runs|approve|reject> [options]
5706
+
5707
+ Subcommands:
5708
+ rules List configured automation rules
5709
+ matches Show recent rule matches from sync events
5710
+ runs Show recent automation action runs
5711
+ approve <id> Approve a pending ask-user action run
5712
+ reject <id> Reject a pending ask-user action run
5713
+
5714
+ Options:
5715
+ --format <value> text, json, yaml (default: text)
5716
+ --limit <n> Number of matches to show (default: 20)
5717
+ --status <value> completed, failed, pending, skipped
5718
+ --note <text> Note to store with approve/reject decisions
5719
+ --rules <path> Path to automation rules JSON
5720
+ --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
5721
+ --supabase <path> Path to supabase.json
5722
+ --debug Enable debug logging
5723
+ --config <path> Path to .granola.toml
5724
+ -h, --help Show help
5725
+ `;
5726
+ }
5727
+ function resolveFormat(value) {
5728
+ switch (value) {
5729
+ case void 0: return "text";
5730
+ case "json":
5731
+ case "text":
5732
+ case "yaml": return value;
5733
+ default: throw new Error("invalid automation format: expected text, json, or yaml");
5734
+ }
5735
+ }
5736
+ function parseLimit$3(value) {
5737
+ if (value === void 0) return 20;
5738
+ if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid automation limit: expected a positive integer");
5739
+ return Number(value);
5740
+ }
5741
+ function renderRules(rules, format) {
5742
+ if (format === "json") return toJson({ rules });
5743
+ if (format === "yaml") return toYaml({ rules });
5744
+ if (rules.length === 0) return "No automation rules configured\n";
5745
+ return `${["ID ENABLED EVENTS ACTIONS FILTERS", ...rules.map((rule) => {
5746
+ const filters = [
5747
+ rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
5748
+ rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
5749
+ rule.when.tags?.length ? `tags=${rule.when.tags.join(",")}` : "",
5750
+ rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
5751
+ rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
5752
+ ].filter(Boolean).join(" ");
5753
+ return `${rule.id.padEnd(23).slice(0, 23)} ${(rule.enabled === false ? "no" : "yes").padEnd(8)} ${(rule.when.eventKinds?.join(",") || "any").padEnd(22).slice(0, 22)} ${String(rule.actions?.length ?? 0).padEnd(8)} ${filters || "-"}`;
5754
+ })].join("\n")}\n`;
5755
+ }
5756
+ function renderMatches(matches, format) {
5757
+ if (format === "json") return toJson({ matches });
5758
+ if (format === "yaml") return toYaml({ matches });
5759
+ if (matches.length === 0) return "No automation matches yet\n";
5760
+ return `${["MATCHED AT RULE EVENT TITLE", ...matches.map((match) => {
5761
+ return `${match.matchedAt.slice(0, 19).padEnd(21)} ${match.ruleName.padEnd(23).slice(0, 23)} ${match.eventKind.padEnd(18).slice(0, 18)} ${match.title} (${match.meetingId})`;
5762
+ })].join("\n")}\n`;
5763
+ }
5764
+ function parseRunStatus(value) {
5765
+ switch (value) {
5766
+ case void 0: return;
5767
+ case "completed":
5768
+ case "failed":
5769
+ case "pending":
5770
+ case "skipped": return value;
5771
+ default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
5772
+ }
5773
+ }
5774
+ function renderRuns(runs, format) {
5775
+ if (format === "json") return toJson({ runs });
5776
+ if (format === "yaml") return toYaml({ runs });
5777
+ if (runs.length === 0) return "No automation runs yet\n";
5778
+ return `${["STARTED AT STATUS ACTION RULE TITLE", ...runs.map((run) => {
5779
+ return `${run.startedAt.slice(0, 19).padEnd(21)} ${run.status.padEnd(11).slice(0, 11)} ${run.actionName.padEnd(23).slice(0, 23)} ${run.ruleName.padEnd(23).slice(0, 23)} ${[run.title, run.result || run.error].filter(Boolean).join(" - ")}`;
5780
+ })].join("\n")}\n`;
5781
+ }
5782
+ const automationCommand = {
5783
+ description: "Inspect automation rules and rule matches",
5784
+ flags: {
5785
+ format: { type: "string" },
5786
+ help: { type: "boolean" },
5787
+ limit: { type: "string" },
5788
+ note: { type: "string" },
5789
+ status: { type: "string" },
5790
+ timeout: { type: "string" }
5791
+ },
5792
+ help: automationHelp,
5793
+ name: "automation",
5794
+ async run({ commandArgs, commandFlags, globalFlags }) {
5795
+ const [action] = commandArgs;
5796
+ const format = resolveFormat(commandFlags.format);
5797
+ const config = await loadConfig({
5798
+ globalFlags,
5799
+ subcommandFlags: commandFlags
5800
+ });
5801
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
5802
+ debug(config.debug, "automationRules", config.automation?.rulesFile ?? "(default)");
5803
+ const app = await createGranolaApp(config);
5804
+ switch (action) {
5805
+ case "rules": {
5806
+ const result = await app.listAutomationRules();
5807
+ console.log(renderRules(result.rules, format).trimEnd());
5808
+ return 0;
5809
+ }
5810
+ case "matches": {
5811
+ const result = await app.listAutomationMatches({ limit: parseLimit$3(commandFlags.limit) });
5812
+ console.log(renderMatches(result.matches, format).trimEnd());
5813
+ return 0;
5814
+ }
5815
+ case "runs": {
5816
+ const result = await app.listAutomationRuns({
5817
+ limit: parseLimit$3(commandFlags.limit),
5818
+ status: parseRunStatus(commandFlags.status)
5819
+ });
5820
+ console.log(renderRuns(result.runs, format).trimEnd());
5821
+ return 0;
5822
+ }
5823
+ case "approve":
5824
+ case "reject": {
5825
+ const id = commandArgs[1]?.trim();
5826
+ if (!id) throw new Error(`missing automation run id for ${action}`);
5827
+ const run = await app.resolveAutomationRun(id, action, { note: typeof commandFlags.note === "string" ? commandFlags.note : void 0 });
5828
+ console.log(`${action === "approve" ? "Approved" : "Rejected"} ${run.actionName} for ${run.title} (${run.id})`);
5829
+ return 0;
5830
+ }
5831
+ case void 0:
5832
+ console.log(automationHelp());
5833
+ return 1;
5834
+ default: throw new Error("invalid automation command: expected rules, matches, runs, approve, or reject");
5835
+ }
5836
+ }
5837
+ };
5838
+ //#endregion
4774
5839
  //#region src/commands/auth.ts
4775
5840
  function authHelp() {
4776
5841
  return `Granola auth
@@ -6719,6 +7784,9 @@ var granolaTransportPaths = {
6719
7784
  authRefresh: "/auth/refresh",
6720
7785
  authStatus: "/auth/status",
6721
7786
  authUnlock: "/auth/unlock",
7787
+ automationMatches: "/automation/matches",
7788
+ automationRules: "/automation/rules",
7789
+ automationRuns: "/automation/runs",
6722
7790
  events: "/events",
6723
7791
  exportJobs: "/exports/jobs",
6724
7792
  exportNotes: "/exports/notes",
@@ -6778,6 +7846,15 @@ function granolaFoldersPath(options = {}) {
6778
7846
  function granolaExportJobsPath(options = {}) {
6779
7847
  return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
6780
7848
  }
7849
+ function granolaAutomationRunsPath(options = {}) {
7850
+ return appendSearchParams(granolaTransportPaths.automationRuns, {
7851
+ limit: options.limit,
7852
+ status: options.status
7853
+ });
7854
+ }
7855
+ function granolaAutomationRunDecisionPath(id, decision) {
7856
+ return \`\${granolaTransportPaths.automationRuns}/\${encodeURIComponent(id)}/\${decision}\`;
7857
+ }
6781
7858
  function granolaExportJobRerunPath(id) {
6782
7859
  return \`\${granolaTransportPaths.exportJobs}/\${encodeURIComponent(id)}/rerun\`;
6783
7860
  }
@@ -6952,6 +8029,23 @@ var GranolaServerClient = class GranolaServerClient {
6952
8029
  async inspectAuth() {
6953
8030
  return await this.requestJson(granolaTransportPaths.authStatus);
6954
8031
  }
8032
+ async listAutomationRules() {
8033
+ return await this.requestJson(granolaTransportPaths.automationRules);
8034
+ }
8035
+ async listAutomationMatches(options = {}) {
8036
+ const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
8037
+ return await this.requestJson(path);
8038
+ }
8039
+ async listAutomationRuns(options = {}) {
8040
+ return await this.requestJson(granolaAutomationRunsPath(options));
8041
+ }
8042
+ async resolveAutomationRun(id, decision, options = {}) {
8043
+ return await this.requestJson(granolaAutomationRunDecisionPath(id, decision), {
8044
+ body: JSON.stringify(options),
8045
+ headers: { "content-type": "application/json" },
8046
+ method: "POST"
8047
+ });
8048
+ }
6955
8049
  async inspectSync() {
6956
8050
  return cloneValue(_classPrivateFieldGet2(_state, this).sync);
6957
8051
  }
@@ -7170,7 +8264,7 @@ function nextWorkspaceTab(currentTab, key) {
7170
8264
  //#endregion
7171
8265
  //#region src/web-app/components.tsx
7172
8266
  /** @jsxImportSource solid-js */
7173
- var _tmpl$$1 = /* @__PURE__ */ template(\`<section class=hero><h1>Granola Toolkit</h1><p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p><input class=search placeholder="Search meetings, ids, or tags"><div class="field-row field-row--inline"><label><span class=field-label>Sort</span><select class=select><option value=updated-desc>Newest first</option><option value=updated-asc>Oldest first</option><option value=title-asc>Title A-Z</option><option value=title-desc>Title Z-A</option></select></label><label><span class=field-label>Updated From</span><input class=field-input type=date></label></div><label class=field-row><span class=field-label>Updated To</span><input class=field-input type=date>\`), _tmpl$2 = /* @__PURE__ */ template(\`<section class=toolbar><div><p>Meetings are loaded from the shared server state so this view can stay aligned with the terminal UI and sync loop.</p></div><div class=toolbar-form><input class=field-input placeholder="Quick open by id or title"><button class="button button--secondary"type=button>Open\`), _tmpl$3 = /* @__PURE__ */ template(\`<div class="folder-empty folder-empty--error">\`), _tmpl$4 = /* @__PURE__ */ template(\`<section class=folder-panel><div class=folder-panel__head><h2>Folders</h2><p>Pick a folder to scope the meeting browser, or stay on All meetings.</p></div><div class=folder-list>\`), _tmpl$5 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title>All meetings</span><span class=folder-row__meta>Browse the full meeting list.\`), _tmpl$6 = /* @__PURE__ */ template(\`<div class=folder-empty>No folders found.\`), _tmpl$7 = /* @__PURE__ */ template(\`<button class=folder-row type=button><span class=folder-row__title></span><span class=folder-row__meta>\`), _tmpl$8 = /* @__PURE__ */ template(\`<div class="meeting-empty meeting-empty--error">\`), _tmpl$9 = /* @__PURE__ */ template(\`<section class=meeting-list>\`), _tmpl$0 = /* @__PURE__ */ template(\`<div class=meeting-empty>\`), _tmpl$1 = /* @__PURE__ */ template(\`<button class=meeting-row type=button><span class=meeting-row__title></span><span class=meeting-row__meta></span><span class=meeting-row__meta>\`), _tmpl$10 = /* @__PURE__ */ template(\`<section class=detail-head><div><h2>Meeting Workspace</h2></div><div class=state-badge>\`), _tmpl$11 = /* @__PURE__ */ template(\`<p>Waiting for server state…\`), _tmpl$12 = /* @__PURE__ */ template(\`<div class=status-grid><div><span class=status-label>Surface</span><strong></strong></div><div><span class=status-label>View</span><strong></strong></div><div><span class=status-label>Auth</span><strong></strong></div><div><span class=status-label>Sync</span><strong></strong></div><div><span class=status-label>Documents</span><strong></strong></div><div><span class=status-label>Folders</span><strong></strong></div><div><span class=status-label>Cache</span><strong></strong></div><div><span class=status-label>Index</span><strong>\`), _tmpl$13 = /* @__PURE__ */ template(\`<section class=security-panel><div class=security-panel__head><h3>Server Access</h3><p>This server is locked with a password. Unlock it to load meetings and live state.</p></div><div class=security-panel__body><input class=field-input placeholder="Server password"type=password><div class=toolbar-actions><button class="button button--primary"type=button>Unlock</button><button class="button button--secondary"type=button>Lock\`), _tmpl$14 = /* @__PURE__ */ template(\`<section class=auth-panel><div class=auth-panel__head><h3>Auth Session</h3><p>Inspect, refresh, and switch between API key, stored session, and <code>supabase.json</code>.</p></div><div class=auth-panel__body>\`), _tmpl$15 = /* @__PURE__ */ template(\`<div class=auth-card><div class=auth-card__meta>Auth state unavailable.\`), _tmpl$16 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Client ID: \`), _tmpl$17 = /* @__PURE__ */ template(\`<div class=auth-card__meta>Sign-in method: \`), _tmpl$18 = /* @__PURE__ */ template(\`<div class=auth-card__meta>supabase path: \`), _tmpl$19 = /* @__PURE__ */ template(\`<div class="auth-card__meta auth-card__error">\`), _tmpl$20 = /* @__PURE__ */ template(\`<div class=auth-card><div class=status-grid><div><span class=status-label>Active</span><strong></strong></div><div><span class=status-label>API key</span><strong></strong></div><div><span class=status-label>Stored</span><strong></strong></div><div><span class=status-label>supabase.json</span><strong></strong></div><div><span class=status-label>Refresh</span><strong></strong></div></div><div class=auth-card__meta>Store a Granola Personal API key here or use <code>granola auth login --api-key &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>\`);
7174
8268
  function authModeLabel(mode) {
7175
8269
  switch (mode) {
7176
8270
  case "api-key": return "API key";
@@ -7387,7 +8481,7 @@ function AppStatePanel(props) {
7387
8481
  return props.appState;
7388
8482
  },
7389
8483
  children: (appState) => (() => {
7390
- var _el$39 = _tmpl$12(), _el$40 = _el$39.firstChild, _el$42 = _el$40.firstChild.nextSibling, _el$43 = _el$40.nextSibling, _el$45 = _el$43.firstChild.nextSibling, _el$46 = _el$43.nextSibling, _el$48 = _el$46.firstChild.nextSibling, _el$49 = _el$46.nextSibling, _el$51 = _el$49.firstChild.nextSibling, _el$52 = _el$49.nextSibling, _el$54 = _el$52.firstChild.nextSibling, _el$55 = _el$52.nextSibling, _el$57 = _el$55.firstChild.nextSibling, _el$58 = _el$55.nextSibling, _el$60 = _el$58.firstChild.nextSibling, _el$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;
7391
8485
  insert(_el$42, () => appState().ui.surface);
7392
8486
  insert(_el$45, () => appState().ui.view);
7393
8487
  insert(_el$48, authStatus);
@@ -7408,6 +8502,7 @@ function AppStatePanel(props) {
7408
8502
  var _c$7 = memo(() => !!appState().index.loaded);
7409
8503
  return () => _c$7() ? \`\${appState().index.meetingCount} meetings\` : appState().index.available ? "available" : "not built";
7410
8504
  })());
8505
+ insert(_el$66, () => \`\${appState().automation.runCount} runs / \${appState().automation.pendingRunCount} pending\`);
7411
8506
  return _el$39;
7412
8507
  })()
7413
8508
  }), null);
@@ -7422,27 +8517,27 @@ function SecurityPanel(props) {
7422
8517
  return props.visible;
7423
8518
  },
7424
8519
  get children() {
7425
- var _el$64 = _tmpl$13(), _el$67 = _el$64.firstChild.nextSibling.firstChild, _el$69 = _el$67.nextSibling.firstChild, _el$70 = _el$69.nextSibling;
7426
- _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) => {
7427
8522
  if (event.key === "Enter") {
7428
8523
  event.preventDefault();
7429
8524
  props.onUnlock();
7430
8525
  }
7431
8526
  };
7432
- _el$67.$$input = (event) => {
8527
+ _el$70.$$input = (event) => {
7433
8528
  props.onPasswordChange(event.currentTarget.value);
7434
8529
  };
7435
- addEventListener(_el$69, "click", props.onUnlock, true);
7436
- addEventListener(_el$70, "click", props.onLock, true);
7437
- createRenderEffect(() => _el$67.value = props.password);
7438
- 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;
7439
8534
  }
7440
8535
  });
7441
8536
  }
7442
8537
  function AuthPanel(props) {
7443
8538
  return (() => {
7444
- var _el$71 = _tmpl$14(), _el$73 = _el$71.firstChild.nextSibling;
7445
- insert(_el$73, createComponent(Show, {
8539
+ var _el$74 = _tmpl$14(), _el$76 = _el$74.firstChild.nextSibling;
8540
+ insert(_el$76, createComponent(Show, {
7446
8541
  get fallback() {
7447
8542
  return _tmpl$15();
7448
8543
  },
@@ -7450,79 +8545,79 @@ function AuthPanel(props) {
7450
8545
  return props.auth;
7451
8546
  },
7452
8547
  children: (auth) => (() => {
7453
- var _el$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;
7454
- insert(_el$79, () => authModeLabel(auth().mode));
7455
- insert(_el$82, () => auth().apiKeyAvailable ? "available" : "missing");
7456
- insert(_el$85, () => auth().storedSessionAvailable ? "available" : "missing");
7457
- insert(_el$88, () => auth().supabaseAvailable ? "available" : "missing");
7458
- insert(_el$91, () => auth().refreshAvailable ? "available" : "missing");
7459
- 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, {
7460
8555
  get when() {
7461
8556
  return auth().clientId;
7462
8557
  },
7463
8558
  get children() {
7464
- var _el$92 = _tmpl$16();
7465
- _el$92.firstChild;
7466
- insert(_el$92, () => auth().clientId, null);
7467
- return _el$92;
8559
+ var _el$95 = _tmpl$16();
8560
+ _el$95.firstChild;
8561
+ insert(_el$95, () => auth().clientId, null);
8562
+ return _el$95;
7468
8563
  }
7469
- }), _el$99);
7470
- insert(_el$75, createComponent(Show, {
8564
+ }), _el$102);
8565
+ insert(_el$78, createComponent(Show, {
7471
8566
  get when() {
7472
8567
  return auth().signInMethod;
7473
8568
  },
7474
8569
  get children() {
7475
- var _el$94 = _tmpl$17();
7476
- _el$94.firstChild;
7477
- insert(_el$94, () => auth().signInMethod, null);
7478
- return _el$94;
8570
+ var _el$97 = _tmpl$17();
8571
+ _el$97.firstChild;
8572
+ insert(_el$97, () => auth().signInMethod, null);
8573
+ return _el$97;
7479
8574
  }
7480
- }), _el$99);
7481
- insert(_el$75, createComponent(Show, {
8575
+ }), _el$102);
8576
+ insert(_el$78, createComponent(Show, {
7482
8577
  get when() {
7483
8578
  return auth().supabasePath;
7484
8579
  },
7485
8580
  get children() {
7486
- var _el$96 = _tmpl$18();
7487
- _el$96.firstChild;
7488
- insert(_el$96, () => auth().supabasePath, null);
7489
- return _el$96;
8581
+ var _el$99 = _tmpl$18();
8582
+ _el$99.firstChild;
8583
+ insert(_el$99, () => auth().supabasePath, null);
8584
+ return _el$99;
7490
8585
  }
7491
- }), _el$99);
7492
- insert(_el$75, createComponent(Show, {
8586
+ }), _el$102);
8587
+ insert(_el$78, createComponent(Show, {
7493
8588
  get when() {
7494
8589
  return auth().lastError;
7495
8590
  },
7496
8591
  get children() {
7497
- var _el$98 = _tmpl$19();
7498
- insert(_el$98, () => auth().lastError);
7499
- return _el$98;
8592
+ var _el$101 = _tmpl$19();
8593
+ insert(_el$101, () => auth().lastError);
8594
+ return _el$101;
7500
8595
  }
7501
- }), _el$99);
7502
- _el$101.$$input = (event) => {
8596
+ }), _el$102);
8597
+ _el$104.$$input = (event) => {
7503
8598
  props.onApiKeyDraftChange(event.currentTarget.value);
7504
8599
  };
7505
- addEventListener(_el$102, "click", props.onSaveApiKey, true);
7506
- addEventListener(_el$103, "click", props.onImportDesktopSession, true);
7507
- addEventListener(_el$104, "click", props.onRefresh, true);
7508
- _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 = () => {
7509
8604
  props.onSwitchMode("api-key");
7510
8605
  };
7511
- _el$106.$$click = () => {
8606
+ _el$109.$$click = () => {
7512
8607
  props.onSwitchMode("stored-session");
7513
8608
  };
7514
- _el$107.$$click = () => {
8609
+ _el$110.$$click = () => {
7515
8610
  props.onSwitchMode("supabase-file");
7516
8611
  };
7517
- addEventListener(_el$108, "click", props.onLogout, true);
8612
+ addEventListener(_el$111, "click", props.onLogout, true);
7518
8613
  createRenderEffect((_p$) => {
7519
8614
  var _v$ = !auth().supabaseAvailable, _v$2 = !auth().storedSessionAvailable || !auth().refreshAvailable, _v$3 = !auth().apiKeyAvailable || auth().mode === "api-key", _v$4 = !auth().storedSessionAvailable || auth().mode === "stored-session", _v$5 = !auth().supabaseAvailable || auth().mode === "supabase-file", _v$6 = !auth().apiKeyAvailable && !auth().storedSessionAvailable;
7520
- _v$ !== _p$.e && (_el$103.disabled = _p$.e = _v$);
7521
- _v$2 !== _p$.t && (_el$104.disabled = _p$.t = _v$2);
7522
- _v$3 !== _p$.a && (_el$105.disabled = _p$.a = _v$3);
7523
- _v$4 !== _p$.o && (_el$106.disabled = _p$.o = _v$4);
7524
- _v$5 !== _p$.i && (_el$107.disabled = _p$.i = _v$5);
7525
- _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);
7526
8621
  return _p$;
7527
8622
  }, {
7528
8623
  e: void 0,
@@ -7532,17 +8627,17 @@ function AuthPanel(props) {
7532
8627
  i: void 0,
7533
8628
  n: void 0
7534
8629
  });
7535
- createRenderEffect(() => _el$101.value = props.apiKeyDraft);
7536
- return _el$75;
8630
+ createRenderEffect(() => _el$104.value = props.apiKeyDraft);
8631
+ return _el$78;
7537
8632
  })()
7538
8633
  }));
7539
- return _el$71;
8634
+ return _el$74;
7540
8635
  })();
7541
8636
  }
7542
8637
  function ExportJobsPanel(props) {
7543
8638
  return (() => {
7544
- var _el$109 = _tmpl$21(), _el$111 = _el$109.firstChild.nextSibling;
7545
- insert(_el$111, createComponent(Show, {
8639
+ var _el$112 = _tmpl$21(), _el$114 = _el$112.firstChild.nextSibling;
8640
+ insert(_el$114, createComponent(Show, {
7546
8641
  get when() {
7547
8642
  return props.jobs.length > 0;
7548
8643
  },
@@ -7555,46 +8650,127 @@ function ExportJobsPanel(props) {
7555
8650
  return props.jobs.slice(0, 6);
7556
8651
  },
7557
8652
  children: (job) => (() => {
7558
- var _el$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;
7559
- _el$121.firstChild;
7560
- var _el$123 = _el$121.nextSibling;
7561
- _el$123.firstChild;
7562
- var _el$126 = _el$123.nextSibling;
7563
- insert(_el$116, () => job.kind, _el$117);
7564
- insert(_el$118, () => job.id);
7565
- insert(_el$119, () => job.status);
7566
- insert(_el$120, () => \`Format: \${job.format} • \${scopeLabel(job.scope)} • \${job.itemCount > 0 ? \`\${job.completedCount}/\${job.itemCount} items\` : "0 items"} • Written: \${job.written}\`);
7567
- insert(_el$121, () => job.startedAt.slice(0, 19), null);
7568
- insert(_el$123, () => job.outputDir, null);
7569
- 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, {
7570
8665
  get when() {
7571
8666
  return job.error;
7572
8667
  },
7573
8668
  get children() {
7574
- var _el$125 = _tmpl$23();
7575
- insert(_el$125, () => job.error);
7576
- return _el$125;
8669
+ var _el$128 = _tmpl$23();
8670
+ insert(_el$128, () => job.error);
8671
+ return _el$128;
7577
8672
  }
7578
- }), _el$126);
7579
- insert(_el$126, createComponent(Show, {
8673
+ }), _el$129);
8674
+ insert(_el$129, createComponent(Show, {
7580
8675
  get when() {
7581
8676
  return job.status !== "running";
7582
8677
  },
7583
8678
  get children() {
7584
- var _el$127 = _tmpl$24();
7585
- _el$127.$$click = () => {
8679
+ var _el$130 = _tmpl$24();
8680
+ _el$130.$$click = () => {
7586
8681
  props.onRerun(job.id);
7587
8682
  };
7588
- 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
+ })()];
7589
8765
  }
7590
8766
  }));
7591
- createRenderEffect(() => setAttribute(_el$119, "data-status", job.status));
7592
- return _el$113;
8767
+ createRenderEffect(() => setAttribute(_el$140, "data-status", run.status));
8768
+ return _el$135;
7593
8769
  })()
7594
8770
  });
7595
8771
  }
7596
8772
  }));
7597
- return _el$109;
8773
+ return _el$131;
7598
8774
  })();
7599
8775
  }
7600
8776
  function Workspace(props) {
@@ -7604,8 +8780,8 @@ function Workspace(props) {
7604
8780
  return workspaceBody(props.bundle, props.selectedMeeting, parsedTab());
7605
8781
  };
7606
8782
  return [(() => {
7607
- var _el$128 = _tmpl$26(), _el$129 = _el$128.firstChild;
7608
- insert(_el$128, createComponent(For, {
8783
+ var _el$149 = _tmpl$31(), _el$150 = _el$149.firstChild;
8784
+ insert(_el$149, createComponent(For, {
7609
8785
  each: [
7610
8786
  "notes",
7611
8787
  "transcript",
@@ -7613,50 +8789,50 @@ function Workspace(props) {
7613
8789
  "raw"
7614
8790
  ],
7615
8791
  children: (tab) => (() => {
7616
- var _el$130 = _tmpl$27();
7617
- _el$130.$$click = () => {
8792
+ var _el$151 = _tmpl$32();
8793
+ _el$151.$$click = () => {
7618
8794
  props.onSelectTab(tab);
7619
8795
  };
7620
- insert(_el$130, tab === "notes" ? "Notes" : tab === "transcript" ? "Transcript" : tab === "metadata" ? "Metadata" : "Raw");
7621
- createRenderEffect(() => setAttribute(_el$130, "data-selected", parsedTab() === tab ? "true" : void 0));
7622
- 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;
7623
8799
  })()
7624
- }), _el$129);
7625
- return _el$128;
8800
+ }), _el$150);
8801
+ return _el$149;
7626
8802
  })(), createComponent(Show, {
7627
8803
  get when() {
7628
8804
  return props.selectedMeeting;
7629
8805
  },
7630
8806
  get fallback() {
7631
8807
  return (() => {
7632
- var _el$131 = _tmpl$28();
7633
- insert(_el$131, () => props.detailError || "Select a meeting to inspect its notes and transcript.");
7634
- 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;
7635
8811
  })();
7636
8812
  },
7637
8813
  children: (meeting) => [(() => {
7638
- var _el$132 = _tmpl$29(), _el$133 = _el$132.firstChild, _el$134 = _el$133.nextSibling, _el$135 = _el$134.nextSibling;
7639
- insert(_el$133, () => \`ID: \${meeting().meeting.id}\`);
7640
- insert(_el$134, () => \`Source: \${meeting().meeting.noteContentSource}\`);
7641
- insert(_el$135, () => \`Transcript: \${meeting().meeting.transcriptSegmentCount} segments\`);
7642
- 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;
7643
8819
  })(), createComponent(Show, {
7644
8820
  get when() {
7645
8821
  return !props.detailError;
7646
8822
  },
7647
8823
  get fallback() {
7648
8824
  return (() => {
7649
- var _el$144 = _tmpl$28();
7650
- insert(_el$144, () => props.detailError);
7651
- return _el$144;
8825
+ var _el$165 = _tmpl$33();
8826
+ insert(_el$165, () => props.detailError);
8827
+ return _el$165;
7652
8828
  })();
7653
8829
  },
7654
8830
  get children() {
7655
- var _el$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;
7656
- insert(_el$140, () => metadataLines(meeting()));
7657
- insert(_el$142, () => details()?.title);
7658
- insert(_el$143, () => details()?.body);
7659
- 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;
7660
8836
  }
7661
8837
  })]
7662
8838
  })];
@@ -7687,6 +8863,7 @@ function App() {
7687
8863
  const [state, setState] = createStore({
7688
8864
  apiKeyDraft: "",
7689
8865
  appState: null,
8866
+ automationRuns: [],
7690
8867
  detailError: "",
7691
8868
  folderError: "",
7692
8869
  folders: [],
@@ -7755,6 +8932,14 @@ function App() {
7755
8932
  setState("selectedFolderId", null);
7756
8933
  }
7757
8934
  };
8935
+ const loadAutomationRuns = async () => {
8936
+ if (!client) return;
8937
+ try {
8938
+ setState("automationRuns", (await client.listAutomationRuns({ limit: 20 })).runs);
8939
+ } catch (error) {
8940
+ setState("detailError", error instanceof Error ? error.message : String(error));
8941
+ }
8942
+ };
7758
8943
  const loadMeeting = async (meetingId) => {
7759
8944
  if (!client) return;
7760
8945
  setState("selectedMeetingId", meetingId);
@@ -7808,7 +8993,11 @@ function App() {
7808
8993
  forceRefresh: true,
7809
8994
  foreground: true
7810
8995
  });
7811
- await Promise.all([loadFolders(forceRefresh), mergeAuthState()]);
8996
+ await Promise.all([
8997
+ loadFolders(forceRefresh),
8998
+ loadAutomationRuns(),
8999
+ mergeAuthState()
9000
+ ]);
7812
9001
  await loadMeetings({ refresh: forceRefresh });
7813
9002
  setState("serverLocked", false);
7814
9003
  setStatus(forceRefresh ? "Sync complete" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
@@ -7941,6 +9130,17 @@ function App() {
7941
9130
  setStatus("Rerun failed", "error");
7942
9131
  }
7943
9132
  };
9133
+ const resolveAutomationRun = async (id, decision) => {
9134
+ if (!client) return;
9135
+ setStatus(decision === "approve" ? "Approving automation…" : "Rejecting automation…", "busy");
9136
+ try {
9137
+ await client.resolveAutomationRun(id, decision);
9138
+ await refreshAll();
9139
+ } catch (error) {
9140
+ setState("detailError", error instanceof Error ? error.message : String(error));
9141
+ setStatus("Automation decision failed", "error");
9142
+ }
9143
+ };
7944
9144
  const unlockServer = async () => {
7945
9145
  if (!state.serverPassword.trim()) {
7946
9146
  setStatus("Enter the server password", "error");
@@ -7968,6 +9168,7 @@ function App() {
7968
9168
  await detachClient();
7969
9169
  setState({
7970
9170
  appState: null,
9171
+ automationRuns: [],
7971
9172
  detailError: "",
7972
9173
  folderError: "",
7973
9174
  folders: [],
@@ -7990,6 +9191,10 @@ function App() {
7990
9191
  });
7991
9192
  if (nextPath !== \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`) history.replaceState(null, "", nextPath);
7992
9193
  });
9194
+ createEffect(() => {
9195
+ if (!state.appState?.automation.loaded || !client) return;
9196
+ loadAutomationRuns();
9197
+ });
7993
9198
  onMount(() => {
7994
9199
  const onKeyDown = (event) => {
7995
9200
  const target = event.target;
@@ -8165,6 +9370,17 @@ function App() {
8165
9370
  rerunJob(jobId);
8166
9371
  }
8167
9372
  }), null);
9373
+ insert(_el$3, createComponent(AutomationRunsPanel, {
9374
+ onApprove: (runId) => {
9375
+ resolveAutomationRun(runId, "approve");
9376
+ },
9377
+ onReject: (runId) => {
9378
+ resolveAutomationRun(runId, "reject");
9379
+ },
9380
+ get runs() {
9381
+ return state.automationRuns;
9382
+ }
9383
+ }), null);
8168
9384
  insert(_el$3, createComponent(Workspace, {
8169
9385
  get bundle() {
8170
9386
  return state.selectedMeetingBundle;
@@ -8262,6 +9478,17 @@ function parseAuthMode(value) {
8262
9478
  default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
8263
9479
  }
8264
9480
  }
9481
+ function parseAutomationRunStatus(value) {
9482
+ switch (value) {
9483
+ case null:
9484
+ case "": return;
9485
+ case "completed":
9486
+ case "failed":
9487
+ case "pending":
9488
+ case "skipped": return value;
9489
+ default: throw new Error("invalid automation status: expected completed, failed, pending, or skipped");
9490
+ }
9491
+ }
8265
9492
  function folderIdFromBody(value) {
8266
9493
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
8267
9494
  }
@@ -8388,6 +9615,7 @@ async function startGranolaServer(app, options = {}) {
8388
9615
  capabilities: {
8389
9616
  attach: true,
8390
9617
  auth: true,
9618
+ automation: true,
8391
9619
  events: true,
8392
9620
  exports: true,
8393
9621
  folders: true,
@@ -8508,6 +9736,28 @@ async function startGranolaServer(app, options = {}) {
8508
9736
  sendJson(response, await app.inspectAuth(), { headers: originHeaders });
8509
9737
  return;
8510
9738
  }
9739
+ if (method === "GET" && path === granolaTransportPaths.automationRules) {
9740
+ sendJson(response, await app.listAutomationRules(), { headers: originHeaders });
9741
+ return;
9742
+ }
9743
+ if (method === "GET" && path === granolaTransportPaths.automationMatches) {
9744
+ sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
9745
+ return;
9746
+ }
9747
+ if (method === "GET" && path === granolaTransportPaths.automationRuns) {
9748
+ sendJson(response, await app.listAutomationRuns({
9749
+ limit: parseInteger(url.searchParams.get("limit")),
9750
+ status: parseAutomationRunStatus(url.searchParams.get("status"))
9751
+ }), { headers: originHeaders });
9752
+ return;
9753
+ }
9754
+ if (method === "POST" && (path.endsWith("/approve") || path.endsWith("/reject")) && path.startsWith(`${granolaTransportPaths.automationRuns}/`)) {
9755
+ const decision = path.endsWith("/approve") ? "approve" : "reject";
9756
+ const id = decodeURIComponent(path.slice(`${granolaTransportPaths.automationRuns}/`.length, -`/${decision}`.length));
9757
+ const body = await readJsonBody(request);
9758
+ sendJson(response, await app.resolveAutomationRun(id, decision, { note: typeof body.note === "string" ? body.note : void 0 }), { headers: originHeaders });
9759
+ return;
9760
+ }
8511
9761
  if (method === "POST" && path === granolaTransportPaths.syncRun) {
8512
9762
  const body = await readJsonBody(request);
8513
9763
  sendJson(response, await app.sync({
@@ -9515,6 +10765,7 @@ Options:
9515
10765
  //#region src/commands/index.ts
9516
10766
  const commands = [
9517
10767
  attachCommand,
10768
+ automationCommand,
9518
10769
  authCommand,
9519
10770
  exportsCommand,
9520
10771
  folderCommand,
@@ -9645,6 +10896,7 @@ Global options:
9645
10896
  --api-key <token> Granola Personal API key
9646
10897
  --config <path> Path to .granola.toml
9647
10898
  --debug Enable debug logging
10899
+ --rules <path> Path to automation rules JSON
9648
10900
  --supabase <path> Path to supabase.json
9649
10901
  -h, --help Show help
9650
10902
 
@@ -9664,6 +10916,7 @@ async function runCli(argv) {
9664
10916
  config: { type: "string" },
9665
10917
  debug: { type: "boolean" },
9666
10918
  help: { type: "boolean" },
10919
+ rules: { type: "string" },
9667
10920
  supabase: { type: "string" }
9668
10921
  });
9669
10922
  if (global.values.help && !command) {