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