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