granola-toolkit 0.46.2 → 0.47.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 +1128 -776
- 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 = {}) {
|
|
@@ -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");
|
|
@@ -2748,632 +2627,1103 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2748
2627
|
};
|
|
2749
2628
|
}
|
|
2750
2629
|
//#endregion
|
|
2751
|
-
//#region src/
|
|
2752
|
-
|
|
2630
|
+
//#region src/client/auth.ts
|
|
2631
|
+
const execFileAsync$1 = promisify(execFile);
|
|
2632
|
+
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
2633
|
+
const KEYCHAIN_SERVICE_NAME = "com.granola.toolkit";
|
|
2634
|
+
const KEYCHAIN_ACCOUNT_NAME_API_KEY = "api-key";
|
|
2635
|
+
const KEYCHAIN_ACCOUNT_NAME = "session";
|
|
2636
|
+
const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
2637
|
+
function numberValue(value) {
|
|
2638
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2639
|
+
}
|
|
2640
|
+
function parseSessionRecord(record) {
|
|
2641
|
+
const accessToken = stringValue(record.access_token);
|
|
2642
|
+
if (!accessToken.trim()) return;
|
|
2753
2643
|
return {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2644
|
+
accessToken,
|
|
2645
|
+
clientId: stringValue(record.client_id) || DEFAULT_CLIENT_ID,
|
|
2646
|
+
expiresIn: numberValue(record.expires_in),
|
|
2647
|
+
externalId: stringValue(record.external_id) || void 0,
|
|
2648
|
+
obtainedAt: stringValue(record.obtained_at) || void 0,
|
|
2649
|
+
refreshToken: stringValue(record.refresh_token) || void 0,
|
|
2650
|
+
sessionId: stringValue(record.session_id) || void 0,
|
|
2651
|
+
signInMethod: stringValue(record.sign_in_method) || void 0,
|
|
2652
|
+
tokenType: stringValue(record.token_type) || void 0
|
|
2757
2653
|
};
|
|
2758
2654
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2655
|
+
function parseNestedRecord(value) {
|
|
2656
|
+
if (typeof value === "string") return parseJsonString(value);
|
|
2657
|
+
return asRecord(value);
|
|
2658
|
+
}
|
|
2659
|
+
function getSessionFromSupabaseContents(supabaseContents) {
|
|
2660
|
+
const wrapper = parseJsonString(supabaseContents);
|
|
2661
|
+
if (!wrapper) throw new Error("failed to parse supabase.json");
|
|
2662
|
+
const workOsSession = parseSessionRecord(parseNestedRecord(wrapper.workos_tokens) ?? {});
|
|
2663
|
+
if (workOsSession) return workOsSession;
|
|
2664
|
+
const cognitoSession = parseSessionRecord(parseNestedRecord(wrapper.cognito_tokens) ?? {});
|
|
2665
|
+
if (cognitoSession) return cognitoSession;
|
|
2666
|
+
const legacySession = parseSessionRecord(wrapper);
|
|
2667
|
+
if (legacySession) return legacySession;
|
|
2668
|
+
throw new Error("access token not found in supabase.json");
|
|
2669
|
+
}
|
|
2670
|
+
function getAccessTokenFromSupabaseContents(supabaseContents) {
|
|
2671
|
+
return getSessionFromSupabaseContents(supabaseContents).accessToken;
|
|
2672
|
+
}
|
|
2673
|
+
var SupabaseFileTokenSource = class {
|
|
2674
|
+
constructor(filePath) {
|
|
2761
2675
|
this.filePath = filePath;
|
|
2762
2676
|
}
|
|
2763
|
-
async
|
|
2764
|
-
|
|
2765
|
-
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2766
|
-
const payload = matches.map((match) => JSON.stringify(match)).join("\n");
|
|
2767
|
-
await appendFile(this.filePath, `${payload}\n`, {
|
|
2768
|
-
encoding: "utf8",
|
|
2769
|
-
mode: 384
|
|
2770
|
-
});
|
|
2677
|
+
async loadAccessToken() {
|
|
2678
|
+
return getAccessTokenFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
2771
2679
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2680
|
+
};
|
|
2681
|
+
var SupabaseFileSessionSource = class {
|
|
2682
|
+
constructor(filePath) {
|
|
2683
|
+
this.filePath = filePath;
|
|
2684
|
+
}
|
|
2685
|
+
async loadSession() {
|
|
2686
|
+
return getSessionFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
2779
2687
|
}
|
|
2780
2688
|
};
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
//#region src/automation-runs.ts
|
|
2789
|
-
function cloneRun(run) {
|
|
2790
|
-
return {
|
|
2791
|
-
...run,
|
|
2792
|
-
folders: run.folders.map((folder) => ({ ...folder })),
|
|
2793
|
-
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
2794
|
-
tags: [...run.tags]
|
|
2795
|
-
};
|
|
2796
|
-
}
|
|
2797
|
-
function sortRuns(runs) {
|
|
2798
|
-
return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
2799
|
-
}
|
|
2800
|
-
function mergeRuns(runs) {
|
|
2801
|
-
const byId = /* @__PURE__ */ new Map();
|
|
2802
|
-
for (const run of runs) byId.set(run.id, cloneRun(run));
|
|
2803
|
-
return sortRuns([...byId.values()]);
|
|
2804
|
-
}
|
|
2805
|
-
var FileAutomationRunStore = class {
|
|
2806
|
-
constructor(filePath = defaultAutomationRunsFilePath()) {
|
|
2689
|
+
var NoopTokenStore = class {
|
|
2690
|
+
async clearToken() {}
|
|
2691
|
+
async readToken() {}
|
|
2692
|
+
async writeToken(_token) {}
|
|
2693
|
+
};
|
|
2694
|
+
var FileSessionStore = class {
|
|
2695
|
+
constructor(filePath = defaultSessionFilePath()) {
|
|
2807
2696
|
this.filePath = filePath;
|
|
2808
2697
|
}
|
|
2809
|
-
async
|
|
2810
|
-
|
|
2698
|
+
async clearSession() {
|
|
2699
|
+
try {
|
|
2700
|
+
await unlink(this.filePath);
|
|
2701
|
+
} catch {}
|
|
2702
|
+
}
|
|
2703
|
+
async readSession() {
|
|
2704
|
+
try {
|
|
2705
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
2706
|
+
return parsed?.accessToken ? parsed : void 0;
|
|
2707
|
+
} catch {
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
async writeSession(session) {
|
|
2811
2712
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2812
|
-
|
|
2813
|
-
await appendFile(this.filePath, `${payload}\n`, {
|
|
2713
|
+
await writeFile(this.filePath, `${JSON.stringify(session, null, 2)}\n`, {
|
|
2814
2714
|
encoding: "utf8",
|
|
2815
2715
|
mode: 384
|
|
2816
2716
|
});
|
|
2817
2717
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2718
|
+
};
|
|
2719
|
+
var FileApiKeyStore = class {
|
|
2720
|
+
constructor(filePath = defaultApiKeyFilePath()) {
|
|
2721
|
+
this.filePath = filePath;
|
|
2820
2722
|
}
|
|
2821
|
-
async
|
|
2723
|
+
async clearApiKey() {
|
|
2822
2724
|
try {
|
|
2823
|
-
|
|
2824
|
-
|
|
2725
|
+
await unlink(this.filePath);
|
|
2726
|
+
} catch {}
|
|
2727
|
+
}
|
|
2728
|
+
async readApiKey() {
|
|
2729
|
+
try {
|
|
2730
|
+
return (await readFile(this.filePath, "utf8")).trim() || void 0;
|
|
2825
2731
|
} catch {
|
|
2826
|
-
return
|
|
2732
|
+
return;
|
|
2827
2733
|
}
|
|
2828
2734
|
}
|
|
2735
|
+
async writeApiKey(apiKey) {
|
|
2736
|
+
const trimmed = apiKey.trim();
|
|
2737
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2738
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2739
|
+
await writeFile(this.filePath, `${trimmed}\n`, {
|
|
2740
|
+
encoding: "utf8",
|
|
2741
|
+
mode: 384
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2829
2744
|
};
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
2842
|
-
when: {
|
|
2843
|
-
...rule.when,
|
|
2844
|
-
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
2845
|
-
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
2846
|
-
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
2847
|
-
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
2848
|
-
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
2849
|
-
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
2850
|
-
}
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
function cloneAction(action) {
|
|
2854
|
-
switch (action.kind) {
|
|
2855
|
-
case "ask-user": return { ...action };
|
|
2856
|
-
case "command": return {
|
|
2857
|
-
...action,
|
|
2858
|
-
args: action.args ? [...action.args] : void 0,
|
|
2859
|
-
env: action.env ? { ...action.env } : void 0
|
|
2860
|
-
};
|
|
2861
|
-
case "export-notes":
|
|
2862
|
-
case "export-transcript": return { ...action };
|
|
2745
|
+
var KeychainSessionStore = class {
|
|
2746
|
+
async clearSession() {
|
|
2747
|
+
try {
|
|
2748
|
+
await execFileAsync$1("security", [
|
|
2749
|
+
"delete-generic-password",
|
|
2750
|
+
"-s",
|
|
2751
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2752
|
+
"-a",
|
|
2753
|
+
KEYCHAIN_ACCOUNT_NAME
|
|
2754
|
+
]);
|
|
2755
|
+
} catch {}
|
|
2863
2756
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
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
|
-
};
|
|
2757
|
+
async readSession() {
|
|
2758
|
+
try {
|
|
2759
|
+
const { stdout } = await execFileAsync$1("security", [
|
|
2760
|
+
"find-generic-password",
|
|
2761
|
+
"-s",
|
|
2762
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2763
|
+
"-a",
|
|
2764
|
+
KEYCHAIN_ACCOUNT_NAME,
|
|
2765
|
+
"-w"
|
|
2766
|
+
]);
|
|
2767
|
+
const parsed = parseJsonString(stdout.trim());
|
|
2768
|
+
return parsed?.accessToken ? parsed : void 0;
|
|
2769
|
+
} catch {
|
|
2770
|
+
return;
|
|
2913
2771
|
}
|
|
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
2772
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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;
|
|
2773
|
+
async writeSession(session) {
|
|
2774
|
+
await execFileAsync$1("security", [
|
|
2775
|
+
"add-generic-password",
|
|
2776
|
+
"-U",
|
|
2777
|
+
"-s",
|
|
2778
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2779
|
+
"-a",
|
|
2780
|
+
KEYCHAIN_ACCOUNT_NAME,
|
|
2781
|
+
"-w",
|
|
2782
|
+
JSON.stringify(session)
|
|
2783
|
+
]);
|
|
2966
2784
|
}
|
|
2967
|
-
|
|
2785
|
+
};
|
|
2786
|
+
var KeychainApiKeyStore = class {
|
|
2787
|
+
async clearApiKey() {
|
|
2968
2788
|
try {
|
|
2969
|
-
|
|
2970
|
-
|
|
2789
|
+
await execFileAsync$1("security", [
|
|
2790
|
+
"delete-generic-password",
|
|
2791
|
+
"-s",
|
|
2792
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2793
|
+
"-a",
|
|
2794
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY
|
|
2795
|
+
]);
|
|
2796
|
+
} catch {}
|
|
2797
|
+
}
|
|
2798
|
+
async readApiKey() {
|
|
2799
|
+
try {
|
|
2800
|
+
const { stdout } = await execFileAsync$1("security", [
|
|
2801
|
+
"find-generic-password",
|
|
2802
|
+
"-s",
|
|
2803
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2804
|
+
"-a",
|
|
2805
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2806
|
+
"-w"
|
|
2807
|
+
]);
|
|
2808
|
+
return stdout.trim() || void 0;
|
|
2971
2809
|
} catch {
|
|
2972
|
-
return
|
|
2810
|
+
return;
|
|
2973
2811
|
}
|
|
2974
2812
|
}
|
|
2813
|
+
async writeApiKey(apiKey) {
|
|
2814
|
+
const trimmed = apiKey.trim();
|
|
2815
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2816
|
+
await execFileAsync$1("security", [
|
|
2817
|
+
"add-generic-password",
|
|
2818
|
+
"-U",
|
|
2819
|
+
"-s",
|
|
2820
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2821
|
+
"-a",
|
|
2822
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2823
|
+
"-w",
|
|
2824
|
+
trimmed
|
|
2825
|
+
]);
|
|
2826
|
+
}
|
|
2975
2827
|
};
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
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;
|
|
2828
|
+
var CachedTokenProvider = class {
|
|
2829
|
+
#token;
|
|
2830
|
+
constructor(source, store = new NoopTokenStore()) {
|
|
2831
|
+
this.source = source;
|
|
2832
|
+
this.store = store;
|
|
2996
2833
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
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);
|
|
3022
|
-
}
|
|
3023
|
-
//#endregion
|
|
3024
|
-
//#region src/cache.ts
|
|
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
|
-
};
|
|
3034
|
-
}
|
|
3035
|
-
function parseTranscriptSegments(value) {
|
|
3036
|
-
if (!Array.isArray(value)) return;
|
|
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
|
-
});
|
|
3050
|
-
}
|
|
3051
|
-
function parseCacheContents(contents) {
|
|
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
|
-
}
|
|
3072
|
-
return {
|
|
3073
|
-
documents,
|
|
3074
|
-
transcripts
|
|
3075
|
-
};
|
|
3076
|
-
}
|
|
3077
|
-
//#endregion
|
|
3078
|
-
//#region src/client/auth.ts
|
|
3079
|
-
const execFileAsync$1 = promisify(execFile);
|
|
3080
|
-
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
3081
|
-
const KEYCHAIN_SERVICE_NAME = "com.granola.toolkit";
|
|
3082
|
-
const KEYCHAIN_ACCOUNT_NAME_API_KEY = "api-key";
|
|
3083
|
-
const KEYCHAIN_ACCOUNT_NAME = "session";
|
|
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;
|
|
3087
|
-
}
|
|
3088
|
-
function parseSessionRecord(record) {
|
|
3089
|
-
const accessToken = stringValue(record.access_token);
|
|
3090
|
-
if (!accessToken.trim()) return;
|
|
3091
|
-
return {
|
|
3092
|
-
accessToken,
|
|
3093
|
-
clientId: stringValue(record.client_id) || DEFAULT_CLIENT_ID,
|
|
3094
|
-
expiresIn: numberValue(record.expires_in),
|
|
3095
|
-
externalId: stringValue(record.external_id) || void 0,
|
|
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
|
|
3101
|
-
};
|
|
3102
|
-
}
|
|
3103
|
-
function parseNestedRecord(value) {
|
|
3104
|
-
if (typeof value === "string") return parseJsonString(value);
|
|
3105
|
-
return asRecord(value);
|
|
3106
|
-
}
|
|
3107
|
-
function getSessionFromSupabaseContents(supabaseContents) {
|
|
3108
|
-
const wrapper = parseJsonString(supabaseContents);
|
|
3109
|
-
if (!wrapper) throw new Error("failed to parse supabase.json");
|
|
3110
|
-
const workOsSession = parseSessionRecord(parseNestedRecord(wrapper.workos_tokens) ?? {});
|
|
3111
|
-
if (workOsSession) return workOsSession;
|
|
3112
|
-
const cognitoSession = parseSessionRecord(parseNestedRecord(wrapper.cognito_tokens) ?? {});
|
|
3113
|
-
if (cognitoSession) return cognitoSession;
|
|
3114
|
-
const legacySession = parseSessionRecord(wrapper);
|
|
3115
|
-
if (legacySession) return legacySession;
|
|
3116
|
-
throw new Error("access token not found in supabase.json");
|
|
3117
|
-
}
|
|
3118
|
-
function getAccessTokenFromSupabaseContents(supabaseContents) {
|
|
3119
|
-
return getSessionFromSupabaseContents(supabaseContents).accessToken;
|
|
3120
|
-
}
|
|
3121
|
-
var SupabaseFileTokenSource = class {
|
|
3122
|
-
constructor(filePath) {
|
|
3123
|
-
this.filePath = filePath;
|
|
2834
|
+
async getAccessToken() {
|
|
2835
|
+
if (this.#token) return this.#token;
|
|
2836
|
+
const storedToken = await this.store.readToken();
|
|
2837
|
+
if (storedToken?.trim()) {
|
|
2838
|
+
this.#token = storedToken;
|
|
2839
|
+
return storedToken;
|
|
2840
|
+
}
|
|
2841
|
+
const token = await this.source.loadAccessToken();
|
|
2842
|
+
this.#token = token;
|
|
2843
|
+
await this.store.writeToken(token);
|
|
2844
|
+
return token;
|
|
3124
2845
|
}
|
|
3125
|
-
async
|
|
3126
|
-
|
|
2846
|
+
async invalidate() {
|
|
2847
|
+
this.#token = void 0;
|
|
2848
|
+
await this.store.clearToken();
|
|
3127
2849
|
}
|
|
3128
2850
|
};
|
|
3129
|
-
var
|
|
3130
|
-
|
|
3131
|
-
|
|
2851
|
+
var StoredSessionTokenProvider = class {
|
|
2852
|
+
#session;
|
|
2853
|
+
constructor(store, options = {}) {
|
|
2854
|
+
this.store = store;
|
|
2855
|
+
this.options = options;
|
|
3132
2856
|
}
|
|
3133
2857
|
async loadSession() {
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
async readToken() {}
|
|
3140
|
-
async writeToken(_token) {}
|
|
3141
|
-
};
|
|
3142
|
-
var FileSessionStore = class {
|
|
3143
|
-
constructor(filePath = defaultSessionFilePath()) {
|
|
3144
|
-
this.filePath = filePath;
|
|
3145
|
-
}
|
|
3146
|
-
async clearSession() {
|
|
3147
|
-
try {
|
|
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;
|
|
2858
|
+
if (this.#session) return this.#session;
|
|
2859
|
+
const storedSession = await this.store.readSession();
|
|
2860
|
+
if (storedSession?.accessToken.trim()) {
|
|
2861
|
+
this.#session = storedSession;
|
|
2862
|
+
return storedSession;
|
|
3157
2863
|
}
|
|
2864
|
+
if (!this.options.source) throw new Error("no stored Granola session found");
|
|
2865
|
+
const sourcedSession = await this.options.source.loadSession();
|
|
2866
|
+
this.#session = sourcedSession;
|
|
2867
|
+
return sourcedSession;
|
|
3158
2868
|
}
|
|
3159
|
-
async
|
|
3160
|
-
await
|
|
3161
|
-
await writeFile(this.filePath, `${JSON.stringify(session, null, 2)}\n`, {
|
|
3162
|
-
encoding: "utf8",
|
|
3163
|
-
mode: 384
|
|
3164
|
-
});
|
|
3165
|
-
}
|
|
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 {}
|
|
2869
|
+
async getAccessToken() {
|
|
2870
|
+
return (await this.loadSession()).accessToken;
|
|
3175
2871
|
}
|
|
3176
|
-
async
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
2872
|
+
async invalidate() {
|
|
2873
|
+
const session = await this.loadSession().catch(() => void 0);
|
|
2874
|
+
if (session?.refreshToken && session.clientId) try {
|
|
2875
|
+
const refreshedSession = await refreshGranolaSession(session, this.options.fetchImpl);
|
|
2876
|
+
this.#session = refreshedSession;
|
|
2877
|
+
await this.store.writeSession(refreshedSession);
|
|
3180
2878
|
return;
|
|
3181
|
-
}
|
|
3182
|
-
}
|
|
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
|
-
};
|
|
3193
|
-
var KeychainSessionStore = class {
|
|
3194
|
-
async clearSession() {
|
|
3195
|
-
try {
|
|
3196
|
-
await execFileAsync$1("security", [
|
|
3197
|
-
"delete-generic-password",
|
|
3198
|
-
"-s",
|
|
3199
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3200
|
-
"-a",
|
|
3201
|
-
KEYCHAIN_ACCOUNT_NAME
|
|
3202
|
-
]);
|
|
3203
|
-
} catch {}
|
|
3204
|
-
}
|
|
3205
|
-
async readSession() {
|
|
3206
|
-
try {
|
|
3207
|
-
const { stdout } = await execFileAsync$1("security", [
|
|
3208
|
-
"find-generic-password",
|
|
3209
|
-
"-s",
|
|
3210
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3211
|
-
"-a",
|
|
3212
|
-
KEYCHAIN_ACCOUNT_NAME,
|
|
3213
|
-
"-w"
|
|
3214
|
-
]);
|
|
3215
|
-
const parsed = parseJsonString(stdout.trim());
|
|
3216
|
-
return parsed?.accessToken ? parsed : void 0;
|
|
3217
2879
|
} catch {
|
|
2880
|
+
if (!this.options.source) {
|
|
2881
|
+
this.#session = void 0;
|
|
2882
|
+
await this.store.clearSession();
|
|
2883
|
+
throw new Error("failed to refresh stored Granola session");
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
if (this.options.source) {
|
|
2887
|
+
const sourcedSession = await this.options.source.loadSession();
|
|
2888
|
+
this.#session = sourcedSession;
|
|
2889
|
+
await this.store.writeSession(sourcedSession);
|
|
3218
2890
|
return;
|
|
3219
2891
|
}
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
await execFileAsync$1("security", [
|
|
3223
|
-
"add-generic-password",
|
|
3224
|
-
"-U",
|
|
3225
|
-
"-s",
|
|
3226
|
-
KEYCHAIN_SERVICE_NAME,
|
|
3227
|
-
"-a",
|
|
3228
|
-
KEYCHAIN_ACCOUNT_NAME,
|
|
3229
|
-
"-w",
|
|
3230
|
-
JSON.stringify(session)
|
|
3231
|
-
]);
|
|
2892
|
+
this.#session = void 0;
|
|
2893
|
+
await this.store.clearSession();
|
|
3232
2894
|
}
|
|
3233
2895
|
};
|
|
3234
|
-
|
|
3235
|
-
|
|
2896
|
+
async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
2897
|
+
if (!session.refreshToken?.trim()) throw new Error("refresh token not available");
|
|
2898
|
+
const response = await fetchImpl(WORKOS_AUTH_URL, {
|
|
2899
|
+
body: JSON.stringify({
|
|
2900
|
+
client_id: session.clientId,
|
|
2901
|
+
grant_type: "refresh_token",
|
|
2902
|
+
refresh_token: session.refreshToken
|
|
2903
|
+
}),
|
|
2904
|
+
headers: { "Content-Type": "application/json" },
|
|
2905
|
+
method: "POST"
|
|
2906
|
+
});
|
|
2907
|
+
if (!response.ok) throw new Error(`failed to refresh session: ${response.status} ${response.statusText}`);
|
|
2908
|
+
const refreshed = parseSessionRecord(await response.json());
|
|
2909
|
+
if (!refreshed) throw new Error("failed to parse refreshed session");
|
|
2910
|
+
return {
|
|
2911
|
+
...session,
|
|
2912
|
+
...refreshed,
|
|
2913
|
+
clientId: refreshed.clientId || session.clientId,
|
|
2914
|
+
obtainedAt: refreshed.obtainedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2915
|
+
refreshToken: refreshed.refreshToken ?? session.refreshToken
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function defaultSessionFilePath() {
|
|
2919
|
+
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
2920
|
+
}
|
|
2921
|
+
function defaultApiKeyFilePath() {
|
|
2922
|
+
return defaultGranolaToolkitPersistenceLayout().apiKeyFile;
|
|
2923
|
+
}
|
|
2924
|
+
function createDefaultSessionStore() {
|
|
2925
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
2926
|
+
}
|
|
2927
|
+
function createDefaultApiKeyStore() {
|
|
2928
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainApiKeyStore() : new FileApiKeyStore();
|
|
2929
|
+
}
|
|
2930
|
+
//#endregion
|
|
2931
|
+
//#region src/client/http.ts
|
|
2932
|
+
const RETRYABLE_STATUS_CODES = new Set([
|
|
2933
|
+
429,
|
|
2934
|
+
500,
|
|
2935
|
+
502,
|
|
2936
|
+
503,
|
|
2937
|
+
504
|
|
2938
|
+
]);
|
|
2939
|
+
function sleep(delayMs) {
|
|
2940
|
+
return new Promise((resolve) => {
|
|
2941
|
+
setTimeout(resolve, delayMs);
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
function parseRetryAfter(headerValue) {
|
|
2945
|
+
if (!headerValue?.trim()) return;
|
|
2946
|
+
if (/^\d+$/.test(headerValue.trim())) return Number(headerValue.trim()) * 1e3;
|
|
2947
|
+
const retryAt = Date.parse(headerValue);
|
|
2948
|
+
if (Number.isNaN(retryAt)) return;
|
|
2949
|
+
return Math.max(0, retryAt - Date.now());
|
|
2950
|
+
}
|
|
2951
|
+
var AuthenticatedHttpClient = class {
|
|
2952
|
+
fetchImpl;
|
|
2953
|
+
constructor(options) {
|
|
2954
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2955
|
+
this.logger = options.logger;
|
|
2956
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
2957
|
+
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;
|
|
2958
|
+
this.retryMaxDelayMs = options.retryMaxDelayMs ?? 5e3;
|
|
2959
|
+
this.sleepImpl = options.sleepImpl ?? sleep;
|
|
2960
|
+
this.tokenProvider = options.tokenProvider;
|
|
2961
|
+
}
|
|
2962
|
+
logger;
|
|
2963
|
+
maxRetries;
|
|
2964
|
+
retryBaseDelayMs;
|
|
2965
|
+
retryMaxDelayMs;
|
|
2966
|
+
sleepImpl;
|
|
2967
|
+
tokenProvider;
|
|
2968
|
+
async retry(options, attempt, reason, response) {
|
|
2969
|
+
const retryAfterMs = parseRetryAfter(response?.headers.get("retry-after") ?? null);
|
|
2970
|
+
const delayMs = Math.min(retryAfterMs ?? this.retryBaseDelayMs * 2 ** attempt, this.retryMaxDelayMs);
|
|
2971
|
+
this.logger?.warn?.(`${reason}; retrying in ${delayMs}ms (${attempt + 1}/${this.maxRetries})`);
|
|
2972
|
+
await this.sleepImpl(delayMs);
|
|
2973
|
+
return this.request(options, attempt + 1);
|
|
2974
|
+
}
|
|
2975
|
+
async request(options, attempt = 0) {
|
|
2976
|
+
const { retryOnUnauthorized = true, timeoutMs, url } = options;
|
|
2977
|
+
const accessToken = await this.tokenProvider.getAccessToken();
|
|
2978
|
+
let response;
|
|
3236
2979
|
try {
|
|
3237
|
-
await
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
2980
|
+
response = await this.fetchImpl(url, {
|
|
2981
|
+
body: options.body,
|
|
2982
|
+
headers: {
|
|
2983
|
+
...options.headers,
|
|
2984
|
+
Authorization: `Bearer ${accessToken}`
|
|
2985
|
+
},
|
|
2986
|
+
method: options.method ?? "GET",
|
|
2987
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2988
|
+
});
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
if (attempt < this.maxRetries) {
|
|
2991
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2992
|
+
return this.retry(options, attempt, `request failed: ${message}`);
|
|
2993
|
+
}
|
|
2994
|
+
throw error;
|
|
2995
|
+
}
|
|
2996
|
+
if (response.status === 401 && retryOnUnauthorized) {
|
|
2997
|
+
this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
|
|
2998
|
+
await this.tokenProvider.invalidate();
|
|
2999
|
+
return this.request({
|
|
3000
|
+
...options,
|
|
3001
|
+
retryOnUnauthorized: false
|
|
3002
|
+
}, attempt);
|
|
3003
|
+
}
|
|
3004
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) return this.retry(options, attempt, `request returned ${response.status} ${response.statusText || ""}`.trim(), response);
|
|
3005
|
+
return response;
|
|
3006
|
+
}
|
|
3007
|
+
async postJson(url, body, options = { timeoutMs: 3e4 }) {
|
|
3008
|
+
return this.request({
|
|
3009
|
+
...options,
|
|
3010
|
+
body: JSON.stringify(body),
|
|
3011
|
+
headers: {
|
|
3012
|
+
Accept: "*/*",
|
|
3013
|
+
"Content-Type": "application/json",
|
|
3014
|
+
...options.headers
|
|
3015
|
+
},
|
|
3016
|
+
method: "POST",
|
|
3017
|
+
url
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
//#endregion
|
|
3022
|
+
//#region src/agents.ts
|
|
3023
|
+
const DEFAULT_CODEX_MODEL = "gpt-5-codex";
|
|
3024
|
+
const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
|
|
3025
|
+
const DEFAULT_OPENROUTER_MODEL = "openai/gpt-5-mini";
|
|
3026
|
+
const OPENROUTER_REFERER = "https://github.com/kkarimi/granola-toolkit";
|
|
3027
|
+
const OPENROUTER_TITLE = "granola-toolkit";
|
|
3028
|
+
function trimString(value) {
|
|
3029
|
+
return value?.trim() ? value.trim() : void 0;
|
|
3030
|
+
}
|
|
3031
|
+
function resolveProvider(request, config, env) {
|
|
3032
|
+
if (request.provider) return request.provider;
|
|
3033
|
+
if (config.agents?.defaultProvider) return config.agents.defaultProvider;
|
|
3034
|
+
if (trimString(env.OPENROUTER_API_KEY) || trimString(env.GRANOLA_OPENROUTER_API_KEY)) return "openrouter";
|
|
3035
|
+
if (trimString(env.OPENAI_API_KEY) || trimString(env.GRANOLA_OPENAI_API_KEY)) return "openai";
|
|
3036
|
+
return "codex";
|
|
3037
|
+
}
|
|
3038
|
+
function resolveModel(provider, request, config) {
|
|
3039
|
+
return trimString(request.model) ?? trimString(config.agents?.defaultModel) ?? (provider === "openrouter" ? DEFAULT_OPENROUTER_MODEL : provider === "openai" ? DEFAULT_OPENAI_MODEL : DEFAULT_CODEX_MODEL);
|
|
3040
|
+
}
|
|
3041
|
+
function resolveTimeoutMs(request, config) {
|
|
3042
|
+
return request.timeoutMs ?? config.agents?.timeoutMs ?? 3e5;
|
|
3043
|
+
}
|
|
3044
|
+
function resolveRetries(request, config) {
|
|
3045
|
+
return request.retries ?? config.agents?.maxRetries ?? 2;
|
|
3046
|
+
}
|
|
3047
|
+
function resolveDryRun(request, config) {
|
|
3048
|
+
return request.dryRun ?? config.agents?.dryRun ?? false;
|
|
3049
|
+
}
|
|
3050
|
+
function openaiApiKey(env) {
|
|
3051
|
+
return trimString(env.OPENAI_API_KEY) ?? trimString(env.GRANOLA_OPENAI_API_KEY);
|
|
3052
|
+
}
|
|
3053
|
+
function openrouterApiKey(env) {
|
|
3054
|
+
return trimString(env.OPENROUTER_API_KEY) ?? trimString(env.GRANOLA_OPENROUTER_API_KEY);
|
|
3055
|
+
}
|
|
3056
|
+
async function responseError(response, label) {
|
|
3057
|
+
let details = `${response.status} ${response.statusText}`.trim();
|
|
3058
|
+
try {
|
|
3059
|
+
const payload = await response.json();
|
|
3060
|
+
if (typeof payload.error === "string" && payload.error.trim()) details = payload.error;
|
|
3061
|
+
else if (payload.error && typeof payload.error === "object" && typeof payload.error.message === "string" && payload.error.message.trim()) details = payload.error.message;
|
|
3062
|
+
else if (typeof payload.message === "string" && payload.message.trim()) details = payload.message;
|
|
3063
|
+
} catch {
|
|
3064
|
+
const text = (await response.text()).trim();
|
|
3065
|
+
if (text) details = text;
|
|
3066
|
+
}
|
|
3067
|
+
return /* @__PURE__ */ new Error(`${label}: ${details}`);
|
|
3068
|
+
}
|
|
3069
|
+
function messageText(content) {
|
|
3070
|
+
if (typeof content === "string") return content.trim();
|
|
3071
|
+
if (!Array.isArray(content)) return "";
|
|
3072
|
+
return content.map((part) => {
|
|
3073
|
+
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") return part.text;
|
|
3074
|
+
return "";
|
|
3075
|
+
}).join("").trim();
|
|
3076
|
+
}
|
|
3077
|
+
async function runCodexCliCommand(request) {
|
|
3078
|
+
const tempDirectory = await mkdtemp(join(tmpdir(), "granola-toolkit-codex-"));
|
|
3079
|
+
const outputFile = join(tempDirectory, "last-message.txt");
|
|
3080
|
+
const args = [
|
|
3081
|
+
"exec",
|
|
3082
|
+
"--skip-git-repo-check",
|
|
3083
|
+
"--color",
|
|
3084
|
+
"never"
|
|
3085
|
+
];
|
|
3086
|
+
if (request.cwd) args.push("-C", request.cwd);
|
|
3087
|
+
if (request.model) args.push("-m", request.model);
|
|
3088
|
+
args.push("--output-last-message", outputFile, "-");
|
|
3089
|
+
const commandText = [request.command, ...args].join(" ");
|
|
3090
|
+
try {
|
|
3091
|
+
return {
|
|
3092
|
+
command: commandText,
|
|
3093
|
+
output: await new Promise((resolve$1, reject) => {
|
|
3094
|
+
const child = spawn(request.command, args, {
|
|
3095
|
+
cwd: request.cwd ? resolve(request.cwd) : process.cwd(),
|
|
3096
|
+
env: process.env,
|
|
3097
|
+
stdio: [
|
|
3098
|
+
"pipe",
|
|
3099
|
+
"pipe",
|
|
3100
|
+
"pipe"
|
|
3101
|
+
]
|
|
3102
|
+
});
|
|
3103
|
+
const stdoutChunks = [];
|
|
3104
|
+
const stderrChunks = [];
|
|
3105
|
+
let timedOut = false;
|
|
3106
|
+
const timeout = setTimeout(() => {
|
|
3107
|
+
timedOut = true;
|
|
3108
|
+
child.kill("SIGTERM");
|
|
3109
|
+
}, request.timeoutMs);
|
|
3110
|
+
child.stdout.on("data", (chunk) => {
|
|
3111
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3112
|
+
});
|
|
3113
|
+
child.stderr.on("data", (chunk) => {
|
|
3114
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3115
|
+
});
|
|
3116
|
+
child.on("error", (error) => {
|
|
3117
|
+
clearTimeout(timeout);
|
|
3118
|
+
reject(error);
|
|
3119
|
+
});
|
|
3120
|
+
child.on("close", async (code) => {
|
|
3121
|
+
clearTimeout(timeout);
|
|
3122
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
3123
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
3124
|
+
if (timedOut) {
|
|
3125
|
+
reject(/* @__PURE__ */ new Error(`codex provider timed out after ${request.timeoutMs}ms`));
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
if (code !== 0) {
|
|
3129
|
+
reject(new Error(stderr || stdout || `codex exited with status ${String(code)}`));
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
try {
|
|
3133
|
+
resolve$1((await readFile(outputFile, "utf8")).trim() || stdout || void 0);
|
|
3134
|
+
} catch {
|
|
3135
|
+
resolve$1(stdout || void 0);
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
child.stdin.write(request.prompt);
|
|
3139
|
+
child.stdin.end();
|
|
3140
|
+
})
|
|
3141
|
+
};
|
|
3142
|
+
} finally {
|
|
3143
|
+
await rm(tempDirectory, {
|
|
3144
|
+
force: true,
|
|
3145
|
+
recursive: true
|
|
3146
|
+
}).catch(() => void 0);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
async function runOpenAiCompatibleRequest(options) {
|
|
3150
|
+
const response = await new AuthenticatedHttpClient({
|
|
3151
|
+
fetchImpl: options.fetchImpl,
|
|
3152
|
+
maxRetries: options.maxRetries,
|
|
3153
|
+
tokenProvider: new CachedTokenProvider({ async loadAccessToken() {
|
|
3154
|
+
return options.token;
|
|
3155
|
+
} })
|
|
3156
|
+
}).postJson(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
3157
|
+
messages: [...options.systemPrompt ? [{
|
|
3158
|
+
content: options.systemPrompt,
|
|
3159
|
+
role: "system"
|
|
3160
|
+
}] : [], {
|
|
3161
|
+
content: options.prompt,
|
|
3162
|
+
role: "user"
|
|
3163
|
+
}],
|
|
3164
|
+
model: options.model
|
|
3165
|
+
}, {
|
|
3166
|
+
headers: {
|
|
3167
|
+
Accept: "application/json",
|
|
3168
|
+
...options.headers
|
|
3169
|
+
},
|
|
3170
|
+
timeoutMs: options.timeoutMs
|
|
3171
|
+
});
|
|
3172
|
+
if (!response.ok) throw await responseError(response, options.label);
|
|
3173
|
+
const content = (await response.json()).choices?.[0]?.message?.content;
|
|
3174
|
+
return messageText(content) || void 0;
|
|
3175
|
+
}
|
|
3176
|
+
function createDefaultAutomationAgentRunner(config, options = {}) {
|
|
3177
|
+
const env = options.env ?? process.env;
|
|
3178
|
+
const runCodexCommand = options.runCodexCommand ?? runCodexCliCommand;
|
|
3179
|
+
return { async run(request) {
|
|
3180
|
+
const provider = resolveProvider(request, config, env);
|
|
3181
|
+
const model = resolveModel(provider, request, config);
|
|
3182
|
+
const timeoutMs = resolveTimeoutMs(request, config);
|
|
3183
|
+
const retries = resolveRetries(request, config);
|
|
3184
|
+
const dryRun = resolveDryRun(request, config);
|
|
3185
|
+
if (dryRun) return {
|
|
3186
|
+
dryRun,
|
|
3187
|
+
model,
|
|
3188
|
+
output: void 0,
|
|
3189
|
+
prompt: request.prompt,
|
|
3190
|
+
provider,
|
|
3191
|
+
systemPrompt: request.systemPrompt
|
|
3192
|
+
};
|
|
3193
|
+
if (provider === "codex") {
|
|
3194
|
+
const result = await runCodexCommand({
|
|
3195
|
+
command: config.agents?.codexCommand ?? "codex",
|
|
3196
|
+
cwd: request.cwd,
|
|
3197
|
+
model,
|
|
3198
|
+
prompt: request.systemPrompt ? `${request.systemPrompt.trim()}\n\n${request.prompt}` : request.prompt,
|
|
3199
|
+
timeoutMs
|
|
3200
|
+
});
|
|
3201
|
+
return {
|
|
3202
|
+
command: result.command,
|
|
3203
|
+
dryRun,
|
|
3204
|
+
model,
|
|
3205
|
+
output: result.output,
|
|
3206
|
+
prompt: request.prompt,
|
|
3207
|
+
provider,
|
|
3208
|
+
systemPrompt: request.systemPrompt
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
const token = provider === "openrouter" ? openrouterApiKey(env) : openaiApiKey(env);
|
|
3212
|
+
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.");
|
|
3213
|
+
return {
|
|
3214
|
+
dryRun,
|
|
3215
|
+
model,
|
|
3216
|
+
output: await runOpenAiCompatibleRequest({
|
|
3217
|
+
baseUrl: provider === "openrouter" ? config.agents?.openrouterBaseUrl ?? "https://openrouter.ai/api/v1" : config.agents?.openaiBaseUrl ?? "https://api.openai.com/v1",
|
|
3218
|
+
fetchImpl: options.fetchImpl,
|
|
3219
|
+
headers: provider === "openrouter" ? {
|
|
3220
|
+
"HTTP-Referer": OPENROUTER_REFERER,
|
|
3221
|
+
"X-Title": OPENROUTER_TITLE
|
|
3222
|
+
} : void 0,
|
|
3223
|
+
label: provider === "openrouter" ? "OpenRouter request failed" : "OpenAI request failed",
|
|
3224
|
+
maxRetries: retries,
|
|
3225
|
+
model,
|
|
3226
|
+
prompt: request.prompt,
|
|
3227
|
+
systemPrompt: request.systemPrompt,
|
|
3228
|
+
timeoutMs,
|
|
3229
|
+
token
|
|
3230
|
+
}),
|
|
3231
|
+
prompt: request.prompt,
|
|
3232
|
+
provider,
|
|
3233
|
+
systemPrompt: request.systemPrompt
|
|
3234
|
+
};
|
|
3235
|
+
} };
|
|
3236
|
+
}
|
|
3237
|
+
//#endregion
|
|
3238
|
+
//#region src/automation-actions.ts
|
|
3239
|
+
function cloneAction$1(action) {
|
|
3240
|
+
switch (action.kind) {
|
|
3241
|
+
case "agent": return { ...action };
|
|
3242
|
+
case "ask-user": return { ...action };
|
|
3243
|
+
case "command": return {
|
|
3244
|
+
...action,
|
|
3245
|
+
args: action.args ? [...action.args] : void 0,
|
|
3246
|
+
env: action.env ? { ...action.env } : void 0
|
|
3247
|
+
};
|
|
3248
|
+
case "export-notes":
|
|
3249
|
+
case "export-transcript": return { ...action };
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
function automationActionName(action) {
|
|
3253
|
+
return action.name || action.id;
|
|
3254
|
+
}
|
|
3255
|
+
function buildAutomationActionRunId(match, actionId) {
|
|
3256
|
+
return `${match.id}:${actionId}`;
|
|
3257
|
+
}
|
|
3258
|
+
function enabledAutomationActions(rule) {
|
|
3259
|
+
return (rule.actions ?? []).filter((action) => action.enabled !== false).map((action) => cloneAction$1(action));
|
|
3260
|
+
}
|
|
3261
|
+
function baseRun(match, rule, action, startedAt) {
|
|
3262
|
+
return {
|
|
3263
|
+
actionId: action.id,
|
|
3264
|
+
actionKind: action.kind,
|
|
3265
|
+
actionName: automationActionName(action),
|
|
3266
|
+
eventId: match.eventId,
|
|
3267
|
+
eventKind: match.eventKind,
|
|
3268
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
3269
|
+
id: buildAutomationActionRunId(match, action.id),
|
|
3270
|
+
matchedAt: match.matchedAt,
|
|
3271
|
+
meetingId: match.meetingId,
|
|
3272
|
+
ruleId: rule.id,
|
|
3273
|
+
ruleName: rule.name,
|
|
3274
|
+
startedAt,
|
|
3275
|
+
status: "completed",
|
|
3276
|
+
tags: [...match.tags],
|
|
3277
|
+
title: match.title,
|
|
3278
|
+
transcriptLoaded: match.transcriptLoaded
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
function completedRun(run, finishedAt, patch = {}) {
|
|
3282
|
+
return {
|
|
3283
|
+
...run,
|
|
3284
|
+
...patch,
|
|
3285
|
+
finishedAt,
|
|
3286
|
+
status: "completed"
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
function failedRun(run, finishedAt, error) {
|
|
3290
|
+
return {
|
|
3291
|
+
...run,
|
|
3292
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3293
|
+
finishedAt,
|
|
3294
|
+
status: "failed"
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function skippedRun(run, finishedAt, reason) {
|
|
3298
|
+
return {
|
|
3299
|
+
...run,
|
|
3300
|
+
finishedAt,
|
|
3301
|
+
result: reason,
|
|
3302
|
+
status: "skipped"
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
async function executeAutomationAction(match, rule, action, handlers) {
|
|
3306
|
+
const run = baseRun(match, rule, action, handlers.nowIso());
|
|
3307
|
+
switch (action.kind) {
|
|
3308
|
+
case "agent": try {
|
|
3309
|
+
const result = await handlers.runAgent(match, rule, action);
|
|
3310
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3311
|
+
meta: {
|
|
3312
|
+
command: result.command,
|
|
3313
|
+
dryRun: result.dryRun,
|
|
3314
|
+
model: result.model,
|
|
3315
|
+
provider: result.provider,
|
|
3316
|
+
systemPrompt: result.systemPrompt
|
|
3317
|
+
},
|
|
3318
|
+
prompt: result.prompt,
|
|
3319
|
+
result: result.output ?? (result.dryRun ? "Dry run: provider request not executed" : "")
|
|
3320
|
+
});
|
|
3321
|
+
} catch (error) {
|
|
3322
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3323
|
+
}
|
|
3324
|
+
case "ask-user": return {
|
|
3325
|
+
...run,
|
|
3326
|
+
meta: action.details ? { details: action.details } : void 0,
|
|
3327
|
+
prompt: action.prompt,
|
|
3328
|
+
result: "Pending user decision",
|
|
3329
|
+
status: "pending"
|
|
3330
|
+
};
|
|
3331
|
+
case "command": try {
|
|
3332
|
+
const result = await handlers.runCommand(match, rule, action);
|
|
3333
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3334
|
+
meta: {
|
|
3335
|
+
command: result.command,
|
|
3336
|
+
cwd: result.cwd
|
|
3337
|
+
},
|
|
3338
|
+
result: result.output
|
|
3339
|
+
});
|
|
3340
|
+
} catch (error) {
|
|
3341
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3342
|
+
}
|
|
3343
|
+
case "export-notes": try {
|
|
3344
|
+
const result = await handlers.exportNotes(match, action);
|
|
3345
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Meeting notes were unavailable for export");
|
|
3346
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3347
|
+
meta: {
|
|
3348
|
+
format: result.format,
|
|
3349
|
+
outputDir: result.outputDir,
|
|
3350
|
+
scope: result.scope,
|
|
3351
|
+
written: result.written
|
|
3352
|
+
},
|
|
3353
|
+
result: `Exported notes to ${result.outputDir}`
|
|
3354
|
+
});
|
|
3355
|
+
} catch (error) {
|
|
3356
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3357
|
+
}
|
|
3358
|
+
case "export-transcript": try {
|
|
3359
|
+
const result = await handlers.exportTranscripts(match, action);
|
|
3360
|
+
if (!result) return skippedRun(run, handlers.nowIso(), "Transcript data was unavailable for export");
|
|
3361
|
+
return completedRun(run, handlers.nowIso(), {
|
|
3362
|
+
meta: {
|
|
3363
|
+
format: result.format,
|
|
3364
|
+
outputDir: result.outputDir,
|
|
3365
|
+
scope: result.scope,
|
|
3366
|
+
written: result.written
|
|
3367
|
+
},
|
|
3368
|
+
result: `Exported transcript to ${result.outputDir}`
|
|
3369
|
+
});
|
|
3370
|
+
} catch (error) {
|
|
3371
|
+
return failedRun(run, handlers.nowIso(), error);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
//#endregion
|
|
3376
|
+
//#region src/automation-matches.ts
|
|
3377
|
+
function cloneMatch(match) {
|
|
3378
|
+
return {
|
|
3379
|
+
...match,
|
|
3380
|
+
folders: match.folders.map((folder) => ({ ...folder })),
|
|
3381
|
+
tags: [...match.tags]
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
var FileAutomationMatchStore = class {
|
|
3385
|
+
constructor(filePath = defaultAutomationMatchesFilePath()) {
|
|
3386
|
+
this.filePath = filePath;
|
|
3387
|
+
}
|
|
3388
|
+
async appendMatches(matches) {
|
|
3389
|
+
if (matches.length === 0) return;
|
|
3390
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3391
|
+
const payload = matches.map((match) => JSON.stringify(match)).join("\n");
|
|
3392
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3393
|
+
encoding: "utf8",
|
|
3394
|
+
mode: 384
|
|
3395
|
+
});
|
|
3245
3396
|
}
|
|
3246
|
-
async
|
|
3397
|
+
async readMatches(limit = 50) {
|
|
3247
3398
|
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;
|
|
3399
|
+
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);
|
|
3400
|
+
return (limit > 0 ? matches.slice(-limit) : matches).reverse();
|
|
3257
3401
|
} catch {
|
|
3258
|
-
return;
|
|
3402
|
+
return [];
|
|
3259
3403
|
}
|
|
3260
3404
|
}
|
|
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
3405
|
};
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3406
|
+
function defaultAutomationMatchesFilePath() {
|
|
3407
|
+
return defaultGranolaToolkitPersistenceLayout().automationMatchesFile;
|
|
3408
|
+
}
|
|
3409
|
+
function createDefaultAutomationMatchStore(filePath) {
|
|
3410
|
+
return new FileAutomationMatchStore(filePath);
|
|
3411
|
+
}
|
|
3412
|
+
//#endregion
|
|
3413
|
+
//#region src/automation-runs.ts
|
|
3414
|
+
function cloneRun(run) {
|
|
3415
|
+
return {
|
|
3416
|
+
...run,
|
|
3417
|
+
folders: run.folders.map((folder) => ({ ...folder })),
|
|
3418
|
+
meta: run.meta ? structuredClone(run.meta) : void 0,
|
|
3419
|
+
tags: [...run.tags]
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function sortRuns(runs) {
|
|
3423
|
+
return runs.slice().sort((left, right) => (right.finishedAt ?? right.startedAt).localeCompare(left.finishedAt ?? left.startedAt));
|
|
3424
|
+
}
|
|
3425
|
+
function mergeRuns(runs) {
|
|
3426
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3427
|
+
for (const run of runs) byId.set(run.id, cloneRun(run));
|
|
3428
|
+
return sortRuns([...byId.values()]);
|
|
3429
|
+
}
|
|
3430
|
+
var FileAutomationRunStore = class {
|
|
3431
|
+
constructor(filePath = defaultAutomationRunsFilePath()) {
|
|
3432
|
+
this.filePath = filePath;
|
|
3281
3433
|
}
|
|
3282
|
-
async
|
|
3283
|
-
if (
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
this.#token = token;
|
|
3291
|
-
await this.store.writeToken(token);
|
|
3292
|
-
return token;
|
|
3434
|
+
async appendRuns(runs) {
|
|
3435
|
+
if (runs.length === 0) return;
|
|
3436
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3437
|
+
const payload = runs.map((run) => JSON.stringify(run)).join("\n");
|
|
3438
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3439
|
+
encoding: "utf8",
|
|
3440
|
+
mode: 384
|
|
3441
|
+
});
|
|
3293
3442
|
}
|
|
3294
|
-
async
|
|
3295
|
-
this
|
|
3296
|
-
|
|
3443
|
+
async readRun(id) {
|
|
3444
|
+
return (await this.readRuns({ limit: 0 })).find((run) => run.id === id);
|
|
3445
|
+
}
|
|
3446
|
+
async readRuns(options = {}) {
|
|
3447
|
+
try {
|
|
3448
|
+
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);
|
|
3449
|
+
return (options.limit && options.limit > 0 ? runs.slice(0, options.limit) : runs).map(cloneRun);
|
|
3450
|
+
} catch {
|
|
3451
|
+
return [];
|
|
3452
|
+
}
|
|
3297
3453
|
}
|
|
3298
3454
|
};
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3455
|
+
function defaultAutomationRunsFilePath() {
|
|
3456
|
+
return defaultGranolaToolkitPersistenceLayout().automationRunsFile;
|
|
3457
|
+
}
|
|
3458
|
+
function createDefaultAutomationRunStore(filePath) {
|
|
3459
|
+
return new FileAutomationRunStore(filePath);
|
|
3460
|
+
}
|
|
3461
|
+
//#endregion
|
|
3462
|
+
//#region src/automation-rules.ts
|
|
3463
|
+
function cloneRule(rule) {
|
|
3464
|
+
return {
|
|
3465
|
+
...rule,
|
|
3466
|
+
actions: rule.actions?.map((action) => cloneAction(action)),
|
|
3467
|
+
when: {
|
|
3468
|
+
...rule.when,
|
|
3469
|
+
eventKinds: rule.when.eventKinds ? [...rule.when.eventKinds] : void 0,
|
|
3470
|
+
folderIds: rule.when.folderIds ? [...rule.when.folderIds] : void 0,
|
|
3471
|
+
folderNames: rule.when.folderNames ? [...rule.when.folderNames] : void 0,
|
|
3472
|
+
meetingIds: rule.when.meetingIds ? [...rule.when.meetingIds] : void 0,
|
|
3473
|
+
tags: rule.when.tags ? [...rule.when.tags] : void 0,
|
|
3474
|
+
titleIncludes: rule.when.titleIncludes ? [...rule.when.titleIncludes] : void 0
|
|
3475
|
+
}
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function cloneAction(action) {
|
|
3479
|
+
switch (action.kind) {
|
|
3480
|
+
case "agent": return { ...action };
|
|
3481
|
+
case "ask-user": return { ...action };
|
|
3482
|
+
case "command": return {
|
|
3483
|
+
...action,
|
|
3484
|
+
args: action.args ? [...action.args] : void 0,
|
|
3485
|
+
env: action.env ? { ...action.env } : void 0
|
|
3486
|
+
};
|
|
3487
|
+
case "export-notes":
|
|
3488
|
+
case "export-transcript": return { ...action };
|
|
3304
3489
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3490
|
+
}
|
|
3491
|
+
function stringArray(value) {
|
|
3492
|
+
if (!Array.isArray(value)) return;
|
|
3493
|
+
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
3494
|
+
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : void 0;
|
|
3495
|
+
}
|
|
3496
|
+
function stringRecord(value) {
|
|
3497
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3498
|
+
const entries = Object.entries(value).filter(([key, item]) => {
|
|
3499
|
+
return typeof key === "string" && key.trim().length > 0 && typeof item === "string" && item.trim().length > 0;
|
|
3500
|
+
});
|
|
3501
|
+
if (entries.length === 0) return;
|
|
3502
|
+
return Object.fromEntries(entries.map(([key, item]) => [key.trim(), item.trim()]));
|
|
3503
|
+
}
|
|
3504
|
+
function parseAction(value, index) {
|
|
3505
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3506
|
+
const record = value;
|
|
3507
|
+
const kind = typeof record.kind === "string" && record.kind.trim() ? record.kind.trim() : void 0;
|
|
3508
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : kind ? `${kind}-${index + 1}` : void 0;
|
|
3509
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
3510
|
+
const enabled = typeof record.enabled === "boolean" ? record.enabled : void 0;
|
|
3511
|
+
switch (kind) {
|
|
3512
|
+
case "agent": {
|
|
3513
|
+
if (!id) return;
|
|
3514
|
+
const provider = record.provider === "codex" || record.provider === "openai" || record.provider === "openrouter" ? record.provider : void 0;
|
|
3515
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
3516
|
+
const promptFile = typeof record.promptFile === "string" && record.promptFile.trim() ? record.promptFile.trim() : void 0;
|
|
3517
|
+
const systemPrompt = typeof record.systemPrompt === "string" && record.systemPrompt.trim() ? record.systemPrompt.trim() : void 0;
|
|
3518
|
+
const systemPromptFile = typeof record.systemPromptFile === "string" && record.systemPromptFile.trim() ? record.systemPromptFile.trim() : void 0;
|
|
3519
|
+
if (!prompt && !promptFile) return;
|
|
3520
|
+
return {
|
|
3521
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
3522
|
+
dryRun: typeof record.dryRun === "boolean" ? record.dryRun : void 0,
|
|
3523
|
+
enabled,
|
|
3524
|
+
id,
|
|
3525
|
+
kind,
|
|
3526
|
+
model: typeof record.model === "string" && record.model.trim() ? record.model.trim() : void 0,
|
|
3527
|
+
name,
|
|
3528
|
+
prompt,
|
|
3529
|
+
promptFile,
|
|
3530
|
+
provider,
|
|
3531
|
+
retries: typeof record.retries === "number" && Number.isFinite(record.retries) ? record.retries : typeof record.retries === "string" && /^\d+$/.test(record.retries) ? Number(record.retries) : void 0,
|
|
3532
|
+
systemPrompt,
|
|
3533
|
+
systemPromptFile,
|
|
3534
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
3535
|
+
};
|
|
3311
3536
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3537
|
+
case "ask-user": {
|
|
3538
|
+
const prompt = typeof record.prompt === "string" && record.prompt.trim() ? record.prompt.trim() : void 0;
|
|
3539
|
+
if (!id || !prompt) return;
|
|
3540
|
+
return {
|
|
3541
|
+
details: typeof record.details === "string" && record.details.trim() ? record.details.trim() : void 0,
|
|
3542
|
+
enabled,
|
|
3543
|
+
id,
|
|
3544
|
+
kind,
|
|
3545
|
+
name,
|
|
3546
|
+
prompt
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
case "command": {
|
|
3550
|
+
const command = typeof record.command === "string" && record.command.trim() ? record.command.trim() : void 0;
|
|
3551
|
+
if (!id || !command) return;
|
|
3552
|
+
return {
|
|
3553
|
+
args: stringArray(record.args),
|
|
3554
|
+
command,
|
|
3555
|
+
cwd: typeof record.cwd === "string" && record.cwd.trim() ? record.cwd.trim() : void 0,
|
|
3556
|
+
enabled,
|
|
3557
|
+
env: stringRecord(record.env),
|
|
3558
|
+
id,
|
|
3559
|
+
kind,
|
|
3560
|
+
name,
|
|
3561
|
+
stdin: record.stdin === "json" || record.stdin === "none" ? record.stdin : void 0,
|
|
3562
|
+
timeoutMs: typeof record.timeoutMs === "number" && Number.isFinite(record.timeoutMs) ? record.timeoutMs : typeof record.timeoutMs === "string" && /^\d+$/.test(record.timeoutMs) ? Number(record.timeoutMs) : void 0
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
case "export-notes":
|
|
3566
|
+
if (!id) return;
|
|
3567
|
+
return {
|
|
3568
|
+
enabled,
|
|
3569
|
+
format: record.format === "json" || record.format === "markdown" || record.format === "raw" || record.format === "yaml" ? record.format : void 0,
|
|
3570
|
+
id,
|
|
3571
|
+
kind,
|
|
3572
|
+
name,
|
|
3573
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
3574
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
3575
|
+
};
|
|
3576
|
+
case "export-transcript":
|
|
3577
|
+
if (!id) return;
|
|
3578
|
+
return {
|
|
3579
|
+
enabled,
|
|
3580
|
+
format: record.format === "json" || record.format === "raw" || record.format === "text" || record.format === "yaml" ? record.format : void 0,
|
|
3581
|
+
id,
|
|
3582
|
+
kind,
|
|
3583
|
+
name,
|
|
3584
|
+
outputDir: typeof record.outputDir === "string" && record.outputDir.trim() ? record.outputDir.trim() : void 0,
|
|
3585
|
+
scopedOutput: typeof record.scopedOutput === "boolean" ? record.scopedOutput : void 0
|
|
3586
|
+
};
|
|
3587
|
+
default: return;
|
|
3316
3588
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3589
|
+
}
|
|
3590
|
+
function parseRule(value) {
|
|
3591
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3592
|
+
const record = value;
|
|
3593
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0;
|
|
3594
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0;
|
|
3595
|
+
const whenValue = record.when && typeof record.when === "object" && !Array.isArray(record.when) ? record.when : void 0;
|
|
3596
|
+
if (!id || !name || !whenValue) return;
|
|
3597
|
+
return {
|
|
3598
|
+
actions: Array.isArray(record.actions) ? record.actions.map((action, index) => parseAction(action, index)).filter((action) => Boolean(action)) : void 0,
|
|
3599
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : void 0,
|
|
3600
|
+
id,
|
|
3601
|
+
name,
|
|
3602
|
+
when: {
|
|
3603
|
+
eventKinds: stringArray(whenValue.eventKinds),
|
|
3604
|
+
folderIds: stringArray(whenValue.folderIds),
|
|
3605
|
+
folderNames: stringArray(whenValue.folderNames),
|
|
3606
|
+
meetingIds: stringArray(whenValue.meetingIds),
|
|
3607
|
+
tags: stringArray(whenValue.tags),
|
|
3608
|
+
titleIncludes: stringArray(whenValue.titleIncludes),
|
|
3609
|
+
titleMatches: typeof whenValue.titleMatches === "string" && whenValue.titleMatches.trim() ? whenValue.titleMatches.trim() : void 0,
|
|
3610
|
+
transcriptLoaded: typeof whenValue.transcriptLoaded === "boolean" ? whenValue.transcriptLoaded : void 0
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
var FileAutomationRuleStore = class {
|
|
3615
|
+
constructor(filePath = defaultAutomationRulesFilePath()) {
|
|
3616
|
+
this.filePath = filePath;
|
|
3319
3617
|
}
|
|
3320
|
-
async
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
this.#session = refreshedSession;
|
|
3325
|
-
await this.store.writeSession(refreshedSession);
|
|
3326
|
-
return;
|
|
3618
|
+
async readRules() {
|
|
3619
|
+
try {
|
|
3620
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
3621
|
+
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);
|
|
3327
3622
|
} catch {
|
|
3328
|
-
|
|
3329
|
-
this.#session = void 0;
|
|
3330
|
-
await this.store.clearSession();
|
|
3331
|
-
throw new Error("failed to refresh stored Granola session");
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
if (this.options.source) {
|
|
3335
|
-
const sourcedSession = await this.options.source.loadSession();
|
|
3336
|
-
this.#session = sourcedSession;
|
|
3337
|
-
await this.store.writeSession(sourcedSession);
|
|
3338
|
-
return;
|
|
3623
|
+
return [];
|
|
3339
3624
|
}
|
|
3340
|
-
this.#session = void 0;
|
|
3341
|
-
await this.store.clearSession();
|
|
3342
3625
|
}
|
|
3343
3626
|
};
|
|
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
|
-
};
|
|
3627
|
+
function defaultAutomationRulesFilePath() {
|
|
3628
|
+
return defaultGranolaToolkitPersistenceLayout().automationRulesFile;
|
|
3365
3629
|
}
|
|
3366
|
-
function
|
|
3367
|
-
|
|
3630
|
+
function includesIgnoreCase(candidate, values) {
|
|
3631
|
+
const lowerCandidate = candidate.toLowerCase();
|
|
3632
|
+
return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
|
|
3368
3633
|
}
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3634
|
+
function matchesRule(rule, event) {
|
|
3635
|
+
if (rule.enabled === false) return false;
|
|
3636
|
+
const { when } = rule;
|
|
3637
|
+
if (when.eventKinds?.length && !when.eventKinds.includes(event.kind)) return false;
|
|
3638
|
+
if (when.meetingIds?.length && !when.meetingIds.includes(event.meetingId)) return false;
|
|
3639
|
+
if (when.folderIds?.length && !event.folders.some((folder) => when.folderIds?.includes(folder.id))) return false;
|
|
3640
|
+
if (when.folderNames?.length && !event.folders.some((folder) => when.folderNames?.includes(folder.name))) return false;
|
|
3641
|
+
if (when.tags?.length && !event.tags.some((tag) => when.tags?.includes(tag))) return false;
|
|
3642
|
+
if (when.titleIncludes?.length && !includesIgnoreCase(event.title, when.titleIncludes)) return false;
|
|
3643
|
+
if (when.titleMatches) try {
|
|
3644
|
+
if (!new RegExp(when.titleMatches, "i").test(event.title)) return false;
|
|
3645
|
+
} catch {
|
|
3646
|
+
return false;
|
|
3647
|
+
}
|
|
3648
|
+
if (typeof when.transcriptLoaded === "boolean" && when.transcriptLoaded !== event.transcriptLoaded) return false;
|
|
3649
|
+
return true;
|
|
3371
3650
|
}
|
|
3372
|
-
function
|
|
3373
|
-
|
|
3651
|
+
function matchAutomationRules(rules, events, matchedAt) {
|
|
3652
|
+
const matches = [];
|
|
3653
|
+
for (const event of events) for (const rule of rules) {
|
|
3654
|
+
if (!matchesRule(rule, event)) continue;
|
|
3655
|
+
matches.push({
|
|
3656
|
+
eventId: event.id,
|
|
3657
|
+
eventKind: event.kind,
|
|
3658
|
+
folders: event.folders.map((folder) => ({ ...folder })),
|
|
3659
|
+
id: `${event.id}:${rule.id}`,
|
|
3660
|
+
matchedAt,
|
|
3661
|
+
meetingId: event.meetingId,
|
|
3662
|
+
ruleId: rule.id,
|
|
3663
|
+
ruleName: rule.name,
|
|
3664
|
+
tags: [...event.tags],
|
|
3665
|
+
title: event.title,
|
|
3666
|
+
transcriptLoaded: event.transcriptLoaded
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
return matches;
|
|
3374
3670
|
}
|
|
3375
|
-
function
|
|
3376
|
-
return
|
|
3671
|
+
function createDefaultAutomationRuleStore(filePath) {
|
|
3672
|
+
return new FileAutomationRuleStore(filePath);
|
|
3673
|
+
}
|
|
3674
|
+
//#endregion
|
|
3675
|
+
//#region src/cache.ts
|
|
3676
|
+
function parseCacheDocument(id, value) {
|
|
3677
|
+
const record = asRecord(value);
|
|
3678
|
+
if (!record) return;
|
|
3679
|
+
return {
|
|
3680
|
+
createdAt: stringValue(record.created_at),
|
|
3681
|
+
id,
|
|
3682
|
+
title: stringValue(record.title),
|
|
3683
|
+
updatedAt: stringValue(record.updated_at)
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
function parseTranscriptSegments(value) {
|
|
3687
|
+
if (!Array.isArray(value)) return;
|
|
3688
|
+
return value.flatMap((segment) => {
|
|
3689
|
+
const record = asRecord(segment);
|
|
3690
|
+
if (!record) return [];
|
|
3691
|
+
return [{
|
|
3692
|
+
documentId: stringValue(record.document_id),
|
|
3693
|
+
endTimestamp: stringValue(record.end_timestamp),
|
|
3694
|
+
id: stringValue(record.id),
|
|
3695
|
+
isFinal: Boolean(record.is_final),
|
|
3696
|
+
source: stringValue(record.source),
|
|
3697
|
+
startTimestamp: stringValue(record.start_timestamp),
|
|
3698
|
+
text: stringValue(record.text)
|
|
3699
|
+
}];
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
function parseCacheContents(contents) {
|
|
3703
|
+
const outer = parseJsonString(contents);
|
|
3704
|
+
if (!outer) throw new Error("failed to parse cache JSON");
|
|
3705
|
+
const rawCache = outer.cache;
|
|
3706
|
+
let cachePayload;
|
|
3707
|
+
if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
|
|
3708
|
+
else cachePayload = asRecord(rawCache);
|
|
3709
|
+
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
3710
|
+
if (!state) throw new Error("failed to parse cache state");
|
|
3711
|
+
const rawDocuments = asRecord(state.documents) ?? {};
|
|
3712
|
+
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
3713
|
+
const documents = {};
|
|
3714
|
+
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
3715
|
+
const document = parseCacheDocument(id, rawDocument);
|
|
3716
|
+
if (document) documents[id] = document;
|
|
3717
|
+
}
|
|
3718
|
+
const transcripts = {};
|
|
3719
|
+
for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
|
|
3720
|
+
const segments = parseTranscriptSegments(rawTranscript);
|
|
3721
|
+
if (segments) transcripts[id] = segments;
|
|
3722
|
+
}
|
|
3723
|
+
return {
|
|
3724
|
+
documents,
|
|
3725
|
+
transcripts
|
|
3726
|
+
};
|
|
3377
3727
|
}
|
|
3378
3728
|
//#endregion
|
|
3379
3729
|
//#region src/client/default-auth.ts
|
|
@@ -3829,97 +4179,6 @@ var GranolaPublicApiClient = class {
|
|
|
3829
4179
|
}
|
|
3830
4180
|
};
|
|
3831
4181
|
//#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
4182
|
//#region src/client/default.ts
|
|
3924
4183
|
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
3925
4184
|
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
@@ -4628,6 +4887,58 @@ function cloneMeetingSummary(meeting) {
|
|
|
4628
4887
|
tags: [...meeting.tags]
|
|
4629
4888
|
};
|
|
4630
4889
|
}
|
|
4890
|
+
function resolveActionFilePath(filePath, cwd) {
|
|
4891
|
+
return cwd ? resolve(cwd, filePath) : resolve(filePath);
|
|
4892
|
+
}
|
|
4893
|
+
function combinePromptSections(...values) {
|
|
4894
|
+
const sections = values.map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
4895
|
+
return sections.length > 0 ? sections.join("\n\n") : void 0;
|
|
4896
|
+
}
|
|
4897
|
+
function meetingTranscriptText(bundle) {
|
|
4898
|
+
const segments = bundle.document.transcriptSegments ?? (bundle.cacheData ? bundle.cacheData.transcripts[bundle.document.id] : void 0);
|
|
4899
|
+
if (!segments?.length) return;
|
|
4900
|
+
return segments.slice().sort((left, right) => left.startTimestamp.localeCompare(right.startTimestamp)).map((segment) => segment.text.trim()).filter(Boolean).join("\n");
|
|
4901
|
+
}
|
|
4902
|
+
function buildAutomationAgentPrompt(match, rule, instructions, bundle) {
|
|
4903
|
+
const transcriptText = bundle ? meetingTranscriptText(bundle)?.trim() : void 0;
|
|
4904
|
+
const context = {
|
|
4905
|
+
event: {
|
|
4906
|
+
id: match.eventId,
|
|
4907
|
+
kind: match.eventKind,
|
|
4908
|
+
matchedAt: match.matchedAt,
|
|
4909
|
+
meetingId: match.meetingId,
|
|
4910
|
+
transcriptLoaded: match.transcriptLoaded
|
|
4911
|
+
},
|
|
4912
|
+
folders: match.folders.map((folder) => ({
|
|
4913
|
+
id: folder.id,
|
|
4914
|
+
name: folder.name
|
|
4915
|
+
})),
|
|
4916
|
+
meeting: bundle ? {
|
|
4917
|
+
id: bundle.document.id,
|
|
4918
|
+
notesPlain: bundle.document.notesPlain,
|
|
4919
|
+
tags: [...bundle.document.tags],
|
|
4920
|
+
title: bundle.document.title,
|
|
4921
|
+
updatedAt: bundle.document.updatedAt
|
|
4922
|
+
} : {
|
|
4923
|
+
id: match.meetingId,
|
|
4924
|
+
tags: [...match.tags],
|
|
4925
|
+
title: match.title
|
|
4926
|
+
},
|
|
4927
|
+
rule: {
|
|
4928
|
+
id: rule.id,
|
|
4929
|
+
name: rule.name
|
|
4930
|
+
}
|
|
4931
|
+
};
|
|
4932
|
+
return [
|
|
4933
|
+
instructions.trim(),
|
|
4934
|
+
"Meeting context (JSON):",
|
|
4935
|
+
"```json",
|
|
4936
|
+
JSON.stringify(context, null, 2),
|
|
4937
|
+
"```",
|
|
4938
|
+
bundle?.document.notesPlain?.trim() ? `Existing notes:\n${bundle.document.notesPlain.trim()}` : "",
|
|
4939
|
+
transcriptText ? `Transcript:\n${transcriptText}` : ""
|
|
4940
|
+
].filter(Boolean).join("\n\n");
|
|
4941
|
+
}
|
|
4631
4942
|
function cloneState(state) {
|
|
4632
4943
|
return {
|
|
4633
4944
|
auth: { ...state.auth },
|
|
@@ -4636,6 +4947,7 @@ function cloneState(state) {
|
|
|
4636
4947
|
config: {
|
|
4637
4948
|
...state.config,
|
|
4638
4949
|
automation: state.config.automation ? { ...state.config.automation } : void 0,
|
|
4950
|
+
agents: state.config.agents ? { ...state.config.agents } : void 0,
|
|
4639
4951
|
notes: { ...state.config.notes },
|
|
4640
4952
|
transcripts: { ...state.config.transcripts }
|
|
4641
4953
|
},
|
|
@@ -4673,6 +4985,7 @@ function defaultState(config, auth, surface) {
|
|
|
4673
4985
|
},
|
|
4674
4986
|
config: {
|
|
4675
4987
|
...config,
|
|
4988
|
+
agents: config.agents ? { ...config.agents } : void 0,
|
|
4676
4989
|
notes: { ...config.notes },
|
|
4677
4990
|
transcripts: { ...config.transcripts }
|
|
4678
4991
|
},
|
|
@@ -4825,6 +5138,7 @@ var GranolaApp = class {
|
|
|
4825
5138
|
...rule,
|
|
4826
5139
|
actions: rule.actions?.map((action) => {
|
|
4827
5140
|
switch (action.kind) {
|
|
5141
|
+
case "agent": return { ...action };
|
|
4828
5142
|
case "ask-user": return { ...action };
|
|
4829
5143
|
case "command": return {
|
|
4830
5144
|
...action,
|
|
@@ -5373,6 +5687,24 @@ var GranolaApp = class {
|
|
|
5373
5687
|
child.stdin.end();
|
|
5374
5688
|
});
|
|
5375
5689
|
}
|
|
5690
|
+
async runAutomationAgent(match, rule, action) {
|
|
5691
|
+
const bundle = match.eventKind === "meeting.removed" ? void 0 : await this.maybeReadMeetingBundleById(match.meetingId, { requireCache: false });
|
|
5692
|
+
const promptFile = action.promptFile ? await readFile(resolveActionFilePath(action.promptFile, action.cwd), "utf8") : void 0;
|
|
5693
|
+
const systemPromptFile = action.systemPromptFile ? await readFile(resolveActionFilePath(action.systemPromptFile, action.cwd), "utf8") : void 0;
|
|
5694
|
+
const instructions = combinePromptSections(promptFile, action.prompt);
|
|
5695
|
+
if (!instructions) throw new Error(`automation agent action ${action.id} is missing prompt instructions`);
|
|
5696
|
+
const request = {
|
|
5697
|
+
cwd: action.cwd,
|
|
5698
|
+
dryRun: action.dryRun,
|
|
5699
|
+
model: action.model,
|
|
5700
|
+
prompt: buildAutomationAgentPrompt(match, rule, instructions, bundle),
|
|
5701
|
+
provider: action.provider,
|
|
5702
|
+
retries: action.retries,
|
|
5703
|
+
systemPrompt: combinePromptSections(systemPromptFile, action.systemPrompt),
|
|
5704
|
+
timeoutMs: action.timeoutMs
|
|
5705
|
+
};
|
|
5706
|
+
return await (this.deps.agentRunner ?? createDefaultAutomationAgentRunner(this.config)).run(request);
|
|
5707
|
+
}
|
|
5376
5708
|
async runAutomationActions(rules, matches) {
|
|
5377
5709
|
const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
|
|
5378
5710
|
const existingRunIds = new Set(this.#automationActionRuns.map((run) => run.id));
|
|
@@ -5388,6 +5720,7 @@ var GranolaApp = class {
|
|
|
5388
5720
|
exportNotes: async (nextMatch, nextAction) => await this.runAutomationNotesAction(nextMatch, nextAction),
|
|
5389
5721
|
exportTranscripts: async (nextMatch, nextAction) => await this.runAutomationTranscriptAction(nextMatch, nextAction),
|
|
5390
5722
|
nowIso: () => this.nowIso(),
|
|
5723
|
+
runAgent: async (nextMatch, nextRule, nextAction) => await this.runAutomationAgent(nextMatch, nextRule, nextAction),
|
|
5391
5724
|
runCommand: async (nextMatch, nextRule, nextAction) => await this.runAutomationCommand(nextMatch, nextRule, nextAction)
|
|
5392
5725
|
}));
|
|
5393
5726
|
}
|
|
@@ -5826,6 +6159,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
5826
6159
|
const syncState = await syncStateStore.readState();
|
|
5827
6160
|
return new GranolaApp(config, {
|
|
5828
6161
|
auth,
|
|
6162
|
+
agentRunner: createDefaultAutomationAgentRunner(config),
|
|
5829
6163
|
authController,
|
|
5830
6164
|
automationMatches,
|
|
5831
6165
|
automationMatchStore,
|
|
@@ -5855,6 +6189,10 @@ function pickString(value) {
|
|
|
5855
6189
|
function pickBoolean(value) {
|
|
5856
6190
|
return typeof value === "boolean" ? value : void 0;
|
|
5857
6191
|
}
|
|
6192
|
+
function pickNumber(value) {
|
|
6193
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
6194
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value.trim());
|
|
6195
|
+
}
|
|
5858
6196
|
function parseTomlScalar(rawValue) {
|
|
5859
6197
|
const value = rawValue.trim();
|
|
5860
6198
|
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) {
|
|
@@ -5907,9 +6245,23 @@ async function loadConfig(options) {
|
|
|
5907
6245
|
const configValues = config.values;
|
|
5908
6246
|
const defaultSupabase = firstExistingPath(granolaSupabaseCandidates());
|
|
5909
6247
|
const defaultCache = firstExistingPath(granolaCacheCandidates());
|
|
6248
|
+
const agentTimeoutValue = pickString(env.GRANOLA_AGENT_TIMEOUT) ?? pickString(configValues["agent-timeout"]) ?? pickString(configValues.agentTimeout) ?? "5m";
|
|
5910
6249
|
const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
|
|
5911
6250
|
return {
|
|
5912
6251
|
automation: { rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile },
|
|
6252
|
+
agents: {
|
|
6253
|
+
codexCommand: pickString(env.GRANOLA_CODEX_COMMAND) ?? pickString(configValues["codex-command"]) ?? pickString(configValues.codexCommand) ?? "codex",
|
|
6254
|
+
defaultModel: pickString(env.GRANOLA_AGENT_MODEL) ?? pickString(configValues["agent-model"]) ?? pickString(configValues.agentModel),
|
|
6255
|
+
defaultProvider: (() => {
|
|
6256
|
+
const value = pickString(env.GRANOLA_AGENT_PROVIDER) ?? pickString(configValues["agent-provider"]) ?? pickString(configValues.agentProvider);
|
|
6257
|
+
return value === "codex" || value === "openai" || value === "openrouter" ? value : void 0;
|
|
6258
|
+
})(),
|
|
6259
|
+
dryRun: envFlag(env.GRANOLA_AGENT_DRY_RUN) ?? pickBoolean(configValues["agent-dry-run"]) ?? pickBoolean(configValues.agentDryRun) ?? false,
|
|
6260
|
+
maxRetries: pickNumber(env.GRANOLA_AGENT_MAX_RETRIES) ?? pickNumber(configValues["agent-max-retries"]) ?? pickNumber(configValues.agentMaxRetries) ?? 2,
|
|
6261
|
+
openaiBaseUrl: pickString(env.GRANOLA_OPENAI_BASE_URL) ?? pickString(env.OPENAI_BASE_URL) ?? pickString(configValues["openai-base-url"]) ?? pickString(configValues.openaiBaseUrl) ?? "https://api.openai.com/v1",
|
|
6262
|
+
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",
|
|
6263
|
+
timeoutMs: parseDuration(agentTimeoutValue)
|
|
6264
|
+
},
|
|
5913
6265
|
apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
|
|
5914
6266
|
configFileUsed: config.path,
|
|
5915
6267
|
debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
|