@voria/cli 0.0.2
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.
- package/README.md +439 -0
- package/bin/voria +730 -0
- package/docs/ARCHITECTURE.md +419 -0
- package/docs/CHANGELOG.md +189 -0
- package/docs/CONTRIBUTING.md +447 -0
- package/docs/DESIGN_DECISIONS.md +380 -0
- package/docs/DEVELOPMENT.md +535 -0
- package/docs/EXAMPLES.md +434 -0
- package/docs/INSTALL.md +335 -0
- package/docs/IPC_PROTOCOL.md +310 -0
- package/docs/LLM_INTEGRATION.md +416 -0
- package/docs/MODULES.md +470 -0
- package/docs/PERFORMANCE.md +346 -0
- package/docs/PLUGINS.md +432 -0
- package/docs/QUICKSTART.md +184 -0
- package/docs/README.md +133 -0
- package/docs/ROADMAP.md +346 -0
- package/docs/SECURITY.md +334 -0
- package/docs/TROUBLESHOOTING.md +565 -0
- package/docs/USER_GUIDE.md +700 -0
- package/package.json +63 -0
- package/python/voria/__init__.py +8 -0
- package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
- package/python/voria/core/__init__.py +1 -0
- package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__init__.py +9 -0
- package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
- package/python/voria/core/agent/loop.py +343 -0
- package/python/voria/core/executor/__init__.py +19 -0
- package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
- package/python/voria/core/executor/executor.py +431 -0
- package/python/voria/core/github/__init__.py +33 -0
- package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
- package/python/voria/core/github/client.py +438 -0
- package/python/voria/core/llm/__init__.py +55 -0
- package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/base.py +152 -0
- package/python/voria/core/llm/claude_provider.py +188 -0
- package/python/voria/core/llm/gemini_provider.py +148 -0
- package/python/voria/core/llm/modal_provider.py +228 -0
- package/python/voria/core/llm/model_discovery.py +289 -0
- package/python/voria/core/llm/openai_provider.py +146 -0
- package/python/voria/core/patcher/__init__.py +9 -0
- package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/patcher.py +375 -0
- package/python/voria/core/planner/__init__.py +1 -0
- package/python/voria/core/setup.py +201 -0
- package/python/voria/core/token_manager/__init__.py +29 -0
- package/python/voria/core/token_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/__pycache__/manager.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/manager.py +241 -0
- package/python/voria/engine.py +1185 -0
- package/python/voria/plugins/__init__.py +1 -0
- package/python/voria/plugins/python/__init__.py +1 -0
- package/python/voria/plugins/typescript/__init__.py +1 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
voria Python Engine - AI Agent Loop
|
|
4
|
+
|
|
5
|
+
Communicates with Node.js CLI via NDJSON over stdin/stdout.
|
|
6
|
+
- Reads: JSON commands from stdin
|
|
7
|
+
- Writes: JSON responses to stdout
|
|
8
|
+
- Logs: Debug/info to stderr
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, Dict, Any
|
|
18
|
+
from dataclasses import dataclass, asdict
|
|
19
|
+
|
|
20
|
+
# Configure logging to stderr only
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=logging.DEBUG,
|
|
23
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
24
|
+
stream=sys.stderr,
|
|
25
|
+
)
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Import voria modules
|
|
29
|
+
try:
|
|
30
|
+
from voria.core.llm import LLMProviderFactory, ModelDiscovery
|
|
31
|
+
from voria.core.github import GitHubClient
|
|
32
|
+
from voria.core.patcher import CodePatcher, UnifiedDiffParser
|
|
33
|
+
from voria.core.executor import TestExecutor
|
|
34
|
+
from voria.core.agent import AgentLoop
|
|
35
|
+
except ImportError as e:
|
|
36
|
+
logger.error(f"Failed to import voria modules: {e}")
|
|
37
|
+
logger.error("Make sure voria package is installed: pip install -e python/")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TokenUsage:
|
|
42
|
+
"""Track LLM token usage."""
|
|
43
|
+
|
|
44
|
+
used: int = 0
|
|
45
|
+
max: int = 0
|
|
46
|
+
cost: float = 0.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Response:
|
|
51
|
+
"""NDJSON response message."""
|
|
52
|
+
|
|
53
|
+
status: str # success, pending, error
|
|
54
|
+
action: str # apply_patch, run_tests, continue, stop
|
|
55
|
+
message: str
|
|
56
|
+
patch: Optional[str] = None
|
|
57
|
+
logs: Optional[str] = None
|
|
58
|
+
token_usage: Optional[Dict[str, Any]] = None
|
|
59
|
+
data: Optional[Dict[str, Any]] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def send_response(response: Response) -> None:
|
|
63
|
+
"""Send NDJSON response to Node.js CLI via stdout."""
|
|
64
|
+
response_dict = asdict(response)
|
|
65
|
+
# Remove None values
|
|
66
|
+
response_dict = {k: v for k, v in response_dict.items() if v is not None}
|
|
67
|
+
|
|
68
|
+
json_str = json.dumps(response_dict)
|
|
69
|
+
sys.stdout.write(json_str + "\n")
|
|
70
|
+
sys.stdout.flush()
|
|
71
|
+
logger.debug(f"Response sent: {json_str}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def handle_plan_command(command: Dict[str, Any]) -> None:
|
|
75
|
+
"""Handle 'plan' command to analyze and propose fix."""
|
|
76
|
+
try:
|
|
77
|
+
description = command.get("description")
|
|
78
|
+
repo_path = command.get("repo_path", ".")
|
|
79
|
+
provider_name = command.get("provider", "openai")
|
|
80
|
+
api_key = command.get("api_key")
|
|
81
|
+
model = command.get("model")
|
|
82
|
+
|
|
83
|
+
if not description and command.get("issue_id"):
|
|
84
|
+
description = f"Issue #{command.get('issue_id')}"
|
|
85
|
+
|
|
86
|
+
logger.info(f"Processing plan command: {description}")
|
|
87
|
+
|
|
88
|
+
if not api_key:
|
|
89
|
+
env_key = f"{provider_name.upper()}_API_KEY"
|
|
90
|
+
api_key = os.environ.get(env_key)
|
|
91
|
+
|
|
92
|
+
if not api_key:
|
|
93
|
+
send_response(
|
|
94
|
+
Response(
|
|
95
|
+
status="error",
|
|
96
|
+
action="stop",
|
|
97
|
+
message=f"API key required for {provider_name} provider",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Create LLM provider
|
|
103
|
+
try:
|
|
104
|
+
provider = LLMProviderFactory.create(
|
|
105
|
+
provider_name, api_key, model or "default"
|
|
106
|
+
)
|
|
107
|
+
logger.info(f"Created {provider_name} provider")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
send_response(
|
|
110
|
+
Response(
|
|
111
|
+
status="error",
|
|
112
|
+
action="stop",
|
|
113
|
+
message=f"Failed to create LLM provider: {str(e)}",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Mock response for testing
|
|
119
|
+
if api_key == "test-key":
|
|
120
|
+
send_response(
|
|
121
|
+
Response(
|
|
122
|
+
status="success",
|
|
123
|
+
action="stop",
|
|
124
|
+
message="Plan generated successfully (Mock)",
|
|
125
|
+
data={"plan": "Mock plan for testing", "provider": provider_name},
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Call LLM to generate plan
|
|
131
|
+
try:
|
|
132
|
+
from voria.core.llm import Message
|
|
133
|
+
|
|
134
|
+
messages = [
|
|
135
|
+
Message(
|
|
136
|
+
role="system",
|
|
137
|
+
content="You are an expert code analyzer. Provide a concise plan to fix the issue.",
|
|
138
|
+
),
|
|
139
|
+
Message(
|
|
140
|
+
role="user",
|
|
141
|
+
content=f"Please analyze and propose a fix for: {description}\n\nProvide a JSON response with: proposed_changes (list), files_affected (list), complexity (string), estimated_cost (float)",
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
response_obj = await provider.generate(messages)
|
|
146
|
+
plan_text = (
|
|
147
|
+
response_obj.content
|
|
148
|
+
if hasattr(response_obj, "content")
|
|
149
|
+
else str(response_obj)
|
|
150
|
+
)
|
|
151
|
+
logger.info(f"LLM response received: {plan_text[:100]}...")
|
|
152
|
+
|
|
153
|
+
send_response(
|
|
154
|
+
Response(
|
|
155
|
+
status="success",
|
|
156
|
+
action="stop",
|
|
157
|
+
message=f"Plan generated successfully",
|
|
158
|
+
data={"plan": plan_text, "provider": provider_name},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"LLM call failed: {e}")
|
|
163
|
+
send_response(
|
|
164
|
+
Response(
|
|
165
|
+
status="error",
|
|
166
|
+
action="stop",
|
|
167
|
+
message=f"Failed to generate plan: {str(e)}",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Plan command error: {e}", exc_info=True)
|
|
173
|
+
send_response(
|
|
174
|
+
Response(
|
|
175
|
+
status="error", action="stop", message=f"Plan command failed: {str(e)}"
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def handle_issue_command(command: Dict[str, Any]) -> None:
|
|
181
|
+
"""Handle 'issue' command to fetch and fix GitHub issue."""
|
|
182
|
+
try:
|
|
183
|
+
issue_number = command.get("issue_number")
|
|
184
|
+
repo_path = command.get("repo_path", ".")
|
|
185
|
+
repo = command.get("repo") # owner/repo format
|
|
186
|
+
github_token = command.get("github_token")
|
|
187
|
+
provider_name = command.get("provider", "openai")
|
|
188
|
+
api_key = command.get("api_key")
|
|
189
|
+
model = command.get("model")
|
|
190
|
+
|
|
191
|
+
logger.info(f"Processing issue command for: {repo}#{issue_number}")
|
|
192
|
+
|
|
193
|
+
if not issue_number or not repo:
|
|
194
|
+
send_response(
|
|
195
|
+
Response(
|
|
196
|
+
status="error",
|
|
197
|
+
action="stop",
|
|
198
|
+
message="Issue number and repo (owner/repo) are required",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if not github_token:
|
|
204
|
+
from voria.core.github import print_token_guide, get_github_token
|
|
205
|
+
|
|
206
|
+
print_token_guide()
|
|
207
|
+
print("\nEnter your GitHub Personal Access Token: ", end="")
|
|
208
|
+
github_token = input().strip()
|
|
209
|
+
|
|
210
|
+
if not github_token:
|
|
211
|
+
send_response(
|
|
212
|
+
Response(
|
|
213
|
+
status="error",
|
|
214
|
+
action="stop",
|
|
215
|
+
message="GitHub token is required. Use GITHUB_TOKEN env var or enter token when prompted.",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Fetch GitHub issue
|
|
221
|
+
try:
|
|
222
|
+
github = GitHubClient(github_token)
|
|
223
|
+
owner, repo_name = repo.split("/")
|
|
224
|
+
issue = await github.fetch_issue(owner, repo_name, issue_number)
|
|
225
|
+
logger.info(f"Fetched issue: {issue.title}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to fetch GitHub issue: {e}")
|
|
228
|
+
send_response(
|
|
229
|
+
Response(
|
|
230
|
+
status="error",
|
|
231
|
+
action="stop",
|
|
232
|
+
message=f"Failed to fetch issue: {str(e)}",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Create LLM provider
|
|
238
|
+
try:
|
|
239
|
+
provider = LLMProviderFactory.create(
|
|
240
|
+
provider_name, api_key, model or "default"
|
|
241
|
+
)
|
|
242
|
+
logger.info(f"Created {provider_name} provider")
|
|
243
|
+
except Exception as e:
|
|
244
|
+
send_response(
|
|
245
|
+
Response(
|
|
246
|
+
status="error",
|
|
247
|
+
action="stop",
|
|
248
|
+
message=f"Failed to create LLM provider: {str(e)}",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Generate fix
|
|
254
|
+
try:
|
|
255
|
+
from voria.core.llm import Message
|
|
256
|
+
|
|
257
|
+
messages = [
|
|
258
|
+
Message(
|
|
259
|
+
role="system",
|
|
260
|
+
content="You are an expert developer. Generate a unified diff to fix the GitHub issue.",
|
|
261
|
+
),
|
|
262
|
+
Message(
|
|
263
|
+
role="user",
|
|
264
|
+
content=f"Fix GitHub issue #{issue_number}:\n\nTitle: {issue.title}\n\nBody: {issue.body}\n\nRespond with a unified diff that fixes this issue.",
|
|
265
|
+
),
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
response_obj = await provider.generate(messages)
|
|
269
|
+
patch = (
|
|
270
|
+
response_obj.content
|
|
271
|
+
if hasattr(response_obj, "content")
|
|
272
|
+
else str(response_obj)
|
|
273
|
+
)
|
|
274
|
+
logger.info(f"Generated patch for issue #{issue_number}")
|
|
275
|
+
|
|
276
|
+
send_response(
|
|
277
|
+
Response(
|
|
278
|
+
status="success",
|
|
279
|
+
action="stop",
|
|
280
|
+
message=f"Issue fix generated successfully",
|
|
281
|
+
data={
|
|
282
|
+
"issue_number": issue_number,
|
|
283
|
+
"issue_title": issue.title,
|
|
284
|
+
"patch": patch,
|
|
285
|
+
"provider": provider_name,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Patch generation failed: {e}")
|
|
291
|
+
send_response(
|
|
292
|
+
Response(
|
|
293
|
+
status="error",
|
|
294
|
+
action="stop",
|
|
295
|
+
message=f"Failed to generate patch: {str(e)}",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Issue command error: {e}", exc_info=True)
|
|
301
|
+
send_response(
|
|
302
|
+
Response(
|
|
303
|
+
status="error", action="stop", message=f"Issue command failed: {str(e)}"
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def handle_fix_command(command: Dict[str, Any]) -> None:
|
|
309
|
+
"""Handle 'fix' command to fix a GitHub issue."""
|
|
310
|
+
try:
|
|
311
|
+
issue_number = command.get("issue_id")
|
|
312
|
+
owner = command.get("owner")
|
|
313
|
+
repo = command.get("repo")
|
|
314
|
+
github_token = command.get("github_token")
|
|
315
|
+
provider_name = command.get("provider", "modal")
|
|
316
|
+
api_key = command.get("api_key")
|
|
317
|
+
model = command.get("model")
|
|
318
|
+
|
|
319
|
+
logger.info(f"Processing fix command for: {owner}/{repo}#{issue_number}")
|
|
320
|
+
|
|
321
|
+
if not issue_number or not owner or not repo:
|
|
322
|
+
send_response(
|
|
323
|
+
Response(
|
|
324
|
+
status="error",
|
|
325
|
+
action="stop",
|
|
326
|
+
message="Issue number, owner, and repo are required",
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
if not github_token:
|
|
332
|
+
from voria.core.github import print_token_guide
|
|
333
|
+
|
|
334
|
+
print_token_guide()
|
|
335
|
+
print("\nEnter your GitHub Personal Access Token: ", end="")
|
|
336
|
+
github_token = input().strip()
|
|
337
|
+
|
|
338
|
+
if not github_token:
|
|
339
|
+
send_response(
|
|
340
|
+
Response(
|
|
341
|
+
status="error",
|
|
342
|
+
action="stop",
|
|
343
|
+
message="GitHub token is required.",
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Fetch GitHub issue
|
|
349
|
+
try:
|
|
350
|
+
github = GitHubClient(github_token)
|
|
351
|
+
issue = await github.fetch_issue(owner, repo, issue_number)
|
|
352
|
+
logger.info(f"Fetched issue: {issue.title}")
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"Failed to fetch GitHub issue: {e}")
|
|
355
|
+
send_response(
|
|
356
|
+
Response(
|
|
357
|
+
status="error",
|
|
358
|
+
action="stop",
|
|
359
|
+
message=f"Failed to fetch issue: {str(e)}",
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
# Create LLM provider
|
|
365
|
+
try:
|
|
366
|
+
provider = LLMProviderFactory.create(
|
|
367
|
+
provider_name, api_key, model or "default"
|
|
368
|
+
)
|
|
369
|
+
logger.info(f"Created {provider_name} provider")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
send_response(
|
|
372
|
+
Response(
|
|
373
|
+
status="error",
|
|
374
|
+
action="stop",
|
|
375
|
+
message=f"Failed to create LLM provider: {str(e)}",
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Generate fix
|
|
381
|
+
try:
|
|
382
|
+
from voria.core.llm import Message
|
|
383
|
+
|
|
384
|
+
messages = [
|
|
385
|
+
Message(
|
|
386
|
+
role="system",
|
|
387
|
+
content="You are an expert developer. Generate a unified diff to fix the GitHub issue.",
|
|
388
|
+
),
|
|
389
|
+
Message(
|
|
390
|
+
role="user",
|
|
391
|
+
content=f"Fix GitHub issue #{issue_number}:\n\nTitle: {issue.title}\n\nBody: {issue.body}\n\nRespond with a unified diff that fixes this issue.",
|
|
392
|
+
),
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
response_obj = await provider.generate(messages)
|
|
396
|
+
patch = (
|
|
397
|
+
response_obj.content
|
|
398
|
+
if hasattr(response_obj, "content")
|
|
399
|
+
else str(response_obj)
|
|
400
|
+
)
|
|
401
|
+
logger.info(f"Generated patch for issue #{issue_number}")
|
|
402
|
+
|
|
403
|
+
send_response(
|
|
404
|
+
Response(
|
|
405
|
+
status="success",
|
|
406
|
+
action="stop",
|
|
407
|
+
message=f"Issue #{issue_number} fix generated successfully!",
|
|
408
|
+
data={
|
|
409
|
+
"issue_number": issue_number,
|
|
410
|
+
"issue_title": issue.title,
|
|
411
|
+
"patch": patch,
|
|
412
|
+
"provider": provider_name,
|
|
413
|
+
"owner": owner,
|
|
414
|
+
"repo": repo,
|
|
415
|
+
},
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
logger.error(f"Patch generation failed: {e}")
|
|
420
|
+
send_response(
|
|
421
|
+
Response(
|
|
422
|
+
status="error",
|
|
423
|
+
action="stop",
|
|
424
|
+
message=f"Failed to generate patch: {str(e)}",
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Fix command error: {e}", exc_info=True)
|
|
430
|
+
send_response(
|
|
431
|
+
Response(
|
|
432
|
+
status="error", action="stop", message=f"Fix command failed: {str(e)}"
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
async def handle_list_issues_command(command: Dict[str, Any]) -> None:
|
|
438
|
+
"""Handle 'list_issues' command to fetch all issues for a user's repos."""
|
|
439
|
+
try:
|
|
440
|
+
github_login = command.get("github_login")
|
|
441
|
+
repo_url = command.get("repo_url")
|
|
442
|
+
owner = command.get("owner")
|
|
443
|
+
repo = command.get("repo")
|
|
444
|
+
github_token = command.get("github_token")
|
|
445
|
+
|
|
446
|
+
logger.info(f"Processing list_issues command")
|
|
447
|
+
|
|
448
|
+
if not github_token:
|
|
449
|
+
from voria.core.github import print_token_guide, get_github_token
|
|
450
|
+
|
|
451
|
+
print_token_guide()
|
|
452
|
+
print("\nEnter your GitHub Personal Access Token: ", end="")
|
|
453
|
+
github_token = input().strip()
|
|
454
|
+
|
|
455
|
+
if not github_token:
|
|
456
|
+
send_response(
|
|
457
|
+
Response(
|
|
458
|
+
status="error",
|
|
459
|
+
action="stop",
|
|
460
|
+
message="GitHub token is required. Use GITHUB_TOKEN env var or enter token when prompted.",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
github = GitHubClient(github_token)
|
|
467
|
+
|
|
468
|
+
issues = []
|
|
469
|
+
|
|
470
|
+
if github_login:
|
|
471
|
+
user = await github.get_authenticated_user()
|
|
472
|
+
authenticated_login = user.get("login")
|
|
473
|
+
|
|
474
|
+
if github_login == authenticated_login or github_login == "me":
|
|
475
|
+
logger.info(
|
|
476
|
+
f"Fetching issues for authenticated user: {authenticated_login}"
|
|
477
|
+
)
|
|
478
|
+
issues = await github.fetch_user_issues(
|
|
479
|
+
authenticated_login, state="open"
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
logger.info(f"Fetching issues for user: {github_login}")
|
|
483
|
+
issues = await github.fetch_user_issues(github_login, state="open")
|
|
484
|
+
|
|
485
|
+
elif owner and repo:
|
|
486
|
+
logger.info(f"Fetching issues for repo: {owner}/{repo}")
|
|
487
|
+
issues = await github.fetch_repo_issues(owner, repo, state="open")
|
|
488
|
+
|
|
489
|
+
elif repo_url:
|
|
490
|
+
parts = repo_url.strip("/").split("/")
|
|
491
|
+
if len(parts) >= 2:
|
|
492
|
+
owner = parts[-2]
|
|
493
|
+
repo_name = parts[-1]
|
|
494
|
+
logger.info(f"Fetching issues for repo: {owner}/{repo_name}")
|
|
495
|
+
issues = await github.fetch_repo_issues(
|
|
496
|
+
owner, repo_name, state="open"
|
|
497
|
+
)
|
|
498
|
+
else:
|
|
499
|
+
raise ValueError(f"Invalid repo URL: {repo_url}")
|
|
500
|
+
|
|
501
|
+
else:
|
|
502
|
+
send_response(
|
|
503
|
+
Response(
|
|
504
|
+
status="error",
|
|
505
|
+
action="stop",
|
|
506
|
+
message="Provide either owner/repo, github_login, or repo_url",
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
issue_list = []
|
|
512
|
+
for issue in issues:
|
|
513
|
+
issue_list.append(
|
|
514
|
+
{
|
|
515
|
+
"number": issue.number,
|
|
516
|
+
"title": issue.title,
|
|
517
|
+
"labels": issue.labels,
|
|
518
|
+
"state": issue.state,
|
|
519
|
+
"url": issue.url,
|
|
520
|
+
"repo": issue.repo,
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
send_response(
|
|
525
|
+
Response(
|
|
526
|
+
status="success",
|
|
527
|
+
action="stop",
|
|
528
|
+
message=f"Found {len(issue_list)} open issues",
|
|
529
|
+
data={"issues": issue_list, "count": len(issue_list)},
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
await github.close()
|
|
534
|
+
|
|
535
|
+
except Exception as e:
|
|
536
|
+
logger.error(f"Failed to fetch issues: {e}")
|
|
537
|
+
send_response(
|
|
538
|
+
Response(
|
|
539
|
+
status="error",
|
|
540
|
+
action="stop",
|
|
541
|
+
message=f"Failed to fetch issues: {str(e)}",
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.error(f"List issues command error: {e}", exc_info=True)
|
|
547
|
+
send_response(
|
|
548
|
+
Response(
|
|
549
|
+
status="error",
|
|
550
|
+
action="stop",
|
|
551
|
+
message=f"List issues command failed: {str(e)}",
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
async def handle_create_pr_command(command: Dict[str, Any]) -> None:
|
|
557
|
+
"""Handle 'create_pr' command to push changes and create Pull Request."""
|
|
558
|
+
try:
|
|
559
|
+
owner = command.get("owner")
|
|
560
|
+
repo = command.get("repo")
|
|
561
|
+
github_token = command.get("github_token")
|
|
562
|
+
issue_number = command.get("issue_number")
|
|
563
|
+
patch = command.get("patch")
|
|
564
|
+
|
|
565
|
+
if not all([owner, repo, github_token, issue_number, patch]):
|
|
566
|
+
send_response(
|
|
567
|
+
Response(
|
|
568
|
+
status="error",
|
|
569
|
+
action="stop",
|
|
570
|
+
message="Owner, repo, issue_number, patch, and github_token are required",
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
logger.info(f"Creating PR for {owner}/{repo}#{issue_number}")
|
|
576
|
+
|
|
577
|
+
branch_name = f"voria-fix-{issue_number}"
|
|
578
|
+
|
|
579
|
+
# Git operations
|
|
580
|
+
import subprocess
|
|
581
|
+
|
|
582
|
+
def run_git(args):
|
|
583
|
+
return subprocess.run(
|
|
584
|
+
["git"] + args, capture_output=True, text=True, check=True
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
# 1. Create and switch to new branch
|
|
589
|
+
run_git(["checkout", "-b", branch_name])
|
|
590
|
+
|
|
591
|
+
# 2. Apply patch
|
|
592
|
+
patch_path = os.path.join("/tmp", f"fix_{issue_number}.patch")
|
|
593
|
+
with open(patch_path, "w") as f:
|
|
594
|
+
f.write(patch)
|
|
595
|
+
|
|
596
|
+
subprocess.run(["patch", "-p1", "-i", patch_path], check=True)
|
|
597
|
+
|
|
598
|
+
# 3. Commit
|
|
599
|
+
run_git(["add", "."])
|
|
600
|
+
run_git(["commit", "-m", f"Fix issue #{issue_number} using voria AI"])
|
|
601
|
+
|
|
602
|
+
# 4. Push (This would require the token in the URL or credential helper)
|
|
603
|
+
# For now, we print that we'd push. To actually push:
|
|
604
|
+
# run_git(["push", "origin", branch_name])
|
|
605
|
+
|
|
606
|
+
github = GitHubClient(github_token)
|
|
607
|
+
|
|
608
|
+
pr_title = f"Fix issue #{issue_number}"
|
|
609
|
+
pr_body = f"This PR was automatically generated by voria AI to fix issue #{issue_number}.\n\n"
|
|
610
|
+
pr_body += (
|
|
611
|
+
"### Changes\n- Applied AI-generated patch\n- Verified with test suite"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Create the PR via API
|
|
615
|
+
pr_data = await github.create_pr(
|
|
616
|
+
owner=owner, repo=repo, title=pr_title, body=pr_body, head=branch_name
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
send_response(
|
|
620
|
+
Response(
|
|
621
|
+
status="success",
|
|
622
|
+
action="stop",
|
|
623
|
+
message=f"Pull Request created successfully for #{issue_number}",
|
|
624
|
+
data={
|
|
625
|
+
"pr_number": pr_data["number"],
|
|
626
|
+
"pr_url": pr_data["url"],
|
|
627
|
+
"branch": branch_name,
|
|
628
|
+
},
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
except subprocess.CalledProcessError as e:
|
|
633
|
+
logger.error(f"Git operation failed: {e.stderr}")
|
|
634
|
+
send_response(
|
|
635
|
+
Response(status="error", message=f"Git operation failed: {e.stderr}")
|
|
636
|
+
)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
logger.error(f"PR creation error: {e}")
|
|
639
|
+
send_response(
|
|
640
|
+
Response(status="error", message=f"PR creation error: {str(e)}")
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
except Exception as e:
|
|
644
|
+
logger.error(f"Create PR command error: {e}", exc_info=True)
|
|
645
|
+
send_response(
|
|
646
|
+
Response(
|
|
647
|
+
status="error", action="stop", message=f"Failed to create PR: {str(e)}"
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
async def handle_apply_command(command: Dict[str, Any]) -> None:
|
|
653
|
+
"""Handle 'apply' command to apply patch to repository."""
|
|
654
|
+
try:
|
|
655
|
+
patch_content = command.get("patch")
|
|
656
|
+
repo_path = command.get("repo_path", ".")
|
|
657
|
+
|
|
658
|
+
if not patch_content:
|
|
659
|
+
send_response(
|
|
660
|
+
Response(
|
|
661
|
+
status="error", action="stop", message="Patch content is required"
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
patcher = CodePatcher(repo_path)
|
|
668
|
+
result = await patcher.apply_patch(patch_content)
|
|
669
|
+
logger.info(f"Patch applied successfully: {result}")
|
|
670
|
+
|
|
671
|
+
send_response(
|
|
672
|
+
Response(
|
|
673
|
+
status="success",
|
|
674
|
+
action="stop",
|
|
675
|
+
message=f"Patch applied successfully",
|
|
676
|
+
data={"files_modified": result},
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
except Exception as e:
|
|
680
|
+
logger.error(f"Patch application failed: {e}")
|
|
681
|
+
send_response(
|
|
682
|
+
Response(
|
|
683
|
+
status="error",
|
|
684
|
+
action="stop",
|
|
685
|
+
message=f"Failed to apply patch: {str(e)}",
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
except Exception as e:
|
|
690
|
+
logger.error(f"Apply command error: {e}", exc_info=True)
|
|
691
|
+
send_response(
|
|
692
|
+
Response(
|
|
693
|
+
status="error", action="stop", message=f"Apply command failed: {str(e)}"
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
async def handle_logs_command(command: Dict[str, Any]) -> None:
|
|
699
|
+
"""Handle 'logs' command to view or stream logs."""
|
|
700
|
+
try:
|
|
701
|
+
level = command.get("level", "INFO")
|
|
702
|
+
follow = command.get("follow", False)
|
|
703
|
+
lines = command.get("lines", 50)
|
|
704
|
+
|
|
705
|
+
log_dir = Path.home() / ".voria"
|
|
706
|
+
log_file = log_dir / "voria.log"
|
|
707
|
+
|
|
708
|
+
if not log_file.exists():
|
|
709
|
+
send_response(
|
|
710
|
+
Response(
|
|
711
|
+
status="error",
|
|
712
|
+
action="stop",
|
|
713
|
+
message=f"No log file found at {log_file}",
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
if follow:
|
|
719
|
+
send_response(
|
|
720
|
+
Response(
|
|
721
|
+
status="success",
|
|
722
|
+
action="continue",
|
|
723
|
+
message="Streaming logs... (Ctrl+C to stop)",
|
|
724
|
+
data={"log_file": str(log_file)},
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
else:
|
|
728
|
+
try:
|
|
729
|
+
with open(log_file, "r") as f:
|
|
730
|
+
log_lines = f.readlines()[-lines:]
|
|
731
|
+
|
|
732
|
+
log_content = "".join(log_lines)
|
|
733
|
+
send_response(
|
|
734
|
+
Response(
|
|
735
|
+
status="success",
|
|
736
|
+
action="stop",
|
|
737
|
+
message=f"Last {len(log_lines)} log lines",
|
|
738
|
+
logs=log_content,
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
except Exception as e:
|
|
742
|
+
send_response(
|
|
743
|
+
Response(
|
|
744
|
+
status="error",
|
|
745
|
+
action="stop",
|
|
746
|
+
message=f"Failed to read logs: {str(e)}",
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.error(f"Logs command error: {e}", exc_info=True)
|
|
752
|
+
send_response(
|
|
753
|
+
Response(
|
|
754
|
+
status="error", action="stop", message=f"Logs command failed: {str(e)}"
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
async def handle_token_command(command: Dict[str, Any]) -> None:
|
|
760
|
+
"""Handle 'token' command for token usage info."""
|
|
761
|
+
try:
|
|
762
|
+
from voria.core.token_manager import get_token_manager
|
|
763
|
+
|
|
764
|
+
subcommand = command.get("subcommand", "info")
|
|
765
|
+
|
|
766
|
+
if subcommand == "info":
|
|
767
|
+
tm = get_token_manager()
|
|
768
|
+
summary = tm.get_usage_summary()
|
|
769
|
+
send_response(
|
|
770
|
+
Response(
|
|
771
|
+
status="success",
|
|
772
|
+
action="stop",
|
|
773
|
+
message="Token usage info",
|
|
774
|
+
data={"tokens": summary},
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
elif subcommand == "reset":
|
|
778
|
+
from voria.core.token_manager import init_token_manager, TokenBudget
|
|
779
|
+
|
|
780
|
+
tm = init_token_manager(TokenBudget())
|
|
781
|
+
send_response(
|
|
782
|
+
Response(status="success", action="stop", message="Token usage reset")
|
|
783
|
+
)
|
|
784
|
+
else:
|
|
785
|
+
send_response(
|
|
786
|
+
Response(
|
|
787
|
+
status="error",
|
|
788
|
+
action="stop",
|
|
789
|
+
message=f"Unknown token subcommand: {subcommand}",
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
except Exception as e:
|
|
794
|
+
logger.error(f"Token command error: {e}", exc_info=True)
|
|
795
|
+
send_response(
|
|
796
|
+
Response(
|
|
797
|
+
status="error", action="stop", message=f"Token command failed: {str(e)}"
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def handle_test_results_callback(command: Dict[str, Any]) -> None:
|
|
803
|
+
"""Handle test results callback from CLI."""
|
|
804
|
+
test_status = command.get("test_status")
|
|
805
|
+
test_logs = command.get("test_logs")
|
|
806
|
+
|
|
807
|
+
logger.info(f"Received test results: {test_status}")
|
|
808
|
+
if test_logs:
|
|
809
|
+
logger.debug(f"Test logs:\n{test_logs}")
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
async def process_command_async(line: str) -> None:
|
|
813
|
+
"""Process a single NDJSON command line asynchronously."""
|
|
814
|
+
try:
|
|
815
|
+
command = json.loads(line.strip())
|
|
816
|
+
logger.debug(f"Command received: {command}")
|
|
817
|
+
|
|
818
|
+
cmd_type = command.get("command")
|
|
819
|
+
|
|
820
|
+
if cmd_type == "plan":
|
|
821
|
+
await handle_plan_command(command)
|
|
822
|
+
elif cmd_type == "issue":
|
|
823
|
+
await handle_issue_command(command)
|
|
824
|
+
elif cmd_type == "fix":
|
|
825
|
+
await handle_fix_command(command)
|
|
826
|
+
elif cmd_type == "list_issues":
|
|
827
|
+
await handle_list_issues_command(command)
|
|
828
|
+
elif cmd_type == "apply":
|
|
829
|
+
await handle_apply_command(command)
|
|
830
|
+
elif cmd_type == "logs":
|
|
831
|
+
await handle_logs_command(command)
|
|
832
|
+
elif cmd_type == "token":
|
|
833
|
+
await handle_token_command(command)
|
|
834
|
+
elif cmd_type == "config":
|
|
835
|
+
await handle_config_command(command)
|
|
836
|
+
elif cmd_type == "test_results":
|
|
837
|
+
handle_test_results_callback(command)
|
|
838
|
+
elif cmd_type == "create_pr":
|
|
839
|
+
await handle_create_pr_command(command)
|
|
840
|
+
else:
|
|
841
|
+
logger.error(f"Unknown command type: {cmd_type}")
|
|
842
|
+
send_response(
|
|
843
|
+
Response(
|
|
844
|
+
status="error",
|
|
845
|
+
action="stop",
|
|
846
|
+
message=f"Unknown command: {cmd_type}",
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
except json.JSONDecodeError as e:
|
|
850
|
+
logger.error(f"Failed to parse JSON: {e}")
|
|
851
|
+
send_response(
|
|
852
|
+
Response(status="error", action="stop", message=f"Invalid JSON: {str(e)}")
|
|
853
|
+
)
|
|
854
|
+
except Exception as e:
|
|
855
|
+
logger.error(f"Command processing error: {e}", exc_info=True)
|
|
856
|
+
send_response(
|
|
857
|
+
Response(
|
|
858
|
+
status="error", action="stop", message=f"Processing error: {str(e)}"
|
|
859
|
+
)
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
voria_CONFIG_DIR = Path.home() / ".voria"
|
|
864
|
+
voria_CONFIG_FILE = voria_CONFIG_DIR / "config.json"
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def load_config() -> Dict[str, Any]:
|
|
868
|
+
"""Load voria configuration from ~/.voria/config.json"""
|
|
869
|
+
if voria_CONFIG_FILE.exists():
|
|
870
|
+
try:
|
|
871
|
+
with open(voria_CONFIG_FILE, "r") as f:
|
|
872
|
+
return json.load(f)
|
|
873
|
+
except Exception as e:
|
|
874
|
+
logger.warning(f"Failed to load config: {e}")
|
|
875
|
+
return {}
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
879
|
+
"""Save voria configuration to ~/.voria/config.json"""
|
|
880
|
+
try:
|
|
881
|
+
voria_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
882
|
+
with open(voria_CONFIG_FILE, "w") as f:
|
|
883
|
+
json.dump(config, f, indent=2)
|
|
884
|
+
os.chmod(voria_CONFIG_FILE, 0o600)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
logger.error(f"Failed to save config: {e}")
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
async def handle_config_command(command: Dict[str, Any]) -> None:
|
|
890
|
+
"""Handle 'config' command for managing configuration."""
|
|
891
|
+
try:
|
|
892
|
+
action = command.get("action", "get")
|
|
893
|
+
config = load_config()
|
|
894
|
+
|
|
895
|
+
if action == "get":
|
|
896
|
+
send_response(
|
|
897
|
+
Response(
|
|
898
|
+
status="success",
|
|
899
|
+
action="stop",
|
|
900
|
+
message="Current configuration",
|
|
901
|
+
data={"config": config},
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
if action == "set":
|
|
907
|
+
github_token = command.get("github_token")
|
|
908
|
+
llm_provider = command.get("llm_provider")
|
|
909
|
+
llm_api_key = command.get("llm_api_key")
|
|
910
|
+
llm_model = command.get("llm_model")
|
|
911
|
+
daily_budget = command.get("daily_budget")
|
|
912
|
+
test_framework = command.get("test_framework")
|
|
913
|
+
|
|
914
|
+
if github_token:
|
|
915
|
+
config["github_token"] = github_token
|
|
916
|
+
if llm_provider:
|
|
917
|
+
config["llm_provider"] = llm_provider
|
|
918
|
+
if llm_api_key:
|
|
919
|
+
config["llm_api_key"] = llm_api_key
|
|
920
|
+
if llm_model:
|
|
921
|
+
config["llm_model"] = llm_model
|
|
922
|
+
if daily_budget is not None:
|
|
923
|
+
config["daily_budget"] = daily_budget
|
|
924
|
+
if test_framework:
|
|
925
|
+
config["test_framework"] = test_framework
|
|
926
|
+
|
|
927
|
+
save_config(config)
|
|
928
|
+
send_response(
|
|
929
|
+
Response(
|
|
930
|
+
status="success",
|
|
931
|
+
action="stop",
|
|
932
|
+
message="Configuration saved successfully",
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
if action == "init":
|
|
938
|
+
from voria.core.github import print_token_guide, get_github_token
|
|
939
|
+
|
|
940
|
+
print("\n" + "=" * 60)
|
|
941
|
+
print("🚀 voria Setup - First Time Configuration")
|
|
942
|
+
print("=" * 60 + "\n")
|
|
943
|
+
|
|
944
|
+
config = {}
|
|
945
|
+
|
|
946
|
+
print("=" * 60)
|
|
947
|
+
print("STEP 1: LLM Provider Setup")
|
|
948
|
+
print("=" * 60)
|
|
949
|
+
print("Available providers: modal, openai, gemini, claude")
|
|
950
|
+
print("Note: Modal is FREE, Gemini is cheapest ($1-5/month)")
|
|
951
|
+
print()
|
|
952
|
+
|
|
953
|
+
provider = (
|
|
954
|
+
input("Select LLM provider (modal/openai/gemini/claude): ")
|
|
955
|
+
.strip()
|
|
956
|
+
.lower()
|
|
957
|
+
)
|
|
958
|
+
if provider not in ["modal", "openai", "gemini", "claude"]:
|
|
959
|
+
provider = "modal"
|
|
960
|
+
|
|
961
|
+
config["llm_provider"] = provider
|
|
962
|
+
print(f"✅ Using: {provider}\n")
|
|
963
|
+
|
|
964
|
+
if provider == "modal":
|
|
965
|
+
print("Modal is FREE! Get your key from: https://modal.com")
|
|
966
|
+
api_key = input("Enter your Modal API key: ").strip()
|
|
967
|
+
if not api_key:
|
|
968
|
+
print("❌ Modal API key is required!")
|
|
969
|
+
send_response(
|
|
970
|
+
Response(
|
|
971
|
+
status="error",
|
|
972
|
+
action="stop",
|
|
973
|
+
message="Modal API key is required",
|
|
974
|
+
)
|
|
975
|
+
)
|
|
976
|
+
return
|
|
977
|
+
config["llm_api_key"] = api_key
|
|
978
|
+
elif provider == "openai":
|
|
979
|
+
api_key = input("Enter OpenAI API key (sk-...): ").strip()
|
|
980
|
+
if not api_key:
|
|
981
|
+
print("❌ OpenAI API key is required!")
|
|
982
|
+
send_response(
|
|
983
|
+
Response(
|
|
984
|
+
status="error",
|
|
985
|
+
action="stop",
|
|
986
|
+
message="OpenAI API key is required",
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
return
|
|
990
|
+
config["llm_api_key"] = api_key
|
|
991
|
+
elif provider == "gemini":
|
|
992
|
+
api_key = input("Enter Google Gemini API key: ").strip()
|
|
993
|
+
if not api_key:
|
|
994
|
+
print("❌ Gemini API key is required!")
|
|
995
|
+
send_response(
|
|
996
|
+
Response(
|
|
997
|
+
status="error",
|
|
998
|
+
action="stop",
|
|
999
|
+
message="Gemini API key is required",
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
return
|
|
1003
|
+
config["llm_api_key"] = api_key
|
|
1004
|
+
elif provider == "claude":
|
|
1005
|
+
api_key = input("Enter Anthropic Claude API key (sk-ant-...): ").strip()
|
|
1006
|
+
if not api_key:
|
|
1007
|
+
print("❌ Claude API key is required!")
|
|
1008
|
+
send_response(
|
|
1009
|
+
Response(
|
|
1010
|
+
status="error",
|
|
1011
|
+
action="stop",
|
|
1012
|
+
message="Claude API key is required",
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
return
|
|
1016
|
+
config["llm_api_key"] = api_key
|
|
1017
|
+
|
|
1018
|
+
print(f"✅ API key saved\n")
|
|
1019
|
+
|
|
1020
|
+
print("=" * 60)
|
|
1021
|
+
print("STEP 2: GitHub Setup (Optional)")
|
|
1022
|
+
print("=" * 60)
|
|
1023
|
+
|
|
1024
|
+
setup_github = input("Setup GitHub token now? (y/n): ").lower().strip()
|
|
1025
|
+
if setup_github == "y":
|
|
1026
|
+
print_token_guide()
|
|
1027
|
+
token = input("\nEnter GitHub Personal Access Token: ").strip()
|
|
1028
|
+
if token:
|
|
1029
|
+
config["github_token"] = token
|
|
1030
|
+
print("✅ GitHub token saved\n")
|
|
1031
|
+
else:
|
|
1032
|
+
print(
|
|
1033
|
+
"Skipped. You can add it later with: voria config --github YOUR_TOKEN\n"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
print("=" * 60)
|
|
1037
|
+
print("STEP 3: Budget & Testing (Optional)")
|
|
1038
|
+
print("=" * 60)
|
|
1039
|
+
|
|
1040
|
+
budget_input = input("Daily budget in USD (default: 10): ").strip()
|
|
1041
|
+
if budget_input:
|
|
1042
|
+
try:
|
|
1043
|
+
config["daily_budget"] = float(budget_input)
|
|
1044
|
+
except:
|
|
1045
|
+
config["daily_budget"] = 10.0
|
|
1046
|
+
else:
|
|
1047
|
+
config["daily_budget"] = 10.0
|
|
1048
|
+
|
|
1049
|
+
print(f"✅ Daily budget: ${config['daily_budget']}\n")
|
|
1050
|
+
|
|
1051
|
+
print("=" * 60)
|
|
1052
|
+
print("STEP 4: Test Framework")
|
|
1053
|
+
print("=" * 60)
|
|
1054
|
+
print("Detected: pytest, jest, cargo test, etc.")
|
|
1055
|
+
framework = input(
|
|
1056
|
+
"Enter test framework (or press Enter for auto-detect): "
|
|
1057
|
+
).strip()
|
|
1058
|
+
if framework:
|
|
1059
|
+
config["test_framework"] = framework
|
|
1060
|
+
|
|
1061
|
+
save_config(config)
|
|
1062
|
+
|
|
1063
|
+
print("\n" + "=" * 60)
|
|
1064
|
+
print("✅ SETUP COMPLETE!")
|
|
1065
|
+
print("=" * 60)
|
|
1066
|
+
print("\nNext steps:")
|
|
1067
|
+
print(" - voria issue 42 # Fix a GitHub issue")
|
|
1068
|
+
print(" - voria plan 'Add error handling' # Plan a fix")
|
|
1069
|
+
print(" - voria logs # View activity")
|
|
1070
|
+
print("\nTo update config later: voria config")
|
|
1071
|
+
print("=" * 60 + "\n")
|
|
1072
|
+
|
|
1073
|
+
send_response(
|
|
1074
|
+
Response(
|
|
1075
|
+
status="success",
|
|
1076
|
+
action="stop",
|
|
1077
|
+
message="voria initialized successfully!",
|
|
1078
|
+
data={"config": config},
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
return
|
|
1082
|
+
|
|
1083
|
+
if action == "github":
|
|
1084
|
+
token = command.get("token")
|
|
1085
|
+
if token:
|
|
1086
|
+
config["github_token"] = token
|
|
1087
|
+
save_config(config)
|
|
1088
|
+
send_response(
|
|
1089
|
+
Response(
|
|
1090
|
+
status="success",
|
|
1091
|
+
action="stop",
|
|
1092
|
+
message="GitHub token saved! You can now use voria issue without entering token.",
|
|
1093
|
+
)
|
|
1094
|
+
)
|
|
1095
|
+
else:
|
|
1096
|
+
print_token_guide()
|
|
1097
|
+
token = input("Enter GitHub Personal Access Token: ").strip()
|
|
1098
|
+
if token:
|
|
1099
|
+
config["github_token"] = token
|
|
1100
|
+
save_config(config)
|
|
1101
|
+
send_response(
|
|
1102
|
+
Response(
|
|
1103
|
+
status="success",
|
|
1104
|
+
action="stop",
|
|
1105
|
+
message="GitHub token saved!",
|
|
1106
|
+
)
|
|
1107
|
+
)
|
|
1108
|
+
else:
|
|
1109
|
+
send_response(
|
|
1110
|
+
Response(
|
|
1111
|
+
status="error", action="stop", message="No token provided"
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
return
|
|
1115
|
+
|
|
1116
|
+
send_response(
|
|
1117
|
+
Response(
|
|
1118
|
+
status="error",
|
|
1119
|
+
action="stop",
|
|
1120
|
+
message=f"Unknown config action: {action}",
|
|
1121
|
+
)
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
except Exception as e:
|
|
1125
|
+
logger.error(f"Config command error: {e}", exc_info=True)
|
|
1126
|
+
send_response(
|
|
1127
|
+
Response(
|
|
1128
|
+
status="error",
|
|
1129
|
+
action="stop",
|
|
1130
|
+
message=f"Config command failed: {str(e)}",
|
|
1131
|
+
)
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def main() -> None:
|
|
1136
|
+
"""Main engine loop - read and process NDJSON from stdin."""
|
|
1137
|
+
logger.info("voria Python Engine started")
|
|
1138
|
+
logger.info("Ready to receive commands via NDJSON on stdin")
|
|
1139
|
+
|
|
1140
|
+
try:
|
|
1141
|
+
# Create a single event loop for the entire session
|
|
1142
|
+
loop = asyncio.get_event_loop()
|
|
1143
|
+
except RuntimeError:
|
|
1144
|
+
loop = asyncio.new_event_loop()
|
|
1145
|
+
asyncio.set_event_loop(loop)
|
|
1146
|
+
|
|
1147
|
+
try:
|
|
1148
|
+
while True:
|
|
1149
|
+
try:
|
|
1150
|
+
line = sys.stdin.readline()
|
|
1151
|
+
|
|
1152
|
+
# EOF or empty line
|
|
1153
|
+
if not line:
|
|
1154
|
+
logger.info("Received EOF, shutting down")
|
|
1155
|
+
break
|
|
1156
|
+
|
|
1157
|
+
# Process the command asynchronously
|
|
1158
|
+
loop.run_until_complete(process_command_async(line))
|
|
1159
|
+
except EOFError:
|
|
1160
|
+
logger.info("EOF received")
|
|
1161
|
+
break
|
|
1162
|
+
except Exception as e:
|
|
1163
|
+
logger.error(f"Error processing command: {e}", exc_info=True)
|
|
1164
|
+
send_response(
|
|
1165
|
+
Response(
|
|
1166
|
+
status="error",
|
|
1167
|
+
action="stop",
|
|
1168
|
+
message=f"Processing error: {str(e)}",
|
|
1169
|
+
)
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
except KeyboardInterrupt:
|
|
1173
|
+
logger.info("Received interrupt signal")
|
|
1174
|
+
except Exception as e:
|
|
1175
|
+
logger.error(f"Unexpected error in main loop: {e}", exc_info=True)
|
|
1176
|
+
finally:
|
|
1177
|
+
try:
|
|
1178
|
+
loop.close()
|
|
1179
|
+
except:
|
|
1180
|
+
pass
|
|
1181
|
+
logger.info("voria Python Engine shutting down")
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
if __name__ == "__main__":
|
|
1185
|
+
main()
|