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.
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/PKG-INFO +2 -1
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/install_commands.py +65 -43
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/session.py +179 -81
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/pyproject.toml +2 -2
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/.gitignore +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/=6.0 +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/=8.1 +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/README.md +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cli.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/redact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/run.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/conftest.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_cli.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.7.1 → openrunner_sdk-2.9.0}/tests/test_trace.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
# ---------------------------------------------------------------------------
|
|
@@ -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
|
-
#
|
|
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
|
-
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"
|
|
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
|
-
"
|
|
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
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
return
|
|
785
|
+
if not sessions:
|
|
786
|
+
return
|
|
692
787
|
|
|
693
|
-
|
|
694
|
-
|
|
788
|
+
latest = sessions[0]
|
|
789
|
+
parsed = parse_claude_session(latest)
|
|
695
790
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
791
|
+
# Only sync if meaningful (>2 user messages)
|
|
792
|
+
if parsed.get("user_message_count", 0) < 2:
|
|
793
|
+
return
|
|
699
794
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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.
|
|
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
|
|
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
|