granola-toolkit 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +411 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,7 @@ The published package exposes both `granola` and `granola-toolkit` as executable
23
23
  granola auth login --api-key grn_...
24
24
  granola sync
25
25
  granola sync --watch
26
+ granola automation rules
26
27
  granola folder list
27
28
  granola meeting list --limit 10
28
29
  granola notes --folder Team
package/dist/cli.js CHANGED
@@ -18,6 +18,8 @@ const granolaTransportPaths = {
18
18
  authRefresh: "/auth/refresh",
19
19
  authStatus: "/auth/status",
20
20
  authUnlock: "/auth/unlock",
21
+ automationMatches: "/automation/matches",
22
+ automationRules: "/automation/rules",
21
23
  events: "/events",
22
24
  exportJobs: "/exports/jobs",
23
25
  exportNotes: "/exports/notes",
@@ -179,6 +181,13 @@ var GranolaServerClient = class GranolaServerClient {
179
181
  async inspectAuth() {
180
182
  return await this.requestJson(granolaTransportPaths.authStatus);
181
183
  }
184
+ async listAutomationRules() {
185
+ return await this.requestJson(granolaTransportPaths.automationRules);
186
+ }
187
+ async listAutomationMatches(options = {}) {
188
+ const path = options.limit ? `${granolaTransportPaths.automationMatches}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.automationMatches;
189
+ return await this.requestJson(path);
190
+ }
182
191
  async inspectSync() {
183
192
  return cloneValue(this.#state.sync);
184
193
  }
@@ -420,7 +429,7 @@ function asRecord(value) {
420
429
  function stringValue(value) {
421
430
  return typeof value === "string" ? value : "";
422
431
  }
423
- function stringArray(value) {
432
+ function stringArray$1(value) {
424
433
  if (!Array.isArray(value)) return [];
425
434
  return value.filter((item) => typeof item === "string");
426
435
  }
@@ -2341,6 +2350,168 @@ const attachCommand = {
2341
2350
  }
2342
2351
  };
2343
2352
  //#endregion
2353
+ //#region src/persistence/layout.ts
2354
+ function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
2355
+ return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
2356
+ }
2357
+ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2358
+ const targetPlatform = options.platform ?? platform();
2359
+ const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
2360
+ return {
2361
+ automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
2362
+ automationRulesFile: join(dataDirectory, "automation-rules.json"),
2363
+ apiKeyFile: join(dataDirectory, "api-key.txt"),
2364
+ dataDirectory,
2365
+ exportJobsFile: join(dataDirectory, "export-jobs.json"),
2366
+ meetingIndexFile: join(dataDirectory, "meeting-index.json"),
2367
+ sessionFile: join(dataDirectory, "session.json"),
2368
+ sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
2369
+ syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
2370
+ syncStateFile: join(dataDirectory, "sync-state.json")
2371
+ };
2372
+ }
2373
+ //#endregion
2374
+ //#region src/automation-matches.ts
2375
+ function cloneMatch(match) {
2376
+ return {
2377
+ ...match,
2378
+ folders: match.folders.map((folder) => ({ ...folder })),
2379
+ tags: [...match.tags]
2380
+ };
2381
+ }
2382
+ var FileAutomationMatchStore = class {
2383
+ constructor(filePath = defaultAutomationMatchesFilePath()) {
2384
+ this.filePath = filePath;
2385
+ }
2386
+ async appendMatches(matches) {
2387
+ if (matches.length === 0) return;
2388
+ await mkdir(dirname(this.filePath), { recursive: true });
2389
+ const payload = matches.map((match) => JSON.stringify(match)).join("\n");
2390
+ await appendFile(this.filePath, `${payload}\n`, {
2391
+ encoding: "utf8",
2392
+ mode: 384
2393
+ });
2394
+ }
2395
+ async readMatches(limit = 50) {
2396
+ try {
2397
+ const matches = (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((match) => Boolean(match)).map(cloneMatch);
2398
+ return (limit > 0 ? matches.slice(-limit) : matches).reverse();
2399
+ } catch {
2400
+ return [];
2401
+ }
2402
+ }
2403
+ };
2404
+ function defaultAutomationMatchesFilePath() {
2405
+ return defaultGranolaToolkitPersistenceLayout().automationMatchesFile;
2406
+ }
2407
+ function createDefaultAutomationMatchStore(filePath) {
2408
+ return new FileAutomationMatchStore(filePath);
2409
+ }
2410
+ //#endregion
2411
+ //#region src/automation-rules.ts
2412
+ function cloneRule(rule) {
2413
+ return {
2414
+ ...rule,
2415
+ when: {
2416
+ ...rule.when,
2417
+ eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
2418
+ folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
2419
+ folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
2420
+ meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
2421
+ tags: rule.when.tags ? [...rule.when.tags] : void 0,
2422
+ titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
2423
+ }
2424
+ };
2425
+ }
2426
+ function stringArray(value) {
2427
+ if (!Array.isArray(value)) return;
2428
+ const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
2429
+ return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
2430
+ }
2431
+ function parseRule(value) {
2432
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2433
+ const record = value;
2434
+ const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
2435
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
2436
+ const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
2437
+ if (!id || !name || !whenValue) return;
2438
+ return {
2439
+ enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
2440
+ id,
2441
+ name,
2442
+ when: {
2443
+ eventKinds: stringArray(whenValue.eventKinds),
2444
+ folderIds: stringArray(whenValue.folderIds),
2445
+ folderNames: stringArray(whenValue.folderNames),
2446
+ meetingIds: stringArray(whenValue.meetingIds),
2447
+ tags: stringArray(whenValue.tags),
2448
+ titleIncludes: stringArray(whenValue.titleIncludes),
2449
+ titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
2450
+ transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
2451
+ }
2452
+ };
2453
+ }
2454
+ var FileAutomationRuleStore = class {
2455
+ constructor(filePath = defaultAutomationRulesFilePath()) {
2456
+ this.filePath = filePath;
2457
+ }
2458
+ async readRules() {
2459
+ try {
2460
+ const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
2461
+ return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.rules) ? parsed.rules : []).map((rule) => parseRule(rule)).filter((rule) => Boolean(rule)).map(cloneRule);
2462
+ } catch {
2463
+ return [];
2464
+ }
2465
+ }
2466
+ };
2467
+ function defaultAutomationRulesFilePath() {
2468
+ return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
2469
+ }
2470
+ function includesIgnoreCase(candidate, values) {
2471
+ const lowerCandidate = candidate.toLowerCase();
2472
+ return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
2473
+ }
2474
+ function matchesRule(rule, event) {
2475
+ if (rule.enabled === false) return false;
2476
+ const { when } = rule;
2477
+ if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
2478
+ if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
2479
+ if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
2480
+ if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
2481
+ if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
2482
+ if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
2483
+ if (when.titleMatches) try {
2484
+ if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
2485
+ } catch {
2486
+ return false;
2487
+ }
2488
+ if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
2489
+ return true;
2490
+ }
2491
+ function matchAutomationRules(rules, events, matchedAt) {
2492
+ const matches = [];
2493
+ for (const event of events) for (const rule of rules) {
2494
+ if (!matchesRule(rule, event)) continue;
2495
+ matches.push({
2496
+ eventId: event.id,
2497
+ eventKind: event.kind,
2498
+ folders: event.folders.map((folder) => ({ ...folder })),
2499
+ id: `${event.id}:${rule.id}`,
2500
+ matchedAt,
2501
+ meetingId: event.meetingId,
2502
+ ruleId: rule.id,
2503
+ ruleName: rule.name,
2504
+ tags: [...event.tags],
2505
+ title: event.title,
2506
+ transcriptLoaded: event.transcriptLoaded
2507
+ });
2508
+ }
2509
+ return matches;
2510
+ }
2511
+ function createDefaultAutomationRuleStore(filePath) {
2512
+ return new FileAutomationRuleStore(filePath);
2513
+ }
2514
+ //#endregion
2344
2515
  //#region src/cache.ts
2345
2516
  function parseCacheDocument(id, value) {
2346
2517
  const record = asRecord(value);
@@ -2395,25 +2566,6 @@ function parseCacheContents(contents) {
2395
2566
  };
2396
2567
  }
2397
2568
  //#endregion
2398
- //#region src/persistence/layout.ts
2399
- function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
2400
- return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
2401
- }
2402
- function defaultGranolaToolkitPersistenceLayout(options = {}) {
2403
- const targetPlatform = options.platform ?? platform();
2404
- const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
2405
- return {
2406
- apiKeyFile: join(dataDirectory, "api-key.txt"),
2407
- dataDirectory,
2408
- exportJobsFile: join(dataDirectory, "export-jobs.json"),
2409
- meetingIndexFile: join(dataDirectory, "meeting-index.json"),
2410
- sessionFile: join(dataDirectory, "session.json"),
2411
- sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
2412
- syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
2413
- syncStateFile: join(dataDirectory, "sync-state.json")
2414
- };
2415
- }
2416
- //#endregion
2417
2569
  //#region src/client/auth.ts
2418
2570
  const execFileAsync$1 = promisify(execFile);
2419
2571
  const DEFAULT_CLIENT_ID = "client_GranolaMac";
@@ -2911,7 +3063,7 @@ function parseDocument(value) {
2911
3063
  lastViewedPanel: parseLastViewedPanel(record.last_viewed_panel),
2912
3064
  notes: parseProseMirrorDoc(record.notes),
2913
3065
  notesPlain: stringValue(record.notes_plain),
2914
- tags: stringArray(record.tags),
3066
+ tags: stringArray$1(record.tags),
2915
3067
  title: stringValue(record.title),
2916
3068
  updatedAt: stringValue(record.updated_at)
2917
3069
  };
@@ -3684,6 +3836,16 @@ function normaliseMeeting(meeting) {
3684
3836
  function meetingChanged(previous, next) {
3685
3837
  return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
3686
3838
  }
3839
+ function cloneFolderSummary$1(folder) {
3840
+ return { ...folder };
3841
+ }
3842
+ function cloneMeetingSummary$1(meeting) {
3843
+ return {
3844
+ ...meeting,
3845
+ folders: meeting.folders.map((folder) => cloneFolderSummary$1(folder)),
3846
+ tags: [...meeting.tags]
3847
+ };
3848
+ }
3687
3849
  function diffMeetingSummaries(previous, next, folderCount) {
3688
3850
  const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
3689
3851
  const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
@@ -3756,15 +3918,20 @@ function diffMeetingSummaries(previous, next, folderCount) {
3756
3918
  }
3757
3919
  };
3758
3920
  }
3759
- function buildSyncEvents(runId, occurredAt, changes) {
3921
+ function buildSyncEvents(runId, occurredAt, changes, previousMeetings, nextMeetings) {
3922
+ const previousById = new Map(previousMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
3923
+ const nextById = new Map(nextMeetings.map((meeting) => [meeting.id, cloneMeetingSummary$1(meeting)]));
3760
3924
  return changes.map((change, index) => ({
3925
+ folders: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.folders.map((folder) => cloneFolderSummary$1(folder)) ?? [],
3761
3926
  id: `${runId}:${index + 1}`,
3762
3927
  kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
3763
3928
  meetingId: change.meetingId,
3764
3929
  occurredAt,
3765
3930
  previousUpdatedAt: change.previousUpdatedAt,
3766
3931
  runId,
3932
+ tags: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.tags.slice() ?? [],
3767
3933
  title: change.title,
3934
+ transcriptLoaded: (change.kind === "removed" ? previousById.get(change.meetingId) : nextById.get(change.meetingId))?.transcriptLoaded ?? false,
3768
3935
  updatedAt: change.updatedAt
3769
3936
  }));
3770
3937
  }
@@ -3811,9 +3978,11 @@ function cloneMeetingSummary(meeting) {
3811
3978
  function cloneState(state) {
3812
3979
  return {
3813
3980
  auth: { ...state.auth },
3981
+ automation: { ...state.automation },
3814
3982
  cache: { ...state.cache },
3815
3983
  config: {
3816
3984
  ...state.config,
3985
+ automation: state.config.automation ? { ...state.config.automation } : void 0,
3817
3986
  notes: { ...state.config.notes },
3818
3987
  transcripts: { ...state.config.transcripts }
3819
3988
  },
@@ -3832,6 +4001,13 @@ function cloneState(state) {
3832
4001
  function defaultState(config, auth, surface) {
3833
4002
  return {
3834
4003
  auth: { ...auth },
4004
+ automation: {
4005
+ loaded: false,
4006
+ matchCount: 0,
4007
+ matchesFile: defaultAutomationMatchesFilePath(),
4008
+ ruleCount: 0,
4009
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4010
+ },
3835
4011
  cache: {
3836
4012
  configured: Boolean(config.transcripts.cacheFile),
3837
4013
  documentCount: 0,
@@ -3873,6 +4049,8 @@ function defaultState(config, auth, surface) {
3873
4049
  };
3874
4050
  }
3875
4051
  var GranolaApp = class {
4052
+ #automationMatches;
4053
+ #automationRules;
3876
4054
  #cacheData;
3877
4055
  #cacheResolved = false;
3878
4056
  #folders;
@@ -3886,7 +4064,32 @@ var GranolaApp = class {
3886
4064
  this.config = config;
3887
4065
  this.deps = deps;
3888
4066
  this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
4067
+ this.#automationMatches = (deps.automationMatches ?? []).map((match) => ({
4068
+ ...match,
4069
+ folders: match.folders.map((folder) => ({ ...folder })),
4070
+ tags: [...match.tags]
4071
+ }));
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
+ }));
3889
4084
  this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
4085
+ this.#state.automation = {
4086
+ lastMatchedAt: this.#automationMatches[0]?.matchedAt,
4087
+ loaded: true,
4088
+ matchCount: this.#automationMatches.length,
4089
+ matchesFile: defaultAutomationMatchesFilePath(),
4090
+ ruleCount: this.#automationRules.length,
4091
+ rulesFile: config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4092
+ };
3890
4093
  this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
3891
4094
  this.#state.index = {
3892
4095
  available: this.#meetingIndex.length > 0,
@@ -3959,6 +4162,51 @@ var GranolaApp = class {
3959
4162
  if (!this.deps.syncStateStore) return;
3960
4163
  await this.deps.syncStateStore.writeState(this.#state.sync);
3961
4164
  }
4165
+ cloneAutomationRule(rule) {
4166
+ return {
4167
+ ...rule,
4168
+ when: {
4169
+ ...rule.when,
4170
+ eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
4171
+ folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
4172
+ folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
4173
+ meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
4174
+ tags: rule.when.tags ? [...rule.when.tags] : void 0,
4175
+ titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
4176
+ }
4177
+ };
4178
+ }
4179
+ cloneAutomationMatch(match) {
4180
+ return {
4181
+ ...match,
4182
+ folders: match.folders.map((folder) => ({ ...folder })),
4183
+ tags: [...match.tags]
4184
+ };
4185
+ }
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));
4190
+ this.#state.automation = {
4191
+ ...this.#state.automation,
4192
+ loaded: true,
4193
+ ruleCount: this.#automationRules.length,
4194
+ rulesFile: this.config.automation?.rulesFile ?? defaultAutomationRulesFilePath()
4195
+ };
4196
+ this.emitStateUpdate();
4197
+ return this.#automationRules.map((rule) => this.cloneAutomationRule(rule));
4198
+ }
4199
+ async appendAutomationMatches(matches) {
4200
+ if (matches.length === 0) return;
4201
+ if (this.deps.automationMatchStore) await this.deps.automationMatchStore.appendMatches(matches);
4202
+ 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
+ };
4209
+ }
3962
4210
  createSyncRunId() {
3963
4211
  return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
3964
4212
  }
@@ -4190,6 +4438,17 @@ var GranolaApp = class {
4190
4438
  if (!this.deps.syncEventStore) return { events: [] };
4191
4439
  return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
4192
4440
  }
4441
+ async listAutomationRules() {
4442
+ const rules = await this.loadAutomationRules({ forceRefresh: true });
4443
+ this.setUiState({ view: "idle" });
4444
+ return { rules: rules.map((rule) => this.cloneAutomationRule(rule)) };
4445
+ }
4446
+ async listAutomationMatches(options = {}) {
4447
+ const limit = options.limit ?? 20;
4448
+ const matches = this.deps.automationMatchStore ? await this.deps.automationMatchStore.readMatches(limit) : this.#automationMatches.slice(-limit).reverse();
4449
+ this.setUiState({ view: "idle" });
4450
+ return { matches: matches.map((match) => this.cloneAutomationMatch(match)) };
4451
+ }
4193
4452
  async loginAuth(options = {}) {
4194
4453
  const controller = this.requireAuthController();
4195
4454
  try {
@@ -4255,8 +4514,10 @@ var GranolaApp = class {
4255
4514
  const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
4256
4515
  const completedAt = this.nowIso();
4257
4516
  const runId = this.createSyncRunId();
4258
- const events = buildSyncEvents(runId, completedAt, changes);
4517
+ const events = buildSyncEvents(runId, completedAt, changes, previousMeetings, snapshot.meetings);
4259
4518
  if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
4519
+ const automationMatches = matchAutomationRules(await this.loadAutomationRules(), events, completedAt);
4520
+ await this.appendAutomationMatches(automationMatches);
4260
4521
  this.#state.sync = {
4261
4522
  ...this.#state.sync,
4262
4523
  eventCount: this.#state.sync.eventCount + events.length,
@@ -4607,6 +4868,10 @@ var GranolaApp = class {
4607
4868
  };
4608
4869
  async function createGranolaApp(config, options = {}) {
4609
4870
  const auth = await inspectDefaultGranolaAuth(config);
4871
+ const automationMatchStore = createDefaultAutomationMatchStore();
4872
+ const automationMatches = await automationMatchStore.readMatches(0);
4873
+ const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
4874
+ const automationRules = await automationRuleStore.readRules();
4610
4875
  const authController = createDefaultGranolaAuthController(config);
4611
4876
  const exportJobStore = createDefaultExportJobStore();
4612
4877
  const exportJobs = await exportJobStore.readJobs();
@@ -4618,6 +4883,10 @@ async function createGranolaApp(config, options = {}) {
4618
4883
  return new GranolaApp(config, {
4619
4884
  auth,
4620
4885
  authController,
4886
+ automationMatches,
4887
+ automationMatchStore,
4888
+ automationRules,
4889
+ automationRuleStore,
4621
4890
  cacheLoader: loadOptionalGranolaCache,
4622
4891
  createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
4623
4892
  exportJobs,
@@ -4692,6 +4961,7 @@ async function loadConfig(options) {
4692
4961
  const defaultCache = firstExistingPath(granolaCacheCandidates());
4693
4962
  const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
4694
4963
  return {
4964
+ automation: { rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile },
4695
4965
  apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
4696
4966
  configFileUsed: config.path,
4697
4967
  debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
@@ -4771,6 +5041,104 @@ async function waitForShutdown(close) {
4771
5041
  });
4772
5042
  }
4773
5043
  //#endregion
5044
+ //#region src/commands/automation.ts
5045
+ function automationHelp() {
5046
+ return `Granola automation
5047
+
5048
+ Usage:
5049
+ granola automation <rules|matches> [options]
5050
+
5051
+ Subcommands:
5052
+ rules List configured automation rules
5053
+ matches Show recent rule matches from sync events
5054
+
5055
+ Options:
5056
+ --format <value> text, json, yaml (default: text)
5057
+ --limit <n> Number of matches to show (default: 20)
5058
+ --rules <path> Path to automation rules JSON
5059
+ --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
5060
+ --supabase <path> Path to supabase.json
5061
+ --debug Enable debug logging
5062
+ --config <path> Path to .granola.toml
5063
+ -h, --help Show help
5064
+ `;
5065
+ }
5066
+ function resolveFormat(value) {
5067
+ switch (value) {
5068
+ case void 0: return "text";
5069
+ case "json":
5070
+ case "text":
5071
+ case "yaml": return value;
5072
+ default: throw new Error("invalid automation format: expected text, json, or yaml");
5073
+ }
5074
+ }
5075
+ function parseLimit$3(value) {
5076
+ if (value === void 0) return 20;
5077
+ if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid automation limit: expected a positive integer");
5078
+ return Number(value);
5079
+ }
5080
+ function renderRules(rules, format) {
5081
+ if (format === "json") return toJson({ rules });
5082
+ if (format === "yaml") return toYaml({ rules });
5083
+ if (rules.length === 0) return "No automation rules configured\n";
5084
+ return `${["ID ENABLED EVENTS FILTERS", ...rules.map((rule) => {
5085
+ const filters = [
5086
+ rule.when.folderIds?.length ? `folderIds=${rule.when.folderIds.join(",")}` : "",
5087
+ rule.when.folderNames?.length ? `folderNames=${rule.when.folderNames.join(",")}` : "",
5088
+ rule.when.tags?.length ? `tags=${rule.when.tags.join(",")}` : "",
5089
+ rule.when.titleIncludes?.length ? `title~=${rule.when.titleIncludes.join(",")}` : "",
5090
+ rule.when.transcriptLoaded === true ? "transcriptLoaded=true" : ""
5091
+ ].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 || "-"}`;
5093
+ })].join("\n")}\n`;
5094
+ }
5095
+ function renderMatches(matches, format) {
5096
+ if (format === "json") return toJson({ matches });
5097
+ if (format === "yaml") return toYaml({ matches });
5098
+ if (matches.length === 0) return "No automation matches yet\n";
5099
+ return `${["MATCHED AT RULE EVENT TITLE", ...matches.map((match) => {
5100
+ 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
+ })].join("\n")}\n`;
5102
+ }
5103
+ const automationCommand = {
5104
+ description: "Inspect automation rules and rule matches",
5105
+ flags: {
5106
+ format: { type: "string" },
5107
+ help: { type: "boolean" },
5108
+ limit: { type: "string" },
5109
+ timeout: { type: "string" }
5110
+ },
5111
+ help: automationHelp,
5112
+ name: "automation",
5113
+ async run({ commandArgs, commandFlags, globalFlags }) {
5114
+ const [action] = commandArgs;
5115
+ const format = resolveFormat(commandFlags.format);
5116
+ const config = await loadConfig({
5117
+ globalFlags,
5118
+ subcommandFlags: commandFlags
5119
+ });
5120
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
5121
+ debug(config.debug, "automationRules", config.automation?.rulesFile ?? "(default)");
5122
+ const app = await createGranolaApp(config);
5123
+ switch (action) {
5124
+ case "rules": {
5125
+ const result = await app.listAutomationRules();
5126
+ console.log(renderRules(result.rules, format).trimEnd());
5127
+ return 0;
5128
+ }
5129
+ case "matches": {
5130
+ const result = await app.listAutomationMatches({ limit: parseLimit$3(commandFlags.limit) });
5131
+ console.log(renderMatches(result.matches, format).trimEnd());
5132
+ return 0;
5133
+ }
5134
+ case void 0:
5135
+ console.log(automationHelp());
5136
+ return 1;
5137
+ default: throw new Error("invalid automation command: expected rules or matches");
5138
+ }
5139
+ }
5140
+ };
5141
+ //#endregion
4774
5142
  //#region src/commands/auth.ts
4775
5143
  function authHelp() {
4776
5144
  return `Granola auth
@@ -6719,6 +7087,8 @@ var granolaTransportPaths = {
6719
7087
  authRefresh: "/auth/refresh",
6720
7088
  authStatus: "/auth/status",
6721
7089
  authUnlock: "/auth/unlock",
7090
+ automationMatches: "/automation/matches",
7091
+ automationRules: "/automation/rules",
6722
7092
  events: "/events",
6723
7093
  exportJobs: "/exports/jobs",
6724
7094
  exportNotes: "/exports/notes",
@@ -6952,6 +7322,13 @@ var GranolaServerClient = class GranolaServerClient {
6952
7322
  async inspectAuth() {
6953
7323
  return await this.requestJson(granolaTransportPaths.authStatus);
6954
7324
  }
7325
+ async listAutomationRules() {
7326
+ return await this.requestJson(granolaTransportPaths.automationRules);
7327
+ }
7328
+ async listAutomationMatches(options = {}) {
7329
+ const path = options.limit ? \`\${granolaTransportPaths.automationMatches}?limit=\${encodeURIComponent(String(options.limit))}\` : granolaTransportPaths.automationMatches;
7330
+ return await this.requestJson(path);
7331
+ }
6955
7332
  async inspectSync() {
6956
7333
  return cloneValue(_classPrivateFieldGet2(_state, this).sync);
6957
7334
  }
@@ -8508,6 +8885,14 @@ async function startGranolaServer(app, options = {}) {
8508
8885
  sendJson(response, await app.inspectAuth(), { headers: originHeaders });
8509
8886
  return;
8510
8887
  }
8888
+ if (method === "GET" && path === granolaTransportPaths.automationRules) {
8889
+ sendJson(response, await app.listAutomationRules(), { headers: originHeaders });
8890
+ return;
8891
+ }
8892
+ if (method === "GET" && path === granolaTransportPaths.automationMatches) {
8893
+ sendJson(response, await app.listAutomationMatches({ limit: parseInteger(url.searchParams.get("limit")) }), { headers: originHeaders });
8894
+ return;
8895
+ }
8511
8896
  if (method === "POST" && path === granolaTransportPaths.syncRun) {
8512
8897
  const body = await readJsonBody(request);
8513
8898
  sendJson(response, await app.sync({
@@ -9515,6 +9900,7 @@ Options:
9515
9900
  //#region src/commands/index.ts
9516
9901
  const commands = [
9517
9902
  attachCommand,
9903
+ automationCommand,
9518
9904
  authCommand,
9519
9905
  exportsCommand,
9520
9906
  folderCommand,
@@ -9645,6 +10031,7 @@ Global options:
9645
10031
  --api-key <token> Granola Personal API key
9646
10032
  --config <path> Path to .granola.toml
9647
10033
  --debug Enable debug logging
10034
+ --rules <path> Path to automation rules JSON
9648
10035
  --supabase <path> Path to supabase.json
9649
10036
  -h, --help Show help
9650
10037
 
@@ -9664,6 +10051,7 @@ async function runCli(argv) {
9664
10051
  config: { type: "string" },
9665
10052
  debug: { type: "boolean" },
9666
10053
  help: { type: "boolean" },
10054
+ rules: { type: "string" },
9667
10055
  supabase: { type: "string" }
9668
10056
  });
9669
10057
  if (global.values.help && !command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",