openrunner-sdk 2.5.0__tar.gz → 2.7.0__tar.gz
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.
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/PKG-INFO +1 -1
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/cli.py +161 -0
- openrunner_sdk-2.7.0/openrunner/install_commands.py +256 -0
- openrunner_sdk-2.7.0/openrunner/session.py +710 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/pyproject.toml +1 -1
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/.gitignore +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/=6.0 +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/=8.1 +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/README.md +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/run.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/conftest.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_cli.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.5.0 → openrunner_sdk-2.7.0}/tests/test_wandb_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrunner-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
|
|
5
5
|
Project-URL: Homepage, https://github.com/jqueguiner/openrunner
|
|
6
6
|
Project-URL: Repository, https://github.com/jqueguiner/openrunner
|
|
@@ -120,7 +120,7 @@ launch.from_run = _launch_from_run # type: ignore[attr-defined]
|
|
|
120
120
|
# openrunner.trace.patch_openai() syntax
|
|
121
121
|
trace.patch_openai = _patch_openai # type: ignore[attr-defined]
|
|
122
122
|
|
|
123
|
-
__version__ = "2.
|
|
123
|
+
__version__ = "2.7.0"
|
|
124
124
|
|
|
125
125
|
logger = logging.getLogger("openrunner")
|
|
126
126
|
|
|
@@ -2362,3 +2362,164 @@ def _builtin_suggest(
|
|
|
2362
2362
|
result[name] = spec
|
|
2363
2363
|
|
|
2364
2364
|
return result if result else None
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
# ---------------------------------------------------------------------------
|
|
2368
|
+
# session commands — AI session capture
|
|
2369
|
+
# ---------------------------------------------------------------------------
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
@main.group()
|
|
2373
|
+
def session() -> None:
|
|
2374
|
+
"""Capture and log AI coding sessions (Claude Code, ChatGPT, Codex, Qwen)."""
|
|
2375
|
+
pass
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
@session.command("setup")
|
|
2379
|
+
def session_setup() -> None:
|
|
2380
|
+
"""Interactive setup: configure API key and target project for session sync."""
|
|
2381
|
+
from openrunner.session import interactive_setup
|
|
2382
|
+
config = interactive_setup()
|
|
2383
|
+
if config:
|
|
2384
|
+
click.echo("Setup complete.")
|
|
2385
|
+
else:
|
|
2386
|
+
click.echo("Setup cancelled.")
|
|
2387
|
+
|
|
2388
|
+
|
|
2389
|
+
@session.command("sync")
|
|
2390
|
+
@click.argument("directory", required=False, type=click.Path(exists=True))
|
|
2391
|
+
@click.option("--hours", "-h", default=24.0, help="Look back N hours (default: 24)")
|
|
2392
|
+
@click.option("--project", "-p", default=None, help="Target project (default: from config)")
|
|
2393
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be synced without uploading")
|
|
2394
|
+
def session_sync(directory: str | None, hours: float, project: str | None, dry_run: bool) -> None:
|
|
2395
|
+
"""Sync AI sessions to OpenRunner.
|
|
2396
|
+
|
|
2397
|
+
If DIRECTORY is given, scan that path for .jsonl/.json session files.
|
|
2398
|
+
Otherwise, scan default locations (~/.claude, ~/.codex, ~/.qwen-code).
|
|
2399
|
+
|
|
2400
|
+
On first run, prompts for API key and project selection.
|
|
2401
|
+
"""
|
|
2402
|
+
from pathlib import Path
|
|
2403
|
+
from openrunner.session import discover_all_sessions, discover_in_directory, sync_all, get_session_config, interactive_setup
|
|
2404
|
+
|
|
2405
|
+
# Check if configured — run setup if not
|
|
2406
|
+
config = get_session_config()
|
|
2407
|
+
if not config.get("api_key"):
|
|
2408
|
+
click.echo("No API key configured. Running first-time setup...")
|
|
2409
|
+
config = interactive_setup()
|
|
2410
|
+
if not config:
|
|
2411
|
+
return
|
|
2412
|
+
|
|
2413
|
+
if directory:
|
|
2414
|
+
sessions = discover_in_directory(Path(directory), since_hours=hours)
|
|
2415
|
+
else:
|
|
2416
|
+
sessions = discover_all_sessions(hours)
|
|
2417
|
+
|
|
2418
|
+
if dry_run or not sessions:
|
|
2419
|
+
if not sessions:
|
|
2420
|
+
click.echo("No sessions found.")
|
|
2421
|
+
return
|
|
2422
|
+
click.echo(f"Found {len(sessions)} session(s):")
|
|
2423
|
+
for s in sessions:
|
|
2424
|
+
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(s["mtime"]))
|
|
2425
|
+
size = s["size"] / 1024
|
|
2426
|
+
click.echo(f" [{s['source']}] {ts} ({size:.0f} KB) {s['path'].name}")
|
|
2427
|
+
if dry_run:
|
|
2428
|
+
return
|
|
2429
|
+
|
|
2430
|
+
synced = sync_all(since_hours=hours, project=project, directory=Path(directory) if directory else None)
|
|
2431
|
+
if synced:
|
|
2432
|
+
click.echo(f"Synced {len(synced)} session(s) to OpenRunner.")
|
|
2433
|
+
for run_id in synced:
|
|
2434
|
+
click.echo(f" -> run {run_id}")
|
|
2435
|
+
else:
|
|
2436
|
+
click.echo("No new sessions to sync.")
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
@session.command("watch")
|
|
2440
|
+
@click.option("--interval", "-i", default=60, help="Check interval in seconds (default: 60)")
|
|
2441
|
+
@click.option("--project", "-p", default=None, help="Target project")
|
|
2442
|
+
def session_watch(interval: int, project: str | None) -> None:
|
|
2443
|
+
"""Watch for new sessions and sync them continuously (daemon mode)."""
|
|
2444
|
+
from openrunner.session import watch
|
|
2445
|
+
click.echo(f"Watching for AI sessions (every {interval}s). Ctrl+C to stop.")
|
|
2446
|
+
try:
|
|
2447
|
+
watch(interval=interval, project=project)
|
|
2448
|
+
except KeyboardInterrupt:
|
|
2449
|
+
click.echo("\nStopped.")
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
@session.command("list")
|
|
2453
|
+
@click.option("--hours", "-h", default=24.0, help="Look back N hours")
|
|
2454
|
+
def session_list(hours: float) -> None:
|
|
2455
|
+
"""List discovered AI sessions."""
|
|
2456
|
+
from openrunner.session import discover_all_sessions, _load_sync_state
|
|
2457
|
+
|
|
2458
|
+
sessions = discover_all_sessions(hours)
|
|
2459
|
+
state = _load_sync_state()
|
|
2460
|
+
|
|
2461
|
+
if not sessions:
|
|
2462
|
+
click.echo("No sessions found.")
|
|
2463
|
+
return
|
|
2464
|
+
|
|
2465
|
+
click.echo(f"{'SOURCE':<14} {'TIME':<18} {'SIZE':<10} {'STATUS'}")
|
|
2466
|
+
click.echo("-" * 60)
|
|
2467
|
+
for s in sessions:
|
|
2468
|
+
from openrunner.session import _session_hash
|
|
2469
|
+
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(s["mtime"]))
|
|
2470
|
+
size = f"{s['size'] / 1024:.0f} KB"
|
|
2471
|
+
h = _session_hash(s["path"])
|
|
2472
|
+
synced_info = state.get("synced", {}).get(h)
|
|
2473
|
+
status = f"synced ({synced_info['run_id']})" if synced_info else "new"
|
|
2474
|
+
click.echo(f" {s['source']:<12} {ts:<18} {size:<10} {status}")
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
@main.command("install")
|
|
2478
|
+
@click.argument("tools", nargs=-1, type=click.Choice(["claude", "codex", "qwen", "opencode"]))
|
|
2479
|
+
def install_commands(tools: tuple) -> None:
|
|
2480
|
+
"""Install /openrunner slash commands for AI coding tools.
|
|
2481
|
+
|
|
2482
|
+
Without arguments, installs for all tools (Claude Code, Codex, Qwen, OpenCode).
|
|
2483
|
+
Specify tool names to install selectively.
|
|
2484
|
+
"""
|
|
2485
|
+
from openrunner.install_commands import install_all
|
|
2486
|
+
|
|
2487
|
+
targets = list(tools) if tools else None
|
|
2488
|
+
results = install_all(targets)
|
|
2489
|
+
|
|
2490
|
+
for tool_name, files in results.items():
|
|
2491
|
+
if files:
|
|
2492
|
+
click.echo(f" {tool_name}: {len(files)} file(s) installed")
|
|
2493
|
+
for f in files:
|
|
2494
|
+
click.echo(f" {f}")
|
|
2495
|
+
else:
|
|
2496
|
+
click.echo(f" {tool_name}: already up to date")
|
|
2497
|
+
|
|
2498
|
+
click.echo("")
|
|
2499
|
+
click.echo("Commands available:")
|
|
2500
|
+
click.echo(" /openrunner:sync-session — log current session to OpenRunner")
|
|
2501
|
+
click.echo(" /openrunner:log-note — save a quick research note")
|
|
2502
|
+
|
|
2503
|
+
|
|
2504
|
+
@session.command("hook")
|
|
2505
|
+
@click.argument("action", type=click.Choice(["install", "uninstall"]))
|
|
2506
|
+
def session_hook(action: str) -> None:
|
|
2507
|
+
"""Install/uninstall Claude Code auto-capture hook."""
|
|
2508
|
+
if action == "install":
|
|
2509
|
+
from openrunner.session import install_claude_hook
|
|
2510
|
+
path = install_claude_hook()
|
|
2511
|
+
click.echo(f"Claude Code hook installed.")
|
|
2512
|
+
click.echo(f" Config: {path}")
|
|
2513
|
+
click.echo(" Sessions will be auto-logged on exit.")
|
|
2514
|
+
else:
|
|
2515
|
+
from pathlib import Path
|
|
2516
|
+
hooks_file = Path.home() / ".claude" / "hooks.json"
|
|
2517
|
+
if hooks_file.exists():
|
|
2518
|
+
import json as json_mod
|
|
2519
|
+
hooks = json_mod.loads(hooks_file.read_text())
|
|
2520
|
+
stop_hooks = hooks.get("hooks", {}).get("Stop", [])
|
|
2521
|
+
hooks["hooks"]["Stop"] = [h for h in stop_hooks if "openrunner" not in str(h)]
|
|
2522
|
+
hooks_file.write_text(json_mod.dumps(hooks, indent=2))
|
|
2523
|
+
click.echo("Claude Code hook removed.")
|
|
2524
|
+
else:
|
|
2525
|
+
click.echo("No hooks.json found.")
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Install OpenRunner slash commands for AI coding tools.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Claude Code: ~/.claude/commands/openrunner/
|
|
5
|
+
- Codex (OpenAI): ~/.codex/commands/ or AGENTS.md instructions
|
|
6
|
+
- Qwen Code: ~/.qwen-code/commands/
|
|
7
|
+
- OpenCode: ~/.opencode/commands/
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
openrunner install # auto-detect and install all
|
|
11
|
+
openrunner install claude # Claude Code only
|
|
12
|
+
openrunner install codex # Codex only
|
|
13
|
+
openrunner install qwen # Qwen Code only
|
|
14
|
+
openrunner install opencode # OpenCode only
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Command templates
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
SYNC_SESSION_CMD = """---
|
|
27
|
+
name: {prefix}sync-session
|
|
28
|
+
description: Sync current coding session to OpenRunner as a research log
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Sync the current session to OpenRunner. Run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -c "
|
|
35
|
+
import sys, os
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# Ensure openrunner is importable
|
|
39
|
+
for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
|
|
40
|
+
os.path.expanduser('~/.local/lib/python3.11/site-packages'),
|
|
41
|
+
os.path.expanduser('~/.local/lib/python3.10/site-packages')]:
|
|
42
|
+
if os.path.isdir(p):
|
|
43
|
+
sys.path.insert(0, p)
|
|
44
|
+
|
|
45
|
+
from openrunner.session import discover_in_directory, parse_claude_session, parse_generic_session, sync_session_to_openrunner
|
|
46
|
+
|
|
47
|
+
# Find session dir for current tool
|
|
48
|
+
session_dirs = [
|
|
49
|
+
Path.home() / '.claude' / 'projects',
|
|
50
|
+
Path.home() / '.codex' / 'sessions',
|
|
51
|
+
Path.home() / '.qwen-code' / 'sessions',
|
|
52
|
+
Path.home() / '.opencode' / 'sessions',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Find most recent session file across all sources
|
|
56
|
+
all_sessions = []
|
|
57
|
+
for d in session_dirs:
|
|
58
|
+
if d.exists():
|
|
59
|
+
for f in d.rglob('*.jsonl'):
|
|
60
|
+
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
61
|
+
all_sessions.append(f)
|
|
62
|
+
for f in d.rglob('*.json'):
|
|
63
|
+
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
64
|
+
all_sessions.append(f)
|
|
65
|
+
|
|
66
|
+
if not all_sessions:
|
|
67
|
+
print('No sessions found.')
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
latest = max(all_sessions, key=lambda f: f.stat().st_mtime)
|
|
71
|
+
print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
|
|
72
|
+
|
|
73
|
+
if latest.suffix == '.jsonl' and '.claude' in str(latest):
|
|
74
|
+
parsed = parse_claude_session(latest)
|
|
75
|
+
else:
|
|
76
|
+
source = 'codex' if '.codex' in str(latest) else 'qwen' if '.qwen' in str(latest) else 'opencode'
|
|
77
|
+
parsed = parse_generic_session(latest, source)
|
|
78
|
+
|
|
79
|
+
print(f' Messages: {{parsed[\"message_count\"]}} ({{parsed[\"user_message_count\"]}} user)')
|
|
80
|
+
print(f' Tokens: {{parsed.get(\"total_tokens\", 0):,}}')
|
|
81
|
+
|
|
82
|
+
project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
|
|
83
|
+
run_id = sync_session_to_openrunner(parsed, project=project)
|
|
84
|
+
if run_id:
|
|
85
|
+
base = os.environ.get('OPENRUNNER_BASE_URL', 'https://openrun.gladia.io')
|
|
86
|
+
print(f'Synced -> {{base}}/runs/{{run_id}}')
|
|
87
|
+
else:
|
|
88
|
+
print('Failed. Run: openrunner login')
|
|
89
|
+
"
|
|
90
|
+
```
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
LOG_NOTE_CMD = """---
|
|
94
|
+
name: {prefix}log-note
|
|
95
|
+
description: Log a research note to OpenRunner
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
Log the user's note text to OpenRunner as a quick research entry. Run:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python3 -c "
|
|
102
|
+
import os, sys, json
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
|
|
106
|
+
os.path.expanduser('~/.local/lib/python3.11/site-packages')]:
|
|
107
|
+
if os.path.isdir(p):
|
|
108
|
+
sys.path.insert(0, p)
|
|
109
|
+
|
|
110
|
+
from openrunner.api_client import APIClient
|
|
111
|
+
|
|
112
|
+
settings_file = Path.home() / '.openrunner' / 'settings.json'
|
|
113
|
+
settings = json.loads(settings_file.read_text()) if settings_file.exists() else {{}}
|
|
114
|
+
api_key = os.environ.get('OPENRUNNER_API_KEY') or settings.get('api_key')
|
|
115
|
+
base_url = os.environ.get('OPENRUNNER_BASE_URL') or settings.get('base_url', 'https://openrun.gladia.io')
|
|
116
|
+
project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
|
|
117
|
+
|
|
118
|
+
note = '''$ARGUMENTS'''
|
|
119
|
+
|
|
120
|
+
client = APIClient(base_url=base_url, api_key=api_key)
|
|
121
|
+
result = client.create_run({{
|
|
122
|
+
'project': project,
|
|
123
|
+
'display_name': f'note: {{note[:50]}}',
|
|
124
|
+
'notes': note,
|
|
125
|
+
'tags': ['note', 'quick'],
|
|
126
|
+
'state': 'finished',
|
|
127
|
+
}})
|
|
128
|
+
if result:
|
|
129
|
+
print(f'Note logged: run {{result[\"id\"]}}')
|
|
130
|
+
else:
|
|
131
|
+
print('Failed. Run: openrunner login')
|
|
132
|
+
client.close()
|
|
133
|
+
"
|
|
134
|
+
```
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Installers per tool
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def install_claude_code() -> list[str]:
|
|
143
|
+
"""Install commands in ~/.claude/commands/openrunner/."""
|
|
144
|
+
cmd_dir = Path.home() / ".claude" / "commands" / "openrunner"
|
|
145
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
files = []
|
|
148
|
+
|
|
149
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
150
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
151
|
+
files.append(str(sync_path))
|
|
152
|
+
|
|
153
|
+
note_path = cmd_dir / "log-note.md"
|
|
154
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
155
|
+
files.append(str(note_path))
|
|
156
|
+
|
|
157
|
+
return files
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def install_codex() -> list[str]:
|
|
161
|
+
"""Install commands for Codex CLI."""
|
|
162
|
+
# Codex uses ~/.codex/ with instructions or custom commands
|
|
163
|
+
cmd_dir = Path.home() / ".codex" / "commands"
|
|
164
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
files = []
|
|
167
|
+
|
|
168
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
169
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
170
|
+
files.append(str(sync_path))
|
|
171
|
+
|
|
172
|
+
note_path = cmd_dir / "log-note.md"
|
|
173
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
174
|
+
files.append(str(note_path))
|
|
175
|
+
|
|
176
|
+
# Also add to AGENTS.md if it exists
|
|
177
|
+
agents_file = Path.home() / ".codex" / "AGENTS.md"
|
|
178
|
+
instruction = "\n\n## OpenRunner Integration\nUse `/openrunner:sync-session` to log this session to OpenRunner.\nUse `/openrunner:log-note <text>` to save a research note.\n"
|
|
179
|
+
if agents_file.exists():
|
|
180
|
+
content = agents_file.read_text()
|
|
181
|
+
if "openrunner" not in content.lower():
|
|
182
|
+
agents_file.write_text(content + instruction)
|
|
183
|
+
files.append(str(agents_file))
|
|
184
|
+
else:
|
|
185
|
+
agents_file.write_text("# Codex Agent Instructions" + instruction)
|
|
186
|
+
files.append(str(agents_file))
|
|
187
|
+
|
|
188
|
+
return files
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def install_qwen_code() -> list[str]:
|
|
192
|
+
"""Install commands for Qwen Code."""
|
|
193
|
+
cmd_dir = Path.home() / ".qwen-code" / "commands"
|
|
194
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
files = []
|
|
197
|
+
|
|
198
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
199
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
200
|
+
files.append(str(sync_path))
|
|
201
|
+
|
|
202
|
+
note_path = cmd_dir / "log-note.md"
|
|
203
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
204
|
+
files.append(str(note_path))
|
|
205
|
+
|
|
206
|
+
return files
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def install_opencode() -> list[str]:
|
|
210
|
+
"""Install commands for OpenCode."""
|
|
211
|
+
cmd_dir = Path.home() / ".opencode" / "commands"
|
|
212
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
files = []
|
|
215
|
+
|
|
216
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
217
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
218
|
+
files.append(str(sync_path))
|
|
219
|
+
|
|
220
|
+
note_path = cmd_dir / "log-note.md"
|
|
221
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
222
|
+
files.append(str(note_path))
|
|
223
|
+
|
|
224
|
+
return files
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Main installer
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
TOOLS = {
|
|
232
|
+
"claude": ("Claude Code", install_claude_code),
|
|
233
|
+
"codex": ("Codex", install_codex),
|
|
234
|
+
"qwen": ("Qwen Code", install_qwen_code),
|
|
235
|
+
"opencode": ("OpenCode", install_opencode),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def install_all(targets: list[str] | None = None) -> dict[str, list[str]]:
|
|
240
|
+
"""Install commands for specified tools (or all if None).
|
|
241
|
+
|
|
242
|
+
Returns dict of tool_name -> list of installed file paths.
|
|
243
|
+
"""
|
|
244
|
+
results = {}
|
|
245
|
+
|
|
246
|
+
if targets:
|
|
247
|
+
to_install = [(k, v) for k, v in TOOLS.items() if k in targets]
|
|
248
|
+
else:
|
|
249
|
+
# Auto-detect: install for all tools
|
|
250
|
+
to_install = list(TOOLS.items())
|
|
251
|
+
|
|
252
|
+
for key, (name, installer) in to_install:
|
|
253
|
+
files = installer()
|
|
254
|
+
results[name] = files
|
|
255
|
+
|
|
256
|
+
return results
|