@cleocode/adapters 2026.3.74 → 2026.4.0
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/dist/index.js +140 -154
- package/dist/index.js.map +4 -4
- package/dist/providers/codex/hooks.d.ts +2 -2
- package/dist/providers/codex/hooks.d.ts.map +1 -1
- package/dist/providers/codex/hooks.js +5 -53
- package/dist/providers/codex/hooks.js.map +1 -1
- package/dist/providers/gemini-cli/hooks.d.ts +2 -2
- package/dist/providers/gemini-cli/hooks.d.ts.map +1 -1
- package/dist/providers/gemini-cli/hooks.js +5 -53
- package/dist/providers/gemini-cli/hooks.js.map +1 -1
- package/dist/providers/shared/transcript-reader.d.ts +43 -0
- package/dist/providers/shared/transcript-reader.d.ts.map +1 -0
- package/dist/providers/shared/transcript-reader.js +109 -0
- package/dist/providers/shared/transcript-reader.js.map +1 -0
- package/package.json +3 -3
- package/src/providers/codex/hooks.ts +5 -52
- package/src/providers/gemini-cli/hooks.ts +5 -52
- package/src/providers/shared/transcript-reader.ts +123 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared transcript-reading utility for provider hook adapters.
|
|
3
|
+
*
|
|
4
|
+
* Several providers (Gemini CLI, Codex CLI) store session data in a
|
|
5
|
+
* flat directory of JSON/JSONL files using the same role/content schema.
|
|
6
|
+
* This module centralises the "find most-recent file, parse turns"
|
|
7
|
+
* logic to avoid duplicating it in each hook provider.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { readLatestTranscript } from '../shared/transcript-reader.js';
|
|
12
|
+
*
|
|
13
|
+
* async getTranscript(_sessionId: string, _projectDir: string) {
|
|
14
|
+
* return readLatestTranscript(join(homedir(), '.gemini'));
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @task T161
|
|
19
|
+
* @epic T134
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** A single parsed conversation turn from a provider session file. */
|
|
30
|
+
interface TranscriptTurn {
|
|
31
|
+
role: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a raw JSONL or JSON session file into an array of transcript turns.
|
|
41
|
+
*
|
|
42
|
+
* Lines that are not valid JSON, or that lack a string `role` and string
|
|
43
|
+
* `content`, are silently skipped.
|
|
44
|
+
*
|
|
45
|
+
* @param raw - Raw file contents (UTF-8 string).
|
|
46
|
+
* @returns Array of `{ role, content }` pairs, in file order.
|
|
47
|
+
*/
|
|
48
|
+
function parseTranscriptLines(raw: string): TranscriptTurn[] {
|
|
49
|
+
const turns: TranscriptTurn[] = [];
|
|
50
|
+
const lines = raw.split('\n').filter((l) => l.trim());
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
const entry = JSON.parse(line) as Record<string, unknown>;
|
|
55
|
+
const role = entry.role;
|
|
56
|
+
const content = entry.content;
|
|
57
|
+
if (typeof role === 'string' && typeof content === 'string') {
|
|
58
|
+
turns.push({ role, content });
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Skip malformed lines
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return turns;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Public API
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read the most recent JSON or JSONL session file from `providerDir` and
|
|
74
|
+
* return its contents as a flat transcript string.
|
|
75
|
+
*
|
|
76
|
+
* Files are sorted in descending order by filename — this works naturally
|
|
77
|
+
* for providers that embed timestamps in filenames. The most recently named
|
|
78
|
+
* file is read first.
|
|
79
|
+
*
|
|
80
|
+
* Returns `null` when:
|
|
81
|
+
* - `providerDir` does not exist or cannot be read
|
|
82
|
+
* - No JSON/JSONL files are present
|
|
83
|
+
* - The most recent file contains no parseable turns
|
|
84
|
+
*
|
|
85
|
+
* @param providerDir - Absolute path to the provider's session directory
|
|
86
|
+
* (e.g. `~/.gemini` or `~/.codex`).
|
|
87
|
+
* @returns A plain-text transcript with lines of the form `role: content`,
|
|
88
|
+
* or `null` if no transcript could be extracted.
|
|
89
|
+
*
|
|
90
|
+
* @task T161
|
|
91
|
+
* @epic T134
|
|
92
|
+
*/
|
|
93
|
+
export async function readLatestTranscript(providerDir: string): Promise<string | null> {
|
|
94
|
+
let allFiles: string[] = [];
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const entries = await readdir(providerDir, { withFileTypes: true });
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (!entry.isFile()) continue;
|
|
100
|
+
const name = entry.name;
|
|
101
|
+
if (name.endsWith('.json') || name.endsWith('.jsonl')) {
|
|
102
|
+
allFiles.push(join(providerDir, name));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (allFiles.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
// Sort descending — timestamps in filenames sort naturally
|
|
112
|
+
allFiles = allFiles.sort((a, b) => b.localeCompare(a));
|
|
113
|
+
const mostRecent = allFiles[0];
|
|
114
|
+
if (!mostRecent) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const raw = await readFile(mostRecent, 'utf-8');
|
|
118
|
+
const turns = parseTranscriptLines(raw);
|
|
119
|
+
return turns.length > 0 ? turns.map((t) => `${t.role}: ${t.content}`).join('\n') : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|