@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 +2 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +1 -0
- package/src/adapters/index.ts +7 -0
- package/src/cli.ts +36 -1
- package/src/sync.ts +175 -0
- package/src/types.ts +2 -0
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`:
|
|
47
|
+
- `Adapter`: name, file patterns, parse function
|
|
47
48
|
|
|
48
49
|
## Adding an Adapter
|
|
49
50
|
|
package/package.json
CHANGED
|
@@ -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);
|
package/src/adapters/index.ts
CHANGED
|
@@ -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
|
}
|