ai-consultants 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.claude/commands/ai-consultants:consult.md +94 -0
  2. package/.claude/commands/ai-consultants:debate.md +103 -0
  3. package/.claude/commands/ai-consultants:help.md +30 -0
  4. package/CLAUDE.md +839 -0
  5. package/LICENSE +21 -0
  6. package/README.md +758 -0
  7. package/SKILL.md +156 -0
  8. package/bin/ai-consultants +96 -0
  9. package/docs/COST_RATES.md +172 -0
  10. package/docs/JSON_SCHEMA.md +305 -0
  11. package/docs/SETUP.md +879 -0
  12. package/docs/SMART_ROUTING.md +219 -0
  13. package/docs/cost_rates.json +143 -0
  14. package/package.json +47 -0
  15. package/references/configuration.md +141 -0
  16. package/references/details.md +60 -0
  17. package/scripts/build_context.sh +562 -0
  18. package/scripts/classify_question.sh +148 -0
  19. package/scripts/config.sh +753 -0
  20. package/scripts/configure.sh +1093 -0
  21. package/scripts/consult_all.sh +934 -0
  22. package/scripts/debate_round.sh +421 -0
  23. package/scripts/doctor.sh +642 -0
  24. package/scripts/followup.sh +197 -0
  25. package/scripts/install.sh +319 -0
  26. package/scripts/lib/api.sh +640 -0
  27. package/scripts/lib/api_query.sh +228 -0
  28. package/scripts/lib/cache.sh +338 -0
  29. package/scripts/lib/chunking.sh +880 -0
  30. package/scripts/lib/code_optimizer.sh +671 -0
  31. package/scripts/lib/common.sh +1088 -0
  32. package/scripts/lib/costs.sh +674 -0
  33. package/scripts/lib/personas.sh +372 -0
  34. package/scripts/lib/progress.sh +251 -0
  35. package/scripts/lib/reflection.sh +447 -0
  36. package/scripts/lib/routing.sh +566 -0
  37. package/scripts/lib/schema.json +165 -0
  38. package/scripts/lib/session.sh +327 -0
  39. package/scripts/lib/symbol_map.sh +445 -0
  40. package/scripts/lib/voting.sh +495 -0
  41. package/scripts/peer_review.sh +460 -0
  42. package/scripts/preflight_check.sh +452 -0
  43. package/scripts/query_aider.sh +83 -0
  44. package/scripts/query_amp.sh +67 -0
  45. package/scripts/query_claude.sh +99 -0
  46. package/scripts/query_codex.sh +92 -0
  47. package/scripts/query_cursor.sh +72 -0
  48. package/scripts/query_deepseek.sh +17 -0
  49. package/scripts/query_gemini.sh +85 -0
  50. package/scripts/query_glm.sh +17 -0
  51. package/scripts/query_grok.sh +17 -0
  52. package/scripts/query_kilo.sh +140 -0
  53. package/scripts/query_kimi.sh +67 -0
  54. package/scripts/query_minimax.sh +17 -0
  55. package/scripts/query_mistral.sh +97 -0
  56. package/scripts/query_ollama.sh +328 -0
  57. package/scripts/query_qwen3.sh +93 -0
  58. package/scripts/setup_wizard.sh +367 -0
  59. package/scripts/skillport-install.sh +196 -0
  60. package/scripts/synthesize.sh +332 -0
  61. package/templates/consultation_report.md +323 -0
  62. package/templates/synthesis_prompt.md +120 -0
@@ -0,0 +1,880 @@
1
+ #!/bin/bash
2
+ # chunking.sh - Semantic chunking for code files (Bash 3.2 compatible)
3
+
4
+ # Source common utilities
5
+ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$_SCRIPT_DIR/common.sh" 2>/dev/null || true
7
+
8
+ # Logging stubs if common.sh unavailable
9
+ if ! command -v log_debug &>/dev/null; then
10
+ log_debug() { :; }
11
+ log_info() { echo "[INFO] $1" >&2; }
12
+ log_warn() { echo "[WARN] $1" >&2; }
13
+ log_error() { echo "[ERROR] $1" >&2; }
14
+ estimate_tokens() { echo $(( $(echo -n "$1" | wc -c | tr -d ' ') / 4 )); }
15
+ fi
16
+
17
+ # =============================================================================
18
+ # CONFIGURATION
19
+ # =============================================================================
20
+
21
+ # Maximum tokens per chunk (approximate: 1 token ~ 4 chars)
22
+ CHUNK_MAX_TOKENS="${CHUNK_MAX_TOKENS:-500}"
23
+
24
+ # Number of lines to overlap between adjacent chunks
25
+ CHUNK_OVERLAP_LINES="${CHUNK_OVERLAP_LINES:-5}"
26
+
27
+ # Priority keywords (comma-separated) for scoring chunks
28
+ CHUNK_PRIORITY_KEYWORDS="${CHUNK_PRIORITY_KEYWORDS:-main,export,public,api,handler,controller}"
29
+
30
+ # Temporary directory for chunk storage
31
+ CHUNK_TEMP_DIR="${CHUNK_TEMP_DIR:-/tmp/ai_consultants_chunks}"
32
+
33
+ # Initialize chunk storage directory with secure permissions
34
+ if [[ ! -d "$CHUNK_TEMP_DIR" ]]; then
35
+ mkdir -p "$CHUNK_TEMP_DIR" 2>/dev/null || true
36
+ fi
37
+ chmod 700 "$CHUNK_TEMP_DIR" 2>/dev/null || true
38
+
39
+ # =============================================================================
40
+ # INTERNAL STATE (file-scoped, cleared between operations)
41
+ # =============================================================================
42
+
43
+ # Current file being processed
44
+ _CHUNK_CURRENT_FILE=""
45
+
46
+ # Chunk counter for generating IDs
47
+ _CHUNK_COUNTER=0
48
+
49
+ # Session ID for current chunking operation
50
+ _CHUNK_SESSION_ID=""
51
+
52
+ # =============================================================================
53
+ # LANGUAGE DETECTION
54
+ # =============================================================================
55
+
56
+ # Detect programming language from file extension
57
+ # Usage: _detect_language "file.py"
58
+ # Returns: python|javascript|typescript|go|java|ruby|bash|c|cpp|rust|unknown
59
+ _detect_language() {
60
+ local file_path="$1"
61
+ local extension="${file_path##*.}"
62
+
63
+ case "$extension" in
64
+ py) echo "python" ;;
65
+ js|mjs|cjs) echo "javascript" ;;
66
+ ts|tsx) echo "typescript" ;;
67
+ go) echo "go" ;;
68
+ java) echo "java" ;;
69
+ rb) echo "ruby" ;;
70
+ sh|bash) echo "bash" ;;
71
+ c|h) echo "c" ;;
72
+ cpp|cc|hpp) echo "cpp" ;;
73
+ rs) echo "rust" ;;
74
+ *) echo "unknown" ;;
75
+ esac
76
+ }
77
+
78
+ # =============================================================================
79
+ # PATTERN DEFINITIONS
80
+ # =============================================================================
81
+
82
+ # Get function pattern for a language
83
+ # Usage: _get_function_pattern "python"
84
+ _get_function_pattern() {
85
+ local lang="$1"
86
+
87
+ case "$lang" in
88
+ python)
89
+ echo '^[[:space:]]*(async[[:space:]]+)?def[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\('
90
+ ;;
91
+ javascript|typescript)
92
+ # Match: function name(), async function name(), const name = () =>, const name = function()
93
+ echo '^[[:space:]]*(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*|^[[:space:]]*(export[[:space:]]+)?(const|let|var)[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=[[:space:]]*(async[[:space:]]*)?\('
94
+ ;;
95
+ go)
96
+ echo '^func[[:space:]]+(\([^)]+\)[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\('
97
+ ;;
98
+ java)
99
+ echo '^[[:space:]]*(public|private|protected)?[[:space:]]*(static)?[[:space:]]*(void|[A-Za-z_][A-Za-z0-9_<>,]*)[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\('
100
+ ;;
101
+ ruby)
102
+ echo '^[[:space:]]*def[[:space:]]+[a-zA-Z_][a-zA-Z0-9_?!]*'
103
+ ;;
104
+ bash)
105
+ echo '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)[[:space:]]*\{|^[[:space:]]*function[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
106
+ ;;
107
+ c|cpp)
108
+ echo '^[a-zA-Z_][a-zA-Z0-9_*[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\([^;]*$'
109
+ ;;
110
+ rust)
111
+ echo '^[[:space:]]*(pub[[:space:]]+)?(async[[:space:]]+)?fn[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
112
+ ;;
113
+ *)
114
+ echo '^(function|def|fn|func)[[:space:]]+[a-zA-Z_]'
115
+ ;;
116
+ esac
117
+ }
118
+
119
+ # Get class pattern for a language
120
+ # Usage: _get_class_pattern "python"
121
+ _get_class_pattern() {
122
+ local lang="$1"
123
+
124
+ case "$lang" in
125
+ python)
126
+ echo '^class[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
127
+ ;;
128
+ javascript|typescript)
129
+ echo '^[[:space:]]*(export[[:space:]]+)?(default[[:space:]]+)?class[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
130
+ ;;
131
+ java)
132
+ echo '^[[:space:]]*(public|private)?[[:space:]]*(abstract)?[[:space:]]*class[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
133
+ ;;
134
+ ruby)
135
+ echo '^class[[:space:]]+[A-Z][a-zA-Z0-9_]*'
136
+ ;;
137
+ cpp)
138
+ echo '^[[:space:]]*(class|struct)[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
139
+ ;;
140
+ rust)
141
+ echo '^[[:space:]]*(pub[[:space:]]+)?(struct|enum|impl)[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*'
142
+ ;;
143
+ go)
144
+ echo '^type[[:space:]]+[A-Z][a-zA-Z0-9_]*[[:space:]]+(struct|interface)'
145
+ ;;
146
+ *)
147
+ echo '^class[[:space:]]+[a-zA-Z_]'
148
+ ;;
149
+ esac
150
+ }
151
+
152
+ # Get import pattern for a language
153
+ # Usage: _get_import_pattern "python"
154
+ _get_import_pattern() {
155
+ local lang="$1"
156
+
157
+ case "$lang" in
158
+ python)
159
+ echo '^(import[[:space:]]|from[[:space:]])'
160
+ ;;
161
+ javascript|typescript)
162
+ echo "^(import[[:space:]]|const[[:space:]]+.*=[[:space:]]*require\(|export[[:space:]]+\{)"
163
+ ;;
164
+ go)
165
+ echo '^import[[:space:]]'
166
+ ;;
167
+ java)
168
+ echo '^import[[:space:]]'
169
+ ;;
170
+ ruby)
171
+ echo "^(require[[:space:]]|require_relative[[:space:]]|include[[:space:]])"
172
+ ;;
173
+ rust)
174
+ echo '^use[[:space:]]'
175
+ ;;
176
+ c|cpp)
177
+ echo '^#include[[:space:]]'
178
+ ;;
179
+ *)
180
+ echo '^import[[:space:]]'
181
+ ;;
182
+ esac
183
+ }
184
+
185
+ # =============================================================================
186
+ # CHECKSUM UTILITIES
187
+ # =============================================================================
188
+
189
+ # Calculate MD5 checksum of content (portable)
190
+ # Usage: _calculate_checksum "content"
191
+ _calculate_checksum() {
192
+ local content="$1"
193
+
194
+ # Try md5sum (Linux) first, then md5 (macOS)
195
+ if command -v md5sum &>/dev/null; then
196
+ echo -n "$content" | md5sum | cut -d' ' -f1
197
+ elif command -v md5 &>/dev/null; then
198
+ echo -n "$content" | md5 -q
199
+ else
200
+ # Fallback: use character count and line count as pseudo-checksum
201
+ local chars lines
202
+ chars=$(echo -n "$content" | wc -c | tr -d ' ')
203
+ lines=$(echo "$content" | wc -l | tr -d ' ')
204
+ echo "fallback_${chars}_${lines}"
205
+ fi
206
+ }
207
+
208
+ # =============================================================================
209
+ # LINE EXTRACTION UTILITIES
210
+ # =============================================================================
211
+
212
+ # Extract lines from file (1-indexed, inclusive)
213
+ # Usage: _extract_lines "file" start_line end_line
214
+ _extract_lines() {
215
+ local file="$1"
216
+ local start_line="$2"
217
+ local end_line="$3"
218
+
219
+ # Validate inputs
220
+ if [[ ! -f "$file" ]]; then
221
+ log_error "File not found: $file"
222
+ return 1
223
+ fi
224
+
225
+ # Use sed for portable line extraction
226
+ sed -n "${start_line},${end_line}p" "$file"
227
+ }
228
+
229
+ # Count total lines in file
230
+ # Usage: _count_lines "file"
231
+ _count_lines() {
232
+ local file="$1"
233
+ wc -l < "$file" | tr -d ' '
234
+ }
235
+
236
+ # =============================================================================
237
+ # SEMANTIC BOUNDARY DETECTION
238
+ # =============================================================================
239
+
240
+ # Find all semantic boundaries in a file
241
+ # Usage: _find_semantic_boundaries "file" "language"
242
+ # Output: Line numbers where semantic units start (one per line)
243
+ _find_semantic_boundaries() {
244
+ local file="$1"
245
+ local lang="$2"
246
+
247
+ local func_pattern class_pattern import_pattern
248
+ func_pattern=$(_get_function_pattern "$lang")
249
+ class_pattern=$(_get_class_pattern "$lang")
250
+ import_pattern=$(_get_import_pattern "$lang")
251
+
252
+ local line_num=0
253
+ local in_import_block=0
254
+ local last_import_line=0
255
+ local boundaries=""
256
+
257
+ while IFS= read -r line || [[ -n "$line" ]]; do
258
+ line_num=$((line_num + 1))
259
+
260
+ # Check for import lines (group consecutive imports)
261
+ if echo "$line" | grep -qE "$import_pattern" 2>/dev/null; then
262
+ if [[ $in_import_block -eq 0 ]]; then
263
+ # Start of import block
264
+ boundaries="${boundaries}${line_num}:import
265
+ "
266
+ in_import_block=1
267
+ fi
268
+ last_import_line=$line_num
269
+ continue
270
+ fi
271
+
272
+ # End import block if we've moved past imports
273
+ if [[ $in_import_block -eq 1 && $line_num -gt $((last_import_line + 1)) ]]; then
274
+ in_import_block=0
275
+ fi
276
+
277
+ # Check for class definitions
278
+ if echo "$line" | grep -qE "$class_pattern" 2>/dev/null; then
279
+ boundaries="${boundaries}${line_num}:class
280
+ "
281
+ continue
282
+ fi
283
+
284
+ # Check for function definitions
285
+ if echo "$line" | grep -qE "$func_pattern" 2>/dev/null; then
286
+ boundaries="${boundaries}${line_num}:function
287
+ "
288
+ continue
289
+ fi
290
+
291
+ done < "$file"
292
+
293
+ echo -n "$boundaries"
294
+ }
295
+
296
+ # Find the end of a semantic unit (function, class, etc.)
297
+ # Usage: _find_unit_end "file" start_line unit_type language
298
+ _find_unit_end() {
299
+ local file="$1"
300
+ local start_line="$2"
301
+ local unit_type="$3"
302
+ local lang="$4"
303
+
304
+ local total_lines
305
+ total_lines=$(_count_lines "$file")
306
+
307
+ # For imports, find consecutive import lines
308
+ if [[ "$unit_type" == "import" ]]; then
309
+ local import_pattern
310
+ import_pattern=$(_get_import_pattern "$lang")
311
+ local current_line=$start_line
312
+ local last_import=$start_line
313
+
314
+ while [[ $current_line -le $total_lines ]]; do
315
+ local line
316
+ line=$(sed -n "${current_line}p" "$file")
317
+
318
+ # Check if line is empty or comment (allow gaps)
319
+ if [[ -z "$line" || "$line" =~ ^[[:space:]]*# || "$line" =~ ^[[:space:]]*// ]]; then
320
+ current_line=$((current_line + 1))
321
+ # Allow up to 2 blank lines in import block
322
+ if [[ $current_line -gt $((last_import + 3)) ]]; then
323
+ break
324
+ fi
325
+ continue
326
+ fi
327
+
328
+ # Check if still in imports
329
+ if echo "$line" | grep -qE "$import_pattern" 2>/dev/null; then
330
+ last_import=$current_line
331
+ current_line=$((current_line + 1))
332
+ else
333
+ break
334
+ fi
335
+ done
336
+
337
+ echo $last_import
338
+ return
339
+ fi
340
+
341
+ # For functions and classes, use brace/indent counting
342
+ local brace_count=0
343
+ local indent_based=0
344
+ local base_indent=""
345
+ local found_start=0
346
+ local current_line=$start_line
347
+
348
+ # Python and Ruby use indentation
349
+ if [[ "$lang" == "python" || "$lang" == "ruby" ]]; then
350
+ indent_based=1
351
+ # Get base indentation of the definition line
352
+ local def_line
353
+ def_line=$(sed -n "${start_line}p" "$file")
354
+ base_indent=$(echo "$def_line" | sed 's/[^[:space:]].*//')
355
+ fi
356
+
357
+ while [[ $current_line -le $total_lines ]]; do
358
+ local line
359
+ line=$(sed -n "${current_line}p" "$file")
360
+
361
+ if [[ $indent_based -eq 1 ]]; then
362
+ # Skip the definition line itself
363
+ if [[ $current_line -eq $start_line ]]; then
364
+ current_line=$((current_line + 1))
365
+ continue
366
+ fi
367
+
368
+ # Skip empty lines
369
+ if [[ -z "${line// /}" ]]; then
370
+ current_line=$((current_line + 1))
371
+ continue
372
+ fi
373
+
374
+ # Get current line's indentation
375
+ local current_indent
376
+ current_indent=$(echo "$line" | sed 's/[^[:space:]].*//')
377
+
378
+ # If we've returned to base indentation (or less), we're done
379
+ if [[ ${#current_indent} -le ${#base_indent} ]]; then
380
+ echo $((current_line - 1))
381
+ return
382
+ fi
383
+ else
384
+ # Brace counting for C-like languages
385
+ local open_braces close_braces
386
+
387
+ # Count braces in line
388
+ open_braces=$(echo "$line" | tr -cd '{' | wc -c | tr -d ' ')
389
+ close_braces=$(echo "$line" | tr -cd '}' | wc -c | tr -d ' ')
390
+
391
+ brace_count=$((brace_count + open_braces - close_braces))
392
+
393
+ # Mark that we've found the opening brace
394
+ if [[ $open_braces -gt 0 ]]; then
395
+ found_start=1
396
+ fi
397
+
398
+ # If we've closed all braces after opening, we're done
399
+ if [[ $found_start -eq 1 && $brace_count -le 0 ]]; then
400
+ echo $current_line
401
+ return
402
+ fi
403
+ fi
404
+
405
+ current_line=$((current_line + 1))
406
+ done
407
+
408
+ # If we reach end of file, return last line
409
+ echo $total_lines
410
+ }
411
+
412
+ # =============================================================================
413
+ # NESTING DEPTH CALCULATION
414
+ # =============================================================================
415
+
416
+ # Calculate maximum nesting depth in a code block
417
+ # Usage: _calculate_nesting_depth "content"
418
+ _calculate_nesting_depth() {
419
+ local content="$1"
420
+ local max_depth=0
421
+ local current_depth=0
422
+
423
+ while IFS= read -r line; do
424
+ # Count opening and closing braces/indentation markers
425
+ local opens closes
426
+ opens=$(echo "$line" | tr -cd '{(' | wc -c | tr -d ' ')
427
+ closes=$(echo "$line" | tr -cd '})' | wc -c | tr -d ' ')
428
+
429
+ current_depth=$((current_depth + opens))
430
+ if [[ $current_depth -gt $max_depth ]]; then
431
+ max_depth=$current_depth
432
+ fi
433
+ current_depth=$((current_depth - closes))
434
+ if [[ $current_depth -lt 0 ]]; then
435
+ current_depth=0
436
+ fi
437
+ done <<< "$content"
438
+
439
+ echo $max_depth
440
+ }
441
+
442
+ # =============================================================================
443
+ # DEPENDENCY EXTRACTION
444
+ # =============================================================================
445
+
446
+ # Extract dependencies (function/class references) from content
447
+ # Usage: _extract_dependencies "content" "language"
448
+ _extract_dependencies() {
449
+ local content="$1"
450
+ local lang="$2"
451
+
452
+ local deps=""
453
+
454
+ case "$lang" in
455
+ python)
456
+ # Look for function calls and class instantiations
457
+ deps=$(echo "$content" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*\(' 2>/dev/null | \
458
+ sed 's/($//' | sort -u | tr '\n' ',' | sed 's/,$//')
459
+ ;;
460
+ javascript|typescript)
461
+ # Look for function calls, imports references
462
+ deps=$(echo "$content" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*\(' 2>/dev/null | \
463
+ sed 's/($//' | sort -u | tr '\n' ',' | sed 's/,$//')
464
+ ;;
465
+ go)
466
+ # Look for function calls and type references
467
+ deps=$(echo "$content" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*\(' 2>/dev/null | \
468
+ sed 's/($//' | sort -u | tr '\n' ',' | sed 's/,$//')
469
+ ;;
470
+ *)
471
+ # Generic function call detection
472
+ deps=$(echo "$content" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*\(' 2>/dev/null | \
473
+ sed 's/($//' | sort -u | tr '\n' ',' | sed 's/,$//')
474
+ ;;
475
+ esac
476
+
477
+ echo "$deps"
478
+ }
479
+
480
+ # =============================================================================
481
+ # PRIORITY SCORING
482
+ # =============================================================================
483
+
484
+ # Calculate priority score for a chunk
485
+ # Usage: _calculate_priority "content" "chunk_type" "query_keywords"
486
+ _calculate_priority() {
487
+ local content="$1"
488
+ local chunk_type="$2"
489
+ local query_keywords="${3:-}"
490
+
491
+ local score=50 # Base score
492
+
493
+ # Boost for entry points
494
+ if echo "$content" | grep -qiE '(^|\s)(main|__main__|export\s+default|module\.exports)' 2>/dev/null; then
495
+ score=$((score + 20))
496
+ fi
497
+
498
+ # Boost for public/exported items
499
+ if echo "$content" | grep -qiE '(^|\s)(public|export|pub\s+fn)' 2>/dev/null; then
500
+ score=$((score + 15))
501
+ fi
502
+
503
+ # Boost for API handlers
504
+ if echo "$content" | grep -qiE '(handler|controller|endpoint|route|api)' 2>/dev/null; then
505
+ score=$((score + 10))
506
+ fi
507
+
508
+ # Check against configured priority keywords
509
+ local IFS=','
510
+ local keywords
511
+ read -ra keywords <<< "$CHUNK_PRIORITY_KEYWORDS"
512
+ for keyword in "${keywords[@]}"; do
513
+ keyword=$(echo "$keyword" | tr -d ' ')
514
+ if [[ -n "$keyword" ]] && echo "$content" | grep -qi "$keyword" 2>/dev/null; then
515
+ score=$((score + 5))
516
+ fi
517
+ done
518
+
519
+ # Check against query-specific keywords
520
+ if [[ -n "$query_keywords" ]]; then
521
+ read -ra keywords <<< "$query_keywords"
522
+ for keyword in "${keywords[@]}"; do
523
+ keyword=$(echo "$keyword" | tr -d ' ')
524
+ if [[ -n "$keyword" ]] && echo "$content" | grep -qi "$keyword" 2>/dev/null; then
525
+ score=$((score + 10))
526
+ fi
527
+ done
528
+ fi
529
+
530
+ # Adjust based on complexity (moderate complexity is good)
531
+ local lines nesting
532
+ lines=$(echo "$content" | wc -l | tr -d ' ')
533
+ nesting=$(_calculate_nesting_depth "$content")
534
+
535
+ # Penalize very short chunks
536
+ if [[ $lines -lt 5 ]]; then
537
+ score=$((score - 10))
538
+ fi
539
+
540
+ # Penalize very complex chunks
541
+ if [[ $nesting -gt 5 ]]; then
542
+ score=$((score - 5))
543
+ fi
544
+
545
+ # Cap score between 0 and 100
546
+ if [[ $score -lt 0 ]]; then
547
+ score=0
548
+ elif [[ $score -gt 100 ]]; then
549
+ score=100
550
+ fi
551
+
552
+ echo $score
553
+ }
554
+
555
+ # =============================================================================
556
+ # CHUNK GENERATION
557
+ # =============================================================================
558
+
559
+ # Generate a unique chunk ID
560
+ # Usage: _generate_chunk_id "file_path" chunk_number
561
+ _generate_chunk_id() {
562
+ local file_path="$1"
563
+ local chunk_num="$2"
564
+
565
+ local file_hash
566
+ file_hash=$(echo -n "$file_path" | _calculate_checksum "$(cat)")
567
+
568
+ echo "chunk_${file_hash:0:8}_${chunk_num}"
569
+ }
570
+
571
+ # Create a chunk JSON object
572
+ # Usage: _create_chunk_json id content start_line end_line type priority deps checksum
573
+ _create_chunk_json() {
574
+ local chunk_id="$1"
575
+ local content="$2"
576
+ local start_line="$3"
577
+ local end_line="$4"
578
+ local chunk_type="$5"
579
+ local priority="$6"
580
+ local dependencies="$7"
581
+ local checksum="$8"
582
+
583
+ local lines token_estimate
584
+ lines=$((end_line - start_line + 1))
585
+ token_estimate=$(estimate_tokens "$content")
586
+
587
+ # Build dependencies array
588
+ local deps_array="[]"
589
+ if [[ -n "$dependencies" ]]; then
590
+ deps_array=$(echo "$dependencies" | tr ',' '\n' | jq -R . | jq -s .)
591
+ fi
592
+
593
+ jq -n \
594
+ --arg id "$chunk_id" \
595
+ --arg content "$content" \
596
+ --argjson start "$start_line" \
597
+ --argjson end "$end_line" \
598
+ --arg type "$chunk_type" \
599
+ --argjson lines "$lines" \
600
+ --argjson tokens "$token_estimate" \
601
+ --argjson priority "$priority" \
602
+ --argjson deps "$deps_array" \
603
+ --arg checksum "$checksum" \
604
+ '{
605
+ id: $id,
606
+ content: $content,
607
+ metadata: {
608
+ start_line: $start,
609
+ end_line: $end,
610
+ type: $type,
611
+ lines: $lines,
612
+ estimated_tokens: $tokens,
613
+ priority: $priority,
614
+ dependencies: $deps,
615
+ checksum: $checksum
616
+ }
617
+ }'
618
+ }
619
+
620
+ # =============================================================================
621
+ # MAIN CHUNKING FUNCTION
622
+ # =============================================================================
623
+
624
+ # Split a file into semantic chunks
625
+ # Usage: chunk_file_semantically "file_path" [query_keywords]
626
+ # Output: JSON array of chunks
627
+ chunk_file_semantically() {
628
+ local file_path="$1"
629
+ local query_keywords="${2:-}"
630
+
631
+ # Basic path validation (bash 3.2 compatible)
632
+ if [[ -z "$file_path" ]]; then
633
+ log_error "Empty file path provided"
634
+ echo "[]"
635
+ return 1
636
+ fi
637
+
638
+ # Check for path traversal
639
+ if [[ "$file_path" == *".."* ]]; then
640
+ log_error "Path traversal detected in: $file_path"
641
+ echo "[]"
642
+ return 1
643
+ fi
644
+
645
+ if [[ ! -f "$file_path" ]]; then
646
+ log_error "File not found: $file_path"
647
+ echo "[]"
648
+ return 1
649
+ fi
650
+
651
+ # Initialize session
652
+ _CHUNK_CURRENT_FILE="$file_path"
653
+ _CHUNK_COUNTER=0
654
+ _CHUNK_SESSION_ID=$(date +%Y%m%d_%H%M%S)_$$
655
+
656
+ local lang
657
+ lang=$(_detect_language "$file_path")
658
+ log_debug "Detected language: $lang for file: $file_path"
659
+
660
+ # Find semantic boundaries
661
+ local boundaries
662
+ boundaries=$(_find_semantic_boundaries "$file_path" "$lang")
663
+
664
+ if [[ -z "$boundaries" ]]; then
665
+ log_warn "No semantic boundaries found, chunking by token limit"
666
+ _chunk_by_tokens "$file_path" "$query_keywords"
667
+ return
668
+ fi
669
+
670
+ local total_lines
671
+ total_lines=$(_count_lines "$file_path")
672
+
673
+ local chunks="[]"
674
+ local prev_end=0
675
+ local max_chars=$((CHUNK_MAX_TOKENS * 4)) # Approximate chars from tokens
676
+
677
+ # Process each boundary
678
+ while IFS= read -r boundary; do
679
+ [[ -z "$boundary" ]] && continue
680
+
681
+ local line_num unit_type
682
+ line_num=$(echo "$boundary" | cut -d: -f1)
683
+ unit_type=$(echo "$boundary" | cut -d: -f2)
684
+
685
+ # Handle gap before this boundary (if significant)
686
+ if [[ $((line_num - 1)) -gt $((prev_end + CHUNK_OVERLAP_LINES)) ]]; then
687
+ local gap_start=$((prev_end + 1))
688
+ local gap_end=$((line_num - 1))
689
+
690
+ # Only create chunk if gap has content
691
+ local gap_content
692
+ gap_content=$(_extract_lines "$file_path" "$gap_start" "$gap_end")
693
+
694
+ if [[ -n "${gap_content// /}" ]]; then
695
+ local gap_tokens
696
+ gap_tokens=$(estimate_tokens "$gap_content")
697
+
698
+ if [[ $gap_tokens -gt 10 ]]; then
699
+ _CHUNK_COUNTER=$((_CHUNK_COUNTER + 1))
700
+ local gap_chunk
701
+ gap_chunk=$(_create_chunk_for_content "$file_path" "$gap_content" "$gap_start" "$gap_end" "gap" "$lang" "$query_keywords")
702
+ chunks=$(echo "$chunks" | jq --argjson chunk "$gap_chunk" '. + [$chunk]')
703
+ fi
704
+ fi
705
+ fi
706
+
707
+ # Find end of this semantic unit
708
+ local unit_end
709
+ unit_end=$(_find_unit_end "$file_path" "$line_num" "$unit_type" "$lang")
710
+
711
+ # Extract content with overlap
712
+ local overlap_start=$((line_num - CHUNK_OVERLAP_LINES))
713
+ if [[ $overlap_start -lt 1 ]]; then
714
+ overlap_start=1
715
+ fi
716
+
717
+ local content
718
+ content=$(_extract_lines "$file_path" "$overlap_start" "$unit_end")
719
+
720
+ # Check if chunk exceeds token limit
721
+ local token_count
722
+ token_count=$(estimate_tokens "$content")
723
+
724
+ if [[ $token_count -gt $CHUNK_MAX_TOKENS ]]; then
725
+ # Split large unit into sub-chunks
726
+ local sub_chunks
727
+ sub_chunks=$(_split_large_unit "$file_path" "$overlap_start" "$unit_end" "$unit_type" "$lang" "$query_keywords")
728
+ chunks=$(echo "$chunks" | jq --argjson sub "$sub_chunks" '. + $sub')
729
+ else
730
+ _CHUNK_COUNTER=$((_CHUNK_COUNTER + 1))
731
+ local chunk
732
+ chunk=$(_create_chunk_for_content "$file_path" "$content" "$overlap_start" "$unit_end" "$unit_type" "$lang" "$query_keywords")
733
+ chunks=$(echo "$chunks" | jq --argjson chunk "$chunk" '. + [$chunk]')
734
+ fi
735
+
736
+ prev_end=$unit_end
737
+ done <<< "$boundaries"
738
+
739
+ # Handle remaining content after last boundary
740
+ if [[ $prev_end -lt $total_lines ]]; then
741
+ local remaining_start=$((prev_end + 1))
742
+ local remaining_content
743
+ remaining_content=$(_extract_lines "$file_path" "$remaining_start" "$total_lines")
744
+
745
+ if [[ -n "${remaining_content// /}" ]]; then
746
+ local remaining_tokens
747
+ remaining_tokens=$(estimate_tokens "$remaining_content")
748
+
749
+ if [[ $remaining_tokens -gt 10 ]]; then
750
+ _CHUNK_COUNTER=$((_CHUNK_COUNTER + 1))
751
+ local remaining_chunk
752
+ remaining_chunk=$(_create_chunk_for_content "$file_path" "$remaining_content" "$remaining_start" "$total_lines" "trailing" "$lang" "$query_keywords")
753
+ chunks=$(echo "$chunks" | jq --argjson chunk "$remaining_chunk" '. + [$chunk]')
754
+ fi
755
+ fi
756
+ fi
757
+
758
+ # Save chunks to session file
759
+ local session_file="$CHUNK_TEMP_DIR/${_CHUNK_SESSION_ID}_chunks.json"
760
+ echo "$chunks" > "$session_file"
761
+ chmod 600 "$session_file" 2>/dev/null || true
762
+
763
+ log_info "Created $_CHUNK_COUNTER chunks for: $file_path"
764
+ echo "$chunks"
765
+ }
766
+
767
+ # Helper to create chunk for extracted content
768
+ # Usage: _create_chunk_for_content file content start end type lang query_keywords
769
+ _create_chunk_for_content() {
770
+ local file_path="$1"
771
+ local content="$2"
772
+ local start_line="$3"
773
+ local end_line="$4"
774
+ local chunk_type="$5"
775
+ local lang="$6"
776
+ local query_keywords="$7"
777
+
778
+ local chunk_id priority deps checksum
779
+ chunk_id=$(_generate_chunk_id "$file_path" "$_CHUNK_COUNTER")
780
+ priority=$(_calculate_priority "$content" "$chunk_type" "$query_keywords")
781
+ deps=$(_extract_dependencies "$content" "$lang")
782
+ checksum=$(_calculate_checksum "$content")
783
+
784
+ _create_chunk_json "$chunk_id" "$content" "$start_line" "$end_line" "$chunk_type" "$priority" "$deps" "$checksum"
785
+ }
786
+
787
+ # Split a large semantic unit into smaller chunks
788
+ # Usage: _split_large_unit file start end type lang query_keywords
789
+ _split_large_unit() {
790
+ local file_path="$1"
791
+ local start_line="$2"
792
+ local end_line="$3"
793
+ local unit_type="$4"
794
+ local lang="$5"
795
+ local query_keywords="$6"
796
+
797
+ local chunks="[]"
798
+ local max_lines=$((CHUNK_MAX_TOKENS / 10)) # Rough estimate: 10 tokens per line
799
+ local current_start=$start_line
800
+
801
+ while [[ $current_start -le $end_line ]]; do
802
+ local current_end=$((current_start + max_lines - 1))
803
+ if [[ $current_end -gt $end_line ]]; then
804
+ current_end=$end_line
805
+ fi
806
+
807
+ # Add overlap for non-first chunks
808
+ local overlap_start=$current_start
809
+ if [[ $current_start -gt $start_line ]]; then
810
+ overlap_start=$((current_start - CHUNK_OVERLAP_LINES))
811
+ if [[ $overlap_start -lt $start_line ]]; then
812
+ overlap_start=$start_line
813
+ fi
814
+ fi
815
+
816
+ local content
817
+ content=$(_extract_lines "$file_path" "$overlap_start" "$current_end")
818
+
819
+ _CHUNK_COUNTER=$((_CHUNK_COUNTER + 1))
820
+ local chunk
821
+ chunk=$(_create_chunk_for_content "$file_path" "$content" "$overlap_start" "$current_end" "${unit_type}_part" "$lang" "$query_keywords")
822
+ chunks=$(echo "$chunks" | jq --argjson chunk "$chunk" '. + [$chunk]')
823
+
824
+ current_start=$((current_end + 1))
825
+ done
826
+
827
+ echo "$chunks"
828
+ }
829
+
830
+ # Fallback chunking by token limit when no semantic boundaries found
831
+ # Usage: _chunk_by_tokens file query_keywords
832
+ _chunk_by_tokens() {
833
+ local file_path="$1"
834
+ local query_keywords="$2"
835
+
836
+ local lang
837
+ lang=$(_detect_language "$file_path")
838
+
839
+ local total_lines
840
+ total_lines=$(_count_lines "$file_path")
841
+
842
+ local chunks="[]"
843
+ local max_lines=$((CHUNK_MAX_TOKENS / 10))
844
+ local current_start=1
845
+
846
+ while [[ $current_start -le $total_lines ]]; do
847
+ local current_end=$((current_start + max_lines - 1))
848
+ if [[ $current_end -gt $total_lines ]]; then
849
+ current_end=$total_lines
850
+ fi
851
+
852
+ # Add overlap for non-first chunks
853
+ local overlap_start=$current_start
854
+ if [[ $current_start -gt 1 ]]; then
855
+ overlap_start=$((current_start - CHUNK_OVERLAP_LINES))
856
+ if [[ $overlap_start -lt 1 ]]; then
857
+ overlap_start=1
858
+ fi
859
+ fi
860
+
861
+ local content
862
+ content=$(_extract_lines "$file_path" "$overlap_start" "$current_end")
863
+
864
+ _CHUNK_COUNTER=$((_CHUNK_COUNTER + 1))
865
+ local chunk
866
+ chunk=$(_create_chunk_for_content "$file_path" "$content" "$overlap_start" "$current_end" "token_based" "$lang" "$query_keywords")
867
+ chunks=$(echo "$chunks" | jq --argjson chunk "$chunk" '. + [$chunk]')
868
+
869
+ current_start=$((current_end + 1))
870
+ done
871
+
872
+ # Save to session file
873
+ local session_file="$CHUNK_TEMP_DIR/${_CHUNK_SESSION_ID}_chunks.json"
874
+ echo "$chunks" > "$session_file"
875
+ chmod 600 "$session_file" 2>/dev/null || true
876
+
877
+ log_info "Created $_CHUNK_COUNTER token-based chunks for: $file_path"
878
+ echo "$chunks"
879
+ }
880
+