granola-toolkit 0.41.0 → 0.43.0

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