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.
- includecpp/__init__.py +59 -0
- includecpp/__init__.pyi +255 -0
- includecpp/__main__.py +4 -0
- includecpp/cli/__init__.py +4 -0
- includecpp/cli/commands.py +8270 -0
- includecpp/cli/config_parser.py +127 -0
- includecpp/core/__init__.py +19 -0
- includecpp/core/ai_integration.py +2132 -0
- includecpp/core/build_manager.py +2416 -0
- includecpp/core/cpp_api.py +376 -0
- includecpp/core/cpp_api.pyi +95 -0
- includecpp/core/cppy_converter.py +3448 -0
- includecpp/core/cssl/CSSL_DOCUMENTATION.md +2075 -0
- includecpp/core/cssl/__init__.py +42 -0
- includecpp/core/cssl/cssl_builtins.py +2271 -0
- includecpp/core/cssl/cssl_builtins.pyi +1393 -0
- includecpp/core/cssl/cssl_events.py +621 -0
- includecpp/core/cssl/cssl_modules.py +2803 -0
- includecpp/core/cssl/cssl_parser.py +2575 -0
- includecpp/core/cssl/cssl_runtime.py +3051 -0
- includecpp/core/cssl/cssl_syntax.py +488 -0
- includecpp/core/cssl/cssl_types.py +1512 -0
- includecpp/core/cssl_bridge.py +882 -0
- includecpp/core/cssl_bridge.pyi +488 -0
- includecpp/core/error_catalog.py +802 -0
- includecpp/core/error_formatter.py +1016 -0
- includecpp/core/exceptions.py +97 -0
- includecpp/core/path_discovery.py +77 -0
- includecpp/core/project_ui.py +3370 -0
- includecpp/core/settings_ui.py +326 -0
- includecpp/generator/__init__.py +1 -0
- includecpp/generator/parser.cpp +1903 -0
- includecpp/generator/parser.h +281 -0
- includecpp/generator/type_resolver.cpp +363 -0
- includecpp/generator/type_resolver.h +68 -0
- includecpp/py.typed +0 -0
- includecpp/templates/cpp.proj.template +18 -0
- includecpp/vscode/__init__.py +1 -0
- includecpp/vscode/cssl/__init__.py +1 -0
- includecpp/vscode/cssl/language-configuration.json +38 -0
- includecpp/vscode/cssl/package.json +50 -0
- includecpp/vscode/cssl/snippets/cssl.snippets.json +1080 -0
- includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +341 -0
- includecpp-3.7.3.dist-info/METADATA +1076 -0
- includecpp-3.7.3.dist-info/RECORD +49 -0
- includecpp-3.7.3.dist-info/WHEEL +5 -0
- includecpp-3.7.3.dist-info/entry_points.txt +2 -0
- includecpp-3.7.3.dist-info/licenses/LICENSE +21 -0
- 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
|