granola-toolkit 0.36.0 → 0.37.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 +109 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
@@ -30,6 +30,7 @@ const granolaTransportPaths = {
|
|
|
30
30
|
root: "/",
|
|
31
31
|
serverInfo: "/server/info",
|
|
32
32
|
syncRun: "/sync",
|
|
33
|
+
syncEvents: "/sync/events",
|
|
33
34
|
state: "/state"
|
|
34
35
|
};
|
|
35
36
|
function appendSearchParams(path, params) {
|
|
@@ -181,6 +182,10 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
181
182
|
async inspectSync() {
|
|
182
183
|
return cloneValue(this.#state.sync);
|
|
183
184
|
}
|
|
185
|
+
async listSyncEvents(options = {}) {
|
|
186
|
+
const path = options.limit ? `${granolaTransportPaths.syncEvents}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.syncEvents;
|
|
187
|
+
return await this.requestJson(path);
|
|
188
|
+
}
|
|
184
189
|
async loginAuth(options = {}) {
|
|
185
190
|
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
186
191
|
body: JSON.stringify(options),
|
|
@@ -2388,6 +2393,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2388
2393
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2389
2394
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2390
2395
|
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2396
|
+
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
2391
2397
|
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2392
2398
|
};
|
|
2393
2399
|
}
|
|
@@ -3281,14 +3287,17 @@ function cloneSyncSummary(summary) {
|
|
|
3281
3287
|
}
|
|
3282
3288
|
function normaliseSyncState(filePath, file) {
|
|
3283
3289
|
return {
|
|
3290
|
+
eventCount: file?.eventCount ?? 0,
|
|
3291
|
+
eventsFile: file?.eventsFile ?? defaultSyncEventsFilePath$1(),
|
|
3284
3292
|
filePath,
|
|
3285
3293
|
lastChanges: (file?.lastChanges ?? []).slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3286
|
-
lastCompletedAt: file?.lastCompletedAt,
|
|
3287
|
-
lastError: file?.lastError,
|
|
3288
|
-
lastFailedAt: file?.lastFailedAt,
|
|
3289
|
-
lastStartedAt: file?.lastStartedAt,
|
|
3290
3294
|
running: false,
|
|
3291
|
-
|
|
3295
|
+
...file?.lastCompletedAt ? { lastCompletedAt: file.lastCompletedAt } : {},
|
|
3296
|
+
...file?.lastError ? { lastError: file.lastError } : {},
|
|
3297
|
+
...file?.lastFailedAt ? { lastFailedAt: file.lastFailedAt } : {},
|
|
3298
|
+
...file?.lastRunId ? { lastRunId: file.lastRunId } : {},
|
|
3299
|
+
...file?.lastStartedAt ? { lastStartedAt: file.lastStartedAt } : {},
|
|
3300
|
+
...file?.summary ? { summary: cloneSyncSummary(file.summary) } : {}
|
|
3292
3301
|
};
|
|
3293
3302
|
}
|
|
3294
3303
|
var FileSyncStateStore = class {
|
|
@@ -3307,10 +3316,13 @@ var FileSyncStateStore = class {
|
|
|
3307
3316
|
async writeState(state) {
|
|
3308
3317
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3309
3318
|
const payload = {
|
|
3319
|
+
eventCount: state.eventCount,
|
|
3320
|
+
eventsFile: state.eventsFile,
|
|
3310
3321
|
lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3311
3322
|
lastCompletedAt: state.lastCompletedAt,
|
|
3312
3323
|
lastError: state.lastError,
|
|
3313
3324
|
lastFailedAt: state.lastFailedAt,
|
|
3325
|
+
lastRunId: state.lastRunId,
|
|
3314
3326
|
lastStartedAt: state.lastStartedAt,
|
|
3315
3327
|
summary: cloneSyncSummary(state.summary),
|
|
3316
3328
|
version: SYNC_STATE_VERSION
|
|
@@ -3324,10 +3336,45 @@ var FileSyncStateStore = class {
|
|
|
3324
3336
|
function defaultSyncStateFilePath() {
|
|
3325
3337
|
return defaultGranolaToolkitPersistenceLayout().syncStateFile;
|
|
3326
3338
|
}
|
|
3339
|
+
function defaultSyncEventsFilePath$1() {
|
|
3340
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3341
|
+
}
|
|
3327
3342
|
function createDefaultSyncStateStore() {
|
|
3328
3343
|
return new FileSyncStateStore();
|
|
3329
3344
|
}
|
|
3330
3345
|
//#endregion
|
|
3346
|
+
//#region src/sync-events.ts
|
|
3347
|
+
function cloneSyncEvent$1(event) {
|
|
3348
|
+
return { ...event };
|
|
3349
|
+
}
|
|
3350
|
+
var FileSyncEventStore = class {
|
|
3351
|
+
constructor(filePath = defaultSyncEventsFilePath()) {
|
|
3352
|
+
this.filePath = filePath;
|
|
3353
|
+
}
|
|
3354
|
+
async appendEvents(events) {
|
|
3355
|
+
if (events.length === 0) return;
|
|
3356
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3357
|
+
const payload = events.map((event) => JSON.stringify(event)).join("\n");
|
|
3358
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3359
|
+
encoding: "utf8",
|
|
3360
|
+
mode: 384
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
async readEvents(limit = 50) {
|
|
3364
|
+
try {
|
|
3365
|
+
return (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((event) => Boolean(event)).map(cloneSyncEvent$1).slice(-limit).reverse();
|
|
3366
|
+
} catch {
|
|
3367
|
+
return [];
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
function defaultSyncEventsFilePath() {
|
|
3372
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3373
|
+
}
|
|
3374
|
+
function createDefaultSyncEventStore() {
|
|
3375
|
+
return new FileSyncEventStore();
|
|
3376
|
+
}
|
|
3377
|
+
//#endregion
|
|
3331
3378
|
//#region src/sync.ts
|
|
3332
3379
|
function normaliseMeeting(meeting) {
|
|
3333
3380
|
return {
|
|
@@ -3425,6 +3472,18 @@ function diffMeetingSummaries(previous, next, folderCount) {
|
|
|
3425
3472
|
}
|
|
3426
3473
|
};
|
|
3427
3474
|
}
|
|
3475
|
+
function buildSyncEvents(runId, occurredAt, changes) {
|
|
3476
|
+
return changes.map((change, index) => ({
|
|
3477
|
+
id: `${runId}:${index + 1}`,
|
|
3478
|
+
kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
|
|
3479
|
+
meetingId: change.meetingId,
|
|
3480
|
+
occurredAt,
|
|
3481
|
+
previousUpdatedAt: change.previousUpdatedAt,
|
|
3482
|
+
runId,
|
|
3483
|
+
title: change.title,
|
|
3484
|
+
updatedAt: change.updatedAt
|
|
3485
|
+
}));
|
|
3486
|
+
}
|
|
3428
3487
|
//#endregion
|
|
3429
3488
|
//#region src/app/core.ts
|
|
3430
3489
|
function transcriptCount(cacheData) {
|
|
@@ -3455,6 +3514,9 @@ function cloneSyncState(state) {
|
|
|
3455
3514
|
summary: state.summary ? { ...state.summary } : void 0
|
|
3456
3515
|
};
|
|
3457
3516
|
}
|
|
3517
|
+
function cloneSyncEvent(event) {
|
|
3518
|
+
return { ...event };
|
|
3519
|
+
}
|
|
3458
3520
|
function cloneMeetingSummary(meeting) {
|
|
3459
3521
|
return {
|
|
3460
3522
|
...meeting,
|
|
@@ -3514,6 +3576,8 @@ function defaultState(config, auth, surface) {
|
|
|
3514
3576
|
meetingCount: 0
|
|
3515
3577
|
},
|
|
3516
3578
|
sync: {
|
|
3579
|
+
eventCount: 0,
|
|
3580
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3517
3581
|
filePath: defaultSyncStateFilePath(),
|
|
3518
3582
|
lastChanges: [],
|
|
3519
3583
|
running: false
|
|
@@ -3550,6 +3614,8 @@ var GranolaApp = class {
|
|
|
3550
3614
|
this.#state.sync = {
|
|
3551
3615
|
...this.#state.sync,
|
|
3552
3616
|
...cloneSyncState(deps.syncState ?? {
|
|
3617
|
+
eventCount: 0,
|
|
3618
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3553
3619
|
filePath: defaultSyncStateFilePath(),
|
|
3554
3620
|
lastChanges: [],
|
|
3555
3621
|
running: false
|
|
@@ -3609,6 +3675,9 @@ var GranolaApp = class {
|
|
|
3609
3675
|
if (!this.deps.syncStateStore) return;
|
|
3610
3676
|
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3611
3677
|
}
|
|
3678
|
+
createSyncRunId() {
|
|
3679
|
+
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
3680
|
+
}
|
|
3612
3681
|
applyAuthState(auth, options = {}) {
|
|
3613
3682
|
if (options.resetDocuments) this.resetRemoteState();
|
|
3614
3683
|
this.#state.auth = { ...auth };
|
|
@@ -3793,6 +3862,10 @@ var GranolaApp = class {
|
|
|
3793
3862
|
async inspectSync() {
|
|
3794
3863
|
return cloneSyncState(this.#state.sync);
|
|
3795
3864
|
}
|
|
3865
|
+
async listSyncEvents(options = {}) {
|
|
3866
|
+
if (!this.deps.syncEventStore) return { events: [] };
|
|
3867
|
+
return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
|
|
3868
|
+
}
|
|
3796
3869
|
async loginAuth(options = {}) {
|
|
3797
3870
|
const controller = this.requireAuthController();
|
|
3798
3871
|
try {
|
|
@@ -3856,11 +3929,17 @@ var GranolaApp = class {
|
|
|
3856
3929
|
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
3857
3930
|
await this.persistMeetingIndex(snapshot.meetings);
|
|
3858
3931
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
3932
|
+
const completedAt = this.nowIso();
|
|
3933
|
+
const runId = this.createSyncRunId();
|
|
3934
|
+
const events = buildSyncEvents(runId, completedAt, changes);
|
|
3935
|
+
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
3859
3936
|
this.#state.sync = {
|
|
3860
3937
|
...this.#state.sync,
|
|
3938
|
+
eventCount: this.#state.sync.eventCount + events.length,
|
|
3861
3939
|
lastChanges: changes.slice(0, 50).map(cloneSyncChange),
|
|
3862
|
-
lastCompletedAt:
|
|
3940
|
+
lastCompletedAt: completedAt,
|
|
3863
3941
|
lastError: void 0,
|
|
3942
|
+
lastRunId: runId,
|
|
3864
3943
|
running: false,
|
|
3865
3944
|
summary: { ...summary }
|
|
3866
3945
|
};
|
|
@@ -4209,6 +4288,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4209
4288
|
const exportJobs = await exportJobStore.readJobs();
|
|
4210
4289
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4211
4290
|
const meetingIndex = await meetingIndexStore.readIndex();
|
|
4291
|
+
const syncEventStore = createDefaultSyncEventStore();
|
|
4212
4292
|
const syncStateStore = createDefaultSyncStateStore();
|
|
4213
4293
|
const syncState = await syncStateStore.readState();
|
|
4214
4294
|
return new GranolaApp(config, {
|
|
@@ -4221,6 +4301,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4221
4301
|
meetingIndex,
|
|
4222
4302
|
meetingIndexStore,
|
|
4223
4303
|
now: options.now,
|
|
4304
|
+
syncEventStore,
|
|
4224
4305
|
syncState,
|
|
4225
4306
|
syncStateStore
|
|
4226
4307
|
}, { surface: options.surface });
|
|
@@ -6603,6 +6684,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6603
6684
|
exportJobs: true,
|
|
6604
6685
|
meetingIndex: true,
|
|
6605
6686
|
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
|
|
6687
|
+
syncEvents: true,
|
|
6606
6688
|
syncState: true
|
|
6607
6689
|
},
|
|
6608
6690
|
product: "granola-toolkit",
|
|
@@ -6707,6 +6789,10 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6707
6789
|
}), { headers: originHeaders });
|
|
6708
6790
|
return;
|
|
6709
6791
|
}
|
|
6792
|
+
if (method === "GET" && path === granolaTransportPaths.syncEvents) {
|
|
6793
|
+
sendJson(response, await app.listSyncEvents({ limit: parseInteger(url.searchParams.get("limit")) ?? 20 }), { headers: originHeaders });
|
|
6794
|
+
return;
|
|
6795
|
+
}
|
|
6710
6796
|
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
6711
6797
|
sendJson(response, { ok: true }, { headers: {
|
|
6712
6798
|
...originHeaders,
|
|
@@ -6991,6 +7077,7 @@ function printWebRoutes() {
|
|
|
6991
7077
|
console.log(" POST /exports/notes");
|
|
6992
7078
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
6993
7079
|
console.log(" POST /exports/transcripts");
|
|
7080
|
+
console.log(" GET /sync/events");
|
|
6994
7081
|
console.log(" POST /sync");
|
|
6995
7082
|
}
|
|
6996
7083
|
async function runGranolaWebWorkspace(app, options) {
|
|
@@ -7443,6 +7530,7 @@ const serveCommand = {
|
|
|
7443
7530
|
console.log(" POST /exports/notes");
|
|
7444
7531
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
7445
7532
|
console.log(" POST /exports/transcripts");
|
|
7533
|
+
console.log(" GET /sync/events");
|
|
7446
7534
|
console.log(" POST /sync");
|
|
7447
7535
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
7448
7536
|
if (password) console.log("Attach password: add --password <value>");
|
|
@@ -7460,10 +7548,12 @@ function syncHelp() {
|
|
|
7460
7548
|
|
|
7461
7549
|
Usage:
|
|
7462
7550
|
granola sync [options]
|
|
7551
|
+
granola sync events [options]
|
|
7463
7552
|
|
|
7464
7553
|
Options:
|
|
7465
7554
|
--watch Keep syncing in the background until interrupted
|
|
7466
7555
|
--interval <value> Poll interval for --watch, e.g. 60s or 5m (default: 60s)
|
|
7556
|
+
--limit <value> Event count for sync events output (default: 20)
|
|
7467
7557
|
--cache <path> Path to Granola cache JSON
|
|
7468
7558
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7469
7559
|
--supabase <path> Path to supabase.json
|
|
@@ -7489,12 +7579,13 @@ const syncCommand = {
|
|
|
7489
7579
|
cache: { type: "string" },
|
|
7490
7580
|
help: { type: "boolean" },
|
|
7491
7581
|
interval: { type: "string" },
|
|
7582
|
+
limit: { type: "string" },
|
|
7492
7583
|
timeout: { type: "string" },
|
|
7493
7584
|
watch: { type: "boolean" }
|
|
7494
7585
|
},
|
|
7495
7586
|
help: syncHelp,
|
|
7496
7587
|
name: "sync",
|
|
7497
|
-
async run({ commandFlags, globalFlags }) {
|
|
7588
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
7498
7589
|
const config = await loadConfig({
|
|
7499
7590
|
globalFlags,
|
|
7500
7591
|
subcommandFlags: commandFlags
|
|
@@ -7505,6 +7596,16 @@ const syncCommand = {
|
|
|
7505
7596
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7506
7597
|
const app = await createGranolaApp(config);
|
|
7507
7598
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7599
|
+
if (commandArgs[0] === "events") {
|
|
7600
|
+
const limit = typeof commandFlags.limit === "string" && /^\d+$/.test(commandFlags.limit) ? Number(commandFlags.limit) : 20;
|
|
7601
|
+
const result = await app.listSyncEvents({ limit });
|
|
7602
|
+
if (result.events.length === 0) {
|
|
7603
|
+
console.log("No sync events yet.");
|
|
7604
|
+
return 0;
|
|
7605
|
+
}
|
|
7606
|
+
for (const event of result.events) console.log(`${event.occurredAt} ${event.kind.padEnd(18)} ${event.title} (${event.meetingId})`);
|
|
7607
|
+
return 0;
|
|
7608
|
+
}
|
|
7508
7609
|
const result = await app.sync();
|
|
7509
7610
|
printSyncResult(result);
|
|
7510
7611
|
if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
|