granola-toolkit 0.51.0 → 0.52.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 +403 -39
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3745,6 +3745,7 @@ function cloneAction$1(action) {
3745
3745
  switch (action.kind) {
3746
3746
  case "agent": return {
3747
3747
  ...action,
3748
+ approvalMode: action.approvalMode,
3748
3749
  fallbackHarnessIds: action.fallbackHarnessIds ? [...action.fallbackHarnessIds] : void 0,
3749
3750
  pipeline: action.pipeline ? { ...action.pipeline } : void 0
3750
3751
  };
@@ -3756,23 +3757,50 @@ function cloneAction$1(action) {
3756
3757
  };
3757
3758
  case "export-notes":
3758
3759
  case "export-transcript": return { ...action };
3760
+ case "slack-message": return { ...action };
3761
+ case "webhook": return {
3762
+ ...action,
3763
+ headers: action.headers ? { ...action.headers } : void 0
3764
+ };
3765
+ case "write-file": return { ...action };
3759
3766
  }
3760
3767
  }
3761
3768
  function automationActionName(action) {
3762
3769
  return action.name || action.id;
3763
3770
  }
3771
+ function automationActionTrigger(action) {
3772
+ switch (action.kind) {
3773
+ case "command":
3774
+ case "slack-message":
3775
+ case "webhook":
3776
+ case "write-file": return action.trigger ?? "match";
3777
+ default: return "match";
3778
+ }
3779
+ }
3764
3780
  function buildAutomationActionRunId(match, actionId) {
3765
3781
  return `${match.id}:${actionId}`;
3766
3782
  }
3767
- function enabledAutomationActions(rule) {
3768
- return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
3783
+ function buildAutomationApprovalActionRunId(artefact, actionId) {
3784
+ return `approval:${artefact.id}:${actionId}`;
3785
+ }
3786
+ function enabledAutomationActions(rule, options = {}) {
3787
+ return (rule.actions ?? []).filter((action) => action.enabled !== false).filter((action) => automationActionTrigger(action) === (options.trigger ?? "match")).filter((action) => {
3788
+ if (options.trigger !== "approval") return true;
3789
+ switch (action.kind) {
3790
+ case "command":
3791
+ case "slack-message":
3792
+ case "webhook":
3793
+ case "write-file": return !options.sourceActionId || action.sourceActionId === options.sourceActionId;
3794
+ default: return false;
3795
+ }
3796
+ }).map((action) => cloneAction$1(action));
3769
3797
  }
3770
- function baseRun(match, rule, action, startedAt, options = {}) {
3798
+ function baseRun(match, rule, action, startedAt, context, options = {}) {
3771
3799
  return {
3772
3800
  actionId: action.id,
3773
3801
  actionKind: action.kind,
3774
3802
  actionName: automationActionName(action),
3775
- artefactIds: void 0,
3803
+ artefactIds: context.artefact ? [context.artefact.id] : void 0,
3776
3804
  eventId: match.eventId,
3777
3805
  eventKind: match.eventKind,
3778
3806
  folders: match.folders.map((folder) => ({ ...folder })),
@@ -3780,6 +3808,10 @@ function baseRun(match, rule, action, startedAt, options = {}) {
3780
3808
  matchId: match.id,
3781
3809
  matchedAt: match.matchedAt,
3782
3810
  meetingId: match.meetingId,
3811
+ meta: {
3812
+ sourceActionId: action.kind === "command" || action.kind === "slack-message" || action.kind === "webhook" || action.kind === "write-file" ? action.sourceActionId : void 0,
3813
+ trigger: context.trigger
3814
+ },
3783
3815
  ruleId: rule.id,
3784
3816
  ruleName: rule.name,
3785
3817
  rerunOfId: options.rerunOfId,
@@ -3815,7 +3847,8 @@ function skippedRun(run, finishedAt, reason) {
3815
3847
  };
3816
3848
  }
3817
3849
  async function executeAutomationAction(match, rule, action, handlers, options = {}) {
3818
- const run = baseRun(match, rule, action, handlers.nowIso(), options);
3850
+ const context = options.context ?? { trigger: automationActionTrigger(action) };
3851
+ const run = baseRun(match, rule, action, handlers.nowIso(), context, options);
3819
3852
  switch (action.kind) {
3820
3853
  case "agent": try {
3821
3854
  const result = await handlers.runAgent(match, rule, action, run);
@@ -3845,9 +3878,10 @@ async function executeAutomationAction(match, rule, action, handlers, options =
3845
3878
  status: "pending"
3846
3879
  };
3847
3880
  case "command": try {
3848
- const result = await handlers.runCommand(match, rule, action);
3881
+ const result = await handlers.runCommand(match, rule, action, context);
3849
3882
  return completedRun(run, handlers.nowIso(), {
3850
3883
  meta: {
3884
+ ...run.meta ? structuredClone(run.meta) : {},
3851
3885
  command: result.command,
3852
3886
  cwd: result.cwd
3853
3887
  },
@@ -3886,8 +3920,162 @@ async function executeAutomationAction(match, rule, action, handlers, options =
3886
3920
  } catch (error) {
3887
3921
  return failedRun(run, handlers.nowIso(), error);
3888
3922
  }
3923
+ case "slack-message": try {
3924
+ const result = await handlers.runSlackMessage(match, rule, action, context);
3925
+ return completedRun(run, handlers.nowIso(), {
3926
+ meta: {
3927
+ ...run.meta ? structuredClone(run.meta) : {},
3928
+ status: result.status,
3929
+ text: result.text,
3930
+ url: result.url
3931
+ },
3932
+ result: result.output ?? `Posted Slack message (${result.status})`
3933
+ });
3934
+ } catch (error) {
3935
+ return failedRun(run, handlers.nowIso(), error);
3936
+ }
3937
+ case "webhook": try {
3938
+ const result = await handlers.runWebhook(match, rule, action, context);
3939
+ return completedRun(run, handlers.nowIso(), {
3940
+ meta: {
3941
+ ...run.meta ? structuredClone(run.meta) : {},
3942
+ status: result.status,
3943
+ url: result.url
3944
+ },
3945
+ result: result.output ?? `Posted webhook (${result.status})`
3946
+ });
3947
+ } catch (error) {
3948
+ return failedRun(run, handlers.nowIso(), error);
3949
+ }
3950
+ case "write-file": try {
3951
+ const result = await handlers.writeFile(match, rule, action, context);
3952
+ return completedRun(run, handlers.nowIso(), {
3953
+ meta: {
3954
+ ...run.meta ? structuredClone(run.meta) : {},
3955
+ bytes: result.bytes,
3956
+ filePath: result.filePath,
3957
+ format: result.format
3958
+ },
3959
+ result: `Wrote ${result.format} file to ${result.filePath}`
3960
+ });
3961
+ } catch (error) {
3962
+ return failedRun(run, handlers.nowIso(), error);
3963
+ }
3964
+ }
3965
+ }
3966
+ //#endregion
3967
+ //#region src/automation-delivery.ts
3968
+ function getTemplateValue(record, path) {
3969
+ let current = record;
3970
+ for (const segment of path.split(".")) {
3971
+ if (!segment) return;
3972
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
3973
+ current = current[segment];
3974
+ }
3975
+ return current;
3976
+ }
3977
+ function templateValueAsString(value) {
3978
+ if (value == null) return "";
3979
+ if (typeof value === "string") return value;
3980
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3981
+ return JSON.stringify(value);
3982
+ }
3983
+ function renderAutomationTemplate(template, payload) {
3984
+ return template.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_, path) => templateValueAsString(getTemplateValue(payload, path)));
3985
+ }
3986
+ function defaultMeetingTitle(bundle, match) {
3987
+ return bundle?.meeting.meeting.title || bundle?.document.title || match.title;
3988
+ }
3989
+ function buildAutomationDeliveryPayload(context) {
3990
+ return {
3991
+ action: {
3992
+ id: context.action.id,
3993
+ kind: context.action.kind,
3994
+ name: context.action.name || context.action.id
3995
+ },
3996
+ approval: context.trigger === "approval" && context.artefact && context.decision ? {
3997
+ artefactId: context.artefact.id,
3998
+ decidedAt: context.generatedAt,
3999
+ decision: context.decision,
4000
+ note: context.note?.trim() || void 0
4001
+ } : void 0,
4002
+ artefact: context.artefact ? {
4003
+ actionId: context.artefact.actionId,
4004
+ id: context.artefact.id,
4005
+ kind: context.artefact.kind,
4006
+ markdown: context.artefact.structured.markdown,
4007
+ metadata: context.artefact.structured.metadata,
4008
+ model: context.artefact.model,
4009
+ prompt: context.artefact.prompt,
4010
+ provider: context.artefact.provider,
4011
+ status: context.artefact.status,
4012
+ summary: context.artefact.structured.summary,
4013
+ title: context.artefact.structured.title
4014
+ } : void 0,
4015
+ generatedAt: context.generatedAt,
4016
+ match: {
4017
+ ...context.match,
4018
+ folders: context.match.folders.map((folder) => ({ ...folder })),
4019
+ tags: [...context.match.tags]
4020
+ },
4021
+ meeting: context.bundle ? {
4022
+ document: context.bundle.document,
4023
+ id: context.bundle.document.id,
4024
+ meeting: context.bundle.meeting,
4025
+ title: defaultMeetingTitle(context.bundle, context.match)
4026
+ } : void 0,
4027
+ rule: {
4028
+ id: context.rule.id,
4029
+ name: context.rule.name
4030
+ }
4031
+ };
4032
+ }
4033
+ function defaultDeliveryText(payload) {
4034
+ if (payload.artefact?.summary?.trim()) return payload.artefact.summary.trim();
4035
+ if (payload.artefact?.markdown?.trim()) return payload.artefact.markdown.trim();
4036
+ return `${payload.action.name} for ${payload.meeting?.title || payload.match.title}`;
4037
+ }
4038
+ function renderSlackMessageText(action, payload) {
4039
+ const text = action.text?.trim();
4040
+ if (text) return renderAutomationTemplate(text, payload).trim();
4041
+ return defaultDeliveryText(payload);
4042
+ }
4043
+ function renderWebhookBody(action, payload) {
4044
+ const format = action.payload ?? "json";
4045
+ if (format === "json") return {
4046
+ body: JSON.stringify(payload, null, 2),
4047
+ contentType: "application/json"
4048
+ };
4049
+ const template = action.bodyTemplate?.trim();
4050
+ return {
4051
+ body: template ? renderAutomationTemplate(template, payload).trim() : defaultDeliveryText(payload),
4052
+ contentType: format === "markdown" ? "text/markdown; charset=utf-8" : "text/plain; charset=utf-8"
4053
+ };
4054
+ }
4055
+ function defaultWriteFileExtension(format) {
4056
+ switch (format) {
4057
+ case "json": return "json";
4058
+ case "text": return "txt";
4059
+ default: return "md";
3889
4060
  }
3890
4061
  }
4062
+ function renderWriteFileName(action, payload) {
4063
+ const format = action.format ?? "markdown";
4064
+ const template = action.filenameTemplate?.trim();
4065
+ if (template) return sanitiseFilename(renderAutomationTemplate(template, payload), payload.action.id);
4066
+ return `${sanitiseFilename(`${payload.meeting?.title || payload.match.title}-${payload.artefact?.kind || payload.action.id}`)}.${defaultWriteFileExtension(format)}`;
4067
+ }
4068
+ function resolveWriteFilePath(action, payload) {
4069
+ return resolve(action.outputDir, renderWriteFileName(action, payload));
4070
+ }
4071
+ function renderWriteFileContent(action, payload) {
4072
+ const format = action.format ?? "markdown";
4073
+ const template = action.contentTemplate?.trim();
4074
+ if (template) return renderAutomationTemplate(template, payload);
4075
+ if (format === "json") return JSON.stringify(payload, null, 2);
4076
+ if (format === "text") return `${defaultDeliveryText(payload)}\n`;
4077
+ return `${payload.artefact?.markdown?.trim() || defaultDeliveryText(payload)}\n`;
4078
+ }
3891
4079
  //#endregion
3892
4080
  //#region src/automation-matches.ts
3893
4081
  function cloneMatch(match) {
@@ -3996,6 +4184,7 @@ function cloneAction(action) {
3996
4184
  switch (action.kind) {
3997
4185
  case "agent": return {
3998
4186
  ...action,
4187
+ approvalMode: action.approvalMode,
3999
4188
  fallbackHarnessIds: action.fallbackHarnessIds ? [...action.fallbackHarnessIds] : void 0,
4000
4189
  pipeline: action.pipeline ? { ...action.pipeline } : void 0
4001
4190
  };
@@ -4007,6 +4196,12 @@ function cloneAction(action) {
4007
4196
  };
4008
4197
  case "export-notes":
4009
4198
  case "export-transcript": return { ...action };
4199
+ case "slack-message": return { ...action };
4200
+ case "webhook": return {
4201
+ ...action,
4202
+ headers: action.headers ? { ...action.headers } : void 0
4203
+ };
4204
+ case "write-file": return { ...action };
4010
4205
  }
4011
4206
  }
4012
4207
  function stringArray(value) {
@@ -4027,6 +4222,9 @@ function parsePipeline(value) {
4027
4222
  const kind = record ? stringValue(record.kind).trim() : typeof value === "string" ? value.trim() : "";
4028
4223
  return kind === "enrichment" || kind === "notes" ? { kind } : void 0;
4029
4224
  }
4225
+ function parseTrigger(value) {
4226
+ if (value === "approval" || value === "match") return value;
4227
+ }
4030
4228
  function parseAction(value, index) {
4031
4229
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
4032
4230
  const record = value;
@@ -4046,6 +4244,7 @@ function parseAction(value, index) {
4046
4244
  const systemPromptFile = typeof record.systemPromptFile === "string" && record.systemPromptFile.trim() ? record.systemPromptFile.trim() : void 0;
4047
4245
  if (!prompt && !promptFile && !harnessId) return;
4048
4246
  return {
4247
+ approvalMode: record.approvalMode === "auto" || record.approvalMode === "manual" ? record.approvalMode : void 0,
4049
4248
  cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
4050
4249
  dryRun: typeof record.dryRun === "boolean" ? record.dryRun : void 0,
4051
4250
  enabled,
@@ -4089,8 +4288,10 @@ function parseAction(value, index) {
4089
4288
  id,
4090
4289
  kind,
4091
4290
  name,
4291
+ sourceActionId: typeof record.sourceActionId === "string" && record.sourceActionId.trim() ? record.sourceActionId.trim() : void 0,
4092
4292
  stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
4093
- timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
4293
+ timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0,
4294
+ trigger: parseTrigger(record.trigger)
4094
4295
  };
4095
4296
  }
4096
4297
  case "export-notes":
@@ -4115,6 +4316,52 @@ function parseAction(value, index) {
4115
4316
  outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
4116
4317
  scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
4117
4318
  };
4319
+ case "slack-message":
4320
+ if (!id) return;
4321
+ return {
4322
+ enabled,
4323
+ id,
4324
+ kind,
4325
+ name,
4326
+ sourceActionId: typeof record.sourceActionId === "string" && record.sourceActionId.trim() ? record.sourceActionId.trim() : void 0,
4327
+ text: typeof record.text === "string" && record.text.trim() ? record.text.trim() : void 0,
4328
+ trigger: parseTrigger(record.trigger),
4329
+ webhookUrl: typeof record.webhookUrl === "string" && record.webhookUrl.trim() ? record.webhookUrl.trim() : void 0,
4330
+ webhookUrlEnv: typeof record.webhookUrlEnv === "string" && record.webhookUrlEnv.trim() ? record.webhookUrlEnv.trim() : void 0
4331
+ };
4332
+ case "webhook":
4333
+ if (!id) return;
4334
+ return {
4335
+ bodyTemplate: typeof record.bodyTemplate === "string" && record.bodyTemplate.trim() ? record.bodyTemplate.trim() : void 0,
4336
+ enabled,
4337
+ headers: stringRecord(record.headers),
4338
+ id,
4339
+ kind,
4340
+ method: typeof record.method === "string" && record.method.trim() ? record.method.trim().toUpperCase() : void 0,
4341
+ name,
4342
+ payload: record.payload === "json" || record.payload === "markdown" || record.payload === "text" ? record.payload : void 0,
4343
+ sourceActionId: typeof record.sourceActionId === "string" && record.sourceActionId.trim() ? record.sourceActionId.trim() : void 0,
4344
+ trigger: parseTrigger(record.trigger),
4345
+ url: typeof record.url === "string" && record.url.trim() ? record.url.trim() : void 0,
4346
+ urlEnv: typeof record.urlEnv === "string" && record.urlEnv.trim() ? record.urlEnv.trim() : void 0
4347
+ };
4348
+ case "write-file": {
4349
+ const outputDir = typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0;
4350
+ if (!id || !outputDir) return;
4351
+ return {
4352
+ contentTemplate: typeof record.contentTemplate === "string" && record.contentTemplate.trim() ? record.contentTemplate.trim() : void 0,
4353
+ enabled,
4354
+ filenameTemplate: typeof record.filenameTemplate === "string" && record.filenameTemplate.trim() ? record.filenameTemplate.trim() : void 0,
4355
+ format: record.format === "json" || record.format === "markdown" || record.format === "text" ? record.format : void 0,
4356
+ id,
4357
+ kind,
4358
+ name,
4359
+ outputDir,
4360
+ overwrite: typeof record.overwrite === "boolean" ? record.overwrite : void 0,
4361
+ sourceActionId: typeof record.sourceActionId === "string" && record.sourceActionId.trim() ? record.sourceActionId.trim() : void 0,
4362
+ trigger: parseTrigger(record.trigger)
4363
+ };
4364
+ }
4118
4365
  default: return;
4119
4366
  }
4120
4367
  }
@@ -6017,6 +6264,12 @@ var GranolaApp = class {
6017
6264
  };
6018
6265
  case "export-notes":
6019
6266
  case "export-transcript": return { ...action };
6267
+ case "slack-message": return { ...action };
6268
+ case "webhook": return {
6269
+ ...action,
6270
+ headers: action.headers ? { ...action.headers } : void 0
6271
+ };
6272
+ case "write-file": return { ...action };
6020
6273
  }
6021
6274
  }),
6022
6275
  when: {
@@ -6145,7 +6398,10 @@ var GranolaApp = class {
6145
6398
  exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
6146
6399
  nowIso: () => this.nowIso(),
6147
6400
  runAgent: async (nextMatch, nextRule, nextAction, run) => await this.runAutomationAgent(nextMatch, nextRule, nextAction, run),
6148
- runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
6401
+ runCommand: async (nextMatch, nextRule, nextAction, context) => await this.runAutomationCommand(nextMatch, nextRule, nextAction, context),
6402
+ runSlackMessage: async (nextMatch, nextRule, nextAction, context) => await this.runAutomationSlackMessage(nextMatch, nextRule, nextAction, context),
6403
+ runWebhook: async (nextMatch, nextRule, nextAction, context) => await this.runAutomationWebhook(nextMatch, nextRule, nextAction, context),
6404
+ writeFile: async (nextMatch, nextRule, nextAction, context) => await this.runAutomationWriteFile(nextMatch, nextRule, nextAction, context)
6149
6405
  };
6150
6406
  }
6151
6407
  async currentMeetingSummariesForProcessing() {
@@ -6482,17 +6738,60 @@ var GranolaApp = class {
6482
6738
  this.emitStateUpdate();
6483
6739
  return this.cloneAutomationRun(resolved);
6484
6740
  }
6741
+ async readAutomationMatchById(id) {
6742
+ return (this.deps.automationMatchStore ? (await this.deps.automationMatchStore.readMatches(0)).find((candidate) => candidate.id === id) : void 0) ?? this.#automationMatches.find((candidate) => candidate.id === id);
6743
+ }
6744
+ pipelineApprovalMode(action) {
6745
+ return action.approvalMode ?? "manual";
6746
+ }
6747
+ async runPostApprovalActions(artefact, options) {
6748
+ if (options.decision !== "approve") return [];
6749
+ const rule = (await this.loadAutomationRules({ forceRefresh: true })).find((candidate) => candidate.id === artefact.ruleId);
6750
+ if (!rule) return [];
6751
+ const match = await this.readAutomationMatchById(artefact.matchId);
6752
+ if (!match) return [];
6753
+ const actions = enabledAutomationActions(rule, {
6754
+ sourceActionId: artefact.actionId,
6755
+ trigger: "approval"
6756
+ });
6757
+ if (actions.length === 0) return [];
6758
+ const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
6759
+ const runs = [];
6760
+ for (const action of actions) {
6761
+ const runId = buildAutomationApprovalActionRunId(artefact, action.id);
6762
+ if (existingRunIds.has(runId)) continue;
6763
+ existingRunIds.add(runId);
6764
+ runs.push(await executeAutomationAction(this.cloneAutomationMatch(match), rule, action, this.automationActionHandlers(), {
6765
+ context: {
6766
+ artefact: this.cloneAutomationArtefact(artefact),
6767
+ decision: options.decision,
6768
+ note: options.note,
6769
+ trigger: "approval"
6770
+ },
6771
+ runId
6772
+ }));
6773
+ }
6774
+ await this.appendAutomationRuns(runs);
6775
+ this.emitStateUpdate();
6776
+ return runs.map((run) => this.cloneAutomationRun(run));
6777
+ }
6485
6778
  async resolveAutomationArtefact(id, decision, options = {}) {
6486
6779
  const current = await this.readAutomationArtefactById(id);
6487
6780
  if (!current) throw new Error(`automation artefact not found: ${id}`);
6488
6781
  this.assertMutableAutomationArtefact(current);
6782
+ const shouldRunPostApproval = decision === "approve" && current.status !== "approved";
6489
6783
  const nextArtefact = {
6490
6784
  ...this.cloneAutomationArtefact(current),
6491
6785
  history: [...current.history.map((entry) => ({ ...entry })), this.buildAutomationArtefactHistoryEntry(decision === "approve" ? "approved" : "rejected", options.note)],
6492
6786
  status: decision === "approve" ? "approved" : "rejected",
6493
6787
  updatedAt: this.nowIso()
6494
6788
  };
6495
- return await this.replaceAutomationArtefact(nextArtefact);
6789
+ const replaced = await this.replaceAutomationArtefact(nextArtefact);
6790
+ if (shouldRunPostApproval) await this.runPostApprovalActions(replaced, {
6791
+ decision,
6792
+ note: options.note
6793
+ });
6794
+ return replaced;
6496
6795
  }
6497
6796
  async updateAutomationArtefact(id, patch) {
6498
6797
  const current = await this.readAutomationArtefactById(id);
@@ -6589,13 +6888,7 @@ var GranolaApp = class {
6589
6888
  if (!action || action.kind !== "agent" || !action.pipeline) throw new Error(`automation artefact is not rerunnable: ${id}`);
6590
6889
  const match = (this.deps.automationMatchStore ? (await this.deps.automationMatchStore.readMatches(0)).find((candidate) => candidate.id === current.matchId) : void 0) ?? this.#automationMatches.find((candidate) => candidate.id === current.matchId);
6591
6890
  if (!match) throw new Error(`automation match not found: ${current.matchId}`);
6592
- const nextRun = await executeAutomationAction(this.cloneAutomationMatch(match), rule, action, {
6593
- exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
6594
- exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
6595
- nowIso: () => this.nowIso(),
6596
- runAgent: async (nextMatch, nextRule, nextAction, run) => await this.runAutomationAgent(nextMatch, nextRule, nextAction, run),
6597
- runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
6598
- }, {
6891
+ const nextRun = await executeAutomationAction(this.cloneAutomationMatch(match), rule, action, this.automationActionHandlers(), {
6599
6892
  rerunOfId: current.runId,
6600
6893
  runId: `${current.runId}:rerun:${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`
6601
6894
  });
@@ -6720,26 +7013,33 @@ var GranolaApp = class {
6720
7013
  written: result.written
6721
7014
  };
6722
7015
  }
6723
- async runAutomationCommand(match, rule, action) {
6724
- const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
7016
+ async buildAutomationExecutionBundle(match) {
7017
+ if (match.eventKind === "meeting.removed") return;
7018
+ return await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
7019
+ }
7020
+ buildAutomationDeliveryPayloadForAction(match, rule, action, context, bundle) {
7021
+ return buildAutomationDeliveryPayload({
7022
+ action,
7023
+ artefact: context.artefact ? this.cloneAutomationArtefact(context.artefact) : void 0,
7024
+ bundle,
7025
+ decision: context.decision,
7026
+ generatedAt: this.nowIso(),
7027
+ match: this.cloneAutomationMatch(match),
7028
+ note: context.note,
7029
+ rule,
7030
+ trigger: context.trigger
7031
+ });
7032
+ }
7033
+ async runAutomationCommand(match, rule, action, context) {
7034
+ const bundle = await this.buildAutomationExecutionBundle(match);
6725
7035
  const cwd = action.cwd ? resolve(action.cwd) : process.cwd();
6726
7036
  const payload = JSON.stringify({
6727
- action: {
7037
+ ...this.buildAutomationDeliveryPayloadForAction(match, rule, {
6728
7038
  id: action.id,
6729
7039
  kind: "command",
6730
7040
  name: automationActionName(action)
6731
- },
6732
- authMode: this.#state.auth.mode,
6733
- generatedAt: this.nowIso(),
6734
- match: this.cloneAutomationMatch(match),
6735
- meeting: bundle ? {
6736
- document: bundle.document,
6737
- meeting: bundle.meeting
6738
- } : void 0,
6739
- rule: {
6740
- id: rule.id,
6741
- name: rule.name
6742
- }
7041
+ }, context, bundle),
7042
+ authMode: this.#state.auth.mode
6743
7043
  }, null, 2);
6744
7044
  return await new Promise((resolve, reject) => {
6745
7045
  const child = spawn(action.command, action.args ?? [], {
@@ -6748,6 +7048,9 @@ var GranolaApp = class {
6748
7048
  ...process.env,
6749
7049
  ...action.env,
6750
7050
  GRANOLA_ACTION_KIND: "command",
7051
+ GRANOLA_ACTION_TRIGGER: context.trigger,
7052
+ GRANOLA_APPROVAL_DECISION: context.decision,
7053
+ GRANOLA_ARTEFACT_ID: context.artefact?.id,
6751
7054
  GRANOLA_EVENT_ID: match.eventId,
6752
7055
  GRANOLA_EVENT_KIND: match.eventKind,
6753
7056
  GRANOLA_MATCH_ID: match.id,
@@ -6800,6 +7103,73 @@ var GranolaApp = class {
6800
7103
  child.stdin.end();
6801
7104
  });
6802
7105
  }
7106
+ async runAutomationWebhook(match, rule, action, context) {
7107
+ const bundle = await this.buildAutomationExecutionBundle(match);
7108
+ const payload = this.buildAutomationDeliveryPayloadForAction(match, rule, {
7109
+ id: action.id,
7110
+ kind: "webhook",
7111
+ name: automationActionName(action)
7112
+ }, context, bundle);
7113
+ const url = action.url?.trim() || (action.urlEnv ? process.env[action.urlEnv]?.trim() : "");
7114
+ if (!url) throw new Error(`automation webhook action ${action.id} is missing a URL`);
7115
+ const rendered = renderWebhookBody(action, payload);
7116
+ const response = await fetch(url, {
7117
+ body: rendered.body,
7118
+ headers: {
7119
+ "content-type": rendered.contentType,
7120
+ ...action.headers
7121
+ },
7122
+ method: action.method ?? "POST"
7123
+ });
7124
+ const output = (await response.text()).trim() || void 0;
7125
+ if (!response.ok) throw new Error(output || `automation webhook failed with status ${response.status}`);
7126
+ return {
7127
+ output,
7128
+ status: response.status,
7129
+ url
7130
+ };
7131
+ }
7132
+ async runAutomationSlackMessage(match, rule, action, context) {
7133
+ const bundle = await this.buildAutomationExecutionBundle(match);
7134
+ const payload = this.buildAutomationDeliveryPayloadForAction(match, rule, {
7135
+ id: action.id,
7136
+ kind: "slack-message",
7137
+ name: automationActionName(action)
7138
+ }, context, bundle);
7139
+ const url = action.webhookUrl?.trim() || (action.webhookUrlEnv ? process.env[action.webhookUrlEnv]?.trim() : process.env.SLACK_WEBHOOK_URL?.trim());
7140
+ if (!url) throw new Error(`automation Slack action ${action.id} is missing a webhook URL`);
7141
+ const text = renderSlackMessageText(action, payload);
7142
+ const response = await fetch(url, {
7143
+ body: JSON.stringify({ text }),
7144
+ headers: { "content-type": "application/json" },
7145
+ method: "POST"
7146
+ });
7147
+ const output = (await response.text()).trim() || void 0;
7148
+ if (!response.ok) throw new Error(output || `automation Slack action failed with status ${response.status}`);
7149
+ return {
7150
+ output,
7151
+ status: response.status,
7152
+ text,
7153
+ url
7154
+ };
7155
+ }
7156
+ async runAutomationWriteFile(match, rule, action, context) {
7157
+ const bundle = await this.buildAutomationExecutionBundle(match);
7158
+ const payload = this.buildAutomationDeliveryPayloadForAction(match, rule, {
7159
+ id: action.id,
7160
+ kind: "write-file",
7161
+ name: automationActionName(action)
7162
+ }, context, bundle);
7163
+ const filePath = resolveWriteFilePath(action, payload);
7164
+ if (existsSync(filePath) && action.overwrite === false) throw new Error(`automation write-file target already exists: ${filePath}`);
7165
+ const content = renderWriteFileContent(action, payload);
7166
+ await writeTextFile(filePath, content);
7167
+ return {
7168
+ bytes: Buffer.byteLength(content, "utf8"),
7169
+ filePath,
7170
+ format: action.format ?? "markdown"
7171
+ };
7172
+ }
6803
7173
  async buildAutomationAgentAttempt(match, rule, action, bundle, harness) {
6804
7174
  const harnessCwd = harness?.cwd;
6805
7175
  const promptFile = await readOptionalActionFile(action.promptFile, action.cwd ?? harnessCwd);
@@ -6887,7 +7257,7 @@ var GranolaApp = class {
6887
7257
  };
6888
7258
  await this.writeAutomationArtefacts([artefact, ...this.#automationArtefacts]);
6889
7259
  return {
6890
- artefactIds: [artefact.id],
7260
+ artefactIds: [(this.pipelineApprovalMode(action) === "auto" ? await this.resolveAutomationArtefact(artefact.id, "approve", { note: "Auto-approved by automation rule" }) : artefact).id],
6891
7261
  attempts: attemptMeta,
6892
7262
  command: result.command,
6893
7263
  dryRun: result.dryRun,
@@ -6931,13 +7301,7 @@ var GranolaApp = class {
6931
7301
  const runId = buildAutomationActionRunId(match, action.id);
6932
7302
  if (existingRunIds.has(runId)) continue;
6933
7303
  existingRunIds.add(runId);
6934
- runs.push(await executeAutomationAction(match, rule, action, {
6935
- exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
6936
- exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
6937
- nowIso: () => this.nowIso(),
6938
- runAgent: async (nextMatch, nextRule, nextAction, run) => await this.runAutomationAgent(nextMatch, nextRule, nextAction, run),
6939
- runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
6940
- }));
7304
+ runs.push(await executeAutomationAction(match, rule, action, this.automationActionHandlers()));
6941
7305
  }
6942
7306
  }
6943
7307
  await this.appendAutomationRuns(runs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",