@arcreflex/agent-transcripts 0.1.5 → 0.1.8

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.
@@ -9,10 +9,15 @@ jobs:
9
9
  publish:
10
10
  runs-on: ubuntu-latest
11
11
  permissions:
12
+ contents: read
12
13
  id-token: write
13
14
  steps:
14
15
  - uses: actions/checkout@v4
15
16
 
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: "24"
20
+
16
21
  - uses: oven-sh/setup-bun@v2
17
22
 
18
23
  - run: bun install
package/README.md CHANGED
@@ -13,16 +13,16 @@ CLI tool that transforms AI coding agent session files into readable transcripts
13
13
  ```
14
14
  src/
15
15
  cli.ts # CLI entry point, subcommand routing
16
- parse.ts # Source → intermediate JSON
17
- render.ts # Intermediate JSON → markdown
16
+ parse.ts # Source → intermediate format
17
+ render.ts # Intermediate format → markdown
18
18
  convert.ts # Full pipeline with provenance tracking
19
19
  sync.ts # Batch sync sessions → markdown
20
20
  types.ts # Core types (Transcript, Message, Adapter)
21
21
  adapters/ # Source format adapters (currently: claude-code)
22
22
  utils/
23
- naming.ts # Descriptive output file naming
24
- provenance.ts # Source tracking via YAML front matter
25
- summary.ts # Summary extraction
23
+ naming.ts # Deterministic output file naming
24
+ provenance.ts # Source tracking via transcripts.json + YAML front matter
25
+ summary.ts # Tool call summary extraction
26
26
  test/
27
27
  fixtures/ # Snapshot test inputs/outputs
28
28
  snapshots.test.ts
@@ -40,34 +40,50 @@ bun run format # auto-format
40
40
 
41
41
  ```bash
42
42
  # Subcommands (convert is default if omitted)
43
- agent-transcripts convert <file> # Full pipeline: parse render
44
- agent-transcripts parse <file> # Source intermediate JSON
45
- agent-transcripts render <file> # JSON markdown
46
- agent-transcripts sync <dir> -o <out> # Batch sync sessions
43
+ agent-transcripts convert <file> # Parse and render to stdout
44
+ agent-transcripts convert <file> -o <dir> # Parse and render to directory
45
+ agent-transcripts sync <dir> -o <out> # Batch sync sessions
47
46
 
48
47
  # Use "-" for stdin
49
48
  cat session.jsonl | agent-transcripts -
50
-
51
- # Environment variables
52
- OPENROUTER_API_KEY=... # Enables LLM-based descriptive output naming
53
49
  ```
54
50
 
55
51
  ## Architecture
56
52
 
57
- Two-stage pipeline: Parse (source → JSON) → Render (JSON → markdown).
53
+ Two-stage pipeline: Parse (source → intermediate) → Render (intermediate → markdown).
58
54
 
59
55
  - Adapters handle source formats (see `src/adapters/index.ts` for registry)
60
56
  - Auto-detection: paths containing `.claude/` → claude-code adapter
61
57
  - Branching conversations preserved via `parentMessageRef` on messages
62
- - Provenance tracking: rendered markdown includes YAML front matter with source path
63
- - Descriptive naming: output files named by date + summary (LLM-enhanced if API key set)
64
- - Sync uses mtime to skip unchanged sources
58
+ - Provenance tracking via `transcripts.json` index + YAML front matter
59
+ - Deterministic naming: `{datetime}-{sessionId}.md`
60
+ - Sync uses sessions-index.json for discovery (claude-code), skipping subagent files
61
+ - Sync uses mtime via index to skip unchanged sources
62
+
63
+ ### transcripts.json
64
+
65
+ The index file tracks the relationship between source files and outputs:
66
+
67
+ ```typescript
68
+ interface TranscriptsIndex {
69
+ version: 1;
70
+ entries: {
71
+ [outputFilename: string]: {
72
+ source: string; // absolute path to source
73
+ sourceMtime: number; // ms since epoch
74
+ sessionId: string; // full session ID from filename
75
+ segmentIndex?: number; // for multi-transcript sources (1-indexed)
76
+ syncedAt: string; // ISO timestamp
77
+ };
78
+ };
79
+ }
80
+ ```
65
81
 
66
82
  ## Key Types
67
83
 
68
84
  - `Transcript`: source info, warnings, messages array
69
85
  - `Message`: union of UserMessage | AssistantMessage | SystemMessage | ToolCallGroup | ErrorMessage
70
- - `Adapter`: name, file patterns, parse function
86
+ - `Adapter`: name, discover function, parse function
71
87
 
72
88
  ## Adding an Adapter
73
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcreflex/agent-transcripts",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Transform AI coding agent session files into readable transcripts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -4,8 +4,12 @@
4
4
  * Parses session files from ~/.claude/projects/{project}/sessions/{session}.jsonl
5
5
  */
6
6
 
7
+ import { Glob } from "bun";
8
+ import { basename, join, relative } from "path";
9
+ import { stat } from "fs/promises";
7
10
  import type {
8
11
  Adapter,
12
+ DiscoveredSession,
9
13
  Transcript,
10
14
  Message,
11
15
  Warning,
@@ -13,6 +17,21 @@ import type {
13
17
  } from "../types.ts";
14
18
  import { extractToolSummary } from "../utils/summary.ts";
15
19
 
20
+ /**
21
+ * Claude Code sessions-index.json structure.
22
+ */
23
+ interface SessionsIndex {
24
+ version: number;
25
+ entries: SessionIndexEntry[];
26
+ }
27
+
28
+ interface SessionIndexEntry {
29
+ sessionId: string;
30
+ fullPath: string;
31
+ fileMtime: number;
32
+ isSidechain: boolean;
33
+ }
34
+
16
35
  // Claude Code JSONL record types
17
36
  interface ClaudeRecord {
18
37
  type: string;
@@ -333,9 +352,86 @@ function transformConversation(
333
352
  };
334
353
  }
335
354
 
355
+ /**
356
+ * Discover sessions from sessions-index.json.
357
+ * Returns undefined if index doesn't exist or is invalid.
358
+ */
359
+ async function discoverFromIndex(
360
+ source: string,
361
+ ): Promise<DiscoveredSession[] | undefined> {
362
+ const indexPath = join(source, "sessions-index.json");
363
+
364
+ try {
365
+ const content = await Bun.file(indexPath).text();
366
+ const index: SessionsIndex = JSON.parse(content);
367
+
368
+ if (index.version !== 1 || !Array.isArray(index.entries)) {
369
+ return undefined;
370
+ }
371
+
372
+ const sessions: DiscoveredSession[] = [];
373
+
374
+ for (const entry of index.entries) {
375
+ // Skip sidechains (subagents)
376
+ if (entry.isSidechain) continue;
377
+
378
+ // Verify the file exists and get current mtime
379
+ try {
380
+ const fileStat = await stat(entry.fullPath);
381
+ sessions.push({
382
+ path: entry.fullPath,
383
+ relativePath:
384
+ relative(source, entry.fullPath) || basename(entry.fullPath),
385
+ mtime: fileStat.mtime.getTime(),
386
+ });
387
+ } catch {
388
+ // Skip files that no longer exist
389
+ }
390
+ }
391
+
392
+ return sessions;
393
+ } catch {
394
+ // Index doesn't exist or is invalid
395
+ return undefined;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Discover sessions via glob pattern fallback.
401
+ */
402
+ async function discoverByGlob(source: string): Promise<DiscoveredSession[]> {
403
+ const sessions: DiscoveredSession[] = [];
404
+ const glob = new Glob("**/*.jsonl");
405
+
406
+ for await (const file of glob.scan({ cwd: source, absolute: false })) {
407
+ // Skip files in subagents directories
408
+ if (file.includes("/subagents/")) continue;
409
+
410
+ const fullPath = join(source, file);
411
+
412
+ try {
413
+ const fileStat = await stat(fullPath);
414
+ sessions.push({
415
+ path: fullPath,
416
+ relativePath: file,
417
+ mtime: fileStat.mtime.getTime(),
418
+ });
419
+ } catch {
420
+ // Skip files we can't stat
421
+ }
422
+ }
423
+
424
+ return sessions;
425
+ }
426
+
336
427
  export const claudeCodeAdapter: Adapter = {
337
428
  name: "claude-code",
338
- filePatterns: ["*.jsonl"],
429
+
430
+ async discover(source: string): Promise<DiscoveredSession[]> {
431
+ // Try index-based discovery first, fall back to glob
432
+ const fromIndex = await discoverFromIndex(source);
433
+ return fromIndex ?? (await discoverByGlob(source));
434
+ },
339
435
 
340
436
  parse(content: string, sourcePath: string): Transcript[] {
341
437
  const { records, warnings } = parseJsonl(content);
package/src/cli.ts CHANGED
@@ -12,14 +12,11 @@ import {
12
12
  positional,
13
13
  flag,
14
14
  } from "cmd-ts";
15
- import { parse, parseToTranscripts } from "./parse.ts";
16
- import { render, renderTranscript } from "./render.ts";
15
+ import { parseToTranscripts } from "./parse.ts";
16
+ import { renderTranscript } from "./render.ts";
17
17
  import { sync } from "./sync.ts";
18
18
  import { convertToDirectory } from "./convert.ts";
19
19
 
20
- // Read OpenRouter API key from environment for LLM-based slug generation
21
- const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
22
-
23
20
  // Shared options
24
21
  const inputArg = positional({
25
22
  type: string,
@@ -31,7 +28,7 @@ const outputOpt = option({
31
28
  type: optional(string),
32
29
  long: "output",
33
30
  short: "o",
34
- description: "Output path (prints to stdout if not specified)",
31
+ description: "Output directory (prints to stdout if not specified)",
35
32
  });
36
33
 
37
34
  const adapterOpt = option({
@@ -47,46 +44,6 @@ const headOpt = option({
47
44
  description: "Render branch ending at this message ID (default: latest)",
48
45
  });
49
46
 
50
- // Parse subcommand
51
- const parseCmd = command({
52
- name: "parse",
53
- description: "Parse source format to intermediate JSON",
54
- args: {
55
- input: inputArg,
56
- output: outputOpt,
57
- adapter: adapterOpt,
58
- },
59
- async handler({ input, output, adapter }) {
60
- const naming = OPENROUTER_API_KEY
61
- ? { apiKey: OPENROUTER_API_KEY }
62
- : undefined;
63
-
64
- if (output) {
65
- await parse({ input, output, adapter, naming });
66
- } else {
67
- // Print JSONL to stdout (one transcript per line)
68
- const { transcripts } = await parseToTranscripts({ input, adapter });
69
- for (const transcript of transcripts) {
70
- console.log(JSON.stringify(transcript));
71
- }
72
- }
73
- },
74
- });
75
-
76
- // Render subcommand
77
- const renderCmd = command({
78
- name: "render",
79
- description: "Render intermediate JSON to markdown",
80
- args: {
81
- input: inputArg,
82
- output: outputOpt,
83
- head: headOpt,
84
- },
85
- async handler({ input, output, head }) {
86
- await render({ input, output, head });
87
- },
88
- });
89
-
90
47
  // Sync subcommand
91
48
  const syncCmd = command({
92
49
  name: "sync",
@@ -115,10 +72,7 @@ const syncCmd = command({
115
72
  }),
116
73
  },
117
74
  async handler({ source, output, force, quiet }) {
118
- const naming = OPENROUTER_API_KEY
119
- ? { apiKey: OPENROUTER_API_KEY }
120
- : undefined;
121
- await sync({ source, output, force, quiet, naming });
75
+ await sync({ source, output, force, quiet });
122
76
  },
123
77
  });
124
78
 
@@ -140,26 +94,20 @@ const convertCmd = command({
140
94
  head: headOpt,
141
95
  },
142
96
  async handler({ input, output, adapter, head }) {
143
- const naming = OPENROUTER_API_KEY
144
- ? { apiKey: OPENROUTER_API_KEY }
145
- : undefined;
146
-
147
97
  if (output && isDirectoryOutput(output)) {
148
- // Directory output: use sync-like behavior with provenance tracking
98
+ // Directory output: use provenance tracking
149
99
  await convertToDirectory({
150
100
  input,
151
101
  outputDir: output,
152
102
  adapter,
153
103
  head,
154
- naming,
155
104
  });
156
105
  } else if (output) {
157
- // Explicit file output: write intermediate JSON and markdown
158
- const { outputPaths } = await parse({ input, output, adapter, naming });
159
- for (const jsonPath of outputPaths) {
160
- const mdPath = jsonPath.replace(/\.json$/, ".md");
161
- await render({ input: jsonPath, output: mdPath, head });
162
- }
106
+ // Explicit file output: not supported anymore (use directory)
107
+ console.error(
108
+ "Error: Explicit file output not supported. Use a directory path instead.",
109
+ );
110
+ process.exit(1);
163
111
  } else {
164
112
  // No output: stream to stdout
165
113
  const { transcripts } = await parseToTranscripts({ input, adapter });
@@ -171,7 +119,7 @@ const convertCmd = command({
171
119
  },
172
120
  });
173
121
 
174
- const SUBCOMMANDS = ["convert", "parse", "render", "sync"] as const;
122
+ const SUBCOMMANDS = ["convert", "sync"] as const;
175
123
 
176
124
  // Main CLI with subcommands
177
125
  const cli = subcommands({
@@ -179,8 +127,6 @@ const cli = subcommands({
179
127
  description: "Transform agent session files to readable transcripts",
180
128
  cmds: {
181
129
  convert: convertCmd,
182
- parse: parseCmd,
183
- render: renderCmd,
184
130
  sync: syncCmd,
185
131
  },
186
132
  });
package/src/convert.ts CHANGED
@@ -1,18 +1,23 @@
1
1
  /**
2
2
  * Convert command: full pipeline with provenance tracking.
3
3
  *
4
- * When output is a directory, uses the same replace-existing behavior
5
- * as sync: scans for existing outputs by provenance and replaces them.
4
+ * When output is a directory, uses provenance tracking via transcripts.json
5
+ * index to manage output files.
6
6
  */
7
7
 
8
- import { dirname, join, resolve } from "path";
9
- import { mkdir, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import { mkdir } from "fs/promises";
10
10
  import { parseToTranscripts } from "./parse.ts";
11
11
  import { renderTranscript } from "./render.ts";
12
- import { generateOutputName, type NamingOptions } from "./utils/naming.ts";
12
+ import { generateOutputName, extractSessionId } from "./utils/naming.ts";
13
13
  import {
14
- findExistingOutputs,
15
- deleteExistingOutputs,
14
+ loadIndex,
15
+ saveIndex,
16
+ removeEntriesForSource,
17
+ restoreEntries,
18
+ deleteOutputFiles,
19
+ setEntry,
20
+ normalizeSourcePath,
16
21
  } from "./utils/provenance.ts";
17
22
 
18
23
  export interface ConvertToDirectoryOptions {
@@ -20,7 +25,6 @@ export interface ConvertToDirectoryOptions {
20
25
  outputDir: string;
21
26
  adapter?: string;
22
27
  head?: string;
23
- naming?: NamingOptions;
24
28
  }
25
29
 
26
30
  /**
@@ -30,7 +34,10 @@ export interface ConvertToDirectoryOptions {
30
34
  export async function convertToDirectory(
31
35
  options: ConvertToDirectoryOptions,
32
36
  ): Promise<void> {
33
- const { input, outputDir, adapter, head, naming } = options;
37
+ const { input, outputDir, adapter, head } = options;
38
+
39
+ // Ensure output directory exists
40
+ await mkdir(outputDir, { recursive: true });
34
41
 
35
42
  // Parse input to transcripts
36
43
  const { transcripts, inputPath } = await parseToTranscripts({
@@ -38,41 +45,82 @@ export async function convertToDirectory(
38
45
  adapter,
39
46
  });
40
47
 
41
- // Resolve absolute source path for provenance tracking
42
- const sourcePath = inputPath === "<stdin>" ? "<stdin>" : resolve(inputPath);
48
+ // Normalize source path for consistent index keys
49
+ const sourcePath = normalizeSourcePath(inputPath);
50
+
51
+ // Load index and handle existing outputs
52
+ const index = await loadIndex(outputDir);
43
53
 
44
- // Find and delete existing outputs for this source
54
+ // Remove old entries (save for restoration on error)
55
+ const removedEntries =
56
+ sourcePath !== "<stdin>" ? removeEntriesForSource(index, sourcePath) : [];
57
+
58
+ // Get source mtime for index entry
59
+ let sourceMtime = Date.now();
45
60
  if (sourcePath !== "<stdin>") {
46
- const existingOutputs = await findExistingOutputs(outputDir, sourcePath);
47
- if (existingOutputs.length > 0) {
48
- await deleteExistingOutputs(existingOutputs);
61
+ try {
62
+ const stat = await Bun.file(sourcePath).stat();
63
+ if (stat) {
64
+ sourceMtime = stat.mtime.getTime();
65
+ }
66
+ } catch {
67
+ // Use current time as fallback
49
68
  }
50
69
  }
51
70
 
52
- // Generate fresh outputs
53
- for (let i = 0; i < transcripts.length; i++) {
54
- const transcript = transcripts[i];
55
- const suffix = transcripts.length > 1 ? `_${i + 1}` : undefined;
56
-
57
- // Generate descriptive name
58
- const baseName = await generateOutputName(
59
- transcript,
60
- inputPath,
61
- naming || {},
62
- );
63
- const finalName = suffix ? `${baseName}${suffix}` : baseName;
64
- const outputPath = join(outputDir, `${finalName}.md`);
65
-
66
- // Ensure output directory exists
67
- await mkdir(dirname(outputPath), { recursive: true });
68
-
69
- // Render with provenance front matter
70
- const markdown = renderTranscript(transcript, {
71
- head,
72
- sourcePath: sourcePath !== "<stdin>" ? sourcePath : undefined,
73
- });
74
- await Bun.write(outputPath, markdown);
75
-
76
- console.error(`Wrote: ${outputPath}`);
71
+ const sessionId = extractSessionId(inputPath);
72
+ const newOutputs: string[] = [];
73
+
74
+ try {
75
+ // Generate fresh outputs
76
+ for (let i = 0; i < transcripts.length; i++) {
77
+ const transcript = transcripts[i];
78
+ const segmentIndex = transcripts.length > 1 ? i + 1 : undefined;
79
+
80
+ // Generate deterministic name
81
+ const baseName = generateOutputName(transcript, inputPath);
82
+ const suffix = segmentIndex ? `_${segmentIndex}` : "";
83
+ const relativePath = `${baseName}${suffix}.md`;
84
+ const outputPath = join(outputDir, relativePath);
85
+
86
+ // Render with provenance front matter
87
+ const markdown = renderTranscript(transcript, {
88
+ head,
89
+ sourcePath: sourcePath !== "<stdin>" ? sourcePath : undefined,
90
+ });
91
+ await Bun.write(outputPath, markdown);
92
+ newOutputs.push(relativePath);
93
+
94
+ // Update index (only for non-stdin sources)
95
+ if (sourcePath !== "<stdin>") {
96
+ setEntry(index, relativePath, {
97
+ source: sourcePath,
98
+ sourceMtime,
99
+ sessionId,
100
+ segmentIndex,
101
+ syncedAt: new Date().toISOString(),
102
+ });
103
+ }
104
+
105
+ console.error(`Wrote: ${outputPath}`);
106
+ }
107
+
108
+ // Success: delete old output files (after new ones are written)
109
+ const oldFilenames = removedEntries.map((e) => e.filename);
110
+ const toDelete = oldFilenames.filter((f) => !newOutputs.includes(f));
111
+ if (toDelete.length > 0) {
112
+ await deleteOutputFiles(outputDir, toDelete);
113
+ }
114
+ } catch (error) {
115
+ // Clean up any newly written files before restoring old entries
116
+ if (newOutputs.length > 0) {
117
+ await deleteOutputFiles(outputDir, newOutputs);
118
+ }
119
+ // Restore old entries on error to preserve provenance
120
+ restoreEntries(index, removedEntries);
121
+ throw error;
77
122
  }
123
+
124
+ // Save index
125
+ await saveIndex(outputDir, index);
78
126
  }
package/src/parse.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Parse command: source format → intermediate JSON
2
+ * Parse: source format → intermediate transcript format
3
3
  */
4
4
 
5
- import { dirname, join } from "path";
6
- import { mkdir } from "fs/promises";
7
5
  import type { Transcript } from "./types.ts";
8
6
  import { detectAdapter, getAdapter, listAdapters } from "./adapters/index.ts";
9
- import { generateOutputName, type NamingOptions } from "./utils/naming.ts";
10
7
 
11
8
  export interface ParseOptions {
12
9
  input: string; // file path, or "-" for stdin
13
- output?: string; // output path/dir
14
10
  adapter?: string; // explicit adapter name
15
- naming?: NamingOptions; // options for output file naming
11
+ }
12
+
13
+ export interface ParseResult {
14
+ transcripts: Transcript[];
15
+ inputPath: string;
16
16
  }
17
17
 
18
18
  /**
@@ -40,73 +40,7 @@ async function readInput(
40
40
  }
41
41
 
42
42
  /**
43
- * Determine output file paths for transcripts.
44
- */
45
- async function getOutputPaths(
46
- transcripts: Transcript[],
47
- inputPath: string,
48
- outputOption?: string,
49
- namingOptions?: NamingOptions,
50
- ): Promise<string[]> {
51
- // Determine output directory
52
- let outputDir: string;
53
- let explicitBaseName: string | undefined;
54
-
55
- if (outputOption) {
56
- // If output looks like a file (has extension), use its directory and name
57
- if (outputOption.match(/\.\w+$/)) {
58
- outputDir = dirname(outputOption);
59
- explicitBaseName = outputOption
60
- .split("/")
61
- .pop()!
62
- .replace(/\.\w+$/, "");
63
- } else {
64
- outputDir = outputOption;
65
- }
66
- } else {
67
- outputDir = process.cwd();
68
- }
69
-
70
- // Generate paths with descriptive names
71
- const paths: string[] = [];
72
-
73
- for (let i = 0; i < transcripts.length; i++) {
74
- let baseName: string;
75
-
76
- if (explicitBaseName) {
77
- // User provided explicit filename
78
- baseName = explicitBaseName;
79
- } else {
80
- // Generate descriptive name
81
- baseName = await generateOutputName(
82
- transcripts[i],
83
- inputPath,
84
- namingOptions || {},
85
- );
86
- }
87
-
88
- // Add suffix for multiple transcripts
89
- if (transcripts.length > 1) {
90
- baseName = `${baseName}_${i + 1}`;
91
- }
92
-
93
- paths.push(join(outputDir, `${baseName}.json`));
94
- }
95
-
96
- return paths;
97
- }
98
-
99
- export interface ParseResult {
100
- transcripts: Transcript[];
101
- inputPath: string;
102
- }
103
-
104
- export interface ParseAndWriteResult extends ParseResult {
105
- outputPaths: string[];
106
- }
107
-
108
- /**
109
- * Parse source file(s) to transcripts (no file I/O beyond reading input).
43
+ * Parse source file(s) to transcripts.
110
44
  */
111
45
  export async function parseToTranscripts(
112
46
  options: ParseOptions,
@@ -135,31 +69,3 @@ export async function parseToTranscripts(
135
69
  const transcripts = adapter.parse(content, inputPath);
136
70
  return { transcripts, inputPath };
137
71
  }
138
-
139
- /**
140
- * Parse source file(s) to intermediate JSON and write to files.
141
- */
142
- export async function parse(
143
- options: ParseOptions,
144
- ): Promise<ParseAndWriteResult> {
145
- const { transcripts, inputPath } = await parseToTranscripts(options);
146
-
147
- // Write output files
148
- const outputPaths = await getOutputPaths(
149
- transcripts,
150
- inputPath,
151
- options.output,
152
- options.naming,
153
- );
154
-
155
- for (let i = 0; i < transcripts.length; i++) {
156
- const json = JSON.stringify(transcripts[i], null, 2);
157
- // Ensure directory exists
158
- const dir = dirname(outputPaths[i]);
159
- await mkdir(dir, { recursive: true });
160
- await Bun.write(outputPaths[i], json);
161
- console.error(`Wrote: ${outputPaths[i]}`);
162
- }
163
-
164
- return { transcripts, inputPath, outputPaths };
165
- }