recappi 0.1.3 → 0.1.5

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,353 @@ 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/liveCaptions.ts
272
+ function initialLiveCaptionsState() {
273
+ return { status: "connecting", lines: [] };
274
+ }
275
+ function liveCaptionReducer(state, event) {
276
+ switch (event.kind) {
277
+ case "status": {
278
+ const next = { ...state, status: event.status };
279
+ if (event.status === "live" && state.startedAtMs == null) {
280
+ next.startedAtMs = event.atMs ?? state.startedAtMs;
281
+ }
282
+ if (event.status !== "error") next.error = void 0;
283
+ return next;
284
+ }
285
+ case "partial":
286
+ return { ...state, partial: event.text };
287
+ case "final": {
288
+ const lines = [...state.lines, event.line];
289
+ return { ...state, lines, partial: void 0 };
290
+ }
291
+ case "translationPartial":
292
+ return { ...state, translationPartial: event.text };
293
+ case "translationFinal": {
294
+ const lines = [...state.lines];
295
+ let idx = event.segmentId ? lines.findIndex((l) => l.id === event.segmentId) : -1;
296
+ if (idx < 0) idx = lines.length - 1;
297
+ if (idx >= 0) lines[idx] = { ...lines[idx], translation: event.text };
298
+ return { ...state, lines, translationPartial: void 0 };
299
+ }
300
+ case "error":
301
+ return { ...state, status: "error", error: event.message };
302
+ default:
303
+ return state;
304
+ }
305
+ }
306
+ function recordingStateToStatus(state) {
307
+ switch (state) {
308
+ case "idle":
309
+ case "starting":
310
+ return "connecting";
311
+ case "recording":
312
+ return "live";
313
+ case "stopping":
314
+ case "finalizing":
315
+ case "uploading":
316
+ case "completed":
317
+ case "cancelled":
318
+ return "stopped";
319
+ case "failed":
320
+ return "error";
321
+ }
322
+ }
323
+ function sidecarToLiveCaptionEvent(event) {
324
+ switch (event.type) {
325
+ case "ready":
326
+ return { kind: "status", status: "connecting" };
327
+ case "recording.state":
328
+ if (event.state === "failed") {
329
+ return { kind: "error", message: event.message ?? "Recording failed" };
330
+ }
331
+ return { kind: "status", status: recordingStateToStatus(event.state) };
332
+ case "live_caption.delta": {
333
+ if (event.stream === "translation") {
334
+ return event.isFinal ? { kind: "translationFinal", segmentId: event.segmentId, text: event.text } : { kind: "translationPartial", text: event.text };
335
+ }
336
+ if (event.isFinal) {
337
+ return {
338
+ kind: "final",
339
+ line: {
340
+ id: event.segmentId ?? `${event.startMs ?? event.atMs ?? 0}`,
341
+ text: event.text,
342
+ speaker: event.speaker,
343
+ atMs: event.startMs ?? event.atMs
344
+ }
345
+ };
346
+ }
347
+ return { kind: "partial", text: event.text };
348
+ }
349
+ case "error":
350
+ return { kind: "error", message: event.message };
351
+ case "audio.level":
352
+ case "local_artifact.upserted":
353
+ return null;
354
+ default:
355
+ return null;
356
+ }
357
+ }
358
+ function liveCaptionStatusLabel(status) {
359
+ switch (status) {
360
+ case "connecting":
361
+ return "Connecting\u2026";
362
+ case "live":
363
+ return "\u25CF LIVE";
364
+ case "reconnecting":
365
+ return "Reconnecting\u2026";
366
+ case "stopped":
367
+ return "Stopped";
368
+ case "error":
369
+ return "Error";
370
+ }
371
+ }
372
+ var init_liveCaptions = __esm({
373
+ "src/tui/liveCaptions.ts"() {
374
+ "use strict";
375
+ }
376
+ });
377
+
378
+ // src/tui/LiveCaptionsView.tsx
379
+ import { useMemo, useState } from "react";
380
+ import { Box, Text, useInput } from "ink";
381
+ import { jsx, jsxs } from "react/jsx-runtime";
382
+ function LiveCaptionsView({
383
+ state,
384
+ nowMs
385
+ }) {
386
+ const size = useTerminalSize();
387
+ const [scrollUp, setScrollUp] = useState(0);
388
+ const innerWidth = Math.max(10, size.columns - 2);
389
+ const items = useMemo(() => {
390
+ const rows = [];
391
+ for (const l of state.lines) {
392
+ rows.push({ key: l.id, kind: "final", speaker: l.speaker, text: l.text });
393
+ if (l.translation) rows.push({ key: `${l.id}__t`, kind: "translation", text: l.translation });
394
+ }
395
+ if (state.partial && state.partial.length > 0) {
396
+ rows.push({ key: "__partial__", kind: "partial", text: state.partial });
397
+ }
398
+ if (state.translationPartial && state.translationPartial.length > 0) {
399
+ rows.push({ key: "__tpartial__", kind: "translation", text: state.translationPartial });
400
+ }
401
+ return rows;
402
+ }, [state.lines, state.partial, state.translationPartial]);
403
+ const heights = useMemo(
404
+ () => items.map((it) => {
405
+ const prefix = it.kind === "translation" ? "\u21B3 " : it.speaker ? `${it.speaker}: ` : "";
406
+ return Math.max(1, Math.ceil(displayWidth(prefix + it.text) / innerWidth));
407
+ }),
408
+ [items, innerWidth]
409
+ );
410
+ const budget = Math.max(3, size.rows - 3);
411
+ const maxScroll = windowByHeights(heights, Number.MAX_SAFE_INTEGER, budget).maxScroll;
412
+ const top = Math.max(0, maxScroll - scrollUp);
413
+ const win = windowByHeights(heights, top, budget);
414
+ const following = scrollUp === 0;
415
+ const page = Math.max(1, budget - 1);
416
+ useInput((input, key) => {
417
+ if (key.upArrow || input === "k") setScrollUp((s) => Math.min(maxScroll, s + 1));
418
+ else if (key.downArrow || input === "j") setScrollUp((s) => Math.max(0, s - 1));
419
+ else if (key.pageUp || input === "b") setScrollUp((s) => Math.min(maxScroll, s + page));
420
+ else if (key.pageDown || input === " ") setScrollUp((s) => Math.max(0, s - page));
421
+ else if (input === "G") setScrollUp(0);
422
+ else if (input === "g") setScrollUp(maxScroll);
423
+ });
424
+ const elapsed = state.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - state.startedAtMs)) : null;
425
+ const statusColor = STATUS_COLOR[state.status] ?? "white";
426
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
427
+ /* @__PURE__ */ jsxs(Text, { children: [
428
+ /* @__PURE__ */ jsx(Text, { bold: true, color: statusColor, children: liveCaptionStatusLabel(state.status) }),
429
+ elapsed ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${elapsed}` }) : null,
430
+ !following ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u23F8 scrolled \u2014 G for live" }) : null
431
+ ] }),
432
+ /* @__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(
433
+ (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: [
434
+ it.speaker ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: `${it.speaker}: ` }) : null,
435
+ it.text
436
+ ] }, it.key)
437
+ ) }),
438
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
439
+ maxScroll > 0 ? "\u2191\u2193 scroll \xB7 G live \xB7 " : "",
440
+ "q / esc / \u2190 back"
441
+ ] }) })
442
+ ] });
443
+ }
444
+ var STATUS_COLOR;
445
+ var init_LiveCaptionsView = __esm({
446
+ "src/tui/LiveCaptionsView.tsx"() {
447
+ "use strict";
448
+ init_format();
449
+ init_terminal();
450
+ init_liveCaptions();
451
+ STATUS_COLOR = {
452
+ connecting: "yellow",
453
+ live: "red",
454
+ reconnecting: "yellow",
455
+ stopped: "gray",
456
+ error: "red"
457
+ };
458
+ }
459
+ });
460
+
461
+ // src/tui/LiveCaptionsScreen.tsx
462
+ import { useEffect, useState as useState2 } from "react";
463
+ import { jsx as jsx2 } from "react/jsx-runtime";
464
+ function LiveCaptionsScreen({
465
+ source,
466
+ now = () => Date.now()
467
+ }) {
468
+ const [state, setState] = useState2(initialLiveCaptionsState);
469
+ const [tick, setTick] = useState2(() => now());
470
+ useEffect(() => {
471
+ const unsubscribe = source.onEvent((event) => {
472
+ const mapped = sidecarToLiveCaptionEvent(event);
473
+ if (mapped) setState((s) => liveCaptionReducer(s, mapped));
474
+ });
475
+ return unsubscribe;
476
+ }, [source]);
477
+ useEffect(() => {
478
+ const id = setInterval(() => setTick(now()), 1e3);
479
+ return () => clearInterval(id);
480
+ }, []);
481
+ return /* @__PURE__ */ jsx2(LiveCaptionsView, { state, nowMs: tick });
482
+ }
483
+ var init_LiveCaptionsScreen = __esm({
484
+ "src/tui/LiveCaptionsScreen.tsx"() {
485
+ "use strict";
486
+ init_LiveCaptionsView();
487
+ init_liveCaptions();
488
+ }
489
+ });
490
+
491
+ // src/tui/AccountView.tsx
271
492
  import { Box as Box2, Text as Text2 } from "ink";
272
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
493
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
494
+ function AccountView({ status }) {
495
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
496
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "\u2039 Account" }),
497
+ status === "loading" || status === void 0 ? /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Loading account\u2026" }) }) : status === "error" ? /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { color: "red", children: "Couldn't load account status" }) }) : !status.loggedIn ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
498
+ /* @__PURE__ */ jsx4(Text2, { color: "yellow", children: "Not signed in" }),
499
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `origin ${status.origin}` }),
500
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Run `recappi auth login` to sign in." })
501
+ ] }) : /* @__PURE__ */ jsx4(AccountBody, { status }),
502
+ /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "r refresh \xB7 esc back \xB7 q quit" }) })
503
+ ] });
504
+ }
505
+ function AccountBody({ status }) {
506
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
507
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
508
+ /* @__PURE__ */ jsx4(Text2, { bold: true, color: "magenta", children: status.email ?? status.userId ?? "Signed in" }),
509
+ status.email && status.userId ? /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: status.userId }) : null,
510
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `origin ${status.origin}` })
511
+ ] }),
512
+ status.billing ? /* @__PURE__ */ jsx4(Usage, { billing: status.billing }) : null,
513
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
514
+ /* @__PURE__ */ jsx4(Text2, { bold: true, children: "Local store" }),
515
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, wrap: "truncate-middle", children: status.localStore.path }),
516
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
517
+ `${status.localStore.accountScopedArtifacts} artifact${status.localStore.accountScopedArtifacts === 1 ? "" : "s"} for this account`,
518
+ status.localStore.unattributedArtifacts > 0 ? ` \xB7 ${status.localStore.unattributedArtifacts} unattributed` : ""
519
+ ] })
520
+ ] })
521
+ ] });
522
+ }
523
+ function Usage({ billing }) {
524
+ const minutesCap = billing.minutesCap;
525
+ const minutesUsed = billing.minutesUsed;
526
+ const storageCap = billing.storageCapBytes;
527
+ return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
528
+ /* @__PURE__ */ jsxs2(Text2, { children: [
529
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Plan " }),
530
+ /* @__PURE__ */ jsx4(Text2, { bold: true, children: billing.tier })
531
+ ] }),
532
+ /* @__PURE__ */ jsxs2(Text2, { children: [
533
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Minutes " }),
534
+ minutesCap != null ? /* @__PURE__ */ jsx4(Text2, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
535
+ /* @__PURE__ */ jsx4(Text2, { color: billing.isOverMinutes ? "red" : void 0, children: `${Math.round(minutesUsed)}` }),
536
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` / ${minutesCap != null ? Math.round(minutesCap) : "\u221E"} min` }),
537
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` (batch ${Math.round(billing.batchMinutesUsed)} \xB7 live ${Math.round(billing.realtimeMinutesUsed)})` })
538
+ ] }),
539
+ /* @__PURE__ */ jsxs2(Text2, { children: [
540
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Storage " }),
541
+ storageCap != null ? /* @__PURE__ */ jsx4(Text2, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
542
+ /* @__PURE__ */ jsx4(Text2, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes2(billing.storageBytes) }),
543
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` / ${storageCap != null ? formatBytes2(storageCap) : "\u221E"}` })
544
+ ] }),
545
+ billing.isOverMinutes || billing.isOverStorage ? /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
546
+ billing.isOverMinutes ? "Over minutes limit. " : "",
547
+ billing.isOverStorage ? "Over storage limit." : ""
548
+ ] }) : null,
549
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `Period ${periodText(billing)}` })
550
+ ] });
551
+ }
552
+ function periodText(billing) {
553
+ const remainingMs = epochToMs(billing.periodEnd) - Date.now();
554
+ if (!Number.isFinite(remainingMs) || remainingMs <= 0) return "\u2014";
555
+ const days = Math.floor(remainingMs / 864e5);
556
+ if (days >= 1) return `${days}d left`;
557
+ return `${formatClockMs(remainingMs)} left`;
558
+ }
559
+ function epochToMs(value) {
560
+ return value > 1e12 ? value : value * 1e3;
561
+ }
562
+ var init_AccountView = __esm({
563
+ "src/tui/AccountView.tsx"() {
564
+ "use strict";
565
+ init_format();
566
+ }
567
+ });
568
+
569
+ // src/tui/chrome.tsx
570
+ import { Box as Box3, Text as Text3 } from "ink";
571
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
572
+ function Header({ active }) {
573
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
574
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "magenta", children: [
575
+ "Recappi",
576
+ " "
577
+ ] }),
578
+ /* @__PURE__ */ jsx5(Tab, { num: "1", label: "Overview", active: active === "overview" }),
579
+ /* @__PURE__ */ jsx5(Tab, { num: "2", label: "Jobs", active: active === "jobs" }),
580
+ /* @__PURE__ */ jsx5(Tab, { num: "3", label: "Account", active: active === "account" }),
581
+ /* @__PURE__ */ jsx5(Tab, { num: "4", label: "Record", active: active === "record" })
582
+ ] });
583
+ }
584
+ function Tab({
585
+ num,
586
+ label,
587
+ active
588
+ }) {
589
+ const text = ` ${num} ${label} `;
590
+ if (active) {
591
+ return /* @__PURE__ */ jsx5(Text3, { bold: true, inverse: true, color: "cyan", children: text });
592
+ }
593
+ return /* @__PURE__ */ jsx5(Text3, { children: text });
594
+ }
595
+ function Footer({ keys }) {
596
+ return /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: keys }) });
597
+ }
598
+ var init_chrome = __esm({
599
+ "src/tui/chrome.tsx"() {
600
+ "use strict";
601
+ }
602
+ });
603
+
604
+ // src/tui/JobRow.tsx
605
+ import { Box as Box4, Text as Text4 } from "ink";
606
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
273
607
  function JobRow({
274
608
  item,
275
609
  selected,
@@ -278,11 +612,11 @@ function JobRow({
278
612
  const style = statusStyle(item.status);
279
613
  const glyph = statusGlyph(item.status, spinnerFrame);
280
614
  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) })
615
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
616
+ /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
617
+ /* @__PURE__ */ jsx6(Text4, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
618
+ /* @__PURE__ */ jsx6(Text4, { bold: selected, children: padCell(title, 24) }),
619
+ /* @__PURE__ */ jsx6(Text4, { dimColor: !selected, children: jobDetail(item) })
286
620
  ] });
287
621
  }
288
622
  var init_JobRow = __esm({
@@ -293,17 +627,17 @@ var init_JobRow = __esm({
293
627
  });
294
628
 
295
629
  // src/tui/JobsView.tsx
296
- import { Box as Box3, Text as Text3 } from "ink";
297
- import { jsx as jsx3 } from "react/jsx-runtime";
630
+ import { Box as Box5, Text as Text5 } from "ink";
631
+ import { jsx as jsx7 } from "react/jsx-runtime";
298
632
  function JobsView({
299
633
  items,
300
634
  selectedIndex,
301
635
  spinnerFrame
302
636
  }) {
303
637
  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" }) });
638
+ return /* @__PURE__ */ jsx7(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
305
639
  }
306
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx3(
640
+ return /* @__PURE__ */ jsx7(Box5, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx7(
307
641
  JobRow,
308
642
  {
309
643
  item,
@@ -321,8 +655,8 @@ var init_JobsView = __esm({
321
655
  });
322
656
 
323
657
  // 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";
658
+ import { Box as Box6, Text as Text6 } from "ink";
659
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
326
660
  function recordingTitle2(item) {
327
661
  const named = (item.title || item.summaryTitle || "").trim();
328
662
  if (named && !UUID_RE.test(named)) return named;
@@ -352,26 +686,28 @@ function RecordingRow({
352
686
  nowMs,
353
687
  columns,
354
688
  jobStatus,
355
- spinnerFrame = 0
689
+ spinnerFrame = 0,
690
+ downloaded = false
356
691
  }) {
357
692
  const { title, showWhen } = recordingLayout(columns);
358
693
  const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
359
694
  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
695
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
696
+ /* @__PURE__ */ jsx8(Text6, { color: "cyan", children: selected ? "\u25B8 " : " " }),
697
+ /* @__PURE__ */ jsx8(Text6, { color, children: `${glyph} ` }),
698
+ /* @__PURE__ */ jsx8(Text6, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
699
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
700
+ showWhen ? /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
701
+ /* @__PURE__ */ jsx8(Text6, { color: "green", children: downloaded ? " \u2913" : " " })
366
702
  ] });
367
703
  }
368
704
  function RecordingHeader({ columns }) {
369
705
  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
706
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
707
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
708
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("TITLE", title) }),
709
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
710
+ showWhen ? /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "WHEN" }) : null
375
711
  ] });
376
712
  }
377
713
  var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
@@ -388,28 +724,29 @@ var init_RecordingRow = __esm({
388
724
  });
389
725
 
390
726
  // 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";
727
+ import React3 from "react";
728
+ import { Box as Box7, Text as Text7 } from "ink";
729
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
394
730
  function RecordingsView({
395
731
  items,
396
732
  selectedIndex,
397
733
  nowMs,
398
734
  columns,
399
735
  jobStatusByRecording,
736
+ downloadedRecordingIds,
400
737
  spinnerFrame = 0
401
738
  }) {
402
739
  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>" }) });
740
+ return /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
404
741
  }
405
- return /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
406
- /* @__PURE__ */ jsx5(RecordingHeader, { columns }),
742
+ return /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
743
+ /* @__PURE__ */ jsx9(RecordingHeader, { columns }),
407
744
  items.map((item, index) => {
408
745
  const bucket = dateBucket(item.createdAt, nowMs);
409
746
  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(
747
+ return /* @__PURE__ */ jsxs6(React3.Fragment, { children: [
748
+ showHeader ? /* @__PURE__ */ jsx9(Box7, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx9(Text7, { bold: true, color: "blue", children: bucket }) }) : null,
749
+ /* @__PURE__ */ jsx9(
413
750
  RecordingRow,
414
751
  {
415
752
  item,
@@ -417,6 +754,7 @@ function RecordingsView({
417
754
  nowMs,
418
755
  columns,
419
756
  jobStatus: jobStatusByRecording?.get(item.recordingId),
757
+ downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
420
758
  spinnerFrame
421
759
  }
422
760
  )
@@ -433,15 +771,15 @@ var init_RecordingsView = __esm({
433
771
  });
434
772
 
435
773
  // 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";
774
+ import { Box as Box8, Text as Text8 } from "ink";
775
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
438
776
  function RecordingPeek({
439
777
  item,
440
778
  summary,
441
779
  nowMs,
442
780
  width
443
781
  }) {
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 }) });
782
+ return /* @__PURE__ */ jsx10(Box8, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx10(PeekBody, { item, summary, nowMs }) });
445
783
  }
446
784
  function PeekBody({
447
785
  item,
@@ -454,30 +792,30 @@ function PeekBody({
454
792
  formatBytes2(item.sizeBytes) || null,
455
793
  item.contentType || null
456
794
  ].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" }) })
795
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
796
+ /* @__PURE__ */ jsx10(Text8, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
797
+ /* @__PURE__ */ jsx10(Text8, { color: style.color, children: `${style.glyph} ${style.label}` }),
798
+ meta3 ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: meta3 }) : null,
799
+ /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
800
+ /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(SummarySection, { item, summary }) }),
801
+ /* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
464
802
  ] });
465
803
  }
466
804
  function SummarySection({
467
805
  item,
468
806
  summary
469
807
  }) {
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)" });
808
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No transcript yet" });
809
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Loading summary\u2026" });
810
+ if (summary === "error") return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "(summary unavailable)" });
473
811
  if (summary.status !== "succeeded" || !summary.tldr) {
474
- return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: `Summary ${summary.status}` });
812
+ return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: `Summary ${summary.status}` });
475
813
  }
476
814
  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
815
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
816
+ /* @__PURE__ */ jsx10(Text8, { bold: true, children: "Summary" }),
817
+ /* @__PURE__ */ jsx10(Text8, { children: summary.tldr }),
818
+ points.length > 0 ? /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx10(Text8, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
481
819
  ] });
482
820
  }
483
821
  var init_RecordingPeek = __esm({
@@ -489,8 +827,8 @@ var init_RecordingPeek = __esm({
489
827
  });
490
828
 
491
829
  // 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";
830
+ import { Box as Box9, Text as Text9 } from "ink";
831
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
494
832
  function OverviewView({
495
833
  recordings,
496
834
  jobs,
@@ -500,6 +838,7 @@ function OverviewView({
500
838
  nowMs,
501
839
  columns,
502
840
  jobStatusByRecording,
841
+ downloadedRecordingIds,
503
842
  peekItem,
504
843
  peekSummary,
505
844
  showPeek = false,
@@ -508,17 +847,17 @@ function OverviewView({
508
847
  const jobCounts = countJobs(jobs);
509
848
  const running = stats?.jobs.running ?? jobCounts.running;
510
849
  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
850
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
851
+ /* @__PURE__ */ jsxs8(Box9, { children: [
852
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Recordings " }),
853
+ /* @__PURE__ */ jsx11(Text9, { bold: true, children: stats?.recordings.total ?? recordings.length }),
854
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
855
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
856
+ running > 0 ? /* @__PURE__ */ jsx11(Text9, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
857
+ queued > 0 ? /* @__PURE__ */ jsx11(Text9, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
519
858
  ] }),
520
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", alignItems: "flex-start", children: [
521
- /* @__PURE__ */ jsx7(Box7, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx7(
859
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", alignItems: "flex-start", children: [
860
+ /* @__PURE__ */ jsx11(Box9, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(
522
861
  RecordingsView,
523
862
  {
524
863
  items: recordings,
@@ -526,10 +865,11 @@ function OverviewView({
526
865
  nowMs,
527
866
  columns,
528
867
  jobStatusByRecording,
868
+ downloadedRecordingIds,
529
869
  spinnerFrame
530
870
  }
531
871
  ) }),
532
- showPeek ? /* @__PURE__ */ jsx7(Box7, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx7(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
872
+ showPeek ? /* @__PURE__ */ jsx11(Box9, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx11(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
533
873
  ] })
534
874
  ] });
535
875
  }
@@ -543,8 +883,8 @@ var init_OverviewView = __esm({
543
883
  });
544
884
 
545
885
  // 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";
886
+ import { Box as Box10, Text as Text10 } from "ink";
887
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
548
888
  function JobDetailView({
549
889
  item,
550
890
  origin,
@@ -554,13 +894,13 @@ function JobDetailView({
554
894
  const style = statusStyle(item.status);
555
895
  const links = resolveJobLinks(item, origin);
556
896
  const title = item.recording?.title ?? item.recordingId;
557
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 1, children: [
558
- /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
897
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
898
+ /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
559
899
  "\u2039 Jobs / ",
560
900
  title
561
901
  ] }),
562
- /* @__PURE__ */ jsxs7(
563
- Box8,
902
+ /* @__PURE__ */ jsxs9(
903
+ Box10,
564
904
  {
565
905
  marginTop: 1,
566
906
  borderStyle: "round",
@@ -568,17 +908,17 @@ function JobDetailView({
568
908
  paddingX: 1,
569
909
  flexDirection: "column",
570
910
  children: [
571
- /* @__PURE__ */ jsxs7(Text8, { color: style.color, bold: true, children: [
911
+ /* @__PURE__ */ jsxs9(Text10, { color: style.color, bold: true, children: [
572
912
  style.label,
573
- item.provider ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${item.provider}` }) : null
913
+ item.provider ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${item.provider}` }) : null
574
914
  ] }),
575
- /* @__PURE__ */ jsx8(StatusLine, { item, spinnerFrame, nowMs })
915
+ /* @__PURE__ */ jsx12(StatusLine, { item, spinnerFrame, nowMs })
576
916
  ]
577
917
  }
578
918
  ),
579
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
580
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Timeline" }),
581
- /* @__PURE__ */ jsx8(
919
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
920
+ /* @__PURE__ */ jsx12(Text10, { bold: true, children: "Timeline" }),
921
+ /* @__PURE__ */ jsx12(
582
922
  TimelineRow,
583
923
  {
584
924
  label: "Enqueued",
@@ -587,7 +927,7 @@ function JobDetailView({
587
927
  nowMs
588
928
  }
589
929
  ),
590
- /* @__PURE__ */ jsx8(
930
+ /* @__PURE__ */ jsx12(
591
931
  TimelineRow,
592
932
  {
593
933
  label: "Started",
@@ -596,7 +936,7 @@ function JobDetailView({
596
936
  nowMs
597
937
  }
598
938
  ),
599
- /* @__PURE__ */ jsx8(
939
+ /* @__PURE__ */ jsx12(
600
940
  TimelineRow,
601
941
  {
602
942
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -608,21 +948,21 @@ function JobDetailView({
608
948
  }
609
949
  )
610
950
  ] }),
611
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
612
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Recording " }),
951
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
952
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Recording " }),
613
953
  title,
614
- item.recording?.durationMs ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
954
+ item.recording?.durationMs ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
615
955
  ] }) }),
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" })
956
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
957
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
958
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
959
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
960
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
961
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "m mac app (soon)" }),
962
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
963
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
624
964
  ] }) }),
625
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
965
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
626
966
  "esc back \xB7 t transcript",
627
967
  item.transcriptId ? "" : " (when ready)",
628
968
  " \xB7 q quit"
@@ -639,24 +979,24 @@ function StatusLine({
639
979
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
640
980
  if (fraction != null) {
641
981
  const pct = Math.round(fraction * 100);
642
- return /* @__PURE__ */ jsxs7(Text8, { children: [
982
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
643
983
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
644
984
  item.recording?.durationMs
645
985
  )}`,
646
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
986
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: elapsed })
647
987
  ] });
648
988
  }
649
989
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
650
- return /* @__PURE__ */ jsxs7(Text8, { children: [
990
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
651
991
  `${spinner} transcribing\u2026`,
652
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
992
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: elapsed })
653
993
  ] });
654
994
  }
655
995
  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 });
996
+ return /* @__PURE__ */ jsx12(Text10, { children: item.transcriptId ? "transcript ready" : "done" });
997
+ if (item.status === "queued") return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "waiting to start\u2026" });
998
+ if (item.status === "failed") return /* @__PURE__ */ jsx12(Text10, { color: "red", children: "transcription failed" });
999
+ return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: item.status });
660
1000
  }
661
1001
  function TimelineRow({
662
1002
  label,
@@ -669,10 +1009,10 @@ function TimelineRow({
669
1009
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
670
1010
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
671
1011
  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
1012
+ return /* @__PURE__ */ jsxs9(Box10, { children: [
1013
+ /* @__PURE__ */ jsx12(Text10, { color, children: ` ${glyph} ` }),
1014
+ /* @__PURE__ */ jsx12(Text10, { dimColor: !done && !running, children: label }),
1015
+ age ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${age}` }) : null
676
1016
  ] });
677
1017
  }
678
1018
  var init_JobDetailView = __esm({
@@ -683,14 +1023,19 @@ var init_JobDetailView = __esm({
683
1023
  });
684
1024
 
685
1025
  // 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";
1026
+ import React4, { useMemo as useMemo2, useState as useState3 } from "react";
1027
+ import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
1028
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
688
1029
  function RecordingDetailView({
689
1030
  item,
690
1031
  nowMs,
691
1032
  transcript,
692
1033
  audio
693
1034
  }) {
1035
+ const size = useTerminalSize();
1036
+ const [tab, setTab] = useState3("summary");
1037
+ const [scroll, setScroll] = useState3(0);
1038
+ const [chapterSel, setChapterSel] = useState3(0);
694
1039
  const style = recordingStatusStyle(item.status);
695
1040
  const links = resolveRecordingLinks(item.recordingId, item.origin);
696
1041
  const title = recordingTitle2(item);
@@ -699,27 +1044,81 @@ function RecordingDetailView({
699
1044
  formatBytes2(item.sizeBytes) || void 0,
700
1045
  item.contentType || void 0
701
1046
  ].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"}` })
1047
+ const ready = typeof transcript === "object";
1048
+ const summary = ready ? transcript.summary : void 0;
1049
+ const segments = ready ? transcript.segments : [];
1050
+ const chapters = summary?.timeline ?? [];
1051
+ const innerWidth = Math.max(10, size.columns - 2);
1052
+ const paneBudget = Math.max(3, size.rows - 12);
1053
+ const segHeights = useMemo2(
1054
+ () => segments.map((seg) => {
1055
+ const prefix = `[${formatClockMs(seg.startMs)}] ${seg.speaker ? `${seg.speaker}: ` : ""}`;
1056
+ return Math.max(1, Math.ceil(displayWidth(prefix + seg.text) / innerWidth));
1057
+ }),
1058
+ [segments, innerWidth]
1059
+ );
1060
+ const segWin = windowByHeights(segHeights, scroll, paneBudget);
1061
+ const chapWin = listWindow(Math.min(chapterSel, Math.max(0, chapters.length - 1)), chapters.length, paneBudget);
1062
+ const page = Math.max(1, paneBudget - 1);
1063
+ const scrollable = tab === "transcript" ? segWin.maxScroll > 0 : false;
1064
+ const jumpToChapter = (index) => {
1065
+ const chapter = chapters[index];
1066
+ if (!chapter) return;
1067
+ const found = segments.findIndex((s) => s.startMs >= chapter.startMs);
1068
+ setScroll(found < 0 ? Math.max(0, segments.length - 1) : found);
1069
+ setTab("transcript");
1070
+ };
1071
+ useInput3((input, key) => {
1072
+ if (!item.activeTranscriptId || !ready) return;
1073
+ if (key.tab) {
1074
+ setTab((t) => TAB_ORDER[(TAB_ORDER.indexOf(t) + (key.shift ? TAB_ORDER.length - 1 : 1)) % TAB_ORDER.length]);
1075
+ return;
1076
+ }
1077
+ if (tab === "summary") return;
1078
+ if (tab === "chapters") {
1079
+ if (key.downArrow || input === "j") setChapterSel((i) => Math.min(chapters.length - 1, i + 1));
1080
+ else if (key.upArrow || input === "k") setChapterSel((i) => Math.max(0, i - 1));
1081
+ else if (key.return || key.rightArrow) jumpToChapter(chapterSel);
1082
+ return;
1083
+ }
1084
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(segWin.maxScroll, s + 1));
1085
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
1086
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(segWin.maxScroll, s + page));
1087
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
1088
+ else if (input === "g") setScroll(0);
1089
+ else if (input === "G") setScroll(segWin.maxScroll);
1090
+ });
1091
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1092
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "\u2039 Recordings" }),
1093
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { bold: true, color: "magenta", children: title }) }),
1094
+ /* @__PURE__ */ jsxs10(Text11, { children: [
1095
+ /* @__PURE__ */ jsx13(Text11, { color: style.color, children: `${style.glyph} ${style.label}` }),
1096
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
1097
+ ] }),
1098
+ meta3 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: meta3 }) : null,
1099
+ /* @__PURE__ */ jsx13(AudioActionRow, { item, audio }),
1100
+ !item.activeTranscriptId ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
1101
+ /* @__PURE__ */ jsx13(TabBar, { active: tab }),
1102
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx13(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx13(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx13(TranscriptPane, { segments, win: segWin }) })
710
1103
  ] }),
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" : "",
1104
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1105
+ ready ? "tab switch" : "",
1106
+ ready && tab === "chapters" ? " \xB7 \u2191\u2193 select \xB7 \u23CE jump" : "",
1107
+ scrollable ? " \xB7 \u2191\u2193 scroll" : "",
1108
+ ready ? " \xB7 " : "",
1109
+ `o open \xB7 d download \xB7 f finder`,
1110
+ item.activeTranscriptId ? " \xB7 t full" : "",
718
1111
  links.webUrl ? " \xB7 w web" : "",
719
- " \xB7 q quit"
1112
+ " \xB7 esc back"
720
1113
  ] }) })
721
1114
  ] });
722
1115
  }
1116
+ function TabBar({ active }) {
1117
+ return /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs10(React4.Fragment, { children: [
1118
+ i > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " " }) : null,
1119
+ tab === active ? /* @__PURE__ */ jsx13(Text11, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
1120
+ ] }, tab)) });
1121
+ }
723
1122
  function AudioActionRow({
724
1123
  item,
725
1124
  audio
@@ -728,149 +1127,185 @@ function AudioActionRow({
728
1127
  const status = audio?.status ?? "idle";
729
1128
  let line;
730
1129
  if (!ready) {
731
- line = /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Audio available once the recording is ready" });
1130
+ line = /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Audio available once the recording is ready" });
732
1131
  } else if (status === "downloading") {
733
- line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Downloading audio\u2026" });
1132
+ line = /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "Downloading audio\u2026" });
734
1133
  } else if (status === "opening") {
735
- line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Opening\u2026" });
1134
+ line = /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "Opening\u2026" });
736
1135
  } else if (status === "error") {
737
- line = /* @__PURE__ */ jsx9(Text9, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
1136
+ line = /* @__PURE__ */ jsx13(Text11, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
738
1137
  } 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 })
1138
+ line = /* @__PURE__ */ jsxs10(Text11, { children: [
1139
+ /* @__PURE__ */ jsx13(Text11, { color: "green", children: "\u2713 Downloaded " }),
1140
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
742
1141
  ] });
743
1142
  } 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" })
1143
+ line = /* @__PURE__ */ jsxs10(Text11, { children: [
1144
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "o" }),
1145
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " open in player \xB7 " }),
1146
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "d" }),
1147
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " download \xB7 " }),
1148
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "f" }),
1149
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " reveal in Finder" })
751
1150
  ] });
752
1151
  }
753
- return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
754
- /* @__PURE__ */ jsx9(Text9, { color: ready ? "cyan" : "gray", children: "\u266A " }),
1152
+ return /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
1153
+ /* @__PURE__ */ jsx13(Text11, { color: ready ? "cyan" : "gray", children: "\u266A " }),
755
1154
  line
756
1155
  ] });
757
1156
  }
758
- function SummaryCard({
1157
+ function SummaryPane({
759
1158
  summary,
760
- transcript,
761
- hasTranscript
1159
+ budget
1160
+ }) {
1161
+ if (!summary || summary.status !== "succeeded" || !summary.tldr) {
1162
+ return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
1163
+ }
1164
+ const points = (summary.keyPoints ?? []).slice(0, Math.max(1, budget - 4));
1165
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1166
+ /* @__PURE__ */ jsx13(Text11, { children: summary.tldr }),
1167
+ points.length > 0 ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
1168
+ ] });
1169
+ }
1170
+ function ChaptersPane({
1171
+ chapters,
1172
+ win,
1173
+ selectedIndex
762
1174
  }) {
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"}` })
1175
+ if (chapters.length === 0) return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "No chapters" });
1176
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1177
+ chapters.slice(win.start, win.end).map((chapter, i) => {
1178
+ const index = win.start + i;
1179
+ const selected = index === selectedIndex;
1180
+ return /* @__PURE__ */ jsxs10(Text11, { wrap: "truncate-end", children: [
1181
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: selected ? "\u25B8 " : " " }),
1182
+ /* @__PURE__ */ jsx13(Text11, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
1183
+ /* @__PURE__ */ jsx13(Text11, { bold: selected, children: chapter.title })
1184
+ ] }, index);
1185
+ }),
1186
+ win.end < chapters.length || win.start > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${selectedIndex + 1} / ${chapters.length}` }) : null
770
1187
  ] });
771
1188
  }
772
- function TranscriptPreview({
1189
+ function TranscriptPane({
773
1190
  segments,
774
- transcript,
775
- hasTranscript
1191
+ win
776
1192
  }) {
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
1193
+ if (segments.length === 0) return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "(no segments)" });
1194
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1195
+ segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs10(Text11, { children: [
1196
+ /* @__PURE__ */ jsx13(Text11, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
1197
+ seg.speaker ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `${seg.speaker} ` }) : null,
1198
+ /* @__PURE__ */ jsx13(Text11, { children: seg.text })
1199
+ ] }, win.start + i)),
1200
+ win.maxScroll > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
792
1201
  ] });
793
1202
  }
794
- var PREVIEW_SEGMENTS;
1203
+ var TAB_ORDER, TAB_LABEL;
795
1204
  var init_RecordingDetailView = __esm({
796
1205
  "src/tui/RecordingDetailView.tsx"() {
797
1206
  "use strict";
798
1207
  init_format();
799
1208
  init_RecordingRow();
800
- PREVIEW_SEGMENTS = 6;
1209
+ init_terminal();
1210
+ TAB_ORDER = ["summary", "chapters", "transcript"];
1211
+ TAB_LABEL = {
1212
+ summary: "Summary",
1213
+ chapters: "Chapters",
1214
+ transcript: "Transcript"
1215
+ };
801
1216
  }
802
1217
  });
803
1218
 
804
1219
  // 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";
1220
+ import { useMemo as useMemo3, useState as useState4 } from "react";
1221
+ import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
1222
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
807
1223
  function TranscriptView({ loading, data, error: error51 }) {
1224
+ const size = useTerminalSize();
1225
+ const [scroll, setScroll] = useState4(0);
1226
+ const segments = data?.segments ?? [];
1227
+ const innerWidth = Math.max(10, size.columns - 2);
1228
+ const heights = useMemo3(
1229
+ () => segments.map((s) => {
1230
+ const prefix = `[${formatClockMs(s.startMs)}] ${s.speaker ? `${s.speaker}: ` : ""}`;
1231
+ return Math.max(1, Math.ceil(displayWidth(prefix + s.text) / innerWidth));
1232
+ }),
1233
+ [segments, innerWidth]
1234
+ );
1235
+ const budget = Math.max(3, size.rows - 3);
1236
+ const win = windowByHeights(heights, scroll, budget);
1237
+ const page = Math.max(1, budget - 1);
1238
+ useInput4((input, key) => {
1239
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(win.maxScroll, s + 1));
1240
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
1241
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(win.maxScroll, s + page));
1242
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
1243
+ else if (input === "g") setScroll(0);
1244
+ else if (input === "G") setScroll(win.maxScroll);
1245
+ });
808
1246
  if (loading) {
809
- return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Loading transcript\u2026" }) });
1247
+ return /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Loading transcript\u2026" }) });
810
1248
  }
811
1249
  if (error51) {
812
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
813
- /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
1250
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1251
+ /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
814
1252
  "! ",
815
1253
  error51
816
1254
  ] }),
817
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "q / esc / \u2190 back" })
1255
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "q / esc / \u2190 back" })
818
1256
  ] });
819
1257
  }
820
1258
  if (!data) {
821
- return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "No transcript." }) });
822
- }
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: [
1259
+ return /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "No transcript." }) });
1260
+ }
1261
+ const title = data.summary?.title ?? "Transcript";
1262
+ const total = segments.length;
1263
+ const more = win.maxScroll > 0;
1264
+ const position = total === 0 ? "" : `${win.start + 1}\u2013${win.end} / ${total}`;
1265
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1266
+ /* @__PURE__ */ jsxs11(Text12, { bold: true, color: "magenta", children: [
1267
+ title,
1268
+ more ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${position}` }) : null
1269
+ ] }),
1270
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx14(Text12, { children: data.text }) : segments.slice(win.start, win.end).map((segment, index) => /* @__PURE__ */ jsxs11(Text12, { children: [
1271
+ /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
829
1272
  "[",
830
1273
  formatClockMs(segment.startMs),
831
1274
  "] "
832
1275
  ] }),
833
- segment.speaker ? /* @__PURE__ */ jsxs9(Text10, { color: "cyan", children: [
1276
+ segment.speaker ? /* @__PURE__ */ jsxs11(Text12, { color: "cyan", children: [
834
1277
  segment.speaker,
835
1278
  ": "
836
1279
  ] }) : null,
837
1280
  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" }) })
1281
+ ] }, win.start + index)) }),
1282
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
1283
+ more ? "\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 g/G top/bottom \xB7 " : "",
1284
+ "q / esc / \u2190 back"
1285
+ ] }) })
844
1286
  ] });
845
1287
  }
846
1288
  var init_TranscriptView = __esm({
847
1289
  "src/tui/TranscriptView.tsx"() {
848
1290
  "use strict";
849
1291
  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";
1292
+ init_terminal();
861
1293
  }
862
1294
  });
863
1295
 
864
1296
  // 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";
1297
+ import { useCallback, useEffect as useEffect2, useState as useState5 } from "react";
1298
+ import { Box as Box13, Text as Text13, useApp, useInput as useInput5 } from "ink";
1299
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
868
1300
  function AppShell({
869
1301
  fetchJobs,
870
1302
  fetchTranscript,
871
1303
  fetchRecordings,
872
1304
  fetchDashboardStats,
1305
+ fetchAccountStatus,
873
1306
  recordingAudio,
1307
+ listDownloadedRecordingIds,
1308
+ startLiveRecord,
874
1309
  initialView = "overview",
875
1310
  openUrl: openUrl2,
876
1311
  copyText: copyText2,
@@ -880,27 +1315,76 @@ function AppShell({
880
1315
  }) {
881
1316
  const { exit } = useApp();
882
1317
  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(
1318
+ const [jobs, setJobs] = useState5([]);
1319
+ const [recordings, setRecordings] = useState5([]);
1320
+ const [recordingsNextCursor, setRecordingsNextCursor] = useState5(null);
1321
+ const [recordingsTotalCount, setRecordingsTotalCount] = useState5(void 0);
1322
+ const [stats, setStats] = useState5(void 0);
1323
+ const [accountStatus, setAccountStatus] = useState5("loading");
1324
+ const [origin, setOrigin] = useState5("");
1325
+ const [stack, setStack] = useState5([{ kind: initialView }]);
1326
+ const [selected, setSelected] = useState5(0);
1327
+ const [spinnerFrame, setSpinnerFrame] = useState5(0);
1328
+ const [loadingMoreRecordings, setLoadingMoreRecordings] = useState5(false);
1329
+ const [loadError, setLoadError] = useState5(void 0);
1330
+ const [notice, setNotice] = useState5(void 0);
1331
+ const [summaryCache, setSummaryCache] = useState5(() => /* @__PURE__ */ new Map());
1332
+ const [transcriptCache, setTranscriptCache] = useState5(
897
1333
  () => /* @__PURE__ */ new Map()
898
1334
  );
899
- const [audioCache, setAudioCache] = useState(() => /* @__PURE__ */ new Map());
1335
+ const [audioCache, setAudioCache] = useState5(() => /* @__PURE__ */ new Map());
1336
+ const [downloadedIds, setDownloadedIds] = useState5(() => /* @__PURE__ */ new Set());
1337
+ const [liveRecord, setLiveRecord] = useState5(void 0);
1338
+ const refreshDownloadedIds = useCallback(async () => {
1339
+ if (!listDownloadedRecordingIds) return;
1340
+ try {
1341
+ setDownloadedIds(await listDownloadedRecordingIds());
1342
+ } catch {
1343
+ }
1344
+ }, [listDownloadedRecordingIds]);
1345
+ useEffect2(() => {
1346
+ void refreshDownloadedIds();
1347
+ }, [refreshDownloadedIds]);
900
1348
  const screen = stack[stack.length - 1];
1349
+ const stopLiveRecord = useCallback(async () => {
1350
+ const current = liveRecord;
1351
+ if (current?.kind === "live") {
1352
+ setLiveRecord({ kind: "starting" });
1353
+ try {
1354
+ await current.session.stop();
1355
+ } catch (error51) {
1356
+ setNotice(error51 instanceof Error ? error51.message : String(error51));
1357
+ }
1358
+ }
1359
+ setLiveRecord(void 0);
1360
+ setStack([{ kind: "overview" }]);
1361
+ }, [liveRecord]);
1362
+ useEffect2(() => {
1363
+ if (screen.kind !== "record") return;
1364
+ if (!startLiveRecord) {
1365
+ setLiveRecord({ kind: "error", message: "Live recording is not available" });
1366
+ return;
1367
+ }
1368
+ let cancelled = false;
1369
+ setLiveRecord({ kind: "starting" });
1370
+ startLiveRecord().then((session) => {
1371
+ if (cancelled) {
1372
+ void session.stop();
1373
+ return;
1374
+ }
1375
+ setLiveRecord({ kind: "live", session });
1376
+ }).catch((error51) => {
1377
+ if (!cancelled) {
1378
+ setLiveRecord({ kind: "error", message: error51 instanceof Error ? error51.message : String(error51) });
1379
+ }
1380
+ });
1381
+ return () => {
1382
+ cancelled = true;
1383
+ };
1384
+ }, [screen.kind, startLiveRecord]);
901
1385
  const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
902
1386
  const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
903
- useEffect(() => {
1387
+ useEffect2(() => {
904
1388
  if (!peekTranscriptId || summaryCache.has(peekTranscriptId)) return;
905
1389
  let cancelled = false;
906
1390
  const timer = setTimeout(() => {
@@ -918,10 +1402,11 @@ function AppShell({
918
1402
  }, [peekTranscriptId, fetchTranscript]);
919
1403
  const peekSummary = peekTranscriptId ? summaryCache.get(peekTranscriptId) : void 0;
920
1404
  const refresh = useCallback(async ({ resetRecordings = false } = {}) => {
921
- const [jobsR, recR, statsR] = await Promise.allSettled([
1405
+ const [jobsR, recR, statsR, accountR] = await Promise.allSettled([
922
1406
  fetchJobs(),
923
1407
  resetRecordings && fetchRecordings ? fetchRecordings({ limit: RECORDINGS_PAGE_SIZE }) : Promise.resolve(void 0),
924
- fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
1408
+ fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0),
1409
+ fetchAccountStatus ? fetchAccountStatus() : Promise.resolve(void 0)
925
1410
  ]);
926
1411
  if (jobsR.status === "fulfilled") {
927
1412
  setJobs(jobsR.value.items);
@@ -936,7 +1421,12 @@ function AppShell({
936
1421
  setRecordingsTotalCount(recR.value.totalCount);
937
1422
  }
938
1423
  if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
939
- }, [fetchJobs, fetchRecordings, fetchDashboardStats]);
1424
+ if (accountR.status === "fulfilled") {
1425
+ setAccountStatus(accountR.value);
1426
+ } else {
1427
+ setAccountStatus("error");
1428
+ }
1429
+ }, [fetchJobs, fetchRecordings, fetchDashboardStats, fetchAccountStatus]);
940
1430
  const loadMoreRecordings = useCallback(async () => {
941
1431
  if (!fetchRecordings || !recordingsNextCursor || loadingMoreRecordings) return;
942
1432
  setLoadingMoreRecordings(true);
@@ -962,13 +1452,13 @@ function AppShell({
962
1452
  setLoadingMoreRecordings(false);
963
1453
  }
964
1454
  }, [fetchRecordings, loadingMoreRecordings, recordingsNextCursor]);
965
- useEffect(() => {
1455
+ useEffect2(() => {
966
1456
  void refresh({ resetRecordings: true });
967
1457
  const id = setInterval(() => void refresh(), pollMs);
968
1458
  return () => clearInterval(id);
969
1459
  }, [refresh, pollMs]);
970
1460
  const hasRunning = jobs.some((item) => item.status === "running");
971
- useEffect(() => {
1461
+ useEffect2(() => {
972
1462
  if (!hasRunning) return;
973
1463
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
974
1464
  return () => clearInterval(id);
@@ -981,12 +1471,12 @@ function AppShell({
981
1471
  jobStatusByRecording.set(job.recordingId, job.status);
982
1472
  }
983
1473
  }
984
- const listLength = screen.kind === "jobs" ? jobs.length : recordings.length;
985
- useEffect(() => {
1474
+ const listLength = screen.kind === "jobs" ? jobs.length : screen.kind === "overview" ? recordings.length : 0;
1475
+ useEffect2(() => {
986
1476
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
987
1477
  }, [listLength]);
988
1478
  const visibleRecordingRows = Math.max(3, size.rows - 6);
989
- useEffect(() => {
1479
+ useEffect2(() => {
990
1480
  if (screen.kind !== "overview" || !recordingsNextCursor) return;
991
1481
  const nearLoadedEnd = recordings.length - selected <= RECORDINGS_PREFETCH_REMAINING;
992
1482
  const underfilledViewport = recordings.length < visibleRecordingRows;
@@ -1019,7 +1509,7 @@ function AppShell({
1019
1509
  [fetchTranscript]
1020
1510
  );
1021
1511
  const detailTranscriptId = screen.kind === "recordingDetail" ? recordings.find((r) => r.recordingId === screen.recordingId)?.activeTranscriptId : void 0;
1022
- useEffect(() => {
1512
+ useEffect2(() => {
1023
1513
  if (!detailTranscriptId || transcriptCache.has(detailTranscriptId)) return;
1024
1514
  let cancelled = false;
1025
1515
  setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "loading"));
@@ -1050,6 +1540,7 @@ function AppShell({
1050
1540
  await recordingAudio.revealInFinder(localPath);
1051
1541
  }
1052
1542
  setAudio(recordingId, { status: "ready", localPath });
1543
+ void refreshDownloadedIds();
1053
1544
  } catch (error51) {
1054
1545
  setAudio(recordingId, {
1055
1546
  status: "error",
@@ -1057,7 +1548,7 @@ function AppShell({
1057
1548
  });
1058
1549
  }
1059
1550
  },
1060
- [recordingAudio]
1551
+ [recordingAudio, refreshDownloadedIds]
1061
1552
  );
1062
1553
  const goTab = (tab2) => {
1063
1554
  setStack([{ kind: tab2 }]);
@@ -1065,12 +1556,18 @@ function AppShell({
1065
1556
  setNotice(void 0);
1066
1557
  };
1067
1558
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
1068
- useInput((input, key) => {
1559
+ useInput5((input, key) => {
1069
1560
  setNotice(void 0);
1561
+ if (screen.kind === "record") {
1562
+ if (input === "q" || key.escape || key.leftArrow) void stopLiveRecord();
1563
+ return;
1564
+ }
1070
1565
  if (input === "q") return exit();
1071
1566
  if (key.escape || key.leftArrow) return back();
1072
1567
  if (input === "1") return goTab("overview");
1073
1568
  if (input === "2") return goTab("jobs");
1569
+ if (input === "3") return goTab("account");
1570
+ if (input === "4") return goTab("record");
1074
1571
  if (input === "r") return void refresh({ resetRecordings: true });
1075
1572
  if (screen.kind === "overview") {
1076
1573
  if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
@@ -1117,18 +1614,18 @@ function AppShell({
1117
1614
  }
1118
1615
  });
1119
1616
  if (screen.kind === "transcript") {
1120
- return /* @__PURE__ */ jsx11(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1617
+ return /* @__PURE__ */ jsx15(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1121
1618
  }
1122
1619
  if (screen.kind === "jobDetail") {
1123
1620
  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() }) });
1621
+ if (!job) return /* @__PURE__ */ jsx15(Missing, { label: "Job" });
1622
+ return /* @__PURE__ */ jsx15(Detail, { notice, children: /* @__PURE__ */ jsx15(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1126
1623
  }
1127
1624
  if (screen.kind === "recordingDetail") {
1128
1625
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
1129
- if (!rec) return /* @__PURE__ */ jsx11(Missing, { label: "Recording" });
1626
+ if (!rec) return /* @__PURE__ */ jsx15(Missing, { label: "Recording" });
1130
1627
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
1131
- return /* @__PURE__ */ jsx11(Detail, { notice, children: /* @__PURE__ */ jsx11(
1628
+ return /* @__PURE__ */ jsx15(Detail, { notice, children: /* @__PURE__ */ jsx15(
1132
1629
  RecordingDetailView,
1133
1630
  {
1134
1631
  item: rec,
@@ -1138,7 +1635,20 @@ function AppShell({
1138
1635
  }
1139
1636
  ) });
1140
1637
  }
1141
- const tab = screen.kind === "jobs" ? "jobs" : "overview";
1638
+ if (screen.kind === "record") {
1639
+ if (liveRecord?.kind === "live") {
1640
+ return /* @__PURE__ */ jsx15(LiveCaptionsScreen, { source: liveRecord.session.source, now });
1641
+ }
1642
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1643
+ /* @__PURE__ */ jsx15(Header, { active: "record" }),
1644
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingTop: 1, children: liveRecord?.kind === "error" ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
1645
+ /* @__PURE__ */ jsx15(Text13, { color: "red", children: "Couldn't start live recording" }),
1646
+ /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: liveRecord.message })
1647
+ ] }) : /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "Starting live recording\u2026" }) }),
1648
+ /* @__PURE__ */ jsx15(Footer, { keys: "q / esc / \u2190 back" })
1649
+ ] });
1650
+ }
1651
+ const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "account" ? "account" : "overview";
1142
1652
  let body;
1143
1653
  let position = "";
1144
1654
  if (screen.kind === "overview") {
@@ -1153,7 +1663,7 @@ function AppShell({
1153
1663
  const showPeek = size.columns >= 100;
1154
1664
  const peekWidth = showPeek ? 34 : 0;
1155
1665
  const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
1156
- body = /* @__PURE__ */ jsx11(
1666
+ body = /* @__PURE__ */ jsx15(
1157
1667
  OverviewView,
1158
1668
  {
1159
1669
  recordings: recordings.slice(win.start, win.end),
@@ -1163,6 +1673,7 @@ function AppShell({
1163
1673
  nowMs: now(),
1164
1674
  columns: listColumns,
1165
1675
  jobStatusByRecording,
1676
+ downloadedRecordingIds: downloadedIds,
1166
1677
  spinnerFrame,
1167
1678
  peekItem: recordings[selected],
1168
1679
  peekSummary,
@@ -1170,10 +1681,13 @@ function AppShell({
1170
1681
  peekWidth
1171
1682
  }
1172
1683
  );
1684
+ } else if (screen.kind === "account") {
1685
+ position = "";
1686
+ body = /* @__PURE__ */ jsx15(AccountView, { status: accountStatus });
1173
1687
  } else {
1174
1688
  const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
1175
1689
  position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
1176
- body = /* @__PURE__ */ jsx11(
1690
+ body = /* @__PURE__ */ jsx15(
1177
1691
  JobsView,
1178
1692
  {
1179
1693
  items: jobs.slice(win.start, win.end),
@@ -1182,47 +1696,49 @@ function AppShell({
1182
1696
  }
1183
1697
  );
1184
1698
  }
1185
- 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: [
1699
+ const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 1 overview \xB7 3 account \xB7 4 record \xB7 r refresh \xB7 q quit` : screen.kind === "account" ? "3 account \xB7 1 overview \xB7 2 jobs \xB7 4 record \xB7 r refresh \xB7 q quit" : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 2 jobs \xB7 3 account \xB7 4 record \xB7 r refresh \xB7 q quit`;
1700
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1701
+ /* @__PURE__ */ jsx15(Header, { active: tab }),
1702
+ /* @__PURE__ */ jsxs12(Box13, { flexGrow: 1, flexDirection: "column", children: [
1189
1703
  body,
1190
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
1704
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { color: "red", children: [
1191
1705
  "! ",
1192
1706
  loadError
1193
1707
  ] }) }) : null
1194
1708
  ] }),
1195
- /* @__PURE__ */ jsx11(Footer, { keys: footerKeys })
1709
+ /* @__PURE__ */ jsx15(Footer, { keys: footerKeys })
1196
1710
  ] });
1197
1711
  }
1198
1712
  function Detail({
1199
1713
  notice,
1200
1714
  children
1201
1715
  }) {
1202
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
1716
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
1203
1717
  children,
1204
- notice ? /* @__PURE__ */ jsx11(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "green", children: notice }) }) : null
1718
+ notice ? /* @__PURE__ */ jsx15(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx15(Text13, { color: "green", children: notice }) }) : null
1205
1719
  ] });
1206
1720
  }
1207
1721
  function Missing({ label }) {
1208
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1209
- /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1722
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 1, children: [
1723
+ /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
1210
1724
  label,
1211
1725
  " no longer in the list."
1212
1726
  ] }),
1213
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "esc back \xB7 q quit" })
1727
+ /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "esc back \xB7 q quit" })
1214
1728
  ] });
1215
1729
  }
1216
1730
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
1217
1731
  var init_AppShell = __esm({
1218
1732
  "src/tui/AppShell.tsx"() {
1219
1733
  "use strict";
1734
+ init_AccountView();
1220
1735
  init_chrome();
1221
1736
  init_JobsView();
1222
1737
  init_OverviewView();
1223
1738
  init_JobDetailView();
1224
1739
  init_RecordingDetailView();
1225
1740
  init_TranscriptView();
1741
+ init_LiveCaptionsScreen();
1226
1742
  init_format();
1227
1743
  init_terminal();
1228
1744
  RECORDINGS_PAGE_SIZE = 50;
@@ -1241,33 +1757,36 @@ __export(tui_exports, {
1241
1757
  runDashboard: () => runDashboard,
1242
1758
  useTerminalSize: () => useTerminalSize
1243
1759
  });
1244
- import React3 from "react";
1245
- import { render } from "ink";
1246
- import { spawn as spawn2 } from "child_process";
1760
+ import React7 from "react";
1761
+ import { render as render2 } from "ink";
1762
+ import { spawn as spawn3 } from "child_process";
1247
1763
  function openUrl(url2) {
1248
1764
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1249
1765
  try {
1250
- spawn2(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1766
+ spawn3(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1251
1767
  } catch {
1252
1768
  }
1253
1769
  }
1254
1770
  function copyText(text) {
1255
1771
  if (process.platform !== "darwin") return;
1256
1772
  try {
1257
- const child = spawn2("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1773
+ const child = spawn3("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1258
1774
  child.stdin.end(text);
1259
1775
  } catch {
1260
1776
  }
1261
1777
  }
1262
1778
  async function runDashboard(deps) {
1263
- const renderApp = deps.renderApp ?? render;
1779
+ const renderApp = deps.renderApp ?? render2;
1264
1780
  const app = renderApp(
1265
- React3.createElement(AppShell, {
1781
+ React7.createElement(AppShell, {
1266
1782
  fetchJobs: deps.fetchJobs,
1267
1783
  fetchTranscript: deps.fetchTranscript,
1268
1784
  fetchRecordings: deps.fetchRecordings,
1269
1785
  fetchDashboardStats: deps.fetchDashboardStats,
1786
+ fetchAccountStatus: deps.fetchAccountStatus,
1270
1787
  recordingAudio: deps.recordingAudio,
1788
+ listDownloadedRecordingIds: deps.listDownloadedRecordingIds,
1789
+ startLiveRecord: deps.startLiveRecord,
1271
1790
  initialView: deps.initialView ?? "overview",
1272
1791
  openUrl,
1273
1792
  copyText
@@ -1295,7 +1814,7 @@ var init_tui = __esm({
1295
1814
 
1296
1815
  // src/cli.ts
1297
1816
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
1298
- import os4 from "os";
1817
+ import os5 from "os";
1299
1818
 
1300
1819
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
1301
1820
  var external_exports = {};
@@ -2063,10 +2582,10 @@ function mergeDefs(...defs) {
2063
2582
  function cloneDef(schema) {
2064
2583
  return mergeDefs(schema._zod.def);
2065
2584
  }
2066
- function getElementAtPath(obj, path4) {
2067
- if (!path4)
2585
+ function getElementAtPath(obj, path6) {
2586
+ if (!path6)
2068
2587
  return obj;
2069
- return path4.reduce((acc, key) => acc?.[key], obj);
2588
+ return path6.reduce((acc, key) => acc?.[key], obj);
2070
2589
  }
2071
2590
  function promiseAllObject(promisesObj) {
2072
2591
  const keys = Object.keys(promisesObj);
@@ -2475,11 +2994,11 @@ function explicitlyAborted(x, startIndex = 0) {
2475
2994
  }
2476
2995
  return false;
2477
2996
  }
2478
- function prefixIssues(path4, issues) {
2997
+ function prefixIssues(path6, issues) {
2479
2998
  return issues.map((iss) => {
2480
2999
  var _a3;
2481
3000
  (_a3 = iss).path ?? (_a3.path = []);
2482
- iss.path.unshift(path4);
3001
+ iss.path.unshift(path6);
2483
3002
  return iss;
2484
3003
  });
2485
3004
  }
@@ -2626,16 +3145,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2626
3145
  }
2627
3146
  function formatError(error51, mapper = (issue2) => issue2.message) {
2628
3147
  const fieldErrors = { _errors: [] };
2629
- const processError = (error52, path4 = []) => {
3148
+ const processError = (error52, path6 = []) => {
2630
3149
  for (const issue2 of error52.issues) {
2631
3150
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2632
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
3151
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2633
3152
  } else if (issue2.code === "invalid_key") {
2634
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
3153
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2635
3154
  } else if (issue2.code === "invalid_element") {
2636
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
3155
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2637
3156
  } else {
2638
- const fullpath = [...path4, ...issue2.path];
3157
+ const fullpath = [...path6, ...issue2.path];
2639
3158
  if (fullpath.length === 0) {
2640
3159
  fieldErrors._errors.push(mapper(issue2));
2641
3160
  } else {
@@ -2662,17 +3181,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2662
3181
  }
2663
3182
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2664
3183
  const result = { errors: [] };
2665
- const processError = (error52, path4 = []) => {
3184
+ const processError = (error52, path6 = []) => {
2666
3185
  var _a3, _b;
2667
3186
  for (const issue2 of error52.issues) {
2668
3187
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2669
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
3188
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2670
3189
  } else if (issue2.code === "invalid_key") {
2671
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
3190
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2672
3191
  } else if (issue2.code === "invalid_element") {
2673
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
3192
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2674
3193
  } else {
2675
- const fullpath = [...path4, ...issue2.path];
3194
+ const fullpath = [...path6, ...issue2.path];
2676
3195
  if (fullpath.length === 0) {
2677
3196
  result.errors.push(mapper(issue2));
2678
3197
  continue;
@@ -2704,8 +3223,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2704
3223
  }
2705
3224
  function toDotPath(_path) {
2706
3225
  const segs = [];
2707
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2708
- for (const seg of path4) {
3226
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3227
+ for (const seg of path6) {
2709
3228
  if (typeof seg === "number")
2710
3229
  segs.push(`[${seg}]`);
2711
3230
  else if (typeof seg === "symbol")
@@ -15397,13 +15916,13 @@ function resolveRef(ref, ctx) {
15397
15916
  if (!ref.startsWith("#")) {
15398
15917
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15399
15918
  }
15400
- const path4 = ref.slice(1).split("/").filter(Boolean);
15401
- if (path4.length === 0) {
15919
+ const path6 = ref.slice(1).split("/").filter(Boolean);
15920
+ if (path6.length === 0) {
15402
15921
  return ctx.rootSchema;
15403
15922
  }
15404
15923
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15405
- if (path4[0] === defsKey) {
15406
- const key = path4[1];
15924
+ if (path6[0] === defsKey) {
15925
+ const key = path6[1];
15407
15926
  if (!key || !ctx.defs[key]) {
15408
15927
  throw new Error(`Reference not found: ${ref}`);
15409
15928
  }
@@ -15882,20 +16401,262 @@ var authImportDataSchema = external_exports.object({
15882
16401
  origin: external_exports.string(),
15883
16402
  source: external_exports.literal("macos-keychain")
15884
16403
  });
15885
- var uploadSuccessSchema = external_exports.object({
15886
- filePath: external_exports.string(),
15887
- recordingId: external_exports.string(),
15888
- jobId: external_exports.string().optional(),
15889
- transcriptId: external_exports.string().optional(),
15890
- status: external_exports.string(),
15891
- origin: external_exports.string()
16404
+ var planTierSchema = external_exports.enum(["free", "starter", "pro", "business", "unlimited"]);
16405
+ var billingStatusDataSchema = external_exports.object({
16406
+ origin: external_exports.string(),
16407
+ tier: planTierSchema,
16408
+ periodStart: external_exports.number().int(),
16409
+ periodEnd: external_exports.number().int(),
16410
+ storageBytes: external_exports.number().int().nonnegative(),
16411
+ storageCapBytes: external_exports.number().int().nonnegative().nullable(),
16412
+ minutesUsed: external_exports.number().nonnegative(),
16413
+ batchMinutesUsed: external_exports.number().nonnegative(),
16414
+ realtimeMinutesUsed: external_exports.number().nonnegative(),
16415
+ minutesCap: external_exports.number().nonnegative().nullable(),
16416
+ isOverStorage: external_exports.boolean(),
16417
+ isOverMinutes: external_exports.boolean()
15892
16418
  });
15893
- var uploadFailureSchema = external_exports.object({
15894
- filePath: external_exports.string(),
15895
- error: cliErrorDescriptorSchema
16419
+ var accountStatusDataSchema = external_exports.object({
16420
+ origin: external_exports.string(),
16421
+ loggedIn: external_exports.boolean(),
16422
+ email: external_exports.string().optional(),
16423
+ userId: external_exports.string().optional(),
16424
+ localStore: external_exports.object({
16425
+ path: external_exports.string(),
16426
+ accountScopedArtifacts: external_exports.number().int().nonnegative(),
16427
+ unattributedArtifacts: external_exports.number().int().nonnegative()
16428
+ }),
16429
+ billing: billingStatusDataSchema.optional()
15896
16430
  });
15897
- var uploadBatchDataSchema = external_exports.object({
15898
- successes: external_exports.array(uploadSuccessSchema),
16431
+ var SIDECAR_PROTOCOL_VERSION = 1;
16432
+ var sidecarJsonRpcIdSchema = external_exports.union([external_exports.string(), external_exports.number().int()]);
16433
+ var sidecarCapabilitySchema = external_exports.enum([
16434
+ "recording.capture",
16435
+ "recording.upload",
16436
+ "live_captions.stream",
16437
+ "local_artifacts.index"
16438
+ ]);
16439
+ var sidecarAccountSchema = external_exports.object({
16440
+ backendOrigin: external_exports.string(),
16441
+ userId: external_exports.string(),
16442
+ email: external_exports.string().optional()
16443
+ });
16444
+ var sidecarClientInfoSchema = external_exports.object({
16445
+ name: external_exports.string(),
16446
+ version: external_exports.string()
16447
+ });
16448
+ var sidecarInfoSchema = external_exports.object({
16449
+ name: external_exports.string(),
16450
+ version: external_exports.string()
16451
+ });
16452
+ var sidecarRecordingOptionsSchema = external_exports.object({
16453
+ includeSystemAudio: external_exports.boolean().default(true),
16454
+ includeMicrophone: external_exports.boolean().default(true),
16455
+ liveCaptions: external_exports.boolean().default(false),
16456
+ translationLanguage: external_exports.string().optional(),
16457
+ transcriptionLanguage: external_exports.string().optional(),
16458
+ title: external_exports.string().optional()
16459
+ });
16460
+ var sidecarRecordingStateSchema = external_exports.enum([
16461
+ "idle",
16462
+ "starting",
16463
+ "recording",
16464
+ "stopping",
16465
+ "finalizing",
16466
+ "uploading",
16467
+ "completed",
16468
+ "failed",
16469
+ "cancelled"
16470
+ ]);
16471
+ var sidecarLocalArtifactKindSchema = external_exports.enum([
16472
+ "recording_session",
16473
+ "download",
16474
+ "live_caption_draft"
16475
+ ]);
16476
+ var sidecarLocalArtifactSchema = external_exports.object({
16477
+ kind: sidecarLocalArtifactKindSchema,
16478
+ localPath: external_exports.string(),
16479
+ remoteId: external_exports.string().optional(),
16480
+ metadata: external_exports.unknown().optional()
16481
+ });
16482
+ var sidecarHandshakeParamsSchema = external_exports.object({
16483
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16484
+ client: sidecarClientInfoSchema,
16485
+ account: sidecarAccountSchema.optional(),
16486
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16487
+ });
16488
+ var sidecarHandshakeResultSchema = external_exports.object({
16489
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16490
+ sidecar: sidecarInfoSchema,
16491
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16492
+ });
16493
+ var sidecarRecordingStartParamsSchema = external_exports.object({
16494
+ account: sidecarAccountSchema,
16495
+ options: sidecarRecordingOptionsSchema
16496
+ });
16497
+ var sidecarRecordingStartResultSchema = external_exports.object({
16498
+ sessionId: external_exports.string(),
16499
+ state: sidecarRecordingStateSchema,
16500
+ localSessionRef: external_exports.string().optional()
16501
+ });
16502
+ var sidecarSessionParamsSchema = external_exports.object({
16503
+ sessionId: external_exports.string()
16504
+ });
16505
+ var sidecarRecordingStopResultSchema = external_exports.object({
16506
+ sessionId: external_exports.string(),
16507
+ state: sidecarRecordingStateSchema,
16508
+ recordingId: external_exports.string().optional(),
16509
+ localSessionRef: external_exports.string().optional(),
16510
+ artifacts: external_exports.array(sidecarLocalArtifactSchema).optional()
16511
+ });
16512
+ var sidecarRecordingStatusResultSchema = external_exports.object({
16513
+ sessionId: external_exports.string(),
16514
+ state: sidecarRecordingStateSchema,
16515
+ recordingId: external_exports.string().optional(),
16516
+ localSessionRef: external_exports.string().optional()
16517
+ });
16518
+ var sidecarRequestSchema = external_exports.discriminatedUnion("method", [
16519
+ external_exports.object({
16520
+ jsonrpc: external_exports.literal("2.0"),
16521
+ id: sidecarJsonRpcIdSchema,
16522
+ method: external_exports.literal("recappi.handshake"),
16523
+ params: sidecarHandshakeParamsSchema
16524
+ }),
16525
+ external_exports.object({
16526
+ jsonrpc: external_exports.literal("2.0"),
16527
+ id: sidecarJsonRpcIdSchema,
16528
+ method: external_exports.literal("recappi.recording.start"),
16529
+ params: sidecarRecordingStartParamsSchema
16530
+ }),
16531
+ external_exports.object({
16532
+ jsonrpc: external_exports.literal("2.0"),
16533
+ id: sidecarJsonRpcIdSchema,
16534
+ method: external_exports.literal("recappi.recording.stop"),
16535
+ params: sidecarSessionParamsSchema
16536
+ }),
16537
+ external_exports.object({
16538
+ jsonrpc: external_exports.literal("2.0"),
16539
+ id: sidecarJsonRpcIdSchema,
16540
+ method: external_exports.literal("recappi.recording.cancel"),
16541
+ params: sidecarSessionParamsSchema
16542
+ }),
16543
+ external_exports.object({
16544
+ jsonrpc: external_exports.literal("2.0"),
16545
+ id: sidecarJsonRpcIdSchema,
16546
+ method: external_exports.literal("recappi.recording.status"),
16547
+ params: sidecarSessionParamsSchema
16548
+ })
16549
+ ]);
16550
+ var sidecarErrorSchema = external_exports.object({
16551
+ code: external_exports.number().int(),
16552
+ message: external_exports.string(),
16553
+ data: external_exports.unknown().optional()
16554
+ });
16555
+ var sidecarResponseSchema = external_exports.union([
16556
+ external_exports.object({
16557
+ jsonrpc: external_exports.literal("2.0"),
16558
+ id: sidecarJsonRpcIdSchema,
16559
+ result: external_exports.unknown()
16560
+ }),
16561
+ external_exports.object({
16562
+ jsonrpc: external_exports.literal("2.0"),
16563
+ id: sidecarJsonRpcIdSchema,
16564
+ error: sidecarErrorSchema
16565
+ })
16566
+ ]);
16567
+ var sidecarEventSchema = external_exports.discriminatedUnion("type", [
16568
+ external_exports.object({
16569
+ type: external_exports.literal("ready"),
16570
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16571
+ sidecar: sidecarInfoSchema
16572
+ }),
16573
+ external_exports.object({
16574
+ type: external_exports.literal("recording.state"),
16575
+ sessionId: external_exports.string(),
16576
+ state: sidecarRecordingStateSchema,
16577
+ recordingId: external_exports.string().optional(),
16578
+ localSessionRef: external_exports.string().optional(),
16579
+ message: external_exports.string().optional()
16580
+ }),
16581
+ external_exports.object({
16582
+ type: external_exports.literal("audio.level"),
16583
+ sessionId: external_exports.string(),
16584
+ input: external_exports.enum(["system", "microphone", "mixed"]),
16585
+ rmsDb: external_exports.number().optional(),
16586
+ peakDb: external_exports.number().optional(),
16587
+ at: external_exports.number().int().optional()
16588
+ }),
16589
+ external_exports.object({
16590
+ type: external_exports.literal("live_caption.delta"),
16591
+ sessionId: external_exports.string(),
16592
+ stream: external_exports.enum(["source", "translation"]),
16593
+ text: external_exports.string(),
16594
+ isFinal: external_exports.boolean().optional(),
16595
+ segmentId: external_exports.string().optional(),
16596
+ speaker: external_exports.string().optional(),
16597
+ language: external_exports.string().optional(),
16598
+ atMs: external_exports.number().int().nonnegative().optional(),
16599
+ startMs: external_exports.number().nonnegative().optional(),
16600
+ endMs: external_exports.number().nonnegative().optional()
16601
+ }),
16602
+ external_exports.object({
16603
+ type: external_exports.literal("local_artifact.upserted"),
16604
+ sessionId: external_exports.string().optional(),
16605
+ artifact: sidecarLocalArtifactSchema
16606
+ }),
16607
+ external_exports.object({
16608
+ type: external_exports.literal("error"),
16609
+ sessionId: external_exports.string().optional(),
16610
+ code: external_exports.string(),
16611
+ message: external_exports.string(),
16612
+ retryable: external_exports.boolean().optional()
16613
+ })
16614
+ ]);
16615
+ var sidecarNotificationSchema = external_exports.object({
16616
+ jsonrpc: external_exports.literal("2.0"),
16617
+ method: external_exports.literal("recappi.event"),
16618
+ params: sidecarEventSchema
16619
+ });
16620
+ var sidecarMessageSchema = external_exports.union([
16621
+ sidecarRequestSchema,
16622
+ sidecarResponseSchema,
16623
+ sidecarNotificationSchema
16624
+ ]);
16625
+ var recordCommandDataSchema = external_exports.object({
16626
+ origin: external_exports.string(),
16627
+ userId: external_exports.string(),
16628
+ live: external_exports.boolean(),
16629
+ sessionId: external_exports.string(),
16630
+ state: sidecarRecordingStateSchema,
16631
+ recordingId: external_exports.string().optional(),
16632
+ localSessionRef: external_exports.string().optional(),
16633
+ sidecar: sidecarInfoSchema.optional(),
16634
+ artifacts: external_exports.array(sidecarLocalArtifactSchema)
16635
+ });
16636
+ var audioCommandDataSchema = external_exports.object({
16637
+ origin: external_exports.string(),
16638
+ recordingId: external_exports.string(),
16639
+ localPath: external_exports.string(),
16640
+ action: external_exports.enum(["download", "open", "reveal"]),
16641
+ reused: external_exports.boolean(),
16642
+ artifactId: external_exports.number().int().positive().optional(),
16643
+ contentType: external_exports.string().optional(),
16644
+ contentLength: external_exports.number().int().nonnegative().optional()
16645
+ });
16646
+ var uploadSuccessSchema = external_exports.object({
16647
+ filePath: external_exports.string(),
16648
+ recordingId: external_exports.string(),
16649
+ jobId: external_exports.string().optional(),
16650
+ transcriptId: external_exports.string().optional(),
16651
+ status: external_exports.string(),
16652
+ origin: external_exports.string()
16653
+ });
16654
+ var uploadFailureSchema = external_exports.object({
16655
+ filePath: external_exports.string(),
16656
+ error: cliErrorDescriptorSchema
16657
+ });
16658
+ var uploadBatchDataSchema = external_exports.object({
16659
+ successes: external_exports.array(uploadSuccessSchema),
15899
16660
  failures: external_exports.array(uploadFailureSchema),
15900
16661
  totalCount: external_exports.number().int().nonnegative(),
15901
16662
  attemptedCount: external_exports.number().int().nonnegative()
@@ -16488,8 +17249,8 @@ function isRecord(value) {
16488
17249
 
16489
17250
  // src/api.ts
16490
17251
  import { createWriteStream, promises as fs3 } from "fs";
16491
- import os3 from "os";
16492
- import path3 from "path";
17252
+ import os4 from "os";
17253
+ import path4 from "path";
16493
17254
  import { Readable } from "stream";
16494
17255
  import { pipeline } from "stream/promises";
16495
17256
 
@@ -16648,6 +17409,352 @@ async function readDurationMs(filePath, contentType) {
16648
17409
  );
16649
17410
  }
16650
17411
 
17412
+ // src/store.ts
17413
+ import { mkdirSync } from "fs";
17414
+ import os3 from "os";
17415
+ import path3 from "path";
17416
+ import { createRequire } from "module";
17417
+ var require2 = createRequire(import.meta.url);
17418
+ var Database = require2("better-sqlite3");
17419
+ var CLI_STORE_SCHEMA_VERSION = 2;
17420
+ function defaultStorePath(homeDir = os3.homedir(), env = process.env) {
17421
+ const explicit = env.RECAPPI_CLI_STORE_PATH?.trim();
17422
+ if (explicit) return explicit;
17423
+ const dataHome = env.XDG_DATA_HOME?.trim() || path3.join(homeDir, ".local", "share");
17424
+ return path3.join(dataHome, "recappi", "cli-state.sqlite");
17425
+ }
17426
+ function requireAccountPartition(input) {
17427
+ const account = parseAccountPartition(input, "strict");
17428
+ if (!account) {
17429
+ throw cliError(
17430
+ "usage.invalid_argument",
17431
+ "Account partition requires a backend origin and user id.",
17432
+ {
17433
+ hint: "Resolve Recappi auth first, then use the normalized origin and authenticated user id."
17434
+ }
17435
+ );
17436
+ }
17437
+ return account;
17438
+ }
17439
+ function openCliStore(opts = {}) {
17440
+ return new CliLocalStore(opts);
17441
+ }
17442
+ var CliLocalStore = class {
17443
+ db;
17444
+ now;
17445
+ constructor(opts = {}) {
17446
+ const dbPath = opts.dbPath ?? defaultStorePath(opts.homeDir, opts.env);
17447
+ if (!opts.readonly && dbPath !== ":memory:") {
17448
+ mkdirSync(path3.dirname(dbPath), { recursive: true, mode: 448 });
17449
+ }
17450
+ this.db = new Database(dbPath, opts.readonly === true ? { readonly: true } : void 0);
17451
+ this.now = opts.now ?? Date.now;
17452
+ if (!opts.readonly) this.migrate();
17453
+ }
17454
+ close() {
17455
+ this.db.close();
17456
+ }
17457
+ recordAccountSeen(account, email3) {
17458
+ const now = this.now();
17459
+ this.db.prepare(
17460
+ `
17461
+ INSERT INTO account_scopes (backend_origin, user_id, email, created_at, updated_at)
17462
+ VALUES (?, ?, ?, ?, ?)
17463
+ ON CONFLICT (backend_origin, user_id) DO UPDATE SET
17464
+ email = excluded.email,
17465
+ updated_at = excluded.updated_at
17466
+ `
17467
+ ).run(account.backendOrigin, account.userId, email3?.trim() || null, now, now);
17468
+ }
17469
+ addLocalArtifact(input) {
17470
+ const account = input.account ? requireAccountPartition(input.account) : null;
17471
+ const localPath = input.localPath.trim();
17472
+ if (!localPath) {
17473
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17474
+ }
17475
+ if (account) this.recordAccountSeen(account);
17476
+ const now = this.now();
17477
+ const result = this.db.prepare(
17478
+ `
17479
+ INSERT INTO local_artifacts (
17480
+ kind,
17481
+ backend_origin,
17482
+ user_id,
17483
+ remote_id,
17484
+ local_path,
17485
+ metadata_json,
17486
+ created_at,
17487
+ updated_at,
17488
+ last_opened_at
17489
+ )
17490
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
17491
+ `
17492
+ ).run(
17493
+ input.kind,
17494
+ account?.backendOrigin ?? null,
17495
+ account?.userId ?? null,
17496
+ input.remoteId?.trim() || null,
17497
+ localPath,
17498
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17499
+ now,
17500
+ now,
17501
+ input.lastOpenedAt ?? null
17502
+ );
17503
+ return this.getLocalArtifact(Number(result.lastInsertRowid));
17504
+ }
17505
+ upsertLocalArtifact(input) {
17506
+ const remoteId = input.remoteId?.trim();
17507
+ if (!remoteId) return this.addLocalArtifact(input);
17508
+ const account = input.account ? requireAccountPartition(input.account) : null;
17509
+ if (account) this.recordAccountSeen(account);
17510
+ const existing = this.findLocalArtifact({
17511
+ account,
17512
+ kind: input.kind,
17513
+ remoteId
17514
+ });
17515
+ if (!existing) return this.addLocalArtifact({ ...input, remoteId });
17516
+ const localPath = input.localPath.trim();
17517
+ if (!localPath) {
17518
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17519
+ }
17520
+ const now = this.now();
17521
+ this.db.prepare(
17522
+ `
17523
+ UPDATE local_artifacts
17524
+ SET local_path = ?,
17525
+ metadata_json = ?,
17526
+ updated_at = ?,
17527
+ last_opened_at = COALESCE(?, last_opened_at)
17528
+ WHERE id = ?
17529
+ `
17530
+ ).run(
17531
+ localPath,
17532
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17533
+ now,
17534
+ input.lastOpenedAt ?? null,
17535
+ existing.id
17536
+ );
17537
+ return this.getLocalArtifact(existing.id);
17538
+ }
17539
+ getLocalArtifact(id) {
17540
+ const row = this.db.prepare("SELECT * FROM local_artifacts WHERE id = ?").get(id);
17541
+ if (!row) {
17542
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17543
+ }
17544
+ return mapArtifactRow(row);
17545
+ }
17546
+ listLocalArtifactsForAccount(accountInput, opts = {}) {
17547
+ const account = requireAccountPartition(accountInput);
17548
+ const params = [account.backendOrigin, account.userId];
17549
+ let source = `
17550
+ SELECT * FROM local_artifacts
17551
+ WHERE backend_origin = ? AND user_id = ?
17552
+ `;
17553
+ if (opts.kind) {
17554
+ source += " AND kind = ?";
17555
+ params.push(opts.kind);
17556
+ }
17557
+ if (opts.remoteId) {
17558
+ source += " AND remote_id = ?";
17559
+ params.push(opts.remoteId);
17560
+ }
17561
+ source += " ORDER BY updated_at DESC, id DESC";
17562
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17563
+ }
17564
+ listUnattributedLocalArtifacts(opts = {}) {
17565
+ const params = [];
17566
+ let source = `
17567
+ SELECT * FROM local_artifacts
17568
+ WHERE backend_origin IS NULL AND user_id IS NULL
17569
+ `;
17570
+ if (opts.kind) {
17571
+ source += " AND kind = ?";
17572
+ params.push(opts.kind);
17573
+ }
17574
+ if (opts.remoteId) {
17575
+ source += " AND remote_id = ?";
17576
+ params.push(opts.remoteId);
17577
+ }
17578
+ source += " ORDER BY updated_at DESC, id DESC";
17579
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17580
+ }
17581
+ findLocalArtifactForAccount(accountInput, opts) {
17582
+ const account = requireAccountPartition(accountInput);
17583
+ return this.findLocalArtifact({ account, kind: opts.kind, remoteId: opts.remoteId });
17584
+ }
17585
+ listDownloadedRecordingIdsForAccount(accountInput) {
17586
+ return new Set(
17587
+ this.listLocalArtifactsForAccount(accountInput, { kind: "download" }).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
17588
+ );
17589
+ }
17590
+ markLocalArtifactOpened(id) {
17591
+ const now = this.now();
17592
+ const result = this.db.prepare(
17593
+ `
17594
+ UPDATE local_artifacts
17595
+ SET last_opened_at = ?, updated_at = ?
17596
+ WHERE id = ?
17597
+ `
17598
+ ).run(now, now, id);
17599
+ if (result.changes !== 1) {
17600
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17601
+ }
17602
+ return this.getLocalArtifact(id);
17603
+ }
17604
+ claimUnattributedLocalArtifact(id, accountInput) {
17605
+ const account = requireAccountPartition(accountInput);
17606
+ this.recordAccountSeen(account);
17607
+ const result = this.db.prepare(
17608
+ `
17609
+ UPDATE local_artifacts
17610
+ SET backend_origin = ?, user_id = ?, updated_at = ?
17611
+ WHERE id = ? AND backend_origin IS NULL AND user_id IS NULL
17612
+ `
17613
+ ).run(account.backendOrigin, account.userId, this.now(), id);
17614
+ return result.changes === 1;
17615
+ }
17616
+ migrate() {
17617
+ this.db.pragma("journal_mode = WAL");
17618
+ this.db.pragma("foreign_keys = ON");
17619
+ this.db.exec(`
17620
+ CREATE TABLE IF NOT EXISTS schema_meta (
17621
+ key TEXT PRIMARY KEY NOT NULL,
17622
+ value TEXT NOT NULL
17623
+ );
17624
+
17625
+ INSERT INTO schema_meta (key, value)
17626
+ VALUES ('schema_version', '${CLI_STORE_SCHEMA_VERSION}')
17627
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value;
17628
+
17629
+ CREATE TABLE IF NOT EXISTS account_scopes (
17630
+ backend_origin TEXT NOT NULL,
17631
+ user_id TEXT NOT NULL,
17632
+ email TEXT,
17633
+ created_at INTEGER NOT NULL,
17634
+ updated_at INTEGER NOT NULL,
17635
+ PRIMARY KEY (backend_origin, user_id)
17636
+ );
17637
+
17638
+ CREATE TABLE IF NOT EXISTS local_artifacts (
17639
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17640
+ kind TEXT NOT NULL,
17641
+ backend_origin TEXT,
17642
+ user_id TEXT,
17643
+ remote_id TEXT,
17644
+ local_path TEXT NOT NULL,
17645
+ metadata_json TEXT,
17646
+ created_at INTEGER NOT NULL,
17647
+ updated_at INTEGER NOT NULL,
17648
+ last_opened_at INTEGER,
17649
+ CHECK (
17650
+ (backend_origin IS NULL AND user_id IS NULL)
17651
+ OR (backend_origin IS NOT NULL AND user_id IS NOT NULL)
17652
+ )
17653
+ );
17654
+
17655
+ CREATE INDEX IF NOT EXISTS local_artifacts_account_kind_idx
17656
+ ON local_artifacts (backend_origin, user_id, kind, updated_at DESC);
17657
+
17658
+ CREATE INDEX IF NOT EXISTS local_artifacts_remote_idx
17659
+ ON local_artifacts (backend_origin, user_id, kind, remote_id);
17660
+ `);
17661
+ if (!hasColumn(this.db, "local_artifacts", "last_opened_at")) {
17662
+ this.db.exec("ALTER TABLE local_artifacts ADD COLUMN last_opened_at INTEGER");
17663
+ }
17664
+ }
17665
+ findLocalArtifact({
17666
+ account,
17667
+ kind,
17668
+ remoteId
17669
+ }) {
17670
+ const row = account ? this.db.prepare(
17671
+ `
17672
+ SELECT * FROM local_artifacts
17673
+ WHERE backend_origin = ? AND user_id = ? AND kind = ? AND remote_id = ?
17674
+ ORDER BY updated_at DESC, id DESC
17675
+ LIMIT 1
17676
+ `
17677
+ ).get(account.backendOrigin, account.userId, kind, remoteId) : this.db.prepare(
17678
+ `
17679
+ SELECT * FROM local_artifacts
17680
+ WHERE backend_origin IS NULL AND user_id IS NULL AND kind = ? AND remote_id = ?
17681
+ ORDER BY updated_at DESC, id DESC
17682
+ LIMIT 1
17683
+ `
17684
+ ).get(kind, remoteId);
17685
+ return row ? mapArtifactRow(row) : null;
17686
+ }
17687
+ };
17688
+ function parseAccountPartition(input, mode) {
17689
+ const rawOrigin = cleanString(input?.backendOrigin);
17690
+ const rawUserId = cleanString(input?.userId);
17691
+ if (!rawOrigin && !rawUserId) return null;
17692
+ if (!rawOrigin || !rawUserId) {
17693
+ if (mode === "strict") {
17694
+ throw cliError(
17695
+ "usage.invalid_argument",
17696
+ "Account stamp must include both backend origin and user id.",
17697
+ {
17698
+ hint: "Partial account stamps are treated as unattributed when reading legacy local state."
17699
+ }
17700
+ );
17701
+ }
17702
+ return null;
17703
+ }
17704
+ try {
17705
+ return { backendOrigin: validateOrigin(rawOrigin), userId: rawUserId };
17706
+ } catch (error51) {
17707
+ if (mode === "strict") throw error51;
17708
+ return null;
17709
+ }
17710
+ }
17711
+ function cleanString(value) {
17712
+ const trimmed = value?.trim();
17713
+ return trimmed ? trimmed : null;
17714
+ }
17715
+ function mapArtifactRow(row) {
17716
+ const backendOrigin = stringOrNull(row.backend_origin);
17717
+ const userId = stringOrNull(row.user_id);
17718
+ const lastOpenedAt = numberOrNull(row.last_opened_at);
17719
+ return {
17720
+ id: numberValue(row.id),
17721
+ kind: localArtifactKind(row.kind),
17722
+ account: backendOrigin && userId ? { backendOrigin, userId } : null,
17723
+ localPath: stringValue(row.local_path),
17724
+ ...stringOrNull(row.remote_id) ? { remoteId: stringOrNull(row.remote_id) ?? void 0 } : {},
17725
+ ...typeof row.metadata_json === "string" ? { metadata: JSON.parse(row.metadata_json) } : {},
17726
+ createdAt: numberValue(row.created_at),
17727
+ updatedAt: numberValue(row.updated_at),
17728
+ ...lastOpenedAt ? { lastOpenedAt } : {}
17729
+ };
17730
+ }
17731
+ function hasColumn(db, table, column) {
17732
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
17733
+ }
17734
+ function localArtifactKind(value) {
17735
+ if (value === "recording_session" || value === "download" || value === "live_caption_draft") {
17736
+ return value;
17737
+ }
17738
+ throw cliError("cloud.invalid_response", "CLI store contains an unknown artifact kind.");
17739
+ }
17740
+ function stringValue(value) {
17741
+ if (typeof value === "string") return value;
17742
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid string value.");
17743
+ }
17744
+ function stringOrNull(value) {
17745
+ return typeof value === "string" ? value : null;
17746
+ }
17747
+ function numberValue(value) {
17748
+ if (typeof value === "number") return value;
17749
+ if (typeof value === "bigint") return Number(value);
17750
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid number value.");
17751
+ }
17752
+ function numberOrNull(value) {
17753
+ if (typeof value === "number") return value;
17754
+ if (typeof value === "bigint") return Number(value);
17755
+ return null;
17756
+ }
17757
+
16651
17758
  // src/api.ts
16652
17759
  var RecappiApiClient = class {
16653
17760
  constructor(auth, opts = {}) {
@@ -16655,10 +17762,12 @@ var RecappiApiClient = class {
16655
17762
  this.fetchImpl = opts.fetchImpl ?? fetch;
16656
17763
  this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16657
17764
  this.env = opts.env ?? process.env;
17765
+ this.homeDir = opts.homeDir;
16658
17766
  }
16659
17767
  fetchImpl;
16660
17768
  sleep;
16661
17769
  env;
17770
+ homeDir;
16662
17771
  async authStatus() {
16663
17772
  if (!this.auth.token) {
16664
17773
  return { loggedIn: false, origin: this.auth.origin };
@@ -16806,9 +17915,9 @@ var RecappiApiClient = class {
16806
17915
  }
16807
17916
  const contentType = normalizeContentType(response.headers.get("content-type"));
16808
17917
  const contentLength = numberHeader(response.headers.get("content-length"));
16809
- const dir = opts.directory ?? await fs3.mkdtemp(path3.join(os3.tmpdir(), "recappi-cli-audio-"));
17918
+ const dir = opts.directory ?? await fs3.mkdtemp(path4.join(os4.tmpdir(), "recappi-cli-audio-"));
16810
17919
  if (opts.directory) await fs3.mkdir(dir, { recursive: true });
16811
- const filePath = path3.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
17920
+ const filePath = path4.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
16812
17921
  try {
16813
17922
  await pipeline(
16814
17923
  Readable.fromWeb(response.body),
@@ -16830,6 +17939,47 @@ var RecappiApiClient = class {
16830
17939
  const parsed = await this.getJson("/api/dashboard/stats");
16831
17940
  return mapDashboardStats(parsed, this.auth.origin);
16832
17941
  }
17942
+ async billingStatus() {
17943
+ const parsed = await this.getJson("/api/billing/status");
17944
+ return mapBillingStatus(parsed, this.auth.origin);
17945
+ }
17946
+ async accountStatus() {
17947
+ const status = await this.authStatus();
17948
+ const storePath = defaultStorePath(this.homeDir, this.env);
17949
+ let accountScopedArtifacts = 0;
17950
+ let unattributedArtifacts = 0;
17951
+ if (status.loggedIn && status.userId) {
17952
+ const store = openCliStore({
17953
+ dbPath: storePath,
17954
+ env: this.env,
17955
+ homeDir: this.homeDir
17956
+ });
17957
+ try {
17958
+ const account = requireAccountPartition({
17959
+ backendOrigin: this.auth.origin,
17960
+ userId: status.userId
17961
+ });
17962
+ store.recordAccountSeen(account, status.email);
17963
+ accountScopedArtifacts = store.listLocalArtifactsForAccount(account).length;
17964
+ unattributedArtifacts = store.listUnattributedLocalArtifacts().length;
17965
+ } finally {
17966
+ store.close();
17967
+ }
17968
+ }
17969
+ const billing = status.loggedIn ? await this.billingStatus() : void 0;
17970
+ return accountStatusDataSchema.parse({
17971
+ origin: this.auth.origin,
17972
+ loggedIn: status.loggedIn,
17973
+ ...status.email ? { email: status.email } : {},
17974
+ ...status.userId ? { userId: status.userId } : {},
17975
+ localStore: {
17976
+ path: storePath,
17977
+ accountScopedArtifacts,
17978
+ unattributedArtifacts
17979
+ },
17980
+ ...billing ? { billing } : {}
17981
+ });
17982
+ }
16833
17983
  async uploadPathBatch(opts) {
16834
17984
  const files = await collectAudioFiles(opts.inputPath);
16835
17985
  if (files.length === 0) {
@@ -17010,12 +18160,12 @@ var RecappiApiClient = class {
17010
18160
  ...typeof parsed.language === "string" || parsed.language === null ? { language: parsed.language } : {}
17011
18161
  };
17012
18162
  }
17013
- async getJson(path4) {
17014
- const response = await this.request("GET", path4);
18163
+ async getJson(path6) {
18164
+ const response = await this.request("GET", path6);
17015
18165
  return await parseJson(response);
17016
18166
  }
17017
- async postJson(path4, body) {
17018
- const response = await this.request("POST", path4, JSON.stringify(body), {
18167
+ async postJson(path6, body) {
18168
+ const response = await this.request("POST", path6, JSON.stringify(body), {
17019
18169
  headers: { "content-type": "application/json" }
17020
18170
  });
17021
18171
  return await parseJson(response);
@@ -17237,9 +18387,9 @@ function parseSummary(row) {
17237
18387
  function mapJobListItem(row) {
17238
18388
  const recording = isRecord2(row.recording) ? row.recording : {};
17239
18389
  return {
17240
- jobId: stringValue(row.jobId) ?? stringValue(row.id) ?? "",
17241
- recordingId: stringValue(row.recordingId) ?? "",
17242
- status: stringValue(row.status) ?? "queued",
18390
+ jobId: stringValue2(row.jobId) ?? stringValue2(row.id) ?? "",
18391
+ recordingId: stringValue2(row.recordingId) ?? "",
18392
+ status: stringValue2(row.status) ?? "queued",
17243
18393
  ...typeof row.provider === "string" ? { provider: row.provider } : {},
17244
18394
  ...typeof row.model === "string" ? { model: row.model } : {},
17245
18395
  ...typeof row.language === "string" || row.language === null ? { language: row.language } : {},
@@ -17258,10 +18408,10 @@ function mapJobListItem(row) {
17258
18408
  };
17259
18409
  }
17260
18410
  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);
18411
+ const recordingId = stringValue2(row.id) ?? stringValue2(row.recordingId);
18412
+ const status = stringValue2(row.status);
18413
+ const createdAt = numberValue2(row.createdAt);
18414
+ const updatedAt = numberValue2(row.updatedAt);
17265
18415
  if (!recordingId) {
17266
18416
  throw cliError("cloud.invalid_response", "Recording response was missing id.");
17267
18417
  }
@@ -17300,16 +18450,37 @@ function mapDashboardStats(row, origin) {
17300
18450
  jobs: mapCountObject(row.jobs, ["active", "queued", "running", "succeeded", "failed"])
17301
18451
  });
17302
18452
  }
18453
+ function mapBillingStatus(row, origin) {
18454
+ return billingStatusDataSchema.parse({
18455
+ origin,
18456
+ tier: row.tier,
18457
+ periodStart: numberValue2(row.periodStart),
18458
+ periodEnd: numberValue2(row.periodEnd),
18459
+ storageBytes: numberValue2(row.storageBytes) ?? 0,
18460
+ storageCapBytes: nullableCap(row.storageCapBytes),
18461
+ minutesUsed: numberValue2(row.minutesUsed) ?? 0,
18462
+ batchMinutesUsed: numberValue2(row.batchMinutesUsed) ?? 0,
18463
+ realtimeMinutesUsed: numberValue2(row.realtimeMinutesUsed) ?? 0,
18464
+ minutesCap: nullableCap(row.minutesCap),
18465
+ isOverStorage: row.isOverStorage === true,
18466
+ isOverMinutes: row.isOverMinutes === true
18467
+ });
18468
+ }
17303
18469
  function mapCountObject(value, keys) {
17304
18470
  const source = isRecord2(value) ? value : {};
17305
- return Object.fromEntries(keys.map((key) => [key, numberValue(source[key]) ?? 0]));
18471
+ return Object.fromEntries(keys.map((key) => [key, numberValue2(source[key]) ?? 0]));
17306
18472
  }
17307
- function stringValue(value) {
18473
+ function stringValue2(value) {
17308
18474
  return typeof value === "string" ? value : void 0;
17309
18475
  }
17310
- function numberValue(value) {
18476
+ function numberValue2(value) {
17311
18477
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
17312
18478
  }
18479
+ function nullableCap(value) {
18480
+ if (value === null) return null;
18481
+ const number4 = numberValue2(value);
18482
+ return number4 === void 0 ? null : number4;
18483
+ }
17313
18484
  function parseSummaryStatus(value) {
17314
18485
  const allowed = /* @__PURE__ */ new Set([
17315
18486
  "pending",
@@ -17344,11 +18515,37 @@ function decodeJsonRecord(value) {
17344
18515
 
17345
18516
  // src/audio.ts
17346
18517
  import { spawn } from "child_process";
18518
+ import { promises as fs4 } from "fs";
18519
+ import path5 from "path";
17347
18520
  function createRecordingAudioRuntime(client, deps = {}) {
18521
+ const downloadRecordingAudioFile = async (recordingId, opts) => {
18522
+ const cached2 = await findReusableDownload(recordingId, deps);
18523
+ if (cached2) return cached2;
18524
+ const directory = opts?.directory ?? (deps.account ? defaultDownloadDirectory(deps) : void 0);
18525
+ const download = await client.downloadRecordingAudio(recordingId, {
18526
+ ...opts,
18527
+ ...directory ? { directory } : {}
18528
+ });
18529
+ const artifact = await rememberDownload(download, deps);
18530
+ return {
18531
+ recordingId: download.recordingId,
18532
+ localPath: download.localPath,
18533
+ reused: false,
18534
+ ...artifact ? { artifactId: artifact.id } : {},
18535
+ contentType: download.contentType,
18536
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18537
+ origin: download.origin
18538
+ };
18539
+ };
17348
18540
  return {
17349
- downloadRecordingAudio: async (recordingId, opts) => (await client.downloadRecordingAudio(recordingId, opts)).localPath,
18541
+ downloadRecordingAudio: async (recordingId, opts) => (await downloadRecordingAudioFile(recordingId, opts)).localPath,
18542
+ downloadRecordingAudioFile,
17350
18543
  openPath: (localPath) => openPath(localPath, deps),
17351
- revealInFinder: (localPath) => revealInFinder(localPath, deps)
18544
+ revealInFinder: (localPath) => revealInFinder(localPath, deps),
18545
+ listDownloads: () => listExistingDownloads(deps),
18546
+ listDownloadedRecordingIds: async () => new Set(
18547
+ (await listExistingDownloads(deps)).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
18548
+ )
17352
18549
  };
17353
18550
  }
17354
18551
  function openPath(localPath, deps = {}) {
@@ -17357,6 +18554,80 @@ function openPath(localPath, deps = {}) {
17357
18554
  function revealInFinder(localPath, deps = {}) {
17358
18555
  return runMacOpen(["-R", localPath], deps);
17359
18556
  }
18557
+ async function findReusableDownload(recordingId, deps) {
18558
+ return withStore(deps, async (store, account) => {
18559
+ if (!account) return null;
18560
+ const artifact = store.findLocalArtifactForAccount(account, {
18561
+ kind: "download",
18562
+ remoteId: recordingId
18563
+ });
18564
+ if (!artifact || !await isReadableFile(artifact.localPath)) return null;
18565
+ const opened = store.markLocalArtifactOpened(artifact.id);
18566
+ return artifactToDownload(opened, recordingId);
18567
+ });
18568
+ }
18569
+ async function rememberDownload(download, deps) {
18570
+ return withStore(deps, (store, account) => {
18571
+ if (!account) return null;
18572
+ const artifact = store.upsertLocalArtifact({
18573
+ kind: "download",
18574
+ account,
18575
+ remoteId: download.recordingId,
18576
+ localPath: download.localPath,
18577
+ metadata: {
18578
+ resource: "recording_audio",
18579
+ contentType: download.contentType,
18580
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18581
+ origin: download.origin
18582
+ }
18583
+ });
18584
+ return store.markLocalArtifactOpened(artifact.id);
18585
+ });
18586
+ }
18587
+ async function listExistingDownloads(deps) {
18588
+ const artifacts = await withStore(
18589
+ deps,
18590
+ (store, account) => account ? store.listLocalArtifactsForAccount(account, { kind: "download" }) : []
18591
+ );
18592
+ const existing = [];
18593
+ for (const artifact of artifacts) {
18594
+ if (await isReadableFile(artifact.localPath)) existing.push(artifact);
18595
+ }
18596
+ return existing;
18597
+ }
18598
+ async function withStore(deps, run) {
18599
+ const store = deps.store ?? openCliStore({ homeDir: deps.homeDir, env: deps.env });
18600
+ try {
18601
+ return await run(store, deps.account ?? null);
18602
+ } finally {
18603
+ if (!deps.store) store.close();
18604
+ }
18605
+ }
18606
+ function defaultDownloadDirectory(deps) {
18607
+ return path5.join(path5.dirname(defaultStorePath(deps.homeDir, deps.env)), "downloads");
18608
+ }
18609
+ async function isReadableFile(localPath) {
18610
+ try {
18611
+ return (await fs4.stat(localPath)).isFile();
18612
+ } catch {
18613
+ return false;
18614
+ }
18615
+ }
18616
+ function artifactToDownload(artifact, recordingId) {
18617
+ const metadata = isRecord3(artifact.metadata) ? artifact.metadata : {};
18618
+ return {
18619
+ recordingId,
18620
+ localPath: artifact.localPath,
18621
+ reused: true,
18622
+ artifactId: artifact.id,
18623
+ ...typeof metadata.contentType === "string" ? { contentType: metadata.contentType } : {},
18624
+ ...typeof metadata.contentLength === "number" ? { contentLength: metadata.contentLength } : {},
18625
+ ...typeof metadata.origin === "string" ? { origin: metadata.origin } : {}
18626
+ };
18627
+ }
18628
+ function isRecord3(value) {
18629
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18630
+ }
17360
18631
  function runMacOpen(args, deps) {
17361
18632
  if ((deps.platform ?? process.platform) !== "darwin") {
17362
18633
  return Promise.reject(
@@ -17478,20 +18749,20 @@ function renderEnvelope(envelope, opts) {
17478
18749
  `);
17479
18750
  }
17480
18751
  function renderHumanSuccess(command, data, opts) {
17481
- if (command === "auth login" && isRecord3(data)) {
18752
+ if (command === "auth login" && isRecord4(data)) {
17482
18753
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
17483
18754
  `);
17484
18755
  return;
17485
18756
  }
17486
- if (command === "auth logout" && isRecord3(data)) {
18757
+ if (command === "auth logout" && isRecord4(data)) {
17487
18758
  opts.stdout(data.cleared ? "Signed out of Recappi CLI\n" : "No Recappi CLI session to clear\n");
17488
18759
  return;
17489
18760
  }
17490
- if (command === "auth import-macos" && isRecord3(data)) {
18761
+ if (command === "auth import-macos" && isRecord4(data)) {
17491
18762
  opts.stdout("Imported the Recappi Mini app session into Recappi CLI\n");
17492
18763
  return;
17493
18764
  }
17494
- if (command === "auth status" && isRecord3(data)) {
18765
+ if (command === "auth status" && isRecord4(data)) {
17495
18766
  if (data.loggedIn) {
17496
18767
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
17497
18768
  `);
@@ -17500,17 +18771,50 @@ function renderHumanSuccess(command, data, opts) {
17500
18771
  opts.stdout("Not logged in\n");
17501
18772
  return;
17502
18773
  }
17503
- if (command === "version" && isRecord3(data) && typeof data.version === "string") {
18774
+ if (command === "account status" && isRecord4(data)) {
18775
+ if (!data.loggedIn) {
18776
+ opts.stdout("Not logged in\n");
18777
+ return;
18778
+ }
18779
+ opts.stdout(`Account: ${typeof data.email === "string" ? data.email : "signed in"}
18780
+ `);
18781
+ if (typeof data.origin === "string") opts.stdout(` origin: ${data.origin}
18782
+ `);
18783
+ if (typeof data.userId === "string") opts.stdout(` userId: ${data.userId}
18784
+ `);
18785
+ const billing = isRecord4(data.billing) ? data.billing : {};
18786
+ if (typeof billing.tier === "string") opts.stdout(` plan: ${billing.tier}
18787
+ `);
18788
+ if (typeof billing.minutesUsed === "number") {
18789
+ const cap = formatNullableCap(billing.minutesCap, "minutes");
18790
+ opts.stdout(` minutes: ${billing.minutesUsed} / ${cap}
18791
+ `);
18792
+ }
18793
+ if (typeof billing.storageBytes === "number") {
18794
+ const cap = formatNullableCap(billing.storageCapBytes, "bytes");
18795
+ opts.stdout(` storage: ${formatBytes(billing.storageBytes)} / ${cap}
18796
+ `);
18797
+ }
18798
+ const localStore = isRecord4(data.localStore) ? data.localStore : {};
18799
+ if (typeof localStore.path === "string") opts.stdout(` localStore: ${localStore.path}
18800
+ `);
18801
+ opts.stdout(
18802
+ ` localArtifacts: ${numberText(localStore.accountScopedArtifacts)} current, ${numberText(localStore.unattributedArtifacts)} unattributed
18803
+ `
18804
+ );
18805
+ return;
18806
+ }
18807
+ if (command === "version" && isRecord4(data) && typeof data.version === "string") {
17504
18808
  opts.stdout(`${data.version}
17505
18809
  `);
17506
18810
  return;
17507
18811
  }
17508
- if (command === "doctor" && isRecord3(data) && Array.isArray(data.checks)) {
18812
+ if (command === "doctor" && isRecord4(data) && Array.isArray(data.checks)) {
17509
18813
  const status = typeof data.status === "string" ? data.status : "unknown";
17510
18814
  opts.stdout(`Doctor: ${status}
17511
18815
  `);
17512
18816
  for (const check2 of data.checks) {
17513
- if (!isRecord3(check2)) continue;
18817
+ if (!isRecord4(check2)) continue;
17514
18818
  const checkStatus = typeof check2.status === "string" ? check2.status : "unknown";
17515
18819
  const name = typeof check2.name === "string" ? check2.name : "check";
17516
18820
  const message = typeof check2.message === "string" ? ` \u2014 ${check2.message}` : "";
@@ -17521,14 +18825,14 @@ function renderHumanSuccess(command, data, opts) {
17521
18825
  }
17522
18826
  return;
17523
18827
  }
17524
- if (command === "transcript get" && isRecord3(data)) {
18828
+ if (command === "transcript get" && isRecord4(data)) {
17525
18829
  renderTranscriptHuman(data, opts);
17526
18830
  return;
17527
18831
  }
17528
- if (command === "recordings list" && isRecord3(data) && Array.isArray(data.items)) {
18832
+ if (command === "recordings list" && isRecord4(data) && Array.isArray(data.items)) {
17529
18833
  opts.stdout("Recordings:\n");
17530
18834
  for (const item of data.items) {
17531
- if (!isRecord3(item)) continue;
18835
+ if (!isRecord4(item)) continue;
17532
18836
  opts.stdout(` ${recordingLabel(item)}
17533
18837
  `);
17534
18838
  }
@@ -17540,7 +18844,7 @@ Next cursor: ${data.nextCursor}
17540
18844
  }
17541
18845
  return;
17542
18846
  }
17543
- if (command === "recordings get" && isRecord3(data)) {
18847
+ if (command === "recordings get" && isRecord4(data)) {
17544
18848
  opts.stdout(`${recordingTitle(data)}
17545
18849
  `);
17546
18850
  opts.stdout(` recordingId: ${String(data.recordingId)}
@@ -17562,9 +18866,9 @@ Next:
17562
18866
  }
17563
18867
  return;
17564
18868
  }
17565
- if (command === "dashboard stats" && isRecord3(data)) {
17566
- const recordings = isRecord3(data.recordings) ? data.recordings : {};
17567
- const jobs = isRecord3(data.jobs) ? data.jobs : {};
18869
+ if (command === "dashboard stats" && isRecord4(data)) {
18870
+ const recordings = isRecord4(data.recordings) ? data.recordings : {};
18871
+ const jobs = isRecord4(data.jobs) ? data.jobs : {};
17568
18872
  opts.stdout(
17569
18873
  `Recordings: ${numberText(recordings.total)} total, ${numberText(recordings.ready)} ready
17570
18874
  `
@@ -17604,7 +18908,50 @@ Next:
17604
18908
  }
17605
18909
  return;
17606
18910
  }
17607
- if ((command === "jobs wait" || command === "upload") && isRecord3(data)) {
18911
+ if (command === "record" && isRecord4(data)) {
18912
+ opts.stdout("Recording complete\n");
18913
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18914
+ `);
18915
+ if (typeof data.sessionId === "string") opts.stdout(` sessionId: ${data.sessionId}
18916
+ `);
18917
+ if (typeof data.localSessionRef === "string") {
18918
+ opts.stdout(` localSessionRef: ${data.localSessionRef}
18919
+ `);
18920
+ }
18921
+ if (Array.isArray(data.artifacts) && data.artifacts.length > 0) {
18922
+ opts.stdout(" artifacts:\n");
18923
+ for (const artifact of data.artifacts) {
18924
+ if (!isRecord4(artifact)) continue;
18925
+ const kind = typeof artifact.kind === "string" ? artifact.kind : "artifact";
18926
+ const localPath = typeof artifact.localPath === "string" ? artifact.localPath : "";
18927
+ opts.stdout(` - ${kind}: ${localPath}
18928
+ `);
18929
+ }
18930
+ }
18931
+ if (typeof data.recordingId === "string") {
18932
+ opts.stdout(`
18933
+ Next:
18934
+ recappi recordings get ${data.recordingId}
18935
+ `);
18936
+ }
18937
+ return;
18938
+ }
18939
+ if (command === "audio" && isRecord4(data)) {
18940
+ const action = typeof data.action === "string" ? data.action : "download";
18941
+ opts.stdout(
18942
+ action === "open" ? "Audio opened\n" : action === "reveal" ? "Audio revealed\n" : "Audio ready\n"
18943
+ );
18944
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18945
+ `);
18946
+ if (typeof data.localPath === "string") opts.stdout(` localPath: ${data.localPath}
18947
+ `);
18948
+ if (typeof data.reused === "boolean") {
18949
+ opts.stdout(` source: ${data.reused ? "local cache" : "downloaded"}
18950
+ `);
18951
+ }
18952
+ return;
18953
+ }
18954
+ if ((command === "jobs wait" || command === "upload") && isRecord4(data)) {
17608
18955
  if (typeof data.transcriptId === "string") {
17609
18956
  opts.stdout("Transcription ready\n");
17610
18957
  opts.stdout(` transcriptId: ${data.transcriptId}
@@ -17625,10 +18972,10 @@ Next:
17625
18972
  }
17626
18973
  return;
17627
18974
  }
17628
- if (command === "schema" && isRecord3(data) && Array.isArray(data.commands)) {
18975
+ if (command === "schema" && isRecord4(data) && Array.isArray(data.commands)) {
17629
18976
  opts.stdout("Commands:\n");
17630
18977
  for (const entry of data.commands) {
17631
- if (!isRecord3(entry) || typeof entry.name !== "string") continue;
18978
+ if (!isRecord4(entry) || typeof entry.name !== "string") continue;
17632
18979
  const summary = typeof entry.summary === "string" ? ` \u2014 ${entry.summary}` : "";
17633
18980
  opts.stdout(` ${entry.name}${summary}
17634
18981
  `);
@@ -17718,7 +19065,7 @@ function renderTranscriptHuman(data, opts) {
17718
19065
  const segments = Array.isArray(data.segments) ? data.segments : [];
17719
19066
  let printedBody = false;
17720
19067
  for (const segment of segments) {
17721
- if (!isRecord3(segment) || typeof segment.text !== "string") continue;
19068
+ if (!isRecord4(segment) || typeof segment.text !== "string") continue;
17722
19069
  const clock = typeof segment.startMs === "number" ? `[${formatClock(segment.startMs / 1e3)}] ` : "";
17723
19070
  const speaker = typeof segment.speaker === "string" ? `${segment.speaker}: ` : "";
17724
19071
  opts.stdout(`${clock}${speaker}${segment.text}
@@ -17730,7 +19077,7 @@ function renderTranscriptHuman(data, opts) {
17730
19077
  `);
17731
19078
  printedBody = true;
17732
19079
  }
17733
- const summary = isRecord3(data.summary) ? data.summary : void 0;
19080
+ const summary = isRecord4(data.summary) ? data.summary : void 0;
17734
19081
  if (!summary || summary.status !== "succeeded") return;
17735
19082
  if (typeof summary.tldr === "string" && summary.tldr.length > 0) {
17736
19083
  opts.stdout(`
@@ -17748,7 +19095,7 @@ Summary:
17748
19095
  if (Array.isArray(summary.actionItems) && summary.actionItems.length > 0) {
17749
19096
  opts.stdout("\nAction items:\n");
17750
19097
  for (const item of summary.actionItems) {
17751
- if (!isRecord3(item) || typeof item.what !== "string") continue;
19098
+ if (!isRecord4(item) || typeof item.what !== "string") continue;
17752
19099
  const who = typeof item.who === "string" ? `${item.who}: ` : "";
17753
19100
  opts.stdout(` - ${who}${item.what}
17754
19101
  `);
@@ -17786,6 +19133,11 @@ function formatBytes(bytes) {
17786
19133
  }
17787
19134
  return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
17788
19135
  }
19136
+ function formatNullableCap(value, unit) {
19137
+ if (value === null || value === void 0) return "Unlimited";
19138
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Unlimited";
19139
+ return unit === "bytes" ? formatBytes(value) : String(value);
19140
+ }
17789
19141
  function formatClock(seconds) {
17790
19142
  const total = Math.max(0, Math.floor(seconds));
17791
19143
  const hours = Math.floor(total / 3600);
@@ -17814,7 +19166,7 @@ function applyFields(command, data, fields, compact) {
17814
19166
  };
17815
19167
  return compact ? compactData(filtered2) : filtered2;
17816
19168
  }
17817
- if (!isRecord3(data)) return data;
19169
+ if (!isRecord4(data)) return data;
17818
19170
  const allowed = new Set(Object.keys(data));
17819
19171
  assertKnownFields(fields, allowed);
17820
19172
  const filtered = pickFields(data, fields);
@@ -17839,7 +19191,7 @@ function compactData(value) {
17839
19191
  if (Array.isArray(value)) {
17840
19192
  return value.map(compactData).filter((item) => item !== void 0);
17841
19193
  }
17842
- if (isRecord3(value)) {
19194
+ if (isRecord4(value)) {
17843
19195
  const out = {};
17844
19196
  for (const [key, child] of Object.entries(value)) {
17845
19197
  const compacted = compactData(child);
@@ -17857,16 +19209,16 @@ function stableStringify(value, compact) {
17857
19209
  }
17858
19210
  function sortKeys(value) {
17859
19211
  if (Array.isArray(value)) return value.map(sortKeys);
17860
- if (!isRecord3(value)) return value;
19212
+ if (!isRecord4(value)) return value;
17861
19213
  return Object.fromEntries(
17862
19214
  Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
17863
19215
  );
17864
19216
  }
17865
- function isRecord3(value) {
19217
+ function isRecord4(value) {
17866
19218
  return typeof value === "object" && value !== null && !Array.isArray(value);
17867
19219
  }
17868
19220
  function isUploadBatch(value) {
17869
- return isRecord3(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
19221
+ return isRecord4(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
17870
19222
  }
17871
19223
 
17872
19224
  // src/schema.ts
@@ -17875,9 +19227,12 @@ var COMMAND_DATA_SCHEMAS = {
17875
19227
  "auth logout": authLogoutDataSchema,
17876
19228
  "auth import-macos": authImportDataSchema,
17877
19229
  "auth status": authStatusDataSchema,
19230
+ "account status": accountStatusDataSchema,
19231
+ audio: audioCommandDataSchema,
17878
19232
  doctor: doctorDataSchema,
17879
19233
  "dashboard stats": dashboardStatsDataSchema,
17880
19234
  upload: uploadBatchDataSchema,
19235
+ record: recordCommandDataSchema,
17881
19236
  "recordings get": recordingDataSchema,
17882
19237
  "recordings list": recordingListDataSchema,
17883
19238
  "jobs list": jobListDataSchema,
@@ -17914,11 +19269,11 @@ function buildSchemaDocument(program) {
17914
19269
  event: toJsonSchema(operationEventSchema)
17915
19270
  };
17916
19271
  }
17917
- function walkCommands(command, path4, out) {
19272
+ function walkCommands(command, path6, out) {
17918
19273
  for (const sub of subcommandsOf(command)) {
17919
19274
  const name = sub.name();
17920
19275
  if (name === "help") continue;
17921
- const fullPath = [...path4, name];
19276
+ const fullPath = [...path6, name];
17922
19277
  const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
17923
19278
  if (children.length === 0) {
17924
19279
  out.push(leafCommandDoc(sub, fullPath.join(" ")));
@@ -17977,6 +19332,414 @@ function readCliVersion() {
17977
19332
  }
17978
19333
  var CLI_VERSION = readCliVersion();
17979
19334
 
19335
+ // src/record.tsx
19336
+ import { render, useInput as useInput2 } from "ink";
19337
+
19338
+ // src/sidecar.ts
19339
+ import { spawn as spawn2 } from "child_process";
19340
+ import { createInterface } from "readline";
19341
+ var MiniSidecarClient = class {
19342
+ input;
19343
+ requestTimeoutMs;
19344
+ pending = /* @__PURE__ */ new Map();
19345
+ eventListeners = /* @__PURE__ */ new Set();
19346
+ lineReader;
19347
+ nextId = 1;
19348
+ closed = false;
19349
+ constructor(opts) {
19350
+ this.input = opts.input;
19351
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 1e4;
19352
+ this.lineReader = createInterface({ input: opts.output });
19353
+ this.lineReader.on("line", (line) => this.handleLine(line));
19354
+ this.lineReader.on("close", () => this.rejectAll("Sidecar output closed."));
19355
+ }
19356
+ onEvent(listener) {
19357
+ this.eventListeners.add(listener);
19358
+ return () => {
19359
+ this.eventListeners.delete(listener);
19360
+ };
19361
+ }
19362
+ handshake(params) {
19363
+ return this.request(
19364
+ "recappi.handshake",
19365
+ sidecarHandshakeParamsSchema.parse(params),
19366
+ sidecarHandshakeResultSchema
19367
+ );
19368
+ }
19369
+ startRecording(params) {
19370
+ return this.request(
19371
+ "recappi.recording.start",
19372
+ sidecarRecordingStartParamsSchema.parse(params),
19373
+ sidecarRecordingStartResultSchema
19374
+ );
19375
+ }
19376
+ stopRecording(params) {
19377
+ return this.request(
19378
+ "recappi.recording.stop",
19379
+ sidecarSessionParamsSchema.parse(params),
19380
+ sidecarRecordingStopResultSchema
19381
+ );
19382
+ }
19383
+ cancelRecording(params) {
19384
+ return this.request(
19385
+ "recappi.recording.cancel",
19386
+ sidecarSessionParamsSchema.parse(params),
19387
+ sidecarRecordingStopResultSchema
19388
+ );
19389
+ }
19390
+ getRecordingStatus(params) {
19391
+ return this.request(
19392
+ "recappi.recording.status",
19393
+ sidecarSessionParamsSchema.parse(params),
19394
+ sidecarRecordingStatusResultSchema
19395
+ );
19396
+ }
19397
+ close() {
19398
+ if (this.closed) return;
19399
+ this.closed = true;
19400
+ this.lineReader.close();
19401
+ this.rejectAll("Sidecar client closed.");
19402
+ }
19403
+ request(method, params, resultSchema) {
19404
+ if (this.closed) {
19405
+ return Promise.reject(
19406
+ cliError("internal.unexpected", "Sidecar client is already closed.", {
19407
+ hint: "Start a new sidecar session and retry."
19408
+ })
19409
+ );
19410
+ }
19411
+ const id = this.nextId;
19412
+ this.nextId += 1;
19413
+ const payload = {
19414
+ jsonrpc: "2.0",
19415
+ id,
19416
+ method,
19417
+ params
19418
+ };
19419
+ return new Promise((resolve, reject) => {
19420
+ const timer = setTimeout(() => {
19421
+ this.pending.delete(id);
19422
+ reject(
19423
+ cliError("internal.unexpected", `Sidecar request timed out: ${method}.`, {
19424
+ retryable: true
19425
+ })
19426
+ );
19427
+ }, this.requestTimeoutMs);
19428
+ this.pending.set(id, {
19429
+ resolve: (value) => {
19430
+ try {
19431
+ const parsed = resultSchema.parse(value);
19432
+ resolve(parsed);
19433
+ } catch (error51) {
19434
+ reject(toCliError(error51));
19435
+ }
19436
+ },
19437
+ reject,
19438
+ timer
19439
+ });
19440
+ this.input.write(`${JSON.stringify(payload)}
19441
+ `, (error51) => {
19442
+ if (!error51) return;
19443
+ clearTimeout(timer);
19444
+ this.pending.delete(id);
19445
+ reject(cliError("internal.unexpected", `Could not write to sidecar: ${error51.message}`));
19446
+ });
19447
+ });
19448
+ }
19449
+ handleLine(line) {
19450
+ const trimmed = line.trim();
19451
+ if (!trimmed) return;
19452
+ let raw;
19453
+ try {
19454
+ raw = JSON.parse(trimmed);
19455
+ } catch {
19456
+ this.rejectAll("Sidecar wrote invalid JSON.");
19457
+ return;
19458
+ }
19459
+ const maybeNotification = sidecarNotificationSchema.safeParse(raw);
19460
+ if (maybeNotification.success) {
19461
+ const event = sidecarEventSchema.parse(maybeNotification.data.params);
19462
+ for (const listener of this.eventListeners) listener(event);
19463
+ return;
19464
+ }
19465
+ const response = sidecarResponseSchema.safeParse(raw);
19466
+ if (!response.success) {
19467
+ this.rejectAll("Sidecar wrote an invalid JSON-RPC message.");
19468
+ return;
19469
+ }
19470
+ const id = sidecarJsonRpcIdSchema.parse(response.data.id);
19471
+ const pending = this.pending.get(id);
19472
+ if (!pending) return;
19473
+ this.pending.delete(id);
19474
+ clearTimeout(pending.timer);
19475
+ if ("error" in response.data) {
19476
+ pending.reject(
19477
+ cliError("internal.unexpected", response.data.error.message, {
19478
+ data: response.data.error,
19479
+ retryable: response.data.error.code >= -32099 && response.data.error.code <= -32e3
19480
+ })
19481
+ );
19482
+ return;
19483
+ }
19484
+ pending.resolve(response.data.result);
19485
+ }
19486
+ rejectAll(message) {
19487
+ for (const [id, pending] of this.pending) {
19488
+ this.pending.delete(id);
19489
+ clearTimeout(pending.timer);
19490
+ pending.reject(cliError("internal.unexpected", message));
19491
+ }
19492
+ }
19493
+ };
19494
+ function spawnMiniSidecar(opts) {
19495
+ const spawnProcess = opts.spawnProcess ?? spawn2;
19496
+ const child = spawnProcess(opts.command, opts.args ?? [], {
19497
+ env: opts.env,
19498
+ stdio: ["pipe", "pipe", "pipe"]
19499
+ });
19500
+ const client = new MiniSidecarClient({
19501
+ input: child.stdin,
19502
+ output: child.stdout,
19503
+ requestTimeoutMs: opts.requestTimeoutMs
19504
+ });
19505
+ return {
19506
+ client,
19507
+ kill: () => {
19508
+ client.close();
19509
+ child.kill();
19510
+ }
19511
+ };
19512
+ }
19513
+ function defaultSidecarHandshakeParams(params) {
19514
+ return {
19515
+ protocolVersion: SIDECAR_PROTOCOL_VERSION,
19516
+ ...params
19517
+ };
19518
+ }
19519
+
19520
+ // src/record.tsx
19521
+ init_LiveCaptionsScreen();
19522
+ import { jsx as jsx3 } from "react/jsx-runtime";
19523
+ var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
19524
+ async function recordViaSidecar(opts) {
19525
+ let liveRenderer;
19526
+ let session;
19527
+ try {
19528
+ session = await startRecordSession(opts);
19529
+ if (opts.renderLive) {
19530
+ liveRenderer = opts.runtime?.createLiveRenderer?.(session.source) ?? createInkLiveRenderer({
19531
+ source: session.source,
19532
+ renderApp: opts.runtime?.renderApp,
19533
+ now: opts.runtime?.now
19534
+ });
19535
+ }
19536
+ if (liveRenderer) {
19537
+ await liveRenderer.waitUntilStop();
19538
+ } else {
19539
+ await (opts.runtime?.waitForStop ?? waitForStopSignal)();
19540
+ }
19541
+ return await session.stop();
19542
+ } catch (error51) {
19543
+ if (session) {
19544
+ try {
19545
+ await session.cancel();
19546
+ } catch {
19547
+ }
19548
+ }
19549
+ throw error51;
19550
+ } finally {
19551
+ liveRenderer?.close();
19552
+ session?.close();
19553
+ }
19554
+ }
19555
+ async function startLiveRecordSession(opts) {
19556
+ const session = await startRecordSession({ ...opts, live: true });
19557
+ return {
19558
+ source: session.source,
19559
+ stop: async () => {
19560
+ await session.stop();
19561
+ }
19562
+ };
19563
+ }
19564
+ async function startRecordSession(opts) {
19565
+ const command = resolveSidecarCommand(opts);
19566
+ const sidecarArgs = opts.sidecarArgs ?? [];
19567
+ const spawnSidecar = opts.runtime?.spawnSidecar ?? spawnMiniSidecar;
19568
+ const sidecar = spawnSidecar({ command, args: sidecarArgs, env: opts.env });
19569
+ const account = requireAccountPartition(opts.account);
19570
+ const artifacts = [];
19571
+ let handshake;
19572
+ let sessionId;
19573
+ let latestState;
19574
+ let recordingId;
19575
+ let localSessionRef;
19576
+ let stopPromise;
19577
+ let closed = false;
19578
+ const unsubscribe = sidecar.client.onEvent((event) => {
19579
+ if (event.type === "recording.state") {
19580
+ latestState = event.state;
19581
+ if (event.recordingId) recordingId = event.recordingId;
19582
+ if (event.localSessionRef) localSessionRef = event.localSessionRef;
19583
+ }
19584
+ if (event.type === "local_artifact.upserted") {
19585
+ artifacts.push(event.artifact);
19586
+ }
19587
+ });
19588
+ const close = () => {
19589
+ if (closed) return;
19590
+ closed = true;
19591
+ unsubscribe();
19592
+ sidecar.kill();
19593
+ };
19594
+ const cancel = async () => {
19595
+ if (sessionId && latestState && latestState !== "completed" && latestState !== "cancelled") {
19596
+ try {
19597
+ await sidecar.client.cancelRecording({ sessionId });
19598
+ } catch {
19599
+ }
19600
+ }
19601
+ close();
19602
+ };
19603
+ try {
19604
+ handshake = await sidecar.client.handshake(
19605
+ defaultSidecarHandshakeParams({
19606
+ client: { name: "recappi-cli", version: opts.cliVersion },
19607
+ account: opts.account,
19608
+ capabilities: opts.live ? ["recording.capture", "recording.upload", "live_captions.stream"] : ["recording.capture", "recording.upload"]
19609
+ })
19610
+ );
19611
+ const started = await sidecar.client.startRecording({
19612
+ account,
19613
+ options: {
19614
+ includeSystemAudio: opts.includeSystemAudio ?? true,
19615
+ includeMicrophone: opts.includeMicrophone ?? true,
19616
+ liveCaptions: opts.live === true,
19617
+ ...opts.translationLanguage ? { translationLanguage: opts.translationLanguage } : {},
19618
+ ...opts.transcriptionLanguage ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
19619
+ ...opts.title ? { title: opts.title } : {}
19620
+ }
19621
+ });
19622
+ sessionId = started.sessionId;
19623
+ latestState = started.state;
19624
+ localSessionRef = started.localSessionRef;
19625
+ return {
19626
+ source: sidecar.client,
19627
+ stop: () => {
19628
+ stopPromise ??= (async () => {
19629
+ try {
19630
+ const stopped = await sidecar.client.stopRecording({ sessionId });
19631
+ latestState = stopped.state;
19632
+ recordingId = stopped.recordingId ?? recordingId;
19633
+ localSessionRef = stopped.localSessionRef ?? localSessionRef;
19634
+ artifacts.push(...stopped.artifacts ?? []);
19635
+ const uniqueArtifacts = dedupeArtifacts(artifacts);
19636
+ persistArtifacts(uniqueArtifacts, account, opts);
19637
+ return recordCommandDataSchema.parse({
19638
+ origin: account.backendOrigin,
19639
+ userId: account.userId,
19640
+ live: opts.live === true,
19641
+ sessionId: stopped.sessionId,
19642
+ state: stopped.state,
19643
+ ...recordingId ? { recordingId } : {},
19644
+ ...localSessionRef ? { localSessionRef } : {},
19645
+ ...handshake?.sidecar ? { sidecar: handshake.sidecar } : {},
19646
+ artifacts: uniqueArtifacts
19647
+ });
19648
+ } finally {
19649
+ close();
19650
+ }
19651
+ })();
19652
+ return stopPromise;
19653
+ },
19654
+ cancel,
19655
+ close
19656
+ };
19657
+ } catch (error51) {
19658
+ await cancel();
19659
+ throw error51;
19660
+ }
19661
+ }
19662
+ function resolveSidecarCommand(opts) {
19663
+ const command = opts.sidecarCommand?.trim() || opts.env?.[SIDECAR_COMMAND_ENV]?.trim();
19664
+ if (!command) {
19665
+ throw cliError("usage.invalid_argument", "Missing Recappi Mini sidecar command.", {
19666
+ hint: `Pass --sidecar-command, or set ${SIDECAR_COMMAND_ENV} to the Mini sidecar executable.`
19667
+ });
19668
+ }
19669
+ return command;
19670
+ }
19671
+ function persistArtifacts(artifacts, account, opts) {
19672
+ if (artifacts.length === 0) return;
19673
+ const store = openCliStore({ homeDir: opts.homeDir, env: opts.env });
19674
+ try {
19675
+ for (const artifact of artifacts) {
19676
+ store.addLocalArtifact({
19677
+ kind: artifact.kind,
19678
+ account,
19679
+ localPath: artifact.localPath,
19680
+ remoteId: artifact.remoteId,
19681
+ metadata: artifact.metadata
19682
+ });
19683
+ }
19684
+ } finally {
19685
+ store.close();
19686
+ }
19687
+ }
19688
+ function dedupeArtifacts(artifacts) {
19689
+ const seen = /* @__PURE__ */ new Set();
19690
+ const out = [];
19691
+ for (const artifact of artifacts) {
19692
+ const key = [artifact.kind, artifact.localPath, artifact.remoteId ?? ""].join("\0");
19693
+ if (seen.has(key)) continue;
19694
+ seen.add(key);
19695
+ out.push(artifact);
19696
+ }
19697
+ return out;
19698
+ }
19699
+ function waitForStopSignal() {
19700
+ return new Promise((resolve) => {
19701
+ const stop = () => {
19702
+ process.off("SIGINT", stop);
19703
+ process.off("SIGTERM", stop);
19704
+ resolve();
19705
+ };
19706
+ process.once("SIGINT", stop);
19707
+ process.once("SIGTERM", stop);
19708
+ });
19709
+ }
19710
+ function createInkLiveRenderer(opts) {
19711
+ let resolveStop;
19712
+ const stopped = new Promise((resolve) => {
19713
+ resolveStop = resolve;
19714
+ });
19715
+ const renderApp = opts.renderApp ?? render;
19716
+ const app = renderApp(
19717
+ /* @__PURE__ */ jsx3(
19718
+ RecordLiveScreen,
19719
+ {
19720
+ source: opts.source,
19721
+ onStop: () => resolveStop?.(),
19722
+ now: opts.now ?? Date.now
19723
+ }
19724
+ ),
19725
+ { alternateScreen: true, interactive: true }
19726
+ );
19727
+ return {
19728
+ waitUntilStop: () => stopped,
19729
+ close: () => app.unmount()
19730
+ };
19731
+ }
19732
+ function RecordLiveScreen({
19733
+ source,
19734
+ onStop,
19735
+ now
19736
+ }) {
19737
+ useInput2((input, key) => {
19738
+ if (input === "q" || key.escape || key.leftArrow) onStop();
19739
+ });
19740
+ return /* @__PURE__ */ jsx3(LiveCaptionsScreen, { source, now });
19741
+ }
19742
+
17980
19743
  // src/cli.ts
17981
19744
  var DASHBOARD_RECORDINGS_PAGE_SIZE = 50;
17982
19745
  async function runCli(deps = {}) {
@@ -17992,7 +19755,7 @@ async function runCli(deps = {}) {
17992
19755
  return 0;
17993
19756
  }
17994
19757
  const mode = parsed.options.mode ?? (isTTY ? "human" : "json");
17995
- const render2 = {
19758
+ const render3 = {
17996
19759
  mode,
17997
19760
  compact: parsed.options.compact,
17998
19761
  fields: parsed.options.fields,
@@ -18001,11 +19764,11 @@ async function runCli(deps = {}) {
18001
19764
  progress: createHumanProgressState(mode === "human" && isTTY)
18002
19765
  };
18003
19766
  if (parsed.kind === "schema") {
18004
- renderSuccess("schema", parsed.document, render2);
19767
+ renderSuccess("schema", parsed.document, render3);
18005
19768
  return 0;
18006
19769
  }
18007
19770
  if (parsed.kind === "version") {
18008
- renderSuccess("version", { version: CLI_VERSION }, render2);
19771
+ renderSuccess("version", { version: CLI_VERSION }, render3);
18009
19772
  return 0;
18010
19773
  }
18011
19774
  const auth = await resolveAuthContext({
@@ -18016,23 +19779,58 @@ async function runCli(deps = {}) {
18016
19779
  const client = new RecappiApiClient(auth, {
18017
19780
  fetchImpl: deps.fetchImpl,
18018
19781
  sleep: deps.sleep,
18019
- env: deps.env
19782
+ env: deps.env,
19783
+ homeDir: deps.homeDir
18020
19784
  });
18021
19785
  if (parsed.kind === "dashboard") {
19786
+ const status = await client.authStatus();
19787
+ const account = status.loggedIn && status.userId ? { backendOrigin: auth.origin, userId: status.userId } : null;
19788
+ const recordingAudio = createRecordingAudioRuntime(client, {
19789
+ account,
19790
+ env: deps.env,
19791
+ homeDir: deps.homeDir
19792
+ });
18022
19793
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
18023
19794
  await runDashboard2({
18024
19795
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
18025
19796
  fetchRecordings: ({ cursor, limit = DASHBOARD_RECORDINGS_PAGE_SIZE } = {}) => client.listRecordings({ limit, cursor }),
18026
19797
  fetchDashboardStats: () => client.dashboardStats(),
19798
+ fetchAccountStatus: () => client.accountStatus(),
18027
19799
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
18028
- recordingAudio: createRecordingAudioRuntime(client),
19800
+ recordingAudio,
19801
+ listDownloadedRecordingIds: () => recordingAudio.listDownloadedRecordingIds(),
19802
+ listDownloads: () => recordingAudio.listDownloads(),
19803
+ startLiveRecord: async () => {
19804
+ const liveStatus = await client.authStatus();
19805
+ if (!liveStatus.loggedIn || !liveStatus.userId) {
19806
+ throw cliError("auth.not_logged_in", "Sign in before starting a sidecar recording.", {
19807
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19808
+ });
19809
+ }
19810
+ return startLiveRecordSession({
19811
+ account: {
19812
+ backendOrigin: auth.origin,
19813
+ userId: liveStatus.userId,
19814
+ ...liveStatus.email ? { email: liveStatus.email } : {}
19815
+ },
19816
+ cliVersion: CLI_VERSION,
19817
+ env: deps.env,
19818
+ homeDir: deps.homeDir,
19819
+ runtime: deps.recordRuntime
19820
+ });
19821
+ },
18029
19822
  initialView: parsed.initialView
18030
19823
  });
18031
19824
  return 0;
18032
19825
  }
18033
19826
  if (parsed.kind === "auth-status") {
18034
19827
  const data = await client.authStatus();
18035
- renderSuccess("auth status", data, render2);
19828
+ renderSuccess("auth status", data, render3);
19829
+ return data.loggedIn ? 0 : 3;
19830
+ }
19831
+ if (parsed.kind === "account-status") {
19832
+ const data = await client.accountStatus();
19833
+ renderSuccess("account status", data, render3);
18036
19834
  return data.loggedIn ? 0 : 3;
18037
19835
  }
18038
19836
  if (parsed.kind === "auth-login") {
@@ -18047,12 +19845,12 @@ async function runCli(deps = {}) {
18047
19845
  sleep: deps.sleep
18048
19846
  }
18049
19847
  });
18050
- renderSuccess("auth login", data, render2);
19848
+ renderSuccess("auth login", data, render3);
18051
19849
  return 0;
18052
19850
  }
18053
19851
  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);
19852
+ const cleared = await clearAuthConfig(deps.homeDir ?? os5.homedir());
19853
+ renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render3);
18056
19854
  return 0;
18057
19855
  }
18058
19856
  if (parsed.kind === "auth-import-macos") {
@@ -18062,20 +19860,20 @@ async function runCli(deps = {}) {
18062
19860
  hint: keychain.hint ?? "Run recappi auth login instead."
18063
19861
  });
18064
19862
  }
18065
- await saveAuthConfig(deps.homeDir ?? os4.homedir(), {
19863
+ await saveAuthConfig(deps.homeDir ?? os5.homedir(), {
18066
19864
  origin: auth.origin,
18067
19865
  token: keychain.token
18068
19866
  });
18069
19867
  renderSuccess(
18070
19868
  "auth import-macos",
18071
19869
  { imported: true, origin: auth.origin, source: "macos-keychain" },
18072
- render2
19870
+ render3
18073
19871
  );
18074
19872
  return 0;
18075
19873
  }
18076
19874
  if (parsed.kind === "doctor") {
18077
19875
  const data = await client.doctor();
18078
- renderSuccess("doctor", data, render2);
19876
+ renderSuccess("doctor", data, render3);
18079
19877
  if (data.status !== "error") return 0;
18080
19878
  return data.checks.some((check2) => check2.name.startsWith("auth.")) ? 3 : 1;
18081
19879
  }
@@ -18089,7 +19887,7 @@ async function runCli(deps = {}) {
18089
19887
  provider: parsed.provider,
18090
19888
  prompt: parsed.prompt,
18091
19889
  force: parsed.force,
18092
- onEvent: (event) => renderEvent(event, render2)
19890
+ onEvent: (event) => renderEvent(event, render3)
18093
19891
  });
18094
19892
  if (data.failures.length > 0) {
18095
19893
  const worst = data.failures.reduce((max, item) => Math.max(max, item.error.exitCode), 1);
@@ -18100,26 +19898,95 @@ async function runCli(deps = {}) {
18100
19898
  message: `${data.failures.length} of ${data.totalCount} upload(s) failed.`,
18101
19899
  hint: "Inspect data.failures[].error for per-file codes; retry only the failed files."
18102
19900
  };
18103
- renderFailure("upload", descriptor, render2, data);
19901
+ renderFailure("upload", descriptor, render3, data);
18104
19902
  return worst;
18105
19903
  }
18106
- renderSuccess("upload", data, render2);
19904
+ renderSuccess("upload", data, render3);
19905
+ return 0;
19906
+ }
19907
+ if (parsed.kind === "record") {
19908
+ const status = await client.authStatus();
19909
+ if (!status.loggedIn || !status.userId) {
19910
+ throw cliError("auth.not_logged_in", "Sign in before starting a sidecar recording.", {
19911
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19912
+ });
19913
+ }
19914
+ if (mode === "human" && isTTY && !parsed.live) {
19915
+ stderr("Recording\u2026 press Ctrl-C to stop.\n");
19916
+ }
19917
+ const data = await recordViaSidecar({
19918
+ account: {
19919
+ backendOrigin: auth.origin,
19920
+ userId: status.userId,
19921
+ ...status.email ? { email: status.email } : {}
19922
+ },
19923
+ cliVersion: CLI_VERSION,
19924
+ env: deps.env,
19925
+ homeDir: deps.homeDir,
19926
+ title: parsed.title,
19927
+ live: parsed.live,
19928
+ includeSystemAudio: parsed.includeSystemAudio,
19929
+ includeMicrophone: parsed.includeMicrophone,
19930
+ translationLanguage: parsed.translationLanguage,
19931
+ transcriptionLanguage: parsed.transcriptionLanguage,
19932
+ sidecarCommand: parsed.sidecarCommand,
19933
+ renderLive: parsed.live === true && mode === "human" && isTTY,
19934
+ runtime: deps.recordRuntime
19935
+ });
19936
+ renderSuccess("record", data, render3);
19937
+ return 0;
19938
+ }
19939
+ if (parsed.kind === "audio") {
19940
+ const status = await client.authStatus();
19941
+ if (!status.loggedIn || !status.userId) {
19942
+ throw cliError("auth.not_logged_in", "Sign in before using local audio actions.", {
19943
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19944
+ });
19945
+ }
19946
+ const recordingAudio = createRecordingAudioRuntime(client, {
19947
+ account: { backendOrigin: auth.origin, userId: status.userId },
19948
+ env: deps.env,
19949
+ homeDir: deps.homeDir
19950
+ });
19951
+ const download = await recordingAudio.downloadRecordingAudioFile(
19952
+ parsed.recordingId,
19953
+ parsed.outputDir ? { directory: parsed.outputDir } : void 0
19954
+ );
19955
+ if (parsed.action === "open") {
19956
+ await recordingAudio.openPath(download.localPath);
19957
+ } else if (parsed.action === "reveal") {
19958
+ await recordingAudio.revealInFinder(download.localPath);
19959
+ }
19960
+ renderSuccess(
19961
+ "audio",
19962
+ {
19963
+ origin: auth.origin,
19964
+ recordingId: parsed.recordingId,
19965
+ localPath: download.localPath,
19966
+ action: parsed.action,
19967
+ reused: download.reused,
19968
+ ...download.artifactId !== void 0 ? { artifactId: download.artifactId } : {},
19969
+ ...download.contentType ? { contentType: download.contentType } : {},
19970
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {}
19971
+ },
19972
+ render3
19973
+ );
18107
19974
  return 0;
18108
19975
  }
18109
19976
  if (parsed.kind === "jobs-wait") {
18110
19977
  const parsedOptions = parsed.options;
18111
19978
  const data = await client.waitForJob(parsed.jobId, {
18112
19979
  onEvent: (event) => renderEvent(event, {
18113
- ...render2,
19980
+ ...render3,
18114
19981
  mode: parsedOptions.mode === "jsonl" ? "jsonl" : "human"
18115
19982
  })
18116
19983
  });
18117
- renderSuccess("jobs wait", data, render2);
19984
+ renderSuccess("jobs wait", data, render3);
18118
19985
  return 0;
18119
19986
  }
18120
19987
  if (parsed.kind === "jobs-list") {
18121
19988
  const data = await client.listJobs({ status: parsed.status, limit: parsed.limit });
18122
- renderSuccess("jobs list", data, render2);
19989
+ renderSuccess("jobs list", data, render3);
18123
19990
  return 0;
18124
19991
  }
18125
19992
  if (parsed.kind === "recordings-list") {
@@ -18128,22 +19995,22 @@ async function runCli(deps = {}) {
18128
19995
  cursor: parsed.cursor,
18129
19996
  search: parsed.search
18130
19997
  });
18131
- renderSuccess("recordings list", data, render2);
19998
+ renderSuccess("recordings list", data, render3);
18132
19999
  return 0;
18133
20000
  }
18134
20001
  if (parsed.kind === "recordings-get") {
18135
20002
  const data = await client.getRecording(parsed.recordingId);
18136
- renderSuccess("recordings get", data, render2);
20003
+ renderSuccess("recordings get", data, render3);
18137
20004
  return 0;
18138
20005
  }
18139
20006
  if (parsed.kind === "dashboard-stats") {
18140
20007
  const data = await client.dashboardStats();
18141
- renderSuccess("dashboard stats", data, render2);
20008
+ renderSuccess("dashboard stats", data, render3);
18142
20009
  return 0;
18143
20010
  }
18144
20011
  if (parsed.kind === "transcript-get") {
18145
20012
  const data = await client.getTranscript(parsed.transcriptId);
18146
- renderSuccess("transcript get", data, render2);
20013
+ renderSuccess("transcript get", data, render3);
18147
20014
  return 0;
18148
20015
  }
18149
20016
  throw cliError("usage.invalid_argument", "Unknown command.");
@@ -18335,6 +20202,17 @@ Agent mode:
18335
20202
  commandName: "doctor"
18336
20203
  });
18337
20204
  });
20205
+ const account = program.command("account").description("Account and quota commands");
20206
+ addCommonOptions(account);
20207
+ const accountStatus = account.command("status").description("Show account, quota, and local state");
20208
+ addCommonOptions(accountStatus);
20209
+ accountStatus.action((_options, command) => {
20210
+ onSelect({
20211
+ kind: "account-status",
20212
+ options: collectGlobalOptions(command),
20213
+ commandName: "account status"
20214
+ });
20215
+ });
18338
20216
  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
20217
  addCommonOptions(upload);
18340
20218
  upload.action((inputPath, opts, command) => {
@@ -18352,6 +20230,55 @@ Agent mode:
18352
20230
  ...opts.force === true ? { force: true } : {}
18353
20231
  });
18354
20232
  });
20233
+ 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(
20234
+ "--translation-language <lang>",
20235
+ "live caption translation language",
20236
+ parseStringOption("--translation-language")
20237
+ ).option(
20238
+ "--transcription-language <lang>",
20239
+ "recording/transcription language hint",
20240
+ parseStringOption("--transcription-language")
20241
+ ).option(
20242
+ "--sidecar-command <path>",
20243
+ "Recappi Mini sidecar executable",
20244
+ parseStringOption("--sidecar-command")
20245
+ );
20246
+ addCommonOptions(record2);
20247
+ record2.action((opts, command) => {
20248
+ if (opts.systemAudio === false && opts.microphone === false) {
20249
+ throw cliError("usage.invalid_argument", "Choose at least one recording input.", {
20250
+ hint: "Use system audio, microphone, or both."
20251
+ });
20252
+ }
20253
+ onSelect({
20254
+ kind: "record",
20255
+ options: collectGlobalOptions(command),
20256
+ commandName: "record",
20257
+ ...typeof opts.title === "string" ? { title: opts.title } : {},
20258
+ ...opts.live === true ? { live: true } : {},
20259
+ ...opts.systemAudio === false ? { includeSystemAudio: false } : {},
20260
+ ...opts.microphone === false ? { includeMicrophone: false } : {},
20261
+ ...typeof opts.translationLanguage === "string" ? { translationLanguage: opts.translationLanguage } : {},
20262
+ ...typeof opts.transcriptionLanguage === "string" ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
20263
+ ...typeof opts.sidecarCommand === "string" ? { sidecarCommand: opts.sidecarCommand } : {}
20264
+ });
20265
+ });
20266
+ 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(
20267
+ "--output-dir <dir>",
20268
+ "directory for downloaded audio",
20269
+ parseStringOption("--output-dir")
20270
+ );
20271
+ addCommonOptions(audio);
20272
+ audio.action((recordingId, opts, command) => {
20273
+ onSelect({
20274
+ kind: "audio",
20275
+ options: collectGlobalOptions(command),
20276
+ commandName: "audio",
20277
+ recordingId,
20278
+ action: audioAction(opts),
20279
+ ...opts.outputDir ? { outputDir: opts.outputDir } : {}
20280
+ });
20281
+ });
18355
20282
  const schema = program.command("schema").description("Print the machine-readable CLI contract (commands, error codes, JSON Schemas)");
18356
20283
  addCommonOptions(schema);
18357
20284
  schema.action((_options, command) => {
@@ -18448,6 +20375,17 @@ Agent mode:
18448
20375
  function addCommonOptions(command) {
18449
20376
  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
20377
  }
20378
+ function audioAction(opts) {
20379
+ const selected = [
20380
+ opts.download ? "download" : null,
20381
+ opts.open ? "open" : null,
20382
+ opts.reveal ? "reveal" : null
20383
+ ].filter((action) => action !== null);
20384
+ if (selected.length > 1) {
20385
+ throw cliError("usage.invalid_argument", "Choose only one of --download, --open, or --reveal.");
20386
+ }
20387
+ return selected[0] ?? "download";
20388
+ }
18451
20389
  function parseStringOption(flag) {
18452
20390
  return (value) => {
18453
20391
  if (!value || value.startsWith("-")) {
@@ -18513,6 +20451,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
18513
20451
  "--language",
18514
20452
  "--provider",
18515
20453
  "--prompt",
20454
+ "--translation-language",
20455
+ "--transcription-language",
20456
+ "--sidecar-command",
18516
20457
  "--status",
18517
20458
  "--limit",
18518
20459
  "--cursor",