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.
Files changed (2) hide show
  1. package/dist/cli.js +109 -8
  2. 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
- summary: cloneSyncSummary(file?.summary)
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: this.nowIso(),
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",