openrunner-sdk 2.7.1__tar.gz → 2.9.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 (118) hide show
  1. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/PKG-INFO +2 -1
  2. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/__init__.py +1 -1
  3. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/install_commands.py +65 -43
  4. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/session.py +179 -81
  5. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/pyproject.toml +2 -2
  6. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/.gitignore +0 -0
  7. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/=6.0 +0 -0
  8. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/=8.1 +0 -0
  9. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/README.md +0 -0
  10. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/api_client.py +0 -0
  11. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/artifact.py +0 -0
  12. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/buffer.py +0 -0
  13. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cache.py +0 -0
  14. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cli.py +0 -0
  15. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/config.py +0 -0
  16. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cost.py +0 -0
  17. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/dataset.py +0 -0
  18. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/environment.py +0 -0
  19. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/evaluation.py +0 -0
  20. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/feedback.py +0 -0
  21. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/git_info.py +0 -0
  22. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/guardrails.py +0 -0
  23. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/__init__.py +0 -0
  24. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/accelerate.py +0 -0
  25. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/anthropic_tracer.py +0 -0
  26. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/catboost.py +0 -0
  27. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/diffusers.py +0 -0
  28. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/fastai.py +0 -0
  29. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/forced_alignment.py +0 -0
  30. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/gladia.py +0 -0
  31. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/gymnasium.py +0 -0
  32. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/huggingface.py +0 -0
  33. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/hydra.py +0 -0
  34. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/ignite.py +0 -0
  35. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/jax.py +0 -0
  36. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/keras.py +0 -0
  37. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/langchain.py +0 -0
  38. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/lightgbm.py +0 -0
  39. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/lightning.py +0 -0
  40. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/llamaindex.py +0 -0
  41. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/openai_finetune.py +0 -0
  42. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/openai_tracer.py +0 -0
  43. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/optuna.py +0 -0
  44. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/pytorch.py +0 -0
  45. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/sb3.py +0 -0
  46. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/sklearn.py +0 -0
  47. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/tensorflow.py +0 -0
  48. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/trl.py +0 -0
  49. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/tts.py +0 -0
  50. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/ultralytics.py +0 -0
  51. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/voice_agent.py +0 -0
  52. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/whisper.py +0 -0
  53. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/xgboost.py +0 -0
  54. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/launch.py +0 -0
  55. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/media.py +0 -0
  56. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/migrate.py +0 -0
  57. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/model.py +0 -0
  58. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/offline.py +0 -0
  59. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/pii.py +0 -0
  60. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/plot.py +0 -0
  61. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/prompt.py +0 -0
  62. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/query_api.py +0 -0
  63. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/redact.py +0 -0
  64. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/run.py +0 -0
  65. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/scorers.py +0 -0
  66. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/sender.py +0 -0
  67. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/settings.py +0 -0
  68. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/summary.py +0 -0
  69. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/sweep.py +0 -0
  70. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/system_metrics.py +0 -0
  71. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/tensorboard.py +0 -0
  72. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/trace.py +0 -0
  73. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/transcript_formatter.py +0 -0
  74. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wal.py +0 -0
  75. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wandb_compat/__init__.py +0 -0
  76. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wandb_compat/_shim.py +0 -0
  77. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wer.py +0 -0
  78. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/__init__.py +0 -0
  79. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/conftest.py +0 -0
  80. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_alert.py +0 -0
  81. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_aliases.py +0 -0
  82. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_api_client.py +0 -0
  83. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_artifact.py +0 -0
  84. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_buffer.py +0 -0
  85. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_cache.py +0 -0
  86. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_class_scorers.py +0 -0
  87. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_cli.py +0 -0
  88. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_config.py +0 -0
  89. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_evaluation.py +0 -0
  90. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_finish.py +0 -0
  91. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_git_info.py +0 -0
  92. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_init.py +0 -0
  93. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_fastai.py +0 -0
  94. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_huggingface.py +0 -0
  95. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_keras.py +0 -0
  96. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_langchain.py +0 -0
  97. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_lightning.py +0 -0
  98. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_pytorch.py +0 -0
  99. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_sklearn.py +0 -0
  100. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_xgboost.py +0 -0
  101. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_launch.py +0 -0
  102. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_log.py +0 -0
  103. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_log_code.py +0 -0
  104. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_media.py +0 -0
  105. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_migrate.py +0 -0
  106. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_offline.py +0 -0
  107. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_offline_sync.py +0 -0
  108. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_pii.py +0 -0
  109. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_plot.py +0 -0
  110. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_query_api.py +0 -0
  111. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_resume.py +0 -0
  112. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sdk_features.py +0 -0
  113. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sender.py +0 -0
  114. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_summary.py +0 -0
  115. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sweep.py +0 -0
  116. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_system_metrics.py +0 -0
  117. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_trace.py +0 -0
  118. {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.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.7.1
3
+ Version: 2.9.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
@@ -33,6 +33,7 @@ Requires-Dist: catboost>=1.2; extra == 'catboost'
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: numpy>=1.24; extra == 'dev'
35
35
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.0; extra == 'dev'
37
38
  Provides-Extra: diffusers
38
39
  Requires-Dist: diffusers>=0.25; extra == 'diffusers'
@@ -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.7.1"
123
+ __version__ = "2.9.0"
124
124
 
125
125
  logger = logging.getLogger("openrunner")
126
126
 
@@ -26,68 +26,90 @@ from pathlib import Path
26
26
  SYNC_SESSION_CMD = """---
27
27
  name: {prefix}sync-session
28
28
  description: Sync current coding session to OpenRunner as a research log
29
+ argument-hint: "[org/project]"
30
+ allowed-tools:
31
+ - Bash
32
+ - Read
33
+ - AskUserQuestion
29
34
  ---
30
35
 
31
- Sync the current session to OpenRunner. Run:
36
+ Sync the current Claude Code session to OpenRunner.
37
+
38
+ ## Process
39
+
40
+ ### Step 1: Check configuration
41
+
42
+ Run this to check if session config exists:
32
43
 
33
44
  ```bash
34
45
  python3 -c "
35
- import sys, os
46
+ import json
36
47
  from pathlib import Path
48
+ config_file = Path.home() / '.openrunner' / 'session_config.json'
49
+ if config_file.exists():
50
+ config = json.loads(config_file.read_text())
51
+ if config.get('api_key') and config.get('project'):
52
+ print('CONFIGURED')
53
+ print(f'project={{config[\"project\"]}}')
54
+ else:
55
+ print('INCOMPLETE')
56
+ else:
57
+ print('NOT_CONFIGURED')
58
+ "
59
+ ```
37
60
 
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)
61
+ ### Step 2a: If NOT_CONFIGURED or INCOMPLETE
44
62
 
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)
63
+ Ask the user for their OpenRunner API key and base URL. Then list projects and ask them to pick one. Save to ~/.openrunner/session_config.json.
69
64
 
70
- latest = max(all_sessions, key=lambda f: f.stat().st_mtime)
71
- print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
65
+ ### Step 2b: If CONFIGURED (or after setup)
72
66
 
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)
67
+ Sync the current session. Use $ARGUMENTS as project override if provided:
68
+
69
+ ```bash
70
+ python3 -c "
71
+ import sys, os
72
+ from pathlib import Path
73
+ for p in [os.path.expanduser(f'~/.local/lib/python3.{{v}}/site-packages') for v in (12,11,10)]:
74
+ if os.path.isdir(p): sys.path.insert(0, p)
75
+ from openrunner.session import parse_claude_session, sync_session_to_openrunner, get_session_config
76
+
77
+ cwd = Path.cwd()
78
+ cwd_key = '-' + str(cwd).replace('/', '-').lstrip('-')
79
+ project_dir = Path.home() / '.claude' / 'projects' / cwd_key
80
+ if not project_dir.exists():
81
+ for d in (Path.home() / '.claude' / 'projects').iterdir():
82
+ if d.is_dir() and cwd_key in d.name:
83
+ project_dir = d
84
+ break
85
+
86
+ sessions = sorted(
87
+ [f for f in project_dir.rglob('*.jsonl') if f.stat().st_size > 100 and '.meta.' not in f.name],
88
+ key=lambda f: f.stat().st_mtime, reverse=True,
89
+ ) if project_dir.exists() else []
90
+
91
+ if not sessions:
92
+ print('No session files found.')
93
+ sys.exit(1)
78
94
 
95
+ latest = sessions[0]
96
+ print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
97
+ parsed = parse_claude_session(latest)
79
98
  print(f' Messages: {{parsed[\"message_count\"]}} ({{parsed[\"user_message_count\"]}} user)')
80
99
  print(f' Tokens: {{parsed.get(\"total_tokens\", 0):,}}')
81
100
 
82
- project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
83
- run_id = sync_session_to_openrunner(parsed, project=project)
101
+ project_override = '$ARGUMENTS'.strip() or None
102
+ run_id = sync_session_to_openrunner(parsed, project=project_override)
84
103
  if run_id:
85
- base = os.environ.get('OPENRUNNER_BASE_URL', 'https://openrun.gladia.io')
104
+ config = get_session_config()
105
+ base = config.get('base_url', 'https://openrun.gladia.io')
86
106
  print(f'Synced -> {{base}}/runs/{{run_id}}')
87
107
  else:
88
- print('Failed. Run: openrunner login')
108
+ print('Sync failed. Run: openrunner session setup')
89
109
  "
90
110
  ```
111
+
112
+ ### Step 3: Report the run URL and stats to the user.
91
113
  """
92
114
 
93
115
  LOG_NOTE_CMD = """---
@@ -137,8 +137,15 @@ def parse_claude_session(path: Path) -> dict[str, Any]:
137
137
  total_tokens += usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
138
138
 
139
139
  # Extract project context from path
140
- # ~/.claude/projects/-home-user-myproject/session.jsonl
141
- project_hint = path.parent.name.replace("-", "/").lstrip("/")
140
+ # ~/.claude/projects/-home-user-myproject/session-id.jsonl
141
+ # or ~/.claude/projects/-home-user-myproject/subagents/agent-xxx.jsonl
142
+ parts = path.parts
143
+ # Find the project dir (child of "projects")
144
+ project_hint = ""
145
+ for i, p in enumerate(parts):
146
+ if p == "projects" and i + 1 < len(parts):
147
+ project_hint = parts[i + 1].replace("-", "/").lstrip("/")
148
+ break
142
149
 
143
150
  # Build summary
144
151
  user_messages = [m for m in messages if m["role"] == "user"]
@@ -170,19 +177,41 @@ def _estimate_duration(path: Path) -> float:
170
177
 
171
178
 
172
179
  def _summarize_session(messages: list[dict]) -> str:
173
- """Generate a text summary from messages."""
174
- user_msgs = [m["content"] for m in messages if m["role"] == "user"]
180
+ """Generate a structured summary from messages.
181
+
182
+ Extracts: topic, key actions, outcomes.
183
+ """
184
+ user_msgs = [m["content"] for m in messages if m["role"] == "user" and m.get("content")]
185
+ assistant_msgs = [m["content"] for m in messages if m["role"] == "assistant" and m.get("content")]
186
+
175
187
  if not user_msgs:
176
188
  return "Empty session"
177
189
 
178
- # Use first and last user messages as summary
179
190
  parts = []
180
- if user_msgs:
181
- parts.append(f"Started: {user_msgs[0][:100]}")
191
+
192
+ # Topic from first message
193
+ parts.append(f"**Topic:** {user_msgs[0][:150]}")
194
+
195
+ # Key user requests (sample up to 5 distinct short ones)
196
+ if len(user_msgs) > 2:
197
+ key_requests = []
198
+ for msg in user_msgs[1:]:
199
+ # Skip very short messages (confirmations) and very long ones (pastes)
200
+ if 10 < len(msg) < 200:
201
+ key_requests.append(msg.strip()[:80])
202
+ if len(key_requests) >= 5:
203
+ break
204
+ if key_requests:
205
+ parts.append("**Key requests:** " + " → ".join(key_requests))
206
+
207
+ # Last request
182
208
  if len(user_msgs) > 1:
183
- parts.append(f"Last: {user_msgs[-1][:100]}")
184
- parts.append(f"({len(user_msgs)} user messages)")
185
- return " | ".join(parts)
209
+ parts.append(f"**Last:** {user_msgs[-1][:100]}")
210
+
211
+ # Stats
212
+ parts.append(f"**Stats:** {len(user_msgs)} requests, {len(assistant_msgs)} responses")
213
+
214
+ return "\n".join(parts)
186
215
 
187
216
 
188
217
  # ---------------------------------------------------------------------------
@@ -477,97 +506,146 @@ def sync_session_to_openrunner(
477
506
 
478
507
  client = APIClient(base_url=base_url, api_key=api_key)
479
508
 
480
- # Create run
509
+ # Resolve project_id from "org/project" format
510
+ project_id = None
511
+ try:
512
+ projects = client.list_projects()
513
+ for p in projects:
514
+ org_name = p.get("org_name", "")
515
+ proj_name = p.get("name", "")
516
+ full_name = f"{org_name}/{proj_name}" if org_name else proj_name
517
+ if full_name == project or proj_name == project:
518
+ project_id = p.get("id")
519
+ break
520
+ except Exception:
521
+ pass
522
+
523
+ if not project_id:
524
+ # Fallback: try creating as a run (backward compat with older servers)
525
+ logger.warning(f"Could not resolve project '{project}', falling back to run creation")
526
+ return _sync_as_run_fallback(client, parsed, project, visibility)
527
+
528
+ # Build AI session data
481
529
  source = parsed["source"]
530
+ project_hint = parsed.get("project_hint", "")
482
531
  timestamp = parsed.get("ended_at", "")[:16].replace("T", " ")
483
- run_name = f"{source}/{timestamp}"
484
532
 
485
- run_data = {
486
- "project": project,
487
- "display_name": run_name,
488
- "config": {
489
- "source": source,
490
- "session_file": parsed.get("session_file", ""),
491
- "project_hint": parsed.get("project_hint", ""),
492
- "message_count": parsed.get("message_count", 0),
493
- "user_message_count": parsed.get("user_message_count", 0),
494
- "tools_used": parsed.get("tools_used", []),
495
- "total_tokens": parsed.get("total_tokens", 0),
496
- },
497
- "tags": [f"source:{source}", "ai-session", f"visibility:{visibility}"],
533
+ if project_hint:
534
+ short_hint = project_hint.rsplit("/", 1)[-1] if "/" in project_hint else project_hint
535
+ title = f"{source}/{short_hint}/{timestamp}"
536
+ else:
537
+ title = f"{source}/{timestamp}"
538
+
539
+ group_key = f"{source}:{project_hint}" if project_hint else f"{source}:default"
540
+
541
+ session_data = {
542
+ "title": title,
543
+ "source": source,
544
+ "worktree": project_hint,
545
+ "group_key": group_key,
546
+ "summary": parsed.get("summary"),
498
547
  "notes": _format_session_notes(parsed),
499
- "state": "finished",
548
+ "first_message": parsed.get("first_message"),
549
+ "messages": parsed.get("messages"),
550
+ "message_count": parsed.get("message_count", 0),
551
+ "user_message_count": parsed.get("user_message_count", 0),
552
+ "total_tokens": parsed.get("total_tokens", 0),
553
+ "tools_used": parsed.get("tools_used", []),
554
+ "files_touched": parsed.get("files_touched", []),
555
+ "visibility": visibility,
556
+ "tags": [f"source:{source}", f"worktree:{group_key}"],
557
+ "redacted": bool(parsed.get("_redaction")),
558
+ "redaction_mode": parsed.get("_redaction", {}).get("mode") if parsed.get("_redaction") else None,
559
+ "session_file": parsed.get("session_file"),
560
+ "started_at": parsed.get("started_at"),
561
+ "ended_at": parsed.get("ended_at"),
500
562
  }
501
563
 
502
- # Add redaction metadata if applied
503
- if parsed.get("_redaction"):
504
- run_data["config"]["redacted"] = True
505
- run_data["config"]["redaction_mode"] = parsed["_redaction"]["mode"]
506
- run_data["config"]["entities_redacted"] = parsed["_redaction"]["entities_redacted"]
507
-
508
- result = client.create_run(run_data)
509
- if not result:
510
- logger.warning("Failed to create session run")
564
+ # POST to /projects/{project_id}/ai-sessions
565
+ try:
566
+ resp = client._request("post", f"/projects/{project_id}/ai-sessions", json=session_data)
567
+ if resp.status_code in (200, 201):
568
+ result = resp.json()
569
+ client.close()
570
+ return result.get("id")
571
+ else:
572
+ # Server doesn't have AI sessions endpoint yet — fall back to run
573
+ logger.info("AI sessions endpoint not available, falling back to run")
574
+ return _sync_as_run_fallback(client, parsed, project, visibility)
575
+ except Exception as e:
576
+ logger.warning(f"AI session sync failed: {e}")
511
577
  client.close()
512
578
  return None
513
579
 
514
- run_id = result.get("id")
515
580
 
516
- # Log metrics
517
- metrics = []
518
- if parsed.get("total_tokens"):
519
- metrics.append({"key": "tokens", "value": parsed["total_tokens"], "step": 1})
520
- if parsed.get("message_count"):
521
- metrics.append({"key": "messages", "value": parsed["message_count"], "step": 1})
522
- if parsed.get("user_message_count"):
523
- metrics.append({"key": "user_messages", "value": parsed["user_message_count"], "step": 1})
524
- if parsed.get("files_touched"):
525
- metrics.append({"key": "files_touched", "value": len(parsed["files_touched"]), "step": 1})
526
-
527
- if metrics and run_id:
528
- client.post_metrics(run_id, metrics)
529
-
530
- # Log files touched as summary
531
- if parsed.get("files_touched") and run_id:
532
- client.update_run(run_id, {
533
- "summary": {
534
- "files_touched": len(parsed["files_touched"]),
535
- "tools_used": len(parsed.get("tools_used", [])),
536
- "tokens": parsed.get("total_tokens", 0),
537
- }
538
- })
581
+ def _sync_as_run_fallback(client, parsed: dict, project: str, visibility: str) -> str | None:
582
+ """Fallback: create a run if AI sessions endpoint not available."""
583
+ source = parsed["source"]
584
+ timestamp = parsed.get("ended_at", "")[:16].replace("T", " ")
585
+ project_hint = parsed.get("project_hint", "")
586
+
587
+ if project_hint:
588
+ short_hint = project_hint.rsplit("/", 1)[-1] if "/" in project_hint else project_hint
589
+ run_name = f"{source}/{short_hint}/{timestamp}"
590
+ else:
591
+ run_name = f"{source}/{timestamp}"
592
+
593
+ run_data = {
594
+ "project": project,
595
+ "display_name": run_name,
596
+ "group_name": f"{source}:{project_hint}" if project_hint else f"{source}:default",
597
+ "tags": [f"source:{source}", "ai-session"],
598
+ "notes": _format_session_notes(parsed),
599
+ "state": "finished",
600
+ }
539
601
 
602
+ result = client.create_run(run_data)
540
603
  client.close()
541
- return run_id
604
+ return result.get("id") if result else None
542
605
 
543
606
 
544
607
  def _format_session_notes(parsed: dict) -> str:
545
608
  """Format parsed session into readable notes."""
546
609
  lines = []
547
- lines.append(f"# {parsed['source'].title()} Session")
610
+ source = parsed.get("source", "unknown").replace("-", " ").title()
611
+ lines.append(f"# {source} Session")
548
612
  lines.append(f"**Time:** {parsed.get('started_at', '?')[:16]} → {parsed.get('ended_at', '?')[:16]}")
613
+ if parsed.get("project_hint"):
614
+ lines.append(f"**Project:** `{parsed['project_hint']}`")
615
+ lines.append(f"**Tokens:** {parsed.get('total_tokens', 0):,}")
549
616
  lines.append("")
550
617
 
551
618
  if parsed.get("first_message"):
552
- lines.append(f"## First Request")
619
+ lines.append("## Initial Request")
553
620
  lines.append(parsed["first_message"])
554
621
  lines.append("")
555
622
 
623
+ if parsed.get("summary"):
624
+ lines.append("## Summary")
625
+ lines.append(parsed["summary"])
626
+ lines.append("")
627
+
556
628
  if parsed.get("tools_used"):
557
629
  lines.append(f"## Tools Used ({len(parsed['tools_used'])})")
558
- for t in parsed["tools_used"][:20]:
559
- lines.append(f"- {t}")
630
+ lines.append(", ".join(parsed["tools_used"][:20]))
560
631
  lines.append("")
561
632
 
562
633
  if parsed.get("files_touched"):
563
- lines.append(f"## Files Touched ({len(parsed['files_touched'])})")
634
+ lines.append(f"## Files Modified ({len(parsed['files_touched'])})")
564
635
  for f in parsed["files_touched"][:30]:
565
636
  lines.append(f"- `{f}`")
566
637
  lines.append("")
567
638
 
568
- if parsed.get("summary"):
569
- lines.append(f"## Summary")
570
- lines.append(parsed["summary"])
639
+ # Extract key conversation flow (first 10 user messages as bullet points)
640
+ messages = parsed.get("messages", [])
641
+ user_msgs = [m["content"] for m in messages if m.get("role") == "user" and m.get("content") and len(m["content"]) > 10]
642
+ if len(user_msgs) > 2:
643
+ lines.append("## Conversation Flow")
644
+ for msg in user_msgs[:15]:
645
+ lines.append(f"- {msg[:120]}")
646
+ if len(user_msgs) > 15:
647
+ lines.append(f"- ... ({len(user_msgs) - 15} more)")
648
+ lines.append("")
571
649
 
572
650
  return "\n".join(lines)
573
651
 
@@ -680,26 +758,46 @@ def sync_all(
680
758
  HOOK_SCRIPT = '''#!/usr/bin/env python3
681
759
  """Auto-log Claude Code session to OpenRunner on exit."""
682
760
  import sys
761
+ import os
683
762
  from pathlib import Path
684
763
 
685
764
  def main():
686
- # Find the most recent session file that was just written
687
- from openrunner.session import discover_claude_sessions, parse_claude_session, sync_session_to_openrunner
765
+ try:
766
+ from openrunner.session import discover_claude_sessions, parse_claude_session, sync_session_to_openrunner
767
+
768
+ # Find the session for the CWD project (not just any recent session)
769
+ cwd = Path.cwd()
770
+ cwd_key = "-" + str(cwd).replace("/", "-").lstrip("-")
771
+ project_dir = Path.home() / ".claude" / "projects" / cwd_key
772
+
773
+ if project_dir.exists():
774
+ # Find most recent .jsonl in this project
775
+ sessions = sorted(
776
+ [f for f in project_dir.glob("*.jsonl") if f.stat().st_size > 100 and ".meta." not in f.name],
777
+ key=lambda f: f.stat().st_mtime,
778
+ reverse=True,
779
+ )
780
+ else:
781
+ # Fallback: most recent globally
782
+ sessions_info = discover_claude_sessions(since_hours=0.1)
783
+ sessions = [s["path"] for s in sessions_info]
688
784
 
689
- sessions = discover_claude_sessions(since_hours=0.1) # last 6 minutes
690
- if not sessions:
691
- return
785
+ if not sessions:
786
+ return
692
787
 
693
- latest = sessions[0]
694
- parsed = parse_claude_session(latest["path"])
788
+ latest = sessions[0]
789
+ parsed = parse_claude_session(latest)
695
790
 
696
- # Only sync if meaningful (>2 user messages)
697
- if parsed.get("user_message_count", 0) < 2:
698
- return
791
+ # Only sync if meaningful (>2 user messages)
792
+ if parsed.get("user_message_count", 0) < 2:
793
+ return
699
794
 
700
- run_id = sync_session_to_openrunner(parsed)
701
- if run_id:
702
- print(f"openrunner: Session logged as run {run_id}", file=sys.stderr)
795
+ run_id = sync_session_to_openrunner(parsed)
796
+ if run_id:
797
+ print(f"openrunner: Session logged as run {run_id}", file=sys.stderr)
798
+ except Exception as e:
799
+ # Never crash Claude Code on hook failure
800
+ print(f"openrunner hook: {e}", file=sys.stderr)
703
801
 
704
802
  if __name__ == "__main__":
705
803
  main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openrunner-sdk"
3
- version = "2.7.1"
3
+ version = "2.9.0"
4
4
  description = "OpenRunner SDK - W&B-compatible ML experiment tracking client"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -58,7 +58,7 @@ whisper = ["openai-whisper>=20231117"]
58
58
  tts = ["matplotlib>=3.7", "numpy>=1.24"]
59
59
  voice-agent = []
60
60
  forced-alignment = ["matplotlib>=3.7", "numpy>=1.24"]
61
- dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "numpy>=1.24"]
61
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=5.0", "numpy>=1.24"]
62
62
 
63
63
  [tool.hatch.build.targets.wheel]
64
64
  packages = ["openrunner"]
File without changes
File without changes
File without changes