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