deepparallel 0.6.0__tar.gz → 0.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.
- {deepparallel-0.6.0 → deepparallel-0.7.0}/PKG-INFO +2 -1
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/__init__.py +1 -1
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/branding.py +80 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/cli.py +206 -24
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/licensing.py +89 -24
- deepparallel-0.7.0/deepparallel/research/compound_discovery.py +321 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/serve.py +11 -9
- deepparallel-0.7.0/deepparallel/tui/__init__.py +7 -0
- deepparallel-0.7.0/deepparallel/tui/app.py +156 -0
- deepparallel-0.7.0/deepparallel/tui/renderer.py +136 -0
- deepparallel-0.7.0/deepparallel/tui/widgets/animations.py +171 -0
- deepparallel-0.7.0/deepparallel/tui/widgets/confirm.py +85 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/PKG-INFO +2 -1
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/SOURCES.txt +8 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/requires.txt +1 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/pyproject.toml +2 -1
- deepparallel-0.7.0/tests/test_cli_licensing.py +297 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_crowe_gateway_backend.py +0 -2
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_crowe_payment_required.py +0 -1
- deepparallel-0.7.0/tests/test_licensing_files.py +128 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_mcp.py +1 -1
- {deepparallel-0.6.0 → deepparallel-0.7.0}/README.md +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/agent.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/backend.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/cockpit.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/cockpit_observe.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/cockpit_panel.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/cockpit_sim.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/config.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/crowe_id.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/dsml.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/fusion.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/memory.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/mesh.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/registry.json +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/renderer.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/research/provider.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/routing.example.json +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/routing.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/session.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/git_ops.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/mcp.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/memory.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel/userinput.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/setup.cfg +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_agent.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_backend.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_branding.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_cli.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_cockpit.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_cockpit_panel.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_cockpit_sim.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_config.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_config_file.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_crowe_backend.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_crowe_id_auth.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_dsml.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_fusion.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_git_ops.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_licensing.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_memory.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_renderer.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_research.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_research_provider.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_routing.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_serve.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_serve_session.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_spinner_color.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_files.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_search.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_tools_web.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_userinput.py +0 -0
- {deepparallel-0.6.0 → deepparallel-0.7.0}/tests/test_userinput_paste.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
|
|
5
5
|
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -16,6 +16,7 @@ Requires-Dist: python-dotenv>=1.0.0
|
|
|
16
16
|
Requires-Dist: tree-sitter>=0.25.0
|
|
17
17
|
Requires-Dist: tree-sitter-language-pack>=1.8.0
|
|
18
18
|
Requires-Dist: cryptography>=42.0.0
|
|
19
|
+
Requires-Dist: textual>=0.52.0
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
21
22
|
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
@@ -173,6 +173,86 @@ def info(msg: str) -> None:
|
|
|
173
173
|
console.print(f"[{DIM}]{msg}[/]")
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def upgrade_card(feature: str, current_tier: str, needed_tier: str, unlocks: str, url: str) -> None:
|
|
177
|
+
body = Text()
|
|
178
|
+
body.append(f"{feature} ", style=f"bold {DP_ACCENT}")
|
|
179
|
+
body.append("is a ", style="white")
|
|
180
|
+
body.append(needed_tier, style=f"bold {CROWE_ACCENT}")
|
|
181
|
+
body.append(" feature. You are on ", style="white")
|
|
182
|
+
body.append(f"{current_tier}", style=AMBER_HEX)
|
|
183
|
+
body.append(".\n\n", style="white")
|
|
184
|
+
body.append("Unlocks ", style=DIM)
|
|
185
|
+
body.append(f"{unlocks}\n\n", style="white")
|
|
186
|
+
body.append(f"{ARROW} ", style=CROWE_ACCENT)
|
|
187
|
+
body.append("dp upgrade", style=f"bold {CROWE_ACCENT}")
|
|
188
|
+
body.append(f" {url}", style=DIM)
|
|
189
|
+
console.print(
|
|
190
|
+
_gutter(
|
|
191
|
+
Panel(
|
|
192
|
+
body,
|
|
193
|
+
title=Text(f"{MARK} Upgrade to {needed_tier}", style=DP_ACCENT),
|
|
194
|
+
title_align="left",
|
|
195
|
+
border_style=AMBER_HEX,
|
|
196
|
+
box=box.ROUNDED,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def license_panel(
|
|
203
|
+
title: str,
|
|
204
|
+
header_lines: list[tuple[str, str]],
|
|
205
|
+
rows: list[tuple[bool, str]],
|
|
206
|
+
footer_lines: list[str] | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
body = Text()
|
|
209
|
+
for label, value in header_lines:
|
|
210
|
+
body.append(f"{label:<9}", style=DIM)
|
|
211
|
+
body.append(f"{value}\n", style="white")
|
|
212
|
+
if rows:
|
|
213
|
+
body.append("\n")
|
|
214
|
+
for unlocked, desc in rows:
|
|
215
|
+
if unlocked:
|
|
216
|
+
body.append(f"{CHECK} ", style=GREEN_HEX)
|
|
217
|
+
body.append(f"{desc}\n", style="white")
|
|
218
|
+
else:
|
|
219
|
+
body.append(f"{CROSS} ", style=RED_HEX)
|
|
220
|
+
body.append(f"{desc}\n", style=DIM)
|
|
221
|
+
if footer_lines:
|
|
222
|
+
body.append("\n")
|
|
223
|
+
for line in footer_lines:
|
|
224
|
+
body.append(f"{line}\n", style=DIM)
|
|
225
|
+
console.print(
|
|
226
|
+
_gutter(
|
|
227
|
+
Panel(
|
|
228
|
+
body,
|
|
229
|
+
title=Text(f"{MARK} {title}", style=DP_ACCENT),
|
|
230
|
+
title_align="left",
|
|
231
|
+
border_style=DP_ACCENT,
|
|
232
|
+
box=box.ROUNDED,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def first_run_banner() -> None:
|
|
239
|
+
body = Text()
|
|
240
|
+
body.append("You're on ", style=DIM)
|
|
241
|
+
body.append("Free", style=f"bold {AMBER_HEX}")
|
|
242
|
+
body.append(". Unlock Guardian, fusion, and the ", style=DIM)
|
|
243
|
+
body.append("--deep", style="white")
|
|
244
|
+
body.append(" council: ", style=DIM)
|
|
245
|
+
body.append("dp upgrade", style=f"bold {CROWE_ACCENT}")
|
|
246
|
+
console.print(_gutter(Panel(body, border_style=DP_ACCENT, box=box.ROUNDED)))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def upgrade_link(url: str, opened: bool) -> None:
|
|
250
|
+
if opened:
|
|
251
|
+
console.print(f"[{DIM}]Opening your browser to DeepParallel pricing:[/] {url}")
|
|
252
|
+
else:
|
|
253
|
+
console.print(f"[{DIM}]Upgrade at[/] {url}")
|
|
254
|
+
|
|
255
|
+
|
|
176
256
|
def error(msg: str) -> None:
|
|
177
257
|
console.print(f"[bold red]error[/] {msg}")
|
|
178
258
|
|
|
@@ -58,6 +58,27 @@ from deepparallel.cockpit import Cockpit
|
|
|
58
58
|
from deepparallel.cockpit_observe import make_observer
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
_soft_cards_shown: set[str] = set()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def require_feature(feature: str, *, hard: bool, exit_code: int = 3) -> bool:
|
|
65
|
+
have = licensing.resolve_tier()
|
|
66
|
+
if licensing.check_feature(feature, have)[0]:
|
|
67
|
+
return True
|
|
68
|
+
if hard or feature not in _soft_cards_shown:
|
|
69
|
+
branding.upgrade_card(
|
|
70
|
+
feature,
|
|
71
|
+
have.label,
|
|
72
|
+
licensing.feature_tier(feature).label,
|
|
73
|
+
licensing.feature_unlocks(feature),
|
|
74
|
+
licensing.upgrade_url(),
|
|
75
|
+
)
|
|
76
|
+
if hard:
|
|
77
|
+
sys.exit(exit_code)
|
|
78
|
+
_soft_cards_shown.add(feature)
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def _build_messages(history: list[tuple[str, str]], system: str, user_msg: str) -> list[dict]:
|
|
62
83
|
msgs: list[dict] = [{"role": "system", "content": system}]
|
|
63
84
|
for role, content in history:
|
|
@@ -82,9 +103,7 @@ def _effective_backend(base: Backend, settings: Settings, mode: str) -> Backend:
|
|
|
82
103
|
"""
|
|
83
104
|
if mode not in ("reason", "escalate"):
|
|
84
105
|
return base
|
|
85
|
-
|
|
86
|
-
if not ok:
|
|
87
|
-
branding.info(msg)
|
|
106
|
+
if not require_feature("fusion", hard=False):
|
|
88
107
|
return base
|
|
89
108
|
reasoner = backend_for_deployment(settings, settings.reasoner_deployment)
|
|
90
109
|
if mode == "reason":
|
|
@@ -100,10 +119,7 @@ def _wrap_fusion(backend: Backend, settings: Settings) -> Backend:
|
|
|
100
119
|
def _run_deep(settings: Settings, messages: list[dict]) -> None:
|
|
101
120
|
"""Heavy multi-model fan-out + judge, one-shot. Chains are shown with
|
|
102
121
|
generic labels (no raw model names) per the brand rule. Paid (Pro+)."""
|
|
103
|
-
|
|
104
|
-
if not ok:
|
|
105
|
-
branding.error(msg)
|
|
106
|
-
sys.exit(2)
|
|
122
|
+
require_feature("deep", hard=True, exit_code=2)
|
|
107
123
|
chains = [
|
|
108
124
|
(f"candidate-{i + 1}", backend_for_deployment(settings, dep))
|
|
109
125
|
for i, dep in enumerate(settings.parallel_deployments)
|
|
@@ -135,10 +151,7 @@ def _run_deep(settings: Settings, messages: list[dict]) -> None:
|
|
|
135
151
|
|
|
136
152
|
|
|
137
153
|
def _run_dual(settings: Settings, messages: list[dict], dual: str, synth: bool) -> None:
|
|
138
|
-
|
|
139
|
-
if not ok:
|
|
140
|
-
branding.error(msg)
|
|
141
|
-
sys.exit(2)
|
|
154
|
+
require_feature("dual", hard=True, exit_code=2)
|
|
142
155
|
names = [s.strip() for s in dual.split(",") if s.strip()] if dual else []
|
|
143
156
|
if len(names) >= 2:
|
|
144
157
|
la, ra, lname, rname = names[0], names[1], names[0], names[1]
|
|
@@ -170,9 +183,7 @@ def _build_guardian(settings: Settings) -> Backend | None:
|
|
|
170
183
|
"""
|
|
171
184
|
if not settings.guardian_enabled:
|
|
172
185
|
return None
|
|
173
|
-
|
|
174
|
-
if not ok:
|
|
175
|
-
branding.info(msg)
|
|
186
|
+
if not require_feature("guardian", hard=False):
|
|
176
187
|
return None
|
|
177
188
|
return backend_for_deployment(settings, settings.guardian_deployment)
|
|
178
189
|
|
|
@@ -185,8 +196,7 @@ def _build_mesh(settings: Settings):
|
|
|
185
196
|
"""
|
|
186
197
|
if not settings.guardian_enabled:
|
|
187
198
|
return None
|
|
188
|
-
|
|
189
|
-
if not ok:
|
|
199
|
+
if not licensing.check_feature("mesh")[0]:
|
|
190
200
|
return None
|
|
191
201
|
from deepparallel import mesh
|
|
192
202
|
|
|
@@ -439,6 +449,33 @@ def _chat_loop(settings: Settings) -> None:
|
|
|
439
449
|
_stream_repl(_wrap_fusion(backend, settings), settings)
|
|
440
450
|
|
|
441
451
|
|
|
452
|
+
_LICENSE_COMMANDS = {"activate", "deactivate", "upgrade", "account", "whoami"}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _is_interactive() -> bool:
|
|
456
|
+
return sys.stdout.isatty() and not _bool_env("DEEPPARALLEL_PLAIN", False)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _first_run_marker() -> Path:
|
|
460
|
+
return licensing.license_file_path().parent / ".first_run_shown"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _maybe_first_run_nudge() -> None:
|
|
464
|
+
if not _is_interactive():
|
|
465
|
+
return
|
|
466
|
+
if licensing.resolve_tier() is not licensing.Tier.FREE:
|
|
467
|
+
return
|
|
468
|
+
marker = _first_run_marker()
|
|
469
|
+
if marker.exists():
|
|
470
|
+
return
|
|
471
|
+
try:
|
|
472
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
473
|
+
marker.write_text("shown\n")
|
|
474
|
+
except OSError:
|
|
475
|
+
pass
|
|
476
|
+
branding.first_run_banner()
|
|
477
|
+
|
|
478
|
+
|
|
442
479
|
@click.group(
|
|
443
480
|
invoke_without_command=True,
|
|
444
481
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
@@ -459,6 +496,8 @@ def main(ctx: click.Context, temperature: float | None, assume_yes: bool) -> Non
|
|
|
459
496
|
if assume_yes:
|
|
460
497
|
settings = replace(settings, auto_approve=True)
|
|
461
498
|
ctx.obj["settings"] = settings
|
|
499
|
+
if ctx.invoked_subcommand not in _LICENSE_COMMANDS:
|
|
500
|
+
_maybe_first_run_nudge()
|
|
462
501
|
if ctx.invoked_subcommand is None:
|
|
463
502
|
_chat_loop(settings)
|
|
464
503
|
|
|
@@ -481,6 +520,14 @@ def chat(ctx: click.Context, no_tools: bool, assume_yes: bool, fuse: str | None)
|
|
|
481
520
|
_chat_loop(settings)
|
|
482
521
|
|
|
483
522
|
|
|
523
|
+
@main.command()
|
|
524
|
+
def tui() -> None:
|
|
525
|
+
"""Launch the DeepParallel TUI (persistent multi-panel terminal interface)."""
|
|
526
|
+
from deepparallel.tui.app import run_tui
|
|
527
|
+
|
|
528
|
+
run_tui()
|
|
529
|
+
|
|
530
|
+
|
|
484
531
|
@main.command()
|
|
485
532
|
@click.option("--no-tools", is_flag=True, help="Disable tools (plain chat).")
|
|
486
533
|
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Auto-approve tool actions.")
|
|
@@ -617,10 +664,7 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
617
664
|
"""
|
|
618
665
|
settings: Settings = ctx.obj["settings"]
|
|
619
666
|
if not settings.byok:
|
|
620
|
-
|
|
621
|
-
if not ok:
|
|
622
|
-
branding.error(msg)
|
|
623
|
-
sys.exit(3)
|
|
667
|
+
require_feature("review", hard=True, exit_code=3)
|
|
624
668
|
if as_diff:
|
|
625
669
|
content = sys.stdin.read()
|
|
626
670
|
elif path:
|
|
@@ -682,10 +726,7 @@ def audit(ctx: click.Context, path: str) -> None:
|
|
|
682
726
|
seen) in source files and manifests. Exit code: 0 clean, 2 if a likely
|
|
683
727
|
hallucinated dependency is found - so it can gate a commit or PR. Paid (Pro+).
|
|
684
728
|
"""
|
|
685
|
-
|
|
686
|
-
if not ok:
|
|
687
|
-
branding.error(msg)
|
|
688
|
-
sys.exit(3)
|
|
729
|
+
require_feature("audit", hard=True, exit_code=3)
|
|
689
730
|
try:
|
|
690
731
|
content = Path(path).expanduser().read_text(encoding="utf-8")
|
|
691
732
|
except OSError as e:
|
|
@@ -760,6 +801,47 @@ def research_conduit() -> None:
|
|
|
760
801
|
)
|
|
761
802
|
|
|
762
803
|
|
|
804
|
+
@research.command("compound-discovery")
|
|
805
|
+
@click.option("--target", default="EGFR", help="Biological target (gene symbol).")
|
|
806
|
+
@click.option(
|
|
807
|
+
"--compounds",
|
|
808
|
+
default="erlotinib,gefitinib,osimertinib",
|
|
809
|
+
help="Comma-separated compound names.",
|
|
810
|
+
)
|
|
811
|
+
@click.option("--dry-run", is_flag=True, help="Print the MCP tool call manifest.")
|
|
812
|
+
@click.option("--output", default=None, help="Output report path (Markdown + JSON).")
|
|
813
|
+
def research_compound_discovery(
|
|
814
|
+
target: str, compounds: str, dry_run: bool, output: str | None
|
|
815
|
+
) -> None:
|
|
816
|
+
"""PubChem compound discovery workflow across 10 stages.
|
|
817
|
+
|
|
818
|
+
Generates a full intelligence report: target assays, compound profiling,
|
|
819
|
+
bioactivity, safety, interactions, 3D structure, cross-references, and
|
|
820
|
+
analog discovery. Uses PubChem MCP tools when available; --dry-run prints
|
|
821
|
+
the tool call manifest without executing.
|
|
822
|
+
"""
|
|
823
|
+
from deepparallel.research.compound_discovery import (
|
|
824
|
+
generate_markdown_report,
|
|
825
|
+
run_dry_run,
|
|
826
|
+
run_workflow,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
compound_list = [c.strip() for c in compounds.split(",")]
|
|
830
|
+
if dry_run:
|
|
831
|
+
console.print(run_dry_run(target, compound_list))
|
|
832
|
+
return
|
|
833
|
+
result = run_workflow(target, compound_list)
|
|
834
|
+
report = generate_markdown_report(result)
|
|
835
|
+
print(report)
|
|
836
|
+
if output:
|
|
837
|
+
path = Path(output)
|
|
838
|
+
path.write_text(report)
|
|
839
|
+
json_path = path.with_suffix(".json")
|
|
840
|
+
json_path.write_text(result.to_json())
|
|
841
|
+
branding.info(f"Report written to {path}")
|
|
842
|
+
branding.info(f"JSON data written to {json_path}")
|
|
843
|
+
|
|
844
|
+
|
|
763
845
|
@main.command(name="tools")
|
|
764
846
|
def tools_cmd() -> None:
|
|
765
847
|
"""List the agent tools available to DeepParallel."""
|
|
@@ -821,5 +903,105 @@ def doctor(ctx: click.Context) -> None:
|
|
|
821
903
|
sys.exit(1)
|
|
822
904
|
|
|
823
905
|
|
|
906
|
+
def _format_exp(payload: dict) -> str:
|
|
907
|
+
exp = int(payload.get("exp") or 0)
|
|
908
|
+
if not exp:
|
|
909
|
+
return "no expiry"
|
|
910
|
+
import datetime
|
|
911
|
+
|
|
912
|
+
return datetime.datetime.fromtimestamp(exp).strftime("%Y-%m-%d")
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _show_account(settings: Settings) -> None:
|
|
916
|
+
source, payload = licensing.active_license()
|
|
917
|
+
tier = licensing.resolve_tier()
|
|
918
|
+
if source == "env":
|
|
919
|
+
src_label = "DEEPPARALLEL_LICENSE env var"
|
|
920
|
+
elif source == "file":
|
|
921
|
+
src_label = str(licensing.license_file_path())
|
|
922
|
+
else:
|
|
923
|
+
src_label = "none"
|
|
924
|
+
if source != "none" and payload is None:
|
|
925
|
+
src_label += " (invalid or expired)"
|
|
926
|
+
header = [("Tier", tier.label), ("Source", src_label)]
|
|
927
|
+
if payload and payload.get("email"):
|
|
928
|
+
header.append(("Account", str(payload["email"])))
|
|
929
|
+
if payload:
|
|
930
|
+
header.append(("Expires", _format_exp(payload)))
|
|
931
|
+
rows = [(unlocked, desc) for _f, desc, _need, unlocked in licensing.features_matrix(tier)]
|
|
932
|
+
footer = None
|
|
933
|
+
if tier is licensing.Tier.FREE:
|
|
934
|
+
footer = ["Unlock Guardian, fusion, and the --deep council: dp upgrade"]
|
|
935
|
+
branding.license_panel("DeepParallel account", header, rows, footer)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@main.command()
|
|
939
|
+
@click.argument("key", required=True)
|
|
940
|
+
@click.pass_context
|
|
941
|
+
def activate(ctx: click.Context, key: str) -> None:
|
|
942
|
+
payload = licensing.verify_token(key.strip())
|
|
943
|
+
if not payload:
|
|
944
|
+
branding.error(
|
|
945
|
+
"that license key is invalid or expired. Copy it exactly from your "
|
|
946
|
+
"purchase email, or run dp upgrade to buy one."
|
|
947
|
+
)
|
|
948
|
+
sys.exit(2)
|
|
949
|
+
tier = licensing.tier_from_payload(payload)
|
|
950
|
+
try:
|
|
951
|
+
path = licensing.write_license(key)
|
|
952
|
+
except OSError as e:
|
|
953
|
+
branding.error(f"could not write the license file: {e}")
|
|
954
|
+
sys.exit(1)
|
|
955
|
+
header = [("Tier", tier.label)]
|
|
956
|
+
if payload.get("email"):
|
|
957
|
+
header.append(("Account", str(payload["email"])))
|
|
958
|
+
header.append(("Expires", _format_exp(payload)))
|
|
959
|
+
header.append(("License", str(path)))
|
|
960
|
+
rows = [(unlocked, desc) for _f, desc, _need, unlocked in licensing.features_matrix(tier)]
|
|
961
|
+
branding.license_panel("License activated", header, rows)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
@main.command()
|
|
965
|
+
@click.pass_context
|
|
966
|
+
def account(ctx: click.Context) -> None:
|
|
967
|
+
_show_account(ctx.obj["settings"])
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
@main.command()
|
|
971
|
+
@click.pass_context
|
|
972
|
+
def whoami(ctx: click.Context) -> None:
|
|
973
|
+
_show_account(ctx.obj["settings"])
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@main.command()
|
|
977
|
+
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Skip the confirmation prompt.")
|
|
978
|
+
@click.pass_context
|
|
979
|
+
def deactivate(ctx: click.Context, assume_yes: bool) -> None:
|
|
980
|
+
path = licensing.license_file_path()
|
|
981
|
+
if not path.exists():
|
|
982
|
+
branding.info("no stored license; already on Free.")
|
|
983
|
+
return
|
|
984
|
+
if not assume_yes and not click.confirm(f"Remove the license at {path} and revert to Free?"):
|
|
985
|
+
branding.info("kept the current license.")
|
|
986
|
+
return
|
|
987
|
+
if licensing.remove_license():
|
|
988
|
+
branding.info("license removed; you are now on Free.")
|
|
989
|
+
else:
|
|
990
|
+
branding.error("could not remove the license file.")
|
|
991
|
+
sys.exit(1)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@main.command()
|
|
995
|
+
def upgrade() -> None:
|
|
996
|
+
import webbrowser
|
|
997
|
+
|
|
998
|
+
url = licensing.upgrade_url()
|
|
999
|
+
try:
|
|
1000
|
+
opened = bool(webbrowser.open(url))
|
|
1001
|
+
except Exception:
|
|
1002
|
+
opened = False
|
|
1003
|
+
branding.upgrade_link(url, opened)
|
|
1004
|
+
|
|
1005
|
+
|
|
824
1006
|
if __name__ == "__main__":
|
|
825
1007
|
main()
|
|
@@ -24,12 +24,9 @@ import time
|
|
|
24
24
|
from enum import IntEnum
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
-
# Issuer public key (Ed25519, raw, base64). The matching private key is the
|
|
28
|
-
# issuance secret and is never shipped.
|
|
29
27
|
_EMBEDDED_PUBKEY = "BdxSRIB5F2K2bXf7c0UqsR6jC/cSRMSxojpzRpTCUQg="
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
_FEATURE_TIER: dict[str, "Tier"] = {}
|
|
29
|
+
DEFAULT_PRICING_URL = "https://deepparallel.dev/pricing"
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
class Tier(IntEnum):
|
|
@@ -42,12 +39,24 @@ class Tier(IntEnum):
|
|
|
42
39
|
return {0: "Free", 1: "Pro", 2: "Team"}[int(self)]
|
|
43
40
|
|
|
44
41
|
|
|
42
|
+
FEATURE_INFO: dict[str, tuple["Tier", str]] = {
|
|
43
|
+
"fusion": (Tier.PRO, "Reasoner-plus-answerer fusion (//fuse, //escalate)"),
|
|
44
|
+
"deep": (Tier.PRO, "The --deep multi-model council with a judge"),
|
|
45
|
+
"dual": (Tier.PRO, "Side-by-side dual-model compare (--dual)"),
|
|
46
|
+
"guardian": (Tier.PRO, "Guardian edit review before changes apply"),
|
|
47
|
+
"mesh": (Tier.PRO, "Verification mesh: a parallel reviewer panel per edit"),
|
|
48
|
+
"review": (Tier.PRO, "Hosted cross-model code review gate (dp review)"),
|
|
49
|
+
"audit": (Tier.PRO, "Supply-chain audit gate for hallucinated deps (dp audit)"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_FEATURE_TIER: dict[str, "Tier"] = {name: tier for name, (tier, _desc) in FEATURE_INFO.items()}
|
|
53
|
+
|
|
54
|
+
|
|
45
55
|
def _b64url_decode(s: str) -> bytes:
|
|
46
56
|
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
|
|
50
|
-
"""Return the payload dict if the token is validly signed and unexpired."""
|
|
51
60
|
if not token or "." not in token:
|
|
52
61
|
return None
|
|
53
62
|
try:
|
|
@@ -58,7 +67,7 @@ def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
|
|
|
58
67
|
try:
|
|
59
68
|
pub = Ed25519PublicKey.from_public_bytes(_b64url_decode(pubkey_b64 or _EMBEDDED_PUBKEY))
|
|
60
69
|
pub.verify(_b64url_decode(sig), body.encode())
|
|
61
|
-
except Exception:
|
|
70
|
+
except Exception:
|
|
62
71
|
return None
|
|
63
72
|
try:
|
|
64
73
|
payload = json.loads(_b64url_decode(body))
|
|
@@ -71,11 +80,6 @@ def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
|
|
|
71
80
|
|
|
72
81
|
|
|
73
82
|
def sign_token(payload: dict, private_key_b64: str) -> str:
|
|
74
|
-
"""Sign a license payload into a `body.signature` token (issuer-side).
|
|
75
|
-
|
|
76
|
-
Requires the private issuance key; verify_token() checks it with the public
|
|
77
|
-
key. Used by the license-issuer (Stripe webhook) and the admin CLI.
|
|
78
|
-
"""
|
|
79
83
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
80
84
|
|
|
81
85
|
priv = Ed25519PrivateKey.from_private_bytes(_b64url_decode_std(private_key_b64))
|
|
@@ -85,35 +89,39 @@ def sign_token(payload: dict, private_key_b64: str) -> str:
|
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def _b64url_decode_std(s: str) -> bytes:
|
|
88
|
-
"""Decode standard OR url-safe base64 (keys are emitted as standard b64)."""
|
|
89
92
|
s = s.strip()
|
|
90
93
|
pad = "=" * (-len(s) % 4)
|
|
91
94
|
try:
|
|
92
95
|
return base64.b64decode(s + pad)
|
|
93
|
-
except Exception:
|
|
96
|
+
except Exception:
|
|
94
97
|
return base64.urlsafe_b64decode(s + pad)
|
|
95
98
|
|
|
96
99
|
|
|
97
|
-
def
|
|
98
|
-
|
|
100
|
+
def license_file_path() -> Path:
|
|
101
|
+
return Path(
|
|
99
102
|
os.environ.get("DEEPPARALLEL_LICENSE_FILE", "~/.config/deepparallel/license")
|
|
100
103
|
).expanduser()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _license_file_token() -> str | None:
|
|
101
107
|
try:
|
|
102
|
-
return
|
|
108
|
+
return license_file_path().read_text().strip() or None
|
|
103
109
|
except OSError:
|
|
104
110
|
return None
|
|
105
111
|
|
|
106
112
|
|
|
113
|
+
def tier_from_payload(payload: dict | None) -> Tier:
|
|
114
|
+
if not payload:
|
|
115
|
+
return Tier.FREE
|
|
116
|
+
name = str(payload.get("tier", "")).upper()
|
|
117
|
+
return Tier[name] if name in Tier.__members__ else Tier.FREE
|
|
118
|
+
|
|
119
|
+
|
|
107
120
|
def resolve_tier() -> Tier:
|
|
108
|
-
"""Resolve the active tier from the license env var or file. FREE on absence/failure."""
|
|
109
121
|
token = os.environ.get("DEEPPARALLEL_LICENSE") or _license_file_token()
|
|
110
122
|
if not token:
|
|
111
123
|
return Tier.FREE
|
|
112
|
-
|
|
113
|
-
if not payload:
|
|
114
|
-
return Tier.FREE
|
|
115
|
-
name = str(payload.get("tier", "")).upper()
|
|
116
|
-
return getattr(Tier, name, Tier.FREE) if name in Tier.__members__ else Tier.FREE
|
|
124
|
+
return tier_from_payload(verify_token(token))
|
|
117
125
|
|
|
118
126
|
|
|
119
127
|
def tier_allows(have: Tier, need: Tier) -> bool:
|
|
@@ -121,12 +129,69 @@ def tier_allows(have: Tier, need: Tier) -> bool:
|
|
|
121
129
|
|
|
122
130
|
|
|
123
131
|
def check_feature(feature: str, have: Tier | None = None) -> tuple[bool, str]:
|
|
124
|
-
"""Return (allowed, message). Paid features need PRO+; message nudges upgrade."""
|
|
125
132
|
have = resolve_tier() if have is None else have
|
|
126
133
|
need = _FEATURE_TIER.get(feature, Tier.PRO)
|
|
127
134
|
if tier_allows(have, need):
|
|
128
135
|
return True, ""
|
|
129
136
|
return False, (
|
|
130
137
|
f"{feature} requires DeepParallel {need.label} (you are on {have.label}). "
|
|
131
|
-
f"Upgrade at
|
|
138
|
+
f"Upgrade at {upgrade_url()}"
|
|
132
139
|
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def feature_tier(feature: str) -> Tier:
|
|
143
|
+
return _FEATURE_TIER.get(feature, Tier.PRO)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def feature_unlocks(feature: str) -> str:
|
|
147
|
+
info = FEATURE_INFO.get(feature)
|
|
148
|
+
return info[1] if info else feature
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def features_matrix(have: Tier | None = None) -> list[tuple[str, str, Tier, bool]]:
|
|
152
|
+
have = resolve_tier() if have is None else have
|
|
153
|
+
return [
|
|
154
|
+
(feature, desc, need, tier_allows(have, need))
|
|
155
|
+
for feature, (need, desc) in FEATURE_INFO.items()
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def upgrade_url() -> str:
|
|
160
|
+
return (
|
|
161
|
+
os.environ.get("DEEPPARALLEL_CHECKOUT_URL")
|
|
162
|
+
or os.environ.get("DEEPPARALLEL_PRICING_URL")
|
|
163
|
+
or DEFAULT_PRICING_URL
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def write_license(token: str) -> Path:
|
|
168
|
+
path = license_file_path()
|
|
169
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
try:
|
|
171
|
+
os.chmod(path.parent, 0o700)
|
|
172
|
+
except OSError:
|
|
173
|
+
pass
|
|
174
|
+
path.write_text(token.strip() + "\n")
|
|
175
|
+
try:
|
|
176
|
+
os.chmod(path, 0o600)
|
|
177
|
+
except OSError:
|
|
178
|
+
pass
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def remove_license() -> bool:
|
|
183
|
+
try:
|
|
184
|
+
license_file_path().unlink()
|
|
185
|
+
return True
|
|
186
|
+
except (FileNotFoundError, OSError):
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def active_license() -> tuple[str, dict | None]:
|
|
191
|
+
env_token = os.environ.get("DEEPPARALLEL_LICENSE")
|
|
192
|
+
if env_token:
|
|
193
|
+
return "env", verify_token(env_token)
|
|
194
|
+
file_token = _license_file_token()
|
|
195
|
+
if file_token:
|
|
196
|
+
return "file", verify_token(file_token)
|
|
197
|
+
return "none", None
|