recappi 0.1.73 → 0.1.75
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 +644 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1255,7 +1255,7 @@ function RecordingDetailView({
|
|
|
1255
1255
|
" \xB7 T re-transcribe \xB7 s re-summarize",
|
|
1256
1256
|
item.activeTranscriptId ? " \xB7 t full" : "",
|
|
1257
1257
|
links.webUrl ? " \xB7 w web" : "",
|
|
1258
|
-
" \xB7 esc back"
|
|
1258
|
+
" \xB7 r refresh \xB7 esc back"
|
|
1259
1259
|
] }) })
|
|
1260
1260
|
] });
|
|
1261
1261
|
}
|
|
@@ -2034,6 +2034,7 @@ function AppShell({
|
|
|
2034
2034
|
);
|
|
2035
2035
|
const [audioCache, setAudioCache] = useState8(() => /* @__PURE__ */ new Map());
|
|
2036
2036
|
const [downloadedIds, setDownloadedIds] = useState8(() => /* @__PURE__ */ new Set());
|
|
2037
|
+
const summaryRefreshRef = useRef3(null);
|
|
2037
2038
|
const [liveRecord, setLiveRecord] = useState8(void 0);
|
|
2038
2039
|
const [recordSetupInputs, setRecordSetupInputs] = useState8({
|
|
2039
2040
|
sources: DEFAULT_RECORDING_SOURCES,
|
|
@@ -2423,6 +2424,18 @@ function AppShell({
|
|
|
2423
2424
|
},
|
|
2424
2425
|
[onRetranscribe, refresh]
|
|
2425
2426
|
);
|
|
2427
|
+
const refetchTranscript = useCallback(
|
|
2428
|
+
(transcriptId) => {
|
|
2429
|
+
setTranscriptCache((m) => new Map(m).set(transcriptId, "loading"));
|
|
2430
|
+
setSummaryCache((m) => {
|
|
2431
|
+
const next = new Map(m);
|
|
2432
|
+
next.delete(transcriptId);
|
|
2433
|
+
return next;
|
|
2434
|
+
});
|
|
2435
|
+
fetchTranscript(transcriptId).then((tr) => setTranscriptCache((m) => new Map(m).set(transcriptId, tr))).catch(() => setTranscriptCache((m) => new Map(m).set(transcriptId, "error")));
|
|
2436
|
+
},
|
|
2437
|
+
[fetchTranscript]
|
|
2438
|
+
);
|
|
2426
2439
|
const resummarizeExistingRecording = useCallback(
|
|
2427
2440
|
async (recordingId) => {
|
|
2428
2441
|
if (!onResummarize) {
|
|
@@ -2431,14 +2444,19 @@ function AppShell({
|
|
|
2431
2444
|
}
|
|
2432
2445
|
setNotice("Re-summarize started\u2026");
|
|
2433
2446
|
try {
|
|
2434
|
-
await onResummarize(recordingId);
|
|
2435
|
-
setNotice("Re-summarize started \u2014 the summary
|
|
2447
|
+
const data = await onResummarize(recordingId);
|
|
2448
|
+
setNotice("Re-summarize started \u2014 the summary updates here as it finishes.");
|
|
2436
2449
|
await refresh({ resetRecordings: true });
|
|
2450
|
+
const transcriptId = data.transcriptId ?? recordings.find((r) => r.recordingId === recordingId)?.activeTranscriptId;
|
|
2451
|
+
if (transcriptId) {
|
|
2452
|
+
summaryRefreshRef.current = { transcriptId, until: now() + 12e4 };
|
|
2453
|
+
refetchTranscript(transcriptId);
|
|
2454
|
+
}
|
|
2437
2455
|
} catch (error51) {
|
|
2438
2456
|
setNotice(transcribeHandoffErrorCopy(error51));
|
|
2439
2457
|
}
|
|
2440
2458
|
},
|
|
2441
|
-
[onResummarize, refresh]
|
|
2459
|
+
[onResummarize, refresh, recordings, now, refetchTranscript]
|
|
2442
2460
|
);
|
|
2443
2461
|
useEffect4(() => {
|
|
2444
2462
|
if (liveRecord?.kind !== "stopped") return;
|
|
@@ -2481,6 +2499,26 @@ function AppShell({
|
|
|
2481
2499
|
const id = setInterval(() => void refresh(), pollMs);
|
|
2482
2500
|
return () => clearInterval(id);
|
|
2483
2501
|
}, [refresh, pollMs]);
|
|
2502
|
+
const transcriptCacheRef = useRef3(transcriptCache);
|
|
2503
|
+
transcriptCacheRef.current = transcriptCache;
|
|
2504
|
+
useEffect4(() => {
|
|
2505
|
+
const id = setInterval(() => {
|
|
2506
|
+
const pending = summaryRefreshRef.current;
|
|
2507
|
+
if (!pending) return;
|
|
2508
|
+
if (now() >= pending.until) {
|
|
2509
|
+
summaryRefreshRef.current = null;
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const cached2 = transcriptCacheRef.current.get(pending.transcriptId);
|
|
2513
|
+
const summary = cached2 && cached2 !== "loading" && cached2 !== "error" ? cached2.summary : void 0;
|
|
2514
|
+
if (summary?.status === "succeeded") {
|
|
2515
|
+
summaryRefreshRef.current = null;
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
refetchTranscript(pending.transcriptId);
|
|
2519
|
+
}, 3e3);
|
|
2520
|
+
return () => clearInterval(id);
|
|
2521
|
+
}, [now, refetchTranscript]);
|
|
2484
2522
|
const hasRunning = jobs.some((item) => item.status === "running");
|
|
2485
2523
|
useEffect4(() => {
|
|
2486
2524
|
if (!hasRunning && loaded) return;
|
|
@@ -2665,14 +2703,21 @@ function AppShell({
|
|
|
2665
2703
|
}
|
|
2666
2704
|
return;
|
|
2667
2705
|
}
|
|
2668
|
-
if (input === "r")
|
|
2706
|
+
if (input === "r") {
|
|
2707
|
+
void refresh({ resetRecordings: true });
|
|
2708
|
+
if (screen.kind === "recordingDetail") {
|
|
2709
|
+
const detailRec = recordings.find((r) => r.recordingId === screen.recordingId);
|
|
2710
|
+
if (detailRec?.activeTranscriptId) refetchTranscript(detailRec.activeTranscriptId);
|
|
2711
|
+
}
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2669
2714
|
if (screen.kind === "overview") {
|
|
2670
2715
|
if (input === "g") setSelected(0);
|
|
2671
2716
|
if (input === "G") setSelected(Math.max(0, recordings.length - 1));
|
|
2672
2717
|
if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
|
|
2673
2718
|
if (key.downArrow || input === "j") setSelected((i) => Math.min(recordings.length - 1, i + 1));
|
|
2674
2719
|
const rec = recordings[selected];
|
|
2675
|
-
if (key.return && rec)
|
|
2720
|
+
if ((key.return || key.rightArrow) && rec)
|
|
2676
2721
|
setStack((st) => [...st, { kind: "recordingDetail", recordingId: rec.recordingId }]);
|
|
2677
2722
|
if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
|
|
2678
2723
|
return;
|
|
@@ -2683,7 +2728,8 @@ function AppShell({
|
|
|
2683
2728
|
if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
|
|
2684
2729
|
if (key.downArrow || input === "j") setSelected((i) => Math.min(jobs.length - 1, i + 1));
|
|
2685
2730
|
const job = jobs[selected];
|
|
2686
|
-
if (key.return
|
|
2731
|
+
if ((key.return || key.rightArrow) && job)
|
|
2732
|
+
setStack((st) => [...st, { kind: "jobDetail", jobId: job.jobId }]);
|
|
2687
2733
|
if (input === "t" && job?.transcriptId) void openTranscript(job.transcriptId);
|
|
2688
2734
|
return;
|
|
2689
2735
|
}
|
|
@@ -2955,6 +3001,9 @@ async function runDashboard(deps) {
|
|
|
2955
3001
|
transcribeRecordingArtifact: deps.transcribeRecordingArtifact,
|
|
2956
3002
|
onRetranscribe: deps.retranscribeRecording,
|
|
2957
3003
|
onResummarize: deps.resummarizeRecording,
|
|
3004
|
+
onSyncRecordingText: deps.syncRecordingText,
|
|
3005
|
+
onSyncRecordingAudio: deps.syncRecordingAudio,
|
|
3006
|
+
onExportRecording: deps.exportRecording,
|
|
2958
3007
|
initialView: deps.initialView ?? "overview",
|
|
2959
3008
|
openUrl,
|
|
2960
3009
|
copyText
|
|
@@ -2982,7 +3031,7 @@ var init_tui = __esm({
|
|
|
2982
3031
|
|
|
2983
3032
|
// src/cli.ts
|
|
2984
3033
|
import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
|
|
2985
|
-
import
|
|
3034
|
+
import os6 from "os";
|
|
2986
3035
|
|
|
2987
3036
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
2988
3037
|
var external_exports = {};
|
|
@@ -3750,10 +3799,10 @@ function mergeDefs(...defs) {
|
|
|
3750
3799
|
function cloneDef(schema) {
|
|
3751
3800
|
return mergeDefs(schema._zod.def);
|
|
3752
3801
|
}
|
|
3753
|
-
function getElementAtPath(obj,
|
|
3754
|
-
if (!
|
|
3802
|
+
function getElementAtPath(obj, path7) {
|
|
3803
|
+
if (!path7)
|
|
3755
3804
|
return obj;
|
|
3756
|
-
return
|
|
3805
|
+
return path7.reduce((acc, key) => acc?.[key], obj);
|
|
3757
3806
|
}
|
|
3758
3807
|
function promiseAllObject(promisesObj) {
|
|
3759
3808
|
const keys = Object.keys(promisesObj);
|
|
@@ -4162,11 +4211,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
4162
4211
|
}
|
|
4163
4212
|
return false;
|
|
4164
4213
|
}
|
|
4165
|
-
function prefixIssues(
|
|
4214
|
+
function prefixIssues(path7, issues) {
|
|
4166
4215
|
return issues.map((iss) => {
|
|
4167
4216
|
var _a3;
|
|
4168
4217
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
4169
|
-
iss.path.unshift(
|
|
4218
|
+
iss.path.unshift(path7);
|
|
4170
4219
|
return iss;
|
|
4171
4220
|
});
|
|
4172
4221
|
}
|
|
@@ -4313,16 +4362,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
4313
4362
|
}
|
|
4314
4363
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
4315
4364
|
const fieldErrors = { _errors: [] };
|
|
4316
|
-
const processError = (error52,
|
|
4365
|
+
const processError = (error52, path7 = []) => {
|
|
4317
4366
|
for (const issue2 of error52.issues) {
|
|
4318
4367
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
4319
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
4368
|
+
issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
|
|
4320
4369
|
} else if (issue2.code === "invalid_key") {
|
|
4321
|
-
processError({ issues: issue2.issues }, [...
|
|
4370
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
4322
4371
|
} else if (issue2.code === "invalid_element") {
|
|
4323
|
-
processError({ issues: issue2.issues }, [...
|
|
4372
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
4324
4373
|
} else {
|
|
4325
|
-
const fullpath = [...
|
|
4374
|
+
const fullpath = [...path7, ...issue2.path];
|
|
4326
4375
|
if (fullpath.length === 0) {
|
|
4327
4376
|
fieldErrors._errors.push(mapper(issue2));
|
|
4328
4377
|
} else {
|
|
@@ -4349,17 +4398,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
4349
4398
|
}
|
|
4350
4399
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
4351
4400
|
const result = { errors: [] };
|
|
4352
|
-
const processError = (error52,
|
|
4401
|
+
const processError = (error52, path7 = []) => {
|
|
4353
4402
|
var _a3, _b;
|
|
4354
4403
|
for (const issue2 of error52.issues) {
|
|
4355
4404
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
4356
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
4405
|
+
issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
|
|
4357
4406
|
} else if (issue2.code === "invalid_key") {
|
|
4358
|
-
processError({ issues: issue2.issues }, [...
|
|
4407
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
4359
4408
|
} else if (issue2.code === "invalid_element") {
|
|
4360
|
-
processError({ issues: issue2.issues }, [...
|
|
4409
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
4361
4410
|
} else {
|
|
4362
|
-
const fullpath = [...
|
|
4411
|
+
const fullpath = [...path7, ...issue2.path];
|
|
4363
4412
|
if (fullpath.length === 0) {
|
|
4364
4413
|
result.errors.push(mapper(issue2));
|
|
4365
4414
|
continue;
|
|
@@ -4391,8 +4440,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
4391
4440
|
}
|
|
4392
4441
|
function toDotPath(_path) {
|
|
4393
4442
|
const segs = [];
|
|
4394
|
-
const
|
|
4395
|
-
for (const seg of
|
|
4443
|
+
const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
4444
|
+
for (const seg of path7) {
|
|
4396
4445
|
if (typeof seg === "number")
|
|
4397
4446
|
segs.push(`[${seg}]`);
|
|
4398
4447
|
else if (typeof seg === "symbol")
|
|
@@ -17084,13 +17133,13 @@ function resolveRef(ref, ctx) {
|
|
|
17084
17133
|
if (!ref.startsWith("#")) {
|
|
17085
17134
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
17086
17135
|
}
|
|
17087
|
-
const
|
|
17088
|
-
if (
|
|
17136
|
+
const path7 = ref.slice(1).split("/").filter(Boolean);
|
|
17137
|
+
if (path7.length === 0) {
|
|
17089
17138
|
return ctx.rootSchema;
|
|
17090
17139
|
}
|
|
17091
17140
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
17092
|
-
if (
|
|
17093
|
-
const key =
|
|
17141
|
+
if (path7[0] === defsKey) {
|
|
17142
|
+
const key = path7[1];
|
|
17094
17143
|
if (!key || !ctx.defs[key]) {
|
|
17095
17144
|
throw new Error(`Reference not found: ${ref}`);
|
|
17096
17145
|
}
|
|
@@ -18093,6 +18142,31 @@ var transcriptDataSchema = external_exports.object({
|
|
|
18093
18142
|
segments: external_exports.array(transcriptSegmentSchema),
|
|
18094
18143
|
summary: transcriptSummarySchema
|
|
18095
18144
|
});
|
|
18145
|
+
var recordingExportDataSchema = external_exports.object({
|
|
18146
|
+
origin: external_exports.string(),
|
|
18147
|
+
recordingId: external_exports.string(),
|
|
18148
|
+
exportDir: external_exports.string(),
|
|
18149
|
+
textPath: external_exports.string(),
|
|
18150
|
+
manifestPath: external_exports.string(),
|
|
18151
|
+
remoteManifestPath: external_exports.string(),
|
|
18152
|
+
sessionMetadataPath: external_exports.string(),
|
|
18153
|
+
recordingJsonPath: external_exports.string(),
|
|
18154
|
+
subscriptionPath: external_exports.string(),
|
|
18155
|
+
subscriptionJsonPath: external_exports.string(),
|
|
18156
|
+
audioPath: external_exports.string(),
|
|
18157
|
+
transcriptId: external_exports.string().nullable().optional(),
|
|
18158
|
+
transcriptPath: external_exports.string().optional(),
|
|
18159
|
+
transcriptJsonPath: external_exports.string().optional(),
|
|
18160
|
+
summaryPath: external_exports.string().optional(),
|
|
18161
|
+
summaryJsonPath: external_exports.string().optional(),
|
|
18162
|
+
actionItemsPath: external_exports.string().optional(),
|
|
18163
|
+
summaryStatus: summaryStatusSchema.optional(),
|
|
18164
|
+
audio: external_exports.object({
|
|
18165
|
+
contentType: external_exports.string().optional(),
|
|
18166
|
+
contentLength: external_exports.number().int().nonnegative().optional(),
|
|
18167
|
+
reused: external_exports.boolean().optional()
|
|
18168
|
+
}).optional()
|
|
18169
|
+
});
|
|
18096
18170
|
var doctorCheckStatusSchema = external_exports.enum(["ok", "warn", "error"]);
|
|
18097
18171
|
var doctorCheckSchema = external_exports.object({
|
|
18098
18172
|
name: external_exports.string(),
|
|
@@ -19355,7 +19429,10 @@ var RecappiApiClient = class {
|
|
|
19355
19429
|
const contentLength = numberHeader(response.headers.get("content-length"));
|
|
19356
19430
|
const dir = opts.directory ?? await fs3.mkdtemp(path4.join(os4.tmpdir(), "recappi-cli-audio-"));
|
|
19357
19431
|
if (opts.directory) await fs3.mkdir(dir, { recursive: true });
|
|
19358
|
-
const filePath = path4.join(
|
|
19432
|
+
const filePath = path4.join(
|
|
19433
|
+
dir,
|
|
19434
|
+
recordingAudioFileName(recordingId, opts.title, contentType, opts.filenameStem)
|
|
19435
|
+
);
|
|
19359
19436
|
try {
|
|
19360
19437
|
await pipeline(
|
|
19361
19438
|
Readable.fromWeb(response.body),
|
|
@@ -19654,12 +19731,12 @@ var RecappiApiClient = class {
|
|
|
19654
19731
|
} : {}
|
|
19655
19732
|
};
|
|
19656
19733
|
}
|
|
19657
|
-
async getJson(
|
|
19658
|
-
const response = await this.request("GET",
|
|
19734
|
+
async getJson(path7) {
|
|
19735
|
+
const response = await this.request("GET", path7);
|
|
19659
19736
|
return await parseJson(response);
|
|
19660
19737
|
}
|
|
19661
|
-
async postJson(
|
|
19662
|
-
const response = await this.request("POST",
|
|
19738
|
+
async postJson(path7, body) {
|
|
19739
|
+
const response = await this.request("POST", path7, JSON.stringify(body), {
|
|
19663
19740
|
headers: { "content-type": "application/json" }
|
|
19664
19741
|
});
|
|
19665
19742
|
return await parseJson(response);
|
|
@@ -19753,7 +19830,10 @@ function audioExtensionForContentType(contentType) {
|
|
|
19753
19830
|
return "wav";
|
|
19754
19831
|
}
|
|
19755
19832
|
}
|
|
19756
|
-
function recordingAudioFileName(recordingId, title, contentType) {
|
|
19833
|
+
function recordingAudioFileName(recordingId, title, contentType, filenameStem) {
|
|
19834
|
+
if (filenameStem && filenameStem.trim()) {
|
|
19835
|
+
return `${truncateFileStem(safeFileStem(filenameStem), 96)}.${audioExtensionForContentType(contentType)}`;
|
|
19836
|
+
}
|
|
19757
19837
|
const idStem = truncateFileStem(safeFileStem(recordingId), 48);
|
|
19758
19838
|
const titleStem = title ? truncateFileStem(safeFileStem(title), 80) : "";
|
|
19759
19839
|
const stem = titleStem ? `${titleStem}-${idStem}` : idStem;
|
|
@@ -20047,7 +20127,7 @@ import { promises as fs4 } from "fs";
|
|
|
20047
20127
|
import path5 from "path";
|
|
20048
20128
|
function createRecordingAudioRuntime(client, deps = {}) {
|
|
20049
20129
|
const downloadRecordingAudioFile = async (recordingId, opts) => {
|
|
20050
|
-
const cached2 = await findReusableDownload(recordingId, deps);
|
|
20130
|
+
const cached2 = opts?.directory ? null : await findReusableDownload(recordingId, deps);
|
|
20051
20131
|
if (cached2) return cached2;
|
|
20052
20132
|
const directory = opts?.directory ?? (deps.account ? defaultDownloadDirectory(deps) : void 0);
|
|
20053
20133
|
const download = await client.downloadRecordingAudio(recordingId, {
|
|
@@ -20203,6 +20283,7 @@ var COMMON_TASKS = [
|
|
|
20203
20283
|
{ label: "Transcribe a local file", command: "recappi upload <file> --transcribe --wait" },
|
|
20204
20284
|
{ label: "Re-transcribe a recording", command: "recappi recordings retranscribe <recordingId> --wait" },
|
|
20205
20285
|
{ label: "Re-summarize a recording", command: "recappi recordings resummarize <recordingId>" },
|
|
20286
|
+
{ label: "Export recording bundle", command: "recappi recordings export <recordingId> --dir ./bundle" },
|
|
20206
20287
|
{ label: "List / find recordings", command: "recappi recordings list" },
|
|
20207
20288
|
{ label: "Read a transcript", command: "recappi transcript get <transcriptId>" },
|
|
20208
20289
|
{ label: "Download / open audio", command: "recappi audio <recordingId> --open" },
|
|
@@ -20309,7 +20390,24 @@ var COMMAND_METADATA = {
|
|
|
20309
20390
|
"recordings get": {
|
|
20310
20391
|
capabilities: ["Fetch one recording's metadata and status by id"],
|
|
20311
20392
|
examples: [{ description: "Fetch one recording", command: "recappi recordings get <recordingId>" }],
|
|
20312
|
-
relatedCommands: ["recordings list", "transcript get", "audio"]
|
|
20393
|
+
relatedCommands: ["recordings list", "recordings export", "transcript get", "audio"]
|
|
20394
|
+
},
|
|
20395
|
+
"recordings export": {
|
|
20396
|
+
capabilities: [
|
|
20397
|
+
"Write a plain-text Markdown handoff file for agents",
|
|
20398
|
+
"Export raw audio, transcript, summary, action items, subscription/account data, and JSON sidecars"
|
|
20399
|
+
],
|
|
20400
|
+
examples: [
|
|
20401
|
+
{
|
|
20402
|
+
description: "Export a recording bundle into a chosen directory",
|
|
20403
|
+
command: "recappi recordings export <recordingId> --dir ./recappi-export"
|
|
20404
|
+
},
|
|
20405
|
+
{
|
|
20406
|
+
description: "Export and return machine-readable paths",
|
|
20407
|
+
command: "recappi recordings export <recordingId> --json --compact"
|
|
20408
|
+
}
|
|
20409
|
+
],
|
|
20410
|
+
relatedCommands: ["recordings get", "transcript get", "audio", "account status"]
|
|
20313
20411
|
},
|
|
20314
20412
|
"recordings list": {
|
|
20315
20413
|
capabilities: ["List recent recordings", "Search recordings and transcripts", "Find a recordingId"],
|
|
@@ -20317,7 +20415,7 @@ var COMMAND_METADATA = {
|
|
|
20317
20415
|
{ description: "List recent recordings", command: "recappi recordings list" },
|
|
20318
20416
|
{ description: "Search recordings and transcripts", command: "recappi recordings list --search <query>" }
|
|
20319
20417
|
],
|
|
20320
|
-
relatedCommands: ["recordings get", "audio", "transcript get"]
|
|
20418
|
+
relatedCommands: ["recordings get", "recordings export", "audio", "transcript get"]
|
|
20321
20419
|
},
|
|
20322
20420
|
"recordings retranscribe": {
|
|
20323
20421
|
capabilities: ["Re-transcribe an existing recording", "Re-transcribe with new language/prompt/scene/model"],
|
|
@@ -20394,6 +20492,408 @@ ${lines.join("\n")}
|
|
|
20394
20492
|
` : "";
|
|
20395
20493
|
}
|
|
20396
20494
|
|
|
20495
|
+
// src/export.ts
|
|
20496
|
+
import { promises as fs5 } from "fs";
|
|
20497
|
+
import os5 from "os";
|
|
20498
|
+
import path6 from "path";
|
|
20499
|
+
async function syncRecordingText(opts) {
|
|
20500
|
+
const context = await loadRecordingBundleContext(opts);
|
|
20501
|
+
const sessionDir = await resolveRecordingSessionDir(context.recording, opts);
|
|
20502
|
+
return writeRecordingTextFiles(context, sessionDir, {
|
|
20503
|
+
now: opts.now
|
|
20504
|
+
});
|
|
20505
|
+
}
|
|
20506
|
+
async function syncRecordingAudio(opts) {
|
|
20507
|
+
const context = await loadRecordingBundleContext(opts);
|
|
20508
|
+
const sessionDir = await resolveRecordingSessionDir(context.recording, opts);
|
|
20509
|
+
const audio = await downloadRecordingAudioToDir(context.recording, opts.recordingAudio, sessionDir);
|
|
20510
|
+
const text = await writeRecordingTextFiles(context, sessionDir, {
|
|
20511
|
+
now: opts.now,
|
|
20512
|
+
uploadFilename: path6.basename(audio.localPath)
|
|
20513
|
+
});
|
|
20514
|
+
return { ...text, audioPath: audio.localPath, audio: audioMetadata(audio) };
|
|
20515
|
+
}
|
|
20516
|
+
async function exportRecording(opts) {
|
|
20517
|
+
const context = await loadRecordingBundleContext(opts);
|
|
20518
|
+
const exportDir = await resolveRecordingSessionDir(context.recording, opts);
|
|
20519
|
+
const audio = await downloadRecordingAudioToDir(context.recording, opts.recordingAudio, exportDir);
|
|
20520
|
+
const textFiles = await writeRecordingTextFiles(context, exportDir, {
|
|
20521
|
+
now: opts.now,
|
|
20522
|
+
uploadFilename: path6.basename(audio.localPath)
|
|
20523
|
+
});
|
|
20524
|
+
const { recording, subscription, transcript } = context;
|
|
20525
|
+
const subscriptionPath = path6.join(exportDir, "subscription.md");
|
|
20526
|
+
const subscriptionJsonPath = path6.join(exportDir, "subscription.json");
|
|
20527
|
+
await fs5.writeFile(subscriptionPath, renderSubscriptionMarkdown(subscription), "utf8");
|
|
20528
|
+
await writeJson(subscriptionJsonPath, subscription);
|
|
20529
|
+
const textPath = path6.join(exportDir, "handoff.md");
|
|
20530
|
+
const manifestPath = path6.join(exportDir, "manifest.json");
|
|
20531
|
+
const data = recordingExportDataSchema.parse({
|
|
20532
|
+
origin: recording.origin,
|
|
20533
|
+
recordingId: recording.recordingId,
|
|
20534
|
+
exportDir,
|
|
20535
|
+
textPath,
|
|
20536
|
+
manifestPath,
|
|
20537
|
+
remoteManifestPath: textFiles.remoteManifestPath,
|
|
20538
|
+
sessionMetadataPath: textFiles.sessionMetadataPath,
|
|
20539
|
+
recordingJsonPath: textFiles.recordingJsonPath,
|
|
20540
|
+
subscriptionPath,
|
|
20541
|
+
subscriptionJsonPath,
|
|
20542
|
+
audioPath: audio.localPath,
|
|
20543
|
+
...textFiles.transcriptId !== void 0 ? { transcriptId: textFiles.transcriptId } : {},
|
|
20544
|
+
...textFiles.transcriptPath ? { transcriptPath: textFiles.transcriptPath } : {},
|
|
20545
|
+
...textFiles.transcriptJsonPath ? { transcriptJsonPath: textFiles.transcriptJsonPath } : {},
|
|
20546
|
+
...textFiles.summaryPath ? { summaryPath: textFiles.summaryPath } : {},
|
|
20547
|
+
...textFiles.summaryJsonPath ? { summaryJsonPath: textFiles.summaryJsonPath } : {},
|
|
20548
|
+
...textFiles.actionItemsPath ? { actionItemsPath: textFiles.actionItemsPath } : {},
|
|
20549
|
+
...textFiles.summaryStatus ? { summaryStatus: textFiles.summaryStatus } : {},
|
|
20550
|
+
audio: audioMetadata(audio)
|
|
20551
|
+
});
|
|
20552
|
+
await fs5.writeFile(
|
|
20553
|
+
textPath,
|
|
20554
|
+
renderHandoffMarkdown(recording, subscription, audio, data, transcript),
|
|
20555
|
+
"utf8"
|
|
20556
|
+
);
|
|
20557
|
+
await writeJson(manifestPath, {
|
|
20558
|
+
exportedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
20559
|
+
command: "recordings export",
|
|
20560
|
+
data
|
|
20561
|
+
});
|
|
20562
|
+
return data;
|
|
20563
|
+
}
|
|
20564
|
+
async function loadRecordingBundleContext(opts) {
|
|
20565
|
+
const recording = await opts.client.getRecording(opts.recordingId);
|
|
20566
|
+
const subscription = await opts.client.accountStatus();
|
|
20567
|
+
const transcript = recording.activeTranscriptId ? await opts.client.getTranscript(recording.activeTranscriptId) : void 0;
|
|
20568
|
+
return {
|
|
20569
|
+
recording,
|
|
20570
|
+
subscription,
|
|
20571
|
+
...transcript ? { transcript } : {},
|
|
20572
|
+
transcriptId: transcript?.transcriptId ?? recording.activeTranscriptId ?? void 0
|
|
20573
|
+
};
|
|
20574
|
+
}
|
|
20575
|
+
async function downloadRecordingAudioToDir(recording, recordingAudio, directory) {
|
|
20576
|
+
return recordingAudio.downloadRecordingAudioFile(recording.recordingId, {
|
|
20577
|
+
directory,
|
|
20578
|
+
filenameStem: "recording",
|
|
20579
|
+
title: recording.title ?? recording.summaryTitle ?? recording.recordingId
|
|
20580
|
+
});
|
|
20581
|
+
}
|
|
20582
|
+
async function writeRecordingTextFiles(context, sessionDir, opts) {
|
|
20583
|
+
const { recording, transcript, subscription } = context;
|
|
20584
|
+
await fs5.mkdir(sessionDir, { recursive: true });
|
|
20585
|
+
const recordingJsonPath = path6.join(sessionDir, "recording.json");
|
|
20586
|
+
const sessionMetadataPath = path6.join(sessionDir, "session-metadata.json");
|
|
20587
|
+
const remoteManifestPath = path6.join(sessionDir, "remote-session.json");
|
|
20588
|
+
const existingManifest = await readJsonRecord(remoteManifestPath);
|
|
20589
|
+
const uploadFilename = opts.uploadFilename ?? (typeof existingManifest?.uploadFilename === "string" ? existingManifest.uploadFilename : void 0);
|
|
20590
|
+
await writeJson(recordingJsonPath, recording);
|
|
20591
|
+
await writeJson(sessionMetadataPath, renderSessionMetadata(recording));
|
|
20592
|
+
await writeJson(
|
|
20593
|
+
remoteManifestPath,
|
|
20594
|
+
renderRemoteSessionManifest(recording, uploadFilename, transcript, subscription, opts.now)
|
|
20595
|
+
);
|
|
20596
|
+
let transcriptPath;
|
|
20597
|
+
let transcriptJsonPath;
|
|
20598
|
+
let summaryPath;
|
|
20599
|
+
let summaryJsonPath;
|
|
20600
|
+
let actionItemsPath;
|
|
20601
|
+
let summaryStatus;
|
|
20602
|
+
if (transcript) {
|
|
20603
|
+
transcriptPath = path6.join(sessionDir, "transcript.md");
|
|
20604
|
+
transcriptJsonPath = path6.join(sessionDir, "transcript.json");
|
|
20605
|
+
summaryPath = path6.join(sessionDir, "summary.md");
|
|
20606
|
+
summaryJsonPath = path6.join(sessionDir, "summary.json");
|
|
20607
|
+
actionItemsPath = path6.join(sessionDir, "action-items.md");
|
|
20608
|
+
summaryStatus = transcript.summary.status;
|
|
20609
|
+
await fs5.writeFile(transcriptPath, renderTranscriptMarkdown(transcript), "utf8");
|
|
20610
|
+
await writeJson(transcriptJsonPath, transcript);
|
|
20611
|
+
await writeJson(summaryJsonPath, transcript.summary);
|
|
20612
|
+
await fs5.writeFile(summaryPath, renderSummaryMarkdown(recording, transcript), "utf8");
|
|
20613
|
+
await writeOptionalText(actionItemsPath, renderActionItemsMarkdown(transcript.summary));
|
|
20614
|
+
}
|
|
20615
|
+
return {
|
|
20616
|
+
recordingId: recording.recordingId,
|
|
20617
|
+
sessionDir,
|
|
20618
|
+
remoteManifestPath,
|
|
20619
|
+
sessionMetadataPath,
|
|
20620
|
+
recordingJsonPath,
|
|
20621
|
+
...context.transcriptId !== void 0 ? { transcriptId: context.transcriptId } : {},
|
|
20622
|
+
...transcriptPath ? { transcriptPath } : {},
|
|
20623
|
+
...transcriptJsonPath ? { transcriptJsonPath } : {},
|
|
20624
|
+
...summaryPath ? { summaryPath } : {},
|
|
20625
|
+
...summaryJsonPath ? { summaryJsonPath } : {},
|
|
20626
|
+
...actionItemsPath ? { actionItemsPath } : {},
|
|
20627
|
+
...summaryStatus ? { summaryStatus } : {}
|
|
20628
|
+
};
|
|
20629
|
+
}
|
|
20630
|
+
async function resolveRecordingSessionDir(recording, opts) {
|
|
20631
|
+
if (opts.directory) return path6.resolve(opts.directory);
|
|
20632
|
+
const existing = await findExistingRecordingSessionDir(recording.recordingId, opts.homeDir, opts.env);
|
|
20633
|
+
if (existing) return existing;
|
|
20634
|
+
return createRecordingSessionDir(recording, opts.homeDir, opts.env);
|
|
20635
|
+
}
|
|
20636
|
+
async function findExistingRecordingSessionDir(recordingId, homeDir, env) {
|
|
20637
|
+
const base = recordingSessionBaseDirectory(homeDir, env);
|
|
20638
|
+
let entries;
|
|
20639
|
+
try {
|
|
20640
|
+
entries = await fs5.readdir(base, { withFileTypes: true });
|
|
20641
|
+
} catch {
|
|
20642
|
+
return void 0;
|
|
20643
|
+
}
|
|
20644
|
+
for (const entry of entries) {
|
|
20645
|
+
if (!entry.isDirectory()) continue;
|
|
20646
|
+
const dir = path6.join(base, entry.name);
|
|
20647
|
+
const manifest = await readJsonRecord(path6.join(dir, "remote-session.json"));
|
|
20648
|
+
if (manifest?.recordingId === recordingId) return dir;
|
|
20649
|
+
}
|
|
20650
|
+
return void 0;
|
|
20651
|
+
}
|
|
20652
|
+
async function createRecordingSessionDir(recording, homeDir, env) {
|
|
20653
|
+
const base = recordingSessionBaseDirectory(homeDir, env);
|
|
20654
|
+
await fs5.mkdir(base, { recursive: true });
|
|
20655
|
+
const stem = formatSessionDirectoryDate(new Date(recording.createdAt));
|
|
20656
|
+
let candidate = path6.join(base, stem);
|
|
20657
|
+
let suffix = 2;
|
|
20658
|
+
while (await pathExists(candidate)) {
|
|
20659
|
+
candidate = path6.join(base, `${stem}-cloud-${suffix}`);
|
|
20660
|
+
suffix += 1;
|
|
20661
|
+
}
|
|
20662
|
+
await fs5.mkdir(candidate, { recursive: true });
|
|
20663
|
+
return candidate;
|
|
20664
|
+
}
|
|
20665
|
+
function recordingSessionBaseDirectory(homeDir = os5.homedir(), env = process.env) {
|
|
20666
|
+
const explicit = env.RECAPPI_LOCAL_SESSIONS_DIR?.trim();
|
|
20667
|
+
if (explicit) return explicit;
|
|
20668
|
+
return path6.join(homeDir, "Documents", "Recappi Mini");
|
|
20669
|
+
}
|
|
20670
|
+
async function readJsonRecord(filePath) {
|
|
20671
|
+
try {
|
|
20672
|
+
const parsed = JSON.parse(await fs5.readFile(filePath, "utf8"));
|
|
20673
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
20674
|
+
} catch {
|
|
20675
|
+
return void 0;
|
|
20676
|
+
}
|
|
20677
|
+
}
|
|
20678
|
+
async function pathExists(filePath) {
|
|
20679
|
+
try {
|
|
20680
|
+
await fs5.access(filePath);
|
|
20681
|
+
return true;
|
|
20682
|
+
} catch {
|
|
20683
|
+
return false;
|
|
20684
|
+
}
|
|
20685
|
+
}
|
|
20686
|
+
function formatSessionDirectoryDate(date5) {
|
|
20687
|
+
const year = date5.getFullYear();
|
|
20688
|
+
const month = String(date5.getMonth() + 1).padStart(2, "0");
|
|
20689
|
+
const day = String(date5.getDate()).padStart(2, "0");
|
|
20690
|
+
const hour = String(date5.getHours()).padStart(2, "0");
|
|
20691
|
+
const minute = String(date5.getMinutes()).padStart(2, "0");
|
|
20692
|
+
const second = String(date5.getSeconds()).padStart(2, "0");
|
|
20693
|
+
return `${year}-${month}-${day}_${hour}${minute}${second}`;
|
|
20694
|
+
}
|
|
20695
|
+
async function writeJson(filePath, value) {
|
|
20696
|
+
await fs5.writeFile(filePath, `${JSON.stringify(value, null, 2)}
|
|
20697
|
+
`, "utf8");
|
|
20698
|
+
}
|
|
20699
|
+
async function writeOptionalText(filePath, value) {
|
|
20700
|
+
if (value && value.trim()) {
|
|
20701
|
+
await fs5.writeFile(filePath, value, "utf8");
|
|
20702
|
+
return;
|
|
20703
|
+
}
|
|
20704
|
+
await fs5.rm(filePath, { force: true });
|
|
20705
|
+
}
|
|
20706
|
+
function renderHandoffMarkdown(recording, subscription, audio, data, transcript) {
|
|
20707
|
+
const lines = [];
|
|
20708
|
+
lines.push(`# ${recording.title ?? recording.summaryTitle ?? "Recappi Recording"}`);
|
|
20709
|
+
lines.push("");
|
|
20710
|
+
lines.push("## Files", "");
|
|
20711
|
+
lines.push(`- Audio: ${audio.localPath}`);
|
|
20712
|
+
lines.push(`- Subscription: ${data.subscriptionPath}`);
|
|
20713
|
+
if (data.summaryPath) lines.push(`- Summary: ${data.summaryPath}`);
|
|
20714
|
+
if (data.transcriptPath) lines.push(`- Transcript: ${data.transcriptPath}`);
|
|
20715
|
+
if (data.actionItemsPath) lines.push(`- Action items: ${data.actionItemsPath}`);
|
|
20716
|
+
lines.push(`- Remote session manifest: ${data.remoteManifestPath}`);
|
|
20717
|
+
lines.push(`- Session metadata: ${data.sessionMetadataPath}`);
|
|
20718
|
+
lines.push(`- Manifest: ${data.manifestPath}`);
|
|
20719
|
+
lines.push("");
|
|
20720
|
+
lines.push("## Recording", "");
|
|
20721
|
+
lines.push(`- recordingId: ${recording.recordingId}`);
|
|
20722
|
+
lines.push(`- status: ${recording.status}`);
|
|
20723
|
+
if (recording.durationMs !== void 0 && recording.durationMs !== null) {
|
|
20724
|
+
lines.push(`- duration: ${formatTimestamp(recording.durationMs)}`);
|
|
20725
|
+
}
|
|
20726
|
+
if (recording.sizeBytes !== void 0 && recording.sizeBytes !== null) {
|
|
20727
|
+
lines.push(`- sizeBytes: ${recording.sizeBytes}`);
|
|
20728
|
+
}
|
|
20729
|
+
if (transcript) {
|
|
20730
|
+
lines.push(`- transcriptId: ${transcript.transcriptId}`);
|
|
20731
|
+
lines.push(`- summaryStatus: ${transcript.summary.status}`);
|
|
20732
|
+
} else {
|
|
20733
|
+
lines.push("- transcript: not available");
|
|
20734
|
+
}
|
|
20735
|
+
lines.push("");
|
|
20736
|
+
lines.push("## Subscription", "");
|
|
20737
|
+
appendSubscriptionLines(lines, subscription);
|
|
20738
|
+
lines.push("");
|
|
20739
|
+
if (transcript) {
|
|
20740
|
+
lines.push(renderSummaryMarkdown(recording, transcript).trimEnd());
|
|
20741
|
+
lines.push("");
|
|
20742
|
+
lines.push(renderTranscriptMarkdown(transcript).trimEnd());
|
|
20743
|
+
lines.push("");
|
|
20744
|
+
}
|
|
20745
|
+
return `${lines.join("\n").trimEnd()}
|
|
20746
|
+
`;
|
|
20747
|
+
}
|
|
20748
|
+
function renderSubscriptionMarkdown(subscription) {
|
|
20749
|
+
const lines = ["# Subscription", ""];
|
|
20750
|
+
appendSubscriptionLines(lines, subscription);
|
|
20751
|
+
return `${lines.join("\n").trimEnd()}
|
|
20752
|
+
`;
|
|
20753
|
+
}
|
|
20754
|
+
function renderSessionMetadata(recording) {
|
|
20755
|
+
const sourceTitle = recording.title ?? recording.summaryTitle ?? "Recappi Cloud";
|
|
20756
|
+
return stripUndefined({
|
|
20757
|
+
summaryTitle: recording.summaryTitle ?? recording.title ?? void 0,
|
|
20758
|
+
sourceTitle,
|
|
20759
|
+
sourceAppName: void 0,
|
|
20760
|
+
sourceBundleID: void 0,
|
|
20761
|
+
startedAt: new Date(recording.createdAt).toISOString(),
|
|
20762
|
+
sceneTemplate: void 0,
|
|
20763
|
+
extraPrompt: void 0,
|
|
20764
|
+
includesMicrophoneAudio: void 0
|
|
20765
|
+
});
|
|
20766
|
+
}
|
|
20767
|
+
function renderRemoteSessionManifest(recording, uploadFilename, transcript, subscription, now) {
|
|
20768
|
+
return stripUndefined({
|
|
20769
|
+
recordingId: recording.recordingId,
|
|
20770
|
+
jobId: transcript?.jobId,
|
|
20771
|
+
transcriptId: transcript?.transcriptId ?? recording.activeTranscriptId ?? void 0,
|
|
20772
|
+
stage: transcript ? "done" : "synced",
|
|
20773
|
+
errorMessage: void 0,
|
|
20774
|
+
uploadFilename,
|
|
20775
|
+
provider: transcript?.provider,
|
|
20776
|
+
model: transcript?.model,
|
|
20777
|
+
updatedAt: (now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
20778
|
+
accountUserId: subscription.userId,
|
|
20779
|
+
accountBackendOrigin: subscription.origin
|
|
20780
|
+
});
|
|
20781
|
+
}
|
|
20782
|
+
function stripUndefined(value) {
|
|
20783
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
20784
|
+
}
|
|
20785
|
+
function appendSubscriptionLines(lines, subscription) {
|
|
20786
|
+
lines.push(`- origin: ${subscription.origin}`);
|
|
20787
|
+
lines.push(`- loggedIn: ${subscription.loggedIn}`);
|
|
20788
|
+
if (subscription.email) lines.push(`- email: ${subscription.email}`);
|
|
20789
|
+
if (subscription.userId) lines.push(`- userId: ${subscription.userId}`);
|
|
20790
|
+
if (subscription.billing) {
|
|
20791
|
+
lines.push(`- plan: ${subscription.billing.tier}`);
|
|
20792
|
+
lines.push(`- minutesUsed: ${subscription.billing.minutesUsed}`);
|
|
20793
|
+
lines.push(`- minutesCap: ${subscription.billing.minutesCap ?? "unlimited"}`);
|
|
20794
|
+
lines.push(`- batchMinutesUsed: ${subscription.billing.batchMinutesUsed}`);
|
|
20795
|
+
lines.push(`- realtimeMinutesUsed: ${subscription.billing.realtimeMinutesUsed}`);
|
|
20796
|
+
lines.push(`- storageBytes: ${subscription.billing.storageBytes}`);
|
|
20797
|
+
lines.push(`- storageCapBytes: ${subscription.billing.storageCapBytes ?? "unlimited"}`);
|
|
20798
|
+
lines.push(`- periodStart: ${subscription.billing.periodStart}`);
|
|
20799
|
+
lines.push(`- periodEnd: ${subscription.billing.periodEnd}`);
|
|
20800
|
+
}
|
|
20801
|
+
}
|
|
20802
|
+
function renderTranscriptMarkdown(transcript) {
|
|
20803
|
+
return `# Transcript
|
|
20804
|
+
|
|
20805
|
+
${renderTranscriptLines(transcript)}
|
|
20806
|
+
`;
|
|
20807
|
+
}
|
|
20808
|
+
function renderTranscriptLines(transcript) {
|
|
20809
|
+
const lines = transcript.segments.map((segment) => {
|
|
20810
|
+
const speaker = segment.speaker ? `${segment.speaker}: ` : "";
|
|
20811
|
+
return `[${formatTimestamp(segment.startMs)}] ${speaker}${segment.text}`;
|
|
20812
|
+
});
|
|
20813
|
+
if (lines.length > 0) return lines.join("\n");
|
|
20814
|
+
return transcript.text;
|
|
20815
|
+
}
|
|
20816
|
+
function renderSummaryMarkdown(recording, transcript) {
|
|
20817
|
+
const summary = transcript.summary;
|
|
20818
|
+
const lines = [];
|
|
20819
|
+
lines.push(`# ${summary.title ?? recording.title ?? recording.summaryTitle ?? "Summary"}`);
|
|
20820
|
+
lines.push("");
|
|
20821
|
+
lines.push(`- recordingId: ${recording.recordingId}`);
|
|
20822
|
+
lines.push(`- transcriptId: ${transcript.transcriptId}`);
|
|
20823
|
+
lines.push(`- summaryStatus: ${summary.status}`);
|
|
20824
|
+
if (summary.error) lines.push(`- error: ${summary.error}`);
|
|
20825
|
+
lines.push("");
|
|
20826
|
+
if (summary.tldr) {
|
|
20827
|
+
lines.push("## TL;DR", "");
|
|
20828
|
+
lines.push(summary.tldr, "");
|
|
20829
|
+
}
|
|
20830
|
+
appendStringList(lines, "Key Points", summary.keyPoints);
|
|
20831
|
+
appendStringList(lines, "Topics", summary.topics);
|
|
20832
|
+
appendStringList(lines, "Decisions", summary.decisions);
|
|
20833
|
+
appendActionItems(lines, summary.actionItems);
|
|
20834
|
+
appendTimeline(lines, summary.timeline);
|
|
20835
|
+
appendQuotes(lines, summary.quotes);
|
|
20836
|
+
if (lines[lines.length - 1] !== "") lines.push("");
|
|
20837
|
+
return lines.join("\n");
|
|
20838
|
+
}
|
|
20839
|
+
function renderActionItemsMarkdown(summary) {
|
|
20840
|
+
if (!summary.actionItems || summary.actionItems.length === 0) return null;
|
|
20841
|
+
const lines = ["# Action Items", ""];
|
|
20842
|
+
for (const item of summary.actionItems) {
|
|
20843
|
+
lines.push(`- ${item.who ? `${item.who} - ` : ""}${item.what}`);
|
|
20844
|
+
}
|
|
20845
|
+
lines.push("");
|
|
20846
|
+
return lines.join("\n");
|
|
20847
|
+
}
|
|
20848
|
+
function appendStringList(lines, title, values) {
|
|
20849
|
+
if (!values || values.length === 0) return;
|
|
20850
|
+
lines.push(`## ${title}`, "");
|
|
20851
|
+
for (const value of values) lines.push(`- ${value}`);
|
|
20852
|
+
lines.push("");
|
|
20853
|
+
}
|
|
20854
|
+
function appendActionItems(lines, values) {
|
|
20855
|
+
if (!values || values.length === 0) return;
|
|
20856
|
+
lines.push("## Action Items", "");
|
|
20857
|
+
for (const item of values) {
|
|
20858
|
+
lines.push(`- ${item.who ? `${item.who}: ` : ""}${item.what}`);
|
|
20859
|
+
}
|
|
20860
|
+
lines.push("");
|
|
20861
|
+
}
|
|
20862
|
+
function appendTimeline(lines, values) {
|
|
20863
|
+
if (!values || values.length === 0) return;
|
|
20864
|
+
lines.push("## Timeline", "");
|
|
20865
|
+
for (const item of values) {
|
|
20866
|
+
lines.push(
|
|
20867
|
+
`- ${formatTimestamp(item.startMs)}-${formatTimestamp(item.endMs)}: ${item.title} - ${item.summary}`
|
|
20868
|
+
);
|
|
20869
|
+
}
|
|
20870
|
+
lines.push("");
|
|
20871
|
+
}
|
|
20872
|
+
function appendQuotes(lines, values) {
|
|
20873
|
+
if (!values || values.length === 0) return;
|
|
20874
|
+
lines.push("## Quotes", "");
|
|
20875
|
+
for (const item of values) {
|
|
20876
|
+
lines.push(`- ${item.speaker ? `${item.speaker}: ` : ""}"${item.text}"`);
|
|
20877
|
+
}
|
|
20878
|
+
lines.push("");
|
|
20879
|
+
}
|
|
20880
|
+
function audioMetadata(audio) {
|
|
20881
|
+
return {
|
|
20882
|
+
...audio.contentType ? { contentType: audio.contentType } : {},
|
|
20883
|
+
...audio.contentLength !== void 0 ? { contentLength: audio.contentLength } : {},
|
|
20884
|
+
reused: audio.reused
|
|
20885
|
+
};
|
|
20886
|
+
}
|
|
20887
|
+
function formatTimestamp(ms) {
|
|
20888
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
20889
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
20890
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
20891
|
+
const seconds = totalSeconds % 60;
|
|
20892
|
+
const mm = String(minutes).padStart(2, "0");
|
|
20893
|
+
const ss = String(seconds).padStart(2, "0");
|
|
20894
|
+
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
20895
|
+
}
|
|
20896
|
+
|
|
20397
20897
|
// src/progressStepper.ts
|
|
20398
20898
|
var STEP_DEFS = [
|
|
20399
20899
|
{ key: "check", label: "Check" },
|
|
@@ -20888,6 +21388,28 @@ Next:
|
|
|
20888
21388
|
}
|
|
20889
21389
|
return;
|
|
20890
21390
|
}
|
|
21391
|
+
if (command === "recordings export" && isRecord4(data)) {
|
|
21392
|
+
opts.stdout("Recording export ready\n");
|
|
21393
|
+
printStringField(opts, data, "recordingId");
|
|
21394
|
+
printStringField(opts, data, "exportDir");
|
|
21395
|
+
printStringField(opts, data, "textPath");
|
|
21396
|
+
printStringField(opts, data, "audioPath");
|
|
21397
|
+
printStringField(opts, data, "transcriptPath");
|
|
21398
|
+
printStringField(opts, data, "summaryPath");
|
|
21399
|
+
printStringField(opts, data, "actionItemsPath");
|
|
21400
|
+
printStringField(opts, data, "subscriptionPath");
|
|
21401
|
+
printStringField(opts, data, "transcriptJsonPath");
|
|
21402
|
+
printStringField(opts, data, "summaryJsonPath");
|
|
21403
|
+
printStringField(opts, data, "subscriptionJsonPath");
|
|
21404
|
+
printStringField(opts, data, "recordingJsonPath");
|
|
21405
|
+
printStringField(opts, data, "remoteManifestPath");
|
|
21406
|
+
printStringField(opts, data, "sessionMetadataPath");
|
|
21407
|
+
printStringField(opts, data, "manifestPath");
|
|
21408
|
+
if (typeof data.transcriptPath !== "string") {
|
|
21409
|
+
opts.stdout(" transcript: no active transcript\n");
|
|
21410
|
+
}
|
|
21411
|
+
return;
|
|
21412
|
+
}
|
|
20891
21413
|
if (command === "recordings retranscribe" && isRecord4(data)) {
|
|
20892
21414
|
opts.stdout("Transcription started\n");
|
|
20893
21415
|
if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
|
|
@@ -21265,6 +21787,10 @@ function recordingTitle(item) {
|
|
|
21265
21787
|
function numberText(value) {
|
|
21266
21788
|
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "0";
|
|
21267
21789
|
}
|
|
21790
|
+
function printStringField(opts, data, field) {
|
|
21791
|
+
if (typeof data[field] === "string") opts.stdout(` ${field}: ${data[field]}
|
|
21792
|
+
`);
|
|
21793
|
+
}
|
|
21268
21794
|
function formatDurationMs(ms) {
|
|
21269
21795
|
return formatClock(ms / 1e3);
|
|
21270
21796
|
}
|
|
@@ -21381,6 +21907,7 @@ var COMMAND_DATA_SCHEMAS = {
|
|
|
21381
21907
|
upload: uploadBatchDataSchema,
|
|
21382
21908
|
record: recordCommandDataSchema,
|
|
21383
21909
|
"recordings get": recordingDataSchema,
|
|
21910
|
+
"recordings export": recordingExportDataSchema,
|
|
21384
21911
|
"recordings list": recordingListDataSchema,
|
|
21385
21912
|
"recordings retranscribe": recordingTranscribeDataSchema,
|
|
21386
21913
|
"recordings resummarize": recordingSummarizeDataSchema,
|
|
@@ -21419,11 +21946,11 @@ function buildSchemaDocument(program) {
|
|
|
21419
21946
|
event: toJsonSchema(operationEventSchema)
|
|
21420
21947
|
};
|
|
21421
21948
|
}
|
|
21422
|
-
function walkCommands(command,
|
|
21949
|
+
function walkCommands(command, path7, out) {
|
|
21423
21950
|
for (const sub of subcommandsOf(command)) {
|
|
21424
21951
|
const name = sub.name();
|
|
21425
21952
|
if (name === "help") continue;
|
|
21426
|
-
const fullPath = [...
|
|
21953
|
+
const fullPath = [...path7, name];
|
|
21427
21954
|
const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
|
|
21428
21955
|
if (children.length === 0) {
|
|
21429
21956
|
out.push(leafCommandDoc(sub, fullPath.join(" ")));
|
|
@@ -21825,8 +22352,8 @@ function requestLaunchServicesSidecarShutdown(input) {
|
|
|
21825
22352
|
} catch {
|
|
21826
22353
|
}
|
|
21827
22354
|
}
|
|
21828
|
-
function createFifo(
|
|
21829
|
-
const result = spawnSync("mkfifo", [
|
|
22355
|
+
function createFifo(path7) {
|
|
22356
|
+
const result = spawnSync("mkfifo", [path7], { encoding: "utf8" });
|
|
21830
22357
|
if (result.status !== 0) {
|
|
21831
22358
|
throw cliError("record.helper_unavailable", "Recappi recording helper could not start.", {
|
|
21832
22359
|
hint: result.stderr || "Could not create the local recorder pipes. Try again."
|
|
@@ -22450,9 +22977,9 @@ function resolveSidecarCommand(opts) {
|
|
|
22450
22977
|
hint: `No bundled helper is registered for ${platform}. Set ${SIDECAR_COMMAND_ENV} to a compatible helper when one is available.`
|
|
22451
22978
|
});
|
|
22452
22979
|
}
|
|
22453
|
-
function ensureBundledHelperExecutable(
|
|
22454
|
-
if (process.platform === "darwin" &&
|
|
22455
|
-
const stableApp = ensureStableDarwinHelperApp(
|
|
22980
|
+
function ensureBundledHelperExecutable(path7, opts = {}) {
|
|
22981
|
+
if (process.platform === "darwin" && path7.endsWith(".app")) {
|
|
22982
|
+
const stableApp = ensureStableDarwinHelperApp(path7, opts);
|
|
22456
22983
|
const executable = darwinAppExecutablePath(stableApp);
|
|
22457
22984
|
if (!existsSync(executable)) {
|
|
22458
22985
|
throw cliError("record.helper_unavailable", "Recappi recording helper is not available.", {
|
|
@@ -22462,19 +22989,19 @@ function ensureBundledHelperExecutable(path6, opts = {}) {
|
|
|
22462
22989
|
ensureExecutableMode(executable);
|
|
22463
22990
|
return stableApp;
|
|
22464
22991
|
}
|
|
22465
|
-
if (process.platform === "win32") return
|
|
22466
|
-
ensureExecutableMode(
|
|
22467
|
-
return
|
|
22992
|
+
if (process.platform === "win32") return path7;
|
|
22993
|
+
ensureExecutableMode(path7);
|
|
22994
|
+
return path7;
|
|
22468
22995
|
}
|
|
22469
|
-
function ensureExecutableMode(
|
|
22470
|
-
const mode = statSync(
|
|
22996
|
+
function ensureExecutableMode(path7) {
|
|
22997
|
+
const mode = statSync(path7).mode;
|
|
22471
22998
|
if ((mode & 73) !== 0) return;
|
|
22472
22999
|
try {
|
|
22473
|
-
chmodSync(
|
|
23000
|
+
chmodSync(path7, mode | 493);
|
|
22474
23001
|
} catch (error51) {
|
|
22475
23002
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
22476
23003
|
throw cliError("record.helper_unavailable", "Recappi recording helper is not executable.", {
|
|
22477
|
-
hint: `Could not make bundled helper executable at ${
|
|
23004
|
+
hint: `Could not make bundled helper executable at ${path7}: ${message}. Reinstall recappi, or set ${SIDECAR_COMMAND_ENV} to a compatible helper.`
|
|
22478
23005
|
});
|
|
22479
23006
|
}
|
|
22480
23007
|
}
|
|
@@ -22519,14 +23046,14 @@ function helperSourceSignature(sourceApp) {
|
|
|
22519
23046
|
codeSignature: existsSync(signaturePath) ? fileDigest(signaturePath) : null
|
|
22520
23047
|
});
|
|
22521
23048
|
}
|
|
22522
|
-
function fileDigest(
|
|
23049
|
+
function fileDigest(path7) {
|
|
22523
23050
|
const hash2 = createHash("sha256");
|
|
22524
|
-
hash2.update(readFileSync2(
|
|
23051
|
+
hash2.update(readFileSync2(path7));
|
|
22525
23052
|
return hash2.digest("hex");
|
|
22526
23053
|
}
|
|
22527
|
-
function readTextIfExists(
|
|
23054
|
+
function readTextIfExists(path7) {
|
|
22528
23055
|
try {
|
|
22529
|
-
return readFileSync2(
|
|
23056
|
+
return readFileSync2(path7, "utf8");
|
|
22530
23057
|
} catch {
|
|
22531
23058
|
return null;
|
|
22532
23059
|
}
|
|
@@ -22889,6 +23416,26 @@ async function runCli(deps = {}) {
|
|
|
22889
23416
|
},
|
|
22890
23417
|
retranscribeRecording: (recordingId, options = {}) => client.transcribeRecording({ recordingId, ...options }),
|
|
22891
23418
|
resummarizeRecording: (recordingId) => client.summarizeRecording({ recordingId }),
|
|
23419
|
+
syncRecordingText: (recordingId) => syncRecordingText({
|
|
23420
|
+
recordingId,
|
|
23421
|
+
client,
|
|
23422
|
+
env: deps.env,
|
|
23423
|
+
homeDir: deps.homeDir
|
|
23424
|
+
}),
|
|
23425
|
+
syncRecordingAudio: (recordingId) => syncRecordingAudio({
|
|
23426
|
+
recordingId,
|
|
23427
|
+
client,
|
|
23428
|
+
recordingAudio,
|
|
23429
|
+
env: deps.env,
|
|
23430
|
+
homeDir: deps.homeDir
|
|
23431
|
+
}),
|
|
23432
|
+
exportRecording: (recordingId) => exportRecording({
|
|
23433
|
+
recordingId,
|
|
23434
|
+
client,
|
|
23435
|
+
recordingAudio,
|
|
23436
|
+
env: deps.env,
|
|
23437
|
+
homeDir: deps.homeDir
|
|
23438
|
+
}),
|
|
22892
23439
|
initialView: parsed.initialView
|
|
22893
23440
|
});
|
|
22894
23441
|
return 0;
|
|
@@ -22919,7 +23466,7 @@ async function runCli(deps = {}) {
|
|
|
22919
23466
|
return 0;
|
|
22920
23467
|
}
|
|
22921
23468
|
if (parsed.kind === "auth-logout") {
|
|
22922
|
-
const cleared = await clearAuthConfig(deps.homeDir ??
|
|
23469
|
+
const cleared = await clearAuthConfig(deps.homeDir ?? os6.homedir());
|
|
22923
23470
|
renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render3);
|
|
22924
23471
|
return 0;
|
|
22925
23472
|
}
|
|
@@ -22930,7 +23477,7 @@ async function runCli(deps = {}) {
|
|
|
22930
23477
|
hint: keychain.hint ?? "Run recappi auth login instead."
|
|
22931
23478
|
});
|
|
22932
23479
|
}
|
|
22933
|
-
await saveAuthConfig(deps.homeDir ??
|
|
23480
|
+
await saveAuthConfig(deps.homeDir ?? os6.homedir(), {
|
|
22934
23481
|
origin: auth.origin,
|
|
22935
23482
|
token: keychain.token
|
|
22936
23483
|
});
|
|
@@ -23079,6 +23626,29 @@ async function runCli(deps = {}) {
|
|
|
23079
23626
|
renderSuccess("recordings get", data, render3);
|
|
23080
23627
|
return 0;
|
|
23081
23628
|
}
|
|
23629
|
+
if (parsed.kind === "recordings-export") {
|
|
23630
|
+
const status = await client.authStatus();
|
|
23631
|
+
if (!status.loggedIn || !status.userId) {
|
|
23632
|
+
throw cliError("auth.not_logged_in", "Sign in before exporting a recording bundle.", {
|
|
23633
|
+
hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
|
|
23634
|
+
});
|
|
23635
|
+
}
|
|
23636
|
+
const recordingAudio = createRecordingAudioRuntime(client, {
|
|
23637
|
+
account: { backendOrigin: auth.origin, userId: status.userId },
|
|
23638
|
+
env: deps.env,
|
|
23639
|
+
homeDir: deps.homeDir
|
|
23640
|
+
});
|
|
23641
|
+
const data = await exportRecording({
|
|
23642
|
+
recordingId: parsed.recordingId,
|
|
23643
|
+
...parsed.directory ? { directory: parsed.directory } : {},
|
|
23644
|
+
client,
|
|
23645
|
+
recordingAudio,
|
|
23646
|
+
env: deps.env,
|
|
23647
|
+
homeDir: deps.homeDir
|
|
23648
|
+
});
|
|
23649
|
+
renderSuccess("recordings export", data, render3);
|
|
23650
|
+
return 0;
|
|
23651
|
+
}
|
|
23082
23652
|
if (parsed.kind === "recordings-retranscribe") {
|
|
23083
23653
|
const eventMode = parsed.options.mode === "jsonl" ? "jsonl" : "human";
|
|
23084
23654
|
const data = await client.transcribeRecording({
|
|
@@ -23434,6 +24004,20 @@ Agent mode:
|
|
|
23434
24004
|
});
|
|
23435
24005
|
}
|
|
23436
24006
|
);
|
|
24007
|
+
const recordingsExport = recordings.command("export <recordingId>").description("Export audio, transcript, summary, and subscription/account files for agents").option("--dir <dir>", "directory for exported files", parseStringOption("--dir")).addHelpText("after", commandMetadataHelpText("recordings export"));
|
|
24008
|
+
addCommonOptions(recordingsExport);
|
|
24009
|
+
recordingsExport.action(
|
|
24010
|
+
(recordingId, _options, command) => {
|
|
24011
|
+
const opts = command.opts();
|
|
24012
|
+
onSelect({
|
|
24013
|
+
kind: "recordings-export",
|
|
24014
|
+
options: collectGlobalOptions(command),
|
|
24015
|
+
commandName: "recordings export",
|
|
24016
|
+
recordingId,
|
|
24017
|
+
...typeof opts.dir === "string" ? { directory: opts.dir } : {}
|
|
24018
|
+
});
|
|
24019
|
+
}
|
|
24020
|
+
);
|
|
23437
24021
|
const recordingsRetranscribe = recordings.command("retranscribe <recordingId>").description("Start a fresh transcription job for an existing recording").option("--language <lang>", "transcription language hint", parseStringOption("--language")).option("--provider <name>", "transcription provider", parseStringOption("--provider")).option("--model <name>", "transcription model", parseStringOption("--model")).option("--prompt <text>", "custom transcription prompt/context", parseStringOption("--prompt")).option("--scene <id>", "transcription scene preset", parseStringOption("--scene")).option("--wait", "wait for the transcription job to reach a terminal state").addHelpText("after", commandMetadataHelpText("recordings retranscribe"));
|
|
23438
24022
|
addCommonOptions(recordingsRetranscribe);
|
|
23439
24023
|
recordingsRetranscribe.action(
|
|
@@ -23608,7 +24192,8 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
23608
24192
|
"--status",
|
|
23609
24193
|
"--limit",
|
|
23610
24194
|
"--cursor",
|
|
23611
|
-
"--search"
|
|
24195
|
+
"--search",
|
|
24196
|
+
"--dir"
|
|
23612
24197
|
]);
|
|
23613
24198
|
function hasCommandToken(argv) {
|
|
23614
24199
|
return commandTokens(argv).length > 0;
|