recappi 0.1.3 → 0.1.4

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/dist/index.js CHANGED
@@ -9,39 +9,6 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/tui/chrome.tsx
13
- import { Box, Text } from "ink";
14
- import { jsx, jsxs } from "react/jsx-runtime";
15
- function Header({ active }) {
16
- return /* @__PURE__ */ jsxs(Box, { children: [
17
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "magenta", children: [
18
- "Recappi",
19
- " "
20
- ] }),
21
- /* @__PURE__ */ jsx(Tab, { num: "1", label: "Overview", active: active === "overview" }),
22
- /* @__PURE__ */ jsx(Tab, { num: "2", label: "Jobs", active: active === "jobs" })
23
- ] });
24
- }
25
- function Tab({
26
- num,
27
- label,
28
- active
29
- }) {
30
- const text = ` ${num} ${label} `;
31
- if (active) {
32
- return /* @__PURE__ */ jsx(Text, { bold: true, inverse: true, color: "cyan", children: text });
33
- }
34
- return /* @__PURE__ */ jsx(Text, { children: text });
35
- }
36
- function Footer({ keys }) {
37
- return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: keys }) });
38
- }
39
- var init_chrome = __esm({
40
- "src/tui/chrome.tsx"() {
41
- "use strict";
42
- }
43
- });
44
-
45
12
  // src/tui/format.ts
46
13
  function formatClockMs(ms) {
47
14
  if (ms == null || !Number.isFinite(ms) || ms < 0) return "--:--";
@@ -230,6 +197,29 @@ function listWindow(selected, total, size) {
230
197
  start = Math.max(0, Math.min(start, total - size));
231
198
  return { start, end: start + size };
232
199
  }
200
+ function windowByHeights(heights, scroll, budget) {
201
+ const n = heights.length;
202
+ if (n === 0 || budget <= 0) return { start: 0, end: 0, maxScroll: 0 };
203
+ let acc = 0;
204
+ let maxScroll = 0;
205
+ for (let i = n - 1; i >= 0; i--) {
206
+ acc += Math.max(1, heights[i]);
207
+ if (acc > budget) {
208
+ maxScroll = i + 1;
209
+ break;
210
+ }
211
+ }
212
+ const start = Math.max(0, Math.min(scroll, maxScroll));
213
+ let used = 0;
214
+ let end = start;
215
+ for (let i = start; i < n; i++) {
216
+ const h = Math.max(1, heights[i]);
217
+ if (used + h > budget && end > start) break;
218
+ used += h;
219
+ end = i + 1;
220
+ }
221
+ return { start, end, maxScroll };
222
+ }
233
223
  function groupedListWindow(buckets, selected, budget) {
234
224
  const total = buckets.length;
235
225
  if (budget <= 0 || total <= 0) return { start: 0, end: 0 };
@@ -267,9 +257,53 @@ var init_format = __esm({
267
257
  }
268
258
  });
269
259
 
270
- // src/tui/JobRow.tsx
260
+ // src/tui/terminal.ts
261
+ import { useWindowSize } from "ink";
262
+ function useTerminalSize() {
263
+ return useWindowSize();
264
+ }
265
+ var init_terminal = __esm({
266
+ "src/tui/terminal.ts"() {
267
+ "use strict";
268
+ }
269
+ });
270
+
271
+ // src/tui/chrome.tsx
271
272
  import { Box as Box2, Text as Text2 } from "ink";
272
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
273
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
274
+ function Header({ active }) {
275
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
276
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "magenta", children: [
277
+ "Recappi",
278
+ " "
279
+ ] }),
280
+ /* @__PURE__ */ jsx4(Tab, { num: "1", label: "Overview", active: active === "overview" }),
281
+ /* @__PURE__ */ jsx4(Tab, { num: "2", label: "Jobs", active: active === "jobs" })
282
+ ] });
283
+ }
284
+ function Tab({
285
+ num,
286
+ label,
287
+ active
288
+ }) {
289
+ const text = ` ${num} ${label} `;
290
+ if (active) {
291
+ return /* @__PURE__ */ jsx4(Text2, { bold: true, inverse: true, color: "cyan", children: text });
292
+ }
293
+ return /* @__PURE__ */ jsx4(Text2, { children: text });
294
+ }
295
+ function Footer({ keys }) {
296
+ return /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: keys }) });
297
+ }
298
+ var init_chrome = __esm({
299
+ "src/tui/chrome.tsx"() {
300
+ "use strict";
301
+ }
302
+ });
303
+
304
+ // src/tui/JobRow.tsx
305
+ import { Box as Box3, Text as Text3 } from "ink";
306
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
273
307
  function JobRow({
274
308
  item,
275
309
  selected,
@@ -278,11 +312,11 @@ function JobRow({
278
312
  const style = statusStyle(item.status);
279
313
  const glyph = statusGlyph(item.status, spinnerFrame);
280
314
  const title = item.recording?.title ?? item.recordingId;
281
- return /* @__PURE__ */ jsxs2(Box2, { children: [
282
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: selected ? "\u25B8 " : " " }),
283
- /* @__PURE__ */ jsx2(Text2, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
284
- /* @__PURE__ */ jsx2(Text2, { bold: selected, children: padCell(title, 24) }),
285
- /* @__PURE__ */ jsx2(Text2, { dimColor: !selected, children: jobDetail(item) })
315
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
316
+ /* @__PURE__ */ jsx5(Text3, { color: "cyan", children: selected ? "\u25B8 " : " " }),
317
+ /* @__PURE__ */ jsx5(Text3, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
318
+ /* @__PURE__ */ jsx5(Text3, { bold: selected, children: padCell(title, 24) }),
319
+ /* @__PURE__ */ jsx5(Text3, { dimColor: !selected, children: jobDetail(item) })
286
320
  ] });
287
321
  }
288
322
  var init_JobRow = __esm({
@@ -293,17 +327,17 @@ var init_JobRow = __esm({
293
327
  });
294
328
 
295
329
  // src/tui/JobsView.tsx
296
- import { Box as Box3, Text as Text3 } from "ink";
297
- import { jsx as jsx3 } from "react/jsx-runtime";
330
+ import { Box as Box4, Text as Text4 } from "ink";
331
+ import { jsx as jsx6 } from "react/jsx-runtime";
298
332
  function JobsView({
299
333
  items,
300
334
  selectedIndex,
301
335
  spinnerFrame
302
336
  }) {
303
337
  if (items.length === 0) {
304
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
338
+ return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
305
339
  }
306
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx3(
340
+ return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx6(
307
341
  JobRow,
308
342
  {
309
343
  item,
@@ -321,8 +355,8 @@ var init_JobsView = __esm({
321
355
  });
322
356
 
323
357
  // src/tui/RecordingRow.tsx
324
- import { Box as Box4, Text as Text4 } from "ink";
325
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
358
+ import { Box as Box5, Text as Text5 } from "ink";
359
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
326
360
  function recordingTitle2(item) {
327
361
  const named = (item.title || item.summaryTitle || "").trim();
328
362
  if (named && !UUID_RE.test(named)) return named;
@@ -352,26 +386,28 @@ function RecordingRow({
352
386
  nowMs,
353
387
  columns,
354
388
  jobStatus,
355
- spinnerFrame = 0
389
+ spinnerFrame = 0,
390
+ downloaded = false
356
391
  }) {
357
392
  const { title, showWhen } = recordingLayout(columns);
358
393
  const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
359
394
  const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
360
- return /* @__PURE__ */ jsxs3(Box4, { children: [
361
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
362
- /* @__PURE__ */ jsx4(Text4, { color, children: `${glyph} ` }),
363
- /* @__PURE__ */ jsx4(Text4, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
364
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
365
- showWhen ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: formatAge(item.createdAt, nowMs) }) : null
395
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
396
+ /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8 " : " " }),
397
+ /* @__PURE__ */ jsx7(Text5, { color, children: `${glyph} ` }),
398
+ /* @__PURE__ */ jsx7(Text5, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
399
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
400
+ showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
401
+ /* @__PURE__ */ jsx7(Text5, { color: "green", children: downloaded ? " \u2913" : " " })
366
402
  ] });
367
403
  }
368
404
  function RecordingHeader({ columns }) {
369
405
  const { title, showWhen } = recordingLayout(columns);
370
- return /* @__PURE__ */ jsxs3(Box4, { children: [
371
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
372
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("TITLE", title) }),
373
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
374
- showWhen ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "WHEN" }) : null
406
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
407
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
408
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("TITLE", title) }),
409
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
410
+ showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: "WHEN" }) : null
375
411
  ] });
376
412
  }
377
413
  var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
@@ -388,28 +424,29 @@ var init_RecordingRow = __esm({
388
424
  });
389
425
 
390
426
  // src/tui/RecordingsView.tsx
391
- import React from "react";
392
- import { Box as Box5, Text as Text5 } from "ink";
393
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
427
+ import React3 from "react";
428
+ import { Box as Box6, Text as Text6 } from "ink";
429
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
394
430
  function RecordingsView({
395
431
  items,
396
432
  selectedIndex,
397
433
  nowMs,
398
434
  columns,
399
435
  jobStatusByRecording,
436
+ downloadedRecordingIds,
400
437
  spinnerFrame = 0
401
438
  }) {
402
439
  if (items.length === 0) {
403
- return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
440
+ return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
404
441
  }
405
- return /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
406
- /* @__PURE__ */ jsx5(RecordingHeader, { columns }),
442
+ return /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
443
+ /* @__PURE__ */ jsx8(RecordingHeader, { columns }),
407
444
  items.map((item, index) => {
408
445
  const bucket = dateBucket(item.createdAt, nowMs);
409
446
  const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
410
- return /* @__PURE__ */ jsxs4(React.Fragment, { children: [
411
- showHeader ? /* @__PURE__ */ jsx5(Box5, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: "blue", children: bucket }) }) : null,
412
- /* @__PURE__ */ jsx5(
447
+ return /* @__PURE__ */ jsxs5(React3.Fragment, { children: [
448
+ showHeader ? /* @__PURE__ */ jsx8(Box6, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx8(Text6, { bold: true, color: "blue", children: bucket }) }) : null,
449
+ /* @__PURE__ */ jsx8(
413
450
  RecordingRow,
414
451
  {
415
452
  item,
@@ -417,6 +454,7 @@ function RecordingsView({
417
454
  nowMs,
418
455
  columns,
419
456
  jobStatus: jobStatusByRecording?.get(item.recordingId),
457
+ downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
420
458
  spinnerFrame
421
459
  }
422
460
  )
@@ -433,15 +471,15 @@ var init_RecordingsView = __esm({
433
471
  });
434
472
 
435
473
  // src/tui/RecordingPeek.tsx
436
- import { Box as Box6, Text as Text6 } from "ink";
437
- import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
474
+ import { Box as Box7, Text as Text7 } from "ink";
475
+ import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
438
476
  function RecordingPeek({
439
477
  item,
440
478
  summary,
441
479
  nowMs,
442
480
  width
443
481
  }) {
444
- return /* @__PURE__ */ jsx6(Box6, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx6(PeekBody, { item, summary, nowMs }) });
482
+ return /* @__PURE__ */ jsx9(Box7, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx9(PeekBody, { item, summary, nowMs }) });
445
483
  }
446
484
  function PeekBody({
447
485
  item,
@@ -454,30 +492,30 @@ function PeekBody({
454
492
  formatBytes2(item.sizeBytes) || null,
455
493
  item.contentType || null
456
494
  ].filter(Boolean).join(" \xB7 ");
457
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
458
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
459
- /* @__PURE__ */ jsx6(Text6, { color: style.color, children: `${style.glyph} ${style.label}` }),
460
- meta3 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: meta3 }) : null,
461
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
462
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx6(SummarySection, { item, summary }) }),
463
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
495
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
496
+ /* @__PURE__ */ jsx9(Text7, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
497
+ /* @__PURE__ */ jsx9(Text7, { color: style.color, children: `${style.glyph} ${style.label}` }),
498
+ meta3 ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: meta3 }) : null,
499
+ /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
500
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx9(SummarySection, { item, summary }) }),
501
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
464
502
  ] });
465
503
  }
466
504
  function SummarySection({
467
505
  item,
468
506
  summary
469
507
  }) {
470
- if (!item.activeTranscriptId) return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No transcript yet" });
471
- if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading summary\u2026" });
472
- if (summary === "error") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "(summary unavailable)" });
508
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No transcript yet" });
509
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "Loading summary\u2026" });
510
+ if (summary === "error") return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "(summary unavailable)" });
473
511
  if (summary.status !== "succeeded" || !summary.tldr) {
474
- return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: `Summary ${summary.status}` });
512
+ return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: `Summary ${summary.status}` });
475
513
  }
476
514
  const points = (summary.keyPoints ?? []).slice(0, 3);
477
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
478
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Summary" }),
479
- /* @__PURE__ */ jsx6(Text6, { children: summary.tldr }),
480
- points.length > 0 ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
515
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
516
+ /* @__PURE__ */ jsx9(Text7, { bold: true, children: "Summary" }),
517
+ /* @__PURE__ */ jsx9(Text7, { children: summary.tldr }),
518
+ points.length > 0 ? /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx9(Text7, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
481
519
  ] });
482
520
  }
483
521
  var init_RecordingPeek = __esm({
@@ -489,8 +527,8 @@ var init_RecordingPeek = __esm({
489
527
  });
490
528
 
491
529
  // src/tui/OverviewView.tsx
492
- import { Box as Box7, Text as Text7 } from "ink";
493
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
530
+ import { Box as Box8, Text as Text8 } from "ink";
531
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
494
532
  function OverviewView({
495
533
  recordings,
496
534
  jobs,
@@ -500,6 +538,7 @@ function OverviewView({
500
538
  nowMs,
501
539
  columns,
502
540
  jobStatusByRecording,
541
+ downloadedRecordingIds,
503
542
  peekItem,
504
543
  peekSummary,
505
544
  showPeek = false,
@@ -508,17 +547,17 @@ function OverviewView({
508
547
  const jobCounts = countJobs(jobs);
509
548
  const running = stats?.jobs.running ?? jobCounts.running;
510
549
  const queued = stats?.jobs.queued ?? jobCounts.queued;
511
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
512
- /* @__PURE__ */ jsxs6(Box7, { children: [
513
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recordings " }),
514
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: stats?.recordings.total ?? recordings.length }),
515
- stats?.recordings.ready != null ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
516
- stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
517
- running > 0 ? /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
518
- queued > 0 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
550
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
551
+ /* @__PURE__ */ jsxs7(Box8, { children: [
552
+ /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Recordings " }),
553
+ /* @__PURE__ */ jsx10(Text8, { bold: true, children: stats?.recordings.total ?? recordings.length }),
554
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
555
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
556
+ running > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
557
+ queued > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
519
558
  ] }),
520
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", alignItems: "flex-start", children: [
521
- /* @__PURE__ */ jsx7(Box7, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx7(
559
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", alignItems: "flex-start", children: [
560
+ /* @__PURE__ */ jsx10(Box8, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(
522
561
  RecordingsView,
523
562
  {
524
563
  items: recordings,
@@ -526,10 +565,11 @@ function OverviewView({
526
565
  nowMs,
527
566
  columns,
528
567
  jobStatusByRecording,
568
+ downloadedRecordingIds,
529
569
  spinnerFrame
530
570
  }
531
571
  ) }),
532
- showPeek ? /* @__PURE__ */ jsx7(Box7, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx7(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
572
+ showPeek ? /* @__PURE__ */ jsx10(Box8, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx10(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
533
573
  ] })
534
574
  ] });
535
575
  }
@@ -543,8 +583,8 @@ var init_OverviewView = __esm({
543
583
  });
544
584
 
545
585
  // src/tui/JobDetailView.tsx
546
- import { Box as Box8, Text as Text8 } from "ink";
547
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
586
+ import { Box as Box9, Text as Text9 } from "ink";
587
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
548
588
  function JobDetailView({
549
589
  item,
550
590
  origin,
@@ -554,13 +594,13 @@ function JobDetailView({
554
594
  const style = statusStyle(item.status);
555
595
  const links = resolveJobLinks(item, origin);
556
596
  const title = item.recording?.title ?? item.recordingId;
557
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 1, children: [
558
- /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
597
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 1, children: [
598
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
559
599
  "\u2039 Jobs / ",
560
600
  title
561
601
  ] }),
562
- /* @__PURE__ */ jsxs7(
563
- Box8,
602
+ /* @__PURE__ */ jsxs8(
603
+ Box9,
564
604
  {
565
605
  marginTop: 1,
566
606
  borderStyle: "round",
@@ -568,17 +608,17 @@ function JobDetailView({
568
608
  paddingX: 1,
569
609
  flexDirection: "column",
570
610
  children: [
571
- /* @__PURE__ */ jsxs7(Text8, { color: style.color, bold: true, children: [
611
+ /* @__PURE__ */ jsxs8(Text9, { color: style.color, bold: true, children: [
572
612
  style.label,
573
- item.provider ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${item.provider}` }) : null
613
+ item.provider ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${item.provider}` }) : null
574
614
  ] }),
575
- /* @__PURE__ */ jsx8(StatusLine, { item, spinnerFrame, nowMs })
615
+ /* @__PURE__ */ jsx11(StatusLine, { item, spinnerFrame, nowMs })
576
616
  ]
577
617
  }
578
618
  ),
579
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
580
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Timeline" }),
581
- /* @__PURE__ */ jsx8(
619
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
620
+ /* @__PURE__ */ jsx11(Text9, { bold: true, children: "Timeline" }),
621
+ /* @__PURE__ */ jsx11(
582
622
  TimelineRow,
583
623
  {
584
624
  label: "Enqueued",
@@ -587,7 +627,7 @@ function JobDetailView({
587
627
  nowMs
588
628
  }
589
629
  ),
590
- /* @__PURE__ */ jsx8(
630
+ /* @__PURE__ */ jsx11(
591
631
  TimelineRow,
592
632
  {
593
633
  label: "Started",
@@ -596,7 +636,7 @@ function JobDetailView({
596
636
  nowMs
597
637
  }
598
638
  ),
599
- /* @__PURE__ */ jsx8(
639
+ /* @__PURE__ */ jsx11(
600
640
  TimelineRow,
601
641
  {
602
642
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -608,21 +648,21 @@ function JobDetailView({
608
648
  }
609
649
  )
610
650
  ] }),
611
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
612
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Recording " }),
651
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
652
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Recording " }),
613
653
  title,
614
- item.recording?.durationMs ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
654
+ item.recording?.durationMs ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
615
655
  ] }) }),
616
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
617
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
618
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
619
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
620
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
621
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "m mac app (soon)" }),
622
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
623
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
656
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
657
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
658
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
659
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
660
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
661
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "m mac app (soon)" }),
662
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
663
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
624
664
  ] }) }),
625
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
665
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
626
666
  "esc back \xB7 t transcript",
627
667
  item.transcriptId ? "" : " (when ready)",
628
668
  " \xB7 q quit"
@@ -639,24 +679,24 @@ function StatusLine({
639
679
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
640
680
  if (fraction != null) {
641
681
  const pct = Math.round(fraction * 100);
642
- return /* @__PURE__ */ jsxs7(Text8, { children: [
682
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
643
683
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
644
684
  item.recording?.durationMs
645
685
  )}`,
646
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
686
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
647
687
  ] });
648
688
  }
649
689
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
650
- return /* @__PURE__ */ jsxs7(Text8, { children: [
690
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
651
691
  `${spinner} transcribing\u2026`,
652
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
692
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
653
693
  ] });
654
694
  }
655
695
  if (item.status === "succeeded")
656
- return /* @__PURE__ */ jsx8(Text8, { children: item.transcriptId ? "transcript ready" : "done" });
657
- if (item.status === "queued") return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "waiting to start\u2026" });
658
- if (item.status === "failed") return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "transcription failed" });
659
- return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: item.status });
696
+ return /* @__PURE__ */ jsx11(Text9, { children: item.transcriptId ? "transcript ready" : "done" });
697
+ if (item.status === "queued") return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "waiting to start\u2026" });
698
+ if (item.status === "failed") return /* @__PURE__ */ jsx11(Text9, { color: "red", children: "transcription failed" });
699
+ return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: item.status });
660
700
  }
661
701
  function TimelineRow({
662
702
  label,
@@ -669,10 +709,10 @@ function TimelineRow({
669
709
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
670
710
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
671
711
  const age = at ? formatAge(at, nowMs) : running ? "now" : "";
672
- return /* @__PURE__ */ jsxs7(Box8, { children: [
673
- /* @__PURE__ */ jsx8(Text8, { color, children: ` ${glyph} ` }),
674
- /* @__PURE__ */ jsx8(Text8, { dimColor: !done && !running, children: label }),
675
- age ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${age}` }) : null
712
+ return /* @__PURE__ */ jsxs8(Box9, { children: [
713
+ /* @__PURE__ */ jsx11(Text9, { color, children: ` ${glyph} ` }),
714
+ /* @__PURE__ */ jsx11(Text9, { dimColor: !done && !running, children: label }),
715
+ age ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${age}` }) : null
676
716
  ] });
677
717
  }
678
718
  var init_JobDetailView = __esm({
@@ -683,14 +723,19 @@ var init_JobDetailView = __esm({
683
723
  });
684
724
 
685
725
  // src/tui/RecordingDetailView.tsx
686
- import { Box as Box9, Text as Text9 } from "ink";
687
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
726
+ import React4, { useMemo as useMemo2, useState as useState3 } from "react";
727
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
728
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
688
729
  function RecordingDetailView({
689
730
  item,
690
731
  nowMs,
691
732
  transcript,
692
733
  audio
693
734
  }) {
735
+ const size = useTerminalSize();
736
+ const [tab, setTab] = useState3("summary");
737
+ const [scroll, setScroll] = useState3(0);
738
+ const [chapterSel, setChapterSel] = useState3(0);
694
739
  const style = recordingStatusStyle(item.status);
695
740
  const links = resolveRecordingLinks(item.recordingId, item.origin);
696
741
  const title = recordingTitle2(item);
@@ -699,27 +744,81 @@ function RecordingDetailView({
699
744
  formatBytes2(item.sizeBytes) || void 0,
700
745
  item.contentType || void 0
701
746
  ].filter(Boolean).join(" \xB7 ");
702
- const summary = typeof transcript === "object" ? transcript.summary : void 0;
703
- const segments = typeof transcript === "object" ? transcript.segments : void 0;
704
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 1, children: [
705
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2039 Recordings" }),
706
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: title }) }),
707
- /* @__PURE__ */ jsxs8(Text9, { children: [
708
- /* @__PURE__ */ jsx9(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
709
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
747
+ const ready = typeof transcript === "object";
748
+ const summary = ready ? transcript.summary : void 0;
749
+ const segments = ready ? transcript.segments : [];
750
+ const chapters = summary?.timeline ?? [];
751
+ const innerWidth = Math.max(10, size.columns - 2);
752
+ const paneBudget = Math.max(3, size.rows - 12);
753
+ const segHeights = useMemo2(
754
+ () => segments.map((seg) => {
755
+ const prefix = `[${formatClockMs(seg.startMs)}] ${seg.speaker ? `${seg.speaker}: ` : ""}`;
756
+ return Math.max(1, Math.ceil(displayWidth(prefix + seg.text) / innerWidth));
757
+ }),
758
+ [segments, innerWidth]
759
+ );
760
+ const segWin = windowByHeights(segHeights, scroll, paneBudget);
761
+ const chapWin = listWindow(Math.min(chapterSel, Math.max(0, chapters.length - 1)), chapters.length, paneBudget);
762
+ const page = Math.max(1, paneBudget - 1);
763
+ const scrollable = tab === "transcript" ? segWin.maxScroll > 0 : false;
764
+ const jumpToChapter = (index) => {
765
+ const chapter = chapters[index];
766
+ if (!chapter) return;
767
+ const found = segments.findIndex((s) => s.startMs >= chapter.startMs);
768
+ setScroll(found < 0 ? Math.max(0, segments.length - 1) : found);
769
+ setTab("transcript");
770
+ };
771
+ useInput3((input, key) => {
772
+ if (!item.activeTranscriptId || !ready) return;
773
+ if (key.tab) {
774
+ setTab((t) => TAB_ORDER[(TAB_ORDER.indexOf(t) + (key.shift ? TAB_ORDER.length - 1 : 1)) % TAB_ORDER.length]);
775
+ return;
776
+ }
777
+ if (tab === "summary") return;
778
+ if (tab === "chapters") {
779
+ if (key.downArrow || input === "j") setChapterSel((i) => Math.min(chapters.length - 1, i + 1));
780
+ else if (key.upArrow || input === "k") setChapterSel((i) => Math.max(0, i - 1));
781
+ else if (key.return || key.rightArrow) jumpToChapter(chapterSel);
782
+ return;
783
+ }
784
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(segWin.maxScroll, s + 1));
785
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
786
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(segWin.maxScroll, s + page));
787
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
788
+ else if (input === "g") setScroll(0);
789
+ else if (input === "G") setScroll(segWin.maxScroll);
790
+ });
791
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
792
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "\u2039 Recordings" }),
793
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { bold: true, color: "magenta", children: title }) }),
794
+ /* @__PURE__ */ jsxs9(Text10, { children: [
795
+ /* @__PURE__ */ jsx12(Text10, { color: style.color, children: `${style.glyph} ${style.label}` }),
796
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
710
797
  ] }),
711
- meta3 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: meta3 }) : null,
712
- /* @__PURE__ */ jsx9(AudioActionRow, { item, audio }),
713
- /* @__PURE__ */ jsx9(SummaryCard, { summary, transcript, hasTranscript: !!item.activeTranscriptId }),
714
- /* @__PURE__ */ jsx9(TranscriptPreview, { segments, transcript, hasTranscript: !!item.activeTranscriptId }),
715
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
716
- `esc back \xB7 o open \xB7 d download \xB7 f finder`,
717
- item.activeTranscriptId ? " \xB7 t transcript" : "",
798
+ meta3 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: meta3 }) : null,
799
+ /* @__PURE__ */ jsx12(AudioActionRow, { item, audio }),
800
+ !item.activeTranscriptId ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
801
+ /* @__PURE__ */ jsx12(TabBar, { active: tab }),
802
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx12(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx12(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx12(TranscriptPane, { segments, win: segWin }) })
803
+ ] }),
804
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
805
+ ready ? "tab switch" : "",
806
+ ready && tab === "chapters" ? " \xB7 \u2191\u2193 select \xB7 \u23CE jump" : "",
807
+ scrollable ? " \xB7 \u2191\u2193 scroll" : "",
808
+ ready ? " \xB7 " : "",
809
+ `o open \xB7 d download \xB7 f finder`,
810
+ item.activeTranscriptId ? " \xB7 t full" : "",
718
811
  links.webUrl ? " \xB7 w web" : "",
719
- " \xB7 q quit"
812
+ " \xB7 esc back"
720
813
  ] }) })
721
814
  ] });
722
815
  }
816
+ function TabBar({ active }) {
817
+ return /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs9(React4.Fragment, { children: [
818
+ i > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " " }) : null,
819
+ tab === active ? /* @__PURE__ */ jsx12(Text10, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
820
+ ] }, tab)) });
821
+ }
723
822
  function AudioActionRow({
724
823
  item,
725
824
  audio
@@ -728,149 +827,183 @@ function AudioActionRow({
728
827
  const status = audio?.status ?? "idle";
729
828
  let line;
730
829
  if (!ready) {
731
- line = /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Audio available once the recording is ready" });
830
+ line = /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Audio available once the recording is ready" });
732
831
  } else if (status === "downloading") {
733
- line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Downloading audio\u2026" });
832
+ line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Downloading audio\u2026" });
734
833
  } else if (status === "opening") {
735
- line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Opening\u2026" });
834
+ line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Opening\u2026" });
736
835
  } else if (status === "error") {
737
- line = /* @__PURE__ */ jsx9(Text9, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
836
+ line = /* @__PURE__ */ jsx12(Text10, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
738
837
  } else if (status === "ready" && audio?.localPath) {
739
- line = /* @__PURE__ */ jsxs8(Text9, { children: [
740
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Downloaded " }),
741
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
838
+ line = /* @__PURE__ */ jsxs9(Text10, { children: [
839
+ /* @__PURE__ */ jsx12(Text10, { color: "green", children: "\u2713 Downloaded " }),
840
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
742
841
  ] });
743
842
  } else {
744
- line = /* @__PURE__ */ jsxs8(Text9, { children: [
745
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "o" }),
746
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " open in player \xB7 " }),
747
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "d" }),
748
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " download \xB7 " }),
749
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "f" }),
750
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " reveal in Finder" })
843
+ line = /* @__PURE__ */ jsxs9(Text10, { children: [
844
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "o" }),
845
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " open in player \xB7 " }),
846
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "d" }),
847
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " download \xB7 " }),
848
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "f" }),
849
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " reveal in Finder" })
751
850
  ] });
752
851
  }
753
- return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
754
- /* @__PURE__ */ jsx9(Text9, { color: ready ? "cyan" : "gray", children: "\u266A " }),
852
+ return /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
853
+ /* @__PURE__ */ jsx12(Text10, { color: ready ? "cyan" : "gray", children: "\u266A " }),
755
854
  line
756
855
  ] });
757
856
  }
758
- function SummaryCard({
857
+ function SummaryPane({
759
858
  summary,
760
- transcript,
761
- hasTranscript
859
+ budget
860
+ }) {
861
+ if (!summary || summary.status !== "succeeded" || !summary.tldr) {
862
+ return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
863
+ }
864
+ const points = (summary.keyPoints ?? []).slice(0, Math.max(1, budget - 4));
865
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
866
+ /* @__PURE__ */ jsx12(Text10, { children: summary.tldr }),
867
+ points.length > 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
868
+ ] });
869
+ }
870
+ function ChaptersPane({
871
+ chapters,
872
+ win,
873
+ selectedIndex
762
874
  }) {
763
- if (!hasTranscript) return null;
764
- return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
765
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Summary" }),
766
- transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading\u2026" }) : transcript === "error" ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(summary unavailable)" }) : summary && summary.status === "succeeded" && summary.tldr ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
767
- /* @__PURE__ */ jsx9(Text9, { children: summary.tldr }),
768
- (summary.keyPoints ?? []).slice(0, 5).map((point, i) => /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `\u2022 ${point}` }, i))
769
- ] }) : /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` })
875
+ if (chapters.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "No chapters" });
876
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
877
+ chapters.slice(win.start, win.end).map((chapter, i) => {
878
+ const index = win.start + i;
879
+ const selected = index === selectedIndex;
880
+ return /* @__PURE__ */ jsxs9(Text10, { wrap: "truncate-end", children: [
881
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: selected ? "\u25B8 " : " " }),
882
+ /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
883
+ /* @__PURE__ */ jsx12(Text10, { bold: selected, children: chapter.title })
884
+ ] }, index);
885
+ }),
886
+ win.end < chapters.length || win.start > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${selectedIndex + 1} / ${chapters.length}` }) : null
770
887
  ] });
771
888
  }
772
- function TranscriptPreview({
889
+ function TranscriptPane({
773
890
  segments,
774
- transcript,
775
- hasTranscript
891
+ win
776
892
  }) {
777
- if (!hasTranscript) {
778
- return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Transcript not available yet" }) });
779
- }
780
- if (transcript === "loading" || transcript === void 0 || transcript === "error") {
781
- return null;
782
- }
783
- const shown = (segments ?? []).slice(0, PREVIEW_SEGMENTS);
784
- return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
785
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Transcript" }),
786
- shown.length === 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(no segments)" }) : shown.map((seg, i) => /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate-end", children: [
787
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
788
- seg.speaker ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${seg.speaker} ` }) : null,
789
- /* @__PURE__ */ jsx9(Text9, { children: seg.text })
790
- ] }, i)),
791
- (segments?.length ?? 0) > PREVIEW_SEGMENTS ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: ` \u2026${(segments?.length ?? 0) - PREVIEW_SEGMENTS} more \u2014 press t for full transcript` }) : null
893
+ if (segments.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(no segments)" });
894
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
895
+ segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs9(Text10, { children: [
896
+ /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
897
+ seg.speaker ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `${seg.speaker} ` }) : null,
898
+ /* @__PURE__ */ jsx12(Text10, { children: seg.text })
899
+ ] }, win.start + i)),
900
+ win.maxScroll > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
792
901
  ] });
793
902
  }
794
- var PREVIEW_SEGMENTS;
903
+ var TAB_ORDER, TAB_LABEL;
795
904
  var init_RecordingDetailView = __esm({
796
905
  "src/tui/RecordingDetailView.tsx"() {
797
906
  "use strict";
798
907
  init_format();
799
908
  init_RecordingRow();
800
- PREVIEW_SEGMENTS = 6;
909
+ init_terminal();
910
+ TAB_ORDER = ["summary", "chapters", "transcript"];
911
+ TAB_LABEL = {
912
+ summary: "Summary",
913
+ chapters: "Chapters",
914
+ transcript: "Transcript"
915
+ };
801
916
  }
802
917
  });
803
918
 
804
919
  // src/tui/TranscriptView.tsx
805
- import { Box as Box10, Text as Text10 } from "ink";
806
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
920
+ import { useMemo as useMemo3, useState as useState4 } from "react";
921
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
922
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
807
923
  function TranscriptView({ loading, data, error: error51 }) {
924
+ const size = useTerminalSize();
925
+ const [scroll, setScroll] = useState4(0);
926
+ const segments = data?.segments ?? [];
927
+ const innerWidth = Math.max(10, size.columns - 2);
928
+ const heights = useMemo3(
929
+ () => segments.map((s) => {
930
+ const prefix = `[${formatClockMs(s.startMs)}] ${s.speaker ? `${s.speaker}: ` : ""}`;
931
+ return Math.max(1, Math.ceil(displayWidth(prefix + s.text) / innerWidth));
932
+ }),
933
+ [segments, innerWidth]
934
+ );
935
+ const budget = Math.max(3, size.rows - 3);
936
+ const win = windowByHeights(heights, scroll, budget);
937
+ const page = Math.max(1, budget - 1);
938
+ useInput4((input, key) => {
939
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(win.maxScroll, s + 1));
940
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
941
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(win.maxScroll, s + page));
942
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
943
+ else if (input === "g") setScroll(0);
944
+ else if (input === "G") setScroll(win.maxScroll);
945
+ });
808
946
  if (loading) {
809
- return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Loading transcript\u2026" }) });
947
+ return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Loading transcript\u2026" }) });
810
948
  }
811
949
  if (error51) {
812
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
813
- /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
950
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
951
+ /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
814
952
  "! ",
815
953
  error51
816
954
  ] }),
817
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "q / esc / \u2190 back" })
955
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "q / esc / \u2190 back" })
818
956
  ] });
819
957
  }
820
958
  if (!data) {
821
- return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "No transcript." }) });
959
+ return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "No transcript." }) });
822
960
  }
823
- const summary = data.summary;
824
- const showSummary = summary?.status === "succeeded";
825
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
826
- /* @__PURE__ */ jsx10(Text10, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
827
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx10(Text10, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs9(Text10, { children: [
828
- /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
961
+ const title = data.summary?.title ?? "Transcript";
962
+ const total = segments.length;
963
+ const more = win.maxScroll > 0;
964
+ const position = total === 0 ? "" : `${win.start + 1}\u2013${win.end} / ${total}`;
965
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
966
+ /* @__PURE__ */ jsxs10(Text11, { bold: true, color: "magenta", children: [
967
+ title,
968
+ more ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${position}` }) : null
969
+ ] }),
970
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx13(Text11, { children: data.text }) : segments.slice(win.start, win.end).map((segment, index) => /* @__PURE__ */ jsxs10(Text11, { children: [
971
+ /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
829
972
  "[",
830
973
  formatClockMs(segment.startMs),
831
974
  "] "
832
975
  ] }),
833
- segment.speaker ? /* @__PURE__ */ jsxs9(Text10, { color: "cyan", children: [
976
+ segment.speaker ? /* @__PURE__ */ jsxs10(Text11, { color: "cyan", children: [
834
977
  segment.speaker,
835
978
  ": "
836
979
  ] }) : null,
837
980
  segment.text
838
- ] }, index)) }),
839
- showSummary && summary?.tldr ? /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
840
- /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Summary" }),
841
- /* @__PURE__ */ jsx10(Text10, { children: summary.tldr })
842
- ] }) : null,
843
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "q / esc / \u2190 back" }) })
981
+ ] }, win.start + index)) }),
982
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
983
+ more ? "\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 g/G top/bottom \xB7 " : "",
984
+ "q / esc / \u2190 back"
985
+ ] }) })
844
986
  ] });
845
987
  }
846
988
  var init_TranscriptView = __esm({
847
989
  "src/tui/TranscriptView.tsx"() {
848
990
  "use strict";
849
991
  init_format();
850
- }
851
- });
852
-
853
- // src/tui/terminal.ts
854
- import { useWindowSize } from "ink";
855
- function useTerminalSize() {
856
- return useWindowSize();
857
- }
858
- var init_terminal = __esm({
859
- "src/tui/terminal.ts"() {
860
- "use strict";
992
+ init_terminal();
861
993
  }
862
994
  });
863
995
 
864
996
  // src/tui/AppShell.tsx
865
- import { useCallback, useEffect, useState } from "react";
866
- import { Box as Box11, Text as Text11, useApp, useInput } from "ink";
867
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
997
+ import { useCallback, useEffect as useEffect2, useState as useState5 } from "react";
998
+ import { Box as Box12, Text as Text12, useApp, useInput as useInput5 } from "ink";
999
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
868
1000
  function AppShell({
869
1001
  fetchJobs,
870
1002
  fetchTranscript,
871
1003
  fetchRecordings,
872
1004
  fetchDashboardStats,
873
1005
  recordingAudio,
1006
+ listDownloadedRecordingIds,
874
1007
  initialView = "overview",
875
1008
  openUrl: openUrl2,
876
1009
  copyText: copyText2,
@@ -880,27 +1013,38 @@ function AppShell({
880
1013
  }) {
881
1014
  const { exit } = useApp();
882
1015
  const size = useTerminalSize();
883
- const [jobs, setJobs] = useState([]);
884
- const [recordings, setRecordings] = useState([]);
885
- const [recordingsNextCursor, setRecordingsNextCursor] = useState(null);
886
- const [recordingsTotalCount, setRecordingsTotalCount] = useState(void 0);
887
- const [stats, setStats] = useState(void 0);
888
- const [origin, setOrigin] = useState("");
889
- const [stack, setStack] = useState([{ kind: initialView }]);
890
- const [selected, setSelected] = useState(0);
891
- const [spinnerFrame, setSpinnerFrame] = useState(0);
892
- const [loadingMoreRecordings, setLoadingMoreRecordings] = useState(false);
893
- const [loadError, setLoadError] = useState(void 0);
894
- const [notice, setNotice] = useState(void 0);
895
- const [summaryCache, setSummaryCache] = useState(() => /* @__PURE__ */ new Map());
896
- const [transcriptCache, setTranscriptCache] = useState(
1016
+ const [jobs, setJobs] = useState5([]);
1017
+ const [recordings, setRecordings] = useState5([]);
1018
+ const [recordingsNextCursor, setRecordingsNextCursor] = useState5(null);
1019
+ const [recordingsTotalCount, setRecordingsTotalCount] = useState5(void 0);
1020
+ const [stats, setStats] = useState5(void 0);
1021
+ const [origin, setOrigin] = useState5("");
1022
+ const [stack, setStack] = useState5([{ kind: initialView }]);
1023
+ const [selected, setSelected] = useState5(0);
1024
+ const [spinnerFrame, setSpinnerFrame] = useState5(0);
1025
+ const [loadingMoreRecordings, setLoadingMoreRecordings] = useState5(false);
1026
+ const [loadError, setLoadError] = useState5(void 0);
1027
+ const [notice, setNotice] = useState5(void 0);
1028
+ const [summaryCache, setSummaryCache] = useState5(() => /* @__PURE__ */ new Map());
1029
+ const [transcriptCache, setTranscriptCache] = useState5(
897
1030
  () => /* @__PURE__ */ new Map()
898
1031
  );
899
- const [audioCache, setAudioCache] = useState(() => /* @__PURE__ */ new Map());
1032
+ const [audioCache, setAudioCache] = useState5(() => /* @__PURE__ */ new Map());
1033
+ const [downloadedIds, setDownloadedIds] = useState5(() => /* @__PURE__ */ new Set());
1034
+ const refreshDownloadedIds = useCallback(async () => {
1035
+ if (!listDownloadedRecordingIds) return;
1036
+ try {
1037
+ setDownloadedIds(await listDownloadedRecordingIds());
1038
+ } catch {
1039
+ }
1040
+ }, [listDownloadedRecordingIds]);
1041
+ useEffect2(() => {
1042
+ void refreshDownloadedIds();
1043
+ }, [refreshDownloadedIds]);
900
1044
  const screen = stack[stack.length - 1];
901
1045
  const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
902
1046
  const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
903
- useEffect(() => {
1047
+ useEffect2(() => {
904
1048
  if (!peekTranscriptId || summaryCache.has(peekTranscriptId)) return;
905
1049
  let cancelled = false;
906
1050
  const timer = setTimeout(() => {
@@ -962,13 +1106,13 @@ function AppShell({
962
1106
  setLoadingMoreRecordings(false);
963
1107
  }
964
1108
  }, [fetchRecordings, loadingMoreRecordings, recordingsNextCursor]);
965
- useEffect(() => {
1109
+ useEffect2(() => {
966
1110
  void refresh({ resetRecordings: true });
967
1111
  const id = setInterval(() => void refresh(), pollMs);
968
1112
  return () => clearInterval(id);
969
1113
  }, [refresh, pollMs]);
970
1114
  const hasRunning = jobs.some((item) => item.status === "running");
971
- useEffect(() => {
1115
+ useEffect2(() => {
972
1116
  if (!hasRunning) return;
973
1117
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
974
1118
  return () => clearInterval(id);
@@ -982,11 +1126,11 @@ function AppShell({
982
1126
  }
983
1127
  }
984
1128
  const listLength = screen.kind === "jobs" ? jobs.length : recordings.length;
985
- useEffect(() => {
1129
+ useEffect2(() => {
986
1130
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
987
1131
  }, [listLength]);
988
1132
  const visibleRecordingRows = Math.max(3, size.rows - 6);
989
- useEffect(() => {
1133
+ useEffect2(() => {
990
1134
  if (screen.kind !== "overview" || !recordingsNextCursor) return;
991
1135
  const nearLoadedEnd = recordings.length - selected <= RECORDINGS_PREFETCH_REMAINING;
992
1136
  const underfilledViewport = recordings.length < visibleRecordingRows;
@@ -1019,7 +1163,7 @@ function AppShell({
1019
1163
  [fetchTranscript]
1020
1164
  );
1021
1165
  const detailTranscriptId = screen.kind === "recordingDetail" ? recordings.find((r) => r.recordingId === screen.recordingId)?.activeTranscriptId : void 0;
1022
- useEffect(() => {
1166
+ useEffect2(() => {
1023
1167
  if (!detailTranscriptId || transcriptCache.has(detailTranscriptId)) return;
1024
1168
  let cancelled = false;
1025
1169
  setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "loading"));
@@ -1050,6 +1194,7 @@ function AppShell({
1050
1194
  await recordingAudio.revealInFinder(localPath);
1051
1195
  }
1052
1196
  setAudio(recordingId, { status: "ready", localPath });
1197
+ void refreshDownloadedIds();
1053
1198
  } catch (error51) {
1054
1199
  setAudio(recordingId, {
1055
1200
  status: "error",
@@ -1057,7 +1202,7 @@ function AppShell({
1057
1202
  });
1058
1203
  }
1059
1204
  },
1060
- [recordingAudio]
1205
+ [recordingAudio, refreshDownloadedIds]
1061
1206
  );
1062
1207
  const goTab = (tab2) => {
1063
1208
  setStack([{ kind: tab2 }]);
@@ -1065,7 +1210,7 @@ function AppShell({
1065
1210
  setNotice(void 0);
1066
1211
  };
1067
1212
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
1068
- useInput((input, key) => {
1213
+ useInput5((input, key) => {
1069
1214
  setNotice(void 0);
1070
1215
  if (input === "q") return exit();
1071
1216
  if (key.escape || key.leftArrow) return back();
@@ -1117,18 +1262,18 @@ function AppShell({
1117
1262
  }
1118
1263
  });
1119
1264
  if (screen.kind === "transcript") {
1120
- return /* @__PURE__ */ jsx11(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1265
+ return /* @__PURE__ */ jsx14(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1121
1266
  }
1122
1267
  if (screen.kind === "jobDetail") {
1123
1268
  const job = jobs.find((j) => j.jobId === screen.jobId);
1124
- if (!job) return /* @__PURE__ */ jsx11(Missing, { label: "Job" });
1125
- return /* @__PURE__ */ jsx11(Detail, { notice, children: /* @__PURE__ */ jsx11(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1269
+ if (!job) return /* @__PURE__ */ jsx14(Missing, { label: "Job" });
1270
+ return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1126
1271
  }
1127
1272
  if (screen.kind === "recordingDetail") {
1128
1273
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
1129
- if (!rec) return /* @__PURE__ */ jsx11(Missing, { label: "Recording" });
1274
+ if (!rec) return /* @__PURE__ */ jsx14(Missing, { label: "Recording" });
1130
1275
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
1131
- return /* @__PURE__ */ jsx11(Detail, { notice, children: /* @__PURE__ */ jsx11(
1276
+ return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(
1132
1277
  RecordingDetailView,
1133
1278
  {
1134
1279
  item: rec,
@@ -1153,7 +1298,7 @@ function AppShell({
1153
1298
  const showPeek = size.columns >= 100;
1154
1299
  const peekWidth = showPeek ? 34 : 0;
1155
1300
  const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
1156
- body = /* @__PURE__ */ jsx11(
1301
+ body = /* @__PURE__ */ jsx14(
1157
1302
  OverviewView,
1158
1303
  {
1159
1304
  recordings: recordings.slice(win.start, win.end),
@@ -1163,6 +1308,7 @@ function AppShell({
1163
1308
  nowMs: now(),
1164
1309
  columns: listColumns,
1165
1310
  jobStatusByRecording,
1311
+ downloadedRecordingIds: downloadedIds,
1166
1312
  spinnerFrame,
1167
1313
  peekItem: recordings[selected],
1168
1314
  peekSummary,
@@ -1173,7 +1319,7 @@ function AppShell({
1173
1319
  } else {
1174
1320
  const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
1175
1321
  position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
1176
- body = /* @__PURE__ */ jsx11(
1322
+ body = /* @__PURE__ */ jsx14(
1177
1323
  JobsView,
1178
1324
  {
1179
1325
  items: jobs.slice(win.start, win.end),
@@ -1183,34 +1329,34 @@ function AppShell({
1183
1329
  );
1184
1330
  }
1185
1331
  const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 1 overview \xB7 r refresh \xB7 q quit` : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 2 jobs \xB7 r refresh \xB7 q quit`;
1186
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1187
- /* @__PURE__ */ jsx11(Header, { active: tab }),
1188
- /* @__PURE__ */ jsxs10(Box11, { flexGrow: 1, flexDirection: "column", children: [
1332
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1333
+ /* @__PURE__ */ jsx14(Header, { active: tab }),
1334
+ /* @__PURE__ */ jsxs11(Box12, { flexGrow: 1, flexDirection: "column", children: [
1189
1335
  body,
1190
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
1336
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
1191
1337
  "! ",
1192
1338
  loadError
1193
1339
  ] }) }) : null
1194
1340
  ] }),
1195
- /* @__PURE__ */ jsx11(Footer, { keys: footerKeys })
1341
+ /* @__PURE__ */ jsx14(Footer, { keys: footerKeys })
1196
1342
  ] });
1197
1343
  }
1198
1344
  function Detail({
1199
1345
  notice,
1200
1346
  children
1201
1347
  }) {
1202
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
1348
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", children: [
1203
1349
  children,
1204
- notice ? /* @__PURE__ */ jsx11(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "green", children: notice }) }) : null
1350
+ notice ? /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { color: "green", children: notice }) }) : null
1205
1351
  ] });
1206
1352
  }
1207
1353
  function Missing({ label }) {
1208
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1209
- /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1354
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1355
+ /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
1210
1356
  label,
1211
1357
  " no longer in the list."
1212
1358
  ] }),
1213
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "esc back \xB7 q quit" })
1359
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "esc back \xB7 q quit" })
1214
1360
  ] });
1215
1361
  }
1216
1362
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
@@ -1241,33 +1387,34 @@ __export(tui_exports, {
1241
1387
  runDashboard: () => runDashboard,
1242
1388
  useTerminalSize: () => useTerminalSize
1243
1389
  });
1244
- import React3 from "react";
1245
- import { render } from "ink";
1246
- import { spawn as spawn2 } from "child_process";
1390
+ import React7 from "react";
1391
+ import { render as render2 } from "ink";
1392
+ import { spawn as spawn3 } from "child_process";
1247
1393
  function openUrl(url2) {
1248
1394
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1249
1395
  try {
1250
- spawn2(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1396
+ spawn3(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1251
1397
  } catch {
1252
1398
  }
1253
1399
  }
1254
1400
  function copyText(text) {
1255
1401
  if (process.platform !== "darwin") return;
1256
1402
  try {
1257
- const child = spawn2("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1403
+ const child = spawn3("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1258
1404
  child.stdin.end(text);
1259
1405
  } catch {
1260
1406
  }
1261
1407
  }
1262
1408
  async function runDashboard(deps) {
1263
- const renderApp = deps.renderApp ?? render;
1409
+ const renderApp = deps.renderApp ?? render2;
1264
1410
  const app = renderApp(
1265
- React3.createElement(AppShell, {
1411
+ React7.createElement(AppShell, {
1266
1412
  fetchJobs: deps.fetchJobs,
1267
1413
  fetchTranscript: deps.fetchTranscript,
1268
1414
  fetchRecordings: deps.fetchRecordings,
1269
1415
  fetchDashboardStats: deps.fetchDashboardStats,
1270
1416
  recordingAudio: deps.recordingAudio,
1417
+ listDownloadedRecordingIds: deps.listDownloadedRecordingIds,
1271
1418
  initialView: deps.initialView ?? "overview",
1272
1419
  openUrl,
1273
1420
  copyText
@@ -1295,7 +1442,7 @@ var init_tui = __esm({
1295
1442
 
1296
1443
  // src/cli.ts
1297
1444
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
1298
- import os4 from "os";
1445
+ import os5 from "os";
1299
1446
 
1300
1447
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
1301
1448
  var external_exports = {};
@@ -2063,10 +2210,10 @@ function mergeDefs(...defs) {
2063
2210
  function cloneDef(schema) {
2064
2211
  return mergeDefs(schema._zod.def);
2065
2212
  }
2066
- function getElementAtPath(obj, path4) {
2067
- if (!path4)
2213
+ function getElementAtPath(obj, path6) {
2214
+ if (!path6)
2068
2215
  return obj;
2069
- return path4.reduce((acc, key) => acc?.[key], obj);
2216
+ return path6.reduce((acc, key) => acc?.[key], obj);
2070
2217
  }
2071
2218
  function promiseAllObject(promisesObj) {
2072
2219
  const keys = Object.keys(promisesObj);
@@ -2475,11 +2622,11 @@ function explicitlyAborted(x, startIndex = 0) {
2475
2622
  }
2476
2623
  return false;
2477
2624
  }
2478
- function prefixIssues(path4, issues) {
2625
+ function prefixIssues(path6, issues) {
2479
2626
  return issues.map((iss) => {
2480
2627
  var _a3;
2481
2628
  (_a3 = iss).path ?? (_a3.path = []);
2482
- iss.path.unshift(path4);
2629
+ iss.path.unshift(path6);
2483
2630
  return iss;
2484
2631
  });
2485
2632
  }
@@ -2626,16 +2773,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2626
2773
  }
2627
2774
  function formatError(error51, mapper = (issue2) => issue2.message) {
2628
2775
  const fieldErrors = { _errors: [] };
2629
- const processError = (error52, path4 = []) => {
2776
+ const processError = (error52, path6 = []) => {
2630
2777
  for (const issue2 of error52.issues) {
2631
2778
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2632
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2779
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2633
2780
  } else if (issue2.code === "invalid_key") {
2634
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2781
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2635
2782
  } else if (issue2.code === "invalid_element") {
2636
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2783
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2637
2784
  } else {
2638
- const fullpath = [...path4, ...issue2.path];
2785
+ const fullpath = [...path6, ...issue2.path];
2639
2786
  if (fullpath.length === 0) {
2640
2787
  fieldErrors._errors.push(mapper(issue2));
2641
2788
  } else {
@@ -2662,17 +2809,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2662
2809
  }
2663
2810
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2664
2811
  const result = { errors: [] };
2665
- const processError = (error52, path4 = []) => {
2812
+ const processError = (error52, path6 = []) => {
2666
2813
  var _a3, _b;
2667
2814
  for (const issue2 of error52.issues) {
2668
2815
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2669
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2816
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2670
2817
  } else if (issue2.code === "invalid_key") {
2671
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2818
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2672
2819
  } else if (issue2.code === "invalid_element") {
2673
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2820
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2674
2821
  } else {
2675
- const fullpath = [...path4, ...issue2.path];
2822
+ const fullpath = [...path6, ...issue2.path];
2676
2823
  if (fullpath.length === 0) {
2677
2824
  result.errors.push(mapper(issue2));
2678
2825
  continue;
@@ -2704,8 +2851,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2704
2851
  }
2705
2852
  function toDotPath(_path) {
2706
2853
  const segs = [];
2707
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2708
- for (const seg of path4) {
2854
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2855
+ for (const seg of path6) {
2709
2856
  if (typeof seg === "number")
2710
2857
  segs.push(`[${seg}]`);
2711
2858
  else if (typeof seg === "symbol")
@@ -15397,13 +15544,13 @@ function resolveRef(ref, ctx) {
15397
15544
  if (!ref.startsWith("#")) {
15398
15545
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15399
15546
  }
15400
- const path4 = ref.slice(1).split("/").filter(Boolean);
15401
- if (path4.length === 0) {
15547
+ const path6 = ref.slice(1).split("/").filter(Boolean);
15548
+ if (path6.length === 0) {
15402
15549
  return ctx.rootSchema;
15403
15550
  }
15404
15551
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15405
- if (path4[0] === defsKey) {
15406
- const key = path4[1];
15552
+ if (path6[0] === defsKey) {
15553
+ const key = path6[1];
15407
15554
  if (!key || !ctx.defs[key]) {
15408
15555
  throw new Error(`Reference not found: ${ref}`);
15409
15556
  }
@@ -15882,6 +16029,248 @@ var authImportDataSchema = external_exports.object({
15882
16029
  origin: external_exports.string(),
15883
16030
  source: external_exports.literal("macos-keychain")
15884
16031
  });
16032
+ var planTierSchema = external_exports.enum(["free", "starter", "pro", "business", "unlimited"]);
16033
+ var billingStatusDataSchema = external_exports.object({
16034
+ origin: external_exports.string(),
16035
+ tier: planTierSchema,
16036
+ periodStart: external_exports.number().int(),
16037
+ periodEnd: external_exports.number().int(),
16038
+ storageBytes: external_exports.number().int().nonnegative(),
16039
+ storageCapBytes: external_exports.number().int().nonnegative().nullable(),
16040
+ minutesUsed: external_exports.number().nonnegative(),
16041
+ batchMinutesUsed: external_exports.number().nonnegative(),
16042
+ realtimeMinutesUsed: external_exports.number().nonnegative(),
16043
+ minutesCap: external_exports.number().nonnegative().nullable(),
16044
+ isOverStorage: external_exports.boolean(),
16045
+ isOverMinutes: external_exports.boolean()
16046
+ });
16047
+ var accountStatusDataSchema = external_exports.object({
16048
+ origin: external_exports.string(),
16049
+ loggedIn: external_exports.boolean(),
16050
+ email: external_exports.string().optional(),
16051
+ userId: external_exports.string().optional(),
16052
+ localStore: external_exports.object({
16053
+ path: external_exports.string(),
16054
+ accountScopedArtifacts: external_exports.number().int().nonnegative(),
16055
+ unattributedArtifacts: external_exports.number().int().nonnegative()
16056
+ }),
16057
+ billing: billingStatusDataSchema.optional()
16058
+ });
16059
+ var SIDECAR_PROTOCOL_VERSION = 1;
16060
+ var sidecarJsonRpcIdSchema = external_exports.union([external_exports.string(), external_exports.number().int()]);
16061
+ var sidecarCapabilitySchema = external_exports.enum([
16062
+ "recording.capture",
16063
+ "recording.upload",
16064
+ "live_captions.stream",
16065
+ "local_artifacts.index"
16066
+ ]);
16067
+ var sidecarAccountSchema = external_exports.object({
16068
+ backendOrigin: external_exports.string(),
16069
+ userId: external_exports.string(),
16070
+ email: external_exports.string().optional()
16071
+ });
16072
+ var sidecarClientInfoSchema = external_exports.object({
16073
+ name: external_exports.string(),
16074
+ version: external_exports.string()
16075
+ });
16076
+ var sidecarInfoSchema = external_exports.object({
16077
+ name: external_exports.string(),
16078
+ version: external_exports.string()
16079
+ });
16080
+ var sidecarRecordingOptionsSchema = external_exports.object({
16081
+ includeSystemAudio: external_exports.boolean().default(true),
16082
+ includeMicrophone: external_exports.boolean().default(true),
16083
+ liveCaptions: external_exports.boolean().default(false),
16084
+ translationLanguage: external_exports.string().optional(),
16085
+ transcriptionLanguage: external_exports.string().optional(),
16086
+ title: external_exports.string().optional()
16087
+ });
16088
+ var sidecarRecordingStateSchema = external_exports.enum([
16089
+ "idle",
16090
+ "starting",
16091
+ "recording",
16092
+ "stopping",
16093
+ "finalizing",
16094
+ "uploading",
16095
+ "completed",
16096
+ "failed",
16097
+ "cancelled"
16098
+ ]);
16099
+ var sidecarLocalArtifactKindSchema = external_exports.enum([
16100
+ "recording_session",
16101
+ "download",
16102
+ "live_caption_draft"
16103
+ ]);
16104
+ var sidecarLocalArtifactSchema = external_exports.object({
16105
+ kind: sidecarLocalArtifactKindSchema,
16106
+ localPath: external_exports.string(),
16107
+ remoteId: external_exports.string().optional(),
16108
+ metadata: external_exports.unknown().optional()
16109
+ });
16110
+ var sidecarHandshakeParamsSchema = external_exports.object({
16111
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16112
+ client: sidecarClientInfoSchema,
16113
+ account: sidecarAccountSchema.optional(),
16114
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16115
+ });
16116
+ var sidecarHandshakeResultSchema = external_exports.object({
16117
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16118
+ sidecar: sidecarInfoSchema,
16119
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16120
+ });
16121
+ var sidecarRecordingStartParamsSchema = external_exports.object({
16122
+ account: sidecarAccountSchema,
16123
+ options: sidecarRecordingOptionsSchema
16124
+ });
16125
+ var sidecarRecordingStartResultSchema = external_exports.object({
16126
+ sessionId: external_exports.string(),
16127
+ state: sidecarRecordingStateSchema,
16128
+ localSessionRef: external_exports.string().optional()
16129
+ });
16130
+ var sidecarSessionParamsSchema = external_exports.object({
16131
+ sessionId: external_exports.string()
16132
+ });
16133
+ var sidecarRecordingStopResultSchema = external_exports.object({
16134
+ sessionId: external_exports.string(),
16135
+ state: sidecarRecordingStateSchema,
16136
+ recordingId: external_exports.string().optional(),
16137
+ localSessionRef: external_exports.string().optional(),
16138
+ artifacts: external_exports.array(sidecarLocalArtifactSchema).optional()
16139
+ });
16140
+ var sidecarRecordingStatusResultSchema = external_exports.object({
16141
+ sessionId: external_exports.string(),
16142
+ state: sidecarRecordingStateSchema,
16143
+ recordingId: external_exports.string().optional(),
16144
+ localSessionRef: external_exports.string().optional()
16145
+ });
16146
+ var sidecarRequestSchema = external_exports.discriminatedUnion("method", [
16147
+ external_exports.object({
16148
+ jsonrpc: external_exports.literal("2.0"),
16149
+ id: sidecarJsonRpcIdSchema,
16150
+ method: external_exports.literal("recappi.handshake"),
16151
+ params: sidecarHandshakeParamsSchema
16152
+ }),
16153
+ external_exports.object({
16154
+ jsonrpc: external_exports.literal("2.0"),
16155
+ id: sidecarJsonRpcIdSchema,
16156
+ method: external_exports.literal("recappi.recording.start"),
16157
+ params: sidecarRecordingStartParamsSchema
16158
+ }),
16159
+ external_exports.object({
16160
+ jsonrpc: external_exports.literal("2.0"),
16161
+ id: sidecarJsonRpcIdSchema,
16162
+ method: external_exports.literal("recappi.recording.stop"),
16163
+ params: sidecarSessionParamsSchema
16164
+ }),
16165
+ external_exports.object({
16166
+ jsonrpc: external_exports.literal("2.0"),
16167
+ id: sidecarJsonRpcIdSchema,
16168
+ method: external_exports.literal("recappi.recording.cancel"),
16169
+ params: sidecarSessionParamsSchema
16170
+ }),
16171
+ external_exports.object({
16172
+ jsonrpc: external_exports.literal("2.0"),
16173
+ id: sidecarJsonRpcIdSchema,
16174
+ method: external_exports.literal("recappi.recording.status"),
16175
+ params: sidecarSessionParamsSchema
16176
+ })
16177
+ ]);
16178
+ var sidecarErrorSchema = external_exports.object({
16179
+ code: external_exports.number().int(),
16180
+ message: external_exports.string(),
16181
+ data: external_exports.unknown().optional()
16182
+ });
16183
+ var sidecarResponseSchema = external_exports.union([
16184
+ external_exports.object({
16185
+ jsonrpc: external_exports.literal("2.0"),
16186
+ id: sidecarJsonRpcIdSchema,
16187
+ result: external_exports.unknown()
16188
+ }),
16189
+ external_exports.object({
16190
+ jsonrpc: external_exports.literal("2.0"),
16191
+ id: sidecarJsonRpcIdSchema,
16192
+ error: sidecarErrorSchema
16193
+ })
16194
+ ]);
16195
+ var sidecarEventSchema = external_exports.discriminatedUnion("type", [
16196
+ external_exports.object({
16197
+ type: external_exports.literal("ready"),
16198
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16199
+ sidecar: sidecarInfoSchema
16200
+ }),
16201
+ external_exports.object({
16202
+ type: external_exports.literal("recording.state"),
16203
+ sessionId: external_exports.string(),
16204
+ state: sidecarRecordingStateSchema,
16205
+ recordingId: external_exports.string().optional(),
16206
+ localSessionRef: external_exports.string().optional(),
16207
+ message: external_exports.string().optional()
16208
+ }),
16209
+ external_exports.object({
16210
+ type: external_exports.literal("audio.level"),
16211
+ sessionId: external_exports.string(),
16212
+ input: external_exports.enum(["system", "microphone", "mixed"]),
16213
+ rmsDb: external_exports.number().optional(),
16214
+ peakDb: external_exports.number().optional(),
16215
+ at: external_exports.number().int().optional()
16216
+ }),
16217
+ external_exports.object({
16218
+ type: external_exports.literal("live_caption.delta"),
16219
+ sessionId: external_exports.string(),
16220
+ stream: external_exports.enum(["source", "translation"]),
16221
+ text: external_exports.string(),
16222
+ isFinal: external_exports.boolean().optional(),
16223
+ segmentId: external_exports.string().optional(),
16224
+ speaker: external_exports.string().optional(),
16225
+ language: external_exports.string().optional(),
16226
+ atMs: external_exports.number().int().nonnegative().optional(),
16227
+ startMs: external_exports.number().nonnegative().optional(),
16228
+ endMs: external_exports.number().nonnegative().optional()
16229
+ }),
16230
+ external_exports.object({
16231
+ type: external_exports.literal("local_artifact.upserted"),
16232
+ sessionId: external_exports.string().optional(),
16233
+ artifact: sidecarLocalArtifactSchema
16234
+ }),
16235
+ external_exports.object({
16236
+ type: external_exports.literal("error"),
16237
+ sessionId: external_exports.string().optional(),
16238
+ code: external_exports.string(),
16239
+ message: external_exports.string(),
16240
+ retryable: external_exports.boolean().optional()
16241
+ })
16242
+ ]);
16243
+ var sidecarNotificationSchema = external_exports.object({
16244
+ jsonrpc: external_exports.literal("2.0"),
16245
+ method: external_exports.literal("recappi.event"),
16246
+ params: sidecarEventSchema
16247
+ });
16248
+ var sidecarMessageSchema = external_exports.union([
16249
+ sidecarRequestSchema,
16250
+ sidecarResponseSchema,
16251
+ sidecarNotificationSchema
16252
+ ]);
16253
+ var recordCommandDataSchema = external_exports.object({
16254
+ origin: external_exports.string(),
16255
+ userId: external_exports.string(),
16256
+ live: external_exports.boolean(),
16257
+ sessionId: external_exports.string(),
16258
+ state: sidecarRecordingStateSchema,
16259
+ recordingId: external_exports.string().optional(),
16260
+ localSessionRef: external_exports.string().optional(),
16261
+ sidecar: sidecarInfoSchema.optional(),
16262
+ artifacts: external_exports.array(sidecarLocalArtifactSchema)
16263
+ });
16264
+ var audioCommandDataSchema = external_exports.object({
16265
+ origin: external_exports.string(),
16266
+ recordingId: external_exports.string(),
16267
+ localPath: external_exports.string(),
16268
+ action: external_exports.enum(["download", "open", "reveal"]),
16269
+ reused: external_exports.boolean(),
16270
+ artifactId: external_exports.number().int().positive().optional(),
16271
+ contentType: external_exports.string().optional(),
16272
+ contentLength: external_exports.number().int().nonnegative().optional()
16273
+ });
15885
16274
  var uploadSuccessSchema = external_exports.object({
15886
16275
  filePath: external_exports.string(),
15887
16276
  recordingId: external_exports.string(),
@@ -16488,8 +16877,8 @@ function isRecord(value) {
16488
16877
 
16489
16878
  // src/api.ts
16490
16879
  import { createWriteStream, promises as fs3 } from "fs";
16491
- import os3 from "os";
16492
- import path3 from "path";
16880
+ import os4 from "os";
16881
+ import path4 from "path";
16493
16882
  import { Readable } from "stream";
16494
16883
  import { pipeline } from "stream/promises";
16495
16884
 
@@ -16648,37 +17037,385 @@ async function readDurationMs(filePath, contentType) {
16648
17037
  );
16649
17038
  }
16650
17039
 
16651
- // src/api.ts
16652
- var RecappiApiClient = class {
16653
- constructor(auth, opts = {}) {
16654
- this.auth = auth;
16655
- this.fetchImpl = opts.fetchImpl ?? fetch;
16656
- this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16657
- this.env = opts.env ?? process.env;
17040
+ // src/store.ts
17041
+ import { mkdirSync } from "fs";
17042
+ import os3 from "os";
17043
+ import path3 from "path";
17044
+ import { createRequire } from "module";
17045
+ var require2 = createRequire(import.meta.url);
17046
+ var Database = require2("better-sqlite3");
17047
+ var CLI_STORE_SCHEMA_VERSION = 2;
17048
+ function defaultStorePath(homeDir = os3.homedir(), env = process.env) {
17049
+ const explicit = env.RECAPPI_CLI_STORE_PATH?.trim();
17050
+ if (explicit) return explicit;
17051
+ const dataHome = env.XDG_DATA_HOME?.trim() || path3.join(homeDir, ".local", "share");
17052
+ return path3.join(dataHome, "recappi", "cli-state.sqlite");
17053
+ }
17054
+ function requireAccountPartition(input) {
17055
+ const account = parseAccountPartition(input, "strict");
17056
+ if (!account) {
17057
+ throw cliError(
17058
+ "usage.invalid_argument",
17059
+ "Account partition requires a backend origin and user id.",
17060
+ {
17061
+ hint: "Resolve Recappi auth first, then use the normalized origin and authenticated user id."
17062
+ }
17063
+ );
16658
17064
  }
16659
- fetchImpl;
16660
- sleep;
16661
- env;
16662
- async authStatus() {
16663
- if (!this.auth.token) {
16664
- return { loggedIn: false, origin: this.auth.origin };
16665
- }
16666
- const response = await this.request("GET", "/api/auth/get-session", void 0, {
16667
- allowAuthFailure: true
17065
+ return account;
17066
+ }
17067
+ function openCliStore(opts = {}) {
17068
+ return new CliLocalStore(opts);
17069
+ }
17070
+ var CliLocalStore = class {
17071
+ db;
17072
+ now;
17073
+ constructor(opts = {}) {
17074
+ const dbPath = opts.dbPath ?? defaultStorePath(opts.homeDir, opts.env);
17075
+ if (!opts.readonly && dbPath !== ":memory:") {
17076
+ mkdirSync(path3.dirname(dbPath), { recursive: true, mode: 448 });
17077
+ }
17078
+ this.db = new Database(dbPath, opts.readonly === true ? { readonly: true } : void 0);
17079
+ this.now = opts.now ?? Date.now;
17080
+ if (!opts.readonly) this.migrate();
17081
+ }
17082
+ close() {
17083
+ this.db.close();
17084
+ }
17085
+ recordAccountSeen(account, email3) {
17086
+ const now = this.now();
17087
+ this.db.prepare(
17088
+ `
17089
+ INSERT INTO account_scopes (backend_origin, user_id, email, created_at, updated_at)
17090
+ VALUES (?, ?, ?, ?, ?)
17091
+ ON CONFLICT (backend_origin, user_id) DO UPDATE SET
17092
+ email = excluded.email,
17093
+ updated_at = excluded.updated_at
17094
+ `
17095
+ ).run(account.backendOrigin, account.userId, email3?.trim() || null, now, now);
17096
+ }
17097
+ addLocalArtifact(input) {
17098
+ const account = input.account ? requireAccountPartition(input.account) : null;
17099
+ const localPath = input.localPath.trim();
17100
+ if (!localPath) {
17101
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17102
+ }
17103
+ if (account) this.recordAccountSeen(account);
17104
+ const now = this.now();
17105
+ const result = this.db.prepare(
17106
+ `
17107
+ INSERT INTO local_artifacts (
17108
+ kind,
17109
+ backend_origin,
17110
+ user_id,
17111
+ remote_id,
17112
+ local_path,
17113
+ metadata_json,
17114
+ created_at,
17115
+ updated_at,
17116
+ last_opened_at
17117
+ )
17118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
17119
+ `
17120
+ ).run(
17121
+ input.kind,
17122
+ account?.backendOrigin ?? null,
17123
+ account?.userId ?? null,
17124
+ input.remoteId?.trim() || null,
17125
+ localPath,
17126
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17127
+ now,
17128
+ now,
17129
+ input.lastOpenedAt ?? null
17130
+ );
17131
+ return this.getLocalArtifact(Number(result.lastInsertRowid));
17132
+ }
17133
+ upsertLocalArtifact(input) {
17134
+ const remoteId = input.remoteId?.trim();
17135
+ if (!remoteId) return this.addLocalArtifact(input);
17136
+ const account = input.account ? requireAccountPartition(input.account) : null;
17137
+ if (account) this.recordAccountSeen(account);
17138
+ const existing = this.findLocalArtifact({
17139
+ account,
17140
+ kind: input.kind,
17141
+ remoteId
16668
17142
  });
16669
- if (response.status === 401 || response.status === 403) {
16670
- return { loggedIn: false, origin: this.auth.origin };
16671
- }
16672
- const body = await parseJson(response);
16673
- const user = isRecord2(body) && isRecord2(body.user) ? body.user : void 0;
16674
- return {
16675
- loggedIn: Boolean(user),
16676
- origin: this.auth.origin,
16677
- ...typeof user?.email === "string" ? { email: user.email } : {},
16678
- ...typeof user?.id === "string" ? { userId: user.id } : {}
16679
- };
17143
+ if (!existing) return this.addLocalArtifact({ ...input, remoteId });
17144
+ const localPath = input.localPath.trim();
17145
+ if (!localPath) {
17146
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17147
+ }
17148
+ const now = this.now();
17149
+ this.db.prepare(
17150
+ `
17151
+ UPDATE local_artifacts
17152
+ SET local_path = ?,
17153
+ metadata_json = ?,
17154
+ updated_at = ?,
17155
+ last_opened_at = COALESCE(?, last_opened_at)
17156
+ WHERE id = ?
17157
+ `
17158
+ ).run(
17159
+ localPath,
17160
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17161
+ now,
17162
+ input.lastOpenedAt ?? null,
17163
+ existing.id
17164
+ );
17165
+ return this.getLocalArtifact(existing.id);
17166
+ }
17167
+ getLocalArtifact(id) {
17168
+ const row = this.db.prepare("SELECT * FROM local_artifacts WHERE id = ?").get(id);
17169
+ if (!row) {
17170
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17171
+ }
17172
+ return mapArtifactRow(row);
17173
+ }
17174
+ listLocalArtifactsForAccount(accountInput, opts = {}) {
17175
+ const account = requireAccountPartition(accountInput);
17176
+ const params = [account.backendOrigin, account.userId];
17177
+ let source = `
17178
+ SELECT * FROM local_artifacts
17179
+ WHERE backend_origin = ? AND user_id = ?
17180
+ `;
17181
+ if (opts.kind) {
17182
+ source += " AND kind = ?";
17183
+ params.push(opts.kind);
17184
+ }
17185
+ if (opts.remoteId) {
17186
+ source += " AND remote_id = ?";
17187
+ params.push(opts.remoteId);
17188
+ }
17189
+ source += " ORDER BY updated_at DESC, id DESC";
17190
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17191
+ }
17192
+ listUnattributedLocalArtifacts(opts = {}) {
17193
+ const params = [];
17194
+ let source = `
17195
+ SELECT * FROM local_artifacts
17196
+ WHERE backend_origin IS NULL AND user_id IS NULL
17197
+ `;
17198
+ if (opts.kind) {
17199
+ source += " AND kind = ?";
17200
+ params.push(opts.kind);
17201
+ }
17202
+ if (opts.remoteId) {
17203
+ source += " AND remote_id = ?";
17204
+ params.push(opts.remoteId);
17205
+ }
17206
+ source += " ORDER BY updated_at DESC, id DESC";
17207
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17208
+ }
17209
+ findLocalArtifactForAccount(accountInput, opts) {
17210
+ const account = requireAccountPartition(accountInput);
17211
+ return this.findLocalArtifact({ account, kind: opts.kind, remoteId: opts.remoteId });
17212
+ }
17213
+ listDownloadedRecordingIdsForAccount(accountInput) {
17214
+ return new Set(
17215
+ this.listLocalArtifactsForAccount(accountInput, { kind: "download" }).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
17216
+ );
16680
17217
  }
16681
- async doctor() {
17218
+ markLocalArtifactOpened(id) {
17219
+ const now = this.now();
17220
+ const result = this.db.prepare(
17221
+ `
17222
+ UPDATE local_artifacts
17223
+ SET last_opened_at = ?, updated_at = ?
17224
+ WHERE id = ?
17225
+ `
17226
+ ).run(now, now, id);
17227
+ if (result.changes !== 1) {
17228
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17229
+ }
17230
+ return this.getLocalArtifact(id);
17231
+ }
17232
+ claimUnattributedLocalArtifact(id, accountInput) {
17233
+ const account = requireAccountPartition(accountInput);
17234
+ this.recordAccountSeen(account);
17235
+ const result = this.db.prepare(
17236
+ `
17237
+ UPDATE local_artifacts
17238
+ SET backend_origin = ?, user_id = ?, updated_at = ?
17239
+ WHERE id = ? AND backend_origin IS NULL AND user_id IS NULL
17240
+ `
17241
+ ).run(account.backendOrigin, account.userId, this.now(), id);
17242
+ return result.changes === 1;
17243
+ }
17244
+ migrate() {
17245
+ this.db.pragma("journal_mode = WAL");
17246
+ this.db.pragma("foreign_keys = ON");
17247
+ this.db.exec(`
17248
+ CREATE TABLE IF NOT EXISTS schema_meta (
17249
+ key TEXT PRIMARY KEY NOT NULL,
17250
+ value TEXT NOT NULL
17251
+ );
17252
+
17253
+ INSERT INTO schema_meta (key, value)
17254
+ VALUES ('schema_version', '${CLI_STORE_SCHEMA_VERSION}')
17255
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value;
17256
+
17257
+ CREATE TABLE IF NOT EXISTS account_scopes (
17258
+ backend_origin TEXT NOT NULL,
17259
+ user_id TEXT NOT NULL,
17260
+ email TEXT,
17261
+ created_at INTEGER NOT NULL,
17262
+ updated_at INTEGER NOT NULL,
17263
+ PRIMARY KEY (backend_origin, user_id)
17264
+ );
17265
+
17266
+ CREATE TABLE IF NOT EXISTS local_artifacts (
17267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17268
+ kind TEXT NOT NULL,
17269
+ backend_origin TEXT,
17270
+ user_id TEXT,
17271
+ remote_id TEXT,
17272
+ local_path TEXT NOT NULL,
17273
+ metadata_json TEXT,
17274
+ created_at INTEGER NOT NULL,
17275
+ updated_at INTEGER NOT NULL,
17276
+ last_opened_at INTEGER,
17277
+ CHECK (
17278
+ (backend_origin IS NULL AND user_id IS NULL)
17279
+ OR (backend_origin IS NOT NULL AND user_id IS NOT NULL)
17280
+ )
17281
+ );
17282
+
17283
+ CREATE INDEX IF NOT EXISTS local_artifacts_account_kind_idx
17284
+ ON local_artifacts (backend_origin, user_id, kind, updated_at DESC);
17285
+
17286
+ CREATE INDEX IF NOT EXISTS local_artifacts_remote_idx
17287
+ ON local_artifacts (backend_origin, user_id, kind, remote_id);
17288
+ `);
17289
+ if (!hasColumn(this.db, "local_artifacts", "last_opened_at")) {
17290
+ this.db.exec("ALTER TABLE local_artifacts ADD COLUMN last_opened_at INTEGER");
17291
+ }
17292
+ }
17293
+ findLocalArtifact({
17294
+ account,
17295
+ kind,
17296
+ remoteId
17297
+ }) {
17298
+ const row = account ? this.db.prepare(
17299
+ `
17300
+ SELECT * FROM local_artifacts
17301
+ WHERE backend_origin = ? AND user_id = ? AND kind = ? AND remote_id = ?
17302
+ ORDER BY updated_at DESC, id DESC
17303
+ LIMIT 1
17304
+ `
17305
+ ).get(account.backendOrigin, account.userId, kind, remoteId) : this.db.prepare(
17306
+ `
17307
+ SELECT * FROM local_artifacts
17308
+ WHERE backend_origin IS NULL AND user_id IS NULL AND kind = ? AND remote_id = ?
17309
+ ORDER BY updated_at DESC, id DESC
17310
+ LIMIT 1
17311
+ `
17312
+ ).get(kind, remoteId);
17313
+ return row ? mapArtifactRow(row) : null;
17314
+ }
17315
+ };
17316
+ function parseAccountPartition(input, mode) {
17317
+ const rawOrigin = cleanString(input?.backendOrigin);
17318
+ const rawUserId = cleanString(input?.userId);
17319
+ if (!rawOrigin && !rawUserId) return null;
17320
+ if (!rawOrigin || !rawUserId) {
17321
+ if (mode === "strict") {
17322
+ throw cliError(
17323
+ "usage.invalid_argument",
17324
+ "Account stamp must include both backend origin and user id.",
17325
+ {
17326
+ hint: "Partial account stamps are treated as unattributed when reading legacy local state."
17327
+ }
17328
+ );
17329
+ }
17330
+ return null;
17331
+ }
17332
+ try {
17333
+ return { backendOrigin: validateOrigin(rawOrigin), userId: rawUserId };
17334
+ } catch (error51) {
17335
+ if (mode === "strict") throw error51;
17336
+ return null;
17337
+ }
17338
+ }
17339
+ function cleanString(value) {
17340
+ const trimmed = value?.trim();
17341
+ return trimmed ? trimmed : null;
17342
+ }
17343
+ function mapArtifactRow(row) {
17344
+ const backendOrigin = stringOrNull(row.backend_origin);
17345
+ const userId = stringOrNull(row.user_id);
17346
+ const lastOpenedAt = numberOrNull(row.last_opened_at);
17347
+ return {
17348
+ id: numberValue(row.id),
17349
+ kind: localArtifactKind(row.kind),
17350
+ account: backendOrigin && userId ? { backendOrigin, userId } : null,
17351
+ localPath: stringValue(row.local_path),
17352
+ ...stringOrNull(row.remote_id) ? { remoteId: stringOrNull(row.remote_id) ?? void 0 } : {},
17353
+ ...typeof row.metadata_json === "string" ? { metadata: JSON.parse(row.metadata_json) } : {},
17354
+ createdAt: numberValue(row.created_at),
17355
+ updatedAt: numberValue(row.updated_at),
17356
+ ...lastOpenedAt ? { lastOpenedAt } : {}
17357
+ };
17358
+ }
17359
+ function hasColumn(db, table, column) {
17360
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
17361
+ }
17362
+ function localArtifactKind(value) {
17363
+ if (value === "recording_session" || value === "download" || value === "live_caption_draft") {
17364
+ return value;
17365
+ }
17366
+ throw cliError("cloud.invalid_response", "CLI store contains an unknown artifact kind.");
17367
+ }
17368
+ function stringValue(value) {
17369
+ if (typeof value === "string") return value;
17370
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid string value.");
17371
+ }
17372
+ function stringOrNull(value) {
17373
+ return typeof value === "string" ? value : null;
17374
+ }
17375
+ function numberValue(value) {
17376
+ if (typeof value === "number") return value;
17377
+ if (typeof value === "bigint") return Number(value);
17378
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid number value.");
17379
+ }
17380
+ function numberOrNull(value) {
17381
+ if (typeof value === "number") return value;
17382
+ if (typeof value === "bigint") return Number(value);
17383
+ return null;
17384
+ }
17385
+
17386
+ // src/api.ts
17387
+ var RecappiApiClient = class {
17388
+ constructor(auth, opts = {}) {
17389
+ this.auth = auth;
17390
+ this.fetchImpl = opts.fetchImpl ?? fetch;
17391
+ this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
17392
+ this.env = opts.env ?? process.env;
17393
+ this.homeDir = opts.homeDir;
17394
+ }
17395
+ fetchImpl;
17396
+ sleep;
17397
+ env;
17398
+ homeDir;
17399
+ async authStatus() {
17400
+ if (!this.auth.token) {
17401
+ return { loggedIn: false, origin: this.auth.origin };
17402
+ }
17403
+ const response = await this.request("GET", "/api/auth/get-session", void 0, {
17404
+ allowAuthFailure: true
17405
+ });
17406
+ if (response.status === 401 || response.status === 403) {
17407
+ return { loggedIn: false, origin: this.auth.origin };
17408
+ }
17409
+ const body = await parseJson(response);
17410
+ const user = isRecord2(body) && isRecord2(body.user) ? body.user : void 0;
17411
+ return {
17412
+ loggedIn: Boolean(user),
17413
+ origin: this.auth.origin,
17414
+ ...typeof user?.email === "string" ? { email: user.email } : {},
17415
+ ...typeof user?.id === "string" ? { userId: user.id } : {}
17416
+ };
17417
+ }
17418
+ async doctor() {
16682
17419
  const checks = [];
16683
17420
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
16684
17421
  checks.push({
@@ -16806,9 +17543,9 @@ var RecappiApiClient = class {
16806
17543
  }
16807
17544
  const contentType = normalizeContentType(response.headers.get("content-type"));
16808
17545
  const contentLength = numberHeader(response.headers.get("content-length"));
16809
- const dir = opts.directory ?? await fs3.mkdtemp(path3.join(os3.tmpdir(), "recappi-cli-audio-"));
17546
+ const dir = opts.directory ?? await fs3.mkdtemp(path4.join(os4.tmpdir(), "recappi-cli-audio-"));
16810
17547
  if (opts.directory) await fs3.mkdir(dir, { recursive: true });
16811
- const filePath = path3.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
17548
+ const filePath = path4.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
16812
17549
  try {
16813
17550
  await pipeline(
16814
17551
  Readable.fromWeb(response.body),
@@ -16830,6 +17567,47 @@ var RecappiApiClient = class {
16830
17567
  const parsed = await this.getJson("/api/dashboard/stats");
16831
17568
  return mapDashboardStats(parsed, this.auth.origin);
16832
17569
  }
17570
+ async billingStatus() {
17571
+ const parsed = await this.getJson("/api/billing/status");
17572
+ return mapBillingStatus(parsed, this.auth.origin);
17573
+ }
17574
+ async accountStatus() {
17575
+ const status = await this.authStatus();
17576
+ const storePath = defaultStorePath(this.homeDir, this.env);
17577
+ let accountScopedArtifacts = 0;
17578
+ let unattributedArtifacts = 0;
17579
+ if (status.loggedIn && status.userId) {
17580
+ const store = openCliStore({
17581
+ dbPath: storePath,
17582
+ env: this.env,
17583
+ homeDir: this.homeDir
17584
+ });
17585
+ try {
17586
+ const account = requireAccountPartition({
17587
+ backendOrigin: this.auth.origin,
17588
+ userId: status.userId
17589
+ });
17590
+ store.recordAccountSeen(account, status.email);
17591
+ accountScopedArtifacts = store.listLocalArtifactsForAccount(account).length;
17592
+ unattributedArtifacts = store.listUnattributedLocalArtifacts().length;
17593
+ } finally {
17594
+ store.close();
17595
+ }
17596
+ }
17597
+ const billing = status.loggedIn ? await this.billingStatus() : void 0;
17598
+ return accountStatusDataSchema.parse({
17599
+ origin: this.auth.origin,
17600
+ loggedIn: status.loggedIn,
17601
+ ...status.email ? { email: status.email } : {},
17602
+ ...status.userId ? { userId: status.userId } : {},
17603
+ localStore: {
17604
+ path: storePath,
17605
+ accountScopedArtifacts,
17606
+ unattributedArtifacts
17607
+ },
17608
+ ...billing ? { billing } : {}
17609
+ });
17610
+ }
16833
17611
  async uploadPathBatch(opts) {
16834
17612
  const files = await collectAudioFiles(opts.inputPath);
16835
17613
  if (files.length === 0) {
@@ -17010,12 +17788,12 @@ var RecappiApiClient = class {
17010
17788
  ...typeof parsed.language === "string" || parsed.language === null ? { language: parsed.language } : {}
17011
17789
  };
17012
17790
  }
17013
- async getJson(path4) {
17014
- const response = await this.request("GET", path4);
17791
+ async getJson(path6) {
17792
+ const response = await this.request("GET", path6);
17015
17793
  return await parseJson(response);
17016
17794
  }
17017
- async postJson(path4, body) {
17018
- const response = await this.request("POST", path4, JSON.stringify(body), {
17795
+ async postJson(path6, body) {
17796
+ const response = await this.request("POST", path6, JSON.stringify(body), {
17019
17797
  headers: { "content-type": "application/json" }
17020
17798
  });
17021
17799
  return await parseJson(response);
@@ -17237,9 +18015,9 @@ function parseSummary(row) {
17237
18015
  function mapJobListItem(row) {
17238
18016
  const recording = isRecord2(row.recording) ? row.recording : {};
17239
18017
  return {
17240
- jobId: stringValue(row.jobId) ?? stringValue(row.id) ?? "",
17241
- recordingId: stringValue(row.recordingId) ?? "",
17242
- status: stringValue(row.status) ?? "queued",
18018
+ jobId: stringValue2(row.jobId) ?? stringValue2(row.id) ?? "",
18019
+ recordingId: stringValue2(row.recordingId) ?? "",
18020
+ status: stringValue2(row.status) ?? "queued",
17243
18021
  ...typeof row.provider === "string" ? { provider: row.provider } : {},
17244
18022
  ...typeof row.model === "string" ? { model: row.model } : {},
17245
18023
  ...typeof row.language === "string" || row.language === null ? { language: row.language } : {},
@@ -17258,10 +18036,10 @@ function mapJobListItem(row) {
17258
18036
  };
17259
18037
  }
17260
18038
  function mapRecording(row, origin) {
17261
- const recordingId = stringValue(row.id) ?? stringValue(row.recordingId);
17262
- const status = stringValue(row.status);
17263
- const createdAt = numberValue(row.createdAt);
17264
- const updatedAt = numberValue(row.updatedAt);
18039
+ const recordingId = stringValue2(row.id) ?? stringValue2(row.recordingId);
18040
+ const status = stringValue2(row.status);
18041
+ const createdAt = numberValue2(row.createdAt);
18042
+ const updatedAt = numberValue2(row.updatedAt);
17265
18043
  if (!recordingId) {
17266
18044
  throw cliError("cloud.invalid_response", "Recording response was missing id.");
17267
18045
  }
@@ -17300,16 +18078,37 @@ function mapDashboardStats(row, origin) {
17300
18078
  jobs: mapCountObject(row.jobs, ["active", "queued", "running", "succeeded", "failed"])
17301
18079
  });
17302
18080
  }
18081
+ function mapBillingStatus(row, origin) {
18082
+ return billingStatusDataSchema.parse({
18083
+ origin,
18084
+ tier: row.tier,
18085
+ periodStart: numberValue2(row.periodStart),
18086
+ periodEnd: numberValue2(row.periodEnd),
18087
+ storageBytes: numberValue2(row.storageBytes) ?? 0,
18088
+ storageCapBytes: nullableCap(row.storageCapBytes),
18089
+ minutesUsed: numberValue2(row.minutesUsed) ?? 0,
18090
+ batchMinutesUsed: numberValue2(row.batchMinutesUsed) ?? 0,
18091
+ realtimeMinutesUsed: numberValue2(row.realtimeMinutesUsed) ?? 0,
18092
+ minutesCap: nullableCap(row.minutesCap),
18093
+ isOverStorage: row.isOverStorage === true,
18094
+ isOverMinutes: row.isOverMinutes === true
18095
+ });
18096
+ }
17303
18097
  function mapCountObject(value, keys) {
17304
18098
  const source = isRecord2(value) ? value : {};
17305
- return Object.fromEntries(keys.map((key) => [key, numberValue(source[key]) ?? 0]));
18099
+ return Object.fromEntries(keys.map((key) => [key, numberValue2(source[key]) ?? 0]));
17306
18100
  }
17307
- function stringValue(value) {
18101
+ function stringValue2(value) {
17308
18102
  return typeof value === "string" ? value : void 0;
17309
18103
  }
17310
- function numberValue(value) {
18104
+ function numberValue2(value) {
17311
18105
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
17312
18106
  }
18107
+ function nullableCap(value) {
18108
+ if (value === null) return null;
18109
+ const number4 = numberValue2(value);
18110
+ return number4 === void 0 ? null : number4;
18111
+ }
17313
18112
  function parseSummaryStatus(value) {
17314
18113
  const allowed = /* @__PURE__ */ new Set([
17315
18114
  "pending",
@@ -17344,11 +18143,37 @@ function decodeJsonRecord(value) {
17344
18143
 
17345
18144
  // src/audio.ts
17346
18145
  import { spawn } from "child_process";
18146
+ import { promises as fs4 } from "fs";
18147
+ import path5 from "path";
17347
18148
  function createRecordingAudioRuntime(client, deps = {}) {
18149
+ const downloadRecordingAudioFile = async (recordingId, opts) => {
18150
+ const cached2 = await findReusableDownload(recordingId, deps);
18151
+ if (cached2) return cached2;
18152
+ const directory = opts?.directory ?? (deps.account ? defaultDownloadDirectory(deps) : void 0);
18153
+ const download = await client.downloadRecordingAudio(recordingId, {
18154
+ ...opts,
18155
+ ...directory ? { directory } : {}
18156
+ });
18157
+ const artifact = await rememberDownload(download, deps);
18158
+ return {
18159
+ recordingId: download.recordingId,
18160
+ localPath: download.localPath,
18161
+ reused: false,
18162
+ ...artifact ? { artifactId: artifact.id } : {},
18163
+ contentType: download.contentType,
18164
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18165
+ origin: download.origin
18166
+ };
18167
+ };
17348
18168
  return {
17349
- downloadRecordingAudio: async (recordingId, opts) => (await client.downloadRecordingAudio(recordingId, opts)).localPath,
18169
+ downloadRecordingAudio: async (recordingId, opts) => (await downloadRecordingAudioFile(recordingId, opts)).localPath,
18170
+ downloadRecordingAudioFile,
17350
18171
  openPath: (localPath) => openPath(localPath, deps),
17351
- revealInFinder: (localPath) => revealInFinder(localPath, deps)
18172
+ revealInFinder: (localPath) => revealInFinder(localPath, deps),
18173
+ listDownloads: () => listExistingDownloads(deps),
18174
+ listDownloadedRecordingIds: async () => new Set(
18175
+ (await listExistingDownloads(deps)).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
18176
+ )
17352
18177
  };
17353
18178
  }
17354
18179
  function openPath(localPath, deps = {}) {
@@ -17357,6 +18182,80 @@ function openPath(localPath, deps = {}) {
17357
18182
  function revealInFinder(localPath, deps = {}) {
17358
18183
  return runMacOpen(["-R", localPath], deps);
17359
18184
  }
18185
+ async function findReusableDownload(recordingId, deps) {
18186
+ return withStore(deps, async (store, account) => {
18187
+ if (!account) return null;
18188
+ const artifact = store.findLocalArtifactForAccount(account, {
18189
+ kind: "download",
18190
+ remoteId: recordingId
18191
+ });
18192
+ if (!artifact || !await isReadableFile(artifact.localPath)) return null;
18193
+ const opened = store.markLocalArtifactOpened(artifact.id);
18194
+ return artifactToDownload(opened, recordingId);
18195
+ });
18196
+ }
18197
+ async function rememberDownload(download, deps) {
18198
+ return withStore(deps, (store, account) => {
18199
+ if (!account) return null;
18200
+ const artifact = store.upsertLocalArtifact({
18201
+ kind: "download",
18202
+ account,
18203
+ remoteId: download.recordingId,
18204
+ localPath: download.localPath,
18205
+ metadata: {
18206
+ resource: "recording_audio",
18207
+ contentType: download.contentType,
18208
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18209
+ origin: download.origin
18210
+ }
18211
+ });
18212
+ return store.markLocalArtifactOpened(artifact.id);
18213
+ });
18214
+ }
18215
+ async function listExistingDownloads(deps) {
18216
+ const artifacts = await withStore(
18217
+ deps,
18218
+ (store, account) => account ? store.listLocalArtifactsForAccount(account, { kind: "download" }) : []
18219
+ );
18220
+ const existing = [];
18221
+ for (const artifact of artifacts) {
18222
+ if (await isReadableFile(artifact.localPath)) existing.push(artifact);
18223
+ }
18224
+ return existing;
18225
+ }
18226
+ async function withStore(deps, run) {
18227
+ const store = deps.store ?? openCliStore({ homeDir: deps.homeDir, env: deps.env });
18228
+ try {
18229
+ return await run(store, deps.account ?? null);
18230
+ } finally {
18231
+ if (!deps.store) store.close();
18232
+ }
18233
+ }
18234
+ function defaultDownloadDirectory(deps) {
18235
+ return path5.join(path5.dirname(defaultStorePath(deps.homeDir, deps.env)), "downloads");
18236
+ }
18237
+ async function isReadableFile(localPath) {
18238
+ try {
18239
+ return (await fs4.stat(localPath)).isFile();
18240
+ } catch {
18241
+ return false;
18242
+ }
18243
+ }
18244
+ function artifactToDownload(artifact, recordingId) {
18245
+ const metadata = isRecord3(artifact.metadata) ? artifact.metadata : {};
18246
+ return {
18247
+ recordingId,
18248
+ localPath: artifact.localPath,
18249
+ reused: true,
18250
+ artifactId: artifact.id,
18251
+ ...typeof metadata.contentType === "string" ? { contentType: metadata.contentType } : {},
18252
+ ...typeof metadata.contentLength === "number" ? { contentLength: metadata.contentLength } : {},
18253
+ ...typeof metadata.origin === "string" ? { origin: metadata.origin } : {}
18254
+ };
18255
+ }
18256
+ function isRecord3(value) {
18257
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18258
+ }
17360
18259
  function runMacOpen(args, deps) {
17361
18260
  if ((deps.platform ?? process.platform) !== "darwin") {
17362
18261
  return Promise.reject(
@@ -17478,20 +18377,20 @@ function renderEnvelope(envelope, opts) {
17478
18377
  `);
17479
18378
  }
17480
18379
  function renderHumanSuccess(command, data, opts) {
17481
- if (command === "auth login" && isRecord3(data)) {
18380
+ if (command === "auth login" && isRecord4(data)) {
17482
18381
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
17483
18382
  `);
17484
18383
  return;
17485
18384
  }
17486
- if (command === "auth logout" && isRecord3(data)) {
18385
+ if (command === "auth logout" && isRecord4(data)) {
17487
18386
  opts.stdout(data.cleared ? "Signed out of Recappi CLI\n" : "No Recappi CLI session to clear\n");
17488
18387
  return;
17489
18388
  }
17490
- if (command === "auth import-macos" && isRecord3(data)) {
18389
+ if (command === "auth import-macos" && isRecord4(data)) {
17491
18390
  opts.stdout("Imported the Recappi Mini app session into Recappi CLI\n");
17492
18391
  return;
17493
18392
  }
17494
- if (command === "auth status" && isRecord3(data)) {
18393
+ if (command === "auth status" && isRecord4(data)) {
17495
18394
  if (data.loggedIn) {
17496
18395
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
17497
18396
  `);
@@ -17500,17 +18399,50 @@ function renderHumanSuccess(command, data, opts) {
17500
18399
  opts.stdout("Not logged in\n");
17501
18400
  return;
17502
18401
  }
17503
- if (command === "version" && isRecord3(data) && typeof data.version === "string") {
18402
+ if (command === "account status" && isRecord4(data)) {
18403
+ if (!data.loggedIn) {
18404
+ opts.stdout("Not logged in\n");
18405
+ return;
18406
+ }
18407
+ opts.stdout(`Account: ${typeof data.email === "string" ? data.email : "signed in"}
18408
+ `);
18409
+ if (typeof data.origin === "string") opts.stdout(` origin: ${data.origin}
18410
+ `);
18411
+ if (typeof data.userId === "string") opts.stdout(` userId: ${data.userId}
18412
+ `);
18413
+ const billing = isRecord4(data.billing) ? data.billing : {};
18414
+ if (typeof billing.tier === "string") opts.stdout(` plan: ${billing.tier}
18415
+ `);
18416
+ if (typeof billing.minutesUsed === "number") {
18417
+ const cap = formatNullableCap(billing.minutesCap, "minutes");
18418
+ opts.stdout(` minutes: ${billing.minutesUsed} / ${cap}
18419
+ `);
18420
+ }
18421
+ if (typeof billing.storageBytes === "number") {
18422
+ const cap = formatNullableCap(billing.storageCapBytes, "bytes");
18423
+ opts.stdout(` storage: ${formatBytes(billing.storageBytes)} / ${cap}
18424
+ `);
18425
+ }
18426
+ const localStore = isRecord4(data.localStore) ? data.localStore : {};
18427
+ if (typeof localStore.path === "string") opts.stdout(` localStore: ${localStore.path}
18428
+ `);
18429
+ opts.stdout(
18430
+ ` localArtifacts: ${numberText(localStore.accountScopedArtifacts)} current, ${numberText(localStore.unattributedArtifacts)} unattributed
18431
+ `
18432
+ );
18433
+ return;
18434
+ }
18435
+ if (command === "version" && isRecord4(data) && typeof data.version === "string") {
17504
18436
  opts.stdout(`${data.version}
17505
18437
  `);
17506
18438
  return;
17507
18439
  }
17508
- if (command === "doctor" && isRecord3(data) && Array.isArray(data.checks)) {
18440
+ if (command === "doctor" && isRecord4(data) && Array.isArray(data.checks)) {
17509
18441
  const status = typeof data.status === "string" ? data.status : "unknown";
17510
18442
  opts.stdout(`Doctor: ${status}
17511
18443
  `);
17512
18444
  for (const check2 of data.checks) {
17513
- if (!isRecord3(check2)) continue;
18445
+ if (!isRecord4(check2)) continue;
17514
18446
  const checkStatus = typeof check2.status === "string" ? check2.status : "unknown";
17515
18447
  const name = typeof check2.name === "string" ? check2.name : "check";
17516
18448
  const message = typeof check2.message === "string" ? ` \u2014 ${check2.message}` : "";
@@ -17521,14 +18453,14 @@ function renderHumanSuccess(command, data, opts) {
17521
18453
  }
17522
18454
  return;
17523
18455
  }
17524
- if (command === "transcript get" && isRecord3(data)) {
18456
+ if (command === "transcript get" && isRecord4(data)) {
17525
18457
  renderTranscriptHuman(data, opts);
17526
18458
  return;
17527
18459
  }
17528
- if (command === "recordings list" && isRecord3(data) && Array.isArray(data.items)) {
18460
+ if (command === "recordings list" && isRecord4(data) && Array.isArray(data.items)) {
17529
18461
  opts.stdout("Recordings:\n");
17530
18462
  for (const item of data.items) {
17531
- if (!isRecord3(item)) continue;
18463
+ if (!isRecord4(item)) continue;
17532
18464
  opts.stdout(` ${recordingLabel(item)}
17533
18465
  `);
17534
18466
  }
@@ -17540,7 +18472,7 @@ Next cursor: ${data.nextCursor}
17540
18472
  }
17541
18473
  return;
17542
18474
  }
17543
- if (command === "recordings get" && isRecord3(data)) {
18475
+ if (command === "recordings get" && isRecord4(data)) {
17544
18476
  opts.stdout(`${recordingTitle(data)}
17545
18477
  `);
17546
18478
  opts.stdout(` recordingId: ${String(data.recordingId)}
@@ -17562,9 +18494,9 @@ Next:
17562
18494
  }
17563
18495
  return;
17564
18496
  }
17565
- if (command === "dashboard stats" && isRecord3(data)) {
17566
- const recordings = isRecord3(data.recordings) ? data.recordings : {};
17567
- const jobs = isRecord3(data.jobs) ? data.jobs : {};
18497
+ if (command === "dashboard stats" && isRecord4(data)) {
18498
+ const recordings = isRecord4(data.recordings) ? data.recordings : {};
18499
+ const jobs = isRecord4(data.jobs) ? data.jobs : {};
17568
18500
  opts.stdout(
17569
18501
  `Recordings: ${numberText(recordings.total)} total, ${numberText(recordings.ready)} ready
17570
18502
  `
@@ -17604,7 +18536,50 @@ Next:
17604
18536
  }
17605
18537
  return;
17606
18538
  }
17607
- if ((command === "jobs wait" || command === "upload") && isRecord3(data)) {
18539
+ if (command === "record" && isRecord4(data)) {
18540
+ opts.stdout("Recording complete\n");
18541
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18542
+ `);
18543
+ if (typeof data.sessionId === "string") opts.stdout(` sessionId: ${data.sessionId}
18544
+ `);
18545
+ if (typeof data.localSessionRef === "string") {
18546
+ opts.stdout(` localSessionRef: ${data.localSessionRef}
18547
+ `);
18548
+ }
18549
+ if (Array.isArray(data.artifacts) && data.artifacts.length > 0) {
18550
+ opts.stdout(" artifacts:\n");
18551
+ for (const artifact of data.artifacts) {
18552
+ if (!isRecord4(artifact)) continue;
18553
+ const kind = typeof artifact.kind === "string" ? artifact.kind : "artifact";
18554
+ const localPath = typeof artifact.localPath === "string" ? artifact.localPath : "";
18555
+ opts.stdout(` - ${kind}: ${localPath}
18556
+ `);
18557
+ }
18558
+ }
18559
+ if (typeof data.recordingId === "string") {
18560
+ opts.stdout(`
18561
+ Next:
18562
+ recappi recordings get ${data.recordingId}
18563
+ `);
18564
+ }
18565
+ return;
18566
+ }
18567
+ if (command === "audio" && isRecord4(data)) {
18568
+ const action = typeof data.action === "string" ? data.action : "download";
18569
+ opts.stdout(
18570
+ action === "open" ? "Audio opened\n" : action === "reveal" ? "Audio revealed\n" : "Audio ready\n"
18571
+ );
18572
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18573
+ `);
18574
+ if (typeof data.localPath === "string") opts.stdout(` localPath: ${data.localPath}
18575
+ `);
18576
+ if (typeof data.reused === "boolean") {
18577
+ opts.stdout(` source: ${data.reused ? "local cache" : "downloaded"}
18578
+ `);
18579
+ }
18580
+ return;
18581
+ }
18582
+ if ((command === "jobs wait" || command === "upload") && isRecord4(data)) {
17608
18583
  if (typeof data.transcriptId === "string") {
17609
18584
  opts.stdout("Transcription ready\n");
17610
18585
  opts.stdout(` transcriptId: ${data.transcriptId}
@@ -17625,10 +18600,10 @@ Next:
17625
18600
  }
17626
18601
  return;
17627
18602
  }
17628
- if (command === "schema" && isRecord3(data) && Array.isArray(data.commands)) {
18603
+ if (command === "schema" && isRecord4(data) && Array.isArray(data.commands)) {
17629
18604
  opts.stdout("Commands:\n");
17630
18605
  for (const entry of data.commands) {
17631
- if (!isRecord3(entry) || typeof entry.name !== "string") continue;
18606
+ if (!isRecord4(entry) || typeof entry.name !== "string") continue;
17632
18607
  const summary = typeof entry.summary === "string" ? ` \u2014 ${entry.summary}` : "";
17633
18608
  opts.stdout(` ${entry.name}${summary}
17634
18609
  `);
@@ -17718,7 +18693,7 @@ function renderTranscriptHuman(data, opts) {
17718
18693
  const segments = Array.isArray(data.segments) ? data.segments : [];
17719
18694
  let printedBody = false;
17720
18695
  for (const segment of segments) {
17721
- if (!isRecord3(segment) || typeof segment.text !== "string") continue;
18696
+ if (!isRecord4(segment) || typeof segment.text !== "string") continue;
17722
18697
  const clock = typeof segment.startMs === "number" ? `[${formatClock(segment.startMs / 1e3)}] ` : "";
17723
18698
  const speaker = typeof segment.speaker === "string" ? `${segment.speaker}: ` : "";
17724
18699
  opts.stdout(`${clock}${speaker}${segment.text}
@@ -17730,7 +18705,7 @@ function renderTranscriptHuman(data, opts) {
17730
18705
  `);
17731
18706
  printedBody = true;
17732
18707
  }
17733
- const summary = isRecord3(data.summary) ? data.summary : void 0;
18708
+ const summary = isRecord4(data.summary) ? data.summary : void 0;
17734
18709
  if (!summary || summary.status !== "succeeded") return;
17735
18710
  if (typeof summary.tldr === "string" && summary.tldr.length > 0) {
17736
18711
  opts.stdout(`
@@ -17748,7 +18723,7 @@ Summary:
17748
18723
  if (Array.isArray(summary.actionItems) && summary.actionItems.length > 0) {
17749
18724
  opts.stdout("\nAction items:\n");
17750
18725
  for (const item of summary.actionItems) {
17751
- if (!isRecord3(item) || typeof item.what !== "string") continue;
18726
+ if (!isRecord4(item) || typeof item.what !== "string") continue;
17752
18727
  const who = typeof item.who === "string" ? `${item.who}: ` : "";
17753
18728
  opts.stdout(` - ${who}${item.what}
17754
18729
  `);
@@ -17786,6 +18761,11 @@ function formatBytes(bytes) {
17786
18761
  }
17787
18762
  return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
17788
18763
  }
18764
+ function formatNullableCap(value, unit) {
18765
+ if (value === null || value === void 0) return "Unlimited";
18766
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Unlimited";
18767
+ return unit === "bytes" ? formatBytes(value) : String(value);
18768
+ }
17789
18769
  function formatClock(seconds) {
17790
18770
  const total = Math.max(0, Math.floor(seconds));
17791
18771
  const hours = Math.floor(total / 3600);
@@ -17814,7 +18794,7 @@ function applyFields(command, data, fields, compact) {
17814
18794
  };
17815
18795
  return compact ? compactData(filtered2) : filtered2;
17816
18796
  }
17817
- if (!isRecord3(data)) return data;
18797
+ if (!isRecord4(data)) return data;
17818
18798
  const allowed = new Set(Object.keys(data));
17819
18799
  assertKnownFields(fields, allowed);
17820
18800
  const filtered = pickFields(data, fields);
@@ -17839,7 +18819,7 @@ function compactData(value) {
17839
18819
  if (Array.isArray(value)) {
17840
18820
  return value.map(compactData).filter((item) => item !== void 0);
17841
18821
  }
17842
- if (isRecord3(value)) {
18822
+ if (isRecord4(value)) {
17843
18823
  const out = {};
17844
18824
  for (const [key, child] of Object.entries(value)) {
17845
18825
  const compacted = compactData(child);
@@ -17857,16 +18837,16 @@ function stableStringify(value, compact) {
17857
18837
  }
17858
18838
  function sortKeys(value) {
17859
18839
  if (Array.isArray(value)) return value.map(sortKeys);
17860
- if (!isRecord3(value)) return value;
18840
+ if (!isRecord4(value)) return value;
17861
18841
  return Object.fromEntries(
17862
18842
  Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
17863
18843
  );
17864
18844
  }
17865
- function isRecord3(value) {
18845
+ function isRecord4(value) {
17866
18846
  return typeof value === "object" && value !== null && !Array.isArray(value);
17867
18847
  }
17868
18848
  function isUploadBatch(value) {
17869
- return isRecord3(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
18849
+ return isRecord4(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
17870
18850
  }
17871
18851
 
17872
18852
  // src/schema.ts
@@ -17875,9 +18855,12 @@ var COMMAND_DATA_SCHEMAS = {
17875
18855
  "auth logout": authLogoutDataSchema,
17876
18856
  "auth import-macos": authImportDataSchema,
17877
18857
  "auth status": authStatusDataSchema,
18858
+ "account status": accountStatusDataSchema,
18859
+ audio: audioCommandDataSchema,
17878
18860
  doctor: doctorDataSchema,
17879
18861
  "dashboard stats": dashboardStatsDataSchema,
17880
18862
  upload: uploadBatchDataSchema,
18863
+ record: recordCommandDataSchema,
17881
18864
  "recordings get": recordingDataSchema,
17882
18865
  "recordings list": recordingListDataSchema,
17883
18866
  "jobs list": jobListDataSchema,
@@ -17914,11 +18897,11 @@ function buildSchemaDocument(program) {
17914
18897
  event: toJsonSchema(operationEventSchema)
17915
18898
  };
17916
18899
  }
17917
- function walkCommands(command, path4, out) {
18900
+ function walkCommands(command, path6, out) {
17918
18901
  for (const sub of subcommandsOf(command)) {
17919
18902
  const name = sub.name();
17920
18903
  if (name === "help") continue;
17921
- const fullPath = [...path4, name];
18904
+ const fullPath = [...path6, name];
17922
18905
  const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
17923
18906
  if (children.length === 0) {
17924
18907
  out.push(leafCommandDoc(sub, fullPath.join(" ")));
@@ -17977,6 +18960,569 @@ function readCliVersion() {
17977
18960
  }
17978
18961
  var CLI_VERSION = readCliVersion();
17979
18962
 
18963
+ // src/record.tsx
18964
+ import { render, useInput as useInput2 } from "ink";
18965
+
18966
+ // src/sidecar.ts
18967
+ import { spawn as spawn2 } from "child_process";
18968
+ import { createInterface } from "readline";
18969
+ var MiniSidecarClient = class {
18970
+ input;
18971
+ requestTimeoutMs;
18972
+ pending = /* @__PURE__ */ new Map();
18973
+ eventListeners = /* @__PURE__ */ new Set();
18974
+ lineReader;
18975
+ nextId = 1;
18976
+ closed = false;
18977
+ constructor(opts) {
18978
+ this.input = opts.input;
18979
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 1e4;
18980
+ this.lineReader = createInterface({ input: opts.output });
18981
+ this.lineReader.on("line", (line) => this.handleLine(line));
18982
+ this.lineReader.on("close", () => this.rejectAll("Sidecar output closed."));
18983
+ }
18984
+ onEvent(listener) {
18985
+ this.eventListeners.add(listener);
18986
+ return () => {
18987
+ this.eventListeners.delete(listener);
18988
+ };
18989
+ }
18990
+ handshake(params) {
18991
+ return this.request(
18992
+ "recappi.handshake",
18993
+ sidecarHandshakeParamsSchema.parse(params),
18994
+ sidecarHandshakeResultSchema
18995
+ );
18996
+ }
18997
+ startRecording(params) {
18998
+ return this.request(
18999
+ "recappi.recording.start",
19000
+ sidecarRecordingStartParamsSchema.parse(params),
19001
+ sidecarRecordingStartResultSchema
19002
+ );
19003
+ }
19004
+ stopRecording(params) {
19005
+ return this.request(
19006
+ "recappi.recording.stop",
19007
+ sidecarSessionParamsSchema.parse(params),
19008
+ sidecarRecordingStopResultSchema
19009
+ );
19010
+ }
19011
+ cancelRecording(params) {
19012
+ return this.request(
19013
+ "recappi.recording.cancel",
19014
+ sidecarSessionParamsSchema.parse(params),
19015
+ sidecarRecordingStopResultSchema
19016
+ );
19017
+ }
19018
+ getRecordingStatus(params) {
19019
+ return this.request(
19020
+ "recappi.recording.status",
19021
+ sidecarSessionParamsSchema.parse(params),
19022
+ sidecarRecordingStatusResultSchema
19023
+ );
19024
+ }
19025
+ close() {
19026
+ if (this.closed) return;
19027
+ this.closed = true;
19028
+ this.lineReader.close();
19029
+ this.rejectAll("Sidecar client closed.");
19030
+ }
19031
+ request(method, params, resultSchema) {
19032
+ if (this.closed) {
19033
+ return Promise.reject(
19034
+ cliError("internal.unexpected", "Sidecar client is already closed.", {
19035
+ hint: "Start a new sidecar session and retry."
19036
+ })
19037
+ );
19038
+ }
19039
+ const id = this.nextId;
19040
+ this.nextId += 1;
19041
+ const payload = {
19042
+ jsonrpc: "2.0",
19043
+ id,
19044
+ method,
19045
+ params
19046
+ };
19047
+ return new Promise((resolve, reject) => {
19048
+ const timer = setTimeout(() => {
19049
+ this.pending.delete(id);
19050
+ reject(
19051
+ cliError("internal.unexpected", `Sidecar request timed out: ${method}.`, {
19052
+ retryable: true
19053
+ })
19054
+ );
19055
+ }, this.requestTimeoutMs);
19056
+ this.pending.set(id, {
19057
+ resolve: (value) => {
19058
+ try {
19059
+ const parsed = resultSchema.parse(value);
19060
+ resolve(parsed);
19061
+ } catch (error51) {
19062
+ reject(toCliError(error51));
19063
+ }
19064
+ },
19065
+ reject,
19066
+ timer
19067
+ });
19068
+ this.input.write(`${JSON.stringify(payload)}
19069
+ `, (error51) => {
19070
+ if (!error51) return;
19071
+ clearTimeout(timer);
19072
+ this.pending.delete(id);
19073
+ reject(cliError("internal.unexpected", `Could not write to sidecar: ${error51.message}`));
19074
+ });
19075
+ });
19076
+ }
19077
+ handleLine(line) {
19078
+ const trimmed = line.trim();
19079
+ if (!trimmed) return;
19080
+ let raw;
19081
+ try {
19082
+ raw = JSON.parse(trimmed);
19083
+ } catch {
19084
+ this.rejectAll("Sidecar wrote invalid JSON.");
19085
+ return;
19086
+ }
19087
+ const maybeNotification = sidecarNotificationSchema.safeParse(raw);
19088
+ if (maybeNotification.success) {
19089
+ const event = sidecarEventSchema.parse(maybeNotification.data.params);
19090
+ for (const listener of this.eventListeners) listener(event);
19091
+ return;
19092
+ }
19093
+ const response = sidecarResponseSchema.safeParse(raw);
19094
+ if (!response.success) {
19095
+ this.rejectAll("Sidecar wrote an invalid JSON-RPC message.");
19096
+ return;
19097
+ }
19098
+ const id = sidecarJsonRpcIdSchema.parse(response.data.id);
19099
+ const pending = this.pending.get(id);
19100
+ if (!pending) return;
19101
+ this.pending.delete(id);
19102
+ clearTimeout(pending.timer);
19103
+ if ("error" in response.data) {
19104
+ pending.reject(
19105
+ cliError("internal.unexpected", response.data.error.message, {
19106
+ data: response.data.error,
19107
+ retryable: response.data.error.code >= -32099 && response.data.error.code <= -32e3
19108
+ })
19109
+ );
19110
+ return;
19111
+ }
19112
+ pending.resolve(response.data.result);
19113
+ }
19114
+ rejectAll(message) {
19115
+ for (const [id, pending] of this.pending) {
19116
+ this.pending.delete(id);
19117
+ clearTimeout(pending.timer);
19118
+ pending.reject(cliError("internal.unexpected", message));
19119
+ }
19120
+ }
19121
+ };
19122
+ function spawnMiniSidecar(opts) {
19123
+ const spawnProcess = opts.spawnProcess ?? spawn2;
19124
+ const child = spawnProcess(opts.command, opts.args ?? [], {
19125
+ env: opts.env,
19126
+ stdio: ["pipe", "pipe", "pipe"]
19127
+ });
19128
+ const client = new MiniSidecarClient({
19129
+ input: child.stdin,
19130
+ output: child.stdout,
19131
+ requestTimeoutMs: opts.requestTimeoutMs
19132
+ });
19133
+ return {
19134
+ client,
19135
+ kill: () => {
19136
+ client.close();
19137
+ child.kill();
19138
+ }
19139
+ };
19140
+ }
19141
+ function defaultSidecarHandshakeParams(params) {
19142
+ return {
19143
+ protocolVersion: SIDECAR_PROTOCOL_VERSION,
19144
+ ...params
19145
+ };
19146
+ }
19147
+
19148
+ // src/tui/LiveCaptionsScreen.tsx
19149
+ import { useEffect, useState as useState2 } from "react";
19150
+
19151
+ // src/tui/LiveCaptionsView.tsx
19152
+ init_format();
19153
+ init_terminal();
19154
+ import { useMemo, useState } from "react";
19155
+ import { Box, Text, useInput } from "ink";
19156
+
19157
+ // src/tui/liveCaptions.ts
19158
+ function initialLiveCaptionsState() {
19159
+ return { status: "connecting", lines: [] };
19160
+ }
19161
+ function liveCaptionReducer(state, event) {
19162
+ switch (event.kind) {
19163
+ case "status": {
19164
+ const next = { ...state, status: event.status };
19165
+ if (event.status === "live" && state.startedAtMs == null) {
19166
+ next.startedAtMs = event.atMs ?? state.startedAtMs;
19167
+ }
19168
+ if (event.status !== "error") next.error = void 0;
19169
+ return next;
19170
+ }
19171
+ case "partial":
19172
+ return { ...state, partial: event.text };
19173
+ case "final": {
19174
+ const lines = [...state.lines, event.line];
19175
+ return { ...state, lines, partial: void 0 };
19176
+ }
19177
+ case "translationPartial":
19178
+ return { ...state, translationPartial: event.text };
19179
+ case "translationFinal": {
19180
+ const lines = [...state.lines];
19181
+ let idx = event.segmentId ? lines.findIndex((l) => l.id === event.segmentId) : -1;
19182
+ if (idx < 0) idx = lines.length - 1;
19183
+ if (idx >= 0) lines[idx] = { ...lines[idx], translation: event.text };
19184
+ return { ...state, lines, translationPartial: void 0 };
19185
+ }
19186
+ case "error":
19187
+ return { ...state, status: "error", error: event.message };
19188
+ default:
19189
+ return state;
19190
+ }
19191
+ }
19192
+ function recordingStateToStatus(state) {
19193
+ switch (state) {
19194
+ case "idle":
19195
+ case "starting":
19196
+ return "connecting";
19197
+ case "recording":
19198
+ return "live";
19199
+ case "stopping":
19200
+ case "finalizing":
19201
+ case "uploading":
19202
+ case "completed":
19203
+ case "cancelled":
19204
+ return "stopped";
19205
+ case "failed":
19206
+ return "error";
19207
+ }
19208
+ }
19209
+ function sidecarToLiveCaptionEvent(event) {
19210
+ switch (event.type) {
19211
+ case "ready":
19212
+ return { kind: "status", status: "connecting" };
19213
+ case "recording.state":
19214
+ if (event.state === "failed") {
19215
+ return { kind: "error", message: event.message ?? "Recording failed" };
19216
+ }
19217
+ return { kind: "status", status: recordingStateToStatus(event.state) };
19218
+ case "live_caption.delta": {
19219
+ if (event.stream === "translation") {
19220
+ return event.isFinal ? { kind: "translationFinal", segmentId: event.segmentId, text: event.text } : { kind: "translationPartial", text: event.text };
19221
+ }
19222
+ if (event.isFinal) {
19223
+ return {
19224
+ kind: "final",
19225
+ line: {
19226
+ id: event.segmentId ?? `${event.startMs ?? event.atMs ?? 0}`,
19227
+ text: event.text,
19228
+ speaker: event.speaker,
19229
+ atMs: event.startMs ?? event.atMs
19230
+ }
19231
+ };
19232
+ }
19233
+ return { kind: "partial", text: event.text };
19234
+ }
19235
+ case "error":
19236
+ return { kind: "error", message: event.message };
19237
+ case "audio.level":
19238
+ case "local_artifact.upserted":
19239
+ return null;
19240
+ default:
19241
+ return null;
19242
+ }
19243
+ }
19244
+ function liveCaptionStatusLabel(status) {
19245
+ switch (status) {
19246
+ case "connecting":
19247
+ return "Connecting\u2026";
19248
+ case "live":
19249
+ return "\u25CF LIVE";
19250
+ case "reconnecting":
19251
+ return "Reconnecting\u2026";
19252
+ case "stopped":
19253
+ return "Stopped";
19254
+ case "error":
19255
+ return "Error";
19256
+ }
19257
+ }
19258
+
19259
+ // src/tui/LiveCaptionsView.tsx
19260
+ import { jsx, jsxs } from "react/jsx-runtime";
19261
+ var STATUS_COLOR = {
19262
+ connecting: "yellow",
19263
+ live: "red",
19264
+ reconnecting: "yellow",
19265
+ stopped: "gray",
19266
+ error: "red"
19267
+ };
19268
+ function LiveCaptionsView({
19269
+ state,
19270
+ nowMs
19271
+ }) {
19272
+ const size = useTerminalSize();
19273
+ const [scrollUp, setScrollUp] = useState(0);
19274
+ const innerWidth = Math.max(10, size.columns - 2);
19275
+ const items = useMemo(() => {
19276
+ const rows = [];
19277
+ for (const l of state.lines) {
19278
+ rows.push({ key: l.id, kind: "final", speaker: l.speaker, text: l.text });
19279
+ if (l.translation) rows.push({ key: `${l.id}__t`, kind: "translation", text: l.translation });
19280
+ }
19281
+ if (state.partial && state.partial.length > 0) {
19282
+ rows.push({ key: "__partial__", kind: "partial", text: state.partial });
19283
+ }
19284
+ if (state.translationPartial && state.translationPartial.length > 0) {
19285
+ rows.push({ key: "__tpartial__", kind: "translation", text: state.translationPartial });
19286
+ }
19287
+ return rows;
19288
+ }, [state.lines, state.partial, state.translationPartial]);
19289
+ const heights = useMemo(
19290
+ () => items.map((it) => {
19291
+ const prefix = it.kind === "translation" ? "\u21B3 " : it.speaker ? `${it.speaker}: ` : "";
19292
+ return Math.max(1, Math.ceil(displayWidth(prefix + it.text) / innerWidth));
19293
+ }),
19294
+ [items, innerWidth]
19295
+ );
19296
+ const budget = Math.max(3, size.rows - 3);
19297
+ const maxScroll = windowByHeights(heights, Number.MAX_SAFE_INTEGER, budget).maxScroll;
19298
+ const top = Math.max(0, maxScroll - scrollUp);
19299
+ const win = windowByHeights(heights, top, budget);
19300
+ const following = scrollUp === 0;
19301
+ const page = Math.max(1, budget - 1);
19302
+ useInput((input, key) => {
19303
+ if (key.upArrow || input === "k") setScrollUp((s) => Math.min(maxScroll, s + 1));
19304
+ else if (key.downArrow || input === "j") setScrollUp((s) => Math.max(0, s - 1));
19305
+ else if (key.pageUp || input === "b") setScrollUp((s) => Math.min(maxScroll, s + page));
19306
+ else if (key.pageDown || input === " ") setScrollUp((s) => Math.max(0, s - page));
19307
+ else if (input === "G") setScrollUp(0);
19308
+ else if (input === "g") setScrollUp(maxScroll);
19309
+ });
19310
+ const elapsed = state.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - state.startedAtMs)) : null;
19311
+ const statusColor = STATUS_COLOR[state.status] ?? "white";
19312
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
19313
+ /* @__PURE__ */ jsxs(Text, { children: [
19314
+ /* @__PURE__ */ jsx(Text, { bold: true, color: statusColor, children: liveCaptionStatusLabel(state.status) }),
19315
+ elapsed ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${elapsed}` }) : null,
19316
+ !following ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u23F8 scrolled \u2014 G for live" }) : null
19317
+ ] }),
19318
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: items.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.status === "error" ? state.error ? `Error: ${state.error}` : "Live captions error" : "Waiting for captions\u2026" }) : items.slice(win.start, win.end).map(
19319
+ (it) => it.kind === "translation" ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u21B3 ${it.text}` }, it.key) : /* @__PURE__ */ jsxs(Text, { dimColor: it.kind === "partial", italic: it.kind === "partial", children: [
19320
+ it.speaker ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: `${it.speaker}: ` }) : null,
19321
+ it.text
19322
+ ] }, it.key)
19323
+ ) }),
19324
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
19325
+ maxScroll > 0 ? "\u2191\u2193 scroll \xB7 G live \xB7 " : "",
19326
+ "q / esc / \u2190 back"
19327
+ ] }) })
19328
+ ] });
19329
+ }
19330
+
19331
+ // src/tui/LiveCaptionsScreen.tsx
19332
+ import { jsx as jsx2 } from "react/jsx-runtime";
19333
+ function LiveCaptionsScreen({
19334
+ source,
19335
+ now = () => Date.now()
19336
+ }) {
19337
+ const [state, setState] = useState2(initialLiveCaptionsState);
19338
+ const [tick, setTick] = useState2(() => now());
19339
+ useEffect(() => {
19340
+ const unsubscribe = source.onEvent((event) => {
19341
+ const mapped = sidecarToLiveCaptionEvent(event);
19342
+ if (mapped) setState((s) => liveCaptionReducer(s, mapped));
19343
+ });
19344
+ return unsubscribe;
19345
+ }, [source]);
19346
+ useEffect(() => {
19347
+ const id = setInterval(() => setTick(now()), 1e3);
19348
+ return () => clearInterval(id);
19349
+ }, []);
19350
+ return /* @__PURE__ */ jsx2(LiveCaptionsView, { state, nowMs: tick });
19351
+ }
19352
+
19353
+ // src/record.tsx
19354
+ import { jsx as jsx3 } from "react/jsx-runtime";
19355
+ var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
19356
+ async function recordViaSidecar(opts) {
19357
+ const command = resolveSidecarCommand(opts);
19358
+ const sidecarArgs = opts.sidecarArgs ?? [];
19359
+ const spawnSidecar = opts.runtime?.spawnSidecar ?? spawnMiniSidecar;
19360
+ const sidecar = spawnSidecar({ command, args: sidecarArgs, env: opts.env });
19361
+ const account = requireAccountPartition(opts.account);
19362
+ const artifacts = [];
19363
+ let liveRenderer;
19364
+ let handshake;
19365
+ let sessionId;
19366
+ let latestState;
19367
+ let recordingId;
19368
+ let localSessionRef;
19369
+ const unsubscribe = sidecar.client.onEvent((event) => {
19370
+ if (event.type === "recording.state") {
19371
+ latestState = event.state;
19372
+ if (event.recordingId) recordingId = event.recordingId;
19373
+ if (event.localSessionRef) localSessionRef = event.localSessionRef;
19374
+ }
19375
+ if (event.type === "local_artifact.upserted") {
19376
+ artifacts.push(event.artifact);
19377
+ }
19378
+ });
19379
+ try {
19380
+ if (opts.renderLive) {
19381
+ liveRenderer = opts.runtime?.createLiveRenderer?.(sidecar.client) ?? createInkLiveRenderer({
19382
+ source: sidecar.client,
19383
+ renderApp: opts.runtime?.renderApp,
19384
+ now: opts.runtime?.now
19385
+ });
19386
+ }
19387
+ handshake = await sidecar.client.handshake(
19388
+ defaultSidecarHandshakeParams({
19389
+ client: { name: "recappi-cli", version: opts.cliVersion },
19390
+ account: opts.account,
19391
+ capabilities: opts.live ? ["recording.capture", "recording.upload", "live_captions.stream"] : ["recording.capture", "recording.upload"]
19392
+ })
19393
+ );
19394
+ const started = await sidecar.client.startRecording({
19395
+ account,
19396
+ options: {
19397
+ includeSystemAudio: opts.includeSystemAudio ?? true,
19398
+ includeMicrophone: opts.includeMicrophone ?? true,
19399
+ liveCaptions: opts.live === true,
19400
+ ...opts.translationLanguage ? { translationLanguage: opts.translationLanguage } : {},
19401
+ ...opts.transcriptionLanguage ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
19402
+ ...opts.title ? { title: opts.title } : {}
19403
+ }
19404
+ });
19405
+ sessionId = started.sessionId;
19406
+ latestState = started.state;
19407
+ localSessionRef = started.localSessionRef;
19408
+ if (liveRenderer) {
19409
+ await liveRenderer.waitUntilStop();
19410
+ } else {
19411
+ await (opts.runtime?.waitForStop ?? waitForStopSignal)();
19412
+ }
19413
+ const stopped = await sidecar.client.stopRecording({ sessionId });
19414
+ latestState = stopped.state;
19415
+ recordingId = stopped.recordingId ?? recordingId;
19416
+ localSessionRef = stopped.localSessionRef ?? localSessionRef;
19417
+ artifacts.push(...stopped.artifacts ?? []);
19418
+ const uniqueArtifacts = dedupeArtifacts(artifacts);
19419
+ persistArtifacts(uniqueArtifacts, account, opts);
19420
+ return recordCommandDataSchema.parse({
19421
+ origin: account.backendOrigin,
19422
+ userId: account.userId,
19423
+ live: opts.live === true,
19424
+ sessionId: stopped.sessionId,
19425
+ state: stopped.state,
19426
+ ...recordingId ? { recordingId } : {},
19427
+ ...localSessionRef ? { localSessionRef } : {},
19428
+ ...handshake?.sidecar ? { sidecar: handshake.sidecar } : {},
19429
+ artifacts: uniqueArtifacts
19430
+ });
19431
+ } catch (error51) {
19432
+ if (sessionId && latestState && latestState !== "completed" && latestState !== "cancelled") {
19433
+ try {
19434
+ await sidecar.client.cancelRecording({ sessionId });
19435
+ } catch {
19436
+ }
19437
+ }
19438
+ throw error51;
19439
+ } finally {
19440
+ unsubscribe();
19441
+ liveRenderer?.close();
19442
+ sidecar.kill();
19443
+ }
19444
+ }
19445
+ function resolveSidecarCommand(opts) {
19446
+ const command = opts.sidecarCommand?.trim() || opts.env?.[SIDECAR_COMMAND_ENV]?.trim();
19447
+ if (!command) {
19448
+ throw cliError("usage.invalid_argument", "Missing Recappi Mini sidecar command.", {
19449
+ hint: `Pass --sidecar-command, or set ${SIDECAR_COMMAND_ENV} to the Mini sidecar executable.`
19450
+ });
19451
+ }
19452
+ return command;
19453
+ }
19454
+ function persistArtifacts(artifacts, account, opts) {
19455
+ if (artifacts.length === 0) return;
19456
+ const store = openCliStore({ homeDir: opts.homeDir, env: opts.env });
19457
+ try {
19458
+ for (const artifact of artifacts) {
19459
+ store.addLocalArtifact({
19460
+ kind: artifact.kind,
19461
+ account,
19462
+ localPath: artifact.localPath,
19463
+ remoteId: artifact.remoteId,
19464
+ metadata: artifact.metadata
19465
+ });
19466
+ }
19467
+ } finally {
19468
+ store.close();
19469
+ }
19470
+ }
19471
+ function dedupeArtifacts(artifacts) {
19472
+ const seen = /* @__PURE__ */ new Set();
19473
+ const out = [];
19474
+ for (const artifact of artifacts) {
19475
+ const key = [artifact.kind, artifact.localPath, artifact.remoteId ?? ""].join("\0");
19476
+ if (seen.has(key)) continue;
19477
+ seen.add(key);
19478
+ out.push(artifact);
19479
+ }
19480
+ return out;
19481
+ }
19482
+ function waitForStopSignal() {
19483
+ return new Promise((resolve) => {
19484
+ const stop = () => {
19485
+ process.off("SIGINT", stop);
19486
+ process.off("SIGTERM", stop);
19487
+ resolve();
19488
+ };
19489
+ process.once("SIGINT", stop);
19490
+ process.once("SIGTERM", stop);
19491
+ });
19492
+ }
19493
+ function createInkLiveRenderer(opts) {
19494
+ let resolveStop;
19495
+ const stopped = new Promise((resolve) => {
19496
+ resolveStop = resolve;
19497
+ });
19498
+ const renderApp = opts.renderApp ?? render;
19499
+ const app = renderApp(
19500
+ /* @__PURE__ */ jsx3(
19501
+ RecordLiveScreen,
19502
+ {
19503
+ source: opts.source,
19504
+ onStop: () => resolveStop?.(),
19505
+ now: opts.now ?? Date.now
19506
+ }
19507
+ ),
19508
+ { alternateScreen: true, interactive: true }
19509
+ );
19510
+ return {
19511
+ waitUntilStop: () => stopped,
19512
+ close: () => app.unmount()
19513
+ };
19514
+ }
19515
+ function RecordLiveScreen({
19516
+ source,
19517
+ onStop,
19518
+ now
19519
+ }) {
19520
+ useInput2((input, key) => {
19521
+ if (input === "q" || key.escape || key.leftArrow) onStop();
19522
+ });
19523
+ return /* @__PURE__ */ jsx3(LiveCaptionsScreen, { source, now });
19524
+ }
19525
+
17980
19526
  // src/cli.ts
17981
19527
  var DASHBOARD_RECORDINGS_PAGE_SIZE = 50;
17982
19528
  async function runCli(deps = {}) {
@@ -17992,7 +19538,7 @@ async function runCli(deps = {}) {
17992
19538
  return 0;
17993
19539
  }
17994
19540
  const mode = parsed.options.mode ?? (isTTY ? "human" : "json");
17995
- const render2 = {
19541
+ const render3 = {
17996
19542
  mode,
17997
19543
  compact: parsed.options.compact,
17998
19544
  fields: parsed.options.fields,
@@ -18001,11 +19547,11 @@ async function runCli(deps = {}) {
18001
19547
  progress: createHumanProgressState(mode === "human" && isTTY)
18002
19548
  };
18003
19549
  if (parsed.kind === "schema") {
18004
- renderSuccess("schema", parsed.document, render2);
19550
+ renderSuccess("schema", parsed.document, render3);
18005
19551
  return 0;
18006
19552
  }
18007
19553
  if (parsed.kind === "version") {
18008
- renderSuccess("version", { version: CLI_VERSION }, render2);
19554
+ renderSuccess("version", { version: CLI_VERSION }, render3);
18009
19555
  return 0;
18010
19556
  }
18011
19557
  const auth = await resolveAuthContext({
@@ -18016,23 +19562,38 @@ async function runCli(deps = {}) {
18016
19562
  const client = new RecappiApiClient(auth, {
18017
19563
  fetchImpl: deps.fetchImpl,
18018
19564
  sleep: deps.sleep,
18019
- env: deps.env
19565
+ env: deps.env,
19566
+ homeDir: deps.homeDir
18020
19567
  });
18021
19568
  if (parsed.kind === "dashboard") {
19569
+ const status = await client.authStatus();
19570
+ const account = status.loggedIn && status.userId ? { backendOrigin: auth.origin, userId: status.userId } : null;
19571
+ const recordingAudio = createRecordingAudioRuntime(client, {
19572
+ account,
19573
+ env: deps.env,
19574
+ homeDir: deps.homeDir
19575
+ });
18022
19576
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
18023
19577
  await runDashboard2({
18024
19578
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
18025
19579
  fetchRecordings: ({ cursor, limit = DASHBOARD_RECORDINGS_PAGE_SIZE } = {}) => client.listRecordings({ limit, cursor }),
18026
19580
  fetchDashboardStats: () => client.dashboardStats(),
18027
19581
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
18028
- recordingAudio: createRecordingAudioRuntime(client),
19582
+ recordingAudio,
19583
+ listDownloadedRecordingIds: () => recordingAudio.listDownloadedRecordingIds(),
19584
+ listDownloads: () => recordingAudio.listDownloads(),
18029
19585
  initialView: parsed.initialView
18030
19586
  });
18031
19587
  return 0;
18032
19588
  }
18033
19589
  if (parsed.kind === "auth-status") {
18034
19590
  const data = await client.authStatus();
18035
- renderSuccess("auth status", data, render2);
19591
+ renderSuccess("auth status", data, render3);
19592
+ return data.loggedIn ? 0 : 3;
19593
+ }
19594
+ if (parsed.kind === "account-status") {
19595
+ const data = await client.accountStatus();
19596
+ renderSuccess("account status", data, render3);
18036
19597
  return data.loggedIn ? 0 : 3;
18037
19598
  }
18038
19599
  if (parsed.kind === "auth-login") {
@@ -18047,12 +19608,12 @@ async function runCli(deps = {}) {
18047
19608
  sleep: deps.sleep
18048
19609
  }
18049
19610
  });
18050
- renderSuccess("auth login", data, render2);
19611
+ renderSuccess("auth login", data, render3);
18051
19612
  return 0;
18052
19613
  }
18053
19614
  if (parsed.kind === "auth-logout") {
18054
- const cleared = await clearAuthConfig(deps.homeDir ?? os4.homedir());
18055
- renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render2);
19615
+ const cleared = await clearAuthConfig(deps.homeDir ?? os5.homedir());
19616
+ renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render3);
18056
19617
  return 0;
18057
19618
  }
18058
19619
  if (parsed.kind === "auth-import-macos") {
@@ -18062,20 +19623,20 @@ async function runCli(deps = {}) {
18062
19623
  hint: keychain.hint ?? "Run recappi auth login instead."
18063
19624
  });
18064
19625
  }
18065
- await saveAuthConfig(deps.homeDir ?? os4.homedir(), {
19626
+ await saveAuthConfig(deps.homeDir ?? os5.homedir(), {
18066
19627
  origin: auth.origin,
18067
19628
  token: keychain.token
18068
19629
  });
18069
19630
  renderSuccess(
18070
19631
  "auth import-macos",
18071
19632
  { imported: true, origin: auth.origin, source: "macos-keychain" },
18072
- render2
19633
+ render3
18073
19634
  );
18074
19635
  return 0;
18075
19636
  }
18076
19637
  if (parsed.kind === "doctor") {
18077
19638
  const data = await client.doctor();
18078
- renderSuccess("doctor", data, render2);
19639
+ renderSuccess("doctor", data, render3);
18079
19640
  if (data.status !== "error") return 0;
18080
19641
  return data.checks.some((check2) => check2.name.startsWith("auth.")) ? 3 : 1;
18081
19642
  }
@@ -18089,7 +19650,7 @@ async function runCli(deps = {}) {
18089
19650
  provider: parsed.provider,
18090
19651
  prompt: parsed.prompt,
18091
19652
  force: parsed.force,
18092
- onEvent: (event) => renderEvent(event, render2)
19653
+ onEvent: (event) => renderEvent(event, render3)
18093
19654
  });
18094
19655
  if (data.failures.length > 0) {
18095
19656
  const worst = data.failures.reduce((max, item) => Math.max(max, item.error.exitCode), 1);
@@ -18100,26 +19661,95 @@ async function runCli(deps = {}) {
18100
19661
  message: `${data.failures.length} of ${data.totalCount} upload(s) failed.`,
18101
19662
  hint: "Inspect data.failures[].error for per-file codes; retry only the failed files."
18102
19663
  };
18103
- renderFailure("upload", descriptor, render2, data);
19664
+ renderFailure("upload", descriptor, render3, data);
18104
19665
  return worst;
18105
19666
  }
18106
- renderSuccess("upload", data, render2);
19667
+ renderSuccess("upload", data, render3);
19668
+ return 0;
19669
+ }
19670
+ if (parsed.kind === "record") {
19671
+ const status = await client.authStatus();
19672
+ if (!status.loggedIn || !status.userId) {
19673
+ throw cliError("auth.not_logged_in", "Sign in before starting a sidecar recording.", {
19674
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19675
+ });
19676
+ }
19677
+ if (mode === "human" && isTTY && !parsed.live) {
19678
+ stderr("Recording\u2026 press Ctrl-C to stop.\n");
19679
+ }
19680
+ const data = await recordViaSidecar({
19681
+ account: {
19682
+ backendOrigin: auth.origin,
19683
+ userId: status.userId,
19684
+ ...status.email ? { email: status.email } : {}
19685
+ },
19686
+ cliVersion: CLI_VERSION,
19687
+ env: deps.env,
19688
+ homeDir: deps.homeDir,
19689
+ title: parsed.title,
19690
+ live: parsed.live,
19691
+ includeSystemAudio: parsed.includeSystemAudio,
19692
+ includeMicrophone: parsed.includeMicrophone,
19693
+ translationLanguage: parsed.translationLanguage,
19694
+ transcriptionLanguage: parsed.transcriptionLanguage,
19695
+ sidecarCommand: parsed.sidecarCommand,
19696
+ renderLive: parsed.live === true && mode === "human" && isTTY,
19697
+ runtime: deps.recordRuntime
19698
+ });
19699
+ renderSuccess("record", data, render3);
19700
+ return 0;
19701
+ }
19702
+ if (parsed.kind === "audio") {
19703
+ const status = await client.authStatus();
19704
+ if (!status.loggedIn || !status.userId) {
19705
+ throw cliError("auth.not_logged_in", "Sign in before using local audio actions.", {
19706
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19707
+ });
19708
+ }
19709
+ const recordingAudio = createRecordingAudioRuntime(client, {
19710
+ account: { backendOrigin: auth.origin, userId: status.userId },
19711
+ env: deps.env,
19712
+ homeDir: deps.homeDir
19713
+ });
19714
+ const download = await recordingAudio.downloadRecordingAudioFile(
19715
+ parsed.recordingId,
19716
+ parsed.outputDir ? { directory: parsed.outputDir } : void 0
19717
+ );
19718
+ if (parsed.action === "open") {
19719
+ await recordingAudio.openPath(download.localPath);
19720
+ } else if (parsed.action === "reveal") {
19721
+ await recordingAudio.revealInFinder(download.localPath);
19722
+ }
19723
+ renderSuccess(
19724
+ "audio",
19725
+ {
19726
+ origin: auth.origin,
19727
+ recordingId: parsed.recordingId,
19728
+ localPath: download.localPath,
19729
+ action: parsed.action,
19730
+ reused: download.reused,
19731
+ ...download.artifactId !== void 0 ? { artifactId: download.artifactId } : {},
19732
+ ...download.contentType ? { contentType: download.contentType } : {},
19733
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {}
19734
+ },
19735
+ render3
19736
+ );
18107
19737
  return 0;
18108
19738
  }
18109
19739
  if (parsed.kind === "jobs-wait") {
18110
19740
  const parsedOptions = parsed.options;
18111
19741
  const data = await client.waitForJob(parsed.jobId, {
18112
19742
  onEvent: (event) => renderEvent(event, {
18113
- ...render2,
19743
+ ...render3,
18114
19744
  mode: parsedOptions.mode === "jsonl" ? "jsonl" : "human"
18115
19745
  })
18116
19746
  });
18117
- renderSuccess("jobs wait", data, render2);
19747
+ renderSuccess("jobs wait", data, render3);
18118
19748
  return 0;
18119
19749
  }
18120
19750
  if (parsed.kind === "jobs-list") {
18121
19751
  const data = await client.listJobs({ status: parsed.status, limit: parsed.limit });
18122
- renderSuccess("jobs list", data, render2);
19752
+ renderSuccess("jobs list", data, render3);
18123
19753
  return 0;
18124
19754
  }
18125
19755
  if (parsed.kind === "recordings-list") {
@@ -18128,22 +19758,22 @@ async function runCli(deps = {}) {
18128
19758
  cursor: parsed.cursor,
18129
19759
  search: parsed.search
18130
19760
  });
18131
- renderSuccess("recordings list", data, render2);
19761
+ renderSuccess("recordings list", data, render3);
18132
19762
  return 0;
18133
19763
  }
18134
19764
  if (parsed.kind === "recordings-get") {
18135
19765
  const data = await client.getRecording(parsed.recordingId);
18136
- renderSuccess("recordings get", data, render2);
19766
+ renderSuccess("recordings get", data, render3);
18137
19767
  return 0;
18138
19768
  }
18139
19769
  if (parsed.kind === "dashboard-stats") {
18140
19770
  const data = await client.dashboardStats();
18141
- renderSuccess("dashboard stats", data, render2);
19771
+ renderSuccess("dashboard stats", data, render3);
18142
19772
  return 0;
18143
19773
  }
18144
19774
  if (parsed.kind === "transcript-get") {
18145
19775
  const data = await client.getTranscript(parsed.transcriptId);
18146
- renderSuccess("transcript get", data, render2);
19776
+ renderSuccess("transcript get", data, render3);
18147
19777
  return 0;
18148
19778
  }
18149
19779
  throw cliError("usage.invalid_argument", "Unknown command.");
@@ -18335,6 +19965,17 @@ Agent mode:
18335
19965
  commandName: "doctor"
18336
19966
  });
18337
19967
  });
19968
+ const account = program.command("account").description("Account and quota commands");
19969
+ addCommonOptions(account);
19970
+ const accountStatus = account.command("status").description("Show account, quota, and local state");
19971
+ addCommonOptions(accountStatus);
19972
+ accountStatus.action((_options, command) => {
19973
+ onSelect({
19974
+ kind: "account-status",
19975
+ options: collectGlobalOptions(command),
19976
+ commandName: "account status"
19977
+ });
19978
+ });
18338
19979
  const upload = program.command("upload <file-or-dir>").description("Upload an audio file or directory to Recappi Cloud").option("--title <title>", "recording title", parseStringOption("--title")).option("--transcribe", "start transcription after upload").option("--wait", "wait for the transcription job to reach a terminal state").option("--language <lang>", "transcription language hint", parseStringOption("--language")).option("--provider <name>", "transcription provider", parseStringOption("--provider")).option("--prompt <text>", "transcription prompt/context", parseStringOption("--prompt")).option("--force", "force upload if a conflict is retryable");
18339
19980
  addCommonOptions(upload);
18340
19981
  upload.action((inputPath, opts, command) => {
@@ -18352,6 +19993,55 @@ Agent mode:
18352
19993
  ...opts.force === true ? { force: true } : {}
18353
19994
  });
18354
19995
  });
19996
+ const record2 = program.command("record").description("Start a Recappi Mini sidecar recording").option("--title <title>", "recording title", parseStringOption("--title")).option("--live", "show live captions while recording").option("--no-system-audio", "record microphone only").option("--no-microphone", "record system audio only").option(
19997
+ "--translation-language <lang>",
19998
+ "live caption translation language",
19999
+ parseStringOption("--translation-language")
20000
+ ).option(
20001
+ "--transcription-language <lang>",
20002
+ "recording/transcription language hint",
20003
+ parseStringOption("--transcription-language")
20004
+ ).option(
20005
+ "--sidecar-command <path>",
20006
+ "Recappi Mini sidecar executable",
20007
+ parseStringOption("--sidecar-command")
20008
+ );
20009
+ addCommonOptions(record2);
20010
+ record2.action((opts, command) => {
20011
+ if (opts.systemAudio === false && opts.microphone === false) {
20012
+ throw cliError("usage.invalid_argument", "Choose at least one recording input.", {
20013
+ hint: "Use system audio, microphone, or both."
20014
+ });
20015
+ }
20016
+ onSelect({
20017
+ kind: "record",
20018
+ options: collectGlobalOptions(command),
20019
+ commandName: "record",
20020
+ ...typeof opts.title === "string" ? { title: opts.title } : {},
20021
+ ...opts.live === true ? { live: true } : {},
20022
+ ...opts.systemAudio === false ? { includeSystemAudio: false } : {},
20023
+ ...opts.microphone === false ? { includeMicrophone: false } : {},
20024
+ ...typeof opts.translationLanguage === "string" ? { translationLanguage: opts.translationLanguage } : {},
20025
+ ...typeof opts.transcriptionLanguage === "string" ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
20026
+ ...typeof opts.sidecarCommand === "string" ? { sidecarCommand: opts.sidecarCommand } : {}
20027
+ });
20028
+ });
20029
+ const audio = program.command("audio").description("Download or open a recording audio file").argument("<recording-id>", "recording id").option("--download", "download audio and print the local path").option("--open", "download if needed, then open the audio file").option("--reveal", "download if needed, then reveal the audio file in Finder").option(
20030
+ "--output-dir <dir>",
20031
+ "directory for downloaded audio",
20032
+ parseStringOption("--output-dir")
20033
+ );
20034
+ addCommonOptions(audio);
20035
+ audio.action((recordingId, opts, command) => {
20036
+ onSelect({
20037
+ kind: "audio",
20038
+ options: collectGlobalOptions(command),
20039
+ commandName: "audio",
20040
+ recordingId,
20041
+ action: audioAction(opts),
20042
+ ...opts.outputDir ? { outputDir: opts.outputDir } : {}
20043
+ });
20044
+ });
18355
20045
  const schema = program.command("schema").description("Print the machine-readable CLI contract (commands, error codes, JSON Schemas)");
18356
20046
  addCommonOptions(schema);
18357
20047
  schema.action((_options, command) => {
@@ -18448,6 +20138,17 @@ Agent mode:
18448
20138
  function addCommonOptions(command) {
18449
20139
  command.option("--json", "write one JSON envelope to stdout").option("--jsonl", "write JSONL operation events to stdout").option("--human", "write human-readable output").option("--fields <list>", "comma-separated data fields to keep", parseFieldsOption).option("--compact", "omit empty optional data and print compact JSON").option("--origin <url>", "Recappi Cloud origin", parseStringOption("--origin"));
18450
20140
  }
20141
+ function audioAction(opts) {
20142
+ const selected = [
20143
+ opts.download ? "download" : null,
20144
+ opts.open ? "open" : null,
20145
+ opts.reveal ? "reveal" : null
20146
+ ].filter((action) => action !== null);
20147
+ if (selected.length > 1) {
20148
+ throw cliError("usage.invalid_argument", "Choose only one of --download, --open, or --reveal.");
20149
+ }
20150
+ return selected[0] ?? "download";
20151
+ }
18451
20152
  function parseStringOption(flag) {
18452
20153
  return (value) => {
18453
20154
  if (!value || value.startsWith("-")) {
@@ -18513,6 +20214,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
18513
20214
  "--language",
18514
20215
  "--provider",
18515
20216
  "--prompt",
20217
+ "--translation-language",
20218
+ "--transcription-language",
20219
+ "--sidecar-command",
18516
20220
  "--status",
18517
20221
  "--limit",
18518
20222
  "--cursor",