@researai/deepscientist 1.5.0 → 1.5.1
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.
- package/AGENTS.md +26 -0
- package/README.md +19 -179
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +233 -53
- package/docs/en/00_QUICK_START.md +134 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +108 -0
- package/docs/en/90_ARCHITECTURE.md +245 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +134 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +112 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +32 -8
- package/package.json +4 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +482 -22
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +201 -6
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +50 -5
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +442 -15
- package/src/deepscientist/doctor.py +444 -0
- package/src/deepscientist/home.py +1 -0
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/server.py +49 -2
- package/src/deepscientist/prompts/builder.py +181 -58
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +63 -2
- package/src/deepscientist/quest/stage_views.py +19 -1
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +12 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +177 -33
- package/src/skills/analysis-campaign/SKILL.md +22 -6
- package/src/skills/baseline/SKILL.md +5 -4
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +5 -4
- package/src/skills/finalize/SKILL.md +5 -4
- package/src/skills/idea/SKILL.md +5 -4
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +407 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +293 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +5 -4
- package/src/skills/write/SKILL.md +7 -3
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
- package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.error import URLError
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
from .config import ConfigManager
|
|
12
|
+
from .home import ensure_home_layout
|
|
13
|
+
from .runtime_tools import RuntimeToolService
|
|
14
|
+
from .shared import resolve_runner_binary, utc_now
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _browser_ui_url(host: str, port: int) -> str:
|
|
18
|
+
normalized = str(host or "").strip()
|
|
19
|
+
browser_host = "127.0.0.1" if normalized in {"", "0.0.0.0", "::", "[::]"} else normalized
|
|
20
|
+
return f"http://{browser_host}:{port}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_status(ok: bool, warnings: list[str] | None = None) -> str:
|
|
24
|
+
if not ok:
|
|
25
|
+
return "error"
|
|
26
|
+
if warnings:
|
|
27
|
+
return "warn"
|
|
28
|
+
return "ok"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_check(
|
|
32
|
+
*,
|
|
33
|
+
check_id: str,
|
|
34
|
+
label: str,
|
|
35
|
+
ok: bool,
|
|
36
|
+
summary: str,
|
|
37
|
+
warnings: list[str] | None = None,
|
|
38
|
+
errors: list[str] | None = None,
|
|
39
|
+
guidance: list[str] | None = None,
|
|
40
|
+
details: dict[str, Any] | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
normalized_warnings = list(warnings or [])
|
|
43
|
+
normalized_errors = list(errors or [])
|
|
44
|
+
return {
|
|
45
|
+
"id": check_id,
|
|
46
|
+
"label": label,
|
|
47
|
+
"ok": ok and not normalized_errors,
|
|
48
|
+
"status": _check_status(ok and not normalized_errors, normalized_warnings),
|
|
49
|
+
"summary": summary,
|
|
50
|
+
"warnings": normalized_warnings,
|
|
51
|
+
"errors": normalized_errors,
|
|
52
|
+
"guidance": list(guidance or []),
|
|
53
|
+
"details": dict(details or {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _check_python_runtime() -> dict[str, Any]:
|
|
58
|
+
try:
|
|
59
|
+
import _cffi_backend # noqa: F401
|
|
60
|
+
import cryptography # noqa: F401
|
|
61
|
+
import deepscientist.cli # noqa: F401
|
|
62
|
+
except Exception as exc: # pragma: no cover - import failures are environment-dependent
|
|
63
|
+
return _make_check(
|
|
64
|
+
check_id="python_runtime",
|
|
65
|
+
label="Python runtime",
|
|
66
|
+
ok=False,
|
|
67
|
+
summary="Local Python runtime is not healthy.",
|
|
68
|
+
errors=[str(exc)],
|
|
69
|
+
guidance=[
|
|
70
|
+
"Reinstall the package or rerun `ds` so DeepScientist can rebuild its local Python runtime.",
|
|
71
|
+
],
|
|
72
|
+
details={"python": sys.executable, "version": sys.version.split()[0]},
|
|
73
|
+
)
|
|
74
|
+
return _make_check(
|
|
75
|
+
check_id="python_runtime",
|
|
76
|
+
label="Python runtime",
|
|
77
|
+
ok=True,
|
|
78
|
+
summary="Local Python runtime imports succeeded.",
|
|
79
|
+
details={"python": sys.executable, "version": sys.version.split()[0]},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_home_writable(home: Path) -> dict[str, Any]:
|
|
84
|
+
try:
|
|
85
|
+
ensure_home_layout(home)
|
|
86
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=home / "runtime", delete=False) as handle:
|
|
87
|
+
handle.write("doctor\n")
|
|
88
|
+
temp_path = Path(handle.name)
|
|
89
|
+
temp_path.unlink(missing_ok=True)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
return _make_check(
|
|
92
|
+
check_id="home_writable",
|
|
93
|
+
label="Home path",
|
|
94
|
+
ok=False,
|
|
95
|
+
summary="DeepScientist home is not writable.",
|
|
96
|
+
errors=[str(exc)],
|
|
97
|
+
guidance=[
|
|
98
|
+
f"Ensure `{home}` exists and is writable by the current user.",
|
|
99
|
+
],
|
|
100
|
+
details={"home": str(home)},
|
|
101
|
+
)
|
|
102
|
+
return _make_check(
|
|
103
|
+
check_id="home_writable",
|
|
104
|
+
label="Home path",
|
|
105
|
+
ok=True,
|
|
106
|
+
summary="DeepScientist home exists and is writable.",
|
|
107
|
+
details={"home": str(home)},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_git(config_manager: ConfigManager) -> dict[str, Any]:
|
|
112
|
+
readiness = config_manager.git_readiness()
|
|
113
|
+
return _make_check(
|
|
114
|
+
check_id="git",
|
|
115
|
+
label="Git",
|
|
116
|
+
ok=bool(readiness.get("installed")) and not list(readiness.get("errors") or []),
|
|
117
|
+
summary="Git is available for quest repositories." if readiness.get("installed") else "Git is not available.",
|
|
118
|
+
warnings=list(readiness.get("warnings") or []),
|
|
119
|
+
errors=list(readiness.get("errors") or []),
|
|
120
|
+
guidance=list(readiness.get("guidance") or []),
|
|
121
|
+
details={
|
|
122
|
+
"user_name": readiness.get("user_name"),
|
|
123
|
+
"user_email": readiness.get("user_email"),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _check_config_validation(config_manager: ConfigManager) -> dict[str, Any]:
|
|
129
|
+
validation = config_manager.validate_all()
|
|
130
|
+
warnings: list[str] = []
|
|
131
|
+
errors: list[str] = []
|
|
132
|
+
optional_missing_prefix = "Optional config file is missing"
|
|
133
|
+
|
|
134
|
+
for item in validation.get("files") or []:
|
|
135
|
+
item_errors = [str(value) for value in item.get("errors") or []]
|
|
136
|
+
item_warnings = [str(value) for value in item.get("warnings") or []]
|
|
137
|
+
errors.extend(item_errors)
|
|
138
|
+
warnings.extend([value for value in item_warnings if not value.startswith(optional_missing_prefix)])
|
|
139
|
+
|
|
140
|
+
return _make_check(
|
|
141
|
+
check_id="config_validation",
|
|
142
|
+
label="Config files",
|
|
143
|
+
ok=len(errors) == 0,
|
|
144
|
+
summary="Required config files validated successfully." if len(errors) == 0 else "Config validation failed.",
|
|
145
|
+
warnings=warnings,
|
|
146
|
+
errors=errors,
|
|
147
|
+
guidance=["Run `ds config validate` for the full structured validation report."] if errors else [],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _check_runner_support(config_manager: ConfigManager) -> dict[str, Any]:
|
|
152
|
+
config_payload = config_manager.load_named_normalized("config")
|
|
153
|
+
runners_payload = config_manager.load_named_normalized("runners")
|
|
154
|
+
|
|
155
|
+
default_runner = str(config_payload.get("default_runner") or "codex").strip().lower() or "codex"
|
|
156
|
+
codex_cfg = runners_payload.get("codex") if isinstance(runners_payload.get("codex"), dict) else {}
|
|
157
|
+
claude_cfg = runners_payload.get("claude") if isinstance(runners_payload.get("claude"), dict) else {}
|
|
158
|
+
|
|
159
|
+
errors: list[str] = []
|
|
160
|
+
warnings: list[str] = []
|
|
161
|
+
guidance: list[str] = []
|
|
162
|
+
|
|
163
|
+
if default_runner != "codex":
|
|
164
|
+
errors.append("Current open-source release supports `codex` as the runnable default runner.")
|
|
165
|
+
guidance.append("Set `default_runner: codex` in `~/DeepScientist/config/config.yaml`.")
|
|
166
|
+
if not bool(codex_cfg.get("enabled", False)):
|
|
167
|
+
errors.append("`runners.codex.enabled` must stay `true` in the current release.")
|
|
168
|
+
guidance.append("Set `runners.codex.enabled: true` in `~/DeepScientist/config/runners.yaml`.")
|
|
169
|
+
if bool(claude_cfg.get("enabled", False)):
|
|
170
|
+
errors.append("`claude` is still TODO in the current release and should stay disabled.")
|
|
171
|
+
guidance.append("Set `runners.claude.enabled: false` in `~/DeepScientist/config/runners.yaml`.")
|
|
172
|
+
else:
|
|
173
|
+
warnings.append("`claude` remains a TODO/reserved runner slot and is not runnable yet.")
|
|
174
|
+
|
|
175
|
+
return _make_check(
|
|
176
|
+
check_id="runner_support",
|
|
177
|
+
label="Supported runners",
|
|
178
|
+
ok=len(errors) == 0,
|
|
179
|
+
summary="Runner policy matches the current release surface." if len(errors) == 0 else "Runner policy needs adjustment.",
|
|
180
|
+
warnings=warnings,
|
|
181
|
+
errors=errors,
|
|
182
|
+
guidance=guidance,
|
|
183
|
+
details={"default_runner": default_runner},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
|
|
188
|
+
runners_payload = config_manager.load_named_normalized("runners")
|
|
189
|
+
codex_cfg = runners_payload.get("codex") if isinstance(runners_payload.get("codex"), dict) else {}
|
|
190
|
+
binary = str(codex_cfg.get("binary") or "codex").strip() or "codex"
|
|
191
|
+
resolved_binary = resolve_runner_binary(binary, runner_name="codex")
|
|
192
|
+
|
|
193
|
+
if not resolved_binary:
|
|
194
|
+
return _make_check(
|
|
195
|
+
check_id="codex",
|
|
196
|
+
label="Codex CLI",
|
|
197
|
+
ok=False,
|
|
198
|
+
summary="Codex CLI is not available on PATH.",
|
|
199
|
+
errors=[f"Runner binary `{binary}` could not be resolved."],
|
|
200
|
+
guidance=[
|
|
201
|
+
"Install Codex first: `npm install -g @openai/codex`.",
|
|
202
|
+
"Then run `codex` once and complete login.",
|
|
203
|
+
],
|
|
204
|
+
details={"binary": binary},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
probe = config_manager.probe_codex_bootstrap(persist=False, payload=runners_payload)
|
|
208
|
+
probe_errors = [str(value) for value in probe.get("errors") or []]
|
|
209
|
+
probe_warnings = [str(value) for value in probe.get("warnings") or []]
|
|
210
|
+
probe_guidance = [str(value) for value in probe.get("guidance") or []]
|
|
211
|
+
summary = str(probe.get("summary") or "Codex startup probe completed.")
|
|
212
|
+
if probe.get("ok"):
|
|
213
|
+
return _make_check(
|
|
214
|
+
check_id="codex",
|
|
215
|
+
label="Codex CLI",
|
|
216
|
+
ok=True,
|
|
217
|
+
summary=summary,
|
|
218
|
+
warnings=probe_warnings,
|
|
219
|
+
details={"resolved_binary": resolved_binary},
|
|
220
|
+
)
|
|
221
|
+
if not probe_guidance:
|
|
222
|
+
probe_guidance = ["Run `codex` manually once and complete login, then retry `ds doctor`."]
|
|
223
|
+
return _make_check(
|
|
224
|
+
check_id="codex",
|
|
225
|
+
label="Codex CLI",
|
|
226
|
+
ok=False,
|
|
227
|
+
summary=summary,
|
|
228
|
+
warnings=probe_warnings,
|
|
229
|
+
errors=probe_errors or ["Codex startup probe did not succeed."],
|
|
230
|
+
guidance=probe_guidance,
|
|
231
|
+
details={"resolved_binary": resolved_binary},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _check_bundles(repo_root: Path) -> dict[str, Any]:
|
|
236
|
+
web_entry = repo_root / "src" / "ui" / "dist" / "index.html"
|
|
237
|
+
tui_entry = repo_root / "src" / "tui" / "dist" / "index.js"
|
|
238
|
+
errors: list[str] = []
|
|
239
|
+
guidance: list[str] = []
|
|
240
|
+
|
|
241
|
+
if not web_entry.exists():
|
|
242
|
+
errors.append(f"Missing web bundle: {web_entry}")
|
|
243
|
+
guidance.append("Build the web UI: `npm --prefix src/ui install && npm --prefix src/ui run build`.")
|
|
244
|
+
if not tui_entry.exists():
|
|
245
|
+
errors.append(f"Missing TUI bundle: {tui_entry}")
|
|
246
|
+
guidance.append("Build the TUI: `npm --prefix src/tui install && npm --prefix src/tui run build`.")
|
|
247
|
+
|
|
248
|
+
return _make_check(
|
|
249
|
+
check_id="bundles",
|
|
250
|
+
label="UI bundles",
|
|
251
|
+
ok=len(errors) == 0,
|
|
252
|
+
summary="Web and TUI bundles are present." if len(errors) == 0 else "One or more UI bundles are missing.",
|
|
253
|
+
errors=errors,
|
|
254
|
+
guidance=guidance,
|
|
255
|
+
details={"web_bundle": str(web_entry), "tui_bundle": str(tui_entry)},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _check_latex_runtime(home: Path) -> dict[str, Any]:
|
|
260
|
+
runtime = RuntimeToolService(home).status("tinytex")
|
|
261
|
+
pdflatex = runtime.get("binaries", {}).get("pdflatex") or {}
|
|
262
|
+
details = {
|
|
263
|
+
"latex_runtime_summary": runtime.get("summary"),
|
|
264
|
+
"latex_pdflatex_path": pdflatex.get("path"),
|
|
265
|
+
"latex_pdflatex_source": pdflatex.get("source"),
|
|
266
|
+
"latex_tinytex_root": runtime.get("tinytex", {}).get("root"),
|
|
267
|
+
}
|
|
268
|
+
return _make_check(
|
|
269
|
+
check_id="latex_runtime",
|
|
270
|
+
label="LaTeX runtime (optional)",
|
|
271
|
+
ok=True,
|
|
272
|
+
summary=str(runtime.get("summary") or "Optional local LaTeX runtime was checked."),
|
|
273
|
+
warnings=list(runtime.get("warnings") or []),
|
|
274
|
+
guidance=list(runtime.get("guidance") or []),
|
|
275
|
+
details=details,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _query_local_health(url: str) -> dict[str, Any] | None:
|
|
280
|
+
request = Request(f"{url}/api/health", headers={"Accept": "application/json"})
|
|
281
|
+
try:
|
|
282
|
+
with urlopen(request, timeout=1.5) as response: # noqa: S310
|
|
283
|
+
import json
|
|
284
|
+
|
|
285
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
286
|
+
return payload if isinstance(payload, dict) else None
|
|
287
|
+
except (OSError, TimeoutError, URLError, ValueError):
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _port_is_bindable(host: str, port: int) -> tuple[bool, str | None]:
|
|
292
|
+
normalized = str(host or "").strip() or "0.0.0.0"
|
|
293
|
+
family = socket.AF_INET6 if ":" in normalized and normalized != "0.0.0.0" else socket.AF_INET
|
|
294
|
+
bind_host = "::" if normalized in {"[::]", "::"} else normalized
|
|
295
|
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
|
296
|
+
try:
|
|
297
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
298
|
+
sock.bind((bind_host, port))
|
|
299
|
+
except OSError as exc:
|
|
300
|
+
return False, str(exc)
|
|
301
|
+
finally:
|
|
302
|
+
sock.close()
|
|
303
|
+
return True, None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _check_ui_port(home: Path, config_manager: ConfigManager) -> dict[str, Any]:
|
|
307
|
+
config_payload = config_manager.load_named_normalized("config")
|
|
308
|
+
ui_payload = config_payload.get("ui") if isinstance(config_payload.get("ui"), dict) else {}
|
|
309
|
+
host = str(ui_payload.get("host") or "0.0.0.0").strip() or "0.0.0.0"
|
|
310
|
+
port = int(ui_payload.get("port") or 20999)
|
|
311
|
+
browser_url = _browser_ui_url(host, port)
|
|
312
|
+
health = _query_local_health(browser_url)
|
|
313
|
+
|
|
314
|
+
if health and health.get("status") == "ok":
|
|
315
|
+
daemon_home = str(health.get("home") or "").strip()
|
|
316
|
+
if daemon_home == str(home.resolve()):
|
|
317
|
+
return _make_check(
|
|
318
|
+
check_id="ui_port",
|
|
319
|
+
label="Web port",
|
|
320
|
+
ok=True,
|
|
321
|
+
summary="DeepScientist daemon is already running on the configured port.",
|
|
322
|
+
guidance=[f"Open {browser_url} in your browser or stop it with `ds --stop`."],
|
|
323
|
+
details={"browser_url": browser_url, "host": host, "port": port},
|
|
324
|
+
)
|
|
325
|
+
return _make_check(
|
|
326
|
+
check_id="ui_port",
|
|
327
|
+
label="Web port",
|
|
328
|
+
ok=False,
|
|
329
|
+
summary="The configured port is already used by another DeepScientist home.",
|
|
330
|
+
errors=[f"{browser_url} is already serving a daemon for `{daemon_home}`."],
|
|
331
|
+
guidance=[
|
|
332
|
+
"Stop the other daemon first or change `ui.port` in `~/DeepScientist/config/config.yaml`.",
|
|
333
|
+
],
|
|
334
|
+
details={"browser_url": browser_url, "host": host, "port": port},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
bindable, bind_error = _port_is_bindable(host, port)
|
|
338
|
+
if bindable:
|
|
339
|
+
return _make_check(
|
|
340
|
+
check_id="ui_port",
|
|
341
|
+
label="Web port",
|
|
342
|
+
ok=True,
|
|
343
|
+
summary="The configured web port is free.",
|
|
344
|
+
details={"browser_url": browser_url, "host": host, "port": port},
|
|
345
|
+
)
|
|
346
|
+
return _make_check(
|
|
347
|
+
check_id="ui_port",
|
|
348
|
+
label="Web port",
|
|
349
|
+
ok=False,
|
|
350
|
+
summary="The configured web port is not available.",
|
|
351
|
+
errors=[bind_error or "Port bind failed."],
|
|
352
|
+
guidance=[
|
|
353
|
+
"Run `ds --stop` if this is an old managed daemon.",
|
|
354
|
+
"Otherwise set a different `ui.port` in `~/DeepScientist/config/config.yaml`.",
|
|
355
|
+
],
|
|
356
|
+
details={"browser_url": browser_url, "host": host, "port": port},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def run_doctor(home: Path, *, repo_root: Path) -> dict[str, Any]:
|
|
361
|
+
ensure_home_layout(home)
|
|
362
|
+
config_manager = ConfigManager(home)
|
|
363
|
+
config_manager.ensure_files()
|
|
364
|
+
config_payload = config_manager.load_named_normalized("config")
|
|
365
|
+
ui_payload = config_payload.get("ui") if isinstance(config_payload.get("ui"), dict) else {}
|
|
366
|
+
host = str(ui_payload.get("host") or "0.0.0.0").strip() or "0.0.0.0"
|
|
367
|
+
port = int(ui_payload.get("port") or 20999)
|
|
368
|
+
browser_url = _browser_ui_url(host, port)
|
|
369
|
+
|
|
370
|
+
checks = [
|
|
371
|
+
_check_python_runtime(),
|
|
372
|
+
_check_home_writable(home),
|
|
373
|
+
_check_git(config_manager),
|
|
374
|
+
_check_config_validation(config_manager),
|
|
375
|
+
_check_runner_support(config_manager),
|
|
376
|
+
_check_codex(config_manager),
|
|
377
|
+
_check_latex_runtime(home),
|
|
378
|
+
_check_bundles(repo_root),
|
|
379
|
+
_check_ui_port(home, config_manager),
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
"ok": all(item["ok"] for item in checks),
|
|
384
|
+
"timestamp": utc_now(),
|
|
385
|
+
"home": str(home),
|
|
386
|
+
"browser_url": browser_url,
|
|
387
|
+
"checks": checks,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def render_doctor_report(report: dict[str, Any]) -> str:
|
|
392
|
+
lines = [
|
|
393
|
+
"DeepScientist doctor",
|
|
394
|
+
"",
|
|
395
|
+
f"Home: {report.get('home')}",
|
|
396
|
+
f"Web UI: {report.get('browser_url')}",
|
|
397
|
+
f"Checked at: {report.get('timestamp')}",
|
|
398
|
+
"",
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
for item in report.get("checks") or []:
|
|
402
|
+
status = str(item.get("status") or "ok").upper()
|
|
403
|
+
icon = {"OK": "[ok]", "WARN": "[warn]", "ERROR": "[fail]"}.get(status, "[info]")
|
|
404
|
+
lines.append(f"{icon} {item.get('label')}: {item.get('summary')}")
|
|
405
|
+
for warning in item.get("warnings") or []:
|
|
406
|
+
lines.append(f" warning: {warning}")
|
|
407
|
+
for error in item.get("errors") or []:
|
|
408
|
+
lines.append(f" error: {error}")
|
|
409
|
+
details = item.get("details") or {}
|
|
410
|
+
if isinstance(details, dict):
|
|
411
|
+
resolved_binary = str(details.get("resolved_binary") or "").strip()
|
|
412
|
+
if resolved_binary:
|
|
413
|
+
lines.append(f" resolved binary: {resolved_binary}")
|
|
414
|
+
latex_pdflatex_path = str(details.get("latex_pdflatex_path") or "").strip()
|
|
415
|
+
if latex_pdflatex_path:
|
|
416
|
+
lines.append(f" pdflatex: {latex_pdflatex_path}")
|
|
417
|
+
latex_tinytex_root = str(details.get("latex_tinytex_root") or "").strip()
|
|
418
|
+
if latex_tinytex_root:
|
|
419
|
+
lines.append(f" tinytex root: {latex_tinytex_root}")
|
|
420
|
+
browser_url = str(details.get("browser_url") or "").strip()
|
|
421
|
+
if browser_url:
|
|
422
|
+
lines.append(f" url: {browser_url}")
|
|
423
|
+
lines.append("")
|
|
424
|
+
|
|
425
|
+
guidance: list[str] = []
|
|
426
|
+
seen_guidance: set[str] = set()
|
|
427
|
+
for item in report.get("checks") or []:
|
|
428
|
+
for line in item.get("guidance") or []:
|
|
429
|
+
if line not in seen_guidance:
|
|
430
|
+
seen_guidance.add(line)
|
|
431
|
+
guidance.append(str(line))
|
|
432
|
+
|
|
433
|
+
if guidance:
|
|
434
|
+
lines.append("Next steps")
|
|
435
|
+
lines.append("")
|
|
436
|
+
for index, line in enumerate(guidance, start=1):
|
|
437
|
+
lines.append(f"{index}. {line}")
|
|
438
|
+
lines.append("")
|
|
439
|
+
|
|
440
|
+
if report.get("ok"):
|
|
441
|
+
lines.append("Everything looks ready. Run `ds` to start DeepScientist.")
|
|
442
|
+
else:
|
|
443
|
+
lines.append("DeepScientist is not fully ready yet. Fix the failed checks above, then rerun `ds doctor`.")
|
|
444
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -17,6 +17,7 @@ def ensure_home_layout(home: Path) -> dict[str, Path]:
|
|
|
17
17
|
runtime = ensure_dir(home / "runtime")
|
|
18
18
|
ensure_dir(runtime / "venv")
|
|
19
19
|
ensure_dir(runtime / "bundle")
|
|
20
|
+
ensure_dir(runtime / "tools")
|
|
20
21
|
|
|
21
22
|
config = ensure_dir(home / "config")
|
|
22
23
|
ensure_dir(config / "baselines")
|
|
@@ -11,7 +11,8 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
from urllib.parse import quote, unquote
|
|
13
13
|
|
|
14
|
-
from .
|
|
14
|
+
from .runtime_tools import RuntimeToolService
|
|
15
|
+
from .shared import ensure_dir, generate_id, resolve_within, utc_now, write_json
|
|
15
16
|
|
|
16
17
|
_QUEST_DIR_PREFIX = "quest-dir::"
|
|
17
18
|
_QUEST_FILE_PREFIX = "quest-file::"
|
|
@@ -409,6 +410,8 @@ class QuestLatexService:
|
|
|
409
410
|
"main_file_id": main_file_id,
|
|
410
411
|
"main_file_path": main_tex_relative,
|
|
411
412
|
"compiler": selected_compiler,
|
|
413
|
+
"compiler_binary": None,
|
|
414
|
+
"compiler_source": None,
|
|
412
415
|
"status": "running",
|
|
413
416
|
"created_at": utc_now(),
|
|
414
417
|
"started_at": utc_now(),
|
|
@@ -421,18 +424,26 @@ class QuestLatexService:
|
|
|
421
424
|
"log_items": [],
|
|
422
425
|
"output_pdf_path": None,
|
|
423
426
|
"log_path": None,
|
|
427
|
+
"bibtex_binary": None,
|
|
424
428
|
"auto": bool(auto),
|
|
425
429
|
"stop_on_first_error": bool(stop_on_first_error),
|
|
426
430
|
}
|
|
427
431
|
write_json(metadata_path, build)
|
|
428
432
|
|
|
429
|
-
|
|
433
|
+
runtime_tools = RuntimeToolService(self.quest_service.home)
|
|
434
|
+
compiler_match = runtime_tools.resolve_binary(selected_compiler, preferred_tools=("tinytex",))
|
|
435
|
+
compiler_bin = compiler_match.get("path")
|
|
436
|
+
build["compiler_binary"] = compiler_bin
|
|
437
|
+
build["compiler_source"] = compiler_match.get("source")
|
|
430
438
|
if not compiler_bin:
|
|
431
439
|
build.update(
|
|
432
440
|
{
|
|
433
441
|
"status": "error",
|
|
434
442
|
"finished_at": utc_now(),
|
|
435
|
-
"error_message":
|
|
443
|
+
"error_message": (
|
|
444
|
+
f"`{selected_compiler}` is not installed on this machine. "
|
|
445
|
+
"Install TinyTeX with `ds latex install-runtime` or install a system LaTeX distribution."
|
|
446
|
+
),
|
|
436
447
|
"log_ready": True,
|
|
437
448
|
"log_path": str(log_path),
|
|
438
449
|
}
|
|
@@ -442,7 +453,9 @@ class QuestLatexService:
|
|
|
442
453
|
self._write_compile_report(project_id, folder_relative, build)
|
|
443
454
|
return build
|
|
444
455
|
|
|
445
|
-
|
|
456
|
+
bibtex_match = runtime_tools.resolve_binary("bibtex", preferred_tools=("tinytex",))
|
|
457
|
+
bibtex_bin = bibtex_match.get("path")
|
|
458
|
+
build["bibtex_binary"] = bibtex_bin
|
|
446
459
|
command = [
|
|
447
460
|
compiler_bin,
|
|
448
461
|
"-interaction=nonstopmode",
|