@arcreflex/agent-transcripts 0.1.10 → 0.1.12
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/CLAUDE.md +3 -1
- package/README.md +60 -53
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +1 -0
- package/src/adapters/index.ts +0 -6
- package/src/archive.ts +267 -0
- package/src/cli.ts +96 -63
- package/src/convert.ts +19 -86
- package/src/parse.ts +0 -3
- package/src/render-html.ts +38 -195
- package/src/render-index.ts +15 -178
- package/src/render.ts +25 -88
- package/src/serve.ts +124 -215
- package/src/title.ts +24 -102
- package/src/types.ts +3 -0
- package/src/utils/naming.ts +8 -13
- package/src/utils/summary.ts +1 -4
- package/src/utils/text.ts +5 -0
- package/src/utils/theme.ts +152 -0
- package/src/utils/tree.ts +85 -1
- package/src/watch.ts +111 -0
- package/test/archive.test.ts +264 -0
- package/test/fixtures/claude/branching.input.jsonl +6 -0
- package/test/fixtures/claude/branching.output.md +25 -0
- package/test/naming.test.ts +98 -0
- package/test/summary.test.ts +144 -0
- package/test/tree.test.ts +217 -0
- package/tsconfig.json +1 -1
- package/src/cache.ts +0 -129
- package/src/sync.ts +0 -295
- package/src/utils/provenance.ts +0 -212
package/src/cli.ts
CHANGED
|
@@ -15,20 +15,12 @@ import {
|
|
|
15
15
|
} from "cmd-ts";
|
|
16
16
|
import { parseToTranscripts } from "./parse.ts";
|
|
17
17
|
import { renderTranscript } from "./render.ts";
|
|
18
|
-
import { sync, type OutputFormat } from "./sync.ts";
|
|
19
18
|
import { convertToDirectory } from "./convert.ts";
|
|
20
19
|
import { generateTitles } from "./title.ts";
|
|
21
20
|
import { serve } from "./serve.ts";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async from(value: string): Promise<OutputFormat> {
|
|
26
|
-
if (value !== "md" && value !== "html") {
|
|
27
|
-
throw new Error(`Invalid format: ${value}. Must be "md" or "html".`);
|
|
28
|
-
}
|
|
29
|
-
return value;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
21
|
+
import { archiveAll, DEFAULT_ARCHIVE_DIR } from "./archive.ts";
|
|
22
|
+
import { getAdapters } from "./adapters/index.ts";
|
|
23
|
+
import { ArchiveWatcher } from "./watch.ts";
|
|
32
24
|
|
|
33
25
|
// Shared options
|
|
34
26
|
const inputArg = positional({
|
|
@@ -57,57 +49,47 @@ const headOpt = option({
|
|
|
57
49
|
description: "Render branch ending at this message ID (default: latest)",
|
|
58
50
|
});
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
description:
|
|
52
|
+
const archiveDirOpt = option({
|
|
53
|
+
type: optional(string),
|
|
54
|
+
long: "archive-dir",
|
|
55
|
+
description: `Archive directory (default: ${DEFAULT_ARCHIVE_DIR})`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Archive subcommand
|
|
59
|
+
const archiveCmd = command({
|
|
60
|
+
name: "archive",
|
|
61
|
+
description: "Archive session files from source directory",
|
|
64
62
|
args: {
|
|
65
63
|
source: positional({
|
|
66
64
|
type: string,
|
|
67
65
|
displayName: "source",
|
|
68
66
|
description: "Source directory to scan for session files",
|
|
69
67
|
}),
|
|
70
|
-
|
|
71
|
-
type: string,
|
|
72
|
-
long: "output",
|
|
73
|
-
short: "o",
|
|
74
|
-
description: "Output directory for transcripts",
|
|
75
|
-
}),
|
|
76
|
-
format: option({
|
|
77
|
-
type: optional(formatType),
|
|
78
|
-
long: "format",
|
|
79
|
-
description: "Output format: md (default) or html",
|
|
80
|
-
}),
|
|
81
|
-
noTitle: flag({
|
|
82
|
-
long: "no-title",
|
|
83
|
-
description: "Skip LLM title generation (for HTML format)",
|
|
84
|
-
}),
|
|
85
|
-
force: flag({
|
|
86
|
-
long: "force",
|
|
87
|
-
short: "f",
|
|
88
|
-
description: "Re-render all sessions, ignoring mtime",
|
|
89
|
-
}),
|
|
68
|
+
archiveDir: archiveDirOpt,
|
|
90
69
|
quiet: flag({
|
|
91
70
|
long: "quiet",
|
|
92
71
|
short: "q",
|
|
93
72
|
description: "Suppress progress output",
|
|
94
73
|
}),
|
|
95
74
|
},
|
|
96
|
-
async handler({ source,
|
|
97
|
-
|
|
75
|
+
async handler({ source, archiveDir, quiet }) {
|
|
76
|
+
const dir = archiveDir ?? DEFAULT_ARCHIVE_DIR;
|
|
77
|
+
const result = await archiveAll(dir, source, getAdapters(), { quiet });
|
|
78
|
+
|
|
79
|
+
if (!quiet) {
|
|
80
|
+
console.error(
|
|
81
|
+
`\nArchive complete: ${result.updated.length} updated, ${result.current.length} current, ${result.errors.length} errors`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
98
84
|
},
|
|
99
85
|
});
|
|
100
86
|
|
|
101
87
|
// Title subcommand
|
|
102
88
|
const titleCmd = command({
|
|
103
89
|
name: "title",
|
|
104
|
-
description: "Generate LLM titles for
|
|
90
|
+
description: "Generate LLM titles for archive entries",
|
|
105
91
|
args: {
|
|
106
|
-
|
|
107
|
-
type: string,
|
|
108
|
-
displayName: "output",
|
|
109
|
-
description: "Output directory containing transcripts.json",
|
|
110
|
-
}),
|
|
92
|
+
archiveDir: archiveDirOpt,
|
|
111
93
|
force: flag({
|
|
112
94
|
long: "force",
|
|
113
95
|
short: "f",
|
|
@@ -119,21 +101,21 @@ const titleCmd = command({
|
|
|
119
101
|
description: "Suppress progress output",
|
|
120
102
|
}),
|
|
121
103
|
},
|
|
122
|
-
async handler({
|
|
123
|
-
await generateTitles({
|
|
104
|
+
async handler({ archiveDir, force, quiet }) {
|
|
105
|
+
await generateTitles({
|
|
106
|
+
archiveDir: archiveDir ?? undefined,
|
|
107
|
+
force,
|
|
108
|
+
quiet,
|
|
109
|
+
});
|
|
124
110
|
},
|
|
125
111
|
});
|
|
126
112
|
|
|
127
113
|
// Serve subcommand
|
|
128
114
|
const serveCmd = command({
|
|
129
115
|
name: "serve",
|
|
130
|
-
description: "Serve transcripts via HTTP
|
|
116
|
+
description: "Serve transcripts from archive via HTTP",
|
|
131
117
|
args: {
|
|
132
|
-
|
|
133
|
-
type: string,
|
|
134
|
-
displayName: "source",
|
|
135
|
-
description: "Source directory to scan for session files",
|
|
136
|
-
}),
|
|
118
|
+
archiveDir: archiveDirOpt,
|
|
137
119
|
port: option({
|
|
138
120
|
type: optional(number),
|
|
139
121
|
long: "port",
|
|
@@ -145,13 +127,66 @@ const serveCmd = command({
|
|
|
145
127
|
short: "q",
|
|
146
128
|
description: "Suppress request logging",
|
|
147
129
|
}),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
130
|
+
},
|
|
131
|
+
async handler({ archiveDir, port, quiet }) {
|
|
132
|
+
await serve({
|
|
133
|
+
archiveDir: archiveDir ?? undefined,
|
|
134
|
+
port: port ?? 3000,
|
|
135
|
+
quiet,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Watch subcommand
|
|
141
|
+
const watchCmd = command({
|
|
142
|
+
name: "watch",
|
|
143
|
+
description: "Watch source directories and keep archive updated",
|
|
144
|
+
args: {
|
|
145
|
+
source: positional({
|
|
146
|
+
type: string,
|
|
147
|
+
displayName: "source",
|
|
148
|
+
description: "Source directory to watch for session files",
|
|
149
|
+
}),
|
|
150
|
+
archiveDir: archiveDirOpt,
|
|
151
|
+
pollInterval: option({
|
|
152
|
+
type: optional(number),
|
|
153
|
+
long: "poll-interval",
|
|
154
|
+
description: "Poll interval in milliseconds (default: 30000)",
|
|
155
|
+
}),
|
|
156
|
+
quiet: flag({
|
|
157
|
+
long: "quiet",
|
|
158
|
+
short: "q",
|
|
159
|
+
description: "Suppress progress output",
|
|
151
160
|
}),
|
|
152
161
|
},
|
|
153
|
-
async handler({ source,
|
|
154
|
-
|
|
162
|
+
async handler({ source, archiveDir, pollInterval, quiet }) {
|
|
163
|
+
const watcher = new ArchiveWatcher([source], {
|
|
164
|
+
archiveDir: archiveDir ?? undefined,
|
|
165
|
+
pollIntervalMs: pollInterval ?? undefined,
|
|
166
|
+
quiet,
|
|
167
|
+
onUpdate(result) {
|
|
168
|
+
if (!quiet && result.updated.length > 0) {
|
|
169
|
+
console.error(`Updated: ${result.updated.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
onError(error) {
|
|
173
|
+
console.error(`Watch error: ${error.message}`);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!quiet) {
|
|
178
|
+
console.error(`Watching ${source}...`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await watcher.start();
|
|
182
|
+
|
|
183
|
+
process.on("SIGINT", () => {
|
|
184
|
+
if (!quiet) {
|
|
185
|
+
console.error("\nStopping watcher...");
|
|
186
|
+
}
|
|
187
|
+
watcher.stop();
|
|
188
|
+
process.exit(0);
|
|
189
|
+
});
|
|
155
190
|
},
|
|
156
191
|
});
|
|
157
192
|
|
|
@@ -174,7 +209,6 @@ const convertCmd = command({
|
|
|
174
209
|
},
|
|
175
210
|
async handler({ input, output, adapter, head }) {
|
|
176
211
|
if (output && isDirectoryOutput(output)) {
|
|
177
|
-
// Directory output: use provenance tracking
|
|
178
212
|
await convertToDirectory({
|
|
179
213
|
input,
|
|
180
214
|
outputDir: output,
|
|
@@ -182,23 +216,21 @@ const convertCmd = command({
|
|
|
182
216
|
head,
|
|
183
217
|
});
|
|
184
218
|
} else if (output) {
|
|
185
|
-
// Explicit file output: not supported anymore (use directory)
|
|
186
219
|
console.error(
|
|
187
220
|
"Error: Explicit file output not supported. Use a directory path instead.",
|
|
188
221
|
);
|
|
189
222
|
process.exit(1);
|
|
190
223
|
} else {
|
|
191
|
-
// No output: stream to stdout
|
|
192
224
|
const { transcripts } = await parseToTranscripts({ input, adapter });
|
|
193
225
|
for (let i = 0; i < transcripts.length; i++) {
|
|
194
|
-
if (i > 0) console.log();
|
|
195
|
-
console.log(renderTranscript(transcripts[i], head));
|
|
226
|
+
if (i > 0) console.log();
|
|
227
|
+
console.log(renderTranscript(transcripts[i], { head }));
|
|
196
228
|
}
|
|
197
229
|
}
|
|
198
230
|
},
|
|
199
231
|
});
|
|
200
232
|
|
|
201
|
-
const SUBCOMMANDS = ["convert", "
|
|
233
|
+
const SUBCOMMANDS = ["convert", "archive", "title", "serve", "watch"] as const;
|
|
202
234
|
|
|
203
235
|
// Main CLI with subcommands
|
|
204
236
|
const cli = subcommands({
|
|
@@ -206,9 +238,10 @@ const cli = subcommands({
|
|
|
206
238
|
description: "Transform agent session files to readable transcripts",
|
|
207
239
|
cmds: {
|
|
208
240
|
convert: convertCmd,
|
|
209
|
-
|
|
241
|
+
archive: archiveCmd,
|
|
210
242
|
title: titleCmd,
|
|
211
243
|
serve: serveCmd,
|
|
244
|
+
watch: watchCmd,
|
|
212
245
|
},
|
|
213
246
|
});
|
|
214
247
|
|
package/src/convert.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert command:
|
|
2
|
+
* Convert command: parse source and render to markdown.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Standalone pipeline with no archive dependency. Directory output
|
|
5
|
+
* writes markdown files with deterministic names.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { join } from "path";
|
|
8
|
+
import { join, resolve } from "path";
|
|
9
9
|
import { mkdir } from "fs/promises";
|
|
10
10
|
import { parseToTranscripts } from "./parse.ts";
|
|
11
11
|
import { renderTranscript } from "./render.ts";
|
|
12
|
-
import { generateOutputName
|
|
13
|
-
import {
|
|
14
|
-
loadIndex,
|
|
15
|
-
saveIndex,
|
|
16
|
-
removeEntriesForSource,
|
|
17
|
-
restoreEntries,
|
|
18
|
-
deleteOutputFiles,
|
|
19
|
-
setEntry,
|
|
20
|
-
normalizeSourcePath,
|
|
21
|
-
extractFirstUserMessage,
|
|
22
|
-
} from "./utils/provenance.ts";
|
|
12
|
+
import { generateOutputName } from "./utils/naming.ts";
|
|
23
13
|
|
|
24
14
|
export interface ConvertToDirectoryOptions {
|
|
25
15
|
input: string;
|
|
@@ -28,91 +18,34 @@ export interface ConvertToDirectoryOptions {
|
|
|
28
18
|
head?: string;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
/**
|
|
32
|
-
* Convert source file to markdown in output directory.
|
|
33
|
-
* Uses provenance tracking to replace existing outputs.
|
|
34
|
-
*/
|
|
35
21
|
export async function convertToDirectory(
|
|
36
22
|
options: ConvertToDirectoryOptions,
|
|
37
23
|
): Promise<void> {
|
|
38
24
|
const { input, outputDir, adapter, head } = options;
|
|
39
25
|
|
|
40
|
-
// Ensure output directory exists
|
|
41
26
|
await mkdir(outputDir, { recursive: true });
|
|
42
27
|
|
|
43
|
-
// Parse input to transcripts
|
|
44
28
|
const { transcripts, inputPath } = await parseToTranscripts({
|
|
45
29
|
input,
|
|
46
30
|
adapter,
|
|
47
31
|
});
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
const sourcePath = normalizeSourcePath(inputPath);
|
|
51
|
-
|
|
52
|
-
// Load index and handle existing outputs
|
|
53
|
-
const index = await loadIndex(outputDir);
|
|
54
|
-
|
|
55
|
-
// Remove old entries (save for restoration on error)
|
|
56
|
-
const removedEntries =
|
|
57
|
-
sourcePath !== "<stdin>" ? removeEntriesForSource(index, sourcePath) : [];
|
|
58
|
-
|
|
59
|
-
const sessionId = extractSessionId(inputPath);
|
|
60
|
-
const newOutputs: string[] = [];
|
|
33
|
+
const sourcePath = inputPath === "<stdin>" ? undefined : resolve(inputPath);
|
|
61
34
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const transcript = transcripts[i];
|
|
66
|
-
const segmentIndex = transcripts.length > 1 ? i + 1 : undefined;
|
|
35
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
36
|
+
const transcript = transcripts[i];
|
|
37
|
+
const segmentIndex = transcripts.length > 1 ? i + 1 : undefined;
|
|
67
38
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const outputPath = join(outputDir, relativePath);
|
|
39
|
+
const baseName = generateOutputName(transcript, inputPath);
|
|
40
|
+
const suffix = segmentIndex ? `_${segmentIndex}` : "";
|
|
41
|
+
const relativePath = `${baseName}${suffix}.md`;
|
|
42
|
+
const outputPath = join(outputDir, relativePath);
|
|
73
43
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
newOutputs.push(relativePath);
|
|
81
|
-
|
|
82
|
-
// Update index (only for non-stdin sources)
|
|
83
|
-
if (sourcePath !== "<stdin>") {
|
|
84
|
-
setEntry(index, relativePath, {
|
|
85
|
-
source: sourcePath,
|
|
86
|
-
sessionId,
|
|
87
|
-
segmentIndex,
|
|
88
|
-
syncedAt: new Date().toISOString(),
|
|
89
|
-
firstUserMessage: extractFirstUserMessage(transcript),
|
|
90
|
-
messageCount: transcript.metadata.messageCount,
|
|
91
|
-
startTime: transcript.metadata.startTime,
|
|
92
|
-
endTime: transcript.metadata.endTime,
|
|
93
|
-
cwd: transcript.metadata.cwd,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
console.error(`Wrote: ${outputPath}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Success: delete old output files (after new ones are written)
|
|
101
|
-
const oldFilenames = removedEntries.map((e) => e.filename);
|
|
102
|
-
const toDelete = oldFilenames.filter((f) => !newOutputs.includes(f));
|
|
103
|
-
if (toDelete.length > 0) {
|
|
104
|
-
await deleteOutputFiles(outputDir, toDelete);
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
// Clean up any newly written files before restoring old entries
|
|
108
|
-
if (newOutputs.length > 0) {
|
|
109
|
-
await deleteOutputFiles(outputDir, newOutputs);
|
|
110
|
-
}
|
|
111
|
-
// Restore old entries on error to preserve provenance
|
|
112
|
-
restoreEntries(index, removedEntries);
|
|
113
|
-
throw error;
|
|
44
|
+
const markdown = renderTranscript(transcript, {
|
|
45
|
+
head,
|
|
46
|
+
sourcePath,
|
|
47
|
+
});
|
|
48
|
+
await Bun.write(outputPath, markdown);
|
|
49
|
+
console.error(`Wrote: ${outputPath}`);
|
|
114
50
|
}
|
|
115
|
-
|
|
116
|
-
// Save index
|
|
117
|
-
await saveIndex(outputDir, index);
|
|
118
51
|
}
|