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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- skills/tool.py +121 -0
raggie.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Raggie - AI Coding Agent
|
|
4
|
+
Main entry point for the Raggie coding agent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
GREEN = "\033[32m"
|
|
15
|
+
DIM = "\033[2m"
|
|
16
|
+
RESET = "\033[0m"
|
|
17
|
+
|
|
18
|
+
from cli import parse_args
|
|
19
|
+
from Agent.agent import Agent
|
|
20
|
+
from Agent.chat_history_db import init_db, select_chat, create_chat
|
|
21
|
+
from Agent.config import load_keys, load_roles, save_roles, USER_CONFIG_DIR
|
|
22
|
+
from skills import SkillManager
|
|
23
|
+
from Tools import setup_toolcalls
|
|
24
|
+
from Commands import setup_commands
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def mask_key(key):
|
|
28
|
+
if len(key) <= 8:
|
|
29
|
+
return "*" * len(key)
|
|
30
|
+
return key[:4] + "*" * (len(key) - 8) + key[-4:]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_roles_command():
|
|
34
|
+
"""Interactive menu to list roles and edit their base URL and model."""
|
|
35
|
+
while True:
|
|
36
|
+
roles = load_roles()
|
|
37
|
+
print()
|
|
38
|
+
print("Agent Roles")
|
|
39
|
+
print("-" * 60)
|
|
40
|
+
if not roles:
|
|
41
|
+
print(" (no roles defined)")
|
|
42
|
+
else:
|
|
43
|
+
for i, (name, config) in enumerate(roles.items(), 1):
|
|
44
|
+
model = config.get("model", "?")
|
|
45
|
+
base_url = config.get("base_url", "?")
|
|
46
|
+
prompt_src = config.get("system_prompt_file", "") or config.get("system_prompt", "")
|
|
47
|
+
prompt_short = (prompt_src[:50] + "...") if len(str(prompt_src)) > 50 else prompt_src
|
|
48
|
+
ctx_window = config.get("context_window", "?")
|
|
49
|
+
reasoning = "on" if config.get("reasoning", False) else "off"
|
|
50
|
+
streaming = "on" if config.get("stream", False) else "off"
|
|
51
|
+
print(f" {i}. {name}")
|
|
52
|
+
print(f" Model: {model}")
|
|
53
|
+
print(f" Base URL: {base_url}")
|
|
54
|
+
print(f" Context Window: {ctx_window}")
|
|
55
|
+
print(f" Prompt: {prompt_short}")
|
|
56
|
+
print(f" Reasoning: {reasoning}")
|
|
57
|
+
print(f" Streaming: {streaming}")
|
|
58
|
+
print()
|
|
59
|
+
print("q. Exit")
|
|
60
|
+
print("1. Edit role's base URL / model")
|
|
61
|
+
print()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
choice = input("Choice: ").strip()
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
print()
|
|
67
|
+
return
|
|
68
|
+
except EOFError:
|
|
69
|
+
print()
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if choice == "1":
|
|
73
|
+
if not roles:
|
|
74
|
+
print("No roles to edit.")
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if len(roles) == 1:
|
|
78
|
+
idx = 1
|
|
79
|
+
else:
|
|
80
|
+
try:
|
|
81
|
+
idx = input("Number to edit (or 0 to cancel): ").strip()
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
print()
|
|
84
|
+
return
|
|
85
|
+
except EOFError:
|
|
86
|
+
print()
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
idx = int(idx)
|
|
91
|
+
except ValueError:
|
|
92
|
+
print("Invalid number.")
|
|
93
|
+
continue
|
|
94
|
+
if idx == 0:
|
|
95
|
+
continue
|
|
96
|
+
if idx < 1 or idx > len(roles):
|
|
97
|
+
print("Invalid selection.")
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
role_name = list(roles.keys())[idx - 1]
|
|
101
|
+
role = roles[role_name]
|
|
102
|
+
print(f"\nEditing '{role_name}'. Press Enter to keep current value.")
|
|
103
|
+
|
|
104
|
+
new_model = ""
|
|
105
|
+
new_url = ""
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
new_model = input(f"Model [{role.get('model', '')}]: ").strip()
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
print()
|
|
111
|
+
return
|
|
112
|
+
except EOFError:
|
|
113
|
+
print()
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if new_model:
|
|
117
|
+
role['model'] = new_model
|
|
118
|
+
|
|
119
|
+
# Base URL selection from keys.json
|
|
120
|
+
keys = load_keys()
|
|
121
|
+
key_urls = list(keys.keys())
|
|
122
|
+
current_url = role.get('base_url', '')
|
|
123
|
+
|
|
124
|
+
if key_urls:
|
|
125
|
+
print(f"\nAvailable base URLs (from your keys):")
|
|
126
|
+
for i, url in enumerate(key_urls, 1):
|
|
127
|
+
marker = " (current)" if url == current_url else ""
|
|
128
|
+
print(f" {i}. {url}{marker}")
|
|
129
|
+
print(f" 0. Enter a custom URL")
|
|
130
|
+
print()
|
|
131
|
+
try:
|
|
132
|
+
url_choice = input(f"Select base URL (or press Enter to keep current): ").strip()
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
print()
|
|
135
|
+
return
|
|
136
|
+
except EOFError:
|
|
137
|
+
print()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
if url_choice == "":
|
|
141
|
+
pass
|
|
142
|
+
elif url_choice == "0":
|
|
143
|
+
try:
|
|
144
|
+
new_url = input(f"Base URL [{current_url}]: ").strip()
|
|
145
|
+
except KeyboardInterrupt:
|
|
146
|
+
print()
|
|
147
|
+
return
|
|
148
|
+
except EOFError:
|
|
149
|
+
print()
|
|
150
|
+
return
|
|
151
|
+
if new_url:
|
|
152
|
+
role['base_url'] = new_url
|
|
153
|
+
else:
|
|
154
|
+
try:
|
|
155
|
+
url_idx = int(url_choice)
|
|
156
|
+
except ValueError:
|
|
157
|
+
print("Invalid selection, keeping current base URL.")
|
|
158
|
+
url_idx = -1
|
|
159
|
+
if url_idx >= 1 and url_idx <= len(key_urls):
|
|
160
|
+
role['base_url'] = key_urls[url_idx - 1]
|
|
161
|
+
elif url_idx != -1:
|
|
162
|
+
print("Invalid selection, keeping current base URL.")
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
new_url = input(f"Base URL [{current_url}] (no keys configured, run 'raggie keys' first): ").strip()
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
print()
|
|
168
|
+
return
|
|
169
|
+
except EOFError:
|
|
170
|
+
print()
|
|
171
|
+
return
|
|
172
|
+
if new_url:
|
|
173
|
+
role['base_url'] = new_url
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
new_ctx = input(f"Context Window [{role.get('context_window', '')}]: ").strip()
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
print()
|
|
179
|
+
return
|
|
180
|
+
except EOFError:
|
|
181
|
+
print()
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if new_ctx:
|
|
185
|
+
try:
|
|
186
|
+
role['context_window'] = int(new_ctx)
|
|
187
|
+
except ValueError:
|
|
188
|
+
print("Invalid context window value, keeping previous.")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
current_reasoning = role.get('reasoning', False)
|
|
192
|
+
new_reasoning = input(f"Reasoning (y/n) [{'on' if current_reasoning else 'off'}]: ").strip().lower()
|
|
193
|
+
except KeyboardInterrupt:
|
|
194
|
+
print()
|
|
195
|
+
return
|
|
196
|
+
except EOFError:
|
|
197
|
+
print()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if new_reasoning in ('y', 'yes'):
|
|
201
|
+
role['reasoning'] = True
|
|
202
|
+
elif new_reasoning in ('n', 'no'):
|
|
203
|
+
role['reasoning'] = False
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
current_stream = role.get('stream', False)
|
|
207
|
+
new_stream = input(f"Streaming (y/n) [{'on' if current_stream else 'off'}]: ").strip().lower()
|
|
208
|
+
except KeyboardInterrupt:
|
|
209
|
+
print()
|
|
210
|
+
return
|
|
211
|
+
except EOFError:
|
|
212
|
+
print()
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if new_stream in ('y', 'yes'):
|
|
216
|
+
role['stream'] = True
|
|
217
|
+
elif new_stream in ('n', 'no'):
|
|
218
|
+
role['stream'] = False
|
|
219
|
+
|
|
220
|
+
roles[role_name] = role
|
|
221
|
+
save_roles(roles)
|
|
222
|
+
print(f"Role '{role_name}' updated.")
|
|
223
|
+
|
|
224
|
+
elif choice == "q":
|
|
225
|
+
break
|
|
226
|
+
else:
|
|
227
|
+
print("Invalid choice.")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def handle_keys_command():
|
|
231
|
+
keys_file = USER_CONFIG_DIR / "keys.json"
|
|
232
|
+
|
|
233
|
+
while True:
|
|
234
|
+
print()
|
|
235
|
+
print("API Keys")
|
|
236
|
+
print("-" * 40)
|
|
237
|
+
keys = load_keys()
|
|
238
|
+
if not keys:
|
|
239
|
+
print(" (none)")
|
|
240
|
+
else:
|
|
241
|
+
for i, (url, key) in enumerate(keys.items(), 1):
|
|
242
|
+
print(f" {i}. {url} -> {mask_key(key)}")
|
|
243
|
+
print()
|
|
244
|
+
print("q. Skip")
|
|
245
|
+
print("1. Add key")
|
|
246
|
+
print("2. Remove key")
|
|
247
|
+
print()
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
choice = input("Choice: ").strip()
|
|
251
|
+
except KeyboardInterrupt:
|
|
252
|
+
print()
|
|
253
|
+
return
|
|
254
|
+
except EOFError:
|
|
255
|
+
print()
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if choice == "1":
|
|
259
|
+
url = input("Base URL: ").strip()
|
|
260
|
+
if not url:
|
|
261
|
+
print("Base URL cannot be empty.")
|
|
262
|
+
continue
|
|
263
|
+
key = input("API Key: ").strip()
|
|
264
|
+
if not key:
|
|
265
|
+
print("API Key cannot be empty.")
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
keys = load_keys()
|
|
269
|
+
keys[url] = key
|
|
270
|
+
keys_file.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
with open(keys_file, "w") as f:
|
|
272
|
+
json.dump(keys, f, indent=2)
|
|
273
|
+
print(f"Key for {url} saved.")
|
|
274
|
+
|
|
275
|
+
elif choice == "2":
|
|
276
|
+
if not keys:
|
|
277
|
+
print("No keys to remove.")
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
idx = input("Number to remove (or 0 to cancel): ").strip()
|
|
282
|
+
except KeyboardInterrupt:
|
|
283
|
+
print()
|
|
284
|
+
continue
|
|
285
|
+
except EOFError:
|
|
286
|
+
print()
|
|
287
|
+
continue
|
|
288
|
+
try:
|
|
289
|
+
idx = int(idx)
|
|
290
|
+
except ValueError:
|
|
291
|
+
print("Invalid number.")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if idx == 0:
|
|
295
|
+
continue
|
|
296
|
+
if idx < 1 or idx > len(keys):
|
|
297
|
+
print("Invalid selection.")
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
removed = list(keys.keys())[idx - 1]
|
|
301
|
+
del keys[removed]
|
|
302
|
+
with open(keys_file, "w") as f:
|
|
303
|
+
json.dump(keys, f, indent=2)
|
|
304
|
+
print(f"Key for {removed} removed.")
|
|
305
|
+
|
|
306
|
+
elif choice == "q":
|
|
307
|
+
break
|
|
308
|
+
else:
|
|
309
|
+
print("Invalid choice.")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def handle_setup_command():
|
|
313
|
+
"""First-time setup wizard: configure API keys, then review agent roles."""
|
|
314
|
+
print()
|
|
315
|
+
print("=" * 60)
|
|
316
|
+
print(" Raggie Setup Wizard")
|
|
317
|
+
print("=" * 60)
|
|
318
|
+
print()
|
|
319
|
+
print("Step 1: Configure your API keys.")
|
|
320
|
+
print("You'll need at least one API key to use Raggie.")
|
|
321
|
+
handle_keys_command()
|
|
322
|
+
print()
|
|
323
|
+
print("Step 2: Review your agent roles.")
|
|
324
|
+
print("Roles define which model and base URL each agent uses.")
|
|
325
|
+
handle_roles_command()
|
|
326
|
+
print()
|
|
327
|
+
print("=" * 60)
|
|
328
|
+
print(" Setup complete! You're ready to use Raggie.")
|
|
329
|
+
print(" Try: raggie code .")
|
|
330
|
+
print("=" * 60)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def handle_skill_command(args):
|
|
334
|
+
"""Handle skill subcommands."""
|
|
335
|
+
# Initialize database to ensure skills table exists
|
|
336
|
+
init_db()
|
|
337
|
+
|
|
338
|
+
manager = SkillManager()
|
|
339
|
+
|
|
340
|
+
# --list-all: list all skills across all roles (role arg not required)
|
|
341
|
+
if getattr(args, "list_all", False):
|
|
342
|
+
skills = manager.list_skills()
|
|
343
|
+
if skills:
|
|
344
|
+
print("All skills:")
|
|
345
|
+
print("-" * 60)
|
|
346
|
+
for skill in skills:
|
|
347
|
+
print(f" {skill['role']}/{skill['name']}: {skill['summary']}")
|
|
348
|
+
print("-" * 60)
|
|
349
|
+
else:
|
|
350
|
+
print("No skills found.")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Flag-based operations (non-interactive)
|
|
354
|
+
if args.import_skill:
|
|
355
|
+
if not args.role:
|
|
356
|
+
print("Error: role is required when using --import-skill", file=sys.stderr)
|
|
357
|
+
sys.exit(1)
|
|
358
|
+
if not args.name:
|
|
359
|
+
print("Error: --name is required when using --import-skill", file=sys.stderr)
|
|
360
|
+
sys.exit(1)
|
|
361
|
+
try:
|
|
362
|
+
manager.import_from_markdown(args.role, args.name, args.import_skill)
|
|
363
|
+
print(f"Successfully imported skill '{args.name}' for role '{args.role}' from {args.import_skill}")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
print(f"Error importing skill: {e}", file=sys.stderr)
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if args.export_skill:
|
|
370
|
+
if not args.role:
|
|
371
|
+
print("Error: role is required when using --export-skill", file=sys.stderr)
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
if not args.name:
|
|
374
|
+
print("Error: --name is required when using --export-skill", file=sys.stderr)
|
|
375
|
+
sys.exit(1)
|
|
376
|
+
try:
|
|
377
|
+
manager.export_to_markdown(args.role, args.name, args.export_skill)
|
|
378
|
+
print(f"Successfully exported skill '{args.name}' for role '{args.role}' to {args.export_skill}")
|
|
379
|
+
except Exception as e:
|
|
380
|
+
print(f"Error exporting skill: {e}", file=sys.stderr)
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
if getattr(args, "delete", False):
|
|
385
|
+
if not args.role:
|
|
386
|
+
print("Error: role is required when using --delete", file=sys.stderr)
|
|
387
|
+
sys.exit(1)
|
|
388
|
+
if not args.name:
|
|
389
|
+
print("Error: --name is required when using --delete", file=sys.stderr)
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
deleted = manager.delete_skill(args.role, args.name)
|
|
392
|
+
if deleted:
|
|
393
|
+
print(f"Successfully deleted skill '{args.name}' for role '{args.role}'")
|
|
394
|
+
else:
|
|
395
|
+
print(f"No skill '{args.name}' found for role '{args.role}'")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
if args.show and args.name:
|
|
399
|
+
# Show specific skill (non-interactive)
|
|
400
|
+
if not args.role:
|
|
401
|
+
print("Error: role is required", file=sys.stderr)
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
skill = manager.get_skill(args.role, args.name)
|
|
404
|
+
if skill:
|
|
405
|
+
print(f"Skill '{args.name}' for role '{args.role}':")
|
|
406
|
+
print("-" * 60)
|
|
407
|
+
print(skill)
|
|
408
|
+
print("-" * 60)
|
|
409
|
+
else:
|
|
410
|
+
print(f"No skill '{args.name}' found for role '{args.role}'")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Interactive mode: no flags, or --show without --name
|
|
414
|
+
_skill_interactive_menu(manager, args.role)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _skill_interactive_menu(manager, role_filter=None):
|
|
418
|
+
"""Interactive menu for managing skills."""
|
|
419
|
+
while True:
|
|
420
|
+
if role_filter:
|
|
421
|
+
skills = manager.list_skills_by_role(role_filter)
|
|
422
|
+
header = f"Skills for role '{role_filter}'"
|
|
423
|
+
else:
|
|
424
|
+
skills = manager.list_skills()
|
|
425
|
+
header = "All Skills"
|
|
426
|
+
|
|
427
|
+
print()
|
|
428
|
+
print(header)
|
|
429
|
+
print("-" * 60)
|
|
430
|
+
if not skills:
|
|
431
|
+
print(" (no skills found)")
|
|
432
|
+
else:
|
|
433
|
+
for i, skill in enumerate(skills, 1):
|
|
434
|
+
r = skill['role']
|
|
435
|
+
n = skill['name']
|
|
436
|
+
s = skill['summary']
|
|
437
|
+
if role_filter:
|
|
438
|
+
print(f" {i}. {n}: {s}")
|
|
439
|
+
else:
|
|
440
|
+
print(f" {i}. {r}/{n}: {s}")
|
|
441
|
+
print()
|
|
442
|
+
print("q. Exit")
|
|
443
|
+
print("1. View skill content")
|
|
444
|
+
print("2. Delete skill")
|
|
445
|
+
print("3. Export skill to file")
|
|
446
|
+
print("4. Import skill from file")
|
|
447
|
+
print("5. List all skills (all roles)")
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
choice = input("Choice: ").strip()
|
|
452
|
+
except KeyboardInterrupt:
|
|
453
|
+
print()
|
|
454
|
+
return
|
|
455
|
+
except EOFError:
|
|
456
|
+
print()
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
if choice == "q":
|
|
460
|
+
break
|
|
461
|
+
|
|
462
|
+
elif choice == "1":
|
|
463
|
+
# View skill content
|
|
464
|
+
if not skills:
|
|
465
|
+
print("No skills to view.")
|
|
466
|
+
continue
|
|
467
|
+
try:
|
|
468
|
+
idx = input("Number to view (or 0 to cancel): ").strip()
|
|
469
|
+
except (KeyboardInterrupt, EOFError):
|
|
470
|
+
print()
|
|
471
|
+
return
|
|
472
|
+
try:
|
|
473
|
+
idx = int(idx)
|
|
474
|
+
except ValueError:
|
|
475
|
+
print("Invalid number.")
|
|
476
|
+
continue
|
|
477
|
+
if idx == 0:
|
|
478
|
+
continue
|
|
479
|
+
if idx < 1 or idx > len(skills):
|
|
480
|
+
print("Invalid selection.")
|
|
481
|
+
continue
|
|
482
|
+
skill = skills[idx - 1]
|
|
483
|
+
content = manager.get_skill(skill['role'], skill['name'])
|
|
484
|
+
print()
|
|
485
|
+
print(f"Skill '{skill['name']}' for role '{skill['role']}':")
|
|
486
|
+
print("=" * 60)
|
|
487
|
+
print(content or "(empty)")
|
|
488
|
+
print("=" * 60)
|
|
489
|
+
|
|
490
|
+
elif choice == "2":
|
|
491
|
+
# Delete skill
|
|
492
|
+
if not skills:
|
|
493
|
+
print("No skills to delete.")
|
|
494
|
+
continue
|
|
495
|
+
try:
|
|
496
|
+
idx = input("Number to delete (or 0 to cancel): ").strip()
|
|
497
|
+
except (KeyboardInterrupt, EOFError):
|
|
498
|
+
print()
|
|
499
|
+
return
|
|
500
|
+
try:
|
|
501
|
+
idx = int(idx)
|
|
502
|
+
except ValueError:
|
|
503
|
+
print("Invalid number.")
|
|
504
|
+
continue
|
|
505
|
+
if idx == 0:
|
|
506
|
+
continue
|
|
507
|
+
if idx < 1 or idx > len(skills):
|
|
508
|
+
print("Invalid selection.")
|
|
509
|
+
continue
|
|
510
|
+
skill = skills[idx - 1]
|
|
511
|
+
try:
|
|
512
|
+
confirm = input(f"Delete '{skill['role']}/{skill['name']}'? (y/n): ").strip().lower()
|
|
513
|
+
except (KeyboardInterrupt, EOFError):
|
|
514
|
+
print()
|
|
515
|
+
return
|
|
516
|
+
if confirm == 'y' or confirm == 'yes':
|
|
517
|
+
manager.delete_skill(skill['role'], skill['name'])
|
|
518
|
+
print(f"Deleted '{skill['role']}/{skill['name']}'.")
|
|
519
|
+
else:
|
|
520
|
+
print("Cancelled.")
|
|
521
|
+
|
|
522
|
+
elif choice == "3":
|
|
523
|
+
# Export skill to file
|
|
524
|
+
if not skills:
|
|
525
|
+
print("No skills to export.")
|
|
526
|
+
continue
|
|
527
|
+
try:
|
|
528
|
+
idx = input("Number to export (or 0 to cancel): ").strip()
|
|
529
|
+
except (KeyboardInterrupt, EOFError):
|
|
530
|
+
print()
|
|
531
|
+
return
|
|
532
|
+
try:
|
|
533
|
+
idx = int(idx)
|
|
534
|
+
except ValueError:
|
|
535
|
+
print("Invalid number.")
|
|
536
|
+
continue
|
|
537
|
+
if idx == 0:
|
|
538
|
+
continue
|
|
539
|
+
if idx < 1 or idx > len(skills):
|
|
540
|
+
print("Invalid selection.")
|
|
541
|
+
continue
|
|
542
|
+
skill = skills[idx - 1]
|
|
543
|
+
try:
|
|
544
|
+
file_path = input(f"Export to file [skill-{skill['name']}.md]: ").strip()
|
|
545
|
+
except (KeyboardInterrupt, EOFError):
|
|
546
|
+
print()
|
|
547
|
+
return
|
|
548
|
+
if not file_path:
|
|
549
|
+
file_path = f"skill-{skill['name']}.md"
|
|
550
|
+
try:
|
|
551
|
+
manager.export_to_markdown(skill['role'], skill['name'], file_path)
|
|
552
|
+
print(f"Exported '{skill['role']}/{skill['name']}' to {file_path}")
|
|
553
|
+
except Exception as e:
|
|
554
|
+
print(f"Error exporting: {e}")
|
|
555
|
+
|
|
556
|
+
elif choice == "4":
|
|
557
|
+
# Import skill from file
|
|
558
|
+
try:
|
|
559
|
+
role = input(f"Role [{role_filter or 'code'}]: ").strip()
|
|
560
|
+
except (KeyboardInterrupt, EOFError):
|
|
561
|
+
print()
|
|
562
|
+
return
|
|
563
|
+
if not role:
|
|
564
|
+
role = role_filter or "code"
|
|
565
|
+
try:
|
|
566
|
+
name = input("Skill name: ").strip()
|
|
567
|
+
except (KeyboardInterrupt, EOFError):
|
|
568
|
+
print()
|
|
569
|
+
return
|
|
570
|
+
if not name:
|
|
571
|
+
print("Skill name is required.")
|
|
572
|
+
continue
|
|
573
|
+
try:
|
|
574
|
+
file_path = input("File path: ").strip()
|
|
575
|
+
except (KeyboardInterrupt, EOFError):
|
|
576
|
+
print()
|
|
577
|
+
return
|
|
578
|
+
if not file_path:
|
|
579
|
+
print("File path is required.")
|
|
580
|
+
continue
|
|
581
|
+
try:
|
|
582
|
+
manager.import_from_markdown(role, name, file_path)
|
|
583
|
+
print(f"Imported skill '{name}' for role '{role}' from {file_path}")
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"Error importing: {e}")
|
|
586
|
+
|
|
587
|
+
elif choice == "5":
|
|
588
|
+
# Switch to all-roles view
|
|
589
|
+
role_filter = None
|
|
590
|
+
|
|
591
|
+
else:
|
|
592
|
+
print("Invalid choice.")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def main():
|
|
596
|
+
args = parse_args()
|
|
597
|
+
|
|
598
|
+
# Handle skill command
|
|
599
|
+
if getattr(args, "command", None) == "skill":
|
|
600
|
+
handle_skill_command(args)
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
# Handle roles command
|
|
604
|
+
if getattr(args, "command", None) == "roles":
|
|
605
|
+
handle_roles_command()
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
# Handle keys command
|
|
609
|
+
if getattr(args, "command", None) == "keys":
|
|
610
|
+
handle_keys_command()
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
# Handle setup command
|
|
614
|
+
if getattr(args, "command", None) == "setup":
|
|
615
|
+
handle_setup_command()
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
# Resolve and prepare project directory
|
|
619
|
+
project_dir = Path(args.project_dir).resolve()
|
|
620
|
+
if not project_dir.exists():
|
|
621
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
622
|
+
os.chdir(str(project_dir))
|
|
623
|
+
|
|
624
|
+
# Initialize database
|
|
625
|
+
init_db()
|
|
626
|
+
|
|
627
|
+
# Get or create chat
|
|
628
|
+
# Auto-detect interactive mode: if no prompt provided, use interactive mode
|
|
629
|
+
interactive_mode = not args.prompt
|
|
630
|
+
|
|
631
|
+
if interactive_mode:
|
|
632
|
+
# In interactive mode, let user select chat
|
|
633
|
+
chat_id = select_chat(args.role)
|
|
634
|
+
if chat_id is None:
|
|
635
|
+
chat_id = create_chat(args.role)
|
|
636
|
+
else:
|
|
637
|
+
# In non-interactive mode, use latest chat or create new one
|
|
638
|
+
from Agent.chat_history_db import list_chats
|
|
639
|
+
chats = list_chats(args.role)
|
|
640
|
+
if chats:
|
|
641
|
+
chat_id = chats[0]['id'] # Use most recent chat
|
|
642
|
+
else:
|
|
643
|
+
chat_id = create_chat(args.role)
|
|
644
|
+
|
|
645
|
+
# Create agent with the selected chat
|
|
646
|
+
try:
|
|
647
|
+
agent = Agent(args.role, chat_id=chat_id, debug=args.debug)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
print(f"\nError: {e}", file=sys.stderr)
|
|
650
|
+
print("\nRun 'raggie setup' to configure your roles and API keys.", file=sys.stderr)
|
|
651
|
+
sys.exit(1)
|
|
652
|
+
|
|
653
|
+
# Setup tools and commands
|
|
654
|
+
setup_toolcalls(agent.tool_registry)
|
|
655
|
+
setup_commands(agent.command_registry)
|
|
656
|
+
|
|
657
|
+
# Run agent
|
|
658
|
+
if interactive_mode:
|
|
659
|
+
from interactive import run_interactive
|
|
660
|
+
run_interactive(agent, args.role)
|
|
661
|
+
else:
|
|
662
|
+
if not args.prompt:
|
|
663
|
+
print("Error: prompt is required in non-interactive mode")
|
|
664
|
+
from cli import _create_agent_parser
|
|
665
|
+
_create_agent_parser().print_help()
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
|
|
668
|
+
from interactive import run_non_interactive
|
|
669
|
+
run_non_interactive(agent, args.prompt, effort=getattr(args, 'effort', None))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
if __name__ == "__main__":
|
|
673
|
+
main()
|