granola-toolkit 0.34.6 → 0.36.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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +546 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,8 @@ The published package exposes both `granola` and `granola-toolkit` as executable
21
21
 
22
22
  ```bash
23
23
  granola auth login
24
+ granola sync
25
+ granola sync --watch
24
26
  granola folder list
25
27
  granola meeting list --limit 10
26
28
  granola notes --folder Team
package/dist/cli.js CHANGED
@@ -29,6 +29,7 @@ const granolaTransportPaths = {
29
29
  meetings: "/meetings",
30
30
  root: "/",
31
31
  serverInfo: "/server/info",
32
+ syncRun: "/sync",
32
33
  state: "/state"
33
34
  };
34
35
  function appendSearchParams(path, params) {
@@ -177,6 +178,9 @@ var GranolaServerClient = class GranolaServerClient {
177
178
  async inspectAuth() {
178
179
  return await this.requestJson(granolaTransportPaths.authStatus);
179
180
  }
181
+ async inspectSync() {
182
+ return cloneValue(this.#state.sync);
183
+ }
180
184
  async loginAuth(options = {}) {
181
185
  return await this.requestJson(granolaTransportPaths.authLogin, {
182
186
  body: JSON.stringify(options),
@@ -197,6 +201,13 @@ var GranolaServerClient = class GranolaServerClient {
197
201
  method: "POST"
198
202
  });
199
203
  }
204
+ async sync(options = {}) {
205
+ return await this.requestJson(granolaTransportPaths.syncRun, {
206
+ body: JSON.stringify(options),
207
+ headers: { "content-type": "application/json" },
208
+ method: "POST"
209
+ });
210
+ }
200
211
  async listFolders(options = {}) {
201
212
  return await this.requestJson(granolaFoldersPath(options));
202
213
  }
@@ -1367,7 +1378,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
1367
1378
  }
1368
1379
  }
1369
1380
  function buildGranolaTuiSummary(state, meetingSource) {
1370
- return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | list ${meetingSource}`;
1381
+ return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
1371
1382
  }
1372
1383
  //#endregion
1373
1384
  //#region src/tui/theme.ts
@@ -1838,6 +1849,10 @@ var GranolaTuiWorkspace = class {
1838
1849
  }
1839
1850
  async refresh(forceRefresh) {
1840
1851
  try {
1852
+ if (forceRefresh) {
1853
+ this.setStatus("Syncing…");
1854
+ await this.app.sync();
1855
+ }
1841
1856
  await this.loadFolders({
1842
1857
  forceRefresh,
1843
1858
  setStatus: false
@@ -1847,7 +1862,9 @@ var GranolaTuiWorkspace = class {
1847
1862
  preferredMeetingId: this.#selectedMeetingId
1848
1863
  });
1849
1864
  if (this.#selectedMeetingId) await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
1850
- } catch {}
1865
+ } catch (error) {
1866
+ if (error instanceof Error && error.message) this.setStatus(error.message, "error");
1867
+ }
1851
1868
  }
1852
1869
  async moveMeetingSelection(delta) {
1853
1870
  if (this.#meetings.length === 0) return;
@@ -2234,7 +2251,7 @@ var GranolaTuiWorkspace = class {
2234
2251
  const bodyLines = [];
2235
2252
  for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
2236
2253
  const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
2237
- const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
2254
+ const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2238
2255
  return [
2239
2256
  headerTitle,
2240
2257
  headerSummary,
@@ -2252,7 +2269,7 @@ async function runGranolaTui(app, options = {}) {
2252
2269
  onExit: () => {
2253
2270
  workspace.dispose();
2254
2271
  tui.stop();
2255
- Promise.resolve(app.close?.()).catch(() => {}).finally(() => {
2272
+ Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {}).finally(() => {
2256
2273
  resolve(0);
2257
2274
  });
2258
2275
  }
@@ -2262,7 +2279,7 @@ async function runGranolaTui(app, options = {}) {
2262
2279
  await workspace.initialise();
2263
2280
  } catch (error) {
2264
2281
  workspace.dispose();
2265
- await Promise.resolve(app.close?.()).catch(() => {});
2282
+ await Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {});
2266
2283
  reject(error);
2267
2284
  return;
2268
2285
  }
@@ -2370,7 +2387,8 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
2370
2387
  exportJobsFile: join(dataDirectory, "export-jobs.json"),
2371
2388
  meetingIndexFile: join(dataDirectory, "meeting-index.json"),
2372
2389
  sessionFile: join(dataDirectory, "session.json"),
2373
- sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file"
2390
+ sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
2391
+ syncStateFile: join(dataDirectory, "sync-state.json")
2374
2392
  };
2375
2393
  }
2376
2394
  //#endregion
@@ -3252,6 +3270,162 @@ function createDefaultMeetingIndexStore() {
3252
3270
  return new FileMeetingIndexStore();
3253
3271
  }
3254
3272
  //#endregion
3273
+ //#region src/sync-state.ts
3274
+ const SYNC_STATE_VERSION = 1;
3275
+ const MAX_STORED_CHANGES = 50;
3276
+ function cloneSyncChange$1(change) {
3277
+ return { ...change };
3278
+ }
3279
+ function cloneSyncSummary(summary) {
3280
+ return summary ? { ...summary } : void 0;
3281
+ }
3282
+ function normaliseSyncState(filePath, file) {
3283
+ return {
3284
+ filePath,
3285
+ 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
+ running: false,
3291
+ summary: cloneSyncSummary(file?.summary)
3292
+ };
3293
+ }
3294
+ var FileSyncStateStore = class {
3295
+ constructor(filePath = defaultSyncStateFilePath()) {
3296
+ this.filePath = filePath;
3297
+ }
3298
+ async readState() {
3299
+ try {
3300
+ const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
3301
+ if (!parsed || parsed.version !== SYNC_STATE_VERSION) return normaliseSyncState(this.filePath);
3302
+ return normaliseSyncState(this.filePath, parsed);
3303
+ } catch {
3304
+ return normaliseSyncState(this.filePath);
3305
+ }
3306
+ }
3307
+ async writeState(state) {
3308
+ await mkdir(dirname(this.filePath), { recursive: true });
3309
+ const payload = {
3310
+ lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
3311
+ lastCompletedAt: state.lastCompletedAt,
3312
+ lastError: state.lastError,
3313
+ lastFailedAt: state.lastFailedAt,
3314
+ lastStartedAt: state.lastStartedAt,
3315
+ summary: cloneSyncSummary(state.summary),
3316
+ version: SYNC_STATE_VERSION
3317
+ };
3318
+ await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
3319
+ encoding: "utf8",
3320
+ mode: 384
3321
+ });
3322
+ }
3323
+ };
3324
+ function defaultSyncStateFilePath() {
3325
+ return defaultGranolaToolkitPersistenceLayout().syncStateFile;
3326
+ }
3327
+ function createDefaultSyncStateStore() {
3328
+ return new FileSyncStateStore();
3329
+ }
3330
+ //#endregion
3331
+ //#region src/sync.ts
3332
+ function normaliseMeeting(meeting) {
3333
+ return {
3334
+ createdAt: meeting.createdAt,
3335
+ folders: meeting.folders.map((folder) => ({
3336
+ createdAt: folder.createdAt,
3337
+ description: folder.description,
3338
+ documentCount: folder.documentCount,
3339
+ id: folder.id,
3340
+ isFavourite: folder.isFavourite,
3341
+ name: folder.name,
3342
+ updatedAt: folder.updatedAt,
3343
+ workspaceId: folder.workspaceId
3344
+ })).sort((left, right) => left.id.localeCompare(right.id)),
3345
+ noteContentSource: meeting.noteContentSource,
3346
+ tags: [...meeting.tags].sort((left, right) => left.localeCompare(right)),
3347
+ title: meeting.title,
3348
+ transcriptLoaded: meeting.transcriptLoaded,
3349
+ transcriptSegmentCount: meeting.transcriptSegmentCount,
3350
+ updatedAt: meeting.updatedAt
3351
+ };
3352
+ }
3353
+ function meetingChanged(previous, next) {
3354
+ return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
3355
+ }
3356
+ function diffMeetingSummaries(previous, next, folderCount) {
3357
+ const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
3358
+ const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
3359
+ const changes = [];
3360
+ let createdCount = 0;
3361
+ let changedCount = 0;
3362
+ let removedCount = 0;
3363
+ let transcriptReadyCount = 0;
3364
+ for (const meeting of next) {
3365
+ const previousMeeting = previousById.get(meeting.id);
3366
+ if (!previousMeeting) {
3367
+ createdCount += 1;
3368
+ changes.push({
3369
+ kind: "created",
3370
+ meetingId: meeting.id,
3371
+ title: meeting.title,
3372
+ updatedAt: meeting.updatedAt
3373
+ });
3374
+ if (meeting.transcriptLoaded) {
3375
+ transcriptReadyCount += 1;
3376
+ changes.push({
3377
+ kind: "transcript-ready",
3378
+ meetingId: meeting.id,
3379
+ title: meeting.title,
3380
+ updatedAt: meeting.updatedAt
3381
+ });
3382
+ }
3383
+ continue;
3384
+ }
3385
+ if (meetingChanged(previousMeeting, meeting)) {
3386
+ changedCount += 1;
3387
+ changes.push({
3388
+ kind: "changed",
3389
+ meetingId: meeting.id,
3390
+ previousUpdatedAt: previousMeeting.updatedAt,
3391
+ title: meeting.title,
3392
+ updatedAt: meeting.updatedAt
3393
+ });
3394
+ }
3395
+ if (!previousMeeting.transcriptLoaded && meeting.transcriptLoaded) {
3396
+ transcriptReadyCount += 1;
3397
+ changes.push({
3398
+ kind: "transcript-ready",
3399
+ meetingId: meeting.id,
3400
+ previousUpdatedAt: previousMeeting.updatedAt,
3401
+ title: meeting.title,
3402
+ updatedAt: meeting.updatedAt
3403
+ });
3404
+ }
3405
+ }
3406
+ for (const meeting of previous) {
3407
+ if (nextById.has(meeting.id)) continue;
3408
+ removedCount += 1;
3409
+ changes.push({
3410
+ kind: "removed",
3411
+ meetingId: meeting.id,
3412
+ previousUpdatedAt: meeting.updatedAt,
3413
+ title: meeting.title
3414
+ });
3415
+ }
3416
+ return {
3417
+ changes,
3418
+ summary: {
3419
+ changedCount,
3420
+ createdCount,
3421
+ folderCount,
3422
+ meetingCount: next.length,
3423
+ removedCount,
3424
+ transcriptReadyCount
3425
+ }
3426
+ };
3427
+ }
3428
+ //#endregion
3255
3429
  //#region src/app/core.ts
3256
3430
  function transcriptCount(cacheData) {
3257
3431
  return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
@@ -3271,6 +3445,16 @@ function cloneExportJob(job) {
3271
3445
  function cloneFolderSummary(folder) {
3272
3446
  return { ...folder };
3273
3447
  }
3448
+ function cloneSyncChange(change) {
3449
+ return { ...change };
3450
+ }
3451
+ function cloneSyncState(state) {
3452
+ return {
3453
+ ...state,
3454
+ lastChanges: state.lastChanges.map(cloneSyncChange),
3455
+ summary: state.summary ? { ...state.summary } : void 0
3456
+ };
3457
+ }
3274
3458
  function cloneMeetingSummary(meeting) {
3275
3459
  return {
3276
3460
  ...meeting,
@@ -3295,6 +3479,7 @@ function cloneState(state) {
3295
3479
  transcripts: cloneExportState(state.exports.transcripts)
3296
3480
  },
3297
3481
  index: { ...state.index },
3482
+ sync: cloneSyncState(state.sync),
3298
3483
  ui: { ...state.ui }
3299
3484
  };
3300
3485
  }
@@ -3328,6 +3513,11 @@ function defaultState(config, auth, surface) {
3328
3513
  loaded: false,
3329
3514
  meetingCount: 0
3330
3515
  },
3516
+ sync: {
3517
+ filePath: defaultSyncStateFilePath(),
3518
+ lastChanges: [],
3519
+ running: false
3520
+ },
3331
3521
  ui: {
3332
3522
  surface,
3333
3523
  view: "idle"
@@ -3357,6 +3547,14 @@ var GranolaApp = class {
3357
3547
  loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
3358
3548
  meetingCount: this.#meetingIndex.length
3359
3549
  };
3550
+ this.#state.sync = {
3551
+ ...this.#state.sync,
3552
+ ...cloneSyncState(deps.syncState ?? {
3553
+ filePath: defaultSyncStateFilePath(),
3554
+ lastChanges: [],
3555
+ running: false
3556
+ })
3557
+ };
3360
3558
  }
3361
3559
  getState() {
3362
3560
  return cloneState(this.#state);
@@ -3378,16 +3576,39 @@ var GranolaApp = class {
3378
3576
  resetRemoteState() {
3379
3577
  this.#granolaClient = void 0;
3380
3578
  this.#folders = void 0;
3579
+ this.#documents = void 0;
3580
+ this.resetDocumentsState();
3581
+ this.resetFoldersState();
3582
+ }
3583
+ resetDocumentsState() {
3381
3584
  this.#documents = void 0;
3382
3585
  this.#state.documents = {
3383
3586
  count: 0,
3384
3587
  loaded: false
3385
3588
  };
3589
+ }
3590
+ resetFoldersState() {
3591
+ this.#folders = void 0;
3386
3592
  this.#state.folders = {
3387
3593
  count: 0,
3388
3594
  loaded: false
3389
3595
  };
3390
3596
  }
3597
+ resetCacheState() {
3598
+ this.#cacheData = void 0;
3599
+ this.#cacheResolved = false;
3600
+ this.#state.cache = {
3601
+ configured: Boolean(this.config.transcripts.cacheFile),
3602
+ documentCount: 0,
3603
+ filePath: this.config.transcripts.cacheFile || void 0,
3604
+ loaded: false,
3605
+ transcriptCount: 0
3606
+ };
3607
+ }
3608
+ async persistSyncState() {
3609
+ if (!this.deps.syncStateStore) return;
3610
+ await this.deps.syncStateStore.writeState(this.#state.sync);
3611
+ }
3391
3612
  applyAuthState(auth, options = {}) {
3392
3613
  if (options.resetDocuments) this.resetRemoteState();
3393
3614
  this.#state.auth = { ...auth };
@@ -3410,23 +3631,27 @@ var GranolaApp = class {
3410
3631
  if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
3411
3632
  this.emitStateUpdate();
3412
3633
  }
3413
- async refreshMeetingIndexFromLiveData() {
3414
- const cacheData = await this.loadCache();
3415
- const documents = await this.listDocuments();
3416
- const folders = await this.loadFolders();
3417
- const meetings = listMeetings(documents, {
3634
+ async liveMeetingSnapshot(options = {}) {
3635
+ const cacheData = await this.loadCache({ forceRefresh: options.forceRefresh });
3636
+ const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
3637
+ const folders = await this.loadFolders({ forceRefresh: options.forceRefresh });
3638
+ return {
3418
3639
  cacheData,
3419
- foldersByDocumentId: this.buildFoldersByDocumentId(folders),
3420
- limit: documents.length || 1,
3421
- sort: "updated-desc"
3422
- });
3423
- await this.persistMeetingIndex(meetings);
3640
+ documents,
3641
+ folders,
3642
+ meetings: listMeetings(documents, {
3643
+ cacheData,
3644
+ foldersByDocumentId: this.buildFoldersByDocumentId(folders),
3645
+ limit: Math.max(documents.length, 1),
3646
+ sort: "updated-desc"
3647
+ })
3648
+ };
3424
3649
  }
3425
3650
  triggerMeetingIndexRefresh() {
3426
3651
  if (this.#refreshingMeetingIndex) return;
3427
3652
  this.#refreshingMeetingIndex = (async () => {
3428
3653
  try {
3429
- await this.refreshMeetingIndexFromLiveData();
3654
+ await this.runSync({ foreground: false });
3430
3655
  } catch {} finally {
3431
3656
  this.#refreshingMeetingIndex = void 0;
3432
3657
  }
@@ -3471,7 +3696,7 @@ var GranolaApp = class {
3471
3696
  }
3472
3697
  async loadFolders(options = {}) {
3473
3698
  if (options.forceRefresh) {
3474
- this.resetRemoteState();
3699
+ this.resetFoldersState();
3475
3700
  this.emitStateUpdate();
3476
3701
  }
3477
3702
  if (this.#folders) return this.#folders.map((folder) => ({
@@ -3565,6 +3790,9 @@ var GranolaApp = class {
3565
3790
  const auth = await this.deps.authController.inspect();
3566
3791
  return this.applyAuthState(auth, { view: "auth" });
3567
3792
  }
3793
+ async inspectSync() {
3794
+ return cloneSyncState(this.#state.sync);
3795
+ }
3568
3796
  async loginAuth(options = {}) {
3569
3797
  const controller = this.requireAuthController();
3570
3798
  try {
@@ -3614,9 +3842,56 @@ var GranolaApp = class {
3614
3842
  throw error;
3615
3843
  }
3616
3844
  }
3845
+ async runSync(options) {
3846
+ const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
3847
+ this.#state.sync = {
3848
+ ...this.#state.sync,
3849
+ lastError: void 0,
3850
+ lastStartedAt: this.nowIso(),
3851
+ running: true
3852
+ };
3853
+ if (options.foreground) this.setUiState({ view: "sync" });
3854
+ else this.emitStateUpdate();
3855
+ try {
3856
+ const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
3857
+ await this.persistMeetingIndex(snapshot.meetings);
3858
+ const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
3859
+ this.#state.sync = {
3860
+ ...this.#state.sync,
3861
+ lastChanges: changes.slice(0, 50).map(cloneSyncChange),
3862
+ lastCompletedAt: this.nowIso(),
3863
+ lastError: void 0,
3864
+ running: false,
3865
+ summary: { ...summary }
3866
+ };
3867
+ await this.persistSyncState();
3868
+ this.emitStateUpdate();
3869
+ return {
3870
+ changes: changes.map(cloneSyncChange),
3871
+ state: cloneSyncState(this.#state.sync),
3872
+ summary: { ...summary }
3873
+ };
3874
+ } catch (error) {
3875
+ this.#state.sync = {
3876
+ ...this.#state.sync,
3877
+ lastError: error instanceof Error ? error.message : String(error),
3878
+ lastFailedAt: this.nowIso(),
3879
+ running: false
3880
+ };
3881
+ await this.persistSyncState();
3882
+ this.emitStateUpdate();
3883
+ throw error;
3884
+ }
3885
+ }
3886
+ async sync(options = {}) {
3887
+ return await this.runSync({
3888
+ forceRefresh: options.forceRefresh,
3889
+ foreground: options.foreground ?? true
3890
+ });
3891
+ }
3617
3892
  async listDocuments(options = {}) {
3618
3893
  if (options.forceRefresh) {
3619
- this.resetRemoteState();
3894
+ this.resetDocumentsState();
3620
3895
  this.emitStateUpdate();
3621
3896
  }
3622
3897
  if (this.#documents) return this.#documents;
@@ -3631,6 +3906,10 @@ var GranolaApp = class {
3631
3906
  return documents;
3632
3907
  }
3633
3908
  async loadCache(options = {}) {
3909
+ if (options.forceRefresh) {
3910
+ this.resetCacheState();
3911
+ this.emitStateUpdate();
3912
+ }
3634
3913
  if (this.#cacheResolved) {
3635
3914
  if (options.required && !this.#cacheData) throw this.missingCacheError();
3636
3915
  return this.#cacheData;
@@ -3717,28 +3996,19 @@ var GranolaApp = class {
3717
3996
  source: "index"
3718
3997
  };
3719
3998
  }
3720
- const cacheData = await this.loadCache();
3721
- const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
3722
- const folders = await this.loadFolders({
3723
- forceRefresh: options.forceRefresh,
3724
- required: Boolean(options.folderId)
3725
- });
3726
- const meetings = listMeetings(documents, {
3727
- cacheData,
3999
+ const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh });
4000
+ if (options.folderId && !snapshot.folders) throw new Error("Granola folder API is not configured");
4001
+ const meetings = listMeetings(snapshot.documents, {
4002
+ cacheData: snapshot.cacheData,
3728
4003
  folderId: options.folderId,
3729
- foldersByDocumentId: this.buildFoldersByDocumentId(folders),
4004
+ foldersByDocumentId: this.buildFoldersByDocumentId(snapshot.folders),
3730
4005
  limit: options.limit,
3731
4006
  search: options.search,
3732
4007
  sort: options.sort,
3733
4008
  updatedFrom: options.updatedFrom,
3734
4009
  updatedTo: options.updatedTo
3735
4010
  });
3736
- await this.persistMeetingIndex(listMeetings(documents, {
3737
- cacheData,
3738
- foldersByDocumentId: this.buildFoldersByDocumentId(folders),
3739
- limit: Math.max(documents.length, 1),
3740
- sort: "updated-desc"
3741
- }));
4011
+ await this.persistMeetingIndex(snapshot.meetings);
3742
4012
  this.setUiState({
3743
4013
  folderSearch: void 0,
3744
4014
  meetingListSource: "live",
@@ -3938,6 +4208,9 @@ async function createGranolaApp(config, options = {}) {
3938
4208
  const exportJobStore = createDefaultExportJobStore();
3939
4209
  const exportJobs = await exportJobStore.readJobs();
3940
4210
  const meetingIndexStore = createDefaultMeetingIndexStore();
4211
+ const meetingIndex = await meetingIndexStore.readIndex();
4212
+ const syncStateStore = createDefaultSyncStateStore();
4213
+ const syncState = await syncStateStore.readState();
3941
4214
  return new GranolaApp(config, {
3942
4215
  auth,
3943
4216
  authController,
@@ -3945,9 +4218,11 @@ async function createGranolaApp(config, options = {}) {
3945
4218
  createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
3946
4219
  exportJobs,
3947
4220
  exportJobStore,
3948
- meetingIndex: await meetingIndexStore.readIndex(),
4221
+ meetingIndex,
3949
4222
  meetingIndexStore,
3950
- now: options.now
4223
+ now: options.now,
4224
+ syncState,
4225
+ syncStateStore
3951
4226
  }, { surface: options.surface });
3952
4227
  }
3953
4228
  //#endregion
@@ -4056,6 +4331,14 @@ function parseTrustedOrigins(value) {
4056
4331
  if (typeof value !== "string" || !value.trim()) return [];
4057
4332
  return value.split(",").map((origin) => origin.trim()).filter(Boolean);
4058
4333
  }
4334
+ function parseSyncInterval(value, fallbackMs = 6e4) {
4335
+ if (value === void 0) return fallbackMs;
4336
+ if (typeof value !== "string" || !value.trim()) throw new Error("invalid sync interval: expected a duration like 60s or 5m");
4337
+ return parseDuration(value);
4338
+ }
4339
+ function syncEnabled(commandFlags) {
4340
+ return commandFlags["no-sync"] !== true;
4341
+ }
4059
4342
  async function waitForShutdown(close) {
4060
4343
  await new Promise((resolve, reject) => {
4061
4344
  let closing = false;
@@ -4634,12 +4917,20 @@ function renderAppState() {
4634
4917
  const folderStatus = appState.folders.loaded
4635
4918
  ? appState.folders.count + " folders"
4636
4919
  : "not loaded";
4920
+ const syncStatus = appState.sync.running
4921
+ ? "running"
4922
+ : appState.sync.lastError
4923
+ ? "error"
4924
+ : appState.sync.lastCompletedAt
4925
+ ? "last " + appState.sync.lastCompletedAt.slice(11, 19)
4926
+ : "idle";
4637
4927
 
4638
4928
  els.appState.innerHTML = [
4639
4929
  '<div class="status-grid">',
4640
4930
  '<div><span class="status-label">Surface</span><strong>' + escapeHtml(appState.ui.surface) + "</strong></div>",
4641
4931
  '<div><span class="status-label">View</span><strong>' + escapeHtml(appState.ui.view) + "</strong></div>",
4642
4932
  '<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
4933
+ '<div><span class="status-label">Sync</span><strong>' + escapeHtml(syncStatus) + "</strong></div>",
4643
4934
  '<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
4644
4935
  '<div><span class="status-label">Folders</span><strong>' + escapeHtml(folderStatus) + "</strong></div>",
4645
4936
  '<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
@@ -4786,7 +5077,7 @@ function renderMeetingList() {
4786
5077
  });
4787
5078
  const message = filterSummary
4788
5079
  ? "No meetings match " + filterSummary + "."
4789
- : "No meetings yet. Try Refresh.";
5080
+ : "No meetings yet. Try Sync now.";
4790
5081
  els.list.innerHTML = '<div class="meeting-empty">' + escapeHtml(message) + "</div>";
4791
5082
  renderMeetingDetail();
4792
5083
  return;
@@ -5065,8 +5356,16 @@ async function quickOpenMeeting() {
5065
5356
  }
5066
5357
 
5067
5358
  async function refreshAll(forceLiveMeetings = false) {
5068
- setStatus("Refreshing…", "busy");
5359
+ setStatus(forceLiveMeetings ? "Syncing…" : "Refreshing…", "busy");
5069
5360
  try {
5361
+ if (forceLiveMeetings) {
5362
+ await fetchJson("/sync", {
5363
+ body: JSON.stringify({ forceRefresh: true }),
5364
+ headers: { "content-type": "application/json" },
5365
+ method: "POST",
5366
+ });
5367
+ }
5368
+
5070
5369
  await loadFolders({ refresh: forceLiveMeetings });
5071
5370
  const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
5072
5371
  await loadMeetings({ refresh: forceLiveMeetings });
@@ -5076,7 +5375,14 @@ async function refreshAll(forceLiveMeetings = false) {
5076
5375
  auth: authState,
5077
5376
  };
5078
5377
  renderAppState();
5079
- setStatus(forceLiveMeetings ? "Live data refreshed" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
5378
+ setStatus(
5379
+ forceLiveMeetings
5380
+ ? "Sync complete"
5381
+ : state.meetingSource === "index"
5382
+ ? "Loaded from index"
5383
+ : "Connected",
5384
+ "ok",
5385
+ );
5080
5386
  } catch (error) {
5081
5387
  if (error.authRequired) {
5082
5388
  setStatus("Server locked", "error");
@@ -5521,7 +5827,7 @@ const granolaWebMarkup = String.raw`
5521
5827
  </section>
5522
5828
  <section class="toolbar">
5523
5829
  <div class="toolbar-actions">
5524
- <button class="button button--primary" data-refresh>Refresh</button>
5830
+ <button class="button button--primary" data-refresh>Sync now</button>
5525
5831
  <button class="button button--secondary" data-export-notes>Export Notes</button>
5526
5832
  <button class="button button--secondary" data-export-transcripts>Export Transcripts</button>
5527
5833
  </div>
@@ -6290,12 +6596,14 @@ async function startGranolaServer(app, options = {}) {
6290
6596
  exports: true,
6291
6597
  folders: true,
6292
6598
  meetingOpen: true,
6599
+ sync: true,
6293
6600
  webClient: enableWebClient
6294
6601
  },
6295
6602
  persistence: {
6296
6603
  exportJobs: true,
6297
6604
  meetingIndex: true,
6298
- sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
6605
+ sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
6606
+ syncState: true
6299
6607
  },
6300
6608
  product: "granola-toolkit",
6301
6609
  protocolVersion: 2,
@@ -6391,6 +6699,14 @@ async function startGranolaServer(app, options = {}) {
6391
6699
  sendJson(response, await app.inspectAuth(), { headers: originHeaders });
6392
6700
  return;
6393
6701
  }
6702
+ if (method === "POST" && path === granolaTransportPaths.syncRun) {
6703
+ const body = await readJsonBody(request);
6704
+ sendJson(response, await app.sync({
6705
+ foreground: typeof body.foreground === "boolean" ? body.foreground : void 0,
6706
+ forceRefresh: typeof body.forceRefresh === "boolean" ? body.forceRefresh : void 0
6707
+ }), { headers: originHeaders });
6708
+ return;
6709
+ }
6394
6710
  if (method === "POST" && path === granolaTransportPaths.authLock) {
6395
6711
  sendJson(response, { ok: true }, { headers: {
6396
6712
  ...originHeaders,
@@ -6575,6 +6891,59 @@ async function startGranolaServer(app, options = {}) {
6575
6891
  };
6576
6892
  }
6577
6893
  //#endregion
6894
+ //#region src/sync-loop.ts
6895
+ function createGranolaSyncLoop(options) {
6896
+ const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
6897
+ const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
6898
+ let inFlight;
6899
+ let stopped = true;
6900
+ let timer;
6901
+ const schedule = () => {
6902
+ if (stopped) return;
6903
+ timer = setTimeoutImpl(() => {
6904
+ runCycle();
6905
+ }, options.intervalMs);
6906
+ };
6907
+ const runCycle = async () => {
6908
+ if (stopped || inFlight) return;
6909
+ inFlight = (async () => {
6910
+ try {
6911
+ const result = await options.app.sync({
6912
+ forceRefresh: true,
6913
+ foreground: false
6914
+ });
6915
+ await options.onSynced?.(result);
6916
+ } catch (error) {
6917
+ options.logger?.warn?.(`background sync failed: ${error instanceof Error ? error.message : String(error)}`);
6918
+ await options.onError?.(error);
6919
+ } finally {
6920
+ inFlight = void 0;
6921
+ schedule();
6922
+ }
6923
+ })();
6924
+ await inFlight;
6925
+ };
6926
+ return {
6927
+ start(loopOptions = {}) {
6928
+ if (!stopped) return;
6929
+ stopped = false;
6930
+ if (loopOptions.immediate === false) {
6931
+ schedule();
6932
+ return;
6933
+ }
6934
+ runCycle();
6935
+ },
6936
+ async stop() {
6937
+ stopped = true;
6938
+ if (timer !== void 0) {
6939
+ clearTimeoutImpl(timer);
6940
+ timer = void 0;
6941
+ }
6942
+ await inFlight;
6943
+ }
6944
+ };
6945
+ }
6946
+ //#endregion
6578
6947
  //#region src/web-url.ts
6579
6948
  function buildGranolaMeetingUrl(baseUrl, meetingId) {
6580
6949
  const url = new URL(baseUrl);
@@ -6593,6 +6962,8 @@ function resolveGranolaWebWorkspaceOptions(commandFlags) {
6593
6962
  openBrowser: commandFlags.open !== false,
6594
6963
  password: typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password.trim() : void 0,
6595
6964
  port,
6965
+ syncEnabled: syncEnabled(commandFlags),
6966
+ syncIntervalMs: parseSyncInterval(commandFlags["sync-interval"]),
6596
6967
  trustedOrigins: parseTrustedOrigins(commandFlags["trusted-origins"])
6597
6968
  };
6598
6969
  }
@@ -6620,6 +6991,7 @@ function printWebRoutes() {
6620
6991
  console.log(" POST /exports/notes");
6621
6992
  console.log(" POST /exports/jobs/:id/rerun");
6622
6993
  console.log(" POST /exports/transcripts");
6994
+ console.log(" POST /sync");
6623
6995
  }
6624
6996
  async function runGranolaWebWorkspace(app, options) {
6625
6997
  const server = await startGranolaServer(app, {
@@ -6631,6 +7003,12 @@ async function runGranolaWebWorkspace(app, options) {
6631
7003
  trustedOrigins: options.trustedOrigins
6632
7004
  }
6633
7005
  });
7006
+ const syncLoop = options.syncEnabled ? createGranolaSyncLoop({
7007
+ app,
7008
+ intervalMs: options.syncIntervalMs,
7009
+ logger: console
7010
+ }) : void 0;
7011
+ syncLoop?.start();
6634
7012
  const targetUrl = options.targetMeetingId ? buildGranolaMeetingUrl(server.url, options.targetMeetingId) : new URL(server.url);
6635
7013
  console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
6636
7014
  if (targetUrl.href !== server.url.href) console.log(`Focused meeting URL: ${targetUrl.href}`);
@@ -6638,6 +7016,7 @@ async function runGranolaWebWorkspace(app, options) {
6638
7016
  if (options.password) console.log("Server password protection: enabled");
6639
7017
  else if (options.networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
6640
7018
  if (options.trustedOrigins.length > 0) console.log(`Trusted origins: ${options.trustedOrigins.join(", ")}`);
7019
+ console.log(options.syncEnabled ? `Background sync: enabled (${options.syncIntervalMs}ms)` : "Background sync: disabled");
6641
7020
  printWebRoutes();
6642
7021
  console.log(`Attach: granola attach ${server.url.href}`);
6643
7022
  if (options.password) console.log("Attach password: add --password <value>");
@@ -6648,7 +7027,10 @@ async function runGranolaWebWorkspace(app, options) {
6648
7027
  console.error(`failed to open browser automatically: ${message}`);
6649
7028
  console.error(`open ${targetUrl.href} manually`);
6650
7029
  }
6651
- await waitForShutdown(async () => await server.close());
7030
+ await waitForShutdown(async () => {
7031
+ await syncLoop?.stop();
7032
+ await server.close();
7033
+ });
6652
7034
  return 0;
6653
7035
  }
6654
7036
  //#endregion
@@ -6979,6 +7361,8 @@ Options:
6979
7361
  --hostname <value> Hostname to bind (overrides network default)
6980
7362
  --port <value> Port to bind (default: 0 for any available port)
6981
7363
  --password <value> Optional server password for API and browser access
7364
+ --sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
7365
+ --no-sync Disable the background sync loop
6982
7366
  --trusted-origins <v> Comma-separated extra browser origins to trust
6983
7367
  --cache <path> Path to Granola cache JSON
6984
7368
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
@@ -6995,8 +7379,10 @@ const serveCommand = {
6995
7379
  help: { type: "boolean" },
6996
7380
  hostname: { type: "string" },
6997
7381
  network: { type: "string" },
7382
+ "no-sync": { type: "boolean" },
6998
7383
  password: { type: "string" },
6999
7384
  port: { type: "string" },
7385
+ "sync-interval": { type: "string" },
7000
7386
  timeout: { type: "string" },
7001
7387
  "trusted-origins": { type: "string" }
7002
7388
  },
@@ -7016,6 +7402,8 @@ const serveCommand = {
7016
7402
  const hostname = resolveServerHostname(networkMode, commandFlags.hostname);
7017
7403
  const port = parsePort(commandFlags.port);
7018
7404
  const password = typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password : void 0;
7405
+ const backgroundSyncEnabled = syncEnabled(commandFlags);
7406
+ const syncIntervalMs = parseSyncInterval(commandFlags["sync-interval"]);
7019
7407
  const trustedOrigins = parseTrustedOrigins(commandFlags["trusted-origins"]);
7020
7408
  const server = await startGranolaServer(app, {
7021
7409
  hostname,
@@ -7025,11 +7413,18 @@ const serveCommand = {
7025
7413
  trustedOrigins
7026
7414
  }
7027
7415
  });
7416
+ const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
7417
+ app,
7418
+ intervalMs: syncIntervalMs,
7419
+ logger: console
7420
+ }) : void 0;
7421
+ syncLoop?.start();
7028
7422
  console.log(`Granola server listening on ${server.url.href}`);
7029
7423
  console.log(`Network mode: ${networkMode}`);
7030
7424
  if (password) console.log("Server password protection: enabled");
7031
7425
  else if (networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
7032
7426
  if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
7427
+ console.log(backgroundSyncEnabled ? `Background sync: enabled (${syncIntervalMs}ms)` : "Background sync: disabled");
7033
7428
  console.log("Endpoints:");
7034
7429
  console.log(" GET /health");
7035
7430
  console.log(" GET /server/info");
@@ -7048,9 +7443,90 @@ const serveCommand = {
7048
7443
  console.log(" POST /exports/notes");
7049
7444
  console.log(" POST /exports/jobs/:id/rerun");
7050
7445
  console.log(" POST /exports/transcripts");
7446
+ console.log(" POST /sync");
7051
7447
  console.log(`Attach: granola attach ${server.url.href}`);
7052
7448
  if (password) console.log("Attach password: add --password <value>");
7053
- await waitForShutdown(async () => await server.close());
7449
+ await waitForShutdown(async () => {
7450
+ await syncLoop?.stop();
7451
+ await server.close();
7452
+ });
7453
+ return 0;
7454
+ }
7455
+ };
7456
+ //#endregion
7457
+ //#region src/commands/sync.ts
7458
+ function syncHelp() {
7459
+ return `Granola sync
7460
+
7461
+ Usage:
7462
+ granola sync [options]
7463
+
7464
+ Options:
7465
+ --watch Keep syncing in the background until interrupted
7466
+ --interval <value> Poll interval for --watch, e.g. 60s or 5m (default: 60s)
7467
+ --cache <path> Path to Granola cache JSON
7468
+ --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
7469
+ --supabase <path> Path to supabase.json
7470
+ --debug Enable debug logging
7471
+ --config <path> Path to .granola.toml
7472
+ -h, --help Show help
7473
+ `;
7474
+ }
7475
+ function pluralise(count, singular, plural = singular) {
7476
+ return `${count} ${count === 1 ? singular : plural}`;
7477
+ }
7478
+ function printSyncResult(result, log = console.log) {
7479
+ log(`✓ Synced ${pluralise(result.summary.meetingCount, "meeting", "meetings")} across ${pluralise(result.summary.folderCount, "folder", "folders")} (${pluralise(result.summary.createdCount, "created")}, ${pluralise(result.summary.changedCount, "updated")}, ${pluralise(result.summary.removedCount, "removed")}, ${pluralise(result.summary.transcriptReadyCount, "transcript ready", "transcripts ready")})`);
7480
+ const lines = result.changes.slice(0, 10).map((change) => {
7481
+ return ` ${change.kind.padEnd(16)} ${change.title} (${change.meetingId})`;
7482
+ });
7483
+ for (const line of lines) log(line);
7484
+ if (result.changes.length > lines.length) log(` ...and ${result.changes.length - lines.length} more change(s)`);
7485
+ }
7486
+ const syncCommand = {
7487
+ description: "Refresh the local meeting index and sync state",
7488
+ flags: {
7489
+ cache: { type: "string" },
7490
+ help: { type: "boolean" },
7491
+ interval: { type: "string" },
7492
+ timeout: { type: "string" },
7493
+ watch: { type: "boolean" }
7494
+ },
7495
+ help: syncHelp,
7496
+ name: "sync",
7497
+ async run({ commandFlags, globalFlags }) {
7498
+ const config = await loadConfig({
7499
+ globalFlags,
7500
+ subcommandFlags: commandFlags
7501
+ });
7502
+ debug(config.debug, "using config", config.configFileUsed ?? "(none)");
7503
+ debug(config.debug, "supabase", config.supabase);
7504
+ debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
7505
+ debug(config.debug, "timeoutMs", config.notes.timeoutMs);
7506
+ const app = await createGranolaApp(config);
7507
+ debug(config.debug, "authMode", app.getState().auth.mode);
7508
+ const result = await app.sync();
7509
+ printSyncResult(result);
7510
+ if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
7511
+ if (commandFlags.watch === true) {
7512
+ const intervalMs = parseSyncInterval(commandFlags.interval);
7513
+ const syncLoop = createGranolaSyncLoop({
7514
+ app,
7515
+ intervalMs,
7516
+ logger: console,
7517
+ onError: async (error) => {
7518
+ console.error(error instanceof Error ? error.message : String(error));
7519
+ },
7520
+ onSynced: async (nextResult) => {
7521
+ printSyncResult(nextResult);
7522
+ }
7523
+ });
7524
+ syncLoop.start({ immediate: false });
7525
+ console.log(`Watching for Granola changes every ${intervalMs}ms. Press Ctrl+C to stop.`);
7526
+ await waitForShutdown(async () => {
7527
+ await syncLoop.stop();
7528
+ });
7529
+ }
7054
7530
  return 0;
7055
7531
  }
7056
7532
  };
@@ -7064,6 +7540,8 @@ Usage:
7064
7540
 
7065
7541
  Options:
7066
7542
  --meeting <id> Open the workspace focused on a specific meeting
7543
+ --sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
7544
+ --no-sync Disable the background sync loop
7067
7545
  --cache <path> Path to Granola cache JSON
7068
7546
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
7069
7547
  --supabase <path> Path to supabase.json
@@ -7078,6 +7556,8 @@ const tuiCommand = {
7078
7556
  cache: { type: "string" },
7079
7557
  help: { type: "boolean" },
7080
7558
  meeting: { type: "string" },
7559
+ "no-sync": { type: "boolean" },
7560
+ "sync-interval": { type: "string" },
7081
7561
  timeout: { type: "string" }
7082
7562
  },
7083
7563
  help: tuiHelp,
@@ -7091,7 +7571,23 @@ const tuiCommand = {
7091
7571
  debug(config.debug, "supabase", config.supabase);
7092
7572
  debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
7093
7573
  debug(config.debug, "timeoutMs", config.notes.timeoutMs);
7094
- return await runGranolaTui(await createGranolaApp(config, { surface: "tui" }), { initialMeetingId: typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0 });
7574
+ const app = await createGranolaApp(config, { surface: "tui" });
7575
+ const initialMeetingId = typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0;
7576
+ const backgroundSyncEnabled = syncEnabled(commandFlags);
7577
+ const syncIntervalMs = parseSyncInterval(commandFlags["sync-interval"]);
7578
+ const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
7579
+ app,
7580
+ intervalMs: syncIntervalMs,
7581
+ logger: console
7582
+ }) : void 0;
7583
+ syncLoop?.start();
7584
+ debug(config.debug, "backgroundSync", backgroundSyncEnabled ? `${syncIntervalMs}ms` : "disabled");
7585
+ return await runGranolaTui(app, {
7586
+ initialMeetingId,
7587
+ onClose: async () => {
7588
+ await syncLoop?.stop();
7589
+ }
7590
+ });
7095
7591
  }
7096
7592
  };
7097
7593
  //#endregion
@@ -7171,6 +7667,8 @@ Options:
7171
7667
  --hostname <value> Hostname to bind (overrides network default)
7172
7668
  --port <value> Port to bind (default: 0 for any available port)
7173
7669
  --password <value> Optional server password for API and browser access
7670
+ --sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
7671
+ --no-sync Disable the background sync loop
7174
7672
  --trusted-origins <v> Comma-separated extra browser origins to trust
7175
7673
  --cache <path> Path to Granola cache JSON
7176
7674
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
@@ -7191,6 +7689,7 @@ const commands = [
7191
7689
  meetingCommand,
7192
7690
  notesCommand,
7193
7691
  serveCommand,
7692
+ syncCommand,
7194
7693
  tuiCommand,
7195
7694
  transcriptsCommand,
7196
7695
  {
@@ -7201,9 +7700,11 @@ const commands = [
7201
7700
  hostname: { type: "string" },
7202
7701
  meeting: { type: "string" },
7203
7702
  network: { type: "string" },
7703
+ "no-sync": { type: "boolean" },
7204
7704
  open: { type: "boolean" },
7205
7705
  password: { type: "string" },
7206
7706
  port: { type: "string" },
7707
+ "sync-interval": { type: "string" },
7207
7708
  timeout: { type: "string" },
7208
7709
  "trusted-origins": { type: "string" }
7209
7710
  },
@@ -7317,6 +7818,7 @@ Global options:
7317
7818
  Examples:
7318
7819
  granola attach http://127.0.0.1:4123
7319
7820
  granola folder list
7821
+ granola sync
7320
7822
  granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
7321
7823
  granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
7322
7824
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.34.6",
3
+ "version": "0.36.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",