granola-toolkit 0.47.0 → 0.48.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 (2) hide show
  1. package/dist/cli.js +189 -10
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -449,7 +449,7 @@ function asRecord(value) {
449
449
  function stringValue(value) {
450
450
  return typeof value === "string" ? value : "";
451
451
  }
452
- function stringArray$1(value) {
452
+ function stringArray$2(value) {
453
453
  if (!Array.isArray(value)) return [];
454
454
  return value.filter((item) => typeof item === "string");
455
455
  }
@@ -2612,6 +2612,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2612
2612
  const targetPlatform = options.platform ?? platform();
2613
2613
  const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
2614
2614
  return {
2615
+ agentHarnessesFile: join(dataDirectory, "agent-harnesses.json"),
2615
2616
  automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
2616
2617
  automationRulesFile: join(dataDirectory, "automation-rules.json"),
2617
2618
  automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
@@ -2627,6 +2628,145 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2627
2628
  };
2628
2629
  }
2629
2630
  //#endregion
2631
+ //#region src/agent-harnesses.ts
2632
+ function stringArray$1(value) {
2633
+ if (!Array.isArray(value)) return;
2634
+ const values = value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
2635
+ return values.length > 0 ? [...new Set(values)] : void 0;
2636
+ }
2637
+ function parseMatch(value) {
2638
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2639
+ const record = value;
2640
+ return {
2641
+ calendarEventIds: stringArray$1(record.calendarEventIds),
2642
+ eventKinds: stringArray$1(record.eventKinds),
2643
+ folderIds: stringArray$1(record.folderIds),
2644
+ folderNames: stringArray$1(record.folderNames),
2645
+ meetingIds: stringArray$1(record.meetingIds),
2646
+ recurringEventIds: stringArray$1(record.recurringEventIds),
2647
+ tags: stringArray$1(record.tags),
2648
+ titleIncludes: stringArray$1(record.titleIncludes),
2649
+ titleMatches: typeof record.titleMatches === "string" && record.titleMatches.trim() ? record.titleMatches.trim() : void 0,
2650
+ transcriptLoaded: typeof record.transcriptLoaded === "boolean" ? record.transcriptLoaded : void 0
2651
+ };
2652
+ }
2653
+ function parseHarness(value) {
2654
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2655
+ const record = value;
2656
+ const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
2657
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
2658
+ if (!id || !name) return;
2659
+ const provider = record.provider === "codex" || record.provider === "openai" || record.provider === "openrouter" ? record.provider : void 0;
2660
+ const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
2661
+ const promptFile = typeof record.promptFile === "string" && record.promptFile.trim() ? record.promptFile.trim() : void 0;
2662
+ if (!prompt && !promptFile) return;
2663
+ return {
2664
+ cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
2665
+ id,
2666
+ match: parseMatch(record.match),
2667
+ model: typeof record.model === "string" && record.model.trim() ? record.model.trim() : void 0,
2668
+ name,
2669
+ priority: typeof record.priority === "number" && Number.isFinite(record.priority) ? record.priority : typeof record.priority === "string" && /^-?\d+$/.test(record.priority) ? Number(record.priority) : void 0,
2670
+ prompt,
2671
+ promptFile,
2672
+ provider,
2673
+ systemPrompt: typeof record.systemPrompt === "string" && record.systemPrompt.trim() ? record.systemPrompt.trim() : void 0,
2674
+ systemPromptFile: typeof record.systemPromptFile === "string" && record.systemPromptFile.trim() ? record.systemPromptFile.trim() : void 0
2675
+ };
2676
+ }
2677
+ function cloneHarness(harness) {
2678
+ return {
2679
+ ...harness,
2680
+ match: harness.match ? {
2681
+ ...harness.match,
2682
+ calendarEventIds: harness.match.calendarEventIds ? [...harness.match.calendarEventIds] : void 0,
2683
+ eventKinds: harness.match.eventKinds ? [...harness.match.eventKinds] : void 0,
2684
+ folderIds: harness.match.folderIds ? [...harness.match.folderIds] : void 0,
2685
+ folderNames: harness.match.folderNames ? [...harness.match.folderNames] : void 0,
2686
+ meetingIds: harness.match.meetingIds ? [...harness.match.meetingIds] : void 0,
2687
+ recurringEventIds: harness.match.recurringEventIds ? [...harness.match.recurringEventIds] : void 0,
2688
+ tags: harness.match.tags ? [...harness.match.tags] : void 0,
2689
+ titleIncludes: harness.match.titleIncludes ? [...harness.match.titleIncludes] : void 0
2690
+ } : void 0
2691
+ };
2692
+ }
2693
+ function includesIgnoreCase$1(candidate, values) {
2694
+ const lowerCandidate = candidate.toLowerCase();
2695
+ return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
2696
+ }
2697
+ function harnessSpecificity(match) {
2698
+ if (!match) return 0;
2699
+ return [
2700
+ match.calendarEventIds?.length ?? 0,
2701
+ match.eventKinds?.length ?? 0,
2702
+ match.folderIds?.length ?? 0,
2703
+ match.folderNames?.length ?? 0,
2704
+ match.meetingIds?.length ?? 0,
2705
+ match.recurringEventIds?.length ?? 0,
2706
+ match.tags?.length ?? 0,
2707
+ match.titleIncludes?.length ?? 0,
2708
+ match.titleMatches ? 1 : 0,
2709
+ match.transcriptLoaded != null ? 1 : 0
2710
+ ].reduce((total, count) => total + count, 0);
2711
+ }
2712
+ function matchesHarness(harness, context) {
2713
+ const match = harness.match;
2714
+ if (!match) return true;
2715
+ if (match.eventKinds?.length && !match.eventKinds.includes(context.match.eventKind)) return false;
2716
+ if (match.meetingIds?.length && !match.meetingIds.includes(context.match.meetingId)) return false;
2717
+ if (match.folderIds?.length && !context.match.folders.some((folder) => match.folderIds?.includes(folder.id))) return false;
2718
+ if (match.folderNames?.length && !context.match.folders.some((folder) => match.folderNames?.includes(folder.name))) return false;
2719
+ if (match.tags?.length && !context.match.tags.some((tag) => match.tags?.includes(tag))) return false;
2720
+ if (match.transcriptLoaded != null && context.match.transcriptLoaded !== match.transcriptLoaded) return false;
2721
+ if (match.titleIncludes?.length && !includesIgnoreCase$1(context.match.title, match.titleIncludes)) return false;
2722
+ if (match.titleMatches) try {
2723
+ if (!new RegExp(match.titleMatches, "i").test(context.match.title)) return false;
2724
+ } catch {
2725
+ return false;
2726
+ }
2727
+ const calendarEventId = context.bundle?.document.calendarEvent?.id;
2728
+ if (match.calendarEventIds?.length && !calendarEventId) return false;
2729
+ if (match.calendarEventIds?.length && !match.calendarEventIds.includes(calendarEventId)) return false;
2730
+ const recurringEventId = context.bundle?.document.calendarEvent?.recurringEventId;
2731
+ if (match.recurringEventIds?.length && !recurringEventId) return false;
2732
+ if (match.recurringEventIds?.length && !match.recurringEventIds.includes(recurringEventId)) return false;
2733
+ return true;
2734
+ }
2735
+ function matchAgentHarnesses(harnesses, context) {
2736
+ return harnesses.filter((harness) => matchesHarness(harness, context)).slice().sort((left, right) => {
2737
+ const priority = (right.priority ?? 0) - (left.priority ?? 0);
2738
+ if (priority !== 0) return priority;
2739
+ return harnessSpecificity(right.match) - harnessSpecificity(left.match);
2740
+ }).map(cloneHarness);
2741
+ }
2742
+ function resolveAgentHarness(harnesses, context, harnessId) {
2743
+ if (harnessId?.trim()) {
2744
+ const harness = harnesses.find((candidate) => candidate.id === harnessId.trim());
2745
+ if (!harness) throw new Error(`agent harness not found: ${harnessId.trim()}`);
2746
+ return cloneHarness(harness);
2747
+ }
2748
+ return matchAgentHarnesses(harnesses, context)[0];
2749
+ }
2750
+ var FileAgentHarnessStore = class {
2751
+ constructor(filePath = defaultAgentHarnessesFilePath()) {
2752
+ this.filePath = filePath;
2753
+ }
2754
+ async readHarnesses() {
2755
+ try {
2756
+ const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
2757
+ return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.harnesses) ? parsed.harnesses : []).map((harness) => parseHarness(harness)).filter((harness) => Boolean(harness)).map(cloneHarness);
2758
+ } catch {
2759
+ return [];
2760
+ }
2761
+ }
2762
+ };
2763
+ function defaultAgentHarnessesFilePath() {
2764
+ return defaultGranolaToolkitPersistenceLayout().agentHarnessesFile;
2765
+ }
2766
+ function createDefaultAgentHarnessStore(filePath) {
2767
+ return new FileAgentHarnessStore(filePath);
2768
+ }
2769
+ //#endregion
2630
2770
  //#region src/client/auth.ts
2631
2771
  const execFileAsync$1 = promisify(execFile);
2632
2772
  const DEFAULT_CLIENT_ID = "client_GranolaMac";
@@ -3514,13 +3654,15 @@ function parseAction(value, index) {
3514
3654
  const provider = record.provider === "codex" || record.provider === "openai" || record.provider === "openrouter" ? record.provider : void 0;
3515
3655
  const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
3516
3656
  const promptFile = typeof record.promptFile === "string" && record.promptFile.trim() ? record.promptFile.trim() : void 0;
3657
+ const harnessId = typeof record.harnessId === "string" && record.harnessId.trim() ? record.harnessId.trim() : void 0;
3517
3658
  const systemPrompt = typeof record.systemPrompt === "string" && record.systemPrompt.trim() ? record.systemPrompt.trim() : void 0;
3518
3659
  const systemPromptFile = typeof record.systemPromptFile === "string" && record.systemPromptFile.trim() ? record.systemPromptFile.trim() : void 0;
3519
- if (!prompt && !promptFile) return;
3660
+ if (!prompt && !promptFile && !harnessId) return;
3520
3661
  return {
3521
3662
  cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
3522
3663
  dryRun: typeof record.dryRun === "boolean" ? record.dryRun : void 0,
3523
3664
  enabled,
3665
+ harnessId,
3524
3666
  id,
3525
3667
  kind,
3526
3668
  model: typeof record.model === "string" && record.model.trim() ? record.model.trim() : void 0,
@@ -3912,17 +4054,39 @@ function parseLastViewedPanel(value) {
3912
4054
  updatedAt: stringValue(panel.updated_at)
3913
4055
  };
3914
4056
  }
4057
+ function parseCalendarEvent(value) {
4058
+ const record = asRecord(value);
4059
+ if (!record) return;
4060
+ const id = stringValue(record.id);
4061
+ const recurringEventId = stringValue(record.recurring_event_id) || stringValue(record.recurringEventId);
4062
+ const calendarId = stringValue(record.calendar_id) || stringValue(record.calendarId);
4063
+ const url = stringValue(record.url);
4064
+ const htmlLink = stringValue(record.html_link) || stringValue(record.htmlLink);
4065
+ const startTime = stringValue(record.start_time) || stringValue(record.startTime);
4066
+ const endTime = stringValue(record.end_time) || stringValue(record.endTime);
4067
+ if (!id && !recurringEventId && !calendarId && !url && !htmlLink && !startTime && !endTime) return;
4068
+ return {
4069
+ calendarId: calendarId || void 0,
4070
+ endTime: endTime || void 0,
4071
+ htmlLink: htmlLink || void 0,
4072
+ id: id || void 0,
4073
+ recurringEventId: recurringEventId || void 0,
4074
+ startTime: startTime || void 0,
4075
+ url: url || void 0
4076
+ };
4077
+ }
3915
4078
  function parseDocument(value) {
3916
4079
  const record = asRecord(value);
3917
4080
  if (!record) throw new Error("document payload is not an object");
3918
4081
  return {
4082
+ calendarEvent: parseCalendarEvent(record.google_calendar_event),
3919
4083
  content: stringValue(record.content),
3920
4084
  createdAt: stringValue(record.created_at),
3921
4085
  id: stringValue(record.id),
3922
4086
  lastViewedPanel: parseLastViewedPanel(record.last_viewed_panel),
3923
4087
  notes: parseProseMirrorDoc(record.notes),
3924
4088
  notesPlain: stringValue(record.notes_plain),
3925
- tags: stringArray$1(record.tags),
4089
+ tags: stringArray$2(record.tags),
3926
4090
  title: stringValue(record.title),
3927
4091
  updatedAt: stringValue(record.updated_at)
3928
4092
  };
@@ -3973,6 +4137,7 @@ function parsePublicNote(value) {
3973
4137
  const summaryMarkdown = stringValue(record.summary_markdown);
3974
4138
  const summaryText = stringValue(record.summary_text);
3975
4139
  return {
4140
+ calendarEvent: parseCalendarEvent(record.google_calendar_event),
3976
4141
  content: summaryMarkdown || summaryText,
3977
4142
  createdAt: stringValue(record.created_at),
3978
4143
  folderMemberships: Array.isArray(record.folder_membership) ? record.folder_membership.map(parseFolderMembership).filter((membership) => Boolean(membership)) : [],
@@ -4890,6 +5055,10 @@ function cloneMeetingSummary(meeting) {
4890
5055
  function resolveActionFilePath(filePath, cwd) {
4891
5056
  return cwd ? resolve(cwd, filePath) : resolve(filePath);
4892
5057
  }
5058
+ async function readOptionalActionFile(filePath, cwd) {
5059
+ if (!filePath) return;
5060
+ return await readFile(resolveActionFilePath(filePath, cwd), "utf8");
5061
+ }
4893
5062
  function combinePromptSections(...values) {
4894
5063
  const sections = values.map((value) => value?.trim()).filter((value) => Boolean(value));
4895
5064
  return sections.length > 0 ? sections.join("\n\n") : void 0;
@@ -5689,18 +5858,25 @@ var GranolaApp = class {
5689
5858
  }
5690
5859
  async runAutomationAgent(match, rule, action) {
5691
5860
  const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
5692
- const promptFile = action.promptFile ? await readFile(resolveActionFilePath(action.promptFile, action.cwd), "utf8") : void 0;
5693
- const systemPromptFile = action.systemPromptFile ? await readFile(resolveActionFilePath(action.systemPromptFile, action.cwd), "utf8") : void 0;
5694
- const instructions = combinePromptSections(promptFile, action.prompt);
5861
+ const harness = resolveAgentHarness(this.deps.agentHarnessStore ? await this.deps.agentHarnessStore.readHarnesses() : [], {
5862
+ bundle,
5863
+ match
5864
+ }, action.harnessId);
5865
+ const harnessCwd = harness?.cwd;
5866
+ const promptFile = await readOptionalActionFile(action.promptFile, action.cwd ?? harnessCwd);
5867
+ const harnessPromptFile = await readOptionalActionFile(harness?.promptFile, harnessCwd);
5868
+ const systemPromptFile = await readOptionalActionFile(action.systemPromptFile, action.cwd ?? harnessCwd);
5869
+ const harnessSystemPromptFile = await readOptionalActionFile(harness?.systemPromptFile, harnessCwd);
5870
+ const instructions = combinePromptSections(harnessPromptFile, harness?.prompt, promptFile, action.prompt);
5695
5871
  if (!instructions) throw new Error(`automation agent action ${action.id} is missing prompt instructions`);
5696
5872
  const request = {
5697
- cwd: action.cwd,
5873
+ cwd: action.cwd ?? harnessCwd,
5698
5874
  dryRun: action.dryRun,
5699
- model: action.model,
5875
+ model: action.model ?? harness?.model,
5700
5876
  prompt: buildAutomationAgentPrompt(match, rule, instructions, bundle),
5701
- provider: action.provider,
5877
+ provider: action.provider ?? harness?.provider,
5702
5878
  retries: action.retries,
5703
- systemPrompt: combinePromptSections(systemPromptFile, action.systemPrompt),
5879
+ systemPrompt: combinePromptSections(harnessSystemPromptFile, harness?.systemPrompt, systemPromptFile, action.systemPrompt),
5704
5880
  timeoutMs: action.timeoutMs
5705
5881
  };
5706
5882
  return await (this.deps.agentRunner ?? createDefaultAutomationAgentRunner(this.config)).run(request);
@@ -6147,6 +6323,7 @@ async function createGranolaApp(config, options = {}) {
6147
6323
  const automationRuns = await automationRunStore.readRuns({ limit: 0 });
6148
6324
  const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
6149
6325
  const automationRules = await automationRuleStore.readRules();
6326
+ const agentHarnessStore = createDefaultAgentHarnessStore(config.agents?.harnessesFile);
6150
6327
  const authController = createDefaultGranolaAuthController(config);
6151
6328
  const exportJobStore = createDefaultExportJobStore();
6152
6329
  const exportJobs = await exportJobStore.readJobs();
@@ -6160,6 +6337,7 @@ async function createGranolaApp(config, options = {}) {
6160
6337
  return new GranolaApp(config, {
6161
6338
  auth,
6162
6339
  agentRunner: createDefaultAutomationAgentRunner(config),
6340
+ agentHarnessStore,
6163
6341
  authController,
6164
6342
  automationMatches,
6165
6343
  automationMatchStore,
@@ -6257,6 +6435,7 @@ async function loadConfig(options) {
6257
6435
  return value === "codex" || value === "openai" || value === "openrouter" ? value : void 0;
6258
6436
  })(),
6259
6437
  dryRun: envFlag(env.GRANOLA_AGENT_DRY_RUN) ?? pickBoolean(configValues["agent-dry-run"]) ?? pickBoolean(configValues.agentDryRun) ?? false,
6438
+ harnessesFile: pickString(env.GRANOLA_AGENT_HARNESSES_FILE) ?? pickString(configValues["agent-harnesses-file"]) ?? pickString(configValues.agentHarnessesFile) ?? defaultGranolaToolkitPersistenceLayout().agentHarnessesFile,
6260
6439
  maxRetries: pickNumber(env.GRANOLA_AGENT_MAX_RETRIES) ?? pickNumber(configValues["agent-max-retries"]) ?? pickNumber(configValues.agentMaxRetries) ?? 2,
6261
6440
  openaiBaseUrl: pickString(env.GRANOLA_OPENAI_BASE_URL) ?? pickString(env.OPENAI_BASE_URL) ?? pickString(configValues["openai-base-url"]) ?? pickString(configValues.openaiBaseUrl) ?? "https://api.openai.com/v1",
6262
6441
  openrouterBaseUrl: pickString(env.GRANOLA_OPENROUTER_BASE_URL) ?? pickString(env.OPENROUTER_BASE_URL) ?? pickString(configValues["openrouter-base-url"]) ?? pickString(configValues.openrouterBaseUrl) ?? "https://openrouter.ai/api/v1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",