openrunner-sdk 2.6.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/PKG-INFO +1 -1
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cli.py +66 -7
- openrunner_sdk-2.7.0/openrunner/install_commands.py +256 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/session.py +197 -18
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/pyproject.toml +1 -1
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/.gitignore +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/=6.0 +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/=8.1 +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/README.md +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/run.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/conftest.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_cli.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.6.0 → openrunner_sdk-2.7.0}/tests/test_wandb_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrunner-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
|
|
5
5
|
Project-URL: Homepage, https://github.com/jqueguiner/openrunner
|
|
6
6
|
Project-URL: Repository, https://github.com/jqueguiner/openrunner
|
|
@@ -120,7 +120,7 @@ launch.from_run = _launch_from_run # type: ignore[attr-defined]
|
|
|
120
120
|
# openrunner.trace.patch_openai() syntax
|
|
121
121
|
trace.patch_openai = _patch_openai # type: ignore[attr-defined]
|
|
122
122
|
|
|
123
|
-
__version__ = "2.
|
|
123
|
+
__version__ = "2.7.0"
|
|
124
124
|
|
|
125
125
|
logger = logging.getLogger("openrunner")
|
|
126
126
|
|
|
@@ -2375,16 +2375,47 @@ def session() -> None:
|
|
|
2375
2375
|
pass
|
|
2376
2376
|
|
|
2377
2377
|
|
|
2378
|
+
@session.command("setup")
|
|
2379
|
+
def session_setup() -> None:
|
|
2380
|
+
"""Interactive setup: configure API key and target project for session sync."""
|
|
2381
|
+
from openrunner.session import interactive_setup
|
|
2382
|
+
config = interactive_setup()
|
|
2383
|
+
if config:
|
|
2384
|
+
click.echo("Setup complete.")
|
|
2385
|
+
else:
|
|
2386
|
+
click.echo("Setup cancelled.")
|
|
2387
|
+
|
|
2388
|
+
|
|
2378
2389
|
@session.command("sync")
|
|
2390
|
+
@click.argument("directory", required=False, type=click.Path(exists=True))
|
|
2379
2391
|
@click.option("--hours", "-h", default=24.0, help="Look back N hours (default: 24)")
|
|
2380
|
-
@click.option("--project", "-p", default=None, help="Target project (default:
|
|
2392
|
+
@click.option("--project", "-p", default=None, help="Target project (default: from config)")
|
|
2381
2393
|
@click.option("--dry-run", is_flag=True, help="Show what would be synced without uploading")
|
|
2382
|
-
def session_sync(hours: float, project: str | None, dry_run: bool) -> None:
|
|
2383
|
-
"""Sync
|
|
2384
|
-
|
|
2394
|
+
def session_sync(directory: str | None, hours: float, project: str | None, dry_run: bool) -> None:
|
|
2395
|
+
"""Sync AI sessions to OpenRunner.
|
|
2396
|
+
|
|
2397
|
+
If DIRECTORY is given, scan that path for .jsonl/.json session files.
|
|
2398
|
+
Otherwise, scan default locations (~/.claude, ~/.codex, ~/.qwen-code).
|
|
2385
2399
|
|
|
2386
|
-
|
|
2400
|
+
On first run, prompts for API key and project selection.
|
|
2401
|
+
"""
|
|
2402
|
+
from pathlib import Path
|
|
2403
|
+
from openrunner.session import discover_all_sessions, discover_in_directory, sync_all, get_session_config, interactive_setup
|
|
2404
|
+
|
|
2405
|
+
# Check if configured — run setup if not
|
|
2406
|
+
config = get_session_config()
|
|
2407
|
+
if not config.get("api_key"):
|
|
2408
|
+
click.echo("No API key configured. Running first-time setup...")
|
|
2409
|
+
config = interactive_setup()
|
|
2410
|
+
if not config:
|
|
2411
|
+
return
|
|
2412
|
+
|
|
2413
|
+
if directory:
|
|
2414
|
+
sessions = discover_in_directory(Path(directory), since_hours=hours)
|
|
2415
|
+
else:
|
|
2387
2416
|
sessions = discover_all_sessions(hours)
|
|
2417
|
+
|
|
2418
|
+
if dry_run or not sessions:
|
|
2388
2419
|
if not sessions:
|
|
2389
2420
|
click.echo("No sessions found.")
|
|
2390
2421
|
return
|
|
@@ -2393,9 +2424,10 @@ def session_sync(hours: float, project: str | None, dry_run: bool) -> None:
|
|
|
2393
2424
|
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(s["mtime"]))
|
|
2394
2425
|
size = s["size"] / 1024
|
|
2395
2426
|
click.echo(f" [{s['source']}] {ts} ({size:.0f} KB) {s['path'].name}")
|
|
2396
|
-
|
|
2427
|
+
if dry_run:
|
|
2428
|
+
return
|
|
2397
2429
|
|
|
2398
|
-
synced = sync_all(since_hours=hours, project=project)
|
|
2430
|
+
synced = sync_all(since_hours=hours, project=project, directory=Path(directory) if directory else None)
|
|
2399
2431
|
if synced:
|
|
2400
2432
|
click.echo(f"Synced {len(synced)} session(s) to OpenRunner.")
|
|
2401
2433
|
for run_id in synced:
|
|
@@ -2442,6 +2474,33 @@ def session_list(hours: float) -> None:
|
|
|
2442
2474
|
click.echo(f" {s['source']:<12} {ts:<18} {size:<10} {status}")
|
|
2443
2475
|
|
|
2444
2476
|
|
|
2477
|
+
@main.command("install")
|
|
2478
|
+
@click.argument("tools", nargs=-1, type=click.Choice(["claude", "codex", "qwen", "opencode"]))
|
|
2479
|
+
def install_commands(tools: tuple) -> None:
|
|
2480
|
+
"""Install /openrunner slash commands for AI coding tools.
|
|
2481
|
+
|
|
2482
|
+
Without arguments, installs for all tools (Claude Code, Codex, Qwen, OpenCode).
|
|
2483
|
+
Specify tool names to install selectively.
|
|
2484
|
+
"""
|
|
2485
|
+
from openrunner.install_commands import install_all
|
|
2486
|
+
|
|
2487
|
+
targets = list(tools) if tools else None
|
|
2488
|
+
results = install_all(targets)
|
|
2489
|
+
|
|
2490
|
+
for tool_name, files in results.items():
|
|
2491
|
+
if files:
|
|
2492
|
+
click.echo(f" {tool_name}: {len(files)} file(s) installed")
|
|
2493
|
+
for f in files:
|
|
2494
|
+
click.echo(f" {f}")
|
|
2495
|
+
else:
|
|
2496
|
+
click.echo(f" {tool_name}: already up to date")
|
|
2497
|
+
|
|
2498
|
+
click.echo("")
|
|
2499
|
+
click.echo("Commands available:")
|
|
2500
|
+
click.echo(" /openrunner:sync-session — log current session to OpenRunner")
|
|
2501
|
+
click.echo(" /openrunner:log-note — save a quick research note")
|
|
2502
|
+
|
|
2503
|
+
|
|
2445
2504
|
@session.command("hook")
|
|
2446
2505
|
@click.argument("action", type=click.Choice(["install", "uninstall"]))
|
|
2447
2506
|
def session_hook(action: str) -> None:
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Install OpenRunner slash commands for AI coding tools.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Claude Code: ~/.claude/commands/openrunner/
|
|
5
|
+
- Codex (OpenAI): ~/.codex/commands/ or AGENTS.md instructions
|
|
6
|
+
- Qwen Code: ~/.qwen-code/commands/
|
|
7
|
+
- OpenCode: ~/.opencode/commands/
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
openrunner install # auto-detect and install all
|
|
11
|
+
openrunner install claude # Claude Code only
|
|
12
|
+
openrunner install codex # Codex only
|
|
13
|
+
openrunner install qwen # Qwen Code only
|
|
14
|
+
openrunner install opencode # OpenCode only
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Command templates
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
SYNC_SESSION_CMD = """---
|
|
27
|
+
name: {prefix}sync-session
|
|
28
|
+
description: Sync current coding session to OpenRunner as a research log
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Sync the current session to OpenRunner. Run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -c "
|
|
35
|
+
import sys, os
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# Ensure openrunner is importable
|
|
39
|
+
for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
|
|
40
|
+
os.path.expanduser('~/.local/lib/python3.11/site-packages'),
|
|
41
|
+
os.path.expanduser('~/.local/lib/python3.10/site-packages')]:
|
|
42
|
+
if os.path.isdir(p):
|
|
43
|
+
sys.path.insert(0, p)
|
|
44
|
+
|
|
45
|
+
from openrunner.session import discover_in_directory, parse_claude_session, parse_generic_session, sync_session_to_openrunner
|
|
46
|
+
|
|
47
|
+
# Find session dir for current tool
|
|
48
|
+
session_dirs = [
|
|
49
|
+
Path.home() / '.claude' / 'projects',
|
|
50
|
+
Path.home() / '.codex' / 'sessions',
|
|
51
|
+
Path.home() / '.qwen-code' / 'sessions',
|
|
52
|
+
Path.home() / '.opencode' / 'sessions',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Find most recent session file across all sources
|
|
56
|
+
all_sessions = []
|
|
57
|
+
for d in session_dirs:
|
|
58
|
+
if d.exists():
|
|
59
|
+
for f in d.rglob('*.jsonl'):
|
|
60
|
+
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
61
|
+
all_sessions.append(f)
|
|
62
|
+
for f in d.rglob('*.json'):
|
|
63
|
+
if f.stat().st_size > 100 and '.meta.' not in f.name:
|
|
64
|
+
all_sessions.append(f)
|
|
65
|
+
|
|
66
|
+
if not all_sessions:
|
|
67
|
+
print('No sessions found.')
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
latest = max(all_sessions, key=lambda f: f.stat().st_mtime)
|
|
71
|
+
print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
|
|
72
|
+
|
|
73
|
+
if latest.suffix == '.jsonl' and '.claude' in str(latest):
|
|
74
|
+
parsed = parse_claude_session(latest)
|
|
75
|
+
else:
|
|
76
|
+
source = 'codex' if '.codex' in str(latest) else 'qwen' if '.qwen' in str(latest) else 'opencode'
|
|
77
|
+
parsed = parse_generic_session(latest, source)
|
|
78
|
+
|
|
79
|
+
print(f' Messages: {{parsed[\"message_count\"]}} ({{parsed[\"user_message_count\"]}} user)')
|
|
80
|
+
print(f' Tokens: {{parsed.get(\"total_tokens\", 0):,}}')
|
|
81
|
+
|
|
82
|
+
project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
|
|
83
|
+
run_id = sync_session_to_openrunner(parsed, project=project)
|
|
84
|
+
if run_id:
|
|
85
|
+
base = os.environ.get('OPENRUNNER_BASE_URL', 'https://openrun.gladia.io')
|
|
86
|
+
print(f'Synced -> {{base}}/runs/{{run_id}}')
|
|
87
|
+
else:
|
|
88
|
+
print('Failed. Run: openrunner login')
|
|
89
|
+
"
|
|
90
|
+
```
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
LOG_NOTE_CMD = """---
|
|
94
|
+
name: {prefix}log-note
|
|
95
|
+
description: Log a research note to OpenRunner
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
Log the user's note text to OpenRunner as a quick research entry. Run:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python3 -c "
|
|
102
|
+
import os, sys, json
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
|
|
106
|
+
os.path.expanduser('~/.local/lib/python3.11/site-packages')]:
|
|
107
|
+
if os.path.isdir(p):
|
|
108
|
+
sys.path.insert(0, p)
|
|
109
|
+
|
|
110
|
+
from openrunner.api_client import APIClient
|
|
111
|
+
|
|
112
|
+
settings_file = Path.home() / '.openrunner' / 'settings.json'
|
|
113
|
+
settings = json.loads(settings_file.read_text()) if settings_file.exists() else {{}}
|
|
114
|
+
api_key = os.environ.get('OPENRUNNER_API_KEY') or settings.get('api_key')
|
|
115
|
+
base_url = os.environ.get('OPENRUNNER_BASE_URL') or settings.get('base_url', 'https://openrun.gladia.io')
|
|
116
|
+
project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
|
|
117
|
+
|
|
118
|
+
note = '''$ARGUMENTS'''
|
|
119
|
+
|
|
120
|
+
client = APIClient(base_url=base_url, api_key=api_key)
|
|
121
|
+
result = client.create_run({{
|
|
122
|
+
'project': project,
|
|
123
|
+
'display_name': f'note: {{note[:50]}}',
|
|
124
|
+
'notes': note,
|
|
125
|
+
'tags': ['note', 'quick'],
|
|
126
|
+
'state': 'finished',
|
|
127
|
+
}})
|
|
128
|
+
if result:
|
|
129
|
+
print(f'Note logged: run {{result[\"id\"]}}')
|
|
130
|
+
else:
|
|
131
|
+
print('Failed. Run: openrunner login')
|
|
132
|
+
client.close()
|
|
133
|
+
"
|
|
134
|
+
```
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Installers per tool
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def install_claude_code() -> list[str]:
|
|
143
|
+
"""Install commands in ~/.claude/commands/openrunner/."""
|
|
144
|
+
cmd_dir = Path.home() / ".claude" / "commands" / "openrunner"
|
|
145
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
files = []
|
|
148
|
+
|
|
149
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
150
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
151
|
+
files.append(str(sync_path))
|
|
152
|
+
|
|
153
|
+
note_path = cmd_dir / "log-note.md"
|
|
154
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
155
|
+
files.append(str(note_path))
|
|
156
|
+
|
|
157
|
+
return files
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def install_codex() -> list[str]:
|
|
161
|
+
"""Install commands for Codex CLI."""
|
|
162
|
+
# Codex uses ~/.codex/ with instructions or custom commands
|
|
163
|
+
cmd_dir = Path.home() / ".codex" / "commands"
|
|
164
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
files = []
|
|
167
|
+
|
|
168
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
169
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
170
|
+
files.append(str(sync_path))
|
|
171
|
+
|
|
172
|
+
note_path = cmd_dir / "log-note.md"
|
|
173
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
174
|
+
files.append(str(note_path))
|
|
175
|
+
|
|
176
|
+
# Also add to AGENTS.md if it exists
|
|
177
|
+
agents_file = Path.home() / ".codex" / "AGENTS.md"
|
|
178
|
+
instruction = "\n\n## OpenRunner Integration\nUse `/openrunner:sync-session` to log this session to OpenRunner.\nUse `/openrunner:log-note <text>` to save a research note.\n"
|
|
179
|
+
if agents_file.exists():
|
|
180
|
+
content = agents_file.read_text()
|
|
181
|
+
if "openrunner" not in content.lower():
|
|
182
|
+
agents_file.write_text(content + instruction)
|
|
183
|
+
files.append(str(agents_file))
|
|
184
|
+
else:
|
|
185
|
+
agents_file.write_text("# Codex Agent Instructions" + instruction)
|
|
186
|
+
files.append(str(agents_file))
|
|
187
|
+
|
|
188
|
+
return files
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def install_qwen_code() -> list[str]:
|
|
192
|
+
"""Install commands for Qwen Code."""
|
|
193
|
+
cmd_dir = Path.home() / ".qwen-code" / "commands"
|
|
194
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
files = []
|
|
197
|
+
|
|
198
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
199
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
200
|
+
files.append(str(sync_path))
|
|
201
|
+
|
|
202
|
+
note_path = cmd_dir / "log-note.md"
|
|
203
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
204
|
+
files.append(str(note_path))
|
|
205
|
+
|
|
206
|
+
return files
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def install_opencode() -> list[str]:
|
|
210
|
+
"""Install commands for OpenCode."""
|
|
211
|
+
cmd_dir = Path.home() / ".opencode" / "commands"
|
|
212
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
files = []
|
|
215
|
+
|
|
216
|
+
sync_path = cmd_dir / "sync-session.md"
|
|
217
|
+
sync_path.write_text(SYNC_SESSION_CMD.format(prefix="openrunner:"))
|
|
218
|
+
files.append(str(sync_path))
|
|
219
|
+
|
|
220
|
+
note_path = cmd_dir / "log-note.md"
|
|
221
|
+
note_path.write_text(LOG_NOTE_CMD.format(prefix="openrunner:"))
|
|
222
|
+
files.append(str(note_path))
|
|
223
|
+
|
|
224
|
+
return files
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Main installer
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
TOOLS = {
|
|
232
|
+
"claude": ("Claude Code", install_claude_code),
|
|
233
|
+
"codex": ("Codex", install_codex),
|
|
234
|
+
"qwen": ("Qwen Code", install_qwen_code),
|
|
235
|
+
"opencode": ("OpenCode", install_opencode),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def install_all(targets: list[str] | None = None) -> dict[str, list[str]]:
|
|
240
|
+
"""Install commands for specified tools (or all if None).
|
|
241
|
+
|
|
242
|
+
Returns dict of tool_name -> list of installed file paths.
|
|
243
|
+
"""
|
|
244
|
+
results = {}
|
|
245
|
+
|
|
246
|
+
if targets:
|
|
247
|
+
to_install = [(k, v) for k, v in TOOLS.items() if k in targets]
|
|
248
|
+
else:
|
|
249
|
+
# Auto-detect: install for all tools
|
|
250
|
+
to_install = list(TOOLS.items())
|
|
251
|
+
|
|
252
|
+
for key, (name, installer) in to_install:
|
|
253
|
+
files = installer()
|
|
254
|
+
results[name] = files
|
|
255
|
+
|
|
256
|
+
return results
|
|
@@ -72,13 +72,16 @@ def discover_claude_sessions(since_hours: float = 24) -> list[dict]:
|
|
|
72
72
|
|
|
73
73
|
cutoff = time.time() - (since_hours * 3600)
|
|
74
74
|
for jsonl in CLAUDE_CODE_DIR.rglob("*.jsonl"):
|
|
75
|
-
|
|
75
|
+
stat = jsonl.stat()
|
|
76
|
+
if stat.st_size < 100 or stat.st_mtime < cutoff:
|
|
77
|
+
continue
|
|
78
|
+
if ".meta." in jsonl.name:
|
|
76
79
|
continue
|
|
77
80
|
sessions.append({
|
|
78
81
|
"source": "claude-code",
|
|
79
82
|
"path": jsonl,
|
|
80
|
-
"mtime":
|
|
81
|
-
"size":
|
|
83
|
+
"mtime": stat.st_mtime,
|
|
84
|
+
"size": stat.st_size,
|
|
82
85
|
})
|
|
83
86
|
return sorted(sessions, key=lambda s: s["mtime"], reverse=True)
|
|
84
87
|
|
|
@@ -269,6 +272,147 @@ def parse_generic_session(path: Path, source: str) -> dict[str, Any]:
|
|
|
269
272
|
# ---------------------------------------------------------------------------
|
|
270
273
|
|
|
271
274
|
|
|
275
|
+
def get_session_config(interactive: bool = False) -> dict:
|
|
276
|
+
"""Load or interactively configure session sync settings.
|
|
277
|
+
|
|
278
|
+
Config stored in ~/.openrunner/session_config.json:
|
|
279
|
+
{
|
|
280
|
+
"api_key": "or_...",
|
|
281
|
+
"base_url": "https://...",
|
|
282
|
+
"project": "org/project-name"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Per-worktree overrides in .openrunner/session.json (local to cwd).
|
|
286
|
+
"""
|
|
287
|
+
global_config_file = Path.home() / ".openrunner" / "session_config.json"
|
|
288
|
+
local_config_file = Path.cwd() / ".openrunner" / "session.json"
|
|
289
|
+
global_settings_file = Path.home() / ".openrunner" / "settings.json"
|
|
290
|
+
|
|
291
|
+
# Load cascade: local > global session config > global settings > env
|
|
292
|
+
config: dict[str, Any] = {}
|
|
293
|
+
|
|
294
|
+
# Global settings (login credentials)
|
|
295
|
+
if global_settings_file.exists():
|
|
296
|
+
try:
|
|
297
|
+
config.update(json.loads(global_settings_file.read_text()))
|
|
298
|
+
except (json.JSONDecodeError, OSError):
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
# Global session config
|
|
302
|
+
if global_config_file.exists():
|
|
303
|
+
try:
|
|
304
|
+
config.update(json.loads(global_config_file.read_text()))
|
|
305
|
+
except (json.JSONDecodeError, OSError):
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
# Local override
|
|
309
|
+
if local_config_file.exists():
|
|
310
|
+
try:
|
|
311
|
+
config.update(json.loads(local_config_file.read_text()))
|
|
312
|
+
except (json.JSONDecodeError, OSError):
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
# Env vars take precedence
|
|
316
|
+
if os.environ.get("OPENRUNNER_API_KEY"):
|
|
317
|
+
config["api_key"] = os.environ["OPENRUNNER_API_KEY"]
|
|
318
|
+
if os.environ.get("OPENRUNNER_BASE_URL"):
|
|
319
|
+
config["base_url"] = os.environ["OPENRUNNER_BASE_URL"]
|
|
320
|
+
if os.environ.get("OPENRUNNER_SESSION_PROJECT"):
|
|
321
|
+
config["project"] = os.environ["OPENRUNNER_SESSION_PROJECT"]
|
|
322
|
+
|
|
323
|
+
return config
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def save_session_config(config: dict, local: bool = False) -> None:
|
|
327
|
+
"""Save session config globally or locally."""
|
|
328
|
+
if local:
|
|
329
|
+
config_file = Path.cwd() / ".openrunner" / "session.json"
|
|
330
|
+
else:
|
|
331
|
+
config_file = Path.home() / ".openrunner" / "session_config.json"
|
|
332
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
333
|
+
config_file.write_text(json.dumps(config, indent=2))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def interactive_setup() -> dict:
|
|
337
|
+
"""Interactive first-run setup: prompt for API key and project selection.
|
|
338
|
+
|
|
339
|
+
Returns config dict with api_key, base_url, project.
|
|
340
|
+
"""
|
|
341
|
+
from openrunner.api_client import APIClient
|
|
342
|
+
|
|
343
|
+
config = get_session_config()
|
|
344
|
+
|
|
345
|
+
# Step 1: API key
|
|
346
|
+
api_key = config.get("api_key")
|
|
347
|
+
base_url = config.get("base_url", "https://openrun.gladia.io")
|
|
348
|
+
|
|
349
|
+
if not api_key:
|
|
350
|
+
print("OpenRunner session sync — first-time setup")
|
|
351
|
+
print("=" * 50)
|
|
352
|
+
api_key = input("API Key (from OpenRunner settings): ").strip()
|
|
353
|
+
if not api_key:
|
|
354
|
+
print("No API key provided. Aborting.")
|
|
355
|
+
return {}
|
|
356
|
+
base_url_input = input(f"Server URL [{base_url}]: ").strip()
|
|
357
|
+
if base_url_input:
|
|
358
|
+
base_url = base_url_input
|
|
359
|
+
|
|
360
|
+
# Step 2: Verify key works
|
|
361
|
+
client = APIClient(base_url=base_url, api_key=api_key)
|
|
362
|
+
try:
|
|
363
|
+
resp = client._request("get", "/auth/me")
|
|
364
|
+
if resp.status_code != 200:
|
|
365
|
+
print(f"API key invalid (HTTP {resp.status_code}). Check your key.")
|
|
366
|
+
client.close()
|
|
367
|
+
return {}
|
|
368
|
+
user_info = resp.json()
|
|
369
|
+
print(f"Authenticated as: {user_info.get('display_name', user_info.get('email', '?'))}")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
print(f"Connection failed: {e}")
|
|
372
|
+
client.close()
|
|
373
|
+
return {}
|
|
374
|
+
|
|
375
|
+
# Step 3: Select project
|
|
376
|
+
project = config.get("project")
|
|
377
|
+
if not project:
|
|
378
|
+
print("\nSelect project for session logs:")
|
|
379
|
+
projects = client.list_projects()
|
|
380
|
+
if not projects:
|
|
381
|
+
print("No projects found. Create one in OpenRunner first.")
|
|
382
|
+
client.close()
|
|
383
|
+
return {}
|
|
384
|
+
|
|
385
|
+
for i, p in enumerate(projects, 1):
|
|
386
|
+
org = p.get("org_name", p.get("organization", {}).get("name", "?"))
|
|
387
|
+
name = p.get("name", "?")
|
|
388
|
+
print(f" [{i}] {org}/{name}")
|
|
389
|
+
|
|
390
|
+
choice = input(f"\nProject number [1-{len(projects)}]: ").strip()
|
|
391
|
+
try:
|
|
392
|
+
idx = int(choice) - 1
|
|
393
|
+
p = projects[idx]
|
|
394
|
+
org = p.get("org_name", p.get("organization", {}).get("name", ""))
|
|
395
|
+
project = f"{org}/{p['name']}" if org else p["name"]
|
|
396
|
+
except (ValueError, IndexError):
|
|
397
|
+
print("Invalid selection.")
|
|
398
|
+
client.close()
|
|
399
|
+
return {}
|
|
400
|
+
|
|
401
|
+
client.close()
|
|
402
|
+
|
|
403
|
+
config_out = {
|
|
404
|
+
"api_key": api_key,
|
|
405
|
+
"base_url": base_url,
|
|
406
|
+
"project": project,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Save
|
|
410
|
+
save_session_config(config_out)
|
|
411
|
+
print(f"\nConfig saved. Sessions will sync to: {project}")
|
|
412
|
+
print(f"Config file: ~/.openrunner/session_config.json")
|
|
413
|
+
return config_out
|
|
414
|
+
|
|
415
|
+
|
|
272
416
|
def sync_session_to_openrunner(
|
|
273
417
|
parsed: dict[str, Any],
|
|
274
418
|
project: str | None = None,
|
|
@@ -281,25 +425,18 @@ def sync_session_to_openrunner(
|
|
|
281
425
|
"""
|
|
282
426
|
from openrunner.api_client import APIClient
|
|
283
427
|
|
|
284
|
-
# Load
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if settings_file.exists():
|
|
289
|
-
try:
|
|
290
|
-
settings = json.loads(settings_file.read_text())
|
|
291
|
-
except (json.JSONDecodeError, OSError):
|
|
292
|
-
pass
|
|
293
|
-
api_key = api_key or os.environ.get("OPENRUNNER_API_KEY") or settings.get("api_key")
|
|
294
|
-
base_url = base_url or os.environ.get("OPENRUNNER_BASE_URL") or settings.get("base_url")
|
|
428
|
+
# Load config (cascade: args > env > session_config > settings)
|
|
429
|
+
config = get_session_config()
|
|
430
|
+
api_key = api_key or config.get("api_key")
|
|
431
|
+
base_url = base_url or config.get("base_url")
|
|
295
432
|
|
|
296
433
|
if not api_key or not base_url:
|
|
297
434
|
logger.warning("No API key or base URL configured. Run 'openrunner login' first.")
|
|
298
435
|
return None
|
|
299
436
|
|
|
300
|
-
# Determine project
|
|
437
|
+
# Determine project from config cascade
|
|
301
438
|
if not project:
|
|
302
|
-
project =
|
|
439
|
+
project = config.get("project") or "research-sessions"
|
|
303
440
|
|
|
304
441
|
client = APIClient(base_url=base_url, api_key=api_key)
|
|
305
442
|
|
|
@@ -397,8 +534,49 @@ def _format_session_notes(parsed: dict) -> str:
|
|
|
397
534
|
# ---------------------------------------------------------------------------
|
|
398
535
|
|
|
399
536
|
|
|
537
|
+
def discover_in_directory(directory: Path, since_hours: float = 24) -> list[dict]:
|
|
538
|
+
"""Discover session files in a given directory (recursive).
|
|
539
|
+
|
|
540
|
+
Detects source by path heuristics:
|
|
541
|
+
- .claude in path → claude-code
|
|
542
|
+
- .codex in path → codex
|
|
543
|
+
- .qwen in path → qwen
|
|
544
|
+
- Otherwise → generic
|
|
545
|
+
"""
|
|
546
|
+
sessions = []
|
|
547
|
+
cutoff = time.time() - (since_hours * 3600)
|
|
548
|
+
|
|
549
|
+
for pattern in ("**/*.jsonl", "**/*.json"):
|
|
550
|
+
for f in directory.glob(pattern):
|
|
551
|
+
stat = f.stat()
|
|
552
|
+
# Skip empty files, meta files, and old files
|
|
553
|
+
if stat.st_size < 100 or stat.st_mtime < cutoff:
|
|
554
|
+
continue
|
|
555
|
+
if ".meta." in f.name:
|
|
556
|
+
continue
|
|
557
|
+
# Detect source from path
|
|
558
|
+
path_str = str(f).lower()
|
|
559
|
+
if ".claude" in path_str:
|
|
560
|
+
source = "claude-code"
|
|
561
|
+
elif ".codex" in path_str:
|
|
562
|
+
source = "codex"
|
|
563
|
+
elif ".qwen" in path_str:
|
|
564
|
+
source = "qwen"
|
|
565
|
+
elif "openai" in path_str or "chatgpt" in path_str:
|
|
566
|
+
source = "chatgpt"
|
|
567
|
+
else:
|
|
568
|
+
source = "claude-code" # default — most common format
|
|
569
|
+
sessions.append({
|
|
570
|
+
"source": source,
|
|
571
|
+
"path": f,
|
|
572
|
+
"mtime": stat.st_mtime,
|
|
573
|
+
"size": stat.st_size,
|
|
574
|
+
})
|
|
575
|
+
return sorted(sessions, key=lambda s: s["mtime"], reverse=True)
|
|
576
|
+
|
|
577
|
+
|
|
400
578
|
def discover_all_sessions(since_hours: float = 24) -> list[dict]:
|
|
401
|
-
"""Discover sessions from all sources."""
|
|
579
|
+
"""Discover sessions from all default sources."""
|
|
402
580
|
all_sessions = []
|
|
403
581
|
all_sessions.extend(discover_claude_sessions(since_hours))
|
|
404
582
|
all_sessions.extend(discover_codex_sessions(since_hours))
|
|
@@ -411,10 +589,11 @@ def sync_all(
|
|
|
411
589
|
since_hours: float = 24,
|
|
412
590
|
project: str | None = None,
|
|
413
591
|
dry_run: bool = False,
|
|
592
|
+
directory: Path | None = None,
|
|
414
593
|
) -> list[str]:
|
|
415
594
|
"""Discover and sync all new sessions. Returns list of synced run IDs."""
|
|
416
595
|
state = _load_sync_state()
|
|
417
|
-
sessions = discover_all_sessions(since_hours)
|
|
596
|
+
sessions = discover_in_directory(directory, since_hours) if directory else discover_all_sessions(since_hours)
|
|
418
597
|
synced_ids = []
|
|
419
598
|
|
|
420
599
|
for session_info in sessions:
|
|
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
|