pantheon-opencode 1.4.2 β†’ 1.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.pantheon/code-mode/.gitkeep +0 -0
  2. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
  3. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
  4. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
  5. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
  6. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
  7. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
  8. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
  9. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
  10. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
  11. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
  12. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
  13. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
  14. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
  15. package/.pantheon/code-mode/README.md +93 -0
  16. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  17. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  18. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  19. package/.pantheon/code-mode/compress-inline.py +450 -0
  20. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  21. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  22. package/.pantheon/code-mode/eval-run.py +197 -0
  23. package/.pantheon/code-mode/eval-static.py +321 -0
  24. package/.pantheon/code-mode/example-sync.sh +5 -0
  25. package/.pantheon/code-mode/session-end-save.py +186 -0
  26. package/.pantheon/code-mode/session-end-save.sh +18 -0
  27. package/.pantheon/code-mode/test-allowed.sh +5 -0
  28. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  29. package/.pantheon/code-mode/test-env.sh +7 -0
  30. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  31. package/.pantheon/code-mode/test-timeout.sh +6 -0
  32. package/README.md +158 -1189
  33. package/README.pt-BR.md +222 -0
  34. package/bin/pantheon-init.mjs +30 -11
  35. package/opencode.json +1 -0
  36. package/package.json +14 -3
  37. package/plugin.json +15 -0
  38. package/scripts/_pantheon_paths.py +48 -7
  39. package/scripts/ci-validate-yaml.py +7 -2
  40. package/scripts/code_mode_server.py +33 -2
  41. package/scripts/doctor.mjs +135 -127
  42. package/scripts/generate-preset-docs.mjs +127 -66
  43. package/scripts/generate-prompts.sh +2 -2
  44. package/scripts/install/cli-ui.mjs +8 -1
  45. package/scripts/install/config-migration.mjs +401 -0
  46. package/scripts/install/health-check.mjs +3 -3
  47. package/scripts/install/model-picker.mjs +72 -23
  48. package/scripts/install/opencode-version.mjs +47 -0
  49. package/scripts/install/opencode.mjs +261 -88
  50. package/scripts/install/plugin.mjs +112 -42
  51. package/scripts/install/shared.mjs +13 -158
  52. package/scripts/install-mcp.mjs +44 -232
  53. package/scripts/mcp_persistence_server.py +344 -15
  54. package/scripts/mcp_resources_server.py +144 -40
  55. package/scripts/memory_mcp_server.py +2 -2
  56. package/scripts/recover-zenodo-deposition.mjs +146 -0
  57. package/scripts/sync-tui.mjs +32 -7
  58. package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
  59. package/scripts/uninstall.mjs +298 -954
  60. package/scripts/validate-zenodo-release.mjs +154 -0
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/mcp/_pantheon_paths.py +48 -7
  64. package/src/mcp/code_mode_server.py +33 -2
  65. package/src/mcp/eval_store.py +1 -1
  66. package/src/mcp/mcp_persistence_server.py +344 -15
  67. package/src/mcp/mcp_resources_server.py +144 -40
  68. package/src/mcp/pantheon_vision_server.py +1 -1
  69. package/src/mcp/toon_codec.py +491 -0
  70. package/src/pantheon/background-job-board.ts +67 -1
  71. package/src/pantheon/cost-command.ts +6 -2
  72. package/src/pantheon/delegate-manager.ts +384 -0
  73. package/src/pantheon/delegate-task-adapter.ts +444 -0
  74. package/src/pantheon/delegation-notify.ts +35 -20
  75. package/src/pantheon/model-command.ts +86 -32
  76. package/src/pantheon/token-opt.ts +555 -0
  77. package/src/pantheon/toon-codec.ts +481 -0
  78. package/src/pantheon/tui-version-check.ts +1 -3
  79. package/src/pantheon/v2-bridge.ts +129 -0
  80. package/src/pantheon/v2-events.ts +131 -0
  81. package/src/pantheon/v2-hooks.ts +287 -0
  82. package/src/pantheon/v2-tool-definitions.ts +331 -0
  83. package/src/plugin-v2/index.ts +21 -0
  84. package/src/plugin-v2.ts +836 -0
  85. package/src/plugin.ts +57 -10
  86. package/src/plugins/tui/dist/server.d.ts +1 -1
  87. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  88. package/src/plugins/tui/dist/server.js +1 -1
  89. package/src/plugins/tui/dist/server.js.map +1 -1
  90. package/src/plugins/tui/dist/tui.d.ts +10 -7
  91. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  92. package/src/plugins/tui/dist/tui.js +11 -8
  93. package/src/plugins/tui/dist/tui.js.map +1 -1
  94. package/src/plugins/tui/dist/tui.tsx +22 -16
  95. package/src/plugins/tui/package-lock.json +51 -51
  96. package/src/plugins/tui/package.json +7 -7
  97. package/src/plugins/tui/src/index.tsx +22 -16
  98. package/src/skills/README.md +0 -2
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Checkpoint Session Manager for Auto-Continue V2.
4
+
5
+ Structured persistence for session checkpoints, heartbeat tracking, and
6
+ session metadata for long autonomous deepwork sessions.
7
+
8
+ Usage:
9
+ python checkpoint_session.py <command> <slug> [options]
10
+
11
+ Commands:
12
+ init β€” Initialize session files (session.json + heartbeat.json)
13
+ save β€” Save checkpoint and update heartbeat
14
+ status β€” Show current session status
15
+ resume β€” Read latest checkpoint for resume
16
+ list β€” List all checkpoints for slug
17
+ health β€” Validate session consistency
18
+ archive β€” Move session to archive/
19
+ cleanup β€” Clean orphaned sessions and temp files (--dry-run for preview)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import sys
27
+ import shutil
28
+ from datetime import UTC, datetime, timedelta
29
+ from pathlib import Path
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Constants
33
+ # ---------------------------------------------------------------------------
34
+
35
+ SCHEMA_VERSION = 2
36
+ DEEPDIR = Path(".pantheon/deepwork")
37
+ ARCHIVEDIR = DEEPDIR / "archive"
38
+ STALE_THRESHOLD = timedelta(hours=24)
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Path helpers
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ def get_slug_dir(slug: str) -> Path:
47
+ """Return the deepwork directory path for a given slug."""
48
+ return DEEPDIR / slug
49
+
50
+
51
+ def get_checkpoints(slug_dir: Path) -> list[tuple[int, Path]]:
52
+ """Return sorted list of (number, path) for all checkpoint files.
53
+
54
+ Only files matching checkpoint-N.json where *N* is a positive
55
+ integer are considered. Dotfiles and non-numeric suffixes are
56
+ ignored.
57
+ """
58
+ checkpoints: list[tuple[int, Path]] = []
59
+ for f in slug_dir.glob("checkpoint-*.json"):
60
+ stem = f.stem # e.g. "checkpoint-3"
61
+ parts = stem.split("-", 1)
62
+ if len(parts) != 2: # noqa: PLR2004
63
+ continue
64
+ try:
65
+ n = int(parts[1])
66
+ except (ValueError, IndexError):
67
+ continue
68
+ checkpoints.append((n, f))
69
+ checkpoints.sort(key=lambda x: x[0])
70
+ return checkpoints
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Commands
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ def cmd_init(slug: str) -> None:
79
+ """Create session.json and heartbeat.json for *slug*."""
80
+ sdir = get_slug_dir(slug)
81
+ sdir.mkdir(parents=True, exist_ok=True)
82
+
83
+ now = datetime.now(UTC).isoformat()
84
+
85
+ session = {
86
+ "task_id": slug,
87
+ "mode": "deepwork",
88
+ "start_time": now,
89
+ "last_activity": now,
90
+ "status": "in_progress",
91
+ "gate_history": [],
92
+ "stop_reason": None,
93
+ "stopped_at": None,
94
+ }
95
+ _write_json(sdir / "session.json", session)
96
+
97
+ heartbeat = {
98
+ "slug": slug,
99
+ "last_action": now,
100
+ "turn_count": 0,
101
+ "current_phase": 0,
102
+ "status": "alive",
103
+ "next_action": "initialized",
104
+ }
105
+ _write_json(sdir / "heartbeat.json", heartbeat)
106
+
107
+ print(f"Session initialized for {slug}")
108
+
109
+
110
+ def cmd_save(slug: str) -> None:
111
+ """Save a checkpoint and update heartbeat / session metadata."""
112
+ sdir = get_slug_dir(slug)
113
+ if not sdir.exists():
114
+ print(f"No session found for {slug}")
115
+ return
116
+
117
+ now = datetime.now(UTC).isoformat()
118
+
119
+ # Read existing STATUS.md for context hash
120
+ context_hash = _compute_context_hash(sdir)
121
+
122
+ # Load or create session metadata
123
+ session_file = sdir / "session.json"
124
+ if session_file.exists():
125
+ session = _read_json(session_file)
126
+ else:
127
+ session = {
128
+ "task_id": slug,
129
+ "mode": "deepwork",
130
+ "start_time": now,
131
+ "last_activity": now,
132
+ "status": "in_progress",
133
+ "gate_history": [],
134
+ "stop_reason": None,
135
+ "stopped_at": None,
136
+ }
137
+
138
+ turn_count = session.get("turn_count", 0) + 1
139
+ current_phase = session.get("current_phase", 0)
140
+
141
+ # Update session
142
+ session["last_activity"] = now
143
+ session["turn_count"] = turn_count
144
+ _write_json(session_file, session)
145
+
146
+ # Update heartbeat
147
+ heartbeat = {
148
+ "slug": slug,
149
+ "last_action": now,
150
+ "turn_count": turn_count,
151
+ "current_phase": current_phase,
152
+ "status": "alive",
153
+ "next_action": "checkpoint saved",
154
+ }
155
+ _write_json(sdir / "heartbeat.json", heartbeat)
156
+
157
+ # Determine next checkpoint number
158
+ checkpoints = get_checkpoints(sdir)
159
+ n = (checkpoints[-1][0] + 1) if checkpoints else 1
160
+
161
+ # Build checkpoint
162
+ checkpoint = {
163
+ "slug": slug,
164
+ "phase": current_phase,
165
+ "turn_count": turn_count,
166
+ "timestamp": now,
167
+ "context_hash": context_hash,
168
+ "version": SCHEMA_VERSION,
169
+ }
170
+ _write_json(sdir / f"checkpoint-{n}.json", checkpoint)
171
+
172
+ print(f"Checkpoint {n} saved for {slug}")
173
+
174
+
175
+ def cmd_status(slug: str) -> None:
176
+ """Print human-readable session status."""
177
+ sdir = get_slug_dir(slug)
178
+ if not sdir.exists():
179
+ print(f"No session found for {slug}")
180
+ return
181
+
182
+ print(f"=== Session: {slug} ===\n")
183
+
184
+ session_file = sdir / "session.json"
185
+ if session_file.exists():
186
+ session = _read_json(session_file)
187
+ print(f"Status: {session.get('status', 'unknown')}")
188
+ print(f"Started: {session.get('start_time', 'unknown')}")
189
+ print(f"Last Activity: {session.get('last_activity', 'unknown')}")
190
+ gates = session.get("gate_history", [])
191
+ print(f"Gates Passed: {len(gates)}")
192
+ print(f"Turn Count: {session.get('turn_count', 0)}")
193
+ print(f"Phase: {session.get('current_phase', 0)}")
194
+ else:
195
+ print("No session.json found")
196
+
197
+ hb_file = sdir / "heartbeat.json"
198
+ if hb_file.exists():
199
+ hb = _read_json(hb_file)
200
+ print(f"Agent Status: {hb.get('status', 'unknown')}")
201
+ print(f"Next Action: {hb.get('next_action', 'unknown')}")
202
+
203
+ checkpoints = get_checkpoints(sdir)
204
+ print(f"\nCheckpoints: {len(checkpoints)}")
205
+ for n, _ in checkpoints[-5:]: # show last 5
206
+ print(f" checkpoint-{n}.json")
207
+
208
+
209
+ def cmd_resume(slug: str) -> None:
210
+ """Print the latest checkpoint as JSON (for programmatic consumption)."""
211
+ sdir = get_slug_dir(slug)
212
+ checkpoints = get_checkpoints(sdir)
213
+ if not checkpoints:
214
+ print(f"No checkpoints found for {slug}")
215
+ return
216
+
217
+ _, path = checkpoints[-1]
218
+ cp = _read_json(path)
219
+ print(json.dumps(cp, indent=2))
220
+
221
+
222
+ def cmd_list(slug: str) -> None:
223
+ """List all checkpoints for *slug*."""
224
+ sdir = get_slug_dir(slug)
225
+ checkpoints = get_checkpoints(sdir)
226
+ if not checkpoints:
227
+ print(f"No checkpoints found for {slug}")
228
+ return
229
+
230
+ print(f"Checkpoints for {slug}:")
231
+ for n, path in checkpoints:
232
+ cp = _read_json(path)
233
+ ts = cp.get("timestamp", "unknown")
234
+ phase = cp.get("phase", "?")
235
+ turns = cp.get("turn_count", "?")
236
+ print(f" [{n}] phase={phase} turns={turns} @ {ts}")
237
+
238
+
239
+ def cmd_health(slug: str) -> None:
240
+ """Validate session consistency."""
241
+ sdir = get_slug_dir(slug)
242
+ issues = []
243
+
244
+ if not sdir.exists():
245
+ print(f"❌ Session not found: {slug}")
246
+ return
247
+
248
+ session: dict = {}
249
+ hb: dict = {}
250
+
251
+ # Check session.json
252
+ session_file = sdir / "session.json"
253
+ if not session_file.exists():
254
+ issues.append("Missing session.json")
255
+ else:
256
+ try:
257
+ session = _read_json(session_file)
258
+ required = ["task_id", "status", "start_time", "last_activity"]
259
+ for field in required:
260
+ if field not in session:
261
+ issues.append(f"session.json missing field: {field}")
262
+ except (json.JSONDecodeError, ValueError):
263
+ issues.append("session.json is corrupted (invalid JSON)")
264
+
265
+ # Check heartbeat.json
266
+ hb_file = sdir / "heartbeat.json"
267
+ if not hb_file.exists():
268
+ issues.append("Missing heartbeat.json")
269
+ else:
270
+ try:
271
+ hb = _read_json(hb_file)
272
+ if hb.get("status") == "stalled":
273
+ issues.append("Session is marked as STALLED")
274
+ except (json.JSONDecodeError, ValueError):
275
+ issues.append("heartbeat.json is corrupted (invalid JSON)")
276
+
277
+ # Check checkpoint integrity
278
+ checkpoints = get_checkpoints(sdir)
279
+ if checkpoints:
280
+ for n, path in checkpoints:
281
+ try:
282
+ cp = _read_json(path)
283
+ if cp.get("version", 1) > SCHEMA_VERSION:
284
+ issues.append(f"checkpoint-{n}.json has newer schema version")
285
+ if "slug" not in cp or "timestamp" not in cp:
286
+ issues.append(f"checkpoint-{n}.json missing required fields")
287
+ except (json.JSONDecodeError, ValueError):
288
+ issues.append(f"checkpoint-{n}.json is corrupted")
289
+
290
+ # Check sequential numbering
291
+ numbers = [n for n, _ in checkpoints]
292
+ expected = list(range(numbers[0], numbers[-1] + 1))
293
+ if numbers != expected:
294
+ missing = set(expected) - set(numbers)
295
+ issues.append(f"Missing checkpoints: {sorted(missing)}")
296
+
297
+ # Check for stale activity
298
+ if session_file.exists() and hb_file.exists() and session:
299
+ try:
300
+ last_action = session.get("last_activity", "")
301
+ if last_action:
302
+ last_time = datetime.fromisoformat(last_action)
303
+ if datetime.now(UTC) - last_time > STALE_THRESHOLD:
304
+ issues.append(f"No activity for > {STALE_THRESHOLD.total_seconds() / 3600:.0f}h (stale)")
305
+ except (ValueError, TypeError):
306
+ issues.append("Could not parse last_activity timestamp")
307
+
308
+ # Report
309
+ if not issues:
310
+ print(f"βœ… Session '{slug}' is healthy")
311
+ print(f" Checkpoints: {len(checkpoints)}, Status: {session.get('status', 'unknown')}")
312
+ else:
313
+ print(f"⚠️ Session '{slug}' has {len(issues)} issue(s):")
314
+ for issue in issues:
315
+ print(f" - {issue}")
316
+
317
+
318
+ def cmd_archive(slug: str) -> None:
319
+ """Move session from deepwork/<slug>/ to deepwork/archive/<slug>/."""
320
+ sdir = get_slug_dir(slug)
321
+ if not sdir.exists():
322
+ print(f"Session not found: {slug}")
323
+ return
324
+
325
+ archive_dir = ARCHIVEDIR / slug
326
+ if archive_dir.exists():
327
+ print(f"Session '{slug}' already exists in archive. Remove it first or use a different slug.")
328
+ return
329
+
330
+ archive_dir.parent.mkdir(parents=True, exist_ok=True)
331
+ shutil.move(str(sdir), str(archive_dir))
332
+ print(f"πŸ“¦ Session '{slug}' archived to .pantheon/deepwork/archive/{slug}/")
333
+
334
+
335
+ def cmd_cleanup(dry_run: bool = False) -> None:
336
+ """Clean orphaned sessions and temp files.
337
+
338
+ Scans deepwork/ for:
339
+ - Empty directories (no session.json)
340
+ - Sessions with stale heartbeats (> 24h no activity)
341
+ - Temp files (.tmp left from atomic writes)
342
+ """
343
+ if not DEEPDIR.exists():
344
+ print("No deepwork directory found.")
345
+ return
346
+
347
+ now = datetime.now(UTC)
348
+ removed_dirs = 0
349
+ removed_files = 0
350
+
351
+ for item in sorted(DEEPDIR.iterdir()):
352
+ if not item.is_dir() or item.name == "archive" or item.name.startswith("."):
353
+ continue
354
+
355
+ session_file = item / "session.json"
356
+ hb_file = item / "heartbeat.json"
357
+
358
+ # Check if it's an orphan (no session.json)
359
+ if not session_file.exists():
360
+ if dry_run:
361
+ print(f" [dry-run] Would remove orphan directory: {item.name}/")
362
+ else:
363
+ shutil.rmtree(item)
364
+ removed_dirs += 1
365
+ print(f" Removed orphan directory: {item.name}/")
366
+ continue
367
+
368
+ # Check for stale sessions
369
+ if hb_file.exists():
370
+ try:
371
+ hb = _read_json(hb_file)
372
+ last_action = hb.get("last_action", "")
373
+ if last_action:
374
+ last_time = datetime.fromisoformat(last_action)
375
+ if now - last_time > STALE_THRESHOLD:
376
+ if dry_run:
377
+ print(f" [dry-run] Would flag stale session: {item.name}/ (last action: {last_action})")
378
+ else:
379
+ print(f" ⏸️ Session '{item.name}' stale since {last_action}")
380
+ except (ValueError, json.JSONDecodeError):
381
+ pass
382
+
383
+ # Clean temp files
384
+ for tmp_file in item.glob("*.tmp"):
385
+ if dry_run:
386
+ print(f" [dry-run] Would remove temp file: {tmp_file.name}")
387
+ else:
388
+ tmp_file.unlink()
389
+ removed_files += 1
390
+ print(f" Removed temp file: {item.name}/{tmp_file.name}")
391
+
392
+ if not dry_run:
393
+ total = removed_dirs + removed_files
394
+ if total == 0:
395
+ print("βœ… Nothing to clean.")
396
+ else:
397
+ print(f"βœ… Cleaned {removed_dirs} dir(s) and {removed_files} file(s).")
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # Internal helpers
402
+ # ---------------------------------------------------------------------------
403
+
404
+
405
+ def _write_json(path: Path, data: dict) -> None:
406
+ """Atomically write *data* to *path* as JSON."""
407
+ tmp = path.with_suffix(".tmp")
408
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False))
409
+ tmp.replace(path)
410
+
411
+
412
+ def _read_json(path: Path) -> dict:
413
+ """Read and return JSON content from *path*."""
414
+ return json.loads(path.read_text(encoding="utf-8"))
415
+
416
+
417
+ def _compute_context_hash(sdir: Path) -> str:
418
+ """Return a short SHA-256 hex digest of STATUS.md (if it exists)."""
419
+ status_file = sdir / "STATUS.md"
420
+ if status_file.exists():
421
+ return hashlib.sha256(status_file.read_bytes()).hexdigest()[:8]
422
+ return ""
423
+
424
+
425
+ # ---------------------------------------------------------------------------
426
+ # CLI entry-point
427
+ # ---------------------------------------------------------------------------
428
+
429
+ COMMANDS = {
430
+ "init": cmd_init,
431
+ "save": cmd_save,
432
+ "status": cmd_status,
433
+ "resume": cmd_resume,
434
+ "list": cmd_list,
435
+ "health": cmd_health,
436
+ "archive": cmd_archive,
437
+ "cleanup": cmd_cleanup,
438
+ }
439
+
440
+
441
+ def main(argv: list[str] | None = None) -> None:
442
+ """Parse CLI arguments and dispatch the requested command."""
443
+ if argv is None:
444
+ argv = sys.argv[1:]
445
+
446
+ if len(argv) < 1:
447
+ print(
448
+ "Usage: python checkpoint_session.py <command> [slug] [options]\n"
449
+ f"Commands: {', '.join(sorted(COMMANDS))}\n"
450
+ "Note: 'cleanup' does not require a slug (use --dry-run for preview).",
451
+ file=sys.stderr,
452
+ )
453
+ sys.exit(1)
454
+
455
+ command = argv[0]
456
+
457
+ # Special handling for cleanup (no slug required)
458
+ if command == "cleanup":
459
+ dry_run = "--dry-run" in argv[1:]
460
+ cmd_cleanup(dry_run=dry_run)
461
+ return
462
+
463
+ # All other commands require a slug
464
+ if len(argv) < 2: # noqa: PLR2004
465
+ print(f"Usage: python checkpoint_session.py {command} <slug>", file=sys.stderr)
466
+ sys.exit(1)
467
+
468
+ slug = argv[1]
469
+
470
+ if command not in COMMANDS:
471
+ print(
472
+ f"Unknown command: {command!r}. Available: {', '.join(sorted(COMMANDS))}",
473
+ file=sys.stderr,
474
+ )
475
+ sys.exit(1)
476
+
477
+ COMMANDS[command](slug)
478
+
479
+
480
+ if __name__ == "__main__":
481
+ main()