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 ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ PocketCoder-A1: Autonomous Coding Agent
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __codename__ = "Autonomous Gnome"
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())})"