pi-read-map 1.0.0 → 1.2.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/.codemap/cache.db +0 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +7 -0
- package/AGENTS.md +158 -0
- package/CHANGELOG.md +49 -0
- package/README.md +10 -6
- package/demo.md +108 -0
- package/knip.json +11 -0
- package/package.json +11 -3
- package/scripts/go_outline.go +43 -11
- package/scripts/python_outline.py +16 -0
- package/src/.codemap/cache.db +0 -0
- package/src/formatter.ts +29 -12
- package/src/index.ts +124 -2
- package/src/language-detect.ts +6 -0
- package/src/mapper.ts +4 -0
- package/src/mappers/c.ts +7 -0
- package/src/mappers/clojure.ts +613 -0
- package/src/mappers/cpp.ts +58 -0
- package/src/mappers/csv.ts +1 -0
- package/src/mappers/ctags.ts +33 -4
- package/src/mappers/fallback.ts +1 -0
- package/src/mappers/go.ts +11 -4
- package/src/mappers/json.ts +1 -0
- package/src/mappers/jsonl.ts +468 -68
- package/src/mappers/markdown.ts +1 -0
- package/src/mappers/python.ts +12 -4
- package/src/mappers/rust.ts +46 -0
- package/src/mappers/sql.ts +1 -0
- package/src/mappers/toml.ts +1 -0
- package/src/mappers/typescript.ts +36 -1
- package/src/mappers/yaml.ts +1 -0
- package/src/types.ts +6 -2
package/src/mappers/jsonl.ts
CHANGED
|
@@ -36,95 +36,466 @@ function analyzeJsonLine(
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Pi session detection and parsing
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
interface PiSessionHeader {
|
|
44
|
+
type: "session";
|
|
45
|
+
version: number;
|
|
46
|
+
id: string;
|
|
47
|
+
timestamp: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
parentSession?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Each line is treated as a separate JSON record.
|
|
53
|
+
* Check if the first line of a JSONL file is a pi session header.
|
|
42
54
|
*/
|
|
43
|
-
|
|
55
|
+
function parsePiSessionHeader(line: string): PiSessionHeader | null {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
58
|
+
if (
|
|
59
|
+
parsed["type"] === "session" &&
|
|
60
|
+
typeof parsed["version"] === "number" &&
|
|
61
|
+
typeof parsed["id"] === "string" &&
|
|
62
|
+
typeof parsed["timestamp"] === "string" &&
|
|
63
|
+
typeof parsed["cwd"] === "string"
|
|
64
|
+
) {
|
|
65
|
+
return parsed as unknown as PiSessionHeader;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Not valid JSON
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Accumulated entry type counts for a pi session. */
|
|
74
|
+
interface SessionCounts {
|
|
75
|
+
user: number;
|
|
76
|
+
assistant: number;
|
|
77
|
+
toolResult: number;
|
|
78
|
+
compaction: number;
|
|
79
|
+
branchSummary: number;
|
|
80
|
+
modelChange: number;
|
|
81
|
+
sessionInfo: number;
|
|
82
|
+
other: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A structural symbol collected while streaming session entries. */
|
|
86
|
+
interface SessionSymbolRecord {
|
|
87
|
+
symbol: FileSymbol;
|
|
88
|
+
/** Line where this symbol's "span" starts (used to compute endLine). */
|
|
89
|
+
spanStart: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract a short text preview from a user message content field.
|
|
94
|
+
* Handles both string and content-block-array formats.
|
|
95
|
+
*/
|
|
96
|
+
function extractUserPreview(content: unknown, maxLength = 80): string {
|
|
97
|
+
let text = "";
|
|
98
|
+
if (typeof content === "string") {
|
|
99
|
+
text = content;
|
|
100
|
+
} else if (Array.isArray(content)) {
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (
|
|
103
|
+
typeof block === "object" &&
|
|
104
|
+
block !== null &&
|
|
105
|
+
(block as Record<string, unknown>)["type"] === "text" &&
|
|
106
|
+
typeof (block as Record<string, unknown>)["text"] === "string"
|
|
107
|
+
) {
|
|
108
|
+
text = (block as Record<string, unknown>)["text"] as string;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const cleaned = text.replaceAll(/\s+/g, " ").trim();
|
|
114
|
+
if (cleaned.length <= maxLength) {
|
|
115
|
+
return cleaned;
|
|
116
|
+
}
|
|
117
|
+
return `${cleaned.slice(0, maxLength - 3)}...`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format the session timestamp for display.
|
|
124
|
+
* Returns "YYYY-MM-DD HH:MM UTC" or the raw string on parse failure.
|
|
125
|
+
*/
|
|
126
|
+
function formatSessionTimestamp(ts: string): string {
|
|
127
|
+
const d = new Date(ts);
|
|
128
|
+
if (!Number.isFinite(d.getTime())) {
|
|
129
|
+
return ts;
|
|
130
|
+
}
|
|
131
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build a stats summary string from entry type counts.
|
|
136
|
+
*/
|
|
137
|
+
function formatStatsSummary(counts: SessionCounts): string {
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
if (counts.user > 0) {
|
|
140
|
+
parts.push(`${counts.user} user`);
|
|
141
|
+
}
|
|
142
|
+
if (counts.assistant > 0) {
|
|
143
|
+
parts.push(`${counts.assistant} assistant`);
|
|
144
|
+
}
|
|
145
|
+
if (counts.toolResult > 0) {
|
|
146
|
+
parts.push(`${counts.toolResult} tool results`);
|
|
147
|
+
}
|
|
148
|
+
if (counts.compaction > 0) {
|
|
149
|
+
parts.push(`${counts.compaction} compaction`);
|
|
150
|
+
}
|
|
151
|
+
if (counts.branchSummary > 0) {
|
|
152
|
+
parts.push(`${counts.branchSummary} branch summary`);
|
|
153
|
+
}
|
|
154
|
+
if (counts.modelChange > 0) {
|
|
155
|
+
parts.push(`${counts.modelChange} model change`);
|
|
156
|
+
}
|
|
157
|
+
return `Stats: ${parts.join(", ")}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Count an entry by its type, incrementing the appropriate counter.
|
|
162
|
+
*/
|
|
163
|
+
function countEntryType(
|
|
164
|
+
counts: SessionCounts,
|
|
165
|
+
entry: Record<string, unknown>,
|
|
166
|
+
entryType: string | undefined
|
|
167
|
+
): void {
|
|
168
|
+
if (entryType === "message") {
|
|
169
|
+
const msg = entry["message"] as Record<string, unknown> | undefined;
|
|
170
|
+
const role = msg?.["role"] as string | undefined;
|
|
171
|
+
if (role === "user") {
|
|
172
|
+
counts.user++;
|
|
173
|
+
} else if (role === "assistant") {
|
|
174
|
+
counts.assistant++;
|
|
175
|
+
} else if (role === "toolResult") {
|
|
176
|
+
counts.toolResult++;
|
|
177
|
+
}
|
|
178
|
+
} else if (entryType === "compaction") {
|
|
179
|
+
counts.compaction++;
|
|
180
|
+
} else if (entryType === "branch_summary") {
|
|
181
|
+
counts.branchSummary++;
|
|
182
|
+
} else if (entryType === "model_change") {
|
|
183
|
+
counts.modelChange++;
|
|
184
|
+
} else if (entryType === "session_info") {
|
|
185
|
+
counts.sessionInfo++;
|
|
186
|
+
} else {
|
|
187
|
+
counts.other++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse a pi session JSONL file into a conversation-aware structural map.
|
|
193
|
+
*
|
|
194
|
+
* Streams line-by-line, producing symbols for:
|
|
195
|
+
* - Session header (Module)
|
|
196
|
+
* - Stats summary (Property)
|
|
197
|
+
* - User message turns (Function) — line range spans through responses
|
|
198
|
+
* - Compaction boundaries (Namespace)
|
|
199
|
+
* - Model changes (Namespace)
|
|
200
|
+
* - Session name (Property)
|
|
201
|
+
*/
|
|
202
|
+
async function parsePiSession(
|
|
44
203
|
filePath: string,
|
|
204
|
+
header: PiSessionHeader,
|
|
205
|
+
totalBytes: number,
|
|
45
206
|
signal?: AbortSignal
|
|
46
207
|
): Promise<FileMap | null> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
208
|
+
const counts: SessionCounts = {
|
|
209
|
+
user: 0,
|
|
210
|
+
assistant: 0,
|
|
211
|
+
toolResult: 0,
|
|
212
|
+
compaction: 0,
|
|
213
|
+
branchSummary: 0,
|
|
214
|
+
modelChange: 0,
|
|
215
|
+
sessionInfo: 0,
|
|
216
|
+
other: 0,
|
|
217
|
+
};
|
|
50
218
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
219
|
+
// Collect structural symbols while streaming.
|
|
220
|
+
// We track "open" user turns to compute their endLine once the next
|
|
221
|
+
// structural entry appears.
|
|
222
|
+
const records: SessionSymbolRecord[] = [];
|
|
223
|
+
let lineCount = 0;
|
|
224
|
+
let openUserTurn: SessionSymbolRecord | null = null;
|
|
54
225
|
|
|
55
|
-
|
|
56
|
-
|
|
226
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
227
|
+
const rl = createInterface({ input: stream });
|
|
57
228
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
229
|
+
signal?.addEventListener("abort", () => {
|
|
230
|
+
rl.close();
|
|
231
|
+
stream.destroy();
|
|
232
|
+
});
|
|
63
233
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
234
|
+
for await (const line of rl) {
|
|
235
|
+
lineCount++;
|
|
236
|
+
|
|
237
|
+
// Skip the header (line 1), already parsed
|
|
238
|
+
if (lineCount === 1) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let entry: Record<string, unknown>;
|
|
248
|
+
try {
|
|
249
|
+
entry = JSON.parse(trimmed) as Record<string, unknown>;
|
|
250
|
+
} catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const entryType = entry["type"] as string | undefined;
|
|
255
|
+
|
|
256
|
+
countEntryType(counts, entry, entryType);
|
|
257
|
+
|
|
258
|
+
// Collect structural symbols
|
|
259
|
+
if (entryType === "message") {
|
|
260
|
+
const msg = entry["message"] as Record<string, unknown> | undefined;
|
|
261
|
+
const role = msg?.["role"] as string | undefined;
|
|
262
|
+
|
|
263
|
+
if (role === "user") {
|
|
264
|
+
// Close previous user turn
|
|
265
|
+
if (openUserTurn) {
|
|
266
|
+
openUserTurn.symbol.endLine = lineCount - 1;
|
|
81
267
|
}
|
|
268
|
+
|
|
269
|
+
const preview = extractUserPreview(msg?.["content"]);
|
|
270
|
+
const record: SessionSymbolRecord = {
|
|
271
|
+
symbol: {
|
|
272
|
+
name: `[User] ${preview}`,
|
|
273
|
+
kind: SymbolKind.Function,
|
|
274
|
+
startLine: lineCount,
|
|
275
|
+
endLine: lineCount, // updated when next structural entry appears
|
|
276
|
+
},
|
|
277
|
+
spanStart: lineCount,
|
|
278
|
+
};
|
|
279
|
+
records.push(record);
|
|
280
|
+
openUserTurn = record;
|
|
281
|
+
}
|
|
282
|
+
// assistant and toolResult messages fold into the current user turn.
|
|
283
|
+
// Future: showing tool calls as nested child symbols under
|
|
284
|
+
// user turns would provide richer navigation (currently folded for simplicity).
|
|
285
|
+
} else if (entryType === "compaction") {
|
|
286
|
+
// Close previous user turn
|
|
287
|
+
if (openUserTurn) {
|
|
288
|
+
openUserTurn.symbol.endLine = lineCount - 1;
|
|
289
|
+
openUserTurn = null;
|
|
82
290
|
}
|
|
83
291
|
|
|
84
|
-
|
|
85
|
-
|
|
292
|
+
records.push({
|
|
293
|
+
symbol: {
|
|
294
|
+
name: "[Compaction]",
|
|
295
|
+
kind: SymbolKind.Namespace,
|
|
296
|
+
startLine: lineCount,
|
|
297
|
+
endLine: lineCount,
|
|
298
|
+
},
|
|
299
|
+
spanStart: lineCount,
|
|
300
|
+
});
|
|
301
|
+
} else if (entryType === "model_change") {
|
|
302
|
+
const provider = entry["provider"] as string | undefined;
|
|
303
|
+
const modelId = entry["modelId"] as string | undefined;
|
|
304
|
+
const label =
|
|
305
|
+
provider && modelId
|
|
306
|
+
? `${provider}/${modelId}`
|
|
307
|
+
: (provider ?? modelId ?? "unknown");
|
|
86
308
|
|
|
87
|
-
|
|
88
|
-
|
|
309
|
+
records.push({
|
|
310
|
+
symbol: {
|
|
311
|
+
name: `[Model] ${label}`,
|
|
312
|
+
kind: SymbolKind.Namespace,
|
|
313
|
+
startLine: lineCount,
|
|
314
|
+
endLine: lineCount,
|
|
315
|
+
},
|
|
316
|
+
spanStart: lineCount,
|
|
317
|
+
});
|
|
318
|
+
} else if (entryType === "session_info") {
|
|
319
|
+
const name = entry["name"] as string | undefined;
|
|
320
|
+
if (name) {
|
|
321
|
+
records.push({
|
|
322
|
+
symbol: {
|
|
323
|
+
name: `[Session] ${name}`,
|
|
324
|
+
kind: SymbolKind.Property,
|
|
325
|
+
startLine: lineCount,
|
|
326
|
+
endLine: lineCount,
|
|
327
|
+
},
|
|
328
|
+
spanStart: lineCount,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} else if (entryType === "branch_summary") {
|
|
332
|
+
// Close previous user turn
|
|
333
|
+
if (openUserTurn) {
|
|
334
|
+
openUserTurn.symbol.endLine = lineCount - 1;
|
|
335
|
+
openUserTurn = null;
|
|
336
|
+
}
|
|
89
337
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
338
|
+
records.push({
|
|
339
|
+
symbol: {
|
|
340
|
+
name: "[Branch Summary]",
|
|
341
|
+
kind: SymbolKind.Namespace,
|
|
342
|
+
startLine: lineCount,
|
|
343
|
+
endLine: lineCount,
|
|
344
|
+
},
|
|
345
|
+
spanStart: lineCount,
|
|
97
346
|
});
|
|
98
347
|
}
|
|
348
|
+
}
|
|
99
349
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
350
|
+
// Close final open user turn
|
|
351
|
+
if (openUserTurn) {
|
|
352
|
+
openUserTurn.symbol.endLine = lineCount;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Assemble symbols in line order
|
|
356
|
+
const symbols: FileSymbol[] = [
|
|
357
|
+
// Header symbol
|
|
358
|
+
{
|
|
359
|
+
name: `Pi Session: ${header.cwd} (${formatSessionTimestamp(header.timestamp)})`,
|
|
360
|
+
kind: SymbolKind.Module,
|
|
361
|
+
startLine: 1,
|
|
362
|
+
endLine: 1,
|
|
363
|
+
},
|
|
364
|
+
// Stats summary
|
|
365
|
+
{
|
|
366
|
+
name: formatStatsSummary(counts),
|
|
367
|
+
kind: SymbolKind.Property,
|
|
368
|
+
startLine: 1,
|
|
369
|
+
endLine: lineCount,
|
|
370
|
+
},
|
|
371
|
+
// Conversation symbols (already in line order from streaming)
|
|
372
|
+
...records.map((r) => r.symbol),
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
path: filePath,
|
|
377
|
+
totalLines: lineCount,
|
|
378
|
+
totalBytes,
|
|
379
|
+
language: "Pi Session",
|
|
380
|
+
symbols,
|
|
381
|
+
imports: [],
|
|
382
|
+
detailLevel: DetailLevel.Full,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Generic JSONL parsing (original logic)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Generate a generic file map for a JSON Lines file.
|
|
392
|
+
*/
|
|
393
|
+
async function parseGenericJsonl(
|
|
394
|
+
filePath: string,
|
|
395
|
+
totalBytes: number,
|
|
396
|
+
signal?: AbortSignal
|
|
397
|
+
): Promise<FileMap | null> {
|
|
398
|
+
const samples: JsonlSample[] = [];
|
|
399
|
+
let lineCount = 0;
|
|
400
|
+
let schema: { keys: string[]; type: string } | null = null;
|
|
401
|
+
|
|
402
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
403
|
+
const rl = createInterface({ input: stream });
|
|
404
|
+
|
|
405
|
+
signal?.addEventListener("abort", () => {
|
|
406
|
+
rl.close();
|
|
407
|
+
stream.destroy();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
for await (const line of rl) {
|
|
411
|
+
lineCount++;
|
|
412
|
+
|
|
413
|
+
// Collect first few samples (only from first 100 lines)
|
|
414
|
+
if (lineCount <= 100 && samples.length < 10 && line.trim()) {
|
|
415
|
+
const lineSchema = analyzeJsonLine(line);
|
|
416
|
+
if (lineSchema) {
|
|
417
|
+
samples.push({
|
|
418
|
+
lineNumber: lineCount,
|
|
419
|
+
preview: line.slice(0, 80) + (line.length > 80 ? "..." : ""),
|
|
420
|
+
keys: lineSchema.keys,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (!schema) {
|
|
424
|
+
schema = lineSchema;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
108
427
|
}
|
|
428
|
+
}
|
|
109
429
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
430
|
+
const symbols: FileSymbol[] = [];
|
|
431
|
+
|
|
432
|
+
if (schema) {
|
|
433
|
+
symbols.push({
|
|
434
|
+
name: `Schema: ${schema.type}${schema.keys.length > 0 ? ` {${schema.keys.slice(0, 5).join(", ")}${schema.keys.length > 5 ? "..." : ""}}` : ""}`,
|
|
435
|
+
kind: SymbolKind.Class,
|
|
436
|
+
startLine: 1,
|
|
437
|
+
endLine: 1,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const sample of samples.slice(0, 5)) {
|
|
442
|
+
symbols.push({
|
|
443
|
+
name: `Line ${sample.lineNumber}: ${sample.preview}`,
|
|
444
|
+
kind: SymbolKind.Variable,
|
|
445
|
+
startLine: sample.lineNumber,
|
|
446
|
+
endLine: sample.lineNumber,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (lineCount > samples.length) {
|
|
451
|
+
symbols.push({
|
|
452
|
+
name: `... ${lineCount - samples.length} more lines`,
|
|
453
|
+
kind: SymbolKind.Variable,
|
|
454
|
+
startLine: samples.length + 1,
|
|
455
|
+
endLine: lineCount,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
path: filePath,
|
|
461
|
+
totalLines: lineCount,
|
|
462
|
+
totalBytes,
|
|
463
|
+
language: "JSON Lines",
|
|
464
|
+
symbols,
|
|
465
|
+
imports: [],
|
|
466
|
+
detailLevel: DetailLevel.Full,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Entry point
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generate a file map for a JSON Lines file.
|
|
476
|
+
*
|
|
477
|
+
* If the first line is a pi session header (`{"type":"session",...}`),
|
|
478
|
+
* produces a conversation-aware structural map. Otherwise falls back to
|
|
479
|
+
* generic schema + sample display.
|
|
480
|
+
*/
|
|
481
|
+
export async function jsonlMapper(
|
|
482
|
+
filePath: string,
|
|
483
|
+
signal?: AbortSignal
|
|
484
|
+
): Promise<FileMap | null> {
|
|
485
|
+
try {
|
|
486
|
+
const stats = await stat(filePath);
|
|
487
|
+
const totalBytes = stats.size;
|
|
488
|
+
|
|
489
|
+
// Read just the first line to detect pi sessions
|
|
490
|
+
const firstLine = await readFirstLine(filePath, signal);
|
|
491
|
+
if (firstLine !== null) {
|
|
492
|
+
const sessionHeader = parsePiSessionHeader(firstLine);
|
|
493
|
+
if (sessionHeader) {
|
|
494
|
+
return parsePiSession(filePath, sessionHeader, totalBytes, signal);
|
|
495
|
+
}
|
|
118
496
|
}
|
|
119
497
|
|
|
120
|
-
return
|
|
121
|
-
path: filePath,
|
|
122
|
-
totalLines: lineCount,
|
|
123
|
-
totalBytes,
|
|
124
|
-
language: "JSON Lines",
|
|
125
|
-
symbols,
|
|
126
|
-
detailLevel: DetailLevel.Full,
|
|
127
|
-
};
|
|
498
|
+
return parseGenericJsonl(filePath, totalBytes, signal);
|
|
128
499
|
} catch (error) {
|
|
129
500
|
if (signal?.aborted) {
|
|
130
501
|
return null;
|
|
@@ -133,3 +504,32 @@ export async function jsonlMapper(
|
|
|
133
504
|
return null;
|
|
134
505
|
}
|
|
135
506
|
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Read the first non-empty line from a file.
|
|
510
|
+
*/
|
|
511
|
+
async function readFirstLine(
|
|
512
|
+
filePath: string,
|
|
513
|
+
signal?: AbortSignal
|
|
514
|
+
): Promise<string | null> {
|
|
515
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
516
|
+
const rl = createInterface({ input: stream });
|
|
517
|
+
|
|
518
|
+
signal?.addEventListener("abort", () => {
|
|
519
|
+
rl.close();
|
|
520
|
+
stream.destroy();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
for await (const line of rl) {
|
|
525
|
+
const trimmed = line.trim();
|
|
526
|
+
if (trimmed) {
|
|
527
|
+
return trimmed;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
} finally {
|
|
532
|
+
rl.close();
|
|
533
|
+
stream.destroy();
|
|
534
|
+
}
|
|
535
|
+
}
|
package/src/mappers/markdown.ts
CHANGED
package/src/mappers/python.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface PythonSymbol {
|
|
|
21
21
|
signature?: string;
|
|
22
22
|
modifiers?: string[];
|
|
23
23
|
children?: PythonSymbol[];
|
|
24
|
+
docstring?: string;
|
|
25
|
+
is_exported?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface PythonOutlineResult {
|
|
@@ -72,6 +74,14 @@ function convertSymbol(ps: PythonSymbol): FileSymbol {
|
|
|
72
74
|
symbol.children = ps.children.map(convertSymbol);
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
if (ps.docstring) {
|
|
78
|
+
symbol.docstring = ps.docstring;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (ps.is_exported !== undefined) {
|
|
82
|
+
symbol.isExported = ps.is_exported;
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
return symbol;
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -99,6 +109,7 @@ export async function pythonMapper(
|
|
|
99
109
|
{
|
|
100
110
|
signal,
|
|
101
111
|
timeout: 10_000,
|
|
112
|
+
maxBuffer: 5 * 1024 * 1024, // 5MB
|
|
102
113
|
}
|
|
103
114
|
);
|
|
104
115
|
|
|
@@ -120,13 +131,10 @@ export async function pythonMapper(
|
|
|
120
131
|
totalBytes,
|
|
121
132
|
language: "Python",
|
|
122
133
|
symbols: result.symbols.map(convertSymbol),
|
|
134
|
+
imports: result.imports ?? [],
|
|
123
135
|
detailLevel: DetailLevel.Full,
|
|
124
136
|
};
|
|
125
137
|
|
|
126
|
-
if (result.imports && result.imports.length > 0) {
|
|
127
|
-
fileMap.imports = result.imports;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
138
|
return fileMap;
|
|
131
139
|
} catch (error) {
|
|
132
140
|
if (signal?.aborted) {
|