openrunner-sdk 2.7.1__tar.gz → 2.8.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.7.1 → openrunner_sdk-2.8.0}/PKG-INFO +2 -1
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/install_commands.py +65 -43
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/session.py +110 -34
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/pyproject.toml +2 -2
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/.gitignore +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/=6.0 +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/=8.1 +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/README.md +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cli.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/redact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/run.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/conftest.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_cli.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.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.8.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.
|
|
123
|
+
__version__ = "2.8.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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
|
|
65
|
+
### Step 2b: If CONFIGURED (or after setup)
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
run_id = sync_session_to_openrunner(parsed, 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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
# ---------------------------------------------------------------------------
|
|
@@ -480,21 +509,33 @@ def sync_session_to_openrunner(
|
|
|
480
509
|
# Create run
|
|
481
510
|
source = parsed["source"]
|
|
482
511
|
timestamp = parsed.get("ended_at", "")[:16].replace("T", " ")
|
|
483
|
-
|
|
512
|
+
project_hint = parsed.get("project_hint", "")
|
|
513
|
+
|
|
514
|
+
# Build display name: include worktree/project context
|
|
515
|
+
if project_hint:
|
|
516
|
+
# Shorten: /home/ubuntu/openrun → openrun
|
|
517
|
+
short_hint = project_hint.rsplit("/", 1)[-1] if "/" in project_hint else project_hint
|
|
518
|
+
run_name = f"{source}/{short_hint}/{timestamp}"
|
|
519
|
+
else:
|
|
520
|
+
run_name = f"{source}/{timestamp}"
|
|
521
|
+
|
|
522
|
+
# Group sessions by source project (worktree)
|
|
523
|
+
group_name = f"{source}:{project_hint}" if project_hint else f"{source}:default"
|
|
484
524
|
|
|
485
525
|
run_data = {
|
|
486
526
|
"project": project,
|
|
487
527
|
"display_name": run_name,
|
|
528
|
+
"group_name": group_name,
|
|
488
529
|
"config": {
|
|
489
530
|
"source": source,
|
|
490
531
|
"session_file": parsed.get("session_file", ""),
|
|
491
|
-
"project_hint":
|
|
532
|
+
"project_hint": project_hint,
|
|
492
533
|
"message_count": parsed.get("message_count", 0),
|
|
493
534
|
"user_message_count": parsed.get("user_message_count", 0),
|
|
494
535
|
"tools_used": parsed.get("tools_used", []),
|
|
495
536
|
"total_tokens": parsed.get("total_tokens", 0),
|
|
496
537
|
},
|
|
497
|
-
"tags": [f"source:{source}", "ai-session", f"visibility:{visibility}"],
|
|
538
|
+
"tags": [f"source:{source}", "ai-session", f"visibility:{visibility}", f"worktree:{group_name}"],
|
|
498
539
|
"notes": _format_session_notes(parsed),
|
|
499
540
|
"state": "finished",
|
|
500
541
|
}
|
|
@@ -544,30 +585,45 @@ def sync_session_to_openrunner(
|
|
|
544
585
|
def _format_session_notes(parsed: dict) -> str:
|
|
545
586
|
"""Format parsed session into readable notes."""
|
|
546
587
|
lines = []
|
|
547
|
-
|
|
588
|
+
source = parsed.get("source", "unknown").replace("-", " ").title()
|
|
589
|
+
lines.append(f"# {source} Session")
|
|
548
590
|
lines.append(f"**Time:** {parsed.get('started_at', '?')[:16]} → {parsed.get('ended_at', '?')[:16]}")
|
|
591
|
+
if parsed.get("project_hint"):
|
|
592
|
+
lines.append(f"**Project:** `{parsed['project_hint']}`")
|
|
593
|
+
lines.append(f"**Tokens:** {parsed.get('total_tokens', 0):,}")
|
|
549
594
|
lines.append("")
|
|
550
595
|
|
|
551
596
|
if parsed.get("first_message"):
|
|
552
|
-
lines.append(
|
|
597
|
+
lines.append("## Initial Request")
|
|
553
598
|
lines.append(parsed["first_message"])
|
|
554
599
|
lines.append("")
|
|
555
600
|
|
|
601
|
+
if parsed.get("summary"):
|
|
602
|
+
lines.append("## Summary")
|
|
603
|
+
lines.append(parsed["summary"])
|
|
604
|
+
lines.append("")
|
|
605
|
+
|
|
556
606
|
if parsed.get("tools_used"):
|
|
557
607
|
lines.append(f"## Tools Used ({len(parsed['tools_used'])})")
|
|
558
|
-
|
|
559
|
-
lines.append(f"- {t}")
|
|
608
|
+
lines.append(", ".join(parsed["tools_used"][:20]))
|
|
560
609
|
lines.append("")
|
|
561
610
|
|
|
562
611
|
if parsed.get("files_touched"):
|
|
563
|
-
lines.append(f"## Files
|
|
612
|
+
lines.append(f"## Files Modified ({len(parsed['files_touched'])})")
|
|
564
613
|
for f in parsed["files_touched"][:30]:
|
|
565
614
|
lines.append(f"- `{f}`")
|
|
566
615
|
lines.append("")
|
|
567
616
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
617
|
+
# Extract key conversation flow (first 10 user messages as bullet points)
|
|
618
|
+
messages = parsed.get("messages", [])
|
|
619
|
+
user_msgs = [m["content"] for m in messages if m.get("role") == "user" and m.get("content") and len(m["content"]) > 10]
|
|
620
|
+
if len(user_msgs) > 2:
|
|
621
|
+
lines.append("## Conversation Flow")
|
|
622
|
+
for msg in user_msgs[:15]:
|
|
623
|
+
lines.append(f"- {msg[:120]}")
|
|
624
|
+
if len(user_msgs) > 15:
|
|
625
|
+
lines.append(f"- ... ({len(user_msgs) - 15} more)")
|
|
626
|
+
lines.append("")
|
|
571
627
|
|
|
572
628
|
return "\n".join(lines)
|
|
573
629
|
|
|
@@ -680,26 +736,46 @@ def sync_all(
|
|
|
680
736
|
HOOK_SCRIPT = '''#!/usr/bin/env python3
|
|
681
737
|
"""Auto-log Claude Code session to OpenRunner on exit."""
|
|
682
738
|
import sys
|
|
739
|
+
import os
|
|
683
740
|
from pathlib import Path
|
|
684
741
|
|
|
685
742
|
def main():
|
|
686
|
-
|
|
687
|
-
|
|
743
|
+
try:
|
|
744
|
+
from openrunner.session import discover_claude_sessions, parse_claude_session, sync_session_to_openrunner
|
|
745
|
+
|
|
746
|
+
# Find the session for the CWD project (not just any recent session)
|
|
747
|
+
cwd = Path.cwd()
|
|
748
|
+
cwd_key = "-" + str(cwd).replace("/", "-").lstrip("-")
|
|
749
|
+
project_dir = Path.home() / ".claude" / "projects" / cwd_key
|
|
750
|
+
|
|
751
|
+
if project_dir.exists():
|
|
752
|
+
# Find most recent .jsonl in this project
|
|
753
|
+
sessions = sorted(
|
|
754
|
+
[f for f in project_dir.glob("*.jsonl") if f.stat().st_size > 100 and ".meta." not in f.name],
|
|
755
|
+
key=lambda f: f.stat().st_mtime,
|
|
756
|
+
reverse=True,
|
|
757
|
+
)
|
|
758
|
+
else:
|
|
759
|
+
# Fallback: most recent globally
|
|
760
|
+
sessions_info = discover_claude_sessions(since_hours=0.1)
|
|
761
|
+
sessions = [s["path"] for s in sessions_info]
|
|
688
762
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return
|
|
763
|
+
if not sessions:
|
|
764
|
+
return
|
|
692
765
|
|
|
693
|
-
|
|
694
|
-
|
|
766
|
+
latest = sessions[0]
|
|
767
|
+
parsed = parse_claude_session(latest)
|
|
695
768
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
769
|
+
# Only sync if meaningful (>2 user messages)
|
|
770
|
+
if parsed.get("user_message_count", 0) < 2:
|
|
771
|
+
return
|
|
699
772
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
773
|
+
run_id = sync_session_to_openrunner(parsed)
|
|
774
|
+
if run_id:
|
|
775
|
+
print(f"openrunner: Session logged as run {run_id}", file=sys.stderr)
|
|
776
|
+
except Exception as e:
|
|
777
|
+
# Never crash Claude Code on hook failure
|
|
778
|
+
print(f"openrunner hook: {e}", file=sys.stderr)
|
|
703
779
|
|
|
704
780
|
if __name__ == "__main__":
|
|
705
781
|
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openrunner-sdk"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.8.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|