kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -96,7 +96,16 @@ class FileOperationParser:
96
96
  re.DOTALL | re.IGNORECASE
97
97
  )
98
98
 
99
- logger.debug("File operation parser initialized with 14 operation patterns")
99
+ # Special pattern for agent/skill file generation
100
+ # Uses @@@FILE/@@@END syntax to avoid XML tag conflicts
101
+ # Format: @@@FILE path/to/file.md\ncontent\n@@@END
102
+ # Path must contain / to be valid (prevents matching garbage like "@@@FILE blocks.")
103
+ self.agent_files_pattern = re.compile(
104
+ r'@@@FILE\s+(\S+/\S+)\n(.*?)@@@END',
105
+ re.DOTALL
106
+ )
107
+
108
+ logger.debug("File operation parser initialized with 15 operation patterns")
100
109
 
101
110
  def parse_response(self, llm_response: str) -> List[Dict[str, Any]]:
102
111
  """Extract all file operations from LLM response.
@@ -109,7 +118,16 @@ class FileOperationParser:
109
118
  """
110
119
  operations = []
111
120
 
112
- # Parse each operation type in order of appearance
121
+ # FIRST: Parse @@@FILE blocks before any content stripping
122
+ # This protects inner content from being mangled by tag stripping
123
+ agent_files_ops, response_without_agent_files = self._parse_agent_files(llm_response)
124
+ operations.extend(agent_files_ops)
125
+
126
+ # Use the cleaned response for remaining parsing
127
+ llm_response = response_without_agent_files
128
+
129
+ # Parse operations that contain <content> blocks from full text first
130
+ # These need the full response to extract their content properly
113
131
  operations.extend(self._parse_operations(
114
132
  self.edit_pattern, self._parse_edit_block, llm_response, "edit"
115
133
  ))
@@ -121,40 +139,51 @@ class FileOperationParser:
121
139
  llm_response, "create_overwrite"
122
140
  ))
123
141
  operations.extend(self._parse_operations(
124
- self.delete_pattern, self._parse_delete_block, llm_response, "delete"
142
+ self.append_pattern, self._parse_append_block, llm_response, "append"
125
143
  ))
126
144
  operations.extend(self._parse_operations(
127
- self.move_pattern, self._parse_move_block, llm_response, "move"
145
+ self.insert_after_pattern, self._parse_insert_after_block,
146
+ llm_response, "insert_after"
128
147
  ))
129
148
  operations.extend(self._parse_operations(
130
- self.copy_pattern, self._parse_copy_block, llm_response, "copy"
149
+ self.insert_before_pattern, self._parse_insert_before_block,
150
+ llm_response, "insert_before"
131
151
  ))
152
+
153
+ # Strip <content>...</content> blocks to avoid parsing XML examples
154
+ # inside file content as actual commands (e.g., skill files with <read> examples)
155
+ text_without_content = re.sub(
156
+ r'<content>.*?</content>',
157
+ '',
158
+ llm_response,
159
+ flags=re.DOTALL | re.IGNORECASE
160
+ )
161
+
162
+ # Parse remaining operations from text with content blocks removed
132
163
  operations.extend(self._parse_operations(
133
- self.copy_overwrite_pattern, self._parse_copy_overwrite_block,
134
- llm_response, "copy_overwrite"
164
+ self.delete_pattern, self._parse_delete_block, text_without_content, "delete"
135
165
  ))
136
166
  operations.extend(self._parse_operations(
137
- self.append_pattern, self._parse_append_block, llm_response, "append"
167
+ self.move_pattern, self._parse_move_block, text_without_content, "move"
138
168
  ))
139
169
  operations.extend(self._parse_operations(
140
- self.insert_after_pattern, self._parse_insert_after_block,
141
- llm_response, "insert_after"
170
+ self.copy_pattern, self._parse_copy_block, text_without_content, "copy"
142
171
  ))
143
172
  operations.extend(self._parse_operations(
144
- self.insert_before_pattern, self._parse_insert_before_block,
145
- llm_response, "insert_before"
173
+ self.copy_overwrite_pattern, self._parse_copy_overwrite_block,
174
+ text_without_content, "copy_overwrite"
146
175
  ))
147
176
  operations.extend(self._parse_operations(
148
- self.mkdir_pattern, self._parse_mkdir_block, llm_response, "mkdir"
177
+ self.mkdir_pattern, self._parse_mkdir_block, text_without_content, "mkdir"
149
178
  ))
150
179
  operations.extend(self._parse_operations(
151
- self.rmdir_pattern, self._parse_rmdir_block, llm_response, "rmdir"
180
+ self.rmdir_pattern, self._parse_rmdir_block, text_without_content, "rmdir"
152
181
  ))
153
182
  operations.extend(self._parse_operations(
154
- self.read_pattern, self._parse_read_block, llm_response, "read"
183
+ self.read_pattern, self._parse_read_block, text_without_content, "read"
155
184
  ))
156
185
  operations.extend(self._parse_operations(
157
- self.grep_pattern, self._parse_grep_block, llm_response, "grep"
186
+ self.grep_pattern, self._parse_grep_block, text_without_content, "grep"
158
187
  ))
159
188
 
160
189
  if operations:
@@ -162,6 +191,49 @@ class FileOperationParser:
162
191
 
163
192
  return operations
164
193
 
194
+ def _parse_agent_files(self, llm_response: str) -> Tuple[List[Dict[str, Any]], str]:
195
+ """Parse @@@FILE blocks and extract file operations.
196
+
197
+ Uses simple line-based syntax to avoid XML tag conflicts:
198
+ @@@FILE path/to/file.md
199
+ content with <create> <edit> examples - no conflicts
200
+ @@@END
201
+
202
+ This parser runs FIRST to protect inner content from tag stripping.
203
+
204
+ Args:
205
+ llm_response: Raw LLM response text
206
+
207
+ Returns:
208
+ Tuple of (operations list, response with @@@FILE blocks removed)
209
+ """
210
+ operations = []
211
+ cleaned_response = llm_response
212
+
213
+ for i, match in enumerate(self.agent_files_pattern.finditer(llm_response)):
214
+ filepath = match.group(1).strip()
215
+ content = match.group(2)
216
+
217
+ # Remove trailing newline if present (before @@@END)
218
+ if content.endswith('\n'):
219
+ content = content[:-1]
220
+
221
+ operations.append({
222
+ "type": "file_create",
223
+ "id": f"agent_file_{i}",
224
+ "file": filepath,
225
+ "content": content
226
+ })
227
+ logger.debug(f"Parsed agent file operation: {filepath}")
228
+
229
+ # Remove this block from the response
230
+ cleaned_response = cleaned_response.replace(match.group(0), '', 1)
231
+
232
+ if operations:
233
+ logger.info(f"Parsed {len(operations)} file operations from @@@FILE blocks")
234
+
235
+ return operations, cleaned_response
236
+
165
237
  def _parse_operations(
166
238
  self,
167
239
  pattern: re.Pattern,
@@ -362,6 +434,8 @@ class FileOperationParser:
362
434
  """Parse <read> block."""
363
435
  file_path = self._extract_tag("file", content).strip()
364
436
  lines_spec = self._extract_tag("lines", content, required=False)
437
+ offset = self._extract_tag("offset", content, required=False)
438
+ limit = self._extract_tag("limit", content, required=False)
365
439
 
366
440
  result = {
367
441
  "type": "file_read",
@@ -370,6 +444,10 @@ class FileOperationParser:
370
444
 
371
445
  if lines_spec:
372
446
  result["lines"] = lines_spec.strip()
447
+ if offset:
448
+ result["offset"] = int(offset.strip())
449
+ if limit:
450
+ result["limit"] = int(limit.strip())
373
451
 
374
452
  return result
375
453
 
@@ -422,11 +500,31 @@ class ResponseParser:
422
500
  re.DOTALL | re.IGNORECASE
423
501
  )
424
502
 
503
+ # Native-style tool_call tags: <tool_call>name</tool_call> or with JSON args
504
+ # Supports: <tool_call>search_nodes</tool_call>
505
+ # <tool_call>{"name": "search_nodes", "arguments": {...}}</tool_call>
506
+ self.tool_call_pattern = re.compile(
507
+ r'<tool_call>(.*?)</tool_call>',
508
+ re.DOTALL | re.IGNORECASE
509
+ )
510
+
511
+ # Question gate tags - suspend tool execution when present
512
+ self.question_pattern = re.compile(
513
+ r'<question>(.*?)</question>',
514
+ re.DOTALL | re.IGNORECASE
515
+ )
516
+
425
517
  # File operations parser
426
518
  self.file_ops_parser = FileOperationParser()
427
519
 
428
520
  logger.info("Response parser initialized with comprehensive tag support + file operations")
429
521
 
522
+ async def initialize(self) -> bool:
523
+ """Initialize the response parser."""
524
+ self.is_initialized = True
525
+ logger.debug("Response parser async initialization complete")
526
+ return True
527
+
430
528
  def parse_response(self, raw_response: str) -> Dict[str, Any]:
431
529
  """Parse LLM response and extract all components.
432
530
 
@@ -449,13 +547,21 @@ class ResponseParser:
449
547
  logger.warning(f"🔍 BUG-011 DIAGNOSTIC: Found {abs(orphaned_closes)} orphaned <think> tags (unclosed)")
450
548
 
451
549
  # Extract all components
452
- thinking_blocks = self._extract_thinking(raw_response)
453
- terminal_commands = self._extract_terminal_commands(raw_response)
454
- tool_calls = self._extract_tool_calls(raw_response)
550
+ # IMPORTANT: Parse @@@FILE blocks FIRST to get cleaned response
551
+ # This prevents XML examples inside @@@FILE from being executed
455
552
  file_operations = self.file_ops_parser.parse_response(raw_response)
456
553
 
457
- # Clean content (remove all tags)
458
- clean_content = self._clean_content(raw_response)
554
+ # Get response with @@@FILE blocks removed for other parsing
555
+ _, response_without_agent_files = self.file_ops_parser._parse_agent_files(raw_response)
556
+
557
+ # Extract other tools from the CLEANED response (not raw)
558
+ thinking_blocks = self._extract_thinking(response_without_agent_files)
559
+ terminal_commands = self._extract_terminal_commands(response_without_agent_files)
560
+ tool_calls = self._extract_tool_calls(response_without_agent_files)
561
+ question_content = self._extract_question(response_without_agent_files)
562
+
563
+ # Clean content (remove all tags) - use cleaned response
564
+ clean_content = self._clean_content(response_without_agent_files)
459
565
 
460
566
  # DIAGNOSTIC: Verify defensive fix effectiveness
461
567
  if '</think>' in clean_content or '<think>' in clean_content:
@@ -466,29 +572,36 @@ class ResponseParser:
466
572
  elif orphaned_closes > 0:
467
573
  logger.info(f"✅ BUG-011 SUCCESS: Defensive fix removed {orphaned_closes} orphaned tags")
468
574
 
469
- # Determine if turn is completed (no tools pending execution)
470
- turn_completed = (
471
- len(terminal_commands) == 0 and
472
- len(tool_calls) == 0 and
473
- len(file_operations) == 0
474
- )
575
+ # Count total tools
576
+ total_tools = len(terminal_commands) + len(tool_calls) + len(file_operations)
577
+
578
+ # Question gate: if question present, mark turn as completed but flag tools as pending
579
+ # This causes the system to stop and wait for user input
580
+ has_question = question_content is not None
581
+
582
+ # Determine if turn is completed
583
+ # Turn is completed if: no tools OR question present (tools suspended)
584
+ turn_completed = (total_tools == 0) or has_question
475
585
 
476
586
  parsed = {
477
587
  "raw": raw_response,
478
588
  "content": clean_content,
479
589
  "turn_completed": turn_completed,
590
+ "question_gate_active": has_question and total_tools > 0, # Tools suspended
480
591
  "components": {
481
592
  "thinking": thinking_blocks,
482
593
  "terminal_commands": terminal_commands,
483
594
  "tool_calls": tool_calls,
484
- "file_operations": file_operations
595
+ "file_operations": file_operations,
596
+ "question": question_content
485
597
  },
486
598
  "metadata": {
487
599
  "has_thinking": bool(thinking_blocks),
488
600
  "has_terminal_commands": bool(terminal_commands),
489
601
  "has_tool_calls": bool(tool_calls),
490
602
  "has_file_operations": bool(file_operations),
491
- "total_tools": len(terminal_commands) + len(tool_calls) + len(file_operations),
603
+ "has_question": has_question,
604
+ "total_tools": total_tools,
492
605
  "content_length": len(clean_content)
493
606
  }
494
607
  }
@@ -501,28 +614,53 @@ class ResponseParser:
501
614
 
502
615
  def _extract_thinking(self, content: str) -> List[str]:
503
616
  """Extract thinking content blocks.
504
-
617
+
505
618
  Args:
506
619
  content: Raw response content
507
-
620
+
508
621
  Returns:
509
622
  List of thinking content strings
510
623
  """
511
624
  matches = self.thinking_pattern.findall(content)
512
625
  return [match.strip() for match in matches if match.strip()]
626
+
627
+ def _extract_question(self, content: str) -> Optional[str]:
628
+ """Extract question gate content.
629
+
630
+ When a <question> tag is present, the agent is asking for user input
631
+ and all tool calls should be suspended until the user responds.
632
+
633
+ Args:
634
+ content: Raw response content
635
+
636
+ Returns:
637
+ Question content if found, None otherwise
638
+ """
639
+ match = self.question_pattern.search(content)
640
+ if match:
641
+ return match.group(1).strip()
642
+ return None
513
643
 
514
644
  def _extract_terminal_commands(self, content: str) -> List[Dict[str, Any]]:
515
645
  """Extract terminal command blocks.
516
-
646
+
517
647
  Args:
518
648
  content: Raw response content
519
-
649
+
520
650
  Returns:
521
651
  List of terminal command dictionaries
522
652
  """
653
+ # Strip <content> blocks to avoid parsing terminal examples inside file content
654
+ text_without_content = re.sub(
655
+ r'<content>.*?</content>',
656
+ '',
657
+ content,
658
+ flags=re.DOTALL | re.IGNORECASE
659
+ )
660
+
523
661
  commands = []
524
- matches = self.terminal_pattern.findall(content)
525
-
662
+ matches = self.terminal_pattern.findall(text_without_content)
663
+
526
664
  for i, match in enumerate(matches):
527
665
  command = match.strip()
528
666
  if command:
@@ -532,49 +670,122 @@ class ResponseParser:
532
670
  "command": command,
533
671
  "raw": match
534
672
  })
535
-
673
+
536
674
  return commands
537
675
 
538
676
  def _extract_tool_calls(self, content: str) -> List[Dict[str, Any]]:
539
- """Extract MCP tool call blocks.
540
-
677
+ """Extract MCP tool call blocks from both <tool> and <tool_call> tags.
678
+
679
+ Supports:
680
+ - <tool name="tool_name" arg="value">content</tool>
681
+ - <tool_call>tool_name</tool_call>
682
+ - <tool_call>{"name": "tool_name", "arguments": {...}}</tool_call>
683
+
541
684
  Args:
542
685
  content: Raw response content
543
-
686
+
544
687
  Returns:
545
688
  List of tool call dictionaries
546
689
  """
547
690
  tool_calls = []
548
- matches = self.tool_pattern.findall(content)
549
-
550
- for i, (attributes_str, tool_content) in enumerate(matches):
691
+ tool_index = 0
692
+
693
+ # Extract <tool> style calls (attribute-based)
694
+ for match in self.tool_pattern.finditer(content):
695
+ attributes_str, tool_content = match.groups()
551
696
  try:
552
- # Parse tool attributes
553
697
  tool_info = self._parse_tool_attributes(attributes_str)
554
-
555
- # Build tool call
556
- tool_call = {
698
+ tool_calls.append({
557
699
  "type": "mcp_tool",
558
- "id": f"mcp_tool_{i}",
700
+ "id": f"mcp_tool_{tool_index}",
559
701
  "name": tool_info.get("name", "unknown"),
560
702
  "arguments": tool_info.get("arguments", {}),
561
703
  "content": tool_content.strip(),
562
- "raw": f"<tool {attributes_str}>{tool_content}</tool>"
563
- }
564
-
704
+ "raw": match.group(0)
705
+ })
706
+ tool_index += 1
707
+ except Exception as e:
708
+ logger.warning(f"Failed to parse <tool> call: {e}")
709
+ tool_calls.append({
710
+ "type": "malformed_tool",
711
+ "id": f"malformed_{tool_index}",
712
+ "error": str(e),
713
+ "raw": match.group(0)
714
+ })
715
+ tool_index += 1
716
+
717
+ # Extract <tool_call> style calls (content-based)
718
+ for match in self.tool_call_pattern.finditer(content):
719
+ call_content = match.group(1).strip()
720
+ try:
721
+ tool_call = self._parse_tool_call_content(call_content, tool_index)
565
722
  tool_calls.append(tool_call)
566
-
723
+ tool_index += 1
567
724
  except Exception as e:
568
- logger.warning(f"Failed to parse tool call: {e}")
569
- # Add as malformed tool call for debugging
725
+ logger.warning(f"Failed to parse <tool_call> content: {e}")
570
726
  tool_calls.append({
571
727
  "type": "malformed_tool",
572
- "id": f"malformed_{i}",
728
+ "id": f"malformed_{tool_index}",
573
729
  "error": str(e),
574
- "raw": f"<tool {attributes_str}>{tool_content}</tool>"
730
+ "raw": match.group(0)
575
731
  })
576
-
732
+ tool_index += 1
733
+
577
734
  return tool_calls
735
+
736
+ def _parse_tool_call_content(self, content: str, index: int) -> Dict[str, Any]:
737
+ """Parse content from <tool_call> tags.
738
+
739
+ Supports:
740
+ - Simple name: "search_nodes"
741
+ - JSON format: {"name": "search_nodes", "arguments": {"query": "test"}}
742
+
743
+ Args:
744
+ content: Content between <tool_call> tags
745
+ index: Tool index for ID generation
746
+
747
+ Returns:
748
+ Parsed tool call dictionary
749
+ """
750
+ content = content.strip()
751
+
752
+ # Try JSON format first
753
+ if content.startswith("{"):
754
+ try:
755
+ data = json.loads(content)
756
+ return {
757
+ "type": "mcp_tool",
758
+ "id": f"mcp_tool_{index}",
759
+ "name": data.get("name", "unknown"),
760
+ "arguments": data.get("arguments", {}),
761
+ "content": "",
762
+ "raw": f"<tool_call>{content}</tool_call>"
763
+ }
764
+ except json.JSONDecodeError:
765
+ pass
766
+
767
+ # Simple name format: just the tool name, maybe with inline args
768
+ # Handle: "search_nodes" or "search_nodes query=test"
769
+ parts = content.split(None, 1)
770
+ tool_name = parts[0] if parts else "unknown"
771
+ arguments = {}
772
+
773
+ # Parse inline arguments if present
774
+ if len(parts) > 1:
775
+ arg_str = parts[1]
776
+ # Try to parse key=value pairs
777
+ for pair in re.findall(r'(\w+)=(["\']?)([^"\'=\s]+)\2', arg_str):
778
+ key, _, value = pair
779
+ arguments[key] = self._convert_value(value)
780
+
781
+ return {
782
+ "type": "mcp_tool",
783
+ "id": f"mcp_tool_{index}",
784
+ "name": tool_name,
785
+ "arguments": arguments,
786
+ "content": "",
787
+ "raw": f"<tool_call>{content}</tool_call>"
788
+ }
578
789
 
579
790
  def _parse_tool_attributes(self, attributes_str: str) -> Dict[str, Any]:
580
791
  """Parse tool tag attributes.
@@ -661,6 +872,13 @@ class ResponseParser:
661
872
  # Remove tool tags but preserve content structure
662
873
  cleaned = self.tool_pattern.sub('', cleaned)
663
874
 
875
+ # Remove <tool_call> tags
876
+ cleaned = self.tool_call_pattern.sub('', cleaned)
877
+
878
+ # Remove question tags but preserve content for display
879
+ # The question content stays visible, just the tags are removed
880
+ cleaned = self.question_pattern.sub(r'\1', cleaned)
881
+
664
882
  # Remove file operation tags (all 14 types)
665
883
  # Only successfully parsed tags are removed; malformed tags remain visible
666
884
  cleaned = self.file_ops_parser.edit_pattern.sub('', cleaned)
@@ -33,6 +33,12 @@ class ResponseProcessor:
33
33
 
34
34
  logger.info("Response processor initialized")
35
35
 
36
+ async def initialize(self) -> bool:
37
+ """Initialize the response processor."""
38
+ self.is_initialized = True
39
+ logger.debug("Response processor async initialization complete")
40
+ return True
41
+
36
42
  def process_response(self, raw_response: str) -> Dict[str, Any]:
37
43
  """Process raw LLM response.
38
44
 
@@ -89,14 +95,21 @@ class ResponseProcessor:
89
95
 
90
96
  def _extract_terminal_commands(self, content: str) -> List[str]:
91
97
  """Extract terminal commands from content.
92
-
98
+
93
99
  Args:
94
100
  content: Raw content with potential terminal tags
95
-
101
+
96
102
  Returns:
97
103
  List of terminal commands
98
104
  """
99
- matches = self.terminal_pattern.findall(content)
105
+ # Strip <content> blocks to avoid parsing terminal examples inside file content
106
+ text_without_content = re.sub(
107
+ r'<content>.*?</content>',
108
+ '',
109
+ content,
110
+ flags=re.DOTALL | re.IGNORECASE
111
+ )
112
+ matches = self.terminal_pattern.findall(text_without_content)
100
113
  return [match.strip() for match in matches if match.strip()]
101
114
 
102
115
  def _extract_tool_calls(self, content: str) -> List[Dict[str, Any]]:
core/llm/tool_executor.py CHANGED
@@ -264,7 +264,7 @@ class ToolExecutor:
264
264
 
265
265
  try:
266
266
  stdout, stderr = await asyncio.wait_for(
267
- process.communicate(),
267
+ process.communicate(),
268
268
  timeout=self.terminal_timeout
269
269
  )
270
270
  except asyncio.TimeoutError:
@@ -276,6 +276,11 @@ class ToolExecutor:
276
276
  success=False,
277
277
  error=f"Command timed out after {self.terminal_timeout} seconds"
278
278
  )
279
+ except asyncio.CancelledError:
280
+ # Clean up subprocess on cancellation to avoid ResourceWarning
281
+ process.kill()
282
+ await process.wait()
283
+ raise # Re-raise to propagate cancellation
279
284
 
280
285
  # Process results
281
286
  stdout_text = stdout.decode('utf-8', errors='replace')
core/logging/__init__.py CHANGED
@@ -5,6 +5,7 @@ from .setup import (
5
5
  setup_from_config,
6
6
  get_current_config,
7
7
  is_configured,
8
+ set_level,
8
9
  CompactFormatter,
9
10
  LoggingSetup
10
11
  )
@@ -14,6 +15,7 @@ __all__ = [
14
15
  'setup_from_config',
15
16
  'get_current_config',
16
17
  'is_configured',
18
+ 'set_level',
17
19
  'CompactFormatter',
18
20
  'LoggingSetup'
19
21
  ]
core/logging/setup.py CHANGED
@@ -11,6 +11,8 @@ import threading
11
11
  from pathlib import Path
12
12
  from typing import Any, Dict, Optional
13
13
 
14
+ from ..utils.config_utils import get_logs_dir
15
+
14
16
 
15
17
  class CompactFormatter(logging.Formatter):
16
18
  """Custom formatter that compacts level names and includes file location."""
@@ -37,12 +39,10 @@ class LoggingSetup:
37
39
  """Setup minimal logging before config system is available.
38
40
 
39
41
  Args:
40
- log_dir: Optional log directory, defaults to .kollabor-cli/logs
42
+ log_dir: Optional log directory, defaults to project-specific logs
41
43
  """
42
44
  if log_dir is None:
43
- from ..utils.config_utils import get_config_directory
44
- config_dir = get_config_directory()
45
- log_dir = config_dir / "logs"
45
+ log_dir = get_logs_dir()
46
46
 
47
47
  # Ensure log directory exists
48
48
  log_dir.mkdir(parents=True, exist_ok=True)
@@ -96,7 +96,8 @@ class LoggingSetup:
96
96
 
97
97
  # Extract configuration values
98
98
  level = logging_config.get('level', 'INFO').upper()
99
- log_file = logging_config.get('file', '.kollabor-cli/logs/kollabor.log')
99
+ default_log_path = get_logs_dir() / "kollabor.log"
100
+ log_file = logging_config.get('file') or str(default_log_path)
100
101
  format_type = logging_config.get('format_type', 'compact')
101
102
  custom_format = logging_config.get('format', None)
102
103
 
@@ -183,6 +184,28 @@ class LoggingSetup:
183
184
  """Check if logging has been configured."""
184
185
  return self._configured
185
186
 
187
+ def set_level(self, level: str) -> None:
188
+ """Set logging level at runtime (hot reload support).
189
+
190
+ Args:
191
+ level: Log level string (DEBUG, INFO, WARNING, ERROR)
192
+ """
193
+ level = level.upper()
194
+ numeric_level = getattr(logging, level, logging.INFO)
195
+
196
+ # Update root logger
197
+ logging.getLogger().setLevel(numeric_level)
198
+
199
+ # Update all existing loggers
200
+ for logger_name in logging.Logger.manager.loggerDict:
201
+ existing_logger = logging.getLogger(logger_name)
202
+ existing_logger.setLevel(numeric_level)
203
+
204
+ # Update current config
205
+ self._current_config['level'] = level
206
+
207
+ logging.info(f"Logging level changed to {level}")
208
+
186
209
 
187
210
  # Global instance
188
211
  logging_setup = LoggingSetup()
@@ -205,4 +228,9 @@ def get_current_config() -> Dict[str, Any]:
205
228
 
206
229
  def is_configured() -> bool:
207
230
  """Check if logging has been configured."""
208
- return logging_setup.is_configured()
231
+ return logging_setup.is_configured()
232
+
233
+
234
+ def set_level(level: str) -> None:
235
+ """Set logging level at runtime (hot reload support)."""
236
+ logging_setup.set_level(level)