@arcreflex/agent-transcripts 0.1.11 → 0.1.13
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 +10 -6
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +2 -0
- package/src/adapters/index.ts +6 -4
- package/src/archive.ts +2 -2
- package/src/cli.ts +71 -11
- package/src/types.ts +8 -0
- package/src/watch.ts +30 -87
- package/test/adapters.test.ts +32 -0
- package/test/watch.test.ts +85 -0
package/README.md
CHANGED
|
@@ -35,7 +35,9 @@ src/
|
|
|
35
35
|
test/
|
|
36
36
|
fixtures/ # Snapshot test inputs/outputs
|
|
37
37
|
snapshots.test.ts
|
|
38
|
+
adapters.test.ts
|
|
38
39
|
archive.test.ts
|
|
40
|
+
watch.test.ts
|
|
39
41
|
tree.test.ts
|
|
40
42
|
naming.test.ts
|
|
41
43
|
summary.test.ts
|
|
@@ -57,8 +59,9 @@ agent-transcripts convert <file> # Parse and render to stdout
|
|
|
57
59
|
agent-transcripts convert <file> -o <dir> # Parse and render to directory
|
|
58
60
|
|
|
59
61
|
# Archive management
|
|
60
|
-
agent-transcripts archive
|
|
61
|
-
agent-transcripts archive
|
|
62
|
+
agent-transcripts archive # Auto-discover from adapter defaults (~/.claude)
|
|
63
|
+
agent-transcripts archive /path --adapter claude-code # Explicit source + adapter
|
|
64
|
+
agent-transcripts archive --archive-dir ~/my-archive # Custom archive location
|
|
62
65
|
|
|
63
66
|
# Serving
|
|
64
67
|
agent-transcripts serve # Serve from default archive
|
|
@@ -66,8 +69,9 @@ agent-transcripts serve --archive-dir <dir> # Serve from custom archive
|
|
|
66
69
|
agent-transcripts serve -p 8080 # Custom port
|
|
67
70
|
|
|
68
71
|
# Watching
|
|
69
|
-
agent-transcripts watch
|
|
70
|
-
agent-transcripts watch
|
|
72
|
+
agent-transcripts watch # Auto-discover from adapter defaults
|
|
73
|
+
agent-transcripts watch /path --adapter claude-code # Explicit source + adapter
|
|
74
|
+
agent-transcripts watch --poll-interval 60000
|
|
71
75
|
|
|
72
76
|
# Title generation
|
|
73
77
|
agent-transcripts title # Generate titles for archive entries
|
|
@@ -98,10 +102,10 @@ HTML (rendered on demand, in-memory LRU)
|
|
|
98
102
|
|
|
99
103
|
### Archive
|
|
100
104
|
|
|
101
|
-
The archive is the central data store at `~/.local/share/agent-transcripts/archive
|
|
105
|
+
The archive is the central data store at `$XDG_DATA_HOME/agent-transcripts/archive/` (defaults to `~/.local/share/agent-transcripts/archive/`):
|
|
102
106
|
|
|
103
107
|
```
|
|
104
|
-
|
|
108
|
+
$XDG_DATA_HOME/agent-transcripts/archive/
|
|
105
109
|
{sessionId}.json → ArchiveEntry
|
|
106
110
|
```
|
|
107
111
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { Glob } from "bun";
|
|
8
8
|
import { basename, join, relative } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
9
10
|
import { stat } from "fs/promises";
|
|
10
11
|
import type {
|
|
11
12
|
Adapter,
|
|
@@ -816,6 +817,7 @@ async function discoverByGlob(source: string): Promise<DiscoveredSession[]> {
|
|
|
816
817
|
export const claudeCodeAdapter: Adapter = {
|
|
817
818
|
name: "claude-code",
|
|
818
819
|
version: "claude-code:1",
|
|
820
|
+
defaultSource: process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
|
|
819
821
|
|
|
820
822
|
async discover(source: string): Promise<DiscoveredSession[]> {
|
|
821
823
|
// Try index-based discovery first, fall back to glob
|
package/src/adapters/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Adapter registry with path-based detection.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Adapter } from "../types.ts";
|
|
5
|
+
import type { Adapter, SourceSpec } from "../types.ts";
|
|
6
6
|
import { claudeCodeAdapter } from "./claude-code.ts";
|
|
7
7
|
|
|
8
8
|
const adapters: Record<string, Adapter> = {
|
|
@@ -39,8 +39,10 @@ export function listAdapters(): string[] {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Get
|
|
42
|
+
* Get default source specs from adapters that define a defaultSource.
|
|
43
43
|
*/
|
|
44
|
-
export function
|
|
45
|
-
return Object.values(adapters)
|
|
44
|
+
export function getDefaultSources(): SourceSpec[] {
|
|
45
|
+
return Object.values(adapters)
|
|
46
|
+
.filter((a): a is Adapter & { defaultSource: string } => !!a.defaultSource)
|
|
47
|
+
.map((a) => ({ adapter: a, source: a.defaultSource }));
|
|
46
48
|
}
|
package/src/archive.ts
CHANGED
|
@@ -12,8 +12,8 @@ import type { Adapter, DiscoveredSession, Transcript } from "./types.ts";
|
|
|
12
12
|
import { extractSessionId } from "./utils/naming.ts";
|
|
13
13
|
|
|
14
14
|
export const DEFAULT_ARCHIVE_DIR = join(
|
|
15
|
-
homedir(),
|
|
16
|
-
"
|
|
15
|
+
process.env.XDG_DATA_HOME || join(homedir(), ".local/share"),
|
|
16
|
+
"agent-transcripts/archive",
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
const ARCHIVE_SCHEMA_VERSION = 1;
|
package/src/cli.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { convertToDirectory } from "./convert.ts";
|
|
|
19
19
|
import { generateTitles } from "./title.ts";
|
|
20
20
|
import { serve } from "./serve.ts";
|
|
21
21
|
import { archiveAll, DEFAULT_ARCHIVE_DIR } from "./archive.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { getAdapter, getDefaultSources } from "./adapters/index.ts";
|
|
23
23
|
import { ArchiveWatcher } from "./watch.ts";
|
|
24
|
+
import type { SourceSpec } from "./types.ts";
|
|
24
25
|
|
|
25
26
|
// Shared options
|
|
26
27
|
const inputArg = positional({
|
|
@@ -55,16 +56,52 @@ const archiveDirOpt = option({
|
|
|
55
56
|
description: `Archive directory (default: ${DEFAULT_ARCHIVE_DIR})`,
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Resolve source specs from CLI args.
|
|
61
|
+
* - No source: use adapter defaults
|
|
62
|
+
* - Source + adapter: explicit pair
|
|
63
|
+
* - Source without adapter: error
|
|
64
|
+
*/
|
|
65
|
+
function resolveSourceSpecs(
|
|
66
|
+
source: string | undefined,
|
|
67
|
+
adapterName: string | undefined,
|
|
68
|
+
): SourceSpec[] {
|
|
69
|
+
if (!source) {
|
|
70
|
+
const defaults = getDefaultSources();
|
|
71
|
+
if (defaults.length === 0) {
|
|
72
|
+
console.error("Error: no adapters have a default source directory.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return defaults;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!adapterName) {
|
|
79
|
+
console.error(
|
|
80
|
+
"Error: --adapter is required when specifying a source directory.",
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const adapter = getAdapter(adapterName);
|
|
86
|
+
if (!adapter) {
|
|
87
|
+
console.error(`Error: unknown adapter "${adapterName}".`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [{ adapter, source }];
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
// Archive subcommand
|
|
59
95
|
const archiveCmd = command({
|
|
60
96
|
name: "archive",
|
|
61
97
|
description: "Archive session files from source directory",
|
|
62
98
|
args: {
|
|
63
99
|
source: positional({
|
|
64
|
-
type: string,
|
|
100
|
+
type: optional(string),
|
|
65
101
|
displayName: "source",
|
|
66
|
-
description: "Source directory to scan
|
|
102
|
+
description: "Source directory to scan (omit to use adapter defaults)",
|
|
67
103
|
}),
|
|
104
|
+
adapter: adapterOpt,
|
|
68
105
|
archiveDir: archiveDirOpt,
|
|
69
106
|
quiet: flag({
|
|
70
107
|
long: "quiet",
|
|
@@ -72,13 +109,26 @@ const archiveCmd = command({
|
|
|
72
109
|
description: "Suppress progress output",
|
|
73
110
|
}),
|
|
74
111
|
},
|
|
75
|
-
async handler({ source, archiveDir, quiet }) {
|
|
112
|
+
async handler({ source, adapter: adapterName, archiveDir, quiet }) {
|
|
76
113
|
const dir = archiveDir ?? DEFAULT_ARCHIVE_DIR;
|
|
77
|
-
const
|
|
114
|
+
const specs = resolveSourceSpecs(source, adapterName);
|
|
115
|
+
|
|
116
|
+
let totalUpdated = 0;
|
|
117
|
+
let totalCurrent = 0;
|
|
118
|
+
let totalErrors = 0;
|
|
119
|
+
|
|
120
|
+
for (const spec of specs) {
|
|
121
|
+
const result = await archiveAll(dir, spec.source, [spec.adapter], {
|
|
122
|
+
quiet,
|
|
123
|
+
});
|
|
124
|
+
totalUpdated += result.updated.length;
|
|
125
|
+
totalCurrent += result.current.length;
|
|
126
|
+
totalErrors += result.errors.length;
|
|
127
|
+
}
|
|
78
128
|
|
|
79
129
|
if (!quiet) {
|
|
80
130
|
console.error(
|
|
81
|
-
`\nArchive complete: ${
|
|
131
|
+
`\nArchive complete: ${totalUpdated} updated, ${totalCurrent} current, ${totalErrors} errors`,
|
|
82
132
|
);
|
|
83
133
|
}
|
|
84
134
|
},
|
|
@@ -143,10 +193,11 @@ const watchCmd = command({
|
|
|
143
193
|
description: "Watch source directories and keep archive updated",
|
|
144
194
|
args: {
|
|
145
195
|
source: positional({
|
|
146
|
-
type: string,
|
|
196
|
+
type: optional(string),
|
|
147
197
|
displayName: "source",
|
|
148
|
-
description: "Source directory to watch
|
|
198
|
+
description: "Source directory to watch (omit to use adapter defaults)",
|
|
149
199
|
}),
|
|
200
|
+
adapter: adapterOpt,
|
|
150
201
|
archiveDir: archiveDirOpt,
|
|
151
202
|
pollInterval: option({
|
|
152
203
|
type: optional(number),
|
|
@@ -159,8 +210,16 @@ const watchCmd = command({
|
|
|
159
210
|
description: "Suppress progress output",
|
|
160
211
|
}),
|
|
161
212
|
},
|
|
162
|
-
async handler({
|
|
163
|
-
|
|
213
|
+
async handler({
|
|
214
|
+
source,
|
|
215
|
+
adapter: adapterName,
|
|
216
|
+
archiveDir,
|
|
217
|
+
pollInterval,
|
|
218
|
+
quiet,
|
|
219
|
+
}) {
|
|
220
|
+
const specs = resolveSourceSpecs(source, adapterName);
|
|
221
|
+
|
|
222
|
+
const watcher = new ArchiveWatcher(specs, {
|
|
164
223
|
archiveDir: archiveDir ?? undefined,
|
|
165
224
|
pollIntervalMs: pollInterval ?? undefined,
|
|
166
225
|
quiet,
|
|
@@ -174,8 +233,9 @@ const watchCmd = command({
|
|
|
174
233
|
},
|
|
175
234
|
});
|
|
176
235
|
|
|
236
|
+
const dirs = [...new Set(specs.map((s) => s.source))].join(", ");
|
|
177
237
|
if (!quiet) {
|
|
178
|
-
console.error(`Watching ${
|
|
238
|
+
console.error(`Watching ${dirs}...`);
|
|
179
239
|
}
|
|
180
240
|
|
|
181
241
|
await watcher.start();
|
package/src/types.ts
CHANGED
|
@@ -91,8 +91,16 @@ export interface Adapter {
|
|
|
91
91
|
name: string;
|
|
92
92
|
/** Versioned identifier for cache invalidation (e.g. "claude-code:1") */
|
|
93
93
|
version: string;
|
|
94
|
+
/** Default source directory for auto-discovery (if the adapter has one) */
|
|
95
|
+
defaultSource?: string;
|
|
94
96
|
/** Discover session files in the given directory */
|
|
95
97
|
discover(source: string): Promise<DiscoveredSession[]>;
|
|
96
98
|
/** Parse source content into one or more transcripts (split by conversation) */
|
|
97
99
|
parse(content: string, sourcePath: string): Transcript[];
|
|
98
100
|
}
|
|
101
|
+
|
|
102
|
+
/** An adapter paired with its source directory. */
|
|
103
|
+
export interface SourceSpec {
|
|
104
|
+
adapter: Adapter;
|
|
105
|
+
source: string;
|
|
106
|
+
}
|
package/src/watch.ts
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Watch module: keep archive in sync with source directories.
|
|
3
3
|
*
|
|
4
4
|
* Uses fs.watch for change detection with periodic full scan as fallback.
|
|
5
|
-
*
|
|
5
|
+
* Multiple watchers can safely target the same archive — writes are atomic
|
|
6
|
+
* (tmp + rename) and archiving is idempotent.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { watch,
|
|
9
|
-
import {
|
|
10
|
-
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
11
|
-
import { getAdapters } from "./adapters/index.ts";
|
|
9
|
+
import { watch, type FSWatcher } from "fs";
|
|
10
|
+
import type { SourceSpec } from "./types.ts";
|
|
12
11
|
import {
|
|
13
12
|
archiveAll,
|
|
14
13
|
DEFAULT_ARCHIVE_DIR,
|
|
@@ -23,65 +22,8 @@ export interface WatchOptions {
|
|
|
23
22
|
quiet?: boolean;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
function lockPath(archiveDir: string): string {
|
|
27
|
-
return join(archiveDir, "archive.lock");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function acquireLock(archiveDir: string): Promise<boolean> {
|
|
31
|
-
await mkdir(archiveDir, { recursive: true });
|
|
32
|
-
const lock = lockPath(archiveDir);
|
|
33
|
-
try {
|
|
34
|
-
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
35
|
-
return true;
|
|
36
|
-
} catch (err: unknown) {
|
|
37
|
-
if (
|
|
38
|
-
err &&
|
|
39
|
-
typeof err === "object" &&
|
|
40
|
-
"code" in err &&
|
|
41
|
-
err.code === "EEXIST"
|
|
42
|
-
) {
|
|
43
|
-
// Lock file exists — check if the holding process is still alive
|
|
44
|
-
try {
|
|
45
|
-
const existing = await readFile(lock, "utf-8");
|
|
46
|
-
const pid = parseInt(existing, 10);
|
|
47
|
-
if (!isNaN(pid)) {
|
|
48
|
-
try {
|
|
49
|
-
process.kill(pid, 0);
|
|
50
|
-
return false; // Process is alive, lock is held
|
|
51
|
-
} catch {
|
|
52
|
-
// Process is dead, stale lock — reclaim
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
await writeFile(lock, String(process.pid), { flag: "w" });
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
throw err;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function releaseLock(archiveDir: string): void {
|
|
66
|
-
try {
|
|
67
|
-
unlinkSync(lockPath(archiveDir));
|
|
68
|
-
} catch (err: unknown) {
|
|
69
|
-
if (
|
|
70
|
-
err &&
|
|
71
|
-
typeof err === "object" &&
|
|
72
|
-
"code" in err &&
|
|
73
|
-
err.code === "ENOENT"
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
console.error(
|
|
78
|
-
`Warning: failed to release lock: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
25
|
export class ArchiveWatcher {
|
|
84
|
-
private
|
|
26
|
+
private sources: SourceSpec[];
|
|
85
27
|
private archiveDir: string;
|
|
86
28
|
private pollIntervalMs: number;
|
|
87
29
|
private onUpdate?: (result: ArchiveResult) => void;
|
|
@@ -91,8 +33,8 @@ export class ArchiveWatcher {
|
|
|
91
33
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
92
34
|
private scanning = false;
|
|
93
35
|
|
|
94
|
-
constructor(
|
|
95
|
-
this.
|
|
36
|
+
constructor(sources: SourceSpec[], options: WatchOptions = {}) {
|
|
37
|
+
this.sources = sources;
|
|
96
38
|
this.archiveDir = options.archiveDir ?? DEFAULT_ARCHIVE_DIR;
|
|
97
39
|
this.pollIntervalMs = options.pollIntervalMs ?? 30_000;
|
|
98
40
|
this.onUpdate = options.onUpdate;
|
|
@@ -101,29 +43,30 @@ export class ArchiveWatcher {
|
|
|
101
43
|
}
|
|
102
44
|
|
|
103
45
|
async start(): Promise<void> {
|
|
104
|
-
const locked = await acquireLock(this.archiveDir);
|
|
105
|
-
if (!locked) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`Another watcher is already running on ${this.archiveDir}`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
46
|
// Initial scan
|
|
112
47
|
await this.scan();
|
|
113
48
|
|
|
114
|
-
// Set up fs.watch on each source dir
|
|
115
|
-
|
|
49
|
+
// Set up fs.watch on each unique source dir
|
|
50
|
+
const watchedDirs = new Set<string>();
|
|
51
|
+
for (const spec of this.sources) {
|
|
52
|
+
if (watchedDirs.has(spec.source)) continue;
|
|
53
|
+
watchedDirs.add(spec.source);
|
|
54
|
+
|
|
116
55
|
try {
|
|
117
|
-
const watcher = watch(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
56
|
+
const watcher = watch(
|
|
57
|
+
spec.source,
|
|
58
|
+
{ recursive: true },
|
|
59
|
+
(_event, filename) => {
|
|
60
|
+
if (filename && filename.endsWith(".jsonl")) {
|
|
61
|
+
this.debouncedScan();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
122
65
|
this.watchers.push(watcher);
|
|
123
66
|
} catch (err) {
|
|
124
67
|
if (!this.quiet) {
|
|
125
68
|
console.error(
|
|
126
|
-
`Warning: could not watch ${
|
|
69
|
+
`Warning: could not watch ${spec.source}: ${err instanceof Error ? err.message : String(err)}`,
|
|
127
70
|
);
|
|
128
71
|
}
|
|
129
72
|
}
|
|
@@ -143,8 +86,6 @@ export class ArchiveWatcher {
|
|
|
143
86
|
clearInterval(this.pollTimer);
|
|
144
87
|
this.pollTimer = null;
|
|
145
88
|
}
|
|
146
|
-
|
|
147
|
-
releaseLock(this.archiveDir);
|
|
148
89
|
}
|
|
149
90
|
|
|
150
91
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -159,11 +100,13 @@ export class ArchiveWatcher {
|
|
|
159
100
|
this.scanning = true;
|
|
160
101
|
|
|
161
102
|
try {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
103
|
+
for (const spec of this.sources) {
|
|
104
|
+
const result = await archiveAll(
|
|
105
|
+
this.archiveDir,
|
|
106
|
+
spec.source,
|
|
107
|
+
[spec.adapter],
|
|
108
|
+
{ quiet: this.quiet },
|
|
109
|
+
);
|
|
167
110
|
if (result.updated.length > 0 || result.errors.length > 0) {
|
|
168
111
|
this.onUpdate?.(result);
|
|
169
112
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { getDefaultSources, getAdapter } from "../src/adapters/index.ts";
|
|
3
|
+
import { claudeCodeAdapter } from "../src/adapters/claude-code.ts";
|
|
4
|
+
|
|
5
|
+
describe("getDefaultSources", () => {
|
|
6
|
+
it("returns claude-code with its defaultSource", () => {
|
|
7
|
+
const specs = getDefaultSources();
|
|
8
|
+
expect(specs.length).toBeGreaterThanOrEqual(1);
|
|
9
|
+
|
|
10
|
+
const cc = specs.find((s) => s.adapter.name === "claude-code");
|
|
11
|
+
expect(cc).toBeDefined();
|
|
12
|
+
expect(cc?.adapter).toBe(claudeCodeAdapter);
|
|
13
|
+
expect(typeof cc?.source).toBe("string");
|
|
14
|
+
expect(cc?.source.length).toBeGreaterThan(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("only includes adapters with defaultSource set", () => {
|
|
18
|
+
for (const spec of getDefaultSources()) {
|
|
19
|
+
expect(spec.source).toBeTruthy();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("getAdapter", () => {
|
|
25
|
+
it("returns adapter by name", () => {
|
|
26
|
+
expect(getAdapter("claude-code")).toBe(claudeCodeAdapter);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns undefined for unknown adapter", () => {
|
|
30
|
+
expect(getAdapter("nonexistent")).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { ArchiveWatcher } from "../src/watch.ts";
|
|
6
|
+
import { claudeCodeAdapter } from "../src/adapters/claude-code.ts";
|
|
7
|
+
import { listEntries } from "../src/archive.ts";
|
|
8
|
+
|
|
9
|
+
const fixturesDir = join(import.meta.dir, "fixtures/claude");
|
|
10
|
+
|
|
11
|
+
let archiveDir: string;
|
|
12
|
+
let sourceDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
archiveDir = await mkdtemp(join(tmpdir(), "watch-archive-"));
|
|
16
|
+
sourceDir = await mkdtemp(join(tmpdir(), "watch-source-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(archiveDir, { recursive: true, force: true });
|
|
21
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("ArchiveWatcher with SourceSpec[]", () => {
|
|
25
|
+
it("archives from a single source spec on initial scan", async () => {
|
|
26
|
+
// Copy a fixture into the source dir
|
|
27
|
+
const content = await Bun.file(
|
|
28
|
+
join(fixturesDir, "basic-conversation.input.jsonl"),
|
|
29
|
+
).text();
|
|
30
|
+
await Bun.write(join(sourceDir, "session.jsonl"), content);
|
|
31
|
+
|
|
32
|
+
const updates: string[] = [];
|
|
33
|
+
const watcher = new ArchiveWatcher(
|
|
34
|
+
[{ adapter: claudeCodeAdapter, source: sourceDir }],
|
|
35
|
+
{
|
|
36
|
+
archiveDir,
|
|
37
|
+
quiet: true,
|
|
38
|
+
onUpdate(result) {
|
|
39
|
+
updates.push(...result.updated);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await watcher.start();
|
|
45
|
+
watcher.stop();
|
|
46
|
+
|
|
47
|
+
expect(updates.length).toBe(1);
|
|
48
|
+
const entries = await listEntries(archiveDir);
|
|
49
|
+
expect(entries.length).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("archives from multiple source specs", async () => {
|
|
53
|
+
const sourceDir2 = await mkdtemp(join(tmpdir(), "watch-source2-"));
|
|
54
|
+
|
|
55
|
+
const content = await Bun.file(
|
|
56
|
+
join(fixturesDir, "basic-conversation.input.jsonl"),
|
|
57
|
+
).text();
|
|
58
|
+
await Bun.write(join(sourceDir, "a.jsonl"), content);
|
|
59
|
+
await Bun.write(join(sourceDir2, "b.jsonl"), content);
|
|
60
|
+
|
|
61
|
+
const updates: string[] = [];
|
|
62
|
+
const watcher = new ArchiveWatcher(
|
|
63
|
+
[
|
|
64
|
+
{ adapter: claudeCodeAdapter, source: sourceDir },
|
|
65
|
+
{ adapter: claudeCodeAdapter, source: sourceDir2 },
|
|
66
|
+
],
|
|
67
|
+
{
|
|
68
|
+
archiveDir,
|
|
69
|
+
quiet: true,
|
|
70
|
+
onUpdate(result) {
|
|
71
|
+
updates.push(...result.updated);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await watcher.start();
|
|
77
|
+
watcher.stop();
|
|
78
|
+
|
|
79
|
+
expect(updates.length).toBe(2);
|
|
80
|
+
const entries = await listEntries(archiveDir);
|
|
81
|
+
expect(entries.length).toBe(2);
|
|
82
|
+
|
|
83
|
+
await rm(sourceDir2, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
});
|