combicode 1.7.3 → 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 +1482 -142
  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;
51
923
  }
52
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;
1091
+ }
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);
1308
+
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
+ }
214
1316
 
215
- buildTree(structure, "");
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,41 +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
 
291
- // Create skip-content manager
1500
+ // Parse .gitmodules for submodule paths
1501
+ const gitModulesPath = path.join(projectRoot, ".gitmodules");
1502
+ if (fs.existsSync(gitModulesPath)) {
1503
+ try {
1504
+ const content = fs.readFileSync(gitModulesPath, "utf8");
1505
+ const lines = content.split(/\r?\n/);
1506
+ for (const line of lines) {
1507
+ const m = line.match(/^\s*path\s*=\s*(.+?)\s*$/);
1508
+ if (m) rootIgnoreManager.add([m[1]]);
1509
+ }
1510
+ } catch (e) {}
1511
+ }
1512
+
292
1513
  const skipContentManager = ignore();
293
1514
  if (argv.skipContent) {
294
1515
  skipContentManager.add(argv.skipContent.split(","));
@@ -300,30 +1521,26 @@ async function main() {
300
1521
  ? new Set(
301
1522
  argv.includeExt
302
1523
  .split(",")
303
- .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
1524
+ .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)),
304
1525
  )
305
1526
  : null;
306
1527
 
307
- // Initialize the ignore chain with the root manager
308
1528
  const ignoreChain = [{ manager: rootIgnoreManager, root: projectRoot }];
309
-
310
- // Statistics container
311
1529
  const stats = { scanned: 0, ignored: 0 };
312
1530
 
313
- // Perform Recursive Walk
314
1531
  const includedFiles = walkDirectory(
315
1532
  projectRoot,
316
1533
  projectRoot,
317
1534
  ignoreChain,
318
1535
  allowedExtensions,
319
1536
  absoluteOutputPath,
320
- !argv.noGitignore,
321
- stats
1537
+ argv.gitignore,
1538
+ stats,
322
1539
  );
323
1540
 
324
1541
  includedFiles.sort((a, b) => a.path.localeCompare(b.path));
325
1542
 
326
- // Determine which files should have content skipped
1543
+ // Determine skip-content set
327
1544
  const skipContentSet = new Set();
328
1545
  if (argv.skipContent) {
329
1546
  includedFiles.forEach((file) => {
@@ -334,96 +1551,219 @@ async function main() {
334
1551
  });
335
1552
  }
336
1553
 
337
- // Calculate total size of included files (excluding files with skipped content)
338
- const totalSizeBytes = includedFiles.reduce((acc, file) => {
339
- // Don't count files with skipped content in the total size
340
- if (skipContentSet.has(file.relativePath)) {
341
- return acc;
342
- }
343
- return acc + file.size;
344
- }, 0);
345
-
346
1554
  if (includedFiles.length === 0) {
347
1555
  console.error(
348
- "\nāŒ No files to include. Check your path, .gitignore, or filters."
1556
+ "\n\u274c No files to include. Check your path, .gitignore, or filters.",
349
1557
  );
350
1558
  process.exit(1);
351
1559
  }
352
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 ---
353
1691
  if (argv.dryRun) {
354
- console.log("\nšŸ“‹ Files to be included (Dry Run):\n");
355
- const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
356
- console.log(tree);
357
- console.log("\nšŸ“Š Summary (Dry Run):");
358
- console.log(
359
- ` • Included: ${includedFiles.length} files (${formatBytes(
360
- totalSizeBytes
361
- )})`
362
- );
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)}`);
363
1697
  if (skipContentSet.size > 0) {
364
- console.log(` • Content omitted: ${skipContentSet.size} files`);
1698
+ console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
365
1699
  }
366
- console.log(` • Ignored: ${stats.ignored} files/dirs`);
1700
+ console.log(`\n\u2705 Done!`);
367
1701
  return;
368
1702
  }
369
1703
 
1704
+ // --- Write output ---
370
1705
  const outputStream = fs.createWriteStream(argv.output);
371
1706
  let totalLines = 0;
372
1707
 
373
- if (!argv.noHeader) {
374
- const systemPrompt = argv.llmsTxt
375
- ? LLMS_TXT_SYSTEM_PROMPT
376
- : DEFAULT_SYSTEM_PROMPT;
1708
+ if (argv.header) {
377
1709
  outputStream.write(systemPrompt + "\n");
378
- totalLines += systemPrompt.split("\n").length;
1710
+ totalLines += systemPrompt.split("\n").length + 1;
379
1711
 
380
- outputStream.write("## Project File Tree\n\n");
381
- outputStream.write("```\n");
382
- const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
383
- outputStream.write(tree);
384
- outputStream.write("```\n\n");
385
- 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;
386
1716
 
387
- 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;
388
1722
  }
389
1723
 
390
1724
  for (const fileObj of includedFiles) {
391
1725
  const relativePath = fileObj.relativePath.replace(/\\/g, "/");
392
- 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;
393
1731
 
394
- outputStream.write(`### **FILE:** \`${relativePath}\`\n`);
1732
+ outputStream.write(
1733
+ `# FILE: ${relativePath} [${olRange} | ${mlRange} | ${sizeStr}]\n`,
1734
+ );
395
1735
  outputStream.write("````\n");
396
- if (shouldSkipContent) {
397
- outputStream.write(
398
- `(Content omitted - file size: ${fileObj.formattedSize})`
399
- );
1736
+ totalLines += 2;
1737
+
1738
+ if (isSkipped) {
1739
+ outputStream.write(`(Content omitted - file size: ${sizeStr})\n`);
400
1740
  totalLines += 1;
401
1741
  } else {
402
- try {
403
- const content = fs.readFileSync(fileObj.path, "utf8");
404
- outputStream.write(content);
405
- totalLines += content.split("\n").length;
406
- } catch (e) {
407
- outputStream.write(`... (error reading file: ${e.message}) ...`);
1742
+ outputStream.write(fileObj.content);
1743
+ if (!fileObj.content.endsWith("\n")) {
1744
+ outputStream.write("\n");
408
1745
  }
1746
+ totalLines += fileObj.lineCount;
409
1747
  }
410
- outputStream.write("\n````\n\n");
411
- totalLines += 4; // Headers/footers lines
1748
+
1749
+ outputStream.write("````\n\n");
1750
+ totalLines += 2;
412
1751
  }
1752
+
1753
+ outputStream.write("</merged_code>\n");
1754
+ totalLines += 1;
413
1755
  outputStream.end();
414
1756
 
415
- console.log(`\nšŸ“Š Summary:`);
1757
+ console.log(`\n\ud83d\udcca Summary:`);
416
1758
  console.log(
417
- ` • Included: ${includedFiles.length} files (${formatBytes(
418
- totalSizeBytes
419
- )})`
1759
+ ` \u2022 Included: ${includedFiles.length} files (${formatBytes(totalSizeBytes)})`,
420
1760
  );
421
1761
  if (skipContentSet.size > 0) {
422
- console.log(` • Content omitted: ${skipContentSet.size} files`);
1762
+ console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
423
1763
  }
424
- console.log(` • Ignored: ${stats.ignored} files/dirs`);
425
- console.log(` • Output: ${argv.output} (~${totalLines} lines)`);
426
- 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!`);
427
1767
  }
428
1768
 
429
1769
  main().catch((err) => {