@v1nvn/agentic-core 0.44.0 → 0.45.1

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/src/draw.js ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The drawing kit — the one home of the repo's terminal drawing model: ink
3
+ * kinds are a closed union, color values are data, surfaces render
4
+ * documents, and `toSgr` is the one skin, resetting each inked segment
5
+ * unless it carries its ink into the next. Loaded only by processes — the
6
+ * engine's island loader imports its own files by relative path alone.
7
+ */
8
+ export function rgb(r, g, b) {
9
+ return { rgb: [r, g, b] };
10
+ }
11
+ export function indexed(n) {
12
+ return { indexed: n };
13
+ }
14
+ export function bg(ink) {
15
+ return { bg: ink };
16
+ }
17
+ export function line(...segments) {
18
+ return segments;
19
+ }
20
+ export function doc(...rows) {
21
+ return rows;
22
+ }
23
+ export function plain(text) {
24
+ return { text };
25
+ }
26
+ export function dim(text) {
27
+ return { text, ink: 'dim' };
28
+ }
29
+ export function bold(text) {
30
+ return { text, ink: 'bold' };
31
+ }
32
+ export function green(text) {
33
+ return { text, ink: 'green' };
34
+ }
35
+ export function magenta(text) {
36
+ return { text, ink: 'magenta' };
37
+ }
38
+ export function red(text) {
39
+ return { text, ink: 'red' };
40
+ }
41
+ export function yellow(text) {
42
+ return { text, ink: 'yellow' };
43
+ }
44
+ function assertNever(kind) {
45
+ throw new Error(`unhandled Ink kind: ${String(kind)}`);
46
+ }
47
+ function paramsOf(ink) {
48
+ if (typeof ink !== 'string') {
49
+ if ('bg' in ink) {
50
+ return paramsOf(ink.bg)
51
+ .replace(/^3(\d)/, '4$1')
52
+ .replace(/^9(\d)/, '10$1');
53
+ }
54
+ return 'indexed' in ink
55
+ ? `38;5;${ink.indexed}`
56
+ : `38;2;${ink.rgb[0]};${ink.rgb[1]};${ink.rgb[2]}`;
57
+ }
58
+ switch (ink) {
59
+ case 'blue':
60
+ return '34';
61
+ case 'bold':
62
+ return '1';
63
+ case 'brightBlack':
64
+ return '90';
65
+ case 'cyan':
66
+ return '36';
67
+ case 'dim':
68
+ return '2';
69
+ case 'green':
70
+ return '32';
71
+ case 'magenta':
72
+ return '35';
73
+ case 'mark':
74
+ return '1;97';
75
+ case 'plain':
76
+ return '39';
77
+ case 'red':
78
+ return '31';
79
+ case 'white':
80
+ return '37';
81
+ case 'yellow':
82
+ return '33';
83
+ default:
84
+ return assertNever(ink);
85
+ }
86
+ }
87
+ function rowsOf(rows) {
88
+ return rows.every(r => Array.isArray(r))
89
+ ? rows
90
+ : [rows];
91
+ }
92
+ export function toSgr(rows) {
93
+ return rowsOf(rows)
94
+ .map(row => {
95
+ let out = '';
96
+ let inkOpen = false;
97
+ for (const seg of row) {
98
+ if (seg.ink === undefined) {
99
+ out += (inkOpen ? '\x1b[0m' : '') + seg.text;
100
+ inkOpen = false;
101
+ continue;
102
+ }
103
+ out += `\x1b[${paramsOf(seg.ink)}m${seg.text}`;
104
+ if (seg.carry === true) {
105
+ inkOpen = true;
106
+ }
107
+ else {
108
+ out += '\x1b[0m';
109
+ inkOpen = false;
110
+ }
111
+ }
112
+ return inkOpen ? `${out}\x1b[0m` : out;
113
+ })
114
+ .join('\n');
115
+ }
116
+ export function stripSgr(text) {
117
+ // eslint-disable-next-line no-control-regex
118
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
119
+ }
120
+ export function textOf(rows) {
121
+ return rowsOf(rows)
122
+ .map(row => row.map(seg => seg.text).join(''))
123
+ .join('\n');
124
+ }
125
+ function stdioOf(result) {
126
+ return typeof result === 'object' && result !== null ? result : {};
127
+ }
128
+ export function stdoutOf(result) {
129
+ const { stdout } = stdioOf(result);
130
+ return typeof stdout === 'string' ? stdout : '';
131
+ }
132
+ /** The one line an exec shows: the refusal's reason, stdout, else stderr or `fallback`. */
133
+ export function lineOf(call, fallback) {
134
+ if (call.deny !== undefined) {
135
+ return call.deny;
136
+ }
137
+ if (call.isError === true) {
138
+ return call.text ?? fallback;
139
+ }
140
+ const out = stdoutOf(call.result).trim();
141
+ if (out !== '') {
142
+ return out;
143
+ }
144
+ const { stderr } = stdioOf(call.result);
145
+ const err = typeof stderr === 'string' ? stderr.trim() : '';
146
+ return err !== '' ? err : fallback;
147
+ }
148
+ export const RULE_WIDTH = 68;
149
+ export const BLOCK = '█';
150
+ export const TRACK = '·';
151
+ export const EIGHTHS = [
152
+ '',
153
+ '▏',
154
+ '▎',
155
+ '▍',
156
+ '▌',
157
+ '▋',
158
+ '▊',
159
+ '▉',
160
+ ];
161
+ export function rule(width = RULE_WIDTH) {
162
+ return '─'.repeat(Math.max(0, width));
163
+ }
164
+ export function pad(text, width, side) {
165
+ if (text.length >= width) {
166
+ return text;
167
+ }
168
+ const gap = ' '.repeat(width - text.length);
169
+ return side === 'left' ? gap + text : text + gap;
170
+ }
171
+ export function meter(pct, width, filled = 'green') {
172
+ const safePct = typeof pct === 'number' && Number.isFinite(pct) ? pct : 0;
173
+ const n = Math.max(0, Math.min(width, Math.round((safePct / 100) * width)));
174
+ return line({ text: BLOCK.repeat(n), ink: filled }, { text: TRACK.repeat(width - n), ink: 'dim' });
175
+ }
176
+ /** Fixed-width bar field: full blocks, an eighth-fraction edge, spaces to width. */
177
+ export function bar(value, max, width) {
178
+ if (!(value > 0) || !(max > 0)) {
179
+ return ' '.repeat(width);
180
+ }
181
+ const scaled = (value / max) * width;
182
+ let full = Math.floor(scaled);
183
+ let fi = Math.round((scaled - full) * 8);
184
+ if (fi === 8) {
185
+ full += 1;
186
+ fi = 0;
187
+ }
188
+ if (full === 0 && fi === 0) {
189
+ fi = 1;
190
+ }
191
+ let out = BLOCK.repeat(Math.min(full, width));
192
+ if (full < width && fi > 0) {
193
+ out += EIGHTHS[fi] ?? '';
194
+ }
195
+ return pad(out, width, 'right');
196
+ }
197
+ export function pad2(n) {
198
+ return String(n).padStart(2, '0');
199
+ }
200
+ export const MONTHS = [
201
+ 'Jan',
202
+ 'Feb',
203
+ 'Mar',
204
+ 'Apr',
205
+ 'May',
206
+ 'Jun',
207
+ 'Jul',
208
+ 'Aug',
209
+ 'Sep',
210
+ 'Oct',
211
+ 'Nov',
212
+ 'Dec',
213
+ ];
214
+ /** Compact count: 1.2K / 3.4M / 5.6B; '—' for absent or NaN. */
215
+ export function fmtTokens(n) {
216
+ if (n == null || Number.isNaN(n)) {
217
+ return '—';
218
+ }
219
+ if (n >= 1e9) {
220
+ return (n / 1e9).toFixed(1) + 'B';
221
+ }
222
+ if (n >= 1e6) {
223
+ return (n / 1e6).toFixed(1) + 'M';
224
+ }
225
+ if (n >= 1e3) {
226
+ return (n / 1e3).toFixed(1) + 'K';
227
+ }
228
+ return String(n);
229
+ }
230
+ /** Grouped integer, en-US separators; 0 for absent. */
231
+ export function fmtNum(n) {
232
+ return (n || 0).toLocaleString('en-US');
233
+ }
234
+ /** Local date-time stamp: `YYYY-MM-DD`, plus `HH:mm` and optionally seconds. */
235
+ export function localStamp(now, clock) {
236
+ const day = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
237
+ if (clock === 'none') {
238
+ return day;
239
+ }
240
+ const hhmm = `${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
241
+ return clock === 'second'
242
+ ? `${day} ${hhmm}:${pad2(now.getSeconds())}`
243
+ : `${day} ${hhmm}`;
244
+ }
package/src/draw.ts ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * The drawing kit — the one home of the repo's terminal drawing model: ink
3
+ * kinds are a closed union, color values are data, surfaces render
4
+ * documents, and `toSgr` is the one skin, resetting each inked segment
5
+ * unless it carries its ink into the next. Loaded only by processes — the
6
+ * engine's island loader imports its own files by relative path alone.
7
+ */
8
+
9
+ export type InkKind =
10
+ | 'blue'
11
+ | 'bold'
12
+ | 'brightBlack'
13
+ | 'cyan'
14
+ | 'dim'
15
+ | 'green'
16
+ | 'magenta'
17
+ | 'mark'
18
+ | 'plain'
19
+ | 'red'
20
+ | 'white'
21
+ | 'yellow';
22
+
23
+ export type Ink =
24
+ | InkKind
25
+ | { readonly bg: Ink }
26
+ | { readonly indexed: number }
27
+ | { readonly rgb: readonly [number, number, number] };
28
+
29
+ export function rgb(r: number, g: number, b: number): Ink {
30
+ return { rgb: [r, g, b] };
31
+ }
32
+
33
+ export function indexed(n: number): Ink {
34
+ return { indexed: n };
35
+ }
36
+
37
+ export function bg(ink: Ink): Ink {
38
+ return { bg: ink };
39
+ }
40
+
41
+ export interface Segment {
42
+ readonly carry?: boolean;
43
+ readonly ink?: Ink;
44
+ readonly text: string;
45
+ }
46
+
47
+ declare const lineBrand: unique symbol;
48
+ declare const docBrand: unique symbol;
49
+
50
+ export type Line = readonly Segment[] & { readonly [lineBrand]: true };
51
+ export type Doc = readonly Line[] & { readonly [docBrand]: true };
52
+
53
+ export function line(...segments: Segment[]): Line {
54
+ return segments as unknown as Line;
55
+ }
56
+
57
+ export function doc(...rows: Line[]): Doc {
58
+ return rows as unknown as Doc;
59
+ }
60
+
61
+ export function plain(text: string): Segment {
62
+ return { text };
63
+ }
64
+
65
+ export function dim(text: string): Segment {
66
+ return { text, ink: 'dim' };
67
+ }
68
+
69
+ export function bold(text: string): Segment {
70
+ return { text, ink: 'bold' };
71
+ }
72
+
73
+ export function green(text: string): Segment {
74
+ return { text, ink: 'green' };
75
+ }
76
+
77
+ export function magenta(text: string): Segment {
78
+ return { text, ink: 'magenta' };
79
+ }
80
+
81
+ export function red(text: string): Segment {
82
+ return { text, ink: 'red' };
83
+ }
84
+
85
+ export function yellow(text: string): Segment {
86
+ return { text, ink: 'yellow' };
87
+ }
88
+
89
+ function assertNever(kind: never): never {
90
+ throw new Error(`unhandled Ink kind: ${String(kind)}`);
91
+ }
92
+
93
+ function paramsOf(ink: Ink): string {
94
+ if (typeof ink !== 'string') {
95
+ if ('bg' in ink) {
96
+ return paramsOf(ink.bg)
97
+ .replace(/^3(\d)/, '4$1')
98
+ .replace(/^9(\d)/, '10$1');
99
+ }
100
+ return 'indexed' in ink
101
+ ? `38;5;${ink.indexed}`
102
+ : `38;2;${ink.rgb[0]};${ink.rgb[1]};${ink.rgb[2]}`;
103
+ }
104
+ switch (ink) {
105
+ case 'blue':
106
+ return '34';
107
+ case 'bold':
108
+ return '1';
109
+ case 'brightBlack':
110
+ return '90';
111
+ case 'cyan':
112
+ return '36';
113
+ case 'dim':
114
+ return '2';
115
+ case 'green':
116
+ return '32';
117
+ case 'magenta':
118
+ return '35';
119
+ case 'mark':
120
+ return '1;97';
121
+ case 'plain':
122
+ return '39';
123
+ case 'red':
124
+ return '31';
125
+ case 'white':
126
+ return '37';
127
+ case 'yellow':
128
+ return '33';
129
+ default:
130
+ return assertNever(ink);
131
+ }
132
+ }
133
+
134
+ function rowsOf(rows: Doc | Line): readonly (readonly Segment[])[] {
135
+ return (rows as readonly unknown[]).every(r => Array.isArray(r))
136
+ ? (rows as Doc)
137
+ : [rows as Line];
138
+ }
139
+
140
+ export function toSgr(rows: Doc | Line): string {
141
+ return rowsOf(rows)
142
+ .map(row => {
143
+ let out = '';
144
+ let inkOpen = false;
145
+ for (const seg of row) {
146
+ if (seg.ink === undefined) {
147
+ out += (inkOpen ? '\x1b[0m' : '') + seg.text;
148
+ inkOpen = false;
149
+ continue;
150
+ }
151
+ out += `\x1b[${paramsOf(seg.ink)}m${seg.text}`;
152
+ if (seg.carry === true) {
153
+ inkOpen = true;
154
+ } else {
155
+ out += '\x1b[0m';
156
+ inkOpen = false;
157
+ }
158
+ }
159
+ return inkOpen ? `${out}\x1b[0m` : out;
160
+ })
161
+ .join('\n');
162
+ }
163
+
164
+ export function stripSgr(text: string): string {
165
+ // eslint-disable-next-line no-control-regex
166
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
167
+ }
168
+
169
+ export function textOf(rows: Doc | Line): string {
170
+ return rowsOf(rows)
171
+ .map(row => row.map(seg => seg.text).join(''))
172
+ .join('\n');
173
+ }
174
+
175
+ export interface ToolLineSource {
176
+ readonly deny?: string;
177
+ readonly isError?: boolean;
178
+ readonly result?: unknown;
179
+ readonly text?: string;
180
+ }
181
+
182
+ interface Stdio {
183
+ readonly stderr?: unknown;
184
+ readonly stdout?: unknown;
185
+ }
186
+
187
+ function stdioOf(result: unknown): Stdio {
188
+ return typeof result === 'object' && result !== null ? result : {};
189
+ }
190
+
191
+ export function stdoutOf(result: unknown): string {
192
+ const { stdout } = stdioOf(result);
193
+ return typeof stdout === 'string' ? stdout : '';
194
+ }
195
+
196
+ /** The one line an exec shows: the refusal's reason, stdout, else stderr or `fallback`. */
197
+ export function lineOf(call: ToolLineSource, fallback: string): string {
198
+ if (call.deny !== undefined) {
199
+ return call.deny;
200
+ }
201
+ if (call.isError === true) {
202
+ return call.text ?? fallback;
203
+ }
204
+ const out = stdoutOf(call.result).trim();
205
+ if (out !== '') {
206
+ return out;
207
+ }
208
+ const { stderr } = stdioOf(call.result);
209
+ const err = typeof stderr === 'string' ? stderr.trim() : '';
210
+ return err !== '' ? err : fallback;
211
+ }
212
+
213
+ export const RULE_WIDTH = 68;
214
+
215
+ export const BLOCK = '█';
216
+ export const TRACK = '·';
217
+ export const EIGHTHS: readonly (string | undefined)[] = [
218
+ '',
219
+ '▏',
220
+ '▎',
221
+ '▍',
222
+ '▌',
223
+ '▋',
224
+ '▊',
225
+ '▉',
226
+ ];
227
+
228
+ export function rule(width: number = RULE_WIDTH): string {
229
+ return '─'.repeat(Math.max(0, width));
230
+ }
231
+
232
+ export function pad(
233
+ text: string,
234
+ width: number,
235
+ side: 'left' | 'right',
236
+ ): string {
237
+ if (text.length >= width) {
238
+ return text;
239
+ }
240
+ const gap = ' '.repeat(width - text.length);
241
+ return side === 'left' ? gap + text : text + gap;
242
+ }
243
+
244
+ export function meter(
245
+ pct: null | number | undefined,
246
+ width: number,
247
+ filled: Ink = 'green',
248
+ ): Line {
249
+ const safePct = typeof pct === 'number' && Number.isFinite(pct) ? pct : 0;
250
+ const n = Math.max(0, Math.min(width, Math.round((safePct / 100) * width)));
251
+ return line(
252
+ { text: BLOCK.repeat(n), ink: filled },
253
+ { text: TRACK.repeat(width - n), ink: 'dim' },
254
+ );
255
+ }
256
+
257
+ /** Fixed-width bar field: full blocks, an eighth-fraction edge, spaces to width. */
258
+ export function bar(value: number, max: number, width: number): string {
259
+ if (!(value > 0) || !(max > 0)) {
260
+ return ' '.repeat(width);
261
+ }
262
+ const scaled = (value / max) * width;
263
+ let full = Math.floor(scaled);
264
+ let fi = Math.round((scaled - full) * 8);
265
+ if (fi === 8) {
266
+ full += 1;
267
+ fi = 0;
268
+ }
269
+ if (full === 0 && fi === 0) {
270
+ fi = 1;
271
+ }
272
+ let out = BLOCK.repeat(Math.min(full, width));
273
+ if (full < width && fi > 0) {
274
+ out += EIGHTHS[fi] ?? '';
275
+ }
276
+ return pad(out, width, 'right');
277
+ }
278
+
279
+ export function pad2(n: number): string {
280
+ return String(n).padStart(2, '0');
281
+ }
282
+
283
+ export const MONTHS = [
284
+ 'Jan',
285
+ 'Feb',
286
+ 'Mar',
287
+ 'Apr',
288
+ 'May',
289
+ 'Jun',
290
+ 'Jul',
291
+ 'Aug',
292
+ 'Sep',
293
+ 'Oct',
294
+ 'Nov',
295
+ 'Dec',
296
+ ] as const;
297
+
298
+ /** Compact count: 1.2K / 3.4M / 5.6B; '—' for absent or NaN. */
299
+ export function fmtTokens(n: null | number | undefined): string {
300
+ if (n == null || Number.isNaN(n)) {
301
+ return '—';
302
+ }
303
+ if (n >= 1e9) {
304
+ return (n / 1e9).toFixed(1) + 'B';
305
+ }
306
+ if (n >= 1e6) {
307
+ return (n / 1e6).toFixed(1) + 'M';
308
+ }
309
+ if (n >= 1e3) {
310
+ return (n / 1e3).toFixed(1) + 'K';
311
+ }
312
+ return String(n);
313
+ }
314
+
315
+ /** Grouped integer, en-US separators; 0 for absent. */
316
+ export function fmtNum(n: null | number | undefined): string {
317
+ return (n || 0).toLocaleString('en-US');
318
+ }
319
+
320
+ export type Clock = 'minute' | 'none' | 'second';
321
+
322
+ /** Local date-time stamp: `YYYY-MM-DD`, plus `HH:mm` and optionally seconds. */
323
+ export function localStamp(now: Date, clock: Clock): string {
324
+ const day = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
325
+ if (clock === 'none') {
326
+ return day;
327
+ }
328
+ const hhmm = `${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
329
+ return clock === 'second'
330
+ ? `${day} ${hhmm}:${pad2(now.getSeconds())}`
331
+ : `${day} ${hhmm}`;
332
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./cli.js";
2
+ export * from "./dev.js";
3
+ export * from "./draw.js";
4
+ export * from "./input.js";
5
+ export * from "./last-reply.js";
6
+ export * from "./logger.js";
7
+ export * from "./mcp.js";
8
+ export * from "./shutdown.js";
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
- export * from './cli.js';
2
- export * from './dev.js';
3
- export * from './input.js';
4
- export * from './last-reply.js';
5
- export * from './logger.js';
6
- export * from './shutdown.js';
1
+ export * from './cli.ts';
2
+ export * from './dev.ts';
3
+ export * from './draw.ts';
4
+ export * from './input.ts';
5
+ export * from './last-reply.ts';
6
+ export * from './logger.ts';
7
+ export * from './mcp.ts';
8
+ export * from './shutdown.ts';
package/src/input.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ function readAll(stream) {
3
+ return new Promise((resolve, reject) => {
4
+ let data = '';
5
+ stream.setEncoding('utf8');
6
+ stream.on('data', (chunk) => {
7
+ data += chunk;
8
+ });
9
+ stream.on('end', () => {
10
+ resolve(data);
11
+ });
12
+ stream.on('error', reject);
13
+ });
14
+ }
15
+ export function readStdin(stream = process.stdin) {
16
+ return readAll(stream);
17
+ }
18
+ /** A markdown file's contents, with the CLI's no-such-file error. */
19
+ export function readMarkdownFile(path) {
20
+ const stats = statSync(path, { throwIfNoEntry: false });
21
+ if (!stats?.isFile()) {
22
+ throw new Error(`no such file: ${path}`);
23
+ }
24
+ return readFileSync(path, 'utf8');
25
+ }
@@ -0,0 +1,75 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ function isFile(path) {
5
+ try {
6
+ return statSync(path).isFile();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ function textBlocks(entry) {
13
+ const content = entry.message?.content;
14
+ if (entry.type !== 'assistant' || !Array.isArray(content)) {
15
+ return undefined;
16
+ }
17
+ const blocks = content.filter((b) => typeof b === 'object' &&
18
+ b !== null &&
19
+ b.type === 'text');
20
+ return blocks.length > 0 ? blocks : undefined;
21
+ }
22
+ export function claudeProjectsDir() {
23
+ const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), '.claude');
24
+ return join(claudeDir, 'projects');
25
+ }
26
+ /**
27
+ * The last assistant text reply from a Claude Code session, reproducing Claude
28
+ * Code's `/copy` byte-for-byte: the last assistant entry that contains a
29
+ * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks
30
+ * joined with a blank line, and no trailing newline.
31
+ *
32
+ * @param arg a transcript file, a session UUID under the project dir, or
33
+ * nothing to use the newest session for the current project.
34
+ */
35
+ export function lastReply(arg) {
36
+ // Claude Code keys transcripts under the project root with "/" → "-".
37
+ // Prefer $CLAUDE_PROJECT_DIR (exported to hook processes) over $PWD.
38
+ const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll('/', '-');
39
+ const projDir = join(claudeProjectsDir(), proj);
40
+ let file;
41
+ if (arg !== undefined) {
42
+ file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);
43
+ }
44
+ else if (existsSync(projDir)) {
45
+ const newest = readdirSync(projDir)
46
+ .filter(f => f.endsWith('.jsonl'))
47
+ .map(f => ({ f, mtimeMs: statSync(join(projDir, f)).mtimeMs }))
48
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
49
+ .at(0);
50
+ if (newest) {
51
+ file = join(projDir, newest.f);
52
+ }
53
+ }
54
+ if (file === undefined || !isFile(file)) {
55
+ throw new Error(`no session transcript found in ${projDir}`);
56
+ }
57
+ let last;
58
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
59
+ if (!line.includes('"assistant"')) {
60
+ continue;
61
+ }
62
+ let entry;
63
+ try {
64
+ entry = JSON.parse(line);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ const blocks = textBlocks(entry);
70
+ if (blocks) {
71
+ last = blocks;
72
+ }
73
+ }
74
+ return (last ?? []).map(b => b.text ?? '').join('\n\n');
75
+ }