cognautic-cli 1.1.1__py3-none-any.whl → 1.1.2__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.
- cognautic/__init__.py +3 -3
- cognautic/ai_engine.py +407 -143
- cognautic/auto_continuation.py +54 -2
- cognautic/cli.py +129 -9
- cognautic/tools/file_reader.py +218 -0
- cognautic/tools/registry.py +2 -0
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/METADATA +8 -11
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/RECORD +12 -11
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/WHEEL +0 -0
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/entry_points.txt +0 -0
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {cognautic_cli-1.1.1.dist-info → cognautic_cli-1.1.2.dist-info}/top_level.txt +0 -0
cognautic/__init__.py
CHANGED
cognautic/ai_engine.py
CHANGED
|
@@ -779,20 +779,274 @@ class AIEngine:
|
|
|
779
779
|
if max_tokens == 0 or max_tokens == -1:
|
|
780
780
|
max_tokens = None # Treat 0 or -1 as unlimited
|
|
781
781
|
|
|
782
|
-
#
|
|
783
|
-
|
|
784
|
-
messages
|
|
785
|
-
model=model,
|
|
786
|
-
max_tokens=max_tokens or 16384, # Use large default if None
|
|
787
|
-
temperature=config.get("temperature", 0.7),
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
# Process response with tools
|
|
791
|
-
async for chunk in self._process_response_with_tools(
|
|
792
|
-
response, project_path, messages, ai_provider, model, config
|
|
782
|
+
# Stream response with real-time tool detection and execution
|
|
783
|
+
async for chunk in self._stream_with_live_tools(
|
|
784
|
+
ai_provider, messages, model, max_tokens, config, project_path
|
|
793
785
|
):
|
|
794
786
|
yield chunk
|
|
795
787
|
|
|
788
|
+
async def _stream_with_live_tools(
|
|
789
|
+
self,
|
|
790
|
+
ai_provider,
|
|
791
|
+
messages: list,
|
|
792
|
+
model: str,
|
|
793
|
+
max_tokens: int,
|
|
794
|
+
config: dict,
|
|
795
|
+
project_path: str,
|
|
796
|
+
recursion_depth: int = 0,
|
|
797
|
+
):
|
|
798
|
+
"""Stream AI response with real-time tool detection and execution"""
|
|
799
|
+
import re
|
|
800
|
+
import json
|
|
801
|
+
|
|
802
|
+
# Prevent infinite recursion
|
|
803
|
+
MAX_RECURSION_DEPTH = 10
|
|
804
|
+
if recursion_depth >= MAX_RECURSION_DEPTH:
|
|
805
|
+
yield "\n⚠️ **Warning:** Maximum continuation depth reached.\n"
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
# Buffer to accumulate streaming response
|
|
809
|
+
buffer = ""
|
|
810
|
+
yielded_length = 0
|
|
811
|
+
in_json_block = False
|
|
812
|
+
json_block_start = -1
|
|
813
|
+
has_executed_tools = False
|
|
814
|
+
tool_results = []
|
|
815
|
+
|
|
816
|
+
# Check if provider supports streaming
|
|
817
|
+
if not hasattr(ai_provider, 'generate_response_stream'):
|
|
818
|
+
# Fall back to non-streaming
|
|
819
|
+
response = await ai_provider.generate_response(
|
|
820
|
+
messages=messages,
|
|
821
|
+
model=model,
|
|
822
|
+
max_tokens=max_tokens or 16384,
|
|
823
|
+
temperature=config.get("temperature", 0.7),
|
|
824
|
+
)
|
|
825
|
+
async for chunk in self._process_response_with_tools(
|
|
826
|
+
response, project_path, messages, ai_provider, model, config
|
|
827
|
+
):
|
|
828
|
+
yield chunk
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
# Stream the response
|
|
832
|
+
try:
|
|
833
|
+
async for chunk in ai_provider.generate_response_stream(
|
|
834
|
+
messages=messages,
|
|
835
|
+
model=model,
|
|
836
|
+
max_tokens=max_tokens or 16384,
|
|
837
|
+
temperature=config.get("temperature", 0.7),
|
|
838
|
+
):
|
|
839
|
+
if not chunk:
|
|
840
|
+
continue
|
|
841
|
+
|
|
842
|
+
buffer += chunk
|
|
843
|
+
|
|
844
|
+
# Check for JSON block markers
|
|
845
|
+
if not in_json_block:
|
|
846
|
+
# Look for start of JSON block
|
|
847
|
+
json_start_match = re.search(r'```json\s*\n', buffer[yielded_length:])
|
|
848
|
+
if json_start_match:
|
|
849
|
+
# Found start of JSON block
|
|
850
|
+
in_json_block = True
|
|
851
|
+
json_block_start = yielded_length + json_start_match.start()
|
|
852
|
+
|
|
853
|
+
# Yield everything BEFORE the JSON block
|
|
854
|
+
if json_block_start > yielded_length:
|
|
855
|
+
text_to_yield = buffer[yielded_length:json_block_start]
|
|
856
|
+
text_to_yield = self._clean_model_syntax(text_to_yield)
|
|
857
|
+
if text_to_yield.strip():
|
|
858
|
+
yield text_to_yield
|
|
859
|
+
yielded_length = json_block_start
|
|
860
|
+
else:
|
|
861
|
+
# No JSON block yet, yield new content (keep small buffer)
|
|
862
|
+
if len(buffer) > yielded_length + 10:
|
|
863
|
+
text_to_yield = buffer[yielded_length:-10]
|
|
864
|
+
text_to_yield = self._clean_model_syntax(text_to_yield)
|
|
865
|
+
if text_to_yield:
|
|
866
|
+
yield text_to_yield
|
|
867
|
+
yielded_length = len(buffer) - 10
|
|
868
|
+
else:
|
|
869
|
+
# Inside JSON block, look for the end
|
|
870
|
+
json_end_match = re.search(r'\n```', buffer[json_block_start:])
|
|
871
|
+
if json_end_match:
|
|
872
|
+
# Found end of JSON block
|
|
873
|
+
json_block_end = json_block_start + json_end_match.end()
|
|
874
|
+
|
|
875
|
+
# Extract the complete JSON block
|
|
876
|
+
json_block = buffer[json_block_start:json_block_end]
|
|
877
|
+
|
|
878
|
+
# Parse and execute the tool call immediately
|
|
879
|
+
json_pattern = r'```json\s*\n(.*?)\n```'
|
|
880
|
+
match = re.search(json_pattern, json_block, re.DOTALL)
|
|
881
|
+
if match:
|
|
882
|
+
try:
|
|
883
|
+
tool_call = json.loads(match.group(1).strip())
|
|
884
|
+
|
|
885
|
+
# Execute tool and yield result immediately
|
|
886
|
+
async for tool_result in self._execute_single_tool_live(
|
|
887
|
+
tool_call, project_path, tool_results
|
|
888
|
+
):
|
|
889
|
+
yield tool_result
|
|
890
|
+
|
|
891
|
+
has_executed_tools = True
|
|
892
|
+
|
|
893
|
+
except json.JSONDecodeError as je:
|
|
894
|
+
yield f"\n⚠️ **JSON Parse Warning:** {str(je)}\n"
|
|
895
|
+
|
|
896
|
+
# Update state
|
|
897
|
+
in_json_block = False
|
|
898
|
+
yielded_length = json_block_end
|
|
899
|
+
json_block_start = -1
|
|
900
|
+
|
|
901
|
+
# Yield any remaining content
|
|
902
|
+
if yielded_length < len(buffer):
|
|
903
|
+
remaining = buffer[yielded_length:]
|
|
904
|
+
if not in_json_block:
|
|
905
|
+
remaining = self._clean_model_syntax(remaining)
|
|
906
|
+
if remaining.strip():
|
|
907
|
+
yield remaining
|
|
908
|
+
|
|
909
|
+
# Check if end_response was called
|
|
910
|
+
has_end_response = any(r.get("type") == "control" and r.get("message") == "end_response" for r in tool_results)
|
|
911
|
+
|
|
912
|
+
# Don't auto-continue if:
|
|
913
|
+
# 1. end_response was explicitly called
|
|
914
|
+
# 2. No tools were executed
|
|
915
|
+
# 3. Max recursion depth reached
|
|
916
|
+
if has_end_response or not has_executed_tools or recursion_depth >= MAX_RECURSION_DEPTH:
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
# Only continue if the auto-continuation manager says so
|
|
920
|
+
if self.auto_continuation.should_continue(tool_results, has_end_response):
|
|
921
|
+
yield "\n\n🔄 **Auto-continuing...**\n\n"
|
|
922
|
+
|
|
923
|
+
# Add assistant's response to messages
|
|
924
|
+
messages.append({"role": "assistant", "content": buffer})
|
|
925
|
+
|
|
926
|
+
# Generate continuation
|
|
927
|
+
final_response = await self.auto_continuation.generate_continuation(
|
|
928
|
+
ai_provider=ai_provider,
|
|
929
|
+
messages=messages,
|
|
930
|
+
tool_results=tool_results,
|
|
931
|
+
model=model,
|
|
932
|
+
config=config
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Recursively stream continuation
|
|
936
|
+
async for chunk in self._stream_with_live_tools(
|
|
937
|
+
ai_provider, messages, model, max_tokens, config, project_path, recursion_depth + 1
|
|
938
|
+
):
|
|
939
|
+
yield chunk
|
|
940
|
+
|
|
941
|
+
except Exception as e:
|
|
942
|
+
import traceback
|
|
943
|
+
yield f"\n❌ **Streaming Error:** {str(e)}\n"
|
|
944
|
+
yield f"\n**Traceback:**\n{traceback.format_exc()}\n"
|
|
945
|
+
|
|
946
|
+
async def _execute_single_tool_live(self, tool_call: dict, project_path: str, tool_results: list):
|
|
947
|
+
"""Execute a single tool call and yield the result immediately"""
|
|
948
|
+
try:
|
|
949
|
+
tool_name = tool_call.get("tool_code")
|
|
950
|
+
args = tool_call.get("args", {})
|
|
951
|
+
|
|
952
|
+
# Check for response control tool
|
|
953
|
+
if tool_name == "response_control":
|
|
954
|
+
operation = args.get("operation", "end_response")
|
|
955
|
+
if operation == "end_response":
|
|
956
|
+
yield "\n✅ **Response Completed**\n"
|
|
957
|
+
tool_results.append({"type": "control", "message": "end_response"})
|
|
958
|
+
return
|
|
959
|
+
|
|
960
|
+
# Execute the tool (reuse existing tool execution logic)
|
|
961
|
+
if tool_name == "command_runner":
|
|
962
|
+
cmd_args = args.copy()
|
|
963
|
+
if "operation" not in cmd_args:
|
|
964
|
+
cmd_args["operation"] = "run_command"
|
|
965
|
+
if "cwd" not in cmd_args:
|
|
966
|
+
cmd_args["cwd"] = project_path or "."
|
|
967
|
+
|
|
968
|
+
result = await self.tool_registry.execute_tool(
|
|
969
|
+
"command_runner", user_id="ai_engine", **cmd_args
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
if result.success:
|
|
973
|
+
command = args.get("command", "unknown")
|
|
974
|
+
operation = cmd_args.get("operation", "run_command")
|
|
975
|
+
|
|
976
|
+
if operation == "run_async_command":
|
|
977
|
+
process_id = result.data.get("process_id", "unknown")
|
|
978
|
+
pid = result.data.get("pid", "unknown")
|
|
979
|
+
yield f"\n✅ **Tool Used:** command_runner (background)\n⚡ **Command:** {command}\n🔄 **Status:** Running in background\n📋 **Process ID:** {process_id} (PID: {pid})\n💡 **Tip:** Use `/ct {process_id}` to terminate\n"
|
|
980
|
+
tool_results.append({"type": "command", "command": command, "success": True})
|
|
981
|
+
else:
|
|
982
|
+
stdout = result.data.get("stdout", "") if isinstance(result.data, dict) else str(result.data)
|
|
983
|
+
stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else ""
|
|
984
|
+
|
|
985
|
+
full_output = stdout
|
|
986
|
+
if stderr and stderr.strip():
|
|
987
|
+
full_output += f"\n[stderr]\n{stderr}"
|
|
988
|
+
|
|
989
|
+
display_output = full_output
|
|
990
|
+
if len(full_output) > 10000:
|
|
991
|
+
display_output = full_output[:10000] + f"\n\n... [Output truncated]"
|
|
992
|
+
|
|
993
|
+
yield f"\n✅ **Tool Used:** command_runner\n⚡ **Command:** {command}\n📄 **Output:**\n```\n{display_output}\n```\n"
|
|
994
|
+
tool_results.append({"type": "command", "command": command, "output": full_output, "success": True})
|
|
995
|
+
else:
|
|
996
|
+
yield f"\n❌ **Command Error:** {result.error}\n"
|
|
997
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
998
|
+
|
|
999
|
+
elif tool_name == "file_operations":
|
|
1000
|
+
file_path = args.get("file_path")
|
|
1001
|
+
if file_path and project_path:
|
|
1002
|
+
from pathlib import Path
|
|
1003
|
+
path = Path(file_path)
|
|
1004
|
+
if not path.is_absolute():
|
|
1005
|
+
args["file_path"] = str(Path(project_path) / file_path)
|
|
1006
|
+
|
|
1007
|
+
result = await self.tool_registry.execute_tool(
|
|
1008
|
+
"file_operations", user_id="ai_engine", **args
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
if result.success:
|
|
1012
|
+
operation = args.get("operation")
|
|
1013
|
+
file_path = args.get("file_path")
|
|
1014
|
+
yield f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n"
|
|
1015
|
+
tool_results.append({"type": "file_op", "operation": operation, "success": True})
|
|
1016
|
+
else:
|
|
1017
|
+
yield f"\n❌ **File Error:** {result.error}\n"
|
|
1018
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
1019
|
+
|
|
1020
|
+
elif tool_name == "web_search":
|
|
1021
|
+
result = await self.tool_registry.execute_tool(
|
|
1022
|
+
"web_search", user_id="ai_engine", **args
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if result.success:
|
|
1026
|
+
operation = args.get("operation", "search_web")
|
|
1027
|
+
yield f"\n✅ **Tool Used:** web_search ({operation})\n"
|
|
1028
|
+
tool_results.append({"type": "web_search", "operation": operation, "success": True})
|
|
1029
|
+
else:
|
|
1030
|
+
yield f"\n❌ **Web Search Error:** {result.error}\n"
|
|
1031
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
1032
|
+
|
|
1033
|
+
elif tool_name == "file_reader":
|
|
1034
|
+
result = await self.tool_registry.execute_tool(
|
|
1035
|
+
"file_reader", user_id="ai_engine", **args
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
if result.success:
|
|
1039
|
+
operation = args.get("operation")
|
|
1040
|
+
yield f"\n✅ **Tool Used:** file_reader ({operation})\n"
|
|
1041
|
+
tool_results.append({"type": "file_read", "operation": operation, "success": True})
|
|
1042
|
+
else:
|
|
1043
|
+
yield f"\n❌ **File Reader Error:** {result.error}\n"
|
|
1044
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
1045
|
+
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
yield f"\n❌ **Tool Error:** {str(e)}\n"
|
|
1048
|
+
tool_results.append({"type": "error", "error": str(e), "success": False})
|
|
1049
|
+
|
|
796
1050
|
def _parse_alternative_tool_calls(self, response: str):
|
|
797
1051
|
"""Parse tool calls from alternative formats (e.g., GPT-OSS with special tokens)"""
|
|
798
1052
|
import re
|
|
@@ -919,9 +1173,10 @@ class AIEngine:
|
|
|
919
1173
|
if text_before_tools:
|
|
920
1174
|
yield text_before_tools + "\n"
|
|
921
1175
|
|
|
922
|
-
# Process
|
|
1176
|
+
# Process and execute tool calls LIVE - show results immediately
|
|
923
1177
|
tool_results = []
|
|
924
1178
|
should_end_response = False
|
|
1179
|
+
has_executed_tools = False
|
|
925
1180
|
|
|
926
1181
|
# Combine standard JSON matches and alternative format tool calls
|
|
927
1182
|
all_tool_calls = []
|
|
@@ -947,14 +1202,13 @@ class AIEngine:
|
|
|
947
1202
|
operation = args.get("operation", "end_response")
|
|
948
1203
|
if operation == "end_response":
|
|
949
1204
|
should_end_response = True
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
"type": "control",
|
|
953
|
-
"display": f"\n✅ **Response Completed**\n",
|
|
954
|
-
}
|
|
955
|
-
)
|
|
1205
|
+
yield "\n✅ **Response Completed**\n"
|
|
1206
|
+
tool_results.append({"type": "control", "message": "end_response"})
|
|
956
1207
|
continue
|
|
957
1208
|
|
|
1209
|
+
# Mark that we've executed tools
|
|
1210
|
+
has_executed_tools = True
|
|
1211
|
+
|
|
958
1212
|
# Execute the tool
|
|
959
1213
|
if tool_name == "command_runner":
|
|
960
1214
|
cmd_args = args.copy()
|
|
@@ -968,38 +1222,56 @@ class AIEngine:
|
|
|
968
1222
|
)
|
|
969
1223
|
if result.success:
|
|
970
1224
|
command = args.get("command", "unknown")
|
|
971
|
-
|
|
972
|
-
result.data.get("stdout", "")
|
|
973
|
-
if isinstance(result.data, dict)
|
|
974
|
-
else str(result.data)
|
|
975
|
-
)
|
|
976
|
-
stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else ""
|
|
977
|
-
|
|
978
|
-
# Combine stdout and stderr for full output
|
|
979
|
-
full_output = stdout
|
|
980
|
-
if stderr and stderr.strip():
|
|
981
|
-
full_output += f"\n[stderr]\n{stderr}"
|
|
1225
|
+
operation = cmd_args.get("operation", "run_command")
|
|
982
1226
|
|
|
983
|
-
#
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1227
|
+
# Check if this is an async command
|
|
1228
|
+
if operation == "run_async_command":
|
|
1229
|
+
# Background command - show process ID
|
|
1230
|
+
process_id = result.data.get("process_id", "unknown")
|
|
1231
|
+
pid = result.data.get("pid", "unknown")
|
|
1232
|
+
display_msg = f"\n✅ **Tool Used:** command_runner (background)\n⚡ **Command:** {command}\n🔄 **Status:** Running in background\n📋 **Process ID:** {process_id} (PID: {pid})\n💡 **Tip:** Use `/ct {process_id}` to terminate this process\n"
|
|
1233
|
+
yield display_msg
|
|
1234
|
+
tool_results.append({
|
|
1235
|
+
"type": "command",
|
|
1236
|
+
"command": command,
|
|
1237
|
+
"output": f"Background process started: {process_id}",
|
|
1238
|
+
"success": True
|
|
1239
|
+
})
|
|
1240
|
+
else:
|
|
1241
|
+
# Synchronous command - show output
|
|
1242
|
+
stdout = (
|
|
1243
|
+
result.data.get("stdout", "")
|
|
1244
|
+
if isinstance(result.data, dict)
|
|
1245
|
+
else str(result.data)
|
|
1246
|
+
)
|
|
1247
|
+
stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else ""
|
|
1248
|
+
|
|
1249
|
+
# Combine stdout and stderr for full output
|
|
1250
|
+
full_output = stdout
|
|
1251
|
+
if stderr and stderr.strip():
|
|
1252
|
+
full_output += f"\n[stderr]\n{stderr}"
|
|
1253
|
+
|
|
1254
|
+
# Truncate very long outputs for display (but keep full output in data)
|
|
1255
|
+
display_output = full_output
|
|
1256
|
+
if len(full_output) > 10000:
|
|
1257
|
+
display_output = full_output[:10000] + f"\n\n... [Output truncated - {len(full_output)} total characters]"
|
|
1258
|
+
|
|
1259
|
+
display_msg = f"\n✅ **Tool Used:** command_runner\n⚡ **Command:** {command}\n📄 **Output:**\n```\n{display_output}\n```\n"
|
|
1260
|
+
yield display_msg
|
|
1261
|
+
tool_results.append({
|
|
990
1262
|
"type": "command",
|
|
991
1263
|
"command": command,
|
|
992
1264
|
"output": full_output,
|
|
993
|
-
"
|
|
994
|
-
}
|
|
995
|
-
)
|
|
1265
|
+
"success": True
|
|
1266
|
+
})
|
|
996
1267
|
else:
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1268
|
+
error_msg = f"\n❌ **Command Error:** {result.error}\n"
|
|
1269
|
+
yield error_msg
|
|
1270
|
+
tool_results.append({
|
|
1271
|
+
"type": "error",
|
|
1272
|
+
"error": result.error,
|
|
1273
|
+
"success": False
|
|
1274
|
+
})
|
|
1003
1275
|
|
|
1004
1276
|
elif tool_name == "file_operations":
|
|
1005
1277
|
# Prepend project_path to relative file paths
|
|
@@ -1042,14 +1314,14 @@ class AIEngine:
|
|
|
1042
1314
|
total = result.data.get("total_lines", 0)
|
|
1043
1315
|
line_info = f" (lines {start}-{end} of {total})"
|
|
1044
1316
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
)
|
|
1317
|
+
display_msg = f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}{line_info}\n📄 **Result:**\n```\n{display_content}\n```\n"
|
|
1318
|
+
yield display_msg
|
|
1319
|
+
tool_results.append({
|
|
1320
|
+
"type": "file_read",
|
|
1321
|
+
"file_path": file_path,
|
|
1322
|
+
"content": content,
|
|
1323
|
+
"success": True
|
|
1324
|
+
})
|
|
1053
1325
|
elif operation == "write_file_lines":
|
|
1054
1326
|
# Show line range info for write_file_lines
|
|
1055
1327
|
line_info = ""
|
|
@@ -1058,26 +1330,17 @@ class AIEngine:
|
|
|
1058
1330
|
end = result.data.get("end_line", 1)
|
|
1059
1331
|
lines_written = result.data.get("lines_written", 0)
|
|
1060
1332
|
line_info = f" (lines {start}-{end}, {lines_written} lines written)"
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
"display": f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}{line_info}\n",
|
|
1065
|
-
}
|
|
1066
|
-
)
|
|
1333
|
+
display_msg = f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}{line_info}\n"
|
|
1334
|
+
yield display_msg
|
|
1335
|
+
tool_results.append({"type": "file_op", "operation": operation, "success": True})
|
|
1067
1336
|
else:
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
"display": f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n",
|
|
1072
|
-
}
|
|
1073
|
-
)
|
|
1337
|
+
display_msg = f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n"
|
|
1338
|
+
yield display_msg
|
|
1339
|
+
tool_results.append({"type": "file_op", "operation": operation, "success": True})
|
|
1074
1340
|
else:
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
"display": f"\n❌ **File Error:** {result.error}\n",
|
|
1079
|
-
}
|
|
1080
|
-
)
|
|
1341
|
+
error_msg = f"\n❌ **File Error:** {result.error}\n"
|
|
1342
|
+
yield error_msg
|
|
1343
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
1081
1344
|
|
|
1082
1345
|
elif tool_name == "web_search":
|
|
1083
1346
|
operation = args.get("operation", "search_web")
|
|
@@ -1100,14 +1363,13 @@ class AIEngine:
|
|
|
1100
1363
|
)
|
|
1101
1364
|
result_text += f" 🔗 {item.get('url', 'No URL')}\n"
|
|
1102
1365
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
)
|
|
1366
|
+
yield result_text
|
|
1367
|
+
tool_results.append({
|
|
1368
|
+
"type": "web_search",
|
|
1369
|
+
"query": query,
|
|
1370
|
+
"results": search_results,
|
|
1371
|
+
"success": True
|
|
1372
|
+
})
|
|
1111
1373
|
|
|
1112
1374
|
elif operation == "fetch_url_content":
|
|
1113
1375
|
url = args.get("url", "unknown")
|
|
@@ -1135,14 +1397,8 @@ class AIEngine:
|
|
|
1135
1397
|
f"**Content:**\n```\n{display_content}\n```\n"
|
|
1136
1398
|
)
|
|
1137
1399
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
"type": "web_fetch",
|
|
1141
|
-
"url": url,
|
|
1142
|
-
"content": content, # Full content for AI processing
|
|
1143
|
-
"display": result_text,
|
|
1144
|
-
}
|
|
1145
|
-
)
|
|
1400
|
+
yield result_text
|
|
1401
|
+
tool_results.append({"type": "web_fetch", "url": url, "content": content, "success": True})
|
|
1146
1402
|
|
|
1147
1403
|
elif operation == "parse_documentation":
|
|
1148
1404
|
url = args.get("url", "unknown")
|
|
@@ -1162,14 +1418,8 @@ class AIEngine:
|
|
|
1162
1418
|
f"\n• {section.get('title', 'Untitled')}\n"
|
|
1163
1419
|
)
|
|
1164
1420
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
"type": "web_docs",
|
|
1168
|
-
"url": url,
|
|
1169
|
-
"doc_data": doc_data,
|
|
1170
|
-
"display": result_text,
|
|
1171
|
-
}
|
|
1172
|
-
)
|
|
1421
|
+
yield result_text
|
|
1422
|
+
tool_results.append({"type": "web_docs", "url": url, "doc_data": doc_data, "success": True})
|
|
1173
1423
|
|
|
1174
1424
|
elif operation == "get_api_docs":
|
|
1175
1425
|
api_name = args.get("api_name", "unknown")
|
|
@@ -1187,53 +1437,35 @@ class AIEngine:
|
|
|
1187
1437
|
result_text += f"📚 **API:** {api_name}\n"
|
|
1188
1438
|
result_text += f"❌ {api_data.get('message', 'Documentation not found')}\n"
|
|
1189
1439
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
"type": "web_api_docs",
|
|
1193
|
-
"api_name": api_name,
|
|
1194
|
-
"api_data": api_data,
|
|
1195
|
-
"display": result_text,
|
|
1196
|
-
}
|
|
1197
|
-
)
|
|
1440
|
+
yield result_text
|
|
1441
|
+
tool_results.append({"type": "web_api_docs", "api_name": api_name, "api_data": api_data, "success": True})
|
|
1198
1442
|
else:
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
"display": f"\n✅ **Tool Used:** web_search ({operation})\n",
|
|
1203
|
-
}
|
|
1204
|
-
)
|
|
1443
|
+
display_msg = f"\n✅ **Tool Used:** web_search ({operation})\n"
|
|
1444
|
+
yield display_msg
|
|
1445
|
+
tool_results.append({"type": "web_search", "operation": operation, "success": True})
|
|
1205
1446
|
else:
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
"display": f"\n❌ **Web Search Error:** {result.error}\n",
|
|
1210
|
-
}
|
|
1211
|
-
)
|
|
1447
|
+
error_msg = f"\n❌ **Web Search Error:** {result.error}\n"
|
|
1448
|
+
yield error_msg
|
|
1449
|
+
tool_results.append({"type": "error", "error": result.error, "success": False})
|
|
1212
1450
|
|
|
1213
1451
|
except json.JSONDecodeError as e:
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
"display": f"\n❌ **JSON Parse Error:** {str(e)}\n",
|
|
1218
|
-
}
|
|
1219
|
-
)
|
|
1452
|
+
error_msg = f"\n❌ **JSON Parse Error:** {str(e)}\n"
|
|
1453
|
+
yield error_msg
|
|
1454
|
+
tool_results.append({"type": "error", "error": str(e), "success": False})
|
|
1220
1455
|
except Exception as e:
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
)
|
|
1224
|
-
|
|
1225
|
-
# Now yield the tool results
|
|
1226
|
-
if tool_results:
|
|
1227
|
-
# Show all tool results
|
|
1228
|
-
for tool_result in tool_results:
|
|
1229
|
-
yield tool_result["display"]
|
|
1456
|
+
error_msg = f"\n❌ **Tool Error:** {str(e)}\n"
|
|
1457
|
+
yield error_msg
|
|
1458
|
+
tool_results.append({"type": "error", "error": str(e), "success": False})
|
|
1230
1459
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1460
|
+
# Tool results have already been yielded immediately during execution
|
|
1461
|
+
# Now check if we should continue or end
|
|
1462
|
+
|
|
1463
|
+
# Check if we should end the response (end_response tool was called)
|
|
1464
|
+
if should_end_response:
|
|
1465
|
+
return
|
|
1234
1466
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1467
|
+
# Use auto-continuation manager to determine if we should continue
|
|
1468
|
+
if has_executed_tools and self.auto_continuation.should_continue(tool_results, should_end_response) and recursion_depth < MAX_RECURSION_DEPTH:
|
|
1237
1469
|
# Show continuation indicator
|
|
1238
1470
|
yield "\n🔄 **Auto-continuing...**\n"
|
|
1239
1471
|
|
|
@@ -1389,7 +1621,11 @@ CRITICAL BEHAVIOR REQUIREMENTS:
|
|
|
1389
1621
|
- PROVIDE COMPREHENSIVE SOLUTIONS: Don't stop after creating just one file - complete the entire functional project.
|
|
1390
1622
|
- BE PROACTIVE: Anticipate what files and functionality are needed and create them all without asking for permission for each step.
|
|
1391
1623
|
- EXPLORATION IS OPTIONAL: You may explore the workspace with 'ls' or 'pwd' if needed, but this is NOT required before creating new files. If the user asks you to BUILD or CREATE something, prioritize creating the files immediately.
|
|
1392
|
-
- ALWAYS USE end_response TOOL: When you have completed ALL tasks,
|
|
1624
|
+
- ALWAYS USE end_response TOOL: When you have completed ALL tasks, you MUST call the end_response tool to signal completion. This prevents unnecessary auto-continuation.
|
|
1625
|
+
- AUTO-CONTINUATION: The system will automatically continue ONLY when:
|
|
1626
|
+
* You created files but haven't run necessary commands yet (e.g., npm install after creating package.json)
|
|
1627
|
+
* There were errors that need to be handled
|
|
1628
|
+
* Otherwise, you MUST explicitly call end_response when done
|
|
1393
1629
|
- NEVER RE-READ SAME FILE: If a file was truncated in the output, use read_file_lines to read the specific truncated section, DO NOT re-read the entire file
|
|
1394
1630
|
|
|
1395
1631
|
WORKSPACE EXPLORATION RULES (CRITICAL - ALWAYS CHECK FIRST):
|
|
@@ -1569,7 +1805,7 @@ For multiple files, use separate tool calls:
|
|
|
1569
1805
|
}
|
|
1570
1806
|
```
|
|
1571
1807
|
|
|
1572
|
-
For commands
|
|
1808
|
+
For commands:
|
|
1573
1809
|
|
|
1574
1810
|
```json
|
|
1575
1811
|
{
|
|
@@ -1580,16 +1816,44 @@ For commands (always explore first):
|
|
|
1580
1816
|
}
|
|
1581
1817
|
```
|
|
1582
1818
|
|
|
1819
|
+
For LONG commands (npm install, servers), use run_async_command to run in background:
|
|
1820
|
+
|
|
1583
1821
|
```json
|
|
1584
1822
|
{
|
|
1585
1823
|
"tool_code": "command_runner",
|
|
1586
1824
|
"args": {
|
|
1587
|
-
"
|
|
1825
|
+
"operation": "run_async_command",
|
|
1826
|
+
"command": "npm install"
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
For file reading and grep:
|
|
1832
|
+
|
|
1833
|
+
```json
|
|
1834
|
+
{
|
|
1835
|
+
"tool_code": "file_reader",
|
|
1836
|
+
"args": {
|
|
1837
|
+
"operation": "read_file",
|
|
1838
|
+
"file_path": "src/App.js"
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
```
|
|
1842
|
+
|
|
1843
|
+
```json
|
|
1844
|
+
{
|
|
1845
|
+
"tool_code": "file_reader",
|
|
1846
|
+
"args": {
|
|
1847
|
+
"operation": "grep_search",
|
|
1848
|
+
"pattern": "function.*Component",
|
|
1849
|
+
"search_path": "src",
|
|
1850
|
+
"file_pattern": "*.js",
|
|
1851
|
+
"recursive": true
|
|
1588
1852
|
}
|
|
1589
1853
|
}
|
|
1590
1854
|
```
|
|
1591
1855
|
|
|
1592
|
-
For web search
|
|
1856
|
+
For web search:
|
|
1593
1857
|
|
|
1594
1858
|
```json
|
|
1595
1859
|
{
|
|
@@ -1672,11 +1936,11 @@ When user explicitly asks for RESEARCH:
|
|
|
1672
1936
|
The tools will execute automatically and show results. Keep explanatory text BRIEF.
|
|
1673
1937
|
|
|
1674
1938
|
Available tools:
|
|
1675
|
-
- file_operations: Create, read, write, delete files
|
|
1676
|
-
-
|
|
1939
|
+
- file_operations: Create, read, write, delete files
|
|
1940
|
+
- file_reader: Read files, grep search, list directories
|
|
1941
|
+
- command_runner: Execute shell commands (use run_async_command for long tasks)
|
|
1677
1942
|
- web_search: Search the web for information
|
|
1678
|
-
-
|
|
1679
|
-
- response_control: Control response continuation (use end_response to stop auto-continuation)
|
|
1943
|
+
- response_control: Use end_response when task is complete
|
|
1680
1944
|
|
|
1681
1945
|
RESPONSE CONTINUATION (CRITICAL - ALWAYS USE end_response):
|
|
1682
1946
|
- By default, after executing tools, the AI will automatically continue to complete the task
|
cognautic/auto_continuation.py
CHANGED
|
@@ -43,8 +43,60 @@ class AutoContinuationManager:
|
|
|
43
43
|
if self.iteration_count >= self.max_iterations:
|
|
44
44
|
return False
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
if tool_results:
|
|
46
|
+
# Don't continue if no tools were executed
|
|
47
|
+
if not tool_results:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
# Check if there were any errors
|
|
51
|
+
has_errors = any(r.get('type') == 'error' for r in tool_results)
|
|
52
|
+
|
|
53
|
+
# Check what types of tools were executed
|
|
54
|
+
has_file_ops = any(r.get('type') in ['file_op', 'file_write', 'file_read'] for r in tool_results)
|
|
55
|
+
has_commands = any(r.get('type') == 'command' for r in tool_results)
|
|
56
|
+
has_background_commands = any(
|
|
57
|
+
r.get('type') == 'command' and 'background' in str(r.get('command', ''))
|
|
58
|
+
for r in tool_results
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check if commands were exploratory (ls, pwd, dir, etc.)
|
|
62
|
+
exploratory_commands = ['ls', 'pwd', 'dir', 'cd', 'find', 'tree', 'cat', 'head', 'tail']
|
|
63
|
+
has_exploratory_commands = any(
|
|
64
|
+
r.get('type') == 'command' and
|
|
65
|
+
any(cmd in str(r.get('command', '')).lower() for cmd in exploratory_commands)
|
|
66
|
+
for r in tool_results
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Smart continuation logic:
|
|
70
|
+
# 1. If there are errors, continue to let AI handle them
|
|
71
|
+
if has_errors:
|
|
72
|
+
self.iteration_count += 1
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# 2. If only file operations were done, likely need to run commands next
|
|
76
|
+
if has_file_ops and not has_commands:
|
|
77
|
+
self.iteration_count += 1
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
# 3. If only exploratory commands (ls, pwd, etc.), continue to do actual work
|
|
81
|
+
if has_exploratory_commands and not has_file_ops:
|
|
82
|
+
self.iteration_count += 1
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# 4. If background commands were started AND files created, task likely complete
|
|
86
|
+
if has_background_commands and has_file_ops:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# 5. If only background commands (no files), don't continue (they're running)
|
|
90
|
+
if has_background_commands and not has_file_ops:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# 6. If regular commands executed with file ops, task likely complete
|
|
94
|
+
if has_commands and has_file_ops and not has_exploratory_commands:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# 7. Default: Continue if we're early in the process
|
|
98
|
+
# This ensures AI completes the full task
|
|
99
|
+
if self.iteration_count < 3:
|
|
48
100
|
self.iteration_count += 1
|
|
49
101
|
return True
|
|
50
102
|
|
cognautic/cli.py
CHANGED
|
@@ -8,8 +8,12 @@ import logging
|
|
|
8
8
|
import os
|
|
9
9
|
import readline
|
|
10
10
|
import signal
|
|
11
|
-
import sys
|
|
12
11
|
from pathlib import Path
|
|
12
|
+
import sys
|
|
13
|
+
import subprocess
|
|
14
|
+
from prompt_toolkit import PromptSession
|
|
15
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
16
|
+
from prompt_toolkit.formatted_text import HTML
|
|
13
17
|
from rich.console import Console
|
|
14
18
|
from rich.panel import Panel
|
|
15
19
|
from rich.text import Text
|
|
@@ -83,7 +87,7 @@ def signal_handler(signum, frame):
|
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
@click.group(invoke_without_command=True)
|
|
86
|
-
@click.version_option(version="1.1.
|
|
90
|
+
@click.version_option(version="1.1.2", prog_name="Cognautic CLI")
|
|
87
91
|
@click.pass_context
|
|
88
92
|
def main(ctx):
|
|
89
93
|
"""Cognautic CLI - AI-powered development assistant"""
|
|
@@ -193,6 +197,7 @@ def chat(provider, model, project_path, websocket_port, session):
|
|
|
193
197
|
|
|
194
198
|
try:
|
|
195
199
|
console.print("💡 Type '/help' for commands, 'exit' to quit")
|
|
200
|
+
console.print("💡 Press Shift+Tab to toggle Terminal mode")
|
|
196
201
|
if project_path:
|
|
197
202
|
console.print(f"📁 Working in: {project_path}")
|
|
198
203
|
|
|
@@ -238,21 +243,81 @@ def chat(provider, model, project_path, websocket_port, session):
|
|
|
238
243
|
# Show current workspace
|
|
239
244
|
console.print(f"📁 Workspace: {current_workspace}")
|
|
240
245
|
|
|
246
|
+
# Terminal mode state
|
|
247
|
+
terminal_mode = [False] # Use list to make it mutable in nested function
|
|
248
|
+
|
|
249
|
+
# Setup key bindings for Shift+Tab toggle
|
|
250
|
+
bindings = KeyBindings()
|
|
251
|
+
|
|
252
|
+
@bindings.add('s-tab') # Shift+Tab
|
|
253
|
+
def toggle_mode(event):
|
|
254
|
+
terminal_mode[0] = not terminal_mode[0]
|
|
255
|
+
mode_name = "🖥️ Terminal" if terminal_mode[0] else "💬 Chat"
|
|
256
|
+
# Clear current line and show mode switch message
|
|
257
|
+
event.app.current_buffer.text = ''
|
|
258
|
+
event.app.exit(result='__MODE_TOGGLE__')
|
|
259
|
+
|
|
260
|
+
# Create prompt session
|
|
261
|
+
session = PromptSession(key_bindings=bindings)
|
|
262
|
+
|
|
263
|
+
console.print("[dim]💡 Press Shift+Tab to toggle between Chat and Terminal modes[/dim]\n")
|
|
264
|
+
|
|
241
265
|
while True:
|
|
242
266
|
try:
|
|
243
|
-
# Show current workspace in prompt
|
|
267
|
+
# Show current workspace and mode in prompt
|
|
244
268
|
workspace_info = f" [{Path(current_workspace).name}]" if current_workspace else ""
|
|
269
|
+
mode_indicator = "🖥️ " if terminal_mode[0] else ""
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
prompt_text = HTML(f'<ansibrightcyan><b>{mode_indicator}You{workspace_info}:</b></ansibrightcyan> ')
|
|
272
|
+
user_input = await session.prompt_async(prompt_text)
|
|
273
|
+
|
|
274
|
+
# Handle mode toggle
|
|
275
|
+
if user_input == '__MODE_TOGGLE__':
|
|
276
|
+
mode_name = "🖥️ Terminal" if terminal_mode[0] else "💬 Chat"
|
|
277
|
+
console.print(f"[bold yellow]Switched to {mode_name} mode[/bold yellow]")
|
|
278
|
+
console.print("[dim]Press Shift+Tab to toggle modes[/dim]")
|
|
279
|
+
continue
|
|
252
280
|
|
|
253
281
|
if user_input.lower() in ['exit', 'quit']:
|
|
254
282
|
break
|
|
255
283
|
|
|
284
|
+
# Handle terminal mode
|
|
285
|
+
if terminal_mode[0]:
|
|
286
|
+
if not user_input.strip():
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Execute command in terminal mode
|
|
290
|
+
try:
|
|
291
|
+
# Change to current workspace directory
|
|
292
|
+
original_dir = os.getcwd()
|
|
293
|
+
os.chdir(current_workspace)
|
|
294
|
+
|
|
295
|
+
# Run command and capture output
|
|
296
|
+
result = subprocess.run(
|
|
297
|
+
user_input,
|
|
298
|
+
shell=True,
|
|
299
|
+
capture_output=True,
|
|
300
|
+
text=True,
|
|
301
|
+
cwd=current_workspace
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Show output
|
|
305
|
+
if result.stdout:
|
|
306
|
+
console.print(result.stdout, end='')
|
|
307
|
+
if result.stderr:
|
|
308
|
+
console.print(f"[red]{result.stderr}[/red]", end='')
|
|
309
|
+
|
|
310
|
+
# Show exit code if non-zero
|
|
311
|
+
if result.returncode != 0:
|
|
312
|
+
console.print(f"[yellow]Exit code: {result.returncode}[/yellow]")
|
|
313
|
+
|
|
314
|
+
# Change back to original directory
|
|
315
|
+
os.chdir(original_dir)
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
console.print(f"[red]Error executing command: {str(e)}[/red]")
|
|
319
|
+
|
|
320
|
+
continue
|
|
256
321
|
|
|
257
322
|
# Handle slash commands
|
|
258
323
|
if user_input.startswith('/'):
|
|
@@ -1022,6 +1087,58 @@ async def handle_slash_command(command, config_manager, ai_engine, context):
|
|
|
1022
1087
|
console.print("💬 Chat cleared")
|
|
1023
1088
|
return True
|
|
1024
1089
|
|
|
1090
|
+
elif cmd == "ps" or cmd == "processes":
|
|
1091
|
+
# List all running background processes
|
|
1092
|
+
try:
|
|
1093
|
+
result = await ai_engine.tool_registry.execute_tool(
|
|
1094
|
+
"command_runner",
|
|
1095
|
+
operation="check_process_status",
|
|
1096
|
+
user_id="ai_engine"
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
if result.success and result.data:
|
|
1100
|
+
processes = result.data.get('processes', [])
|
|
1101
|
+
if not processes:
|
|
1102
|
+
console.print("📋 No background processes running", style="dim")
|
|
1103
|
+
else:
|
|
1104
|
+
console.print(f"\n📋 Running Processes ({len(processes)}):", style="bold")
|
|
1105
|
+
for proc in processes:
|
|
1106
|
+
status_color = "green" if proc['status'] == 'running' else "yellow"
|
|
1107
|
+
console.print(f" • PID: {proc['process_id']} - {proc['command'][:50]}... [{proc['status']}]", style=status_color)
|
|
1108
|
+
console.print(f" Running for: {proc['running_time']:.1f}s", style="dim")
|
|
1109
|
+
console.print("\n💡 Use /ct <process_id> to terminate a process\n", style="dim")
|
|
1110
|
+
else:
|
|
1111
|
+
console.print("❌ Failed to get process list", style="red")
|
|
1112
|
+
except Exception as e:
|
|
1113
|
+
console.print(f"❌ Error: {str(e)}", style="red")
|
|
1114
|
+
|
|
1115
|
+
return True
|
|
1116
|
+
|
|
1117
|
+
elif cmd == "ct" or cmd == "cancel":
|
|
1118
|
+
# Cancel/terminate a background process
|
|
1119
|
+
if len(parts) < 2:
|
|
1120
|
+
console.print("❌ Usage: /ct <process_id>", style="red")
|
|
1121
|
+
console.print("💡 Tip: Process IDs are shown when commands run in background", style="dim")
|
|
1122
|
+
return True
|
|
1123
|
+
|
|
1124
|
+
process_id = parts[1]
|
|
1125
|
+
try:
|
|
1126
|
+
result = await ai_engine.tool_registry.execute_tool(
|
|
1127
|
+
"command_runner",
|
|
1128
|
+
operation="kill_process",
|
|
1129
|
+
process_id=process_id,
|
|
1130
|
+
user_id="ai_engine"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
if result.success:
|
|
1134
|
+
console.print(f"✅ Process {process_id} terminated successfully", style="green")
|
|
1135
|
+
else:
|
|
1136
|
+
console.print(f"❌ Failed to terminate process: {result.error}", style="red")
|
|
1137
|
+
except Exception as e:
|
|
1138
|
+
console.print(f"❌ Error: {str(e)}", style="red")
|
|
1139
|
+
|
|
1140
|
+
return True
|
|
1141
|
+
|
|
1025
1142
|
elif cmd == "exit" or cmd == "quit":
|
|
1026
1143
|
return False
|
|
1027
1144
|
|
|
@@ -1033,6 +1150,7 @@ def show_help():
|
|
|
1033
1150
|
"""Show help information"""
|
|
1034
1151
|
help_text = Text()
|
|
1035
1152
|
help_text.append("Available commands:\n", style="bold")
|
|
1153
|
+
help_text.append("• Press Shift+Tab - Toggle between Chat and Terminal modes\n", style="bold yellow")
|
|
1036
1154
|
help_text.append("• /help - Show this help message\n")
|
|
1037
1155
|
help_text.append("• /workspace <path> or /ws <path> - Change working directory\n")
|
|
1038
1156
|
help_text.append("• /setup - Run interactive setup wizard\n")
|
|
@@ -1050,6 +1168,8 @@ def show_help():
|
|
|
1050
1168
|
help_text.append(" - /rules remove workspace <index> - Remove a workspace rule\n", style="dim")
|
|
1051
1169
|
help_text.append(" - /rules clear global|workspace - Clear all rules of a type\n", style="dim")
|
|
1052
1170
|
help_text.append("• /speed [instant|fast|normal|slow|<number>] - Set typing speed for AI responses\n")
|
|
1171
|
+
help_text.append("• /ps or /processes - List all running background processes\n")
|
|
1172
|
+
help_text.append("• /ct <process_id> or /cancel <process_id> - Terminate a background process\n")
|
|
1053
1173
|
help_text.append("• /clear - Clear chat screen\n")
|
|
1054
1174
|
help_text.append("• /exit or /quit - Exit chat session\n")
|
|
1055
1175
|
help_text.append("• exit or quit - Exit chat session\n")
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File reader tool for reading and searching files
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .base import BaseTool, ToolResult, PermissionLevel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileReaderTool(BaseTool):
|
|
14
|
+
"""Tool for reading files and searching content"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(
|
|
18
|
+
name="file_reader",
|
|
19
|
+
description="Read file contents and search in files",
|
|
20
|
+
permission_level=PermissionLevel.READ_ONLY
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def get_capabilities(self) -> List[str]:
|
|
24
|
+
return [
|
|
25
|
+
"read_file",
|
|
26
|
+
"grep_search",
|
|
27
|
+
"list_directory"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
async def execute(self, operation: str, **kwargs) -> ToolResult:
|
|
31
|
+
"""Execute file reader operation"""
|
|
32
|
+
|
|
33
|
+
operations = {
|
|
34
|
+
'read_file': self._read_file,
|
|
35
|
+
'grep_search': self._grep_search,
|
|
36
|
+
'list_directory': self._list_directory
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if operation not in operations:
|
|
40
|
+
return ToolResult(
|
|
41
|
+
success=False,
|
|
42
|
+
error=f"Unknown operation: {operation}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
result = await operations[operation](**kwargs)
|
|
47
|
+
return ToolResult(success=True, data=result)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return ToolResult(success=False, error=str(e))
|
|
50
|
+
|
|
51
|
+
async def _read_file(
|
|
52
|
+
self,
|
|
53
|
+
file_path: str,
|
|
54
|
+
start_line: int = None,
|
|
55
|
+
end_line: int = None,
|
|
56
|
+
max_lines: int = 1000
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""Read file contents"""
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
path = Path(file_path).expanduser().resolve()
|
|
62
|
+
|
|
63
|
+
if not path.exists():
|
|
64
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
65
|
+
|
|
66
|
+
if not path.is_file():
|
|
67
|
+
raise ValueError(f"Not a file: {file_path}")
|
|
68
|
+
|
|
69
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
70
|
+
lines = f.readlines()
|
|
71
|
+
|
|
72
|
+
total_lines = len(lines)
|
|
73
|
+
|
|
74
|
+
if start_line is not None or end_line is not None:
|
|
75
|
+
start = (start_line - 1) if start_line else 0
|
|
76
|
+
end = end_line if end_line else total_lines
|
|
77
|
+
lines = lines[start:end]
|
|
78
|
+
|
|
79
|
+
if len(lines) > max_lines:
|
|
80
|
+
lines = lines[:max_lines]
|
|
81
|
+
truncated = True
|
|
82
|
+
else:
|
|
83
|
+
truncated = False
|
|
84
|
+
|
|
85
|
+
content = ''.join(lines)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
'file_path': str(path),
|
|
89
|
+
'content': content,
|
|
90
|
+
'total_lines': total_lines,
|
|
91
|
+
'lines_returned': len(lines),
|
|
92
|
+
'truncated': truncated
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise Exception(f"Failed to read file: {str(e)}")
|
|
97
|
+
|
|
98
|
+
async def _grep_search(
|
|
99
|
+
self,
|
|
100
|
+
pattern: str,
|
|
101
|
+
search_path: str,
|
|
102
|
+
file_pattern: str = "*",
|
|
103
|
+
recursive: bool = True,
|
|
104
|
+
case_sensitive: bool = False,
|
|
105
|
+
max_results: int = 100
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""Search for pattern in files"""
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
search_path = Path(search_path).expanduser().resolve()
|
|
111
|
+
|
|
112
|
+
if not search_path.exists():
|
|
113
|
+
raise FileNotFoundError(f"Path not found: {search_path}")
|
|
114
|
+
|
|
115
|
+
results = []
|
|
116
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
117
|
+
regex = re.compile(pattern, flags)
|
|
118
|
+
|
|
119
|
+
if search_path.is_file():
|
|
120
|
+
files_to_search = [search_path]
|
|
121
|
+
else:
|
|
122
|
+
if recursive:
|
|
123
|
+
files_to_search = list(search_path.rglob(file_pattern))
|
|
124
|
+
else:
|
|
125
|
+
files_to_search = list(search_path.glob(file_pattern))
|
|
126
|
+
|
|
127
|
+
files_to_search = [f for f in files_to_search if f.is_file()]
|
|
128
|
+
|
|
129
|
+
for file_path in files_to_search:
|
|
130
|
+
if len(results) >= max_results:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
135
|
+
for line_num, line in enumerate(f, 1):
|
|
136
|
+
if len(results) >= max_results:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if regex.search(line):
|
|
140
|
+
results.append({
|
|
141
|
+
'file': str(file_path),
|
|
142
|
+
'line_number': line_num,
|
|
143
|
+
'line_content': line.rstrip('\n'),
|
|
144
|
+
'match': regex.search(line).group(0)
|
|
145
|
+
})
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
'pattern': pattern,
|
|
151
|
+
'search_path': str(search_path),
|
|
152
|
+
'total_matches': len(results),
|
|
153
|
+
'matches': results,
|
|
154
|
+
'truncated': len(results) >= max_results
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
raise Exception(f"Failed to search: {str(e)}")
|
|
159
|
+
|
|
160
|
+
async def _list_directory(
|
|
161
|
+
self,
|
|
162
|
+
directory_path: str,
|
|
163
|
+
show_hidden: bool = False,
|
|
164
|
+
recursive: bool = False,
|
|
165
|
+
max_depth: int = 3
|
|
166
|
+
) -> Dict[str, Any]:
|
|
167
|
+
"""List directory contents"""
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
path = Path(directory_path).expanduser().resolve()
|
|
171
|
+
|
|
172
|
+
if not path.exists():
|
|
173
|
+
raise FileNotFoundError(f"Directory not found: {directory_path}")
|
|
174
|
+
|
|
175
|
+
if not path.is_dir():
|
|
176
|
+
raise ValueError(f"Not a directory: {directory_path}")
|
|
177
|
+
|
|
178
|
+
items = []
|
|
179
|
+
|
|
180
|
+
if recursive:
|
|
181
|
+
for item in path.rglob('*'):
|
|
182
|
+
if not show_hidden and item.name.startswith('.'):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
relative = item.relative_to(path)
|
|
187
|
+
depth = len(relative.parts)
|
|
188
|
+
if depth > max_depth:
|
|
189
|
+
continue
|
|
190
|
+
except ValueError:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
items.append({
|
|
194
|
+
'path': str(item),
|
|
195
|
+
'name': item.name,
|
|
196
|
+
'type': 'directory' if item.is_dir() else 'file',
|
|
197
|
+
'size': item.stat().st_size if item.is_file() else None
|
|
198
|
+
})
|
|
199
|
+
else:
|
|
200
|
+
for item in path.iterdir():
|
|
201
|
+
if not show_hidden and item.name.startswith('.'):
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
items.append({
|
|
205
|
+
'path': str(item),
|
|
206
|
+
'name': item.name,
|
|
207
|
+
'type': 'directory' if item.is_dir() else 'file',
|
|
208
|
+
'size': item.stat().st_size if item.is_file() else None
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
'directory': str(path),
|
|
213
|
+
'total_items': len(items),
|
|
214
|
+
'items': items
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise Exception(f"Failed to list directory: {str(e)}")
|
cognautic/tools/registry.py
CHANGED
|
@@ -48,6 +48,7 @@ class ToolRegistry:
|
|
|
48
48
|
from .web_search import WebSearchTool
|
|
49
49
|
from .code_analysis import CodeAnalysisTool
|
|
50
50
|
from .response_control import ResponseControlTool
|
|
51
|
+
from .file_reader import FileReaderTool
|
|
51
52
|
|
|
52
53
|
# Register tools
|
|
53
54
|
self.register_tool(FileOperationsTool())
|
|
@@ -55,6 +56,7 @@ class ToolRegistry:
|
|
|
55
56
|
self.register_tool(WebSearchTool())
|
|
56
57
|
self.register_tool(CodeAnalysisTool())
|
|
57
58
|
self.register_tool(ResponseControlTool())
|
|
59
|
+
self.register_tool(FileReaderTool())
|
|
58
60
|
|
|
59
61
|
def register_tool(self, tool: BaseTool):
|
|
60
62
|
"""Register a tool"""
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cognautic-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: A Python-based CLI AI coding agent that provides agentic development capabilities with multi-provider AI support and real-time interaction
|
|
5
|
-
Home-page: https://github.com/cognautic/cli
|
|
6
|
-
Author:
|
|
7
|
-
Author-email:
|
|
8
|
-
Maintainer-email:
|
|
5
|
+
Home-page: https://github.com/cognautic/cognautic-cli
|
|
6
|
+
Author: Cognautic
|
|
7
|
+
Author-email: Cognautic <cognautic@gmail.com>
|
|
8
|
+
Maintainer-email: Cognautic <cognautic@gmail.com>
|
|
9
9
|
License: Proprietary - All Rights Reserved
|
|
10
10
|
Project-URL: Homepage, https://github.com/cognautic/cli
|
|
11
11
|
Project-URL: Documentation, https://cognautic.vercel.app/cognautic-cli.html
|
|
@@ -43,6 +43,7 @@ Requires-Dist: anthropic>=0.7.0
|
|
|
43
43
|
Requires-Dist: google-generativeai>=0.3.0
|
|
44
44
|
Requires-Dist: together>=0.2.0
|
|
45
45
|
Requires-Dist: nest-asyncio>=1.5.0
|
|
46
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
46
47
|
Provides-Extra: tools
|
|
47
48
|
Requires-Dist: gitpython>=3.1.0; extra == "tools"
|
|
48
49
|
Requires-Dist: pyyaml>=6.0; extra == "tools"
|
|
@@ -78,7 +79,7 @@ Cognautic CLI is a Python-based command-line interface that brings AI-powered de
|
|
|
78
79
|
|
|
79
80
|
| Property | Value |
|
|
80
81
|
|----------|-------|
|
|
81
|
-
| **Developer** |
|
|
82
|
+
| **Developer** | Cognautic |
|
|
82
83
|
| **Written in** | Python |
|
|
83
84
|
| **Operating system** | Cross-platform |
|
|
84
85
|
| **Type** | AI Development Tool |
|
|
@@ -597,8 +598,4 @@ You: /model claude-3-sonnet-20240229
|
|
|
597
598
|
|
|
598
599
|
## License
|
|
599
600
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
© 2024 cognautic
|
|
603
|
-
|
|
604
|
-
For licensing inquiries, contact: cognautic@gmail.com
|
|
601
|
+
MIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
cognautic/__init__.py,sha256=
|
|
2
|
-
cognautic/ai_engine.py,sha256=
|
|
3
|
-
cognautic/auto_continuation.py,sha256
|
|
4
|
-
cognautic/cli.py,sha256=
|
|
1
|
+
cognautic/__init__.py,sha256=Wn_G_YckyznLr_f_1-MsYNMuCyHtHomSp2qYHWxiEqE,141
|
|
2
|
+
cognautic/ai_engine.py,sha256=W9liShtN7KL8jnDPcSR6BeqRT9xmtBBQnIx-FJTyuag,106097
|
|
3
|
+
cognautic/auto_continuation.py,sha256=-jJU17fdo6Ww26wtLxO8PkQ5itsARmrNlCFroMgCZSM,9032
|
|
4
|
+
cognautic/cli.py,sha256=Leutgyw3WKLovSsRwxsGPGKeeoAnumDxSnxN7CCWUW4,56518
|
|
5
5
|
cognautic/config.py,sha256=mrkvBA2gz_i9PuER6K75RxVeLBcYbjwNYBV2NskFQVk,9115
|
|
6
6
|
cognautic/file_tagger.py,sha256=DBsBIggtI0pGdbxY9XOMHtVG1k9RYCihqrAEfM68xGI,8532
|
|
7
7
|
cognautic/memory.py,sha256=5oLWmkaIy5rEJ1yJ4xldJIzDfvvAKqkxz0PWk53heis,15208
|
|
@@ -14,12 +14,13 @@ cognautic/tools/base.py,sha256=PtEMTyntWwhO-SFtkMSSnhR0TXrCWqUy5Tg_yPIldCk,1590
|
|
|
14
14
|
cognautic/tools/code_analysis.py,sha256=rWS-yz0yzmF33IEHhJsN22j4e26WDDa7Fv30PF3zBhk,15387
|
|
15
15
|
cognautic/tools/command_runner.py,sha256=-C3VaSlFyZOiytxizspnz-PDONdrfut4rOXh3Vbbb6M,10032
|
|
16
16
|
cognautic/tools/file_operations.py,sha256=_p_e5ZQq3Q8TGL21cRyIDoaeuDshz3_-h_C5kMLnc68,14073
|
|
17
|
-
cognautic/tools/
|
|
17
|
+
cognautic/tools/file_reader.py,sha256=IAi4mCnauIghlWiBshTp_ZYJ_XyWqpnEJ7gJuD4NxvA,7435
|
|
18
|
+
cognautic/tools/registry.py,sha256=Dk_tgj2FyLerha2k92PIgLfLlOw0vTsgzw3t3_3W4Hw,3955
|
|
18
19
|
cognautic/tools/response_control.py,sha256=rb9h5mYi80uc-izUkuCcaUXOeonigX-zFvzeUlTBwcE,1480
|
|
19
20
|
cognautic/tools/web_search.py,sha256=tXHy1oB_33_peopJu3Xc7k7_NWuZNfLGfXzv9w7p9bM,13863
|
|
20
|
-
cognautic_cli-1.1.
|
|
21
|
-
cognautic_cli-1.1.
|
|
22
|
-
cognautic_cli-1.1.
|
|
23
|
-
cognautic_cli-1.1.
|
|
24
|
-
cognautic_cli-1.1.
|
|
25
|
-
cognautic_cli-1.1.
|
|
21
|
+
cognautic_cli-1.1.2.dist-info/licenses/LICENSE,sha256=N_qTILhnbmexBombE-njuJguCu9u2b1gwCvOVKnFWno,1066
|
|
22
|
+
cognautic_cli-1.1.2.dist-info/METADATA,sha256=K-F7985c4YQtGw55hIp6hn-SUPG8Kk-P3S9A5R2PZNk,17481
|
|
23
|
+
cognautic_cli-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
cognautic_cli-1.1.2.dist-info/entry_points.txt,sha256=QoF70RcDm_QElmIC7pkmhbFsWhuWSbxQksGaFXKBs8Q,49
|
|
25
|
+
cognautic_cli-1.1.2.dist-info/top_level.txt,sha256=zikDi43HL2zOV9cF_PAv6RBhJruTjSk6AMkIOTX12do,10
|
|
26
|
+
cognautic_cli-1.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|