granola-toolkit 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +192 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -22,6 +22,7 @@ The published package exposes both `granola` and `granola-toolkit` as executable
22
22
  ```bash
23
23
  granola auth login
24
24
  granola sync
25
+ granola sync --watch
25
26
  granola folder list
26
27
  granola meeting list --limit 10
27
28
  granola notes --folder Team
package/dist/cli.js CHANGED
@@ -1378,7 +1378,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
1378
1378
  }
1379
1379
  }
1380
1380
  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}`;
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}`;
1382
1382
  }
1383
1383
  //#endregion
1384
1384
  //#region src/tui/theme.ts
@@ -1849,6 +1849,10 @@ var GranolaTuiWorkspace = class {
1849
1849
  }
1850
1850
  async refresh(forceRefresh) {
1851
1851
  try {
1852
+ if (forceRefresh) {
1853
+ this.setStatus("Syncing…");
1854
+ await this.app.sync();
1855
+ }
1852
1856
  await this.loadFolders({
1853
1857
  forceRefresh,
1854
1858
  setStatus: false
@@ -1858,7 +1862,9 @@ var GranolaTuiWorkspace = class {
1858
1862
  preferredMeetingId: this.#selectedMeetingId
1859
1863
  });
1860
1864
  if (this.#selectedMeetingId) await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
1861
- } catch {}
1865
+ } catch (error) {
1866
+ if (error instanceof Error && error.message) this.setStatus(error.message, "error");
1867
+ }
1862
1868
  }
1863
1869
  async moveMeetingSelection(delta) {
1864
1870
  if (this.#meetings.length === 0) return;
@@ -2245,7 +2251,7 @@ var GranolaTuiWorkspace = class {
2245
2251
  const bodyLines = [];
2246
2252
  for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
2247
2253
  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 refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
2254
+ const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r sync 1-4 tabs PgUp/PgDn scroll q quit"), width);
2249
2255
  return [
2250
2256
  headerTitle,
2251
2257
  headerSummary,
@@ -2263,7 +2269,7 @@ async function runGranolaTui(app, options = {}) {
2263
2269
  onExit: () => {
2264
2270
  workspace.dispose();
2265
2271
  tui.stop();
2266
- Promise.resolve(app.close?.()).catch(() => {}).finally(() => {
2272
+ Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {}).finally(() => {
2267
2273
  resolve(0);
2268
2274
  });
2269
2275
  }
@@ -2273,7 +2279,7 @@ async function runGranolaTui(app, options = {}) {
2273
2279
  await workspace.initialise();
2274
2280
  } catch (error) {
2275
2281
  workspace.dispose();
2276
- await Promise.resolve(app.close?.()).catch(() => {});
2282
+ await Promise.resolve(options.onClose?.()).then(() => Promise.resolve(app.close?.())).catch(() => {});
2277
2283
  reject(error);
2278
2284
  return;
2279
2285
  }
@@ -3880,7 +3886,7 @@ var GranolaApp = class {
3880
3886
  async sync(options = {}) {
3881
3887
  return await this.runSync({
3882
3888
  forceRefresh: options.forceRefresh,
3883
- foreground: true
3889
+ foreground: options.foreground ?? true
3884
3890
  });
3885
3891
  }
3886
3892
  async listDocuments(options = {}) {
@@ -4325,6 +4331,14 @@ function parseTrustedOrigins(value) {
4325
4331
  if (typeof value !== "string" || !value.trim()) return [];
4326
4332
  return value.split(",").map((origin) => origin.trim()).filter(Boolean);
4327
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
+ }
4328
4342
  async function waitForShutdown(close) {
4329
4343
  await new Promise((resolve, reject) => {
4330
4344
  let closing = false;
@@ -4903,12 +4917,20 @@ function renderAppState() {
4903
4917
  const folderStatus = appState.folders.loaded
4904
4918
  ? appState.folders.count + " folders"
4905
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";
4906
4927
 
4907
4928
  els.appState.innerHTML = [
4908
4929
  '<div class="status-grid">',
4909
4930
  '<div><span class="status-label">Surface</span><strong>' + escapeHtml(appState.ui.surface) + "</strong></div>",
4910
4931
  '<div><span class="status-label">View</span><strong>' + escapeHtml(appState.ui.view) + "</strong></div>",
4911
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>",
4912
4934
  '<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
4913
4935
  '<div><span class="status-label">Folders</span><strong>' + escapeHtml(folderStatus) + "</strong></div>",
4914
4936
  '<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
@@ -5055,7 +5077,7 @@ function renderMeetingList() {
5055
5077
  });
5056
5078
  const message = filterSummary
5057
5079
  ? "No meetings match " + filterSummary + "."
5058
- : "No meetings yet. Try Refresh.";
5080
+ : "No meetings yet. Try Sync now.";
5059
5081
  els.list.innerHTML = '<div class="meeting-empty">' + escapeHtml(message) + "</div>";
5060
5082
  renderMeetingDetail();
5061
5083
  return;
@@ -5334,8 +5356,16 @@ async function quickOpenMeeting() {
5334
5356
  }
5335
5357
 
5336
5358
  async function refreshAll(forceLiveMeetings = false) {
5337
- setStatus("Refreshing…", "busy");
5359
+ setStatus(forceLiveMeetings ? "Syncing…" : "Refreshing…", "busy");
5338
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
+
5339
5369
  await loadFolders({ refresh: forceLiveMeetings });
5340
5370
  const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
5341
5371
  await loadMeetings({ refresh: forceLiveMeetings });
@@ -5345,7 +5375,14 @@ async function refreshAll(forceLiveMeetings = false) {
5345
5375
  auth: authState,
5346
5376
  };
5347
5377
  renderAppState();
5348
- setStatus(forceLiveMeetings ? "Live data refreshed" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
5378
+ setStatus(
5379
+ forceLiveMeetings
5380
+ ? "Sync complete"
5381
+ : state.meetingSource === "index"
5382
+ ? "Loaded from index"
5383
+ : "Connected",
5384
+ "ok",
5385
+ );
5349
5386
  } catch (error) {
5350
5387
  if (error.authRequired) {
5351
5388
  setStatus("Server locked", "error");
@@ -5790,7 +5827,7 @@ const granolaWebMarkup = String.raw`
5790
5827
  </section>
5791
5828
  <section class="toolbar">
5792
5829
  <div class="toolbar-actions">
5793
- <button class="button button--primary" data-refresh>Refresh</button>
5830
+ <button class="button button--primary" data-refresh>Sync now</button>
5794
5831
  <button class="button button--secondary" data-export-notes>Export Notes</button>
5795
5832
  <button class="button button--secondary" data-export-transcripts>Export Transcripts</button>
5796
5833
  </div>
@@ -6664,7 +6701,10 @@ async function startGranolaServer(app, options = {}) {
6664
6701
  }
6665
6702
  if (method === "POST" && path === granolaTransportPaths.syncRun) {
6666
6703
  const body = await readJsonBody(request);
6667
- sendJson(response, await app.sync({ forceRefresh: typeof body.forceRefresh === "boolean" ? body.forceRefresh : void 0 }), { headers: originHeaders });
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 });
6668
6708
  return;
6669
6709
  }
6670
6710
  if (method === "POST" && path === granolaTransportPaths.authLock) {
@@ -6851,6 +6891,59 @@ async function startGranolaServer(app, options = {}) {
6851
6891
  };
6852
6892
  }
6853
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
6854
6947
  //#region src/web-url.ts
6855
6948
  function buildGranolaMeetingUrl(baseUrl, meetingId) {
6856
6949
  const url = new URL(baseUrl);
@@ -6869,6 +6962,8 @@ function resolveGranolaWebWorkspaceOptions(commandFlags) {
6869
6962
  openBrowser: commandFlags.open !== false,
6870
6963
  password: typeof commandFlags.password === "string" && commandFlags.password.trim() ? commandFlags.password.trim() : void 0,
6871
6964
  port,
6965
+ syncEnabled: syncEnabled(commandFlags),
6966
+ syncIntervalMs: parseSyncInterval(commandFlags["sync-interval"]),
6872
6967
  trustedOrigins: parseTrustedOrigins(commandFlags["trusted-origins"])
6873
6968
  };
6874
6969
  }
@@ -6908,6 +7003,12 @@ async function runGranolaWebWorkspace(app, options) {
6908
7003
  trustedOrigins: options.trustedOrigins
6909
7004
  }
6910
7005
  });
7006
+ const syncLoop = options.syncEnabled ? createGranolaSyncLoop({
7007
+ app,
7008
+ intervalMs: options.syncIntervalMs,
7009
+ logger: console
7010
+ }) : void 0;
7011
+ syncLoop?.start();
6911
7012
  const targetUrl = options.targetMeetingId ? buildGranolaMeetingUrl(server.url, options.targetMeetingId) : new URL(server.url);
6912
7013
  console.log(`Granola Toolkit web workspace listening on ${server.url.href}`);
6913
7014
  if (targetUrl.href !== server.url.href) console.log(`Focused meeting URL: ${targetUrl.href}`);
@@ -6915,6 +7016,7 @@ async function runGranolaWebWorkspace(app, options) {
6915
7016
  if (options.password) console.log("Server password protection: enabled");
6916
7017
  else if (options.networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
6917
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");
6918
7020
  printWebRoutes();
6919
7021
  console.log(`Attach: granola attach ${server.url.href}`);
6920
7022
  if (options.password) console.log("Attach password: add --password <value>");
@@ -6925,7 +7027,10 @@ async function runGranolaWebWorkspace(app, options) {
6925
7027
  console.error(`failed to open browser automatically: ${message}`);
6926
7028
  console.error(`open ${targetUrl.href} manually`);
6927
7029
  }
6928
- await waitForShutdown(async () => await server.close());
7030
+ await waitForShutdown(async () => {
7031
+ await syncLoop?.stop();
7032
+ await server.close();
7033
+ });
6929
7034
  return 0;
6930
7035
  }
6931
7036
  //#endregion
@@ -7256,6 +7361,8 @@ Options:
7256
7361
  --hostname <value> Hostname to bind (overrides network default)
7257
7362
  --port <value> Port to bind (default: 0 for any available port)
7258
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
7259
7366
  --trusted-origins <v> Comma-separated extra browser origins to trust
7260
7367
  --cache <path> Path to Granola cache JSON
7261
7368
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
@@ -7272,8 +7379,10 @@ const serveCommand = {
7272
7379
  help: { type: "boolean" },
7273
7380
  hostname: { type: "string" },
7274
7381
  network: { type: "string" },
7382
+ "no-sync": { type: "boolean" },
7275
7383
  password: { type: "string" },
7276
7384
  port: { type: "string" },
7385
+ "sync-interval": { type: "string" },
7277
7386
  timeout: { type: "string" },
7278
7387
  "trusted-origins": { type: "string" }
7279
7388
  },
@@ -7293,6 +7402,8 @@ const serveCommand = {
7293
7402
  const hostname = resolveServerHostname(networkMode, commandFlags.hostname);
7294
7403
  const port = parsePort(commandFlags.port);
7295
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"]);
7296
7407
  const trustedOrigins = parseTrustedOrigins(commandFlags["trusted-origins"]);
7297
7408
  const server = await startGranolaServer(app, {
7298
7409
  hostname,
@@ -7302,11 +7413,18 @@ const serveCommand = {
7302
7413
  trustedOrigins
7303
7414
  }
7304
7415
  });
7416
+ const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
7417
+ app,
7418
+ intervalMs: syncIntervalMs,
7419
+ logger: console
7420
+ }) : void 0;
7421
+ syncLoop?.start();
7305
7422
  console.log(`Granola server listening on ${server.url.href}`);
7306
7423
  console.log(`Network mode: ${networkMode}`);
7307
7424
  if (password) console.log("Server password protection: enabled");
7308
7425
  else if (networkMode === "lan") console.log("Warning: LAN mode is enabled without a server password");
7309
7426
  if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
7427
+ console.log(backgroundSyncEnabled ? `Background sync: enabled (${syncIntervalMs}ms)` : "Background sync: disabled");
7310
7428
  console.log("Endpoints:");
7311
7429
  console.log(" GET /health");
7312
7430
  console.log(" GET /server/info");
@@ -7328,7 +7446,10 @@ const serveCommand = {
7328
7446
  console.log(" POST /sync");
7329
7447
  console.log(`Attach: granola attach ${server.url.href}`);
7330
7448
  if (password) console.log("Attach password: add --password <value>");
7331
- await waitForShutdown(async () => await server.close());
7449
+ await waitForShutdown(async () => {
7450
+ await syncLoop?.stop();
7451
+ await server.close();
7452
+ });
7332
7453
  return 0;
7333
7454
  }
7334
7455
  };
@@ -7341,6 +7462,8 @@ Usage:
7341
7462
  granola sync [options]
7342
7463
 
7343
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)
7344
7467
  --cache <path> Path to Granola cache JSON
7345
7468
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
7346
7469
  --supabase <path> Path to supabase.json
@@ -7352,12 +7475,22 @@ Options:
7352
7475
  function pluralise(count, singular, plural = singular) {
7353
7476
  return `${count} ${count === 1 ? singular : plural}`;
7354
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
+ }
7355
7486
  const syncCommand = {
7356
7487
  description: "Refresh the local meeting index and sync state",
7357
7488
  flags: {
7358
7489
  cache: { type: "string" },
7359
7490
  help: { type: "boolean" },
7360
- timeout: { type: "string" }
7491
+ interval: { type: "string" },
7492
+ timeout: { type: "string" },
7493
+ watch: { type: "boolean" }
7361
7494
  },
7362
7495
  help: syncHelp,
7363
7496
  name: "sync",
@@ -7373,13 +7506,27 @@ const syncCommand = {
7373
7506
  const app = await createGranolaApp(config);
7374
7507
  debug(config.debug, "authMode", app.getState().auth.mode);
7375
7508
  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)`);
7509
+ printSyncResult(result);
7382
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
+ }
7383
7530
  return 0;
7384
7531
  }
7385
7532
  };
@@ -7393,6 +7540,8 @@ Usage:
7393
7540
 
7394
7541
  Options:
7395
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
7396
7545
  --cache <path> Path to Granola cache JSON
7397
7546
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
7398
7547
  --supabase <path> Path to supabase.json
@@ -7407,6 +7556,8 @@ const tuiCommand = {
7407
7556
  cache: { type: "string" },
7408
7557
  help: { type: "boolean" },
7409
7558
  meeting: { type: "string" },
7559
+ "no-sync": { type: "boolean" },
7560
+ "sync-interval": { type: "string" },
7410
7561
  timeout: { type: "string" }
7411
7562
  },
7412
7563
  help: tuiHelp,
@@ -7420,7 +7571,23 @@ const tuiCommand = {
7420
7571
  debug(config.debug, "supabase", config.supabase);
7421
7572
  debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
7422
7573
  debug(config.debug, "timeoutMs", config.notes.timeoutMs);
7423
- return await runGranolaTui(await createGranolaApp(config, { surface: "tui" }), { initialMeetingId: typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0 });
7574
+ const app = await createGranolaApp(config, { surface: "tui" });
7575
+ const initialMeetingId = typeof commandFlags.meeting === "string" && commandFlags.meeting.trim() ? commandFlags.meeting.trim() : void 0;
7576
+ const backgroundSyncEnabled = syncEnabled(commandFlags);
7577
+ const syncIntervalMs = parseSyncInterval(commandFlags["sync-interval"]);
7578
+ const syncLoop = backgroundSyncEnabled ? createGranolaSyncLoop({
7579
+ app,
7580
+ intervalMs: syncIntervalMs,
7581
+ logger: console
7582
+ }) : void 0;
7583
+ syncLoop?.start();
7584
+ debug(config.debug, "backgroundSync", backgroundSyncEnabled ? `${syncIntervalMs}ms` : "disabled");
7585
+ return await runGranolaTui(app, {
7586
+ initialMeetingId,
7587
+ onClose: async () => {
7588
+ await syncLoop?.stop();
7589
+ }
7590
+ });
7424
7591
  }
7425
7592
  };
7426
7593
  //#endregion
@@ -7500,6 +7667,8 @@ Options:
7500
7667
  --hostname <value> Hostname to bind (overrides network default)
7501
7668
  --port <value> Port to bind (default: 0 for any available port)
7502
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
7503
7672
  --trusted-origins <v> Comma-separated extra browser origins to trust
7504
7673
  --cache <path> Path to Granola cache JSON
7505
7674
  --timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
@@ -7531,9 +7700,11 @@ const commands = [
7531
7700
  hostname: { type: "string" },
7532
7701
  meeting: { type: "string" },
7533
7702
  network: { type: "string" },
7703
+ "no-sync": { type: "boolean" },
7534
7704
  open: { type: "boolean" },
7535
7705
  password: { type: "string" },
7536
7706
  port: { type: "string" },
7707
+ "sync-interval": { type: "string" },
7537
7708
  timeout: { type: "string" },
7538
7709
  "trusted-origins": { type: "string" }
7539
7710
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",