granola-toolkit 0.34.6 → 0.35.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 +363 -32
- package/package.json +1 -1
package/README.md
CHANGED
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
|
}
|
|
@@ -2370,7 +2381,8 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2370
2381
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2371
2382
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2372
2383
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2373
|
-
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file"
|
|
2384
|
+
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2385
|
+
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2374
2386
|
};
|
|
2375
2387
|
}
|
|
2376
2388
|
//#endregion
|
|
@@ -3252,6 +3264,162 @@ function createDefaultMeetingIndexStore() {
|
|
|
3252
3264
|
return new FileMeetingIndexStore();
|
|
3253
3265
|
}
|
|
3254
3266
|
//#endregion
|
|
3267
|
+
//#region src/sync-state.ts
|
|
3268
|
+
const SYNC_STATE_VERSION = 1;
|
|
3269
|
+
const MAX_STORED_CHANGES = 50;
|
|
3270
|
+
function cloneSyncChange$1(change) {
|
|
3271
|
+
return { ...change };
|
|
3272
|
+
}
|
|
3273
|
+
function cloneSyncSummary(summary) {
|
|
3274
|
+
return summary ? { ...summary } : void 0;
|
|
3275
|
+
}
|
|
3276
|
+
function normaliseSyncState(filePath, file) {
|
|
3277
|
+
return {
|
|
3278
|
+
filePath,
|
|
3279
|
+
lastChanges: (file?.lastChanges ?? []).slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3280
|
+
lastCompletedAt: file?.lastCompletedAt,
|
|
3281
|
+
lastError: file?.lastError,
|
|
3282
|
+
lastFailedAt: file?.lastFailedAt,
|
|
3283
|
+
lastStartedAt: file?.lastStartedAt,
|
|
3284
|
+
running: false,
|
|
3285
|
+
summary: cloneSyncSummary(file?.summary)
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
var FileSyncStateStore = class {
|
|
3289
|
+
constructor(filePath = defaultSyncStateFilePath()) {
|
|
3290
|
+
this.filePath = filePath;
|
|
3291
|
+
}
|
|
3292
|
+
async readState() {
|
|
3293
|
+
try {
|
|
3294
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
3295
|
+
if (!parsed || parsed.version !== SYNC_STATE_VERSION) return normaliseSyncState(this.filePath);
|
|
3296
|
+
return normaliseSyncState(this.filePath, parsed);
|
|
3297
|
+
} catch {
|
|
3298
|
+
return normaliseSyncState(this.filePath);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
async writeState(state) {
|
|
3302
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3303
|
+
const payload = {
|
|
3304
|
+
lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3305
|
+
lastCompletedAt: state.lastCompletedAt,
|
|
3306
|
+
lastError: state.lastError,
|
|
3307
|
+
lastFailedAt: state.lastFailedAt,
|
|
3308
|
+
lastStartedAt: state.lastStartedAt,
|
|
3309
|
+
summary: cloneSyncSummary(state.summary),
|
|
3310
|
+
version: SYNC_STATE_VERSION
|
|
3311
|
+
};
|
|
3312
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
3313
|
+
encoding: "utf8",
|
|
3314
|
+
mode: 384
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
function defaultSyncStateFilePath() {
|
|
3319
|
+
return defaultGranolaToolkitPersistenceLayout().syncStateFile;
|
|
3320
|
+
}
|
|
3321
|
+
function createDefaultSyncStateStore() {
|
|
3322
|
+
return new FileSyncStateStore();
|
|
3323
|
+
}
|
|
3324
|
+
//#endregion
|
|
3325
|
+
//#region src/sync.ts
|
|
3326
|
+
function normaliseMeeting(meeting) {
|
|
3327
|
+
return {
|
|
3328
|
+
createdAt: meeting.createdAt,
|
|
3329
|
+
folders: meeting.folders.map((folder) => ({
|
|
3330
|
+
createdAt: folder.createdAt,
|
|
3331
|
+
description: folder.description,
|
|
3332
|
+
documentCount: folder.documentCount,
|
|
3333
|
+
id: folder.id,
|
|
3334
|
+
isFavourite: folder.isFavourite,
|
|
3335
|
+
name: folder.name,
|
|
3336
|
+
updatedAt: folder.updatedAt,
|
|
3337
|
+
workspaceId: folder.workspaceId
|
|
3338
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
3339
|
+
noteContentSource: meeting.noteContentSource,
|
|
3340
|
+
tags: [...meeting.tags].sort((left, right) => left.localeCompare(right)),
|
|
3341
|
+
title: meeting.title,
|
|
3342
|
+
transcriptLoaded: meeting.transcriptLoaded,
|
|
3343
|
+
transcriptSegmentCount: meeting.transcriptSegmentCount,
|
|
3344
|
+
updatedAt: meeting.updatedAt
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
function meetingChanged(previous, next) {
|
|
3348
|
+
return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
|
|
3349
|
+
}
|
|
3350
|
+
function diffMeetingSummaries(previous, next, folderCount) {
|
|
3351
|
+
const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
|
|
3352
|
+
const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
|
|
3353
|
+
const changes = [];
|
|
3354
|
+
let createdCount = 0;
|
|
3355
|
+
let changedCount = 0;
|
|
3356
|
+
let removedCount = 0;
|
|
3357
|
+
let transcriptReadyCount = 0;
|
|
3358
|
+
for (const meeting of next) {
|
|
3359
|
+
const previousMeeting = previousById.get(meeting.id);
|
|
3360
|
+
if (!previousMeeting) {
|
|
3361
|
+
createdCount += 1;
|
|
3362
|
+
changes.push({
|
|
3363
|
+
kind: "created",
|
|
3364
|
+
meetingId: meeting.id,
|
|
3365
|
+
title: meeting.title,
|
|
3366
|
+
updatedAt: meeting.updatedAt
|
|
3367
|
+
});
|
|
3368
|
+
if (meeting.transcriptLoaded) {
|
|
3369
|
+
transcriptReadyCount += 1;
|
|
3370
|
+
changes.push({
|
|
3371
|
+
kind: "transcript-ready",
|
|
3372
|
+
meetingId: meeting.id,
|
|
3373
|
+
title: meeting.title,
|
|
3374
|
+
updatedAt: meeting.updatedAt
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
continue;
|
|
3378
|
+
}
|
|
3379
|
+
if (meetingChanged(previousMeeting, meeting)) {
|
|
3380
|
+
changedCount += 1;
|
|
3381
|
+
changes.push({
|
|
3382
|
+
kind: "changed",
|
|
3383
|
+
meetingId: meeting.id,
|
|
3384
|
+
previousUpdatedAt: previousMeeting.updatedAt,
|
|
3385
|
+
title: meeting.title,
|
|
3386
|
+
updatedAt: meeting.updatedAt
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
if (!previousMeeting.transcriptLoaded && meeting.transcriptLoaded) {
|
|
3390
|
+
transcriptReadyCount += 1;
|
|
3391
|
+
changes.push({
|
|
3392
|
+
kind: "transcript-ready",
|
|
3393
|
+
meetingId: meeting.id,
|
|
3394
|
+
previousUpdatedAt: previousMeeting.updatedAt,
|
|
3395
|
+
title: meeting.title,
|
|
3396
|
+
updatedAt: meeting.updatedAt
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
for (const meeting of previous) {
|
|
3401
|
+
if (nextById.has(meeting.id)) continue;
|
|
3402
|
+
removedCount += 1;
|
|
3403
|
+
changes.push({
|
|
3404
|
+
kind: "removed",
|
|
3405
|
+
meetingId: meeting.id,
|
|
3406
|
+
previousUpdatedAt: meeting.updatedAt,
|
|
3407
|
+
title: meeting.title
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
return {
|
|
3411
|
+
changes,
|
|
3412
|
+
summary: {
|
|
3413
|
+
changedCount,
|
|
3414
|
+
createdCount,
|
|
3415
|
+
folderCount,
|
|
3416
|
+
meetingCount: next.length,
|
|
3417
|
+
removedCount,
|
|
3418
|
+
transcriptReadyCount
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
//#endregion
|
|
3255
3423
|
//#region src/app/core.ts
|
|
3256
3424
|
function transcriptCount(cacheData) {
|
|
3257
3425
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -3271,6 +3439,16 @@ function cloneExportJob(job) {
|
|
|
3271
3439
|
function cloneFolderSummary(folder) {
|
|
3272
3440
|
return { ...folder };
|
|
3273
3441
|
}
|
|
3442
|
+
function cloneSyncChange(change) {
|
|
3443
|
+
return { ...change };
|
|
3444
|
+
}
|
|
3445
|
+
function cloneSyncState(state) {
|
|
3446
|
+
return {
|
|
3447
|
+
...state,
|
|
3448
|
+
lastChanges: state.lastChanges.map(cloneSyncChange),
|
|
3449
|
+
summary: state.summary ? { ...state.summary } : void 0
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3274
3452
|
function cloneMeetingSummary(meeting) {
|
|
3275
3453
|
return {
|
|
3276
3454
|
...meeting,
|
|
@@ -3295,6 +3473,7 @@ function cloneState(state) {
|
|
|
3295
3473
|
transcripts: cloneExportState(state.exports.transcripts)
|
|
3296
3474
|
},
|
|
3297
3475
|
index: { ...state.index },
|
|
3476
|
+
sync: cloneSyncState(state.sync),
|
|
3298
3477
|
ui: { ...state.ui }
|
|
3299
3478
|
};
|
|
3300
3479
|
}
|
|
@@ -3328,6 +3507,11 @@ function defaultState(config, auth, surface) {
|
|
|
3328
3507
|
loaded: false,
|
|
3329
3508
|
meetingCount: 0
|
|
3330
3509
|
},
|
|
3510
|
+
sync: {
|
|
3511
|
+
filePath: defaultSyncStateFilePath(),
|
|
3512
|
+
lastChanges: [],
|
|
3513
|
+
running: false
|
|
3514
|
+
},
|
|
3331
3515
|
ui: {
|
|
3332
3516
|
surface,
|
|
3333
3517
|
view: "idle"
|
|
@@ -3357,6 +3541,14 @@ var GranolaApp = class {
|
|
|
3357
3541
|
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
3358
3542
|
meetingCount: this.#meetingIndex.length
|
|
3359
3543
|
};
|
|
3544
|
+
this.#state.sync = {
|
|
3545
|
+
...this.#state.sync,
|
|
3546
|
+
...cloneSyncState(deps.syncState ?? {
|
|
3547
|
+
filePath: defaultSyncStateFilePath(),
|
|
3548
|
+
lastChanges: [],
|
|
3549
|
+
running: false
|
|
3550
|
+
})
|
|
3551
|
+
};
|
|
3360
3552
|
}
|
|
3361
3553
|
getState() {
|
|
3362
3554
|
return cloneState(this.#state);
|
|
@@ -3378,16 +3570,39 @@ var GranolaApp = class {
|
|
|
3378
3570
|
resetRemoteState() {
|
|
3379
3571
|
this.#granolaClient = void 0;
|
|
3380
3572
|
this.#folders = void 0;
|
|
3573
|
+
this.#documents = void 0;
|
|
3574
|
+
this.resetDocumentsState();
|
|
3575
|
+
this.resetFoldersState();
|
|
3576
|
+
}
|
|
3577
|
+
resetDocumentsState() {
|
|
3381
3578
|
this.#documents = void 0;
|
|
3382
3579
|
this.#state.documents = {
|
|
3383
3580
|
count: 0,
|
|
3384
3581
|
loaded: false
|
|
3385
3582
|
};
|
|
3583
|
+
}
|
|
3584
|
+
resetFoldersState() {
|
|
3585
|
+
this.#folders = void 0;
|
|
3386
3586
|
this.#state.folders = {
|
|
3387
3587
|
count: 0,
|
|
3388
3588
|
loaded: false
|
|
3389
3589
|
};
|
|
3390
3590
|
}
|
|
3591
|
+
resetCacheState() {
|
|
3592
|
+
this.#cacheData = void 0;
|
|
3593
|
+
this.#cacheResolved = false;
|
|
3594
|
+
this.#state.cache = {
|
|
3595
|
+
configured: Boolean(this.config.transcripts.cacheFile),
|
|
3596
|
+
documentCount: 0,
|
|
3597
|
+
filePath: this.config.transcripts.cacheFile || void 0,
|
|
3598
|
+
loaded: false,
|
|
3599
|
+
transcriptCount: 0
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
async persistSyncState() {
|
|
3603
|
+
if (!this.deps.syncStateStore) return;
|
|
3604
|
+
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3605
|
+
}
|
|
3391
3606
|
applyAuthState(auth, options = {}) {
|
|
3392
3607
|
if (options.resetDocuments) this.resetRemoteState();
|
|
3393
3608
|
this.#state.auth = { ...auth };
|
|
@@ -3410,23 +3625,27 @@ var GranolaApp = class {
|
|
|
3410
3625
|
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
3411
3626
|
this.emitStateUpdate();
|
|
3412
3627
|
}
|
|
3413
|
-
async
|
|
3414
|
-
const cacheData = await this.loadCache();
|
|
3415
|
-
const documents = await this.listDocuments();
|
|
3416
|
-
const folders = await this.loadFolders();
|
|
3417
|
-
|
|
3628
|
+
async liveMeetingSnapshot(options = {}) {
|
|
3629
|
+
const cacheData = await this.loadCache({ forceRefresh: options.forceRefresh });
|
|
3630
|
+
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
3631
|
+
const folders = await this.loadFolders({ forceRefresh: options.forceRefresh });
|
|
3632
|
+
return {
|
|
3418
3633
|
cacheData,
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3634
|
+
documents,
|
|
3635
|
+
folders,
|
|
3636
|
+
meetings: listMeetings(documents, {
|
|
3637
|
+
cacheData,
|
|
3638
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3639
|
+
limit: Math.max(documents.length, 1),
|
|
3640
|
+
sort: "updated-desc"
|
|
3641
|
+
})
|
|
3642
|
+
};
|
|
3424
3643
|
}
|
|
3425
3644
|
triggerMeetingIndexRefresh() {
|
|
3426
3645
|
if (this.#refreshingMeetingIndex) return;
|
|
3427
3646
|
this.#refreshingMeetingIndex = (async () => {
|
|
3428
3647
|
try {
|
|
3429
|
-
await this.
|
|
3648
|
+
await this.runSync({ foreground: false });
|
|
3430
3649
|
} catch {} finally {
|
|
3431
3650
|
this.#refreshingMeetingIndex = void 0;
|
|
3432
3651
|
}
|
|
@@ -3471,7 +3690,7 @@ var GranolaApp = class {
|
|
|
3471
3690
|
}
|
|
3472
3691
|
async loadFolders(options = {}) {
|
|
3473
3692
|
if (options.forceRefresh) {
|
|
3474
|
-
this.
|
|
3693
|
+
this.resetFoldersState();
|
|
3475
3694
|
this.emitStateUpdate();
|
|
3476
3695
|
}
|
|
3477
3696
|
if (this.#folders) return this.#folders.map((folder) => ({
|
|
@@ -3565,6 +3784,9 @@ var GranolaApp = class {
|
|
|
3565
3784
|
const auth = await this.deps.authController.inspect();
|
|
3566
3785
|
return this.applyAuthState(auth, { view: "auth" });
|
|
3567
3786
|
}
|
|
3787
|
+
async inspectSync() {
|
|
3788
|
+
return cloneSyncState(this.#state.sync);
|
|
3789
|
+
}
|
|
3568
3790
|
async loginAuth(options = {}) {
|
|
3569
3791
|
const controller = this.requireAuthController();
|
|
3570
3792
|
try {
|
|
@@ -3614,9 +3836,56 @@ var GranolaApp = class {
|
|
|
3614
3836
|
throw error;
|
|
3615
3837
|
}
|
|
3616
3838
|
}
|
|
3839
|
+
async runSync(options) {
|
|
3840
|
+
const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
|
|
3841
|
+
this.#state.sync = {
|
|
3842
|
+
...this.#state.sync,
|
|
3843
|
+
lastError: void 0,
|
|
3844
|
+
lastStartedAt: this.nowIso(),
|
|
3845
|
+
running: true
|
|
3846
|
+
};
|
|
3847
|
+
if (options.foreground) this.setUiState({ view: "sync" });
|
|
3848
|
+
else this.emitStateUpdate();
|
|
3849
|
+
try {
|
|
3850
|
+
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
3851
|
+
await this.persistMeetingIndex(snapshot.meetings);
|
|
3852
|
+
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
3853
|
+
this.#state.sync = {
|
|
3854
|
+
...this.#state.sync,
|
|
3855
|
+
lastChanges: changes.slice(0, 50).map(cloneSyncChange),
|
|
3856
|
+
lastCompletedAt: this.nowIso(),
|
|
3857
|
+
lastError: void 0,
|
|
3858
|
+
running: false,
|
|
3859
|
+
summary: { ...summary }
|
|
3860
|
+
};
|
|
3861
|
+
await this.persistSyncState();
|
|
3862
|
+
this.emitStateUpdate();
|
|
3863
|
+
return {
|
|
3864
|
+
changes: changes.map(cloneSyncChange),
|
|
3865
|
+
state: cloneSyncState(this.#state.sync),
|
|
3866
|
+
summary: { ...summary }
|
|
3867
|
+
};
|
|
3868
|
+
} catch (error) {
|
|
3869
|
+
this.#state.sync = {
|
|
3870
|
+
...this.#state.sync,
|
|
3871
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
3872
|
+
lastFailedAt: this.nowIso(),
|
|
3873
|
+
running: false
|
|
3874
|
+
};
|
|
3875
|
+
await this.persistSyncState();
|
|
3876
|
+
this.emitStateUpdate();
|
|
3877
|
+
throw error;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
async sync(options = {}) {
|
|
3881
|
+
return await this.runSync({
|
|
3882
|
+
forceRefresh: options.forceRefresh,
|
|
3883
|
+
foreground: true
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3617
3886
|
async listDocuments(options = {}) {
|
|
3618
3887
|
if (options.forceRefresh) {
|
|
3619
|
-
this.
|
|
3888
|
+
this.resetDocumentsState();
|
|
3620
3889
|
this.emitStateUpdate();
|
|
3621
3890
|
}
|
|
3622
3891
|
if (this.#documents) return this.#documents;
|
|
@@ -3631,6 +3900,10 @@ var GranolaApp = class {
|
|
|
3631
3900
|
return documents;
|
|
3632
3901
|
}
|
|
3633
3902
|
async loadCache(options = {}) {
|
|
3903
|
+
if (options.forceRefresh) {
|
|
3904
|
+
this.resetCacheState();
|
|
3905
|
+
this.emitStateUpdate();
|
|
3906
|
+
}
|
|
3634
3907
|
if (this.#cacheResolved) {
|
|
3635
3908
|
if (options.required && !this.#cacheData) throw this.missingCacheError();
|
|
3636
3909
|
return this.#cacheData;
|
|
@@ -3717,28 +3990,19 @@ var GranolaApp = class {
|
|
|
3717
3990
|
source: "index"
|
|
3718
3991
|
};
|
|
3719
3992
|
}
|
|
3720
|
-
const
|
|
3721
|
-
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
-
required: Boolean(options.folderId)
|
|
3725
|
-
});
|
|
3726
|
-
const meetings = listMeetings(documents, {
|
|
3727
|
-
cacheData,
|
|
3993
|
+
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh });
|
|
3994
|
+
if (options.folderId && !snapshot.folders) throw new Error("Granola folder API is not configured");
|
|
3995
|
+
const meetings = listMeetings(snapshot.documents, {
|
|
3996
|
+
cacheData: snapshot.cacheData,
|
|
3728
3997
|
folderId: options.folderId,
|
|
3729
|
-
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3998
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(snapshot.folders),
|
|
3730
3999
|
limit: options.limit,
|
|
3731
4000
|
search: options.search,
|
|
3732
4001
|
sort: options.sort,
|
|
3733
4002
|
updatedFrom: options.updatedFrom,
|
|
3734
4003
|
updatedTo: options.updatedTo
|
|
3735
4004
|
});
|
|
3736
|
-
await this.persistMeetingIndex(
|
|
3737
|
-
cacheData,
|
|
3738
|
-
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3739
|
-
limit: Math.max(documents.length, 1),
|
|
3740
|
-
sort: "updated-desc"
|
|
3741
|
-
}));
|
|
4005
|
+
await this.persistMeetingIndex(snapshot.meetings);
|
|
3742
4006
|
this.setUiState({
|
|
3743
4007
|
folderSearch: void 0,
|
|
3744
4008
|
meetingListSource: "live",
|
|
@@ -3938,6 +4202,9 @@ async function createGranolaApp(config, options = {}) {
|
|
|
3938
4202
|
const exportJobStore = createDefaultExportJobStore();
|
|
3939
4203
|
const exportJobs = await exportJobStore.readJobs();
|
|
3940
4204
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4205
|
+
const meetingIndex = await meetingIndexStore.readIndex();
|
|
4206
|
+
const syncStateStore = createDefaultSyncStateStore();
|
|
4207
|
+
const syncState = await syncStateStore.readState();
|
|
3941
4208
|
return new GranolaApp(config, {
|
|
3942
4209
|
auth,
|
|
3943
4210
|
authController,
|
|
@@ -3945,9 +4212,11 @@ async function createGranolaApp(config, options = {}) {
|
|
|
3945
4212
|
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
3946
4213
|
exportJobs,
|
|
3947
4214
|
exportJobStore,
|
|
3948
|
-
meetingIndex
|
|
4215
|
+
meetingIndex,
|
|
3949
4216
|
meetingIndexStore,
|
|
3950
|
-
now: options.now
|
|
4217
|
+
now: options.now,
|
|
4218
|
+
syncState,
|
|
4219
|
+
syncStateStore
|
|
3951
4220
|
}, { surface: options.surface });
|
|
3952
4221
|
}
|
|
3953
4222
|
//#endregion
|
|
@@ -6290,12 +6559,14 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6290
6559
|
exports: true,
|
|
6291
6560
|
folders: true,
|
|
6292
6561
|
meetingOpen: true,
|
|
6562
|
+
sync: true,
|
|
6293
6563
|
webClient: enableWebClient
|
|
6294
6564
|
},
|
|
6295
6565
|
persistence: {
|
|
6296
6566
|
exportJobs: true,
|
|
6297
6567
|
meetingIndex: true,
|
|
6298
|
-
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
6568
|
+
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
|
|
6569
|
+
syncState: true
|
|
6299
6570
|
},
|
|
6300
6571
|
product: "granola-toolkit",
|
|
6301
6572
|
protocolVersion: 2,
|
|
@@ -6391,6 +6662,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6391
6662
|
sendJson(response, await app.inspectAuth(), { headers: originHeaders });
|
|
6392
6663
|
return;
|
|
6393
6664
|
}
|
|
6665
|
+
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
6666
|
+
const body = await readJsonBody(request);
|
|
6667
|
+
sendJson(response, await app.sync({ forceRefresh: typeof body.forceRefresh === "boolean" ? body.forceRefresh : void 0 }), { headers: originHeaders });
|
|
6668
|
+
return;
|
|
6669
|
+
}
|
|
6394
6670
|
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
6395
6671
|
sendJson(response, { ok: true }, { headers: {
|
|
6396
6672
|
...originHeaders,
|
|
@@ -6620,6 +6896,7 @@ function printWebRoutes() {
|
|
|
6620
6896
|
console.log(" POST /exports/notes");
|
|
6621
6897
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
6622
6898
|
console.log(" POST /exports/transcripts");
|
|
6899
|
+
console.log(" POST /sync");
|
|
6623
6900
|
}
|
|
6624
6901
|
async function runGranolaWebWorkspace(app, options) {
|
|
6625
6902
|
const server = await startGranolaServer(app, {
|
|
@@ -7048,6 +7325,7 @@ const serveCommand = {
|
|
|
7048
7325
|
console.log(" POST /exports/notes");
|
|
7049
7326
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
7050
7327
|
console.log(" POST /exports/transcripts");
|
|
7328
|
+
console.log(" POST /sync");
|
|
7051
7329
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
7052
7330
|
if (password) console.log("Attach password: add --password <value>");
|
|
7053
7331
|
await waitForShutdown(async () => await server.close());
|
|
@@ -7055,6 +7333,57 @@ const serveCommand = {
|
|
|
7055
7333
|
}
|
|
7056
7334
|
};
|
|
7057
7335
|
//#endregion
|
|
7336
|
+
//#region src/commands/sync.ts
|
|
7337
|
+
function syncHelp() {
|
|
7338
|
+
return `Granola sync
|
|
7339
|
+
|
|
7340
|
+
Usage:
|
|
7341
|
+
granola sync [options]
|
|
7342
|
+
|
|
7343
|
+
Options:
|
|
7344
|
+
--cache <path> Path to Granola cache JSON
|
|
7345
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7346
|
+
--supabase <path> Path to supabase.json
|
|
7347
|
+
--debug Enable debug logging
|
|
7348
|
+
--config <path> Path to .granola.toml
|
|
7349
|
+
-h, --help Show help
|
|
7350
|
+
`;
|
|
7351
|
+
}
|
|
7352
|
+
function pluralise(count, singular, plural = singular) {
|
|
7353
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
7354
|
+
}
|
|
7355
|
+
const syncCommand = {
|
|
7356
|
+
description: "Refresh the local meeting index and sync state",
|
|
7357
|
+
flags: {
|
|
7358
|
+
cache: { type: "string" },
|
|
7359
|
+
help: { type: "boolean" },
|
|
7360
|
+
timeout: { type: "string" }
|
|
7361
|
+
},
|
|
7362
|
+
help: syncHelp,
|
|
7363
|
+
name: "sync",
|
|
7364
|
+
async run({ commandFlags, globalFlags }) {
|
|
7365
|
+
const config = await loadConfig({
|
|
7366
|
+
globalFlags,
|
|
7367
|
+
subcommandFlags: commandFlags
|
|
7368
|
+
});
|
|
7369
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
7370
|
+
debug(config.debug, "supabase", config.supabase);
|
|
7371
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
7372
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7373
|
+
const app = await createGranolaApp(config);
|
|
7374
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7375
|
+
const result = await app.sync();
|
|
7376
|
+
console.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")})`);
|
|
7377
|
+
const lines = result.changes.slice(0, 10).map((change) => {
|
|
7378
|
+
return ` ${change.kind.padEnd(16)} ${change.title} (${change.meetingId})`;
|
|
7379
|
+
});
|
|
7380
|
+
for (const line of lines) console.log(line);
|
|
7381
|
+
if (result.changes.length > lines.length) console.log(` ...and ${result.changes.length - lines.length} more change(s)`);
|
|
7382
|
+
if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
|
|
7383
|
+
return 0;
|
|
7384
|
+
}
|
|
7385
|
+
};
|
|
7386
|
+
//#endregion
|
|
7058
7387
|
//#region src/commands/tui.ts
|
|
7059
7388
|
function tuiHelp() {
|
|
7060
7389
|
return `Granola tui
|
|
@@ -7191,6 +7520,7 @@ const commands = [
|
|
|
7191
7520
|
meetingCommand,
|
|
7192
7521
|
notesCommand,
|
|
7193
7522
|
serveCommand,
|
|
7523
|
+
syncCommand,
|
|
7194
7524
|
tuiCommand,
|
|
7195
7525
|
transcriptsCommand,
|
|
7196
7526
|
{
|
|
@@ -7317,6 +7647,7 @@ Global options:
|
|
|
7317
7647
|
Examples:
|
|
7318
7648
|
granola attach http://127.0.0.1:4123
|
|
7319
7649
|
granola folder list
|
|
7650
|
+
granola sync
|
|
7320
7651
|
granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
|
|
7321
7652
|
granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
|
|
7322
7653
|
`;
|