recappi 0.1.74 → 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 CHANGED
@@ -3001,6 +3001,9 @@ async function runDashboard(deps) {
3001
3001
  transcribeRecordingArtifact: deps.transcribeRecordingArtifact,
3002
3002
  onRetranscribe: deps.retranscribeRecording,
3003
3003
  onResummarize: deps.resummarizeRecording,
3004
+ onSyncRecordingText: deps.syncRecordingText,
3005
+ onSyncRecordingAudio: deps.syncRecordingAudio,
3006
+ onExportRecording: deps.exportRecording,
3004
3007
  initialView: deps.initialView ?? "overview",
3005
3008
  openUrl,
3006
3009
  copyText
@@ -3028,7 +3031,7 @@ var init_tui = __esm({
3028
3031
 
3029
3032
  // src/cli.ts
3030
3033
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
3031
- import os5 from "os";
3034
+ import os6 from "os";
3032
3035
 
3033
3036
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
3034
3037
  var external_exports = {};
@@ -3796,10 +3799,10 @@ function mergeDefs(...defs) {
3796
3799
  function cloneDef(schema) {
3797
3800
  return mergeDefs(schema._zod.def);
3798
3801
  }
3799
- function getElementAtPath(obj, path6) {
3800
- if (!path6)
3802
+ function getElementAtPath(obj, path7) {
3803
+ if (!path7)
3801
3804
  return obj;
3802
- return path6.reduce((acc, key) => acc?.[key], obj);
3805
+ return path7.reduce((acc, key) => acc?.[key], obj);
3803
3806
  }
3804
3807
  function promiseAllObject(promisesObj) {
3805
3808
  const keys = Object.keys(promisesObj);
@@ -4208,11 +4211,11 @@ function explicitlyAborted(x, startIndex = 0) {
4208
4211
  }
4209
4212
  return false;
4210
4213
  }
4211
- function prefixIssues(path6, issues) {
4214
+ function prefixIssues(path7, issues) {
4212
4215
  return issues.map((iss) => {
4213
4216
  var _a3;
4214
4217
  (_a3 = iss).path ?? (_a3.path = []);
4215
- iss.path.unshift(path6);
4218
+ iss.path.unshift(path7);
4216
4219
  return iss;
4217
4220
  });
4218
4221
  }
@@ -4359,16 +4362,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
4359
4362
  }
4360
4363
  function formatError(error51, mapper = (issue2) => issue2.message) {
4361
4364
  const fieldErrors = { _errors: [] };
4362
- const processError = (error52, path6 = []) => {
4365
+ const processError = (error52, path7 = []) => {
4363
4366
  for (const issue2 of error52.issues) {
4364
4367
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4365
- issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
4368
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
4366
4369
  } else if (issue2.code === "invalid_key") {
4367
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
4370
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
4368
4371
  } else if (issue2.code === "invalid_element") {
4369
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
4372
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
4370
4373
  } else {
4371
- const fullpath = [...path6, ...issue2.path];
4374
+ const fullpath = [...path7, ...issue2.path];
4372
4375
  if (fullpath.length === 0) {
4373
4376
  fieldErrors._errors.push(mapper(issue2));
4374
4377
  } else {
@@ -4395,17 +4398,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
4395
4398
  }
4396
4399
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
4397
4400
  const result = { errors: [] };
4398
- const processError = (error52, path6 = []) => {
4401
+ const processError = (error52, path7 = []) => {
4399
4402
  var _a3, _b;
4400
4403
  for (const issue2 of error52.issues) {
4401
4404
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4402
- issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
4405
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
4403
4406
  } else if (issue2.code === "invalid_key") {
4404
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
4407
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
4405
4408
  } else if (issue2.code === "invalid_element") {
4406
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
4409
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
4407
4410
  } else {
4408
- const fullpath = [...path6, ...issue2.path];
4411
+ const fullpath = [...path7, ...issue2.path];
4409
4412
  if (fullpath.length === 0) {
4410
4413
  result.errors.push(mapper(issue2));
4411
4414
  continue;
@@ -4437,8 +4440,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
4437
4440
  }
4438
4441
  function toDotPath(_path) {
4439
4442
  const segs = [];
4440
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4441
- for (const seg of path6) {
4443
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4444
+ for (const seg of path7) {
4442
4445
  if (typeof seg === "number")
4443
4446
  segs.push(`[${seg}]`);
4444
4447
  else if (typeof seg === "symbol")
@@ -17130,13 +17133,13 @@ function resolveRef(ref, ctx) {
17130
17133
  if (!ref.startsWith("#")) {
17131
17134
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17132
17135
  }
17133
- const path6 = ref.slice(1).split("/").filter(Boolean);
17134
- if (path6.length === 0) {
17136
+ const path7 = ref.slice(1).split("/").filter(Boolean);
17137
+ if (path7.length === 0) {
17135
17138
  return ctx.rootSchema;
17136
17139
  }
17137
17140
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17138
- if (path6[0] === defsKey) {
17139
- const key = path6[1];
17141
+ if (path7[0] === defsKey) {
17142
+ const key = path7[1];
17140
17143
  if (!key || !ctx.defs[key]) {
17141
17144
  throw new Error(`Reference not found: ${ref}`);
17142
17145
  }
@@ -18139,6 +18142,31 @@ var transcriptDataSchema = external_exports.object({
18139
18142
  segments: external_exports.array(transcriptSegmentSchema),
18140
18143
  summary: transcriptSummarySchema
18141
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
+ });
18142
18170
  var doctorCheckStatusSchema = external_exports.enum(["ok", "warn", "error"]);
18143
18171
  var doctorCheckSchema = external_exports.object({
18144
18172
  name: external_exports.string(),
@@ -19401,7 +19429,10 @@ var RecappiApiClient = class {
19401
19429
  const contentLength = numberHeader(response.headers.get("content-length"));
19402
19430
  const dir = opts.directory ?? await fs3.mkdtemp(path4.join(os4.tmpdir(), "recappi-cli-audio-"));
19403
19431
  if (opts.directory) await fs3.mkdir(dir, { recursive: true });
19404
- const filePath = path4.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
19432
+ const filePath = path4.join(
19433
+ dir,
19434
+ recordingAudioFileName(recordingId, opts.title, contentType, opts.filenameStem)
19435
+ );
19405
19436
  try {
19406
19437
  await pipeline(
19407
19438
  Readable.fromWeb(response.body),
@@ -19700,12 +19731,12 @@ var RecappiApiClient = class {
19700
19731
  } : {}
19701
19732
  };
19702
19733
  }
19703
- async getJson(path6) {
19704
- const response = await this.request("GET", path6);
19734
+ async getJson(path7) {
19735
+ const response = await this.request("GET", path7);
19705
19736
  return await parseJson(response);
19706
19737
  }
19707
- async postJson(path6, body) {
19708
- const response = await this.request("POST", path6, JSON.stringify(body), {
19738
+ async postJson(path7, body) {
19739
+ const response = await this.request("POST", path7, JSON.stringify(body), {
19709
19740
  headers: { "content-type": "application/json" }
19710
19741
  });
19711
19742
  return await parseJson(response);
@@ -19799,7 +19830,10 @@ function audioExtensionForContentType(contentType) {
19799
19830
  return "wav";
19800
19831
  }
19801
19832
  }
19802
- 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
+ }
19803
19837
  const idStem = truncateFileStem(safeFileStem(recordingId), 48);
19804
19838
  const titleStem = title ? truncateFileStem(safeFileStem(title), 80) : "";
19805
19839
  const stem = titleStem ? `${titleStem}-${idStem}` : idStem;
@@ -20093,7 +20127,7 @@ import { promises as fs4 } from "fs";
20093
20127
  import path5 from "path";
20094
20128
  function createRecordingAudioRuntime(client, deps = {}) {
20095
20129
  const downloadRecordingAudioFile = async (recordingId, opts) => {
20096
- const cached2 = await findReusableDownload(recordingId, deps);
20130
+ const cached2 = opts?.directory ? null : await findReusableDownload(recordingId, deps);
20097
20131
  if (cached2) return cached2;
20098
20132
  const directory = opts?.directory ?? (deps.account ? defaultDownloadDirectory(deps) : void 0);
20099
20133
  const download = await client.downloadRecordingAudio(recordingId, {
@@ -20249,6 +20283,7 @@ var COMMON_TASKS = [
20249
20283
  { label: "Transcribe a local file", command: "recappi upload <file> --transcribe --wait" },
20250
20284
  { label: "Re-transcribe a recording", command: "recappi recordings retranscribe <recordingId> --wait" },
20251
20285
  { label: "Re-summarize a recording", command: "recappi recordings resummarize <recordingId>" },
20286
+ { label: "Export recording bundle", command: "recappi recordings export <recordingId> --dir ./bundle" },
20252
20287
  { label: "List / find recordings", command: "recappi recordings list" },
20253
20288
  { label: "Read a transcript", command: "recappi transcript get <transcriptId>" },
20254
20289
  { label: "Download / open audio", command: "recappi audio <recordingId> --open" },
@@ -20355,7 +20390,24 @@ var COMMAND_METADATA = {
20355
20390
  "recordings get": {
20356
20391
  capabilities: ["Fetch one recording's metadata and status by id"],
20357
20392
  examples: [{ description: "Fetch one recording", command: "recappi recordings get <recordingId>" }],
20358
- 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"]
20359
20411
  },
20360
20412
  "recordings list": {
20361
20413
  capabilities: ["List recent recordings", "Search recordings and transcripts", "Find a recordingId"],
@@ -20363,7 +20415,7 @@ var COMMAND_METADATA = {
20363
20415
  { description: "List recent recordings", command: "recappi recordings list" },
20364
20416
  { description: "Search recordings and transcripts", command: "recappi recordings list --search <query>" }
20365
20417
  ],
20366
- relatedCommands: ["recordings get", "audio", "transcript get"]
20418
+ relatedCommands: ["recordings get", "recordings export", "audio", "transcript get"]
20367
20419
  },
20368
20420
  "recordings retranscribe": {
20369
20421
  capabilities: ["Re-transcribe an existing recording", "Re-transcribe with new language/prompt/scene/model"],
@@ -20440,6 +20492,408 @@ ${lines.join("\n")}
20440
20492
  ` : "";
20441
20493
  }
20442
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
+
20443
20897
  // src/progressStepper.ts
20444
20898
  var STEP_DEFS = [
20445
20899
  { key: "check", label: "Check" },
@@ -20934,6 +21388,28 @@ Next:
20934
21388
  }
20935
21389
  return;
20936
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
+ }
20937
21413
  if (command === "recordings retranscribe" && isRecord4(data)) {
20938
21414
  opts.stdout("Transcription started\n");
20939
21415
  if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
@@ -21311,6 +21787,10 @@ function recordingTitle(item) {
21311
21787
  function numberText(value) {
21312
21788
  return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "0";
21313
21789
  }
21790
+ function printStringField(opts, data, field) {
21791
+ if (typeof data[field] === "string") opts.stdout(` ${field}: ${data[field]}
21792
+ `);
21793
+ }
21314
21794
  function formatDurationMs(ms) {
21315
21795
  return formatClock(ms / 1e3);
21316
21796
  }
@@ -21427,6 +21907,7 @@ var COMMAND_DATA_SCHEMAS = {
21427
21907
  upload: uploadBatchDataSchema,
21428
21908
  record: recordCommandDataSchema,
21429
21909
  "recordings get": recordingDataSchema,
21910
+ "recordings export": recordingExportDataSchema,
21430
21911
  "recordings list": recordingListDataSchema,
21431
21912
  "recordings retranscribe": recordingTranscribeDataSchema,
21432
21913
  "recordings resummarize": recordingSummarizeDataSchema,
@@ -21465,11 +21946,11 @@ function buildSchemaDocument(program) {
21465
21946
  event: toJsonSchema(operationEventSchema)
21466
21947
  };
21467
21948
  }
21468
- function walkCommands(command, path6, out) {
21949
+ function walkCommands(command, path7, out) {
21469
21950
  for (const sub of subcommandsOf(command)) {
21470
21951
  const name = sub.name();
21471
21952
  if (name === "help") continue;
21472
- const fullPath = [...path6, name];
21953
+ const fullPath = [...path7, name];
21473
21954
  const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
21474
21955
  if (children.length === 0) {
21475
21956
  out.push(leafCommandDoc(sub, fullPath.join(" ")));
@@ -21871,8 +22352,8 @@ function requestLaunchServicesSidecarShutdown(input) {
21871
22352
  } catch {
21872
22353
  }
21873
22354
  }
21874
- function createFifo(path6) {
21875
- const result = spawnSync("mkfifo", [path6], { encoding: "utf8" });
22355
+ function createFifo(path7) {
22356
+ const result = spawnSync("mkfifo", [path7], { encoding: "utf8" });
21876
22357
  if (result.status !== 0) {
21877
22358
  throw cliError("record.helper_unavailable", "Recappi recording helper could not start.", {
21878
22359
  hint: result.stderr || "Could not create the local recorder pipes. Try again."
@@ -22496,9 +22977,9 @@ function resolveSidecarCommand(opts) {
22496
22977
  hint: `No bundled helper is registered for ${platform}. Set ${SIDECAR_COMMAND_ENV} to a compatible helper when one is available.`
22497
22978
  });
22498
22979
  }
22499
- function ensureBundledHelperExecutable(path6, opts = {}) {
22500
- if (process.platform === "darwin" && path6.endsWith(".app")) {
22501
- const stableApp = ensureStableDarwinHelperApp(path6, opts);
22980
+ function ensureBundledHelperExecutable(path7, opts = {}) {
22981
+ if (process.platform === "darwin" && path7.endsWith(".app")) {
22982
+ const stableApp = ensureStableDarwinHelperApp(path7, opts);
22502
22983
  const executable = darwinAppExecutablePath(stableApp);
22503
22984
  if (!existsSync(executable)) {
22504
22985
  throw cliError("record.helper_unavailable", "Recappi recording helper is not available.", {
@@ -22508,19 +22989,19 @@ function ensureBundledHelperExecutable(path6, opts = {}) {
22508
22989
  ensureExecutableMode(executable);
22509
22990
  return stableApp;
22510
22991
  }
22511
- if (process.platform === "win32") return path6;
22512
- ensureExecutableMode(path6);
22513
- return path6;
22992
+ if (process.platform === "win32") return path7;
22993
+ ensureExecutableMode(path7);
22994
+ return path7;
22514
22995
  }
22515
- function ensureExecutableMode(path6) {
22516
- const mode = statSync(path6).mode;
22996
+ function ensureExecutableMode(path7) {
22997
+ const mode = statSync(path7).mode;
22517
22998
  if ((mode & 73) !== 0) return;
22518
22999
  try {
22519
- chmodSync(path6, mode | 493);
23000
+ chmodSync(path7, mode | 493);
22520
23001
  } catch (error51) {
22521
23002
  const message = error51 instanceof Error ? error51.message : String(error51);
22522
23003
  throw cliError("record.helper_unavailable", "Recappi recording helper is not executable.", {
22523
- hint: `Could not make bundled helper executable at ${path6}: ${message}. Reinstall recappi, or set ${SIDECAR_COMMAND_ENV} to a compatible helper.`
23004
+ hint: `Could not make bundled helper executable at ${path7}: ${message}. Reinstall recappi, or set ${SIDECAR_COMMAND_ENV} to a compatible helper.`
22524
23005
  });
22525
23006
  }
22526
23007
  }
@@ -22565,14 +23046,14 @@ function helperSourceSignature(sourceApp) {
22565
23046
  codeSignature: existsSync(signaturePath) ? fileDigest(signaturePath) : null
22566
23047
  });
22567
23048
  }
22568
- function fileDigest(path6) {
23049
+ function fileDigest(path7) {
22569
23050
  const hash2 = createHash("sha256");
22570
- hash2.update(readFileSync2(path6));
23051
+ hash2.update(readFileSync2(path7));
22571
23052
  return hash2.digest("hex");
22572
23053
  }
22573
- function readTextIfExists(path6) {
23054
+ function readTextIfExists(path7) {
22574
23055
  try {
22575
- return readFileSync2(path6, "utf8");
23056
+ return readFileSync2(path7, "utf8");
22576
23057
  } catch {
22577
23058
  return null;
22578
23059
  }
@@ -22935,6 +23416,26 @@ async function runCli(deps = {}) {
22935
23416
  },
22936
23417
  retranscribeRecording: (recordingId, options = {}) => client.transcribeRecording({ recordingId, ...options }),
22937
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
+ }),
22938
23439
  initialView: parsed.initialView
22939
23440
  });
22940
23441
  return 0;
@@ -22965,7 +23466,7 @@ async function runCli(deps = {}) {
22965
23466
  return 0;
22966
23467
  }
22967
23468
  if (parsed.kind === "auth-logout") {
22968
- const cleared = await clearAuthConfig(deps.homeDir ?? os5.homedir());
23469
+ const cleared = await clearAuthConfig(deps.homeDir ?? os6.homedir());
22969
23470
  renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render3);
22970
23471
  return 0;
22971
23472
  }
@@ -22976,7 +23477,7 @@ async function runCli(deps = {}) {
22976
23477
  hint: keychain.hint ?? "Run recappi auth login instead."
22977
23478
  });
22978
23479
  }
22979
- await saveAuthConfig(deps.homeDir ?? os5.homedir(), {
23480
+ await saveAuthConfig(deps.homeDir ?? os6.homedir(), {
22980
23481
  origin: auth.origin,
22981
23482
  token: keychain.token
22982
23483
  });
@@ -23125,6 +23626,29 @@ async function runCli(deps = {}) {
23125
23626
  renderSuccess("recordings get", data, render3);
23126
23627
  return 0;
23127
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
+ }
23128
23652
  if (parsed.kind === "recordings-retranscribe") {
23129
23653
  const eventMode = parsed.options.mode === "jsonl" ? "jsonl" : "human";
23130
23654
  const data = await client.transcribeRecording({
@@ -23480,6 +24004,20 @@ Agent mode:
23480
24004
  });
23481
24005
  }
23482
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
+ );
23483
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"));
23484
24022
  addCommonOptions(recordingsRetranscribe);
23485
24023
  recordingsRetranscribe.action(
@@ -23654,7 +24192,8 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
23654
24192
  "--status",
23655
24193
  "--limit",
23656
24194
  "--cursor",
23657
- "--search"
24195
+ "--search",
24196
+ "--dir"
23658
24197
  ]);
23659
24198
  function hasCommandToken(argv) {
23660
24199
  return commandTokens(argv).length > 0;