raggiecode 0.2.1__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 (93) hide show
  1. Agent/__init__.py +0 -0
  2. Agent/agent.py +891 -0
  3. Agent/chat_history_db.py +1500 -0
  4. Agent/command.py +49 -0
  5. Agent/config.py +46 -0
  6. Agent/effort_levels.py +33 -0
  7. Agent/git_manager.py +727 -0
  8. Agent/tools.py +35 -0
  9. Commands/__init__.py +18 -0
  10. Commands/effort.py +42 -0
  11. Commands/global_todo.py +23 -0
  12. Commands/help.py +22 -0
  13. Commands/reasoning.py +24 -0
  14. Commands/redo.py +11 -0
  15. Commands/reindex.py +27 -0
  16. Commands/shell.py +28 -0
  17. Commands/stream.py +24 -0
  18. Commands/undo.py +13 -0
  19. Commands/unlimited_effort.py +8 -0
  20. Commands/window_size.py +29 -0
  21. RAG/__init__.py +0 -0
  22. RAG/document.py +119 -0
  23. RAG/find.py +408 -0
  24. RAG/graph.py +231 -0
  25. Tools/GetFileCodeStructure.py +43 -0
  26. Tools/GetSymbolSourceCode.py +27 -0
  27. Tools/__init__.py +39 -0
  28. Tools/ask_user.py +102 -0
  29. Tools/dispatch_subagent.py +215 -0
  30. Tools/document.py +35 -0
  31. Tools/edit_symbol.py +250 -0
  32. Tools/fuzzy_search.py +119 -0
  33. Tools/list_dir.py +51 -0
  34. Tools/read.py +49 -0
  35. Tools/read_image.py +75 -0
  36. Tools/remove.py +75 -0
  37. Tools/replace.py +305 -0
  38. Tools/search.py +41 -0
  39. Tools/shell.py +149 -0
  40. Tools/shell_kill.py +87 -0
  41. Tools/temp_background_service.py +113 -0
  42. Tools/todo_list.py +481 -0
  43. Tools/utils.py +116 -0
  44. Tools/view_changes.py +179 -0
  45. Tools/walk_call_tree.py +30 -0
  46. Tools/web_fetch.py +175 -0
  47. Tools/web_search.py +69 -0
  48. Tools/write.py +48 -0
  49. cli.py +111 -0
  50. config/__init__.py +0 -0
  51. config/coder_system_prompt.md +119 -0
  52. config/roles.json +43 -0
  53. config/tools.json +709 -0
  54. indexing/__init__.py +0 -0
  55. indexing/cli.py +128 -0
  56. indexing/code_index_sdk.py +832 -0
  57. indexing/code_indexer.py +1763 -0
  58. indexing/db_schema.py +396 -0
  59. indexing/export_to_json.py +346 -0
  60. indexing/extractors.py +189 -0
  61. indexing/file_utils.py +97 -0
  62. indexing/frontend/__init__.py +0 -0
  63. indexing/frontend/css_extractor.py +195 -0
  64. indexing/frontend/css_parser.py +387 -0
  65. indexing/frontend/css_selector_utils.py +226 -0
  66. indexing/frontend/edit_safety.py +573 -0
  67. indexing/frontend/graph.py +838 -0
  68. indexing/frontend/html_extractor.py +496 -0
  69. indexing/frontend/html_parser.py +314 -0
  70. indexing/frontend/jsx_extractor.py +1204 -0
  71. indexing/frontend/location_lookup.py +247 -0
  72. indexing/frontend/resolver.py +485 -0
  73. indexing/frontend/runtime_resolver.py +862 -0
  74. indexing/frontend/semantic_output.py +705 -0
  75. indexing/frontend/source_location.py +69 -0
  76. indexing/frontend_config.py +72 -0
  77. indexing/frontend_models.py +347 -0
  78. indexing/language_config.py +360 -0
  79. indexing/models.py +284 -0
  80. indexing/node_utils.py +1112 -0
  81. indexing/parse_worker.py +1082 -0
  82. indexing/queries.py +1542 -0
  83. indexing/sdk_examples.py +426 -0
  84. interactive.py +248 -0
  85. raggie.py +673 -0
  86. raggiecode-0.2.1.dist-info/METADATA +944 -0
  87. raggiecode-0.2.1.dist-info/RECORD +93 -0
  88. raggiecode-0.2.1.dist-info/WHEEL +5 -0
  89. raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
  90. raggiecode-0.2.1.dist-info/top_level.txt +10 -0
  91. skills/__init__.py +3 -0
  92. skills/manager.py +114 -0
  93. skills/tool.py +121 -0
@@ -0,0 +1,27 @@
1
+ from RAG.find import find_symbol_implementation
2
+ from .utils import is_within_cwd, BLUE, RESET
3
+
4
+
5
+ def handle(arguments, toolcall_id):
6
+ symbol_name = arguments["symbol_name"]
7
+ file_path = arguments.get("file_path")
8
+ print(f"{BLUE}GetSymbolSourceCode {symbol_name}{RESET}")
9
+
10
+ # Check if the file is outside the current working directory
11
+ if file_path and not is_within_cwd(file_path):
12
+ return {
13
+ "role": "tool",
14
+ "tool_call_id": toolcall_id,
15
+ "content": "Error: access denied - path is outside the current working directory",
16
+ }
17
+
18
+ try:
19
+ result = find_symbol_implementation(symbol_name, file_path)
20
+ except Exception as e:
21
+ result = f"Error finding symbol implementation: {str(e)}"
22
+
23
+ return {
24
+ "role": "tool",
25
+ "tool_call_id": toolcall_id,
26
+ "content": result,
27
+ }
Tools/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ from Agent.tools import ToolRegistry
2
+
3
+ def setup_toolcalls(registry: ToolRegistry):
4
+ from . import shell, temp_background_service, shell_kill, read, write, replace, remove, GetSymbolSourceCode, GetFileCodeStructure, document, walk_call_tree, list_dir, search, fuzzy_search, read_image, dispatch_subagent, todo_list, web_fetch, web_search, view_changes, ask_user, edit_symbol
5
+ from skills.tool import handle as update_skill_handle
6
+ from skills.tool import handle_get_skill as get_skill_handle
7
+
8
+ registry.set_handler("UglyWholeFileContentDump", read.handle)
9
+ registry.set_handler("SearchAllFilesContent", search.handle)
10
+ registry.set_handler("FileNameSearch", fuzzy_search.handle)
11
+ registry.set_handler("ListDir", list_dir.handle)
12
+ registry.set_handler("WriteFile", write.handle)
13
+ registry.set_handler("Shell", shell.handle)
14
+ registry.set_handler("TempBackgroundService", temp_background_service.handle)
15
+ registry.set_handler("ShellKill", shell_kill.handle)
16
+ registry.set_handler("ReplaceText", replace.handle)
17
+ registry.set_handler("RemoveFile", remove.handle)
18
+ registry.set_handler("GetSymbolSourceCode", GetSymbolSourceCode.handle)
19
+ registry.set_handler("GetFileCodeSemantics", GetFileCodeStructure.handle)
20
+ registry.set_handler("WalkCallTree", walk_call_tree.handle)
21
+ registry.set_handler("Document", document.handle)
22
+ registry.set_handler("SetSkill", update_skill_handle)
23
+ registry.set_handler("GetSkill", get_skill_handle)
24
+ registry.set_handler("ReadImage", read_image.handle)
25
+ registry.set_handler("WebFetch", web_fetch.handle)
26
+ registry.set_handler("WebSearch", web_search.handle)
27
+ registry.set_handler("DispatchSubagent", dispatch_subagent.handle)
28
+ registry.set_handler("CreateTodoList", todo_list.handle_create_todo_list)
29
+ registry.set_handler("AddTask", todo_list.handle_add_task)
30
+ registry.set_handler("GetTodoList", todo_list.handle_get_todo_list)
31
+ registry.set_handler("ApproveTodoList", todo_list.handle_approve_todo_list)
32
+ registry.set_handler("ExecuteNextTask", todo_list.handle_execute_next_task)
33
+ registry.set_handler("MarkTaskComplete", todo_list.handle_mark_task_complete)
34
+ registry.set_handler("MarkTaskFailed", todo_list.handle_mark_task_failed)
35
+ registry.set_handler("MarkTaskCancelled", todo_list.handle_mark_task_cancelled)
36
+ registry.set_handler("GetActiveTodoList", todo_list.handle_get_active_todo_list)
37
+ registry.set_handler("AskUser", ask_user.handle)
38
+ registry.set_handler("ViewChanges", view_changes.handle)
39
+ registry.set_handler("EditSymbol", edit_symbol.handle)
Tools/ask_user.py ADDED
@@ -0,0 +1,102 @@
1
+ from .utils import BLUE, GREEN, YELLOW, RED, RESET
2
+
3
+
4
+ def handle(arguments, toolcall_id):
5
+ """Ask the user a question, optionally with predefined options."""
6
+ from prompt_toolkit import prompt
7
+
8
+ question = arguments.get("question", "")
9
+ options = arguments.get("options", [])
10
+ allow_multiple = arguments.get("allow_multiple", False)
11
+
12
+ if not question:
13
+ return {
14
+ "role": "tool",
15
+ "tool_call_id": toolcall_id,
16
+ "content": "Error: 'question' is required",
17
+ }
18
+
19
+ try:
20
+ print(f"\n{BLUE}Agent has a question:{RESET}")
21
+ print(f"{YELLOW}{question}{RESET}")
22
+ print()
23
+
24
+ if not options:
25
+ # Free-form question
26
+ user_input = prompt("Your answer: ", multiline=True).strip()
27
+ print()
28
+ return {
29
+ "role": "tool",
30
+ "tool_call_id": toolcall_id,
31
+ "content": f"User answered: {user_input}",
32
+ }
33
+
34
+ # Display numbered options
35
+ print(f"{BLUE}Options:{RESET}")
36
+ for i, opt in enumerate(options):
37
+ label = opt.get("label", f"Option {i + 1}")
38
+ description = opt.get("description", "")
39
+ print(f" {GREEN}{i + 1}.{RESET} {label}")
40
+ if description:
41
+ print(f" {description}")
42
+ print()
43
+
44
+ if allow_multiple:
45
+ hint = "Enter one or more numbers (comma-separated), or type your own answer"
46
+ else:
47
+ hint = "Enter a number, or type your own answer"
48
+
49
+ user_input = prompt(f"{hint}: ", multiline=True).strip()
50
+ print()
51
+
52
+ if not user_input:
53
+ return {
54
+ "role": "tool",
55
+ "tool_call_id": toolcall_id,
56
+ "content": "User skipped the question (no answer provided)",
57
+ }
58
+
59
+ # Try to parse as number(s)
60
+ selected_labels = []
61
+ try:
62
+ if allow_multiple:
63
+ indices = [int(x.strip()) for x in user_input.split(",")]
64
+ for idx in indices:
65
+ if 1 <= idx <= len(options):
66
+ selected_labels.append(options[idx - 1].get("label", f"Option {idx}"))
67
+ else:
68
+ selected_labels.append(f"(invalid: {idx})")
69
+ else:
70
+ idx = int(user_input)
71
+ if 1 <= idx <= len(options):
72
+ selected_labels.append(options[idx - 1].get("label", f"Option {idx}"))
73
+ else:
74
+ selected_labels.append(f"(invalid: {idx})")
75
+ answer = ", ".join(selected_labels)
76
+ print(f"{GREEN}User selected: {answer}{RESET}\n")
77
+ return {
78
+ "role": "tool",
79
+ "tool_call_id": toolcall_id,
80
+ "content": f"User selected: {answer}",
81
+ }
82
+ except ValueError:
83
+ # User typed a custom answer
84
+ print(f"{GREEN}User answered: {user_input}{RESET}\n")
85
+ return {
86
+ "role": "tool",
87
+ "tool_call_id": toolcall_id,
88
+ "content": f"User answered: {user_input}",
89
+ }
90
+ except (KeyboardInterrupt, EOFError):
91
+ print(f"\n{RED}User cancelled the question{RESET}\n")
92
+ return {
93
+ "role": "tool",
94
+ "tool_call_id": toolcall_id,
95
+ "content": "User cancelled the question (Ctrl+C)",
96
+ }
97
+ except Exception as e:
98
+ return {
99
+ "role": "tool",
100
+ "tool_call_id": toolcall_id,
101
+ "content": f"Failed to ask user: {str(e)}",
102
+ }
@@ -0,0 +1,215 @@
1
+ from .utils import BLUE, RESET
2
+
3
+
4
+ def _get_session_depth(session_id: int) -> int:
5
+ """Calculate the depth of a session in the subagent hierarchy by traversing parent_session_id."""
6
+ import sqlite3
7
+ from pathlib import Path
8
+
9
+ DB_PATH = Path(".raggie/.raggie.chat")
10
+ depth = 0
11
+ current_id = session_id
12
+ visited = set()
13
+
14
+ conn = sqlite3.connect(DB_PATH)
15
+ cursor = conn.cursor()
16
+
17
+ try:
18
+ while current_id is not None and current_id not in visited:
19
+ visited.add(current_id)
20
+ cursor.execute("""
21
+ SELECT parent_session_id FROM sessions WHERE id = ?
22
+ """, (current_id,))
23
+ result = cursor.fetchone()
24
+ if result and result[0] is not None:
25
+ current_id = result[0]
26
+ depth += 1
27
+ else:
28
+ break
29
+ finally:
30
+ conn.close()
31
+
32
+ return depth
33
+
34
+
35
+ def _collect_subagent_output(subagent, subagent_session_id, prompt=None, resume=False):
36
+ """Run a subagent and collect its output. If resume=True, resume dangling work instead of starting fresh."""
37
+ output_parts = []
38
+ error_occurred = False
39
+
40
+ try:
41
+ if resume:
42
+ events = subagent.resume_dangling_tool_work()
43
+ else:
44
+ events = subagent.start(prompt=prompt)
45
+
46
+ if events is None:
47
+ events = []
48
+
49
+ for event_type, *event_data in events:
50
+ if event_type == "response":
51
+ output_parts.append(event_data[0])
52
+ elif event_type == "response_end":
53
+ output_parts.append(event_data[0])
54
+ elif event_type == "error":
55
+ output_parts.append(f"Error: {event_data[0]}")
56
+ error_occurred = True
57
+ break
58
+ except Exception as e:
59
+ output_parts.append(f"Subagent execution failed: {str(e)}")
60
+ error_occurred = True
61
+
62
+ output = "\n".join(output_parts) if output_parts else "No output from subagent"
63
+
64
+ if error_occurred:
65
+ output = f"Subagent encountered errors:\n{output}"
66
+
67
+ from Agent.chat_history_db import get_all_changes_by_session_chain
68
+ changes = get_all_changes_by_session_chain(subagent_session_id)
69
+ if changes:
70
+ changes_summary = "\n".join(
71
+ f" - [{c['change_type']}] {c['file_path'] or 'N/A'}: {c['description']}"
72
+ for c in changes
73
+ )
74
+ output = f"{output}\n\n--- Changes made by subagent for you to review ---\n{changes_summary}"
75
+
76
+ return output
77
+
78
+
79
+ def handle(arguments, toolcall_id, parent_session_id=None, skip_depth_check=False):
80
+ prompt = arguments.get("prompt")
81
+
82
+ if not prompt:
83
+ return {
84
+ "role": "tool",
85
+ "tool_call_id": toolcall_id,
86
+ "content": "Error: 'prompt' is a required parameter",
87
+ }
88
+
89
+ if parent_session_id is None:
90
+ return {
91
+ "role": "tool",
92
+ "tool_call_id": toolcall_id,
93
+ "content": "Error: parent_session_id is required to determine the subagent role",
94
+ }
95
+
96
+ from Agent.chat_history_db import get_session_role, get_session_effort, get_session_depth
97
+ from Agent.effort_levels import is_depth_allowed, effort_name, effort_max_depth
98
+ role = get_session_role(parent_session_id)
99
+ if not role:
100
+ return {
101
+ "role": "tool",
102
+ "tool_call_id": toolcall_id,
103
+ "content": f"Error: Could not determine role for parent session {parent_session_id}",
104
+ }
105
+
106
+ # Check depth limit via the effort system
107
+ if not skip_depth_check:
108
+ effort = get_session_effort(parent_session_id)
109
+ depth = get_session_depth(parent_session_id)
110
+ if effort is not None and not is_depth_allowed(effort, depth):
111
+ max_d = effort_max_depth(effort)
112
+ return {
113
+ "role": "tool",
114
+ "tool_call_id": toolcall_id,
115
+ "content": f"Cannot dispatch subagent: effort level '{effort_name(effort)}' limits depth to {max_d}. Current depth is {depth}.",
116
+ }
117
+
118
+ try:
119
+ from Agent.chat_history_db import (
120
+ get_chat_id_for_session, create_session, get_child_session_by_toolcall,
121
+ is_session_finished, load_messages,
122
+ get_session_effort, get_session_depth,
123
+ resolve_session_id,
124
+ )
125
+ from Agent.agent import Agent
126
+ from Tools import setup_toolcalls
127
+ from Commands import setup_commands
128
+
129
+ chat_id = get_chat_id_for_session(parent_session_id)
130
+ if chat_id is None:
131
+ return {
132
+ "role": "tool",
133
+ "tool_call_id": toolcall_id,
134
+ "content": f"Error: Could not find chat for parent session {parent_session_id}",
135
+ }
136
+
137
+ # Check for existing child session matching this toolcall_id (from a previous interrupted dispatch)
138
+ child = get_child_session_by_toolcall(parent_session_id, toolcall_id)
139
+ if child is not None:
140
+ child_session_id = child["id"]
141
+ # Follow redirect chain to find the active session (may have been handed over)
142
+ active_session_id = resolve_session_id(child_session_id)
143
+ if is_session_finished(child_session_id):
144
+ # Subagent completed but main agent was interrupted before processing the result
145
+ print(f"{BLUE}Found completed subagent session {active_session_id}, retrieving output...{RESET}")
146
+ messages = load_messages(active_session_id)
147
+ output_parts = []
148
+ for msg in messages:
149
+ if msg.get("role") == "assistant" and not msg.get("tool_calls"):
150
+ output_parts.append(msg.get("content", ""))
151
+ output = "\n".join(output_parts) if output_parts else "No output from subagent"
152
+
153
+ from Agent.chat_history_db import get_all_changes_by_session_chain
154
+ changes = get_all_changes_by_session_chain(active_session_id)
155
+ if changes:
156
+ changes_summary = "\n".join(
157
+ f" - [{c['change_type']}] {c['file_path'] or 'N/A'}: {c['description']}"
158
+ for c in changes
159
+ )
160
+ output = f"{output}\n\n--- Changes made by subagent for you to review ---\n{changes_summary}"
161
+
162
+ return {
163
+ "role": "tool",
164
+ "tool_call_id": toolcall_id,
165
+ "content": output,
166
+ "subagent_session_id": active_session_id,
167
+ }
168
+ else:
169
+ # Subagent was interrupted — resume it
170
+ print(f"{BLUE}Resuming interrupted subagent session {active_session_id}...{RESET}")
171
+ subagent = Agent(role=role, chat_id=chat_id, session_id=active_session_id)
172
+ setup_toolcalls(subagent.tool_registry)
173
+ setup_commands(subagent.command_registry)
174
+
175
+ output = _collect_subagent_output(
176
+ subagent, active_session_id, resume=True
177
+ )
178
+
179
+ return {
180
+ "role": "tool",
181
+ "tool_call_id": toolcall_id,
182
+ "content": output,
183
+ "subagent_session_id": active_session_id,
184
+ }
185
+
186
+ # No existing child session — create a new one under the parent's chat
187
+ print(f"{BLUE}Dispatching subagent with role '{role}'{RESET}")
188
+ parent_effort = get_session_effort(parent_session_id)
189
+ parent_depth = get_session_depth(parent_session_id)
190
+ subagent_session_id = create_session(
191
+ chat_id, parent_session_id=parent_session_id, toolcall_id=toolcall_id,
192
+ effort=parent_effort, depth=parent_depth + 1,
193
+ )
194
+
195
+ subagent = Agent(role=role, chat_id=chat_id, session_id=subagent_session_id)
196
+ setup_toolcalls(subagent.tool_registry)
197
+ setup_commands(subagent.command_registry)
198
+
199
+ output = _collect_subagent_output(
200
+ subagent, subagent_session_id, prompt=prompt, resume=False
201
+ )
202
+
203
+ return {
204
+ "role": "tool",
205
+ "tool_call_id": toolcall_id,
206
+ "content": output,
207
+ "subagent_session_id": subagent_session_id,
208
+ }
209
+
210
+ except Exception as e:
211
+ return {
212
+ "role": "tool",
213
+ "tool_call_id": toolcall_id,
214
+ "content": f"Failed to dispatch subagent: {str(e)}",
215
+ }
Tools/document.py ADDED
@@ -0,0 +1,35 @@
1
+ from RAG.document import update_symbol_description, get_symbol_description
2
+ from .utils import is_within_cwd, BLUE, RESET
3
+
4
+
5
+ def handle(arguments, toolcall_id):
6
+ action = arguments.get("action", "update")
7
+ symbol_name = arguments["symbol_name"]
8
+ print(f"{BLUE}Document {symbol_name}{RESET}")
9
+ symbol_type = arguments.get("symbol_type", "function")
10
+ file_path = arguments.get("file_path")
11
+
12
+ # Check if the file is outside the current working directory
13
+ if file_path and not is_within_cwd(file_path):
14
+ return {
15
+ "role": "tool",
16
+ "tool_call_id": toolcall_id,
17
+ "content": "Error: access denied - path is outside the current working directory",
18
+ }
19
+
20
+ try:
21
+ if action == "read":
22
+ result = get_symbol_description(symbol_name, symbol_type, file_path)
23
+ elif action == "update":
24
+ description = arguments["description"]
25
+ result = update_symbol_description(symbol_name, description, symbol_type, file_path)
26
+ else:
27
+ result = f"Error: Unknown action '{action}'. Must be 'read' or 'update'."
28
+ except Exception as e:
29
+ result = f"Error: {str(e)}"
30
+
31
+ return {
32
+ "role": "tool",
33
+ "tool_call_id": toolcall_id,
34
+ "content": result,
35
+ }
Tools/edit_symbol.py ADDED
@@ -0,0 +1,250 @@
1
+ import os
2
+
3
+ from RAG.find import find_symbol_location, find_frontend_entity_location
4
+ from .utils import is_ignored_by_gitignore, is_within_cwd, BLUE, RESET, auto_record_change, reindex_after_change
5
+
6
+ # Frontend entity types that can be edited via edit_safety
7
+ _FRONTEND_ENTITY_TYPES = {
8
+ "markup_element", "css_rule", "custom_property",
9
+ "event_binding", "property_binding", "jsx_subtree",
10
+ }
11
+
12
+
13
+ def handle(arguments, toolcall_id, session_id=None, code_indexer=None):
14
+ symbol_name = arguments["symbol_name"]
15
+ file_path = arguments.get("file_path")
16
+ new_source = arguments.get("new_source")
17
+ entity_type = arguments.get("entity_type")
18
+ entity_id = arguments.get("entity_id")
19
+
20
+ print(f"{BLUE}EditSymbol {symbol_name}{RESET}")
21
+
22
+ if not new_source:
23
+ return {
24
+ "role": "tool",
25
+ "tool_call_id": toolcall_id,
26
+ "content": "Error: 'new_source' cannot be empty.",
27
+ }
28
+
29
+ try:
30
+ # Frontend entity edit path
31
+ if entity_type and entity_type in _FRONTEND_ENTITY_TYPES and entity_id is not None:
32
+ return _handle_frontend_edit(
33
+ entity_type, entity_id, new_source, file_path,
34
+ toolcall_id, session_id, code_indexer,
35
+ )
36
+
37
+ # Standard symbol edit path (functions, classes)
38
+ loc = find_symbol_location(symbol_name, file_path)
39
+ if loc is None:
40
+ hint = ""
41
+ if file_path:
42
+ hint = f" in file '{file_path}'"
43
+ return {
44
+ "role": "tool",
45
+ "tool_call_id": toolcall_id,
46
+ "content": (
47
+ f"Error: Symbol '{symbol_name}' not found in code index{hint}. "
48
+ "Make sure the code index is up to date and the symbol name is correct."
49
+ ),
50
+ }
51
+
52
+ resolved_path = loc["file_path"]
53
+ start_line = loc["start_line"]
54
+ end_line = loc["end_line"]
55
+ old_source = loc["source"]
56
+
57
+ # Security checks
58
+ if not is_within_cwd(resolved_path):
59
+ return {
60
+ "role": "tool",
61
+ "tool_call_id": toolcall_id,
62
+ "content": "Error: access denied - path is outside the current working directory",
63
+ }
64
+
65
+ if is_ignored_by_gitignore(resolved_path):
66
+ return {
67
+ "role": "tool",
68
+ "tool_call_id": toolcall_id,
69
+ "content": (
70
+ f"Error: File '{resolved_path}' is in .gitignore. "
71
+ "Operations on gitignored files are not allowed."
72
+ ),
73
+ }
74
+
75
+ if not os.path.exists(resolved_path):
76
+ return {
77
+ "role": "tool",
78
+ "tool_call_id": toolcall_id,
79
+ "content": f"Error: File '{resolved_path}' does not exist.",
80
+ }
81
+
82
+ with open(resolved_path, "r", encoding="utf-8") as f:
83
+ content = f.read()
84
+
85
+ lines = content.split("\n")
86
+
87
+ # Replace lines [start_line-1 : end_line] (1-indexed inclusive → 0-indexed slice)
88
+ old_lines = lines[start_line - 1:end_line]
89
+ new_lines = new_source.split("\n")
90
+
91
+ new_file_lines = lines[:start_line - 1] + new_lines + lines[end_line:]
92
+ new_content = "\n".join(new_file_lines)
93
+
94
+ with open(resolved_path, "w", encoding="utf-8") as f:
95
+ f.write(new_content)
96
+
97
+ # Build diff view
98
+ CONTEXT = 5
99
+ ctx_start = max(0, start_line - 1 - CONTEXT)
100
+ ctx_end = min(len(lines), end_line + CONTEXT)
101
+
102
+ diff_lines = [
103
+ f"@@ {resolved_path}:{start_line}-{end_line} ({len(old_lines)} lines "
104
+ f"-> {len(new_lines)} lines) @@",
105
+ ]
106
+
107
+ for i in range(ctx_start, start_line - 1):
108
+ diff_lines.append(f" {lines[i]}")
109
+
110
+ for line in old_lines:
111
+ diff_lines.append(f" - {line}")
112
+
113
+ for line in new_lines:
114
+ diff_lines.append(f" + {line}")
115
+
116
+ for i in range(end_line, ctx_end):
117
+ diff_lines.append(f" {lines[i]}")
118
+
119
+ diff_text = "\n".join(diff_lines)
120
+
121
+ if session_id is not None:
122
+ from Agent.chat_history_db import record_session_file
123
+ record_session_file(session_id, resolved_path, "edit_symbol")
124
+ auto_record_change(
125
+ session_id, resolved_path, "file_edit",
126
+ f"Edited symbol '{symbol_name}' in {resolved_path}: replaced lines {start_line}-{end_line}",
127
+ diff_text,
128
+ )
129
+
130
+ reindex_after_change(code_indexer)
131
+
132
+ summary = (
133
+ f"Replaced {loc['kind']} '{symbol_name}' in {resolved_path} "
134
+ f"(lines {start_line}-{end_line} -> {len(new_lines)} lines):\n\n"
135
+ f"{diff_text}"
136
+ )
137
+
138
+ return {
139
+ "role": "tool",
140
+ "tool_call_id": toolcall_id,
141
+ "content": summary,
142
+ }
143
+
144
+ except Exception as e:
145
+ return {
146
+ "role": "tool",
147
+ "tool_call_id": toolcall_id,
148
+ "content": f"Error executing edit_symbol: {str(e)}",
149
+ }
150
+
151
+
152
+ def _handle_frontend_edit(entity_type, entity_id, new_source, file_path,
153
+ toolcall_id, session_id, code_indexer):
154
+ """Handle editing of frontend semantic entities via edit_safety."""
155
+ from pathlib import Path
156
+ from indexing.frontend.edit_safety import validate_edit_range, apply_frontend_edit
157
+
158
+ db_path = Path.cwd() / ".raggie" / ".code_index.raggie"
159
+ if not db_path.exists():
160
+ return {
161
+ "role": "tool",
162
+ "tool_call_id": toolcall_id,
163
+ "content": f"Error: Code index database not found at {db_path}",
164
+ }
165
+
166
+ import sqlite3
167
+ conn = sqlite3.connect(str(db_path))
168
+ conn.row_factory = sqlite3.Row
169
+
170
+ try:
171
+ # Resolve file_path if not provided
172
+ if not file_path:
173
+ loc = find_frontend_entity_location(entity_type, entity_id)
174
+ if loc is None:
175
+ return {
176
+ "role": "tool",
177
+ "tool_call_id": toolcall_id,
178
+ "content": f"Error: Could not resolve file path for {entity_type}#{entity_id}",
179
+ }
180
+ file_path = loc["file_path"]
181
+
182
+ # Security checks
183
+ resolved = file_path
184
+ if not os.path.isabs(resolved):
185
+ for base in [os.getcwd(), os.path.join(os.getcwd(), "src")]:
186
+ candidate = os.path.join(base, resolved)
187
+ if os.path.exists(candidate):
188
+ resolved = candidate
189
+ break
190
+
191
+ if not is_within_cwd(resolved):
192
+ return {
193
+ "role": "tool",
194
+ "tool_call_id": toolcall_id,
195
+ "content": "Error: access denied - path is outside the current working directory",
196
+ }
197
+
198
+ if is_ignored_by_gitignore(resolved):
199
+ return {
200
+ "role": "tool",
201
+ "tool_call_id": toolcall_id,
202
+ "content": (
203
+ f"Error: File '{resolved}' is in .gitignore. "
204
+ "Operations on gitignored files are not allowed."
205
+ ),
206
+ }
207
+
208
+ # Apply the edit
209
+ result = apply_frontend_edit(conn, entity_type, entity_id, new_source, file_path)
210
+
211
+ if not result.success:
212
+ return {
213
+ "role": "tool",
214
+ "tool_call_id": toolcall_id,
215
+ "content": f"Error: Edit rejected: {result.reason}",
216
+ }
217
+
218
+ # Record the change
219
+ if session_id is not None:
220
+ from Agent.chat_history_db import record_session_file
221
+ record_session_file(session_id, result.file_path, "edit_symbol")
222
+ auto_record_change(
223
+ session_id, result.file_path, "file_edit",
224
+ f"Edited {entity_type}#{entity_id} in {result.file_path}",
225
+ result.diff,
226
+ )
227
+
228
+ # Reindex
229
+ reindex_after_change(code_indexer)
230
+
231
+ summary = (
232
+ f"Replaced {entity_type}#{entity_id} in {result.file_path} "
233
+ f"(lines {result.source_range['start_line']}-{result.source_range['end_line']}):\n\n"
234
+ f"{result.diff}"
235
+ )
236
+
237
+ return {
238
+ "role": "tool",
239
+ "tool_call_id": toolcall_id,
240
+ "content": summary,
241
+ }
242
+
243
+ except Exception as e:
244
+ return {
245
+ "role": "tool",
246
+ "tool_call_id": toolcall_id,
247
+ "content": f"Error executing frontend edit: {str(e)}",
248
+ }
249
+ finally:
250
+ conn.close()