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
Tools/view_changes.py ADDED
@@ -0,0 +1,179 @@
1
+ import os
2
+ from Agent.git_manager import GitManager
3
+ from .utils import BLUE, RESET
4
+
5
+
6
+ def handle(arguments, toolcall_id):
7
+ """Handle ViewChanges tool calls."""
8
+ print(f"{BLUE}ViewChanges{RESET}")
9
+
10
+ view_type = arguments.get("view_type", "status")
11
+ path = arguments.get("path")
12
+ max_count = arguments.get("max_count", 10)
13
+ category = arguments.get("category")
14
+ max_diff_lines = arguments.get("max_diff_lines", 500)
15
+
16
+ # --- Input validation ---
17
+ VALID_VIEW_TYPES = {'status', 'diff', 'log'}
18
+ if view_type not in VALID_VIEW_TYPES:
19
+ return {
20
+ "role": "tool",
21
+ "tool_call_id": toolcall_id,
22
+ "content": (
23
+ f"Unknown view_type: '{view_type}'. "
24
+ f"Supported values: {', '.join(sorted(VALID_VIEW_TYPES))}."
25
+ ),
26
+ }
27
+
28
+ # Treat empty string category as None (no filter)
29
+ if category is not None and category == "":
30
+ category = None
31
+
32
+ VALID_CATEGORIES = {'added', 'modified', 'deleted', 'unchanged'}
33
+ if category is not None and category not in VALID_CATEGORIES:
34
+ return {
35
+ "role": "tool",
36
+ "tool_call_id": toolcall_id,
37
+ "content": (
38
+ f"Invalid category: '{category}'. "
39
+ f"Must be one of: {', '.join(sorted(VALID_CATEGORIES))}."
40
+ ),
41
+ }
42
+
43
+ # Validate max_count
44
+ if not isinstance(max_count, (int, float)) or isinstance(max_count, bool):
45
+ return {
46
+ "role": "tool",
47
+ "tool_call_id": toolcall_id,
48
+ "content": f"Invalid max_count: must be a number, got {type(max_count).__name__}.",
49
+ }
50
+ max_count = int(max_count)
51
+ if max_count < 0:
52
+ return {
53
+ "role": "tool",
54
+ "tool_call_id": toolcall_id,
55
+ "content": f"Invalid max_count: must be non-negative, got {max_count}.",
56
+ }
57
+
58
+ # Validate max_diff_lines
59
+ if max_diff_lines is not None:
60
+ if not isinstance(max_diff_lines, (int, float)) or isinstance(max_diff_lines, bool):
61
+ return {
62
+ "role": "tool",
63
+ "tool_call_id": toolcall_id,
64
+ "content": f"Invalid max_diff_lines: must be a number, got {type(max_diff_lines).__name__}.",
65
+ }
66
+ max_diff_lines = int(max_diff_lines)
67
+ if max_diff_lines < 0:
68
+ return {
69
+ "role": "tool",
70
+ "tool_call_id": toolcall_id,
71
+ "content": f"Invalid max_diff_lines: must be non-negative, got {max_diff_lines}.",
72
+ }
73
+
74
+ try:
75
+ git_manager = GitManager(root_dir=os.getcwd())
76
+
77
+ if view_type == "status":
78
+ status = git_manager.get_status(category=category)
79
+ result_parts = []
80
+
81
+ if status["commit_id"]:
82
+ result_parts.append(f"Last commit: {status['commit_id'][:8]} - {status['commit_message']}")
83
+ else:
84
+ result_parts.append("No commits yet.")
85
+
86
+ if category:
87
+ # Only show the requested category
88
+ label = category.capitalize()
89
+ items = status[category]
90
+ result_parts.append("")
91
+ result_parts.append(f"{label} ({len(items)}):")
92
+ for f in items[:50]:
93
+ symbol = {"added": "+", "modified": "~", "deleted": "-", "unchanged": " "}.get(category, "")
94
+ result_parts.append(f" {symbol} {f}")
95
+ if len(items) > 50:
96
+ result_parts.append(f" ... and {len(items) - 50} more")
97
+ else:
98
+ # Show all categories
99
+ result_parts.append("")
100
+ result_parts.append(f"Added ({len(status['added'])}):")
101
+ for f in status["added"][:50]:
102
+ result_parts.append(f" + {f}")
103
+ if len(status["added"]) > 50:
104
+ result_parts.append(f" ... and {len(status['added']) - 50} more")
105
+
106
+ result_parts.append("")
107
+ result_parts.append(f"Modified ({len(status['modified'])}):")
108
+ for f in status["modified"][:50]:
109
+ result_parts.append(f" ~ {f}")
110
+ if len(status["modified"]) > 50:
111
+ result_parts.append(f" ... and {len(status['modified']) - 50} more")
112
+
113
+ result_parts.append("")
114
+ result_parts.append(f"Deleted ({len(status['deleted'])}):")
115
+ for f in status["deleted"][:50]:
116
+ result_parts.append(f" - {f}")
117
+ if len(status["deleted"]) > 50:
118
+ result_parts.append(f" ... and {len(status['deleted']) - 50} more")
119
+
120
+ result_parts.append("")
121
+ result_parts.append(f"Unchanged ({len(status['unchanged'])} files)")
122
+
123
+ content = "\n".join(result_parts)
124
+
125
+ elif view_type == "diff":
126
+ diffs = git_manager.get_diff(path_filter=path, max_diff_lines=max_diff_lines)
127
+
128
+ if not diffs:
129
+ content = "No differences found between working tree and last commit."
130
+ else:
131
+ result_parts = []
132
+ result_parts.append(f"Showing {len(diffs)} changed file(s):")
133
+ if path:
134
+ result_parts.append(f"(filtered to files containing '{path}')")
135
+ if max_diff_lines:
136
+ result_parts.append(f"(diffs truncated to {max_diff_lines} lines each)")
137
+ result_parts.append("")
138
+
139
+ for d in diffs:
140
+ change_symbol = {"added": "+", "modified": "~", "deleted": "-"}.get(d["change_type"], "?")
141
+ result_parts.append(f"{'='*60}")
142
+ result_parts.append(f"{change_symbol} {d['change_type'].upper()}: {d['path']}")
143
+ result_parts.append(f"{'='*60}")
144
+ result_parts.append(d["content"])
145
+ result_parts.append("")
146
+
147
+ content = "\n".join(result_parts)
148
+
149
+ elif view_type == "log":
150
+ commits = git_manager.get_log(max_count=max_count)
151
+
152
+ if not commits:
153
+ content = "No commits found."
154
+ else:
155
+ result_parts = []
156
+ result_parts.append(f"Last {len(commits)} commit(s):")
157
+ result_parts.append("")
158
+ for c in commits:
159
+ short_id = c["commit_id"][:8]
160
+ result_parts.append(f" commit {short_id}")
161
+ result_parts.append(f" Author: {c['author']}")
162
+ result_parts.append(f" Date: {c['timestamp']}")
163
+ result_parts.append("")
164
+ result_parts.append(f" {c['message']}")
165
+ result_parts.append("")
166
+ content = "\n".join(result_parts)
167
+
168
+ return {
169
+ "role": "tool",
170
+ "tool_call_id": toolcall_id,
171
+ "content": content,
172
+ }
173
+
174
+ except Exception as e:
175
+ return {
176
+ "role": "tool",
177
+ "tool_call_id": toolcall_id,
178
+ "content": f"Error executing ViewChanges: {str(e)}",
179
+ }
@@ -0,0 +1,30 @@
1
+ from RAG.graph import walk_call_tree
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
+ max_depth = arguments.get("max_depth", 5)
9
+ include_external = arguments.get("include_external", False)
10
+ exclude = arguments.get("exclude")
11
+ print(f"{BLUE}WalkCallTree {symbol_name} (depth={max_depth}){RESET}")
12
+
13
+ # Check if the file is outside the current working directory
14
+ if file_path and not is_within_cwd(file_path):
15
+ return {
16
+ "role": "tool",
17
+ "tool_call_id": toolcall_id,
18
+ "content": "Error: access denied - path is outside the current working directory",
19
+ }
20
+
21
+ try:
22
+ result = walk_call_tree(symbol_name, file_path, max_depth, include_external, exclude)
23
+ except Exception as e:
24
+ result = f"Error walking call tree: {str(e)}"
25
+
26
+ return {
27
+ "role": "tool",
28
+ "tool_call_id": toolcall_id,
29
+ "content": result,
30
+ }
Tools/web_fetch.py ADDED
@@ -0,0 +1,175 @@
1
+ import re
2
+ import gzip
3
+ import zlib
4
+ from html.parser import HTMLParser
5
+ from urllib.request import Request, urlopen
6
+ from urllib.error import HTTPError, URLError
7
+
8
+ from .utils import BLUE, RED, RESET
9
+
10
+
11
+ DEFAULT_MAX_CHARS = 20000
12
+ DEFAULT_TIMEOUT = 20
13
+ USER_AGENT = "Mozilla/5.0 (compatible; RaggieBot/1.0; +https://github.com/myuser/raggie)"
14
+
15
+ _BLOCK_TAGS = {
16
+ "p", "div", "section", "article", "header", "footer", "main", "br",
17
+ "h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "ul", "ol", "table",
18
+ "blockquote", "pre", "hr", "figure", "figcaption", "nav",
19
+ }
20
+ _SKIP_TAGS = {"script", "style", "noscript", "template", "svg", "head"}
21
+
22
+
23
+ class _TextExtractor(HTMLParser):
24
+ """Convert HTML into readable plain text using only the stdlib.
25
+
26
+ Drops script/style/etc., inserts newlines around block-level elements,
27
+ and collapses runs of whitespace.
28
+ """
29
+
30
+ def __init__(self):
31
+ super().__init__(convert_charrefs=True)
32
+ self._parts = []
33
+ self._skip_depth = 0
34
+
35
+ def handle_starttag(self, tag, attrs):
36
+ if tag in _SKIP_TAGS:
37
+ self._skip_depth += 1
38
+ elif tag in _BLOCK_TAGS:
39
+ self._parts.append("\n")
40
+
41
+ def handle_endtag(self, tag):
42
+ if tag in _SKIP_TAGS and self._skip_depth > 0:
43
+ self._skip_depth -= 1
44
+ elif tag in _BLOCK_TAGS:
45
+ self._parts.append("\n")
46
+
47
+ def handle_data(self, data):
48
+ if self._skip_depth == 0 and data.strip():
49
+ self._parts.append(data)
50
+
51
+ def get_text(self):
52
+ text = "".join(self._parts)
53
+ # Collapse horizontal whitespace, then squeeze blank lines.
54
+ text = re.sub(r"[ \t\r\f\v]+", " ", text)
55
+ text = re.sub(r"\n[ \t]+", "\n", text)
56
+ text = re.sub(r"\n{3,}", "\n\n", text)
57
+ return text.strip()
58
+
59
+
60
+ def _decode_body(raw, headers):
61
+ """Decompress (if needed) and decode the response body to str."""
62
+ encoding = (headers.get("Content-Encoding") or "").lower()
63
+ if "gzip" in encoding:
64
+ try:
65
+ raw = gzip.decompress(raw)
66
+ except OSError as e:
67
+ print(f"{RED}Warning: Failed to decompress gzip response: {e}{RESET}")
68
+ elif "deflate" in encoding:
69
+ try:
70
+ raw = zlib.decompress(raw)
71
+ except zlib.error:
72
+ raw = zlib.decompress(raw, -zlib.MAX_WBITS)
73
+
74
+ charset = "utf-8"
75
+ content_type = headers.get("Content-Type", "")
76
+ match = re.search(r"charset=([\w\-]+)", content_type, re.IGNORECASE)
77
+ if match:
78
+ charset = match.group(1)
79
+ try:
80
+ return raw.decode(charset, errors="replace")
81
+ except (LookupError, TypeError):
82
+ return raw.decode("utf-8", errors="replace")
83
+
84
+
85
+ def handle(arguments, toolcall_id):
86
+ url = arguments.get("url")
87
+ print(f"{BLUE}WebFetch {url}{RESET}")
88
+
89
+ if not url:
90
+ return {
91
+ "role": "tool",
92
+ "tool_call_id": toolcall_id,
93
+ "content": "Error: 'url' is required",
94
+ }
95
+
96
+ if not re.match(r"^https?://", url, re.IGNORECASE):
97
+ return {
98
+ "role": "tool",
99
+ "tool_call_id": toolcall_id,
100
+ "content": "Error: url must start with http:// or https://",
101
+ }
102
+
103
+ max_chars = arguments.get("max_chars", DEFAULT_MAX_CHARS)
104
+ try:
105
+ max_chars = int(max_chars)
106
+ except (TypeError, ValueError):
107
+ max_chars = DEFAULT_MAX_CHARS
108
+
109
+ req = Request(
110
+ url,
111
+ headers={
112
+ "User-Agent": USER_AGENT,
113
+ "Accept": "text/html,application/xhtml+xml,application/json,text/plain,*/*",
114
+ "Accept-Encoding": "gzip, deflate",
115
+ },
116
+ )
117
+
118
+ try:
119
+ with urlopen(req, timeout=DEFAULT_TIMEOUT) as resp:
120
+ headers = resp.headers
121
+ raw = resp.read()
122
+ final_url = resp.geturl()
123
+ except HTTPError as err:
124
+ return {
125
+ "role": "tool",
126
+ "tool_call_id": toolcall_id,
127
+ "content": f"Error: HTTP {err.code} {err.reason} for {url}",
128
+ }
129
+ except URLError as err:
130
+ return {
131
+ "role": "tool",
132
+ "tool_call_id": toolcall_id,
133
+ "content": f"Error: failed to fetch {url}: {err.reason}",
134
+ }
135
+ except Exception as err:
136
+ return {
137
+ "role": "tool",
138
+ "tool_call_id": toolcall_id,
139
+ "content": f"Error fetching {url}: {err}",
140
+ }
141
+
142
+ content_type = (headers.get("Content-Type") or "").lower()
143
+
144
+ if "html" in content_type:
145
+ body = _decode_body(raw, headers)
146
+ parser = _TextExtractor()
147
+ try:
148
+ parser.feed(body)
149
+ text = parser.get_text()
150
+ except Exception:
151
+ text = body
152
+ elif content_type.startswith("text/") or "json" in content_type or "xml" in content_type:
153
+ text = _decode_body(raw, headers)
154
+ else:
155
+ return {
156
+ "role": "tool",
157
+ "tool_call_id": toolcall_id,
158
+ "content": f"Error: unsupported content type '{content_type}' for {url} ({len(raw)} bytes). Only text/HTML/JSON/XML are supported.",
159
+ }
160
+
161
+ truncated = False
162
+ if len(text) > max_chars:
163
+ text = text[:max_chars]
164
+ truncated = True
165
+
166
+ header = f"URL: {final_url}\nContent-Type: {content_type or 'unknown'}\n"
167
+ if truncated:
168
+ header += f"[truncated to {max_chars} chars]\n"
169
+ header += "\n"
170
+
171
+ return {
172
+ "role": "tool",
173
+ "tool_call_id": toolcall_id,
174
+ "content": header + text,
175
+ }
Tools/web_search.py ADDED
@@ -0,0 +1,69 @@
1
+ from .utils import BLUE, RESET
2
+
3
+
4
+ DEFAULT_MAX_RESULTS = 5
5
+ MAX_RESULTS_CAP = 20
6
+
7
+
8
+ def handle(arguments, toolcall_id):
9
+ query = arguments.get("query")
10
+ print(f"{BLUE}WebSearch {query}{RESET}")
11
+
12
+ if not query or not str(query).strip():
13
+ return {
14
+ "role": "tool",
15
+ "tool_call_id": toolcall_id,
16
+ "content": "Error: 'query' is required",
17
+ }
18
+
19
+ max_results = arguments.get("max_results", DEFAULT_MAX_RESULTS)
20
+ try:
21
+ max_results = int(max_results)
22
+ except (TypeError, ValueError):
23
+ max_results = DEFAULT_MAX_RESULTS
24
+ max_results = max(1, min(max_results, MAX_RESULTS_CAP))
25
+
26
+ region = arguments.get("region") or "wt-wt"
27
+
28
+ try:
29
+ from ddgs import DDGS
30
+ except ImportError:
31
+ return {
32
+ "role": "tool",
33
+ "tool_call_id": toolcall_id,
34
+ "content": "Error: the 'ddgs' package is not installed. Run: pip install ddgs",
35
+ }
36
+
37
+ try:
38
+ results = DDGS().text(query, region=region, max_results=max_results)
39
+ except Exception as err:
40
+ return {
41
+ "role": "tool",
42
+ "tool_call_id": toolcall_id,
43
+ "content": f"Error performing web search: {err}",
44
+ }
45
+
46
+ if not results:
47
+ return {
48
+ "role": "tool",
49
+ "tool_call_id": toolcall_id,
50
+ "content": f"No results found for '{query}'.",
51
+ }
52
+
53
+ lines = [f"Search results for '{query}':", ""]
54
+ for i, item in enumerate(results, start=1):
55
+ title = (item.get("title") or "").strip()
56
+ href = (item.get("href") or "").strip()
57
+ body = (item.get("body") or "").strip()
58
+ lines.append(f"{i}. {title}")
59
+ if href:
60
+ lines.append(f" URL: {href}")
61
+ if body:
62
+ lines.append(f" {body}")
63
+ lines.append("")
64
+
65
+ return {
66
+ "role": "tool",
67
+ "tool_call_id": toolcall_id,
68
+ "content": "\n".join(lines).strip(),
69
+ }
Tools/write.py ADDED
@@ -0,0 +1,48 @@
1
+ from .utils import is_ignored_by_gitignore, is_within_cwd, BLUE, RESET, auto_record_change, reindex_after_change, remove_em_dashes
2
+
3
+
4
+ def handle(arguments, toolcall_id, session_id=None, code_indexer=None):
5
+ file_path = arguments["file_path"]
6
+ content = arguments["content"]
7
+ print(f"{BLUE}Writing {file_path}{RESET}")
8
+
9
+ try:
10
+ # Check if the file is outside the current working directory
11
+ if 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
+ # Check if file is in .gitignore
19
+ if is_ignored_by_gitignore(file_path):
20
+ return {
21
+ "role": "tool",
22
+ "tool_call_id": toolcall_id,
23
+ "content": f"Error: File '{file_path}' is in .gitignore. Operations on gitignored files are not allowed.",
24
+ }
25
+
26
+ content = remove_em_dashes(content)
27
+
28
+ with open(file_path, "w") as f:
29
+ f.write(content)
30
+
31
+ if session_id is not None:
32
+ from Agent.chat_history_db import record_session_file
33
+ record_session_file(session_id, file_path, "write")
34
+ auto_record_change(session_id, file_path, "file_create", f"Created file {file_path}")
35
+
36
+ reindex_after_change(code_indexer)
37
+
38
+ return {
39
+ "role": "tool",
40
+ "tool_call_id": toolcall_id,
41
+ "content": f"file {file_path} written successfully",
42
+ }
43
+ except Exception as e:
44
+ return {
45
+ "role": "tool",
46
+ "tool_call_id": toolcall_id,
47
+ "content": f"Error writing file: {str(e)}",
48
+ }
cli.py ADDED
@@ -0,0 +1,111 @@
1
+ import argparse
2
+ import sys
3
+
4
+ KNOWN_COMMANDS = {"skill", "roles", "keys", "setup"}
5
+
6
+
7
+ def _create_agent_parser():
8
+ """Parser for the default agent mode: raggie <role> <project-dir>."""
9
+ parser = argparse.ArgumentParser(
10
+ prog="raggie",
11
+ description="Raggie - AI Coding Agent",
12
+ formatter_class=argparse.RawDescriptionHelpFormatter,
13
+ epilog="""commands:
14
+ raggie <role> <project-dir> Run the AI agent (e.g. raggie code .)
15
+ raggie skill <role> ... Manage agent skills
16
+ raggie roles List and edit agent roles
17
+ raggie keys Manage API keys
18
+ raggie setup First-time setup (keys + roles)
19
+
20
+ examples:
21
+ raggie code . # interactive mode in current dir
22
+ raggie code /path/to/project # interactive mode in a project
23
+ raggie code . --prompt "Write a hello function" # single prompt
24
+ raggie code /tmp/new-project # creates dir if missing
25
+ raggie skill code --show # list all skills for role
26
+ raggie skill code --show --name testing # show a specific skill
27
+ raggie skill code --import-skill skills.md --name testing # import from file
28
+ raggie skill code --delete --name testing # delete a skill
29
+ raggie skill --list-all # list all skills across all roles
30
+ """
31
+ )
32
+ parser.add_argument("--version", action="version", version=f"raggie v{__import__('importlib.metadata').metadata.version('raggiecode')}")
33
+ parser.add_argument("role", nargs="?", help="The agent role to use (defined in roles.json)")
34
+ parser.add_argument("project_dir", nargs="?", default=".", help="Path to the project directory (use '.' for current directory)")
35
+ parser.add_argument("--prompt", help="The initial prompt for the agent (if not provided, runs in interactive mode)")
36
+ parser.add_argument("--effort", type=int, choices=[1, 2, 3, 4, 5], help="Effort level: 1=Zen, 2=Serious, 3=Extreme, 4=Feral, 5=Insane (default: 1=Zen)")
37
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode to display tool call outputs")
38
+ return parser
39
+
40
+
41
+ def _create_subcommand_parser():
42
+ """Parser for subcommands: skill, roles, keys."""
43
+ parser = argparse.ArgumentParser(
44
+ description="Raggie - AI Coding Agent",
45
+ formatter_class=argparse.RawDescriptionHelpFormatter,
46
+ )
47
+ parser.add_argument("--version", action="version", version=f"raggie v{__import__('importlib.metadata').metadata.version('raggiecode')}")
48
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
49
+
50
+ # Skill command
51
+ skill_parser = subparsers.add_parser(
52
+ "skill",
53
+ help="Manage agent skills",
54
+ description="Manage named skills stored in the database. A role can have multiple skills, each identified by a unique name.",
55
+ formatter_class=argparse.RawDescriptionHelpFormatter,
56
+ epilog="""
57
+ Examples:
58
+ raggie skill code --show # list all skills for role 'code'
59
+ raggie skill code --show --name testing # show full content of a specific skill
60
+ raggie skill code --import-skill my-skill.md --name testing # import from file
61
+ raggie skill code --export-skill backup.md --name testing # export to file
62
+ raggie skill code --delete --name testing # delete a skill
63
+ raggie skill --list-all # list all skills across all roles
64
+ """
65
+ )
66
+ skill_parser.add_argument("role", nargs="?", help="The agent role (required unless using --list-all)")
67
+ skill_parser.add_argument("--name", help="The skill name (required for --import-skill, --export-skill, --delete)")
68
+ skill_parser.add_argument("--import-skill", metavar="FILE", help="Import skill from markdown file into the database (requires --name)")
69
+ skill_parser.add_argument("--export-skill", metavar="FILE", help="Export skill from database to markdown file (requires --name)")
70
+ skill_parser.add_argument("--show", action="store_true", help="Display skill(s) for the role (all if --name omitted)")
71
+ skill_parser.add_argument("--delete", action="store_true", help="Delete a skill (requires --name)")
72
+ skill_parser.add_argument("--list-all", action="store_true", help="List all skills across all roles")
73
+
74
+ # Roles command
75
+ subparsers.add_parser(
76
+ "roles",
77
+ help="List and edit agent roles",
78
+ description="List all agent roles and interactively edit their base URL and model settings.",
79
+ )
80
+
81
+ # Keys command
82
+ subparsers.add_parser(
83
+ "keys",
84
+ help="Manage API keys",
85
+ description="Interactive interface to list, add, and remove API keys stored in keys.json.",
86
+ )
87
+
88
+ # Setup command
89
+ subparsers.add_parser(
90
+ "setup",
91
+ help="First-time setup wizard",
92
+ description="Guided first-time setup: configure API keys, then review agent roles.",
93
+ )
94
+
95
+ return parser
96
+
97
+
98
+ def parse_args():
99
+ """Parse command-line arguments and return the parsed args."""
100
+ if len(sys.argv) > 1 and sys.argv[1] in KNOWN_COMMANDS:
101
+ parser = _create_subcommand_parser()
102
+ else:
103
+ parser = _create_agent_parser()
104
+
105
+ args = parser.parse_args()
106
+
107
+ if getattr(args, "command", None) is None and getattr(args, "role", None) is None:
108
+ parser.print_help()
109
+ sys.exit(1)
110
+
111
+ return args
config/__init__.py ADDED
File without changes