scios-cli 0.1.9__tar.gz → 0.3.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 (58) hide show
  1. {scios_cli-0.1.9 → scios_cli-0.3.2}/PKG-INFO +43 -9
  2. {scios_cli-0.1.9 → scios_cli-0.3.2}/README.md +34 -0
  3. {scios_cli-0.1.9 → scios_cli-0.3.2}/pyproject.toml +12 -9
  4. scios_cli-0.3.2/scios_cli/__init__.py +2 -0
  5. scios_cli-0.3.2/scios_cli/ag/__init__.py +17 -0
  6. scios_cli-0.3.2/scios_cli/ag/api_client.py +276 -0
  7. scios_cli-0.3.2/scios_cli/ag/create_flow.py +213 -0
  8. scios_cli-0.3.2/scios_cli/ag/dashboards.py +116 -0
  9. scios_cli-0.3.2/scios_cli/ag/local_runner.py +406 -0
  10. scios_cli-0.3.2/scios_cli/ag/manifest.py +197 -0
  11. scios_cli-0.3.2/scios_cli/ag/plugin.py +283 -0
  12. scios_cli-0.3.2/scios_cli/ag/scaffold.py +350 -0
  13. scios_cli-0.3.2/scios_cli/ag/sync_core.py +314 -0
  14. scios_cli-0.3.2/scios_cli/app.py +243 -0
  15. scios_cli-0.3.2/scios_cli/auth_store.py +104 -0
  16. scios_cli-0.3.2/scios_cli/commands/__init__.py +1 -0
  17. scios_cli-0.3.2/scios_cli/commands/ag.py +1312 -0
  18. scios_cli-0.3.2/scios_cli/commands/agent.py +137 -0
  19. scios_cli-0.3.2/scios_cli/commands/bench.py +796 -0
  20. scios_cli-0.3.2/scios_cli/commands/chat.py +108 -0
  21. scios_cli-0.3.2/scios_cli/commands/db.py +482 -0
  22. scios_cli-0.3.2/scios_cli/commands/deploy.py +189 -0
  23. scios_cli-0.3.2/scios_cli/commands/export.py +149 -0
  24. scios_cli-0.3.2/scios_cli/commands/houdini.py +574 -0
  25. scios_cli-0.3.2/scios_cli/commands/langfuse.py +133 -0
  26. scios_cli-0.3.2/scios_cli/commands/login.py +407 -0
  27. scios_cli-0.3.2/scios_cli/commands/logs.py +91 -0
  28. scios_cli-0.3.2/scios_cli/commands/profile.py +263 -0
  29. scios_cli-0.3.2/scios_cli/commands/run.py +748 -0
  30. scios_cli-0.3.2/scios_cli/commands/setup.py +427 -0
  31. scios_cli-0.3.2/scios_cli/commands/sync.py +66 -0
  32. scios_cli-0.3.2/scios_cli/commands/train.py +289 -0
  33. scios_cli-0.3.2/scios_cli/commands/trust.py +104 -0
  34. scios_cli-0.3.2/scios_cli/commands/video.py +100 -0
  35. scios_cli-0.3.2/scios_cli/commands/workspace.py +197 -0
  36. scios_cli-0.3.2/scios_cli/config.py +351 -0
  37. scios_cli-0.3.2/scios_cli/houdini.py +222 -0
  38. scios_cli-0.3.2/scios_cli/houdini_recorder.py +261 -0
  39. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli/main.py +176 -10
  40. scios_cli-0.3.2/scios_cli/mcp/__init__.py +32 -0
  41. scios_cli-0.3.2/scios_cli/mcp/server_core.py +138 -0
  42. scios_cli-0.3.2/scios_cli/mcp/server_maxwell.py +115 -0
  43. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli.egg-info/PKG-INFO +43 -9
  44. scios_cli-0.3.2/scios_cli.egg-info/SOURCES.txt +52 -0
  45. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli.egg-info/entry_points.txt +2 -0
  46. scios_cli-0.3.2/scios_cli.egg-info/requires.txt +11 -0
  47. scios_cli-0.3.2/setup.py +26 -0
  48. scios_cli-0.3.2/tests/test_ag.py +226 -0
  49. scios_cli-0.1.9/scios_cli/__init__.py +0 -2
  50. scios_cli-0.1.9/scios_cli.egg-info/SOURCES.txt +0 -14
  51. scios_cli-0.1.9/scios_cli.egg-info/requires.txt +0 -2
  52. scios_cli-0.1.9/setup.py +0 -19
  53. {scios_cli-0.1.9 → scios_cli-0.3.2}/LICENSE +0 -0
  54. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli/__main__.py +0 -0
  55. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli/install.py +0 -0
  56. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli.egg-info/dependency_links.txt +0 -0
  57. {scios_cli-0.1.9 → scios_cli-0.3.2}/scios_cli.egg-info/top_level.txt +0 -0
  58. {scios_cli-0.1.9 → scios_cli-0.3.2}/setup.cfg +0 -0
@@ -1,31 +1,31 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scios-cli
3
- Version: 0.1.9
4
- Summary: Scios Local Agent Workspace Sync CLI — bridge your terminal to the Scios Cloud backend
3
+ Version: 0.3.2
4
+ Summary: Scios unified CLI — dev environment, deployment, database, logs, and more
5
5
  Author: Scios Team
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/google-octo/science-studio
8
8
  Project-URL: Repository, https://github.com/google-octo/science-studio
9
- Project-URL: Documentation, https://github.com/google-octo/science-studio/tree/main/packages/cli
10
9
  Keywords: scios,cli,agent,sync,science,workflow
11
10
  Classifier: Development Status :: 4 - Beta
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: Intended Audience :: Science/Research
14
13
  Classifier: License :: OSI Approved :: Apache Software License
15
14
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
15
  Classifier: Topic :: Scientific/Engineering
22
- Classifier: Topic :: Software Development :: Libraries
23
16
  Classifier: Operating System :: OS Independent
24
17
  Requires-Python: >=3.9
25
18
  Description-Content-Type: text/markdown
26
19
  License-File: LICENSE
20
+ Requires-Dist: typer[all]>=0.12.0
21
+ Requires-Dist: rich>=13.0.0
22
+ Requires-Dist: questionary>=2.0.0
27
23
  Requires-Dist: requests>=2.31.0
28
24
  Requires-Dist: watchdog>=3.0.0
25
+ Requires-Dist: python-dotenv>=1.0.0
26
+ Requires-Dist: tomli>=2.0.0; python_version < "3.11"
27
+ Requires-Dist: tomli-w>=1.0.0
28
+ Requires-Dist: mcp>=1.0.0
29
29
  Dynamic: license-file
30
30
 
31
31
  # Scios CLI 🚀
@@ -105,6 +105,40 @@ scios-sync 1234-5678-your-idea-id --endpoint "https://api.scios.ai/v1" --token "
105
105
  ```
106
106
  Passing `--local` directly triggers the Cloud APIs to launch that pipeline on the heavy GPUs immediately without ever having to touch a web button.
107
107
 
108
+ ### 4. Record the Houdini app while it runs (`--record-video`)
109
+ When your workflow drives a **local** application — for example controlling Houdini through the Houdini MCP — you can have `scios-sync` screen-record the desktop locally for the entire execution, producing a single `.mp4` of the run.
110
+
111
+ ```bash
112
+ scios-sync <idea_id> \
113
+ --endpoint "https://api.scios.ai/v1" \
114
+ --token "eyJh..." \
115
+ --local \
116
+ --record-video
117
+ ```
118
+
119
+ Behaviour:
120
+ 1. Starts an `ffmpeg` screen capture **before** posting the execution (no missed frames).
121
+ 2. Triggers the workflow as usual and prints the `execution_id`.
122
+ 3. Polls `GET /workflows/executions/{execution_id}` every `--poll-interval` seconds (default 2s), printing each status transition.
123
+ 4. When the execution reaches a terminal status (`completed`, `failed`, `cancelled`, `error`, `stopped`), sends `q` to ffmpeg so it finalises the mp4 cleanly.
124
+ 5. Renames the file to `recordings/execution_<execution_id>.mp4` next to your current directory (or writes to `--video-out` if you passed it).
125
+
126
+ Requirements:
127
+ - `ffmpeg` on `PATH` (`brew install ffmpeg` / `apt install ffmpeg`).
128
+ - macOS: grant Terminal (or your shell) **Screen Recording** permission under *System Settings → Privacy & Security*.
129
+ - Linux: an X11 (or XWayland) display reachable at `:0.0`.
130
+
131
+ Useful flags:
132
+ | Flag | Default | Notes |
133
+ |------|---------|-------|
134
+ | `--video-fps` | `24` | Frame rate of the capture. |
135
+ | `--video-out PATH` | auto | Override the mp4 output path. |
136
+ | `--ffmpeg-input "..."` | platform default | Full ffmpeg input args, e.g. `"-f avfoundation -framerate 24 -i 2:none"` to record a specific display. List devices on macOS with `ffmpeg -f avfoundation -list_devices true -i ""`. |
137
+ | `--poll-interval` | `2.0` | Seconds between execution-status polls. |
138
+ | `--max-wait` | `3600` | Hard cap on the recording length in seconds. |
139
+
140
+ Make sure the Houdini app + the `houdini_addon.py` MCP listener (port 9876) are already running before you fire the CLI — the Scios agents will reach into Houdini over MCP while the recorder is rolling.
141
+
108
142
  ---
109
143
 
110
144
  ### Manual Fallback Commands
@@ -75,6 +75,40 @@ scios-sync 1234-5678-your-idea-id --endpoint "https://api.scios.ai/v1" --token "
75
75
  ```
76
76
  Passing `--local` directly triggers the Cloud APIs to launch that pipeline on the heavy GPUs immediately without ever having to touch a web button.
77
77
 
78
+ ### 4. Record the Houdini app while it runs (`--record-video`)
79
+ When your workflow drives a **local** application — for example controlling Houdini through the Houdini MCP — you can have `scios-sync` screen-record the desktop locally for the entire execution, producing a single `.mp4` of the run.
80
+
81
+ ```bash
82
+ scios-sync <idea_id> \
83
+ --endpoint "https://api.scios.ai/v1" \
84
+ --token "eyJh..." \
85
+ --local \
86
+ --record-video
87
+ ```
88
+
89
+ Behaviour:
90
+ 1. Starts an `ffmpeg` screen capture **before** posting the execution (no missed frames).
91
+ 2. Triggers the workflow as usual and prints the `execution_id`.
92
+ 3. Polls `GET /workflows/executions/{execution_id}` every `--poll-interval` seconds (default 2s), printing each status transition.
93
+ 4. When the execution reaches a terminal status (`completed`, `failed`, `cancelled`, `error`, `stopped`), sends `q` to ffmpeg so it finalises the mp4 cleanly.
94
+ 5. Renames the file to `recordings/execution_<execution_id>.mp4` next to your current directory (or writes to `--video-out` if you passed it).
95
+
96
+ Requirements:
97
+ - `ffmpeg` on `PATH` (`brew install ffmpeg` / `apt install ffmpeg`).
98
+ - macOS: grant Terminal (or your shell) **Screen Recording** permission under *System Settings → Privacy & Security*.
99
+ - Linux: an X11 (or XWayland) display reachable at `:0.0`.
100
+
101
+ Useful flags:
102
+ | Flag | Default | Notes |
103
+ |------|---------|-------|
104
+ | `--video-fps` | `24` | Frame rate of the capture. |
105
+ | `--video-out PATH` | auto | Override the mp4 output path. |
106
+ | `--ffmpeg-input "..."` | platform default | Full ffmpeg input args, e.g. `"-f avfoundation -framerate 24 -i 2:none"` to record a specific display. List devices on macOS with `ffmpeg -f avfoundation -list_devices true -i ""`. |
107
+ | `--poll-interval` | `2.0` | Seconds between execution-status polls. |
108
+ | `--max-wait` | `3600` | Hard cap on the recording length in seconds. |
109
+
110
+ Make sure the Houdini app + the `houdini_addon.py` MCP listener (port 9876) are already running before you fire the CLI — the Scios agents will reach into Houdini over MCP while the recorder is rolling.
111
+
78
112
  ---
79
113
 
80
114
  ### Manual Fallback Commands
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scios-cli"
7
- version = "0.1.9"
8
- description = "Scios Local Agent Workspace Sync CLI — bridge your terminal to the Scios Cloud backend"
7
+ version = "0.3.2"
8
+ description = "Scios unified CLI — dev environment, deployment, database, logs, and more"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
11
11
  requires-python = ">=3.9"
@@ -19,28 +19,31 @@ classifiers = [
19
19
  "Intended Audience :: Science/Research",
20
20
  "License :: OSI Approved :: Apache Software License",
21
21
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.9",
23
- "Programming Language :: Python :: 3.10",
24
- "Programming Language :: Python :: 3.11",
25
- "Programming Language :: Python :: 3.12",
26
- "Programming Language :: Python :: 3.13",
27
22
  "Topic :: Scientific/Engineering",
28
- "Topic :: Software Development :: Libraries",
29
23
  "Operating System :: OS Independent",
30
24
  ]
31
25
  dependencies = [
26
+ "typer[all]>=0.12.0",
27
+ "rich>=13.0.0",
28
+ "questionary>=2.0.0",
32
29
  "requests>=2.31.0",
33
30
  "watchdog>=3.0.0",
31
+ "python-dotenv>=1.0.0",
32
+ "tomli>=2.0.0; python_version < '3.11'",
33
+ "tomli-w>=1.0.0",
34
+ "mcp>=1.0.0",
34
35
  ]
35
36
 
36
37
  [project.urls]
37
38
  Homepage = "https://github.com/google-octo/science-studio"
38
39
  Repository = "https://github.com/google-octo/science-studio"
39
- Documentation = "https://github.com/google-octo/science-studio/tree/main/packages/cli"
40
40
 
41
41
  [project.scripts]
42
+ scios = "scios_cli.app:main"
43
+ # Backward-compat aliases
42
44
  scios-sync = "scios_cli.main:cli_entry"
43
45
  scios-init = "scios_cli.install:cli_install_protocol"
46
+ scios-houdini = "scios_cli.houdini:cli_houdini"
44
47
 
45
48
  [tool.setuptools.packages.find]
46
49
  include = ["scios_cli*"]
@@ -0,0 +1,2 @@
1
+ """Scios CLI — unified command-line interface for the Scios science-studio platform."""
2
+ __version__ = "0.3.0"
@@ -0,0 +1,17 @@
1
+ """scios ag — Antigravity integration package.
2
+
3
+ Projects a Scios idea into a local Antigravity project folder, keeps agent code
4
+ in two-way sync with the backend, and runs workflows locally or in the cloud.
5
+
6
+ All state lives in the backend DB and is reached over the REST API (never the DB
7
+ directly) so the experience works from a plain `pip install scios-cli`.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "api_client",
13
+ "manifest",
14
+ "sync_core",
15
+ "scaffold",
16
+ "local_runner",
17
+ ]
@@ -0,0 +1,276 @@
1
+ """REST client for the Scios backend used by `scios ag`.
2
+
3
+ Single source of truth for every backend interaction the Antigravity integration
4
+ needs. Uses `requests` (already a CLI dependency) — no direct DB access, so this
5
+ works from a plain `pip install scios-cli`.
6
+
7
+ Auth: a JWT Bearer token (same as `scios sync`). Resolution order:
8
+ 1. explicit `token=` argument
9
+ 2. `SCIOS_API_TOKEN` environment variable
10
+ 3. `[auth].token` in the workspace `scios.toml`
11
+
12
+ All endpoints were verified in Phase 0 (see BUILD_PROGRESS.md).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import time
18
+ from typing import Any
19
+
20
+ import requests
21
+
22
+ DEFAULT_ENDPOINT = "https://api.scios.dev/api/v1"
23
+
24
+
25
+ class SciosAPIError(RuntimeError):
26
+ """Raised when the backend returns a non-2xx response."""
27
+
28
+ def __init__(self, status_code: int, message: str, url: str):
29
+ self.status_code = status_code
30
+ self.url = url
31
+ super().__init__(f"[{status_code}] {message} ({url})")
32
+
33
+
34
+ class SciosClient:
35
+ """Thin, typed wrapper over the Scios REST API.
36
+
37
+ `endpoint` must include the `/api/v1` suffix (matching `scios sync`), e.g.
38
+ ``https://api.scios.dev/api/v1``.
39
+ """
40
+
41
+ def __init__(self, endpoint: str = DEFAULT_ENDPOINT, token: str | None = None, timeout: float = 60.0):
42
+ self.endpoint = endpoint.rstrip("/")
43
+ self.token = token or os.environ.get("SCIOS_API_TOKEN")
44
+ self.timeout = timeout
45
+ self._session = requests.Session()
46
+
47
+ # ── low-level ──────────────────────────────────────────────────────────
48
+ @property
49
+ def _headers(self) -> dict[str, str]:
50
+ if not self.token:
51
+ raise SciosAPIError(401, "No auth token. Set SCIOS_API_TOKEN or scios.toml [auth].token", self.endpoint)
52
+ return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
53
+
54
+ def _request(self, method: str, path: str, *, json: Any = None, params: dict | None = None,
55
+ timeout: float | None = None) -> Any:
56
+ url = f"{self.endpoint}{path}"
57
+ resp = self._session.request(
58
+ method, url, json=json, params=params, headers=self._headers,
59
+ timeout=timeout or self.timeout,
60
+ )
61
+ if not resp.ok:
62
+ detail = resp.text
63
+ try:
64
+ body = resp.json()
65
+ detail = body.get("detail") or body.get("message") or detail
66
+ except Exception:
67
+ pass
68
+ raise SciosAPIError(resp.status_code, str(detail), url)
69
+ if resp.content and "application/json" in resp.headers.get("content-type", ""):
70
+ return resp.json()
71
+ return resp.content
72
+
73
+ def get(self, path: str, **kw) -> Any:
74
+ return self._request("GET", path, **kw)
75
+
76
+ def post(self, path: str, **kw) -> Any:
77
+ return self._request("POST", path, **kw)
78
+
79
+ def patch(self, path: str, **kw) -> Any:
80
+ return self._request("PATCH", path, **kw)
81
+
82
+ # ── mount / sync (Phase 1) ─────────────────────────────────────────────
83
+ def get_sync_manifest(self, idea_id: str) -> dict:
84
+ """GET /ideas/{idea_id}/sync-manifest — the one-shot mount payload.
85
+
86
+ Returns {idea_id, active_workflow_id, workflow_definition, agents[],
87
+ execution, datasets[]}.
88
+ """
89
+ return self.get(f"/ideas/{idea_id}/sync-manifest")
90
+
91
+ def update_agent_code(self, agent_id: str, code_content: str) -> dict:
92
+ """PATCH /agents/{agent_id}/code — the single write path for agent code."""
93
+ return self.patch(f"/agents/{agent_id}/code", json={"code_content": code_content})
94
+
95
+ def get_idea(self, idea_id: str) -> dict:
96
+ """GET /ideas/{idea_id} — includes build_execute_progress/status, scios_score."""
97
+ return self.get(f"/ideas/{idea_id}")
98
+
99
+ # ── creation (Phase 2) ─────────────────────────────────────────────────
100
+ def create_workspace(self, name: str, description: str, category_id: str | None = None,
101
+ metadata: dict | None = None) -> dict:
102
+ body: dict[str, Any] = {"name": name, "description": description}
103
+ if category_id:
104
+ body["categoryId"] = category_id
105
+ if metadata:
106
+ body["metadata"] = metadata
107
+ return self.post("/workspaces", json=body)
108
+
109
+ def create_research_goal(self, workspace_id: str, title: str, description: str,
110
+ details: str | None = None, target: str | None = None,
111
+ parameters: dict | None = None, original_prompt: str | None = None) -> dict:
112
+ body: dict[str, Any] = {"title": title, "description": description, "workspaceId": workspace_id}
113
+ if details:
114
+ body["details"] = details
115
+ if target:
116
+ body["target"] = target
117
+ if parameters:
118
+ body["parameters"] = parameters
119
+ if original_prompt:
120
+ body["originalPrompt"] = original_prompt
121
+ return self.post(f"/workspaces/{workspace_id}/goals", json=body)
122
+
123
+ def start_research_goal(self, workspace_id: str, goal_id: str, configuration: dict | None = None) -> dict:
124
+ """POST /workspaces/{ws}/goals/{goal}/start — generates ideas (Co-Scientist)."""
125
+ body = {"goalId": goal_id, "configuration": configuration or {}}
126
+ return self.post(f"/workspaces/{workspace_id}/goals/{goal_id}/start", json=body, timeout=300.0)
127
+
128
+ def list_session_ideas(self, session_id: str, limit: int = 20) -> list[dict]:
129
+ """GET /ideas/?session_id=... — ideas generated for a session.
130
+
131
+ Co-Scientist generates ideas asynchronously into the session; this is the
132
+ robust way to discover them (the goal/session detail endpoint is flaky).
133
+ """
134
+ resp = self.get("/ideas/", params={"session_id": session_id, "limit": limit})
135
+ if isinstance(resp, dict):
136
+ return resp.get("ideas") or resp.get("items") or []
137
+ return resp if isinstance(resp, list) else []
138
+
139
+ # ── build-execute (Phase 2) ────────────────────────────────────────────
140
+ def trigger_build_execute(self, idea_id: str, stage_config: dict | None = None) -> dict:
141
+ """POST /ideas/build-execute — start Architect→Vibing→Execute→Optimize."""
142
+ body: dict[str, Any] = {"idea_ids": [idea_id]}
143
+ if stage_config is not None:
144
+ body["stage_config"] = stage_config
145
+ return self.post("/ideas/build-execute", json=body)
146
+
147
+ def retry_build_execute(self, idea_id: str, stage_config: dict | None = None,
148
+ force_retry: bool = False, skip_optimization: bool = False) -> dict:
149
+ body: dict[str, Any] = {"forceRetry": force_retry, "skipOptimization": skip_optimization}
150
+ if stage_config is not None:
151
+ body["stage_config"] = stage_config
152
+ return self.post(f"/ideas/{idea_id}/build-execute/retry", json=body)
153
+
154
+ def cancel_build_execute(self, idea_id: str) -> dict:
155
+ return self.post(f"/ideas/{idea_id}/build-execute/cancel")
156
+
157
+ def get_build_execute_progress(self, idea_id: str) -> dict:
158
+ """Return {status, scios_score, progress} derived from GET /ideas/{id}."""
159
+ idea = self.get_idea(idea_id)
160
+ return {
161
+ "status": idea.get("buildExecuteStatus") or idea.get("build_execute_status"),
162
+ "scios_score": idea.get("sciosScore") or idea.get("scios_score"),
163
+ "progress": idea.get("buildExecuteProgress") or idea.get("build_execute_progress"),
164
+ }
165
+
166
+ def wait_for_build_execute(self, idea_id: str, poll_interval: float = 5.0,
167
+ timeout: float = 3600.0, on_progress=None) -> dict:
168
+ """Poll build_execute_progress until a terminal status or timeout.
169
+
170
+ Terminal statuses: 'completed', 'failed'. Returns the final progress dict.
171
+ """
172
+ deadline = time.time() + timeout
173
+ last_status = None
174
+ while time.time() < deadline:
175
+ prog = self.get_build_execute_progress(idea_id)
176
+ status = prog.get("status")
177
+ if on_progress and status != last_status:
178
+ on_progress(prog)
179
+ last_status = status
180
+ elif on_progress:
181
+ on_progress(prog)
182
+ if status in ("completed", "failed"):
183
+ return prog
184
+ time.sleep(poll_interval)
185
+ raise SciosAPIError(408, f"build-execute did not finish within {timeout}s", f"/ideas/{idea_id}")
186
+
187
+ # ── execution (Phase 3 cloud) ──────────────────────────────────────────
188
+ def execute_workflow(self, workflow_id: str, execution_mode: str = "worker",
189
+ inputs: dict | None = None, session_id: str | None = None,
190
+ execution_quantity: int = 1) -> dict:
191
+ """POST /workflows/{id}/execute. `--mode cloud` ⇒ execution_mode='worker'."""
192
+ body: dict[str, Any] = {
193
+ "execution_mode": execution_mode,
194
+ "execution_quantity": execution_quantity,
195
+ }
196
+ if inputs is not None:
197
+ body["inputs"] = inputs
198
+ if session_id:
199
+ body["session_id"] = session_id
200
+ return self.post(f"/workflows/{workflow_id}/execute", json=body)
201
+
202
+ def list_executions(self, workflow_id: str) -> list[dict]:
203
+ return self.get(f"/workflows/{workflow_id}/executions")
204
+
205
+ def get_execution(self, execution_id: str) -> dict:
206
+ """GET /workflows/executions/{id} — execution detail incl. results_dashboard
207
+ and result_analysis. (Route is under the /workflows prefix; a bare
208
+ /executions/{id} returns 404.)"""
209
+ return self.get(f"/workflows/executions/{execution_id}")
210
+
211
+ def get_node_data(self, execution_id: str, node_id: str, data_key: str,
212
+ data_category: str = "input") -> Any:
213
+ """GET /datasets/execution/{exec}/nodes/{node}/data — resolve one offloaded
214
+ node input/output value by its data_key (used to hydrate Zero-Copy start
215
+ inputs for a local run)."""
216
+ return self.get(
217
+ f"/datasets/execution/{execution_id}/nodes/{node_id}/data",
218
+ params={"data_category": data_category, "data_key": data_key},
219
+ )
220
+
221
+ def get_execution_trace(self, execution_id: str, slim: bool = False) -> Any:
222
+ """GET /workflows/executions/{id}/trace — hydrates _stored_in_datasets offloads."""
223
+ return self.get(f"/workflows/executions/{execution_id}/trace", params={"slim": str(slim).lower()})
224
+
225
+ def get_execution_logs(self, execution_id: str) -> Any:
226
+ return self.get(f"/workflows/executions/{execution_id}/logs")
227
+
228
+ # ── datasets (Phase 5) ─────────────────────────────────────────────────
229
+ def get_dataset_content(self, dataset_id: str) -> bytes:
230
+ return self.get(f"/datasets/{dataset_id}/content")
231
+
232
+ # ── maxwell assistant (Phase 4) ────────────────────────────────────────
233
+ def maxwell_create_session(self, workspace_id: str | None = None) -> dict:
234
+ """POST /maxwell/sessions — start a Maxwell assistant session."""
235
+ body: dict[str, Any] = {}
236
+ if workspace_id:
237
+ body["workspace_id"] = workspace_id
238
+ return self.post("/maxwell/sessions", json=body)
239
+
240
+ def suggest_all_fields(self, session_id: str, research_context: str,
241
+ title: str | None = None, description: str | None = None) -> dict:
242
+ """POST /maxwell/sessions/{id}/suggest-all-fields — the same AI auto-amend
243
+ the /dashboard/plan page uses. Turns a raw goal prompt into structured
244
+ {title, description, targetMetrics, constraints, methodology, category, ...}."""
245
+ body: dict[str, Any] = {"researchContext": research_context}
246
+ if title:
247
+ body["title"] = title
248
+ if description:
249
+ body["description"] = description
250
+ return self.post(f"/maxwell/sessions/{session_id}/suggest-all-fields", json=body, timeout=180.0)
251
+
252
+ def maxwell_query(self, session_id: str, query: str) -> dict:
253
+ """POST /maxwell/sessions/{id}/query — send a natural-language query."""
254
+ return self.post(f"/maxwell/sessions/{session_id}/query", json={"query": query}, timeout=300.0)
255
+
256
+ def maxwell_history(self, session_id: str) -> dict:
257
+ return self.get(f"/maxwell/sessions/{session_id}/history")
258
+
259
+ # ── mcp tool call (Phase 3/4) ──────────────────────────────────────────
260
+ def call_tool(self, tool_name: str, arguments: dict, server_name: str = "science",
261
+ poll_interval: float = 2.0) -> Any:
262
+ """POST /mcp/tools/call, transparently awaiting a promoted background job."""
263
+ data = self.post("/mcp/tools/call", json={
264
+ "server_name": server_name, "tool_name": tool_name, "arguments": arguments,
265
+ }, timeout=900.0)
266
+ if isinstance(data, dict) and "job_id" in data:
267
+ job_id = data["job_id"]
268
+ while True:
269
+ time.sleep(poll_interval)
270
+ job = self.get(f"/mcp/tools/status/{job_id}")
271
+ status = job.get("status")
272
+ if status == "completed":
273
+ return job.get("result")
274
+ if status == "failed":
275
+ raise SciosAPIError(500, f"tool '{tool_name}' failed: {job.get('error')}", "/mcp/tools/call")
276
+ return data