granola-toolkit 0.46.2 → 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.
- package/dist/cli.js +1292 -761
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { homedir, platform } from "node:os";
|
|
7
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
8
8
|
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
@@ -118,7 +118,7 @@ function mergeHeaders(...values) {
|
|
|
118
118
|
}
|
|
119
119
|
return headers;
|
|
120
120
|
}
|
|
121
|
-
async function responseError(response) {
|
|
121
|
+
async function responseError$1(response) {
|
|
122
122
|
let message = `${response.status} ${response.statusText}`.trim();
|
|
123
123
|
try {
|
|
124
124
|
const payload = await response.json();
|
|
@@ -159,14 +159,14 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
159
159
|
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
160
160
|
accept: "application/json"
|
|
161
161
|
}) });
|
|
162
|
-
if (!infoResponse.ok) throw await responseError(infoResponse);
|
|
162
|
+
if (!infoResponse.ok) throw await responseError$1(infoResponse);
|
|
163
163
|
const info = await infoResponse.json();
|
|
164
164
|
if (info.protocolVersion !== 2) throw new Error(`unsupported Granola transport protocol: expected 2, got ${info.protocolVersion}`);
|
|
165
165
|
const response = await fetchImpl(new URL(granolaTransportPaths.state, url), { headers: mergeHeaders({
|
|
166
166
|
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
167
167
|
accept: "application/json"
|
|
168
168
|
}) });
|
|
169
|
-
if (!response.ok) throw await responseError(response);
|
|
169
|
+
if (!response.ok) throw await responseError$1(response);
|
|
170
170
|
const client = new GranolaServerClient(info, url, await response.json(), options);
|
|
171
171
|
client.startEvents();
|
|
172
172
|
return client;
|
|
@@ -294,7 +294,7 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
294
294
|
accept: "application/json"
|
|
295
295
|
}, init.headers)
|
|
296
296
|
});
|
|
297
|
-
if (!response.ok) throw await responseError(response);
|
|
297
|
+
if (!response.ok) throw await responseError$1(response);
|
|
298
298
|
return response;
|
|
299
299
|
}
|
|
300
300
|
async requestJson(path, init = {}) {
|
|
@@ -449,7 +449,7 @@ function asRecord(value) {
|
|
|
449
449
|
function stringValue(value) {
|
|
450
450
|
return typeof value === "string" ? value : "";
|
|
451
451
|
}
|
|
452
|
-
function stringArray$
|
|
452
|
+
function stringArray$2(value) {
|
|
453
453
|
if (!Array.isArray(value)) return [];
|
|
454
454
|
return value.filter((item) => typeof item === "string");
|
|
455
455
|
}
|
|
@@ -2604,127 +2604,6 @@ const attachCommand = {
|
|
|
2604
2604
|
}
|
|
2605
2605
|
};
|
|
2606
2606
|
//#endregion
|
|
2607
|
-
//#region src/automation-actions.ts
|
|
2608
|
-
function cloneAction$1(action) {
|
|
2609
|
-
switch (action.kind) {
|
|
2610
|
-
case "ask-user": return { ...action };
|
|
2611
|
-
case "command": return {
|
|
2612
|
-
...action,
|
|
2613
|
-
args: action.args ? [...action.args] : void 0,
|
|
2614
|
-
env: action.env ? { ...action.env } : void 0
|
|
2615
|
-
};
|
|
2616
|
-
case "export-notes":
|
|
2617
|
-
case "export-transcript": return { ...action };
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
function automationActionName(action) {
|
|
2621
|
-
return action.name || action.id;
|
|
2622
|
-
}
|
|
2623
|
-
function buildAutomationActionRunId(match, actionId) {
|
|
2624
|
-
return `${match.id}:${actionId}`;
|
|
2625
|
-
}
|
|
2626
|
-
function enabledAutomationActions(rule) {
|
|
2627
|
-
return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
|
|
2628
|
-
}
|
|
2629
|
-
function baseRun(match, rule, action, startedAt) {
|
|
2630
|
-
return {
|
|
2631
|
-
actionId: action.id,
|
|
2632
|
-
actionKind: action.kind,
|
|
2633
|
-
actionName: automationActionName(action),
|
|
2634
|
-
eventId: match.eventId,
|
|
2635
|
-
eventKind: match.eventKind,
|
|
2636
|
-
folders: match.folders.map((folder) => ({ ...folder })),
|
|
2637
|
-
id: buildAutomationActionRunId(match, action.id),
|
|
2638
|
-
matchedAt: match.matchedAt,
|
|
2639
|
-
meetingId: match.meetingId,
|
|
2640
|
-
ruleId: rule.id,
|
|
2641
|
-
ruleName: rule.name,
|
|
2642
|
-
startedAt,
|
|
2643
|
-
status: "completed",
|
|
2644
|
-
tags: [...match.tags],
|
|
2645
|
-
title: match.title,
|
|
2646
|
-
transcriptLoaded: match.transcriptLoaded
|
|
2647
|
-
};
|
|
2648
|
-
}
|
|
2649
|
-
function completedRun(run, finishedAt, patch = {}) {
|
|
2650
|
-
return {
|
|
2651
|
-
...run,
|
|
2652
|
-
...patch,
|
|
2653
|
-
finishedAt,
|
|
2654
|
-
status: "completed"
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
function failedRun(run, finishedAt, error) {
|
|
2658
|
-
return {
|
|
2659
|
-
...run,
|
|
2660
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2661
|
-
finishedAt,
|
|
2662
|
-
status: "failed"
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
function skippedRun(run, finishedAt, reason) {
|
|
2666
|
-
return {
|
|
2667
|
-
...run,
|
|
2668
|
-
finishedAt,
|
|
2669
|
-
result: reason,
|
|
2670
|
-
status: "skipped"
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
async function executeAutomationAction(match, rule, action, handlers) {
|
|
2674
|
-
const run = baseRun(match, rule, action, handlers.nowIso());
|
|
2675
|
-
switch (action.kind) {
|
|
2676
|
-
case "ask-user": return {
|
|
2677
|
-
...run,
|
|
2678
|
-
meta: action.details ? { details: action.details } : void 0,
|
|
2679
|
-
prompt: action.prompt,
|
|
2680
|
-
result: "Pending user decision",
|
|
2681
|
-
status: "pending"
|
|
2682
|
-
};
|
|
2683
|
-
case "command": try {
|
|
2684
|
-
const result = await handlers.runCommand(match, rule, action);
|
|
2685
|
-
return completedRun(run, handlers.nowIso(), {
|
|
2686
|
-
meta: {
|
|
2687
|
-
command: result.command,
|
|
2688
|
-
cwd: result.cwd
|
|
2689
|
-
},
|
|
2690
|
-
result: result.output
|
|
2691
|
-
});
|
|
2692
|
-
} catch (error) {
|
|
2693
|
-
return failedRun(run, handlers.nowIso(), error);
|
|
2694
|
-
}
|
|
2695
|
-
case "export-notes": try {
|
|
2696
|
-
const result = await handlers.exportNotes(match, action);
|
|
2697
|
-
if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
|
|
2698
|
-
return completedRun(run, handlers.nowIso(), {
|
|
2699
|
-
meta: {
|
|
2700
|
-
format: result.format,
|
|
2701
|
-
outputDir: result.outputDir,
|
|
2702
|
-
scope: result.scope,
|
|
2703
|
-
written: result.written
|
|
2704
|
-
},
|
|
2705
|
-
result: `Exported notes to ${result.outputDir}`
|
|
2706
|
-
});
|
|
2707
|
-
} catch (error) {
|
|
2708
|
-
return failedRun(run, handlers.nowIso(), error);
|
|
2709
|
-
}
|
|
2710
|
-
case "export-transcript": try {
|
|
2711
|
-
const result = await handlers.exportTranscripts(match, action);
|
|
2712
|
-
if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
|
|
2713
|
-
return completedRun(run, handlers.nowIso(), {
|
|
2714
|
-
meta: {
|
|
2715
|
-
format: result.format,
|
|
2716
|
-
outputDir: result.outputDir,
|
|
2717
|
-
scope: result.scope,
|
|
2718
|
-
written: result.written
|
|
2719
|
-
},
|
|
2720
|
-
result: `Exported transcript to ${result.outputDir}`
|
|
2721
|
-
});
|
|
2722
|
-
} catch (error) {
|
|
2723
|
-
return failedRun(run, handlers.nowIso(), error);
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
//#endregion
|
|
2728
2607
|
//#region src/persistence/layout.ts
|
|
2729
2608
|
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2730
2609
|
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
@@ -2733,6 +2612,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2733
2612
|
const targetPlatform = options.platform ?? platform();
|
|
2734
2613
|
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2735
2614
|
return {
|
|
2615
|
+
agentHarnessesFile: join(dataDirectory, "agent-harnesses.json"),
|
|
2736
2616
|
automationMatchesFile: join(dataDirectory, "automation-matches.jsonl"),
|
|
2737
2617
|
automationRulesFile: join(dataDirectory, "automation-rules.json"),
|
|
2738
2618
|
automationRunsFile: join(dataDirectory, "automation-runs.jsonl"),
|
|
@@ -2748,110 +2628,757 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2748
2628
|
};
|
|
2749
2629
|
}
|
|
2750
2630
|
//#endregion
|
|
2751
|
-
//#region src/
|
|
2752
|
-
function
|
|
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;
|
|
2753
2640
|
return {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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
|
|
2757
2651
|
};
|
|
2758
2652
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
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;
|
|
2762
2726
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
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);
|
|
2771
2747
|
}
|
|
2772
|
-
|
|
2748
|
+
return matchAgentHarnesses(harnesses, context)[0];
|
|
2749
|
+
}
|
|
2750
|
+
var FileAgentHarnessStore = class {
|
|
2751
|
+
constructor(filePath = defaultAgentHarnessesFilePath()) {
|
|
2752
|
+
this.filePath = filePath;
|
|
2753
|
+
}
|
|
2754
|
+
async readHarnesses() {
|
|
2773
2755
|
try {
|
|
2774
|
-
const
|
|
2775
|
-
return (
|
|
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);
|
|
2776
2758
|
} catch {
|
|
2777
2759
|
return [];
|
|
2778
2760
|
}
|
|
2779
2761
|
}
|
|
2780
2762
|
};
|
|
2781
|
-
function
|
|
2782
|
-
return defaultGranolaToolkitPersistenceLayout().
|
|
2763
|
+
function defaultAgentHarnessesFilePath() {
|
|
2764
|
+
return defaultGranolaToolkitPersistenceLayout().agentHarnessesFile;
|
|
2783
2765
|
}
|
|
2784
|
-
function
|
|
2785
|
-
return new
|
|
2766
|
+
function createDefaultAgentHarnessStore(filePath) {
|
|
2767
|
+
return new FileAgentHarnessStore(filePath);
|
|
2786
2768
|
}
|
|
2787
2769
|
//#endregion
|
|
2788
|
-
//#region src/
|
|
2789
|
-
|
|
2770
|
+
//#region src/client/auth.ts
|
|
2771
|
+
const execFileAsync$1 = promisify(execFile);
|
|
2772
|
+
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
2773
|
+
const KEYCHAIN_SERVICE_NAME = "com.granola.toolkit";
|
|
2774
|
+
const KEYCHAIN_ACCOUNT_NAME_API_KEY = "api-key";
|
|
2775
|
+
const KEYCHAIN_ACCOUNT_NAME = "session";
|
|
2776
|
+
const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
2777
|
+
function numberValue(value) {
|
|
2778
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2779
|
+
}
|
|
2780
|
+
function parseSessionRecord(record) {
|
|
2781
|
+
const accessToken = stringValue(record.access_token);
|
|
2782
|
+
if (!accessToken.trim()) return;
|
|
2790
2783
|
return {
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2784
|
+
accessToken,
|
|
2785
|
+
clientId: stringValue(record.client_id) || DEFAULT_CLIENT_ID,
|
|
2786
|
+
expiresIn: numberValue(record.expires_in),
|
|
2787
|
+
externalId: stringValue(record.external_id) || void 0,
|
|
2788
|
+
obtainedAt: stringValue(record.obtained_at) || void 0,
|
|
2789
|
+
refreshToken: stringValue(record.refresh_token) || void 0,
|
|
2790
|
+
sessionId: stringValue(record.session_id) || void 0,
|
|
2791
|
+
signInMethod: stringValue(record.sign_in_method) || void 0,
|
|
2792
|
+
tokenType: stringValue(record.token_type) || void 0
|
|
2795
2793
|
};
|
|
2796
2794
|
}
|
|
2797
|
-
function
|
|
2798
|
-
|
|
2795
|
+
function parseNestedRecord(value) {
|
|
2796
|
+
if (typeof value === "string") return parseJsonString(value);
|
|
2797
|
+
return asRecord(value);
|
|
2799
2798
|
}
|
|
2800
|
-
function
|
|
2801
|
-
const
|
|
2802
|
-
|
|
2803
|
-
|
|
2799
|
+
function getSessionFromSupabaseContents(supabaseContents) {
|
|
2800
|
+
const wrapper = parseJsonString(supabaseContents);
|
|
2801
|
+
if (!wrapper) throw new Error("failed to parse supabase.json");
|
|
2802
|
+
const workOsSession = parseSessionRecord(parseNestedRecord(wrapper.workos_tokens) ?? {});
|
|
2803
|
+
if (workOsSession) return workOsSession;
|
|
2804
|
+
const cognitoSession = parseSessionRecord(parseNestedRecord(wrapper.cognito_tokens) ?? {});
|
|
2805
|
+
if (cognitoSession) return cognitoSession;
|
|
2806
|
+
const legacySession = parseSessionRecord(wrapper);
|
|
2807
|
+
if (legacySession) return legacySession;
|
|
2808
|
+
throw new Error("access token not found in supabase.json");
|
|
2804
2809
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2810
|
+
function getAccessTokenFromSupabaseContents(supabaseContents) {
|
|
2811
|
+
return getSessionFromSupabaseContents(supabaseContents).accessToken;
|
|
2812
|
+
}
|
|
2813
|
+
var SupabaseFileTokenSource = class {
|
|
2814
|
+
constructor(filePath) {
|
|
2807
2815
|
this.filePath = filePath;
|
|
2808
2816
|
}
|
|
2809
|
-
async
|
|
2810
|
-
|
|
2811
|
-
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2812
|
-
const payload = runs.map((run) => JSON.stringify(run)).join("\n");
|
|
2813
|
-
await appendFile(this.filePath, `${payload}\n`, {
|
|
2814
|
-
encoding: "utf8",
|
|
2815
|
-
mode: 384
|
|
2816
|
-
});
|
|
2817
|
+
async loadAccessToken() {
|
|
2818
|
+
return getAccessTokenFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
2817
2819
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
+
};
|
|
2821
|
+
var SupabaseFileSessionSource = class {
|
|
2822
|
+
constructor(filePath) {
|
|
2823
|
+
this.filePath = filePath;
|
|
2820
2824
|
}
|
|
2821
|
-
async
|
|
2822
|
-
|
|
2823
|
-
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);
|
|
2824
|
-
return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
|
|
2825
|
-
} catch {
|
|
2826
|
-
return [];
|
|
2827
|
-
}
|
|
2825
|
+
async loadSession() {
|
|
2826
|
+
return getSessionFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
2828
2827
|
}
|
|
2829
2828
|
};
|
|
2830
|
-
|
|
2831
|
-
|
|
2829
|
+
var NoopTokenStore = class {
|
|
2830
|
+
async clearToken() {}
|
|
2831
|
+
async readToken() {}
|
|
2832
|
+
async writeToken(_token) {}
|
|
2833
|
+
};
|
|
2834
|
+
var FileSessionStore = class {
|
|
2835
|
+
constructor(filePath = defaultSessionFilePath()) {
|
|
2836
|
+
this.filePath = filePath;
|
|
2837
|
+
}
|
|
2838
|
+
async clearSession() {
|
|
2839
|
+
try {
|
|
2840
|
+
await unlink(this.filePath);
|
|
2841
|
+
} catch {}
|
|
2842
|
+
}
|
|
2843
|
+
async readSession() {
|
|
2844
|
+
try {
|
|
2845
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
2846
|
+
return parsed?.accessToken ? parsed : void 0;
|
|
2847
|
+
} catch {
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
async writeSession(session) {
|
|
2852
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2853
|
+
await writeFile(this.filePath, `${JSON.stringify(session, null, 2)}\n`, {
|
|
2854
|
+
encoding: "utf8",
|
|
2855
|
+
mode: 384
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
var FileApiKeyStore = class {
|
|
2860
|
+
constructor(filePath = defaultApiKeyFilePath()) {
|
|
2861
|
+
this.filePath = filePath;
|
|
2862
|
+
}
|
|
2863
|
+
async clearApiKey() {
|
|
2864
|
+
try {
|
|
2865
|
+
await unlink(this.filePath);
|
|
2866
|
+
} catch {}
|
|
2867
|
+
}
|
|
2868
|
+
async readApiKey() {
|
|
2869
|
+
try {
|
|
2870
|
+
return (await readFile(this.filePath, "utf8")).trim() || void 0;
|
|
2871
|
+
} catch {
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
async writeApiKey(apiKey) {
|
|
2876
|
+
const trimmed = apiKey.trim();
|
|
2877
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2878
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2879
|
+
await writeFile(this.filePath, `${trimmed}\n`, {
|
|
2880
|
+
encoding: "utf8",
|
|
2881
|
+
mode: 384
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
var KeychainSessionStore = class {
|
|
2886
|
+
async clearSession() {
|
|
2887
|
+
try {
|
|
2888
|
+
await execFileAsync$1("security", [
|
|
2889
|
+
"delete-generic-password",
|
|
2890
|
+
"-s",
|
|
2891
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2892
|
+
"-a",
|
|
2893
|
+
KEYCHAIN_ACCOUNT_NAME
|
|
2894
|
+
]);
|
|
2895
|
+
} catch {}
|
|
2896
|
+
}
|
|
2897
|
+
async readSession() {
|
|
2898
|
+
try {
|
|
2899
|
+
const { stdout } = await execFileAsync$1("security", [
|
|
2900
|
+
"find-generic-password",
|
|
2901
|
+
"-s",
|
|
2902
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2903
|
+
"-a",
|
|
2904
|
+
KEYCHAIN_ACCOUNT_NAME,
|
|
2905
|
+
"-w"
|
|
2906
|
+
]);
|
|
2907
|
+
const parsed = parseJsonString(stdout.trim());
|
|
2908
|
+
return parsed?.accessToken ? parsed : void 0;
|
|
2909
|
+
} catch {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
async writeSession(session) {
|
|
2914
|
+
await execFileAsync$1("security", [
|
|
2915
|
+
"add-generic-password",
|
|
2916
|
+
"-U",
|
|
2917
|
+
"-s",
|
|
2918
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2919
|
+
"-a",
|
|
2920
|
+
KEYCHAIN_ACCOUNT_NAME,
|
|
2921
|
+
"-w",
|
|
2922
|
+
JSON.stringify(session)
|
|
2923
|
+
]);
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
var KeychainApiKeyStore = class {
|
|
2927
|
+
async clearApiKey() {
|
|
2928
|
+
try {
|
|
2929
|
+
await execFileAsync$1("security", [
|
|
2930
|
+
"delete-generic-password",
|
|
2931
|
+
"-s",
|
|
2932
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2933
|
+
"-a",
|
|
2934
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY
|
|
2935
|
+
]);
|
|
2936
|
+
} catch {}
|
|
2937
|
+
}
|
|
2938
|
+
async readApiKey() {
|
|
2939
|
+
try {
|
|
2940
|
+
const { stdout } = await execFileAsync$1("security", [
|
|
2941
|
+
"find-generic-password",
|
|
2942
|
+
"-s",
|
|
2943
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2944
|
+
"-a",
|
|
2945
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2946
|
+
"-w"
|
|
2947
|
+
]);
|
|
2948
|
+
return stdout.trim() || void 0;
|
|
2949
|
+
} catch {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
async writeApiKey(apiKey) {
|
|
2954
|
+
const trimmed = apiKey.trim();
|
|
2955
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2956
|
+
await execFileAsync$1("security", [
|
|
2957
|
+
"add-generic-password",
|
|
2958
|
+
"-U",
|
|
2959
|
+
"-s",
|
|
2960
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2961
|
+
"-a",
|
|
2962
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2963
|
+
"-w",
|
|
2964
|
+
trimmed
|
|
2965
|
+
]);
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
var CachedTokenProvider = class {
|
|
2969
|
+
#token;
|
|
2970
|
+
constructor(source, store = new NoopTokenStore()) {
|
|
2971
|
+
this.source = source;
|
|
2972
|
+
this.store = store;
|
|
2973
|
+
}
|
|
2974
|
+
async getAccessToken() {
|
|
2975
|
+
if (this.#token) return this.#token;
|
|
2976
|
+
const storedToken = await this.store.readToken();
|
|
2977
|
+
if (storedToken?.trim()) {
|
|
2978
|
+
this.#token = storedToken;
|
|
2979
|
+
return storedToken;
|
|
2980
|
+
}
|
|
2981
|
+
const token = await this.source.loadAccessToken();
|
|
2982
|
+
this.#token = token;
|
|
2983
|
+
await this.store.writeToken(token);
|
|
2984
|
+
return token;
|
|
2985
|
+
}
|
|
2986
|
+
async invalidate() {
|
|
2987
|
+
this.#token = void 0;
|
|
2988
|
+
await this.store.clearToken();
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
var StoredSessionTokenProvider = class {
|
|
2992
|
+
#session;
|
|
2993
|
+
constructor(store, options = {}) {
|
|
2994
|
+
this.store = store;
|
|
2995
|
+
this.options = options;
|
|
2996
|
+
}
|
|
2997
|
+
async loadSession() {
|
|
2998
|
+
if (this.#session) return this.#session;
|
|
2999
|
+
const storedSession = await this.store.readSession();
|
|
3000
|
+
if (storedSession?.accessToken.trim()) {
|
|
3001
|
+
this.#session = storedSession;
|
|
3002
|
+
return storedSession;
|
|
3003
|
+
}
|
|
3004
|
+
if (!this.options.source) throw new Error("no stored Granola session found");
|
|
3005
|
+
const sourcedSession = await this.options.source.loadSession();
|
|
3006
|
+
this.#session = sourcedSession;
|
|
3007
|
+
return sourcedSession;
|
|
3008
|
+
}
|
|
3009
|
+
async getAccessToken() {
|
|
3010
|
+
return (await this.loadSession()).accessToken;
|
|
3011
|
+
}
|
|
3012
|
+
async invalidate() {
|
|
3013
|
+
const session = await this.loadSession().catch(() => void 0);
|
|
3014
|
+
if (session?.refreshToken && session.clientId) try {
|
|
3015
|
+
const refreshedSession = await refreshGranolaSession(session, this.options.fetchImpl);
|
|
3016
|
+
this.#session = refreshedSession;
|
|
3017
|
+
await this.store.writeSession(refreshedSession);
|
|
3018
|
+
return;
|
|
3019
|
+
} catch {
|
|
3020
|
+
if (!this.options.source) {
|
|
3021
|
+
this.#session = void 0;
|
|
3022
|
+
await this.store.clearSession();
|
|
3023
|
+
throw new Error("failed to refresh stored Granola session");
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
if (this.options.source) {
|
|
3027
|
+
const sourcedSession = await this.options.source.loadSession();
|
|
3028
|
+
this.#session = sourcedSession;
|
|
3029
|
+
await this.store.writeSession(sourcedSession);
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
this.#session = void 0;
|
|
3033
|
+
await this.store.clearSession();
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
3037
|
+
if (!session.refreshToken?.trim()) throw new Error("refresh token not available");
|
|
3038
|
+
const response = await fetchImpl(WORKOS_AUTH_URL, {
|
|
3039
|
+
body: JSON.stringify({
|
|
3040
|
+
client_id: session.clientId,
|
|
3041
|
+
grant_type: "refresh_token",
|
|
3042
|
+
refresh_token: session.refreshToken
|
|
3043
|
+
}),
|
|
3044
|
+
headers: { "Content-Type": "application/json" },
|
|
3045
|
+
method: "POST"
|
|
3046
|
+
});
|
|
3047
|
+
if (!response.ok) throw new Error(`failed to refresh session: ${response.status} ${response.statusText}`);
|
|
3048
|
+
const refreshed = parseSessionRecord(await response.json());
|
|
3049
|
+
if (!refreshed) throw new Error("failed to parse refreshed session");
|
|
3050
|
+
return {
|
|
3051
|
+
...session,
|
|
3052
|
+
...refreshed,
|
|
3053
|
+
clientId: refreshed.clientId || session.clientId,
|
|
3054
|
+
obtainedAt: refreshed.obtainedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3055
|
+
refreshToken: refreshed.refreshToken ?? session.refreshToken
|
|
3056
|
+
};
|
|
2832
3057
|
}
|
|
2833
|
-
function
|
|
2834
|
-
return
|
|
3058
|
+
function defaultSessionFilePath() {
|
|
3059
|
+
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
3060
|
+
}
|
|
3061
|
+
function defaultApiKeyFilePath() {
|
|
3062
|
+
return defaultGranolaToolkitPersistenceLayout().apiKeyFile;
|
|
3063
|
+
}
|
|
3064
|
+
function createDefaultSessionStore() {
|
|
3065
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
3066
|
+
}
|
|
3067
|
+
function createDefaultApiKeyStore() {
|
|
3068
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainApiKeyStore() : new FileApiKeyStore();
|
|
3069
|
+
}
|
|
3070
|
+
//#endregion
|
|
3071
|
+
//#region src/client/http.ts
|
|
3072
|
+
const RETRYABLE_STATUS_CODES = new Set([
|
|
3073
|
+
429,
|
|
3074
|
+
500,
|
|
3075
|
+
502,
|
|
3076
|
+
503,
|
|
3077
|
+
504
|
|
3078
|
+
]);
|
|
3079
|
+
function sleep(delayMs) {
|
|
3080
|
+
return new Promise((resolve) => {
|
|
3081
|
+
setTimeout(resolve, delayMs);
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
function parseRetryAfter(headerValue) {
|
|
3085
|
+
if (!headerValue?.trim()) return;
|
|
3086
|
+
if (/^\d+$/.test(headerValue.trim())) return Number(headerValue.trim()) * 1e3;
|
|
3087
|
+
const retryAt = Date.parse(headerValue);
|
|
3088
|
+
if (Number.isNaN(retryAt)) return;
|
|
3089
|
+
return Math.max(0, retryAt - Date.now());
|
|
3090
|
+
}
|
|
3091
|
+
var AuthenticatedHttpClient = class {
|
|
3092
|
+
fetchImpl;
|
|
3093
|
+
constructor(options) {
|
|
3094
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
3095
|
+
this.logger = options.logger;
|
|
3096
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
3097
|
+
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;
|
|
3098
|
+
this.retryMaxDelayMs = options.retryMaxDelayMs ?? 5e3;
|
|
3099
|
+
this.sleepImpl = options.sleepImpl ?? sleep;
|
|
3100
|
+
this.tokenProvider = options.tokenProvider;
|
|
3101
|
+
}
|
|
3102
|
+
logger;
|
|
3103
|
+
maxRetries;
|
|
3104
|
+
retryBaseDelayMs;
|
|
3105
|
+
retryMaxDelayMs;
|
|
3106
|
+
sleepImpl;
|
|
3107
|
+
tokenProvider;
|
|
3108
|
+
async retry(options, attempt, reason, response) {
|
|
3109
|
+
const retryAfterMs = parseRetryAfter(response?.headers.get("retry-after") ?? null);
|
|
3110
|
+
const delayMs = Math.min(retryAfterMs ?? this.retryBaseDelayMs * 2 ** attempt, this.retryMaxDelayMs);
|
|
3111
|
+
this.logger?.warn?.(`${reason}; retrying in ${delayMs}ms (${attempt + 1}/${this.maxRetries})`);
|
|
3112
|
+
await this.sleepImpl(delayMs);
|
|
3113
|
+
return this.request(options, attempt + 1);
|
|
3114
|
+
}
|
|
3115
|
+
async request(options, attempt = 0) {
|
|
3116
|
+
const { retryOnUnauthorized = true, timeoutMs, url } = options;
|
|
3117
|
+
const accessToken = await this.tokenProvider.getAccessToken();
|
|
3118
|
+
let response;
|
|
3119
|
+
try {
|
|
3120
|
+
response = await this.fetchImpl(url, {
|
|
3121
|
+
body: options.body,
|
|
3122
|
+
headers: {
|
|
3123
|
+
...options.headers,
|
|
3124
|
+
Authorization: `Bearer ${accessToken}`
|
|
3125
|
+
},
|
|
3126
|
+
method: options.method ?? "GET",
|
|
3127
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3128
|
+
});
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
if (attempt < this.maxRetries) {
|
|
3131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3132
|
+
return this.retry(options, attempt, `request failed: ${message}`);
|
|
3133
|
+
}
|
|
3134
|
+
throw error;
|
|
3135
|
+
}
|
|
3136
|
+
if (response.status === 401 && retryOnUnauthorized) {
|
|
3137
|
+
this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
|
|
3138
|
+
await this.tokenProvider.invalidate();
|
|
3139
|
+
return this.request({
|
|
3140
|
+
...options,
|
|
3141
|
+
retryOnUnauthorized: false
|
|
3142
|
+
}, attempt);
|
|
3143
|
+
}
|
|
3144
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) return this.retry(options, attempt, `request returned ${response.status} ${response.statusText || ""}`.trim(), response);
|
|
3145
|
+
return response;
|
|
3146
|
+
}
|
|
3147
|
+
async postJson(url, body, options = { timeoutMs: 3e4 }) {
|
|
3148
|
+
return this.request({
|
|
3149
|
+
...options,
|
|
3150
|
+
body: JSON.stringify(body),
|
|
3151
|
+
headers: {
|
|
3152
|
+
Accept: "*/*",
|
|
3153
|
+
"Content-Type": "application/json",
|
|
3154
|
+
...options.headers
|
|
3155
|
+
},
|
|
3156
|
+
method: "POST",
|
|
3157
|
+
url
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
//#endregion
|
|
3162
|
+
//#region src/agents.ts
|
|
3163
|
+
const DEFAULT_CODEX_MODEL = "gpt-5-codex";
|
|
3164
|
+
const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
|
|
3165
|
+
const DEFAULT_OPENROUTER_MODEL = "openai/gpt-5-mini";
|
|
3166
|
+
const OPENROUTER_REFERER = "https://github.com/kkarimi/granola-toolkit";
|
|
3167
|
+
const OPENROUTER_TITLE = "granola-toolkit";
|
|
3168
|
+
function trimString(value) {
|
|
3169
|
+
return value?.trim() ? value.trim() : void 0;
|
|
3170
|
+
}
|
|
3171
|
+
function resolveProvider(request, config, env) {
|
|
3172
|
+
if (request.provider) return request.provider;
|
|
3173
|
+
if (config.agents?.defaultProvider) return config.agents.defaultProvider;
|
|
3174
|
+
if (trimString(env.OPENROUTER_API_KEY) || trimString(env.GRANOLA_OPENROUTER_API_KEY)) return "openrouter";
|
|
3175
|
+
if (trimString(env.OPENAI_API_KEY) || trimString(env.GRANOLA_OPENAI_API_KEY)) return "openai";
|
|
3176
|
+
return "codex";
|
|
3177
|
+
}
|
|
3178
|
+
function resolveModel(provider, request, config) {
|
|
3179
|
+
return trimString(request.model) ?? trimString(config.agents?.defaultModel) ?? (provider === "openrouter" ? DEFAULT_OPENROUTER_MODEL : provider === "openai" ? DEFAULT_OPENAI_MODEL : DEFAULT_CODEX_MODEL);
|
|
3180
|
+
}
|
|
3181
|
+
function resolveTimeoutMs(request, config) {
|
|
3182
|
+
return request.timeoutMs ?? config.agents?.timeoutMs ?? 3e5;
|
|
3183
|
+
}
|
|
3184
|
+
function resolveRetries(request, config) {
|
|
3185
|
+
return request.retries ?? config.agents?.maxRetries ?? 2;
|
|
3186
|
+
}
|
|
3187
|
+
function resolveDryRun(request, config) {
|
|
3188
|
+
return request.dryRun ?? config.agents?.dryRun ?? false;
|
|
3189
|
+
}
|
|
3190
|
+
function openaiApiKey(env) {
|
|
3191
|
+
return trimString(env.OPENAI_API_KEY) ?? trimString(env.GRANOLA_OPENAI_API_KEY);
|
|
3192
|
+
}
|
|
3193
|
+
function openrouterApiKey(env) {
|
|
3194
|
+
return trimString(env.OPENROUTER_API_KEY) ?? trimString(env.GRANOLA_OPENROUTER_API_KEY);
|
|
3195
|
+
}
|
|
3196
|
+
async function responseError(response, label) {
|
|
3197
|
+
let details = `${response.status} ${response.statusText}`.trim();
|
|
3198
|
+
try {
|
|
3199
|
+
const payload = await response.json();
|
|
3200
|
+
if (typeof payload.error === "string" && payload.error.trim()) details = payload.error;
|
|
3201
|
+
else if (payload.error && typeof payload.error === "object" && typeof payload.error.message === "string" && payload.error.message.trim()) details = payload.error.message;
|
|
3202
|
+
else if (typeof payload.message === "string" && payload.message.trim()) details = payload.message;
|
|
3203
|
+
} catch {
|
|
3204
|
+
const text = (await response.text()).trim();
|
|
3205
|
+
if (text) details = text;
|
|
3206
|
+
}
|
|
3207
|
+
return /* @__PURE__ */ new Error(`${label}: ${details}`);
|
|
3208
|
+
}
|
|
3209
|
+
function messageText(content) {
|
|
3210
|
+
if (typeof content === "string") return content.trim();
|
|
3211
|
+
if (!Array.isArray(content)) return "";
|
|
3212
|
+
return content.map((part) => {
|
|
3213
|
+
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") return part.text;
|
|
3214
|
+
return "";
|
|
3215
|
+
}).join("").trim();
|
|
3216
|
+
}
|
|
3217
|
+
async function runCodexCliCommand(request) {
|
|
3218
|
+
const tempDirectory = await mkdtemp(join(tmpdir(), "granola-toolkit-codex-"));
|
|
3219
|
+
const outputFile = join(tempDirectory, "last-message.txt");
|
|
3220
|
+
const args = [
|
|
3221
|
+
"exec",
|
|
3222
|
+
"--skip-git-repo-check",
|
|
3223
|
+
"--color",
|
|
3224
|
+
"never"
|
|
3225
|
+
];
|
|
3226
|
+
if (request.cwd) args.push("-C", request.cwd);
|
|
3227
|
+
if (request.model) args.push("-m", request.model);
|
|
3228
|
+
args.push("--output-last-message", outputFile, "-");
|
|
3229
|
+
const commandText = [request.command, ...args].join(" ");
|
|
3230
|
+
try {
|
|
3231
|
+
return {
|
|
3232
|
+
command: commandText,
|
|
3233
|
+
output: await new Promise((resolve$1, reject) => {
|
|
3234
|
+
const child = spawn(request.command, args, {
|
|
3235
|
+
cwd: request.cwd ? resolve(request.cwd) : process.cwd(),
|
|
3236
|
+
env: process.env,
|
|
3237
|
+
stdio: [
|
|
3238
|
+
"pipe",
|
|
3239
|
+
"pipe",
|
|
3240
|
+
"pipe"
|
|
3241
|
+
]
|
|
3242
|
+
});
|
|
3243
|
+
const stdoutChunks = [];
|
|
3244
|
+
const stderrChunks = [];
|
|
3245
|
+
let timedOut = false;
|
|
3246
|
+
const timeout = setTimeout(() => {
|
|
3247
|
+
timedOut = true;
|
|
3248
|
+
child.kill("SIGTERM");
|
|
3249
|
+
}, request.timeoutMs);
|
|
3250
|
+
child.stdout.on("data", (chunk) => {
|
|
3251
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3252
|
+
});
|
|
3253
|
+
child.stderr.on("data", (chunk) => {
|
|
3254
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3255
|
+
});
|
|
3256
|
+
child.on("error", (error) => {
|
|
3257
|
+
clearTimeout(timeout);
|
|
3258
|
+
reject(error);
|
|
3259
|
+
});
|
|
3260
|
+
child.on("close", async (code) => {
|
|
3261
|
+
clearTimeout(timeout);
|
|
3262
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
3263
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
3264
|
+
if (timedOut) {
|
|
3265
|
+
reject(/* @__PURE__ */ new Error(`codex provider timed out after ${request.timeoutMs}ms`));
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
if (code !== 0) {
|
|
3269
|
+
reject(new Error(stderr || stdout || `codex exited with status ${String(code)}`));
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
try {
|
|
3273
|
+
resolve$1((await readFile(outputFile, "utf8")).trim() || stdout || void 0);
|
|
3274
|
+
} catch {
|
|
3275
|
+
resolve$1(stdout || void 0);
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
child.stdin.write(request.prompt);
|
|
3279
|
+
child.stdin.end();
|
|
3280
|
+
})
|
|
3281
|
+
};
|
|
3282
|
+
} finally {
|
|
3283
|
+
await rm(tempDirectory, {
|
|
3284
|
+
force: true,
|
|
3285
|
+
recursive: true
|
|
3286
|
+
}).catch(() => void 0);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
async function runOpenAiCompatibleRequest(options) {
|
|
3290
|
+
const response = await new AuthenticatedHttpClient({
|
|
3291
|
+
fetchImpl: options.fetchImpl,
|
|
3292
|
+
maxRetries: options.maxRetries,
|
|
3293
|
+
tokenProvider: new CachedTokenProvider({ async loadAccessToken() {
|
|
3294
|
+
return options.token;
|
|
3295
|
+
} })
|
|
3296
|
+
}).postJson(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
3297
|
+
messages: [...options.systemPrompt ? [{
|
|
3298
|
+
content: options.systemPrompt,
|
|
3299
|
+
role: "system"
|
|
3300
|
+
}] : [], {
|
|
3301
|
+
content: options.prompt,
|
|
3302
|
+
role: "user"
|
|
3303
|
+
}],
|
|
3304
|
+
model: options.model
|
|
3305
|
+
}, {
|
|
3306
|
+
headers: {
|
|
3307
|
+
Accept: "application/json",
|
|
3308
|
+
...options.headers
|
|
3309
|
+
},
|
|
3310
|
+
timeoutMs: options.timeoutMs
|
|
3311
|
+
});
|
|
3312
|
+
if (!response.ok) throw await responseError(response, options.label);
|
|
3313
|
+
const content = (await response.json()).choices?.[0]?.message?.content;
|
|
3314
|
+
return messageText(content) || void 0;
|
|
2835
3315
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
return {
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3316
|
+
function createDefaultAutomationAgentRunner(config, options = {}) {
|
|
3317
|
+
const env = options.env ?? process.env;
|
|
3318
|
+
const runCodexCommand = options.runCodexCommand ?? runCodexCliCommand;
|
|
3319
|
+
return { async run(request) {
|
|
3320
|
+
const provider = resolveProvider(request, config, env);
|
|
3321
|
+
const model = resolveModel(provider, request, config);
|
|
3322
|
+
const timeoutMs = resolveTimeoutMs(request, config);
|
|
3323
|
+
const retries = resolveRetries(request, config);
|
|
3324
|
+
const dryRun = resolveDryRun(request, config);
|
|
3325
|
+
if (dryRun) return {
|
|
3326
|
+
dryRun,
|
|
3327
|
+
model,
|
|
3328
|
+
output: void 0,
|
|
3329
|
+
prompt: request.prompt,
|
|
3330
|
+
provider,
|
|
3331
|
+
systemPrompt: request.systemPrompt
|
|
3332
|
+
};
|
|
3333
|
+
if (provider === "codex") {
|
|
3334
|
+
const result = await runCodexCommand({
|
|
3335
|
+
command: config.agents?.codexCommand ?? "codex",
|
|
3336
|
+
cwd: request.cwd,
|
|
3337
|
+
model,
|
|
3338
|
+
prompt: request.systemPrompt ? `${request.systemPrompt.trim()}\n\n${request.prompt}` : request.prompt,
|
|
3339
|
+
timeoutMs
|
|
3340
|
+
});
|
|
3341
|
+
return {
|
|
3342
|
+
command: result.command,
|
|
3343
|
+
dryRun,
|
|
3344
|
+
model,
|
|
3345
|
+
output: result.output,
|
|
3346
|
+
prompt: request.prompt,
|
|
3347
|
+
provider,
|
|
3348
|
+
systemPrompt: request.systemPrompt
|
|
3349
|
+
};
|
|
2850
3350
|
}
|
|
2851
|
-
|
|
3351
|
+
const token = provider === "openrouter" ? openrouterApiKey(env) : openaiApiKey(env);
|
|
3352
|
+
if (!token) throw new Error(provider === "openrouter" ? "OpenRouter API key not found. Set OPENROUTER_API_KEY or GRANOLA_OPENROUTER_API_KEY." : "OpenAI API key not found. Set OPENAI_API_KEY or GRANOLA_OPENAI_API_KEY.");
|
|
3353
|
+
return {
|
|
3354
|
+
dryRun,
|
|
3355
|
+
model,
|
|
3356
|
+
output: await runOpenAiCompatibleRequest({
|
|
3357
|
+
baseUrl: provider === "openrouter" ? config.agents?.openrouterBaseUrl ?? "https://openrouter.ai/api/v1" : config.agents?.openaiBaseUrl ?? "https://api.openai.com/v1",
|
|
3358
|
+
fetchImpl: options.fetchImpl,
|
|
3359
|
+
headers: provider === "openrouter" ? {
|
|
3360
|
+
"HTTP-Referer": OPENROUTER_REFERER,
|
|
3361
|
+
"X-Title": OPENROUTER_TITLE
|
|
3362
|
+
} : void 0,
|
|
3363
|
+
label: provider === "openrouter" ? "OpenRouter request failed" : "OpenAI request failed",
|
|
3364
|
+
maxRetries: retries,
|
|
3365
|
+
model,
|
|
3366
|
+
prompt: request.prompt,
|
|
3367
|
+
systemPrompt: request.systemPrompt,
|
|
3368
|
+
timeoutMs,
|
|
3369
|
+
token
|
|
3370
|
+
}),
|
|
3371
|
+
prompt: request.prompt,
|
|
3372
|
+
provider,
|
|
3373
|
+
systemPrompt: request.systemPrompt
|
|
3374
|
+
};
|
|
3375
|
+
} };
|
|
2852
3376
|
}
|
|
2853
|
-
|
|
3377
|
+
//#endregion
|
|
3378
|
+
//#region src/automation-actions.ts
|
|
3379
|
+
function cloneAction$1(action) {
|
|
2854
3380
|
switch (action.kind) {
|
|
3381
|
+
case "agent": return { ...action };
|
|
2855
3382
|
case "ask-user": return { ...action };
|
|
2856
3383
|
case "command": return {
|
|
2857
3384
|
...action,
|
|
@@ -2862,518 +3389,483 @@ function cloneAction(action) {
|
|
|
2862
3389
|
case "export-transcript": return { ...action };
|
|
2863
3390
|
}
|
|
2864
3391
|
}
|
|
2865
|
-
function
|
|
2866
|
-
|
|
2867
|
-
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
2868
|
-
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
2869
|
-
}
|
|
2870
|
-
function stringRecord(value) {
|
|
2871
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2872
|
-
const entries = Object.entries(value).filter(([key, item]) => {
|
|
2873
|
-
return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
|
|
2874
|
-
});
|
|
2875
|
-
if (entries.length === 0) return;
|
|
2876
|
-
return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
|
|
2877
|
-
}
|
|
2878
|
-
function parseAction(value, index) {
|
|
2879
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2880
|
-
const record = value;
|
|
2881
|
-
const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
|
|
2882
|
-
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
|
|
2883
|
-
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
2884
|
-
const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
|
|
2885
|
-
switch (kind) {
|
|
2886
|
-
case "ask-user": {
|
|
2887
|
-
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
2888
|
-
if (!id || !prompt) return;
|
|
2889
|
-
return {
|
|
2890
|
-
details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
|
|
2891
|
-
enabled,
|
|
2892
|
-
id,
|
|
2893
|
-
kind,
|
|
2894
|
-
name,
|
|
2895
|
-
prompt
|
|
2896
|
-
};
|
|
2897
|
-
}
|
|
2898
|
-
case "command": {
|
|
2899
|
-
const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
|
|
2900
|
-
if (!id || !command) return;
|
|
2901
|
-
return {
|
|
2902
|
-
args: stringArray(record.args),
|
|
2903
|
-
command,
|
|
2904
|
-
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
2905
|
-
enabled,
|
|
2906
|
-
env: stringRecord(record.env),
|
|
2907
|
-
id,
|
|
2908
|
-
kind,
|
|
2909
|
-
name,
|
|
2910
|
-
stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
|
|
2911
|
-
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
2912
|
-
};
|
|
2913
|
-
}
|
|
2914
|
-
case "export-notes":
|
|
2915
|
-
if (!id) return;
|
|
2916
|
-
return {
|
|
2917
|
-
enabled,
|
|
2918
|
-
format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
|
|
2919
|
-
id,
|
|
2920
|
-
kind,
|
|
2921
|
-
name,
|
|
2922
|
-
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2923
|
-
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2924
|
-
};
|
|
2925
|
-
case "export-transcript":
|
|
2926
|
-
if (!id) return;
|
|
2927
|
-
return {
|
|
2928
|
-
enabled,
|
|
2929
|
-
format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
|
|
2930
|
-
id,
|
|
2931
|
-
kind,
|
|
2932
|
-
name,
|
|
2933
|
-
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
2934
|
-
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
2935
|
-
};
|
|
2936
|
-
default: return;
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
function parseRule(value) {
|
|
2940
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2941
|
-
const record = value;
|
|
2942
|
-
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
|
|
2943
|
-
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
2944
|
-
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
2945
|
-
if (!id || !name || !whenValue) return;
|
|
2946
|
-
return {
|
|
2947
|
-
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
2948
|
-
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
2949
|
-
id,
|
|
2950
|
-
name,
|
|
2951
|
-
when: {
|
|
2952
|
-
eventKinds: stringArray(whenValue.eventKinds),
|
|
2953
|
-
folderIds: stringArray(whenValue.folderIds),
|
|
2954
|
-
folderNames: stringArray(whenValue.folderNames),
|
|
2955
|
-
meetingIds: stringArray(whenValue.meetingIds),
|
|
2956
|
-
tags: stringArray(whenValue.tags),
|
|
2957
|
-
titleIncludes: stringArray(whenValue.titleIncludes),
|
|
2958
|
-
titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
|
|
2959
|
-
transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
|
|
2960
|
-
}
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
|
-
var FileAutomationRuleStore = class {
|
|
2964
|
-
constructor(filePath = defaultAutomationRulesFilePath()) {
|
|
2965
|
-
this.filePath = filePath;
|
|
2966
|
-
}
|
|
2967
|
-
async readRules() {
|
|
2968
|
-
try {
|
|
2969
|
-
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
2970
|
-
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);
|
|
2971
|
-
} catch {
|
|
2972
|
-
return [];
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
};
|
|
2976
|
-
function defaultAutomationRulesFilePath() {
|
|
2977
|
-
return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
|
|
2978
|
-
}
|
|
2979
|
-
function includesIgnoreCase(candidate, values) {
|
|
2980
|
-
const lowerCandidate = candidate.toLowerCase();
|
|
2981
|
-
return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
|
|
2982
|
-
}
|
|
2983
|
-
function matchesRule(rule, event) {
|
|
2984
|
-
if (rule.enabled === false) return false;
|
|
2985
|
-
const { when } = rule;
|
|
2986
|
-
if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
|
|
2987
|
-
if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
|
|
2988
|
-
if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
|
|
2989
|
-
if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
|
|
2990
|
-
if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
|
|
2991
|
-
if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
|
|
2992
|
-
if (when.titleMatches) try {
|
|
2993
|
-
if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
|
|
2994
|
-
} catch {
|
|
2995
|
-
return false;
|
|
2996
|
-
}
|
|
2997
|
-
if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
|
|
2998
|
-
return true;
|
|
2999
|
-
}
|
|
3000
|
-
function matchAutomationRules(rules, events, matchedAt) {
|
|
3001
|
-
const matches = [];
|
|
3002
|
-
for (const event of events) for (const rule of rules) {
|
|
3003
|
-
if (!matchesRule(rule, event)) continue;
|
|
3004
|
-
matches.push({
|
|
3005
|
-
eventId: event.id,
|
|
3006
|
-
eventKind: event.kind,
|
|
3007
|
-
folders: event.folders.map((folder) => ({ ...folder })),
|
|
3008
|
-
id: `${event.id}:${rule.id}`,
|
|
3009
|
-
matchedAt,
|
|
3010
|
-
meetingId: event.meetingId,
|
|
3011
|
-
ruleId: rule.id,
|
|
3012
|
-
ruleName: rule.name,
|
|
3013
|
-
tags: [...event.tags],
|
|
3014
|
-
title: event.title,
|
|
3015
|
-
transcriptLoaded: event.transcriptLoaded
|
|
3016
|
-
});
|
|
3017
|
-
}
|
|
3018
|
-
return matches;
|
|
3019
|
-
}
|
|
3020
|
-
function createDefaultAutomationRuleStore(filePath) {
|
|
3021
|
-
return new FileAutomationRuleStore(filePath);
|
|
3392
|
+
function automationActionName(action) {
|
|
3393
|
+
return action.name || action.id;
|
|
3022
3394
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
function parseCacheDocument(id, value) {
|
|
3026
|
-
const record = asRecord(value);
|
|
3027
|
-
if (!record) return;
|
|
3028
|
-
return {
|
|
3029
|
-
createdAt: stringValue(record.created_at),
|
|
3030
|
-
id,
|
|
3031
|
-
title: stringValue(record.title),
|
|
3032
|
-
updatedAt: stringValue(record.updated_at)
|
|
3033
|
-
};
|
|
3395
|
+
function buildAutomationActionRunId(match, actionId) {
|
|
3396
|
+
return `${match.id}:${actionId}`;
|
|
3034
3397
|
}
|
|
3035
|
-
function
|
|
3036
|
-
|
|
3037
|
-
return value.flatMap((segment) => {
|
|
3038
|
-
const record = asRecord(segment);
|
|
3039
|
-
if (!record) return [];
|
|
3040
|
-
return [{
|
|
3041
|
-
documentId: stringValue(record.document_id),
|
|
3042
|
-
endTimestamp: stringValue(record.end_timestamp),
|
|
3043
|
-
id: stringValue(record.id),
|
|
3044
|
-
isFinal: Boolean(record.is_final),
|
|
3045
|
-
source: stringValue(record.source),
|
|
3046
|
-
startTimestamp: stringValue(record.start_timestamp),
|
|
3047
|
-
text: stringValue(record.text)
|
|
3048
|
-
}];
|
|
3049
|
-
});
|
|
3398
|
+
function enabledAutomationActions(rule) {
|
|
3399
|
+
return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
|
|
3050
3400
|
}
|
|
3051
|
-
function
|
|
3052
|
-
const outer = parseJsonString(contents);
|
|
3053
|
-
if (!outer) throw new Error("failed to parse cache JSON");
|
|
3054
|
-
const rawCache = outer.cache;
|
|
3055
|
-
let cachePayload;
|
|
3056
|
-
if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
|
|
3057
|
-
else cachePayload = asRecord(rawCache);
|
|
3058
|
-
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
3059
|
-
if (!state) throw new Error("failed to parse cache state");
|
|
3060
|
-
const rawDocuments = asRecord(state.documents) ?? {};
|
|
3061
|
-
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
3062
|
-
const documents = {};
|
|
3063
|
-
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
3064
|
-
const document = parseCacheDocument(id, rawDocument);
|
|
3065
|
-
if (document) documents[id] = document;
|
|
3066
|
-
}
|
|
3067
|
-
const transcripts = {};
|
|
3068
|
-
for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
|
|
3069
|
-
const segments = parseTranscriptSegments(rawTranscript);
|
|
3070
|
-
if (segments) transcripts[id] = segments;
|
|
3071
|
-
}
|
|
3401
|
+
function baseRun(match, rule, action, startedAt) {
|
|
3072
3402
|
return {
|
|
3073
|
-
|
|
3074
|
-
|
|
3403
|
+
actionId: action.id,
|
|
3404
|
+
actionKind: action.kind,
|
|
3405
|
+
actionName: automationActionName(action),
|
|
3406
|
+
eventId: match.eventId,
|
|
3407
|
+
eventKind: match.eventKind,
|
|
3408
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
3409
|
+
id: buildAutomationActionRunId(match, action.id),
|
|
3410
|
+
matchedAt: match.matchedAt,
|
|
3411
|
+
meetingId: match.meetingId,
|
|
3412
|
+
ruleId: rule.id,
|
|
3413
|
+
ruleName: rule.name,
|
|
3414
|
+
startedAt,
|
|
3415
|
+
status: "completed",
|
|
3416
|
+
tags: [...match.tags],
|
|
3417
|
+
title: match.title,
|
|
3418
|
+
transcriptLoaded: match.transcriptLoaded
|
|
3075
3419
|
};
|
|
3076
3420
|
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
3085
|
-
function numberValue(value) {
|
|
3086
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3421
|
+
function completedRun(run, finishedAt, patch = {}) {
|
|
3422
|
+
return {
|
|
3423
|
+
...run,
|
|
3424
|
+
...patch,
|
|
3425
|
+
finishedAt,
|
|
3426
|
+
status: "completed"
|
|
3427
|
+
};
|
|
3087
3428
|
}
|
|
3088
|
-
function
|
|
3089
|
-
const accessToken = stringValue(record.access_token);
|
|
3090
|
-
if (!accessToken.trim()) return;
|
|
3429
|
+
function failedRun(run, finishedAt, error) {
|
|
3091
3430
|
return {
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
obtainedAt: stringValue(record.obtained_at) || void 0,
|
|
3097
|
-
refreshToken: stringValue(record.refresh_token) || void 0,
|
|
3098
|
-
sessionId: stringValue(record.session_id) || void 0,
|
|
3099
|
-
signInMethod: stringValue(record.sign_in_method) || void 0,
|
|
3100
|
-
tokenType: stringValue(record.token_type) || void 0
|
|
3431
|
+
...run,
|
|
3432
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3433
|
+
finishedAt,
|
|
3434
|
+
status: "failed"
|
|
3101
3435
|
};
|
|
3102
3436
|
}
|
|
3103
|
-
function
|
|
3104
|
-
|
|
3105
|
-
|
|
3437
|
+
function skippedRun(run, finishedAt, reason) {
|
|
3438
|
+
return {
|
|
3439
|
+
...run,
|
|
3440
|
+
finishedAt,
|
|
3441
|
+
result: reason,
|
|
3442
|
+
status: "skipped"
|
|
3443
|
+
};
|
|
3106
3444
|
}
|
|
3107
|
-
function
|
|
3108
|
-
const
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3445
|
+
async function executeAutomationAction(match, rule, action, handlers) {
|
|
3446
|
+
const run = baseRun(match, rule, action, handlers.nowIso());
|
|
3447
|
+
switch (action.kind) {
|
|
3448
|
+
case "agent": try {
|
|
3449
|
+
const result = await handlers.runAgent(match, rule, action);
|
|
3450
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3451
|
+
meta: {
|
|
3452
|
+
command: result.command,
|
|
3453
|
+
dryRun: result.dryRun,
|
|
3454
|
+
model: result.model,
|
|
3455
|
+
provider: result.provider,
|
|
3456
|
+
systemPrompt: result.systemPrompt
|
|
3457
|
+
},
|
|
3458
|
+
prompt: result.prompt,
|
|
3459
|
+
result: result.output ?? (result.dryRun ? "Dry run: provider request not executed" : "")
|
|
3460
|
+
});
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3463
|
+
}
|
|
3464
|
+
case "ask-user": return {
|
|
3465
|
+
...run,
|
|
3466
|
+
meta: action.details ? { details: action.details } : void 0,
|
|
3467
|
+
prompt: action.prompt,
|
|
3468
|
+
result: "Pending user decision",
|
|
3469
|
+
status: "pending"
|
|
3470
|
+
};
|
|
3471
|
+
case "command": try {
|
|
3472
|
+
const result = await handlers.runCommand(match, rule, action);
|
|
3473
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3474
|
+
meta: {
|
|
3475
|
+
command: result.command,
|
|
3476
|
+
cwd: result.cwd
|
|
3477
|
+
},
|
|
3478
|
+
result: result.output
|
|
3479
|
+
});
|
|
3480
|
+
} catch (error) {
|
|
3481
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3482
|
+
}
|
|
3483
|
+
case "export-notes": try {
|
|
3484
|
+
const result = await handlers.exportNotes(match, action);
|
|
3485
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
|
|
3486
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3487
|
+
meta: {
|
|
3488
|
+
format: result.format,
|
|
3489
|
+
outputDir: result.outputDir,
|
|
3490
|
+
scope: result.scope,
|
|
3491
|
+
written: result.written
|
|
3492
|
+
},
|
|
3493
|
+
result: `Exported notes to ${result.outputDir}`
|
|
3494
|
+
});
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3497
|
+
}
|
|
3498
|
+
case "export-transcript": try {
|
|
3499
|
+
const result = await handlers.exportTranscripts(match, action);
|
|
3500
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
|
|
3501
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3502
|
+
meta: {
|
|
3503
|
+
format: result.format,
|
|
3504
|
+
outputDir: result.outputDir,
|
|
3505
|
+
scope: result.scope,
|
|
3506
|
+
written: result.written
|
|
3507
|
+
},
|
|
3508
|
+
result: `Exported transcript to ${result.outputDir}`
|
|
3509
|
+
});
|
|
3510
|
+
} catch (error) {
|
|
3511
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3117
3514
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3515
|
+
//#endregion
|
|
3516
|
+
//#region src/automation-matches.ts
|
|
3517
|
+
function cloneMatch(match) {
|
|
3518
|
+
return {
|
|
3519
|
+
...match,
|
|
3520
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
3521
|
+
tags: [...match.tags]
|
|
3522
|
+
};
|
|
3120
3523
|
}
|
|
3121
|
-
var
|
|
3122
|
-
constructor(filePath) {
|
|
3123
|
-
this.filePath = filePath;
|
|
3124
|
-
}
|
|
3125
|
-
async loadAccessToken() {
|
|
3126
|
-
return getAccessTokenFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
3127
|
-
}
|
|
3128
|
-
};
|
|
3129
|
-
var SupabaseFileSessionSource = class {
|
|
3130
|
-
constructor(filePath) {
|
|
3131
|
-
this.filePath = filePath;
|
|
3132
|
-
}
|
|
3133
|
-
async loadSession() {
|
|
3134
|
-
return getSessionFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
3135
|
-
}
|
|
3136
|
-
};
|
|
3137
|
-
var NoopTokenStore = class {
|
|
3138
|
-
async clearToken() {}
|
|
3139
|
-
async readToken() {}
|
|
3140
|
-
async writeToken(_token) {}
|
|
3141
|
-
};
|
|
3142
|
-
var FileSessionStore = class {
|
|
3143
|
-
constructor(filePath = defaultSessionFilePath()) {
|
|
3524
|
+
var FileAutomationMatchStore = class {
|
|
3525
|
+
constructor(filePath = defaultAutomationMatchesFilePath()) {
|
|
3144
3526
|
this.filePath = filePath;
|
|
3145
3527
|
}
|
|
3146
|
-
async
|
|
3147
|
-
|
|
3148
|
-
await unlink(this.filePath);
|
|
3149
|
-
} catch {}
|
|
3150
|
-
}
|
|
3151
|
-
async readSession() {
|
|
3152
|
-
try {
|
|
3153
|
-
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
3154
|
-
return parsed?.accessToken ? parsed : void 0;
|
|
3155
|
-
} catch {
|
|
3156
|
-
return;
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
async writeSession(session) {
|
|
3528
|
+
async appendMatches(matches) {
|
|
3529
|
+
if (matches.length === 0) return;
|
|
3160
3530
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3161
|
-
|
|
3531
|
+
const payload = matches.map((match) => JSON.stringify(match)).join("\n");
|
|
3532
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3162
3533
|
encoding: "utf8",
|
|
3163
3534
|
mode: 384
|
|
3164
3535
|
});
|
|
3165
3536
|
}
|
|
3166
|
-
|
|
3167
|
-
var FileApiKeyStore = class {
|
|
3168
|
-
constructor(filePath = defaultApiKeyFilePath()) {
|
|
3169
|
-
this.filePath = filePath;
|
|
3170
|
-
}
|
|
3171
|
-
async clearApiKey() {
|
|
3172
|
-
try {
|
|
3173
|
-
await unlink(this.filePath);
|
|
3174
|
-
} catch {}
|
|
3175
|
-
}
|
|
3176
|
-
async readApiKey() {
|
|
3537
|
+
async readMatches(limit = 50) {
|
|
3177
3538
|
try {
|
|
3178
|
-
|
|
3539
|
+
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);
|
|
3540
|
+
return (limit > 0 ? matches.slice(-limit) : matches).reverse();
|
|
3179
3541
|
} catch {
|
|
3180
|
-
return;
|
|
3542
|
+
return [];
|
|
3181
3543
|
}
|
|
3182
3544
|
}
|
|
3183
|
-
async writeApiKey(apiKey) {
|
|
3184
|
-
const trimmed = apiKey.trim();
|
|
3185
|
-
if (!trimmed) throw new Error("Granola API key is required");
|
|
3186
|
-
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3187
|
-
await writeFile(this.filePath, `${trimmed}\n`, {
|
|
3188
|
-
encoding: "utf8",
|
|
3189
|
-
mode: 384
|
|
3190
|
-
});
|
|
3191
|
-
}
|
|
3192
3545
|
};
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3546
|
+
function defaultAutomationMatchesFilePath() {
|
|
3547
|
+
return defaultGranolaToolkitPersistenceLayout().automationMatchesFile;
|
|
3548
|
+
}
|
|
3549
|
+
function createDefaultAutomationMatchStore(filePath) {
|
|
3550
|
+
return new FileAutomationMatchStore(filePath);
|
|
3551
|
+
}
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region src/automation-runs.ts
|
|
3554
|
+
function cloneRun(run) {
|
|
3555
|
+
return {
|
|
3556
|
+
...run,
|
|
3557
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
3558
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
3559
|
+
tags: [...run.tags]
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
function sortRuns(runs) {
|
|
3563
|
+
return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
3564
|
+
}
|
|
3565
|
+
function mergeRuns(runs) {
|
|
3566
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3567
|
+
for (const run of runs) byId.set(run.id, cloneRun(run));
|
|
3568
|
+
return sortRuns([...byId.values()]);
|
|
3569
|
+
}
|
|
3570
|
+
var FileAutomationRunStore = class {
|
|
3571
|
+
constructor(filePath = defaultAutomationRunsFilePath()) {
|
|
3572
|
+
this.filePath = filePath;
|
|
3220
3573
|
}
|
|
3221
|
-
async
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
"-w",
|
|
3230
|
-
JSON.stringify(session)
|
|
3231
|
-
]);
|
|
3574
|
+
async appendRuns(runs) {
|
|
3575
|
+
if (runs.length === 0) return;
|
|
3576
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3577
|
+
const payload = runs.map((run) => JSON.stringify(run)).join("\n");
|
|
3578
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3579
|
+
encoding: "utf8",
|
|
3580
|
+
mode: 384
|
|
3581
|
+
});
|
|
3232
3582
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
async clearApiKey() {
|
|
3236
|
-
try {
|
|
3237
|
-
await execFileAsync$1("security", [
|
|
3238
|
-
"delete-generic-password",
|
|
3239
|
-
"-s",
|
|
3240
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3241
|
-
"-a",
|
|
3242
|
-
KEYCHAIN_ACCOUNT_NAME_API_KEY
|
|
3243
|
-
]);
|
|
3244
|
-
} catch {}
|
|
3583
|
+
async readRun(id) {
|
|
3584
|
+
return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
|
|
3245
3585
|
}
|
|
3246
|
-
async
|
|
3586
|
+
async readRuns(options = {}) {
|
|
3247
3587
|
try {
|
|
3248
|
-
const
|
|
3249
|
-
|
|
3250
|
-
"-s",
|
|
3251
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3252
|
-
"-a",
|
|
3253
|
-
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
3254
|
-
"-w"
|
|
3255
|
-
]);
|
|
3256
|
-
return stdout.trim() || void 0;
|
|
3588
|
+
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);
|
|
3589
|
+
return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
|
|
3257
3590
|
} catch {
|
|
3258
|
-
return;
|
|
3591
|
+
return [];
|
|
3259
3592
|
}
|
|
3260
3593
|
}
|
|
3261
|
-
async writeApiKey(apiKey) {
|
|
3262
|
-
const trimmed = apiKey.trim();
|
|
3263
|
-
if (!trimmed) throw new Error("Granola API key is required");
|
|
3264
|
-
await execFileAsync$1("security", [
|
|
3265
|
-
"add-generic-password",
|
|
3266
|
-
"-U",
|
|
3267
|
-
"-s",
|
|
3268
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3269
|
-
"-a",
|
|
3270
|
-
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
3271
|
-
"-w",
|
|
3272
|
-
trimmed
|
|
3273
|
-
]);
|
|
3274
|
-
}
|
|
3275
3594
|
};
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3595
|
+
function defaultAutomationRunsFilePath() {
|
|
3596
|
+
return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
|
|
3597
|
+
}
|
|
3598
|
+
function createDefaultAutomationRunStore(filePath) {
|
|
3599
|
+
return new FileAutomationRunStore(filePath);
|
|
3600
|
+
}
|
|
3601
|
+
//#endregion
|
|
3602
|
+
//#region src/automation-rules.ts
|
|
3603
|
+
function cloneRule(rule) {
|
|
3604
|
+
return {
|
|
3605
|
+
...rule,
|
|
3606
|
+
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
3607
|
+
when: {
|
|
3608
|
+
...rule.when,
|
|
3609
|
+
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
3610
|
+
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
3611
|
+
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
3612
|
+
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
3613
|
+
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
3614
|
+
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
3288
3615
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
this.store = store;
|
|
3303
|
-
this.options = options;
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
function cloneAction(action) {
|
|
3619
|
+
switch (action.kind) {
|
|
3620
|
+
case "agent": return { ...action };
|
|
3621
|
+
case "ask-user": return { ...action };
|
|
3622
|
+
case "command": return {
|
|
3623
|
+
...action,
|
|
3624
|
+
args: action.args ? [...action.args] : void 0,
|
|
3625
|
+
env: action.env ? { ...action.env } : void 0
|
|
3626
|
+
};
|
|
3627
|
+
case "export-notes":
|
|
3628
|
+
case "export-transcript": return { ...action };
|
|
3304
3629
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3630
|
+
}
|
|
3631
|
+
function stringArray(value) {
|
|
3632
|
+
if (!Array.isArray(value)) return;
|
|
3633
|
+
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
3634
|
+
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
3635
|
+
}
|
|
3636
|
+
function stringRecord(value) {
|
|
3637
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3638
|
+
const entries = Object.entries(value).filter(([key, item]) => {
|
|
3639
|
+
return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
|
|
3640
|
+
});
|
|
3641
|
+
if (entries.length === 0) return;
|
|
3642
|
+
return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
|
|
3643
|
+
}
|
|
3644
|
+
function parseAction(value, index) {
|
|
3645
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3646
|
+
const record = value;
|
|
3647
|
+
const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
|
|
3648
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
|
|
3649
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
3650
|
+
const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
|
|
3651
|
+
switch (kind) {
|
|
3652
|
+
case "agent": {
|
|
3653
|
+
if (!id) return;
|
|
3654
|
+
const provider = record.provider === "codex" || record.provider === "openai" || record.provider === "openrouter" ? record.provider : void 0;
|
|
3655
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
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;
|
|
3658
|
+
const systemPrompt = typeof record.systemPrompt === "string" && record.systemPrompt.trim() ? record.systemPrompt.trim() : void 0;
|
|
3659
|
+
const systemPromptFile = typeof record.systemPromptFile === "string" && record.systemPromptFile.trim() ? record.systemPromptFile.trim() : void 0;
|
|
3660
|
+
if (!prompt && !promptFile && !harnessId) return;
|
|
3661
|
+
return {
|
|
3662
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
3663
|
+
dryRun: typeof record.dryRun === "boolean" ? record.dryRun : void 0,
|
|
3664
|
+
enabled,
|
|
3665
|
+
harnessId,
|
|
3666
|
+
id,
|
|
3667
|
+
kind,
|
|
3668
|
+
model: typeof record.model === "string" && record.model.trim() ? record.model.trim() : void 0,
|
|
3669
|
+
name,
|
|
3670
|
+
prompt,
|
|
3671
|
+
promptFile,
|
|
3672
|
+
provider,
|
|
3673
|
+
retries: typeof record.retries === "number" && Number.isFinite(record.retries) ? record.retries : typeof record.retries === "string" && /^\d+$/.test(record.retries) ? Number(record.retries) : void 0,
|
|
3674
|
+
systemPrompt,
|
|
3675
|
+
systemPromptFile,
|
|
3676
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
3677
|
+
};
|
|
3311
3678
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3679
|
+
case "ask-user": {
|
|
3680
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
3681
|
+
if (!id || !prompt) return;
|
|
3682
|
+
return {
|
|
3683
|
+
details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
|
|
3684
|
+
enabled,
|
|
3685
|
+
id,
|
|
3686
|
+
kind,
|
|
3687
|
+
name,
|
|
3688
|
+
prompt
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
case "command": {
|
|
3692
|
+
const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
|
|
3693
|
+
if (!id || !command) return;
|
|
3694
|
+
return {
|
|
3695
|
+
args: stringArray(record.args),
|
|
3696
|
+
command,
|
|
3697
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
3698
|
+
enabled,
|
|
3699
|
+
env: stringRecord(record.env),
|
|
3700
|
+
id,
|
|
3701
|
+
kind,
|
|
3702
|
+
name,
|
|
3703
|
+
stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
|
|
3704
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
case "export-notes":
|
|
3708
|
+
if (!id) return;
|
|
3709
|
+
return {
|
|
3710
|
+
enabled,
|
|
3711
|
+
format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
|
|
3712
|
+
id,
|
|
3713
|
+
kind,
|
|
3714
|
+
name,
|
|
3715
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
3716
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
3717
|
+
};
|
|
3718
|
+
case "export-transcript":
|
|
3719
|
+
if (!id) return;
|
|
3720
|
+
return {
|
|
3721
|
+
enabled,
|
|
3722
|
+
format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
|
|
3723
|
+
id,
|
|
3724
|
+
kind,
|
|
3725
|
+
name,
|
|
3726
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
3727
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
3728
|
+
};
|
|
3729
|
+
default: return;
|
|
3319
3730
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3731
|
+
}
|
|
3732
|
+
function parseRule(value) {
|
|
3733
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3734
|
+
const record = value;
|
|
3735
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
|
|
3736
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
3737
|
+
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
3738
|
+
if (!id || !name || !whenValue) return;
|
|
3739
|
+
return {
|
|
3740
|
+
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
3741
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
3742
|
+
id,
|
|
3743
|
+
name,
|
|
3744
|
+
when: {
|
|
3745
|
+
eventKinds: stringArray(whenValue.eventKinds),
|
|
3746
|
+
folderIds: stringArray(whenValue.folderIds),
|
|
3747
|
+
folderNames: stringArray(whenValue.folderNames),
|
|
3748
|
+
meetingIds: stringArray(whenValue.meetingIds),
|
|
3749
|
+
tags: stringArray(whenValue.tags),
|
|
3750
|
+
titleIncludes: stringArray(whenValue.titleIncludes),
|
|
3751
|
+
titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
|
|
3752
|
+
transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
|
|
3333
3753
|
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
var FileAutomationRuleStore = class {
|
|
3757
|
+
constructor(filePath = defaultAutomationRulesFilePath()) {
|
|
3758
|
+
this.filePath = filePath;
|
|
3759
|
+
}
|
|
3760
|
+
async readRules() {
|
|
3761
|
+
try {
|
|
3762
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
3763
|
+
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);
|
|
3764
|
+
} catch {
|
|
3765
|
+
return [];
|
|
3339
3766
|
}
|
|
3340
|
-
this.#session = void 0;
|
|
3341
|
-
await this.store.clearSession();
|
|
3342
3767
|
}
|
|
3343
3768
|
};
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
const response = await fetchImpl(WORKOS_AUTH_URL, {
|
|
3347
|
-
body: JSON.stringify({
|
|
3348
|
-
client_id: session.clientId,
|
|
3349
|
-
grant_type: "refresh_token",
|
|
3350
|
-
refresh_token: session.refreshToken
|
|
3351
|
-
}),
|
|
3352
|
-
headers: { "Content-Type": "application/json" },
|
|
3353
|
-
method: "POST"
|
|
3354
|
-
});
|
|
3355
|
-
if (!response.ok) throw new Error(`failed to refresh session: ${response.status} ${response.statusText}`);
|
|
3356
|
-
const refreshed = parseSessionRecord(await response.json());
|
|
3357
|
-
if (!refreshed) throw new Error("failed to parse refreshed session");
|
|
3358
|
-
return {
|
|
3359
|
-
...session,
|
|
3360
|
-
...refreshed,
|
|
3361
|
-
clientId: refreshed.clientId || session.clientId,
|
|
3362
|
-
obtainedAt: refreshed.obtainedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3363
|
-
refreshToken: refreshed.refreshToken ?? session.refreshToken
|
|
3364
|
-
};
|
|
3769
|
+
function defaultAutomationRulesFilePath() {
|
|
3770
|
+
return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
|
|
3365
3771
|
}
|
|
3366
|
-
function
|
|
3367
|
-
|
|
3772
|
+
function includesIgnoreCase(candidate, values) {
|
|
3773
|
+
const lowerCandidate = candidate.toLowerCase();
|
|
3774
|
+
return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
|
|
3368
3775
|
}
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3776
|
+
function matchesRule(rule, event) {
|
|
3777
|
+
if (rule.enabled === false) return false;
|
|
3778
|
+
const { when } = rule;
|
|
3779
|
+
if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
|
|
3780
|
+
if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
|
|
3781
|
+
if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
|
|
3782
|
+
if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
|
|
3783
|
+
if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
|
|
3784
|
+
if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
|
|
3785
|
+
if (when.titleMatches) try {
|
|
3786
|
+
if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
|
|
3787
|
+
} catch {
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
|
|
3791
|
+
return true;
|
|
3371
3792
|
}
|
|
3372
|
-
function
|
|
3373
|
-
|
|
3793
|
+
function matchAutomationRules(rules, events, matchedAt) {
|
|
3794
|
+
const matches = [];
|
|
3795
|
+
for (const event of events) for (const rule of rules) {
|
|
3796
|
+
if (!matchesRule(rule, event)) continue;
|
|
3797
|
+
matches.push({
|
|
3798
|
+
eventId: event.id,
|
|
3799
|
+
eventKind: event.kind,
|
|
3800
|
+
folders: event.folders.map((folder) => ({ ...folder })),
|
|
3801
|
+
id: `${event.id}:${rule.id}`,
|
|
3802
|
+
matchedAt,
|
|
3803
|
+
meetingId: event.meetingId,
|
|
3804
|
+
ruleId: rule.id,
|
|
3805
|
+
ruleName: rule.name,
|
|
3806
|
+
tags: [...event.tags],
|
|
3807
|
+
title: event.title,
|
|
3808
|
+
transcriptLoaded: event.transcriptLoaded
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
return matches;
|
|
3374
3812
|
}
|
|
3375
|
-
function
|
|
3376
|
-
return
|
|
3813
|
+
function createDefaultAutomationRuleStore(filePath) {
|
|
3814
|
+
return new FileAutomationRuleStore(filePath);
|
|
3815
|
+
}
|
|
3816
|
+
//#endregion
|
|
3817
|
+
//#region src/cache.ts
|
|
3818
|
+
function parseCacheDocument(id, value) {
|
|
3819
|
+
const record = asRecord(value);
|
|
3820
|
+
if (!record) return;
|
|
3821
|
+
return {
|
|
3822
|
+
createdAt: stringValue(record.created_at),
|
|
3823
|
+
id,
|
|
3824
|
+
title: stringValue(record.title),
|
|
3825
|
+
updatedAt: stringValue(record.updated_at)
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
function parseTranscriptSegments(value) {
|
|
3829
|
+
if (!Array.isArray(value)) return;
|
|
3830
|
+
return value.flatMap((segment) => {
|
|
3831
|
+
const record = asRecord(segment);
|
|
3832
|
+
if (!record) return [];
|
|
3833
|
+
return [{
|
|
3834
|
+
documentId: stringValue(record.document_id),
|
|
3835
|
+
endTimestamp: stringValue(record.end_timestamp),
|
|
3836
|
+
id: stringValue(record.id),
|
|
3837
|
+
isFinal: Boolean(record.is_final),
|
|
3838
|
+
source: stringValue(record.source),
|
|
3839
|
+
startTimestamp: stringValue(record.start_timestamp),
|
|
3840
|
+
text: stringValue(record.text)
|
|
3841
|
+
}];
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
function parseCacheContents(contents) {
|
|
3845
|
+
const outer = parseJsonString(contents);
|
|
3846
|
+
if (!outer) throw new Error("failed to parse cache JSON");
|
|
3847
|
+
const rawCache = outer.cache;
|
|
3848
|
+
let cachePayload;
|
|
3849
|
+
if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
|
|
3850
|
+
else cachePayload = asRecord(rawCache);
|
|
3851
|
+
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
3852
|
+
if (!state) throw new Error("failed to parse cache state");
|
|
3853
|
+
const rawDocuments = asRecord(state.documents) ?? {};
|
|
3854
|
+
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
3855
|
+
const documents = {};
|
|
3856
|
+
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
3857
|
+
const document = parseCacheDocument(id, rawDocument);
|
|
3858
|
+
if (document) documents[id] = document;
|
|
3859
|
+
}
|
|
3860
|
+
const transcripts = {};
|
|
3861
|
+
for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
|
|
3862
|
+
const segments = parseTranscriptSegments(rawTranscript);
|
|
3863
|
+
if (segments) transcripts[id] = segments;
|
|
3864
|
+
}
|
|
3865
|
+
return {
|
|
3866
|
+
documents,
|
|
3867
|
+
transcripts
|
|
3868
|
+
};
|
|
3377
3869
|
}
|
|
3378
3870
|
//#endregion
|
|
3379
3871
|
//#region src/client/default-auth.ts
|
|
@@ -3562,17 +4054,39 @@ function parseLastViewedPanel(value) {
|
|
|
3562
4054
|
updatedAt: stringValue(panel.updated_at)
|
|
3563
4055
|
};
|
|
3564
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
|
+
}
|
|
3565
4078
|
function parseDocument(value) {
|
|
3566
4079
|
const record = asRecord(value);
|
|
3567
4080
|
if (!record) throw new Error("document payload is not an object");
|
|
3568
4081
|
return {
|
|
4082
|
+
calendarEvent: parseCalendarEvent(record.google_calendar_event),
|
|
3569
4083
|
content: stringValue(record.content),
|
|
3570
4084
|
createdAt: stringValue(record.created_at),
|
|
3571
4085
|
id: stringValue(record.id),
|
|
3572
4086
|
lastViewedPanel: parseLastViewedPanel(record.last_viewed_panel),
|
|
3573
4087
|
notes: parseProseMirrorDoc(record.notes),
|
|
3574
4088
|
notesPlain: stringValue(record.notes_plain),
|
|
3575
|
-
tags: stringArray$
|
|
4089
|
+
tags: stringArray$2(record.tags),
|
|
3576
4090
|
title: stringValue(record.title),
|
|
3577
4091
|
updatedAt: stringValue(record.updated_at)
|
|
3578
4092
|
};
|
|
@@ -3623,6 +4137,7 @@ function parsePublicNote(value) {
|
|
|
3623
4137
|
const summaryMarkdown = stringValue(record.summary_markdown);
|
|
3624
4138
|
const summaryText = stringValue(record.summary_text);
|
|
3625
4139
|
return {
|
|
4140
|
+
calendarEvent: parseCalendarEvent(record.google_calendar_event),
|
|
3626
4141
|
content: summaryMarkdown || summaryText,
|
|
3627
4142
|
createdAt: stringValue(record.created_at),
|
|
3628
4143
|
folderMemberships: Array.isArray(record.folder_membership) ? record.folder_membership.map(parseFolderMembership).filter((membership) => Boolean(membership)) : [],
|
|
@@ -3829,97 +4344,6 @@ var GranolaPublicApiClient = class {
|
|
|
3829
4344
|
}
|
|
3830
4345
|
};
|
|
3831
4346
|
//#endregion
|
|
3832
|
-
//#region src/client/http.ts
|
|
3833
|
-
const RETRYABLE_STATUS_CODES = new Set([
|
|
3834
|
-
429,
|
|
3835
|
-
500,
|
|
3836
|
-
502,
|
|
3837
|
-
503,
|
|
3838
|
-
504
|
|
3839
|
-
]);
|
|
3840
|
-
function sleep(delayMs) {
|
|
3841
|
-
return new Promise((resolve) => {
|
|
3842
|
-
setTimeout(resolve, delayMs);
|
|
3843
|
-
});
|
|
3844
|
-
}
|
|
3845
|
-
function parseRetryAfter(headerValue) {
|
|
3846
|
-
if (!headerValue?.trim()) return;
|
|
3847
|
-
if (/^\d+$/.test(headerValue.trim())) return Number(headerValue.trim()) * 1e3;
|
|
3848
|
-
const retryAt = Date.parse(headerValue);
|
|
3849
|
-
if (Number.isNaN(retryAt)) return;
|
|
3850
|
-
return Math.max(0, retryAt - Date.now());
|
|
3851
|
-
}
|
|
3852
|
-
var AuthenticatedHttpClient = class {
|
|
3853
|
-
fetchImpl;
|
|
3854
|
-
constructor(options) {
|
|
3855
|
-
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
3856
|
-
this.logger = options.logger;
|
|
3857
|
-
this.maxRetries = options.maxRetries ?? 2;
|
|
3858
|
-
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;
|
|
3859
|
-
this.retryMaxDelayMs = options.retryMaxDelayMs ?? 5e3;
|
|
3860
|
-
this.sleepImpl = options.sleepImpl ?? sleep;
|
|
3861
|
-
this.tokenProvider = options.tokenProvider;
|
|
3862
|
-
}
|
|
3863
|
-
logger;
|
|
3864
|
-
maxRetries;
|
|
3865
|
-
retryBaseDelayMs;
|
|
3866
|
-
retryMaxDelayMs;
|
|
3867
|
-
sleepImpl;
|
|
3868
|
-
tokenProvider;
|
|
3869
|
-
async retry(options, attempt, reason, response) {
|
|
3870
|
-
const retryAfterMs = parseRetryAfter(response?.headers.get("retry-after") ?? null);
|
|
3871
|
-
const delayMs = Math.min(retryAfterMs ?? this.retryBaseDelayMs * 2 ** attempt, this.retryMaxDelayMs);
|
|
3872
|
-
this.logger?.warn?.(`${reason}; retrying in ${delayMs}ms (${attempt + 1}/${this.maxRetries})`);
|
|
3873
|
-
await this.sleepImpl(delayMs);
|
|
3874
|
-
return this.request(options, attempt + 1);
|
|
3875
|
-
}
|
|
3876
|
-
async request(options, attempt = 0) {
|
|
3877
|
-
const { retryOnUnauthorized = true, timeoutMs, url } = options;
|
|
3878
|
-
const accessToken = await this.tokenProvider.getAccessToken();
|
|
3879
|
-
let response;
|
|
3880
|
-
try {
|
|
3881
|
-
response = await this.fetchImpl(url, {
|
|
3882
|
-
body: options.body,
|
|
3883
|
-
headers: {
|
|
3884
|
-
...options.headers,
|
|
3885
|
-
Authorization: `Bearer ${accessToken}`
|
|
3886
|
-
},
|
|
3887
|
-
method: options.method ?? "GET",
|
|
3888
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
3889
|
-
});
|
|
3890
|
-
} catch (error) {
|
|
3891
|
-
if (attempt < this.maxRetries) {
|
|
3892
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3893
|
-
return this.retry(options, attempt, `request failed: ${message}`);
|
|
3894
|
-
}
|
|
3895
|
-
throw error;
|
|
3896
|
-
}
|
|
3897
|
-
if (response.status === 401 && retryOnUnauthorized) {
|
|
3898
|
-
this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
|
|
3899
|
-
await this.tokenProvider.invalidate();
|
|
3900
|
-
return this.request({
|
|
3901
|
-
...options,
|
|
3902
|
-
retryOnUnauthorized: false
|
|
3903
|
-
}, attempt);
|
|
3904
|
-
}
|
|
3905
|
-
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) return this.retry(options, attempt, `request returned ${response.status} ${response.statusText || ""}`.trim(), response);
|
|
3906
|
-
return response;
|
|
3907
|
-
}
|
|
3908
|
-
async postJson(url, body, options = { timeoutMs: 3e4 }) {
|
|
3909
|
-
return this.request({
|
|
3910
|
-
...options,
|
|
3911
|
-
body: JSON.stringify(body),
|
|
3912
|
-
headers: {
|
|
3913
|
-
Accept: "*/*",
|
|
3914
|
-
"Content-Type": "application/json",
|
|
3915
|
-
...options.headers
|
|
3916
|
-
},
|
|
3917
|
-
method: "POST",
|
|
3918
|
-
url
|
|
3919
|
-
});
|
|
3920
|
-
}
|
|
3921
|
-
};
|
|
3922
|
-
//#endregion
|
|
3923
4347
|
//#region src/client/default.ts
|
|
3924
4348
|
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
3925
4349
|
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
@@ -4628,6 +5052,62 @@ function cloneMeetingSummary(meeting) {
|
|
|
4628
5052
|
tags: [...meeting.tags]
|
|
4629
5053
|
};
|
|
4630
5054
|
}
|
|
5055
|
+
function resolveActionFilePath(filePath, cwd) {
|
|
5056
|
+
return cwd ? resolve(cwd, filePath) : resolve(filePath);
|
|
5057
|
+
}
|
|
5058
|
+
async function readOptionalActionFile(filePath, cwd) {
|
|
5059
|
+
if (!filePath) return;
|
|
5060
|
+
return await readFile(resolveActionFilePath(filePath, cwd), "utf8");
|
|
5061
|
+
}
|
|
5062
|
+
function combinePromptSections(...values) {
|
|
5063
|
+
const sections = values.map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
5064
|
+
return sections.length > 0 ? sections.join("\n\n") : void 0;
|
|
5065
|
+
}
|
|
5066
|
+
function meetingTranscriptText(bundle) {
|
|
5067
|
+
const segments = bundle.document.transcriptSegments ?? (bundle.cacheData ? bundle.cacheData.transcripts[bundle.document.id] : void 0);
|
|
5068
|
+
if (!segments?.length) return;
|
|
5069
|
+
return segments.slice().sort((left, right) => left.startTimestamp.localeCompare(right.startTimestamp)).map((segment) => segment.text.trim()).filter(Boolean).join("\n");
|
|
5070
|
+
}
|
|
5071
|
+
function buildAutomationAgentPrompt(match, rule, instructions, bundle) {
|
|
5072
|
+
const transcriptText = bundle ? meetingTranscriptText(bundle)?.trim() : void 0;
|
|
5073
|
+
const context = {
|
|
5074
|
+
event: {
|
|
5075
|
+
id: match.eventId,
|
|
5076
|
+
kind: match.eventKind,
|
|
5077
|
+
matchedAt: match.matchedAt,
|
|
5078
|
+
meetingId: match.meetingId,
|
|
5079
|
+
transcriptLoaded: match.transcriptLoaded
|
|
5080
|
+
},
|
|
5081
|
+
folders: match.folders.map((folder) => ({
|
|
5082
|
+
id: folder.id,
|
|
5083
|
+
name: folder.name
|
|
5084
|
+
})),
|
|
5085
|
+
meeting: bundle ? {
|
|
5086
|
+
id: bundle.document.id,
|
|
5087
|
+
notesPlain: bundle.document.notesPlain,
|
|
5088
|
+
tags: [...bundle.document.tags],
|
|
5089
|
+
title: bundle.document.title,
|
|
5090
|
+
updatedAt: bundle.document.updatedAt
|
|
5091
|
+
} : {
|
|
5092
|
+
id: match.meetingId,
|
|
5093
|
+
tags: [...match.tags],
|
|
5094
|
+
title: match.title
|
|
5095
|
+
},
|
|
5096
|
+
rule: {
|
|
5097
|
+
id: rule.id,
|
|
5098
|
+
name: rule.name
|
|
5099
|
+
}
|
|
5100
|
+
};
|
|
5101
|
+
return [
|
|
5102
|
+
instructions.trim(),
|
|
5103
|
+
"Meeting context (JSON):",
|
|
5104
|
+
"```json",
|
|
5105
|
+
JSON.stringify(context, null, 2),
|
|
5106
|
+
"```",
|
|
5107
|
+
bundle?.document.notesPlain?.trim() ? `Existing notes:\n${bundle.document.notesPlain.trim()}` : "",
|
|
5108
|
+
transcriptText ? `Transcript:\n${transcriptText}` : ""
|
|
5109
|
+
].filter(Boolean).join("\n\n");
|
|
5110
|
+
}
|
|
4631
5111
|
function cloneState(state) {
|
|
4632
5112
|
return {
|
|
4633
5113
|
auth: { ...state.auth },
|
|
@@ -4636,6 +5116,7 @@ function cloneState(state) {
|
|
|
4636
5116
|
config: {
|
|
4637
5117
|
...state.config,
|
|
4638
5118
|
automation: state.config.automation ? { ...state.config.automation } : void 0,
|
|
5119
|
+
agents: state.config.agents ? { ...state.config.agents } : void 0,
|
|
4639
5120
|
notes: { ...state.config.notes },
|
|
4640
5121
|
transcripts: { ...state.config.transcripts }
|
|
4641
5122
|
},
|
|
@@ -4673,6 +5154,7 @@ function defaultState(config, auth, surface) {
|
|
|
4673
5154
|
},
|
|
4674
5155
|
config: {
|
|
4675
5156
|
...config,
|
|
5157
|
+
agents: config.agents ? { ...config.agents } : void 0,
|
|
4676
5158
|
notes: { ...config.notes },
|
|
4677
5159
|
transcripts: { ...config.transcripts }
|
|
4678
5160
|
},
|
|
@@ -4825,6 +5307,7 @@ var GranolaApp = class {
|
|
|
4825
5307
|
...rule,
|
|
4826
5308
|
actions: rule.actions?.map((action) => {
|
|
4827
5309
|
switch (action.kind) {
|
|
5310
|
+
case "agent": return { ...action };
|
|
4828
5311
|
case "ask-user": return { ...action };
|
|
4829
5312
|
case "command": return {
|
|
4830
5313
|
...action,
|
|
@@ -5373,6 +5856,31 @@ var GranolaApp = class {
|
|
|
5373
5856
|
child.stdin.end();
|
|
5374
5857
|
});
|
|
5375
5858
|
}
|
|
5859
|
+
async runAutomationAgent(match, rule, action) {
|
|
5860
|
+
const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
|
|
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);
|
|
5871
|
+
if (!instructions) throw new Error(`automation agent action ${action.id} is missing prompt instructions`);
|
|
5872
|
+
const request = {
|
|
5873
|
+
cwd: action.cwd ?? harnessCwd,
|
|
5874
|
+
dryRun: action.dryRun,
|
|
5875
|
+
model: action.model ?? harness?.model,
|
|
5876
|
+
prompt: buildAutomationAgentPrompt(match, rule, instructions, bundle),
|
|
5877
|
+
provider: action.provider ?? harness?.provider,
|
|
5878
|
+
retries: action.retries,
|
|
5879
|
+
systemPrompt: combinePromptSections(harnessSystemPromptFile, harness?.systemPrompt, systemPromptFile, action.systemPrompt),
|
|
5880
|
+
timeoutMs: action.timeoutMs
|
|
5881
|
+
};
|
|
5882
|
+
return await (this.deps.agentRunner ?? createDefaultAutomationAgentRunner(this.config)).run(request);
|
|
5883
|
+
}
|
|
5376
5884
|
async runAutomationActions(rules, matches) {
|
|
5377
5885
|
const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
|
|
5378
5886
|
const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
|
|
@@ -5388,6 +5896,7 @@ var GranolaApp = class {
|
|
|
5388
5896
|
exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
|
|
5389
5897
|
exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
|
|
5390
5898
|
nowIso: () => this.nowIso(),
|
|
5899
|
+
runAgent: async (nextMatch, nextRule, nextAction) => await this.runAutomationAgent(nextMatch, nextRule, nextAction),
|
|
5391
5900
|
runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
|
|
5392
5901
|
}));
|
|
5393
5902
|
}
|
|
@@ -5814,6 +6323,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
5814
6323
|
const automationRuns = await automationRunStore.readRuns({ limit: 0 });
|
|
5815
6324
|
const automationRuleStore = createDefaultAutomationRuleStore(config.automation?.rulesFile ?? defaultAutomationRulesFilePath());
|
|
5816
6325
|
const automationRules = await automationRuleStore.readRules();
|
|
6326
|
+
const agentHarnessStore = createDefaultAgentHarnessStore(config.agents?.harnessesFile);
|
|
5817
6327
|
const authController = createDefaultGranolaAuthController(config);
|
|
5818
6328
|
const exportJobStore = createDefaultExportJobStore();
|
|
5819
6329
|
const exportJobs = await exportJobStore.readJobs();
|
|
@@ -5826,6 +6336,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
5826
6336
|
const syncState = await syncStateStore.readState();
|
|
5827
6337
|
return new GranolaApp(config, {
|
|
5828
6338
|
auth,
|
|
6339
|
+
agentRunner: createDefaultAutomationAgentRunner(config),
|
|
6340
|
+
agentHarnessStore,
|
|
5829
6341
|
authController,
|
|
5830
6342
|
automationMatches,
|
|
5831
6343
|
automationMatchStore,
|
|
@@ -5855,6 +6367,10 @@ function pickString(value) {
|
|
|
5855
6367
|
function pickBoolean(value) {
|
|
5856
6368
|
return typeof value === "boolean" ? value : void 0;
|
|
5857
6369
|
}
|
|
6370
|
+
function pickNumber(value) {
|
|
6371
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
6372
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value.trim());
|
|
6373
|
+
}
|
|
5858
6374
|
function parseTomlScalar(rawValue) {
|
|
5859
6375
|
const value = rawValue.trim();
|
|
5860
6376
|
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) {
|
|
@@ -5907,9 +6423,24 @@ async function loadConfig(options) {
|
|
|
5907
6423
|
const configValues = config.values;
|
|
5908
6424
|
const defaultSupabase = firstExistingPath(granolaSupabaseCandidates());
|
|
5909
6425
|
const defaultCache = firstExistingPath(granolaCacheCandidates());
|
|
6426
|
+
const agentTimeoutValue = pickString(env.GRANOLA_AGENT_TIMEOUT) ?? pickString(configValues["agent-timeout"]) ?? pickString(configValues.agentTimeout) ?? "5m";
|
|
5910
6427
|
const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
|
|
5911
6428
|
return {
|
|
5912
6429
|
automation: { rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile },
|
|
6430
|
+
agents: {
|
|
6431
|
+
codexCommand: pickString(env.GRANOLA_CODEX_COMMAND) ?? pickString(configValues["codex-command"]) ?? pickString(configValues.codexCommand) ?? "codex",
|
|
6432
|
+
defaultModel: pickString(env.GRANOLA_AGENT_MODEL) ?? pickString(configValues["agent-model"]) ?? pickString(configValues.agentModel),
|
|
6433
|
+
defaultProvider: (() => {
|
|
6434
|
+
const value = pickString(env.GRANOLA_AGENT_PROVIDER) ?? pickString(configValues["agent-provider"]) ?? pickString(configValues.agentProvider);
|
|
6435
|
+
return value === "codex" || value === "openai" || value === "openrouter" ? value : void 0;
|
|
6436
|
+
})(),
|
|
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,
|
|
6439
|
+
maxRetries: pickNumber(env.GRANOLA_AGENT_MAX_RETRIES) ?? pickNumber(configValues["agent-max-retries"]) ?? pickNumber(configValues.agentMaxRetries) ?? 2,
|
|
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",
|
|
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",
|
|
6442
|
+
timeoutMs: parseDuration(agentTimeoutValue)
|
|
6443
|
+
},
|
|
5913
6444
|
apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
|
|
5914
6445
|
configFileUsed: config.path,
|
|
5915
6446
|
debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
|