granola-toolkit 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.js +301 -29
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
@@ -30,6 +30,7 @@ const granolaTransportPaths = {
|
|
|
30
30
|
root: "/",
|
|
31
31
|
serverInfo: "/server/info",
|
|
32
32
|
syncRun: "/sync",
|
|
33
|
+
syncEvents: "/sync/events",
|
|
33
34
|
state: "/state"
|
|
34
35
|
};
|
|
35
36
|
function appendSearchParams(path, params) {
|
|
@@ -181,6 +182,10 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
181
182
|
async inspectSync() {
|
|
182
183
|
return cloneValue(this.#state.sync);
|
|
183
184
|
}
|
|
185
|
+
async listSyncEvents(options = {}) {
|
|
186
|
+
const path = options.limit ? `${granolaTransportPaths.syncEvents}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.syncEvents;
|
|
187
|
+
return await this.requestJson(path);
|
|
188
|
+
}
|
|
184
189
|
async loginAuth(options = {}) {
|
|
185
190
|
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
186
191
|
body: JSON.stringify(options),
|
|
@@ -1378,7 +1383,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1378
1383
|
}
|
|
1379
1384
|
}
|
|
1380
1385
|
function buildGranolaTuiSummary(state, 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"} | list ${meetingSource}`;
|
|
1386
|
+
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}`;
|
|
1382
1387
|
}
|
|
1383
1388
|
//#endregion
|
|
1384
1389
|
//#region src/tui/theme.ts
|
|
@@ -1849,6 +1854,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1849
1854
|
}
|
|
1850
1855
|
async refresh(forceRefresh) {
|
|
1851
1856
|
try {
|
|
1857
|
+
if (forceRefresh) {
|
|
1858
|
+
this.setStatus("Syncing…");
|
|
1859
|
+
await this.app.sync();
|
|
1860
|
+
}
|
|
1852
1861
|
await this.loadFolders({
|
|
1853
1862
|
forceRefresh,
|
|
1854
1863
|
setStatus: false
|
|
@@ -1858,7 +1867,9 @@ var GranolaTuiWorkspace = class {
|
|
|
1858
1867
|
preferredMeetingId: this.#selectedMeetingId
|
|
1859
1868
|
});
|
|
1860
1869
|
if (this.#selectedMeetingId) await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1861
|
-
} catch {
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
if (error instanceof Error && error.message) this.setStatus(error.message, "error");
|
|
1872
|
+
}
|
|
1862
1873
|
}
|
|
1863
1874
|
async moveMeetingSelection(delta) {
|
|
1864
1875
|
if (this.#meetings.length === 0) return;
|
|
@@ -2245,7 +2256,7 @@ var GranolaTuiWorkspace = class {
|
|
|
2245
2256
|
const bodyLines = [];
|
|
2246
2257
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
2247
2258
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
2248
|
-
const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r
|
|
2259
|
+
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);
|
|
2249
2260
|
return [
|
|
2250
2261
|
headerTitle,
|
|
2251
2262
|
headerSummary,
|
|
@@ -2263,7 +2274,7 @@ async function runGranolaTui(app, options = {}) {
|
|
|
2263
2274
|
onExit: () => {
|
|
2264
2275
|
workspace.dispose();
|
|
2265
2276
|
tui.stop();
|
|
2266
|
-
Promise.resolve(app.close?.()).catch(() => {}).finally(() => {
|
|
2277
|
+
Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {}).finally(() => {
|
|
2267
2278
|
resolve(0);
|
|
2268
2279
|
});
|
|
2269
2280
|
}
|
|
@@ -2273,7 +2284,7 @@ async function runGranolaTui(app, options = {}) {
|
|
|
2273
2284
|
await workspace.initialise();
|
|
2274
2285
|
} catch (error) {
|
|
2275
2286
|
workspace.dispose();
|
|
2276
|
-
await Promise.resolve(app.close?.()).catch(() => {});
|
|
2287
|
+
await Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {});
|
|
2277
2288
|
reject(error);
|
|
2278
2289
|
return;
|
|
2279
2290
|
}
|
|
@@ -2382,6 +2393,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2382
2393
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2383
2394
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2384
2395
|
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2396
|
+
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
2385
2397
|
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2386
2398
|
};
|
|
2387
2399
|
}
|
|
@@ -3275,14 +3287,17 @@ function cloneSyncSummary(summary) {
|
|
|
3275
3287
|
}
|
|
3276
3288
|
function normaliseSyncState(filePath, file) {
|
|
3277
3289
|
return {
|
|
3290
|
+
eventCount: file?.eventCount ?? 0,
|
|
3291
|
+
eventsFile: file?.eventsFile ?? defaultSyncEventsFilePath$1(),
|
|
3278
3292
|
filePath,
|
|
3279
3293
|
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
3294
|
running: false,
|
|
3285
|
-
|
|
3295
|
+
...file?.lastCompletedAt ? { lastCompletedAt: file.lastCompletedAt } : {},
|
|
3296
|
+
...file?.lastError ? { lastError: file.lastError } : {},
|
|
3297
|
+
...file?.lastFailedAt ? { lastFailedAt: file.lastFailedAt } : {},
|
|
3298
|
+
...file?.lastRunId ? { lastRunId: file.lastRunId } : {},
|
|
3299
|
+
...file?.lastStartedAt ? { lastStartedAt: file.lastStartedAt } : {},
|
|
3300
|
+
...file?.summary ? { summary: cloneSyncSummary(file.summary) } : {}
|
|
3286
3301
|
};
|
|
3287
3302
|
}
|
|
3288
3303
|
var FileSyncStateStore = class {
|
|
@@ -3301,10 +3316,13 @@ var FileSyncStateStore = class {
|
|
|
3301
3316
|
async writeState(state) {
|
|
3302
3317
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3303
3318
|
const payload = {
|
|
3319
|
+
eventCount: state.eventCount,
|
|
3320
|
+
eventsFile: state.eventsFile,
|
|
3304
3321
|
lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3305
3322
|
lastCompletedAt: state.lastCompletedAt,
|
|
3306
3323
|
lastError: state.lastError,
|
|
3307
3324
|
lastFailedAt: state.lastFailedAt,
|
|
3325
|
+
lastRunId: state.lastRunId,
|
|
3308
3326
|
lastStartedAt: state.lastStartedAt,
|
|
3309
3327
|
summary: cloneSyncSummary(state.summary),
|
|
3310
3328
|
version: SYNC_STATE_VERSION
|
|
@@ -3318,10 +3336,45 @@ var FileSyncStateStore = class {
|
|
|
3318
3336
|
function defaultSyncStateFilePath() {
|
|
3319
3337
|
return defaultGranolaToolkitPersistenceLayout().syncStateFile;
|
|
3320
3338
|
}
|
|
3339
|
+
function defaultSyncEventsFilePath$1() {
|
|
3340
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3341
|
+
}
|
|
3321
3342
|
function createDefaultSyncStateStore() {
|
|
3322
3343
|
return new FileSyncStateStore();
|
|
3323
3344
|
}
|
|
3324
3345
|
//#endregion
|
|
3346
|
+
//#region src/sync-events.ts
|
|
3347
|
+
function cloneSyncEvent$1(event) {
|
|
3348
|
+
return { ...event };
|
|
3349
|
+
}
|
|
3350
|
+
var FileSyncEventStore = class {
|
|
3351
|
+
constructor(filePath = defaultSyncEventsFilePath()) {
|
|
3352
|
+
this.filePath = filePath;
|
|
3353
|
+
}
|
|
3354
|
+
async appendEvents(events) {
|
|
3355
|
+
if (events.length === 0) return;
|
|
3356
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3357
|
+
const payload = events.map((event) => JSON.stringify(event)).join("\n");
|
|
3358
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3359
|
+
encoding: "utf8",
|
|
3360
|
+
mode: 384
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
async readEvents(limit = 50) {
|
|
3364
|
+
try {
|
|
3365
|
+
return (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((event) => Boolean(event)).map(cloneSyncEvent$1).slice(-limit).reverse();
|
|
3366
|
+
} catch {
|
|
3367
|
+
return [];
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
function defaultSyncEventsFilePath() {
|
|
3372
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3373
|
+
}
|
|
3374
|
+
function createDefaultSyncEventStore() {
|
|
3375
|
+
return new FileSyncEventStore();
|
|
3376
|
+
}
|
|
3377
|
+
//#endregion
|
|
3325
3378
|
//#region src/sync.ts
|
|
3326
3379
|
function normaliseMeeting(meeting) {
|
|
3327
3380
|
return {
|
|
@@ -3419,6 +3472,18 @@ function diffMeetingSummaries(previous, next, folderCount) {
|
|
|
3419
3472
|
}
|
|
3420
3473
|
};
|
|
3421
3474
|
}
|
|
3475
|
+
function buildSyncEvents(runId, occurredAt, changes) {
|
|
3476
|
+
return changes.map((change, index) => ({
|
|
3477
|
+
id: `${runId}:${index + 1}`,
|
|
3478
|
+
kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
|
|
3479
|
+
meetingId: change.meetingId,
|
|
3480
|
+
occurredAt,
|
|
3481
|
+
previousUpdatedAt: change.previousUpdatedAt,
|
|
3482
|
+
runId,
|
|
3483
|
+
title: change.title,
|
|
3484
|
+
updatedAt: change.updatedAt
|
|
3485
|
+
}));
|
|
3486
|
+
}
|
|
3422
3487
|
//#endregion
|
|
3423
3488
|
//#region src/app/core.ts
|
|
3424
3489
|
function transcriptCount(cacheData) {
|
|
@@ -3449,6 +3514,9 @@ function cloneSyncState(state) {
|
|
|
3449
3514
|
summary: state.summary ? { ...state.summary } : void 0
|
|
3450
3515
|
};
|
|
3451
3516
|
}
|
|
3517
|
+
function cloneSyncEvent(event) {
|
|
3518
|
+
return { ...event };
|
|
3519
|
+
}
|
|
3452
3520
|
function cloneMeetingSummary(meeting) {
|
|
3453
3521
|
return {
|
|
3454
3522
|
...meeting,
|
|
@@ -3508,6 +3576,8 @@ function defaultState(config, auth, surface) {
|
|
|
3508
3576
|
meetingCount: 0
|
|
3509
3577
|
},
|
|
3510
3578
|
sync: {
|
|
3579
|
+
eventCount: 0,
|
|
3580
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3511
3581
|
filePath: defaultSyncStateFilePath(),
|
|
3512
3582
|
lastChanges: [],
|
|
3513
3583
|
running: false
|
|
@@ -3544,6 +3614,8 @@ var GranolaApp = class {
|
|
|
3544
3614
|
this.#state.sync = {
|
|
3545
3615
|
...this.#state.sync,
|
|
3546
3616
|
...cloneSyncState(deps.syncState ?? {
|
|
3617
|
+
eventCount: 0,
|
|
3618
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3547
3619
|
filePath: defaultSyncStateFilePath(),
|
|
3548
3620
|
lastChanges: [],
|
|
3549
3621
|
running: false
|
|
@@ -3603,6 +3675,9 @@ var GranolaApp = class {
|
|
|
3603
3675
|
if (!this.deps.syncStateStore) return;
|
|
3604
3676
|
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3605
3677
|
}
|
|
3678
|
+
createSyncRunId() {
|
|
3679
|
+
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
3680
|
+
}
|
|
3606
3681
|
applyAuthState(auth, options = {}) {
|
|
3607
3682
|
if (options.resetDocuments) this.resetRemoteState();
|
|
3608
3683
|
this.#state.auth = { ...auth };
|
|
@@ -3787,6 +3862,10 @@ var GranolaApp = class {
|
|
|
3787
3862
|
async inspectSync() {
|
|
3788
3863
|
return cloneSyncState(this.#state.sync);
|
|
3789
3864
|
}
|
|
3865
|
+
async listSyncEvents(options = {}) {
|
|
3866
|
+
if (!this.deps.syncEventStore) return { events: [] };
|
|
3867
|
+
return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
|
|
3868
|
+
}
|
|
3790
3869
|
async loginAuth(options = {}) {
|
|
3791
3870
|
const controller = this.requireAuthController();
|
|
3792
3871
|
try {
|
|
@@ -3850,11 +3929,17 @@ var GranolaApp = class {
|
|
|
3850
3929
|
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
3851
3930
|
await this.persistMeetingIndex(snapshot.meetings);
|
|
3852
3931
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
3932
|
+
const completedAt = this.nowIso();
|
|
3933
|
+
const runId = this.createSyncRunId();
|
|
3934
|
+
const events = buildSyncEvents(runId, completedAt, changes);
|
|
3935
|
+
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
3853
3936
|
this.#state.sync = {
|
|
3854
3937
|
...this.#state.sync,
|
|
3938
|
+
eventCount: this.#state.sync.eventCount + events.length,
|
|
3855
3939
|
lastChanges: changes.slice(0, 50).map(cloneSyncChange),
|
|
3856
|
-
lastCompletedAt:
|
|
3940
|
+
lastCompletedAt: completedAt,
|
|
3857
3941
|
lastError: void 0,
|
|
3942
|
+
lastRunId: runId,
|
|
3858
3943
|
running: false,
|
|
3859
3944
|
summary: { ...summary }
|
|
3860
3945
|
};
|
|
@@ -3880,7 +3965,7 @@ var GranolaApp = class {
|
|
|
3880
3965
|
async sync(options = {}) {
|
|
3881
3966
|
return await this.runSync({
|
|
3882
3967
|
forceRefresh: options.forceRefresh,
|
|
3883
|
-
foreground: true
|
|
3968
|
+
foreground: options.foreground ?? true
|
|
3884
3969
|
});
|
|
3885
3970
|
}
|
|
3886
3971
|
async listDocuments(options = {}) {
|
|
@@ -4203,6 +4288,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4203
4288
|
const exportJobs = await exportJobStore.readJobs();
|
|
4204
4289
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4205
4290
|
const meetingIndex = await meetingIndexStore.readIndex();
|
|
4291
|
+
const syncEventStore = createDefaultSyncEventStore();
|
|
4206
4292
|
const syncStateStore = createDefaultSyncStateStore();
|
|
4207
4293
|
const syncState = await syncStateStore.readState();
|
|
4208
4294
|
return new GranolaApp(config, {
|
|
@@ -4215,6 +4301,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4215
4301
|
meetingIndex,
|
|
4216
4302
|
meetingIndexStore,
|
|
4217
4303
|
now: options.now,
|
|
4304
|
+
syncEventStore,
|
|
4218
4305
|
syncState,
|
|
4219
4306
|
syncStateStore
|
|
4220
4307
|
}, { surface: options.surface });
|
|
@@ -4325,6 +4412,14 @@ function parseTrustedOrigins(value) {
|
|
|
4325
4412
|
if (typeof value !== "string" || !value.trim()) return [];
|
|
4326
4413
|
return value.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
4327
4414
|
}
|
|
4415
|
+
function parseSyncInterval(value, fallbackMs = 6e4) {
|
|
4416
|
+
if (value === void 0) return fallbackMs;
|
|
4417
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("invalid sync interval: expected a duration like 60s or 5m");
|
|
4418
|
+
return parseDuration(value);
|
|
4419
|
+
}
|
|
4420
|
+
function syncEnabled(commandFlags) {
|
|
4421
|
+
return commandFlags["no-sync"] !== true;
|
|
4422
|
+
}
|
|
4328
4423
|
async function waitForShutdown(close) {
|
|
4329
4424
|
await new Promise((resolve, reject) => {
|
|
4330
4425
|
let closing = false;
|
|
@@ -4903,12 +4998,20 @@ function renderAppState() {
|
|
|
4903
4998
|
const folderStatus = appState.folders.loaded
|
|
4904
4999
|
? appState.folders.count + " folders"
|
|
4905
5000
|
: "not loaded";
|
|
5001
|
+
const syncStatus = appState.sync.running
|
|
5002
|
+
? "running"
|
|
5003
|
+
: appState.sync.lastError
|
|
5004
|
+
? "error"
|
|
5005
|
+
: appState.sync.lastCompletedAt
|
|
5006
|
+
? "last " + appState.sync.lastCompletedAt.slice(11, 19)
|
|
5007
|
+
: "idle";
|
|
4906
5008
|
|
|
4907
5009
|
els.appState.innerHTML = [
|
|
4908
5010
|
'<div class="status-grid">',
|
|
4909
5011
|
'<div><span class="status-label">Surface</span><strong>' + escapeHtml(appState.ui.surface) + "</strong></div>",
|
|
4910
5012
|
'<div><span class="status-label">View</span><strong>' + escapeHtml(appState.ui.view) + "</strong></div>",
|
|
4911
5013
|
'<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
|
|
5014
|
+
'<div><span class="status-label">Sync</span><strong>' + escapeHtml(syncStatus) + "</strong></div>",
|
|
4912
5015
|
'<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
|
|
4913
5016
|
'<div><span class="status-label">Folders</span><strong>' + escapeHtml(folderStatus) + "</strong></div>",
|
|
4914
5017
|
'<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
|
|
@@ -5055,7 +5158,7 @@ function renderMeetingList() {
|
|
|
5055
5158
|
});
|
|
5056
5159
|
const message = filterSummary
|
|
5057
5160
|
? "No meetings match " + filterSummary + "."
|
|
5058
|
-
: "No meetings yet. Try
|
|
5161
|
+
: "No meetings yet. Try Sync now.";
|
|
5059
5162
|
els.list.innerHTML = '<div class="meeting-empty">' + escapeHtml(message) + "</div>";
|
|
5060
5163
|
renderMeetingDetail();
|
|
5061
5164
|
return;
|
|
@@ -5334,8 +5437,16 @@ async function quickOpenMeeting() {
|
|
|
5334
5437
|
}
|
|
5335
5438
|
|
|
5336
5439
|
async function refreshAll(forceLiveMeetings = false) {
|
|
5337
|
-
setStatus("Refreshing…", "busy");
|
|
5440
|
+
setStatus(forceLiveMeetings ? "Syncing…" : "Refreshing…", "busy");
|
|
5338
5441
|
try {
|
|
5442
|
+
if (forceLiveMeetings) {
|
|
5443
|
+
await fetchJson("/sync", {
|
|
5444
|
+
body: JSON.stringify({ forceRefresh: true }),
|
|
5445
|
+
headers: { "content-type": "application/json" },
|
|
5446
|
+
method: "POST",
|
|
5447
|
+
});
|
|
5448
|
+
}
|
|
5449
|
+
|
|
5339
5450
|
await loadFolders({ refresh: forceLiveMeetings });
|
|
5340
5451
|
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
|
|
5341
5452
|
await loadMeetings({ refresh: forceLiveMeetings });
|
|
@@ -5345,7 +5456,14 @@ async function refreshAll(forceLiveMeetings = false) {
|
|
|
5345
5456
|
auth: authState,
|
|
5346
5457
|
};
|
|
5347
5458
|
renderAppState();
|
|
5348
|
-
setStatus(
|
|
5459
|
+
setStatus(
|
|
5460
|
+
forceLiveMeetings
|
|
5461
|
+
? "Sync complete"
|
|
5462
|
+
: state.meetingSource === "index"
|
|
5463
|
+
? "Loaded from index"
|
|
5464
|
+
: "Connected",
|
|
5465
|
+
"ok",
|
|
5466
|
+
);
|
|
5349
5467
|
} catch (error) {
|
|
5350
5468
|
if (error.authRequired) {
|
|
5351
5469
|
setStatus("Server locked", "error");
|
|
@@ -5790,7 +5908,7 @@ const granolaWebMarkup = String.raw`
|
|
|
5790
5908
|
</section>
|
|
5791
5909
|
<section class="toolbar">
|
|
5792
5910
|
<div class="toolbar-actions">
|
|
5793
|
-
<button class="button button--primary" data-refresh>
|
|
5911
|
+
<button class="button button--primary" data-refresh>Sync now</button>
|
|
5794
5912
|
<button class="button button--secondary" data-export-notes>Export Notes</button>
|
|
5795
5913
|
<button class="button button--secondary" data-export-transcripts>Export Transcripts</button>
|
|
5796
5914
|
</div>
|
|
@@ -6566,6 +6684,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6566
6684
|
exportJobs: true,
|
|
6567
6685
|
meetingIndex: true,
|
|
6568
6686
|
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
|
|
6687
|
+
syncEvents: true,
|
|
6569
6688
|
syncState: true
|
|
6570
6689
|
},
|
|
6571
6690
|
product: "granola-toolkit",
|
|
@@ -6664,7 +6783,14 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6664
6783
|
}
|
|
6665
6784
|
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
6666
6785
|
const body = await readJsonBody(request);
|
|
6667
|
-
sendJson(response, await app.sync({
|
|
6786
|
+
sendJson(response, await app.sync({
|
|
6787
|
+
foreground: typeof body.foreground === "boolean" ? body.foreground : void 0,
|
|
6788
|
+
forceRefresh: typeof body.forceRefresh === "boolean" ? body.forceRefresh : void 0
|
|
6789
|
+
}), { headers: originHeaders });
|
|
6790
|
+
return;
|
|
6791
|
+
}
|
|
6792
|
+
if (method === "GET" && path === granolaTransportPaths.syncEvents) {
|
|
6793
|
+
sendJson(response, await app.listSyncEvents({ limit: parseInteger(url.searchParams.get("limit")) ?? 20 }), { headers: originHeaders });
|
|
6668
6794
|
return;
|
|
6669
6795
|
}
|
|
6670
6796
|
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
@@ -6851,6 +6977,59 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6851
6977
|
};
|
|
6852
6978
|
}
|
|
6853
6979
|
//#endregion
|
|
6980
|
+
//#region src/sync-loop.ts
|
|
6981
|
+
function createGranolaSyncLoop(options) {
|
|
6982
|
+
const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
|
|
6983
|
+
const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
|
|
6984
|
+
let inFlight;
|
|
6985
|
+
let stopped = true;
|
|
6986
|
+
let timer;
|
|
6987
|
+
const schedule = () => {
|
|
6988
|
+
if (stopped) return;
|
|
6989
|
+
timer = setTimeoutImpl(() => {
|
|
6990
|
+
runCycle();
|
|
6991
|
+
}, options.intervalMs);
|
|
6992
|
+
};
|
|
6993
|
+
const runCycle = async () => {
|
|
6994
|
+
if (stopped || inFlight) return;
|
|
6995
|
+
inFlight = (async () => {
|
|
6996
|
+
try {
|
|
6997
|
+
const result = await options.app.sync({
|
|
6998
|
+
forceRefresh: true,
|
|
6999
|
+
foreground: false
|
|
7000
|
+
});
|
|
7001
|
+
await options.onSynced?.(result);
|
|
7002
|
+
} catch (error) {
|
|
7003
|
+
options.logger?.warn?.(`background sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7004
|
+
await options.onError?.(error);
|
|
7005
|
+
} finally {
|
|
7006
|
+
inFlight = void 0;
|
|
7007
|
+
schedule();
|
|
7008
|
+
}
|
|
7009
|
+
})();
|
|
7010
|
+
await inFlight;
|
|
7011
|
+
};
|
|
7012
|
+
return {
|
|
7013
|
+
start(loopOptions = {}) {
|
|
7014
|
+
if (!stopped) return;
|
|
7015
|
+
stopped = false;
|
|
7016
|
+
if (loopOptions.immediate === false) {
|
|
7017
|
+
schedule();
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
runCycle();
|
|
7021
|
+
},
|
|
7022
|
+
async stop() {
|
|
7023
|
+
stopped = true;
|
|
7024
|
+
if (timer !== void 0) {
|
|
7025
|
+
clearTimeoutImpl(timer);
|
|
7026
|
+
timer = void 0;
|
|
7027
|
+
}
|
|
7028
|
+
await inFlight;
|
|
7029
|
+
}
|
|
7030
|
+
};
|
|
7031
|
+
}
|
|
7032
|
+
//#endregion
|
|
6854
7033
|
//#region src/web-url.ts
|
|
6855
7034
|
function buildGranolaMeetingUrl(baseUrl, meetingId) {
|
|
6856
7035
|
const url = new URL(baseUrl);
|
|
@@ -6869,6 +7048,8 @@ function resolveGranolaWebWorkspaceOptions(commandFlags) {
|
|
|
6869
7048
|
openBrowser: commandFlags.open !== false,
|
|
6870
7049
|
password: typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password.trim() : void 0,
|
|
6871
7050
|
port,
|
|
7051
|
+
syncEnabled: syncEnabled(commandFlags),
|
|
7052
|
+
syncIntervalMs: parseSyncInterval(commandFlags["sync-interval"]),
|
|
6872
7053
|
trustedOrigins: parseTrustedOrigins(commandFlags["trusted-origins"])
|
|
6873
7054
|
};
|
|
6874
7055
|
}
|
|
@@ -6896,6 +7077,7 @@ function printWebRoutes() {
|
|
|
6896
7077
|
console.log(" POST /exports/notes");
|
|
6897
7078
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
6898
7079
|
console.log(" POST /exports/transcripts");
|
|
7080
|
+
console.log(" GET /sync/events");
|
|
6899
7081
|
console.log(" POST /sync");
|
|
6900
7082
|
}
|
|
6901
7083
|
async function runGranolaWebWorkspace(app, options) {
|
|
@@ -6908,6 +7090,12 @@ async function runGranolaWebWorkspace(app, options) {
|
|
|
6908
7090
|
trustedOrigins: options.trustedOrigins
|
|
6909
7091
|
}
|
|
6910
7092
|
});
|
|
7093
|
+
const syncLoop = options.syncEnabled ? createGranolaSyncLoop({
|
|
7094
|
+
app,
|
|
7095
|
+
intervalMs: options.syncIntervalMs,
|
|
7096
|
+
logger: console
|
|
7097
|
+
}) : void 0;
|
|
7098
|
+
syncLoop?.start();
|
|
6911
7099
|
const targetUrl = options.targetMeetingId ? buildGranolaMeetingUrl(server.url, options.targetMeetingId) : new URL(server.url);
|
|
6912
7100
|
console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
|
|
6913
7101
|
if (targetUrl.href !== server.url.href) console.log(`Focused meeting URL: ${targetUrl.href}`);
|
|
@@ -6915,6 +7103,7 @@ async function runGranolaWebWorkspace(app, options) {
|
|
|
6915
7103
|
if (options.password) console.log("Server password protection: enabled");
|
|
6916
7104
|
else if (options.networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
|
|
6917
7105
|
if (options.trustedOrigins.length > 0) console.log(`Trusted origins: ${options.trustedOrigins.join(", ")}`);
|
|
7106
|
+
console.log(options.syncEnabled ? `Background sync: enabled (${options.syncIntervalMs}ms)` : "Background sync: disabled");
|
|
6918
7107
|
printWebRoutes();
|
|
6919
7108
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
6920
7109
|
if (options.password) console.log("Attach password: add --password <value>");
|
|
@@ -6925,7 +7114,10 @@ async function runGranolaWebWorkspace(app, options) {
|
|
|
6925
7114
|
console.error(`failed to open browser automatically: ${message}`);
|
|
6926
7115
|
console.error(`open ${targetUrl.href} manually`);
|
|
6927
7116
|
}
|
|
6928
|
-
await waitForShutdown(async () =>
|
|
7117
|
+
await waitForShutdown(async () => {
|
|
7118
|
+
await syncLoop?.stop();
|
|
7119
|
+
await server.close();
|
|
7120
|
+
});
|
|
6929
7121
|
return 0;
|
|
6930
7122
|
}
|
|
6931
7123
|
//#endregion
|
|
@@ -7256,6 +7448,8 @@ Options:
|
|
|
7256
7448
|
--hostname <value> Hostname to bind (overrides network default)
|
|
7257
7449
|
--port <value> Port to bind (default: 0 for any available port)
|
|
7258
7450
|
--password <value> Optional server password for API and browser access
|
|
7451
|
+
--sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
|
|
7452
|
+
--no-sync Disable the background sync loop
|
|
7259
7453
|
--trusted-origins <v> Comma-separated extra browser origins to trust
|
|
7260
7454
|
--cache <path> Path to Granola cache JSON
|
|
7261
7455
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
@@ -7272,8 +7466,10 @@ const serveCommand = {
|
|
|
7272
7466
|
help: { type: "boolean" },
|
|
7273
7467
|
hostname: { type: "string" },
|
|
7274
7468
|
network: { type: "string" },
|
|
7469
|
+
"no-sync": { type: "boolean" },
|
|
7275
7470
|
password: { type: "string" },
|
|
7276
7471
|
port: { type: "string" },
|
|
7472
|
+
"sync-interval": { type: "string" },
|
|
7277
7473
|
timeout: { type: "string" },
|
|
7278
7474
|
"trusted-origins": { type: "string" }
|
|
7279
7475
|
},
|
|
@@ -7293,6 +7489,8 @@ const serveCommand = {
|
|
|
7293
7489
|
const hostname = resolveServerHostname(networkMode, commandFlags.hostname);
|
|
7294
7490
|
const port = parsePort(commandFlags.port);
|
|
7295
7491
|
const password = typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password : void 0;
|
|
7492
|
+
const backgroundSyncEnabled = syncEnabled(commandFlags);
|
|
7493
|
+
const syncIntervalMs = parseSyncInterval(commandFlags["sync-interval"]);
|
|
7296
7494
|
const trustedOrigins = parseTrustedOrigins(commandFlags["trusted-origins"]);
|
|
7297
7495
|
const server = await startGranolaServer(app, {
|
|
7298
7496
|
hostname,
|
|
@@ -7302,11 +7500,18 @@ const serveCommand = {
|
|
|
7302
7500
|
trustedOrigins
|
|
7303
7501
|
}
|
|
7304
7502
|
});
|
|
7503
|
+
const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
|
|
7504
|
+
app,
|
|
7505
|
+
intervalMs: syncIntervalMs,
|
|
7506
|
+
logger: console
|
|
7507
|
+
}) : void 0;
|
|
7508
|
+
syncLoop?.start();
|
|
7305
7509
|
console.log(`Granola server listening on ${server.url.href}`);
|
|
7306
7510
|
console.log(`Network mode: ${networkMode}`);
|
|
7307
7511
|
if (password) console.log("Server password protection: enabled");
|
|
7308
7512
|
else if (networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
|
|
7309
7513
|
if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
|
|
7514
|
+
console.log(backgroundSyncEnabled ? `Background sync: enabled (${syncIntervalMs}ms)` : "Background sync: disabled");
|
|
7310
7515
|
console.log("Endpoints:");
|
|
7311
7516
|
console.log(" GET /health");
|
|
7312
7517
|
console.log(" GET /server/info");
|
|
@@ -7325,10 +7530,14 @@ const serveCommand = {
|
|
|
7325
7530
|
console.log(" POST /exports/notes");
|
|
7326
7531
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
7327
7532
|
console.log(" POST /exports/transcripts");
|
|
7533
|
+
console.log(" GET /sync/events");
|
|
7328
7534
|
console.log(" POST /sync");
|
|
7329
7535
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
7330
7536
|
if (password) console.log("Attach password: add --password <value>");
|
|
7331
|
-
await waitForShutdown(async () =>
|
|
7537
|
+
await waitForShutdown(async () => {
|
|
7538
|
+
await syncLoop?.stop();
|
|
7539
|
+
await server.close();
|
|
7540
|
+
});
|
|
7332
7541
|
return 0;
|
|
7333
7542
|
}
|
|
7334
7543
|
};
|
|
@@ -7339,8 +7548,12 @@ function syncHelp() {
|
|
|
7339
7548
|
|
|
7340
7549
|
Usage:
|
|
7341
7550
|
granola sync [options]
|
|
7551
|
+
granola sync events [options]
|
|
7342
7552
|
|
|
7343
7553
|
Options:
|
|
7554
|
+
--watch Keep syncing in the background until interrupted
|
|
7555
|
+
--interval <value> Poll interval for --watch, e.g. 60s or 5m (default: 60s)
|
|
7556
|
+
--limit <value> Event count for sync events output (default: 20)
|
|
7344
7557
|
--cache <path> Path to Granola cache JSON
|
|
7345
7558
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7346
7559
|
--supabase <path> Path to supabase.json
|
|
@@ -7352,16 +7565,27 @@ Options:
|
|
|
7352
7565
|
function pluralise(count, singular, plural = singular) {
|
|
7353
7566
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
7354
7567
|
}
|
|
7568
|
+
function printSyncResult(result, log = console.log) {
|
|
7569
|
+
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")})`);
|
|
7570
|
+
const lines = result.changes.slice(0, 10).map((change) => {
|
|
7571
|
+
return ` ${change.kind.padEnd(16)} ${change.title} (${change.meetingId})`;
|
|
7572
|
+
});
|
|
7573
|
+
for (const line of lines) log(line);
|
|
7574
|
+
if (result.changes.length > lines.length) log(` ...and ${result.changes.length - lines.length} more change(s)`);
|
|
7575
|
+
}
|
|
7355
7576
|
const syncCommand = {
|
|
7356
7577
|
description: "Refresh the local meeting index and sync state",
|
|
7357
7578
|
flags: {
|
|
7358
7579
|
cache: { type: "string" },
|
|
7359
7580
|
help: { type: "boolean" },
|
|
7360
|
-
|
|
7581
|
+
interval: { type: "string" },
|
|
7582
|
+
limit: { type: "string" },
|
|
7583
|
+
timeout: { type: "string" },
|
|
7584
|
+
watch: { type: "boolean" }
|
|
7361
7585
|
},
|
|
7362
7586
|
help: syncHelp,
|
|
7363
7587
|
name: "sync",
|
|
7364
|
-
async run({ commandFlags, globalFlags }) {
|
|
7588
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
7365
7589
|
const config = await loadConfig({
|
|
7366
7590
|
globalFlags,
|
|
7367
7591
|
subcommandFlags: commandFlags
|
|
@@ -7372,14 +7596,38 @@ const syncCommand = {
|
|
|
7372
7596
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7373
7597
|
const app = await createGranolaApp(config);
|
|
7374
7598
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7599
|
+
if (commandArgs[0] === "events") {
|
|
7600
|
+
const limit = typeof commandFlags.limit === "string" && /^\d+$/.test(commandFlags.limit) ? Number(commandFlags.limit) : 20;
|
|
7601
|
+
const result = await app.listSyncEvents({ limit });
|
|
7602
|
+
if (result.events.length === 0) {
|
|
7603
|
+
console.log("No sync events yet.");
|
|
7604
|
+
return 0;
|
|
7605
|
+
}
|
|
7606
|
+
for (const event of result.events) console.log(`${event.occurredAt} ${event.kind.padEnd(18)} ${event.title} (${event.meetingId})`);
|
|
7607
|
+
return 0;
|
|
7608
|
+
}
|
|
7375
7609
|
const result = await app.sync();
|
|
7376
|
-
|
|
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)`);
|
|
7610
|
+
printSyncResult(result);
|
|
7382
7611
|
if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
|
|
7612
|
+
if (commandFlags.watch === true) {
|
|
7613
|
+
const intervalMs = parseSyncInterval(commandFlags.interval);
|
|
7614
|
+
const syncLoop = createGranolaSyncLoop({
|
|
7615
|
+
app,
|
|
7616
|
+
intervalMs,
|
|
7617
|
+
logger: console,
|
|
7618
|
+
onError: async (error) => {
|
|
7619
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7620
|
+
},
|
|
7621
|
+
onSynced: async (nextResult) => {
|
|
7622
|
+
printSyncResult(nextResult);
|
|
7623
|
+
}
|
|
7624
|
+
});
|
|
7625
|
+
syncLoop.start({ immediate: false });
|
|
7626
|
+
console.log(`Watching for Granola changes every ${intervalMs}ms. Press Ctrl+C to stop.`);
|
|
7627
|
+
await waitForShutdown(async () => {
|
|
7628
|
+
await syncLoop.stop();
|
|
7629
|
+
});
|
|
7630
|
+
}
|
|
7383
7631
|
return 0;
|
|
7384
7632
|
}
|
|
7385
7633
|
};
|
|
@@ -7393,6 +7641,8 @@ Usage:
|
|
|
7393
7641
|
|
|
7394
7642
|
Options:
|
|
7395
7643
|
--meeting <id> Open the workspace focused on a specific meeting
|
|
7644
|
+
--sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
|
|
7645
|
+
--no-sync Disable the background sync loop
|
|
7396
7646
|
--cache <path> Path to Granola cache JSON
|
|
7397
7647
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7398
7648
|
--supabase <path> Path to supabase.json
|
|
@@ -7407,6 +7657,8 @@ const tuiCommand = {
|
|
|
7407
7657
|
cache: { type: "string" },
|
|
7408
7658
|
help: { type: "boolean" },
|
|
7409
7659
|
meeting: { type: "string" },
|
|
7660
|
+
"no-sync": { type: "boolean" },
|
|
7661
|
+
"sync-interval": { type: "string" },
|
|
7410
7662
|
timeout: { type: "string" }
|
|
7411
7663
|
},
|
|
7412
7664
|
help: tuiHelp,
|
|
@@ -7420,7 +7672,23 @@ const tuiCommand = {
|
|
|
7420
7672
|
debug(config.debug, "supabase", config.supabase);
|
|
7421
7673
|
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
7422
7674
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7423
|
-
|
|
7675
|
+
const app = await createGranolaApp(config, { surface: "tui" });
|
|
7676
|
+
const initialMeetingId = typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0;
|
|
7677
|
+
const backgroundSyncEnabled = syncEnabled(commandFlags);
|
|
7678
|
+
const syncIntervalMs = parseSyncInterval(commandFlags["sync-interval"]);
|
|
7679
|
+
const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
|
|
7680
|
+
app,
|
|
7681
|
+
intervalMs: syncIntervalMs,
|
|
7682
|
+
logger: console
|
|
7683
|
+
}) : void 0;
|
|
7684
|
+
syncLoop?.start();
|
|
7685
|
+
debug(config.debug, "backgroundSync", backgroundSyncEnabled ? `${syncIntervalMs}ms` : "disabled");
|
|
7686
|
+
return await runGranolaTui(app, {
|
|
7687
|
+
initialMeetingId,
|
|
7688
|
+
onClose: async () => {
|
|
7689
|
+
await syncLoop?.stop();
|
|
7690
|
+
}
|
|
7691
|
+
});
|
|
7424
7692
|
}
|
|
7425
7693
|
};
|
|
7426
7694
|
//#endregion
|
|
@@ -7500,6 +7768,8 @@ Options:
|
|
|
7500
7768
|
--hostname <value> Hostname to bind (overrides network default)
|
|
7501
7769
|
--port <value> Port to bind (default: 0 for any available port)
|
|
7502
7770
|
--password <value> Optional server password for API and browser access
|
|
7771
|
+
--sync-interval <value> Background sync interval, e.g. 60s or 5m (default: 60s)
|
|
7772
|
+
--no-sync Disable the background sync loop
|
|
7503
7773
|
--trusted-origins <v> Comma-separated extra browser origins to trust
|
|
7504
7774
|
--cache <path> Path to Granola cache JSON
|
|
7505
7775
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
@@ -7531,9 +7801,11 @@ const commands = [
|
|
|
7531
7801
|
hostname: { type: "string" },
|
|
7532
7802
|
meeting: { type: "string" },
|
|
7533
7803
|
network: { type: "string" },
|
|
7804
|
+
"no-sync": { type: "boolean" },
|
|
7534
7805
|
open: { type: "boolean" },
|
|
7535
7806
|
password: { type: "string" },
|
|
7536
7807
|
port: { type: "string" },
|
|
7808
|
+
"sync-interval": { type: "string" },
|
|
7537
7809
|
timeout: { type: "string" },
|
|
7538
7810
|
"trusted-origins": { type: "string" }
|
|
7539
7811
|
},
|