openrunner-sdk 2.6.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.
Files changed (117) hide show
  1. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/PKG-INFO +1 -1
  2. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/__init__.py +1 -1
  3. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cli.py +66 -7
  4. openrunner_sdk-2.7.0/openrunner/install_commands.py +256 -0
  5. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/session.py +197 -18
  6. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/pyproject.toml +1 -1
  7. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/.gitignore +0 -0
  8. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/=6.0 +0 -0
  9. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/=8.1 +0 -0
  10. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/README.md +0 -0
  11. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/api_client.py +0 -0
  12. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/artifact.py +0 -0
  13. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/buffer.py +0 -0
  14. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cache.py +0 -0
  15. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/config.py +0 -0
  16. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cost.py +0 -0
  17. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/dataset.py +0 -0
  18. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/environment.py +0 -0
  19. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/evaluation.py +0 -0
  20. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/feedback.py +0 -0
  21. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/git_info.py +0 -0
  22. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/guardrails.py +0 -0
  23. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/__init__.py +0 -0
  24. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/accelerate.py +0 -0
  25. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/anthropic_tracer.py +0 -0
  26. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/catboost.py +0 -0
  27. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/diffusers.py +0 -0
  28. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/fastai.py +0 -0
  29. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/forced_alignment.py +0 -0
  30. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/gladia.py +0 -0
  31. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/gymnasium.py +0 -0
  32. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/huggingface.py +0 -0
  33. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/hydra.py +0 -0
  34. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/ignite.py +0 -0
  35. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/jax.py +0 -0
  36. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/keras.py +0 -0
  37. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/langchain.py +0 -0
  38. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightgbm.py +0 -0
  39. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightning.py +0 -0
  40. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/llamaindex.py +0 -0
  41. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_finetune.py +0 -0
  42. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_tracer.py +0 -0
  43. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/optuna.py +0 -0
  44. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/pytorch.py +0 -0
  45. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/sb3.py +0 -0
  46. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/sklearn.py +0 -0
  47. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/tensorflow.py +0 -0
  48. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/trl.py +0 -0
  49. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/tts.py +0 -0
  50. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/ultralytics.py +0 -0
  51. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/voice_agent.py +0 -0
  52. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/whisper.py +0 -0
  53. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/xgboost.py +0 -0
  54. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/launch.py +0 -0
  55. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/media.py +0 -0
  56. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/migrate.py +0 -0
  57. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/model.py +0 -0
  58. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/offline.py +0 -0
  59. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/pii.py +0 -0
  60. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/plot.py +0 -0
  61. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/prompt.py +0 -0
  62. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/query_api.py +0 -0
  63. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/run.py +0 -0
  64. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/scorers.py +0 -0
  65. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/sender.py +0 -0
  66. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/settings.py +0 -0
  67. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/summary.py +0 -0
  68. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/sweep.py +0 -0
  69. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/system_metrics.py +0 -0
  70. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/tensorboard.py +0 -0
  71. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/trace.py +0 -0
  72. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/transcript_formatter.py +0 -0
  73. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wal.py +0 -0
  74. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/__init__.py +0 -0
  75. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/_shim.py +0 -0
  76. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wer.py +0 -0
  77. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/__init__.py +0 -0
  78. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/conftest.py +0 -0
  79. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_alert.py +0 -0
  80. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_aliases.py +0 -0
  81. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_api_client.py +0 -0
  82. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_artifact.py +0 -0
  83. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_buffer.py +0 -0
  84. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_cache.py +0 -0
  85. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_class_scorers.py +0 -0
  86. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_cli.py +0 -0
  87. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_config.py +0 -0
  88. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_evaluation.py +0 -0
  89. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_finish.py +0 -0
  90. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_git_info.py +0 -0
  91. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_init.py +0 -0
  92. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_fastai.py +0 -0
  93. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_huggingface.py +0 -0
  94. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_keras.py +0 -0
  95. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_langchain.py +0 -0
  96. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_lightning.py +0 -0
  97. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_pytorch.py +0 -0
  98. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_sklearn.py +0 -0
  99. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_xgboost.py +0 -0
  100. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_launch.py +0 -0
  101. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_log.py +0 -0
  102. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_log_code.py +0 -0
  103. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_media.py +0 -0
  104. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_migrate.py +0 -0
  105. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_offline.py +0 -0
  106. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_offline_sync.py +0 -0
  107. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_pii.py +0 -0
  108. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_plot.py +0 -0
  109. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_query_api.py +0 -0
  110. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_resume.py +0 -0
  111. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sdk_features.py +0 -0
  112. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sender.py +0 -0
  113. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_summary.py +0 -0
  114. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sweep.py +0 -0
  115. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_system_metrics.py +0 -0
  116. {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_trace.py +0 -0
  117. {openrunner_sdk-2.6.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.6.0
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.6.0"
123
+ __version__ = "2.7.0"
124
124
 
125
125
  logger = logging.getLogger("openrunner")
126
126
 
@@ -2375,16 +2375,47 @@ def session() -> None:
2375
2375
  pass
2376
2376
 
2377
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
+
2378
2389
  @session.command("sync")
2390
+ @click.argument("directory", required=False, type=click.Path(exists=True))
2379
2391
  @click.option("--hours", "-h", default=24.0, help="Look back N hours (default: 24)")
2380
- @click.option("--project", "-p", default=None, help="Target project (default: research-sessions)")
2392
+ @click.option("--project", "-p", default=None, help="Target project (default: from config)")
2381
2393
  @click.option("--dry-run", is_flag=True, help="Show what would be synced without uploading")
2382
- def session_sync(hours: float, project: str | None, dry_run: bool) -> None:
2383
- """Sync recent AI sessions to OpenRunner."""
2384
- from openrunner.session import discover_all_sessions, sync_all
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).
2385
2399
 
2386
- if dry_run:
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:
2387
2416
  sessions = discover_all_sessions(hours)
2417
+
2418
+ if dry_run or not sessions:
2388
2419
  if not sessions:
2389
2420
  click.echo("No sessions found.")
2390
2421
  return
@@ -2393,9 +2424,10 @@ def session_sync(hours: float, project: str | None, dry_run: bool) -> None:
2393
2424
  ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(s["mtime"]))
2394
2425
  size = s["size"] / 1024
2395
2426
  click.echo(f" [{s['source']}] {ts} ({size:.0f} KB) {s['path'].name}")
2396
- return
2427
+ if dry_run:
2428
+ return
2397
2429
 
2398
- synced = sync_all(since_hours=hours, project=project)
2430
+ synced = sync_all(since_hours=hours, project=project, directory=Path(directory) if directory else None)
2399
2431
  if synced:
2400
2432
  click.echo(f"Synced {len(synced)} session(s) to OpenRunner.")
2401
2433
  for run_id in synced:
@@ -2442,6 +2474,33 @@ def session_list(hours: float) -> None:
2442
2474
  click.echo(f" {s['source']:<12} {ts:<18} {size:<10} {status}")
2443
2475
 
2444
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
+
2445
2504
  @session.command("hook")
2446
2505
  @click.argument("action", type=click.Choice(["install", "uninstall"]))
2447
2506
  def session_hook(action: str) -> None:
@@ -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
@@ -72,13 +72,16 @@ def discover_claude_sessions(since_hours: float = 24) -> list[dict]:
72
72
 
73
73
  cutoff = time.time() - (since_hours * 3600)
74
74
  for jsonl in CLAUDE_CODE_DIR.rglob("*.jsonl"):
75
- if jsonl.stat().st_mtime < cutoff:
75
+ stat = jsonl.stat()
76
+ if stat.st_size < 100 or stat.st_mtime < cutoff:
77
+ continue
78
+ if ".meta." in jsonl.name:
76
79
  continue
77
80
  sessions.append({
78
81
  "source": "claude-code",
79
82
  "path": jsonl,
80
- "mtime": jsonl.stat().st_mtime,
81
- "size": jsonl.stat().st_size,
83
+ "mtime": stat.st_mtime,
84
+ "size": stat.st_size,
82
85
  })
83
86
  return sorted(sessions, key=lambda s: s["mtime"], reverse=True)
84
87
 
@@ -269,6 +272,147 @@ def parse_generic_session(path: Path, source: str) -> dict[str, Any]:
269
272
  # ---------------------------------------------------------------------------
270
273
 
271
274
 
275
+ def get_session_config(interactive: bool = False) -> dict:
276
+ """Load or interactively configure session sync settings.
277
+
278
+ Config stored in ~/.openrunner/session_config.json:
279
+ {
280
+ "api_key": "or_...",
281
+ "base_url": "https://...",
282
+ "project": "org/project-name"
283
+ }
284
+
285
+ Per-worktree overrides in .openrunner/session.json (local to cwd).
286
+ """
287
+ global_config_file = Path.home() / ".openrunner" / "session_config.json"
288
+ local_config_file = Path.cwd() / ".openrunner" / "session.json"
289
+ global_settings_file = Path.home() / ".openrunner" / "settings.json"
290
+
291
+ # Load cascade: local > global session config > global settings > env
292
+ config: dict[str, Any] = {}
293
+
294
+ # Global settings (login credentials)
295
+ if global_settings_file.exists():
296
+ try:
297
+ config.update(json.loads(global_settings_file.read_text()))
298
+ except (json.JSONDecodeError, OSError):
299
+ pass
300
+
301
+ # Global session config
302
+ if global_config_file.exists():
303
+ try:
304
+ config.update(json.loads(global_config_file.read_text()))
305
+ except (json.JSONDecodeError, OSError):
306
+ pass
307
+
308
+ # Local override
309
+ if local_config_file.exists():
310
+ try:
311
+ config.update(json.loads(local_config_file.read_text()))
312
+ except (json.JSONDecodeError, OSError):
313
+ pass
314
+
315
+ # Env vars take precedence
316
+ if os.environ.get("OPENRUNNER_API_KEY"):
317
+ config["api_key"] = os.environ["OPENRUNNER_API_KEY"]
318
+ if os.environ.get("OPENRUNNER_BASE_URL"):
319
+ config["base_url"] = os.environ["OPENRUNNER_BASE_URL"]
320
+ if os.environ.get("OPENRUNNER_SESSION_PROJECT"):
321
+ config["project"] = os.environ["OPENRUNNER_SESSION_PROJECT"]
322
+
323
+ return config
324
+
325
+
326
+ def save_session_config(config: dict, local: bool = False) -> None:
327
+ """Save session config globally or locally."""
328
+ if local:
329
+ config_file = Path.cwd() / ".openrunner" / "session.json"
330
+ else:
331
+ config_file = Path.home() / ".openrunner" / "session_config.json"
332
+ config_file.parent.mkdir(parents=True, exist_ok=True)
333
+ config_file.write_text(json.dumps(config, indent=2))
334
+
335
+
336
+ def interactive_setup() -> dict:
337
+ """Interactive first-run setup: prompt for API key and project selection.
338
+
339
+ Returns config dict with api_key, base_url, project.
340
+ """
341
+ from openrunner.api_client import APIClient
342
+
343
+ config = get_session_config()
344
+
345
+ # Step 1: API key
346
+ api_key = config.get("api_key")
347
+ base_url = config.get("base_url", "https://openrun.gladia.io")
348
+
349
+ if not api_key:
350
+ print("OpenRunner session sync — first-time setup")
351
+ print("=" * 50)
352
+ api_key = input("API Key (from OpenRunner settings): ").strip()
353
+ if not api_key:
354
+ print("No API key provided. Aborting.")
355
+ return {}
356
+ base_url_input = input(f"Server URL [{base_url}]: ").strip()
357
+ if base_url_input:
358
+ base_url = base_url_input
359
+
360
+ # Step 2: Verify key works
361
+ client = APIClient(base_url=base_url, api_key=api_key)
362
+ try:
363
+ resp = client._request("get", "/auth/me")
364
+ if resp.status_code != 200:
365
+ print(f"API key invalid (HTTP {resp.status_code}). Check your key.")
366
+ client.close()
367
+ return {}
368
+ user_info = resp.json()
369
+ print(f"Authenticated as: {user_info.get('display_name', user_info.get('email', '?'))}")
370
+ except Exception as e:
371
+ print(f"Connection failed: {e}")
372
+ client.close()
373
+ return {}
374
+
375
+ # Step 3: Select project
376
+ project = config.get("project")
377
+ if not project:
378
+ print("\nSelect project for session logs:")
379
+ projects = client.list_projects()
380
+ if not projects:
381
+ print("No projects found. Create one in OpenRunner first.")
382
+ client.close()
383
+ return {}
384
+
385
+ for i, p in enumerate(projects, 1):
386
+ org = p.get("org_name", p.get("organization", {}).get("name", "?"))
387
+ name = p.get("name", "?")
388
+ print(f" [{i}] {org}/{name}")
389
+
390
+ choice = input(f"\nProject number [1-{len(projects)}]: ").strip()
391
+ try:
392
+ idx = int(choice) - 1
393
+ p = projects[idx]
394
+ org = p.get("org_name", p.get("organization", {}).get("name", ""))
395
+ project = f"{org}/{p['name']}" if org else p["name"]
396
+ except (ValueError, IndexError):
397
+ print("Invalid selection.")
398
+ client.close()
399
+ return {}
400
+
401
+ client.close()
402
+
403
+ config_out = {
404
+ "api_key": api_key,
405
+ "base_url": base_url,
406
+ "project": project,
407
+ }
408
+
409
+ # Save
410
+ save_session_config(config_out)
411
+ print(f"\nConfig saved. Sessions will sync to: {project}")
412
+ print(f"Config file: ~/.openrunner/session_config.json")
413
+ return config_out
414
+
415
+
272
416
  def sync_session_to_openrunner(
273
417
  parsed: dict[str, Any],
274
418
  project: str | None = None,
@@ -281,25 +425,18 @@ def sync_session_to_openrunner(
281
425
  """
282
426
  from openrunner.api_client import APIClient
283
427
 
284
- # Load settings from ~/.openrunner/settings.json
285
- if not api_key or not base_url:
286
- settings_file = Path.home() / ".openrunner" / "settings.json"
287
- settings = {}
288
- if settings_file.exists():
289
- try:
290
- settings = json.loads(settings_file.read_text())
291
- except (json.JSONDecodeError, OSError):
292
- pass
293
- api_key = api_key or os.environ.get("OPENRUNNER_API_KEY") or settings.get("api_key")
294
- base_url = base_url or os.environ.get("OPENRUNNER_BASE_URL") or settings.get("base_url")
428
+ # Load config (cascade: args > env > session_config > settings)
429
+ config = get_session_config()
430
+ api_key = api_key or config.get("api_key")
431
+ base_url = base_url or config.get("base_url")
295
432
 
296
433
  if not api_key or not base_url:
297
434
  logger.warning("No API key or base URL configured. Run 'openrunner login' first.")
298
435
  return None
299
436
 
300
- # Determine project
437
+ # Determine project from config cascade
301
438
  if not project:
302
- project = os.environ.get("OPENRUNNER_SESSION_PROJECT", "research-sessions")
439
+ project = config.get("project") or "research-sessions"
303
440
 
304
441
  client = APIClient(base_url=base_url, api_key=api_key)
305
442
 
@@ -397,8 +534,49 @@ def _format_session_notes(parsed: dict) -> str:
397
534
  # ---------------------------------------------------------------------------
398
535
 
399
536
 
537
+ def discover_in_directory(directory: Path, since_hours: float = 24) -> list[dict]:
538
+ """Discover session files in a given directory (recursive).
539
+
540
+ Detects source by path heuristics:
541
+ - .claude in path → claude-code
542
+ - .codex in path → codex
543
+ - .qwen in path → qwen
544
+ - Otherwise → generic
545
+ """
546
+ sessions = []
547
+ cutoff = time.time() - (since_hours * 3600)
548
+
549
+ for pattern in ("**/*.jsonl", "**/*.json"):
550
+ for f in directory.glob(pattern):
551
+ stat = f.stat()
552
+ # Skip empty files, meta files, and old files
553
+ if stat.st_size < 100 or stat.st_mtime < cutoff:
554
+ continue
555
+ if ".meta." in f.name:
556
+ continue
557
+ # Detect source from path
558
+ path_str = str(f).lower()
559
+ if ".claude" in path_str:
560
+ source = "claude-code"
561
+ elif ".codex" in path_str:
562
+ source = "codex"
563
+ elif ".qwen" in path_str:
564
+ source = "qwen"
565
+ elif "openai" in path_str or "chatgpt" in path_str:
566
+ source = "chatgpt"
567
+ else:
568
+ source = "claude-code" # default — most common format
569
+ sessions.append({
570
+ "source": source,
571
+ "path": f,
572
+ "mtime": stat.st_mtime,
573
+ "size": stat.st_size,
574
+ })
575
+ return sorted(sessions, key=lambda s: s["mtime"], reverse=True)
576
+
577
+
400
578
  def discover_all_sessions(since_hours: float = 24) -> list[dict]:
401
- """Discover sessions from all sources."""
579
+ """Discover sessions from all default sources."""
402
580
  all_sessions = []
403
581
  all_sessions.extend(discover_claude_sessions(since_hours))
404
582
  all_sessions.extend(discover_codex_sessions(since_hours))
@@ -411,10 +589,11 @@ def sync_all(
411
589
  since_hours: float = 24,
412
590
  project: str | None = None,
413
591
  dry_run: bool = False,
592
+ directory: Path | None = None,
414
593
  ) -> list[str]:
415
594
  """Discover and sync all new sessions. Returns list of synced run IDs."""
416
595
  state = _load_sync_state()
417
- sessions = discover_all_sessions(since_hours)
596
+ sessions = discover_in_directory(directory, since_hours) if directory else discover_all_sessions(since_hours)
418
597
  synced_ids = []
419
598
 
420
599
  for session_info in sessions:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openrunner-sdk"
3
- version = "2.6.0"
3
+ version = "2.7.0"
4
4
  description = "OpenRunner SDK - W&B-compatible ML experiment tracking client"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
File without changes
File without changes
File without changes