recappi 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,41 +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", enabled: true }),
22
- /* @__PURE__ */ jsx(Tab, { num: "2", label: "Jobs", active: active === "jobs", enabled: true }),
23
- /* @__PURE__ */ jsx(Tab, { num: "3", label: "Recordings", active: active === "recordings", enabled: true })
24
- ] });
25
- }
26
- function Tab({
27
- num,
28
- label,
29
- active,
30
- enabled
31
- }) {
32
- const text = ` ${num} ${label}${enabled ? "" : " (soon)"} `;
33
- if (active) {
34
- return /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: text });
35
- }
36
- return /* @__PURE__ */ jsx(Text, { dimColor: !enabled, children: text });
37
- }
38
- function Footer({ keys }) {
39
- return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: keys }) });
40
- }
41
- var init_chrome = __esm({
42
- "src/tui/chrome.tsx"() {
43
- "use strict";
44
- }
45
- });
46
-
47
12
  // src/tui/format.ts
48
13
  function formatClockMs(ms) {
49
14
  if (ms == null || !Number.isFinite(ms) || ms < 0) return "--:--";
@@ -80,6 +45,23 @@ function statusStyle(status) {
80
45
  return { label: status, color: "white" };
81
46
  }
82
47
  }
48
+ function spinnerChar(frame) {
49
+ return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
50
+ }
51
+ function dateBucket(epochMs, nowMs) {
52
+ if (!epochMs) return "Earlier";
53
+ const startOfDay = (ms) => {
54
+ const d = new Date(ms);
55
+ d.setHours(0, 0, 0, 0);
56
+ return d.getTime();
57
+ };
58
+ const days = Math.floor((startOfDay(nowMs) - startOfDay(epochMs)) / 864e5);
59
+ if (days <= 0) return "Today";
60
+ if (days === 1) return "Yesterday";
61
+ if (days < 7) return "Previous 7 days";
62
+ if (days < 30) return "Previous 30 days";
63
+ return "Earlier";
64
+ }
83
65
  function statusGlyph(status, spinnerFrame) {
84
66
  switch (status) {
85
67
  case "running":
@@ -114,6 +96,45 @@ function padCell(text, width) {
114
96
  if (text.length > width) return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
115
97
  return text.padEnd(width);
116
98
  }
99
+ function charWidth(code) {
100
+ if (code >= 4352 && code <= 4447 || // Hangul Jamo
101
+ code === 9001 || code === 9002 || code >= 11904 && code <= 12350 || // CJK radicals … Kangxi
102
+ code >= 12353 && code <= 13311 || // Hiragana … CJK symbols
103
+ code >= 13312 && code <= 19903 || // CJK ext A
104
+ code >= 19968 && code <= 40959 || // CJK unified
105
+ code >= 40960 && code <= 42191 || // Yi
106
+ code >= 44032 && code <= 55203 || // Hangul syllables
107
+ code >= 63744 && code <= 64255 || // CJK compat
108
+ code >= 65072 && code <= 65103 || // CJK compat forms
109
+ code >= 65280 && code <= 65376 || // Fullwidth forms
110
+ code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || // emoji
111
+ code >= 131072 && code <= 262141) {
112
+ return 2;
113
+ }
114
+ return 1;
115
+ }
116
+ function displayWidth(text) {
117
+ let width = 0;
118
+ for (const ch of text) width += charWidth(ch.codePointAt(0) ?? 0);
119
+ return width;
120
+ }
121
+ function padDisplay(text, width) {
122
+ const w = displayWidth(text);
123
+ if (w === width) return text;
124
+ if (w < width) return text + " ".repeat(width - w);
125
+ let out = "";
126
+ let acc = 0;
127
+ for (const ch of text) {
128
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
129
+ if (acc + cw > width - 1) break;
130
+ out += ch;
131
+ acc += cw;
132
+ }
133
+ out += "\u2026";
134
+ acc += 1;
135
+ if (acc < width) out += " ".repeat(width - acc);
136
+ return out;
137
+ }
117
138
  function countJobs(items) {
118
139
  const counts = {
119
140
  total: items.length,
@@ -169,6 +190,53 @@ function recordingStatusStyle(status) {
169
190
  return { label: status, color: "white", glyph: "\u2022" };
170
191
  }
171
192
  }
193
+ function listWindow(selected, total, size) {
194
+ if (size <= 0 || total <= 0) return { start: 0, end: 0 };
195
+ if (total <= size) return { start: 0, end: total };
196
+ let start = selected - Math.floor(size / 2);
197
+ start = Math.max(0, Math.min(start, total - size));
198
+ return { start, end: start + size };
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
+ }
223
+ function groupedListWindow(buckets, selected, budget) {
224
+ const total = buckets.length;
225
+ if (budget <= 0 || total <= 0) return { start: 0, end: 0 };
226
+ const cost = (start, end) => {
227
+ if (end <= start) return 0;
228
+ let boundaries = 0;
229
+ for (let i = start + 1; i < end; i++) {
230
+ if (buckets[i] !== buckets[i - 1]) boundaries += 1;
231
+ }
232
+ return end - start + 1 + boundaries * 2;
233
+ };
234
+ for (let n = Math.min(total, budget); n >= 1; n--) {
235
+ const win = listWindow(selected, total, n);
236
+ if (cost(win.start, win.end) <= budget) return win;
237
+ }
238
+ return listWindow(selected, total, 1);
239
+ }
172
240
  function formatBytes2(bytes) {
173
241
  if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "";
174
242
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -189,9 +257,53 @@ var init_format = __esm({
189
257
  }
190
258
  });
191
259
 
192
- // src/tui/JobRow.tsx
260
+ // src/tui/terminal.ts
261
+ import { useWindowSize } from "ink";
262
+ function useTerminalSize() {
263
+ return useWindowSize();
264
+ }
265
+ var init_terminal = __esm({
266
+ "src/tui/terminal.ts"() {
267
+ "use strict";
268
+ }
269
+ });
270
+
271
+ // src/tui/chrome.tsx
193
272
  import { Box as Box2, Text as Text2 } from "ink";
194
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
273
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
274
+ function Header({ active }) {
275
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
276
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "magenta", children: [
277
+ "Recappi",
278
+ " "
279
+ ] }),
280
+ /* @__PURE__ */ jsx4(Tab, { num: "1", label: "Overview", active: active === "overview" }),
281
+ /* @__PURE__ */ jsx4(Tab, { num: "2", label: "Jobs", active: active === "jobs" })
282
+ ] });
283
+ }
284
+ function Tab({
285
+ num,
286
+ label,
287
+ active
288
+ }) {
289
+ const text = ` ${num} ${label} `;
290
+ if (active) {
291
+ return /* @__PURE__ */ jsx4(Text2, { bold: true, inverse: true, color: "cyan", children: text });
292
+ }
293
+ return /* @__PURE__ */ jsx4(Text2, { children: text });
294
+ }
295
+ function Footer({ keys }) {
296
+ return /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: keys }) });
297
+ }
298
+ var init_chrome = __esm({
299
+ "src/tui/chrome.tsx"() {
300
+ "use strict";
301
+ }
302
+ });
303
+
304
+ // src/tui/JobRow.tsx
305
+ import { Box as Box3, Text as Text3 } from "ink";
306
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
195
307
  function JobRow({
196
308
  item,
197
309
  selected,
@@ -200,11 +312,11 @@ function JobRow({
200
312
  const style = statusStyle(item.status);
201
313
  const glyph = statusGlyph(item.status, spinnerFrame);
202
314
  const title = item.recording?.title ?? item.recordingId;
203
- return /* @__PURE__ */ jsxs2(Box2, { children: [
204
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: selected ? "\u25B8 " : " " }),
205
- /* @__PURE__ */ jsx2(Text2, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
206
- /* @__PURE__ */ jsx2(Text2, { bold: selected, children: padCell(title, 24) }),
207
- /* @__PURE__ */ jsx2(Text2, { dimColor: !selected, children: jobDetail(item) })
315
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
316
+ /* @__PURE__ */ jsx5(Text3, { color: "cyan", children: selected ? "\u25B8 " : " " }),
317
+ /* @__PURE__ */ jsx5(Text3, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
318
+ /* @__PURE__ */ jsx5(Text3, { bold: selected, children: padCell(title, 24) }),
319
+ /* @__PURE__ */ jsx5(Text3, { dimColor: !selected, children: jobDetail(item) })
208
320
  ] });
209
321
  }
210
322
  var init_JobRow = __esm({
@@ -215,17 +327,17 @@ var init_JobRow = __esm({
215
327
  });
216
328
 
217
329
  // src/tui/JobsView.tsx
218
- import { Box as Box3, Text as Text3 } from "ink";
219
- import { jsx as jsx3 } from "react/jsx-runtime";
330
+ import { Box as Box4, Text as Text4 } from "ink";
331
+ import { jsx as jsx6 } from "react/jsx-runtime";
220
332
  function JobsView({
221
333
  items,
222
334
  selectedIndex,
223
335
  spinnerFrame
224
336
  }) {
225
337
  if (items.length === 0) {
226
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
338
+ return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
227
339
  }
228
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx3(
340
+ return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx6(
229
341
  JobRow,
230
342
  {
231
343
  item,
@@ -243,148 +355,236 @@ var init_JobsView = __esm({
243
355
  });
244
356
 
245
357
  // src/tui/RecordingRow.tsx
246
- import { Box as Box4, Text as Text4 } from "ink";
247
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
358
+ import { Box as Box5, Text as Text5 } from "ink";
359
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
248
360
  function recordingTitle2(item) {
249
- return item.title || item.summaryTitle || item.recordingId;
361
+ const named = (item.title || item.summaryTitle || "").trim();
362
+ if (named && !UUID_RE.test(named)) return named;
363
+ return "Untitled";
364
+ }
365
+ function recordingProcessingState(item, jobStatus, spinnerFrame) {
366
+ if (item.status === "uploading") return { glyph: "\u2191", color: "cyan" };
367
+ if (item.status === "failed" || jobStatus === "failed") return { glyph: "\u2717", color: "red" };
368
+ if (jobStatus === "running") return { glyph: spinnerChar(spinnerFrame), color: "cyan" };
369
+ if (jobStatus === "queued") return { glyph: "\u25CB", color: "yellow" };
370
+ if (item.status === "aborted") return { glyph: "\u2022", color: "gray" };
371
+ if (item.activeTranscriptId) return { glyph: "\u2713", color: "green" };
372
+ return { glyph: "\xB7", color: "gray" };
373
+ }
374
+ function recordingLayout(columns) {
375
+ const usable = Math.max(20, columns - 2);
376
+ const showWhen = usable >= 54;
377
+ const title = Math.max(
378
+ 10,
379
+ usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0)
380
+ );
381
+ return { title, showWhen };
250
382
  }
251
383
  function RecordingRow({
252
384
  item,
253
385
  selected,
254
- nowMs
386
+ nowMs,
387
+ columns,
388
+ jobStatus,
389
+ spinnerFrame = 0,
390
+ downloaded = false
255
391
  }) {
256
- const style = recordingStatusStyle(item.status);
257
- const detail = [
258
- item.durationMs ? formatClockMs(item.durationMs) : void 0,
259
- formatAge(item.createdAt, nowMs) || void 0,
260
- item.activeTranscriptId ? "transcript ready" : void 0
261
- ].filter(Boolean).join(" \xB7 ");
262
- return /* @__PURE__ */ jsxs3(Box4, { children: [
263
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
264
- /* @__PURE__ */ jsx4(Text4, { color: style.color, children: `${style.glyph} ${padCell(style.label, 10)}` }),
265
- /* @__PURE__ */ jsx4(Text4, { bold: selected, children: padCell(recordingTitle2(item), 26) }),
266
- /* @__PURE__ */ jsx4(Text4, { dimColor: !selected, children: detail })
392
+ const { title, showWhen } = recordingLayout(columns);
393
+ const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
394
+ const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
395
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
396
+ /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8 " : " " }),
397
+ /* @__PURE__ */ jsx7(Text5, { color, children: `${glyph} ` }),
398
+ /* @__PURE__ */ jsx7(Text5, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
399
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
400
+ showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
401
+ /* @__PURE__ */ jsx7(Text5, { color: "green", children: downloaded ? " \u2913" : " " })
402
+ ] });
403
+ }
404
+ function RecordingHeader({ columns }) {
405
+ const { title, showWhen } = recordingLayout(columns);
406
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
407
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
408
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("TITLE", title) }),
409
+ /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
410
+ showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: "WHEN" }) : null
267
411
  ] });
268
412
  }
413
+ var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
269
414
  var init_RecordingRow = __esm({
270
415
  "src/tui/RecordingRow.tsx"() {
271
416
  "use strict";
272
417
  init_format();
418
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
419
+ MARKER_W = 2;
420
+ GLYPH_W = 2;
421
+ LENGTH_W = 9;
422
+ WHEN_W = 9;
273
423
  }
274
424
  });
275
425
 
276
- // src/tui/OverviewView.tsx
277
- import { Box as Box5, Text as Text5 } from "ink";
278
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
279
- function overviewRecentRecordings(recordings) {
280
- return recordings.slice(0, RECENT_LIMIT);
426
+ // src/tui/RecordingsView.tsx
427
+ import React3 from "react";
428
+ import { Box as Box6, Text as Text6 } from "ink";
429
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
430
+ function RecordingsView({
431
+ items,
432
+ selectedIndex,
433
+ nowMs,
434
+ columns,
435
+ jobStatusByRecording,
436
+ downloadedRecordingIds,
437
+ spinnerFrame = 0
438
+ }) {
439
+ if (items.length === 0) {
440
+ return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
441
+ }
442
+ return /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
443
+ /* @__PURE__ */ jsx8(RecordingHeader, { columns }),
444
+ items.map((item, index) => {
445
+ const bucket = dateBucket(item.createdAt, nowMs);
446
+ const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
447
+ return /* @__PURE__ */ jsxs5(React3.Fragment, { children: [
448
+ showHeader ? /* @__PURE__ */ jsx8(Box6, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx8(Text6, { bold: true, color: "blue", children: bucket }) }) : null,
449
+ /* @__PURE__ */ jsx8(
450
+ RecordingRow,
451
+ {
452
+ item,
453
+ selected: index === selectedIndex,
454
+ nowMs,
455
+ columns,
456
+ jobStatus: jobStatusByRecording?.get(item.recordingId),
457
+ downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
458
+ spinnerFrame
459
+ }
460
+ )
461
+ ] }, item.recordingId);
462
+ })
463
+ ] });
464
+ }
465
+ var init_RecordingsView = __esm({
466
+ "src/tui/RecordingsView.tsx"() {
467
+ "use strict";
468
+ init_RecordingRow();
469
+ init_format();
470
+ }
471
+ });
472
+
473
+ // src/tui/RecordingPeek.tsx
474
+ import { Box as Box7, Text as Text7 } from "ink";
475
+ import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
476
+ function RecordingPeek({
477
+ item,
478
+ summary,
479
+ nowMs,
480
+ width
481
+ }) {
482
+ return /* @__PURE__ */ jsx9(Box7, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx9(PeekBody, { item, summary, nowMs }) });
483
+ }
484
+ function PeekBody({
485
+ item,
486
+ summary,
487
+ nowMs
488
+ }) {
489
+ const style = recordingStatusStyle(item.status);
490
+ const meta3 = [
491
+ item.durationMs ? formatClockMs(item.durationMs) : null,
492
+ formatBytes2(item.sizeBytes) || null,
493
+ item.contentType || null
494
+ ].filter(Boolean).join(" \xB7 ");
495
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
496
+ /* @__PURE__ */ jsx9(Text7, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
497
+ /* @__PURE__ */ jsx9(Text7, { color: style.color, children: `${style.glyph} ${style.label}` }),
498
+ meta3 ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: meta3 }) : null,
499
+ /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
500
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx9(SummarySection, { item, summary }) }),
501
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
502
+ ] });
503
+ }
504
+ function SummarySection({
505
+ item,
506
+ summary
507
+ }) {
508
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No transcript yet" });
509
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "Loading summary\u2026" });
510
+ if (summary === "error") return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "(summary unavailable)" });
511
+ if (summary.status !== "succeeded" || !summary.tldr) {
512
+ return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: `Summary ${summary.status}` });
513
+ }
514
+ const points = (summary.keyPoints ?? []).slice(0, 3);
515
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
516
+ /* @__PURE__ */ jsx9(Text7, { bold: true, children: "Summary" }),
517
+ /* @__PURE__ */ jsx9(Text7, { children: summary.tldr }),
518
+ points.length > 0 ? /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx9(Text7, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
519
+ ] });
281
520
  }
521
+ var init_RecordingPeek = __esm({
522
+ "src/tui/RecordingPeek.tsx"() {
523
+ "use strict";
524
+ init_format();
525
+ init_RecordingRow();
526
+ }
527
+ });
528
+
529
+ // src/tui/OverviewView.tsx
530
+ import { Box as Box8, Text as Text8 } from "ink";
531
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
282
532
  function OverviewView({
283
533
  recordings,
284
534
  jobs,
285
535
  stats,
286
536
  selectedIndex,
287
537
  spinnerFrame,
288
- nowMs
538
+ nowMs,
539
+ columns,
540
+ jobStatusByRecording,
541
+ downloadedRecordingIds,
542
+ peekItem,
543
+ peekSummary,
544
+ showPeek = false,
545
+ peekWidth = 0
289
546
  }) {
290
- const recent = overviewRecentRecordings(recordings);
291
- const activeJobs = jobs.filter((j) => j.status === "running" || j.status === "queued");
292
547
  const jobCounts = countJobs(jobs);
293
- const recTotal = stats?.recordings.total ?? recordings.length;
294
- const recReady = stats?.recordings.ready;
295
- const transcribed = stats?.recordings.totalDurationMs;
296
- return /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
297
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
298
- /* @__PURE__ */ jsxs4(Text5, { children: [
299
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Recordings " }),
300
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: recTotal }),
301
- recReady != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${recReady} ready` }) : null,
302
- transcribed != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${formatClockMs(transcribed)} transcribed` }) : null
303
- ] }),
304
- /* @__PURE__ */ jsxs4(Text5, { children: [
305
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Jobs " }),
306
- /* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
307
- stats?.jobs.running ?? jobCounts.running,
308
- " running"
309
- ] }),
310
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
311
- /* @__PURE__ */ jsxs4(Text5, { color: "yellow", children: [
312
- stats?.jobs.queued ?? jobCounts.queued,
313
- " queued"
314
- ] }),
315
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
316
- /* @__PURE__ */ jsxs4(Text5, { color: "green", children: [
317
- stats?.jobs.succeeded ?? jobCounts.succeeded,
318
- " done"
319
- ] })
320
- ] })
548
+ const running = stats?.jobs.running ?? jobCounts.running;
549
+ const queued = stats?.jobs.queued ?? jobCounts.queued;
550
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
551
+ /* @__PURE__ */ jsxs7(Box8, { children: [
552
+ /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Recordings " }),
553
+ /* @__PURE__ */ jsx10(Text8, { bold: true, children: stats?.recordings.total ?? recordings.length }),
554
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
555
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
556
+ running > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
557
+ queued > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
321
558
  ] }),
322
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
323
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Recent recordings" }),
324
- recent.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " No recordings yet \u2014 run: recappi upload <file>" }) : recent.map((item, index) => /* @__PURE__ */ jsx5(
325
- RecordingRow,
559
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", alignItems: "flex-start", children: [
560
+ /* @__PURE__ */ jsx10(Box8, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(
561
+ RecordingsView,
326
562
  {
327
- item,
328
- selected: index === selectedIndex,
329
- nowMs
330
- },
331
- item.recordingId
332
- ))
333
- ] }),
334
- activeJobs.length > 0 ? /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
335
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Transcribing now" }),
336
- activeJobs.slice(0, 3).map((job) => {
337
- const style = statusStyle(job.status);
338
- return /* @__PURE__ */ jsxs4(Box5, { children: [
339
- /* @__PURE__ */ jsx5(Text5, { children: " " }),
340
- /* @__PURE__ */ jsx5(Text5, { color: style.color, children: `${statusGlyph(job.status, spinnerFrame)} ` }),
341
- /* @__PURE__ */ jsx5(Text5, { children: job.recording?.title ?? job.recordingId })
342
- ] }, job.jobId);
343
- })
344
- ] }) : null
563
+ items: recordings,
564
+ selectedIndex,
565
+ nowMs,
566
+ columns,
567
+ jobStatusByRecording,
568
+ downloadedRecordingIds,
569
+ spinnerFrame
570
+ }
571
+ ) }),
572
+ showPeek ? /* @__PURE__ */ jsx10(Box8, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx10(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
573
+ ] })
345
574
  ] });
346
575
  }
347
- var RECENT_LIMIT;
348
576
  var init_OverviewView = __esm({
349
577
  "src/tui/OverviewView.tsx"() {
350
578
  "use strict";
351
- init_RecordingRow();
579
+ init_RecordingsView();
580
+ init_RecordingPeek();
352
581
  init_format();
353
- RECENT_LIMIT = 6;
354
- }
355
- });
356
-
357
- // src/tui/RecordingsView.tsx
358
- import { Box as Box6, Text as Text6 } from "ink";
359
- import { jsx as jsx6 } from "react/jsx-runtime";
360
- function RecordingsView({
361
- items,
362
- selectedIndex,
363
- nowMs
364
- }) {
365
- if (items.length === 0) {
366
- return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
367
- }
368
- return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx6(
369
- RecordingRow,
370
- {
371
- item,
372
- selected: index === selectedIndex,
373
- nowMs
374
- },
375
- item.recordingId
376
- )) });
377
- }
378
- var init_RecordingsView = __esm({
379
- "src/tui/RecordingsView.tsx"() {
380
- "use strict";
381
- init_RecordingRow();
382
582
  }
383
583
  });
384
584
 
385
585
  // src/tui/JobDetailView.tsx
386
- import { Box as Box7, Text as Text7 } from "ink";
387
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
586
+ import { Box as Box9, Text as Text9 } from "ink";
587
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
388
588
  function JobDetailView({
389
589
  item,
390
590
  origin,
@@ -394,13 +594,13 @@ function JobDetailView({
394
594
  const style = statusStyle(item.status);
395
595
  const links = resolveJobLinks(item, origin);
396
596
  const title = item.recording?.title ?? item.recordingId;
397
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 1, children: [
398
- /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
597
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 1, children: [
598
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
399
599
  "\u2039 Jobs / ",
400
600
  title
401
601
  ] }),
402
- /* @__PURE__ */ jsxs5(
403
- Box7,
602
+ /* @__PURE__ */ jsxs8(
603
+ Box9,
404
604
  {
405
605
  marginTop: 1,
406
606
  borderStyle: "round",
@@ -408,17 +608,17 @@ function JobDetailView({
408
608
  paddingX: 1,
409
609
  flexDirection: "column",
410
610
  children: [
411
- /* @__PURE__ */ jsxs5(Text7, { color: style.color, bold: true, children: [
611
+ /* @__PURE__ */ jsxs8(Text9, { color: style.color, bold: true, children: [
412
612
  style.label,
413
- item.provider ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${item.provider}` }) : null
613
+ item.provider ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${item.provider}` }) : null
414
614
  ] }),
415
- /* @__PURE__ */ jsx7(StatusLine, { item, spinnerFrame, nowMs })
615
+ /* @__PURE__ */ jsx11(StatusLine, { item, spinnerFrame, nowMs })
416
616
  ]
417
617
  }
418
618
  ),
419
- /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "column", children: [
420
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Timeline" }),
421
- /* @__PURE__ */ jsx7(
619
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
620
+ /* @__PURE__ */ jsx11(Text9, { bold: true, children: "Timeline" }),
621
+ /* @__PURE__ */ jsx11(
422
622
  TimelineRow,
423
623
  {
424
624
  label: "Enqueued",
@@ -427,7 +627,7 @@ function JobDetailView({
427
627
  nowMs
428
628
  }
429
629
  ),
430
- /* @__PURE__ */ jsx7(
630
+ /* @__PURE__ */ jsx11(
431
631
  TimelineRow,
432
632
  {
433
633
  label: "Started",
@@ -436,7 +636,7 @@ function JobDetailView({
436
636
  nowMs
437
637
  }
438
638
  ),
439
- /* @__PURE__ */ jsx7(
639
+ /* @__PURE__ */ jsx11(
440
640
  TimelineRow,
441
641
  {
442
642
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -448,21 +648,21 @@ function JobDetailView({
448
648
  }
449
649
  )
450
650
  ] }),
451
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
452
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recording " }),
651
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
652
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Recording " }),
453
653
  title,
454
- item.recording?.durationMs ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
654
+ item.recording?.durationMs ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
455
655
  ] }) }),
456
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
457
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
458
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
459
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
460
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
461
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "m mac app (soon)" }),
462
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
463
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
656
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
657
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
658
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
659
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
660
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
661
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "m mac app (soon)" }),
662
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
663
+ /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
464
664
  ] }) }),
465
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
665
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
466
666
  "esc back \xB7 t transcript",
467
667
  item.transcriptId ? "" : " (when ready)",
468
668
  " \xB7 q quit"
@@ -479,24 +679,24 @@ function StatusLine({
479
679
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
480
680
  if (fraction != null) {
481
681
  const pct = Math.round(fraction * 100);
482
- return /* @__PURE__ */ jsxs5(Text7, { children: [
682
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
483
683
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
484
684
  item.recording?.durationMs
485
685
  )}`,
486
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
686
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
487
687
  ] });
488
688
  }
489
689
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
490
- return /* @__PURE__ */ jsxs5(Text7, { children: [
690
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
491
691
  `${spinner} transcribing\u2026`,
492
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
692
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
493
693
  ] });
494
694
  }
495
695
  if (item.status === "succeeded")
496
- return /* @__PURE__ */ jsx7(Text7, { children: item.transcriptId ? "transcript ready" : "done" });
497
- if (item.status === "queued") return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "waiting to start\u2026" });
498
- if (item.status === "failed") return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "transcription failed" });
499
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: item.status });
696
+ return /* @__PURE__ */ jsx11(Text9, { children: item.transcriptId ? "transcript ready" : "done" });
697
+ if (item.status === "queued") return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "waiting to start\u2026" });
698
+ if (item.status === "failed") return /* @__PURE__ */ jsx11(Text9, { color: "red", children: "transcription failed" });
699
+ return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: item.status });
500
700
  }
501
701
  function TimelineRow({
502
702
  label,
@@ -509,10 +709,10 @@ function TimelineRow({
509
709
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
510
710
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
511
711
  const age = at ? formatAge(at, nowMs) : running ? "now" : "";
512
- return /* @__PURE__ */ jsxs5(Box7, { children: [
513
- /* @__PURE__ */ jsx7(Text7, { color, children: ` ${glyph} ` }),
514
- /* @__PURE__ */ jsx7(Text7, { dimColor: !done && !running, children: label }),
515
- age ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${age}` }) : null
712
+ return /* @__PURE__ */ jsxs8(Box9, { children: [
713
+ /* @__PURE__ */ jsx11(Text9, { color, children: ` ${glyph} ` }),
714
+ /* @__PURE__ */ jsx11(Text9, { dimColor: !done && !running, children: label }),
715
+ age ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${age}` }) : null
516
716
  ] });
517
717
  }
518
718
  var init_JobDetailView = __esm({
@@ -523,12 +723,19 @@ var init_JobDetailView = __esm({
523
723
  });
524
724
 
525
725
  // src/tui/RecordingDetailView.tsx
526
- import { Box as Box8, Text as Text8 } from "ink";
527
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
726
+ import React4, { useMemo as useMemo2, useState as useState3 } from "react";
727
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
728
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
528
729
  function RecordingDetailView({
529
730
  item,
530
- nowMs
731
+ nowMs,
732
+ transcript,
733
+ audio
531
734
  }) {
735
+ const size = useTerminalSize();
736
+ const [tab, setTab] = useState3("summary");
737
+ const [scroll, setScroll] = useState3(0);
738
+ const [chapterSel, setChapterSel] = useState3(0);
532
739
  const style = recordingStatusStyle(item.status);
533
740
  const links = resolveRecordingLinks(item.recordingId, item.origin);
534
741
  const title = recordingTitle2(item);
@@ -537,114 +744,266 @@ function RecordingDetailView({
537
744
  formatBytes2(item.sizeBytes) || void 0,
538
745
  item.contentType || void 0
539
746
  ].filter(Boolean).join(" \xB7 ");
540
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", paddingX: 1, children: [
541
- /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
542
- "\u2039 Recordings / ",
543
- title
747
+ const ready = typeof transcript === "object";
748
+ const summary = ready ? transcript.summary : void 0;
749
+ const segments = ready ? transcript.segments : [];
750
+ const chapters = summary?.timeline ?? [];
751
+ const innerWidth = Math.max(10, size.columns - 2);
752
+ const paneBudget = Math.max(3, size.rows - 12);
753
+ const segHeights = useMemo2(
754
+ () => segments.map((seg) => {
755
+ const prefix = `[${formatClockMs(seg.startMs)}] ${seg.speaker ? `${seg.speaker}: ` : ""}`;
756
+ return Math.max(1, Math.ceil(displayWidth(prefix + seg.text) / innerWidth));
757
+ }),
758
+ [segments, innerWidth]
759
+ );
760
+ const segWin = windowByHeights(segHeights, scroll, paneBudget);
761
+ const chapWin = listWindow(Math.min(chapterSel, Math.max(0, chapters.length - 1)), chapters.length, paneBudget);
762
+ const page = Math.max(1, paneBudget - 1);
763
+ const scrollable = tab === "transcript" ? segWin.maxScroll > 0 : false;
764
+ const jumpToChapter = (index) => {
765
+ const chapter = chapters[index];
766
+ if (!chapter) return;
767
+ const found = segments.findIndex((s) => s.startMs >= chapter.startMs);
768
+ setScroll(found < 0 ? Math.max(0, segments.length - 1) : found);
769
+ setTab("transcript");
770
+ };
771
+ useInput3((input, key) => {
772
+ if (!item.activeTranscriptId || !ready) return;
773
+ if (key.tab) {
774
+ setTab((t) => TAB_ORDER[(TAB_ORDER.indexOf(t) + (key.shift ? TAB_ORDER.length - 1 : 1)) % TAB_ORDER.length]);
775
+ return;
776
+ }
777
+ if (tab === "summary") return;
778
+ if (tab === "chapters") {
779
+ if (key.downArrow || input === "j") setChapterSel((i) => Math.min(chapters.length - 1, i + 1));
780
+ else if (key.upArrow || input === "k") setChapterSel((i) => Math.max(0, i - 1));
781
+ else if (key.return || key.rightArrow) jumpToChapter(chapterSel);
782
+ return;
783
+ }
784
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(segWin.maxScroll, s + 1));
785
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
786
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(segWin.maxScroll, s + page));
787
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
788
+ else if (input === "g") setScroll(0);
789
+ else if (input === "G") setScroll(segWin.maxScroll);
790
+ });
791
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
792
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "\u2039 Recordings" }),
793
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { bold: true, color: "magenta", children: title }) }),
794
+ /* @__PURE__ */ jsxs9(Text10, { children: [
795
+ /* @__PURE__ */ jsx12(Text10, { color: style.color, children: `${style.glyph} ${style.label}` }),
796
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
544
797
  ] }),
545
- /* @__PURE__ */ jsxs6(
546
- Box8,
547
- {
548
- marginTop: 1,
549
- borderStyle: "round",
550
- borderColor: style.color,
551
- paddingX: 1,
552
- flexDirection: "column",
553
- children: [
554
- /* @__PURE__ */ jsxs6(Text8, { color: style.color, bold: true, children: [
555
- style.label,
556
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` created ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
557
- ] }),
558
- meta3 ? /* @__PURE__ */ jsx8(Text8, { children: meta3 }) : null
559
- ]
560
- }
561
- ),
562
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
563
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Transcript " }),
564
- item.activeTranscriptId ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "ready \u2014 press t to view" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "not available yet" })
565
- ] }) }),
566
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
567
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
568
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
569
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
570
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
571
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "m mac app (soon)" }),
572
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
573
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
574
- ] }) }),
575
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
576
- "esc back \xB7 ",
577
- item.activeTranscriptId ? "t transcript \xB7 " : "",
578
- "q quit"
798
+ meta3 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: meta3 }) : null,
799
+ /* @__PURE__ */ jsx12(AudioActionRow, { item, audio }),
800
+ !item.activeTranscriptId ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
801
+ /* @__PURE__ */ jsx12(TabBar, { active: tab }),
802
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx12(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx12(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx12(TranscriptPane, { segments, win: segWin }) })
803
+ ] }),
804
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
805
+ ready ? "tab switch" : "",
806
+ ready && tab === "chapters" ? " \xB7 \u2191\u2193 select \xB7 \u23CE jump" : "",
807
+ scrollable ? " \xB7 \u2191\u2193 scroll" : "",
808
+ ready ? " \xB7 " : "",
809
+ `o open \xB7 d download \xB7 f finder`,
810
+ item.activeTranscriptId ? " \xB7 t full" : "",
811
+ links.webUrl ? " \xB7 w web" : "",
812
+ " \xB7 esc back"
579
813
  ] }) })
580
814
  ] });
581
815
  }
816
+ function TabBar({ active }) {
817
+ return /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs9(React4.Fragment, { children: [
818
+ i > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " " }) : null,
819
+ tab === active ? /* @__PURE__ */ jsx12(Text10, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
820
+ ] }, tab)) });
821
+ }
822
+ function AudioActionRow({
823
+ item,
824
+ audio
825
+ }) {
826
+ const ready = item.status === "ready";
827
+ const status = audio?.status ?? "idle";
828
+ let line;
829
+ if (!ready) {
830
+ line = /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Audio available once the recording is ready" });
831
+ } else if (status === "downloading") {
832
+ line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Downloading audio\u2026" });
833
+ } else if (status === "opening") {
834
+ line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Opening\u2026" });
835
+ } else if (status === "error") {
836
+ line = /* @__PURE__ */ jsx12(Text10, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
837
+ } else if (status === "ready" && audio?.localPath) {
838
+ line = /* @__PURE__ */ jsxs9(Text10, { children: [
839
+ /* @__PURE__ */ jsx12(Text10, { color: "green", children: "\u2713 Downloaded " }),
840
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
841
+ ] });
842
+ } else {
843
+ line = /* @__PURE__ */ jsxs9(Text10, { children: [
844
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "o" }),
845
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " open in player \xB7 " }),
846
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "d" }),
847
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " download \xB7 " }),
848
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "f" }),
849
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " reveal in Finder" })
850
+ ] });
851
+ }
852
+ return /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
853
+ /* @__PURE__ */ jsx12(Text10, { color: ready ? "cyan" : "gray", children: "\u266A " }),
854
+ line
855
+ ] });
856
+ }
857
+ function SummaryPane({
858
+ summary,
859
+ budget
860
+ }) {
861
+ if (!summary || summary.status !== "succeeded" || !summary.tldr) {
862
+ return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
863
+ }
864
+ const points = (summary.keyPoints ?? []).slice(0, Math.max(1, budget - 4));
865
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
866
+ /* @__PURE__ */ jsx12(Text10, { children: summary.tldr }),
867
+ points.length > 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
868
+ ] });
869
+ }
870
+ function ChaptersPane({
871
+ chapters,
872
+ win,
873
+ selectedIndex
874
+ }) {
875
+ if (chapters.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "No chapters" });
876
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
877
+ chapters.slice(win.start, win.end).map((chapter, i) => {
878
+ const index = win.start + i;
879
+ const selected = index === selectedIndex;
880
+ return /* @__PURE__ */ jsxs9(Text10, { wrap: "truncate-end", children: [
881
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: selected ? "\u25B8 " : " " }),
882
+ /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
883
+ /* @__PURE__ */ jsx12(Text10, { bold: selected, children: chapter.title })
884
+ ] }, index);
885
+ }),
886
+ win.end < chapters.length || win.start > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${selectedIndex + 1} / ${chapters.length}` }) : null
887
+ ] });
888
+ }
889
+ function TranscriptPane({
890
+ segments,
891
+ win
892
+ }) {
893
+ if (segments.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(no segments)" });
894
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
895
+ segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs9(Text10, { children: [
896
+ /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
897
+ seg.speaker ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `${seg.speaker} ` }) : null,
898
+ /* @__PURE__ */ jsx12(Text10, { children: seg.text })
899
+ ] }, win.start + i)),
900
+ win.maxScroll > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
901
+ ] });
902
+ }
903
+ var TAB_ORDER, TAB_LABEL;
582
904
  var init_RecordingDetailView = __esm({
583
905
  "src/tui/RecordingDetailView.tsx"() {
584
906
  "use strict";
585
907
  init_format();
586
908
  init_RecordingRow();
909
+ init_terminal();
910
+ TAB_ORDER = ["summary", "chapters", "transcript"];
911
+ TAB_LABEL = {
912
+ summary: "Summary",
913
+ chapters: "Chapters",
914
+ transcript: "Transcript"
915
+ };
587
916
  }
588
917
  });
589
918
 
590
919
  // src/tui/TranscriptView.tsx
591
- import { Box as Box9, Text as Text9 } from "ink";
592
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
920
+ import { useMemo as useMemo3, useState as useState4 } from "react";
921
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
922
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
593
923
  function TranscriptView({ loading, data, error: error51 }) {
924
+ const size = useTerminalSize();
925
+ const [scroll, setScroll] = useState4(0);
926
+ const segments = data?.segments ?? [];
927
+ const innerWidth = Math.max(10, size.columns - 2);
928
+ const heights = useMemo3(
929
+ () => segments.map((s) => {
930
+ const prefix = `[${formatClockMs(s.startMs)}] ${s.speaker ? `${s.speaker}: ` : ""}`;
931
+ return Math.max(1, Math.ceil(displayWidth(prefix + s.text) / innerWidth));
932
+ }),
933
+ [segments, innerWidth]
934
+ );
935
+ const budget = Math.max(3, size.rows - 3);
936
+ const win = windowByHeights(heights, scroll, budget);
937
+ const page = Math.max(1, budget - 1);
938
+ useInput4((input, key) => {
939
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(win.maxScroll, s + 1));
940
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
941
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(win.maxScroll, s + page));
942
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
943
+ else if (input === "g") setScroll(0);
944
+ else if (input === "G") setScroll(win.maxScroll);
945
+ });
594
946
  if (loading) {
595
- return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading transcript\u2026" }) });
947
+ return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Loading transcript\u2026" }) });
596
948
  }
597
949
  if (error51) {
598
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
599
- /* @__PURE__ */ jsxs7(Text9, { color: "red", children: [
950
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
951
+ /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
600
952
  "! ",
601
953
  error51
602
954
  ] }),
603
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" })
955
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "q / esc / \u2190 back" })
604
956
  ] });
605
957
  }
606
958
  if (!data) {
607
- return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "No transcript." }) });
608
- }
609
- const summary = data.summary;
610
- const showSummary = summary?.status === "succeeded";
611
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
612
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
613
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx9(Text9, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs7(Text9, { children: [
614
- /* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
959
+ return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "No transcript." }) });
960
+ }
961
+ const title = data.summary?.title ?? "Transcript";
962
+ const total = segments.length;
963
+ const more = win.maxScroll > 0;
964
+ const position = total === 0 ? "" : `${win.start + 1}\u2013${win.end} / ${total}`;
965
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
966
+ /* @__PURE__ */ jsxs10(Text11, { bold: true, color: "magenta", children: [
967
+ title,
968
+ more ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${position}` }) : null
969
+ ] }),
970
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx13(Text11, { children: data.text }) : segments.slice(win.start, win.end).map((segment, index) => /* @__PURE__ */ jsxs10(Text11, { children: [
971
+ /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
615
972
  "[",
616
973
  formatClockMs(segment.startMs),
617
974
  "] "
618
975
  ] }),
619
- segment.speaker ? /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
976
+ segment.speaker ? /* @__PURE__ */ jsxs10(Text11, { color: "cyan", children: [
620
977
  segment.speaker,
621
978
  ": "
622
979
  ] }) : null,
623
980
  segment.text
624
- ] }, index)) }),
625
- showSummary && summary?.tldr ? /* @__PURE__ */ jsxs7(Box9, { marginTop: 1, flexDirection: "column", children: [
626
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Summary" }),
627
- /* @__PURE__ */ jsx9(Text9, { children: summary.tldr })
628
- ] }) : null,
629
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" }) })
981
+ ] }, win.start + index)) }),
982
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
983
+ more ? "\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 g/G top/bottom \xB7 " : "",
984
+ "q / esc / \u2190 back"
985
+ ] }) })
630
986
  ] });
631
987
  }
632
988
  var init_TranscriptView = __esm({
633
989
  "src/tui/TranscriptView.tsx"() {
634
990
  "use strict";
635
991
  init_format();
992
+ init_terminal();
636
993
  }
637
994
  });
638
995
 
639
996
  // src/tui/AppShell.tsx
640
- import { useCallback, useEffect, useState } from "react";
641
- import { Box as Box10, Text as Text10, useApp, useInput } from "ink";
642
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
997
+ import { useCallback, useEffect as useEffect2, useState as useState5 } from "react";
998
+ import { Box as Box12, Text as Text12, useApp, useInput as useInput5 } from "ink";
999
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
643
1000
  function AppShell({
644
1001
  fetchJobs,
645
1002
  fetchTranscript,
646
1003
  fetchRecordings,
647
1004
  fetchDashboardStats,
1005
+ recordingAudio,
1006
+ listDownloadedRecordingIds,
648
1007
  initialView = "overview",
649
1008
  openUrl: openUrl2,
650
1009
  copyText: copyText2,
@@ -653,20 +1012,59 @@ function AppShell({
653
1012
  spinnerMs = 80
654
1013
  }) {
655
1014
  const { exit } = useApp();
656
- const [jobs, setJobs] = useState([]);
657
- const [recordings, setRecordings] = useState([]);
658
- const [stats, setStats] = useState(void 0);
659
- const [origin, setOrigin] = useState("");
660
- const [stack, setStack] = useState([{ kind: initialView }]);
661
- const [selected, setSelected] = useState(0);
662
- const [spinnerFrame, setSpinnerFrame] = useState(0);
663
- const [loadError, setLoadError] = useState(void 0);
664
- const [notice, setNotice] = useState(void 0);
1015
+ const size = useTerminalSize();
1016
+ const [jobs, setJobs] = useState5([]);
1017
+ const [recordings, setRecordings] = useState5([]);
1018
+ const [recordingsNextCursor, setRecordingsNextCursor] = useState5(null);
1019
+ const [recordingsTotalCount, setRecordingsTotalCount] = useState5(void 0);
1020
+ const [stats, setStats] = useState5(void 0);
1021
+ const [origin, setOrigin] = useState5("");
1022
+ const [stack, setStack] = useState5([{ kind: initialView }]);
1023
+ const [selected, setSelected] = useState5(0);
1024
+ const [spinnerFrame, setSpinnerFrame] = useState5(0);
1025
+ const [loadingMoreRecordings, setLoadingMoreRecordings] = useState5(false);
1026
+ const [loadError, setLoadError] = useState5(void 0);
1027
+ const [notice, setNotice] = useState5(void 0);
1028
+ const [summaryCache, setSummaryCache] = useState5(() => /* @__PURE__ */ new Map());
1029
+ const [transcriptCache, setTranscriptCache] = useState5(
1030
+ () => /* @__PURE__ */ new Map()
1031
+ );
1032
+ const [audioCache, setAudioCache] = useState5(() => /* @__PURE__ */ new Map());
1033
+ const [downloadedIds, setDownloadedIds] = useState5(() => /* @__PURE__ */ new Set());
1034
+ const refreshDownloadedIds = useCallback(async () => {
1035
+ if (!listDownloadedRecordingIds) return;
1036
+ try {
1037
+ setDownloadedIds(await listDownloadedRecordingIds());
1038
+ } catch {
1039
+ }
1040
+ }, [listDownloadedRecordingIds]);
1041
+ useEffect2(() => {
1042
+ void refreshDownloadedIds();
1043
+ }, [refreshDownloadedIds]);
665
1044
  const screen = stack[stack.length - 1];
666
- const refresh = useCallback(async () => {
1045
+ const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
1046
+ const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
1047
+ useEffect2(() => {
1048
+ if (!peekTranscriptId || summaryCache.has(peekTranscriptId)) return;
1049
+ let cancelled = false;
1050
+ const timer = setTimeout(() => {
1051
+ setSummaryCache((m) => new Map(m).set(peekTranscriptId, "loading"));
1052
+ fetchTranscript(peekTranscriptId).then((tr) => {
1053
+ if (!cancelled) setSummaryCache((m) => new Map(m).set(peekTranscriptId, tr.summary));
1054
+ }).catch(() => {
1055
+ if (!cancelled) setSummaryCache((m) => new Map(m).set(peekTranscriptId, "error"));
1056
+ });
1057
+ }, 200);
1058
+ return () => {
1059
+ cancelled = true;
1060
+ clearTimeout(timer);
1061
+ };
1062
+ }, [peekTranscriptId, fetchTranscript]);
1063
+ const peekSummary = peekTranscriptId ? summaryCache.get(peekTranscriptId) : void 0;
1064
+ const refresh = useCallback(async ({ resetRecordings = false } = {}) => {
667
1065
  const [jobsR, recR, statsR] = await Promise.allSettled([
668
1066
  fetchJobs(),
669
- fetchRecordings ? fetchRecordings() : Promise.resolve(void 0),
1067
+ resetRecordings && fetchRecordings ? fetchRecordings({ limit: RECORDINGS_PAGE_SIZE }) : Promise.resolve(void 0),
670
1068
  fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
671
1069
  ]);
672
1070
  if (jobsR.status === "fulfilled") {
@@ -676,25 +1074,75 @@ function AppShell({
676
1074
  } else {
677
1075
  setLoadError(jobsR.reason instanceof Error ? jobsR.reason.message : String(jobsR.reason));
678
1076
  }
679
- if (recR.status === "fulfilled" && recR.value) setRecordings(recR.value.items);
1077
+ if (recR.status === "fulfilled" && recR.value) {
1078
+ setRecordings(recR.value.items);
1079
+ setRecordingsNextCursor(recR.value.nextCursor ?? null);
1080
+ setRecordingsTotalCount(recR.value.totalCount);
1081
+ }
680
1082
  if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
681
1083
  }, [fetchJobs, fetchRecordings, fetchDashboardStats]);
682
- useEffect(() => {
683
- void refresh();
1084
+ const loadMoreRecordings = useCallback(async () => {
1085
+ if (!fetchRecordings || !recordingsNextCursor || loadingMoreRecordings) return;
1086
+ setLoadingMoreRecordings(true);
1087
+ try {
1088
+ const page = await fetchRecordings({
1089
+ limit: RECORDINGS_PAGE_SIZE,
1090
+ cursor: recordingsNextCursor
1091
+ });
1092
+ setRecordings((prev) => {
1093
+ const seen = new Set(prev.map((item) => item.recordingId));
1094
+ const merged = [...prev];
1095
+ for (const item of page.items) {
1096
+ if (!seen.has(item.recordingId)) merged.push(item);
1097
+ }
1098
+ return merged;
1099
+ });
1100
+ setRecordingsNextCursor(page.nextCursor ?? null);
1101
+ setRecordingsTotalCount(page.totalCount);
1102
+ setLoadError(void 0);
1103
+ } catch (error51) {
1104
+ setLoadError(error51 instanceof Error ? error51.message : String(error51));
1105
+ } finally {
1106
+ setLoadingMoreRecordings(false);
1107
+ }
1108
+ }, [fetchRecordings, loadingMoreRecordings, recordingsNextCursor]);
1109
+ useEffect2(() => {
1110
+ void refresh({ resetRecordings: true });
684
1111
  const id = setInterval(() => void refresh(), pollMs);
685
1112
  return () => clearInterval(id);
686
1113
  }, [refresh, pollMs]);
687
1114
  const hasRunning = jobs.some((item) => item.status === "running");
688
- useEffect(() => {
1115
+ useEffect2(() => {
689
1116
  if (!hasRunning) return;
690
1117
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
691
1118
  return () => clearInterval(id);
692
1119
  }, [hasRunning, spinnerMs]);
693
- const recordingList = screen.kind === "overview" ? overviewRecentRecordings(recordings) : recordings;
694
- const listLength = screen.kind === "jobs" ? jobs.length : recordingList.length;
695
- useEffect(() => {
1120
+ const jobRank = (s) => s === "running" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
1121
+ const jobStatusByRecording = /* @__PURE__ */ new Map();
1122
+ for (const job of jobs) {
1123
+ const prev = jobStatusByRecording.get(job.recordingId);
1124
+ if (!prev || jobRank(job.status) > jobRank(prev)) {
1125
+ jobStatusByRecording.set(job.recordingId, job.status);
1126
+ }
1127
+ }
1128
+ const listLength = screen.kind === "jobs" ? jobs.length : recordings.length;
1129
+ useEffect2(() => {
696
1130
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
697
1131
  }, [listLength]);
1132
+ const visibleRecordingRows = Math.max(3, size.rows - 6);
1133
+ useEffect2(() => {
1134
+ if (screen.kind !== "overview" || !recordingsNextCursor) return;
1135
+ const nearLoadedEnd = recordings.length - selected <= RECORDINGS_PREFETCH_REMAINING;
1136
+ const underfilledViewport = recordings.length < visibleRecordingRows;
1137
+ if (nearLoadedEnd || underfilledViewport) void loadMoreRecordings();
1138
+ }, [
1139
+ loadMoreRecordings,
1140
+ recordings.length,
1141
+ recordingsNextCursor,
1142
+ screen.kind,
1143
+ selected,
1144
+ visibleRecordingRows
1145
+ ]);
698
1146
  const openTranscript = useCallback(
699
1147
  async (transcriptId) => {
700
1148
  setStack((st) => [...st, { kind: "transcript", loading: true }]);
@@ -714,25 +1162,65 @@ function AppShell({
714
1162
  },
715
1163
  [fetchTranscript]
716
1164
  );
1165
+ const detailTranscriptId = screen.kind === "recordingDetail" ? recordings.find((r) => r.recordingId === screen.recordingId)?.activeTranscriptId : void 0;
1166
+ useEffect2(() => {
1167
+ if (!detailTranscriptId || transcriptCache.has(detailTranscriptId)) return;
1168
+ let cancelled = false;
1169
+ setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "loading"));
1170
+ fetchTranscript(detailTranscriptId).then((tr) => {
1171
+ if (!cancelled) setTranscriptCache((m) => new Map(m).set(detailTranscriptId, tr));
1172
+ }).catch(() => {
1173
+ if (!cancelled) setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "error"));
1174
+ });
1175
+ return () => {
1176
+ cancelled = true;
1177
+ };
1178
+ }, [detailTranscriptId, fetchTranscript]);
1179
+ const setAudio = (recordingId, action) => setAudioCache((m) => new Map(m).set(recordingId, action));
1180
+ const runAudio = useCallback(
1181
+ async (recordingId, mode) => {
1182
+ if (!recordingAudio) {
1183
+ setNotice("Audio actions are not available");
1184
+ return;
1185
+ }
1186
+ setAudio(recordingId, { status: "downloading" });
1187
+ try {
1188
+ const localPath = await recordingAudio.downloadRecordingAudio(recordingId);
1189
+ if (mode === "open") {
1190
+ setAudio(recordingId, { status: "opening", localPath });
1191
+ await recordingAudio.openPath(localPath);
1192
+ } else if (mode === "finder") {
1193
+ setAudio(recordingId, { status: "opening", localPath });
1194
+ await recordingAudio.revealInFinder(localPath);
1195
+ }
1196
+ setAudio(recordingId, { status: "ready", localPath });
1197
+ void refreshDownloadedIds();
1198
+ } catch (error51) {
1199
+ setAudio(recordingId, {
1200
+ status: "error",
1201
+ error: error51 instanceof Error ? error51.message : String(error51)
1202
+ });
1203
+ }
1204
+ },
1205
+ [recordingAudio, refreshDownloadedIds]
1206
+ );
717
1207
  const goTab = (tab2) => {
718
1208
  setStack([{ kind: tab2 }]);
719
1209
  setSelected(0);
720
1210
  setNotice(void 0);
721
1211
  };
722
1212
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
723
- useInput((input, key) => {
1213
+ useInput5((input, key) => {
724
1214
  setNotice(void 0);
725
1215
  if (input === "q") return exit();
726
1216
  if (key.escape || key.leftArrow) return back();
727
1217
  if (input === "1") return goTab("overview");
728
1218
  if (input === "2") return goTab("jobs");
729
- if (input === "3") return goTab("recordings");
730
- if (input === "r") return void refresh();
731
- if (screen.kind === "overview" || screen.kind === "recordings") {
1219
+ if (input === "r") return void refresh({ resetRecordings: true });
1220
+ if (screen.kind === "overview") {
732
1221
  if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
733
- if (key.downArrow || input === "j")
734
- setSelected((i) => Math.min(recordingList.length - 1, i + 1));
735
- const rec = recordingList[selected];
1222
+ if (key.downArrow || input === "j") setSelected((i) => Math.min(recordings.length - 1, i + 1));
1223
+ const rec = recordings[selected];
736
1224
  if (key.return && rec)
737
1225
  setStack((st) => [...st, { kind: "recordingDetail", recordingId: rec.recordingId }]);
738
1226
  if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
@@ -762,8 +1250,10 @@ function AppShell({
762
1250
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
763
1251
  const links = rec ? resolveRecordingLinks(rec.recordingId, rec.origin) : {};
764
1252
  if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
765
- else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
766
- else if (input === "m") setNotice("Mac app deeplink not available yet");
1253
+ else if (input === "o" && rec) void runAudio(rec.recordingId, "open");
1254
+ else if (input === "d" && rec) void runAudio(rec.recordingId, "download");
1255
+ else if (input === "f" && rec) void runAudio(rec.recordingId, "finder");
1256
+ else if (input === "w" && links.webUrl) openUrl2?.(links.webUrl);
767
1257
  else if (input === "c" && links.webUrl) {
768
1258
  copyText2?.(links.webUrl);
769
1259
  setNotice("Link copied");
@@ -772,69 +1262,117 @@ function AppShell({
772
1262
  }
773
1263
  });
774
1264
  if (screen.kind === "transcript") {
775
- return /* @__PURE__ */ jsx10(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1265
+ return /* @__PURE__ */ jsx14(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
776
1266
  }
777
1267
  if (screen.kind === "jobDetail") {
778
1268
  const job = jobs.find((j) => j.jobId === screen.jobId);
779
- if (!job) return /* @__PURE__ */ jsx10(Missing, { label: "Job" });
780
- return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1269
+ if (!job) return /* @__PURE__ */ jsx14(Missing, { label: "Job" });
1270
+ return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
781
1271
  }
782
1272
  if (screen.kind === "recordingDetail") {
783
1273
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
784
- if (!rec) return /* @__PURE__ */ jsx10(Missing, { label: "Recording" });
785
- return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(RecordingDetailView, { item: rec, nowMs: now() }) });
786
- }
787
- const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "recordings" ? "recordings" : "overview";
788
- const footerKeys = screen.kind === "jobs" ? "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 r refresh \xB7 q quit" : "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE recording \xB7 t transcript \xB7 r refresh \xB7 q quit";
789
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
790
- /* @__PURE__ */ jsx10(Header, { active: tab }),
791
- screen.kind === "overview" ? /* @__PURE__ */ jsx10(
1274
+ if (!rec) return /* @__PURE__ */ jsx14(Missing, { label: "Recording" });
1275
+ const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
1276
+ return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(
1277
+ RecordingDetailView,
1278
+ {
1279
+ item: rec,
1280
+ nowMs: now(),
1281
+ transcript: detailTranscript,
1282
+ audio: audioCache.get(rec.recordingId)
1283
+ }
1284
+ ) });
1285
+ }
1286
+ const tab = screen.kind === "jobs" ? "jobs" : "overview";
1287
+ let body;
1288
+ let position = "";
1289
+ if (screen.kind === "overview") {
1290
+ const listBudget = Math.max(3, size.rows - 6);
1291
+ const buckets = recordings.map((r) => dateBucket(r.createdAt, now()));
1292
+ const win = groupedListWindow(buckets, selected, listBudget);
1293
+ const totalRecordings = Math.max(
1294
+ recordingsTotalCount ?? stats?.recordings.total ?? recordings.length,
1295
+ recordings.length
1296
+ );
1297
+ position = recordings.length ? `${selected + 1} / ${totalRecordings}${loadingMoreRecordings ? " \xB7 loading" : ""}` : loadingMoreRecordings ? "loading" : "0";
1298
+ const showPeek = size.columns >= 100;
1299
+ const peekWidth = showPeek ? 34 : 0;
1300
+ const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
1301
+ body = /* @__PURE__ */ jsx14(
792
1302
  OverviewView,
793
1303
  {
794
- recordings,
1304
+ recordings: recordings.slice(win.start, win.end),
1305
+ selectedIndex: selected - win.start,
795
1306
  jobs,
796
1307
  stats,
797
- selectedIndex: selected,
1308
+ nowMs: now(),
1309
+ columns: listColumns,
1310
+ jobStatusByRecording,
1311
+ downloadedRecordingIds: downloadedIds,
798
1312
  spinnerFrame,
799
- nowMs: now()
800
- }
801
- ) : screen.kind === "recordings" ? /* @__PURE__ */ jsx10(RecordingsView, { items: recordings, selectedIndex: selected, nowMs: now() }) : /* @__PURE__ */ jsx10(JobsView, { items: jobs, selectedIndex: selected, spinnerFrame }),
802
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text10, { color: "red", children: [
803
- "! ",
804
- loadError
805
- ] }) }) : null,
806
- /* @__PURE__ */ jsx10(Footer, { keys: footerKeys })
1313
+ peekItem: recordings[selected],
1314
+ peekSummary,
1315
+ showPeek,
1316
+ peekWidth
1317
+ }
1318
+ );
1319
+ } else {
1320
+ const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
1321
+ position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
1322
+ body = /* @__PURE__ */ jsx14(
1323
+ JobsView,
1324
+ {
1325
+ items: jobs.slice(win.start, win.end),
1326
+ selectedIndex: selected - win.start,
1327
+ spinnerFrame
1328
+ }
1329
+ );
1330
+ }
1331
+ const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 1 overview \xB7 r refresh \xB7 q quit` : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 2 jobs \xB7 r refresh \xB7 q quit`;
1332
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1333
+ /* @__PURE__ */ jsx14(Header, { active: tab }),
1334
+ /* @__PURE__ */ jsxs11(Box12, { flexGrow: 1, flexDirection: "column", children: [
1335
+ body,
1336
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
1337
+ "! ",
1338
+ loadError
1339
+ ] }) }) : null
1340
+ ] }),
1341
+ /* @__PURE__ */ jsx14(Footer, { keys: footerKeys })
807
1342
  ] });
808
1343
  }
809
1344
  function Detail({
810
1345
  notice,
811
1346
  children
812
1347
  }) {
813
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", children: [
1348
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", children: [
814
1349
  children,
815
- notice ? /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: notice }) }) : null
1350
+ notice ? /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { color: "green", children: notice }) }) : null
816
1351
  ] });
817
1352
  }
818
1353
  function Missing({ label }) {
819
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
820
- /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
1354
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1355
+ /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
821
1356
  label,
822
1357
  " no longer in the list."
823
1358
  ] }),
824
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "esc back \xB7 q quit" })
1359
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "esc back \xB7 q quit" })
825
1360
  ] });
826
1361
  }
1362
+ var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
827
1363
  var init_AppShell = __esm({
828
1364
  "src/tui/AppShell.tsx"() {
829
1365
  "use strict";
830
1366
  init_chrome();
831
1367
  init_JobsView();
832
1368
  init_OverviewView();
833
- init_RecordingsView();
834
1369
  init_JobDetailView();
835
1370
  init_RecordingDetailView();
836
1371
  init_TranscriptView();
837
1372
  init_format();
1373
+ init_terminal();
1374
+ RECORDINGS_PAGE_SIZE = 50;
1375
+ RECORDINGS_PREFETCH_REMAINING = 8;
838
1376
  }
839
1377
  });
840
1378
 
@@ -842,43 +1380,50 @@ var init_AppShell = __esm({
842
1380
  var tui_exports = {};
843
1381
  __export(tui_exports, {
844
1382
  AppShell: () => AppShell,
1383
+ DASHBOARD_RENDER_OPTIONS: () => DASHBOARD_RENDER_OPTIONS,
845
1384
  JobDetailView: () => JobDetailView,
846
1385
  JobsView: () => JobsView,
847
1386
  OverviewView: () => OverviewView,
848
- runDashboard: () => runDashboard
1387
+ runDashboard: () => runDashboard,
1388
+ useTerminalSize: () => useTerminalSize
849
1389
  });
850
- import React2 from "react";
851
- import { render } from "ink";
852
- import { spawn } from "child_process";
1390
+ import React7 from "react";
1391
+ import { render as render2 } from "ink";
1392
+ import { spawn as spawn3 } from "child_process";
853
1393
  function openUrl(url2) {
854
1394
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
855
1395
  try {
856
- spawn(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1396
+ spawn3(cmd, [url2], { stdio: "ignore", detached: true }).unref();
857
1397
  } catch {
858
1398
  }
859
1399
  }
860
1400
  function copyText(text) {
861
1401
  if (process.platform !== "darwin") return;
862
1402
  try {
863
- const child = spawn("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1403
+ const child = spawn3("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
864
1404
  child.stdin.end(text);
865
1405
  } catch {
866
1406
  }
867
1407
  }
868
1408
  async function runDashboard(deps) {
869
- const app = render(
870
- React2.createElement(AppShell, {
1409
+ const renderApp = deps.renderApp ?? render2;
1410
+ const app = renderApp(
1411
+ React7.createElement(AppShell, {
871
1412
  fetchJobs: deps.fetchJobs,
872
1413
  fetchTranscript: deps.fetchTranscript,
873
1414
  fetchRecordings: deps.fetchRecordings,
874
1415
  fetchDashboardStats: deps.fetchDashboardStats,
1416
+ recordingAudio: deps.recordingAudio,
1417
+ listDownloadedRecordingIds: deps.listDownloadedRecordingIds,
875
1418
  initialView: deps.initialView ?? "overview",
876
1419
  openUrl,
877
1420
  copyText
878
- })
1421
+ }),
1422
+ DASHBOARD_RENDER_OPTIONS
879
1423
  );
880
1424
  await app.waitUntilExit();
881
1425
  }
1426
+ var DASHBOARD_RENDER_OPTIONS;
882
1427
  var init_tui = __esm({
883
1428
  "src/tui/index.ts"() {
884
1429
  "use strict";
@@ -887,12 +1432,17 @@ var init_tui = __esm({
887
1432
  init_JobsView();
888
1433
  init_OverviewView();
889
1434
  init_JobDetailView();
1435
+ init_terminal();
1436
+ DASHBOARD_RENDER_OPTIONS = {
1437
+ alternateScreen: true,
1438
+ interactive: true
1439
+ };
890
1440
  }
891
1441
  });
892
1442
 
893
1443
  // src/cli.ts
894
1444
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
895
- import os3 from "os";
1445
+ import os5 from "os";
896
1446
 
897
1447
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
898
1448
  var external_exports = {};
@@ -1660,10 +2210,10 @@ function mergeDefs(...defs) {
1660
2210
  function cloneDef(schema) {
1661
2211
  return mergeDefs(schema._zod.def);
1662
2212
  }
1663
- function getElementAtPath(obj, path3) {
1664
- if (!path3)
2213
+ function getElementAtPath(obj, path6) {
2214
+ if (!path6)
1665
2215
  return obj;
1666
- return path3.reduce((acc, key) => acc?.[key], obj);
2216
+ return path6.reduce((acc, key) => acc?.[key], obj);
1667
2217
  }
1668
2218
  function promiseAllObject(promisesObj) {
1669
2219
  const keys = Object.keys(promisesObj);
@@ -2072,11 +2622,11 @@ function explicitlyAborted(x, startIndex = 0) {
2072
2622
  }
2073
2623
  return false;
2074
2624
  }
2075
- function prefixIssues(path3, issues) {
2625
+ function prefixIssues(path6, issues) {
2076
2626
  return issues.map((iss) => {
2077
2627
  var _a3;
2078
2628
  (_a3 = iss).path ?? (_a3.path = []);
2079
- iss.path.unshift(path3);
2629
+ iss.path.unshift(path6);
2080
2630
  return iss;
2081
2631
  });
2082
2632
  }
@@ -2223,16 +2773,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2223
2773
  }
2224
2774
  function formatError(error51, mapper = (issue2) => issue2.message) {
2225
2775
  const fieldErrors = { _errors: [] };
2226
- const processError = (error52, path3 = []) => {
2776
+ const processError = (error52, path6 = []) => {
2227
2777
  for (const issue2 of error52.issues) {
2228
2778
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2229
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2779
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2230
2780
  } else if (issue2.code === "invalid_key") {
2231
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2781
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2232
2782
  } else if (issue2.code === "invalid_element") {
2233
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2783
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2234
2784
  } else {
2235
- const fullpath = [...path3, ...issue2.path];
2785
+ const fullpath = [...path6, ...issue2.path];
2236
2786
  if (fullpath.length === 0) {
2237
2787
  fieldErrors._errors.push(mapper(issue2));
2238
2788
  } else {
@@ -2259,17 +2809,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2259
2809
  }
2260
2810
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2261
2811
  const result = { errors: [] };
2262
- const processError = (error52, path3 = []) => {
2812
+ const processError = (error52, path6 = []) => {
2263
2813
  var _a3, _b;
2264
2814
  for (const issue2 of error52.issues) {
2265
2815
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2266
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2816
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
2267
2817
  } else if (issue2.code === "invalid_key") {
2268
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2818
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2269
2819
  } else if (issue2.code === "invalid_element") {
2270
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2820
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
2271
2821
  } else {
2272
- const fullpath = [...path3, ...issue2.path];
2822
+ const fullpath = [...path6, ...issue2.path];
2273
2823
  if (fullpath.length === 0) {
2274
2824
  result.errors.push(mapper(issue2));
2275
2825
  continue;
@@ -2301,8 +2851,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2301
2851
  }
2302
2852
  function toDotPath(_path) {
2303
2853
  const segs = [];
2304
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2305
- for (const seg of path3) {
2854
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2855
+ for (const seg of path6) {
2306
2856
  if (typeof seg === "number")
2307
2857
  segs.push(`[${seg}]`);
2308
2858
  else if (typeof seg === "symbol")
@@ -14994,13 +15544,13 @@ function resolveRef(ref, ctx) {
14994
15544
  if (!ref.startsWith("#")) {
14995
15545
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14996
15546
  }
14997
- const path3 = ref.slice(1).split("/").filter(Boolean);
14998
- if (path3.length === 0) {
15547
+ const path6 = ref.slice(1).split("/").filter(Boolean);
15548
+ if (path6.length === 0) {
14999
15549
  return ctx.rootSchema;
15000
15550
  }
15001
15551
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15002
- if (path3[0] === defsKey) {
15003
- const key = path3[1];
15552
+ if (path6[0] === defsKey) {
15553
+ const key = path6[1];
15004
15554
  if (!key || !ctx.defs[key]) {
15005
15555
  throw new Error(`Reference not found: ${ref}`);
15006
15556
  }
@@ -15479,6 +16029,248 @@ var authImportDataSchema = external_exports.object({
15479
16029
  origin: external_exports.string(),
15480
16030
  source: external_exports.literal("macos-keychain")
15481
16031
  });
16032
+ var planTierSchema = external_exports.enum(["free", "starter", "pro", "business", "unlimited"]);
16033
+ var billingStatusDataSchema = external_exports.object({
16034
+ origin: external_exports.string(),
16035
+ tier: planTierSchema,
16036
+ periodStart: external_exports.number().int(),
16037
+ periodEnd: external_exports.number().int(),
16038
+ storageBytes: external_exports.number().int().nonnegative(),
16039
+ storageCapBytes: external_exports.number().int().nonnegative().nullable(),
16040
+ minutesUsed: external_exports.number().nonnegative(),
16041
+ batchMinutesUsed: external_exports.number().nonnegative(),
16042
+ realtimeMinutesUsed: external_exports.number().nonnegative(),
16043
+ minutesCap: external_exports.number().nonnegative().nullable(),
16044
+ isOverStorage: external_exports.boolean(),
16045
+ isOverMinutes: external_exports.boolean()
16046
+ });
16047
+ var accountStatusDataSchema = external_exports.object({
16048
+ origin: external_exports.string(),
16049
+ loggedIn: external_exports.boolean(),
16050
+ email: external_exports.string().optional(),
16051
+ userId: external_exports.string().optional(),
16052
+ localStore: external_exports.object({
16053
+ path: external_exports.string(),
16054
+ accountScopedArtifacts: external_exports.number().int().nonnegative(),
16055
+ unattributedArtifacts: external_exports.number().int().nonnegative()
16056
+ }),
16057
+ billing: billingStatusDataSchema.optional()
16058
+ });
16059
+ var SIDECAR_PROTOCOL_VERSION = 1;
16060
+ var sidecarJsonRpcIdSchema = external_exports.union([external_exports.string(), external_exports.number().int()]);
16061
+ var sidecarCapabilitySchema = external_exports.enum([
16062
+ "recording.capture",
16063
+ "recording.upload",
16064
+ "live_captions.stream",
16065
+ "local_artifacts.index"
16066
+ ]);
16067
+ var sidecarAccountSchema = external_exports.object({
16068
+ backendOrigin: external_exports.string(),
16069
+ userId: external_exports.string(),
16070
+ email: external_exports.string().optional()
16071
+ });
16072
+ var sidecarClientInfoSchema = external_exports.object({
16073
+ name: external_exports.string(),
16074
+ version: external_exports.string()
16075
+ });
16076
+ var sidecarInfoSchema = external_exports.object({
16077
+ name: external_exports.string(),
16078
+ version: external_exports.string()
16079
+ });
16080
+ var sidecarRecordingOptionsSchema = external_exports.object({
16081
+ includeSystemAudio: external_exports.boolean().default(true),
16082
+ includeMicrophone: external_exports.boolean().default(true),
16083
+ liveCaptions: external_exports.boolean().default(false),
16084
+ translationLanguage: external_exports.string().optional(),
16085
+ transcriptionLanguage: external_exports.string().optional(),
16086
+ title: external_exports.string().optional()
16087
+ });
16088
+ var sidecarRecordingStateSchema = external_exports.enum([
16089
+ "idle",
16090
+ "starting",
16091
+ "recording",
16092
+ "stopping",
16093
+ "finalizing",
16094
+ "uploading",
16095
+ "completed",
16096
+ "failed",
16097
+ "cancelled"
16098
+ ]);
16099
+ var sidecarLocalArtifactKindSchema = external_exports.enum([
16100
+ "recording_session",
16101
+ "download",
16102
+ "live_caption_draft"
16103
+ ]);
16104
+ var sidecarLocalArtifactSchema = external_exports.object({
16105
+ kind: sidecarLocalArtifactKindSchema,
16106
+ localPath: external_exports.string(),
16107
+ remoteId: external_exports.string().optional(),
16108
+ metadata: external_exports.unknown().optional()
16109
+ });
16110
+ var sidecarHandshakeParamsSchema = external_exports.object({
16111
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16112
+ client: sidecarClientInfoSchema,
16113
+ account: sidecarAccountSchema.optional(),
16114
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16115
+ });
16116
+ var sidecarHandshakeResultSchema = external_exports.object({
16117
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16118
+ sidecar: sidecarInfoSchema,
16119
+ capabilities: external_exports.array(sidecarCapabilitySchema)
16120
+ });
16121
+ var sidecarRecordingStartParamsSchema = external_exports.object({
16122
+ account: sidecarAccountSchema,
16123
+ options: sidecarRecordingOptionsSchema
16124
+ });
16125
+ var sidecarRecordingStartResultSchema = external_exports.object({
16126
+ sessionId: external_exports.string(),
16127
+ state: sidecarRecordingStateSchema,
16128
+ localSessionRef: external_exports.string().optional()
16129
+ });
16130
+ var sidecarSessionParamsSchema = external_exports.object({
16131
+ sessionId: external_exports.string()
16132
+ });
16133
+ var sidecarRecordingStopResultSchema = external_exports.object({
16134
+ sessionId: external_exports.string(),
16135
+ state: sidecarRecordingStateSchema,
16136
+ recordingId: external_exports.string().optional(),
16137
+ localSessionRef: external_exports.string().optional(),
16138
+ artifacts: external_exports.array(sidecarLocalArtifactSchema).optional()
16139
+ });
16140
+ var sidecarRecordingStatusResultSchema = external_exports.object({
16141
+ sessionId: external_exports.string(),
16142
+ state: sidecarRecordingStateSchema,
16143
+ recordingId: external_exports.string().optional(),
16144
+ localSessionRef: external_exports.string().optional()
16145
+ });
16146
+ var sidecarRequestSchema = external_exports.discriminatedUnion("method", [
16147
+ external_exports.object({
16148
+ jsonrpc: external_exports.literal("2.0"),
16149
+ id: sidecarJsonRpcIdSchema,
16150
+ method: external_exports.literal("recappi.handshake"),
16151
+ params: sidecarHandshakeParamsSchema
16152
+ }),
16153
+ external_exports.object({
16154
+ jsonrpc: external_exports.literal("2.0"),
16155
+ id: sidecarJsonRpcIdSchema,
16156
+ method: external_exports.literal("recappi.recording.start"),
16157
+ params: sidecarRecordingStartParamsSchema
16158
+ }),
16159
+ external_exports.object({
16160
+ jsonrpc: external_exports.literal("2.0"),
16161
+ id: sidecarJsonRpcIdSchema,
16162
+ method: external_exports.literal("recappi.recording.stop"),
16163
+ params: sidecarSessionParamsSchema
16164
+ }),
16165
+ external_exports.object({
16166
+ jsonrpc: external_exports.literal("2.0"),
16167
+ id: sidecarJsonRpcIdSchema,
16168
+ method: external_exports.literal("recappi.recording.cancel"),
16169
+ params: sidecarSessionParamsSchema
16170
+ }),
16171
+ external_exports.object({
16172
+ jsonrpc: external_exports.literal("2.0"),
16173
+ id: sidecarJsonRpcIdSchema,
16174
+ method: external_exports.literal("recappi.recording.status"),
16175
+ params: sidecarSessionParamsSchema
16176
+ })
16177
+ ]);
16178
+ var sidecarErrorSchema = external_exports.object({
16179
+ code: external_exports.number().int(),
16180
+ message: external_exports.string(),
16181
+ data: external_exports.unknown().optional()
16182
+ });
16183
+ var sidecarResponseSchema = external_exports.union([
16184
+ external_exports.object({
16185
+ jsonrpc: external_exports.literal("2.0"),
16186
+ id: sidecarJsonRpcIdSchema,
16187
+ result: external_exports.unknown()
16188
+ }),
16189
+ external_exports.object({
16190
+ jsonrpc: external_exports.literal("2.0"),
16191
+ id: sidecarJsonRpcIdSchema,
16192
+ error: sidecarErrorSchema
16193
+ })
16194
+ ]);
16195
+ var sidecarEventSchema = external_exports.discriminatedUnion("type", [
16196
+ external_exports.object({
16197
+ type: external_exports.literal("ready"),
16198
+ protocolVersion: external_exports.literal(SIDECAR_PROTOCOL_VERSION),
16199
+ sidecar: sidecarInfoSchema
16200
+ }),
16201
+ external_exports.object({
16202
+ type: external_exports.literal("recording.state"),
16203
+ sessionId: external_exports.string(),
16204
+ state: sidecarRecordingStateSchema,
16205
+ recordingId: external_exports.string().optional(),
16206
+ localSessionRef: external_exports.string().optional(),
16207
+ message: external_exports.string().optional()
16208
+ }),
16209
+ external_exports.object({
16210
+ type: external_exports.literal("audio.level"),
16211
+ sessionId: external_exports.string(),
16212
+ input: external_exports.enum(["system", "microphone", "mixed"]),
16213
+ rmsDb: external_exports.number().optional(),
16214
+ peakDb: external_exports.number().optional(),
16215
+ at: external_exports.number().int().optional()
16216
+ }),
16217
+ external_exports.object({
16218
+ type: external_exports.literal("live_caption.delta"),
16219
+ sessionId: external_exports.string(),
16220
+ stream: external_exports.enum(["source", "translation"]),
16221
+ text: external_exports.string(),
16222
+ isFinal: external_exports.boolean().optional(),
16223
+ segmentId: external_exports.string().optional(),
16224
+ speaker: external_exports.string().optional(),
16225
+ language: external_exports.string().optional(),
16226
+ atMs: external_exports.number().int().nonnegative().optional(),
16227
+ startMs: external_exports.number().nonnegative().optional(),
16228
+ endMs: external_exports.number().nonnegative().optional()
16229
+ }),
16230
+ external_exports.object({
16231
+ type: external_exports.literal("local_artifact.upserted"),
16232
+ sessionId: external_exports.string().optional(),
16233
+ artifact: sidecarLocalArtifactSchema
16234
+ }),
16235
+ external_exports.object({
16236
+ type: external_exports.literal("error"),
16237
+ sessionId: external_exports.string().optional(),
16238
+ code: external_exports.string(),
16239
+ message: external_exports.string(),
16240
+ retryable: external_exports.boolean().optional()
16241
+ })
16242
+ ]);
16243
+ var sidecarNotificationSchema = external_exports.object({
16244
+ jsonrpc: external_exports.literal("2.0"),
16245
+ method: external_exports.literal("recappi.event"),
16246
+ params: sidecarEventSchema
16247
+ });
16248
+ var sidecarMessageSchema = external_exports.union([
16249
+ sidecarRequestSchema,
16250
+ sidecarResponseSchema,
16251
+ sidecarNotificationSchema
16252
+ ]);
16253
+ var recordCommandDataSchema = external_exports.object({
16254
+ origin: external_exports.string(),
16255
+ userId: external_exports.string(),
16256
+ live: external_exports.boolean(),
16257
+ sessionId: external_exports.string(),
16258
+ state: sidecarRecordingStateSchema,
16259
+ recordingId: external_exports.string().optional(),
16260
+ localSessionRef: external_exports.string().optional(),
16261
+ sidecar: sidecarInfoSchema.optional(),
16262
+ artifacts: external_exports.array(sidecarLocalArtifactSchema)
16263
+ });
16264
+ var audioCommandDataSchema = external_exports.object({
16265
+ origin: external_exports.string(),
16266
+ recordingId: external_exports.string(),
16267
+ localPath: external_exports.string(),
16268
+ action: external_exports.enum(["download", "open", "reveal"]),
16269
+ reused: external_exports.boolean(),
16270
+ artifactId: external_exports.number().int().positive().optional(),
16271
+ contentType: external_exports.string().optional(),
16272
+ contentLength: external_exports.number().int().nonnegative().optional()
16273
+ });
15482
16274
  var uploadSuccessSchema = external_exports.object({
15483
16275
  filePath: external_exports.string(),
15484
16276
  recordingId: external_exports.string(),
@@ -16084,7 +16876,11 @@ function isRecord(value) {
16084
16876
  }
16085
16877
 
16086
16878
  // src/api.ts
16087
- import { promises as fs3 } from "fs";
16879
+ import { createWriteStream, promises as fs3 } from "fs";
16880
+ import os4 from "os";
16881
+ import path4 from "path";
16882
+ import { Readable } from "stream";
16883
+ import { pipeline } from "stream/promises";
16088
16884
 
16089
16885
  // src/files.ts
16090
16886
  import { promises as fs2 } from "fs";
@@ -16241,6 +17037,352 @@ async function readDurationMs(filePath, contentType) {
16241
17037
  );
16242
17038
  }
16243
17039
 
17040
+ // src/store.ts
17041
+ import { mkdirSync } from "fs";
17042
+ import os3 from "os";
17043
+ import path3 from "path";
17044
+ import { createRequire } from "module";
17045
+ var require2 = createRequire(import.meta.url);
17046
+ var Database = require2("better-sqlite3");
17047
+ var CLI_STORE_SCHEMA_VERSION = 2;
17048
+ function defaultStorePath(homeDir = os3.homedir(), env = process.env) {
17049
+ const explicit = env.RECAPPI_CLI_STORE_PATH?.trim();
17050
+ if (explicit) return explicit;
17051
+ const dataHome = env.XDG_DATA_HOME?.trim() || path3.join(homeDir, ".local", "share");
17052
+ return path3.join(dataHome, "recappi", "cli-state.sqlite");
17053
+ }
17054
+ function requireAccountPartition(input) {
17055
+ const account = parseAccountPartition(input, "strict");
17056
+ if (!account) {
17057
+ throw cliError(
17058
+ "usage.invalid_argument",
17059
+ "Account partition requires a backend origin and user id.",
17060
+ {
17061
+ hint: "Resolve Recappi auth first, then use the normalized origin and authenticated user id."
17062
+ }
17063
+ );
17064
+ }
17065
+ return account;
17066
+ }
17067
+ function openCliStore(opts = {}) {
17068
+ return new CliLocalStore(opts);
17069
+ }
17070
+ var CliLocalStore = class {
17071
+ db;
17072
+ now;
17073
+ constructor(opts = {}) {
17074
+ const dbPath = opts.dbPath ?? defaultStorePath(opts.homeDir, opts.env);
17075
+ if (!opts.readonly && dbPath !== ":memory:") {
17076
+ mkdirSync(path3.dirname(dbPath), { recursive: true, mode: 448 });
17077
+ }
17078
+ this.db = new Database(dbPath, opts.readonly === true ? { readonly: true } : void 0);
17079
+ this.now = opts.now ?? Date.now;
17080
+ if (!opts.readonly) this.migrate();
17081
+ }
17082
+ close() {
17083
+ this.db.close();
17084
+ }
17085
+ recordAccountSeen(account, email3) {
17086
+ const now = this.now();
17087
+ this.db.prepare(
17088
+ `
17089
+ INSERT INTO account_scopes (backend_origin, user_id, email, created_at, updated_at)
17090
+ VALUES (?, ?, ?, ?, ?)
17091
+ ON CONFLICT (backend_origin, user_id) DO UPDATE SET
17092
+ email = excluded.email,
17093
+ updated_at = excluded.updated_at
17094
+ `
17095
+ ).run(account.backendOrigin, account.userId, email3?.trim() || null, now, now);
17096
+ }
17097
+ addLocalArtifact(input) {
17098
+ const account = input.account ? requireAccountPartition(input.account) : null;
17099
+ const localPath = input.localPath.trim();
17100
+ if (!localPath) {
17101
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17102
+ }
17103
+ if (account) this.recordAccountSeen(account);
17104
+ const now = this.now();
17105
+ const result = this.db.prepare(
17106
+ `
17107
+ INSERT INTO local_artifacts (
17108
+ kind,
17109
+ backend_origin,
17110
+ user_id,
17111
+ remote_id,
17112
+ local_path,
17113
+ metadata_json,
17114
+ created_at,
17115
+ updated_at,
17116
+ last_opened_at
17117
+ )
17118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
17119
+ `
17120
+ ).run(
17121
+ input.kind,
17122
+ account?.backendOrigin ?? null,
17123
+ account?.userId ?? null,
17124
+ input.remoteId?.trim() || null,
17125
+ localPath,
17126
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17127
+ now,
17128
+ now,
17129
+ input.lastOpenedAt ?? null
17130
+ );
17131
+ return this.getLocalArtifact(Number(result.lastInsertRowid));
17132
+ }
17133
+ upsertLocalArtifact(input) {
17134
+ const remoteId = input.remoteId?.trim();
17135
+ if (!remoteId) return this.addLocalArtifact(input);
17136
+ const account = input.account ? requireAccountPartition(input.account) : null;
17137
+ if (account) this.recordAccountSeen(account);
17138
+ const existing = this.findLocalArtifact({
17139
+ account,
17140
+ kind: input.kind,
17141
+ remoteId
17142
+ });
17143
+ if (!existing) return this.addLocalArtifact({ ...input, remoteId });
17144
+ const localPath = input.localPath.trim();
17145
+ if (!localPath) {
17146
+ throw cliError("usage.invalid_argument", "Local artifact path is required.");
17147
+ }
17148
+ const now = this.now();
17149
+ this.db.prepare(
17150
+ `
17151
+ UPDATE local_artifacts
17152
+ SET local_path = ?,
17153
+ metadata_json = ?,
17154
+ updated_at = ?,
17155
+ last_opened_at = COALESCE(?, last_opened_at)
17156
+ WHERE id = ?
17157
+ `
17158
+ ).run(
17159
+ localPath,
17160
+ input.metadata === void 0 ? null : JSON.stringify(input.metadata),
17161
+ now,
17162
+ input.lastOpenedAt ?? null,
17163
+ existing.id
17164
+ );
17165
+ return this.getLocalArtifact(existing.id);
17166
+ }
17167
+ getLocalArtifact(id) {
17168
+ const row = this.db.prepare("SELECT * FROM local_artifacts WHERE id = ?").get(id);
17169
+ if (!row) {
17170
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17171
+ }
17172
+ return mapArtifactRow(row);
17173
+ }
17174
+ listLocalArtifactsForAccount(accountInput, opts = {}) {
17175
+ const account = requireAccountPartition(accountInput);
17176
+ const params = [account.backendOrigin, account.userId];
17177
+ let source = `
17178
+ SELECT * FROM local_artifacts
17179
+ WHERE backend_origin = ? AND user_id = ?
17180
+ `;
17181
+ if (opts.kind) {
17182
+ source += " AND kind = ?";
17183
+ params.push(opts.kind);
17184
+ }
17185
+ if (opts.remoteId) {
17186
+ source += " AND remote_id = ?";
17187
+ params.push(opts.remoteId);
17188
+ }
17189
+ source += " ORDER BY updated_at DESC, id DESC";
17190
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17191
+ }
17192
+ listUnattributedLocalArtifacts(opts = {}) {
17193
+ const params = [];
17194
+ let source = `
17195
+ SELECT * FROM local_artifacts
17196
+ WHERE backend_origin IS NULL AND user_id IS NULL
17197
+ `;
17198
+ if (opts.kind) {
17199
+ source += " AND kind = ?";
17200
+ params.push(opts.kind);
17201
+ }
17202
+ if (opts.remoteId) {
17203
+ source += " AND remote_id = ?";
17204
+ params.push(opts.remoteId);
17205
+ }
17206
+ source += " ORDER BY updated_at DESC, id DESC";
17207
+ return this.db.prepare(source).all(...params).map(mapArtifactRow);
17208
+ }
17209
+ findLocalArtifactForAccount(accountInput, opts) {
17210
+ const account = requireAccountPartition(accountInput);
17211
+ return this.findLocalArtifact({ account, kind: opts.kind, remoteId: opts.remoteId });
17212
+ }
17213
+ listDownloadedRecordingIdsForAccount(accountInput) {
17214
+ return new Set(
17215
+ this.listLocalArtifactsForAccount(accountInput, { kind: "download" }).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
17216
+ );
17217
+ }
17218
+ markLocalArtifactOpened(id) {
17219
+ const now = this.now();
17220
+ const result = this.db.prepare(
17221
+ `
17222
+ UPDATE local_artifacts
17223
+ SET last_opened_at = ?, updated_at = ?
17224
+ WHERE id = ?
17225
+ `
17226
+ ).run(now, now, id);
17227
+ if (result.changes !== 1) {
17228
+ throw cliError("usage.invalid_argument", `Local artifact ${id} does not exist.`);
17229
+ }
17230
+ return this.getLocalArtifact(id);
17231
+ }
17232
+ claimUnattributedLocalArtifact(id, accountInput) {
17233
+ const account = requireAccountPartition(accountInput);
17234
+ this.recordAccountSeen(account);
17235
+ const result = this.db.prepare(
17236
+ `
17237
+ UPDATE local_artifacts
17238
+ SET backend_origin = ?, user_id = ?, updated_at = ?
17239
+ WHERE id = ? AND backend_origin IS NULL AND user_id IS NULL
17240
+ `
17241
+ ).run(account.backendOrigin, account.userId, this.now(), id);
17242
+ return result.changes === 1;
17243
+ }
17244
+ migrate() {
17245
+ this.db.pragma("journal_mode = WAL");
17246
+ this.db.pragma("foreign_keys = ON");
17247
+ this.db.exec(`
17248
+ CREATE TABLE IF NOT EXISTS schema_meta (
17249
+ key TEXT PRIMARY KEY NOT NULL,
17250
+ value TEXT NOT NULL
17251
+ );
17252
+
17253
+ INSERT INTO schema_meta (key, value)
17254
+ VALUES ('schema_version', '${CLI_STORE_SCHEMA_VERSION}')
17255
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value;
17256
+
17257
+ CREATE TABLE IF NOT EXISTS account_scopes (
17258
+ backend_origin TEXT NOT NULL,
17259
+ user_id TEXT NOT NULL,
17260
+ email TEXT,
17261
+ created_at INTEGER NOT NULL,
17262
+ updated_at INTEGER NOT NULL,
17263
+ PRIMARY KEY (backend_origin, user_id)
17264
+ );
17265
+
17266
+ CREATE TABLE IF NOT EXISTS local_artifacts (
17267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17268
+ kind TEXT NOT NULL,
17269
+ backend_origin TEXT,
17270
+ user_id TEXT,
17271
+ remote_id TEXT,
17272
+ local_path TEXT NOT NULL,
17273
+ metadata_json TEXT,
17274
+ created_at INTEGER NOT NULL,
17275
+ updated_at INTEGER NOT NULL,
17276
+ last_opened_at INTEGER,
17277
+ CHECK (
17278
+ (backend_origin IS NULL AND user_id IS NULL)
17279
+ OR (backend_origin IS NOT NULL AND user_id IS NOT NULL)
17280
+ )
17281
+ );
17282
+
17283
+ CREATE INDEX IF NOT EXISTS local_artifacts_account_kind_idx
17284
+ ON local_artifacts (backend_origin, user_id, kind, updated_at DESC);
17285
+
17286
+ CREATE INDEX IF NOT EXISTS local_artifacts_remote_idx
17287
+ ON local_artifacts (backend_origin, user_id, kind, remote_id);
17288
+ `);
17289
+ if (!hasColumn(this.db, "local_artifacts", "last_opened_at")) {
17290
+ this.db.exec("ALTER TABLE local_artifacts ADD COLUMN last_opened_at INTEGER");
17291
+ }
17292
+ }
17293
+ findLocalArtifact({
17294
+ account,
17295
+ kind,
17296
+ remoteId
17297
+ }) {
17298
+ const row = account ? this.db.prepare(
17299
+ `
17300
+ SELECT * FROM local_artifacts
17301
+ WHERE backend_origin = ? AND user_id = ? AND kind = ? AND remote_id = ?
17302
+ ORDER BY updated_at DESC, id DESC
17303
+ LIMIT 1
17304
+ `
17305
+ ).get(account.backendOrigin, account.userId, kind, remoteId) : this.db.prepare(
17306
+ `
17307
+ SELECT * FROM local_artifacts
17308
+ WHERE backend_origin IS NULL AND user_id IS NULL AND kind = ? AND remote_id = ?
17309
+ ORDER BY updated_at DESC, id DESC
17310
+ LIMIT 1
17311
+ `
17312
+ ).get(kind, remoteId);
17313
+ return row ? mapArtifactRow(row) : null;
17314
+ }
17315
+ };
17316
+ function parseAccountPartition(input, mode) {
17317
+ const rawOrigin = cleanString(input?.backendOrigin);
17318
+ const rawUserId = cleanString(input?.userId);
17319
+ if (!rawOrigin && !rawUserId) return null;
17320
+ if (!rawOrigin || !rawUserId) {
17321
+ if (mode === "strict") {
17322
+ throw cliError(
17323
+ "usage.invalid_argument",
17324
+ "Account stamp must include both backend origin and user id.",
17325
+ {
17326
+ hint: "Partial account stamps are treated as unattributed when reading legacy local state."
17327
+ }
17328
+ );
17329
+ }
17330
+ return null;
17331
+ }
17332
+ try {
17333
+ return { backendOrigin: validateOrigin(rawOrigin), userId: rawUserId };
17334
+ } catch (error51) {
17335
+ if (mode === "strict") throw error51;
17336
+ return null;
17337
+ }
17338
+ }
17339
+ function cleanString(value) {
17340
+ const trimmed = value?.trim();
17341
+ return trimmed ? trimmed : null;
17342
+ }
17343
+ function mapArtifactRow(row) {
17344
+ const backendOrigin = stringOrNull(row.backend_origin);
17345
+ const userId = stringOrNull(row.user_id);
17346
+ const lastOpenedAt = numberOrNull(row.last_opened_at);
17347
+ return {
17348
+ id: numberValue(row.id),
17349
+ kind: localArtifactKind(row.kind),
17350
+ account: backendOrigin && userId ? { backendOrigin, userId } : null,
17351
+ localPath: stringValue(row.local_path),
17352
+ ...stringOrNull(row.remote_id) ? { remoteId: stringOrNull(row.remote_id) ?? void 0 } : {},
17353
+ ...typeof row.metadata_json === "string" ? { metadata: JSON.parse(row.metadata_json) } : {},
17354
+ createdAt: numberValue(row.created_at),
17355
+ updatedAt: numberValue(row.updated_at),
17356
+ ...lastOpenedAt ? { lastOpenedAt } : {}
17357
+ };
17358
+ }
17359
+ function hasColumn(db, table, column) {
17360
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
17361
+ }
17362
+ function localArtifactKind(value) {
17363
+ if (value === "recording_session" || value === "download" || value === "live_caption_draft") {
17364
+ return value;
17365
+ }
17366
+ throw cliError("cloud.invalid_response", "CLI store contains an unknown artifact kind.");
17367
+ }
17368
+ function stringValue(value) {
17369
+ if (typeof value === "string") return value;
17370
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid string value.");
17371
+ }
17372
+ function stringOrNull(value) {
17373
+ return typeof value === "string" ? value : null;
17374
+ }
17375
+ function numberValue(value) {
17376
+ if (typeof value === "number") return value;
17377
+ if (typeof value === "bigint") return Number(value);
17378
+ throw cliError("cloud.invalid_response", "CLI store row contained an invalid number value.");
17379
+ }
17380
+ function numberOrNull(value) {
17381
+ if (typeof value === "number") return value;
17382
+ if (typeof value === "bigint") return Number(value);
17383
+ return null;
17384
+ }
17385
+
16244
17386
  // src/api.ts
16245
17387
  var RecappiApiClient = class {
16246
17388
  constructor(auth, opts = {}) {
@@ -16248,10 +17390,12 @@ var RecappiApiClient = class {
16248
17390
  this.fetchImpl = opts.fetchImpl ?? fetch;
16249
17391
  this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16250
17392
  this.env = opts.env ?? process.env;
17393
+ this.homeDir = opts.homeDir;
16251
17394
  }
16252
17395
  fetchImpl;
16253
17396
  sleep;
16254
17397
  env;
17398
+ homeDir;
16255
17399
  async authStatus() {
16256
17400
  if (!this.auth.token) {
16257
17401
  return { loggedIn: false, origin: this.auth.origin };
@@ -16389,10 +17533,81 @@ var RecappiApiClient = class {
16389
17533
  );
16390
17534
  return mapRecording(parsed, this.auth.origin);
16391
17535
  }
17536
+ async downloadRecordingAudio(recordingId, opts = {}) {
17537
+ const response = await this.request(
17538
+ "GET",
17539
+ `/api/recordings/${encodeURIComponent(recordingId)}/audio`
17540
+ );
17541
+ if (!response.body) {
17542
+ throw cliError("cloud.invalid_response", "Recording audio response was empty.");
17543
+ }
17544
+ const contentType = normalizeContentType(response.headers.get("content-type"));
17545
+ const contentLength = numberHeader(response.headers.get("content-length"));
17546
+ const dir = opts.directory ?? await fs3.mkdtemp(path4.join(os4.tmpdir(), "recappi-cli-audio-"));
17547
+ if (opts.directory) await fs3.mkdir(dir, { recursive: true });
17548
+ const filePath = path4.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
17549
+ try {
17550
+ await pipeline(
17551
+ Readable.fromWeb(response.body),
17552
+ createWriteStream(filePath)
17553
+ );
17554
+ } catch (error51) {
17555
+ await fs3.rm(filePath, { force: true }).catch(() => void 0);
17556
+ throw error51;
17557
+ }
17558
+ return {
17559
+ recordingId,
17560
+ localPath: filePath,
17561
+ contentType,
17562
+ ...contentLength !== void 0 ? { contentLength } : {},
17563
+ origin: this.auth.origin
17564
+ };
17565
+ }
16392
17566
  async dashboardStats() {
16393
17567
  const parsed = await this.getJson("/api/dashboard/stats");
16394
17568
  return mapDashboardStats(parsed, this.auth.origin);
16395
17569
  }
17570
+ async billingStatus() {
17571
+ const parsed = await this.getJson("/api/billing/status");
17572
+ return mapBillingStatus(parsed, this.auth.origin);
17573
+ }
17574
+ async accountStatus() {
17575
+ const status = await this.authStatus();
17576
+ const storePath = defaultStorePath(this.homeDir, this.env);
17577
+ let accountScopedArtifacts = 0;
17578
+ let unattributedArtifacts = 0;
17579
+ if (status.loggedIn && status.userId) {
17580
+ const store = openCliStore({
17581
+ dbPath: storePath,
17582
+ env: this.env,
17583
+ homeDir: this.homeDir
17584
+ });
17585
+ try {
17586
+ const account = requireAccountPartition({
17587
+ backendOrigin: this.auth.origin,
17588
+ userId: status.userId
17589
+ });
17590
+ store.recordAccountSeen(account, status.email);
17591
+ accountScopedArtifacts = store.listLocalArtifactsForAccount(account).length;
17592
+ unattributedArtifacts = store.listUnattributedLocalArtifacts().length;
17593
+ } finally {
17594
+ store.close();
17595
+ }
17596
+ }
17597
+ const billing = status.loggedIn ? await this.billingStatus() : void 0;
17598
+ return accountStatusDataSchema.parse({
17599
+ origin: this.auth.origin,
17600
+ loggedIn: status.loggedIn,
17601
+ ...status.email ? { email: status.email } : {},
17602
+ ...status.userId ? { userId: status.userId } : {},
17603
+ localStore: {
17604
+ path: storePath,
17605
+ accountScopedArtifacts,
17606
+ unattributedArtifacts
17607
+ },
17608
+ ...billing ? { billing } : {}
17609
+ });
17610
+ }
16396
17611
  async uploadPathBatch(opts) {
16397
17612
  const files = await collectAudioFiles(opts.inputPath);
16398
17613
  if (files.length === 0) {
@@ -16573,12 +17788,12 @@ var RecappiApiClient = class {
16573
17788
  ...typeof parsed.language === "string" || parsed.language === null ? { language: parsed.language } : {}
16574
17789
  };
16575
17790
  }
16576
- async getJson(path3) {
16577
- const response = await this.request("GET", path3);
17791
+ async getJson(path6) {
17792
+ const response = await this.request("GET", path6);
16578
17793
  return await parseJson(response);
16579
17794
  }
16580
- async postJson(path3, body) {
16581
- const response = await this.request("POST", path3, JSON.stringify(body), {
17795
+ async postJson(path6, body) {
17796
+ const response = await this.request("POST", path6, JSON.stringify(body), {
16582
17797
  headers: { "content-type": "application/json" }
16583
17798
  });
16584
17799
  return await parseJson(response);
@@ -16621,6 +17836,53 @@ async function responseMessage(response) {
16621
17836
  return response.statusText;
16622
17837
  }
16623
17838
  }
17839
+ function normalizeContentType(value) {
17840
+ return value?.split(";")[0]?.trim().toLowerCase() || "audio/wav";
17841
+ }
17842
+ function numberHeader(value) {
17843
+ if (!value) return void 0;
17844
+ const parsed = Number(value);
17845
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
17846
+ }
17847
+ function audioExtensionForContentType(contentType) {
17848
+ switch (contentType) {
17849
+ case "audio/mpeg":
17850
+ case "audio/mp3":
17851
+ return "mp3";
17852
+ case "audio/aiff":
17853
+ case "audio/x-aiff":
17854
+ return "aiff";
17855
+ case "audio/aac":
17856
+ case "audio/mp4":
17857
+ case "audio/m4a":
17858
+ case "audio/x-m4a":
17859
+ return "m4a";
17860
+ case "audio/ogg":
17861
+ return "ogg";
17862
+ case "audio/flac":
17863
+ case "audio/x-flac":
17864
+ return "flac";
17865
+ case "audio/webm":
17866
+ return "webm";
17867
+ case "audio/wav":
17868
+ case "audio/x-wav":
17869
+ default:
17870
+ return "wav";
17871
+ }
17872
+ }
17873
+ function recordingAudioFileName(recordingId, title, contentType) {
17874
+ const idStem = truncateFileStem(safeFileStem(recordingId), 48);
17875
+ const titleStem = title ? truncateFileStem(safeFileStem(title), 80) : "";
17876
+ const stem = titleStem ? `${titleStem}-${idStem}` : idStem;
17877
+ return `${stem}.${audioExtensionForContentType(contentType)}`;
17878
+ }
17879
+ function truncateFileStem(value, maxLength) {
17880
+ return [...value].slice(0, maxLength).join("");
17881
+ }
17882
+ function safeFileStem(value) {
17883
+ const safe = value.normalize("NFKC").replace(/[^\p{L}\p{N}._-]+/gu, "-").replace(/^-+|-+$/g, "");
17884
+ return safe || "recording";
17885
+ }
16624
17886
  function isRecord2(value) {
16625
17887
  return typeof value === "object" && value !== null && !Array.isArray(value);
16626
17888
  }
@@ -16753,9 +18015,9 @@ function parseSummary(row) {
16753
18015
  function mapJobListItem(row) {
16754
18016
  const recording = isRecord2(row.recording) ? row.recording : {};
16755
18017
  return {
16756
- jobId: stringValue(row.jobId) ?? stringValue(row.id) ?? "",
16757
- recordingId: stringValue(row.recordingId) ?? "",
16758
- status: stringValue(row.status) ?? "queued",
18018
+ jobId: stringValue2(row.jobId) ?? stringValue2(row.id) ?? "",
18019
+ recordingId: stringValue2(row.recordingId) ?? "",
18020
+ status: stringValue2(row.status) ?? "queued",
16759
18021
  ...typeof row.provider === "string" ? { provider: row.provider } : {},
16760
18022
  ...typeof row.model === "string" ? { model: row.model } : {},
16761
18023
  ...typeof row.language === "string" || row.language === null ? { language: row.language } : {},
@@ -16774,10 +18036,10 @@ function mapJobListItem(row) {
16774
18036
  };
16775
18037
  }
16776
18038
  function mapRecording(row, origin) {
16777
- const recordingId = stringValue(row.id) ?? stringValue(row.recordingId);
16778
- const status = stringValue(row.status);
16779
- const createdAt = numberValue(row.createdAt);
16780
- const updatedAt = numberValue(row.updatedAt);
18039
+ const recordingId = stringValue2(row.id) ?? stringValue2(row.recordingId);
18040
+ const status = stringValue2(row.status);
18041
+ const createdAt = numberValue2(row.createdAt);
18042
+ const updatedAt = numberValue2(row.updatedAt);
16781
18043
  if (!recordingId) {
16782
18044
  throw cliError("cloud.invalid_response", "Recording response was missing id.");
16783
18045
  }
@@ -16816,16 +18078,37 @@ function mapDashboardStats(row, origin) {
16816
18078
  jobs: mapCountObject(row.jobs, ["active", "queued", "running", "succeeded", "failed"])
16817
18079
  });
16818
18080
  }
18081
+ function mapBillingStatus(row, origin) {
18082
+ return billingStatusDataSchema.parse({
18083
+ origin,
18084
+ tier: row.tier,
18085
+ periodStart: numberValue2(row.periodStart),
18086
+ periodEnd: numberValue2(row.periodEnd),
18087
+ storageBytes: numberValue2(row.storageBytes) ?? 0,
18088
+ storageCapBytes: nullableCap(row.storageCapBytes),
18089
+ minutesUsed: numberValue2(row.minutesUsed) ?? 0,
18090
+ batchMinutesUsed: numberValue2(row.batchMinutesUsed) ?? 0,
18091
+ realtimeMinutesUsed: numberValue2(row.realtimeMinutesUsed) ?? 0,
18092
+ minutesCap: nullableCap(row.minutesCap),
18093
+ isOverStorage: row.isOverStorage === true,
18094
+ isOverMinutes: row.isOverMinutes === true
18095
+ });
18096
+ }
16819
18097
  function mapCountObject(value, keys) {
16820
18098
  const source = isRecord2(value) ? value : {};
16821
- return Object.fromEntries(keys.map((key) => [key, numberValue(source[key]) ?? 0]));
18099
+ return Object.fromEntries(keys.map((key) => [key, numberValue2(source[key]) ?? 0]));
16822
18100
  }
16823
- function stringValue(value) {
18101
+ function stringValue2(value) {
16824
18102
  return typeof value === "string" ? value : void 0;
16825
18103
  }
16826
- function numberValue(value) {
18104
+ function numberValue2(value) {
16827
18105
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
16828
18106
  }
18107
+ function nullableCap(value) {
18108
+ if (value === null) return null;
18109
+ const number4 = numberValue2(value);
18110
+ return number4 === void 0 ? null : number4;
18111
+ }
16829
18112
  function parseSummaryStatus(value) {
16830
18113
  const allowed = /* @__PURE__ */ new Set([
16831
18114
  "pending",
@@ -16847,15 +18130,171 @@ function decodeJsonArray(value) {
16847
18130
  return [];
16848
18131
  }
16849
18132
  }
16850
- function decodeJsonRecord(value) {
16851
- if (isRecord2(value)) return value;
16852
- if (typeof value !== "string" || !value.trim()) return null;
18133
+ function decodeJsonRecord(value) {
18134
+ if (isRecord2(value)) return value;
18135
+ if (typeof value !== "string" || !value.trim()) return null;
18136
+ try {
18137
+ const decoded = JSON.parse(value);
18138
+ return isRecord2(decoded) ? decoded : null;
18139
+ } catch {
18140
+ return null;
18141
+ }
18142
+ }
18143
+
18144
+ // src/audio.ts
18145
+ import { spawn } from "child_process";
18146
+ import { promises as fs4 } from "fs";
18147
+ import path5 from "path";
18148
+ function createRecordingAudioRuntime(client, deps = {}) {
18149
+ const downloadRecordingAudioFile = async (recordingId, opts) => {
18150
+ const cached2 = await findReusableDownload(recordingId, deps);
18151
+ if (cached2) return cached2;
18152
+ const directory = opts?.directory ?? (deps.account ? defaultDownloadDirectory(deps) : void 0);
18153
+ const download = await client.downloadRecordingAudio(recordingId, {
18154
+ ...opts,
18155
+ ...directory ? { directory } : {}
18156
+ });
18157
+ const artifact = await rememberDownload(download, deps);
18158
+ return {
18159
+ recordingId: download.recordingId,
18160
+ localPath: download.localPath,
18161
+ reused: false,
18162
+ ...artifact ? { artifactId: artifact.id } : {},
18163
+ contentType: download.contentType,
18164
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18165
+ origin: download.origin
18166
+ };
18167
+ };
18168
+ return {
18169
+ downloadRecordingAudio: async (recordingId, opts) => (await downloadRecordingAudioFile(recordingId, opts)).localPath,
18170
+ downloadRecordingAudioFile,
18171
+ openPath: (localPath) => openPath(localPath, deps),
18172
+ revealInFinder: (localPath) => revealInFinder(localPath, deps),
18173
+ listDownloads: () => listExistingDownloads(deps),
18174
+ listDownloadedRecordingIds: async () => new Set(
18175
+ (await listExistingDownloads(deps)).map((artifact) => artifact.remoteId).filter((remoteId) => Boolean(remoteId))
18176
+ )
18177
+ };
18178
+ }
18179
+ function openPath(localPath, deps = {}) {
18180
+ return runMacOpen([localPath], deps);
18181
+ }
18182
+ function revealInFinder(localPath, deps = {}) {
18183
+ return runMacOpen(["-R", localPath], deps);
18184
+ }
18185
+ async function findReusableDownload(recordingId, deps) {
18186
+ return withStore(deps, async (store, account) => {
18187
+ if (!account) return null;
18188
+ const artifact = store.findLocalArtifactForAccount(account, {
18189
+ kind: "download",
18190
+ remoteId: recordingId
18191
+ });
18192
+ if (!artifact || !await isReadableFile(artifact.localPath)) return null;
18193
+ const opened = store.markLocalArtifactOpened(artifact.id);
18194
+ return artifactToDownload(opened, recordingId);
18195
+ });
18196
+ }
18197
+ async function rememberDownload(download, deps) {
18198
+ return withStore(deps, (store, account) => {
18199
+ if (!account) return null;
18200
+ const artifact = store.upsertLocalArtifact({
18201
+ kind: "download",
18202
+ account,
18203
+ remoteId: download.recordingId,
18204
+ localPath: download.localPath,
18205
+ metadata: {
18206
+ resource: "recording_audio",
18207
+ contentType: download.contentType,
18208
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {},
18209
+ origin: download.origin
18210
+ }
18211
+ });
18212
+ return store.markLocalArtifactOpened(artifact.id);
18213
+ });
18214
+ }
18215
+ async function listExistingDownloads(deps) {
18216
+ const artifacts = await withStore(
18217
+ deps,
18218
+ (store, account) => account ? store.listLocalArtifactsForAccount(account, { kind: "download" }) : []
18219
+ );
18220
+ const existing = [];
18221
+ for (const artifact of artifacts) {
18222
+ if (await isReadableFile(artifact.localPath)) existing.push(artifact);
18223
+ }
18224
+ return existing;
18225
+ }
18226
+ async function withStore(deps, run) {
18227
+ const store = deps.store ?? openCliStore({ homeDir: deps.homeDir, env: deps.env });
16853
18228
  try {
16854
- const decoded = JSON.parse(value);
16855
- return isRecord2(decoded) ? decoded : null;
18229
+ return await run(store, deps.account ?? null);
18230
+ } finally {
18231
+ if (!deps.store) store.close();
18232
+ }
18233
+ }
18234
+ function defaultDownloadDirectory(deps) {
18235
+ return path5.join(path5.dirname(defaultStorePath(deps.homeDir, deps.env)), "downloads");
18236
+ }
18237
+ async function isReadableFile(localPath) {
18238
+ try {
18239
+ return (await fs4.stat(localPath)).isFile();
16856
18240
  } catch {
16857
- return null;
18241
+ return false;
18242
+ }
18243
+ }
18244
+ function artifactToDownload(artifact, recordingId) {
18245
+ const metadata = isRecord3(artifact.metadata) ? artifact.metadata : {};
18246
+ return {
18247
+ recordingId,
18248
+ localPath: artifact.localPath,
18249
+ reused: true,
18250
+ artifactId: artifact.id,
18251
+ ...typeof metadata.contentType === "string" ? { contentType: metadata.contentType } : {},
18252
+ ...typeof metadata.contentLength === "number" ? { contentLength: metadata.contentLength } : {},
18253
+ ...typeof metadata.origin === "string" ? { origin: metadata.origin } : {}
18254
+ };
18255
+ }
18256
+ function isRecord3(value) {
18257
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18258
+ }
18259
+ function runMacOpen(args, deps) {
18260
+ if ((deps.platform ?? process.platform) !== "darwin") {
18261
+ return Promise.reject(
18262
+ cliError(
18263
+ "usage.invalid_argument",
18264
+ "Recording audio file actions are supported on macOS only.",
18265
+ {
18266
+ hint: "Download the audio and open the printed local path manually on this platform."
18267
+ }
18268
+ )
18269
+ );
16858
18270
  }
18271
+ const spawnProcess = deps.spawnProcess ?? spawn;
18272
+ return new Promise((resolve, reject) => {
18273
+ let settled = false;
18274
+ const finish = (error51) => {
18275
+ if (settled) return;
18276
+ settled = true;
18277
+ if (error51) reject(error51);
18278
+ else resolve();
18279
+ };
18280
+ try {
18281
+ const child = spawnProcess("open", args, { stdio: "ignore" });
18282
+ child.once(
18283
+ "error",
18284
+ (error51) => finish(error51 instanceof Error ? error51 : new Error(String(error51)))
18285
+ );
18286
+ child.once("close", (code) => {
18287
+ if (code === 0) finish();
18288
+ else {
18289
+ finish(
18290
+ cliError("internal.unexpected", `open failed with exit code ${code ?? "unknown"}.`)
18291
+ );
18292
+ }
18293
+ });
18294
+ } catch (error51) {
18295
+ finish(error51 instanceof Error ? error51 : new Error(String(error51)));
18296
+ }
18297
+ });
16859
18298
  }
16860
18299
 
16861
18300
  // src/render.ts
@@ -16938,20 +18377,20 @@ function renderEnvelope(envelope, opts) {
16938
18377
  `);
16939
18378
  }
16940
18379
  function renderHumanSuccess(command, data, opts) {
16941
- if (command === "auth login" && isRecord3(data)) {
18380
+ if (command === "auth login" && isRecord4(data)) {
16942
18381
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
16943
18382
  `);
16944
18383
  return;
16945
18384
  }
16946
- if (command === "auth logout" && isRecord3(data)) {
18385
+ if (command === "auth logout" && isRecord4(data)) {
16947
18386
  opts.stdout(data.cleared ? "Signed out of Recappi CLI\n" : "No Recappi CLI session to clear\n");
16948
18387
  return;
16949
18388
  }
16950
- if (command === "auth import-macos" && isRecord3(data)) {
18389
+ if (command === "auth import-macos" && isRecord4(data)) {
16951
18390
  opts.stdout("Imported the Recappi Mini app session into Recappi CLI\n");
16952
18391
  return;
16953
18392
  }
16954
- if (command === "auth status" && isRecord3(data)) {
18393
+ if (command === "auth status" && isRecord4(data)) {
16955
18394
  if (data.loggedIn) {
16956
18395
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
16957
18396
  `);
@@ -16960,17 +18399,50 @@ function renderHumanSuccess(command, data, opts) {
16960
18399
  opts.stdout("Not logged in\n");
16961
18400
  return;
16962
18401
  }
16963
- if (command === "version" && isRecord3(data) && typeof data.version === "string") {
18402
+ if (command === "account status" && isRecord4(data)) {
18403
+ if (!data.loggedIn) {
18404
+ opts.stdout("Not logged in\n");
18405
+ return;
18406
+ }
18407
+ opts.stdout(`Account: ${typeof data.email === "string" ? data.email : "signed in"}
18408
+ `);
18409
+ if (typeof data.origin === "string") opts.stdout(` origin: ${data.origin}
18410
+ `);
18411
+ if (typeof data.userId === "string") opts.stdout(` userId: ${data.userId}
18412
+ `);
18413
+ const billing = isRecord4(data.billing) ? data.billing : {};
18414
+ if (typeof billing.tier === "string") opts.stdout(` plan: ${billing.tier}
18415
+ `);
18416
+ if (typeof billing.minutesUsed === "number") {
18417
+ const cap = formatNullableCap(billing.minutesCap, "minutes");
18418
+ opts.stdout(` minutes: ${billing.minutesUsed} / ${cap}
18419
+ `);
18420
+ }
18421
+ if (typeof billing.storageBytes === "number") {
18422
+ const cap = formatNullableCap(billing.storageCapBytes, "bytes");
18423
+ opts.stdout(` storage: ${formatBytes(billing.storageBytes)} / ${cap}
18424
+ `);
18425
+ }
18426
+ const localStore = isRecord4(data.localStore) ? data.localStore : {};
18427
+ if (typeof localStore.path === "string") opts.stdout(` localStore: ${localStore.path}
18428
+ `);
18429
+ opts.stdout(
18430
+ ` localArtifacts: ${numberText(localStore.accountScopedArtifacts)} current, ${numberText(localStore.unattributedArtifacts)} unattributed
18431
+ `
18432
+ );
18433
+ return;
18434
+ }
18435
+ if (command === "version" && isRecord4(data) && typeof data.version === "string") {
16964
18436
  opts.stdout(`${data.version}
16965
18437
  `);
16966
18438
  return;
16967
18439
  }
16968
- if (command === "doctor" && isRecord3(data) && Array.isArray(data.checks)) {
18440
+ if (command === "doctor" && isRecord4(data) && Array.isArray(data.checks)) {
16969
18441
  const status = typeof data.status === "string" ? data.status : "unknown";
16970
18442
  opts.stdout(`Doctor: ${status}
16971
18443
  `);
16972
18444
  for (const check2 of data.checks) {
16973
- if (!isRecord3(check2)) continue;
18445
+ if (!isRecord4(check2)) continue;
16974
18446
  const checkStatus = typeof check2.status === "string" ? check2.status : "unknown";
16975
18447
  const name = typeof check2.name === "string" ? check2.name : "check";
16976
18448
  const message = typeof check2.message === "string" ? ` \u2014 ${check2.message}` : "";
@@ -16981,14 +18453,14 @@ function renderHumanSuccess(command, data, opts) {
16981
18453
  }
16982
18454
  return;
16983
18455
  }
16984
- if (command === "transcript get" && isRecord3(data)) {
18456
+ if (command === "transcript get" && isRecord4(data)) {
16985
18457
  renderTranscriptHuman(data, opts);
16986
18458
  return;
16987
18459
  }
16988
- if (command === "recordings list" && isRecord3(data) && Array.isArray(data.items)) {
18460
+ if (command === "recordings list" && isRecord4(data) && Array.isArray(data.items)) {
16989
18461
  opts.stdout("Recordings:\n");
16990
18462
  for (const item of data.items) {
16991
- if (!isRecord3(item)) continue;
18463
+ if (!isRecord4(item)) continue;
16992
18464
  opts.stdout(` ${recordingLabel(item)}
16993
18465
  `);
16994
18466
  }
@@ -17000,7 +18472,7 @@ Next cursor: ${data.nextCursor}
17000
18472
  }
17001
18473
  return;
17002
18474
  }
17003
- if (command === "recordings get" && isRecord3(data)) {
18475
+ if (command === "recordings get" && isRecord4(data)) {
17004
18476
  opts.stdout(`${recordingTitle(data)}
17005
18477
  `);
17006
18478
  opts.stdout(` recordingId: ${String(data.recordingId)}
@@ -17022,9 +18494,9 @@ Next:
17022
18494
  }
17023
18495
  return;
17024
18496
  }
17025
- if (command === "dashboard stats" && isRecord3(data)) {
17026
- const recordings = isRecord3(data.recordings) ? data.recordings : {};
17027
- const jobs = isRecord3(data.jobs) ? data.jobs : {};
18497
+ if (command === "dashboard stats" && isRecord4(data)) {
18498
+ const recordings = isRecord4(data.recordings) ? data.recordings : {};
18499
+ const jobs = isRecord4(data.jobs) ? data.jobs : {};
17028
18500
  opts.stdout(
17029
18501
  `Recordings: ${numberText(recordings.total)} total, ${numberText(recordings.ready)} ready
17030
18502
  `
@@ -17064,7 +18536,50 @@ Next:
17064
18536
  }
17065
18537
  return;
17066
18538
  }
17067
- if ((command === "jobs wait" || command === "upload") && isRecord3(data)) {
18539
+ if (command === "record" && isRecord4(data)) {
18540
+ opts.stdout("Recording complete\n");
18541
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18542
+ `);
18543
+ if (typeof data.sessionId === "string") opts.stdout(` sessionId: ${data.sessionId}
18544
+ `);
18545
+ if (typeof data.localSessionRef === "string") {
18546
+ opts.stdout(` localSessionRef: ${data.localSessionRef}
18547
+ `);
18548
+ }
18549
+ if (Array.isArray(data.artifacts) && data.artifacts.length > 0) {
18550
+ opts.stdout(" artifacts:\n");
18551
+ for (const artifact of data.artifacts) {
18552
+ if (!isRecord4(artifact)) continue;
18553
+ const kind = typeof artifact.kind === "string" ? artifact.kind : "artifact";
18554
+ const localPath = typeof artifact.localPath === "string" ? artifact.localPath : "";
18555
+ opts.stdout(` - ${kind}: ${localPath}
18556
+ `);
18557
+ }
18558
+ }
18559
+ if (typeof data.recordingId === "string") {
18560
+ opts.stdout(`
18561
+ Next:
18562
+ recappi recordings get ${data.recordingId}
18563
+ `);
18564
+ }
18565
+ return;
18566
+ }
18567
+ if (command === "audio" && isRecord4(data)) {
18568
+ const action = typeof data.action === "string" ? data.action : "download";
18569
+ opts.stdout(
18570
+ action === "open" ? "Audio opened\n" : action === "reveal" ? "Audio revealed\n" : "Audio ready\n"
18571
+ );
18572
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
18573
+ `);
18574
+ if (typeof data.localPath === "string") opts.stdout(` localPath: ${data.localPath}
18575
+ `);
18576
+ if (typeof data.reused === "boolean") {
18577
+ opts.stdout(` source: ${data.reused ? "local cache" : "downloaded"}
18578
+ `);
18579
+ }
18580
+ return;
18581
+ }
18582
+ if ((command === "jobs wait" || command === "upload") && isRecord4(data)) {
17068
18583
  if (typeof data.transcriptId === "string") {
17069
18584
  opts.stdout("Transcription ready\n");
17070
18585
  opts.stdout(` transcriptId: ${data.transcriptId}
@@ -17085,10 +18600,10 @@ Next:
17085
18600
  }
17086
18601
  return;
17087
18602
  }
17088
- if (command === "schema" && isRecord3(data) && Array.isArray(data.commands)) {
18603
+ if (command === "schema" && isRecord4(data) && Array.isArray(data.commands)) {
17089
18604
  opts.stdout("Commands:\n");
17090
18605
  for (const entry of data.commands) {
17091
- if (!isRecord3(entry) || typeof entry.name !== "string") continue;
18606
+ if (!isRecord4(entry) || typeof entry.name !== "string") continue;
17092
18607
  const summary = typeof entry.summary === "string" ? ` \u2014 ${entry.summary}` : "";
17093
18608
  opts.stdout(` ${entry.name}${summary}
17094
18609
  `);
@@ -17178,7 +18693,7 @@ function renderTranscriptHuman(data, opts) {
17178
18693
  const segments = Array.isArray(data.segments) ? data.segments : [];
17179
18694
  let printedBody = false;
17180
18695
  for (const segment of segments) {
17181
- if (!isRecord3(segment) || typeof segment.text !== "string") continue;
18696
+ if (!isRecord4(segment) || typeof segment.text !== "string") continue;
17182
18697
  const clock = typeof segment.startMs === "number" ? `[${formatClock(segment.startMs / 1e3)}] ` : "";
17183
18698
  const speaker = typeof segment.speaker === "string" ? `${segment.speaker}: ` : "";
17184
18699
  opts.stdout(`${clock}${speaker}${segment.text}
@@ -17190,7 +18705,7 @@ function renderTranscriptHuman(data, opts) {
17190
18705
  `);
17191
18706
  printedBody = true;
17192
18707
  }
17193
- const summary = isRecord3(data.summary) ? data.summary : void 0;
18708
+ const summary = isRecord4(data.summary) ? data.summary : void 0;
17194
18709
  if (!summary || summary.status !== "succeeded") return;
17195
18710
  if (typeof summary.tldr === "string" && summary.tldr.length > 0) {
17196
18711
  opts.stdout(`
@@ -17208,7 +18723,7 @@ Summary:
17208
18723
  if (Array.isArray(summary.actionItems) && summary.actionItems.length > 0) {
17209
18724
  opts.stdout("\nAction items:\n");
17210
18725
  for (const item of summary.actionItems) {
17211
- if (!isRecord3(item) || typeof item.what !== "string") continue;
18726
+ if (!isRecord4(item) || typeof item.what !== "string") continue;
17212
18727
  const who = typeof item.who === "string" ? `${item.who}: ` : "";
17213
18728
  opts.stdout(` - ${who}${item.what}
17214
18729
  `);
@@ -17246,6 +18761,11 @@ function formatBytes(bytes) {
17246
18761
  }
17247
18762
  return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
17248
18763
  }
18764
+ function formatNullableCap(value, unit) {
18765
+ if (value === null || value === void 0) return "Unlimited";
18766
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Unlimited";
18767
+ return unit === "bytes" ? formatBytes(value) : String(value);
18768
+ }
17249
18769
  function formatClock(seconds) {
17250
18770
  const total = Math.max(0, Math.floor(seconds));
17251
18771
  const hours = Math.floor(total / 3600);
@@ -17274,7 +18794,7 @@ function applyFields(command, data, fields, compact) {
17274
18794
  };
17275
18795
  return compact ? compactData(filtered2) : filtered2;
17276
18796
  }
17277
- if (!isRecord3(data)) return data;
18797
+ if (!isRecord4(data)) return data;
17278
18798
  const allowed = new Set(Object.keys(data));
17279
18799
  assertKnownFields(fields, allowed);
17280
18800
  const filtered = pickFields(data, fields);
@@ -17299,7 +18819,7 @@ function compactData(value) {
17299
18819
  if (Array.isArray(value)) {
17300
18820
  return value.map(compactData).filter((item) => item !== void 0);
17301
18821
  }
17302
- if (isRecord3(value)) {
18822
+ if (isRecord4(value)) {
17303
18823
  const out = {};
17304
18824
  for (const [key, child] of Object.entries(value)) {
17305
18825
  const compacted = compactData(child);
@@ -17317,16 +18837,16 @@ function stableStringify(value, compact) {
17317
18837
  }
17318
18838
  function sortKeys(value) {
17319
18839
  if (Array.isArray(value)) return value.map(sortKeys);
17320
- if (!isRecord3(value)) return value;
18840
+ if (!isRecord4(value)) return value;
17321
18841
  return Object.fromEntries(
17322
18842
  Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
17323
18843
  );
17324
18844
  }
17325
- function isRecord3(value) {
18845
+ function isRecord4(value) {
17326
18846
  return typeof value === "object" && value !== null && !Array.isArray(value);
17327
18847
  }
17328
18848
  function isUploadBatch(value) {
17329
- return isRecord3(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
18849
+ return isRecord4(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
17330
18850
  }
17331
18851
 
17332
18852
  // src/schema.ts
@@ -17335,9 +18855,12 @@ var COMMAND_DATA_SCHEMAS = {
17335
18855
  "auth logout": authLogoutDataSchema,
17336
18856
  "auth import-macos": authImportDataSchema,
17337
18857
  "auth status": authStatusDataSchema,
18858
+ "account status": accountStatusDataSchema,
18859
+ audio: audioCommandDataSchema,
17338
18860
  doctor: doctorDataSchema,
17339
18861
  "dashboard stats": dashboardStatsDataSchema,
17340
18862
  upload: uploadBatchDataSchema,
18863
+ record: recordCommandDataSchema,
17341
18864
  "recordings get": recordingDataSchema,
17342
18865
  "recordings list": recordingListDataSchema,
17343
18866
  "jobs list": jobListDataSchema,
@@ -17374,11 +18897,11 @@ function buildSchemaDocument(program) {
17374
18897
  event: toJsonSchema(operationEventSchema)
17375
18898
  };
17376
18899
  }
17377
- function walkCommands(command, path3, out) {
18900
+ function walkCommands(command, path6, out) {
17378
18901
  for (const sub of subcommandsOf(command)) {
17379
18902
  const name = sub.name();
17380
18903
  if (name === "help") continue;
17381
- const fullPath = [...path3, name];
18904
+ const fullPath = [...path6, name];
17382
18905
  const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
17383
18906
  if (children.length === 0) {
17384
18907
  out.push(leafCommandDoc(sub, fullPath.join(" ")));
@@ -17437,7 +18960,571 @@ function readCliVersion() {
17437
18960
  }
17438
18961
  var CLI_VERSION = readCliVersion();
17439
18962
 
18963
+ // src/record.tsx
18964
+ import { render, useInput as useInput2 } from "ink";
18965
+
18966
+ // src/sidecar.ts
18967
+ import { spawn as spawn2 } from "child_process";
18968
+ import { createInterface } from "readline";
18969
+ var MiniSidecarClient = class {
18970
+ input;
18971
+ requestTimeoutMs;
18972
+ pending = /* @__PURE__ */ new Map();
18973
+ eventListeners = /* @__PURE__ */ new Set();
18974
+ lineReader;
18975
+ nextId = 1;
18976
+ closed = false;
18977
+ constructor(opts) {
18978
+ this.input = opts.input;
18979
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 1e4;
18980
+ this.lineReader = createInterface({ input: opts.output });
18981
+ this.lineReader.on("line", (line) => this.handleLine(line));
18982
+ this.lineReader.on("close", () => this.rejectAll("Sidecar output closed."));
18983
+ }
18984
+ onEvent(listener) {
18985
+ this.eventListeners.add(listener);
18986
+ return () => {
18987
+ this.eventListeners.delete(listener);
18988
+ };
18989
+ }
18990
+ handshake(params) {
18991
+ return this.request(
18992
+ "recappi.handshake",
18993
+ sidecarHandshakeParamsSchema.parse(params),
18994
+ sidecarHandshakeResultSchema
18995
+ );
18996
+ }
18997
+ startRecording(params) {
18998
+ return this.request(
18999
+ "recappi.recording.start",
19000
+ sidecarRecordingStartParamsSchema.parse(params),
19001
+ sidecarRecordingStartResultSchema
19002
+ );
19003
+ }
19004
+ stopRecording(params) {
19005
+ return this.request(
19006
+ "recappi.recording.stop",
19007
+ sidecarSessionParamsSchema.parse(params),
19008
+ sidecarRecordingStopResultSchema
19009
+ );
19010
+ }
19011
+ cancelRecording(params) {
19012
+ return this.request(
19013
+ "recappi.recording.cancel",
19014
+ sidecarSessionParamsSchema.parse(params),
19015
+ sidecarRecordingStopResultSchema
19016
+ );
19017
+ }
19018
+ getRecordingStatus(params) {
19019
+ return this.request(
19020
+ "recappi.recording.status",
19021
+ sidecarSessionParamsSchema.parse(params),
19022
+ sidecarRecordingStatusResultSchema
19023
+ );
19024
+ }
19025
+ close() {
19026
+ if (this.closed) return;
19027
+ this.closed = true;
19028
+ this.lineReader.close();
19029
+ this.rejectAll("Sidecar client closed.");
19030
+ }
19031
+ request(method, params, resultSchema) {
19032
+ if (this.closed) {
19033
+ return Promise.reject(
19034
+ cliError("internal.unexpected", "Sidecar client is already closed.", {
19035
+ hint: "Start a new sidecar session and retry."
19036
+ })
19037
+ );
19038
+ }
19039
+ const id = this.nextId;
19040
+ this.nextId += 1;
19041
+ const payload = {
19042
+ jsonrpc: "2.0",
19043
+ id,
19044
+ method,
19045
+ params
19046
+ };
19047
+ return new Promise((resolve, reject) => {
19048
+ const timer = setTimeout(() => {
19049
+ this.pending.delete(id);
19050
+ reject(
19051
+ cliError("internal.unexpected", `Sidecar request timed out: ${method}.`, {
19052
+ retryable: true
19053
+ })
19054
+ );
19055
+ }, this.requestTimeoutMs);
19056
+ this.pending.set(id, {
19057
+ resolve: (value) => {
19058
+ try {
19059
+ const parsed = resultSchema.parse(value);
19060
+ resolve(parsed);
19061
+ } catch (error51) {
19062
+ reject(toCliError(error51));
19063
+ }
19064
+ },
19065
+ reject,
19066
+ timer
19067
+ });
19068
+ this.input.write(`${JSON.stringify(payload)}
19069
+ `, (error51) => {
19070
+ if (!error51) return;
19071
+ clearTimeout(timer);
19072
+ this.pending.delete(id);
19073
+ reject(cliError("internal.unexpected", `Could not write to sidecar: ${error51.message}`));
19074
+ });
19075
+ });
19076
+ }
19077
+ handleLine(line) {
19078
+ const trimmed = line.trim();
19079
+ if (!trimmed) return;
19080
+ let raw;
19081
+ try {
19082
+ raw = JSON.parse(trimmed);
19083
+ } catch {
19084
+ this.rejectAll("Sidecar wrote invalid JSON.");
19085
+ return;
19086
+ }
19087
+ const maybeNotification = sidecarNotificationSchema.safeParse(raw);
19088
+ if (maybeNotification.success) {
19089
+ const event = sidecarEventSchema.parse(maybeNotification.data.params);
19090
+ for (const listener of this.eventListeners) listener(event);
19091
+ return;
19092
+ }
19093
+ const response = sidecarResponseSchema.safeParse(raw);
19094
+ if (!response.success) {
19095
+ this.rejectAll("Sidecar wrote an invalid JSON-RPC message.");
19096
+ return;
19097
+ }
19098
+ const id = sidecarJsonRpcIdSchema.parse(response.data.id);
19099
+ const pending = this.pending.get(id);
19100
+ if (!pending) return;
19101
+ this.pending.delete(id);
19102
+ clearTimeout(pending.timer);
19103
+ if ("error" in response.data) {
19104
+ pending.reject(
19105
+ cliError("internal.unexpected", response.data.error.message, {
19106
+ data: response.data.error,
19107
+ retryable: response.data.error.code >= -32099 && response.data.error.code <= -32e3
19108
+ })
19109
+ );
19110
+ return;
19111
+ }
19112
+ pending.resolve(response.data.result);
19113
+ }
19114
+ rejectAll(message) {
19115
+ for (const [id, pending] of this.pending) {
19116
+ this.pending.delete(id);
19117
+ clearTimeout(pending.timer);
19118
+ pending.reject(cliError("internal.unexpected", message));
19119
+ }
19120
+ }
19121
+ };
19122
+ function spawnMiniSidecar(opts) {
19123
+ const spawnProcess = opts.spawnProcess ?? spawn2;
19124
+ const child = spawnProcess(opts.command, opts.args ?? [], {
19125
+ env: opts.env,
19126
+ stdio: ["pipe", "pipe", "pipe"]
19127
+ });
19128
+ const client = new MiniSidecarClient({
19129
+ input: child.stdin,
19130
+ output: child.stdout,
19131
+ requestTimeoutMs: opts.requestTimeoutMs
19132
+ });
19133
+ return {
19134
+ client,
19135
+ kill: () => {
19136
+ client.close();
19137
+ child.kill();
19138
+ }
19139
+ };
19140
+ }
19141
+ function defaultSidecarHandshakeParams(params) {
19142
+ return {
19143
+ protocolVersion: SIDECAR_PROTOCOL_VERSION,
19144
+ ...params
19145
+ };
19146
+ }
19147
+
19148
+ // src/tui/LiveCaptionsScreen.tsx
19149
+ import { useEffect, useState as useState2 } from "react";
19150
+
19151
+ // src/tui/LiveCaptionsView.tsx
19152
+ init_format();
19153
+ init_terminal();
19154
+ import { useMemo, useState } from "react";
19155
+ import { Box, Text, useInput } from "ink";
19156
+
19157
+ // src/tui/liveCaptions.ts
19158
+ function initialLiveCaptionsState() {
19159
+ return { status: "connecting", lines: [] };
19160
+ }
19161
+ function liveCaptionReducer(state, event) {
19162
+ switch (event.kind) {
19163
+ case "status": {
19164
+ const next = { ...state, status: event.status };
19165
+ if (event.status === "live" && state.startedAtMs == null) {
19166
+ next.startedAtMs = event.atMs ?? state.startedAtMs;
19167
+ }
19168
+ if (event.status !== "error") next.error = void 0;
19169
+ return next;
19170
+ }
19171
+ case "partial":
19172
+ return { ...state, partial: event.text };
19173
+ case "final": {
19174
+ const lines = [...state.lines, event.line];
19175
+ return { ...state, lines, partial: void 0 };
19176
+ }
19177
+ case "translationPartial":
19178
+ return { ...state, translationPartial: event.text };
19179
+ case "translationFinal": {
19180
+ const lines = [...state.lines];
19181
+ let idx = event.segmentId ? lines.findIndex((l) => l.id === event.segmentId) : -1;
19182
+ if (idx < 0) idx = lines.length - 1;
19183
+ if (idx >= 0) lines[idx] = { ...lines[idx], translation: event.text };
19184
+ return { ...state, lines, translationPartial: void 0 };
19185
+ }
19186
+ case "error":
19187
+ return { ...state, status: "error", error: event.message };
19188
+ default:
19189
+ return state;
19190
+ }
19191
+ }
19192
+ function recordingStateToStatus(state) {
19193
+ switch (state) {
19194
+ case "idle":
19195
+ case "starting":
19196
+ return "connecting";
19197
+ case "recording":
19198
+ return "live";
19199
+ case "stopping":
19200
+ case "finalizing":
19201
+ case "uploading":
19202
+ case "completed":
19203
+ case "cancelled":
19204
+ return "stopped";
19205
+ case "failed":
19206
+ return "error";
19207
+ }
19208
+ }
19209
+ function sidecarToLiveCaptionEvent(event) {
19210
+ switch (event.type) {
19211
+ case "ready":
19212
+ return { kind: "status", status: "connecting" };
19213
+ case "recording.state":
19214
+ if (event.state === "failed") {
19215
+ return { kind: "error", message: event.message ?? "Recording failed" };
19216
+ }
19217
+ return { kind: "status", status: recordingStateToStatus(event.state) };
19218
+ case "live_caption.delta": {
19219
+ if (event.stream === "translation") {
19220
+ return event.isFinal ? { kind: "translationFinal", segmentId: event.segmentId, text: event.text } : { kind: "translationPartial", text: event.text };
19221
+ }
19222
+ if (event.isFinal) {
19223
+ return {
19224
+ kind: "final",
19225
+ line: {
19226
+ id: event.segmentId ?? `${event.startMs ?? event.atMs ?? 0}`,
19227
+ text: event.text,
19228
+ speaker: event.speaker,
19229
+ atMs: event.startMs ?? event.atMs
19230
+ }
19231
+ };
19232
+ }
19233
+ return { kind: "partial", text: event.text };
19234
+ }
19235
+ case "error":
19236
+ return { kind: "error", message: event.message };
19237
+ case "audio.level":
19238
+ case "local_artifact.upserted":
19239
+ return null;
19240
+ default:
19241
+ return null;
19242
+ }
19243
+ }
19244
+ function liveCaptionStatusLabel(status) {
19245
+ switch (status) {
19246
+ case "connecting":
19247
+ return "Connecting\u2026";
19248
+ case "live":
19249
+ return "\u25CF LIVE";
19250
+ case "reconnecting":
19251
+ return "Reconnecting\u2026";
19252
+ case "stopped":
19253
+ return "Stopped";
19254
+ case "error":
19255
+ return "Error";
19256
+ }
19257
+ }
19258
+
19259
+ // src/tui/LiveCaptionsView.tsx
19260
+ import { jsx, jsxs } from "react/jsx-runtime";
19261
+ var STATUS_COLOR = {
19262
+ connecting: "yellow",
19263
+ live: "red",
19264
+ reconnecting: "yellow",
19265
+ stopped: "gray",
19266
+ error: "red"
19267
+ };
19268
+ function LiveCaptionsView({
19269
+ state,
19270
+ nowMs
19271
+ }) {
19272
+ const size = useTerminalSize();
19273
+ const [scrollUp, setScrollUp] = useState(0);
19274
+ const innerWidth = Math.max(10, size.columns - 2);
19275
+ const items = useMemo(() => {
19276
+ const rows = [];
19277
+ for (const l of state.lines) {
19278
+ rows.push({ key: l.id, kind: "final", speaker: l.speaker, text: l.text });
19279
+ if (l.translation) rows.push({ key: `${l.id}__t`, kind: "translation", text: l.translation });
19280
+ }
19281
+ if (state.partial && state.partial.length > 0) {
19282
+ rows.push({ key: "__partial__", kind: "partial", text: state.partial });
19283
+ }
19284
+ if (state.translationPartial && state.translationPartial.length > 0) {
19285
+ rows.push({ key: "__tpartial__", kind: "translation", text: state.translationPartial });
19286
+ }
19287
+ return rows;
19288
+ }, [state.lines, state.partial, state.translationPartial]);
19289
+ const heights = useMemo(
19290
+ () => items.map((it) => {
19291
+ const prefix = it.kind === "translation" ? "\u21B3 " : it.speaker ? `${it.speaker}: ` : "";
19292
+ return Math.max(1, Math.ceil(displayWidth(prefix + it.text) / innerWidth));
19293
+ }),
19294
+ [items, innerWidth]
19295
+ );
19296
+ const budget = Math.max(3, size.rows - 3);
19297
+ const maxScroll = windowByHeights(heights, Number.MAX_SAFE_INTEGER, budget).maxScroll;
19298
+ const top = Math.max(0, maxScroll - scrollUp);
19299
+ const win = windowByHeights(heights, top, budget);
19300
+ const following = scrollUp === 0;
19301
+ const page = Math.max(1, budget - 1);
19302
+ useInput((input, key) => {
19303
+ if (key.upArrow || input === "k") setScrollUp((s) => Math.min(maxScroll, s + 1));
19304
+ else if (key.downArrow || input === "j") setScrollUp((s) => Math.max(0, s - 1));
19305
+ else if (key.pageUp || input === "b") setScrollUp((s) => Math.min(maxScroll, s + page));
19306
+ else if (key.pageDown || input === " ") setScrollUp((s) => Math.max(0, s - page));
19307
+ else if (input === "G") setScrollUp(0);
19308
+ else if (input === "g") setScrollUp(maxScroll);
19309
+ });
19310
+ const elapsed = state.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - state.startedAtMs)) : null;
19311
+ const statusColor = STATUS_COLOR[state.status] ?? "white";
19312
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
19313
+ /* @__PURE__ */ jsxs(Text, { children: [
19314
+ /* @__PURE__ */ jsx(Text, { bold: true, color: statusColor, children: liveCaptionStatusLabel(state.status) }),
19315
+ elapsed ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${elapsed}` }) : null,
19316
+ !following ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u23F8 scrolled \u2014 G for live" }) : null
19317
+ ] }),
19318
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: items.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.status === "error" ? state.error ? `Error: ${state.error}` : "Live captions error" : "Waiting for captions\u2026" }) : items.slice(win.start, win.end).map(
19319
+ (it) => it.kind === "translation" ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u21B3 ${it.text}` }, it.key) : /* @__PURE__ */ jsxs(Text, { dimColor: it.kind === "partial", italic: it.kind === "partial", children: [
19320
+ it.speaker ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: `${it.speaker}: ` }) : null,
19321
+ it.text
19322
+ ] }, it.key)
19323
+ ) }),
19324
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
19325
+ maxScroll > 0 ? "\u2191\u2193 scroll \xB7 G live \xB7 " : "",
19326
+ "q / esc / \u2190 back"
19327
+ ] }) })
19328
+ ] });
19329
+ }
19330
+
19331
+ // src/tui/LiveCaptionsScreen.tsx
19332
+ import { jsx as jsx2 } from "react/jsx-runtime";
19333
+ function LiveCaptionsScreen({
19334
+ source,
19335
+ now = () => Date.now()
19336
+ }) {
19337
+ const [state, setState] = useState2(initialLiveCaptionsState);
19338
+ const [tick, setTick] = useState2(() => now());
19339
+ useEffect(() => {
19340
+ const unsubscribe = source.onEvent((event) => {
19341
+ const mapped = sidecarToLiveCaptionEvent(event);
19342
+ if (mapped) setState((s) => liveCaptionReducer(s, mapped));
19343
+ });
19344
+ return unsubscribe;
19345
+ }, [source]);
19346
+ useEffect(() => {
19347
+ const id = setInterval(() => setTick(now()), 1e3);
19348
+ return () => clearInterval(id);
19349
+ }, []);
19350
+ return /* @__PURE__ */ jsx2(LiveCaptionsView, { state, nowMs: tick });
19351
+ }
19352
+
19353
+ // src/record.tsx
19354
+ import { jsx as jsx3 } from "react/jsx-runtime";
19355
+ var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
19356
+ async function recordViaSidecar(opts) {
19357
+ const command = resolveSidecarCommand(opts);
19358
+ const sidecarArgs = opts.sidecarArgs ?? [];
19359
+ const spawnSidecar = opts.runtime?.spawnSidecar ?? spawnMiniSidecar;
19360
+ const sidecar = spawnSidecar({ command, args: sidecarArgs, env: opts.env });
19361
+ const account = requireAccountPartition(opts.account);
19362
+ const artifacts = [];
19363
+ let liveRenderer;
19364
+ let handshake;
19365
+ let sessionId;
19366
+ let latestState;
19367
+ let recordingId;
19368
+ let localSessionRef;
19369
+ const unsubscribe = sidecar.client.onEvent((event) => {
19370
+ if (event.type === "recording.state") {
19371
+ latestState = event.state;
19372
+ if (event.recordingId) recordingId = event.recordingId;
19373
+ if (event.localSessionRef) localSessionRef = event.localSessionRef;
19374
+ }
19375
+ if (event.type === "local_artifact.upserted") {
19376
+ artifacts.push(event.artifact);
19377
+ }
19378
+ });
19379
+ try {
19380
+ if (opts.renderLive) {
19381
+ liveRenderer = opts.runtime?.createLiveRenderer?.(sidecar.client) ?? createInkLiveRenderer({
19382
+ source: sidecar.client,
19383
+ renderApp: opts.runtime?.renderApp,
19384
+ now: opts.runtime?.now
19385
+ });
19386
+ }
19387
+ handshake = await sidecar.client.handshake(
19388
+ defaultSidecarHandshakeParams({
19389
+ client: { name: "recappi-cli", version: opts.cliVersion },
19390
+ account: opts.account,
19391
+ capabilities: opts.live ? ["recording.capture", "recording.upload", "live_captions.stream"] : ["recording.capture", "recording.upload"]
19392
+ })
19393
+ );
19394
+ const started = await sidecar.client.startRecording({
19395
+ account,
19396
+ options: {
19397
+ includeSystemAudio: opts.includeSystemAudio ?? true,
19398
+ includeMicrophone: opts.includeMicrophone ?? true,
19399
+ liveCaptions: opts.live === true,
19400
+ ...opts.translationLanguage ? { translationLanguage: opts.translationLanguage } : {},
19401
+ ...opts.transcriptionLanguage ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
19402
+ ...opts.title ? { title: opts.title } : {}
19403
+ }
19404
+ });
19405
+ sessionId = started.sessionId;
19406
+ latestState = started.state;
19407
+ localSessionRef = started.localSessionRef;
19408
+ if (liveRenderer) {
19409
+ await liveRenderer.waitUntilStop();
19410
+ } else {
19411
+ await (opts.runtime?.waitForStop ?? waitForStopSignal)();
19412
+ }
19413
+ const stopped = await sidecar.client.stopRecording({ sessionId });
19414
+ latestState = stopped.state;
19415
+ recordingId = stopped.recordingId ?? recordingId;
19416
+ localSessionRef = stopped.localSessionRef ?? localSessionRef;
19417
+ artifacts.push(...stopped.artifacts ?? []);
19418
+ const uniqueArtifacts = dedupeArtifacts(artifacts);
19419
+ persistArtifacts(uniqueArtifacts, account, opts);
19420
+ return recordCommandDataSchema.parse({
19421
+ origin: account.backendOrigin,
19422
+ userId: account.userId,
19423
+ live: opts.live === true,
19424
+ sessionId: stopped.sessionId,
19425
+ state: stopped.state,
19426
+ ...recordingId ? { recordingId } : {},
19427
+ ...localSessionRef ? { localSessionRef } : {},
19428
+ ...handshake?.sidecar ? { sidecar: handshake.sidecar } : {},
19429
+ artifacts: uniqueArtifacts
19430
+ });
19431
+ } catch (error51) {
19432
+ if (sessionId && latestState && latestState !== "completed" && latestState !== "cancelled") {
19433
+ try {
19434
+ await sidecar.client.cancelRecording({ sessionId });
19435
+ } catch {
19436
+ }
19437
+ }
19438
+ throw error51;
19439
+ } finally {
19440
+ unsubscribe();
19441
+ liveRenderer?.close();
19442
+ sidecar.kill();
19443
+ }
19444
+ }
19445
+ function resolveSidecarCommand(opts) {
19446
+ const command = opts.sidecarCommand?.trim() || opts.env?.[SIDECAR_COMMAND_ENV]?.trim();
19447
+ if (!command) {
19448
+ throw cliError("usage.invalid_argument", "Missing Recappi Mini sidecar command.", {
19449
+ hint: `Pass --sidecar-command, or set ${SIDECAR_COMMAND_ENV} to the Mini sidecar executable.`
19450
+ });
19451
+ }
19452
+ return command;
19453
+ }
19454
+ function persistArtifacts(artifacts, account, opts) {
19455
+ if (artifacts.length === 0) return;
19456
+ const store = openCliStore({ homeDir: opts.homeDir, env: opts.env });
19457
+ try {
19458
+ for (const artifact of artifacts) {
19459
+ store.addLocalArtifact({
19460
+ kind: artifact.kind,
19461
+ account,
19462
+ localPath: artifact.localPath,
19463
+ remoteId: artifact.remoteId,
19464
+ metadata: artifact.metadata
19465
+ });
19466
+ }
19467
+ } finally {
19468
+ store.close();
19469
+ }
19470
+ }
19471
+ function dedupeArtifacts(artifacts) {
19472
+ const seen = /* @__PURE__ */ new Set();
19473
+ const out = [];
19474
+ for (const artifact of artifacts) {
19475
+ const key = [artifact.kind, artifact.localPath, artifact.remoteId ?? ""].join("\0");
19476
+ if (seen.has(key)) continue;
19477
+ seen.add(key);
19478
+ out.push(artifact);
19479
+ }
19480
+ return out;
19481
+ }
19482
+ function waitForStopSignal() {
19483
+ return new Promise((resolve) => {
19484
+ const stop = () => {
19485
+ process.off("SIGINT", stop);
19486
+ process.off("SIGTERM", stop);
19487
+ resolve();
19488
+ };
19489
+ process.once("SIGINT", stop);
19490
+ process.once("SIGTERM", stop);
19491
+ });
19492
+ }
19493
+ function createInkLiveRenderer(opts) {
19494
+ let resolveStop;
19495
+ const stopped = new Promise((resolve) => {
19496
+ resolveStop = resolve;
19497
+ });
19498
+ const renderApp = opts.renderApp ?? render;
19499
+ const app = renderApp(
19500
+ /* @__PURE__ */ jsx3(
19501
+ RecordLiveScreen,
19502
+ {
19503
+ source: opts.source,
19504
+ onStop: () => resolveStop?.(),
19505
+ now: opts.now ?? Date.now
19506
+ }
19507
+ ),
19508
+ { alternateScreen: true, interactive: true }
19509
+ );
19510
+ return {
19511
+ waitUntilStop: () => stopped,
19512
+ close: () => app.unmount()
19513
+ };
19514
+ }
19515
+ function RecordLiveScreen({
19516
+ source,
19517
+ onStop,
19518
+ now
19519
+ }) {
19520
+ useInput2((input, key) => {
19521
+ if (input === "q" || key.escape || key.leftArrow) onStop();
19522
+ });
19523
+ return /* @__PURE__ */ jsx3(LiveCaptionsScreen, { source, now });
19524
+ }
19525
+
17440
19526
  // src/cli.ts
19527
+ var DASHBOARD_RECORDINGS_PAGE_SIZE = 50;
17441
19528
  async function runCli(deps = {}) {
17442
19529
  const argv = deps.argv ?? process.argv.slice(2);
17443
19530
  const stdout = deps.stdout ?? ((text) => process.stdout.write(text));
@@ -17451,7 +19538,7 @@ async function runCli(deps = {}) {
17451
19538
  return 0;
17452
19539
  }
17453
19540
  const mode = parsed.options.mode ?? (isTTY ? "human" : "json");
17454
- const render2 = {
19541
+ const render3 = {
17455
19542
  mode,
17456
19543
  compact: parsed.options.compact,
17457
19544
  fields: parsed.options.fields,
@@ -17460,11 +19547,11 @@ async function runCli(deps = {}) {
17460
19547
  progress: createHumanProgressState(mode === "human" && isTTY)
17461
19548
  };
17462
19549
  if (parsed.kind === "schema") {
17463
- renderSuccess("schema", parsed.document, render2);
19550
+ renderSuccess("schema", parsed.document, render3);
17464
19551
  return 0;
17465
19552
  }
17466
19553
  if (parsed.kind === "version") {
17467
- renderSuccess("version", { version: CLI_VERSION }, render2);
19554
+ renderSuccess("version", { version: CLI_VERSION }, render3);
17468
19555
  return 0;
17469
19556
  }
17470
19557
  const auth = await resolveAuthContext({
@@ -17475,22 +19562,38 @@ async function runCli(deps = {}) {
17475
19562
  const client = new RecappiApiClient(auth, {
17476
19563
  fetchImpl: deps.fetchImpl,
17477
19564
  sleep: deps.sleep,
17478
- env: deps.env
19565
+ env: deps.env,
19566
+ homeDir: deps.homeDir
17479
19567
  });
17480
19568
  if (parsed.kind === "dashboard") {
19569
+ const status = await client.authStatus();
19570
+ const account = status.loggedIn && status.userId ? { backendOrigin: auth.origin, userId: status.userId } : null;
19571
+ const recordingAudio = createRecordingAudioRuntime(client, {
19572
+ account,
19573
+ env: deps.env,
19574
+ homeDir: deps.homeDir
19575
+ });
17481
19576
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
17482
19577
  await runDashboard2({
17483
19578
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
17484
- fetchRecordings: () => client.listRecordings({ limit: 20 }),
19579
+ fetchRecordings: ({ cursor, limit = DASHBOARD_RECORDINGS_PAGE_SIZE } = {}) => client.listRecordings({ limit, cursor }),
17485
19580
  fetchDashboardStats: () => client.dashboardStats(),
17486
19581
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
19582
+ recordingAudio,
19583
+ listDownloadedRecordingIds: () => recordingAudio.listDownloadedRecordingIds(),
19584
+ listDownloads: () => recordingAudio.listDownloads(),
17487
19585
  initialView: parsed.initialView
17488
19586
  });
17489
19587
  return 0;
17490
19588
  }
17491
19589
  if (parsed.kind === "auth-status") {
17492
19590
  const data = await client.authStatus();
17493
- renderSuccess("auth status", data, render2);
19591
+ renderSuccess("auth status", data, render3);
19592
+ return data.loggedIn ? 0 : 3;
19593
+ }
19594
+ if (parsed.kind === "account-status") {
19595
+ const data = await client.accountStatus();
19596
+ renderSuccess("account status", data, render3);
17494
19597
  return data.loggedIn ? 0 : 3;
17495
19598
  }
17496
19599
  if (parsed.kind === "auth-login") {
@@ -17505,12 +19608,12 @@ async function runCli(deps = {}) {
17505
19608
  sleep: deps.sleep
17506
19609
  }
17507
19610
  });
17508
- renderSuccess("auth login", data, render2);
19611
+ renderSuccess("auth login", data, render3);
17509
19612
  return 0;
17510
19613
  }
17511
19614
  if (parsed.kind === "auth-logout") {
17512
- const cleared = await clearAuthConfig(deps.homeDir ?? os3.homedir());
17513
- renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render2);
19615
+ const cleared = await clearAuthConfig(deps.homeDir ?? os5.homedir());
19616
+ renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render3);
17514
19617
  return 0;
17515
19618
  }
17516
19619
  if (parsed.kind === "auth-import-macos") {
@@ -17520,20 +19623,20 @@ async function runCli(deps = {}) {
17520
19623
  hint: keychain.hint ?? "Run recappi auth login instead."
17521
19624
  });
17522
19625
  }
17523
- await saveAuthConfig(deps.homeDir ?? os3.homedir(), {
19626
+ await saveAuthConfig(deps.homeDir ?? os5.homedir(), {
17524
19627
  origin: auth.origin,
17525
19628
  token: keychain.token
17526
19629
  });
17527
19630
  renderSuccess(
17528
19631
  "auth import-macos",
17529
19632
  { imported: true, origin: auth.origin, source: "macos-keychain" },
17530
- render2
19633
+ render3
17531
19634
  );
17532
19635
  return 0;
17533
19636
  }
17534
19637
  if (parsed.kind === "doctor") {
17535
19638
  const data = await client.doctor();
17536
- renderSuccess("doctor", data, render2);
19639
+ renderSuccess("doctor", data, render3);
17537
19640
  if (data.status !== "error") return 0;
17538
19641
  return data.checks.some((check2) => check2.name.startsWith("auth.")) ? 3 : 1;
17539
19642
  }
@@ -17547,7 +19650,7 @@ async function runCli(deps = {}) {
17547
19650
  provider: parsed.provider,
17548
19651
  prompt: parsed.prompt,
17549
19652
  force: parsed.force,
17550
- onEvent: (event) => renderEvent(event, render2)
19653
+ onEvent: (event) => renderEvent(event, render3)
17551
19654
  });
17552
19655
  if (data.failures.length > 0) {
17553
19656
  const worst = data.failures.reduce((max, item) => Math.max(max, item.error.exitCode), 1);
@@ -17558,26 +19661,95 @@ async function runCli(deps = {}) {
17558
19661
  message: `${data.failures.length} of ${data.totalCount} upload(s) failed.`,
17559
19662
  hint: "Inspect data.failures[].error for per-file codes; retry only the failed files."
17560
19663
  };
17561
- renderFailure("upload", descriptor, render2, data);
19664
+ renderFailure("upload", descriptor, render3, data);
17562
19665
  return worst;
17563
19666
  }
17564
- renderSuccess("upload", data, render2);
19667
+ renderSuccess("upload", data, render3);
19668
+ return 0;
19669
+ }
19670
+ if (parsed.kind === "record") {
19671
+ const status = await client.authStatus();
19672
+ if (!status.loggedIn || !status.userId) {
19673
+ throw cliError("auth.not_logged_in", "Sign in before starting a sidecar recording.", {
19674
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19675
+ });
19676
+ }
19677
+ if (mode === "human" && isTTY && !parsed.live) {
19678
+ stderr("Recording\u2026 press Ctrl-C to stop.\n");
19679
+ }
19680
+ const data = await recordViaSidecar({
19681
+ account: {
19682
+ backendOrigin: auth.origin,
19683
+ userId: status.userId,
19684
+ ...status.email ? { email: status.email } : {}
19685
+ },
19686
+ cliVersion: CLI_VERSION,
19687
+ env: deps.env,
19688
+ homeDir: deps.homeDir,
19689
+ title: parsed.title,
19690
+ live: parsed.live,
19691
+ includeSystemAudio: parsed.includeSystemAudio,
19692
+ includeMicrophone: parsed.includeMicrophone,
19693
+ translationLanguage: parsed.translationLanguage,
19694
+ transcriptionLanguage: parsed.transcriptionLanguage,
19695
+ sidecarCommand: parsed.sidecarCommand,
19696
+ renderLive: parsed.live === true && mode === "human" && isTTY,
19697
+ runtime: deps.recordRuntime
19698
+ });
19699
+ renderSuccess("record", data, render3);
19700
+ return 0;
19701
+ }
19702
+ if (parsed.kind === "audio") {
19703
+ const status = await client.authStatus();
19704
+ if (!status.loggedIn || !status.userId) {
19705
+ throw cliError("auth.not_logged_in", "Sign in before using local audio actions.", {
19706
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19707
+ });
19708
+ }
19709
+ const recordingAudio = createRecordingAudioRuntime(client, {
19710
+ account: { backendOrigin: auth.origin, userId: status.userId },
19711
+ env: deps.env,
19712
+ homeDir: deps.homeDir
19713
+ });
19714
+ const download = await recordingAudio.downloadRecordingAudioFile(
19715
+ parsed.recordingId,
19716
+ parsed.outputDir ? { directory: parsed.outputDir } : void 0
19717
+ );
19718
+ if (parsed.action === "open") {
19719
+ await recordingAudio.openPath(download.localPath);
19720
+ } else if (parsed.action === "reveal") {
19721
+ await recordingAudio.revealInFinder(download.localPath);
19722
+ }
19723
+ renderSuccess(
19724
+ "audio",
19725
+ {
19726
+ origin: auth.origin,
19727
+ recordingId: parsed.recordingId,
19728
+ localPath: download.localPath,
19729
+ action: parsed.action,
19730
+ reused: download.reused,
19731
+ ...download.artifactId !== void 0 ? { artifactId: download.artifactId } : {},
19732
+ ...download.contentType ? { contentType: download.contentType } : {},
19733
+ ...download.contentLength !== void 0 ? { contentLength: download.contentLength } : {}
19734
+ },
19735
+ render3
19736
+ );
17565
19737
  return 0;
17566
19738
  }
17567
19739
  if (parsed.kind === "jobs-wait") {
17568
19740
  const parsedOptions = parsed.options;
17569
19741
  const data = await client.waitForJob(parsed.jobId, {
17570
19742
  onEvent: (event) => renderEvent(event, {
17571
- ...render2,
19743
+ ...render3,
17572
19744
  mode: parsedOptions.mode === "jsonl" ? "jsonl" : "human"
17573
19745
  })
17574
19746
  });
17575
- renderSuccess("jobs wait", data, render2);
19747
+ renderSuccess("jobs wait", data, render3);
17576
19748
  return 0;
17577
19749
  }
17578
19750
  if (parsed.kind === "jobs-list") {
17579
19751
  const data = await client.listJobs({ status: parsed.status, limit: parsed.limit });
17580
- renderSuccess("jobs list", data, render2);
19752
+ renderSuccess("jobs list", data, render3);
17581
19753
  return 0;
17582
19754
  }
17583
19755
  if (parsed.kind === "recordings-list") {
@@ -17586,22 +19758,22 @@ async function runCli(deps = {}) {
17586
19758
  cursor: parsed.cursor,
17587
19759
  search: parsed.search
17588
19760
  });
17589
- renderSuccess("recordings list", data, render2);
19761
+ renderSuccess("recordings list", data, render3);
17590
19762
  return 0;
17591
19763
  }
17592
19764
  if (parsed.kind === "recordings-get") {
17593
19765
  const data = await client.getRecording(parsed.recordingId);
17594
- renderSuccess("recordings get", data, render2);
19766
+ renderSuccess("recordings get", data, render3);
17595
19767
  return 0;
17596
19768
  }
17597
19769
  if (parsed.kind === "dashboard-stats") {
17598
19770
  const data = await client.dashboardStats();
17599
- renderSuccess("dashboard stats", data, render2);
19771
+ renderSuccess("dashboard stats", data, render3);
17600
19772
  return 0;
17601
19773
  }
17602
19774
  if (parsed.kind === "transcript-get") {
17603
19775
  const data = await client.getTranscript(parsed.transcriptId);
17604
- renderSuccess("transcript get", data, render2);
19776
+ renderSuccess("transcript get", data, render3);
17605
19777
  return 0;
17606
19778
  }
17607
19779
  throw cliError("usage.invalid_argument", "Unknown command.");
@@ -17793,6 +19965,17 @@ Agent mode:
17793
19965
  commandName: "doctor"
17794
19966
  });
17795
19967
  });
19968
+ const account = program.command("account").description("Account and quota commands");
19969
+ addCommonOptions(account);
19970
+ const accountStatus = account.command("status").description("Show account, quota, and local state");
19971
+ addCommonOptions(accountStatus);
19972
+ accountStatus.action((_options, command) => {
19973
+ onSelect({
19974
+ kind: "account-status",
19975
+ options: collectGlobalOptions(command),
19976
+ commandName: "account status"
19977
+ });
19978
+ });
17796
19979
  const upload = program.command("upload <file-or-dir>").description("Upload an audio file or directory to Recappi Cloud").option("--title <title>", "recording title", parseStringOption("--title")).option("--transcribe", "start transcription after upload").option("--wait", "wait for the transcription job to reach a terminal state").option("--language <lang>", "transcription language hint", parseStringOption("--language")).option("--provider <name>", "transcription provider", parseStringOption("--provider")).option("--prompt <text>", "transcription prompt/context", parseStringOption("--prompt")).option("--force", "force upload if a conflict is retryable");
17797
19980
  addCommonOptions(upload);
17798
19981
  upload.action((inputPath, opts, command) => {
@@ -17810,6 +19993,55 @@ Agent mode:
17810
19993
  ...opts.force === true ? { force: true } : {}
17811
19994
  });
17812
19995
  });
19996
+ const record2 = program.command("record").description("Start a Recappi Mini sidecar recording").option("--title <title>", "recording title", parseStringOption("--title")).option("--live", "show live captions while recording").option("--no-system-audio", "record microphone only").option("--no-microphone", "record system audio only").option(
19997
+ "--translation-language <lang>",
19998
+ "live caption translation language",
19999
+ parseStringOption("--translation-language")
20000
+ ).option(
20001
+ "--transcription-language <lang>",
20002
+ "recording/transcription language hint",
20003
+ parseStringOption("--transcription-language")
20004
+ ).option(
20005
+ "--sidecar-command <path>",
20006
+ "Recappi Mini sidecar executable",
20007
+ parseStringOption("--sidecar-command")
20008
+ );
20009
+ addCommonOptions(record2);
20010
+ record2.action((opts, command) => {
20011
+ if (opts.systemAudio === false && opts.microphone === false) {
20012
+ throw cliError("usage.invalid_argument", "Choose at least one recording input.", {
20013
+ hint: "Use system audio, microphone, or both."
20014
+ });
20015
+ }
20016
+ onSelect({
20017
+ kind: "record",
20018
+ options: collectGlobalOptions(command),
20019
+ commandName: "record",
20020
+ ...typeof opts.title === "string" ? { title: opts.title } : {},
20021
+ ...opts.live === true ? { live: true } : {},
20022
+ ...opts.systemAudio === false ? { includeSystemAudio: false } : {},
20023
+ ...opts.microphone === false ? { includeMicrophone: false } : {},
20024
+ ...typeof opts.translationLanguage === "string" ? { translationLanguage: opts.translationLanguage } : {},
20025
+ ...typeof opts.transcriptionLanguage === "string" ? { transcriptionLanguage: opts.transcriptionLanguage } : {},
20026
+ ...typeof opts.sidecarCommand === "string" ? { sidecarCommand: opts.sidecarCommand } : {}
20027
+ });
20028
+ });
20029
+ const audio = program.command("audio").description("Download or open a recording audio file").argument("<recording-id>", "recording id").option("--download", "download audio and print the local path").option("--open", "download if needed, then open the audio file").option("--reveal", "download if needed, then reveal the audio file in Finder").option(
20030
+ "--output-dir <dir>",
20031
+ "directory for downloaded audio",
20032
+ parseStringOption("--output-dir")
20033
+ );
20034
+ addCommonOptions(audio);
20035
+ audio.action((recordingId, opts, command) => {
20036
+ onSelect({
20037
+ kind: "audio",
20038
+ options: collectGlobalOptions(command),
20039
+ commandName: "audio",
20040
+ recordingId,
20041
+ action: audioAction(opts),
20042
+ ...opts.outputDir ? { outputDir: opts.outputDir } : {}
20043
+ });
20044
+ });
17813
20045
  const schema = program.command("schema").description("Print the machine-readable CLI contract (commands, error codes, JSON Schemas)");
17814
20046
  addCommonOptions(schema);
17815
20047
  schema.action((_options, command) => {
@@ -17906,6 +20138,17 @@ Agent mode:
17906
20138
  function addCommonOptions(command) {
17907
20139
  command.option("--json", "write one JSON envelope to stdout").option("--jsonl", "write JSONL operation events to stdout").option("--human", "write human-readable output").option("--fields <list>", "comma-separated data fields to keep", parseFieldsOption).option("--compact", "omit empty optional data and print compact JSON").option("--origin <url>", "Recappi Cloud origin", parseStringOption("--origin"));
17908
20140
  }
20141
+ function audioAction(opts) {
20142
+ const selected = [
20143
+ opts.download ? "download" : null,
20144
+ opts.open ? "open" : null,
20145
+ opts.reveal ? "reveal" : null
20146
+ ].filter((action) => action !== null);
20147
+ if (selected.length > 1) {
20148
+ throw cliError("usage.invalid_argument", "Choose only one of --download, --open, or --reveal.");
20149
+ }
20150
+ return selected[0] ?? "download";
20151
+ }
17909
20152
  function parseStringOption(flag) {
17910
20153
  return (value) => {
17911
20154
  if (!value || value.startsWith("-")) {
@@ -17971,6 +20214,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
17971
20214
  "--language",
17972
20215
  "--provider",
17973
20216
  "--prompt",
20217
+ "--translation-language",
20218
+ "--transcription-language",
20219
+ "--sidecar-command",
17974
20220
  "--status",
17975
20221
  "--limit",
17976
20222
  "--cursor",