recappi 0.1.0 → 0.1.2
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 +999 -211
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ function Header({ active }) {
|
|
|
20
20
|
] }),
|
|
21
21
|
/* @__PURE__ */ jsx(Tab, { num: "1", label: "Overview", active: active === "overview", enabled: true }),
|
|
22
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:
|
|
23
|
+
/* @__PURE__ */ jsx(Tab, { num: "3", label: "Recordings", active: active === "recordings", enabled: true })
|
|
24
24
|
] });
|
|
25
25
|
}
|
|
26
26
|
function Tab({
|
|
@@ -151,6 +151,36 @@ function resolveJobLinks(item, origin) {
|
|
|
151
151
|
}
|
|
152
152
|
return {};
|
|
153
153
|
}
|
|
154
|
+
function resolveRecordingLinks(recordingId, origin) {
|
|
155
|
+
if (!recordingId) return {};
|
|
156
|
+
return { webUrl: `${origin}/recordings/${recordingId}` };
|
|
157
|
+
}
|
|
158
|
+
function recordingStatusStyle(status) {
|
|
159
|
+
switch (status) {
|
|
160
|
+
case "ready":
|
|
161
|
+
return { label: "Ready", color: "green", glyph: "\u2713" };
|
|
162
|
+
case "uploading":
|
|
163
|
+
return { label: "Uploading", color: "cyan", glyph: "\u2191" };
|
|
164
|
+
case "failed":
|
|
165
|
+
return { label: "Failed", color: "red", glyph: "\u2717" };
|
|
166
|
+
case "aborted":
|
|
167
|
+
return { label: "Aborted", color: "gray", glyph: "\u2022" };
|
|
168
|
+
default:
|
|
169
|
+
return { label: status, color: "white", glyph: "\u2022" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function formatBytes2(bytes) {
|
|
173
|
+
if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "";
|
|
174
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
175
|
+
let value = bytes;
|
|
176
|
+
let unit = 0;
|
|
177
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
178
|
+
value /= 1024;
|
|
179
|
+
unit += 1;
|
|
180
|
+
}
|
|
181
|
+
const rounded = value < 10 && unit > 0 ? value.toFixed(1) : String(Math.round(value));
|
|
182
|
+
return `${rounded}${units[unit]}`;
|
|
183
|
+
}
|
|
154
184
|
var SPINNER_FRAMES;
|
|
155
185
|
var init_format = __esm({
|
|
156
186
|
"src/tui/format.ts"() {
|
|
@@ -212,83 +242,149 @@ var init_JobsView = __esm({
|
|
|
212
242
|
}
|
|
213
243
|
});
|
|
214
244
|
|
|
215
|
-
// src/tui/
|
|
245
|
+
// src/tui/RecordingRow.tsx
|
|
216
246
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
217
247
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
218
|
-
function
|
|
219
|
-
return
|
|
248
|
+
function recordingTitle2(item) {
|
|
249
|
+
return item.title || item.summaryTitle || item.recordingId;
|
|
250
|
+
}
|
|
251
|
+
function RecordingRow({
|
|
252
|
+
item,
|
|
253
|
+
selected,
|
|
254
|
+
nowMs
|
|
255
|
+
}) {
|
|
256
|
+
const style = recordingStatusStyle(item.status);
|
|
257
|
+
const detail = [
|
|
258
|
+
item.durationMs ? formatClockMs(item.durationMs) : void 0,
|
|
259
|
+
formatAge(item.createdAt, nowMs) || void 0,
|
|
260
|
+
item.activeTranscriptId ? "transcript ready" : void 0
|
|
261
|
+
].filter(Boolean).join(" \xB7 ");
|
|
262
|
+
return /* @__PURE__ */ jsxs3(Box4, { children: [
|
|
263
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
|
|
264
|
+
/* @__PURE__ */ jsx4(Text4, { color: style.color, children: `${style.glyph} ${padCell(style.label, 10)}` }),
|
|
265
|
+
/* @__PURE__ */ jsx4(Text4, { bold: selected, children: padCell(recordingTitle2(item), 26) }),
|
|
266
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: !selected, children: detail })
|
|
267
|
+
] });
|
|
268
|
+
}
|
|
269
|
+
var init_RecordingRow = __esm({
|
|
270
|
+
"src/tui/RecordingRow.tsx"() {
|
|
271
|
+
"use strict";
|
|
272
|
+
init_format();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/tui/OverviewView.tsx
|
|
277
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
278
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
279
|
+
function overviewRecentRecordings(recordings) {
|
|
280
|
+
return recordings.slice(0, RECENT_LIMIT);
|
|
220
281
|
}
|
|
221
282
|
function OverviewView({
|
|
222
|
-
|
|
283
|
+
recordings,
|
|
284
|
+
jobs,
|
|
285
|
+
stats,
|
|
223
286
|
selectedIndex,
|
|
224
287
|
spinnerFrame,
|
|
225
288
|
nowMs
|
|
226
289
|
}) {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
290
|
+
const recent = overviewRecentRecordings(recordings);
|
|
291
|
+
const activeJobs = jobs.filter((j) => j.status === "running" || j.status === "queued");
|
|
292
|
+
const jobCounts = countJobs(jobs);
|
|
293
|
+
const recTotal = stats?.recordings.total ?? recordings.length;
|
|
294
|
+
const recReady = stats?.recordings.ready;
|
|
295
|
+
const transcribed = stats?.recordings.totalDurationMs;
|
|
296
|
+
return /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
297
|
+
/* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
298
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
299
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Recordings " }),
|
|
300
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: recTotal }),
|
|
301
|
+
recReady != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${recReady} ready` }) : null,
|
|
302
|
+
transcribed != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${formatClockMs(transcribed)} transcribed` }) : null
|
|
236
303
|
] }),
|
|
237
|
-
/* @__PURE__ */
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
] })
|
|
251
320
|
] })
|
|
252
321
|
] }),
|
|
253
|
-
/* @__PURE__ */
|
|
254
|
-
/* @__PURE__ */
|
|
255
|
-
|
|
256
|
-
|
|
322
|
+
/* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
323
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Recent recordings" }),
|
|
324
|
+
recent.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " No recordings yet \u2014 run: recappi upload <file>" }) : recent.map((item, index) => /* @__PURE__ */ jsx5(
|
|
325
|
+
RecordingRow,
|
|
257
326
|
{
|
|
258
327
|
item,
|
|
259
328
|
selected: index === selectedIndex,
|
|
260
|
-
|
|
329
|
+
nowMs
|
|
261
330
|
},
|
|
262
|
-
item.
|
|
331
|
+
item.recordingId
|
|
263
332
|
))
|
|
264
333
|
] }),
|
|
265
|
-
|
|
266
|
-
/* @__PURE__ */
|
|
267
|
-
|
|
268
|
-
const style = statusStyle(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
/* @__PURE__ */
|
|
273
|
-
|
|
274
|
-
/* @__PURE__ */ jsx4(Text4, { children: title }),
|
|
275
|
-
age ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${age}` }) : null
|
|
276
|
-
] }, item.jobId);
|
|
334
|
+
activeJobs.length > 0 ? /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
335
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Transcribing now" }),
|
|
336
|
+
activeJobs.slice(0, 3).map((job) => {
|
|
337
|
+
const style = statusStyle(job.status);
|
|
338
|
+
return /* @__PURE__ */ jsxs4(Box5, { children: [
|
|
339
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
340
|
+
/* @__PURE__ */ jsx5(Text5, { color: style.color, children: `${statusGlyph(job.status, spinnerFrame)} ` }),
|
|
341
|
+
/* @__PURE__ */ jsx5(Text5, { children: job.recording?.title ?? job.recordingId })
|
|
342
|
+
] }, job.jobId);
|
|
277
343
|
})
|
|
278
344
|
] }) : null
|
|
279
345
|
] });
|
|
280
346
|
}
|
|
347
|
+
var RECENT_LIMIT;
|
|
281
348
|
var init_OverviewView = __esm({
|
|
282
349
|
"src/tui/OverviewView.tsx"() {
|
|
283
350
|
"use strict";
|
|
284
|
-
|
|
351
|
+
init_RecordingRow();
|
|
285
352
|
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();
|
|
286
382
|
}
|
|
287
383
|
});
|
|
288
384
|
|
|
289
385
|
// src/tui/JobDetailView.tsx
|
|
290
|
-
import { Box as
|
|
291
|
-
import { jsx as
|
|
386
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
387
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
292
388
|
function JobDetailView({
|
|
293
389
|
item,
|
|
294
390
|
origin,
|
|
@@ -298,13 +394,13 @@ function JobDetailView({
|
|
|
298
394
|
const style = statusStyle(item.status);
|
|
299
395
|
const links = resolveJobLinks(item, origin);
|
|
300
396
|
const title = item.recording?.title ?? item.recordingId;
|
|
301
|
-
return /* @__PURE__ */
|
|
302
|
-
/* @__PURE__ */
|
|
397
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 1, children: [
|
|
398
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
303
399
|
"\u2039 Jobs / ",
|
|
304
400
|
title
|
|
305
401
|
] }),
|
|
306
|
-
/* @__PURE__ */
|
|
307
|
-
|
|
402
|
+
/* @__PURE__ */ jsxs5(
|
|
403
|
+
Box7,
|
|
308
404
|
{
|
|
309
405
|
marginTop: 1,
|
|
310
406
|
borderStyle: "round",
|
|
@@ -312,17 +408,17 @@ function JobDetailView({
|
|
|
312
408
|
paddingX: 1,
|
|
313
409
|
flexDirection: "column",
|
|
314
410
|
children: [
|
|
315
|
-
/* @__PURE__ */
|
|
411
|
+
/* @__PURE__ */ jsxs5(Text7, { color: style.color, bold: true, children: [
|
|
316
412
|
style.label,
|
|
317
|
-
item.provider ? /* @__PURE__ */
|
|
413
|
+
item.provider ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${item.provider}` }) : null
|
|
318
414
|
] }),
|
|
319
|
-
/* @__PURE__ */
|
|
415
|
+
/* @__PURE__ */ jsx7(StatusLine, { item, spinnerFrame, nowMs })
|
|
320
416
|
]
|
|
321
417
|
}
|
|
322
418
|
),
|
|
323
|
-
/* @__PURE__ */
|
|
324
|
-
/* @__PURE__ */
|
|
325
|
-
/* @__PURE__ */
|
|
419
|
+
/* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
420
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "Timeline" }),
|
|
421
|
+
/* @__PURE__ */ jsx7(
|
|
326
422
|
TimelineRow,
|
|
327
423
|
{
|
|
328
424
|
label: "Enqueued",
|
|
@@ -331,7 +427,7 @@ function JobDetailView({
|
|
|
331
427
|
nowMs
|
|
332
428
|
}
|
|
333
429
|
),
|
|
334
|
-
/* @__PURE__ */
|
|
430
|
+
/* @__PURE__ */ jsx7(
|
|
335
431
|
TimelineRow,
|
|
336
432
|
{
|
|
337
433
|
label: "Started",
|
|
@@ -340,7 +436,7 @@ function JobDetailView({
|
|
|
340
436
|
nowMs
|
|
341
437
|
}
|
|
342
438
|
),
|
|
343
|
-
/* @__PURE__ */
|
|
439
|
+
/* @__PURE__ */ jsx7(
|
|
344
440
|
TimelineRow,
|
|
345
441
|
{
|
|
346
442
|
label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
|
|
@@ -352,21 +448,21 @@ function JobDetailView({
|
|
|
352
448
|
}
|
|
353
449
|
)
|
|
354
450
|
] }),
|
|
355
|
-
/* @__PURE__ */
|
|
356
|
-
/* @__PURE__ */
|
|
451
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
452
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recording " }),
|
|
357
453
|
title,
|
|
358
|
-
item.recording?.durationMs ? /* @__PURE__ */
|
|
454
|
+
item.recording?.durationMs ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
|
|
359
455
|
] }) }),
|
|
360
|
-
/* @__PURE__ */
|
|
361
|
-
/* @__PURE__ */
|
|
362
|
-
/* @__PURE__ */
|
|
363
|
-
/* @__PURE__ */
|
|
364
|
-
/* @__PURE__ */
|
|
365
|
-
/* @__PURE__ */
|
|
366
|
-
/* @__PURE__ */
|
|
367
|
-
/* @__PURE__ */
|
|
456
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
457
|
+
/* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
|
|
458
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
|
|
459
|
+
/* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
|
|
460
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
|
|
461
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "m mac app (soon)" }),
|
|
462
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
|
|
463
|
+
/* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
|
|
368
464
|
] }) }),
|
|
369
|
-
/* @__PURE__ */
|
|
465
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
370
466
|
"esc back \xB7 t transcript",
|
|
371
467
|
item.transcriptId ? "" : " (when ready)",
|
|
372
468
|
" \xB7 q quit"
|
|
@@ -383,24 +479,24 @@ function StatusLine({
|
|
|
383
479
|
const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
|
|
384
480
|
if (fraction != null) {
|
|
385
481
|
const pct = Math.round(fraction * 100);
|
|
386
|
-
return /* @__PURE__ */
|
|
482
|
+
return /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
387
483
|
`${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
|
|
388
484
|
item.recording?.durationMs
|
|
389
485
|
)}`,
|
|
390
|
-
/* @__PURE__ */
|
|
486
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
|
|
391
487
|
] });
|
|
392
488
|
}
|
|
393
489
|
const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
|
|
394
|
-
return /* @__PURE__ */
|
|
490
|
+
return /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
395
491
|
`${spinner} transcribing\u2026`,
|
|
396
|
-
/* @__PURE__ */
|
|
492
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
|
|
397
493
|
] });
|
|
398
494
|
}
|
|
399
495
|
if (item.status === "succeeded")
|
|
400
|
-
return /* @__PURE__ */
|
|
401
|
-
if (item.status === "queued") return /* @__PURE__ */
|
|
402
|
-
if (item.status === "failed") return /* @__PURE__ */
|
|
403
|
-
return /* @__PURE__ */
|
|
496
|
+
return /* @__PURE__ */ jsx7(Text7, { children: item.transcriptId ? "transcript ready" : "done" });
|
|
497
|
+
if (item.status === "queued") return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "waiting to start\u2026" });
|
|
498
|
+
if (item.status === "failed") return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "transcription failed" });
|
|
499
|
+
return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: item.status });
|
|
404
500
|
}
|
|
405
501
|
function TimelineRow({
|
|
406
502
|
label,
|
|
@@ -413,10 +509,10 @@ function TimelineRow({
|
|
|
413
509
|
const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
|
|
414
510
|
const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
|
|
415
511
|
const age = at ? formatAge(at, nowMs) : running ? "now" : "";
|
|
416
|
-
return /* @__PURE__ */
|
|
417
|
-
/* @__PURE__ */
|
|
418
|
-
/* @__PURE__ */
|
|
419
|
-
age ? /* @__PURE__ */
|
|
512
|
+
return /* @__PURE__ */ jsxs5(Box7, { children: [
|
|
513
|
+
/* @__PURE__ */ jsx7(Text7, { color, children: ` ${glyph} ` }),
|
|
514
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: !done && !running, children: label }),
|
|
515
|
+
age ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${age}` }) : null
|
|
420
516
|
] });
|
|
421
517
|
}
|
|
422
518
|
var init_JobDetailView = __esm({
|
|
@@ -426,46 +522,111 @@ var init_JobDetailView = __esm({
|
|
|
426
522
|
}
|
|
427
523
|
});
|
|
428
524
|
|
|
525
|
+
// src/tui/RecordingDetailView.tsx
|
|
526
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
527
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
528
|
+
function RecordingDetailView({
|
|
529
|
+
item,
|
|
530
|
+
nowMs
|
|
531
|
+
}) {
|
|
532
|
+
const style = recordingStatusStyle(item.status);
|
|
533
|
+
const links = resolveRecordingLinks(item.recordingId, item.origin);
|
|
534
|
+
const title = recordingTitle2(item);
|
|
535
|
+
const meta3 = [
|
|
536
|
+
item.durationMs ? formatClockMs(item.durationMs) : void 0,
|
|
537
|
+
formatBytes2(item.sizeBytes) || void 0,
|
|
538
|
+
item.contentType || void 0
|
|
539
|
+
].filter(Boolean).join(" \xB7 ");
|
|
540
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", paddingX: 1, children: [
|
|
541
|
+
/* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
542
|
+
"\u2039 Recordings / ",
|
|
543
|
+
title
|
|
544
|
+
] }),
|
|
545
|
+
/* @__PURE__ */ jsxs6(
|
|
546
|
+
Box8,
|
|
547
|
+
{
|
|
548
|
+
marginTop: 1,
|
|
549
|
+
borderStyle: "round",
|
|
550
|
+
borderColor: style.color,
|
|
551
|
+
paddingX: 1,
|
|
552
|
+
flexDirection: "column",
|
|
553
|
+
children: [
|
|
554
|
+
/* @__PURE__ */ jsxs6(Text8, { color: style.color, bold: true, children: [
|
|
555
|
+
style.label,
|
|
556
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` created ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
|
|
557
|
+
] }),
|
|
558
|
+
meta3 ? /* @__PURE__ */ jsx8(Text8, { children: meta3 }) : null
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
),
|
|
562
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
|
|
563
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Transcript " }),
|
|
564
|
+
item.activeTranscriptId ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "ready \u2014 press t to view" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "not available yet" })
|
|
565
|
+
] }) }),
|
|
566
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
|
|
567
|
+
/* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
|
|
568
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
|
|
569
|
+
/* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
|
|
570
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
|
|
571
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "m mac app (soon)" }),
|
|
572
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
|
|
573
|
+
/* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
|
|
574
|
+
] }) }),
|
|
575
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
576
|
+
"esc back \xB7 ",
|
|
577
|
+
item.activeTranscriptId ? "t transcript \xB7 " : "",
|
|
578
|
+
"q quit"
|
|
579
|
+
] }) })
|
|
580
|
+
] });
|
|
581
|
+
}
|
|
582
|
+
var init_RecordingDetailView = __esm({
|
|
583
|
+
"src/tui/RecordingDetailView.tsx"() {
|
|
584
|
+
"use strict";
|
|
585
|
+
init_format();
|
|
586
|
+
init_RecordingRow();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
429
590
|
// src/tui/TranscriptView.tsx
|
|
430
|
-
import { Box as
|
|
431
|
-
import { jsx as
|
|
591
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
592
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
432
593
|
function TranscriptView({ loading, data, error: error51 }) {
|
|
433
594
|
if (loading) {
|
|
434
|
-
return /* @__PURE__ */
|
|
595
|
+
return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading transcript\u2026" }) });
|
|
435
596
|
}
|
|
436
597
|
if (error51) {
|
|
437
|
-
return /* @__PURE__ */
|
|
438
|
-
/* @__PURE__ */
|
|
598
|
+
return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
|
|
599
|
+
/* @__PURE__ */ jsxs7(Text9, { color: "red", children: [
|
|
439
600
|
"! ",
|
|
440
601
|
error51
|
|
441
602
|
] }),
|
|
442
|
-
/* @__PURE__ */
|
|
603
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" })
|
|
443
604
|
] });
|
|
444
605
|
}
|
|
445
606
|
if (!data) {
|
|
446
|
-
return /* @__PURE__ */
|
|
607
|
+
return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "No transcript." }) });
|
|
447
608
|
}
|
|
448
609
|
const summary = data.summary;
|
|
449
610
|
const showSummary = summary?.status === "succeeded";
|
|
450
|
-
return /* @__PURE__ */
|
|
451
|
-
/* @__PURE__ */
|
|
452
|
-
/* @__PURE__ */
|
|
453
|
-
/* @__PURE__ */
|
|
611
|
+
return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
|
|
612
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
|
|
613
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx9(Text9, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs7(Text9, { children: [
|
|
614
|
+
/* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
|
|
454
615
|
"[",
|
|
455
616
|
formatClockMs(segment.startMs),
|
|
456
617
|
"] "
|
|
457
618
|
] }),
|
|
458
|
-
segment.speaker ? /* @__PURE__ */
|
|
619
|
+
segment.speaker ? /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
|
|
459
620
|
segment.speaker,
|
|
460
621
|
": "
|
|
461
622
|
] }) : null,
|
|
462
623
|
segment.text
|
|
463
624
|
] }, index)) }),
|
|
464
|
-
showSummary && summary?.tldr ? /* @__PURE__ */
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
-
/* @__PURE__ */
|
|
625
|
+
showSummary && summary?.tldr ? /* @__PURE__ */ jsxs7(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
626
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "Summary" }),
|
|
627
|
+
/* @__PURE__ */ jsx9(Text9, { children: summary.tldr })
|
|
467
628
|
] }) : null,
|
|
468
|
-
/* @__PURE__ */
|
|
629
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" }) })
|
|
469
630
|
] });
|
|
470
631
|
}
|
|
471
632
|
var init_TranscriptView = __esm({
|
|
@@ -477,11 +638,13 @@ var init_TranscriptView = __esm({
|
|
|
477
638
|
|
|
478
639
|
// src/tui/AppShell.tsx
|
|
479
640
|
import { useCallback, useEffect, useState } from "react";
|
|
480
|
-
import { Box as
|
|
481
|
-
import { jsx as
|
|
641
|
+
import { Box as Box10, Text as Text10, useApp, useInput } from "ink";
|
|
642
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
482
643
|
function AppShell({
|
|
483
644
|
fetchJobs,
|
|
484
645
|
fetchTranscript,
|
|
646
|
+
fetchRecordings,
|
|
647
|
+
fetchDashboardStats,
|
|
485
648
|
initialView = "overview",
|
|
486
649
|
openUrl: openUrl2,
|
|
487
650
|
copyText: copyText2,
|
|
@@ -490,41 +653,48 @@ function AppShell({
|
|
|
490
653
|
spinnerMs = 80
|
|
491
654
|
}) {
|
|
492
655
|
const { exit } = useApp();
|
|
493
|
-
const [
|
|
656
|
+
const [jobs, setJobs] = useState([]);
|
|
657
|
+
const [recordings, setRecordings] = useState([]);
|
|
658
|
+
const [stats, setStats] = useState(void 0);
|
|
494
659
|
const [origin, setOrigin] = useState("");
|
|
495
|
-
const [stack, setStack] = useState([
|
|
496
|
-
{ kind: initialView === "jobs" ? "jobs" : "overview" }
|
|
497
|
-
]);
|
|
660
|
+
const [stack, setStack] = useState([{ kind: initialView }]);
|
|
498
661
|
const [selected, setSelected] = useState(0);
|
|
499
662
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
500
663
|
const [loadError, setLoadError] = useState(void 0);
|
|
501
664
|
const [notice, setNotice] = useState(void 0);
|
|
502
665
|
const screen = stack[stack.length - 1];
|
|
503
666
|
const refresh = useCallback(async () => {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
667
|
+
const [jobsR, recR, statsR] = await Promise.allSettled([
|
|
668
|
+
fetchJobs(),
|
|
669
|
+
fetchRecordings ? fetchRecordings() : Promise.resolve(void 0),
|
|
670
|
+
fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
|
|
671
|
+
]);
|
|
672
|
+
if (jobsR.status === "fulfilled") {
|
|
673
|
+
setJobs(jobsR.value.items);
|
|
674
|
+
setOrigin(jobsR.value.origin);
|
|
508
675
|
setLoadError(void 0);
|
|
509
|
-
}
|
|
510
|
-
setLoadError(
|
|
676
|
+
} else {
|
|
677
|
+
setLoadError(jobsR.reason instanceof Error ? jobsR.reason.message : String(jobsR.reason));
|
|
511
678
|
}
|
|
512
|
-
|
|
679
|
+
if (recR.status === "fulfilled" && recR.value) setRecordings(recR.value.items);
|
|
680
|
+
if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
|
|
681
|
+
}, [fetchJobs, fetchRecordings, fetchDashboardStats]);
|
|
513
682
|
useEffect(() => {
|
|
514
683
|
void refresh();
|
|
515
684
|
const id = setInterval(() => void refresh(), pollMs);
|
|
516
685
|
return () => clearInterval(id);
|
|
517
686
|
}, [refresh, pollMs]);
|
|
518
|
-
const hasRunning =
|
|
687
|
+
const hasRunning = jobs.some((item) => item.status === "running");
|
|
519
688
|
useEffect(() => {
|
|
520
689
|
if (!hasRunning) return;
|
|
521
690
|
const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
|
|
522
691
|
return () => clearInterval(id);
|
|
523
692
|
}, [hasRunning, spinnerMs]);
|
|
524
|
-
const
|
|
693
|
+
const recordingList = screen.kind === "overview" ? overviewRecentRecordings(recordings) : recordings;
|
|
694
|
+
const listLength = screen.kind === "jobs" ? jobs.length : recordingList.length;
|
|
525
695
|
useEffect(() => {
|
|
526
|
-
setSelected((i) => Math.max(0, Math.min(i, Math.max(0,
|
|
527
|
-
}, [
|
|
696
|
+
setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
|
|
697
|
+
}, [listLength]);
|
|
528
698
|
const openTranscript = useCallback(
|
|
529
699
|
async (transcriptId) => {
|
|
530
700
|
setStack((st) => [...st, { kind: "transcript", loading: true }]);
|
|
@@ -552,33 +722,46 @@ function AppShell({
|
|
|
552
722
|
const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
|
|
553
723
|
useInput((input, key) => {
|
|
554
724
|
setNotice(void 0);
|
|
555
|
-
if (input === "q")
|
|
556
|
-
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (key.escape || key.leftArrow) {
|
|
560
|
-
back();
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
725
|
+
if (input === "q") return exit();
|
|
726
|
+
if (key.escape || key.leftArrow) return back();
|
|
563
727
|
if (input === "1") return goTab("overview");
|
|
564
728
|
if (input === "2") return goTab("jobs");
|
|
565
|
-
if (input === "
|
|
566
|
-
|
|
729
|
+
if (input === "3") return goTab("recordings");
|
|
730
|
+
if (input === "r") return void refresh();
|
|
731
|
+
if (screen.kind === "overview" || screen.kind === "recordings") {
|
|
732
|
+
if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
|
|
733
|
+
if (key.downArrow || input === "j")
|
|
734
|
+
setSelected((i) => Math.min(recordingList.length - 1, i + 1));
|
|
735
|
+
const rec = recordingList[selected];
|
|
736
|
+
if (key.return && rec)
|
|
737
|
+
setStack((st) => [...st, { kind: "recordingDetail", recordingId: rec.recordingId }]);
|
|
738
|
+
if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
|
|
567
739
|
return;
|
|
568
740
|
}
|
|
569
|
-
if (screen.kind === "
|
|
741
|
+
if (screen.kind === "jobs") {
|
|
570
742
|
if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
|
|
571
|
-
if (key.downArrow || input === "j")
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
if (input === "t" && item?.transcriptId) void openTranscript(item.transcriptId);
|
|
743
|
+
if (key.downArrow || input === "j") setSelected((i) => Math.min(jobs.length - 1, i + 1));
|
|
744
|
+
const job = jobs[selected];
|
|
745
|
+
if (key.return && job) setStack((st) => [...st, { kind: "jobDetail", jobId: job.jobId }]);
|
|
746
|
+
if (input === "t" && job?.transcriptId) void openTranscript(job.transcriptId);
|
|
576
747
|
return;
|
|
577
748
|
}
|
|
578
749
|
if (screen.kind === "jobDetail") {
|
|
579
|
-
const
|
|
580
|
-
const links =
|
|
581
|
-
if (input === "t" &&
|
|
750
|
+
const job = jobs.find((j) => j.jobId === screen.jobId);
|
|
751
|
+
const links = job ? resolveJobLinks(job, origin) : {};
|
|
752
|
+
if (input === "t" && job?.transcriptId) void openTranscript(job.transcriptId);
|
|
753
|
+
else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
|
|
754
|
+
else if (input === "m") setNotice("Mac app deeplink not available yet");
|
|
755
|
+
else if (input === "c" && links.webUrl) {
|
|
756
|
+
copyText2?.(links.webUrl);
|
|
757
|
+
setNotice("Link copied");
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (screen.kind === "recordingDetail") {
|
|
762
|
+
const rec = recordings.find((r) => r.recordingId === screen.recordingId);
|
|
763
|
+
const links = rec ? resolveRecordingLinks(rec.recordingId, rec.origin) : {};
|
|
764
|
+
if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
|
|
582
765
|
else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
|
|
583
766
|
else if (input === "m") setNotice("Mac app deeplink not available yet");
|
|
584
767
|
else if (input === "c" && links.webUrl) {
|
|
@@ -589,43 +772,56 @@ function AppShell({
|
|
|
589
772
|
}
|
|
590
773
|
});
|
|
591
774
|
if (screen.kind === "transcript") {
|
|
592
|
-
return /* @__PURE__ */
|
|
775
|
+
return /* @__PURE__ */ jsx10(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
|
|
593
776
|
}
|
|
594
777
|
if (screen.kind === "jobDetail") {
|
|
595
|
-
const
|
|
596
|
-
if (!
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
return /* @__PURE__ */
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
/* @__PURE__ */ jsx7(Header, { active: tab }),
|
|
610
|
-
screen.kind === "overview" ? /* @__PURE__ */ jsx7(
|
|
778
|
+
const job = jobs.find((j) => j.jobId === screen.jobId);
|
|
779
|
+
if (!job) return /* @__PURE__ */ jsx10(Missing, { label: "Job" });
|
|
780
|
+
return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
|
|
781
|
+
}
|
|
782
|
+
if (screen.kind === "recordingDetail") {
|
|
783
|
+
const rec = recordings.find((r) => r.recordingId === screen.recordingId);
|
|
784
|
+
if (!rec) return /* @__PURE__ */ jsx10(Missing, { label: "Recording" });
|
|
785
|
+
return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(RecordingDetailView, { item: rec, nowMs: now() }) });
|
|
786
|
+
}
|
|
787
|
+
const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "recordings" ? "recordings" : "overview";
|
|
788
|
+
const footerKeys = screen.kind === "jobs" ? "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 r refresh \xB7 q quit" : "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE recording \xB7 t transcript \xB7 r refresh \xB7 q quit";
|
|
789
|
+
return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
790
|
+
/* @__PURE__ */ jsx10(Header, { active: tab }),
|
|
791
|
+
screen.kind === "overview" ? /* @__PURE__ */ jsx10(
|
|
611
792
|
OverviewView,
|
|
612
793
|
{
|
|
613
|
-
|
|
794
|
+
recordings,
|
|
795
|
+
jobs,
|
|
796
|
+
stats,
|
|
614
797
|
selectedIndex: selected,
|
|
615
798
|
spinnerFrame,
|
|
616
799
|
nowMs: now()
|
|
617
800
|
}
|
|
618
|
-
) : /* @__PURE__ */
|
|
619
|
-
loadError &&
|
|
801
|
+
) : screen.kind === "recordings" ? /* @__PURE__ */ jsx10(RecordingsView, { items: recordings, selectedIndex: selected, nowMs: now() }) : /* @__PURE__ */ jsx10(JobsView, { items: jobs, selectedIndex: selected, spinnerFrame }),
|
|
802
|
+
loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text10, { color: "red", children: [
|
|
620
803
|
"! ",
|
|
621
804
|
loadError
|
|
622
805
|
] }) }) : null,
|
|
623
|
-
/* @__PURE__ */
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
806
|
+
/* @__PURE__ */ jsx10(Footer, { keys: footerKeys })
|
|
807
|
+
] });
|
|
808
|
+
}
|
|
809
|
+
function Detail({
|
|
810
|
+
notice,
|
|
811
|
+
children
|
|
812
|
+
}) {
|
|
813
|
+
return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", children: [
|
|
814
|
+
children,
|
|
815
|
+
notice ? /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: notice }) }) : null
|
|
816
|
+
] });
|
|
817
|
+
}
|
|
818
|
+
function Missing({ label }) {
|
|
819
|
+
return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
820
|
+
/* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
|
|
821
|
+
label,
|
|
822
|
+
" no longer in the list."
|
|
823
|
+
] }),
|
|
824
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "esc back \xB7 q quit" })
|
|
629
825
|
] });
|
|
630
826
|
}
|
|
631
827
|
var init_AppShell = __esm({
|
|
@@ -634,7 +830,9 @@ var init_AppShell = __esm({
|
|
|
634
830
|
init_chrome();
|
|
635
831
|
init_JobsView();
|
|
636
832
|
init_OverviewView();
|
|
833
|
+
init_RecordingsView();
|
|
637
834
|
init_JobDetailView();
|
|
835
|
+
init_RecordingDetailView();
|
|
638
836
|
init_TranscriptView();
|
|
639
837
|
init_format();
|
|
640
838
|
}
|
|
@@ -672,6 +870,8 @@ async function runDashboard(deps) {
|
|
|
672
870
|
React2.createElement(AppShell, {
|
|
673
871
|
fetchJobs: deps.fetchJobs,
|
|
674
872
|
fetchTranscript: deps.fetchTranscript,
|
|
873
|
+
fetchRecordings: deps.fetchRecordings,
|
|
874
|
+
fetchDashboardStats: deps.fetchDashboardStats,
|
|
675
875
|
initialView: deps.initialView ?? "overview",
|
|
676
876
|
openUrl,
|
|
677
877
|
copyText
|
|
@@ -692,6 +892,7 @@ var init_tui = __esm({
|
|
|
692
892
|
|
|
693
893
|
// src/cli.ts
|
|
694
894
|
import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
|
|
895
|
+
import os3 from "os";
|
|
695
896
|
|
|
696
897
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
697
898
|
var external_exports = {};
|
|
@@ -15208,7 +15409,7 @@ function date4(params) {
|
|
|
15208
15409
|
config(en_default());
|
|
15209
15410
|
|
|
15210
15411
|
// ../../packages/contracts/src/index.ts
|
|
15211
|
-
var CLI_SCHEMA_VERSION = "2026-06-
|
|
15412
|
+
var CLI_SCHEMA_VERSION = "2026-06-25";
|
|
15212
15413
|
function toJsonSchema(schema) {
|
|
15213
15414
|
return external_exports.toJSONSchema(schema, { target: "draft-2020-12" });
|
|
15214
15415
|
}
|
|
@@ -15262,6 +15463,22 @@ var authStatusDataSchema = external_exports.object({
|
|
|
15262
15463
|
email: external_exports.string().optional(),
|
|
15263
15464
|
userId: external_exports.string().optional()
|
|
15264
15465
|
});
|
|
15466
|
+
var authLoginDataSchema = external_exports.object({
|
|
15467
|
+
loggedIn: external_exports.literal(true),
|
|
15468
|
+
origin: external_exports.string(),
|
|
15469
|
+
email: external_exports.string().optional(),
|
|
15470
|
+
userId: external_exports.string().optional()
|
|
15471
|
+
});
|
|
15472
|
+
var authLogoutDataSchema = external_exports.object({
|
|
15473
|
+
loggedIn: external_exports.literal(false),
|
|
15474
|
+
origin: external_exports.string(),
|
|
15475
|
+
cleared: external_exports.boolean()
|
|
15476
|
+
});
|
|
15477
|
+
var authImportDataSchema = external_exports.object({
|
|
15478
|
+
imported: external_exports.boolean(),
|
|
15479
|
+
origin: external_exports.string(),
|
|
15480
|
+
source: external_exports.literal("macos-keychain")
|
|
15481
|
+
});
|
|
15265
15482
|
var uploadSuccessSchema = external_exports.object({
|
|
15266
15483
|
filePath: external_exports.string(),
|
|
15267
15484
|
recordingId: external_exports.string(),
|
|
@@ -15323,6 +15540,45 @@ var jobListDataSchema = external_exports.object({
|
|
|
15323
15540
|
limit: external_exports.number().int().positive(),
|
|
15324
15541
|
origin: external_exports.string()
|
|
15325
15542
|
});
|
|
15543
|
+
var recordingDataSchema = external_exports.object({
|
|
15544
|
+
recordingId: external_exports.string(),
|
|
15545
|
+
title: external_exports.string().nullable().optional(),
|
|
15546
|
+
summaryTitle: external_exports.string().nullable().optional(),
|
|
15547
|
+
status: recordingStatusSchema,
|
|
15548
|
+
durationMs: external_exports.number().int().nonnegative().nullable().optional(),
|
|
15549
|
+
sizeBytes: external_exports.number().int().nonnegative().nullable().optional(),
|
|
15550
|
+
contentType: external_exports.string().optional(),
|
|
15551
|
+
activeTranscriptId: external_exports.string().nullable().optional(),
|
|
15552
|
+
createdAt: external_exports.number().int(),
|
|
15553
|
+
updatedAt: external_exports.number().int(),
|
|
15554
|
+
origin: external_exports.string()
|
|
15555
|
+
});
|
|
15556
|
+
var recordingListDataSchema = external_exports.object({
|
|
15557
|
+
items: external_exports.array(recordingDataSchema),
|
|
15558
|
+
limit: external_exports.number().int().positive(),
|
|
15559
|
+
nextCursor: external_exports.string().nullable().optional(),
|
|
15560
|
+
totalCount: external_exports.number().int().nonnegative().optional(),
|
|
15561
|
+
origin: external_exports.string()
|
|
15562
|
+
});
|
|
15563
|
+
var dashboardStatsDataSchema = external_exports.object({
|
|
15564
|
+
origin: external_exports.string(),
|
|
15565
|
+
recordings: external_exports.object({
|
|
15566
|
+
total: external_exports.number().int().nonnegative(),
|
|
15567
|
+
ready: external_exports.number().int().nonnegative(),
|
|
15568
|
+
uploading: external_exports.number().int().nonnegative(),
|
|
15569
|
+
failed: external_exports.number().int().nonnegative(),
|
|
15570
|
+
aborted: external_exports.number().int().nonnegative(),
|
|
15571
|
+
totalDurationMs: external_exports.number().int().nonnegative(),
|
|
15572
|
+
totalSizeBytes: external_exports.number().int().nonnegative()
|
|
15573
|
+
}),
|
|
15574
|
+
jobs: external_exports.object({
|
|
15575
|
+
active: external_exports.number().int().nonnegative(),
|
|
15576
|
+
queued: external_exports.number().int().nonnegative(),
|
|
15577
|
+
running: external_exports.number().int().nonnegative(),
|
|
15578
|
+
succeeded: external_exports.number().int().nonnegative(),
|
|
15579
|
+
failed: external_exports.number().int().nonnegative()
|
|
15580
|
+
})
|
|
15581
|
+
});
|
|
15326
15582
|
var transcriptSegmentSchema = external_exports.object({
|
|
15327
15583
|
startMs: external_exports.number().nonnegative(),
|
|
15328
15584
|
endMs: external_exports.number().nonnegative(),
|
|
@@ -15543,17 +15799,19 @@ async function resolveAuthContext(opts = {}) {
|
|
|
15543
15799
|
token: config2.token,
|
|
15544
15800
|
source: "config"
|
|
15545
15801
|
};
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15802
|
+
if (opts.includeMacOSKeychain === true) {
|
|
15803
|
+
const keychain = await inspectMacOSAppKeychain({
|
|
15804
|
+
env,
|
|
15805
|
+
platform: opts.platform
|
|
15806
|
+
});
|
|
15807
|
+
if (keychain.token) return { origin, token: keychain.token, source: "macos-keychain" };
|
|
15550
15808
|
}
|
|
15551
15809
|
return { origin, token: null, source: "none" };
|
|
15552
15810
|
}
|
|
15553
15811
|
function requireToken(ctx) {
|
|
15554
15812
|
if (!ctx.token) {
|
|
15555
15813
|
throw cliError("auth.not_logged_in", "Not logged in to Recappi.", {
|
|
15556
|
-
hint: "
|
|
15814
|
+
hint: "Run recappi auth login, or set RECAPPI_AUTH_TOKEN for automation."
|
|
15557
15815
|
});
|
|
15558
15816
|
}
|
|
15559
15817
|
return ctx.token;
|
|
@@ -15579,14 +15837,61 @@ function validateOrigin(value) {
|
|
|
15579
15837
|
}
|
|
15580
15838
|
return url2.origin;
|
|
15581
15839
|
}
|
|
15840
|
+
async function saveAuthConfig(homeDir, config2) {
|
|
15841
|
+
const target = primaryConfigPath(homeDir);
|
|
15842
|
+
await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
15843
|
+
const existing = await readConfigObject(target);
|
|
15844
|
+
const next = {
|
|
15845
|
+
...existing,
|
|
15846
|
+
origin: validateOrigin(config2.origin),
|
|
15847
|
+
authToken: config2.token
|
|
15848
|
+
};
|
|
15849
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
15850
|
+
await fs.writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
15851
|
+
`, { mode: 384 });
|
|
15852
|
+
await fs.rename(tmp, target);
|
|
15853
|
+
await fs.chmod(target, 384).catch(() => {
|
|
15854
|
+
});
|
|
15855
|
+
}
|
|
15856
|
+
async function clearAuthConfig(homeDir) {
|
|
15857
|
+
const target = primaryConfigPath(homeDir);
|
|
15858
|
+
const existing = await readConfigObject(target);
|
|
15859
|
+
if (!("authToken" in existing)) return false;
|
|
15860
|
+
delete existing.authToken;
|
|
15861
|
+
await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
15862
|
+
await fs.writeFile(target, `${JSON.stringify(existing, null, 2)}
|
|
15863
|
+
`, { mode: 384 });
|
|
15864
|
+
await fs.chmod(target, 384).catch(() => {
|
|
15865
|
+
});
|
|
15866
|
+
return true;
|
|
15867
|
+
}
|
|
15868
|
+
async function inspectMacOSAppKeychain(opts = {}) {
|
|
15869
|
+
const env = opts.env ?? process.env;
|
|
15870
|
+
if (env.RECAPPI_DISABLE_KEYCHAIN_AUTH === "1") {
|
|
15871
|
+
return {
|
|
15872
|
+
status: "disabled",
|
|
15873
|
+
token: null,
|
|
15874
|
+
message: "macOS app keychain lookup is disabled by RECAPPI_DISABLE_KEYCHAIN_AUTH=1."
|
|
15875
|
+
};
|
|
15876
|
+
}
|
|
15877
|
+
const platform = opts.platform ?? process.platform;
|
|
15878
|
+
if (platform !== "darwin") {
|
|
15879
|
+
return {
|
|
15880
|
+
status: "unsupported",
|
|
15881
|
+
token: null,
|
|
15882
|
+
message: "macOS app keychain lookup is only available on macOS."
|
|
15883
|
+
};
|
|
15884
|
+
}
|
|
15885
|
+
return readMacOSAppToken();
|
|
15886
|
+
}
|
|
15887
|
+
function primaryConfigPath(homeDir) {
|
|
15888
|
+
return path.join(homeDir, ".config", "recappi", "config.json");
|
|
15889
|
+
}
|
|
15582
15890
|
async function readConfig(homeDir) {
|
|
15583
|
-
const candidates = [
|
|
15584
|
-
path.join(homeDir, ".config", "recappi", "config.json"),
|
|
15585
|
-
path.join(homeDir, ".recappi", "config.json")
|
|
15586
|
-
];
|
|
15891
|
+
const candidates = [primaryConfigPath(homeDir), path.join(homeDir, ".recappi", "config.json")];
|
|
15587
15892
|
for (const candidate of candidates) {
|
|
15588
15893
|
try {
|
|
15589
|
-
const parsed =
|
|
15894
|
+
const parsed = await readConfigObject(candidate);
|
|
15590
15895
|
const token = typeof parsed.authToken === "string" ? parsed.authToken.trim() : void 0;
|
|
15591
15896
|
const origin = typeof parsed.origin === "string" ? parsed.origin.trim() : void 0;
|
|
15592
15897
|
if (token || origin) return { ...token ? { token } : {}, ...origin ? { origin } : {} };
|
|
@@ -15601,6 +15906,14 @@ async function readConfig(homeDir) {
|
|
|
15601
15906
|
}
|
|
15602
15907
|
return {};
|
|
15603
15908
|
}
|
|
15909
|
+
async function readConfigObject(candidate) {
|
|
15910
|
+
try {
|
|
15911
|
+
return JSON.parse(await fs.readFile(candidate, "utf8"));
|
|
15912
|
+
} catch (error51) {
|
|
15913
|
+
if (error51.code === "ENOENT") return {};
|
|
15914
|
+
throw error51;
|
|
15915
|
+
}
|
|
15916
|
+
}
|
|
15604
15917
|
async function readMacOSAppToken() {
|
|
15605
15918
|
try {
|
|
15606
15919
|
const { stdout } = await execFileAsync(
|
|
@@ -15609,12 +15922,167 @@ async function readMacOSAppToken() {
|
|
|
15609
15922
|
{ timeout: 2e3, maxBuffer: 1024 * 1024 }
|
|
15610
15923
|
);
|
|
15611
15924
|
const token = stdout.trim();
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15925
|
+
if (token.length > 0) {
|
|
15926
|
+
return {
|
|
15927
|
+
status: "ok",
|
|
15928
|
+
token,
|
|
15929
|
+
message: "Found a Recappi Mini app token in the macOS keychain."
|
|
15930
|
+
};
|
|
15931
|
+
}
|
|
15932
|
+
return {
|
|
15933
|
+
status: "missing",
|
|
15934
|
+
token: null,
|
|
15935
|
+
message: "Recappi Mini app keychain item was present but empty."
|
|
15936
|
+
};
|
|
15937
|
+
} catch (error51) {
|
|
15938
|
+
const err = error51;
|
|
15939
|
+
const timedOut = err.killed || err.signal === "SIGTERM";
|
|
15940
|
+
const stderr = typeof err.message === "string" ? err.message : "";
|
|
15941
|
+
if (!timedOut && /could not be found|The specified item could not be found/i.test(stderr)) {
|
|
15942
|
+
return {
|
|
15943
|
+
status: "missing",
|
|
15944
|
+
token: null,
|
|
15945
|
+
message: "No Recappi Mini app token was found in the macOS keychain."
|
|
15946
|
+
};
|
|
15947
|
+
}
|
|
15948
|
+
return {
|
|
15949
|
+
status: "error",
|
|
15950
|
+
token: null,
|
|
15951
|
+
message: timedOut ? "Timed out while reading the Recappi Mini app keychain token." : "Could not read the Recappi Mini app keychain token.",
|
|
15952
|
+
hint: "Use recappi auth login for the CLI, or run recappi auth import-macos explicitly if you want to import the app session."
|
|
15953
|
+
};
|
|
15615
15954
|
}
|
|
15616
15955
|
}
|
|
15617
15956
|
|
|
15957
|
+
// src/auth-login.ts
|
|
15958
|
+
import { execFile as execFile2 } from "child_process";
|
|
15959
|
+
import os2 from "os";
|
|
15960
|
+
import { promisify as promisify2 } from "util";
|
|
15961
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
15962
|
+
async function loginWithDeviceCode(opts) {
|
|
15963
|
+
const origin = validateOrigin(opts.origin);
|
|
15964
|
+
const fetchImpl = opts.deps?.fetchImpl ?? fetch;
|
|
15965
|
+
const sleep = opts.deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
15966
|
+
const start = await startDeviceAuth(origin, fetchImpl);
|
|
15967
|
+
opts.onPrompt?.(
|
|
15968
|
+
[
|
|
15969
|
+
"To sign in to Recappi CLI:",
|
|
15970
|
+
` 1. Open ${start.verificationUri}`,
|
|
15971
|
+
` 2. Enter code: ${start.userCode}`,
|
|
15972
|
+
""
|
|
15973
|
+
].join("\n")
|
|
15974
|
+
);
|
|
15975
|
+
if (!opts.noOpen) {
|
|
15976
|
+
const openUrl2 = opts.deps?.openUrl ?? openUrlWithSystemBrowser;
|
|
15977
|
+
await openUrl2(start.verificationUriComplete).catch((error51) => {
|
|
15978
|
+
opts.onPrompt?.(
|
|
15979
|
+
`Could not open the browser automatically: ${error51 instanceof Error ? error51.message : String(error51)}
|
|
15980
|
+
`
|
|
15981
|
+
);
|
|
15982
|
+
});
|
|
15983
|
+
}
|
|
15984
|
+
let intervalMs = start.interval * 1e3;
|
|
15985
|
+
const expiresAt = Date.now() + start.expiresIn * 1e3;
|
|
15986
|
+
while (Date.now() < expiresAt) {
|
|
15987
|
+
await sleep(intervalMs);
|
|
15988
|
+
const poll = await pollDeviceAuth(origin, start.deviceCode, fetchImpl);
|
|
15989
|
+
if (poll.status === "pending") {
|
|
15990
|
+
if (typeof poll.interval === "number") intervalMs = poll.interval * 1e3;
|
|
15991
|
+
continue;
|
|
15992
|
+
}
|
|
15993
|
+
if (poll.status === "slow_down") {
|
|
15994
|
+
intervalMs = (typeof poll.interval === "number" ? poll.interval : intervalMs / 1e3 + 5) * 1e3;
|
|
15995
|
+
continue;
|
|
15996
|
+
}
|
|
15997
|
+
if (poll.status === "denied") {
|
|
15998
|
+
throw cliError("auth.unauthorized", "Recappi CLI sign-in was denied.");
|
|
15999
|
+
}
|
|
16000
|
+
if (poll.status === "expired") {
|
|
16001
|
+
throw cliError("auth.unauthorized", "Recappi CLI sign-in code expired.", {
|
|
16002
|
+
hint: "Run recappi auth login again."
|
|
16003
|
+
});
|
|
16004
|
+
}
|
|
16005
|
+
if (poll.status === "authorized") {
|
|
16006
|
+
const token = typeof poll.token === "string" ? poll.token.trim() : "";
|
|
16007
|
+
if (!token) {
|
|
16008
|
+
throw cliError("cloud.invalid_response", "Recappi device auth returned no token.");
|
|
16009
|
+
}
|
|
16010
|
+
const user = isRecord(poll.user) ? poll.user : {};
|
|
16011
|
+
await saveAuthConfig(opts.homeDir ?? os2.homedir(), { origin, token });
|
|
16012
|
+
return {
|
|
16013
|
+
loggedIn: true,
|
|
16014
|
+
origin,
|
|
16015
|
+
...typeof user.email === "string" ? { email: user.email } : {},
|
|
16016
|
+
...typeof user.id === "string" ? { userId: user.id } : {}
|
|
16017
|
+
};
|
|
16018
|
+
}
|
|
16019
|
+
throw cliError("cloud.invalid_response", "Recappi device auth returned an unknown status.");
|
|
16020
|
+
}
|
|
16021
|
+
throw cliError("auth.unauthorized", "Recappi CLI sign-in timed out.", {
|
|
16022
|
+
hint: "Run recappi auth login again."
|
|
16023
|
+
});
|
|
16024
|
+
}
|
|
16025
|
+
async function startDeviceAuth(origin, fetchImpl) {
|
|
16026
|
+
const response = await fetchImpl(new URL("/api/device-auth/start", origin), {
|
|
16027
|
+
method: "POST"
|
|
16028
|
+
});
|
|
16029
|
+
if (!response.ok) {
|
|
16030
|
+
throw cliError(
|
|
16031
|
+
"cloud.http_error",
|
|
16032
|
+
`Could not start Recappi device sign-in (${response.status}).`,
|
|
16033
|
+
{
|
|
16034
|
+
retryable: response.status >= 500
|
|
16035
|
+
}
|
|
16036
|
+
);
|
|
16037
|
+
}
|
|
16038
|
+
const body = await response.json();
|
|
16039
|
+
const deviceCode = stringField(body.device_code, "device_code");
|
|
16040
|
+
const userCode = stringField(body.user_code, "user_code");
|
|
16041
|
+
const verificationUri = stringField(body.verification_uri, "verification_uri");
|
|
16042
|
+
const verificationUriComplete = stringField(
|
|
16043
|
+
body.verification_uri_complete,
|
|
16044
|
+
"verification_uri_complete"
|
|
16045
|
+
);
|
|
16046
|
+
const expiresIn = numberField(body.expires_in, "expires_in");
|
|
16047
|
+
const interval = numberField(body.interval, "interval");
|
|
16048
|
+
return { deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval };
|
|
16049
|
+
}
|
|
16050
|
+
async function pollDeviceAuth(origin, deviceCode, fetchImpl) {
|
|
16051
|
+
const response = await fetchImpl(new URL("/api/device-auth/poll", origin), {
|
|
16052
|
+
method: "POST",
|
|
16053
|
+
headers: { "content-type": "application/json" },
|
|
16054
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
16055
|
+
});
|
|
16056
|
+
if (!response.ok) {
|
|
16057
|
+
throw cliError("cloud.http_error", `Recappi device sign-in poll failed (${response.status}).`, {
|
|
16058
|
+
retryable: response.status >= 500
|
|
16059
|
+
});
|
|
16060
|
+
}
|
|
16061
|
+
return await response.json();
|
|
16062
|
+
}
|
|
16063
|
+
async function openUrlWithSystemBrowser(url2) {
|
|
16064
|
+
if (process.platform === "darwin") {
|
|
16065
|
+
await execFileAsync2("/usr/bin/open", [url2]);
|
|
16066
|
+
return;
|
|
16067
|
+
}
|
|
16068
|
+
if (process.platform === "win32") {
|
|
16069
|
+
await execFileAsync2("cmd", ["/c", "start", "", url2]);
|
|
16070
|
+
return;
|
|
16071
|
+
}
|
|
16072
|
+
await execFileAsync2("xdg-open", [url2]);
|
|
16073
|
+
}
|
|
16074
|
+
function stringField(value, name) {
|
|
16075
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
16076
|
+
throw cliError("cloud.invalid_response", `Recappi device auth response is missing ${name}.`);
|
|
16077
|
+
}
|
|
16078
|
+
function numberField(value, name) {
|
|
16079
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
16080
|
+
throw cliError("cloud.invalid_response", `Recappi device auth response is missing ${name}.`);
|
|
16081
|
+
}
|
|
16082
|
+
function isRecord(value) {
|
|
16083
|
+
return typeof value === "object" && value !== null;
|
|
16084
|
+
}
|
|
16085
|
+
|
|
15618
16086
|
// src/api.ts
|
|
15619
16087
|
import { promises as fs3 } from "fs";
|
|
15620
16088
|
|
|
@@ -15779,9 +16247,11 @@ var RecappiApiClient = class {
|
|
|
15779
16247
|
this.auth = auth;
|
|
15780
16248
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
15781
16249
|
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
16250
|
+
this.env = opts.env ?? process.env;
|
|
15782
16251
|
}
|
|
15783
16252
|
fetchImpl;
|
|
15784
16253
|
sleep;
|
|
16254
|
+
env;
|
|
15785
16255
|
async authStatus() {
|
|
15786
16256
|
if (!this.auth.token) {
|
|
15787
16257
|
return { loggedIn: false, origin: this.auth.origin };
|
|
@@ -15793,7 +16263,7 @@ var RecappiApiClient = class {
|
|
|
15793
16263
|
return { loggedIn: false, origin: this.auth.origin };
|
|
15794
16264
|
}
|
|
15795
16265
|
const body = await parseJson(response);
|
|
15796
|
-
const user =
|
|
16266
|
+
const user = isRecord2(body) && isRecord2(body.user) ? body.user : void 0;
|
|
15797
16267
|
return {
|
|
15798
16268
|
loggedIn: Boolean(user),
|
|
15799
16269
|
origin: this.auth.origin,
|
|
@@ -15820,7 +16290,7 @@ var RecappiApiClient = class {
|
|
|
15820
16290
|
status: this.auth.token ? "ok" : "error",
|
|
15821
16291
|
message: this.auth.token ? `Found auth token from ${this.auth.source}.` : "No Recappi auth token found.",
|
|
15822
16292
|
...this.auth.token ? {} : {
|
|
15823
|
-
hint: "
|
|
16293
|
+
hint: "Run recappi auth login, or set RECAPPI_AUTH_TOKEN for automation."
|
|
15824
16294
|
}
|
|
15825
16295
|
});
|
|
15826
16296
|
if (this.auth.token) {
|
|
@@ -15830,7 +16300,9 @@ var RecappiApiClient = class {
|
|
|
15830
16300
|
name: "auth.session",
|
|
15831
16301
|
status: status2.loggedIn ? "ok" : "error",
|
|
15832
16302
|
message: status2.loggedIn ? `Cloud session is valid${status2.email ? ` for ${status2.email}` : ""}.` : "Cloud did not accept the configured auth token.",
|
|
15833
|
-
...status2.loggedIn ? {} : {
|
|
16303
|
+
...status2.loggedIn ? {} : {
|
|
16304
|
+
hint: `Run recappi auth login again. If you use --origin, make sure the token belongs to ${this.auth.origin}.`
|
|
16305
|
+
}
|
|
15834
16306
|
});
|
|
15835
16307
|
} catch (error51) {
|
|
15836
16308
|
const cli = toCliError(error51);
|
|
@@ -15842,6 +16314,13 @@ var RecappiApiClient = class {
|
|
|
15842
16314
|
});
|
|
15843
16315
|
}
|
|
15844
16316
|
}
|
|
16317
|
+
const keychain = await inspectMacOSAppKeychain({ env: this.env });
|
|
16318
|
+
checks.push({
|
|
16319
|
+
name: "auth.macos_keychain",
|
|
16320
|
+
status: keychain.status === "error" ? "warn" : "ok",
|
|
16321
|
+
message: keychain.message,
|
|
16322
|
+
...keychain.status === "ok" ? { hint: "Run recappi auth import-macos to copy this app session into CLI config." } : keychain.hint ? { hint: keychain.hint } : {}
|
|
16323
|
+
});
|
|
15845
16324
|
checks.push({
|
|
15846
16325
|
name: "audio.wav",
|
|
15847
16326
|
status: "ok",
|
|
@@ -15882,7 +16361,7 @@ var RecappiApiClient = class {
|
|
|
15882
16361
|
limit: String(opts.limit)
|
|
15883
16362
|
});
|
|
15884
16363
|
const parsed = await this.getJson(`/api/jobs?${params}`);
|
|
15885
|
-
const items = Array.isArray(parsed.items) ? parsed.items.filter(
|
|
16364
|
+
const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord2).map(mapJobListItem) : [];
|
|
15886
16365
|
return jobListDataSchema.parse({
|
|
15887
16366
|
items,
|
|
15888
16367
|
status: typeof parsed.status === "string" ? parsed.status : opts.status,
|
|
@@ -15890,6 +16369,30 @@ var RecappiApiClient = class {
|
|
|
15890
16369
|
origin: this.auth.origin
|
|
15891
16370
|
});
|
|
15892
16371
|
}
|
|
16372
|
+
async listRecordings(opts) {
|
|
16373
|
+
const params = new URLSearchParams({ limit: String(opts.limit) });
|
|
16374
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
16375
|
+
if (opts.search) params.set("search", opts.search);
|
|
16376
|
+
const parsed = await this.getJson(`/api/recordings?${params}`);
|
|
16377
|
+
const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord2).map((row) => mapRecording(row, this.auth.origin)) : [];
|
|
16378
|
+
return recordingListDataSchema.parse({
|
|
16379
|
+
items,
|
|
16380
|
+
limit: opts.limit,
|
|
16381
|
+
...typeof parsed.nextCursor === "string" || parsed.nextCursor === null ? { nextCursor: parsed.nextCursor } : {},
|
|
16382
|
+
...typeof parsed.totalCount === "number" ? { totalCount: parsed.totalCount } : {},
|
|
16383
|
+
origin: this.auth.origin
|
|
16384
|
+
});
|
|
16385
|
+
}
|
|
16386
|
+
async getRecording(recordingId) {
|
|
16387
|
+
const parsed = await this.getJson(
|
|
16388
|
+
`/api/recordings/${encodeURIComponent(recordingId)}`
|
|
16389
|
+
);
|
|
16390
|
+
return mapRecording(parsed, this.auth.origin);
|
|
16391
|
+
}
|
|
16392
|
+
async dashboardStats() {
|
|
16393
|
+
const parsed = await this.getJson("/api/dashboard/stats");
|
|
16394
|
+
return mapDashboardStats(parsed, this.auth.origin);
|
|
16395
|
+
}
|
|
15893
16396
|
async uploadPathBatch(opts) {
|
|
15894
16397
|
const files = await collectAudioFiles(opts.inputPath);
|
|
15895
16398
|
if (files.length === 0) {
|
|
@@ -16035,7 +16538,7 @@ var RecappiApiClient = class {
|
|
|
16035
16538
|
}
|
|
16036
16539
|
);
|
|
16037
16540
|
const parsed = await parseJson(response);
|
|
16038
|
-
if (!
|
|
16541
|
+
if (!isRecord2(parsed) || typeof parsed.etag !== "string") {
|
|
16039
16542
|
throw cliError("cloud.invalid_response", "Upload part response was missing etag.");
|
|
16040
16543
|
}
|
|
16041
16544
|
parts.push({ partNumber, etag: parsed.etag });
|
|
@@ -16107,8 +16610,8 @@ async function parseJson(response) {
|
|
|
16107
16610
|
async function responseMessage(response) {
|
|
16108
16611
|
try {
|
|
16109
16612
|
const parsed = await response.clone().json();
|
|
16110
|
-
if (
|
|
16111
|
-
if (
|
|
16613
|
+
if (isRecord2(parsed) && typeof parsed.message === "string") return parsed.message;
|
|
16614
|
+
if (isRecord2(parsed) && typeof parsed.error === "string") return parsed.error;
|
|
16112
16615
|
} catch {
|
|
16113
16616
|
}
|
|
16114
16617
|
try {
|
|
@@ -16118,7 +16621,7 @@ async function responseMessage(response) {
|
|
|
16118
16621
|
return response.statusText;
|
|
16119
16622
|
}
|
|
16120
16623
|
}
|
|
16121
|
-
function
|
|
16624
|
+
function isRecord2(value) {
|
|
16122
16625
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16123
16626
|
}
|
|
16124
16627
|
function mapTranscript(row) {
|
|
@@ -16162,7 +16665,7 @@ function requiredNumber(row, key) {
|
|
|
16162
16665
|
}
|
|
16163
16666
|
function parseSegments(value, text, durationMs) {
|
|
16164
16667
|
const decoded = decodeJsonArray(value);
|
|
16165
|
-
const rawSegments = decoded.filter(
|
|
16668
|
+
const rawSegments = decoded.filter(isRecord2).map((segment) => {
|
|
16166
16669
|
const segmentText = typeof segment.text === "string" ? segment.text.trim() : "";
|
|
16167
16670
|
if (!segmentText) return null;
|
|
16168
16671
|
const start = typeof segment.start === "number" && segment.start >= 0 ? segment.start : 0;
|
|
@@ -16213,12 +16716,12 @@ function parseSummary(row) {
|
|
|
16213
16716
|
if (strings.length > 0) summary[key] = strings;
|
|
16214
16717
|
}
|
|
16215
16718
|
}
|
|
16216
|
-
const actionItems = Array.isArray(payload?.actionItems) ? payload.actionItems.filter(
|
|
16719
|
+
const actionItems = Array.isArray(payload?.actionItems) ? payload.actionItems.filter(isRecord2).flatMap((item) => {
|
|
16217
16720
|
if (typeof item.what !== "string" || !item.what.trim()) return [];
|
|
16218
16721
|
return [{ what: item.what, ...typeof item.who === "string" ? { who: item.who } : {} }];
|
|
16219
16722
|
}) : [];
|
|
16220
16723
|
if (actionItems.length > 0) summary.actionItems = actionItems;
|
|
16221
|
-
const quotes = Array.isArray(payload?.quotes) ? payload.quotes.filter(
|
|
16724
|
+
const quotes = Array.isArray(payload?.quotes) ? payload.quotes.filter(isRecord2).flatMap((item) => {
|
|
16222
16725
|
if (typeof item.text !== "string" || !item.text.trim()) return [];
|
|
16223
16726
|
return [
|
|
16224
16727
|
{
|
|
@@ -16228,7 +16731,7 @@ function parseSummary(row) {
|
|
|
16228
16731
|
];
|
|
16229
16732
|
}) : [];
|
|
16230
16733
|
if (quotes.length > 0) summary.quotes = quotes;
|
|
16231
|
-
const timeline = Array.isArray(payload?.timeline) ? payload.timeline.filter(
|
|
16734
|
+
const timeline = Array.isArray(payload?.timeline) ? payload.timeline.filter(isRecord2).flatMap((item) => {
|
|
16232
16735
|
if (typeof item.startMs !== "number" || typeof item.endMs !== "number" || typeof item.title !== "string" || typeof item.summary !== "string") {
|
|
16233
16736
|
return [];
|
|
16234
16737
|
}
|
|
@@ -16248,7 +16751,7 @@ function parseSummary(row) {
|
|
|
16248
16751
|
return summary;
|
|
16249
16752
|
}
|
|
16250
16753
|
function mapJobListItem(row) {
|
|
16251
|
-
const recording =
|
|
16754
|
+
const recording = isRecord2(row.recording) ? row.recording : {};
|
|
16252
16755
|
return {
|
|
16253
16756
|
jobId: stringValue(row.jobId) ?? stringValue(row.id) ?? "",
|
|
16254
16757
|
recordingId: stringValue(row.recordingId) ?? "",
|
|
@@ -16270,9 +16773,59 @@ function mapJobListItem(row) {
|
|
|
16270
16773
|
}
|
|
16271
16774
|
};
|
|
16272
16775
|
}
|
|
16776
|
+
function mapRecording(row, origin) {
|
|
16777
|
+
const recordingId = stringValue(row.id) ?? stringValue(row.recordingId);
|
|
16778
|
+
const status = stringValue(row.status);
|
|
16779
|
+
const createdAt = numberValue(row.createdAt);
|
|
16780
|
+
const updatedAt = numberValue(row.updatedAt);
|
|
16781
|
+
if (!recordingId) {
|
|
16782
|
+
throw cliError("cloud.invalid_response", "Recording response was missing id.");
|
|
16783
|
+
}
|
|
16784
|
+
if (!status) {
|
|
16785
|
+
throw cliError("cloud.invalid_response", "Recording response was missing status.");
|
|
16786
|
+
}
|
|
16787
|
+
if (createdAt === void 0 || updatedAt === void 0) {
|
|
16788
|
+
throw cliError("cloud.invalid_response", "Recording response was missing timestamps.");
|
|
16789
|
+
}
|
|
16790
|
+
return recordingDataSchema.parse({
|
|
16791
|
+
recordingId,
|
|
16792
|
+
...typeof row.title === "string" || row.title === null ? { title: row.title } : {},
|
|
16793
|
+
...typeof row.summaryTitle === "string" || row.summaryTitle === null ? { summaryTitle: row.summaryTitle } : {},
|
|
16794
|
+
status,
|
|
16795
|
+
...typeof row.durationMs === "number" || row.durationMs === null ? { durationMs: row.durationMs } : {},
|
|
16796
|
+
...typeof row.sizeBytes === "number" || row.sizeBytes === null ? { sizeBytes: row.sizeBytes } : {},
|
|
16797
|
+
...typeof row.contentType === "string" ? { contentType: row.contentType } : {},
|
|
16798
|
+
...typeof row.activeTranscriptId === "string" || row.activeTranscriptId === null ? { activeTranscriptId: row.activeTranscriptId } : {},
|
|
16799
|
+
createdAt,
|
|
16800
|
+
updatedAt,
|
|
16801
|
+
origin
|
|
16802
|
+
});
|
|
16803
|
+
}
|
|
16804
|
+
function mapDashboardStats(row, origin) {
|
|
16805
|
+
return dashboardStatsDataSchema.parse({
|
|
16806
|
+
origin,
|
|
16807
|
+
recordings: mapCountObject(row.recordings, [
|
|
16808
|
+
"total",
|
|
16809
|
+
"ready",
|
|
16810
|
+
"uploading",
|
|
16811
|
+
"failed",
|
|
16812
|
+
"aborted",
|
|
16813
|
+
"totalDurationMs",
|
|
16814
|
+
"totalSizeBytes"
|
|
16815
|
+
]),
|
|
16816
|
+
jobs: mapCountObject(row.jobs, ["active", "queued", "running", "succeeded", "failed"])
|
|
16817
|
+
});
|
|
16818
|
+
}
|
|
16819
|
+
function mapCountObject(value, keys) {
|
|
16820
|
+
const source = isRecord2(value) ? value : {};
|
|
16821
|
+
return Object.fromEntries(keys.map((key) => [key, numberValue(source[key]) ?? 0]));
|
|
16822
|
+
}
|
|
16273
16823
|
function stringValue(value) {
|
|
16274
16824
|
return typeof value === "string" ? value : void 0;
|
|
16275
16825
|
}
|
|
16826
|
+
function numberValue(value) {
|
|
16827
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
16828
|
+
}
|
|
16276
16829
|
function parseSummaryStatus(value) {
|
|
16277
16830
|
const allowed = /* @__PURE__ */ new Set([
|
|
16278
16831
|
"pending",
|
|
@@ -16295,11 +16848,11 @@ function decodeJsonArray(value) {
|
|
|
16295
16848
|
}
|
|
16296
16849
|
}
|
|
16297
16850
|
function decodeJsonRecord(value) {
|
|
16298
|
-
if (
|
|
16851
|
+
if (isRecord2(value)) return value;
|
|
16299
16852
|
if (typeof value !== "string" || !value.trim()) return null;
|
|
16300
16853
|
try {
|
|
16301
16854
|
const decoded = JSON.parse(value);
|
|
16302
|
-
return
|
|
16855
|
+
return isRecord2(decoded) ? decoded : null;
|
|
16303
16856
|
} catch {
|
|
16304
16857
|
return null;
|
|
16305
16858
|
}
|
|
@@ -16385,7 +16938,20 @@ function renderEnvelope(envelope, opts) {
|
|
|
16385
16938
|
`);
|
|
16386
16939
|
}
|
|
16387
16940
|
function renderHumanSuccess(command, data, opts) {
|
|
16388
|
-
if (command === "auth
|
|
16941
|
+
if (command === "auth login" && isRecord3(data)) {
|
|
16942
|
+
opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
|
|
16943
|
+
`);
|
|
16944
|
+
return;
|
|
16945
|
+
}
|
|
16946
|
+
if (command === "auth logout" && isRecord3(data)) {
|
|
16947
|
+
opts.stdout(data.cleared ? "Signed out of Recappi CLI\n" : "No Recappi CLI session to clear\n");
|
|
16948
|
+
return;
|
|
16949
|
+
}
|
|
16950
|
+
if (command === "auth import-macos" && isRecord3(data)) {
|
|
16951
|
+
opts.stdout("Imported the Recappi Mini app session into Recappi CLI\n");
|
|
16952
|
+
return;
|
|
16953
|
+
}
|
|
16954
|
+
if (command === "auth status" && isRecord3(data)) {
|
|
16389
16955
|
if (data.loggedIn) {
|
|
16390
16956
|
opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
|
|
16391
16957
|
`);
|
|
@@ -16394,17 +16960,17 @@ function renderHumanSuccess(command, data, opts) {
|
|
|
16394
16960
|
opts.stdout("Not logged in\n");
|
|
16395
16961
|
return;
|
|
16396
16962
|
}
|
|
16397
|
-
if (command === "version" &&
|
|
16963
|
+
if (command === "version" && isRecord3(data) && typeof data.version === "string") {
|
|
16398
16964
|
opts.stdout(`${data.version}
|
|
16399
16965
|
`);
|
|
16400
16966
|
return;
|
|
16401
16967
|
}
|
|
16402
|
-
if (command === "doctor" &&
|
|
16968
|
+
if (command === "doctor" && isRecord3(data) && Array.isArray(data.checks)) {
|
|
16403
16969
|
const status = typeof data.status === "string" ? data.status : "unknown";
|
|
16404
16970
|
opts.stdout(`Doctor: ${status}
|
|
16405
16971
|
`);
|
|
16406
16972
|
for (const check2 of data.checks) {
|
|
16407
|
-
if (!
|
|
16973
|
+
if (!isRecord3(check2)) continue;
|
|
16408
16974
|
const checkStatus = typeof check2.status === "string" ? check2.status : "unknown";
|
|
16409
16975
|
const name = typeof check2.name === "string" ? check2.name : "check";
|
|
16410
16976
|
const message = typeof check2.message === "string" ? ` \u2014 ${check2.message}` : "";
|
|
@@ -16415,10 +16981,60 @@ function renderHumanSuccess(command, data, opts) {
|
|
|
16415
16981
|
}
|
|
16416
16982
|
return;
|
|
16417
16983
|
}
|
|
16418
|
-
if (command === "transcript get" &&
|
|
16984
|
+
if (command === "transcript get" && isRecord3(data)) {
|
|
16419
16985
|
renderTranscriptHuman(data, opts);
|
|
16420
16986
|
return;
|
|
16421
16987
|
}
|
|
16988
|
+
if (command === "recordings list" && isRecord3(data) && Array.isArray(data.items)) {
|
|
16989
|
+
opts.stdout("Recordings:\n");
|
|
16990
|
+
for (const item of data.items) {
|
|
16991
|
+
if (!isRecord3(item)) continue;
|
|
16992
|
+
opts.stdout(` ${recordingLabel(item)}
|
|
16993
|
+
`);
|
|
16994
|
+
}
|
|
16995
|
+
if (data.items.length === 0) opts.stdout(" No recordings found.\n");
|
|
16996
|
+
if (typeof data.nextCursor === "string") {
|
|
16997
|
+
opts.stdout(`
|
|
16998
|
+
Next cursor: ${data.nextCursor}
|
|
16999
|
+
`);
|
|
17000
|
+
}
|
|
17001
|
+
return;
|
|
17002
|
+
}
|
|
17003
|
+
if (command === "recordings get" && isRecord3(data)) {
|
|
17004
|
+
opts.stdout(`${recordingTitle(data)}
|
|
17005
|
+
`);
|
|
17006
|
+
opts.stdout(` recordingId: ${String(data.recordingId)}
|
|
17007
|
+
`);
|
|
17008
|
+
if (typeof data.status === "string") opts.stdout(` status: ${data.status}
|
|
17009
|
+
`);
|
|
17010
|
+
if (typeof data.durationMs === "number")
|
|
17011
|
+
opts.stdout(` duration: ${formatDurationMs(data.durationMs)}
|
|
17012
|
+
`);
|
|
17013
|
+
if (typeof data.sizeBytes === "number") opts.stdout(` size: ${formatBytes(data.sizeBytes)}
|
|
17014
|
+
`);
|
|
17015
|
+
if (typeof data.activeTranscriptId === "string") {
|
|
17016
|
+
opts.stdout(` activeTranscriptId: ${data.activeTranscriptId}
|
|
17017
|
+
`);
|
|
17018
|
+
opts.stdout(`
|
|
17019
|
+
Next:
|
|
17020
|
+
recappi transcript get ${data.activeTranscriptId}
|
|
17021
|
+
`);
|
|
17022
|
+
}
|
|
17023
|
+
return;
|
|
17024
|
+
}
|
|
17025
|
+
if (command === "dashboard stats" && isRecord3(data)) {
|
|
17026
|
+
const recordings = isRecord3(data.recordings) ? data.recordings : {};
|
|
17027
|
+
const jobs = isRecord3(data.jobs) ? data.jobs : {};
|
|
17028
|
+
opts.stdout(
|
|
17029
|
+
`Recordings: ${numberText(recordings.total)} total, ${numberText(recordings.ready)} ready
|
|
17030
|
+
`
|
|
17031
|
+
);
|
|
17032
|
+
opts.stdout(
|
|
17033
|
+
`Jobs: ${numberText(jobs.active)} active (${numberText(jobs.queued)} queued, ${numberText(jobs.running)} running)
|
|
17034
|
+
`
|
|
17035
|
+
);
|
|
17036
|
+
return;
|
|
17037
|
+
}
|
|
16422
17038
|
if (command === "upload" && isUploadBatch(data)) {
|
|
16423
17039
|
if (data.successes.length > 0) {
|
|
16424
17040
|
opts.stdout(data.successes.length === 1 ? "Upload complete\n" : "Uploads complete\n");
|
|
@@ -16448,7 +17064,7 @@ Next:
|
|
|
16448
17064
|
}
|
|
16449
17065
|
return;
|
|
16450
17066
|
}
|
|
16451
|
-
if ((command === "jobs wait" || command === "upload") &&
|
|
17067
|
+
if ((command === "jobs wait" || command === "upload") && isRecord3(data)) {
|
|
16452
17068
|
if (typeof data.transcriptId === "string") {
|
|
16453
17069
|
opts.stdout("Transcription ready\n");
|
|
16454
17070
|
opts.stdout(` transcriptId: ${data.transcriptId}
|
|
@@ -16469,10 +17085,10 @@ Next:
|
|
|
16469
17085
|
}
|
|
16470
17086
|
return;
|
|
16471
17087
|
}
|
|
16472
|
-
if (command === "schema" &&
|
|
17088
|
+
if (command === "schema" && isRecord3(data) && Array.isArray(data.commands)) {
|
|
16473
17089
|
opts.stdout("Commands:\n");
|
|
16474
17090
|
for (const entry of data.commands) {
|
|
16475
|
-
if (!
|
|
17091
|
+
if (!isRecord3(entry) || typeof entry.name !== "string") continue;
|
|
16476
17092
|
const summary = typeof entry.summary === "string" ? ` \u2014 ${entry.summary}` : "";
|
|
16477
17093
|
opts.stdout(` ${entry.name}${summary}
|
|
16478
17094
|
`);
|
|
@@ -16562,7 +17178,7 @@ function renderTranscriptHuman(data, opts) {
|
|
|
16562
17178
|
const segments = Array.isArray(data.segments) ? data.segments : [];
|
|
16563
17179
|
let printedBody = false;
|
|
16564
17180
|
for (const segment of segments) {
|
|
16565
|
-
if (!
|
|
17181
|
+
if (!isRecord3(segment) || typeof segment.text !== "string") continue;
|
|
16566
17182
|
const clock = typeof segment.startMs === "number" ? `[${formatClock(segment.startMs / 1e3)}] ` : "";
|
|
16567
17183
|
const speaker = typeof segment.speaker === "string" ? `${segment.speaker}: ` : "";
|
|
16568
17184
|
opts.stdout(`${clock}${speaker}${segment.text}
|
|
@@ -16574,7 +17190,7 @@ function renderTranscriptHuman(data, opts) {
|
|
|
16574
17190
|
`);
|
|
16575
17191
|
printedBody = true;
|
|
16576
17192
|
}
|
|
16577
|
-
const summary =
|
|
17193
|
+
const summary = isRecord3(data.summary) ? data.summary : void 0;
|
|
16578
17194
|
if (!summary || summary.status !== "succeeded") return;
|
|
16579
17195
|
if (typeof summary.tldr === "string" && summary.tldr.length > 0) {
|
|
16580
17196
|
opts.stdout(`
|
|
@@ -16592,13 +17208,44 @@ Summary:
|
|
|
16592
17208
|
if (Array.isArray(summary.actionItems) && summary.actionItems.length > 0) {
|
|
16593
17209
|
opts.stdout("\nAction items:\n");
|
|
16594
17210
|
for (const item of summary.actionItems) {
|
|
16595
|
-
if (!
|
|
17211
|
+
if (!isRecord3(item) || typeof item.what !== "string") continue;
|
|
16596
17212
|
const who = typeof item.who === "string" ? `${item.who}: ` : "";
|
|
16597
17213
|
opts.stdout(` - ${who}${item.what}
|
|
16598
17214
|
`);
|
|
16599
17215
|
}
|
|
16600
17216
|
}
|
|
16601
17217
|
}
|
|
17218
|
+
function recordingLabel(item) {
|
|
17219
|
+
const id = typeof item.recordingId === "string" ? item.recordingId : "unknown";
|
|
17220
|
+
const status = typeof item.status === "string" ? item.status : "unknown";
|
|
17221
|
+
const duration3 = typeof item.durationMs === "number" ? ` \xB7 ${formatDurationMs(item.durationMs)}` : "";
|
|
17222
|
+
return `${recordingTitle(item)} (${status}, ${id}${duration3})`;
|
|
17223
|
+
}
|
|
17224
|
+
function recordingTitle(item) {
|
|
17225
|
+
for (const key of ["title", "summaryTitle"]) {
|
|
17226
|
+
const value = item[key];
|
|
17227
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
17228
|
+
}
|
|
17229
|
+
return "Untitled recording";
|
|
17230
|
+
}
|
|
17231
|
+
function numberText(value) {
|
|
17232
|
+
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "0";
|
|
17233
|
+
}
|
|
17234
|
+
function formatDurationMs(ms) {
|
|
17235
|
+
return formatClock(ms / 1e3);
|
|
17236
|
+
}
|
|
17237
|
+
function formatBytes(bytes) {
|
|
17238
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
17239
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
17240
|
+
let value = bytes / 1024;
|
|
17241
|
+
let unit = units[0];
|
|
17242
|
+
for (const next of units.slice(1)) {
|
|
17243
|
+
if (value < 1024) break;
|
|
17244
|
+
value /= 1024;
|
|
17245
|
+
unit = next;
|
|
17246
|
+
}
|
|
17247
|
+
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
|
|
17248
|
+
}
|
|
16602
17249
|
function formatClock(seconds) {
|
|
16603
17250
|
const total = Math.max(0, Math.floor(seconds));
|
|
16604
17251
|
const hours = Math.floor(total / 3600);
|
|
@@ -16627,7 +17274,7 @@ function applyFields(command, data, fields, compact) {
|
|
|
16627
17274
|
};
|
|
16628
17275
|
return compact ? compactData(filtered2) : filtered2;
|
|
16629
17276
|
}
|
|
16630
|
-
if (!
|
|
17277
|
+
if (!isRecord3(data)) return data;
|
|
16631
17278
|
const allowed = new Set(Object.keys(data));
|
|
16632
17279
|
assertKnownFields(fields, allowed);
|
|
16633
17280
|
const filtered = pickFields(data, fields);
|
|
@@ -16652,7 +17299,7 @@ function compactData(value) {
|
|
|
16652
17299
|
if (Array.isArray(value)) {
|
|
16653
17300
|
return value.map(compactData).filter((item) => item !== void 0);
|
|
16654
17301
|
}
|
|
16655
|
-
if (
|
|
17302
|
+
if (isRecord3(value)) {
|
|
16656
17303
|
const out = {};
|
|
16657
17304
|
for (const [key, child] of Object.entries(value)) {
|
|
16658
17305
|
const compacted = compactData(child);
|
|
@@ -16670,23 +17317,29 @@ function stableStringify(value, compact) {
|
|
|
16670
17317
|
}
|
|
16671
17318
|
function sortKeys(value) {
|
|
16672
17319
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
16673
|
-
if (!
|
|
17320
|
+
if (!isRecord3(value)) return value;
|
|
16674
17321
|
return Object.fromEntries(
|
|
16675
17322
|
Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
|
|
16676
17323
|
);
|
|
16677
17324
|
}
|
|
16678
|
-
function
|
|
17325
|
+
function isRecord3(value) {
|
|
16679
17326
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16680
17327
|
}
|
|
16681
17328
|
function isUploadBatch(value) {
|
|
16682
|
-
return
|
|
17329
|
+
return isRecord3(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
|
|
16683
17330
|
}
|
|
16684
17331
|
|
|
16685
17332
|
// src/schema.ts
|
|
16686
17333
|
var COMMAND_DATA_SCHEMAS = {
|
|
17334
|
+
"auth login": authLoginDataSchema,
|
|
17335
|
+
"auth logout": authLogoutDataSchema,
|
|
17336
|
+
"auth import-macos": authImportDataSchema,
|
|
16687
17337
|
"auth status": authStatusDataSchema,
|
|
16688
17338
|
doctor: doctorDataSchema,
|
|
17339
|
+
"dashboard stats": dashboardStatsDataSchema,
|
|
16689
17340
|
upload: uploadBatchDataSchema,
|
|
17341
|
+
"recordings get": recordingDataSchema,
|
|
17342
|
+
"recordings list": recordingListDataSchema,
|
|
16690
17343
|
"jobs list": jobListDataSchema,
|
|
16691
17344
|
"jobs wait": jobDataSchema,
|
|
16692
17345
|
"transcript get": transcriptDataSchema
|
|
@@ -16814,12 +17467,22 @@ async function runCli(deps = {}) {
|
|
|
16814
17467
|
renderSuccess("version", { version: CLI_VERSION }, render2);
|
|
16815
17468
|
return 0;
|
|
16816
17469
|
}
|
|
16817
|
-
const auth = await resolveAuthContext({
|
|
16818
|
-
|
|
17470
|
+
const auth = await resolveAuthContext({
|
|
17471
|
+
origin: parsed.options.origin,
|
|
17472
|
+
env: deps.env,
|
|
17473
|
+
homeDir: deps.homeDir
|
|
17474
|
+
});
|
|
17475
|
+
const client = new RecappiApiClient(auth, {
|
|
17476
|
+
fetchImpl: deps.fetchImpl,
|
|
17477
|
+
sleep: deps.sleep,
|
|
17478
|
+
env: deps.env
|
|
17479
|
+
});
|
|
16819
17480
|
if (parsed.kind === "dashboard") {
|
|
16820
17481
|
const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
|
|
16821
17482
|
await runDashboard2({
|
|
16822
17483
|
fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
|
|
17484
|
+
fetchRecordings: () => client.listRecordings({ limit: 20 }),
|
|
17485
|
+
fetchDashboardStats: () => client.dashboardStats(),
|
|
16823
17486
|
fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
|
|
16824
17487
|
initialView: parsed.initialView
|
|
16825
17488
|
});
|
|
@@ -16830,6 +17493,44 @@ async function runCli(deps = {}) {
|
|
|
16830
17493
|
renderSuccess("auth status", data, render2);
|
|
16831
17494
|
return data.loggedIn ? 0 : 3;
|
|
16832
17495
|
}
|
|
17496
|
+
if (parsed.kind === "auth-login") {
|
|
17497
|
+
const data = await loginWithDeviceCode({
|
|
17498
|
+
origin: auth.origin,
|
|
17499
|
+
homeDir: deps.homeDir,
|
|
17500
|
+
noOpen: parsed.noOpen,
|
|
17501
|
+
onPrompt: (message) => stderr(message),
|
|
17502
|
+
deps: {
|
|
17503
|
+
fetchImpl: deps.fetchImpl,
|
|
17504
|
+
openUrl: deps.openUrl,
|
|
17505
|
+
sleep: deps.sleep
|
|
17506
|
+
}
|
|
17507
|
+
});
|
|
17508
|
+
renderSuccess("auth login", data, render2);
|
|
17509
|
+
return 0;
|
|
17510
|
+
}
|
|
17511
|
+
if (parsed.kind === "auth-logout") {
|
|
17512
|
+
const cleared = await clearAuthConfig(deps.homeDir ?? os3.homedir());
|
|
17513
|
+
renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render2);
|
|
17514
|
+
return 0;
|
|
17515
|
+
}
|
|
17516
|
+
if (parsed.kind === "auth-import-macos") {
|
|
17517
|
+
const keychain = await inspectMacOSAppKeychain({ env: deps.env });
|
|
17518
|
+
if (!keychain.token) {
|
|
17519
|
+
throw cliError("auth.not_logged_in", keychain.message, {
|
|
17520
|
+
hint: keychain.hint ?? "Run recappi auth login instead."
|
|
17521
|
+
});
|
|
17522
|
+
}
|
|
17523
|
+
await saveAuthConfig(deps.homeDir ?? os3.homedir(), {
|
|
17524
|
+
origin: auth.origin,
|
|
17525
|
+
token: keychain.token
|
|
17526
|
+
});
|
|
17527
|
+
renderSuccess(
|
|
17528
|
+
"auth import-macos",
|
|
17529
|
+
{ imported: true, origin: auth.origin, source: "macos-keychain" },
|
|
17530
|
+
render2
|
|
17531
|
+
);
|
|
17532
|
+
return 0;
|
|
17533
|
+
}
|
|
16833
17534
|
if (parsed.kind === "doctor") {
|
|
16834
17535
|
const data = await client.doctor();
|
|
16835
17536
|
renderSuccess("doctor", data, render2);
|
|
@@ -16879,6 +17580,25 @@ async function runCli(deps = {}) {
|
|
|
16879
17580
|
renderSuccess("jobs list", data, render2);
|
|
16880
17581
|
return 0;
|
|
16881
17582
|
}
|
|
17583
|
+
if (parsed.kind === "recordings-list") {
|
|
17584
|
+
const data = await client.listRecordings({
|
|
17585
|
+
limit: parsed.limit,
|
|
17586
|
+
cursor: parsed.cursor,
|
|
17587
|
+
search: parsed.search
|
|
17588
|
+
});
|
|
17589
|
+
renderSuccess("recordings list", data, render2);
|
|
17590
|
+
return 0;
|
|
17591
|
+
}
|
|
17592
|
+
if (parsed.kind === "recordings-get") {
|
|
17593
|
+
const data = await client.getRecording(parsed.recordingId);
|
|
17594
|
+
renderSuccess("recordings get", data, render2);
|
|
17595
|
+
return 0;
|
|
17596
|
+
}
|
|
17597
|
+
if (parsed.kind === "dashboard-stats") {
|
|
17598
|
+
const data = await client.dashboardStats();
|
|
17599
|
+
renderSuccess("dashboard stats", data, render2);
|
|
17600
|
+
return 0;
|
|
17601
|
+
}
|
|
16882
17602
|
if (parsed.kind === "transcript-get") {
|
|
16883
17603
|
const data = await client.getTranscript(parsed.transcriptId);
|
|
16884
17604
|
renderSuccess("transcript get", data, render2);
|
|
@@ -17027,6 +17747,34 @@ Agent mode:
|
|
|
17027
17747
|
addCommonOptions(program);
|
|
17028
17748
|
const auth = program.command("auth").description("Authentication commands");
|
|
17029
17749
|
addCommonOptions(auth);
|
|
17750
|
+
const authLogin = auth.command("login").description("Sign in to Recappi Cloud with a device code").option("--no-open", "print the device URL without opening a browser");
|
|
17751
|
+
addCommonOptions(authLogin);
|
|
17752
|
+
authLogin.action((opts, command) => {
|
|
17753
|
+
onSelect({
|
|
17754
|
+
kind: "auth-login",
|
|
17755
|
+
options: collectGlobalOptions(command),
|
|
17756
|
+
commandName: "auth login",
|
|
17757
|
+
...opts.noOpen === true ? { noOpen: true } : {}
|
|
17758
|
+
});
|
|
17759
|
+
});
|
|
17760
|
+
const authLogout = auth.command("logout").description("Remove the Recappi CLI sign-in token");
|
|
17761
|
+
addCommonOptions(authLogout);
|
|
17762
|
+
authLogout.action((_options, command) => {
|
|
17763
|
+
onSelect({
|
|
17764
|
+
kind: "auth-logout",
|
|
17765
|
+
options: collectGlobalOptions(command),
|
|
17766
|
+
commandName: "auth logout"
|
|
17767
|
+
});
|
|
17768
|
+
});
|
|
17769
|
+
const authImportMacOS = auth.command("import-macos").description("Copy the Recappi Mini macOS app session into CLI config");
|
|
17770
|
+
addCommonOptions(authImportMacOS);
|
|
17771
|
+
authImportMacOS.action((_options, command) => {
|
|
17772
|
+
onSelect({
|
|
17773
|
+
kind: "auth-import-macos",
|
|
17774
|
+
options: collectGlobalOptions(command),
|
|
17775
|
+
commandName: "auth import-macos"
|
|
17776
|
+
});
|
|
17777
|
+
});
|
|
17030
17778
|
const authStatus = auth.command("status").description("Show Recappi Cloud sign-in status");
|
|
17031
17779
|
addCommonOptions(authStatus);
|
|
17032
17780
|
authStatus.action((_options, command) => {
|
|
@@ -17072,6 +17820,44 @@ Agent mode:
|
|
|
17072
17820
|
document: buildSchemaDocument(program)
|
|
17073
17821
|
});
|
|
17074
17822
|
});
|
|
17823
|
+
const dashboard = program.command("dashboard").description("Dashboard data commands");
|
|
17824
|
+
addCommonOptions(dashboard);
|
|
17825
|
+
const dashboardStats = dashboard.command("stats").description("Fetch dashboard counters");
|
|
17826
|
+
addCommonOptions(dashboardStats);
|
|
17827
|
+
dashboardStats.action((_options, command) => {
|
|
17828
|
+
onSelect({
|
|
17829
|
+
kind: "dashboard-stats",
|
|
17830
|
+
options: collectGlobalOptions(command),
|
|
17831
|
+
commandName: "dashboard stats"
|
|
17832
|
+
});
|
|
17833
|
+
});
|
|
17834
|
+
const recordings = program.command("recordings").description("Recording commands");
|
|
17835
|
+
addCommonOptions(recordings);
|
|
17836
|
+
const recordingsList = recordings.command("list").description("List recent recordings").option("--limit <n>", "number of recordings to show", parseLimitOption("--limit", 1, 100), 20).option("--cursor <cursor>", "pagination cursor", parseStringOption("--cursor")).option("--search <query>", "search recordings and transcripts", parseStringOption("--search"));
|
|
17837
|
+
addCommonOptions(recordingsList);
|
|
17838
|
+
recordingsList.action((_options, command) => {
|
|
17839
|
+
const opts = command.opts();
|
|
17840
|
+
onSelect({
|
|
17841
|
+
kind: "recordings-list",
|
|
17842
|
+
options: collectGlobalOptions(command),
|
|
17843
|
+
commandName: "recordings list",
|
|
17844
|
+
limit: opts.limit ?? 20,
|
|
17845
|
+
...typeof opts.cursor === "string" ? { cursor: opts.cursor } : {},
|
|
17846
|
+
...typeof opts.search === "string" ? { search: opts.search } : {}
|
|
17847
|
+
});
|
|
17848
|
+
});
|
|
17849
|
+
const recordingsGet = recordings.command("get <recordingId>").description("Fetch a recording by recording id");
|
|
17850
|
+
addCommonOptions(recordingsGet);
|
|
17851
|
+
recordingsGet.action(
|
|
17852
|
+
(recordingId, _options, command) => {
|
|
17853
|
+
onSelect({
|
|
17854
|
+
kind: "recordings-get",
|
|
17855
|
+
options: collectGlobalOptions(command),
|
|
17856
|
+
commandName: "recordings get",
|
|
17857
|
+
recordingId
|
|
17858
|
+
});
|
|
17859
|
+
}
|
|
17860
|
+
);
|
|
17075
17861
|
const transcript = program.command("transcript").description("Transcript commands");
|
|
17076
17862
|
addCommonOptions(transcript);
|
|
17077
17863
|
const transcriptGet = transcript.command("get <transcriptId>").description("Fetch a transcript by transcript id");
|
|
@@ -17186,7 +17972,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
17186
17972
|
"--provider",
|
|
17187
17973
|
"--prompt",
|
|
17188
17974
|
"--status",
|
|
17189
|
-
"--limit"
|
|
17975
|
+
"--limit",
|
|
17976
|
+
"--cursor",
|
|
17977
|
+
"--search"
|
|
17190
17978
|
]);
|
|
17191
17979
|
function hasCommandToken(argv) {
|
|
17192
17980
|
return commandTokens(argv).length > 0;
|