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.
- package/README.md +2 -0
- package/dist/cli.js +546 -44
- 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
|
}
|
|
@@ -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
|
|
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
|
|
3414
|
-
const cacheData = await this.loadCache();
|
|
3415
|
-
const documents = await this.listDocuments();
|
|
3416
|
-
const folders = await this.loadFolders();
|
|
3417
|
-
|
|
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
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
3721
|
-
|
|
3722
|
-
const
|
|
3723
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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>
|
|
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 () =>
|
|
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 () =>
|
|
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
|
-
|
|
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
|
`;
|