aicodinggym-cli 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.
@@ -0,0 +1,3 @@
1
+ """AI Coding Gym CLI."""
2
+
3
+ __version__ = "0.1.0"
aicodinggym/api.py ADDED
@@ -0,0 +1,132 @@
1
+ """HTTP API client for the AI Coding Gym backend at aicodinggym.com."""
2
+
3
+ import requests
4
+
5
+ API_BASE = "https://aicodinggym.com/api"
6
+ TIMEOUT = 30
7
+
8
+
9
+ class APIError(Exception):
10
+ """Raised when an API call fails."""
11
+ pass
12
+
13
+
14
+ def _post(endpoint: str, payload: dict, timeout: int = TIMEOUT) -> dict:
15
+ """Make a POST request to the API and return parsed JSON."""
16
+ url = f"{API_BASE}/{endpoint}"
17
+ try:
18
+ resp = requests.post(url, json=payload, timeout=timeout)
19
+ resp.raise_for_status()
20
+ return resp.json()
21
+ except requests.ConnectionError:
22
+ raise APIError(
23
+ f"Cannot connect to {API_BASE}.\n"
24
+ "Check your internet connection and try again."
25
+ )
26
+ except requests.Timeout:
27
+ raise APIError(f"Request to {url} timed out after {timeout}s.")
28
+ except requests.HTTPError as e:
29
+ body = ""
30
+ try:
31
+ body = e.response.json().get("detail", e.response.text)
32
+ except Exception:
33
+ body = e.response.text
34
+ raise APIError(f"API error (HTTP {e.response.status_code}): {body}")
35
+ except requests.RequestException as e:
36
+ raise APIError(f"Request failed: {e}")
37
+
38
+
39
+ def _get(endpoint: str, timeout: int = TIMEOUT, stream: bool = False) -> requests.Response:
40
+ """Make a GET request to the API and return the raw response."""
41
+ url = f"{API_BASE}/{endpoint}"
42
+ try:
43
+ resp = requests.get(url, timeout=timeout, stream=stream)
44
+ resp.raise_for_status()
45
+ return resp
46
+ except requests.ConnectionError:
47
+ raise APIError(
48
+ f"Cannot connect to {API_BASE}.\n"
49
+ "Check your internet connection and try again."
50
+ )
51
+ except requests.Timeout:
52
+ raise APIError(f"Request to {url} timed out after {timeout}s.")
53
+ except requests.HTTPError as e:
54
+ body = ""
55
+ try:
56
+ body = e.response.json().get("detail", e.response.text)
57
+ except Exception:
58
+ body = e.response.text
59
+ raise APIError(f"API error (HTTP {e.response.status_code}): {body}")
60
+ except requests.RequestException as e:
61
+ raise APIError(f"Request failed: {e}")
62
+
63
+
64
+ def configure(user_id: str, public_key: str) -> dict:
65
+ """Register public key with server. Returns {'repo_name': ...}."""
66
+ return _post("configure", {"user_id": user_id, "public_key": public_key})
67
+
68
+
69
+ def fetch_problem(user_id: str, problem_id: str) -> dict:
70
+ """Fetch problem info. Returns {'branch_name': ..., 'repo_url': ..., 'message': ...}."""
71
+ return _post("fetch-problem", {"user_id": user_id, "problem_id": problem_id})
72
+
73
+
74
+ def submit_notification(problem_id: str, user_id: str, commit_hash: str,
75
+ branch: str, commit_message: str, timestamp: str) -> dict:
76
+ """Notify backend of a submission."""
77
+ return _post("submissions", {
78
+ "problem_id": problem_id,
79
+ "user_id": user_id,
80
+ "commit_hash": commit_hash,
81
+ "branch": branch,
82
+ "commit_message": commit_message,
83
+ "timestamp": timestamp,
84
+ })
85
+
86
+
87
+ def mlebench_download_info(user_id: str, competition_id: str, dest_path: str) -> None:
88
+ """Download dataset for an MLE-bench competition directly to dest_path."""
89
+ resp = _get(f"competitions/{competition_id}/download", stream=True)
90
+ with open(dest_path, "wb") as f:
91
+ for chunk in resp.iter_content(chunk_size=8192):
92
+ f.write(chunk)
93
+
94
+
95
+ def mlebench_download_file(url: str, dest_path: str, timeout: int = 300) -> None:
96
+ """Download a file from the given URL to dest_path with progress."""
97
+ try:
98
+ resp = requests.get(url, stream=True, timeout=timeout)
99
+ resp.raise_for_status()
100
+ with open(dest_path, "wb") as f:
101
+ for chunk in resp.iter_content(chunk_size=8192):
102
+ f.write(chunk)
103
+ except requests.RequestException as e:
104
+ raise APIError(f"Download failed: {e}")
105
+
106
+
107
+ def mlebench_submit_csv(user_id: str, competition_id: str, csv_path: str) -> dict:
108
+ """Upload a prediction CSV for an MLE-bench competition."""
109
+ try:
110
+ with open(csv_path, "rb") as f:
111
+ resp = requests.post(
112
+ f"{API_BASE}/competitions/{competition_id}/submit",
113
+ data={"user_id": user_id, "competition_id": competition_id},
114
+ files={"file": (f.name, f, "text/csv")},
115
+ timeout=60,
116
+ )
117
+ resp.raise_for_status()
118
+ return resp.json()
119
+ except requests.ConnectionError:
120
+ raise APIError(
121
+ f"Cannot connect to {API_BASE}.\n"
122
+ "Check your internet connection and try again."
123
+ )
124
+ except requests.HTTPError as e:
125
+ body = ""
126
+ try:
127
+ body = e.response.json().get("detail", e.response.text)
128
+ except Exception:
129
+ body = e.response.text
130
+ raise APIError(f"API error (HTTP {e.response.status_code}): {body}")
131
+ except requests.RequestException as e:
132
+ raise APIError(f"Request failed: {e}")
aicodinggym/cli.py ADDED
@@ -0,0 +1,641 @@
1
+ """AI Coding Gym CLI - main entry point.
2
+
3
+ A command-line tool for the AI Coding Gym platform (https://aicodinggym.com).
4
+ Supports SWE-bench and MLE-bench challenges.
5
+
6
+ SETUP (required before any other command):
7
+ aicodinggym configure --user-id YOUR_USER_ID
8
+
9
+ SWE-BENCH WORKFLOW:
10
+ aicodinggym swe fetch django__django-10097
11
+ # ... edit code to fix the issue ...
12
+ aicodinggym swe submit django__django-10097
13
+
14
+ MLE-BENCH WORKFLOW:
15
+ aicodinggym mle download spaceship-titanic
16
+ # ... train model, generate predictions ...
17
+ aicodinggym mle submit spaceship-titanic -F submission.csv
18
+ """
19
+
20
+ import sys
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+
24
+ import click
25
+
26
+ from . import __version__
27
+ from .api import (
28
+ APIError,
29
+ configure as api_configure,
30
+ fetch_problem as api_fetch_problem,
31
+ mlebench_download_file,
32
+ mlebench_download_info,
33
+ mlebench_submit_csv,
34
+ submit_notification,
35
+ )
36
+ from .config import (
37
+ load_config,
38
+ load_credentials,
39
+ save_config,
40
+ save_credentials,
41
+ )
42
+ from .git_ops import (
43
+ add_commit_push,
44
+ clone_repo,
45
+ generate_ssh_key_pair,
46
+ reset_to_setup_commit,
47
+ )
48
+
49
+
50
+ def _error(msg: str) -> None:
51
+ """Print an error message to stderr and exit."""
52
+ click.echo(f"Error: {msg}", err=True)
53
+ sys.exit(1)
54
+
55
+
56
+ def _warn(msg: str) -> None:
57
+ """Print a warning message to stderr."""
58
+ click.echo(f"Warning: {msg}", err=True)
59
+
60
+
61
+ def _resolve_user_id(config: dict, user_id: str | None) -> str:
62
+ """Resolve user_id from argument or config, with helpful error."""
63
+ if user_id:
64
+ return user_id
65
+ uid = config.get("user_id")
66
+ if not uid:
67
+ _error(
68
+ "User ID is not configured.\n\n"
69
+ "Run 'aicodinggym configure --user-id YOUR_USER_ID' first.\n"
70
+ "This generates an SSH key and registers it with aicodinggym.com."
71
+ )
72
+ return uid
73
+
74
+
75
+ def _resolve_workspace(config: dict, workspace_dir: str | None) -> Path:
76
+ """Resolve workspace directory from argument or config."""
77
+ if workspace_dir:
78
+ return Path(workspace_dir).resolve()
79
+ configured = config.get("workspace_dir")
80
+ if configured:
81
+ return Path(configured).resolve()
82
+ return Path.cwd().resolve()
83
+
84
+
85
+ def _resolve_key_path(config: dict, creds: dict | None = None) -> Path:
86
+ """Resolve SSH private key path from credentials or config."""
87
+ path_str = None
88
+ if creds:
89
+ path_str = creds.get("private_key_path")
90
+ if not path_str:
91
+ path_str = config.get("private_key_path")
92
+ if not path_str:
93
+ _error(
94
+ "SSH key path not found.\n\n"
95
+ "Run 'aicodinggym configure --user-id YOUR_USER_ID' to generate a key.\n"
96
+ "If you previously configured, your config may be corrupted.\n"
97
+ "Config location: ~/.aicodinggym/config.json"
98
+ )
99
+ key_path = Path(path_str)
100
+ if not key_path.exists():
101
+ _error(
102
+ f"SSH key file not found at: {key_path}\n\n"
103
+ "Run 'aicodinggym configure --user-id YOUR_USER_ID' to regenerate.\n"
104
+ "This will create a new SSH key pair and register it with the server."
105
+ )
106
+ return key_path
107
+
108
+
109
+ # ── Top-level group ──────────────────────────────────────────────────────────
110
+
111
+
112
+ @click.group(
113
+ epilog=(
114
+ "\b\n"
115
+ "SETUP (run once before using other commands):\n"
116
+ " aicodinggym configure --user-id YOUR_USER_ID\n"
117
+ " (user_id is required — get yours at https://aicodinggym.com)\n\n"
118
+ "\b\n"
119
+ "EXAMPLES:\n"
120
+ " aicodinggym swe fetch django__django-10097\n"
121
+ " aicodinggym swe submit django__django-10097 --message 'Fix auth bug'\n"
122
+ " aicodinggym mle download spaceship-titanic\n"
123
+ " aicodinggym mle submit spaceship-titanic -F predictions.csv\n\n"
124
+ "\b\n"
125
+ "WEBSITE:\n"
126
+ " https://aicodinggym.com\n"
127
+ ),
128
+ )
129
+ @click.version_option(__version__, prog_name="aicodinggym")
130
+ def main():
131
+ """AI Coding Gym CLI.
132
+
133
+ A command-line interface for the AI Coding Gym platform
134
+ (https://aicodinggym.com). Provides tools to fetch coding problems,
135
+ download datasets, and submit solutions.
136
+
137
+ Designed for use by both humans and LLM/AI agents.
138
+
139
+ \b
140
+ QUICK START:
141
+ 1. Configure: aicodinggym configure --user-id YOUR_USER_ID
142
+ 2. Fetch: aicodinggym swe fetch PROBLEM_ID
143
+ 3. Solve: (edit code in the cloned repository)
144
+ 4. Submit: aicodinggym swe submit PROBLEM_ID
145
+ """
146
+ pass
147
+
148
+
149
+ # ── configure ────────────────────────────────────────────────────────────────
150
+
151
+
152
+ @main.command()
153
+ @click.option(
154
+ "--user-id", required=True,
155
+ help="Your AI Coding Gym user ID. Get one at https://aicodinggym.com.",
156
+ )
157
+ @click.option(
158
+ "--workspace-dir", default=None, type=click.Path(),
159
+ help="Default workspace directory for cloning repositories. "
160
+ "Defaults to the current working directory.",
161
+ )
162
+ def configure(user_id: str, workspace_dir: str | None):
163
+ """Configure credentials and register SSH key with aicodinggym.com.
164
+
165
+ Generates an SSH key pair locally (stored in ~/.aicodinggym/),
166
+ sends the public key to the server, and saves your configuration.
167
+
168
+ \b
169
+ This command must be run once before using any other commands.
170
+ If you've already configured, running again will reuse your existing key.
171
+
172
+ \b
173
+ WHAT IT DOES:
174
+ 1. Generates SSH key pair in ~/.aicodinggym/
175
+ 2. Registers your public key with the AI Coding Gym server
176
+ 3. Receives your assigned repository name
177
+ 4. Saves all settings to ~/.aicodinggym/config.json
178
+
179
+ \b
180
+ EXAMPLE:
181
+ aicodinggym configure --user-id alice123
182
+ aicodinggym configure --user-id alice123 --workspace-dir ~/gym-workspace
183
+ """
184
+ try:
185
+ click.echo(f"Generating SSH key for user '{user_id}'...")
186
+ private_key_path, public_key = generate_ssh_key_pair(user_id)
187
+
188
+ click.echo("Registering public key with aicodinggym.com...")
189
+ try:
190
+ data = api_configure(user_id, public_key)
191
+ repo_name = data.get("repo_name")
192
+ if not repo_name:
193
+ _error("Server did not return a repository name. Please try again or contact support.")
194
+ except APIError as e:
195
+ if "409" in str(e):
196
+ click.echo("Key already registered, reusing existing configuration.")
197
+ existing = load_config()
198
+ repo_name = existing.get("repo_name", f"submission-{user_id}")
199
+ else:
200
+ raise
201
+
202
+ resolved_workspace = str(Path(workspace_dir).resolve()) if workspace_dir else str(Path.cwd().resolve())
203
+
204
+ config = {
205
+ "user_id": user_id,
206
+ "repo_name": repo_name,
207
+ "private_key_path": str(private_key_path),
208
+ "workspace_dir": resolved_workspace,
209
+ }
210
+ save_config(config)
211
+
212
+ click.echo(
213
+ f"\nConfiguration saved successfully!\n"
214
+ f"\n"
215
+ f" User ID: {user_id}\n"
216
+ f" Repository: {repo_name}\n"
217
+ f" Workspace: {resolved_workspace}\n"
218
+ f" SSH Key: {private_key_path}\n"
219
+ f" Config: ~/.aicodinggym/config.json\n"
220
+ f"\n"
221
+ f"You can now use 'aicodinggym swe' and 'aicodinggym mle' commands."
222
+ )
223
+ except APIError as e:
224
+ _error(str(e))
225
+ except Exception as e:
226
+ _error(f"Configuration failed: {e}")
227
+
228
+
229
+ # ── swe group ────────────────────────────────────────────────────────────────
230
+
231
+
232
+ @main.group()
233
+ def swe():
234
+ """SWE-bench coding challenges - fetch, solve, and submit bug fixes.
235
+
236
+ \b
237
+ PREREQUISITE:
238
+ Run 'aicodinggym configure --user-id YOUR_USER_ID' before using these commands.
239
+
240
+ \b
241
+ WORKFLOW:
242
+ 1. aicodinggym swe fetch PROBLEM_ID # Clone the problem repo
243
+ 2. (edit code to fix the issue) # Work on your solution
244
+ 3. aicodinggym swe submit PROBLEM_ID # Submit your fix
245
+ 4. aicodinggym swe reset PROBLEM_ID # (optional) Start over
246
+
247
+ \b
248
+ PROBLEM IDS:
249
+ Problem IDs follow the format: <project>__<repo>-<number>
250
+ Examples: django__django-10097, sympy__sympy-13043, scikit-learn__scikit-learn-11578
251
+ """
252
+ pass
253
+
254
+
255
+ @swe.command("fetch")
256
+ @click.argument("problem_id")
257
+ @click.option("--user-id", default=None, help="Override configured user ID.")
258
+ @click.option(
259
+ "--workspace-dir", default=None, type=click.Path(),
260
+ help="Directory to clone into. Overrides configured workspace.",
261
+ )
262
+ def swe_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
263
+ """Fetch a SWE-bench problem and clone its repository locally.
264
+
265
+ Contacts the AI Coding Gym server to set up the problem branch,
266
+ then clones the repository into your workspace directory.
267
+
268
+ \b
269
+ PREREQUISITE:
270
+ You must run 'aicodinggym configure --user-id YOUR_USER_ID' first.
271
+ If you haven't configured yet, this command will fail with instructions.
272
+
273
+ \b
274
+ ARGUMENTS:
275
+ PROBLEM_ID The unique problem identifier (e.g., 'django__django-10097').
276
+ Get problem IDs from https://aicodinggym.com.
277
+
278
+ \b
279
+ WHAT IT DOES:
280
+ 1. Requests the problem branch from the server
281
+ 2. Clones the repository (shallow clone for efficiency)
282
+ 3. Sets up your local workspace at <workspace>/<problem_id>/
283
+
284
+ \b
285
+ EXAMPLE:
286
+ aicodinggym swe fetch django__django-10097
287
+ aicodinggym swe fetch django__django-10097 --workspace-dir ~/projects
288
+ """
289
+ config = load_config()
290
+ uid = _resolve_user_id(config, user_id)
291
+ workspace = _resolve_workspace(config, workspace_dir)
292
+ key_path = _resolve_key_path(config)
293
+
294
+ try:
295
+ click.echo(f"Fetching problem '{problem_id}' from server...")
296
+ data = api_fetch_problem(uid, problem_id)
297
+ except APIError as e:
298
+ _error(str(e))
299
+
300
+ branch = data.get("branch_name", problem_id)
301
+ repo_url = data.get("repo_url")
302
+ server_msg = data.get("message", "")
303
+
304
+ if not repo_url:
305
+ _error("Server did not return a repository URL. The problem may not exist.")
306
+
307
+ # Save credentials for later submit
308
+ credentials = load_credentials()
309
+ credentials[problem_id] = {
310
+ "repo_url": repo_url,
311
+ "branch": branch,
312
+ "user_id": uid,
313
+ "private_key_path": str(key_path),
314
+ "workspace_dir": str(workspace),
315
+ "benchmark": "swe",
316
+ }
317
+ save_credentials(credentials)
318
+
319
+ workspace.mkdir(parents=True, exist_ok=True)
320
+
321
+ click.echo(f"Cloning branch '{branch}' into {workspace / problem_id}...")
322
+ success, msg = clone_repo(repo_url, branch, problem_id, str(workspace), key_path)
323
+
324
+ if not success:
325
+ _error(msg)
326
+
327
+ click.echo(
328
+ f"\nSuccessfully fetched problem: {problem_id}\n"
329
+ f"\n"
330
+ f" {msg}\n"
331
+ )
332
+ if server_msg:
333
+ click.echo(f" Server: {server_msg}\n")
334
+ click.echo("You can now start working on the solution!")
335
+
336
+
337
+ @swe.command("submit")
338
+ @click.argument("problem_id")
339
+ @click.option("--user-id", default=None, help="Override configured user ID.")
340
+ @click.option(
341
+ "--message", "-m", default=None,
342
+ help="Commit message. Auto-generated if not provided.",
343
+ )
344
+ @click.option(
345
+ "--force", is_flag=True, default=False,
346
+ help="Force push (--force-with-lease). Use with caution.",
347
+ )
348
+ @click.option(
349
+ "--workspace-dir", default=None, type=click.Path(),
350
+ help="Workspace directory. Overrides configured/cached value.",
351
+ )
352
+ def swe_submit(problem_id: str, user_id: str | None, message: str | None,
353
+ force: bool, workspace_dir: str | None):
354
+ """Submit your SWE-bench solution by committing and pushing changes.
355
+
356
+ Stages all changes, commits them, pushes to the remote, and notifies
357
+ the AI Coding Gym server that your submission is ready for evaluation.
358
+
359
+ \b
360
+ PREREQUISITE:
361
+ You must run 'aicodinggym swe fetch PROBLEM_ID' first.
362
+ The fetch command sets up the repository and caches credentials
363
+ needed for submission.
364
+
365
+ \b
366
+ ARGUMENTS:
367
+ PROBLEM_ID The problem identifier you fetched earlier.
368
+
369
+ \b
370
+ WHAT IT DOES:
371
+ 1. Stages all changed files (git add)
372
+ 2. Commits with your message (or auto-generated one)
373
+ 3. Pushes to the remote branch
374
+ 4. Notifies the backend for evaluation
375
+
376
+ \b
377
+ EXAMPLE:
378
+ aicodinggym swe submit django__django-10097
379
+ aicodinggym swe submit django__django-10097 -m "Fix auth validation bug"
380
+ aicodinggym swe submit django__django-10097 --force
381
+ """
382
+ config = load_config()
383
+ uid = _resolve_user_id(config, user_id)
384
+
385
+ credentials = load_credentials()
386
+ if problem_id not in credentials:
387
+ _error(
388
+ f"No credentials found for '{problem_id}'.\n\n"
389
+ f"You must fetch the problem first:\n"
390
+ f" aicodinggym swe fetch {problem_id}\n\n"
391
+ f"This clones the repository and saves the credentials needed for submission."
392
+ )
393
+
394
+ creds = credentials[problem_id]
395
+
396
+ if creds.get("user_id") and creds["user_id"] != uid:
397
+ _error(
398
+ f"User ID mismatch. Problem was fetched by '{creds['user_id']}', not '{uid}'.\n"
399
+ f"Either use --user-id {creds['user_id']} or re-fetch the problem."
400
+ )
401
+
402
+ workspace = _resolve_workspace(config, workspace_dir or creds.get("workspace_dir"))
403
+ problem_dir = workspace / problem_id
404
+
405
+ if not problem_dir.exists():
406
+ _error(
407
+ f"Problem directory not found at: {problem_dir}\n\n"
408
+ f"You must fetch the problem first:\n"
409
+ f" aicodinggym swe fetch {problem_id}\n\n"
410
+ f"Or specify the correct workspace with --workspace-dir."
411
+ )
412
+
413
+ key_path = _resolve_key_path(config, creds)
414
+ branch = creds["branch"]
415
+ commit_msg = message or f"Solution submission for {problem_id} at {datetime.now().isoformat()}"
416
+
417
+ click.echo(f"Submitting solution for '{problem_id}'...")
418
+ success, msg, commit_hash = add_commit_push(str(problem_dir), branch, key_path, commit_msg, force)
419
+
420
+ if not success:
421
+ _error(msg)
422
+
423
+ # Notify backend
424
+ try:
425
+ submit_notification(
426
+ problem_id=problem_id,
427
+ user_id=uid,
428
+ commit_hash=commit_hash,
429
+ branch=branch,
430
+ commit_message=commit_msg,
431
+ timestamp=datetime.now().isoformat(),
432
+ )
433
+ except APIError as e:
434
+ _warn(f"Changes pushed, but failed to notify backend: {e}")
435
+
436
+ click.echo(
437
+ f"\nSuccessfully submitted solution for {problem_id}\n"
438
+ f"\n"
439
+ f" Commit: {commit_hash[:8]}\n"
440
+ f" Branch: {branch}\n"
441
+ f" Status: Pushed and backend notified\n"
442
+ f"\n"
443
+ f"Your solution has been submitted for evaluation!"
444
+ )
445
+
446
+
447
+ @swe.command("reset")
448
+ @click.argument("problem_id")
449
+ @click.option("--user-id", default=None, help="Override configured user ID.")
450
+ @click.option(
451
+ "--workspace-dir", default=None, type=click.Path(),
452
+ help="Workspace directory. Overrides configured/cached value.",
453
+ )
454
+ def swe_reset(problem_id: str, user_id: str | None, workspace_dir: str | None):
455
+ """Reset a SWE-bench problem to its original state.
456
+
457
+ Discards all local changes and resets the repository back to the
458
+ original setup commit. Use this to start over on a problem.
459
+
460
+ \b
461
+ WARNING: This is destructive. All your local changes will be lost.
462
+
463
+ \b
464
+ PREREQUISITE:
465
+ You must run 'aicodinggym swe fetch PROBLEM_ID' first.
466
+
467
+ \b
468
+ ARGUMENTS:
469
+ PROBLEM_ID The problem identifier to reset.
470
+
471
+ \b
472
+ WHAT IT DOES:
473
+ 1. Finds the original 'Setup SWE-bench instance:' commit
474
+ 2. Runs git reset --hard to that commit
475
+ 3. Removes untracked files (git clean -fd)
476
+
477
+ \b
478
+ EXAMPLE:
479
+ aicodinggym swe reset django__django-10097
480
+ """
481
+ config = load_config()
482
+ uid = _resolve_user_id(config, user_id)
483
+
484
+ credentials = load_credentials()
485
+ if problem_id not in credentials:
486
+ _error(
487
+ f"No credentials found for '{problem_id}'.\n\n"
488
+ f"You must fetch the problem first:\n"
489
+ f" aicodinggym swe fetch {problem_id}"
490
+ )
491
+
492
+ creds = credentials[problem_id]
493
+
494
+ if creds.get("user_id") and creds["user_id"] != uid:
495
+ _error(f"User ID mismatch. Problem was fetched by '{creds['user_id']}', not '{uid}'.")
496
+
497
+ workspace = _resolve_workspace(config, workspace_dir or creds.get("workspace_dir"))
498
+ problem_dir = workspace / problem_id
499
+
500
+ if not problem_dir.exists():
501
+ _error(f"Problem directory not found at: {problem_dir}")
502
+
503
+ click.echo(f"Resetting '{problem_id}' to original state...")
504
+ success, msg = reset_to_setup_commit(str(problem_dir))
505
+
506
+ if not success:
507
+ _error(msg)
508
+
509
+ click.echo(f"\n{msg}")
510
+
511
+
512
+ # ── mle group ────────────────────────────────────────────────────────────────
513
+
514
+
515
+ @main.group()
516
+ def mle():
517
+ """MLE-bench ML competitions - download data and submit predictions.
518
+
519
+ \b
520
+ PREREQUISITE:
521
+ Run 'aicodinggym configure --user-id YOUR_USER_ID' before using these commands.
522
+
523
+ \b
524
+ WORKFLOW:
525
+ 1. aicodinggym mle download COMPETITION_ID # Download dataset
526
+ 2. (train your model and generate predictions) # Work on your solution
527
+ 3. aicodinggym mle submit COMPETITION_ID -F submission.csv # Submit predictions
528
+
529
+ \b
530
+ COMPETITION IDS:
531
+ Examples: spaceship-titanic, house-prices, digit-recognizer
532
+ Browse competitions at https://aicodinggym.com
533
+ """
534
+ pass
535
+
536
+
537
+ @mle.command("download")
538
+ @click.argument("competition_id")
539
+ @click.option("--user-id", default=None, help="Override configured user ID.")
540
+ @click.option(
541
+ "--workspace-dir", default=None, type=click.Path(),
542
+ help="Workspace directory. Defaults to configured workspace.",
543
+ )
544
+ def mle_download(competition_id: str, user_id: str | None, workspace_dir: str | None):
545
+ """Download dataset files for an MLE-bench competition.
546
+
547
+ \b
548
+ EXAMPLE:
549
+ aicodinggym mle download spaceship-titanic
550
+ aicodinggym mle download spaceship-titanic --workspace-dir ~/workspace
551
+ """
552
+ config = load_config()
553
+ uid = _resolve_user_id(config, user_id)
554
+
555
+ workspace = _resolve_workspace(config, workspace_dir)
556
+ dest_dir = workspace / competition_id / "data"
557
+ dest_dir.mkdir(parents=True, exist_ok=True)
558
+
559
+ filename = f"{competition_id}.zip"
560
+ dest_path = dest_dir / filename
561
+
562
+ try:
563
+ click.echo(f"Downloading dataset for '{competition_id}'...")
564
+ mlebench_download_info(uid, competition_id, str(dest_path))
565
+ except APIError as e:
566
+ _error(str(e))
567
+
568
+ click.echo(
569
+ f"\nDataset downloaded to: {dest_path}\n"
570
+ f"\nNext step: train your model and submit predictions with:\n"
571
+ f" aicodinggym mle submit {competition_id} -F your_predictions.csv"
572
+ )
573
+
574
+
575
+ @mle.command("submit")
576
+ @click.argument("competition_id")
577
+ @click.option(
578
+ "-F", "csv_path", required=True, type=click.Path(exists=True),
579
+ help="Path to your prediction CSV file (required).",
580
+ )
581
+ @click.option("--user-id", default=None, help="Override configured user ID.")
582
+ @click.option(
583
+ "--message", "-m", default=None,
584
+ help="Description of your submission (optional).",
585
+ )
586
+ def mle_submit(competition_id: str, csv_path: str, user_id: str | None,
587
+ message: str | None):
588
+ """Submit a prediction CSV for an MLE-bench competition.
589
+
590
+ Uploads your prediction CSV directly to the AI Coding Gym server
591
+ for scoring.
592
+
593
+ \b
594
+ PREREQUISITE:
595
+ You must run 'aicodinggym configure --user-id YOUR_USER_ID' first.
596
+
597
+ \b
598
+ ARGUMENTS:
599
+ COMPETITION_ID The competition identifier (e.g., 'spaceship-titanic').
600
+
601
+ \b
602
+ OPTIONS:
603
+ -F FILE Path to your prediction CSV file. This is REQUIRED.
604
+ The file must exist and be a valid CSV matching the
605
+ competition's expected format (see sample_submission.csv).
606
+
607
+ \b
608
+ WHAT IT DOES:
609
+ 1. Validates that the CSV file exists
610
+ 2. Uploads the CSV to the AI Coding Gym server
611
+ 3. Server scores your predictions and returns results
612
+
613
+ \b
614
+ EXAMPLE:
615
+ aicodinggym mle submit spaceship-titanic -F predictions.csv
616
+ aicodinggym mle submit spaceship-titanic -F ./output/pred.csv -m "XGBoost v2"
617
+ """
618
+ config = load_config()
619
+ uid = _resolve_user_id(config, user_id)
620
+
621
+ csv_src = Path(csv_path).resolve()
622
+
623
+ click.echo(f"Uploading {csv_src.name} for '{competition_id}'...")
624
+
625
+ try:
626
+ result = mlebench_submit_csv(uid, competition_id, str(csv_src))
627
+ except APIError as e:
628
+ _error(str(e))
629
+
630
+ score_msg = result.get("message", "Submission received for scoring.")
631
+ score = result.get("score")
632
+
633
+ click.echo(
634
+ f"\nSuccessfully submitted prediction for {competition_id}\n"
635
+ f"\n"
636
+ f" CSV: {csv_src.name}\n"
637
+ f" Status: {score_msg}\n"
638
+ )
639
+ if score is not None:
640
+ click.echo(f" Score: {score}\n")
641
+ click.echo("Your prediction has been submitted for scoring!")
aicodinggym/config.py ADDED
@@ -0,0 +1,80 @@
1
+ """Configuration and credentials management for AI Coding Gym CLI.
2
+
3
+ Stores configuration in ~/.aicodinggym/config.json and per-problem
4
+ credentials in ~/.aicodinggym/credentials.json.
5
+ SSH keys are stored in ~/.aicodinggym/{user_id}_id_rsa.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ CONFIG_DIR = Path.home() / ".aicodinggym"
14
+ CONFIG_PATH = CONFIG_DIR / "config.json"
15
+ CREDENTIALS_PATH = CONFIG_DIR / "credentials.json"
16
+
17
+ # Fields persisted in config.json
18
+ _CONFIG_FIELDS = ("user_id", "repo_name", "private_key_path", "workspace_dir")
19
+
20
+
21
+ def ensure_config_dir() -> Path:
22
+ """Create the config directory with secure permissions if it doesn't exist."""
23
+ CONFIG_DIR.mkdir(mode=0o700, exist_ok=True)
24
+ return CONFIG_DIR
25
+
26
+
27
+ def load_config() -> dict[str, str]:
28
+ """Load global configuration from ~/.aicodinggym/config.json."""
29
+ if not CONFIG_PATH.exists():
30
+ return {}
31
+ try:
32
+ data = json.loads(CONFIG_PATH.read_text())
33
+ if not isinstance(data, dict):
34
+ return {}
35
+ return {k: v for k, v in data.items() if k in _CONFIG_FIELDS and isinstance(v, str) and v}
36
+ except (json.JSONDecodeError, OSError):
37
+ return {}
38
+
39
+
40
+ def save_config(config: dict[str, str]) -> None:
41
+ """Persist global configuration to ~/.aicodinggym/config.json."""
42
+ ensure_config_dir()
43
+ data = {k: v for k, v in config.items() if k in _CONFIG_FIELDS}
44
+ CONFIG_PATH.write_text(json.dumps(data, indent=2) + "\n")
45
+
46
+
47
+ def load_credentials() -> dict[str, dict[str, Any]]:
48
+ """Load per-problem credentials from ~/.aicodinggym/credentials.json."""
49
+ if not CREDENTIALS_PATH.exists():
50
+ return {}
51
+ try:
52
+ data = json.loads(CREDENTIALS_PATH.read_text())
53
+ if not isinstance(data, dict):
54
+ return {}
55
+ return data
56
+ except (json.JSONDecodeError, OSError):
57
+ return {}
58
+
59
+
60
+ def save_credentials(credentials: dict[str, dict[str, Any]]) -> None:
61
+ """Persist per-problem credentials to ~/.aicodinggym/credentials.json."""
62
+ ensure_config_dir()
63
+ CREDENTIALS_PATH.write_text(json.dumps(credentials, indent=2) + "\n")
64
+
65
+
66
+ def require_config(config: dict[str, str], field: str, label: str) -> str:
67
+ """Get a required config field or raise a descriptive error."""
68
+ value = config.get(field)
69
+ if not value:
70
+ raise ConfigError(
71
+ f"{label} is not configured.\n\n"
72
+ f"Run 'aicodinggym configure --user-id YOUR_USER_ID' first to set up your credentials.\n"
73
+ f"This generates an SSH key and registers it with the AI Coding Gym server."
74
+ )
75
+ return value
76
+
77
+
78
+ class ConfigError(Exception):
79
+ """Raised when required configuration is missing."""
80
+ pass
aicodinggym/git_ops.py ADDED
@@ -0,0 +1,175 @@
1
+ """Git and SSH key operations for AI Coding Gym CLI."""
2
+
3
+ import os
4
+ import shlex
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from .config import ensure_config_dir
11
+
12
+
13
+ def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
14
+ """Generate an SSH key pair for the user.
15
+
16
+ First checks ~/.mcp-keys/ for existing keys matching the user_id.
17
+ If found, copies them to ~/.aicodinggym/ and reuses them.
18
+ Otherwise generates new keys in ~/.aicodinggym/{user_id}_id_rsa.
19
+ Returns (private_key_path, public_key_content).
20
+ """
21
+ key_dir = ensure_config_dir()
22
+ key_path = key_dir / f"{user_id}_id_rsa"
23
+
24
+ if not key_path.exists():
25
+ # Check ~/.mcp-keys/ for existing keys matching user_id
26
+ mcp_keys_dir = Path.home() / ".mcp-keys"
27
+ mcp_private = mcp_keys_dir / f"{user_id}_id_rsa"
28
+ mcp_public = mcp_keys_dir / f"{user_id}_id_rsa.pub"
29
+
30
+ if mcp_private.exists() and mcp_public.exists():
31
+ shutil.copy2(mcp_private, key_path)
32
+ shutil.copy2(mcp_public, Path(f"{key_path}.pub"))
33
+ key_path.chmod(0o600)
34
+ else:
35
+ result = subprocess.run(
36
+ ["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path),
37
+ "-N", "", "-C", f"aicodinggym-{user_id}"],
38
+ capture_output=True, text=True,
39
+ )
40
+ if result.returncode != 0:
41
+ raise RuntimeError(f"Failed to generate SSH key: {result.stderr}")
42
+
43
+ pub_key_path = Path(f"{key_path}.pub")
44
+ public_key = pub_key_path.read_text().strip()
45
+ return key_path, public_key
46
+
47
+
48
+ def run_git_command(cmd: str, cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
49
+ """Execute a git command with optional SSH key configuration."""
50
+ env = os.environ.copy()
51
+ if key_path:
52
+ env["GIT_SSH_COMMAND"] = f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
53
+
54
+ return subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, env=env)
55
+
56
+
57
+ def clone_repo(repo_url: str, branch: str, dest_name: str,
58
+ workspace: str, key_path: Path) -> tuple[bool, str]:
59
+ """Clone a repo branch into workspace/dest_name.
60
+
61
+ Returns (success, message).
62
+ """
63
+ problem_dir = Path(workspace) / dest_name
64
+
65
+ if problem_dir.exists():
66
+ # Check if already on the correct branch
67
+ result = run_git_command("git rev-parse --abbrev-ref HEAD", str(problem_dir))
68
+ if result.returncode == 0 and result.stdout.strip() == branch:
69
+ pull = run_git_command(f"git pull origin {branch}", str(problem_dir), key_path)
70
+ if pull.returncode != 0:
71
+ return False, f"Git pull failed:\n{pull.stderr}"
72
+ _remove_github_dir(problem_dir)
73
+ return True, f"Already exists. Updated to latest version.\nRepository: {problem_dir}\nBranch: {branch}"
74
+ return False, (
75
+ f"Directory {problem_dir} already exists with different content.\n"
76
+ "Remove it first or use --workspace-dir to specify a different location."
77
+ )
78
+
79
+ cmd = f"git clone --single-branch --branch {branch} --depth 1 {repo_url} {dest_name}"
80
+ result = run_git_command(cmd, workspace, key_path)
81
+
82
+ if result.returncode != 0:
83
+ return False, f"Git clone failed:\n{result.stderr}\nMake sure the branch '{branch}' exists in the repository."
84
+
85
+ _remove_github_dir(problem_dir)
86
+ return True, f"Cloned to: {problem_dir}\nBranch: {branch}"
87
+
88
+
89
+ def add_commit_push(problem_dir: str, branch: str, key_path: Path,
90
+ message: str, force: bool = False) -> tuple[bool, str, str]:
91
+ """Stage, commit, and push changes.
92
+
93
+ Returns (success, message, commit_hash).
94
+ """
95
+ pdir = Path(problem_dir)
96
+
97
+ # Stage all changes except .github
98
+ result = run_git_command("git add -A -- . ':(exclude).github'", str(pdir))
99
+ if result.returncode != 0:
100
+ return False, f"Git add failed:\n{result.stderr}", ""
101
+
102
+ # Check for staged changes
103
+ status = run_git_command("git diff --cached --name-only", str(pdir))
104
+ if not status.stdout.strip():
105
+ return False, "No changes to commit. Your working directory is clean.", ""
106
+
107
+ # Commit
108
+ safe_msg = message.replace('"', '\\"')
109
+ result = run_git_command(f'git commit -m "{safe_msg}"', str(pdir))
110
+ if result.returncode != 0:
111
+ return False, f"Git commit failed:\n{result.stderr}", ""
112
+
113
+ # Get commit hash
114
+ hash_result = run_git_command("git rev-parse HEAD", str(pdir))
115
+ commit_hash = hash_result.stdout.strip()
116
+
117
+ # Push
118
+ push_flag = "--force-with-lease " if force else ""
119
+ result = run_git_command(f"git push {push_flag}origin {branch}", str(pdir), key_path)
120
+ if result.returncode != 0:
121
+ return False, f"Git push failed:\n{result.stderr}", commit_hash
122
+
123
+ return True, "Committed and pushed successfully.", commit_hash
124
+
125
+
126
+ def reset_to_setup_commit(problem_dir: str) -> tuple[bool, str]:
127
+ """Reset repo to the original 'Setup SWE-bench instance:' commit.
128
+
129
+ Returns (success, message).
130
+ """
131
+ log_result = run_git_command("git log --format=%H:%s --reverse", problem_dir)
132
+ if log_result.returncode != 0:
133
+ return False, f"Git log failed:\n{log_result.stderr}"
134
+
135
+ setup_prefix = "Setup SWE-bench instance:"
136
+ setup_commit = None
137
+ for line in log_result.stdout.splitlines():
138
+ parts = line.split(":", 1)
139
+ if len(parts) != 2:
140
+ continue
141
+ commit_hash, subject = parts[0].strip(), parts[1].strip()
142
+ if subject.startswith(setup_prefix):
143
+ setup_commit = commit_hash
144
+ break
145
+
146
+ if not setup_commit:
147
+ return False, (
148
+ f"Could not find the original setup commit.\n"
149
+ f"Expected a commit message starting with '{setup_prefix}'."
150
+ )
151
+
152
+ reset = run_git_command(f"git reset --hard {setup_commit}", problem_dir)
153
+ if reset.returncode != 0:
154
+ return False, f"Git reset failed:\n{reset.stderr}"
155
+
156
+ clean = run_git_command("git clean -fd", problem_dir)
157
+ if clean.returncode != 0:
158
+ return False, f"Git clean failed:\n{clean.stderr}"
159
+
160
+ return True, f"Reset to setup commit {setup_commit[:8]}.\nLocal changes discarded and untracked files removed."
161
+
162
+
163
+ def _remove_github_dir(problem_dir: Path) -> None:
164
+ """Remove .github directory and mark files as skip-worktree."""
165
+ github_dir = problem_dir / ".github"
166
+ if not github_dir.exists():
167
+ return
168
+
169
+ list_result = run_git_command("git ls-files .github", str(problem_dir))
170
+ if list_result.returncode == 0:
171
+ for rel_path in filter(None, list_result.stdout.splitlines()):
172
+ quoted = shlex.quote(rel_path)
173
+ run_git_command(f"git update-index --skip-worktree -- {quoted}", str(problem_dir))
174
+
175
+ shutil.rmtree(github_dir, ignore_errors=True)
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: aicodinggym-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for AI Coding Gym platform
5
+ Author-email: AICodingGym Team <datasmithlab@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://aicodinggym.com
8
+ Keywords: cli,coding-gym,swebench,mlebench,ai
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: click>=8.0.0
18
+ Requires-Dist: requests>=2.31.0
19
+
20
+ # aicodinggym-cli API Design
21
+
22
+ ## Overview
23
+
24
+ CLI tool for the AI Coding Gym platform (`https://aicodinggym.com`).
25
+ Supports two benchmarks: **SWE-bench** (code bug fixes) and **MLE-bench** (ML competitions).
26
+
27
+ **Install:** `pip install aicodinggym-cli`
28
+ **Entry point:** `aicodinggym`
29
+
30
+ ---
31
+
32
+ ## Commands
33
+
34
+ ### `aicodinggym configure`
35
+
36
+ One-time setup. Generates SSH key, registers with server.
37
+
38
+ ```
39
+ aicodinggym configure --user-id USER_ID [--workspace-dir DIR]
40
+ ```
41
+
42
+ | Option | Required | Description |
43
+ |---|---|---|
44
+ | `--user-id` | Yes | Your AI Coding Gym user ID |
45
+ | `--workspace-dir` | No | Default workspace directory (default: cwd) |
46
+
47
+ **Backend API:** `POST /api/configure`
48
+ ```json
49
+ // Request
50
+ {"user_id": "alice123", "public_key": "ssh-rsa AAAA..."}
51
+ // Response
52
+ {"repo_name": "alice123-swebench"}
53
+ ```
54
+
55
+ **Local storage:** `~/.aicodinggym/config.json`, `~/.aicodinggym/{user_id}_id_rsa`
56
+
57
+ ---
58
+
59
+ ### `aicodinggym swe` — SWE-bench Commands
60
+
61
+ #### `aicodinggym swe fetch PROBLEM_ID`
62
+
63
+ Fetch a problem and clone the repo locally (shallow clone).
64
+
65
+ ```
66
+ aicodinggym swe fetch PROBLEM_ID [--user-id ID] [--workspace-dir DIR]
67
+ ```
68
+
69
+ **Backend API:** `POST /api/fetch-problem`
70
+ ```json
71
+ // Request
72
+ {"user_id": "alice123", "problem_id": "django__django-10097"}
73
+ // Response
74
+ {"branch_name": "django__django-10097-alice123", "repo_url": "git@...", "message": "..."}
75
+ ```
76
+
77
+ **Local effect:** Clones `<workspace>/<problem_id>/` via SSH.
78
+
79
+ ---
80
+
81
+ #### `aicodinggym swe submit PROBLEM_ID`
82
+
83
+ Commit all changes and push to remote. Notifies backend.
84
+
85
+ ```
86
+ aicodinggym swe submit PROBLEM_ID [--message MSG] [--force] [--user-id ID] [--workspace-dir DIR]
87
+ ```
88
+
89
+ | Option | Description |
90
+ |---|---|
91
+ | `--message, -m` | Commit message (auto-generated if omitted) |
92
+ | `--force` | Force push with `--force-with-lease` |
93
+
94
+ **Local effect:** `git add -A`, `git commit`, `git push`
95
+
96
+ **Backend API:** `POST /api/submissions`
97
+ ```json
98
+ // Request
99
+ {
100
+ "problem_id": "django__django-10097",
101
+ "user_id": "alice123",
102
+ "commit_hash": "abc123...",
103
+ "branch": "django__django-10097-alice123",
104
+ "commit_message": "Fix auth bug",
105
+ "timestamp": "2026-03-05T10:30:00"
106
+ }
107
+ // Response
108
+ {"status": "success", "message": "Submission received"}
109
+ ```
110
+
111
+ ---
112
+
113
+ #### `aicodinggym swe reset PROBLEM_ID`
114
+
115
+ Reset repo to original setup commit. Destructive — discards all local changes.
116
+
117
+ ```
118
+ aicodinggym swe reset PROBLEM_ID [--user-id ID] [--workspace-dir DIR]
119
+ ```
120
+
121
+ **Local effect:** `git reset --hard <setup_commit>`, `git clean -fd`
122
+
123
+ ---
124
+
125
+ ### `aicodinggym mle` — MLE-bench Commands
126
+
127
+ #### `aicodinggym mle download COMPETITION_ID`
128
+
129
+ Download dataset files (train/test/sample_submission CSVs).
130
+
131
+ ```
132
+ aicodinggym mle download COMPETITION_ID [--user-id ID] [--output-dir DIR]
133
+ ```
134
+
135
+ | Option | Description |
136
+ |---|---|
137
+ | `--output-dir` | Save location (default: `./<competition_id>/data/`) |
138
+
139
+ **Backend API:** `POST /api/mlebench/download`
140
+ ```json
141
+ // Request
142
+ {"user_id": "alice123", "competition_id": "spaceship-titanic"}
143
+ // Response (single archive)
144
+ {"download_url": "https://...", "filename": "spaceship-titanic_data.zip", "message": "..."}
145
+ // Response (multiple files)
146
+ {"files": [{"name": "train.csv", "url": "https://..."}, ...], "message": "..."}
147
+ ```
148
+
149
+ ---
150
+
151
+ #### `aicodinggym mle submit COMPETITION_ID -F FILE`
152
+
153
+ Upload prediction CSV for scoring.
154
+
155
+ ```
156
+ aicodinggym mle submit COMPETITION_ID -F FILE [--user-id ID] [--message MSG]
157
+ ```
158
+
159
+ | Option | Required | Description |
160
+ |---|---|---|
161
+ | `-F` | Yes | Path to prediction CSV file |
162
+ | `--message, -m` | No | Submission description |
163
+
164
+ **Backend API:** `POST /api/mlebench/submit` (multipart form)
165
+ ```
166
+ POST /api/mlebench/submit
167
+ Content-Type: multipart/form-data
168
+
169
+ user_id=alice123
170
+ competition_id=spaceship-titanic
171
+ file=@predictions.csv
172
+ ```
173
+ ```json
174
+ // Response
175
+ {"message": "Submission received", "score": 0.85}
176
+ ```
177
+
178
+ ---
179
+
180
+ ## File Structure
181
+
182
+ ```
183
+ src/aicodinggym/
184
+ ├── __init__.py # Version
185
+ ├── cli.py # Click CLI commands (entry point)
186
+ ├── config.py # Config + credentials persistence (~/.aicodinggym/)
187
+ ├── api.py # HTTP client for aicodinggym.com/api
188
+ └── git_ops.py # SSH key generation, git clone/commit/push/reset
189
+ ```
190
+
191
+ ## Configuration Files
192
+
193
+ | File | Purpose |
194
+ |---|---|
195
+ | `~/.aicodinggym/config.json` | Global config (user_id, repo_name, key path, workspace) |
196
+ | `~/.aicodinggym/credentials.json` | Per-problem credentials (repo_url, branch, cached after fetch) |
197
+ | `~/.aicodinggym/{user_id}_id_rsa` | SSH private key |
198
+ | `~/.aicodinggym/{user_id}_id_rsa.pub` | SSH public key |
199
+
200
+ ## Error Handling
201
+
202
+ All errors print actionable messages guiding the user (or LLM agent) to the correct next step:
203
+
204
+ - **Not configured** → "Run 'aicodinggym configure --user-id YOUR_USER_ID' first."
205
+ - **Not fetched** → "You must fetch the problem first: aicodinggym swe fetch PROBLEM_ID"
206
+ - **API unreachable** → "Cannot connect to https://aicodinggym.com/api. Check your internet connection."
207
+ - **User ID mismatch** → "Problem was fetched by 'X', not 'Y'. Either use --user-id X or re-fetch."
208
+
209
+ ## Backend API Summary
210
+
211
+ | Endpoint | Method | Used By |
212
+ |---|---|---|
213
+ | `/api/configure` | POST | `configure` |
214
+ | `/api/fetch-problem` | POST | `swe fetch` |
215
+ | `/api/submissions` | POST | `swe submit` |
216
+ | `/api/mlebench/download` | POST | `mle download` |
217
+ | `/api/mlebench/submit` | POST | `mle submit` |
@@ -0,0 +1,10 @@
1
+ aicodinggym/__init__.py,sha256=UJpUmTsBbxwk9-wc0hE9Z8AmBjM06zjL5IY_YfHNdeU,48
2
+ aicodinggym/api.py,sha256=Fxf_W8Loz2R4mlCx4UamQJq7JrxNVpwpXrUWSIvrbZ0,4948
3
+ aicodinggym/cli.py,sha256=QX-tBzF0ReIunl2GuJTNk1ZUiNT0u91GPvaqMaPWn8U,21387
4
+ aicodinggym/config.py,sha256=9jlHh7VQnLKEBxnh8OL0KiN5-SMzI4rZ9sl-HN4P8JU,2684
5
+ aicodinggym/git_ops.py,sha256=NVNdpp_X2yKlKabH8honQ5dbA_4h82ZoNV2eKumDCXs,6831
6
+ aicodinggym_cli-0.1.0.dist-info/METADATA,sha256=2ObygkkQ7Lk0e2x5JlWVhA_Oliw8nIlyGKFzB9tkLbE,6011
7
+ aicodinggym_cli-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
8
+ aicodinggym_cli-0.1.0.dist-info/entry_points.txt,sha256=IT2fCKaEBbaBggkEVid2lMBtOsxc_xKDWl55jagRjGs,53
9
+ aicodinggym_cli-0.1.0.dist-info/top_level.txt,sha256=U7rm2fKrhxYDwglsV3qYwok7DiqyO4iTLXyDE-2rDNc,12
10
+ aicodinggym_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aicodinggym = aicodinggym.cli:main
@@ -0,0 +1 @@
1
+ aicodinggym