pocketcoder-a1 0.1.0__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.
- a1/__init__.py +6 -0
- a1/checkpoint.py +146 -0
- a1/cli.py +368 -0
- a1/config.py +126 -0
- a1/dashboard.py +2589 -0
- a1/loop.py +1151 -0
- a1/tasks.py +211 -0
- a1/tester/__init__.py +6 -0
- a1/tester/analyzer.py +142 -0
- a1/tester/browser.py +124 -0
- a1/tester/report.py +193 -0
- a1/tester/runner.py +419 -0
- a1/tester/scenarios.py +203 -0
- a1/validator.py +361 -0
- pocketcoder_a1-0.1.0.dist-info/METADATA +230 -0
- pocketcoder_a1-0.1.0.dist-info/RECORD +20 -0
- pocketcoder_a1-0.1.0.dist-info/WHEEL +5 -0
- pocketcoder_a1-0.1.0.dist-info/entry_points.txt +2 -0
- pocketcoder_a1-0.1.0.dist-info/licenses/LICENSE +21 -0
- pocketcoder_a1-0.1.0.dist-info/top_level.txt +1 -0
a1/__init__.py
ADDED
a1/checkpoint.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Checkpoint Manager — сохранение состояния между сессиями
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CheckpointManager:
|
|
12
|
+
"""Управление checkpoint'ами для автономной работы"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_dir: Path):
|
|
15
|
+
self.project_dir = Path(project_dir)
|
|
16
|
+
self.a1_dir = self.project_dir / ".a1"
|
|
17
|
+
self.checkpoint_file = self.a1_dir / "checkpoint.json"
|
|
18
|
+
self.checkpoints_dir = self.a1_dir / "checkpoints"
|
|
19
|
+
|
|
20
|
+
# Создаём структуру если не существует
|
|
21
|
+
self.a1_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
self.checkpoints_dir.mkdir(exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def load(self) -> Dict[str, Any]:
|
|
25
|
+
"""Загрузить текущий checkpoint"""
|
|
26
|
+
if not self.checkpoint_file.exists():
|
|
27
|
+
return self._create_initial()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(self.checkpoint_file, "r") as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, IOError):
|
|
33
|
+
return self._create_initial()
|
|
34
|
+
|
|
35
|
+
def save(self, checkpoint: Dict[str, Any]) -> None:
|
|
36
|
+
"""Сохранить checkpoint"""
|
|
37
|
+
checkpoint["updated_at"] = datetime.now().isoformat()
|
|
38
|
+
|
|
39
|
+
# Сохраняем текущий
|
|
40
|
+
with open(self.checkpoint_file, "w") as f:
|
|
41
|
+
json.dump(checkpoint, f, indent=2, ensure_ascii=False)
|
|
42
|
+
|
|
43
|
+
# Архивируем копию
|
|
44
|
+
session = checkpoint.get("session", 0)
|
|
45
|
+
archive_file = self.checkpoints_dir / f"session_{session:03d}.json"
|
|
46
|
+
with open(archive_file, "w") as f:
|
|
47
|
+
json.dump(checkpoint, f, indent=2, ensure_ascii=False)
|
|
48
|
+
|
|
49
|
+
def _create_initial(self) -> Dict[str, Any]:
|
|
50
|
+
"""Создать начальный checkpoint"""
|
|
51
|
+
return {
|
|
52
|
+
"status": "STARTING",
|
|
53
|
+
"session": 0,
|
|
54
|
+
"current_task": None,
|
|
55
|
+
"context_percent": 0,
|
|
56
|
+
"files_modified": [],
|
|
57
|
+
"decisions": [],
|
|
58
|
+
"next_steps": [],
|
|
59
|
+
"last_action": None,
|
|
60
|
+
"created_at": datetime.now().isoformat(),
|
|
61
|
+
"updated_at": datetime.now().isoformat(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def start_session(self) -> Dict[str, Any]:
|
|
65
|
+
"""Начать новую сессию"""
|
|
66
|
+
checkpoint = self.load()
|
|
67
|
+
checkpoint["session"] += 1
|
|
68
|
+
checkpoint["status"] = "WORKING"
|
|
69
|
+
checkpoint["session_started_at"] = datetime.now().isoformat()
|
|
70
|
+
self.save(checkpoint)
|
|
71
|
+
return checkpoint
|
|
72
|
+
|
|
73
|
+
def end_session(
|
|
74
|
+
self,
|
|
75
|
+
current_task: Optional[str] = None,
|
|
76
|
+
files_modified: Optional[List[str]] = None,
|
|
77
|
+
decisions: Optional[List[str]] = None,
|
|
78
|
+
next_steps: Optional[List[str]] = None,
|
|
79
|
+
last_action: Optional[str] = None,
|
|
80
|
+
context_percent: int = 0,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Завершить сессию с сохранением состояния"""
|
|
83
|
+
checkpoint = self.load()
|
|
84
|
+
|
|
85
|
+
if current_task:
|
|
86
|
+
checkpoint["current_task"] = current_task
|
|
87
|
+
if files_modified:
|
|
88
|
+
# Добавляем к существующим, убираем дубли
|
|
89
|
+
existing = set(checkpoint.get("files_modified", []))
|
|
90
|
+
existing.update(files_modified)
|
|
91
|
+
checkpoint["files_modified"] = list(existing)
|
|
92
|
+
if decisions:
|
|
93
|
+
checkpoint["decisions"].extend(decisions)
|
|
94
|
+
checkpoint["decisions"] = checkpoint["decisions"][-20:]
|
|
95
|
+
if next_steps:
|
|
96
|
+
checkpoint["next_steps"] = next_steps
|
|
97
|
+
if last_action:
|
|
98
|
+
checkpoint["last_action"] = last_action
|
|
99
|
+
|
|
100
|
+
checkpoint["context_percent"] = context_percent
|
|
101
|
+
checkpoint["session_ended_at"] = datetime.now().isoformat()
|
|
102
|
+
|
|
103
|
+
self.save(checkpoint)
|
|
104
|
+
|
|
105
|
+
def mark_completed(self) -> None:
|
|
106
|
+
"""Отметить всю работу как завершённую"""
|
|
107
|
+
checkpoint = self.load()
|
|
108
|
+
checkpoint["status"] = "COMPLETED"
|
|
109
|
+
checkpoint["completed_at"] = datetime.now().isoformat()
|
|
110
|
+
self.save(checkpoint)
|
|
111
|
+
|
|
112
|
+
def is_completed(self) -> bool:
|
|
113
|
+
"""Проверить завершена ли работа"""
|
|
114
|
+
checkpoint = self.load()
|
|
115
|
+
return checkpoint.get("status") == "COMPLETED"
|
|
116
|
+
|
|
117
|
+
def get_session_number(self) -> int:
|
|
118
|
+
"""Получить номер текущей сессии"""
|
|
119
|
+
checkpoint = self.load()
|
|
120
|
+
return checkpoint.get("session", 0)
|
|
121
|
+
|
|
122
|
+
def get_summary(self) -> str:
|
|
123
|
+
"""Получить текстовое резюме для промпта"""
|
|
124
|
+
cp = self.load()
|
|
125
|
+
|
|
126
|
+
lines = [
|
|
127
|
+
f"## Checkpoint (Session #{cp['session']})",
|
|
128
|
+
f"Status: {cp['status']}",
|
|
129
|
+
f"Current task: {cp.get('current_task', 'None')}",
|
|
130
|
+
f"Last action: {cp.get('last_action', 'None')}",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if cp.get("files_modified"):
|
|
134
|
+
lines.append(f"Files modified: {', '.join(cp['files_modified'][-10:])}")
|
|
135
|
+
|
|
136
|
+
if cp.get("decisions"):
|
|
137
|
+
lines.append("Recent decisions:")
|
|
138
|
+
for d in cp["decisions"][-5:]:
|
|
139
|
+
lines.append(f" - {d}")
|
|
140
|
+
|
|
141
|
+
if cp.get("next_steps"):
|
|
142
|
+
lines.append("Next steps:")
|
|
143
|
+
for s in cp["next_steps"]:
|
|
144
|
+
lines.append(f" - {s}")
|
|
145
|
+
|
|
146
|
+
return "\n".join(lines)
|
a1/cli.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PocketCoder-A1 CLI — Autonomous Coding Agent
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __codename__, __version__
|
|
11
|
+
from .checkpoint import CheckpointManager
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .dashboard import run_dashboard
|
|
14
|
+
from .loop import SessionLoop
|
|
15
|
+
from .tasks import TaskManager
|
|
16
|
+
from .validator import Validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cmd_init(args):
|
|
20
|
+
"""Initialize A1 in project directory"""
|
|
21
|
+
project_dir = Path(args.dir if hasattr(args, 'dir') else args.project).resolve()
|
|
22
|
+
|
|
23
|
+
if not project_dir.exists():
|
|
24
|
+
print(f"[ERROR] Directory not found: {project_dir}")
|
|
25
|
+
return 1
|
|
26
|
+
|
|
27
|
+
# Создаём структуру
|
|
28
|
+
a1_dir = project_dir / ".a1"
|
|
29
|
+
a1_dir.mkdir(exist_ok=True)
|
|
30
|
+
(a1_dir / "sessions").mkdir(exist_ok=True)
|
|
31
|
+
(a1_dir / "checkpoints").mkdir(exist_ok=True)
|
|
32
|
+
|
|
33
|
+
# Инициализируем менеджеры
|
|
34
|
+
CheckpointManager(project_dir)
|
|
35
|
+
TaskManager(project_dir)
|
|
36
|
+
|
|
37
|
+
print(f"[OK] A1 initialized in {project_dir}")
|
|
38
|
+
print(f" Created: {a1_dir}")
|
|
39
|
+
print()
|
|
40
|
+
print("Next steps:")
|
|
41
|
+
print(" pca think 'your task idea' # Add tasks")
|
|
42
|
+
print(" pca tasks # View tasks")
|
|
43
|
+
print(" pca start # Start autonomous work")
|
|
44
|
+
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_think(args):
|
|
49
|
+
"""Add a raw thought"""
|
|
50
|
+
project_dir = Path(args.project).resolve()
|
|
51
|
+
tasks = TaskManager(project_dir)
|
|
52
|
+
|
|
53
|
+
thought = " ".join(args.thought)
|
|
54
|
+
tasks.add_raw_thought(thought)
|
|
55
|
+
|
|
56
|
+
print(f"[THOUGHT] Added: {thought}")
|
|
57
|
+
print()
|
|
58
|
+
print("Use 'pca transform' to convert thoughts to tasks")
|
|
59
|
+
print("Or 'pca task add \"...\"' to add a specific task")
|
|
60
|
+
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_task_add(args):
|
|
65
|
+
"""Add a specific task"""
|
|
66
|
+
project_dir = Path(args.project).resolve()
|
|
67
|
+
tasks = TaskManager(project_dir)
|
|
68
|
+
|
|
69
|
+
title = " ".join(args.title)
|
|
70
|
+
task = tasks.add_task(title, description=args.description or "")
|
|
71
|
+
|
|
72
|
+
print(f"[OK] Added task: [{task.id}] {task.title}")
|
|
73
|
+
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_tasks(args):
|
|
78
|
+
"""Show all tasks"""
|
|
79
|
+
project_dir = Path(args.project).resolve()
|
|
80
|
+
tasks = TaskManager(project_dir)
|
|
81
|
+
|
|
82
|
+
print(tasks.get_summary())
|
|
83
|
+
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_start(args):
|
|
88
|
+
"""Start autonomous work"""
|
|
89
|
+
project_dir = Path(args.project).resolve()
|
|
90
|
+
|
|
91
|
+
# Проверяем что A1 инициализирован
|
|
92
|
+
if not (project_dir / ".a1").exists():
|
|
93
|
+
print(f"[ERROR] A1 not initialized. Run: pca init {project_dir}")
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
# Проверяем есть ли задачи
|
|
97
|
+
tasks = TaskManager(project_dir)
|
|
98
|
+
pending = tasks.get_tasks(status="pending")
|
|
99
|
+
in_progress = tasks.get_tasks(status="in_progress")
|
|
100
|
+
|
|
101
|
+
if not pending and not in_progress:
|
|
102
|
+
print("[ERROR] No tasks to work on!")
|
|
103
|
+
print(" Add tasks with: pca think 'idea' or pca task add 'task'")
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
# Load config and merge with CLI args
|
|
107
|
+
config = Config(project_dir)
|
|
108
|
+
resolved = config.resolve(cli_args={
|
|
109
|
+
"provider": args.provider if args.provider != "claude-max" else None,
|
|
110
|
+
"model": getattr(args, "model", None),
|
|
111
|
+
"api_key": getattr(args, "api_key", None),
|
|
112
|
+
"ollama_host": getattr(args, "ollama_host", None),
|
|
113
|
+
"ollama_model": getattr(args, "ollama_model", None),
|
|
114
|
+
"max_sessions": args.max_sessions if args.max_sessions != 100 else None,
|
|
115
|
+
"max_turns": getattr(args, "max_turns", None),
|
|
116
|
+
"session_delay": getattr(args, "session_delay", None),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
loop = SessionLoop(project_dir=project_dir, **resolved)
|
|
120
|
+
loop.start()
|
|
121
|
+
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_status(args):
|
|
126
|
+
"""Show current status"""
|
|
127
|
+
project_dir = Path(args.project).resolve()
|
|
128
|
+
|
|
129
|
+
checkpoint = CheckpointManager(project_dir)
|
|
130
|
+
tasks = TaskManager(project_dir)
|
|
131
|
+
validator = Validator(project_dir)
|
|
132
|
+
|
|
133
|
+
print("=" * 50)
|
|
134
|
+
print("AUTONOMOUS GNOME STATUS")
|
|
135
|
+
print("=" * 50)
|
|
136
|
+
print()
|
|
137
|
+
print(checkpoint.get_summary())
|
|
138
|
+
print()
|
|
139
|
+
print(tasks.get_summary())
|
|
140
|
+
|
|
141
|
+
if args.validate:
|
|
142
|
+
print()
|
|
143
|
+
print(validator.get_summary())
|
|
144
|
+
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_validate(args):
|
|
149
|
+
"""Run validation checks"""
|
|
150
|
+
project_dir = Path(args.project).resolve()
|
|
151
|
+
validator = Validator(project_dir)
|
|
152
|
+
|
|
153
|
+
print(validator.get_summary())
|
|
154
|
+
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_dashboard(args):
|
|
159
|
+
"""Launch web dashboard"""
|
|
160
|
+
project_dir = Path(args.project).resolve()
|
|
161
|
+
run_dashboard(project_dir, port=args.port, open_browser=not args.no_browser)
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_test(args):
|
|
166
|
+
"""Run E2E tests with real Playwright browser"""
|
|
167
|
+
project_dir = Path(args.project).resolve()
|
|
168
|
+
|
|
169
|
+
from .tester.runner import VisionTester
|
|
170
|
+
|
|
171
|
+
tester = VisionTester(
|
|
172
|
+
project_dir=project_dir,
|
|
173
|
+
base_url=f"http://localhost:{args.port}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if args.scenario:
|
|
177
|
+
report = tester.run_one(args.scenario)
|
|
178
|
+
else:
|
|
179
|
+
report = tester.run_all()
|
|
180
|
+
|
|
181
|
+
return 0 if report.failed == 0 and report.errors == 0 else 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_log(args):
|
|
185
|
+
"""Show session log"""
|
|
186
|
+
project_dir = Path(args.project).resolve()
|
|
187
|
+
checkpoints_dir = project_dir / ".a1" / "checkpoints"
|
|
188
|
+
|
|
189
|
+
if not checkpoints_dir.exists():
|
|
190
|
+
print("No sessions yet.")
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
sessions = sorted(checkpoints_dir.glob("session_*.json"))
|
|
194
|
+
|
|
195
|
+
if args.session:
|
|
196
|
+
# Показать конкретную сессию
|
|
197
|
+
session_file = checkpoints_dir / f"session_{args.session:03d}.json"
|
|
198
|
+
if session_file.exists():
|
|
199
|
+
print(session_file.read_text())
|
|
200
|
+
else:
|
|
201
|
+
print(f"Session {args.session} not found")
|
|
202
|
+
else:
|
|
203
|
+
# Показать список сессий
|
|
204
|
+
print(f"Sessions: {len(sessions)}")
|
|
205
|
+
for s in sessions[-10:]: # Последние 10
|
|
206
|
+
print(f" - {s.name}")
|
|
207
|
+
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_config(args):
|
|
212
|
+
"""View or edit configuration"""
|
|
213
|
+
project_dir = Path(args.project).resolve()
|
|
214
|
+
config = Config(project_dir)
|
|
215
|
+
|
|
216
|
+
if args.reset:
|
|
217
|
+
config.reset()
|
|
218
|
+
print("[OK] Config reset to defaults")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
if not args.key:
|
|
222
|
+
# Show all config
|
|
223
|
+
from .config import DEFAULTS
|
|
224
|
+
data = config.get_all()
|
|
225
|
+
print("=" * 50)
|
|
226
|
+
print("CONFIGURATION")
|
|
227
|
+
print("=" * 50)
|
|
228
|
+
print(f" File: {config.path}")
|
|
229
|
+
print()
|
|
230
|
+
for key in DEFAULTS:
|
|
231
|
+
val = data.get(key)
|
|
232
|
+
# Mask API key
|
|
233
|
+
if key == "api_key" and val:
|
|
234
|
+
val = config.mask_api_key(val)
|
|
235
|
+
default = DEFAULTS[key]
|
|
236
|
+
marker = "" if val == default else " (custom)"
|
|
237
|
+
print(f" {key}: {val}{marker}")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
if args.value is None:
|
|
241
|
+
# Show single value
|
|
242
|
+
val = config.get(args.key)
|
|
243
|
+
if args.key == "api_key" and val:
|
|
244
|
+
val = config.mask_api_key(val)
|
|
245
|
+
print(f"{args.key}: {val}")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
# Set value (auto-convert types)
|
|
249
|
+
value = args.value
|
|
250
|
+
if value.lower() in ("true", "false"):
|
|
251
|
+
value = value.lower() == "true"
|
|
252
|
+
elif value == "null" or value == "none":
|
|
253
|
+
value = None
|
|
254
|
+
else:
|
|
255
|
+
try:
|
|
256
|
+
value = int(value)
|
|
257
|
+
except ValueError:
|
|
258
|
+
try:
|
|
259
|
+
value = float(value)
|
|
260
|
+
except ValueError:
|
|
261
|
+
pass # keep as string
|
|
262
|
+
|
|
263
|
+
config.set(args.key, value)
|
|
264
|
+
display = config.mask_api_key(value) if args.key == "api_key" and value else value
|
|
265
|
+
print(f"[OK] {args.key} = {display}")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def main():
|
|
270
|
+
parser = argparse.ArgumentParser(
|
|
271
|
+
prog="pca",
|
|
272
|
+
description=f"PocketCoder-A1 v{__version__} ({__codename__})",
|
|
273
|
+
)
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"-p", "--project", default=".", help="Project directory (default: current)"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
282
|
+
|
|
283
|
+
# init
|
|
284
|
+
p_init = subparsers.add_parser("init", help="Initialize A1 in project")
|
|
285
|
+
p_init.add_argument("dir", nargs="?", default=".", help="Project directory")
|
|
286
|
+
p_init.set_defaults(func=cmd_init)
|
|
287
|
+
|
|
288
|
+
# think
|
|
289
|
+
p_think = subparsers.add_parser("think", help="Add a raw thought")
|
|
290
|
+
p_think.add_argument("thought", nargs="+", help="Your thought")
|
|
291
|
+
p_think.set_defaults(func=cmd_think)
|
|
292
|
+
|
|
293
|
+
# task add
|
|
294
|
+
p_task = subparsers.add_parser("task", help="Task management")
|
|
295
|
+
task_sub = p_task.add_subparsers(dest="task_cmd")
|
|
296
|
+
|
|
297
|
+
p_task_add = task_sub.add_parser("add", help="Add a task")
|
|
298
|
+
p_task_add.add_argument("title", nargs="+", help="Task title")
|
|
299
|
+
p_task_add.add_argument("-d", "--description", help="Task description")
|
|
300
|
+
p_task_add.set_defaults(func=cmd_task_add)
|
|
301
|
+
|
|
302
|
+
# tasks
|
|
303
|
+
p_tasks = subparsers.add_parser("tasks", help="Show all tasks")
|
|
304
|
+
p_tasks.set_defaults(func=cmd_tasks)
|
|
305
|
+
|
|
306
|
+
# start
|
|
307
|
+
p_start = subparsers.add_parser("start", help="Start autonomous work")
|
|
308
|
+
p_start.add_argument(
|
|
309
|
+
"--provider",
|
|
310
|
+
default="claude-max",
|
|
311
|
+
choices=["claude-max", "claude-api", "ollama"],
|
|
312
|
+
help="AI provider",
|
|
313
|
+
)
|
|
314
|
+
p_start.add_argument(
|
|
315
|
+
"--max-sessions", type=int, default=100, help="Max sessions"
|
|
316
|
+
)
|
|
317
|
+
p_start.add_argument("--model", help="Model name (provider-specific)")
|
|
318
|
+
p_start.add_argument("--api-key", help="Anthropic API key (or env ANTHROPIC_API_KEY)")
|
|
319
|
+
p_start.add_argument("--ollama-host", help="Ollama host URL")
|
|
320
|
+
p_start.add_argument("--ollama-model", help="Ollama model name")
|
|
321
|
+
p_start.add_argument("--max-turns", type=int, help="Max turns per session (default: 25)")
|
|
322
|
+
p_start.add_argument("--session-delay", type=int, help="Delay between sessions in seconds")
|
|
323
|
+
p_start.set_defaults(func=cmd_start)
|
|
324
|
+
|
|
325
|
+
# status
|
|
326
|
+
p_status = subparsers.add_parser("status", help="Show status")
|
|
327
|
+
p_status.add_argument("-v", "--validate", action="store_true", help="Run validation")
|
|
328
|
+
p_status.set_defaults(func=cmd_status)
|
|
329
|
+
|
|
330
|
+
# validate
|
|
331
|
+
p_validate = subparsers.add_parser("validate", help="Run validation checks")
|
|
332
|
+
p_validate.set_defaults(func=cmd_validate)
|
|
333
|
+
|
|
334
|
+
# log
|
|
335
|
+
p_log = subparsers.add_parser("log", help="Show session log")
|
|
336
|
+
p_log.add_argument("-s", "--session", type=int, help="Session number")
|
|
337
|
+
p_log.set_defaults(func=cmd_log)
|
|
338
|
+
|
|
339
|
+
# test (vision tester)
|
|
340
|
+
p_test = subparsers.add_parser("test", help="Run E2E tests with real Playwright browser")
|
|
341
|
+
p_test.add_argument("-s", "--scenario", type=int, help="Run specific scenario (1-7)")
|
|
342
|
+
p_test.add_argument("--port", type=int, default=7331, help="Dashboard port (default: 7331)")
|
|
343
|
+
p_test.set_defaults(func=cmd_test)
|
|
344
|
+
|
|
345
|
+
# config
|
|
346
|
+
p_config = subparsers.add_parser("config", help="View/edit configuration")
|
|
347
|
+
p_config.add_argument("key", nargs="?", help="Config key to get/set")
|
|
348
|
+
p_config.add_argument("value", nargs="?", help="Value to set")
|
|
349
|
+
p_config.add_argument("--reset", action="store_true", help="Reset to defaults")
|
|
350
|
+
p_config.set_defaults(func=cmd_config)
|
|
351
|
+
|
|
352
|
+
# dashboard (web UI)
|
|
353
|
+
p_dash = subparsers.add_parser("ui", help="Launch web dashboard")
|
|
354
|
+
p_dash.add_argument("--port", type=int, default=None, help="Port (default: auto-find from 7331)")
|
|
355
|
+
p_dash.add_argument("--no-browser", action="store_true", help="Don't open browser")
|
|
356
|
+
p_dash.set_defaults(func=cmd_dashboard)
|
|
357
|
+
|
|
358
|
+
args = parser.parse_args()
|
|
359
|
+
|
|
360
|
+
if not args.command:
|
|
361
|
+
parser.print_help()
|
|
362
|
+
return 0
|
|
363
|
+
|
|
364
|
+
return args.func(args)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
sys.exit(main())
|
a1/config.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
a1/config.py — Configuration management for PocketCoder-A1.
|
|
3
|
+
|
|
4
|
+
Loads/saves .a1/config.json with priority resolution:
|
|
5
|
+
CLI args > env vars > config.json > defaults
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
DEFAULTS = {
|
|
14
|
+
"provider": "claude-max",
|
|
15
|
+
"model": None,
|
|
16
|
+
"api_key": None,
|
|
17
|
+
"ollama_host": "http://localhost:11434",
|
|
18
|
+
"ollama_model": "qwen3:30b-a3b",
|
|
19
|
+
"max_sessions": 100,
|
|
20
|
+
"max_turns": 25,
|
|
21
|
+
"session_delay": 5,
|
|
22
|
+
"context_threshold": 0.70,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Env var → config key mapping
|
|
26
|
+
ENV_MAP = {
|
|
27
|
+
"ANTHROPIC_API_KEY": "api_key",
|
|
28
|
+
"OLLAMA_HOST": "ollama_host",
|
|
29
|
+
"OLLAMA_MODEL": "ollama_model",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Config:
|
|
34
|
+
"""Configuration manager — load/save/resolve .a1/config.json."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, project_dir: Path):
|
|
37
|
+
self.project_dir = Path(project_dir)
|
|
38
|
+
self.path = self.project_dir / ".a1" / "config.json"
|
|
39
|
+
self._data: dict = {}
|
|
40
|
+
self.load()
|
|
41
|
+
|
|
42
|
+
def load(self) -> dict:
|
|
43
|
+
"""Load from disk. Returns stored values (not merged with defaults)."""
|
|
44
|
+
if self.path.exists():
|
|
45
|
+
try:
|
|
46
|
+
self._data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
47
|
+
except (json.JSONDecodeError, OSError):
|
|
48
|
+
self._data = {}
|
|
49
|
+
else:
|
|
50
|
+
self._data = {}
|
|
51
|
+
return self._data
|
|
52
|
+
|
|
53
|
+
def save(self) -> None:
|
|
54
|
+
"""Save current data to .a1/config.json."""
|
|
55
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
self.path.write_text(
|
|
57
|
+
json.dumps(self._data, indent=2, ensure_ascii=False) + "\n",
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
62
|
+
"""Get value with fallback: config → DEFAULTS → default."""
|
|
63
|
+
if key in self._data and self._data[key] is not None:
|
|
64
|
+
return self._data[key]
|
|
65
|
+
if key in DEFAULTS and DEFAULTS[key] is not None:
|
|
66
|
+
return DEFAULTS[key]
|
|
67
|
+
return default
|
|
68
|
+
|
|
69
|
+
def set(self, key: str, value: Any) -> None:
|
|
70
|
+
"""Set a value and save to disk."""
|
|
71
|
+
self._data[key] = value
|
|
72
|
+
self.save()
|
|
73
|
+
|
|
74
|
+
def get_all(self) -> dict:
|
|
75
|
+
"""Get full config merged with defaults."""
|
|
76
|
+
result = dict(DEFAULTS)
|
|
77
|
+
result.update({k: v for k, v in self._data.items() if v is not None})
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def reset(self) -> None:
|
|
81
|
+
"""Reset to defaults."""
|
|
82
|
+
self._data = {}
|
|
83
|
+
self.save()
|
|
84
|
+
|
|
85
|
+
def resolve(self, cli_args: Optional[dict] = None) -> dict:
|
|
86
|
+
"""Merge all sources: CLI > env > config > defaults.
|
|
87
|
+
|
|
88
|
+
Returns dict with keys matching SessionLoop.__init__ params:
|
|
89
|
+
provider, model, api_key, ollama_host, ollama_model,
|
|
90
|
+
max_sessions, max_turns, session_delay
|
|
91
|
+
"""
|
|
92
|
+
# 1. Start with defaults
|
|
93
|
+
result = dict(DEFAULTS)
|
|
94
|
+
|
|
95
|
+
# 2. Overlay config.json values
|
|
96
|
+
for k, v in self._data.items():
|
|
97
|
+
if v is not None:
|
|
98
|
+
result[k] = v
|
|
99
|
+
|
|
100
|
+
# 3. Overlay env vars
|
|
101
|
+
for env_var, config_key in ENV_MAP.items():
|
|
102
|
+
val = os.environ.get(env_var)
|
|
103
|
+
if val:
|
|
104
|
+
result[config_key] = val
|
|
105
|
+
|
|
106
|
+
# 4. Overlay CLI args (skip None values — means "not provided")
|
|
107
|
+
if cli_args:
|
|
108
|
+
for k, v in cli_args.items():
|
|
109
|
+
if v is not None:
|
|
110
|
+
result[k] = v
|
|
111
|
+
|
|
112
|
+
# Remove context_threshold from result (it's not a SessionLoop param)
|
|
113
|
+
# Store it separately so loop.py can access if needed
|
|
114
|
+
result.pop("context_threshold", None)
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def mask_api_key(self, key: Optional[str] = None) -> Optional[str]:
|
|
119
|
+
"""Mask API key for display: sk-ant-api03-...xxxx."""
|
|
120
|
+
k = key or self.get("api_key")
|
|
121
|
+
if not k or len(k) < 8:
|
|
122
|
+
return k
|
|
123
|
+
return k[:10] + "..." + k[-4:]
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"Config({self.path}, keys={list(self._data.keys())})"
|