openrunner-sdk 2.20.0__tar.gz → 2.20.2__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 (120) hide show
  1. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/PKG-INFO +1 -1
  2. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/__init__.py +1 -1
  3. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/install_commands.py +167 -54
  4. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/pyproject.toml +1 -1
  5. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/.gitignore +0 -0
  6. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/=6.0 +0 -0
  7. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/=8.1 +0 -0
  8. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/README.md +0 -0
  9. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/CLAUDE.md +0 -0
  10. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/api_client.py +0 -0
  11. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/artifact.py +0 -0
  12. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/buffer.py +0 -0
  13. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/cache.py +0 -0
  14. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/cli.py +0 -0
  15. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/config.py +0 -0
  16. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/cost.py +0 -0
  17. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/dataset.py +0 -0
  18. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/environment.py +0 -0
  19. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/evaluation.py +0 -0
  20. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/feedback.py +0 -0
  21. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/git_info.py +0 -0
  22. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/guardrails.py +0 -0
  23. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/__init__.py +0 -0
  24. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/accelerate.py +0 -0
  25. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/anthropic_tracer.py +0 -0
  26. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/catboost.py +0 -0
  27. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/diffusers.py +0 -0
  28. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/fastai.py +0 -0
  29. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/forced_alignment.py +0 -0
  30. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/gladia.py +0 -0
  31. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/gymnasium.py +0 -0
  32. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/huggingface.py +0 -0
  33. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/hydra.py +0 -0
  34. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/ignite.py +0 -0
  35. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/jax.py +0 -0
  36. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/keras.py +0 -0
  37. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/langchain.py +0 -0
  38. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/lightgbm.py +0 -0
  39. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/lightning.py +0 -0
  40. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/llamaindex.py +0 -0
  41. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/openai_finetune.py +0 -0
  42. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/openai_tracer.py +0 -0
  43. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/optuna.py +0 -0
  44. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/pytorch.py +0 -0
  45. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/sb3.py +0 -0
  46. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/sklearn.py +0 -0
  47. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/tensorflow.py +0 -0
  48. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/trl.py +0 -0
  49. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/tts.py +0 -0
  50. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/ultralytics.py +0 -0
  51. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/voice_agent.py +0 -0
  52. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/whisper.py +0 -0
  53. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/integration/xgboost.py +0 -0
  54. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/launch.py +0 -0
  55. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/mcp_server.py +0 -0
  56. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/media.py +0 -0
  57. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/migrate.py +0 -0
  58. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/model.py +0 -0
  59. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/offline.py +0 -0
  60. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/pii.py +0 -0
  61. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/plot.py +0 -0
  62. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/prompt.py +0 -0
  63. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/query_api.py +0 -0
  64. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/redact.py +0 -0
  65. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/run.py +0 -0
  66. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/scorers.py +0 -0
  67. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/sender.py +0 -0
  68. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/session.py +0 -0
  69. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/settings.py +0 -0
  70. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/summary.py +0 -0
  71. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/sweep.py +0 -0
  72. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/system_metrics.py +0 -0
  73. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/tensorboard.py +0 -0
  74. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/trace.py +0 -0
  75. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/transcript_formatter.py +0 -0
  76. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/wal.py +0 -0
  77. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/wandb_compat/__init__.py +0 -0
  78. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/wandb_compat/_shim.py +0 -0
  79. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/openrunner/wer.py +0 -0
  80. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/__init__.py +0 -0
  81. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/conftest.py +0 -0
  82. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_alert.py +0 -0
  83. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_aliases.py +0 -0
  84. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_api_client.py +0 -0
  85. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_artifact.py +0 -0
  86. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_buffer.py +0 -0
  87. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_cache.py +0 -0
  88. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_class_scorers.py +0 -0
  89. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_cli.py +0 -0
  90. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_config.py +0 -0
  91. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_evaluation.py +0 -0
  92. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_finish.py +0 -0
  93. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_git_info.py +0 -0
  94. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_init.py +0 -0
  95. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_fastai.py +0 -0
  96. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_huggingface.py +0 -0
  97. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_keras.py +0 -0
  98. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_langchain.py +0 -0
  99. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_lightning.py +0 -0
  100. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_pytorch.py +0 -0
  101. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_sklearn.py +0 -0
  102. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_integration_xgboost.py +0 -0
  103. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_launch.py +0 -0
  104. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_log.py +0 -0
  105. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_log_code.py +0 -0
  106. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_media.py +0 -0
  107. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_migrate.py +0 -0
  108. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_offline.py +0 -0
  109. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_offline_sync.py +0 -0
  110. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_pii.py +0 -0
  111. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_plot.py +0 -0
  112. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_query_api.py +0 -0
  113. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_resume.py +0 -0
  114. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_sdk_features.py +0 -0
  115. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_sender.py +0 -0
  116. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_summary.py +0 -0
  117. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_sweep.py +0 -0
  118. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_system_metrics.py +0 -0
  119. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/tests/test_trace.py +0 -0
  120. {openrunner_sdk-2.20.0 → openrunner_sdk-2.20.2}/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.20.0
3
+ Version: 2.20.2
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.20.0"
123
+ __version__ = "2.20.2"
124
124
 
125
125
  logger = logging.getLogger("openrunner")
126
126
 
@@ -791,20 +791,19 @@ allowed-tools:
791
791
  - AskUserQuestion
792
792
  ---
793
793
  <objective>
794
- Configure which training metrics appear in the Claude Code statusline footer.
795
- Fetches available metrics from the most recent run and lets the user pick.
794
+ Configure which project, run, and metrics appear in the Claude Code statusline footer.
795
+ Interactive setup: project run metrics.
796
796
  </objective>
797
797
 
798
798
  <process>
799
- 1. Fetch available metrics from the latest run in the configured project.
800
- 2. Present them to the user as a numbered list.
801
- 3. Ask which metric to use for the sparkline (primary).
802
- 4. Ask which additional metrics to show (comma-separated numbers).
803
- 5. Save to ~/.openrunner/statusline.json.
804
- 6. Confirm and show preview.
799
+ 1. List projects. Ask user which one (propose current if configured).
800
+ 2. List recent runs in that project. Ask user which run (propose latest).
801
+ 3. List available metrics for that run. Ask user which for sparkline + which extra.
802
+ 4. Save to ~/.openrunner/statusline.json.
803
+ 5. Confirm and show preview.
805
804
  </process>
806
805
 
807
- ### Step 1: Fetch available metrics
806
+ ### Step 1: List projects and runs
808
807
 
809
808
  ```bash
810
809
  python3 -c "
@@ -820,70 +819,130 @@ if not config.get('api_key'):
820
819
  print('NOT_CONFIGURED'); sys.exit(1)
821
820
 
822
821
  client = APIClient(base_url=config['base_url'], api_key=config['api_key'])
823
- pname = config.get('project', '')
822
+
823
+ # List projects
824
+ projects = client.list_projects()
825
+ print('PROJECTS:')
826
+ current = config.get('project', '')
827
+ for i, p in enumerate(projects, 1):
828
+ full = '%s/%s' % (p.get('org_name',''), p.get('name',''))
829
+ marker = ' (current)' if full.lower() == current.lower() else ''
830
+ print(' [%d] %s%s' % (i, full, marker))
831
+
832
+ # Show current statusline config
833
+ cfg_path = Path.home() / '.openrunner' / 'statusline.json'
834
+ if cfg_path.exists():
835
+ cfg = json.loads(cfg_path.read_text())
836
+ print('\\nCURRENT_CONFIG: project=%s run=%s sparkline=%s metrics=%s' % (
837
+ cfg.get('project','auto'), cfg.get('run','latest'),
838
+ cfg.get('sparkline','auto'), cfg.get('metrics',[])))
839
+ else:
840
+ print('\\nCURRENT_CONFIG: not configured')
841
+
842
+ client.close()
843
+ "
844
+ ```
845
+
846
+ ### Step 2: Ask project
847
+
848
+ Ask the user: "Which project? (number, or Enter for current)"
849
+ Default = current project.
850
+
851
+ ### Step 3: List runs for chosen project
852
+
853
+ ```bash
854
+ python3 -c "
855
+ import sys, os, json
856
+ from pathlib import Path
857
+ for p in ([os.path.join(d, 'site-packages') for d in __import__('glob').glob(os.path.expanduser('~/.local/lib/python3.*/')) + __import__('glob').glob(os.path.expanduser('~/Library/Python/3.*/lib/python/'))]):
858
+ if os.path.isdir(p): sys.path.insert(0, p)
859
+ from openrunner.session import get_session_config
860
+ from openrunner.api_client import APIClient
861
+
862
+ config = get_session_config()
863
+ client = APIClient(base_url=config['base_url'], api_key=config['api_key'])
864
+
865
+ # PROJECT_NAME will be replaced by the chosen project
866
+ pname = 'PROJECT_NAME'
824
867
  pid = None
825
868
  for p in client.list_projects():
826
- full = f'{{p.get(\"org_name\",\"\")}}/{{p.get(\"name\",\"\")}}'
869
+ full = '%s/%s' % (p.get('org_name',''), p.get('name',''))
827
870
  if full.lower() == pname.lower() or (p.get('name') or '').lower() == pname.lower():
828
871
  pid = p.get('id'); break
829
872
 
830
873
  if not pid:
831
874
  print('PROJECT_NOT_FOUND'); client.close(); sys.exit(1)
832
875
 
833
- # Get latest run
834
- resp = client._request('get', f'/projects/{{pid}}/runs', params={{'limit': 1, 'order': 'desc'}})
876
+ resp = client._request('get', '/projects/%s/runs' % pid, params={{'limit': 10, 'order': 'desc'}})
835
877
  runs_data = resp.json()
836
878
  runs = runs_data.get('runs', []) if isinstance(runs_data, dict) else runs_data
879
+
880
+ print('RUNS in %s:' % pname)
881
+ for i, r in enumerate(runs, 1):
882
+ name = r.get('display_name') or r.get('id','?')
883
+ state = r.get('state', '?')
884
+ icon = ' [running]' if state == 'running' else ''
885
+ print(' [%d] %s (%s)%s' % (i, name, r.get('id','?')[:8], icon))
886
+
837
887
  if not runs:
838
- print('NO_RUNS'); client.close(); sys.exit(1)
888
+ print(' (no runs)')
839
889
 
840
- rid = runs[0].get('id', '')
841
- run_name = runs[0].get('display_name') or rid
890
+ print('\\n [0] latest (auto-follow newest run)')
891
+ client.close()
892
+ "
893
+ ```
894
+
895
+ ### Step 4: Ask run
896
+
897
+ Ask: "Which run to track? (number, 0=latest auto-follow)" — propose 0 (latest) as default.
898
+
899
+ ### Step 5: List metrics for chosen run
900
+
901
+ ```bash
902
+ python3 -c "
903
+ import sys, os, json
904
+ from pathlib import Path
905
+ for p in ([os.path.join(d, 'site-packages') for d in __import__('glob').glob(os.path.expanduser('~/.local/lib/python3.*/')) + __import__('glob').glob(os.path.expanduser('~/Library/Python/3.*/lib/python/'))]):
906
+ if os.path.isdir(p): sys.path.insert(0, p)
907
+ from openrunner.session import get_session_config
908
+ from openrunner.api_client import APIClient
909
+
910
+ config = get_session_config()
911
+ client = APIClient(base_url=config['base_url'], api_key=config['api_key'])
842
912
 
843
- # Get all metrics for this run
844
- resp = client._request('get', f'/runs/{{rid}}/metrics', params={{'max_points': 10000}})
913
+ # RUN_ID will be replaced
914
+ rid = 'RUN_ID'
915
+ resp = client._request('get', '/runs/%s/metrics' % rid, params={{'max_points': 10000}})
845
916
  data = resp.json()
846
917
  metrics = data.get('metrics', data) if isinstance(data, dict) else {{}}
847
- if 'detail' in metrics:
848
- metrics = {{}}
918
+ if 'detail' in metrics: metrics = {{}}
849
919
 
850
920
  keys = sorted(metrics.keys())
851
- if not keys:
852
- print('NO_METRICS'); client.close(); sys.exit(1)
853
-
854
- print(f'METRICS_FROM_RUN={{run_name}}')
921
+ print('METRICS (%d available):' % len(keys))
855
922
  for i, k in enumerate(keys, 1):
856
- pts = metrics[k]
923
+ pts = metrics.get(k, [])
857
924
  n = len(pts) if isinstance(pts, list) else 0
858
- last = ''
925
+ last_str = ''
859
926
  if n > 0:
860
927
  lp = pts[-1]
861
928
  v = lp.get('value', lp) if isinstance(lp, dict) else lp
862
929
  if isinstance(v, float):
863
- last = f' (latest: {{v:.4f}}, {{n}} pts)'
930
+ last_str = ' (%.4f, %d pts)' % (v, n)
864
931
  else:
865
- last = f' (latest: {{v}}, {{n}} pts)'
866
- print(f' [{{i}}] {{k}}{{last}}')
867
-
868
- # Also show current config if exists
869
- cfg_path = Path.home() / '.openrunner' / 'statusline.json'
870
- if cfg_path.exists():
871
- cfg = json.loads(cfg_path.read_text())
872
- print('\\nCURRENT_CONFIG: sparkline=%s metrics=%s' % (cfg.get('sparkline','auto'), cfg.get('metrics',[])))
873
- else:
874
- print('\\nCURRENT_CONFIG: not configured (auto-detect)')
932
+ last_str = ' (%s, %d pts)' % (v, n)
933
+ print(' [%d] %s%s' % (i, k, last_str))
875
934
 
876
935
  client.close()
877
936
  "
878
937
  ```
879
938
 
880
- ### Step 2: Ask the user
939
+ ### Step 6: Ask metrics
881
940
 
882
- After showing the list, ask:
883
- - "Which metric for the sparkline? (number or name)" — this is the primary one shown as a mini chart
884
- - "Which additional metrics to display? (comma-separated numbers, or 'auto')"
941
+ Ask:
942
+ - "Which metric for the sparkline? (number)" — primary, shown as mini chart
943
+ - "Which additional metrics? (comma-separated numbers, or 'auto')"
885
944
 
886
- ### Step 3: Save config
945
+ ### Step 7: Save config
887
946
 
888
947
  ```bash
889
948
  python3 -c "
@@ -891,17 +950,19 @@ import json
891
950
  from pathlib import Path
892
951
  cfg_path = Path.home() / '.openrunner' / 'statusline.json'
893
952
  cfg_path.parent.mkdir(parents=True, exist_ok=True)
894
- cfg = {{'sparkline': 'SPARKLINE_KEY', 'metrics': METRICS_LIST}}
953
+ cfg = {{'project': 'PROJECT_NAME', 'run': 'RUN_VALUE', 'sparkline': 'SPARKLINE_KEY', 'metrics': METRICS_LIST}}
895
954
  cfg_path.write_text(json.dumps(cfg, indent=2))
896
- print(f'Saved: {{cfg_path}}')
897
- print(f' Sparkline: {{cfg[\"sparkline\"]}}')
898
- print(f' Metrics: {{cfg[\"metrics\"]}}')
955
+ print('Saved: %s' % cfg_path)
956
+ print(' Project: %s' % cfg['project'])
957
+ print(' Run: %s' % cfg['run'])
958
+ print(' Sparkline: %s' % cfg['sparkline'])
959
+ print(' Metrics: %s' % cfg['metrics'])
899
960
  print()
900
- print('Footer will show on next statusline refresh (30s).')
961
+ print('Footer will update on next statusline refresh (30s).')
901
962
  "
902
963
  ```
903
964
 
904
- Replace SPARKLINE_KEY with the user's chosen metric key and METRICS_LIST with the Python list of chosen keys.
965
+ Replace PROJECT_NAME, RUN_VALUE (run_id or 'latest'), SPARKLINE_KEY, and METRICS_LIST with user's choices.
905
966
  """
906
967
 
907
968
  MCP_CMD = """---
@@ -977,15 +1038,27 @@ except ImportError:
977
1038
  cfg = get_session_config()
978
1039
  if not cfg.get('api_key'): print("not configured"); sys.exit(0)
979
1040
  client = APIClient(base_url=cfg['base_url'], api_key=cfg['api_key'])
980
- pname = cfg.get('project', '')
1041
+ # Read statusline config for project/run override
1042
+ import json as _json
1043
+ sl_cfg = {}
1044
+ sl_path = os.path.expanduser('~/.openrunner/statusline.json')
1045
+ if os.path.isfile(sl_path):
1046
+ try: sl_cfg = _json.loads(open(sl_path).read())
1047
+ except: pass
1048
+ pname = sl_cfg.get('project') or cfg.get('project', '')
981
1049
  pid = None
982
1050
  for p in client.list_projects():
983
1051
  full = f"{p.get('org_name','')}/{p.get('name','')}"
984
1052
  if full.lower() == pname.lower() or (p.get('name') or '').lower() == pname.lower():
985
1053
  pid = p.get('id'); break
986
1054
  if not pid: print("?"); client.close(); sys.exit(0)
987
- runs = client._request('get', f'/projects/{pid}/runs', params={'limit': 3, 'order': 'desc'}).json()
988
- if isinstance(runs, dict): runs = runs.get('runs', [])
1055
+ # If specific run configured, only show that one
1056
+ configured_run = sl_cfg.get('run', 'latest')
1057
+ if configured_run and configured_run != 'latest':
1058
+ runs = [{'id': configured_run, 'state': 'unknown'}]
1059
+ else:
1060
+ runs = client._request('get', f'/projects/{pid}/runs', params={'limit': 3, 'order': 'desc'}).json()
1061
+ if isinstance(runs, dict): runs = runs.get('runs', [])
989
1062
  parts = []
990
1063
  for run in runs[:3]:
991
1064
  rid = run.get('id', '?'); state = run.get('state', '?')
@@ -1133,9 +1206,49 @@ When the user asks about papers, research, or sessions:
1133
1206
  statusline_script.chmod(0o755)
1134
1207
  files.append(str(statusline_script))
1135
1208
 
1136
- # Convention: OpenRunner writes to /tmp/claude-statusline-openrunner.txt
1137
- # Any statusline runner that reads /tmp/claude-statusline-*.txt will pick it up.
1138
- # No patching of other plugins — purely convention-based.
1209
+ # Auto-patch any JS statusline to read /tmp/claude-statusline-*.txt plugin files.
1210
+ # Generic: finds the statusline command from settings.json, patches if it's a node script.
1211
+ import json as _json
1212
+ settings_file = Path.home() / ".claude" / "settings.json"
1213
+ if settings_file.exists():
1214
+ try:
1215
+ settings = _json.loads(settings_file.read_text())
1216
+ sl_cmd = settings.get("statusLine", {}).get("command", "")
1217
+ # Extract JS file path from command like 'node "/path/to/script.js"'
1218
+ import shlex
1219
+ parts = shlex.split(sl_cmd) if sl_cmd else []
1220
+ js_file = None
1221
+ for p in parts:
1222
+ if p.endswith(".js") and Path(p).exists():
1223
+ js_file = Path(p)
1224
+ break
1225
+ if js_file:
1226
+ content = js_file.read_text()
1227
+ if "claude-statusline-" not in content and "// Output" in content:
1228
+ patch = """ // Plugin statusline segments: read /tmp/claude-statusline-*.txt
1229
+ let plugins = '';
1230
+ try {
1231
+ const tmpDir = os.tmpdir();
1232
+ const pfiles = fs.readdirSync(tmpDir)
1233
+ .filter(f => f.startsWith('claude-statusline-') && f.endsWith('.txt'));
1234
+ for (const f of pfiles) {
1235
+ try {
1236
+ const fp = path.join(tmpDir, f);
1237
+ const st = fs.statSync(fp);
1238
+ if ((Date.now() - st.mtimeMs) / 1000 > 120) continue;
1239
+ const c = fs.readFileSync(fp, 'utf8').trim();
1240
+ if (c && c.length < 200) plugins += ' \\u2502 ' + c;
1241
+ } catch (e) {}
1242
+ }
1243
+ } catch (e) {}
1244
+
1245
+ """
1246
+ content = content.replace("// Output", patch + " // Output")
1247
+ content = content.replace("${ctx}`)", "${ctx}${plugins}`)")
1248
+ js_file.write_text(content)
1249
+ files.append(str(js_file))
1250
+ except Exception:
1251
+ pass
1139
1252
 
1140
1253
  return files
1141
1254
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openrunner-sdk"
3
- version = "2.20.0"
3
+ version = "2.20.2"
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