IncludeCPP 3.7.3__py3-none-any.whl

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.

Potentially problematic release.


This version of IncludeCPP might be problematic. Click here for more details.

Files changed (49) hide show
  1. includecpp/__init__.py +59 -0
  2. includecpp/__init__.pyi +255 -0
  3. includecpp/__main__.py +4 -0
  4. includecpp/cli/__init__.py +4 -0
  5. includecpp/cli/commands.py +8270 -0
  6. includecpp/cli/config_parser.py +127 -0
  7. includecpp/core/__init__.py +19 -0
  8. includecpp/core/ai_integration.py +2132 -0
  9. includecpp/core/build_manager.py +2416 -0
  10. includecpp/core/cpp_api.py +376 -0
  11. includecpp/core/cpp_api.pyi +95 -0
  12. includecpp/core/cppy_converter.py +3448 -0
  13. includecpp/core/cssl/CSSL_DOCUMENTATION.md +2075 -0
  14. includecpp/core/cssl/__init__.py +42 -0
  15. includecpp/core/cssl/cssl_builtins.py +2271 -0
  16. includecpp/core/cssl/cssl_builtins.pyi +1393 -0
  17. includecpp/core/cssl/cssl_events.py +621 -0
  18. includecpp/core/cssl/cssl_modules.py +2803 -0
  19. includecpp/core/cssl/cssl_parser.py +2575 -0
  20. includecpp/core/cssl/cssl_runtime.py +3051 -0
  21. includecpp/core/cssl/cssl_syntax.py +488 -0
  22. includecpp/core/cssl/cssl_types.py +1512 -0
  23. includecpp/core/cssl_bridge.py +882 -0
  24. includecpp/core/cssl_bridge.pyi +488 -0
  25. includecpp/core/error_catalog.py +802 -0
  26. includecpp/core/error_formatter.py +1016 -0
  27. includecpp/core/exceptions.py +97 -0
  28. includecpp/core/path_discovery.py +77 -0
  29. includecpp/core/project_ui.py +3370 -0
  30. includecpp/core/settings_ui.py +326 -0
  31. includecpp/generator/__init__.py +1 -0
  32. includecpp/generator/parser.cpp +1903 -0
  33. includecpp/generator/parser.h +281 -0
  34. includecpp/generator/type_resolver.cpp +363 -0
  35. includecpp/generator/type_resolver.h +68 -0
  36. includecpp/py.typed +0 -0
  37. includecpp/templates/cpp.proj.template +18 -0
  38. includecpp/vscode/__init__.py +1 -0
  39. includecpp/vscode/cssl/__init__.py +1 -0
  40. includecpp/vscode/cssl/language-configuration.json +38 -0
  41. includecpp/vscode/cssl/package.json +50 -0
  42. includecpp/vscode/cssl/snippets/cssl.snippets.json +1080 -0
  43. includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +341 -0
  44. includecpp-3.7.3.dist-info/METADATA +1076 -0
  45. includecpp-3.7.3.dist-info/RECORD +49 -0
  46. includecpp-3.7.3.dist-info/WHEEL +5 -0
  47. includecpp-3.7.3.dist-info/entry_points.txt +2 -0
  48. includecpp-3.7.3.dist-info/licenses/LICENSE +21 -0
  49. includecpp-3.7.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2132 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import stat
5
+ import requests
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ from typing import Optional, Dict, List, Tuple, Any
9
+
10
+
11
+ def _supports_unicode():
12
+ """Check if terminal supports Unicode output."""
13
+ if sys.platform == 'win32':
14
+ try:
15
+ '✓✗❌'.encode(sys.stdout.encoding or 'utf-8')
16
+ return True
17
+ except (UnicodeEncodeError, LookupError, AttributeError):
18
+ return False
19
+ return True
20
+
21
+
22
+ _UNICODE_OK = _supports_unicode()
23
+
24
+ # Unicode symbols with ASCII fallbacks
25
+ SYM_CHECK = '✓' if _UNICODE_OK else '[OK]'
26
+ SYM_CROSS = '✗' if _UNICODE_OK else '[X]'
27
+ SYM_ERROR = '❌' if _UNICODE_OK else '[ERR]'
28
+ SYM_ARROW = '→' if _UNICODE_OK else '->'
29
+ SYM_BULLET = '•' if _UNICODE_OK else '*'
30
+
31
+ MODELS = {
32
+ 'gpt-3.5-turbo': {'context': 16385, 'endpoint': 'gpt-3.5-turbo'},
33
+ 'gpt-4-turbo': {'context': 128000, 'endpoint': 'gpt-4-turbo'},
34
+ 'gpt-4o': {'context': 128000, 'endpoint': 'gpt-4o'},
35
+ 'gpt-5': {'context': 256000, 'endpoint': 'gpt-5'},
36
+ 'gpt-5-nano': {'context': 32000, 'endpoint': 'gpt-5-nano'},
37
+ }
38
+
39
+ DEFAULT_MODEL = 'gpt-5'
40
+ DEFAULT_DAILY_LIMIT = 220000
41
+
42
+ OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions'
43
+ BRAVE_SEARCH_URL = 'https://api.search.brave.com/res/v1/web/search'
44
+
45
+ CONTEXT_LIMITS = {
46
+ 'standard': 3000,
47
+ 'think': 5000,
48
+ 'think2': 10000,
49
+ 'think3': 25000,
50
+ }
51
+
52
+ QUESTION_PROMPT_ADDITION = '''
53
+ INTERACTIVE MODE:
54
+ If you need clarification before proceeding, you may ask ONE question.
55
+ Format: ASK_USER: <your question>
56
+ OPTIONS: <option1> | <option2> | <option3> (optional, max 4 options)
57
+
58
+ Only ask if genuinely needed. If task is clear, proceed without asking.'''
59
+
60
+ # AI Generate Tools
61
+ GENERATE_TOOLS = {
62
+ 'READ_FILE': {
63
+ 'desc': 'Read file contents',
64
+ 'format': 'TOOL: READ_FILE\nPATH: <absolute_or_relative_path>',
65
+ },
66
+ 'WRITE_FILE': {
67
+ 'desc': 'Create or overwrite a file',
68
+ 'format': 'TOOL: WRITE_FILE\nPATH: <path>\n```<lang>\n<content>\n```',
69
+ },
70
+ 'EDIT_FILE': {
71
+ 'desc': 'Edit existing file with changes',
72
+ 'format': 'TOOL: EDIT_FILE\nPATH: <path>\nCHANGES:\n- <desc>\n```<lang>\n<full_content>\n```',
73
+ },
74
+ 'DELETE_FILE': {
75
+ 'desc': 'Delete a file',
76
+ 'format': 'TOOL: DELETE_FILE\nPATH: <path>',
77
+ },
78
+ 'CREATE_FOLDER': {
79
+ 'desc': 'Create directory (with parents)',
80
+ 'format': 'TOOL: CREATE_FOLDER\nPATH: <path>',
81
+ },
82
+ 'LIST_FOLDER': {
83
+ 'desc': 'List directory contents',
84
+ 'format': 'TOOL: LIST_FOLDER\nPATH: <path>',
85
+ },
86
+ 'SEARCH_FILES': {
87
+ 'desc': 'Find files by glob pattern',
88
+ 'format': 'TOOL: SEARCH_FILES\nPATTERN: <glob_pattern>\nPATH: <base_path>',
89
+ },
90
+ 'GREP': {
91
+ 'desc': 'Search file contents with regex',
92
+ 'format': 'TOOL: GREP\nPATTERN: <regex>\nPATH: <path_or_glob>',
93
+ },
94
+ 'RUN_CMD': {
95
+ 'desc': 'Execute system command',
96
+ 'format': 'TOOL: RUN_CMD\nCMD: <command>',
97
+ },
98
+ 'INCLUDECPP_CMD': {
99
+ 'desc': 'Run includecpp CLI command',
100
+ 'format': 'TOOL: INCLUDECPP_CMD\nCMD: <subcommand> (e.g., plugin mymod, rebuild --fast)',
101
+ },
102
+ }
103
+
104
+ SYSTEM_PROMPT_GENERATE = '''You are an expert AI assistant for IncludeCPP projects.
105
+
106
+ You have access to tools for file operations and command execution.
107
+
108
+ AVAILABLE TOOLS:
109
+ {tools_list}
110
+
111
+ TOOL USAGE:
112
+ - Call tools by outputting the exact format shown above
113
+ - Wait for tool results before continuing
114
+ - You can call multiple tools in sequence
115
+ - Always use absolute paths or paths relative to project root
116
+
117
+ RESPONSE FORMAT:
118
+ 1. If you need to use tools, output tool calls first
119
+ 2. After all tools complete, provide final summary
120
+ 3. For file changes, use EDIT_FILE with full content and change descriptions
121
+
122
+ CONTEXT:
123
+ - Project root: {project_root}
124
+ - System: {system_info}
125
+ - IncludeCPP is always available via `includecpp` command
126
+
127
+ {includecpp_context}
128
+
129
+ RULES:
130
+ 1. All C++ code MUST be in namespace includecpp {{ }}
131
+ 2. Use EDIT_FILE for modifications (shows diff)
132
+ 3. Use WRITE_FILE only for new files
133
+ 4. Always confirm destructive operations
134
+ 5. Keep responses professional and concise
135
+ '''
136
+
137
+ SYSTEM_PROMPT_GENERATE_PLAN = '''You are planning and executing a task for an IncludeCPP project.
138
+
139
+ WORKFLOW (execute ALL phases immediately):
140
+
141
+ PHASE 1: RESEARCH - Use tools NOW
142
+ Call these tools immediately to gather information:
143
+ - SEARCH_FILES to find relevant files
144
+ - GREP to search file contents
145
+ - READ_FILE to examine code
146
+ - LIST_FOLDER to understand structure
147
+
148
+ PHASE 2: PLAN - Brief summary
149
+ After research, output:
150
+ PLAN:
151
+ 1. Step one
152
+ 2. Step two
153
+
154
+ PHASE 3: EXECUTE - Complete the task NOW
155
+ Execute all required tool calls to complete the task.
156
+ DO NOT wait for user confirmation - execute immediately.
157
+ DO NOT ask "Please confirm" or "Should I proceed" - just do it.
158
+
159
+ {base_prompt}
160
+ '''
161
+
162
+ SYSTEM_PROMPT_NEW_MODULE = '''You are creating a new C++ module for IncludeCPP.
163
+
164
+ Module name: {module_name}
165
+ Description: {description}
166
+
167
+ CREATE THESE FILES:
168
+ 1. include/{module_name}.cpp - Main source with namespace includecpp {{ }}
169
+ 2. include/{module_name}.h - Header file with declarations
170
+ 3. plugins/{module_name}.cp - Plugin definition
171
+
172
+ CRITICAL .CP FILE FORMAT:
173
+ - ALWAYS use SOURCE() && HEADER() together on the SAME LINE when you create both .cpp and .h files
174
+ - Example: SOURCE(include/{module_name}.cpp) && HEADER(include/{module_name}.h) {module_name}
175
+
176
+ REQUIREMENTS:
177
+ - All code in namespace includecpp {{ }}
178
+ - Include practical, working implementations
179
+ - Add appropriate FUNC(), CLASS(), METHOD() in .cp file
180
+ - Follow existing project patterns
181
+
182
+ OUTPUT FORMAT - USE THIS EXACT FORMAT FOR EACH FILE:
183
+
184
+ TOOL: WRITE_FILE
185
+ PATH: include/{module_name}.h
186
+ ```cpp
187
+ #pragma once
188
+ namespace includecpp {{
189
+ // declarations
190
+ }}
191
+ ```
192
+
193
+ TOOL: WRITE_FILE
194
+ PATH: include/{module_name}.cpp
195
+ ```cpp
196
+ #include "{module_name}.h"
197
+ namespace includecpp {{
198
+ // implementations
199
+ }}
200
+ ```
201
+
202
+ TOOL: WRITE_FILE
203
+ PATH: plugins/{module_name}.cp
204
+ ```
205
+ SOURCE(include/{module_name}.cpp) && HEADER(include/{module_name}.h) {module_name}
206
+ PUBLIC(
207
+ {module_name} FUNC(...)
208
+ {module_name} CLASS(...) {{ ... }}
209
+ )
210
+ ```
211
+
212
+ Create all required files now using the format above. ALWAYS include && HEADER() in the .cp file!
213
+ '''
214
+
215
+ INCLUDECPP_CONTEXT = '''
216
+ CRITICAL KNOWLEDGE FOR INCLUDECPP:
217
+
218
+ 1. NAMESPACE REQUIREMENT:
219
+ ALL C++ code MUST be inside `namespace includecpp { }` - this is REQUIRED, not optional.
220
+
221
+ 2. PLUGIN FILE (.cp) FORMAT:
222
+ IMPORTANT: Use && to combine SOURCE and HEADER on the SAME LINE!
223
+
224
+ With header file (REQUIRED when you create both .cpp and .h):
225
+ SOURCE(include/math.cpp) && HEADER(include/math.h) math
226
+
227
+ Without header (source only):
228
+ SOURCE(include/math.cpp) math
229
+
230
+ FULL EXAMPLE:
231
+ SOURCE(include/mymodule.cpp) && HEADER(include/mymodule.h) mymodule
232
+ PUBLIC(
233
+ mymodule CLASS(MyClass) {
234
+ CONSTRUCTOR()
235
+ CONSTRUCTOR(int, double)
236
+ METHOD(foo)
237
+ METHOD_CONST(bar, const std::string&)
238
+ FIELD(x)
239
+ }
240
+ mymodule FUNC(standalone_function)
241
+ mymodule TEMPLATE_FUNC(generic_func) TYPES(int, float, double)
242
+ mymodule STRUCT(Point) { FIELD(x) FIELD(y) }
243
+ )
244
+
245
+ 3. HEADER RULES:
246
+ - If you create a .h header file, you MUST include HEADER() in the .cp file
247
+ - SOURCE() and HEADER() MUST be on the SAME LINE with && between them
248
+ - Without && HEADER(), the build system won't find the header
249
+
250
+ 4. BUILD OUTPUT:
251
+ ~/.includecpp/builds/ (Windows: %APPDATA%/IncludeCPP/)
252
+
253
+ 5. COMMON ERRORS AND FIXES:
254
+ * "undefined reference" -> Code not in namespace includecpp { }, or missing FUNC() in .cp
255
+ * "no matching function" -> Wrong parameter types in .cp METHOD() definition
256
+ * "template instantiation" -> Missing TEMPLATE_FUNC() with TYPES() in .cp file
257
+ * "namespace includecpp not found" -> Source file missing namespace wrapper
258
+ * "no member named X" -> Method not in class public section, or missing METHOD() in .cp
259
+ * "header not found" -> Missing HEADER() directive in .cp file, or wrong path
260
+
261
+ 6. FIELD DECLARATIONS:
262
+ FIELD(name) - only the field name is needed, not the type.
263
+ Comma-separated fields like `double x, y, z;` are parsed as separate fields.
264
+ '''
265
+
266
+ SYSTEM_PROMPT_OPTIMIZE = '''You are a C++ expert specializing in pybind11 bindings and the IncludeCPP framework.
267
+
268
+ IMPORTANT: Be conservative. Only suggest safe optimizations that will NOT break compilation.
269
+
270
+ Rules:
271
+ 1. NEVER remove existing functions or code blocks
272
+ 2. Preserve all public API signatures EXACTLY
273
+ 3. NEVER change function parameters or return types
274
+ 4. Only make changes that are 100% safe and backwards compatible
275
+ 5. Maintain namespace includecpp structure
276
+ 6. Do not add comments, docstrings, or explanatory text
277
+ 7. Do not add AI-typical output markers or annotations
278
+ 8. If unsure about a change, DO NOT make it
279
+
280
+ Safe optimizations:
281
+ - Adding const where appropriate
282
+ - Using reserve() before loops
283
+ - Replacing raw loops with STL algorithms (if equivalent)
284
+ - Adding noexcept to functions that don't throw
285
+ - Using move semantics for local variables
286
+
287
+ AVOID:
288
+ - Changing function signatures
289
+ - Removing code that might be used elsewhere
290
+ - Complex refactoring
291
+ - Template changes
292
+
293
+ If a change would alter existing functionality, you MUST prefix with:
294
+ CONFIRM_REQUIRED: <brief description>
295
+
296
+ Output format for each file:
297
+ FILE: <relative_path>
298
+ CHANGES:
299
+ - Line X: <what changed> - <why>
300
+ - Line Y-Z: <what changed> - <why>
301
+ ```cpp
302
+ <complete file content with no comments about changes>
303
+ ```
304
+
305
+ Only output files that need changes. If no safe optimizations found, respond with: NO_CHANGES_NEEDED'''
306
+
307
+ SYSTEM_PROMPT_FIX = '''You are a C++ expert analyzing code for the IncludeCPP framework.
308
+
309
+ Context:
310
+ - IncludeCPP generates pybind11 bindings from .cp plugin files
311
+ - SOURCE() and HEADER() directives link to C++ source files
312
+ - All exposed code MUST be inside namespace includecpp { ... }
313
+ - Modules compile to .pyd (Windows) or .so (Linux/Mac) Python extensions
314
+
315
+ CRITICAL CHECKS (in order):
316
+ 1. SYNTAX ERRORS: Missing semicolons, unmatched braces, invalid tokens
317
+ 2. NAMESPACE: All classes/functions MUST be inside namespace includecpp { }
318
+ 3. TYPE ERRORS: Undefined types, wrong return types, parameter mismatches
319
+ 4. MEMORY: Dangling pointers, leaks, uninitialized variables
320
+ 5. PYBIND11: Return policies, holder types, opaque types
321
+
322
+ Rules:
323
+ 1. Never remove functions or change public API signatures
324
+ 2. Fix ALL syntax errors first - code must compile
325
+ 3. Ensure code is inside namespace includecpp { }
326
+ 4. Do not add comments or docstrings
327
+ 5. Preserve exact logic and behavior
328
+
329
+ If removal is necessary, prefix with:
330
+ CONFIRM_REQUIRED: <description>
331
+
332
+ Output format for each file:
333
+ FILE: <path>
334
+ CHANGES:
335
+ - Line X: <what changed> - <why>
336
+ - Line Y-Z: <what changed> - <why>
337
+ ```cpp
338
+ <complete fixed file content>
339
+ ```
340
+
341
+ Only output files that need changes. If no issues found, respond with: NO_ISSUES_FOUND'''
342
+
343
+ SYSTEM_PROMPT_BUILD_ERROR = '''You are analyzing a C++ build error in an IncludeCPP project.
344
+
345
+ IncludeCPP Architecture:
346
+ - Plugin files (.cp) define bindings: SOURCE(file.cpp), HEADER(file.h), CLASS(), METHOD(), FUNC()
347
+ - SOURCE() links C++ implementation files
348
+ - HEADER() links header files for declarations
349
+ - All exposed code MUST be in namespace includecpp { }
350
+ - Build generates pybind11 bindings in a temp bindings.cpp file
351
+ - Compiles to .pyd (Windows) or .so (Linux/Mac) Python extensions
352
+
353
+ Common Error Categories:
354
+ 1. USER CODE ERROR: Syntax errors, missing includes, type mismatches in user C++ files
355
+ 2. PLUGIN FILE ERROR: Wrong signatures in .cp file, missing METHOD(), wrong parameter types
356
+ 3. NAMESPACE ERROR: Code not inside namespace includecpp { }
357
+ 4. BINDING ERROR: pybind11 type conversion issues, missing opaque declarations
358
+ 5. INCLUDECPP BUG: Framework generated incorrect bindings (rare, report at github.com/liliassg/IncludeCPP/issues)
359
+
360
+ Analyze the error and provide:
361
+
362
+ ERROR TYPE: [USER CODE | PLUGIN FILE | NAMESPACE | BINDING | INCLUDECPP BUG]
363
+
364
+ ROOT CAUSE:
365
+ <one line explanation>
366
+
367
+ FIX:
368
+ File: <exact file path>
369
+ Line: <line number if known>
370
+ ```cpp
371
+ <exact code fix>
372
+ ```
373
+
374
+ WHY:
375
+ <brief explanation of what was wrong>
376
+
377
+ PREVENTION:
378
+ <one line tip to avoid this in future>'''
379
+
380
+ SYSTEM_PROMPT_AGENT = '''You are a C++ expert working on an IncludeCPP project.
381
+
382
+ Context:
383
+ - IncludeCPP generates pybind11 bindings
384
+ - Code must be in namespace includecpp
385
+ - Focus on the specific task requested
386
+
387
+ Rules:
388
+ 1. Never remove existing functions unless explicitly asked
389
+ 2. Preserve public API signatures
390
+ 3. No comments, docstrings, or explanatory markers
391
+ 4. Output complete file contents
392
+
393
+ If removal needed, prefix with:
394
+ CONFIRM_REQUIRED: <description>
395
+
396
+ Output format:
397
+ FILE: <path>
398
+ ```cpp
399
+ <complete file content>
400
+ ```'''
401
+
402
+ SYSTEM_PROMPT_ASK = '''You are an expert assistant for IncludeCPP projects.
403
+
404
+ You have access to:
405
+ - IncludeCPP documentation (README)
406
+ - Project source files (.cpp, .h, .py)
407
+ - Plugin definitions (.cp files)
408
+ - Build system knowledge
409
+
410
+ Answer questions precisely and concisely. Reference specific files and line numbers when relevant.
411
+
412
+ Response format:
413
+ - Direct answer first
414
+ - File references: path:line if applicable
415
+ - Code examples if helpful (brief)
416
+
417
+ Do not explain IncludeCPP basics unless asked. Assume user knows the system.
418
+ Max 200 words unless complexity requires more.'''
419
+
420
+ SYSTEM_PROMPT_EDIT = '''You are a C++ expert editing code in an IncludeCPP project.
421
+
422
+ CRITICAL BEHAVIOR - READ CAREFULLY:
423
+ 1. ALWAYS output FILE: blocks with code changes
424
+ 2. NEVER say "no changes needed" - the user wants changes, make them
425
+ 3. When task says "add X" - you MUST add X
426
+ 4. When task says "improve/optimize/refactor" - you MUST make improvements
427
+ 5. BE ASSERTIVE - implement what is requested, do not refuse
428
+
429
+ Context:
430
+ - IncludeCPP generates pybind11 bindings from .cp plugin files
431
+ - All exposed code must be in namespace includecpp { }
432
+ - Focus on the specific edit task requested
433
+
434
+ Rules:
435
+ 1. ALWAYS make changes when user requests them
436
+ 2. Preserve existing functionality unless told otherwise
437
+ 3. No comments, docstrings, or explanatory text
438
+ 4. Maintain exact coding style
439
+ 5. Output COMPLETE file content, not just changed parts
440
+
441
+ If the edit would break existing functionality:
442
+ CONFIRM_REQUIRED: <description>
443
+
444
+ OUTPUT FORMAT (MANDATORY - YOU MUST USE THIS FORMAT):
445
+ FILE: <exact_path>
446
+ CHANGES:
447
+ - <what changed>
448
+ ```cpp
449
+ <complete file content here>
450
+ ```
451
+
452
+ REQUIREMENTS:
453
+ - Start with FILE: on its own line
454
+ - List changes with - bullet points
455
+ - Use ```cpp code block for the full content
456
+ - You MUST output at least one FILE: block
457
+ - If multiple files need changes, output multiple FILE: blocks
458
+
459
+ DO NOT explain why you cannot make changes. MAKE the changes.'''
460
+
461
+ SYSTEM_PROMPT_AUTO_FIX = '''You are an AI that automatically fixes C++ build errors in IncludeCPP projects.
462
+
463
+ IncludeCPP Architecture:
464
+ - Plugin files (.cp) define bindings: SOURCE(file.cpp), HEADER(file.h), CLASS(), METHOD(), FUNC(), TEMPLATE_FUNC()
465
+ - All exposed code MUST be in namespace includecpp { }
466
+ - Build generates pybind11 bindings and compiles to .pyd/.so
467
+
468
+ You have access to CLI commands:
469
+ - includecpp plugin <modulename>: Regenerate .cp file from source analysis
470
+ - includecpp rebuild --fast <modulename>: Rebuild specific module
471
+ - includecpp rebuild --clean: Full clean rebuild
472
+
473
+ IMPORTANT: You must FIX the error. Analyze the error, determine the fix, and output actionable changes.
474
+
475
+ Output format:
476
+
477
+ ACTION: FILE_CHANGE
478
+ FILE: <exact path>
479
+ ```cpp
480
+ <complete fixed file content>
481
+ ```
482
+
483
+ ACTION: CLI_COMMAND
484
+ COMMAND: <command to run>
485
+ REASON: <why this command is needed>
486
+
487
+ Multiple actions can be output. Process in order:
488
+ 1. FILE_CHANGE actions first (fix source files)
489
+ 2. CLI_COMMAND actions after (regenerate .cp or other commands)
490
+
491
+ Common fixes:
492
+ - Syntax error -> Fix the syntax in source file
493
+ - Missing namespace -> Wrap code in namespace includecpp { }
494
+ - .cp out of sync -> Run: includecpp plugin <modulename>
495
+ - Template not detected -> Run: includecpp plugin <modulename> (regenerates with TEMPLATE_FUNC)
496
+ - Missing include -> Add #include directive
497
+
498
+ If the error cannot be fixed automatically, output:
499
+ CANNOT_FIX: <explanation>
500
+
501
+ Do NOT add comments or docstrings. Output complete file contents for FILE_CHANGE.'''
502
+
503
+ SYSTEM_PROMPT_THINK3_PLAN = '''You are an expert C++ and Python developer analyzing a build error in an IncludeCPP project.
504
+
505
+ PHASE 1: ANALYSIS (take your time)
506
+ - Analyze the exact error message and location
507
+ - Identify the root cause (syntax, linker, template, namespace, etc.)
508
+ - Consider all possible fixes
509
+ - Review relevant documentation and patterns
510
+
511
+ PHASE 2: RESEARCH FINDINGS
512
+ You have access to web research results. Use them to:
513
+ - Find similar issues and solutions
514
+ - Identify best practices for the specific error
515
+ - Check if this is a known issue with pybind11 or compilers
516
+
517
+ PHASE 3: PLANNING
518
+ Create a structured plan:
519
+
520
+ PLAN:
521
+ 1. Primary Fix: <main solution>
522
+ 2. Alternative: <backup approach if primary fails>
523
+ 3. Files to Modify: <list of files>
524
+ 4. Commands Needed: <CLI commands if any>
525
+ 5. Verification: <how to verify the fix worked>
526
+
527
+ PHASE 4: IMPLEMENTATION
528
+ After planning, output the actual fixes using:
529
+
530
+ ACTION: FILE_CHANGE
531
+ FILE: <path>
532
+ ```cpp
533
+ <complete fixed content>
534
+ ```
535
+
536
+ ACTION: CLI_COMMAND
537
+ COMMAND: <command>
538
+ REASON: <why>
539
+
540
+ Be thorough. This is professional-grade analysis.'''
541
+
542
+
543
+ CLI_KEYWORDS = {
544
+ 'plugin': ['def plugin', '@cli.command', 'plugin_name', 'extract_fields', 'extract_methods'],
545
+ 'rebuild': ['def rebuild', 'build_manager', '--fast', '--clean', '--auto-ai'],
546
+ 'build': ['def build', 'build_manager'],
547
+ 'auto': ['def auto', 'auto_plugins', '--all'],
548
+ 'fix': ['def fix', 'fix_code', '--ai', 'ai_mgr'],
549
+ 'init': ['def init', 'cpp.proj', 'plugins/', 'include/'],
550
+ 'ai': ['@ai.command', 'ai_mgr', 'get_ai_manager', 'ai ask', 'ai edit', 'ai optimize'],
551
+ 'settings': ['def settings', 'settings_ui', 'PyQt6'],
552
+ 'flag': ['@click.option', 'is_flag', '--think', '--websearch', '--confirm'],
553
+ 'command': ['@cli.command', '@click.argument', '@click.option'],
554
+ }
555
+
556
+
557
+ class AIManager:
558
+ def __init__(self):
559
+ self.secret_dir = Path.home() / '.includecpp'
560
+ self.secret_path = self.secret_dir / '.secret'
561
+ self.config = self._load_config()
562
+ self._doc_cache = None
563
+ self._cli_cache = None
564
+
565
+ def _get_documentation(self) -> str:
566
+ if self._doc_cache:
567
+ return self._doc_cache
568
+ try:
569
+ readme_path = Path(__file__).parent.parent.parent / 'README.md'
570
+ if readme_path.exists():
571
+ self._doc_cache = readme_path.read_text(encoding='utf-8')
572
+ return self._doc_cache
573
+ except:
574
+ pass
575
+ return ''
576
+
577
+ def _get_build_info(self) -> str:
578
+ """Read build info from AppData for AI context."""
579
+ import platform
580
+ if platform.system() == "Windows":
581
+ appdata = Path(os.environ.get('APPDATA', str(Path.home() / 'AppData' / 'Roaming')))
582
+ build_base = appdata / 'IncludeCPP'
583
+ else:
584
+ build_base = Path.home() / '.includecpp' / 'builds'
585
+
586
+ info_parts = []
587
+ if build_base.exists():
588
+ for item in build_base.iterdir():
589
+ if item.is_dir():
590
+ registry = item / '.module_registry.json'
591
+ if registry.exists():
592
+ try:
593
+ data = json.loads(registry.read_text(encoding='utf-8'))
594
+ modules = list(data.get('modules', {}).keys())
595
+ if modules:
596
+ info_parts.append(f"Project: {item.name}, Modules: {', '.join(modules[:5])}")
597
+ except:
598
+ pass
599
+
600
+ if info_parts:
601
+ return '\n\nBUILD CONTEXT (user\'s projects):\n' + '\n'.join(info_parts[:5])
602
+ return ''
603
+
604
+ def _get_cli_context(self, question: str) -> str:
605
+ """Extract relevant CLI implementation context based on the question.
606
+
607
+ Only extracts code sections matching keywords from the question to save tokens.
608
+ """
609
+ question_lower = question.lower()
610
+
611
+ # Check if question is about CLI/commands
612
+ cli_terms = ['command', 'flag', 'option', 'cli', 'includecpp', '--', 'how to', 'usage']
613
+ if not any(term in question_lower for term in cli_terms):
614
+ return ''
615
+
616
+ # Find which CLI topics are relevant
617
+ relevant_keywords = set()
618
+ for topic, keywords in CLI_KEYWORDS.items():
619
+ if topic in question_lower:
620
+ relevant_keywords.update(keywords)
621
+
622
+ # Add generic command keywords if asking about flags/options
623
+ if '--' in question or 'flag' in question_lower or 'option' in question_lower:
624
+ relevant_keywords.update(CLI_KEYWORDS['flag'])
625
+
626
+ if not relevant_keywords:
627
+ return ''
628
+
629
+ # Load CLI source if not cached
630
+ if self._cli_cache is None:
631
+ try:
632
+ cli_path = Path(__file__).parent.parent / 'cli' / 'commands.py'
633
+ if cli_path.exists():
634
+ self._cli_cache = cli_path.read_text(encoding='utf-8')
635
+ except:
636
+ return ''
637
+
638
+ if not self._cli_cache:
639
+ return ''
640
+
641
+ # Extract relevant sections (functions/decorators containing keywords)
642
+ lines = self._cli_cache.split('\n')
643
+ extracted = []
644
+ in_relevant_block = False
645
+ block_lines = []
646
+ indent_level = 0
647
+
648
+ for i, line in enumerate(lines):
649
+ stripped = line.strip()
650
+
651
+ # Check for function/decorator start
652
+ if stripped.startswith('@') or stripped.startswith('def '):
653
+ # Save previous block if relevant
654
+ if in_relevant_block and block_lines:
655
+ extracted.extend(block_lines[:50]) # Max 50 lines per block
656
+ extracted.append(' # ... (truncated)\n')
657
+
658
+ # Check if new block is relevant
659
+ in_relevant_block = any(kw in line for kw in relevant_keywords)
660
+ block_lines = [f'{i+1}: {line}'] if in_relevant_block else []
661
+ indent_level = len(line) - len(line.lstrip())
662
+
663
+ elif in_relevant_block:
664
+ # Continue block until dedent
665
+ current_indent = len(line) - len(line.lstrip()) if stripped else indent_level + 1
666
+ if stripped and current_indent <= indent_level and not stripped.startswith('@'):
667
+ # Block ended
668
+ if block_lines:
669
+ extracted.extend(block_lines[:50])
670
+ if len(block_lines) > 50:
671
+ extracted.append(' # ... (truncated)\n')
672
+ in_relevant_block = False
673
+ block_lines = []
674
+ else:
675
+ block_lines.append(f'{i+1}: {line}')
676
+
677
+ # Limit total context
678
+ if extracted:
679
+ context = '\n'.join(extracted[:200]) # Max 200 lines total
680
+ return f'\n\nCLI IMPLEMENTATION (relevant sections):\n```python\n{context}\n```'
681
+ return ''
682
+
683
+ def _get_context_limit(self, think: bool = False, think_twice: bool = False,
684
+ think_three: bool = False) -> int:
685
+ """Get the appropriate context limit based on thinking mode."""
686
+ if think_three:
687
+ return CONTEXT_LIMITS['think3']
688
+ elif think_twice:
689
+ return CONTEXT_LIMITS['think2']
690
+ elif think:
691
+ return CONTEXT_LIMITS['think']
692
+ return CONTEXT_LIMITS['standard']
693
+
694
+ def _add_line_numbers(self, content: str) -> str:
695
+ """Add line numbers to source code for AI context."""
696
+ lines = content.split('\n')
697
+ return '\n'.join(f"{i:4d} | {line}" for i, line in enumerate(lines, 1))
698
+
699
+ def _categorize_error(self, error: str) -> str:
700
+ """Categorize build error for better AI context."""
701
+ e = error.lower()
702
+ if 'undefined reference' in e or 'unresolved external' in e:
703
+ return 'LINKER_ERROR - Missing definition, check namespace includecpp and FUNC() in .cp'
704
+ elif 'syntax error' in e or 'expected' in e:
705
+ return 'SYNTAX_ERROR - Check for missing semicolons, braces, or typos'
706
+ elif 'namespace' in e:
707
+ return 'NAMESPACE_ERROR - Code likely not wrapped in namespace includecpp { }'
708
+ elif 'template' in e:
709
+ return 'TEMPLATE_ERROR - Check TEMPLATE_FUNC() with TYPES() in .cp file'
710
+ elif 'no matching function' in e or 'no member named' in e:
711
+ return 'SIGNATURE_ERROR - Method signature in .cp doesn\'t match source'
712
+ elif 'include' in e or 'no such file' in e:
713
+ return 'INCLUDE_ERROR - Missing header file or wrong include path'
714
+ return 'UNKNOWN - Analyze error message carefully'
715
+
716
+ def _reset_daily_if_needed(self):
717
+ """Reset daily usage if it's a new day."""
718
+ from datetime import date
719
+ today = date.today().isoformat()
720
+ if self.config.get('daily_usage', {}).get('date') != today:
721
+ self.config['daily_usage'] = {'date': today, 'tokens': 0}
722
+ self._save_config()
723
+
724
+ def _check_daily_limit(self) -> Tuple[bool, str]:
725
+ """Check if daily limit is exceeded.
726
+ Returns: (can_proceed, warning_message)
727
+ """
728
+ self._reset_daily_if_needed()
729
+ limit = self.config.get('daily_limit', DEFAULT_DAILY_LIMIT)
730
+ used = self.config.get('daily_usage', {}).get('tokens', 0)
731
+
732
+ if used >= limit:
733
+ return False, f"Daily token limit reached ({used:,}/{limit:,}). Resets at midnight."
734
+ if used >= limit * 0.8:
735
+ remaining = limit - used
736
+ return True, f"Warning: {remaining:,} tokens remaining today ({int(used/limit*100)}% used)"
737
+ return True, ""
738
+
739
+ def get_daily_usage_info(self) -> Dict[str, Any]:
740
+ """Get current daily usage information."""
741
+ self._reset_daily_if_needed()
742
+ limit = self.config.get('daily_limit', DEFAULT_DAILY_LIMIT)
743
+ used = self.config.get('daily_usage', {}).get('tokens', 0)
744
+ return {
745
+ 'date': self.config.get('daily_usage', {}).get('date'),
746
+ 'tokens_used': used,
747
+ 'daily_limit': limit,
748
+ 'remaining': max(0, limit - used),
749
+ 'percentage': min(100, int(used / limit * 100)) if limit > 0 else 0
750
+ }
751
+
752
+ def set_daily_limit(self, limit: int) -> Tuple[bool, str]:
753
+ """Set the daily token limit."""
754
+ if limit < 1000:
755
+ return False, "Daily limit must be at least 1,000 tokens"
756
+ if limit > 10000000:
757
+ return False, "Daily limit cannot exceed 10,000,000 tokens"
758
+ self.config['daily_limit'] = limit
759
+ self._save_config()
760
+ return True, f"Daily limit set to {limit:,} tokens"
761
+
762
+ def _load_config(self) -> dict:
763
+ if self.secret_path.exists():
764
+ try:
765
+ data = json.loads(self.secret_path.read_text(encoding='utf-8'))
766
+ if 'usage' not in data:
767
+ data['usage'] = {'total_tokens': 0, 'total_requests': 0, 'last_request': None}
768
+ if 'daily_limit' not in data:
769
+ data['daily_limit'] = DEFAULT_DAILY_LIMIT
770
+ if 'daily_usage' not in data:
771
+ data['daily_usage'] = {'date': None, 'tokens': 0}
772
+ return data
773
+ except (json.JSONDecodeError, IOError):
774
+ pass
775
+ return {
776
+ 'api_key': None,
777
+ 'brave_api_key': None,
778
+ 'enabled': False,
779
+ 'model': DEFAULT_MODEL,
780
+ 'usage': {
781
+ 'total_tokens': 0,
782
+ 'total_requests': 0,
783
+ 'last_request': None
784
+ },
785
+ 'daily_limit': DEFAULT_DAILY_LIMIT,
786
+ 'daily_usage': {'date': None, 'tokens': 0}
787
+ }
788
+
789
+ def _save_config(self):
790
+ self.secret_dir.mkdir(parents=True, exist_ok=True)
791
+ self.secret_path.write_text(json.dumps(self.config, indent=2), encoding='utf-8')
792
+ if os.name != 'nt':
793
+ os.chmod(self.secret_path, stat.S_IRUSR | stat.S_IWUSR)
794
+
795
+ def _mask_key(self) -> str:
796
+ key = self.config.get('api_key')
797
+ if not key:
798
+ return 'Not set'
799
+ if len(key) <= 8:
800
+ return '****'
801
+ return f"{key[:7]}...{key[-4:]}"
802
+
803
+ def set_key(self, key: str) -> Tuple[bool, str]:
804
+ if not key:
805
+ return False, 'API key cannot be empty'
806
+ if not key.startswith('sk-'):
807
+ return False, 'Invalid API key format. Key should start with sk-'
808
+ test_result, test_msg = self._test_key(key)
809
+ if not test_result:
810
+ return False, test_msg
811
+ self.config['api_key'] = key
812
+ self._save_config()
813
+ return True, 'API key saved and verified'
814
+
815
+ def _test_key(self, key: str) -> Tuple[bool, str]:
816
+ try:
817
+ headers = {
818
+ 'Authorization': f'Bearer {key}',
819
+ 'Content-Type': 'application/json'
820
+ }
821
+ data = {
822
+ 'model': 'gpt-3.5-turbo',
823
+ 'messages': [{'role': 'user', 'content': 'test'}],
824
+ 'max_tokens': 1
825
+ }
826
+ response = requests.post(OPENAI_API_URL, headers=headers, json=data, timeout=10)
827
+ if response.status_code == 200:
828
+ return True, 'OK'
829
+ elif response.status_code == 401:
830
+ return False, 'Invalid API key'
831
+ elif response.status_code == 429:
832
+ return True, 'OK (rate limited but valid)'
833
+ else:
834
+ return False, f'API error: {response.status_code}'
835
+ except requests.exceptions.Timeout:
836
+ return False, 'Connection timeout'
837
+ except requests.exceptions.ConnectionError:
838
+ return False, 'Connection failed'
839
+ except Exception as e:
840
+ return False, str(e)
841
+
842
+ def is_enabled(self) -> bool:
843
+ return bool(self.config.get('enabled', False) and self.config.get('api_key'))
844
+
845
+ def has_key(self) -> bool:
846
+ return bool(self.config.get('api_key'))
847
+
848
+ def set_brave_key(self, key: str) -> Tuple[bool, str]:
849
+ if not key:
850
+ return False, 'Brave API key cannot be empty'
851
+ test_result, test_msg = self._test_brave_key(key)
852
+ if not test_result:
853
+ return False, test_msg
854
+ self.config['brave_api_key'] = key
855
+ self._save_config()
856
+ return True, 'Brave Search API key saved and verified'
857
+
858
+ def _test_brave_key(self, key: str) -> Tuple[bool, str]:
859
+ try:
860
+ headers = {
861
+ 'Accept': 'application/json',
862
+ 'X-Subscription-Token': key
863
+ }
864
+ response = requests.get(
865
+ f'{BRAVE_SEARCH_URL}?q=test&count=1',
866
+ headers=headers,
867
+ timeout=10
868
+ )
869
+ if response.status_code == 200:
870
+ return True, 'OK'
871
+ elif response.status_code == 401:
872
+ return False, 'Invalid Brave API key'
873
+ elif response.status_code == 429:
874
+ return True, 'OK (rate limited but valid)'
875
+ else:
876
+ return False, f'Brave API error: {response.status_code}'
877
+ except requests.exceptions.Timeout:
878
+ return False, 'Connection timeout'
879
+ except requests.exceptions.ConnectionError:
880
+ return False, 'Connection failed'
881
+ except Exception as e:
882
+ return False, str(e)
883
+
884
+ def has_brave_key(self) -> bool:
885
+ return bool(self.config.get('brave_api_key'))
886
+
887
+ def brave_search(self, query: str, count: int = 5) -> Tuple[bool, List[Dict]]:
888
+ if not self.has_brave_key():
889
+ return False, []
890
+ try:
891
+ headers = {
892
+ 'Accept': 'application/json',
893
+ 'X-Subscription-Token': self.config['brave_api_key']
894
+ }
895
+ response = requests.get(
896
+ f'{BRAVE_SEARCH_URL}?q={query}&count={count}',
897
+ headers=headers,
898
+ timeout=15
899
+ )
900
+ if response.status_code == 200:
901
+ data = response.json()
902
+ results = []
903
+ for item in data.get('web', {}).get('results', []):
904
+ results.append({
905
+ 'title': item.get('title', ''),
906
+ 'url': item.get('url', ''),
907
+ 'description': item.get('description', '')
908
+ })
909
+ return True, results
910
+ return False, []
911
+ except Exception:
912
+ return False, []
913
+
914
+ def enable(self) -> Tuple[bool, str]:
915
+ if not self.config.get('api_key'):
916
+ return False, 'No API key configured. Use: includecpp ai key <YOUR_KEY>'
917
+ self.config['enabled'] = True
918
+ self._save_config()
919
+ return True, 'AI features enabled'
920
+
921
+ def disable(self) -> Tuple[bool, str]:
922
+ self.config['enabled'] = False
923
+ self._save_config()
924
+ return True, 'AI features disabled'
925
+
926
+ def get_model(self) -> str:
927
+ return self.config.get('model', DEFAULT_MODEL)
928
+
929
+ def set_model(self, model: str) -> Tuple[bool, str]:
930
+ if model not in MODELS:
931
+ available = ', '.join(MODELS.keys())
932
+ return False, f'Unknown model. Available: {available}'
933
+ self.config['model'] = model
934
+ self._save_config()
935
+ return True, f'Model set to {model}'
936
+
937
+ def list_models(self) -> List[Dict[str, Any]]:
938
+ current = self.get_model()
939
+ result = []
940
+ for name, info in MODELS.items():
941
+ result.append({
942
+ 'name': name,
943
+ 'context': info['context'],
944
+ 'active': name == current
945
+ })
946
+ return result
947
+
948
+ def get_info(self) -> Dict[str, Any]:
949
+ usage = self.config.get('usage', {})
950
+ return {
951
+ 'key_set': bool(self.config.get('api_key')),
952
+ 'key_preview': self._mask_key(),
953
+ 'enabled': self.config.get('enabled', False),
954
+ 'model': self.config.get('model', DEFAULT_MODEL),
955
+ 'total_tokens': usage.get('total_tokens', 0),
956
+ 'total_requests': usage.get('total_requests', 0),
957
+ 'last_request': usage.get('last_request')
958
+ }
959
+
960
+ def _update_usage(self, tokens: int):
961
+ if 'usage' not in self.config:
962
+ self.config['usage'] = {'total_tokens': 0, 'total_requests': 0, 'last_request': None}
963
+ self.config['usage']['total_tokens'] = self.config['usage'].get('total_tokens', 0) + tokens
964
+ self.config['usage']['total_requests'] = self.config['usage'].get('total_requests', 0) + 1
965
+ self.config['usage']['last_request'] = datetime.now().isoformat()
966
+ self._reset_daily_if_needed()
967
+ self.config['daily_usage']['tokens'] = self.config['daily_usage'].get('tokens', 0) + tokens
968
+ self._save_config()
969
+
970
+ def query(self, system_prompt: str, user_prompt: str, temperature: float = 0.3,
971
+ timeout: int = 180) -> Tuple[bool, str]:
972
+ if not self.config.get('api_key'):
973
+ return False, 'No API key configured'
974
+ can_proceed, limit_warning = self._check_daily_limit()
975
+ if not can_proceed:
976
+ return False, limit_warning
977
+ model = self.config.get('model', DEFAULT_MODEL)
978
+ model_info = MODELS.get(model, MODELS[DEFAULT_MODEL])
979
+ headers = {
980
+ 'Authorization': f'Bearer {self.config["api_key"]}',
981
+ 'Content-Type': 'application/json'
982
+ }
983
+ token_limit = min(16000, model_info['context'] // 2)
984
+ data = {
985
+ 'model': model_info['endpoint'],
986
+ 'messages': [
987
+ {'role': 'system', 'content': system_prompt},
988
+ {'role': 'user', 'content': user_prompt}
989
+ ]
990
+ }
991
+ if model.startswith('gpt-5'):
992
+ data['max_completion_tokens'] = token_limit
993
+ else:
994
+ data['max_tokens'] = token_limit
995
+ data['temperature'] = temperature
996
+ try:
997
+ response = requests.post(OPENAI_API_URL, headers=headers, json=data, timeout=timeout)
998
+ if response.status_code == 200:
999
+ result = response.json()
1000
+ content = result['choices'][0]['message']['content']
1001
+ tokens = result.get('usage', {}).get('total_tokens', 0)
1002
+ self._update_usage(tokens)
1003
+ return True, content
1004
+ elif response.status_code == 401:
1005
+ return False, 'Invalid API key'
1006
+ elif response.status_code == 429:
1007
+ return False, 'Rate limit exceeded. Please wait and try again'
1008
+ elif response.status_code == 503:
1009
+ return False, 'OpenAI service unavailable'
1010
+ else:
1011
+ try:
1012
+ err = response.json().get('error', {}).get('message', '')
1013
+ except:
1014
+ err = response.text
1015
+ return False, f'API error ({response.status_code}): {err}'
1016
+ except requests.exceptions.Timeout:
1017
+ return False, 'Request timeout'
1018
+ except requests.exceptions.ConnectionError:
1019
+ return False, 'Connection failed. Check your internet connection'
1020
+ except Exception as e:
1021
+ return False, str(e)
1022
+
1023
+ def _build_prompt_with_docs(self, base_prompt: str, include_build_info: bool = True) -> str:
1024
+ parts = [INCLUDECPP_CONTEXT]
1025
+ docs = self._get_documentation()
1026
+ if docs:
1027
+ doc_section = docs[:8000] if len(docs) > 8000 else docs
1028
+ parts.append(f'\nIncludeCPP Documentation:\n{doc_section}')
1029
+ if include_build_info:
1030
+ build_info = self._get_build_info()
1031
+ if build_info:
1032
+ parts.append(build_info)
1033
+ parts.append(f'\n\n---\n\n{base_prompt}')
1034
+ return '\n'.join(parts)
1035
+
1036
+ def optimize_code(self, files: Dict[str, str], custom_task: Optional[str] = None) -> Tuple[bool, str, List[Dict]]:
1037
+ if not files:
1038
+ return False, 'No files provided', []
1039
+ file_content = ''
1040
+ for path, content in files.items():
1041
+ file_content += f'\nFILE: {path}\n```cpp\n{content}\n```\n'
1042
+ if custom_task:
1043
+ prompt = f'Task: {custom_task}\n\nFiles:\n{file_content}'
1044
+ system = SYSTEM_PROMPT_AGENT
1045
+ else:
1046
+ prompt = f'Optimize the following C++ files for performance, safety, and pybind11 compatibility:\n{file_content}'
1047
+ system = SYSTEM_PROMPT_OPTIMIZE
1048
+ prompt = self._build_prompt_with_docs(prompt)
1049
+ # v3.2.2: Use longer timeout (5 min) for optimize operations with multiple files
1050
+ timeout = 300 if len(files) > 1 else 180
1051
+ success, response = self.query(system, prompt, timeout=timeout)
1052
+ if not success:
1053
+ return False, response, []
1054
+ changes = self._parse_file_changes(response)
1055
+ return True, response, changes
1056
+
1057
+ def fix_code(self, files: Dict[str, str]) -> Tuple[bool, str, List[Dict]]:
1058
+ if not files:
1059
+ return False, 'No files provided', []
1060
+ file_content = ''
1061
+ for path, content in files.items():
1062
+ file_content += f'\nFILE: {path}\n```cpp\n{content}\n```\n'
1063
+ prompt = f'Analyze and fix issues in these C++ files:\n{file_content}'
1064
+ prompt = self._build_prompt_with_docs(prompt)
1065
+ success, response = self.query(SYSTEM_PROMPT_FIX, prompt)
1066
+ if not success:
1067
+ return False, response, []
1068
+ changes = self._parse_file_changes(response)
1069
+ return True, response, changes
1070
+
1071
+ def analyze_build_error(self, error: str, source_files: Dict[str, str]) -> Tuple[bool, str]:
1072
+ context = f'Build error:\n{error}\n\n'
1073
+ if source_files:
1074
+ context += 'Relevant source files:\n'
1075
+ for path, content in list(source_files.items())[:3]:
1076
+ lines = content.split('\n')
1077
+ preview = '\n'.join(lines[:100])
1078
+ context += f'\nFILE: {path}\n```cpp\n{preview}\n```\n'
1079
+ context = self._build_prompt_with_docs(context)
1080
+ success, response = self.query(SYSTEM_PROMPT_BUILD_ERROR, context)
1081
+ return success, response
1082
+
1083
+ def auto_fix_build_error(self, error: str, source_files: Dict[str, str],
1084
+ plugin_content: Optional[str] = None,
1085
+ module_name: Optional[str] = None,
1086
+ think: bool = False,
1087
+ think_twice: bool = False,
1088
+ think_three: bool = False,
1089
+ use_websearch: bool = False) -> Tuple[bool, str, List[Dict], List[Dict]]:
1090
+ error_category = self._categorize_error(error)
1091
+ context = f'Build error:\n{error}\n\nError category: {error_category}\n\n'
1092
+ if module_name:
1093
+ context += f'Module name: {module_name}\n\n'
1094
+ if use_websearch and self.has_brave_key():
1095
+ search_query = f'C++ pybind11 {error[:100]}'
1096
+ success, results = self.brave_search(search_query, count=5)
1097
+ if success and results:
1098
+ context += 'Web Research Results:\n'
1099
+ for r in results:
1100
+ context += f'- {r["title"]}: {r["description"][:200]}\n'
1101
+ context += '\n'
1102
+ if source_files:
1103
+ context += 'Source files (with line numbers):\n'
1104
+ max_lines = self._get_context_limit(think, think_twice, think_three)
1105
+ for path, content in source_files.items():
1106
+ lines = content.split('\n')
1107
+ if len(lines) > max_lines:
1108
+ preview = '\n'.join(lines[:max_lines])
1109
+ numbered = self._add_line_numbers(preview)
1110
+ context += f'\nFILE: {path}\n```cpp\n{numbered}\n... ({len(lines) - max_lines} more lines)\n```\n'
1111
+ else:
1112
+ numbered = self._add_line_numbers(content)
1113
+ context += f'\nFILE: {path}\n```cpp\n{numbered}\n```\n'
1114
+ if plugin_content:
1115
+ context += f'\nPlugin definition (.cp file):\n```\n{plugin_content}\n```\n'
1116
+ if think_three:
1117
+ context += '\nIMPORTANT: This is professional-grade analysis. Take your time to plan thoroughly before implementing.'
1118
+ elif think_twice:
1119
+ context += '\nIMPORTANT: Analyze thoroughly. This is a complex codebase requiring careful consideration.'
1120
+ elif think:
1121
+ context += '\nIMPORTANT: Think step by step before fixing.'
1122
+ context = self._build_prompt_with_docs(context)
1123
+ system_prompt = SYSTEM_PROMPT_THINK3_PLAN if think_three else SYSTEM_PROMPT_AUTO_FIX
1124
+ temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
1125
+ success, response = self.query(system_prompt, context, temperature=temperature)
1126
+ if not success:
1127
+ return False, response, [], []
1128
+ file_changes, cli_commands = self._parse_auto_fix_response(response)
1129
+ return True, response, file_changes, cli_commands
1130
+
1131
+ def _parse_auto_fix_response(self, response: str) -> Tuple[List[Dict], List[Dict]]:
1132
+ file_changes = []
1133
+ cli_commands = []
1134
+ lines = response.split('\n')
1135
+ i = 0
1136
+ while i < len(lines):
1137
+ line = lines[i].strip()
1138
+ if line == 'ACTION: FILE_CHANGE':
1139
+ i += 1
1140
+ if i < len(lines) and lines[i].strip().startswith('FILE:'):
1141
+ file_path = lines[i].strip()[5:].strip()
1142
+ i += 1
1143
+ content_lines = []
1144
+ in_code = False
1145
+ while i < len(lines):
1146
+ l = lines[i]
1147
+ if l.strip().startswith('```cpp'):
1148
+ in_code = True
1149
+ i += 1
1150
+ continue
1151
+ if in_code and l.strip().startswith('```'):
1152
+ break
1153
+ if in_code:
1154
+ content_lines.append(l)
1155
+ i += 1
1156
+ if content_lines:
1157
+ file_changes.append({
1158
+ 'file': file_path,
1159
+ 'content': '\n'.join(content_lines)
1160
+ })
1161
+ elif line == 'ACTION: CLI_COMMAND':
1162
+ i += 1
1163
+ command = None
1164
+ reason = None
1165
+ while i < len(lines):
1166
+ l = lines[i].strip()
1167
+ if l.startswith('COMMAND:'):
1168
+ command = l[8:].strip()
1169
+ elif l.startswith('REASON:'):
1170
+ reason = l[7:].strip()
1171
+ elif l.startswith('ACTION:') or l.startswith('CANNOT_FIX:'):
1172
+ break
1173
+ i += 1
1174
+ if command and reason:
1175
+ break
1176
+ if command:
1177
+ cli_commands.append({
1178
+ 'command': command,
1179
+ 'reason': reason or ''
1180
+ })
1181
+ continue
1182
+ elif line.startswith('CANNOT_FIX:'):
1183
+ break
1184
+ i += 1
1185
+ return file_changes, cli_commands
1186
+
1187
+ def ask_question(self, question: str, files: Dict[str, str], plugins: Dict[str, str] = None,
1188
+ think: bool = False, think_twice: bool = False, think_three: bool = False,
1189
+ use_websearch: bool = False) -> Tuple[bool, str]:
1190
+ context_parts = []
1191
+ if use_websearch and self.has_brave_key():
1192
+ search_query = f'C++ pybind11 {question[:100]}'
1193
+ success, results = self.brave_search(search_query, count=5)
1194
+ if success and results:
1195
+ context_parts.append('Web Research Results:')
1196
+ for r in results:
1197
+ context_parts.append(f'- {r["title"]}: {r["description"][:200]}')
1198
+ context_parts.append('')
1199
+ max_lines = self._get_context_limit(think, think_twice, think_three)
1200
+ if files:
1201
+ context_parts.append('Project files:')
1202
+ for path, content in files.items():
1203
+ lines = content.split('\n')
1204
+ if len(lines) > max_lines:
1205
+ preview = '\n'.join(lines[:max_lines])
1206
+ preview += f'\n... ({len(lines) - max_lines} more lines)'
1207
+ else:
1208
+ preview = content
1209
+ ext = Path(path).suffix
1210
+ lang = 'cpp' if ext in ['.cpp', '.h', '.hpp', '.c'] else 'python' if ext == '.py' else ''
1211
+ context_parts.append(f'\nFILE: {path}\n```{lang}\n{preview}\n```')
1212
+ if plugins:
1213
+ context_parts.append('\nPlugin definitions:')
1214
+ for path, content in plugins.items():
1215
+ context_parts.append(f'\nPLUGIN: {path}\n```\n{content}\n```')
1216
+ # v3.2.2: Add CLI context for questions about commands/flags
1217
+ cli_context = self._get_cli_context(question)
1218
+ if cli_context:
1219
+ context_parts.append(cli_context)
1220
+ context = '\n'.join(context_parts)
1221
+ prompt = f'Question: {question}\n\n{context}'
1222
+ if think_three:
1223
+ prompt += '\n\nIMPORTANT: Provide thorough, professional-grade analysis with detailed reasoning.'
1224
+ elif think_twice:
1225
+ prompt += '\n\nIMPORTANT: Analyze carefully and consider multiple angles before answering.'
1226
+ elif think:
1227
+ prompt += '\n\nIMPORTANT: Think step by step before answering.'
1228
+ prompt = self._build_prompt_with_docs(prompt)
1229
+ temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
1230
+ timeout = None if think_three else (420 if think_twice else (300 if think else 180))
1231
+ return self.query(SYSTEM_PROMPT_ASK, prompt, temperature=temperature, timeout=timeout)
1232
+
1233
+ def edit_code(self, task: str, files: Dict[str, str], think: bool = False,
1234
+ think_twice: bool = False, think_three: bool = False,
1235
+ use_websearch: bool = False) -> Tuple[bool, str, List[Dict]]:
1236
+ if not files:
1237
+ return False, 'No files provided', []
1238
+ context_parts = []
1239
+ if use_websearch and self.has_brave_key():
1240
+ search_query = f'C++ pybind11 {task[:100]}'
1241
+ success, results = self.brave_search(search_query, count=5)
1242
+ if success and results:
1243
+ context_parts.append('Web Research Results:')
1244
+ for r in results:
1245
+ context_parts.append(f'- {r["title"]}: {r["description"][:200]}')
1246
+ context_parts.append('')
1247
+ max_lines = self._get_context_limit(think, think_twice, think_three)
1248
+ file_content = ''
1249
+ for path, content in files.items():
1250
+ lines = content.split('\n')
1251
+ if len(lines) > max_lines:
1252
+ preview = '\n'.join(lines[:max_lines])
1253
+ preview += f'\n... ({len(lines) - max_lines} more lines)'
1254
+ else:
1255
+ preview = content
1256
+ ext = Path(path).suffix
1257
+ lang = 'cpp' if ext in ['.cpp', '.h', '.hpp', '.c'] else 'python' if ext == '.py' else ''
1258
+ file_content += f'\nFILE: {path}\n```{lang}\n{preview}\n```\n'
1259
+ prompt = '\n'.join(context_parts) + f'Edit task: {task}\n\nFiles:\n{file_content}'
1260
+ if think_three:
1261
+ prompt += '\n\nIMPORTANT: This is professional-grade editing. Plan thoroughly, consider all implications, and implement carefully.'
1262
+ prompt += QUESTION_PROMPT_ADDITION
1263
+ elif think_twice:
1264
+ prompt += '\n\nIMPORTANT: Think carefully before making changes. Consider edge cases and potential issues.'
1265
+ prompt += QUESTION_PROMPT_ADDITION
1266
+ elif think:
1267
+ prompt += '\n\nIMPORTANT: Think step by step before editing.'
1268
+ prompt = self._build_prompt_with_docs(prompt)
1269
+ temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
1270
+ timeout = None if think_three else (420 if think_twice else (300 if think else 180))
1271
+ success, response = self.query(SYSTEM_PROMPT_EDIT, prompt, temperature=temperature, timeout=timeout)
1272
+ if not success:
1273
+ return False, response, [], None
1274
+ question = self._extract_question(response)
1275
+ if question:
1276
+ return True, response, [], question
1277
+ changes = self._parse_file_changes(response)
1278
+ return True, response, changes, None
1279
+
1280
+ def _extract_question(self, response: str) -> Optional[Dict]:
1281
+ """Extract ASK_USER question from AI response."""
1282
+ import re
1283
+ match = re.search(r'ASK_USER:\s*(.+?)(?:\n|$)', response, re.IGNORECASE)
1284
+ if not match:
1285
+ return None
1286
+ question = match.group(1).strip()
1287
+ options = []
1288
+ opts_match = re.search(r'OPTIONS:\s*(.+?)(?:\n|$)', response, re.IGNORECASE)
1289
+ if opts_match:
1290
+ opts_str = opts_match.group(1).strip()
1291
+ options = [o.strip() for o in opts_str.split('|') if o.strip()]
1292
+ return {'question': question, 'options': options}
1293
+
1294
+ def continue_with_answer(self, original_prompt: str, ai_question: str, user_answer: str,
1295
+ think_twice: bool = False, think_three: bool = False) -> Tuple[bool, str, List[Dict]]:
1296
+ """Continue edit_code after user answers a question."""
1297
+ continuation = f'{original_prompt}\n\nYou asked: {ai_question}\nUser answered: {user_answer}\n\nNow proceed with the edit based on this answer.'
1298
+ temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
1299
+ timeout = None if think_three else (420 if think_twice else 180)
1300
+ success, response = self.query(SYSTEM_PROMPT_EDIT, continuation, temperature=temperature, timeout=timeout)
1301
+ if not success:
1302
+ return False, response, []
1303
+ changes = self._parse_file_changes(response)
1304
+ return True, response, changes
1305
+
1306
+ def generate(self, task: str, files: Dict[str, str], project_root: Path,
1307
+ think: bool = False, think_twice: bool = False, think_three: bool = False,
1308
+ use_websearch: bool = False, max_context: bool = False,
1309
+ plan_mode: bool = False, new_module: str = None,
1310
+ skip_tool_execution: bool = False) -> Tuple[bool, str, List[Dict]]:
1311
+ """Super assistant with tool execution."""
1312
+ import platform
1313
+ import subprocess
1314
+
1315
+ # Build tools list for prompt
1316
+ tools_list = '\n'.join([
1317
+ f"- {name}: {info['desc']}\n Format:\n {info['format']}"
1318
+ for name, info in GENERATE_TOOLS.items()
1319
+ ])
1320
+
1321
+ # System info
1322
+ system_info = f"{platform.system()} ({platform.release()})"
1323
+
1324
+ # Choose prompt based on mode
1325
+ if new_module:
1326
+ system_prompt = SYSTEM_PROMPT_NEW_MODULE.format(
1327
+ module_name=new_module,
1328
+ description=task
1329
+ )
1330
+ elif plan_mode:
1331
+ system_prompt = SYSTEM_PROMPT_GENERATE_PLAN.format(
1332
+ base_prompt=SYSTEM_PROMPT_GENERATE.format(
1333
+ tools_list=tools_list,
1334
+ project_root=str(project_root),
1335
+ system_info=system_info,
1336
+ includecpp_context=INCLUDECPP_CONTEXT
1337
+ )
1338
+ )
1339
+ else:
1340
+ system_prompt = SYSTEM_PROMPT_GENERATE.format(
1341
+ tools_list=tools_list,
1342
+ project_root=str(project_root),
1343
+ system_info=system_info,
1344
+ includecpp_context=INCLUDECPP_CONTEXT
1345
+ )
1346
+
1347
+ # Context limits
1348
+ if max_context:
1349
+ max_lines = 50000
1350
+ else:
1351
+ max_lines = self._get_context_limit(think, think_twice, think_three)
1352
+
1353
+ # Build file context
1354
+ file_context = self._build_file_context(files, max_lines)
1355
+
1356
+ # Web search if enabled
1357
+ web_context = ''
1358
+ if use_websearch and self.has_brave_key():
1359
+ success, results = self.brave_search(f'C++ pybind11 {task[:100]}')
1360
+ if success and results:
1361
+ web_context = '\n\nWEB RESEARCH:\n' + '\n'.join(
1362
+ f"- {r['title']}: {r['description'][:200]}" for r in results[:5]
1363
+ )
1364
+
1365
+ # Build prompt
1366
+ prompt = f'Task: {task}\n\n'
1367
+ if file_context:
1368
+ prompt += f'Project Files:\n{file_context}\n'
1369
+ if web_context:
1370
+ prompt += web_context
1371
+
1372
+ prompt = self._build_prompt_with_docs(prompt)
1373
+
1374
+ # Temperature and timeout
1375
+ temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
1376
+ timeout = None if think_three else (420 if think_twice else (300 if think else 180))
1377
+
1378
+ # Execute with tool loop
1379
+ all_changes = []
1380
+ max_iterations = 10
1381
+
1382
+ for iteration in range(max_iterations):
1383
+ success, response = self.query(system_prompt, prompt, temperature, timeout)
1384
+ if not success:
1385
+ return False, response, []
1386
+
1387
+ # Parse tool calls
1388
+ tool_calls = self._parse_tool_calls(response)
1389
+
1390
+ if not tool_calls:
1391
+ # No more tools, parse final changes
1392
+ changes = self._parse_file_changes(response)
1393
+ all_changes.extend(changes)
1394
+ break
1395
+
1396
+ # Execute tools (unless skip_tool_execution is set)
1397
+ tool_results = []
1398
+ for tool in tool_calls:
1399
+ # Collect file changes from WRITE_FILE/EDIT_FILE
1400
+ if tool['name'] in ['WRITE_FILE', 'EDIT_FILE'] and tool.get('content'):
1401
+ all_changes.append({
1402
+ 'file': tool.get('path', 'unknown'),
1403
+ 'content': tool['content'],
1404
+ 'changes_desc': tool.get('changes', ['Tool-generated']),
1405
+ 'confirm_required': []
1406
+ })
1407
+
1408
+ # Skip actual execution if flag is set
1409
+ if skip_tool_execution:
1410
+ tool_results.append(f"{tool['name']} SKIPPED (parse-only mode)")
1411
+ continue
1412
+
1413
+ result = self._execute_tool(tool, project_root)
1414
+ tool_results.append(result)
1415
+
1416
+ # Add tool results to prompt for next iteration
1417
+ if not skip_tool_execution:
1418
+ prompt += '\n\nTOOL RESULTS:\n' + '\n---\n'.join(tool_results)
1419
+
1420
+ # Deduplicate changes by file path (keep last version of each file)
1421
+ seen_files = {}
1422
+ for change in all_changes:
1423
+ file_path = change.get('file', '')
1424
+ if file_path:
1425
+ seen_files[file_path] = change
1426
+ deduplicated_changes = list(seen_files.values())
1427
+
1428
+ return True, response, deduplicated_changes
1429
+
1430
+ def _parse_tool_calls(self, response: str) -> List[Dict]:
1431
+ """Parse TOOL: blocks from AI response."""
1432
+ import re
1433
+ import json
1434
+ tools = []
1435
+
1436
+ # Primary pattern: TOOL: <NAME>\n<params>
1437
+ pattern = r'TOOL:\s*(\w+)\n((?:(?!TOOL:).)*?)(?=TOOL:|$)'
1438
+ matches = re.findall(pattern, response, re.DOTALL)
1439
+
1440
+ for name, params in matches:
1441
+ tool = {'name': name.strip()}
1442
+
1443
+ # Parse PATH
1444
+ path_match = re.search(r'PATH:\s*(.+?)(?:\n|$)', params)
1445
+ if path_match:
1446
+ tool['path'] = path_match.group(1).strip()
1447
+
1448
+ # Parse PATTERN
1449
+ pattern_match = re.search(r'PATTERN:\s*(.+?)(?:\n|$)', params)
1450
+ if pattern_match:
1451
+ tool['pattern'] = pattern_match.group(1).strip()
1452
+
1453
+ # Parse CMD
1454
+ cmd_match = re.search(r'CMD:\s*(.+?)(?:\n|$)', params)
1455
+ if cmd_match:
1456
+ tool['cmd'] = cmd_match.group(1).strip()
1457
+
1458
+ # Parse CHANGES
1459
+ changes_match = re.search(r'CHANGES:\n((?:- .+\n?)+)', params)
1460
+ if changes_match:
1461
+ tool['changes'] = [c.strip('- \n') for c in changes_match.group(1).split('\n') if c.strip().startswith('-')]
1462
+
1463
+ # Parse code block
1464
+ code_match = re.search(r'```(?:\w+)?\n(.*?)```', params, re.DOTALL)
1465
+ if code_match:
1466
+ tool['content'] = code_match.group(1)
1467
+
1468
+ tools.append(tool)
1469
+
1470
+ # Fallback: Parse JSON-style tool calls (e.g., WRITE_FILE{"path":...,"content":...})
1471
+ if not tools:
1472
+ json_pattern = r'(WRITE_FILE|EDIT_FILE|READ_FILE|DELETE_FILE|CREATE_FOLDER|LIST_FOLDER|SEARCH_FILES|GREP|RUN_CMD|INCLUDECPP_CMD)\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}'
1473
+ json_matches = re.findall(json_pattern, response, re.DOTALL)
1474
+ for name, json_body in json_matches:
1475
+ try:
1476
+ # Try to parse as JSON
1477
+ data = json.loads('{' + json_body + '}')
1478
+ tool = {'name': name}
1479
+ if 'path' in data:
1480
+ tool['path'] = data['path']
1481
+ if 'content' in data:
1482
+ tool['content'] = data['content']
1483
+ if 'pattern' in data:
1484
+ tool['pattern'] = data['pattern']
1485
+ if 'cmd' in data:
1486
+ tool['cmd'] = data['cmd']
1487
+ tools.append(tool)
1488
+ except json.JSONDecodeError:
1489
+ pass
1490
+
1491
+ return tools
1492
+
1493
+ def _execute_tool(self, tool: Dict, project_root: Path) -> str:
1494
+ """Execute a single tool and return result string."""
1495
+ import subprocess
1496
+ import re as re_module
1497
+
1498
+ name = tool['name']
1499
+
1500
+ try:
1501
+ if name == 'READ_FILE':
1502
+ path = self._resolve_path(tool.get('path', ''), project_root)
1503
+ if path.exists():
1504
+ content = path.read_text(encoding='utf-8', errors='replace')
1505
+ lines = content.split('\n')
1506
+ if len(lines) > 500:
1507
+ content = '\n'.join(lines[:500]) + f'\n... ({len(lines)-500} more lines)'
1508
+ return f"READ_FILE {path}:\n```\n{content}\n```"
1509
+ return f"READ_FILE ERROR: File not found: {path}"
1510
+
1511
+ elif name == 'WRITE_FILE':
1512
+ path = self._resolve_path(tool.get('path', ''), project_root)
1513
+ content = tool.get('content', '')
1514
+ path.parent.mkdir(parents=True, exist_ok=True)
1515
+ path.write_text(content, encoding='utf-8')
1516
+ return f"WRITE_FILE SUCCESS: Created {path}"
1517
+
1518
+ elif name == 'EDIT_FILE':
1519
+ path = self._resolve_path(tool.get('path', ''), project_root)
1520
+ content = tool.get('content', '')
1521
+ if path.exists():
1522
+ path.write_text(content, encoding='utf-8')
1523
+ return f"EDIT_FILE SUCCESS: Modified {path}"
1524
+ return f"EDIT_FILE ERROR: File not found: {path}"
1525
+
1526
+ elif name == 'DELETE_FILE':
1527
+ path = self._resolve_path(tool.get('path', ''), project_root)
1528
+ if path.exists():
1529
+ path.unlink()
1530
+ return f"DELETE_FILE SUCCESS: Deleted {path}"
1531
+ return f"DELETE_FILE ERROR: File not found: {path}"
1532
+
1533
+ elif name == 'CREATE_FOLDER':
1534
+ path = self._resolve_path(tool.get('path', ''), project_root)
1535
+ path.mkdir(parents=True, exist_ok=True)
1536
+ return f"CREATE_FOLDER SUCCESS: Created {path}"
1537
+
1538
+ elif name == 'LIST_FOLDER':
1539
+ path = self._resolve_path(tool.get('path', '.'), project_root)
1540
+ if path.is_dir():
1541
+ items = list(path.iterdir())[:50]
1542
+ listing = '\n'.join(f" {'[D]' if p.is_dir() else '[F]'} {p.name}" for p in items)
1543
+ return f"LIST_FOLDER {path}:\n{listing}"
1544
+ return f"LIST_FOLDER ERROR: Not a directory: {path}"
1545
+
1546
+ elif name == 'SEARCH_FILES':
1547
+ pattern = tool.get('pattern', '*')
1548
+ base = self._resolve_path(tool.get('path', '.'), project_root)
1549
+ matches = list(base.glob(pattern))[:100]
1550
+ return f"SEARCH_FILES {pattern}:\n" + '\n'.join(f" {m}" for m in matches)
1551
+
1552
+ elif name == 'GREP':
1553
+ pattern = tool.get('pattern', '')
1554
+ path = self._resolve_path(tool.get('path', '.'), project_root)
1555
+ results = []
1556
+ if path.is_file():
1557
+ files_to_search = [path]
1558
+ else:
1559
+ files_to_search = list(path.rglob('*'))[:50]
1560
+ for f in files_to_search:
1561
+ if f.is_file() and f.suffix in ['.cpp', '.h', '.py', '.cp', '.md', '.txt', '.hpp', '.c']:
1562
+ try:
1563
+ content = f.read_text(encoding='utf-8', errors='replace')
1564
+ for i, line in enumerate(content.split('\n'), 1):
1565
+ if re_module.search(pattern, line, re_module.IGNORECASE):
1566
+ results.append(f" {f}:{i}: {line[:100]}")
1567
+ except:
1568
+ pass
1569
+ return f"GREP {pattern}:\n" + '\n'.join(results[:50])
1570
+
1571
+ elif name == 'RUN_CMD':
1572
+ cmd = tool.get('cmd', '')
1573
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60, cwd=project_root)
1574
+ output = result.stdout[:2000] if result.returncode == 0 else result.stderr[:2000]
1575
+ return f"RUN_CMD `{cmd}`:\nExit: {result.returncode}\n{output}"
1576
+
1577
+ elif name == 'INCLUDECPP_CMD':
1578
+ cmd = tool.get('cmd', '')
1579
+ full_cmd = f'includecpp {cmd}'
1580
+ result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=120, cwd=project_root)
1581
+ output = result.stdout[:2000] if result.returncode == 0 else result.stderr[:2000]
1582
+ return f"INCLUDECPP_CMD `{cmd}`:\nExit: {result.returncode}\n{output}"
1583
+
1584
+ else:
1585
+ return f"UNKNOWN TOOL: {name}"
1586
+
1587
+ except Exception as e:
1588
+ return f"TOOL ERROR ({name}): {str(e)}"
1589
+
1590
+ def _resolve_path(self, path_str: str, project_root: Path) -> Path:
1591
+ """Resolve path relative to project root or absolute."""
1592
+ if not path_str:
1593
+ return project_root
1594
+ p = Path(path_str)
1595
+ if p.is_absolute():
1596
+ return p
1597
+ return project_root / p
1598
+
1599
+ def _build_file_context(self, files: Dict[str, str], max_lines: int) -> str:
1600
+ """Build file context string with line limits."""
1601
+ context_parts = []
1602
+ for path, content in files.items():
1603
+ lines = content.split('\n')
1604
+ if len(lines) > max_lines:
1605
+ preview = '\n'.join(lines[:max_lines])
1606
+ preview += f'\n... ({len(lines) - max_lines} more lines)'
1607
+ else:
1608
+ preview = content
1609
+ ext = Path(path).suffix
1610
+ lang = 'cpp' if ext in ['.cpp', '.h', '.hpp', '.c'] else 'python' if ext == '.py' else ''
1611
+ context_parts.append(f'\nFILE: {path}\n```{lang}\n{preview}\n```')
1612
+ return '\n'.join(context_parts)
1613
+
1614
+ def get_tools_info(self) -> str:
1615
+ """Return formatted tools list for ai tools command."""
1616
+ lines = ["Available AI Tools:", ""]
1617
+ for name, info in GENERATE_TOOLS.items():
1618
+ lines.append(f" {name}")
1619
+ lines.append(f" {info['desc']}")
1620
+ fmt_line = info['format'].split('\n')[0]
1621
+ lines.append(f" Format: {fmt_line}")
1622
+ lines.append("")
1623
+ return '\n'.join(lines)
1624
+
1625
+ def _parse_file_changes(self, response: str) -> List[Dict]:
1626
+ import re
1627
+
1628
+ # Only return empty if response ONLY contains "no changes" with no code blocks
1629
+ lower_response = response.lower()
1630
+ has_code_blocks = '```' in response
1631
+ no_change_phrases = [
1632
+ 'no_changes_needed', 'no_issues_found', 'no changes are needed',
1633
+ 'no changes necessary', 'no modifications required'
1634
+ ]
1635
+ # Only skip if we have no code AND a definite "no changes" statement
1636
+ if not has_code_blocks and any(p in lower_response for p in no_change_phrases):
1637
+ return []
1638
+
1639
+ changes = []
1640
+ lines = response.split('\n')
1641
+ current_file = None
1642
+ current_content = []
1643
+ current_changes_desc = []
1644
+ in_code_block = False
1645
+ in_changes_section = False
1646
+ confirm_required = []
1647
+ code_block_lang = None
1648
+
1649
+ for line in lines:
1650
+ stripped = line.strip()
1651
+
1652
+ if stripped.startswith('CONFIRM_REQUIRED:'):
1653
+ confirm_required.append(stripped[17:].strip())
1654
+ continue
1655
+
1656
+ # Detect FILE: in various formats
1657
+ file_match = re.match(r'^(?:\*\*)?(?:###?\s*)?FILE[:\s*]+(.+?)(?:\*\*)?$', stripped, re.IGNORECASE)
1658
+ if file_match:
1659
+ if current_file and current_content:
1660
+ changes.append({
1661
+ 'file': current_file,
1662
+ 'content': '\n'.join(current_content),
1663
+ 'changes_desc': current_changes_desc.copy(),
1664
+ 'confirm_required': confirm_required.copy()
1665
+ })
1666
+ confirm_required = []
1667
+ current_file = file_match.group(1).strip().strip('`').strip('*').strip()
1668
+ current_content = []
1669
+ current_changes_desc = []
1670
+ in_code_block = False
1671
+ in_changes_section = False
1672
+ continue
1673
+
1674
+ # Detect CHANGES: section
1675
+ if re.match(r'^(?:\*\*)?(?:###?\s*)?CHANGES[:\s*]+(?:\*\*)?$', stripped, re.IGNORECASE):
1676
+ in_changes_section = True
1677
+ continue
1678
+
1679
+ if in_changes_section and not in_code_block:
1680
+ if stripped.startswith('- ') or stripped.startswith('* '):
1681
+ current_changes_desc.append(stripped[2:])
1682
+ elif stripped.startswith('```'):
1683
+ in_changes_section = False
1684
+ in_code_block = True
1685
+ code_block_lang = stripped[3:].strip()
1686
+ continue
1687
+
1688
+ # Detect code block start
1689
+ if stripped.startswith('```') and not in_code_block:
1690
+ in_code_block = True
1691
+ code_block_lang = stripped[3:].strip()
1692
+ in_changes_section = False
1693
+ continue
1694
+
1695
+ # Detect code block end
1696
+ if stripped == '```' and in_code_block:
1697
+ in_code_block = False
1698
+ code_block_lang = None
1699
+ continue
1700
+
1701
+ # Collect code content
1702
+ if in_code_block and current_file:
1703
+ current_content.append(line)
1704
+
1705
+ # Save last file if exists
1706
+ if current_file and current_content:
1707
+ changes.append({
1708
+ 'file': current_file,
1709
+ 'content': '\n'.join(current_content),
1710
+ 'changes_desc': current_changes_desc,
1711
+ 'confirm_required': confirm_required
1712
+ })
1713
+
1714
+ # Fallback: try to parse code blocks if no FILE: markers found
1715
+ if not changes and has_code_blocks:
1716
+ changes = self._fallback_parse_code_blocks(response)
1717
+
1718
+ return changes
1719
+
1720
+ def _fallback_parse_code_blocks(self, response: str) -> List[Dict]:
1721
+ """Fallback parser for responses that don't follow the exact format."""
1722
+ import re
1723
+ changes = []
1724
+
1725
+ # Find all code blocks (cpp, c++, c, h, or unmarked)
1726
+ blocks = re.findall(r'```(?:cpp|c\+\+|c|h)?\n(.*?)```', response, re.DOTALL | re.IGNORECASE)
1727
+
1728
+ # Find file references in text
1729
+ file_patterns = [
1730
+ r'(?:file|path|in|modify|update|editing)[:\s]*[`"]?([^\s`"\n]+\.(?:cpp|h|hpp|c))[`"]?',
1731
+ r'[`"]([^\s`"\n]+\.(?:cpp|h|hpp|c))[`"]',
1732
+ r'\b([a-zA-Z_][a-zA-Z0-9_]*\.(?:cpp|h|hpp|c))\b'
1733
+ ]
1734
+ file_matches = []
1735
+ for pattern in file_patterns:
1736
+ file_matches.extend(re.findall(pattern, response, re.IGNORECASE))
1737
+ # Deduplicate while preserving order
1738
+ seen = set()
1739
+ unique_files = []
1740
+ for f in file_matches:
1741
+ if f.lower() not in seen:
1742
+ seen.add(f.lower())
1743
+ unique_files.append(f)
1744
+
1745
+ for idx, content in enumerate(blocks):
1746
+ if not content.strip():
1747
+ continue
1748
+ # Try to match file from references, otherwise use index
1749
+ file_name = unique_files[idx] if idx < len(unique_files) else f'file_{idx}.cpp'
1750
+ changes.append({
1751
+ 'file': file_name,
1752
+ 'content': content.strip(),
1753
+ 'changes_desc': ['Parsed from code block'],
1754
+ 'confirm_required': []
1755
+ })
1756
+
1757
+ return changes
1758
+
1759
+
1760
+ _ai_manager_instance = None
1761
+
1762
+
1763
+ # ============================================================================
1764
+ # Verbose Output System for AI Operations
1765
+ # ============================================================================
1766
+
1767
+ class AIVerboseOutput:
1768
+ """Verbose output manager for AI operations with real-time status updates."""
1769
+
1770
+ # ANSI color codes
1771
+ COLORS = {
1772
+ 'reset': '\033[0m',
1773
+ 'bold': '\033[1m',
1774
+ 'dim': '\033[2m',
1775
+ 'cyan': '\033[36m',
1776
+ 'green': '\033[32m',
1777
+ 'yellow': '\033[33m',
1778
+ 'magenta': '\033[35m',
1779
+ 'blue': '\033[34m',
1780
+ 'red': '\033[31m',
1781
+ 'white': '\033[37m',
1782
+ }
1783
+
1784
+ # Status icons and messages - with ASCII fallbacks for Windows
1785
+ PHASES = {
1786
+ 'init': ('*' if not _UNICODE_OK else '⚙', 'cyan', 'Initializing'),
1787
+ 'context': ('*' if not _UNICODE_OK else '📋', 'blue', 'Building context'),
1788
+ 'thinking': ('*' if not _UNICODE_OK else '🧠', 'magenta', 'Thinking'),
1789
+ 'planning': ('*' if not _UNICODE_OK else '📝', 'yellow', 'Planning'),
1790
+ 'analyzing': ('>' if not _UNICODE_OK else '🔍', 'cyan', 'Analyzing'),
1791
+ 'generating': ('+' if not _UNICODE_OK else '✨', 'green', 'Generating'),
1792
+ 'writing': ('>' if not _UNICODE_OK else '📄', 'blue', 'Writing'),
1793
+ 'editing': ('>' if not _UNICODE_OK else '✏️', 'yellow', 'Editing'),
1794
+ 'reading': ('>' if not _UNICODE_OK else '👁', 'cyan', 'Reading'),
1795
+ 'searching': ('>' if not _UNICODE_OK else '🔎', 'blue', 'Searching'),
1796
+ 'executing': ('!' if not _UNICODE_OK else '⚡', 'magenta', 'Executing'),
1797
+ 'converting': ('~' if not _UNICODE_OK else '🔄', 'cyan', 'Converting'),
1798
+ 'optimizing': ('!' if not _UNICODE_OK else '⚡', 'green', 'Optimizing'),
1799
+ 'websearch': ('@' if not _UNICODE_OK else '🌐', 'blue', 'Web searching'),
1800
+ 'parsing': ('>' if not _UNICODE_OK else '📊', 'cyan', 'Parsing response'),
1801
+ 'applying': ('+' if not _UNICODE_OK else '💾', 'green', 'Applying changes'),
1802
+ 'complete': ('[OK]' if not _UNICODE_OK else '✅', 'green', 'Complete'),
1803
+ 'error': ('[ERR]' if not _UNICODE_OK else '❌', 'red', 'Error'),
1804
+ 'warning': ('[!]' if not _UNICODE_OK else '⚠️', 'yellow', 'Warning'),
1805
+ 'waiting': ('...' if not _UNICODE_OK else '⏳', 'dim', 'Waiting for API'),
1806
+ 'tool': ('#' if not _UNICODE_OK else '🔧', 'cyan', 'Running tool'),
1807
+ }
1808
+
1809
+ def __init__(self, enabled: bool = True, use_colors: bool = True):
1810
+ self.enabled = enabled
1811
+ self.use_colors = use_colors and self._supports_color()
1812
+ self.current_phase = None
1813
+ self.indent_level = 0
1814
+ self.start_time = None
1815
+ self._last_line_length = 0
1816
+
1817
+ def _supports_color(self) -> bool:
1818
+ """Check if terminal supports colors."""
1819
+ import sys
1820
+ if not hasattr(sys.stdout, 'isatty'):
1821
+ return False
1822
+ if not sys.stdout.isatty():
1823
+ return False
1824
+ import os
1825
+ if os.name == 'nt':
1826
+ # Windows: enable ANSI support
1827
+ try:
1828
+ import ctypes
1829
+ kernel32 = ctypes.windll.kernel32
1830
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
1831
+ return True
1832
+ except Exception:
1833
+ return False
1834
+ return True
1835
+
1836
+ def _color(self, text: str, color: str) -> str:
1837
+ """Apply color to text if enabled."""
1838
+ if not self.use_colors:
1839
+ return text
1840
+ return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
1841
+
1842
+ def _get_indent(self) -> str:
1843
+ """Get current indentation."""
1844
+ return " " * self.indent_level
1845
+
1846
+ def start(self, operation: str):
1847
+ """Start verbose output for an operation."""
1848
+ if not self.enabled:
1849
+ return
1850
+ import time
1851
+ self.start_time = time.time()
1852
+ print()
1853
+ print(self._color("=" * 60, 'dim'))
1854
+ print(self._color(f" AI Operation: {operation}", 'bold'))
1855
+ print(self._color("=" * 60, 'dim'))
1856
+ print()
1857
+
1858
+ def end(self, success: bool = True, message: str = None):
1859
+ """End verbose output."""
1860
+ if not self.enabled:
1861
+ return
1862
+ import time
1863
+ elapsed = time.time() - self.start_time if self.start_time else 0
1864
+ print()
1865
+ print(self._color("-" * 60, 'dim'))
1866
+ if success:
1867
+ icon, color, _ = self.PHASES['complete']
1868
+ status = message or "Operation completed successfully"
1869
+ print(f" {icon} {self._color(status, color)}")
1870
+ else:
1871
+ icon, color, _ = self.PHASES['error']
1872
+ status = message or "Operation failed"
1873
+ print(f" {icon} {self._color(status, color)}")
1874
+ print(f" {self._color(f'Time: {elapsed:.2f}s', 'dim')}")
1875
+ print(self._color("=" * 60, 'dim'))
1876
+ print()
1877
+
1878
+ def phase(self, phase_name: str, detail: str = None):
1879
+ """Show a new phase in the operation."""
1880
+ if not self.enabled:
1881
+ return
1882
+ if phase_name not in self.PHASES:
1883
+ phase_name = 'thinking'
1884
+ icon, color, label = self.PHASES[phase_name]
1885
+ indent = self._get_indent()
1886
+ if detail:
1887
+ print(f"{indent}{icon} {self._color(label, color)}: {detail}")
1888
+ else:
1889
+ print(f"{indent}{icon} {self._color(label, color)}...")
1890
+ self.current_phase = phase_name
1891
+
1892
+ def status(self, message: str, phase: str = None):
1893
+ """Show a status message."""
1894
+ if not self.enabled:
1895
+ return
1896
+ indent = self._get_indent()
1897
+ if phase and phase in self.PHASES:
1898
+ icon, color, _ = self.PHASES[phase]
1899
+ print(f"{indent}{icon} {self._color(message, color)}")
1900
+ else:
1901
+ print(f"{indent} {self._color(message, 'dim')}")
1902
+
1903
+ def detail(self, label: str, value: str):
1904
+ """Show a detail line."""
1905
+ if not self.enabled:
1906
+ return
1907
+ indent = self._get_indent()
1908
+ print(f"{indent} {self._color(label + ':', 'dim')} {value}")
1909
+
1910
+ def progress(self, current: int, total: int, label: str = None):
1911
+ """Show progress indicator."""
1912
+ if not self.enabled:
1913
+ return
1914
+ indent = self._get_indent()
1915
+ pct = int((current / total) * 100) if total > 0 else 0
1916
+ bar_width = 30
1917
+ filled = int(bar_width * current / total) if total > 0 else 0
1918
+ fill_char = '#' if not _UNICODE_OK else '█'
1919
+ empty_char = '-' if not _UNICODE_OK else '░'
1920
+ bar = fill_char * filled + empty_char * (bar_width - filled)
1921
+ label_str = f" {label}" if label else ""
1922
+ print(f"\r{indent} [{self._color(bar, 'cyan')}] {pct}%{label_str}", end='', flush=True)
1923
+ if current >= total:
1924
+ print()
1925
+
1926
+ def tool_call(self, tool_name: str, params: dict = None):
1927
+ """Show a tool being called."""
1928
+ if not self.enabled:
1929
+ return
1930
+ indent = self._get_indent()
1931
+ icon, color, _ = self.PHASES['tool']
1932
+ print(f"{indent}{icon} {self._color('Tool:', color)} {self._color(tool_name, 'bold')}")
1933
+ if params:
1934
+ for key, value in params.items():
1935
+ val_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
1936
+ print(f"{indent} {self._color(key + ':', 'dim')} {val_str}")
1937
+
1938
+ def tool_result(self, success: bool, message: str = None):
1939
+ """Show tool result."""
1940
+ if not self.enabled:
1941
+ return
1942
+ indent = self._get_indent()
1943
+ if success:
1944
+ icon = SYM_CHECK
1945
+ color = 'green'
1946
+ else:
1947
+ icon = SYM_CROSS
1948
+ color = 'red'
1949
+ msg = message[:60] + "..." if message and len(message) > 60 else (message or "")
1950
+ print(f"{indent} {self._color(icon, color)} {msg}")
1951
+
1952
+ def context_info(self, files: int = 0, lines: int = 0, tokens: int = 0, model: str = None):
1953
+ """Show context information."""
1954
+ if not self.enabled:
1955
+ return
1956
+ indent = self._get_indent()
1957
+ print(f"{indent} {self._color('Context:', 'cyan')}")
1958
+ if files > 0:
1959
+ print(f"{indent} Files: {files}")
1960
+ if lines > 0:
1961
+ print(f"{indent} Lines: {lines:,}")
1962
+ if tokens > 0:
1963
+ print(f"{indent} Tokens: ~{tokens:,}")
1964
+ if model:
1965
+ print(f"{indent} Model: {model}")
1966
+
1967
+ def api_call(self, endpoint: str = None, tokens_in: int = 0, tokens_out: int = 0):
1968
+ """Show API call information."""
1969
+ if not self.enabled:
1970
+ return
1971
+ indent = self._get_indent()
1972
+ print(f"{indent} {self._color('API Call:', 'blue')}")
1973
+ if endpoint:
1974
+ print(f"{indent} Endpoint: {endpoint}")
1975
+ if tokens_in > 0:
1976
+ print(f"{indent} Input tokens: ~{tokens_in:,}")
1977
+
1978
+ def api_response(self, tokens: int = 0, time_ms: int = 0):
1979
+ """Show API response information."""
1980
+ if not self.enabled:
1981
+ return
1982
+ indent = self._get_indent()
1983
+ if tokens > 0:
1984
+ print(f"{indent} Output tokens: ~{tokens:,}")
1985
+ if time_ms > 0:
1986
+ print(f"{indent} Response time: {time_ms}ms")
1987
+
1988
+ def code_block(self, filename: str, lines: int = 0, lang: str = None):
1989
+ """Show code block being processed."""
1990
+ if not self.enabled:
1991
+ return
1992
+ indent = self._get_indent()
1993
+ lang_str = f" ({lang})" if lang else ""
1994
+ lines_str = f" [{lines} lines]" if lines > 0 else ""
1995
+ print(f"{indent} 📄 {self._color(filename, 'cyan')}{lang_str}{lines_str}")
1996
+
1997
+ def changes_summary(self, changes=None, files_changed: int = 0, lines_added: int = 0, lines_removed: int = 0):
1998
+ """Show summary of changes.
1999
+
2000
+ Args:
2001
+ changes: List of dicts with 'file' and 'changes' keys, OR None
2002
+ files_changed: Number of files changed (if not using changes list)
2003
+ lines_added: Lines added (if not using changes list)
2004
+ lines_removed: Lines removed (if not using changes list)
2005
+ """
2006
+ if not self.enabled:
2007
+ return
2008
+ indent = self._get_indent()
2009
+ print(f"{indent} {self._color('Changes:', 'yellow')}")
2010
+
2011
+ # Handle list of change dicts
2012
+ if isinstance(changes, list):
2013
+ for change in changes:
2014
+ if isinstance(change, dict):
2015
+ fname = change.get('file', 'unknown')
2016
+ change_list = change.get('changes', [])
2017
+ is_new = change.get('new', False)
2018
+ prefix = "[NEW] " if is_new else ""
2019
+ print(f"{indent} {prefix}{self._color(fname, 'cyan')}")
2020
+ for c in change_list:
2021
+ print(f"{indent} - {c}")
2022
+ return
2023
+
2024
+ # Handle integer format (legacy)
2025
+ if files_changed > 0:
2026
+ print(f"{indent} Files: {files_changed}")
2027
+ if lines_added > 0:
2028
+ print(f"{indent} Added: {self._color(f'+{lines_added}', 'green')}")
2029
+ if lines_removed > 0:
2030
+ print(f"{indent} Removed: {self._color(f'-{lines_removed}', 'red')}")
2031
+
2032
+ def section(self, title: str):
2033
+ """Show a section header."""
2034
+ if not self.enabled:
2035
+ return
2036
+ print()
2037
+ print(f" {self._color('─' * 40, 'dim')}")
2038
+ print(f" {self._color(title, 'bold')}")
2039
+ print(f" {self._color('─' * 40, 'dim')}")
2040
+
2041
+ def indent(self):
2042
+ """Increase indentation."""
2043
+ self.indent_level += 1
2044
+
2045
+ def dedent(self):
2046
+ """Decrease indentation."""
2047
+ self.indent_level = max(0, self.indent_level - 1)
2048
+
2049
+ def warning(self, message: str):
2050
+ """Show a warning."""
2051
+ if not self.enabled:
2052
+ return
2053
+ icon, color, _ = self.PHASES['warning']
2054
+ print(f" {icon} {self._color(message, color)}")
2055
+
2056
+ def error(self, message: str):
2057
+ """Show an error."""
2058
+ if not self.enabled:
2059
+ return
2060
+ icon, color, _ = self.PHASES['error']
2061
+ print(f" {icon} {self._color(message, color)}")
2062
+
2063
+ def thinking_indicator(self, message: str = "Processing"):
2064
+ """Show a thinking indicator (for long operations)."""
2065
+ if not self.enabled:
2066
+ return
2067
+ icon, color, _ = self.PHASES['thinking']
2068
+ print(f" {icon} {self._color(message, color)}...", end='', flush=True)
2069
+
2070
+ def thinking_done(self):
2071
+ """Complete thinking indicator."""
2072
+ if not self.enabled:
2073
+ return
2074
+ print(f" {self._color('done', 'green')}")
2075
+
2076
+ def websearch_result(self, query: str, results: int = 0):
2077
+ """Show websearch results."""
2078
+ if not self.enabled:
2079
+ return
2080
+ indent = self._get_indent()
2081
+ icon, color, _ = self.PHASES['websearch']
2082
+ print(f"{indent}{icon} {self._color('Web search:', color)} \"{query}\"")
2083
+ if results > 0:
2084
+ print(f"{indent} Found: {results} results")
2085
+
2086
+ def file_operation(self, operation: str, path: str, success: bool = True):
2087
+ """Show a file operation."""
2088
+ if not self.enabled:
2089
+ return
2090
+ indent = self._get_indent()
2091
+ if _UNICODE_OK:
2092
+ ops = {
2093
+ 'read': ('👁', 'Reading'),
2094
+ 'write': ('📝', 'Writing'),
2095
+ 'edit': ('✏️', 'Editing'),
2096
+ 'delete': ('🗑', 'Deleting'),
2097
+ 'create': ('📁', 'Creating'),
2098
+ }
2099
+ default_icon = '📄'
2100
+ else:
2101
+ ops = {
2102
+ 'read': ('>', 'Reading'),
2103
+ 'write': ('>', 'Writing'),
2104
+ 'edit': ('>', 'Editing'),
2105
+ 'delete': ('x', 'Deleting'),
2106
+ 'create': ('+', 'Creating'),
2107
+ }
2108
+ default_icon = '>'
2109
+ icon, label = ops.get(operation, (default_icon, operation))
2110
+ status = self._color(SYM_CHECK, 'green') if success else self._color(SYM_CROSS, 'red')
2111
+ # Truncate path if too long
2112
+ display_path = path if len(path) <= 40 else "..." + path[-37:]
2113
+ print(f"{indent} {icon} {label}: {display_path} {status}")
2114
+
2115
+
2116
+ # Global verbose output instance
2117
+ _verbose_output = None
2118
+
2119
+
2120
+ def get_verbose_output(enabled: bool = True) -> AIVerboseOutput:
2121
+ """Get or create verbose output instance."""
2122
+ global _verbose_output
2123
+ if _verbose_output is None or _verbose_output.enabled != enabled:
2124
+ _verbose_output = AIVerboseOutput(enabled=enabled)
2125
+ return _verbose_output
2126
+
2127
+
2128
+ def get_ai_manager() -> AIManager:
2129
+ global _ai_manager_instance
2130
+ if _ai_manager_instance is None:
2131
+ _ai_manager_instance = AIManager()
2132
+ return _ai_manager_instance