@researai/deepscientist 1.5.15 → 1.5.16

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 (193) hide show
  1. package/README.md +336 -98
  2. package/bin/ds.js +691 -91
  3. package/docs/en/00_QUICK_START.md +36 -15
  4. package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +11 -5
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/README.md +18 -0
  15. package/docs/zh/00_QUICK_START.md +36 -15
  16. package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
  17. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  18. package/docs/zh/05_TUI_GUIDE.md +6 -0
  19. package/docs/zh/09_DOCTOR.md +11 -5
  20. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  21. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  22. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  23. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  24. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  25. package/docs/zh/README.md +18 -0
  26. package/package.json +1 -1
  27. package/pyproject.toml +1 -1
  28. package/src/deepscientist/__init__.py +1 -1
  29. package/src/deepscientist/acp/envelope.py +6 -0
  30. package/src/deepscientist/artifact/service.py +647 -22
  31. package/src/deepscientist/bash_exec/service.py +234 -9
  32. package/src/deepscientist/cli.py +115 -19
  33. package/src/deepscientist/codex_cli_compat.py +232 -0
  34. package/src/deepscientist/config/models.py +2 -1
  35. package/src/deepscientist/config/service.py +31 -9
  36. package/src/deepscientist/daemon/api/handlers.py +125 -6
  37. package/src/deepscientist/daemon/api/router.py +4 -0
  38. package/src/deepscientist/daemon/app.py +715 -98
  39. package/src/deepscientist/gitops/__init__.py +10 -1
  40. package/src/deepscientist/gitops/diff.py +129 -0
  41. package/src/deepscientist/gitops/service.py +4 -1
  42. package/src/deepscientist/mcp/server.py +39 -0
  43. package/src/deepscientist/prompts/builder.py +255 -32
  44. package/src/deepscientist/quest/layout.py +15 -2
  45. package/src/deepscientist/quest/service.py +295 -43
  46. package/src/deepscientist/quest/stage_views.py +6 -1
  47. package/src/deepscientist/runners/codex.py +86 -31
  48. package/src/deepscientist/skills/__init__.py +2 -2
  49. package/src/deepscientist/skills/installer.py +196 -5
  50. package/src/deepscientist/skills/registry.py +66 -0
  51. package/src/prompts/connectors/qq.md +18 -8
  52. package/src/prompts/connectors/weixin.md +16 -6
  53. package/src/prompts/contracts/shared_interaction.md +12 -1
  54. package/src/prompts/system.md +10 -5
  55. package/src/prompts/system_copilot.md +43 -0
  56. package/src/skills/analysis-campaign/SKILL.md +1 -0
  57. package/src/skills/baseline/SKILL.md +8 -0
  58. package/src/skills/decision/SKILL.md +8 -0
  59. package/src/skills/experiment/SKILL.md +8 -0
  60. package/src/skills/figure-polish/SKILL.md +1 -0
  61. package/src/skills/finalize/SKILL.md +1 -0
  62. package/src/skills/idea/SKILL.md +1 -0
  63. package/src/skills/intake-audit/SKILL.md +8 -0
  64. package/src/skills/mentor/SKILL.md +217 -0
  65. package/src/skills/mentor/references/correction-rules.md +210 -0
  66. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  67. package/src/skills/mentor/references/persona-profile.md +138 -0
  68. package/src/skills/mentor/references/taste-profile.md +128 -0
  69. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  70. package/src/skills/mentor/references/work-profile.md +289 -0
  71. package/src/skills/mentor/references/workflow-profile.md +240 -0
  72. package/src/skills/optimize/SKILL.md +1 -0
  73. package/src/skills/rebuttal/SKILL.md +1 -0
  74. package/src/skills/review/SKILL.md +1 -0
  75. package/src/skills/scout/SKILL.md +8 -0
  76. package/src/skills/write/SKILL.md +1 -0
  77. package/src/tui/dist/app/AppContainer.js +19 -11
  78. package/src/tui/dist/index.js +4 -1
  79. package/src/tui/dist/lib/api.js +33 -3
  80. package/src/tui/package.json +1 -1
  81. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  82. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  83. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  84. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  85. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  86. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  87. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  88. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  89. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  90. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  91. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  92. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  93. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  94. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  95. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  96. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  97. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  98. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  99. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  100. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  101. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  102. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  103. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  104. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  105. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  106. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  107. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  108. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  109. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  110. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  111. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  112. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  113. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  114. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  115. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  116. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  117. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  118. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  119. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  120. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  121. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  122. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  123. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  124. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  125. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  126. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  127. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  128. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  129. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  130. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  131. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  132. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  133. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  134. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  135. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  136. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  137. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  138. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  139. package/src/ui/dist/index.html +5 -2
  140. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  141. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  142. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  143. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  144. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  145. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  146. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  147. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  148. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  149. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  150. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  151. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  152. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  153. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  154. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  155. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  156. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  157. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  158. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  159. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  160. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  161. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  162. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  163. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  164. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  165. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  166. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  167. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  168. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  169. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  170. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  171. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  172. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  173. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  174. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  175. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  176. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  177. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  178. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  179. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  180. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  181. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  182. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  183. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  184. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  185. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  186. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  187. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  188. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  189. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  190. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  191. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  192. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  193. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -2,12 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
+ import shutil
5
6
  import subprocess
6
7
  import tomllib
7
8
  from functools import lru_cache
9
+ from pathlib import Path
10
+
11
+ from .shared import ensure_dir, read_text, write_text
8
12
 
9
13
  _MIN_XHIGH_SUPPORTED_VERSION = (0, 63, 0)
10
14
  _CODEX_VERSION_PATTERN = re.compile(r"codex-cli\s+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE)
15
+ _CODEX_HOME_SYNCED_FILES = ("config.toml", "auth.json")
16
+ _CODEX_HOME_SYNCED_DIRS = ("skills", "agents", "prompts")
17
+ _CODEX_HOME_QUEST_OVERLAY_DIRS = ("skills", "prompts")
11
18
 
12
19
 
13
20
  def parse_codex_cli_version(text: str) -> tuple[int, int, int] | None:
@@ -115,3 +122,228 @@ def adapt_profile_only_provider_config(
115
122
  f"{', '.join(injected_fields)} to the top level for Codex compatibility."
116
123
  ),
117
124
  )
125
+
126
+
127
+ def _remove_tree_path(path: Path) -> None:
128
+ if not path.exists() and not path.is_symlink():
129
+ return
130
+ if path.is_symlink() or path.is_file():
131
+ path.unlink()
132
+ return
133
+ shutil.rmtree(path)
134
+
135
+
136
+ def _overlay_file_sources(*roots: Path) -> dict[Path, Path]:
137
+ merged: dict[Path, Path] = {}
138
+ for root in roots:
139
+ if not root.exists() or not root.is_dir():
140
+ continue
141
+ for source_path in sorted(root.rglob("*")):
142
+ if not source_path.is_file():
143
+ continue
144
+ merged[source_path.relative_to(root)] = source_path
145
+ return merged
146
+
147
+
148
+ def _sync_overlay_directory(target_dir: Path, *source_dirs: Path) -> None:
149
+ desired_files = _overlay_file_sources(*source_dirs)
150
+ if not desired_files:
151
+ _remove_tree_path(target_dir)
152
+ return
153
+
154
+ desired_dirs: set[Path] = {Path(".")}
155
+ for relative in desired_files:
156
+ parent = relative.parent
157
+ while True:
158
+ desired_dirs.add(parent)
159
+ if parent == Path("."):
160
+ break
161
+ parent = parent.parent
162
+
163
+ if target_dir.exists() or target_dir.is_symlink():
164
+ for existing_path in sorted(target_dir.rglob("*"), reverse=True):
165
+ relative = existing_path.relative_to(target_dir)
166
+ if existing_path.is_dir() and not existing_path.is_symlink():
167
+ if relative not in desired_dirs:
168
+ shutil.rmtree(existing_path)
169
+ continue
170
+ if relative not in desired_files:
171
+ existing_path.unlink()
172
+
173
+ ensure_dir(target_dir)
174
+ for relative in sorted(desired_dirs, key=lambda item: (len(item.parts), item.as_posix())):
175
+ if relative == Path("."):
176
+ continue
177
+ current = target_dir / relative
178
+ if current.exists() and (current.is_symlink() or current.is_file()):
179
+ current.unlink()
180
+ ensure_dir(current)
181
+
182
+ for relative, source_path in desired_files.items():
183
+ target_path = target_dir / relative
184
+ if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
185
+ shutil.rmtree(target_path)
186
+ ensure_dir(target_path.parent)
187
+ try:
188
+ same_path = source_path.resolve() == target_path.resolve()
189
+ except FileNotFoundError:
190
+ same_path = False
191
+ if same_path:
192
+ continue
193
+ shutil.copy2(source_path, target_path)
194
+
195
+
196
+ def materialize_codex_runtime_home(
197
+ *,
198
+ source_home: str | Path,
199
+ target_home: str | Path,
200
+ profile: str = "",
201
+ quest_codex_root: str | Path | None = None,
202
+ ) -> str | None:
203
+ source_root = Path(source_home).expanduser()
204
+ target_root = ensure_dir(Path(target_home))
205
+
206
+ for filename in _CODEX_HOME_SYNCED_FILES:
207
+ source_path = source_root / filename
208
+ target_path = target_root / filename
209
+ if not source_path.exists():
210
+ _remove_tree_path(target_path)
211
+ continue
212
+ if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
213
+ shutil.rmtree(target_path)
214
+ ensure_dir(target_path.parent)
215
+ try:
216
+ same_path = source_path.resolve() == target_path.resolve()
217
+ except FileNotFoundError:
218
+ same_path = False
219
+ if not same_path:
220
+ shutil.copy2(source_path, target_path)
221
+
222
+ overlay_root = Path(quest_codex_root) if quest_codex_root is not None else None
223
+ for dirname in _CODEX_HOME_SYNCED_DIRS:
224
+ overlay_dir = overlay_root / dirname if overlay_root is not None and dirname in _CODEX_HOME_QUEST_OVERLAY_DIRS else None
225
+ source_dirs: list[Path] = [source_root / dirname]
226
+ if overlay_dir is not None:
227
+ source_dirs.append(overlay_dir)
228
+ _sync_overlay_directory(target_root / dirname, *source_dirs)
229
+
230
+ warning: str | None = None
231
+ config_path = target_root / "config.toml"
232
+ if profile and config_path.exists():
233
+ adapted_text, warning = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
234
+ write_text(config_path, adapted_text)
235
+ return warning
236
+
237
+
238
+ def provider_profile_metadata(
239
+ config_text: str,
240
+ *,
241
+ profile: str,
242
+ ) -> dict[str, str | bool | None]:
243
+ normalized_profile = str(profile or "").strip()
244
+ if not normalized_profile or not str(config_text or "").strip():
245
+ return {
246
+ "provider": None,
247
+ "model": None,
248
+ "env_key": None,
249
+ "base_url": None,
250
+ "wire_api": None,
251
+ "requires_openai_auth": None,
252
+ }
253
+ try:
254
+ parsed = tomllib.loads(config_text)
255
+ except tomllib.TOMLDecodeError:
256
+ return {
257
+ "provider": None,
258
+ "model": None,
259
+ "env_key": None,
260
+ "base_url": None,
261
+ "wire_api": None,
262
+ "requires_openai_auth": None,
263
+ }
264
+
265
+ profiles = parsed.get("profiles")
266
+ if not isinstance(profiles, dict):
267
+ return {
268
+ "provider": None,
269
+ "model": None,
270
+ "env_key": None,
271
+ "base_url": None,
272
+ "wire_api": None,
273
+ "requires_openai_auth": None,
274
+ }
275
+ profile_payload = profiles.get(normalized_profile)
276
+ if not isinstance(profile_payload, dict):
277
+ return {
278
+ "provider": None,
279
+ "model": None,
280
+ "env_key": None,
281
+ "base_url": None,
282
+ "wire_api": None,
283
+ "requires_openai_auth": None,
284
+ }
285
+
286
+ model_provider = str(
287
+ profile_payload.get("model_provider")
288
+ or parsed.get("model_provider")
289
+ or ""
290
+ ).strip() or None
291
+ model = str(
292
+ profile_payload.get("model")
293
+ or parsed.get("model")
294
+ or ""
295
+ ).strip() or None
296
+ provider_payload = None
297
+ model_providers = parsed.get("model_providers")
298
+ if model_provider and isinstance(model_providers, dict):
299
+ candidate = model_providers.get(model_provider)
300
+ if isinstance(candidate, dict):
301
+ provider_payload = candidate
302
+
303
+ env_key = (
304
+ str(provider_payload.get("env_key") or "").strip()
305
+ if isinstance(provider_payload, dict)
306
+ else None
307
+ ) or None
308
+ base_url = (
309
+ str(provider_payload.get("base_url") or "").strip()
310
+ if isinstance(provider_payload, dict)
311
+ else None
312
+ ) or None
313
+ wire_api = (
314
+ str(provider_payload.get("wire_api") or "").strip()
315
+ if isinstance(provider_payload, dict)
316
+ else None
317
+ ) or None
318
+ requires_openai_auth = (
319
+ bool(provider_payload.get("requires_openai_auth"))
320
+ if isinstance(provider_payload, dict) and "requires_openai_auth" in provider_payload
321
+ else None
322
+ )
323
+
324
+ return {
325
+ "provider": model_provider,
326
+ "model": model,
327
+ "env_key": env_key,
328
+ "base_url": base_url,
329
+ "wire_api": wire_api,
330
+ "requires_openai_auth": requires_openai_auth,
331
+ }
332
+
333
+
334
+ def provider_profile_metadata_from_home(
335
+ config_home: str | Path,
336
+ *,
337
+ profile: str,
338
+ ) -> dict[str, str | bool | None]:
339
+ config_path = Path(config_home).expanduser() / "config.toml"
340
+ if not config_path.exists():
341
+ return {
342
+ "provider": None,
343
+ "model": None,
344
+ "env_key": None,
345
+ "base_url": None,
346
+ "wire_api": None,
347
+ "requires_openai_auth": None,
348
+ }
349
+ return provider_profile_metadata(config_path.read_text(encoding="utf-8"), profile=profile)
@@ -38,6 +38,7 @@ def default_config(home: Path) -> dict:
38
38
  "ui": {
39
39
  "host": "0.0.0.0",
40
40
  "port": 20999,
41
+ "auth_enabled": False,
41
42
  "auto_open_browser": True,
42
43
  "default_mode": "web",
43
44
  },
@@ -96,7 +97,7 @@ def default_runners() -> dict:
96
97
  "binary": "codex",
97
98
  "config_dir": "~/.codex",
98
99
  "profile": "",
99
- "model": "gpt-5.4",
100
+ "model": "inherit",
100
101
  "model_reasoning_effort": "xhigh",
101
102
  "approval_policy": "never",
102
103
  "sandbox_mode": "danger-full-access",
@@ -4,13 +4,17 @@ import json
4
4
  import os
5
5
  import subprocess
6
6
  import tempfile
7
- from shutil import copy2
8
7
  from copy import deepcopy
9
8
  from pathlib import Path
10
9
  from urllib.error import URLError
11
10
  from urllib.request import Request
12
11
 
13
- from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
12
+ from ..codex_cli_compat import (
13
+ adapt_profile_only_provider_config,
14
+ materialize_codex_runtime_home,
15
+ normalize_codex_reasoning_effort,
16
+ provider_profile_metadata_from_home,
17
+ )
14
18
  from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
15
19
  from ..connector_runtime import build_discovered_target, infer_connector_transport
16
20
  from ..home import repo_root
@@ -1192,6 +1196,14 @@ Use **Test** when the file exposes runtime dependencies.
1192
1196
  return "gpt-5.4"
1193
1197
  return str(raw_model).strip()
1194
1198
 
1199
+ @staticmethod
1200
+ def _codex_effective_model(config: dict) -> str:
1201
+ requested = ConfigManager._codex_requested_model(config)
1202
+ profile = ConfigManager._codex_profile_name(config)
1203
+ if profile and not ConfigManager._codex_should_inherit_model(requested):
1204
+ return "inherit"
1205
+ return requested
1206
+
1195
1207
  @staticmethod
1196
1208
  def _codex_profile_name(config: dict) -> str:
1197
1209
  raw_profile = config.get("profile")
@@ -1233,10 +1245,11 @@ Use **Test** when the file exposes runtime dependencies.
1233
1245
 
1234
1246
  temp_home = tempfile.TemporaryDirectory(prefix="ds-codex-probe-")
1235
1247
  temp_root = Path(temp_home.name)
1236
- for filename in ("auth.json",):
1237
- source_path = expanded / filename
1238
- if source_path.exists():
1239
- copy2(source_path, temp_root / filename)
1248
+ materialize_codex_runtime_home(
1249
+ source_home=expanded,
1250
+ target_home=temp_root,
1251
+ profile=profile,
1252
+ )
1240
1253
  write_text(temp_root / "config.toml", adapted_text)
1241
1254
  return str(temp_root), warning, temp_home
1242
1255
 
@@ -1361,6 +1374,7 @@ Use **Test** when the file exposes runtime dependencies.
1361
1374
  resolved_binary = resolve_runner_binary(binary, runner_name="codex")
1362
1375
  profile = self._codex_profile_name(config)
1363
1376
  requested_model = self._codex_requested_model(config)
1377
+ effective_model = self._codex_effective_model(config)
1364
1378
  raw_reasoning_effort = config.get("model_reasoning_effort")
1365
1379
  requested_reasoning_effort = (
1366
1380
  str(raw_reasoning_effort).strip()
@@ -1376,9 +1390,9 @@ Use **Test** when the file exposes runtime dependencies.
1376
1390
  "resolved_binary": resolved_binary,
1377
1391
  "config_dir": str(config.get("config_dir") or "~/.codex"),
1378
1392
  "profile": profile,
1379
- "model": requested_model or "inherit",
1393
+ "model": effective_model or "inherit",
1380
1394
  "requested_model": requested_model or "inherit",
1381
- "effective_model": requested_model or "inherit",
1395
+ "effective_model": effective_model or "inherit",
1382
1396
  "approval_policy": str(config.get("approval_policy") or "on-request"),
1383
1397
  "sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
1384
1398
  "reasoning_effort": reasoning_effort,
@@ -1416,9 +1430,17 @@ Use **Test** when the file exposes runtime dependencies.
1416
1430
  env["CODEX_HOME"] = prepared_home
1417
1431
  if profile_config_warning:
1418
1432
  compatibility_warnings.append(profile_config_warning)
1433
+ metadata = provider_profile_metadata_from_home(env.get("CODEX_HOME") or config_dir, profile=profile)
1434
+ if metadata.get("requires_openai_auth") is False:
1435
+ env.pop("OPENAI_API_KEY", None)
1436
+ env.pop("OPENAI_BASE_URL", None)
1419
1437
  prompt = "Reply with exactly HELLO."
1420
1438
  if reasoning_effort_warning:
1421
1439
  compatibility_warnings.append(reasoning_effort_warning)
1440
+ if profile and effective_model == "inherit" and not self._codex_should_inherit_model(requested_model):
1441
+ compatibility_warnings.append(
1442
+ f"Codex profile `{profile}` is provider-backed. DeepScientist is probing it with `model: inherit`."
1443
+ )
1422
1444
  base_warnings: list[str] = list(compatibility_warnings)
1423
1445
 
1424
1446
  def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
@@ -1445,7 +1467,7 @@ Use **Test** when the file exposes runtime dependencies.
1445
1467
  return command, None, exc
1446
1468
  return command, result, None
1447
1469
 
1448
- command, result, timeout_error = run_probe_once(requested_model)
1470
+ command, result, timeout_error = run_probe_once(effective_model)
1449
1471
  if timeout_error is not None:
1450
1472
  details.update(
1451
1473
  {
@@ -17,6 +17,11 @@ from ...quest import QuestService
17
17
  from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
18
18
  from ...runners import RunRequest
19
19
 
20
+ _COPILOT_LEAD_MESSAGE = (
21
+ "我是 DeepScientist,任何事情都可以找我帮忙。"
22
+ "你可以让我读论文、改代码、看实验、整理思路,或者直接开始执行一个任务。"
23
+ )
24
+
20
25
 
21
26
  class ApiHandlers:
22
27
  def __init__(self, app: "DaemonApp") -> None:
@@ -73,6 +78,8 @@ class ApiHandlers:
73
78
  runtime_payload = {
74
79
  "surface": "quest",
75
80
  "version": DEEPSCIENTIST_VERSION,
81
+ "homePath": str(self.app.home),
82
+ "auth": self.app.browser_auth_runtime_payload(),
76
83
  "supports": {
77
84
  "productApis": False,
78
85
  "socketIo": False,
@@ -158,9 +165,87 @@ npm --prefix src/ui run build</pre>
158
165
  "daemon_id": self.app.daemon_id,
159
166
  "managed_by": self.app.daemon_managed_by,
160
167
  "pid": os.getpid(),
168
+ "auth_enabled": self.app.browser_auth_enabled,
161
169
  "sessions": self.app.sessions.snapshot(),
162
170
  }
163
171
 
172
+ def auth_login(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
173
+ if not self.app.browser_auth_enabled:
174
+ payload = {
175
+ "ok": True,
176
+ "authenticated": True,
177
+ "auth_enabled": False,
178
+ }
179
+ return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
180
+
181
+ candidate = str(((body or {}) if isinstance(body, dict) else {}).get("token") or "").strip()
182
+ if not candidate:
183
+ return 400, {
184
+ "ok": False,
185
+ "message": "Token is required.",
186
+ "auth_required": True,
187
+ "auth_enabled": True,
188
+ }
189
+ if not self.app.browser_auth_matches(candidate):
190
+ return 401, {
191
+ "ok": False,
192
+ "message": "Invalid token.",
193
+ "auth_required": True,
194
+ "auth_enabled": True,
195
+ }
196
+ payload = {
197
+ "ok": True,
198
+ "authenticated": True,
199
+ "auth_enabled": True,
200
+ "token_masked": self.app.masked_browser_auth_token(),
201
+ }
202
+ return (
203
+ 200,
204
+ {
205
+ "Content-Type": "application/json; charset=utf-8",
206
+ "Cache-Control": "no-store, max-age=0, must-revalidate",
207
+ "Set-Cookie": self.app._browser_auth_cookie_header(candidate),
208
+ },
209
+ json.dumps(payload, ensure_ascii=False),
210
+ )
211
+
212
+ def auth_token(self) -> dict:
213
+ return {
214
+ "ok": True,
215
+ "auth_enabled": self.app.browser_auth_enabled,
216
+ "token": self.app.browser_auth_token,
217
+ "token_masked": self.app.masked_browser_auth_token(),
218
+ }
219
+
220
+ def auth_rotate(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
221
+ if not self.app.browser_auth_enabled:
222
+ payload = {
223
+ "ok": True,
224
+ "auth_enabled": False,
225
+ "rotated": False,
226
+ "token": None,
227
+ "token_masked": None,
228
+ }
229
+ return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
230
+
231
+ rotated = self.app.rotate_browser_auth_token()
232
+ payload = {
233
+ "ok": True,
234
+ "auth_enabled": True,
235
+ "rotated": True,
236
+ "token": rotated,
237
+ "token_masked": self.app.masked_browser_auth_token(),
238
+ }
239
+ return (
240
+ 200,
241
+ {
242
+ "Content-Type": "application/json; charset=utf-8",
243
+ "Cache-Control": "no-store, max-age=0, must-revalidate",
244
+ "Set-Cookie": self.app._browser_auth_cookie_header(rotated),
245
+ },
246
+ json.dumps(payload, ensure_ascii=False),
247
+ )
248
+
164
249
  def system_update(self) -> dict:
165
250
  return self.app.system_update_status()
166
251
 
@@ -324,6 +409,33 @@ npm --prefix src/ui run build</pre>
324
409
  return 400, {"ok": False, "message": str(exc)}
325
410
  except RuntimeError as exc:
326
411
  return 409, {"ok": False, "message": str(exc)}
412
+ workspace_mode = (
413
+ str(startup_contract.get("workspace_mode") or "").strip().lower()
414
+ if isinstance(startup_contract, dict)
415
+ else ""
416
+ )
417
+ if workspace_mode in {"copilot", "autonomous"}:
418
+ quest_root = self.app.quest_service._quest_root(snapshot["quest_id"])
419
+ self.app.quest_service.update_research_state(quest_root, workspace_mode=workspace_mode)
420
+ if workspace_mode == "copilot":
421
+ self.app.quest_service.append_message(
422
+ snapshot["quest_id"],
423
+ "assistant",
424
+ _COPILOT_LEAD_MESSAGE,
425
+ source="deepscientist",
426
+ )
427
+ self.app.quest_service.update_runtime_state(
428
+ quest_root=quest_root,
429
+ status="idle",
430
+ display_status="idle",
431
+ )
432
+ self.app.quest_service.set_continuation_state(
433
+ quest_root,
434
+ policy="wait_for_user_or_resume",
435
+ anchor="decision",
436
+ reason="copilot_mode",
437
+ )
438
+ snapshot = self.app.quest_service.snapshot(snapshot["quest_id"])
327
439
  payload: dict[str, object] = {"ok": True, "snapshot": snapshot}
328
440
  if auto_start:
329
441
  startup = self.app.submit_user_message(
@@ -474,11 +586,12 @@ npm --prefix src/ui run build</pre>
474
586
 
475
587
  def quest_session(self, quest_id: str) -> dict:
476
588
  snapshot = self.app.quest_service.snapshot_fast(quest_id)
477
- for kind in ("details", "canvas"):
589
+ for kind in ("details", "canvas", "git_canvas"):
478
590
  try:
479
591
  self.app.quest_service.prime_projection(quest_id, kind)
480
592
  except Exception:
481
593
  continue
594
+ self.app.schedule_latest_quest_terminal_prewarm(quest_id)
482
595
  return {
483
596
  "ok": True,
484
597
  "quest_id": quest_id,
@@ -496,7 +609,7 @@ npm --prefix src/ui run build</pre>
496
609
  tail = tail_raw in {"1", "true", "yes", "on"}
497
610
  format_name = ((query.get("format") or ["both"])[0] or "both").lower()
498
611
  session_id = ((query.get("session_id") or [f"quest:{quest_id}"])[0] or f"quest:{quest_id}")
499
- payload = self._fresh_quest_service().events(
612
+ payload = self.app.quest_service.events(
500
613
  quest_id,
501
614
  after=after,
502
615
  before=before,
@@ -1028,6 +1141,15 @@ npm --prefix src/ui run build</pre>
1028
1141
  node["optimization_candidate_count"] = candidate_count_by_branch.get(ref, 0)
1029
1142
  return payload
1030
1143
 
1144
+ def git_canvas(self, quest_id: str) -> dict:
1145
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
1146
+ payload = self.app.quest_service.git_commit_canvas(quest_id)
1147
+ research_state = self.app.quest_service.read_research_state(quest_root)
1148
+ active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
1149
+ payload["active_workspace_ref"] = active_workspace_branch
1150
+ payload["workspace_mode"] = str(research_state.get("workspace_mode") or "copilot").strip() or "copilot"
1151
+ return payload
1152
+
1031
1153
  def git_log(self, quest_id: str, path: str) -> dict:
1032
1154
  query = self.parse_query(path)
1033
1155
  ref = ((query.get("ref") or [""])[0] or "").strip()
@@ -1390,10 +1512,7 @@ npm --prefix src/ui run build</pre>
1390
1512
  mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
1391
1513
  content = quest_service._read_git_bytes(quest_root, revision, relative)
1392
1514
  return 200, self._asset_headers(mime_type), content
1393
- path, _writable, _scope, _source_kind = quest_service._resolve_document(
1394
- quest_service._quest_root(quest_id),
1395
- document_id,
1396
- )
1515
+ path, _writable, _scope, _source_kind = quest_service.resolve_document(quest_id, document_id)
1397
1516
  if not path.exists() or not path.is_file():
1398
1517
  return 404, {"Content-Type": "text/plain; charset=utf-8"}, b"Not Found"
1399
1518
  mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
@@ -9,6 +9,9 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
9
9
  ("GET", re.compile(r"^/metis/agent/api/health$"), "lingzhu_health"),
10
10
  ("POST", re.compile(r"^/metis/agent/api/sse$"), "lingzhu_sse"),
11
11
  ("GET", re.compile(r"^/(?P<spa_path>(?!api(?:/|$)|metis(?:/|$)|ui(?:/|$)|assets(?:/|$)).+)$"), "spa_root"),
12
+ ("POST", re.compile(r"^/api/auth/login$"), "auth_login"),
13
+ ("GET", re.compile(r"^/api/auth/token$"), "auth_token"),
14
+ ("POST", re.compile(r"^/api/auth/rotate$"), "auth_rotate"),
12
15
  ("GET", re.compile(r"^/api/health$"), "health"),
13
16
  ("GET", re.compile(r"^/api/system/update$"), "system_update"),
14
17
  ("POST", re.compile(r"^/api/system/update$"), "system_update_action"),
@@ -63,6 +66,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
63
66
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/metrics/timeline$"), "metrics_timeline"),
64
67
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/baselines/compare$"), "baseline_compare"),
65
68
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/branches$"), "git_branches"),
69
+ ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/canvas$"), "git_canvas"),
66
70
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/log$"), "git_log"),
67
71
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/compare$"), "git_compare"),
68
72
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),