@arcreflex/agent-transcripts 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ src/
15
15
  cli.ts # CLI entry point, subcommand routing
16
16
  parse.ts # Source → intermediate JSON
17
17
  render.ts # Intermediate JSON → markdown
18
+ sync.ts # Batch sync sessions → markdown
18
19
  types.ts # Core types (Transcript, Message, Adapter)
19
20
  adapters/ # Source format adapters (currently: claude-code)
20
21
  utils/ # Helpers (summary extraction)
@@ -43,7 +44,7 @@ Two-stage pipeline: Parse (source → JSON) → Render (JSON → markdown).
43
44
 
44
45
  - `Transcript`: source info, warnings, messages array
45
46
  - `Message`: union of UserMessage | AssistantMessage | SystemMessage | ToolCallGroup | ErrorMessage
46
- - `Adapter`: `{ name: string, parse(content, sourcePath): Transcript[] }`
47
+ - `Adapter`: name, file patterns, parse function
47
48
 
48
49
  ## Adding an Adapter
49
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcreflex/agent-transcripts",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Transform AI coding agent session files into readable transcripts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -335,6 +335,7 @@ function transformConversation(
335
335
 
336
336
  export const claudeCodeAdapter: Adapter = {
337
337
  name: "claude-code",
338
+ filePatterns: ["*.jsonl"],
338
339
 
339
340
  parse(content: string, sourcePath: string): Transcript[] {
340
341
  const { records, warnings } = parseJsonl(content);
@@ -43,3 +43,10 @@ export function getAdapter(name: string): Adapter | undefined {
43
43
  export function listAdapters(): string[] {
44
44
  return Object.keys(adapters);
45
45
  }
46
+
47
+ /**
48
+ * Get all registered adapters.
49
+ */
50
+ export function getAdapters(): Adapter[] {
51
+ return Object.values(adapters);
52
+ }
package/src/cli.ts CHANGED
@@ -10,9 +10,11 @@ import {
10
10
  option,
11
11
  optional,
12
12
  positional,
13
+ flag,
13
14
  } from "cmd-ts";
14
15
  import { parse, parseToTranscripts } from "./parse.ts";
15
16
  import { render, renderTranscript } from "./render.ts";
17
+ import { sync } from "./sync.ts";
16
18
 
17
19
  // Read OpenRouter API key from environment for LLM-based slug generation
18
20
  const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
@@ -84,6 +86,38 @@ const renderCmd = command({
84
86
  },
85
87
  });
86
88
 
89
+ // Sync subcommand
90
+ const syncCmd = command({
91
+ name: "sync",
92
+ description: "Sync session files to markdown transcripts",
93
+ args: {
94
+ source: positional({
95
+ type: string,
96
+ displayName: "source",
97
+ description: "Source directory to scan for session files",
98
+ }),
99
+ output: option({
100
+ type: string,
101
+ long: "output",
102
+ short: "o",
103
+ description: "Output directory (mirrors source structure)",
104
+ }),
105
+ force: flag({
106
+ long: "force",
107
+ short: "f",
108
+ description: "Re-render all sessions, ignoring mtime",
109
+ }),
110
+ quiet: flag({
111
+ long: "quiet",
112
+ short: "q",
113
+ description: "Suppress progress output",
114
+ }),
115
+ },
116
+ async handler({ source, output, force, quiet }) {
117
+ await sync({ source, output, force, quiet });
118
+ },
119
+ });
120
+
87
121
  // Default command: full pipeline (parse → render)
88
122
  const defaultCmd = command({
89
123
  name: "agent-transcripts",
@@ -124,6 +158,7 @@ const cli = subcommands({
124
158
  cmds: {
125
159
  parse: parseCmd,
126
160
  render: renderCmd,
161
+ sync: syncCmd,
127
162
  },
128
163
  // Default command when no subcommand is specified
129
164
  });
@@ -132,7 +167,7 @@ const cli = subcommands({
132
167
  const args = process.argv.slice(2);
133
168
 
134
169
  // Check if first arg is a subcommand
135
- if (args[0] === "parse" || args[0] === "render") {
170
+ if (args[0] === "parse" || args[0] === "render" || args[0] === "sync") {
136
171
  run(cli, args);
137
172
  } else {
138
173
  // Run default command for full pipeline
package/src/sync.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Sync command: batch export sessions to markdown transcripts.
3
+ *
4
+ * Discovers session files in source directory, parses them,
5
+ * and writes rendered markdown to output directory.
6
+ * Output structure mirrors source structure with extension changed.
7
+ */
8
+
9
+ import { Glob } from "bun";
10
+ import { dirname, join, relative } from "path";
11
+ import { mkdir, stat } from "fs/promises";
12
+ import { getAdapters } from "./adapters/index.ts";
13
+ import type { Adapter } from "./types.ts";
14
+ import { renderTranscript } from "./render.ts";
15
+
16
+ export interface SyncOptions {
17
+ source: string;
18
+ output: string;
19
+ force?: boolean;
20
+ quiet?: boolean;
21
+ }
22
+
23
+ export interface SyncResult {
24
+ synced: number;
25
+ skipped: number;
26
+ errors: number;
27
+ }
28
+
29
+ interface SessionFile {
30
+ path: string;
31
+ relativePath: string;
32
+ mtime: number;
33
+ adapter: Adapter;
34
+ }
35
+
36
+ /**
37
+ * Discover session files for a specific adapter.
38
+ */
39
+ async function discoverForAdapter(
40
+ source: string,
41
+ adapter: Adapter,
42
+ ): Promise<SessionFile[]> {
43
+ const sessions: SessionFile[] = [];
44
+
45
+ for (const pattern of adapter.filePatterns) {
46
+ const glob = new Glob(`**/${pattern}`);
47
+
48
+ for await (const file of glob.scan({ cwd: source, absolute: false })) {
49
+ const fullPath = join(source, file);
50
+
51
+ try {
52
+ const fileStat = await stat(fullPath);
53
+ sessions.push({
54
+ path: fullPath,
55
+ relativePath: file,
56
+ mtime: fileStat.mtime.getTime(),
57
+ adapter,
58
+ });
59
+ } catch {
60
+ // Skip files we can't stat
61
+ }
62
+ }
63
+ }
64
+
65
+ return sessions;
66
+ }
67
+
68
+ /**
69
+ * Compute output path for a session file.
70
+ * Mirrors input structure, changing extension to .md.
71
+ */
72
+ function computeOutputPath(
73
+ relativePath: string,
74
+ outputDir: string,
75
+ suffix?: string,
76
+ ): string {
77
+ // Replace extension with .md
78
+ const mdPath = relativePath.replace(/\.[^.]+$/, ".md");
79
+ // Add suffix if provided (for multiple transcripts from same file)
80
+ const finalPath = suffix ? mdPath.replace(/\.md$/, `${suffix}.md`) : mdPath;
81
+ return join(outputDir, finalPath);
82
+ }
83
+
84
+ /**
85
+ * Check if output file needs to be re-rendered based on mtime.
86
+ */
87
+ async function needsSync(
88
+ outputPath: string,
89
+ sourceMtime: number,
90
+ force: boolean,
91
+ ): Promise<boolean> {
92
+ if (force) return true;
93
+
94
+ try {
95
+ const outputStat = await stat(outputPath);
96
+ return outputStat.mtime.getTime() < sourceMtime;
97
+ } catch {
98
+ // Output doesn't exist, needs sync
99
+ return true;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Sync session files from source to output directory.
105
+ */
106
+ export async function sync(options: SyncOptions): Promise<SyncResult> {
107
+ const { source, output, force = false, quiet = false } = options;
108
+
109
+ const result: SyncResult = { synced: 0, skipped: 0, errors: 0 };
110
+
111
+ // Discover sessions for each adapter
112
+ const sessions: SessionFile[] = [];
113
+ for (const adapter of getAdapters()) {
114
+ const adapterSessions = await discoverForAdapter(source, adapter);
115
+ sessions.push(...adapterSessions);
116
+ }
117
+
118
+ if (!quiet) {
119
+ console.error(`Found ${sessions.length} session file(s)`);
120
+ }
121
+
122
+ // Process each session
123
+ for (const session of sessions) {
124
+ try {
125
+ // Read and parse using the adapter that discovered this file
126
+ const content = await Bun.file(session.path).text();
127
+ const transcripts = session.adapter.parse(content, session.path);
128
+
129
+ // Process each transcript (usually just one per file)
130
+ for (let i = 0; i < transcripts.length; i++) {
131
+ const transcript = transcripts[i];
132
+ const suffix = transcripts.length > 1 ? `_${i + 1}` : undefined;
133
+ const outputPath = computeOutputPath(
134
+ session.relativePath,
135
+ output,
136
+ suffix,
137
+ );
138
+
139
+ // Check if sync needed
140
+ if (!(await needsSync(outputPath, session.mtime, force))) {
141
+ if (!quiet) {
142
+ console.error(`Skip (up to date): ${outputPath}`);
143
+ }
144
+ result.skipped++;
145
+ continue;
146
+ }
147
+
148
+ // Ensure output directory exists
149
+ await mkdir(dirname(outputPath), { recursive: true });
150
+
151
+ // Render and write
152
+ const markdown = renderTranscript(transcript);
153
+ await Bun.write(outputPath, markdown);
154
+
155
+ if (!quiet) {
156
+ console.error(`Synced: ${outputPath}`);
157
+ }
158
+ result.synced++;
159
+ }
160
+ } catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ console.error(`Error: ${session.relativePath}: ${message}`);
163
+ result.errors++;
164
+ }
165
+ }
166
+
167
+ // Summary
168
+ if (!quiet) {
169
+ console.error(
170
+ `\nSync complete: ${result.synced} synced, ${result.skipped} skipped, ${result.errors} errors`,
171
+ );
172
+ }
173
+
174
+ return result;
175
+ }
package/src/types.ts CHANGED
@@ -70,6 +70,8 @@ export interface ErrorMessage extends BaseMessage {
70
70
  */
71
71
  export interface Adapter {
72
72
  name: string;
73
+ /** Glob patterns for discovering session files (e.g., ["*.jsonl"]) */
74
+ filePatterns: string[];
73
75
  /** Parse source content into one or more transcripts (split by conversation) */
74
76
  parse(content: string, sourcePath: string): Transcript[];
75
77
  }