combicode 1.7.4 → 2.0.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.
Files changed (5) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +340 -19
  3. package/index.js +1473 -157
  4. package/package.json +22 -1
  5. package/test/test.js +473 -119
package/index.js CHANGED
@@ -8,25 +8,34 @@ const ignore = require("ignore");
8
8
 
9
9
  const { version } = require("./package.json");
10
10
 
11
+ // ---------------------------------------------------------------------------
12
+ // System Prompts (v2.0.0)
13
+ // ---------------------------------------------------------------------------
14
+
11
15
  const DEFAULT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the complete source code for a project, contained in a single file. Your task is to meticulously analyze the provided codebase to gain a comprehensive understanding of its structure, functionality, dependencies, and overall architecture.
12
16
 
13
- A file tree is provided below to give you a high-level overview. The subsequent sections contain the full content of each file, clearly marked with a file header.
17
+ A code map with expanded tree structure \`<code_index>\` is provided below to give you a high-level overview. The subsequent section \`<merged_code>\` contain the full content of each file (read using the command \`sed -n '<ML_START>,<ML_END>p' combicode.txt\`), clearly marked with a file header.
14
18
 
15
19
  Your instructions are:
16
- 1. **Analyze Thoroughly:** Read through every file to understand its purpose and how it interacts with other files.
17
- 2. **Identify Key Components:** Pay close attention to configuration files (like package.json, pyproject.toml), entry points (like index.js, main.py), and core logic.
20
+ 1. Analyze Thoroughly: Read through every file to understand its purpose and how it interacts with other files.
21
+ 2. Identify Key Components: Pay close attention to configuration files (like package.json, pyproject.toml), entry points (like index.js, main.py), and core logic.
22
+ 3. Use the Code Map: The code map shows classes, functions, loops with their line numbers (OL = Original Line, ML = Merged Line) and sizes for precise navigation.
18
23
  `;
19
24
 
20
- const LLMS_TXT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the full documentation for a project, sourced from the project's 'llms.txt' file. This file contains the complete context needed to understand the project's features, APIs, and usage for a specific version. Your task is to act as a definitive source of truth based *only* on this provided documentation.
25
+ const LLMS_TXT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the full documentation for a project. This file contains the complete context needed to understand the project's features, APIs, and usage for a specific version. Your task is to act as a definitive source of truth based *only* on this provided documentation.
21
26
 
22
27
  When answering questions or writing code, adhere strictly to the functions, variables, and methods described in this context. Do not use or suggest any deprecated or older functionalities that are not present here.
23
28
 
24
- A file tree of the documentation source is provided below for a high-level overview. The subsequent sections contain the full content of each file, clearly marked with a file header.
29
+ A code map with expanded tree structure is provided below for a high-level overview.
25
30
  `;
26
31
 
27
- // Minimal safety ignores that should always apply
32
+ // Minimal safety ignores
28
33
  const SAFETY_IGNORES = [".git", ".DS_Store"];
29
34
 
35
+ // ---------------------------------------------------------------------------
36
+ // Utility helpers
37
+ // ---------------------------------------------------------------------------
38
+
30
39
  function isLikelyBinary(filePath) {
31
40
  const buffer = Buffer.alloc(512);
32
41
  let fd;
@@ -42,17 +51,1093 @@ function isLikelyBinary(filePath) {
42
51
  }
43
52
 
44
53
  function formatBytes(bytes, decimals = 1) {
45
- if (bytes === 0) return "0 B";
54
+ if (bytes === 0) return "0B";
46
55
  const k = 1024;
47
56
  const dm = decimals < 0 ? 0 : decimals;
48
57
  const sizes = ["B", "KB", "MB", "GB", "TB"];
49
58
  const i = Math.floor(Math.log(bytes) / Math.log(k));
50
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + "" + sizes[i];
59
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Code Parsers (regex-based for all languages)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Parse code structure from file content. Returns array of elements:
68
+ * { type, label, startLine, endLine, startByte, endByte }
69
+ *
70
+ * type: "class" | "fn" | "async" | "ctor" | "loop" | "impl" | "test" | "describe"
71
+ */
72
+ function parseCodeStructure(filePath, content) {
73
+ const ext = path.extname(filePath).toLowerCase();
74
+ const lines = content.split("\n");
75
+
76
+ switch (ext) {
77
+ case ".py":
78
+ return parsePython(lines);
79
+ case ".js":
80
+ case ".jsx":
81
+ case ".mjs":
82
+ case ".cjs":
83
+ return parseJavaScript(lines);
84
+ case ".ts":
85
+ case ".tsx":
86
+ case ".mts":
87
+ case ".cts":
88
+ return parseTypeScript(lines);
89
+ case ".go":
90
+ return parseGo(lines);
91
+ case ".rs":
92
+ return parseRust(lines);
93
+ case ".java":
94
+ return parseJava(lines);
95
+ case ".c":
96
+ case ".h":
97
+ case ".cpp":
98
+ case ".hpp":
99
+ case ".cc":
100
+ case ".cxx":
101
+ return parseCCpp(lines);
102
+ case ".cs":
103
+ return parseCSharp(lines);
104
+ case ".php":
105
+ return parsePHP(lines);
106
+ case ".rb":
107
+ return parseRuby(lines);
108
+ case ".swift":
109
+ return parseSwift(lines);
110
+ case ".kt":
111
+ case ".kts":
112
+ return parseKotlin(lines);
113
+ case ".scala":
114
+ case ".sc":
115
+ return parseScala(lines);
116
+ case ".lua":
117
+ return parseLua(lines);
118
+ case ".pl":
119
+ case ".pm":
120
+ return parsePerl(lines);
121
+ case ".sh":
122
+ case ".bash":
123
+ case ".zsh":
124
+ return parseBash(lines);
125
+ default:
126
+ return [];
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Find the end of a block that starts at `startLine` using brace/indent counting.
132
+ * For brace-based languages.
133
+ */
134
+ function findBraceBlockEnd(lines, startLine) {
135
+ let depth = 0;
136
+ let foundOpen = false;
137
+ for (let i = startLine; i < lines.length; i++) {
138
+ const line = lines[i];
139
+ for (const ch of line) {
140
+ if (ch === "{") {
141
+ depth++;
142
+ foundOpen = true;
143
+ } else if (ch === "}") {
144
+ depth--;
145
+ if (foundOpen && depth === 0) {
146
+ return i;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return lines.length - 1;
152
+ }
153
+
154
+ /**
155
+ * Find block end for Python (indent-based).
156
+ */
157
+ function findIndentBlockEnd(lines, startLine) {
158
+ if (startLine >= lines.length) return startLine;
159
+ const defLine = lines[startLine];
160
+ const baseIndent = defLine.match(/^(\s*)/)[1].length;
161
+
162
+ for (let i = startLine + 1; i < lines.length; i++) {
163
+ const line = lines[i];
164
+ if (line.trim() === "") continue; // skip blank lines
165
+ const indent = line.match(/^(\s*)/)[1].length;
166
+ if (indent <= baseIndent) {
167
+ return i - 1;
168
+ }
169
+ }
170
+ return lines.length - 1;
171
+ }
172
+
173
+ /**
174
+ * Find block end for Ruby-like (def/end, class/end, module/end).
175
+ */
176
+ function findRubyBlockEnd(lines, startLine) {
177
+ let depth = 0;
178
+ for (let i = startLine; i < lines.length; i++) {
179
+ const trimmed = lines[i].trim();
180
+ // Keywords that open blocks
181
+ if (
182
+ /^(class|module|def|do|if|unless|case|while|until|for|begin)\b/.test(
183
+ trimmed,
184
+ ) ||
185
+ /\bdo\s*(\|[^|]*\|)?\s*$/.test(trimmed)
186
+ ) {
187
+ depth++;
188
+ }
189
+ if (/^end\b/.test(trimmed)) {
190
+ depth--;
191
+ if (depth === 0) return i;
192
+ }
193
+ }
194
+ return lines.length - 1;
195
+ }
196
+
197
+ /**
198
+ * Find block end for Lua (function/end).
199
+ */
200
+ function findLuaBlockEnd(lines, startLine) {
201
+ let depth = 0;
202
+ for (let i = startLine; i < lines.length; i++) {
203
+ const trimmed = lines[i].trim();
204
+ if (/\b(function|if|for|while|repeat)\b/.test(trimmed)) depth++;
205
+ if (
206
+ /^end\b/.test(trimmed) ||
207
+ /\bend\s*[,)\]]/.test(trimmed) ||
208
+ trimmed === "end"
209
+ ) {
210
+ depth--;
211
+ if (depth === 0) return i;
212
+ }
213
+ }
214
+ return lines.length - 1;
215
+ }
216
+
217
+ function computeByteSize(lines, startLine, endLine) {
218
+ let size = 0;
219
+ for (let i = startLine; i <= endLine && i < lines.length; i++) {
220
+ size += Buffer.byteLength(lines[i], "utf8") + 1; // +1 for newline
221
+ }
222
+ return size;
223
+ }
224
+
225
+ function buildElement(type, label, startLine, endLine, lines) {
226
+ return {
227
+ type,
228
+ label,
229
+ startLine: startLine + 1, // 1-indexed
230
+ endLine: endLine + 1,
231
+ size: computeByteSize(lines, startLine, endLine),
232
+ };
233
+ }
234
+
235
+ // --- Language Parsers ---
236
+
237
+ function parsePython(lines) {
238
+ const elements = [];
239
+ for (let i = 0; i < lines.length; i++) {
240
+ const line = lines[i];
241
+ const trimmed = line.trim();
242
+
243
+ // class
244
+ let m = trimmed.match(/^class\s+(\w+)(\(.*?\))?\s*:/);
245
+ if (m) {
246
+ const end = findIndentBlockEnd(lines, i);
247
+ elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
248
+ continue;
249
+ }
250
+
251
+ // async def
252
+ m = trimmed.match(/^async\s+def\s+(\w+)\s*\((.*?)\)(\s*->.*?)?\s*:/);
253
+ if (m) {
254
+ const end = findIndentBlockEnd(lines, i);
255
+ const sig = `${m[1]}(${m[2]})${m[3] || ""}`;
256
+ const type = m[1] === "__init__" ? "ctor" : "async";
257
+ elements.push(
258
+ buildElement(
259
+ type,
260
+ `${type === "ctor" ? "ctor" : "async"} ${sig}`,
261
+ i,
262
+ end,
263
+ lines,
264
+ ),
265
+ );
266
+ continue;
267
+ }
268
+
269
+ // def
270
+ m = trimmed.match(/^def\s+(\w+)\s*\((.*?)\)(\s*->.*?)?\s*:/);
271
+ if (m) {
272
+ const end = findIndentBlockEnd(lines, i);
273
+ const sig = `${m[1]}(${m[2]})${m[3] || ""}`;
274
+ let type = "fn";
275
+ if (m[1] === "__init__") type = "ctor";
276
+ else if (m[1].startsWith("test_")) type = "test";
277
+ const label =
278
+ type === "ctor"
279
+ ? `ctor ${sig}`
280
+ : type === "test"
281
+ ? `test ${sig}`
282
+ : `fn ${sig}`;
283
+ elements.push(buildElement(type, label, i, end, lines));
284
+ continue;
285
+ }
286
+
287
+ // for/while loops (> 5 lines)
288
+ m = trimmed.match(/^(for|while)\s+(.+):\s*$/);
289
+ if (m) {
290
+ const end = findIndentBlockEnd(lines, i);
291
+ if (end - i + 1 > 5) {
292
+ elements.push(
293
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
294
+ );
295
+ }
296
+ continue;
297
+ }
298
+ }
299
+ return elements;
300
+ }
301
+
302
+ function parseJavaScript(lines) {
303
+ const elements = [];
304
+ for (let i = 0; i < lines.length; i++) {
305
+ const line = lines[i];
306
+ const trimmed = line.trim();
307
+
308
+ // class
309
+ let m = trimmed.match(/^(export\s+)?(default\s+)?class\s+(\w+)/);
310
+ if (m) {
311
+ const end = findBraceBlockEnd(lines, i);
312
+ elements.push(buildElement("class", `class ${m[3]}`, i, end, lines));
313
+ continue;
314
+ }
315
+
316
+ // describe (test suite)
317
+ m = trimmed.match(/^describe\s*\(\s*['"`]([^'"`]+)['"`]/);
318
+ if (m) {
319
+ const end = findBraceBlockEnd(lines, i);
320
+ elements.push(
321
+ buildElement("describe", `describe ${m[1]}`, i, end, lines),
322
+ );
323
+ continue;
324
+ }
325
+
326
+ // test/it blocks
327
+ m = trimmed.match(/^(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
328
+ if (m) {
329
+ const end = findBraceBlockEnd(lines, i);
330
+ elements.push(buildElement("test", `test ${m[2]}`, i, end, lines));
331
+ continue;
332
+ }
333
+
334
+ // async function
335
+ m = trimmed.match(
336
+ /^(export\s+)?(default\s+)?async\s+function\s+(\w+)\s*\((.*?)\)/,
337
+ );
338
+ if (m) {
339
+ const end = findBraceBlockEnd(lines, i);
340
+ elements.push(
341
+ buildElement("async", `async ${m[3]}(${m[4]})`, i, end, lines),
342
+ );
343
+ continue;
344
+ }
345
+
346
+ // function
347
+ m = trimmed.match(/^(export\s+)?(default\s+)?function\s+(\w+)\s*\((.*?)\)/);
348
+ if (m) {
349
+ const end = findBraceBlockEnd(lines, i);
350
+ const type = m[3] === "constructor" ? "ctor" : "fn";
351
+ elements.push(
352
+ buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
353
+ );
354
+ continue;
355
+ }
356
+
357
+ // arrow functions assigned to const/let/var (with explicit function body)
358
+ m = trimmed.match(
359
+ /^(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(?(.*?)\)?\s*=>/,
360
+ );
361
+ if (m && (trimmed.includes("{") || i + 1 < lines.length)) {
362
+ // Only include if it has a block body
363
+ if (
364
+ trimmed.includes("{") ||
365
+ (i + 1 < lines.length && lines[i + 1].trim().startsWith("{"))
366
+ ) {
367
+ const end = findBraceBlockEnd(lines, i);
368
+ if (end > i) {
369
+ const isAsync = !!m[4];
370
+ const type = isAsync ? "async" : "fn";
371
+ elements.push(
372
+ buildElement(type, `${type} ${m[3]}(${m[5] || ""})`, i, end, lines),
373
+ );
374
+ }
375
+ }
376
+ continue;
377
+ }
378
+
379
+ // for/while loops (> 5 lines)
380
+ m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
381
+ if (m) {
382
+ const end = findBraceBlockEnd(lines, i);
383
+ if (end - i + 1 > 5) {
384
+ elements.push(
385
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
386
+ );
387
+ }
388
+ continue;
389
+ }
390
+ }
391
+ return elements;
392
+ }
393
+
394
+ function parseTypeScript(lines) {
395
+ const elements = [];
396
+ for (let i = 0; i < lines.length; i++) {
397
+ const line = lines[i];
398
+ const trimmed = line.trim();
399
+
400
+ // interface
401
+ let m = trimmed.match(/^(export\s+)?(default\s+)?interface\s+(\w+)/);
402
+ if (m) {
403
+ const end = findBraceBlockEnd(lines, i);
404
+ elements.push(buildElement("class", `interface ${m[3]}`, i, end, lines));
405
+ continue;
406
+ }
407
+
408
+ // class
409
+ m = trimmed.match(/^(export\s+)?(default\s+)?(abstract\s+)?class\s+(\w+)/);
410
+ if (m) {
411
+ const end = findBraceBlockEnd(lines, i);
412
+ elements.push(buildElement("class", `class ${m[4]}`, i, end, lines));
413
+ continue;
414
+ }
415
+
416
+ // describe
417
+ m = trimmed.match(/^describe\s*\(\s*['"`]([^'"`]+)['"`]/);
418
+ if (m) {
419
+ const end = findBraceBlockEnd(lines, i);
420
+ elements.push(
421
+ buildElement("describe", `describe ${m[1]}`, i, end, lines),
422
+ );
423
+ continue;
424
+ }
425
+
426
+ // test/it
427
+ m = trimmed.match(/^(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
428
+ if (m) {
429
+ const end = findBraceBlockEnd(lines, i);
430
+ elements.push(buildElement("test", `test ${m[2]}`, i, end, lines));
431
+ continue;
432
+ }
433
+
434
+ // async function
435
+ m = trimmed.match(
436
+ /^(export\s+)?(default\s+)?async\s+function\s+(\w+)\s*\((.*?)\)/,
437
+ );
438
+ if (m) {
439
+ const end = findBraceBlockEnd(lines, i);
440
+ elements.push(
441
+ buildElement("async", `async ${m[3]}(${m[4]})`, i, end, lines),
442
+ );
443
+ continue;
444
+ }
445
+
446
+ // function
447
+ m = trimmed.match(/^(export\s+)?(default\s+)?function\s+(\w+)\s*\((.*?)\)/);
448
+ if (m) {
449
+ const end = findBraceBlockEnd(lines, i);
450
+ elements.push(buildElement("fn", `fn ${m[3]}(${m[4]})`, i, end, lines));
451
+ continue;
452
+ }
453
+
454
+ // arrow functions
455
+ m = trimmed.match(
456
+ /^(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(?(.*?)\)?\s*=>/,
457
+ );
458
+ if (m && trimmed.includes("{")) {
459
+ const end = findBraceBlockEnd(lines, i);
460
+ if (end > i) {
461
+ const isAsync = !!m[4];
462
+ const type = isAsync ? "async" : "fn";
463
+ elements.push(
464
+ buildElement(type, `${type} ${m[3]}(${m[5] || ""})`, i, end, lines),
465
+ );
466
+ }
467
+ continue;
468
+ }
469
+
470
+ // for/while loops (> 5 lines)
471
+ m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
472
+ if (m) {
473
+ const end = findBraceBlockEnd(lines, i);
474
+ if (end - i + 1 > 5) {
475
+ elements.push(
476
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
477
+ );
478
+ }
479
+ }
480
+ }
481
+ return elements;
482
+ }
483
+
484
+ function parseGo(lines) {
485
+ const elements = [];
486
+ for (let i = 0; i < lines.length; i++) {
487
+ const trimmed = lines[i].trim();
488
+
489
+ // struct
490
+ let m = trimmed.match(/^type\s+(\w+)\s+struct\b/);
491
+ if (m) {
492
+ const end = findBraceBlockEnd(lines, i);
493
+ elements.push(buildElement("class", `struct ${m[1]}`, i, end, lines));
494
+ continue;
495
+ }
496
+
497
+ // interface
498
+ m = trimmed.match(/^type\s+(\w+)\s+interface\b/);
499
+ if (m) {
500
+ const end = findBraceBlockEnd(lines, i);
501
+ elements.push(buildElement("class", `interface ${m[1]}`, i, end, lines));
502
+ continue;
503
+ }
504
+
505
+ // func
506
+ m = trimmed.match(/^func\s+(\(.*?\)\s*)?(\w+)\s*\((.*?)\)/);
507
+ if (m) {
508
+ const end = findBraceBlockEnd(lines, i);
509
+ const receiver = m[1] ? m[1].trim() + " " : "";
510
+ const name = m[2];
511
+ const type = name.startsWith("Test") ? "test" : "fn";
512
+ elements.push(
513
+ buildElement(
514
+ type,
515
+ `${type === "test" ? "test" : "fn"} ${receiver}${name}(${m[3]})`,
516
+ i,
517
+ end,
518
+ lines,
519
+ ),
520
+ );
521
+ continue;
522
+ }
523
+
524
+ // for loops (> 5 lines) - Go only has for
525
+ m = trimmed.match(/^for\s+(.+)\s*\{/);
526
+ if (m) {
527
+ const end = findBraceBlockEnd(lines, i);
528
+ if (end - i + 1 > 5) {
529
+ elements.push(buildElement("loop", `loop for ${m[1]}`, i, end, lines));
530
+ }
531
+ }
532
+ }
533
+ return elements;
534
+ }
535
+
536
+ function parseRust(lines) {
537
+ const elements = [];
538
+ for (let i = 0; i < lines.length; i++) {
539
+ const trimmed = lines[i].trim();
540
+
541
+ // struct
542
+ let m = trimmed.match(/^(pub\s+)?struct\s+(\w+)/);
543
+ if (m) {
544
+ const end = findBraceBlockEnd(lines, i);
545
+ elements.push(buildElement("class", `struct ${m[2]}`, i, end, lines));
546
+ continue;
547
+ }
548
+
549
+ // enum
550
+ m = trimmed.match(/^(pub\s+)?enum\s+(\w+)/);
551
+ if (m) {
552
+ const end = findBraceBlockEnd(lines, i);
553
+ elements.push(buildElement("class", `enum ${m[2]}`, i, end, lines));
554
+ continue;
555
+ }
556
+
557
+ // trait
558
+ m = trimmed.match(/^(pub\s+)?trait\s+(\w+)/);
559
+ if (m) {
560
+ const end = findBraceBlockEnd(lines, i);
561
+ elements.push(buildElement("class", `trait ${m[2]}`, i, end, lines));
562
+ continue;
563
+ }
564
+
565
+ // impl
566
+ m = trimmed.match(/^impl\s+(.+?)\s*\{/);
567
+ if (m) {
568
+ const end = findBraceBlockEnd(lines, i);
569
+ elements.push(buildElement("impl", `impl ${m[1]}`, i, end, lines));
570
+ continue;
571
+ }
572
+
573
+ // fn
574
+ m = trimmed.match(/^(pub\s+)?(async\s+)?fn\s+(\w+)\s*\((.*?)\)/);
575
+ if (m) {
576
+ const end = findBraceBlockEnd(lines, i);
577
+ const isAsync = !!m[2];
578
+ const isTest = m[3].startsWith("test_");
579
+ const type = isTest ? "test" : isAsync ? "async" : "fn";
580
+ elements.push(
581
+ buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
582
+ );
583
+ continue;
584
+ }
585
+
586
+ // loop/for/while (> 5 lines)
587
+ m = trimmed.match(/^(for|while|loop)\b(.*)?\{/);
588
+ if (m) {
589
+ const end = findBraceBlockEnd(lines, i);
590
+ if (end - i + 1 > 5) {
591
+ elements.push(
592
+ buildElement(
593
+ "loop",
594
+ `loop ${m[1]}${m[2] ? " " + m[2].trim() : ""}`,
595
+ i,
596
+ end,
597
+ lines,
598
+ ),
599
+ );
600
+ }
601
+ }
602
+ }
603
+ return elements;
604
+ }
605
+
606
+ function parseJava(lines) {
607
+ const elements = [];
608
+ for (let i = 0; i < lines.length; i++) {
609
+ const trimmed = lines[i].trim();
610
+
611
+ // class
612
+ let m = trimmed.match(
613
+ /^(public\s+|private\s+|protected\s+)?(static\s+)?(abstract\s+)?(final\s+)?class\s+(\w+)/,
614
+ );
615
+ if (m) {
616
+ const end = findBraceBlockEnd(lines, i);
617
+ elements.push(buildElement("class", `class ${m[5]}`, i, end, lines));
618
+ continue;
619
+ }
620
+
621
+ // interface
622
+ m = trimmed.match(/^(public\s+|private\s+|protected\s+)?interface\s+(\w+)/);
623
+ if (m) {
624
+ const end = findBraceBlockEnd(lines, i);
625
+ elements.push(buildElement("class", `interface ${m[2]}`, i, end, lines));
626
+ continue;
627
+ }
628
+
629
+ // enum
630
+ m = trimmed.match(/^(public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
631
+ if (m) {
632
+ const end = findBraceBlockEnd(lines, i);
633
+ elements.push(buildElement("class", `enum ${m[2]}`, i, end, lines));
634
+ continue;
635
+ }
636
+
637
+ // method (including constructors)
638
+ m = trimmed.match(
639
+ /^(public\s+|private\s+|protected\s+)?(static\s+)?(abstract\s+)?(final\s+)?(synchronized\s+)?(\w+\s+)?(\w+)\s*\((.*?)\)\s*(\{|throws)/,
640
+ );
641
+ if (
642
+ m &&
643
+ !["if", "for", "while", "switch", "catch", "return"].includes(m[7])
644
+ ) {
645
+ const end = findBraceBlockEnd(lines, i);
646
+ const name = m[7];
647
+ // Constructor: return type is absent and name matches class-like pattern
648
+ const hasReturnType = m[6] && m[6].trim();
649
+ const type = !hasReturnType
650
+ ? "ctor"
651
+ : name.startsWith("test")
652
+ ? "test"
653
+ : "fn";
654
+ elements.push(
655
+ buildElement(type, `${type} ${name}(${m[8]})`, i, end, lines),
656
+ );
657
+ continue;
658
+ }
659
+
660
+ // for/while loops (> 5 lines)
661
+ m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
662
+ if (m) {
663
+ const end = findBraceBlockEnd(lines, i);
664
+ if (end - i + 1 > 5) {
665
+ elements.push(
666
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
667
+ );
668
+ }
669
+ }
670
+ }
671
+ return elements;
672
+ }
673
+
674
+ function parseCCpp(lines) {
675
+ const elements = [];
676
+ for (let i = 0; i < lines.length; i++) {
677
+ const trimmed = lines[i].trim();
678
+
679
+ // class
680
+ let m = trimmed.match(/^class\s+(\w+)/);
681
+ if (m) {
682
+ const end = findBraceBlockEnd(lines, i);
683
+ elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
684
+ continue;
685
+ }
686
+
687
+ // struct
688
+ m = trimmed.match(/^(typedef\s+)?struct\s+(\w+)/);
689
+ if (m) {
690
+ const end = findBraceBlockEnd(lines, i);
691
+ elements.push(buildElement("class", `struct ${m[2]}`, i, end, lines));
692
+ continue;
693
+ }
694
+
695
+ // function (C-style: return_type name(...))
696
+ m = trimmed.match(/^(\w[\w\s*&]+?)\s+(\w+)\s*\(([^)]*)\)\s*(\{|$)/);
697
+ if (
698
+ m &&
699
+ ![
700
+ "if",
701
+ "for",
702
+ "while",
703
+ "switch",
704
+ "return",
705
+ "typedef",
706
+ "struct",
707
+ "class",
708
+ "enum",
709
+ ].includes(m[2])
710
+ ) {
711
+ const end = findBraceBlockEnd(lines, i);
712
+ elements.push(buildElement("fn", `fn ${m[2]}(${m[3]})`, i, end, lines));
713
+ continue;
714
+ }
715
+
716
+ // for/while loops (> 5 lines)
717
+ m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
718
+ if (m) {
719
+ const end = findBraceBlockEnd(lines, i);
720
+ if (end - i + 1 > 5) {
721
+ elements.push(
722
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
723
+ );
724
+ }
725
+ }
726
+ }
727
+ return elements;
728
+ }
729
+
730
+ function parseCSharp(lines) {
731
+ const elements = [];
732
+ for (let i = 0; i < lines.length; i++) {
733
+ const trimmed = lines[i].trim();
734
+
735
+ // class / struct / interface / enum / record
736
+ let m = trimmed.match(
737
+ /^(public\s+|private\s+|protected\s+|internal\s+)?(static\s+)?(abstract\s+|sealed\s+)?(partial\s+)?(class|struct|interface|enum|record)\s+(\w+)/,
738
+ );
739
+ if (m) {
740
+ const end = findBraceBlockEnd(lines, i);
741
+ elements.push(buildElement("class", `${m[5]} ${m[6]}`, i, end, lines));
742
+ continue;
743
+ }
744
+
745
+ // method
746
+ m = trimmed.match(
747
+ /^(public\s+|private\s+|protected\s+|internal\s+)?(static\s+)?(async\s+)?(virtual\s+|override\s+|abstract\s+)?(\w[\w<>\[\],\s]*?)\s+(\w+)\s*\((.*?)\)\s*\{?/,
748
+ );
749
+ if (
750
+ m &&
751
+ ![
752
+ "if",
753
+ "for",
754
+ "while",
755
+ "switch",
756
+ "catch",
757
+ "return",
758
+ "class",
759
+ "struct",
760
+ "interface",
761
+ "enum",
762
+ ].includes(m[6])
763
+ ) {
764
+ const end = findBraceBlockEnd(lines, i);
765
+ const isAsync = !!m[3];
766
+ const type = isAsync ? "async" : "fn";
767
+ elements.push(
768
+ buildElement(type, `${type} ${m[6]}(${m[7]})`, i, end, lines),
769
+ );
770
+ continue;
771
+ }
772
+
773
+ // for/while loops (> 5 lines)
774
+ m = trimmed.match(/^(for|foreach|while)\s*\((.+)\)\s*\{?/);
775
+ if (m) {
776
+ const end = findBraceBlockEnd(lines, i);
777
+ if (end - i + 1 > 5) {
778
+ elements.push(
779
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
780
+ );
781
+ }
782
+ }
783
+ }
784
+ return elements;
785
+ }
786
+
787
+ function parsePHP(lines) {
788
+ const elements = [];
789
+ for (let i = 0; i < lines.length; i++) {
790
+ const trimmed = lines[i].trim();
791
+
792
+ // class / interface / trait
793
+ let m = trimmed.match(
794
+ /^(abstract\s+)?(final\s+)?(class|interface|trait)\s+(\w+)/,
795
+ );
796
+ if (m) {
797
+ const end = findBraceBlockEnd(lines, i);
798
+ elements.push(buildElement("class", `${m[3]} ${m[4]}`, i, end, lines));
799
+ continue;
800
+ }
801
+
802
+ // function
803
+ m = trimmed.match(
804
+ /^(public\s+|private\s+|protected\s+)?(static\s+)?function\s+(\w+)\s*\((.*?)\)/,
805
+ );
806
+ if (m) {
807
+ const end = findBraceBlockEnd(lines, i);
808
+ const type =
809
+ m[3] === "__construct"
810
+ ? "ctor"
811
+ : m[3].startsWith("test")
812
+ ? "test"
813
+ : "fn";
814
+ elements.push(
815
+ buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
816
+ );
817
+ continue;
818
+ }
819
+
820
+ // for/while loops (> 5 lines)
821
+ m = trimmed.match(/^(for|foreach|while)\s*\((.+)\)\s*\{?/);
822
+ if (m) {
823
+ const end = findBraceBlockEnd(lines, i);
824
+ if (end - i + 1 > 5) {
825
+ elements.push(
826
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
827
+ );
828
+ }
829
+ }
830
+ }
831
+ return elements;
832
+ }
833
+
834
+ function parseRuby(lines) {
835
+ const elements = [];
836
+ for (let i = 0; i < lines.length; i++) {
837
+ const trimmed = lines[i].trim();
838
+
839
+ // class
840
+ let m = trimmed.match(/^class\s+(\w+)/);
841
+ if (m) {
842
+ const end = findRubyBlockEnd(lines, i);
843
+ elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
844
+ continue;
845
+ }
846
+
847
+ // module
848
+ m = trimmed.match(/^module\s+(\w+)/);
849
+ if (m) {
850
+ const end = findRubyBlockEnd(lines, i);
851
+ elements.push(buildElement("class", `module ${m[1]}`, i, end, lines));
852
+ continue;
853
+ }
854
+
855
+ // def
856
+ m = trimmed.match(/^def\s+(self\.)?(\w+[?!=]?)\s*(\(.*?\))?/);
857
+ if (m) {
858
+ const end = findRubyBlockEnd(lines, i);
859
+ const prefix = m[1] || "";
860
+ const type =
861
+ m[2] === "initialize"
862
+ ? "ctor"
863
+ : m[2].startsWith("test_")
864
+ ? "test"
865
+ : "fn";
866
+ elements.push(
867
+ buildElement(
868
+ type,
869
+ `${type} ${prefix}${m[2]}${m[3] || ""}`,
870
+ i,
871
+ end,
872
+ lines,
873
+ ),
874
+ );
875
+ continue;
876
+ }
877
+ }
878
+ return elements;
879
+ }
880
+
881
+ function parseSwift(lines) {
882
+ const elements = [];
883
+ for (let i = 0; i < lines.length; i++) {
884
+ const trimmed = lines[i].trim();
885
+
886
+ // class / struct / enum / protocol
887
+ let m = trimmed.match(
888
+ /^(public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(final\s+)?(class|struct|enum|protocol)\s+(\w+)/,
889
+ );
890
+ if (m) {
891
+ const end = findBraceBlockEnd(lines, i);
892
+ elements.push(buildElement("class", `${m[3]} ${m[4]}`, i, end, lines));
893
+ continue;
894
+ }
895
+
896
+ // func
897
+ m = trimmed.match(
898
+ /^(public\s+|private\s+|internal\s+|open\s+)?(static\s+|class\s+)?(override\s+)?func\s+(\w+)\s*\((.*?)\)/,
899
+ );
900
+ if (m) {
901
+ const end = findBraceBlockEnd(lines, i);
902
+ const name = m[4];
903
+ const type =
904
+ name === "init" ? "ctor" : name.startsWith("test") ? "test" : "fn";
905
+ elements.push(
906
+ buildElement(type, `${type} ${name}(${m[5]})`, i, end, lines),
907
+ );
908
+ continue;
909
+ }
910
+
911
+ // for/while loops (> 5 lines)
912
+ m = trimmed.match(/^(for|while)\s+(.+)\s*\{/);
913
+ if (m) {
914
+ const end = findBraceBlockEnd(lines, i);
915
+ if (end - i + 1 > 5) {
916
+ elements.push(
917
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
918
+ );
919
+ }
920
+ }
921
+ }
922
+ return elements;
923
+ }
924
+
925
+ function parseKotlin(lines) {
926
+ const elements = [];
927
+ for (let i = 0; i < lines.length; i++) {
928
+ const trimmed = lines[i].trim();
929
+
930
+ // class / interface / object
931
+ let m = trimmed.match(
932
+ /^(open\s+|abstract\s+|data\s+|sealed\s+)?(class|interface|object)\s+(\w+)/,
933
+ );
934
+ if (m) {
935
+ const end = findBraceBlockEnd(lines, i);
936
+ elements.push(buildElement("class", `${m[2]} ${m[3]}`, i, end, lines));
937
+ continue;
938
+ }
939
+
940
+ // fun
941
+ m = trimmed.match(
942
+ /^(public\s+|private\s+|protected\s+|internal\s+)?(override\s+)?(suspend\s+)?fun\s+(\w+)\s*\((.*?)\)/,
943
+ );
944
+ if (m) {
945
+ const end = findBraceBlockEnd(lines, i);
946
+ const isSuspend = !!m[3];
947
+ const type = m[4].startsWith("test")
948
+ ? "test"
949
+ : isSuspend
950
+ ? "async"
951
+ : "fn";
952
+ elements.push(
953
+ buildElement(type, `${type} ${m[4]}(${m[5]})`, i, end, lines),
954
+ );
955
+ continue;
956
+ }
957
+
958
+ // for/while loops (> 5 lines)
959
+ m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
960
+ if (m) {
961
+ const end = findBraceBlockEnd(lines, i);
962
+ if (end - i + 1 > 5) {
963
+ elements.push(
964
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
965
+ );
966
+ }
967
+ }
968
+ }
969
+ return elements;
970
+ }
971
+
972
+ function parseScala(lines) {
973
+ const elements = [];
974
+ for (let i = 0; i < lines.length; i++) {
975
+ const trimmed = lines[i].trim();
976
+
977
+ // class / object / trait
978
+ let m = trimmed.match(/^(case\s+)?(class|object|trait)\s+(\w+)/);
979
+ if (m) {
980
+ const end = findBraceBlockEnd(lines, i);
981
+ elements.push(
982
+ buildElement("class", `${m[1] || ""}${m[2]} ${m[3]}`, i, end, lines),
983
+ );
984
+ continue;
985
+ }
986
+
987
+ // def
988
+ m = trimmed.match(/^(override\s+)?def\s+(\w+)\s*(\(.*?\))?/);
989
+ if (m) {
990
+ const end = findBraceBlockEnd(lines, i);
991
+ const type = m[2].startsWith("test") ? "test" : "fn";
992
+ elements.push(
993
+ buildElement(type, `${type} ${m[2]}${m[3] || ""}`, i, end, lines),
994
+ );
995
+ continue;
996
+ }
997
+ }
998
+ return elements;
999
+ }
1000
+
1001
+ function parseLua(lines) {
1002
+ const elements = [];
1003
+ for (let i = 0; i < lines.length; i++) {
1004
+ const trimmed = lines[i].trim();
1005
+
1006
+ // function / local function
1007
+ let m = trimmed.match(/^(local\s+)?function\s+([\w.:]+)\s*\((.*?)\)/);
1008
+ if (m) {
1009
+ const end = findLuaBlockEnd(lines, i);
1010
+ elements.push(buildElement("fn", `fn ${m[2]}(${m[3]})`, i, end, lines));
1011
+ continue;
1012
+ }
1013
+
1014
+ // for/while loops (> 5 lines)
1015
+ m = trimmed.match(/^(for|while)\s+(.+)\s+do/);
1016
+ if (m) {
1017
+ const end = findLuaBlockEnd(lines, i);
1018
+ if (end - i + 1 > 5) {
1019
+ elements.push(
1020
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
1021
+ );
1022
+ }
1023
+ }
1024
+ }
1025
+ return elements;
1026
+ }
1027
+
1028
+ function parsePerl(lines) {
1029
+ const elements = [];
1030
+ for (let i = 0; i < lines.length; i++) {
1031
+ const trimmed = lines[i].trim();
1032
+
1033
+ // package
1034
+ let m = trimmed.match(/^package\s+([\w:]+)/);
1035
+ if (m) {
1036
+ elements.push(buildElement("class", `package ${m[1]}`, i, i, lines));
1037
+ continue;
1038
+ }
1039
+
1040
+ // sub
1041
+ m = trimmed.match(/^sub\s+(\w+)/);
1042
+ if (m) {
1043
+ const end = findBraceBlockEnd(lines, i);
1044
+ elements.push(buildElement("fn", `fn ${m[1]}`, i, end, lines));
1045
+ continue;
1046
+ }
1047
+ }
1048
+ return elements;
1049
+ }
1050
+
1051
+ function parseBash(lines) {
1052
+ const elements = [];
1053
+ for (let i = 0; i < lines.length; i++) {
1054
+ const trimmed = lines[i].trim();
1055
+
1056
+ // function keyword or name()
1057
+ let m = trimmed.match(/^(function\s+)?(\w+)\s*\(\s*\)\s*\{?/);
1058
+ if (m && m[1]) {
1059
+ // function keyword form
1060
+ const end = findBraceBlockEnd(lines, i);
1061
+ elements.push(buildElement("fn", `fn ${m[2]}`, i, end, lines));
1062
+ continue;
1063
+ }
1064
+ if (m && !m[1] && trimmed.includes("()")) {
1065
+ // name() form
1066
+ const end = findBraceBlockEnd(lines, i);
1067
+ elements.push(buildElement("fn", `fn ${m[2]}`, i, end, lines));
1068
+ continue;
1069
+ }
1070
+
1071
+ // for/while loops (> 5 lines)
1072
+ m = trimmed.match(/^(for|while)\s+(.+?);\s*do/);
1073
+ if (!m) m = trimmed.match(/^(for|while)\s+(.+)/);
1074
+ if (m) {
1075
+ // Look for done
1076
+ let end = i;
1077
+ for (let j = i + 1; j < lines.length; j++) {
1078
+ if (lines[j].trim() === "done") {
1079
+ end = j;
1080
+ break;
1081
+ }
1082
+ }
1083
+ if (end - i + 1 > 5) {
1084
+ elements.push(
1085
+ buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
1086
+ );
1087
+ }
1088
+ }
1089
+ }
1090
+ return elements;
51
1091
  }
52
1092
 
1093
+ // ---------------------------------------------------------------------------
1094
+ // Nesting + Tree Building
1095
+ // ---------------------------------------------------------------------------
1096
+
53
1097
  /**
54
- * Recursively walks directories, respecting .gitignore files at each level.
1098
+ * Nest flat elements into a tree based on line ranges.
1099
+ * Elements that fall within the range of a parent become children.
55
1100
  */
1101
+ function nestElements(elements) {
1102
+ if (!elements.length) return [];
1103
+
1104
+ // Sort by start line, then by larger range first (parents before children)
1105
+ const sorted = [...elements].sort((a, b) => {
1106
+ if (a.startLine !== b.startLine) return a.startLine - b.startLine;
1107
+ return b.endLine - b.startLine - (a.endLine - a.startLine);
1108
+ });
1109
+
1110
+ const root = [];
1111
+ const stack = []; // stack of { element, children }
1112
+
1113
+ for (const el of sorted) {
1114
+ const node = { ...el, children: [] };
1115
+
1116
+ // Pop from stack if current element is outside of parent's range
1117
+ while (stack.length > 0) {
1118
+ const parent = stack[stack.length - 1];
1119
+ if (el.startLine >= parent.startLine && el.endLine <= parent.endLine) {
1120
+ break;
1121
+ }
1122
+ stack.pop();
1123
+ }
1124
+
1125
+ if (stack.length > 0) {
1126
+ stack[stack.length - 1].children.push(node);
1127
+ } else {
1128
+ root.push(node);
1129
+ }
1130
+
1131
+ stack.push(node);
1132
+ }
1133
+
1134
+ return root;
1135
+ }
1136
+
1137
+ // ---------------------------------------------------------------------------
1138
+ // Directory Walker (unchanged from v1)
1139
+ // ---------------------------------------------------------------------------
1140
+
56
1141
  function walkDirectory(
57
1142
  currentDir,
58
1143
  rootDir,
@@ -60,12 +1145,11 @@ function walkDirectory(
60
1145
  allowedExts,
61
1146
  absoluteOutputPath,
62
1147
  useGitIgnore,
63
- stats // { scanned: 0, ignored: 0 }
1148
+ stats,
64
1149
  ) {
65
1150
  let results = [];
66
1151
  let currentIgnoreManager = null;
67
1152
 
68
- // 1. Check for local .gitignore and add to chain for this scope
69
1153
  if (useGitIgnore) {
70
1154
  const gitignorePath = path.join(currentDir, ".gitignore");
71
1155
  if (fs.existsSync(gitignorePath)) {
@@ -73,13 +1157,10 @@ function walkDirectory(
73
1157
  const content = fs.readFileSync(gitignorePath, "utf8");
74
1158
  const ig = ignore().add(content);
75
1159
  currentIgnoreManager = { manager: ig, root: currentDir };
76
- } catch (e) {
77
- // Warning could go here
78
- }
1160
+ } catch (e) {}
79
1161
  }
80
1162
  }
81
1163
 
82
- // Create a new chain for this directory and its children
83
1164
  const nextIgnoreChain = currentIgnoreManager
84
1165
  ? [...ignoreChain, currentIgnoreManager]
85
1166
  : ignoreChain;
@@ -94,25 +1175,17 @@ function walkDirectory(
94
1175
  for (const entry of entries) {
95
1176
  const fullPath = path.join(currentDir, entry.name);
96
1177
 
97
- // SKIP CHECK: Output file
98
1178
  if (path.resolve(fullPath) === absoluteOutputPath) continue;
99
1179
 
100
- // SKIP CHECK: Ignore Chain
101
1180
  let shouldIgnore = false;
102
1181
  for (const item of nextIgnoreChain) {
103
- // Calculate path relative to the specific ignore manager's root
104
- // IMPORTANT: Normalize to POSIX slashes for 'ignore' package compatibility
105
1182
  let relToIgnoreRoot = path.relative(item.root, fullPath);
106
-
107
1183
  if (path.sep === "\\") {
108
1184
  relToIgnoreRoot = relToIgnoreRoot.replace(/\\/g, "/");
109
1185
  }
110
-
111
- // If checking a directory, ensure trailing slash for proper 'ignore' directory matching
112
1186
  if (entry.isDirectory() && !relToIgnoreRoot.endsWith("/")) {
113
1187
  relToIgnoreRoot += "/";
114
1188
  }
115
-
116
1189
  if (item.manager.ignores(relToIgnoreRoot)) {
117
1190
  shouldIgnore = true;
118
1191
  break;
@@ -125,7 +1198,6 @@ function walkDirectory(
125
1198
  }
126
1199
 
127
1200
  if (entry.isDirectory()) {
128
- // Recurse
129
1201
  results = results.concat(
130
1202
  walkDirectory(
131
1203
  fullPath,
@@ -134,22 +1206,18 @@ function walkDirectory(
134
1206
  allowedExts,
135
1207
  absoluteOutputPath,
136
1208
  useGitIgnore,
137
- stats
138
- )
1209
+ stats,
1210
+ ),
139
1211
  );
140
1212
  } else if (entry.isFile()) {
141
- // SKIP CHECK: Binary
142
1213
  if (isLikelyBinary(fullPath)) {
143
1214
  stats.ignored++;
144
1215
  continue;
145
1216
  }
146
-
147
- // SKIP CHECK: Extensions
148
1217
  if (allowedExts && !allowedExts.has(path.extname(entry.name))) {
149
1218
  stats.ignored++;
150
1219
  continue;
151
1220
  }
152
-
153
1221
  try {
154
1222
  const fileStats = fs.statSync(fullPath);
155
1223
  const relativeToRoot = path.relative(rootDir, fullPath);
@@ -160,62 +1228,175 @@ function walkDirectory(
160
1228
  size: fileStats.size,
161
1229
  formattedSize: formatBytes(fileStats.size),
162
1230
  });
163
- } catch (e) {
164
- // Skip inaccessible files
165
- }
1231
+ } catch (e) {}
166
1232
  }
167
1233
  }
168
1234
 
169
1235
  return results;
170
1236
  }
171
1237
 
172
- function generateFileTree(filesWithSize, root, skipContentSet = null) {
1238
+ // ---------------------------------------------------------------------------
1239
+ // Code Index Tree Generator (v2.0.0)
1240
+ // ---------------------------------------------------------------------------
1241
+
1242
+ /**
1243
+ * Build the <code_index> tree with expanded code elements.
1244
+ */
1245
+ function generateCodeIndex(filesWithMeta, root, skipContentSet, noParse) {
173
1246
  let tree = `${path.basename(root)}/\n`;
174
- const structure = {};
175
1247
 
176
- filesWithSize.forEach(({ relativePath, formattedSize }) => {
177
- const parts = relativePath.split(path.sep);
1248
+ // Build directory structure
1249
+ const structure = {};
1250
+ for (const file of filesWithMeta) {
1251
+ const parts = file.relativePath.split(path.sep);
178
1252
  let currentLevel = structure;
179
- parts.forEach((part, index) => {
180
- const isFile = index === parts.length - 1;
1253
+ for (let i = 0; i < parts.length; i++) {
1254
+ const part = parts[i];
1255
+ const isFile = i === parts.length - 1;
181
1256
  if (isFile) {
182
- const shouldSkipContent =
183
- skipContentSet && skipContentSet.has(relativePath);
184
- currentLevel[part] = {
185
- size: formattedSize,
186
- skipContent: shouldSkipContent,
187
- };
1257
+ currentLevel[part] = { __file: file };
188
1258
  } else {
189
- if (!currentLevel[part]) {
190
- currentLevel[part] = {};
191
- }
1259
+ if (!currentLevel[part]) currentLevel[part] = {};
192
1260
  currentLevel = currentLevel[part];
193
1261
  }
194
- });
195
- });
1262
+ }
1263
+ }
196
1264
 
197
- const buildTree = (level, prefix) => {
198
- const entries = Object.keys(level);
199
- entries.forEach((entry, index) => {
200
- const isLast = index === entries.length - 1;
201
- const value = level[entry];
202
- const isFile = typeof value === "object" && value.size !== undefined;
203
- const connector = isLast ? "└── " : "ā”œā”€ā”€ ";
1265
+ function renderTree(level, prefix) {
1266
+ const keys = Object.keys(level);
1267
+ for (let i = 0; i < keys.length; i++) {
1268
+ const key = keys[i];
1269
+ const isLast = i === keys.length - 1;
1270
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251c\u2500\u2500 ";
1271
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1272
+ const value = level[key];
204
1273
 
205
- if (isFile) {
206
- const marker = value.skipContent ? " (content omitted)" : "";
207
- tree += `${prefix}${connector}[${value.size}] ${entry}${marker}\n`;
1274
+ if (value.__file) {
1275
+ const f = value.__file;
1276
+ const olRange = `OL: 1-${f.lineCount}`;
1277
+ const mlRange = `ML: ${f.mlStart}-${f.mlEnd}`;
1278
+ const sizeStr = f.formattedSize;
1279
+ const isSkipped = skipContentSet && skipContentSet.has(f.relativePath);
1280
+
1281
+ tree += `${prefix}${connector}${key} [${olRange} | ${mlRange} | ${sizeStr}]\n`;
1282
+
1283
+ if (isSkipped) {
1284
+ tree += `${childPrefix}(Content omitted - file size: ${sizeStr})\n`;
1285
+ } else if (!noParse && f.codeElements && f.codeElements.length > 0) {
1286
+ renderCodeElements(f.codeElements, childPrefix, f.mlStart);
1287
+ }
208
1288
  } else {
209
- tree += `${prefix}${connector}${entry}\n`;
210
- buildTree(value, `${prefix}${isLast ? " " : "│ "}`);
1289
+ // Directory
1290
+ tree += `${prefix}${connector}${key}/\n`;
1291
+ renderTree(value, childPrefix);
211
1292
  }
212
- });
213
- };
1293
+ }
1294
+ }
1295
+
1296
+ function renderCodeElements(elements, prefix, mlOffset) {
1297
+ for (let i = 0; i < elements.length; i++) {
1298
+ const el = elements[i];
1299
+ const isLast = i === elements.length - 1;
1300
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251c\u2500\u2500 ";
1301
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1302
+
1303
+ const olRange = `OL: ${el.startLine}-${el.endLine}`;
1304
+ const mlStart = mlOffset + el.startLine - 1;
1305
+ const mlEnd = mlOffset + el.endLine - 1;
1306
+ const mlRange = `ML: ${mlStart}-${mlEnd}`;
1307
+ const sizeStr = formatBytes(el.size);
214
1308
 
215
- buildTree(structure, "");
1309
+ tree += `${prefix}${connector}${el.label} [${olRange} | ${mlRange} | ${sizeStr}]\n`;
1310
+
1311
+ if (el.children && el.children.length > 0) {
1312
+ renderCodeElements(el.children, childPrefix, mlOffset);
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ renderTree(structure, "");
216
1318
  return tree;
217
1319
  }
218
1320
 
1321
+ // ---------------------------------------------------------------------------
1322
+ // Recreate functionality (v2.0.0)
1323
+ // ---------------------------------------------------------------------------
1324
+
1325
+ function recreateFromFile(inputFile, outputDir, dryRun, overwrite) {
1326
+ if (!fs.existsSync(inputFile)) {
1327
+ console.error(`\n\u274c Input file not found: ${inputFile}`);
1328
+ process.exit(1);
1329
+ }
1330
+
1331
+ const content = fs.readFileSync(inputFile, "utf8");
1332
+
1333
+ // Parse files from the merged_code section (or legacy format)
1334
+ const files = [];
1335
+ // Match: # FILE: path [...] followed by ```` block
1336
+ const fileRegex = /# FILE:\s*(.+?)\s*\[.*?\]\n````\n([\s\S]*?)\n````/g;
1337
+ let match;
1338
+
1339
+ while ((match = fileRegex.exec(content)) !== null) {
1340
+ const filePath = match[1].trim();
1341
+ const fileContent = match[2];
1342
+
1343
+ // Skip content-omitted files
1344
+ if (fileContent.trim().startsWith("(Content omitted")) continue;
1345
+
1346
+ files.push({ path: filePath, content: fileContent });
1347
+ }
1348
+
1349
+ if (files.length === 0) {
1350
+ // Try legacy format: ### **FILE:** `path`
1351
+ const legacyRegex = /### \*\*FILE:\*\*\s*`(.+?)`\n````\n([\s\S]*?)\n````/g;
1352
+ while ((match = legacyRegex.exec(content)) !== null) {
1353
+ const filePath = match[1].trim();
1354
+ const fileContent = match[2];
1355
+ if (fileContent.trim().startsWith("(Content omitted")) continue;
1356
+ files.push({ path: filePath, content: fileContent });
1357
+ }
1358
+ }
1359
+
1360
+ if (files.length === 0) {
1361
+ console.error("\n\u274c No files found in the input file.");
1362
+ process.exit(1);
1363
+ }
1364
+
1365
+ const resolvedOutputDir = path.resolve(outputDir);
1366
+ console.log(`\n\ud83d\udcc2 Output directory: ${resolvedOutputDir}\n`);
1367
+
1368
+ let totalSize = 0;
1369
+
1370
+ for (const file of files) {
1371
+ const fullPath = path.join(resolvedOutputDir, file.path);
1372
+ const size = Buffer.byteLength(file.content, "utf8");
1373
+ totalSize += size;
1374
+
1375
+ console.log(` ${file.path} (${formatBytes(size)})`);
1376
+
1377
+ if (!dryRun) {
1378
+ if (fs.existsSync(fullPath) && !overwrite) {
1379
+ console.error(` \u26a0\ufe0f Skipped (exists): ${file.path}`);
1380
+ continue;
1381
+ }
1382
+ const dir = path.dirname(fullPath);
1383
+ fs.mkdirSync(dir, { recursive: true });
1384
+ fs.writeFileSync(fullPath, file.content, "utf8");
1385
+ }
1386
+ }
1387
+
1388
+ console.log(`\n\ud83d\udcca Summary:`);
1389
+ console.log(
1390
+ ` \u2022 ${dryRun ? "Files to recreate" : "Files recreated"}: ${files.length}`,
1391
+ );
1392
+ console.log(` \u2022 Total size: ${formatBytes(totalSize)}`);
1393
+ console.log(`\n\u2705 Done!`);
1394
+ }
1395
+
1396
+ // ---------------------------------------------------------------------------
1397
+ // Main
1398
+ // ---------------------------------------------------------------------------
1399
+
219
1400
  async function main() {
220
1401
  const rawArgv = hideBin(process.argv);
221
1402
  if (rawArgv.includes("--version") || rawArgv.includes("-v")) {
@@ -228,13 +1409,13 @@ async function main() {
228
1409
  .usage("$0 [options]")
229
1410
  .option("o", {
230
1411
  alias: "output",
231
- describe: "Output file name",
1412
+ describe: "Output file (combine) or directory (recreate)",
232
1413
  type: "string",
233
1414
  default: "combicode.txt",
234
1415
  })
235
1416
  .option("d", {
236
1417
  alias: "dry-run",
237
- describe: "Preview files without creating the output file",
1418
+ describe: "Preview without making changes",
238
1419
  type: "boolean",
239
1420
  default: false,
240
1421
  })
@@ -254,65 +1435,81 @@ async function main() {
254
1435
  type: "boolean",
255
1436
  default: false,
256
1437
  })
257
- .option("no-gitignore", {
258
- describe: "Ignore the project's .gitignore file",
1438
+ .option("gitignore", {
1439
+ describe: "Use patterns from the project's .gitignore file",
259
1440
  type: "boolean",
260
- default: false,
1441
+ default: true,
261
1442
  })
262
- .option("no-header", {
263
- describe: "Omit the introductory prompt and file tree from the output",
1443
+ .option("header", {
1444
+ describe: "Include the introductory prompt and code index in the output",
264
1445
  type: "boolean",
265
- default: false,
1446
+ default: true,
266
1447
  })
267
1448
  .option("skip-content", {
268
1449
  describe:
269
1450
  "Comma-separated glob patterns for files to include in tree but omit content",
270
1451
  type: "string",
271
1452
  })
1453
+ .option("parse", {
1454
+ describe: "Enable code structure parsing",
1455
+ type: "boolean",
1456
+ default: true,
1457
+ })
1458
+ .option("r", {
1459
+ alias: "recreate",
1460
+ describe: "Recreate project from a combicode.txt file",
1461
+ type: "boolean",
1462
+ default: false,
1463
+ })
1464
+ .option("input", {
1465
+ describe: "Input combicode.txt file for recreate",
1466
+ type: "string",
1467
+ default: "combicode.txt",
1468
+ })
1469
+ .option("overwrite", {
1470
+ describe: "Overwrite existing files when recreating",
1471
+ type: "boolean",
1472
+ default: false,
1473
+ })
272
1474
  .version(version)
273
1475
  .alias("v", "version")
274
1476
  .help()
275
1477
  .alias("h", "help").argv;
276
1478
 
277
1479
  const projectRoot = process.cwd();
278
- console.log(`\n✨ Combicode v${version}`);
279
- console.log(`šŸ“‚ Root: ${projectRoot}`);
1480
+ console.log(`\u2728 Combicode v${version}`);
1481
+ console.log(`\ud83d\udcc2 Root: ${projectRoot}`);
280
1482
 
281
- const rootIgnoreManager = ignore();
1483
+ // --- Recreate mode ---
1484
+ if (argv.recreate) {
1485
+ const inputFile = path.resolve(projectRoot, argv.input);
1486
+ const outputDir =
1487
+ argv.output !== "combicode.txt" ? argv.output : projectRoot;
1488
+ recreateFromFile(inputFile, outputDir, argv.dryRun, argv.overwrite);
1489
+ return;
1490
+ }
282
1491
 
283
- // Only add minimal safety ignores + CLI excludes.
284
- // No external JSON config is loaded.
1492
+ // --- Combine mode ---
1493
+ const rootIgnoreManager = ignore();
285
1494
  rootIgnoreManager.add(SAFETY_IGNORES);
286
1495
 
287
1496
  if (argv.exclude) {
288
1497
  rootIgnoreManager.add(argv.exclude.split(","));
289
1498
  }
290
1499
 
1500
+ // Parse .gitmodules for submodule paths
291
1501
  const gitModulesPath = path.join(projectRoot, ".gitmodules");
292
1502
  if (fs.existsSync(gitModulesPath)) {
293
1503
  try {
294
1504
  const content = fs.readFileSync(gitModulesPath, "utf8");
295
1505
  const lines = content.split(/\r?\n/);
296
- const submodulePaths = [];
297
-
298
1506
  for (const line of lines) {
299
- // Match lines like: path = libs/my-lib
300
- const match = line.match(/^\s*path\s*=\s*(.+?)\s*$/);
301
- if (match) {
302
- submodulePaths.push(match[1]);
303
- }
304
- }
305
-
306
- if (submodulePaths.length > 0) {
307
- // Add identified submodule paths to the ignore manager
308
- rootIgnoreManager.add(submodulePaths);
1507
+ const m = line.match(/^\s*path\s*=\s*(.+?)\s*$/);
1508
+ if (m) rootIgnoreManager.add([m[1]]);
309
1509
  }
310
- } catch {
311
- // Fail silently if .gitmodules cannot be read
312
- }
1510
+ } catch (e) {}
313
1511
  }
314
1512
 
315
- // Create skip-content manager
316
1513
  const skipContentManager = ignore();
317
1514
  if (argv.skipContent) {
318
1515
  skipContentManager.add(argv.skipContent.split(","));
@@ -324,30 +1521,26 @@ async function main() {
324
1521
  ? new Set(
325
1522
  argv.includeExt
326
1523
  .split(",")
327
- .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
1524
+ .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)),
328
1525
  )
329
1526
  : null;
330
1527
 
331
- // Initialize the ignore chain with the root manager
332
1528
  const ignoreChain = [{ manager: rootIgnoreManager, root: projectRoot }];
333
-
334
- // Statistics container
335
1529
  const stats = { scanned: 0, ignored: 0 };
336
1530
 
337
- // Perform Recursive Walk
338
1531
  const includedFiles = walkDirectory(
339
1532
  projectRoot,
340
1533
  projectRoot,
341
1534
  ignoreChain,
342
1535
  allowedExtensions,
343
1536
  absoluteOutputPath,
344
- !argv.noGitignore,
345
- stats
1537
+ argv.gitignore,
1538
+ stats,
346
1539
  );
347
1540
 
348
1541
  includedFiles.sort((a, b) => a.path.localeCompare(b.path));
349
1542
 
350
- // Determine which files should have content skipped
1543
+ // Determine skip-content set
351
1544
  const skipContentSet = new Set();
352
1545
  if (argv.skipContent) {
353
1546
  includedFiles.forEach((file) => {
@@ -358,96 +1551,219 @@ async function main() {
358
1551
  });
359
1552
  }
360
1553
 
361
- // Calculate total size of included files (excluding files with skipped content)
362
- const totalSizeBytes = includedFiles.reduce((acc, file) => {
363
- // Don't count files with skipped content in the total size
364
- if (skipContentSet.has(file.relativePath)) {
365
- return acc;
366
- }
367
- return acc + file.size;
368
- }, 0);
369
-
370
1554
  if (includedFiles.length === 0) {
371
1555
  console.error(
372
- "\nāŒ No files to include. Check your path, .gitignore, or filters."
1556
+ "\n\u274c No files to include. Check your path, .gitignore, or filters.",
373
1557
  );
374
1558
  process.exit(1);
375
1559
  }
376
1560
 
1561
+ // --- Read file contents & parse code structure ---
1562
+ for (const fileObj of includedFiles) {
1563
+ const isSkipped = skipContentSet.has(fileObj.relativePath);
1564
+ if (isSkipped) {
1565
+ fileObj.content = null;
1566
+ fileObj.lineCount = 0;
1567
+ fileObj.codeElements = [];
1568
+ } else {
1569
+ try {
1570
+ fileObj.content = fs.readFileSync(fileObj.path, "utf8");
1571
+ // Count actual lines: strings ending with \n get an extra empty element from split
1572
+ const parts = fileObj.content.split("\n");
1573
+ fileObj.lineCount = fileObj.content.endsWith("\n")
1574
+ ? parts.length - 1
1575
+ : parts.length;
1576
+
1577
+ if (argv.parse) {
1578
+ const flat = parseCodeStructure(
1579
+ fileObj.relativePath,
1580
+ fileObj.content,
1581
+ );
1582
+ fileObj.codeElements = nestElements(flat);
1583
+ } else {
1584
+ fileObj.codeElements = [];
1585
+ }
1586
+ } catch (e) {
1587
+ fileObj.content = `... (error reading file: ${e.message}) ...`;
1588
+ fileObj.lineCount = 1;
1589
+ fileObj.codeElements = [];
1590
+ }
1591
+ }
1592
+ }
1593
+
1594
+ // --- Calculate ML (Merged Line) offsets ---
1595
+ // First pass: figure out how many header lines come before merged_code
1596
+ const systemPrompt = argv.llmsTxt
1597
+ ? LLMS_TXT_SYSTEM_PROMPT
1598
+ : DEFAULT_SYSTEM_PROMPT;
1599
+ let headerLines = 0;
1600
+ if (argv.header) {
1601
+ headerLines += systemPrompt.split("\n").length; // system prompt
1602
+ headerLines += 1; // blank line after prompt
1603
+ // <code_index> section will be added but we need to calculate it after ML offsets are known
1604
+ // We'll do a two-pass approach
1605
+ }
1606
+
1607
+ // Two-pass: first compute code_index size, then compute ML offsets
1608
+ // Pass 1: Assign provisional ML offsets (we'll need the code_index line count first)
1609
+ // The structure is: header + code_index_block + merged_code_block
1610
+
1611
+ // Compute provisional line count for each file in merged_code
1612
+ // Each file: "# FILE: path [...]\n````\n" + content + "\n````\n" = 4 lines + content lines
1613
+ let mergedCodeHeaderLine = 1; // Will be set properly
1614
+ let currentMl = 1;
1615
+
1616
+ // We need to do this iteratively:
1617
+ // 1. Calculate code_index with placeholder MLs
1618
+ // 2. Count code_index lines
1619
+ // 3. Calculate real MLs
1620
+ // 4. Regenerate code_index with real MLs
1621
+
1622
+ // Step 1: Assign temporary ML values
1623
+ let tempMl = 1; // placeholder
1624
+ for (const fileObj of includedFiles) {
1625
+ fileObj.mlStart = tempMl;
1626
+ const isSkipped = skipContentSet.has(fileObj.relativePath);
1627
+ if (isSkipped) {
1628
+ fileObj.mlEnd = tempMl + 1; // placeholder line
1629
+ tempMl += 4 + 1; // header(2) + placeholder(1) + footer(1) = 4
1630
+ } else {
1631
+ fileObj.mlEnd = tempMl + fileObj.lineCount - 1;
1632
+ tempMl += 4 + fileObj.lineCount; // header(2) + content + footer(2)
1633
+ }
1634
+ }
1635
+
1636
+ // Step 2: Generate provisional code_index to count lines
1637
+ let codeIndex = generateCodeIndex(
1638
+ includedFiles,
1639
+ projectRoot,
1640
+ skipContentSet,
1641
+ !argv.parse,
1642
+ );
1643
+ // codeIndex ends with \n, so split produces an extra empty element
1644
+ const codeIndexLineCount = codeIndex.endsWith("\n")
1645
+ ? codeIndex.split("\n").length - 1
1646
+ : codeIndex.split("\n").length;
1647
+
1648
+ // Step 3: Calculate real header line count
1649
+ let totalHeaderLines = 0;
1650
+ if (argv.header) {
1651
+ totalHeaderLines += systemPrompt.split("\n").length; // prompt lines + blank line from extra \n
1652
+ totalHeaderLines += 1; // "<code_index>"
1653
+ totalHeaderLines += codeIndexLineCount; // code index content
1654
+ totalHeaderLines += 1; // "</code_index>"
1655
+ totalHeaderLines += 1; // blank line
1656
+ totalHeaderLines += 1; // "<merged_code>"
1657
+ } else {
1658
+ totalHeaderLines += 1; // "<merged_code>"
1659
+ }
1660
+
1661
+ // Step 4: Recalculate ML offsets with real header
1662
+ currentMl = totalHeaderLines + 1;
1663
+ for (const fileObj of includedFiles) {
1664
+ const isSkipped = skipContentSet.has(fileObj.relativePath);
1665
+ // File header: "# FILE: path [...]\n````\n" = 2 lines
1666
+ fileObj.mlStart = currentMl + 2; // Content starts after header
1667
+ if (isSkipped) {
1668
+ fileObj.mlEnd = fileObj.mlStart; // 1 line placeholder
1669
+ currentMl += 2 + 1 + 2; // header(2) + placeholder(1) + footer(2: "\n````\n")
1670
+ } else {
1671
+ fileObj.mlEnd = fileObj.mlStart + fileObj.lineCount - 1;
1672
+ currentMl += 2 + fileObj.lineCount + 2; // header(2) + content + footer(2)
1673
+ }
1674
+ }
1675
+
1676
+ // Step 5: Regenerate code_index with correct MLs
1677
+ codeIndex = generateCodeIndex(
1678
+ includedFiles,
1679
+ projectRoot,
1680
+ skipContentSet,
1681
+ !argv.parse,
1682
+ );
1683
+
1684
+ // Calculate total content size
1685
+ const totalSizeBytes = includedFiles.reduce((acc, file) => {
1686
+ if (skipContentSet.has(file.relativePath)) return acc;
1687
+ return acc + file.size;
1688
+ }, 0);
1689
+
1690
+ // --- Dry run ---
377
1691
  if (argv.dryRun) {
378
- console.log("\nšŸ“‹ Files to be included (Dry Run):\n");
379
- const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
380
- console.log(tree);
381
- console.log("\nšŸ“Š Summary (Dry Run):");
382
- console.log(
383
- ` • Included: ${includedFiles.length} files (${formatBytes(
384
- totalSizeBytes
385
- )})`
386
- );
1692
+ console.log("\n\ud83d\udccb Files to include (dry run):\n");
1693
+ console.log(codeIndex);
1694
+ console.log(`\n\ud83d\udcca Summary:`);
1695
+ console.log(` \u2022 Total files: ${includedFiles.length}`);
1696
+ console.log(` \u2022 Total size: ${formatBytes(totalSizeBytes)}`);
387
1697
  if (skipContentSet.size > 0) {
388
- console.log(` • Content omitted: ${skipContentSet.size} files`);
1698
+ console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
389
1699
  }
390
- console.log(` • Ignored: ${stats.ignored} files/dirs`);
1700
+ console.log(`\n\u2705 Done!`);
391
1701
  return;
392
1702
  }
393
1703
 
1704
+ // --- Write output ---
394
1705
  const outputStream = fs.createWriteStream(argv.output);
395
1706
  let totalLines = 0;
396
1707
 
397
- if (!argv.noHeader) {
398
- const systemPrompt = argv.llmsTxt
399
- ? LLMS_TXT_SYSTEM_PROMPT
400
- : DEFAULT_SYSTEM_PROMPT;
1708
+ if (argv.header) {
401
1709
  outputStream.write(systemPrompt + "\n");
402
- totalLines += systemPrompt.split("\n").length;
1710
+ totalLines += systemPrompt.split("\n").length + 1;
403
1711
 
404
- outputStream.write("## Project File Tree\n\n");
405
- outputStream.write("```\n");
406
- const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
407
- outputStream.write(tree);
408
- outputStream.write("```\n\n");
409
- outputStream.write("---\n\n");
1712
+ outputStream.write("<code_index>\n");
1713
+ outputStream.write(codeIndex);
1714
+ outputStream.write("</code_index>\n\n");
1715
+ totalLines += codeIndexLineCount + 3;
410
1716
 
411
- totalLines += tree.split("\n").length + 5;
1717
+ outputStream.write("<merged_code>\n");
1718
+ totalLines += 1;
1719
+ } else {
1720
+ outputStream.write("<merged_code>\n");
1721
+ totalLines += 1;
412
1722
  }
413
1723
 
414
1724
  for (const fileObj of includedFiles) {
415
1725
  const relativePath = fileObj.relativePath.replace(/\\/g, "/");
416
- const shouldSkipContent = skipContentSet.has(fileObj.relativePath);
1726
+ const isSkipped = skipContentSet.has(fileObj.relativePath);
1727
+
1728
+ const olRange = `OL: 1-${fileObj.lineCount}`;
1729
+ const mlRange = `ML: ${fileObj.mlStart}-${fileObj.mlEnd}`;
1730
+ const sizeStr = fileObj.formattedSize;
417
1731
 
418
- outputStream.write(`### **FILE:** \`${relativePath}\`\n`);
1732
+ outputStream.write(
1733
+ `# FILE: ${relativePath} [${olRange} | ${mlRange} | ${sizeStr}]\n`,
1734
+ );
419
1735
  outputStream.write("````\n");
420
- if (shouldSkipContent) {
421
- outputStream.write(
422
- `(Content omitted - file size: ${fileObj.formattedSize})`
423
- );
1736
+ totalLines += 2;
1737
+
1738
+ if (isSkipped) {
1739
+ outputStream.write(`(Content omitted - file size: ${sizeStr})\n`);
424
1740
  totalLines += 1;
425
1741
  } else {
426
- try {
427
- const content = fs.readFileSync(fileObj.path, "utf8");
428
- outputStream.write(content);
429
- totalLines += content.split("\n").length;
430
- } catch (e) {
431
- outputStream.write(`... (error reading file: ${e.message}) ...`);
1742
+ outputStream.write(fileObj.content);
1743
+ if (!fileObj.content.endsWith("\n")) {
1744
+ outputStream.write("\n");
432
1745
  }
1746
+ totalLines += fileObj.lineCount;
433
1747
  }
434
- outputStream.write("\n````\n\n");
435
- totalLines += 4; // Headers/footers lines
1748
+
1749
+ outputStream.write("````\n\n");
1750
+ totalLines += 2;
436
1751
  }
1752
+
1753
+ outputStream.write("</merged_code>\n");
1754
+ totalLines += 1;
437
1755
  outputStream.end();
438
1756
 
439
- console.log(`\nšŸ“Š Summary:`);
1757
+ console.log(`\n\ud83d\udcca Summary:`);
440
1758
  console.log(
441
- ` • Included: ${includedFiles.length} files (${formatBytes(
442
- totalSizeBytes
443
- )})`
1759
+ ` \u2022 Included: ${includedFiles.length} files (${formatBytes(totalSizeBytes)})`,
444
1760
  );
445
1761
  if (skipContentSet.size > 0) {
446
- console.log(` • Content omitted: ${skipContentSet.size} files`);
1762
+ console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
447
1763
  }
448
- console.log(` • Ignored: ${stats.ignored} files/dirs`);
449
- console.log(` • Output: ${argv.output} (~${totalLines} lines)`);
450
- console.log(`\nāœ… Done!`);
1764
+ console.log(` \u2022 Ignored: ${stats.ignored} files/dirs`);
1765
+ console.log(` \u2022 Output: ${argv.output} (~${totalLines} lines)`);
1766
+ console.log(`\n\u2705 Done!`);
451
1767
  }
452
1768
 
453
1769
  main().catch((err) => {