@researai/deepscientist 1.5.15 → 1.5.17

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 (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  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 +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  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/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -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(
@@ -411,7 +523,10 @@ npm --prefix src/ui run build</pre>
411
523
  }
412
524
 
413
525
  def quest(self, quest_id: str) -> dict:
414
- return self.app.quest_service.snapshot(quest_id)
526
+ try:
527
+ return self.app.quest_service.snapshot(quest_id)
528
+ except FileNotFoundError:
529
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
415
530
 
416
531
  def quest_delete(self, quest_id: str, body: dict | None = None) -> dict | tuple[int, dict]:
417
532
  source = "web"
@@ -424,6 +539,7 @@ npm --prefix src/ui run build</pre>
424
539
  "title": body.get("title") if "title" in body else None,
425
540
  "active_anchor": body.get("active_anchor") if "active_anchor" in body else None,
426
541
  "default_runner": body.get("default_runner") if "default_runner" in body else None,
542
+ "workspace_mode": body.get("workspace_mode") if "workspace_mode" in body else None,
427
543
  }
428
544
  if all(value is None for value in updates.values()):
429
545
  return {
@@ -473,12 +589,16 @@ npm --prefix src/ui run build</pre>
473
589
  }
474
590
 
475
591
  def quest_session(self, quest_id: str) -> dict:
476
- snapshot = self.app.quest_service.snapshot_fast(quest_id)
477
- for kind in ("details", "canvas"):
592
+ try:
593
+ snapshot = self.app.quest_service.snapshot_fast(quest_id)
594
+ except FileNotFoundError:
595
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
596
+ for kind in ("details", "canvas", "git_canvas"):
478
597
  try:
479
598
  self.app.quest_service.prime_projection(quest_id, kind)
480
599
  except Exception:
481
600
  continue
601
+ self.app.schedule_latest_quest_terminal_prewarm(quest_id)
482
602
  return {
483
603
  "ok": True,
484
604
  "quest_id": quest_id,
@@ -496,7 +616,7 @@ npm --prefix src/ui run build</pre>
496
616
  tail = tail_raw in {"1", "true", "yes", "on"}
497
617
  format_name = ((query.get("format") or ["both"])[0] or "both").lower()
498
618
  session_id = ((query.get("session_id") or [f"quest:{quest_id}"])[0] or f"quest:{quest_id}")
499
- payload = self._fresh_quest_service().events(
619
+ payload = self.app.quest_service.events(
500
620
  quest_id,
501
621
  after=after,
502
622
  before=before,
@@ -1028,6 +1148,15 @@ npm --prefix src/ui run build</pre>
1028
1148
  node["optimization_candidate_count"] = candidate_count_by_branch.get(ref, 0)
1029
1149
  return payload
1030
1150
 
1151
+ def git_canvas(self, quest_id: str) -> dict:
1152
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
1153
+ payload = self.app.quest_service.git_commit_canvas(quest_id)
1154
+ research_state = self.app.quest_service.read_research_state(quest_root)
1155
+ active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
1156
+ payload["active_workspace_ref"] = active_workspace_branch
1157
+ payload["workspace_mode"] = str(research_state.get("workspace_mode") or "copilot").strip() or "copilot"
1158
+ return payload
1159
+
1031
1160
  def git_log(self, quest_id: str, path: str) -> dict:
1032
1161
  query = self.parse_query(path)
1033
1162
  ref = ((query.get("ref") or [""])[0] or "").strip()
@@ -1352,19 +1481,25 @@ npm --prefix src/ui run build</pre>
1352
1481
  )
1353
1482
 
1354
1483
  def documents(self, quest_id: str) -> list[dict]:
1355
- return self.app.quest_service.list_documents(quest_id)
1484
+ try:
1485
+ return self.app.quest_service.list_documents(quest_id)
1486
+ except FileNotFoundError:
1487
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1356
1488
 
1357
1489
  def explorer(self, quest_id: str, path: str) -> dict:
1358
1490
  query = self.parse_query(path)
1359
1491
  revision = ((query.get("revision") or [""])[0] or "").strip() or None
1360
1492
  mode = ((query.get("mode") or [""])[0] or "").strip() or None
1361
1493
  profile = ((query.get("profile") or [""])[0] or "").strip() or None
1362
- return self.app.quest_service.explorer(
1363
- quest_id,
1364
- revision=revision,
1365
- mode=mode,
1366
- profile=profile,
1367
- )
1494
+ try:
1495
+ return self.app.quest_service.explorer(
1496
+ quest_id,
1497
+ revision=revision,
1498
+ mode=mode,
1499
+ profile=profile,
1500
+ )
1501
+ except FileNotFoundError:
1502
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1368
1503
 
1369
1504
  def quest_search(self, quest_id: str, path: str) -> dict:
1370
1505
  query = self.parse_query(path)
@@ -1373,7 +1508,90 @@ npm --prefix src/ui run build</pre>
1373
1508
  limit = int(((query.get("limit") or ["50"])[0] or "50").strip())
1374
1509
  except ValueError:
1375
1510
  limit = 50
1376
- return self.app.quest_service.search_files(quest_id, term=term, limit=limit)
1511
+ try:
1512
+ return self.app.quest_service.search_files(quest_id, term=term, limit=limit)
1513
+ except FileNotFoundError:
1514
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1515
+
1516
+ def quest_file_create_folder(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
1517
+ try:
1518
+ return self._fresh_quest_service().create_workspace_folder(
1519
+ quest_id,
1520
+ name=body.get("name"),
1521
+ parent_path=body.get("parent_path"),
1522
+ )
1523
+ except FileNotFoundError as exc:
1524
+ return 404, {"ok": False, "message": str(exc)}
1525
+ except FileExistsError as exc:
1526
+ return 409, {"ok": False, "message": str(exc)}
1527
+ except ValueError as exc:
1528
+ return 400, {"ok": False, "message": str(exc)}
1529
+
1530
+ def quest_file_upload(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
1531
+ file_name = str(body.get("file_name") or "").strip()
1532
+ content_base64 = str(body.get("content_base64") or "").strip()
1533
+ mime_type = str(body.get("mime_type") or "").strip() or None
1534
+ if not file_name:
1535
+ return 400, {"ok": False, "message": "`file_name` is required."}
1536
+ if not content_base64:
1537
+ return 400, {"ok": False, "message": "`content_base64` is required."}
1538
+ try:
1539
+ content = base64.b64decode(content_base64, validate=True)
1540
+ except (ValueError, TypeError):
1541
+ return 400, {"ok": False, "message": "Invalid `content_base64` payload."}
1542
+ try:
1543
+ return self._fresh_quest_service().upload_workspace_file(
1544
+ quest_id,
1545
+ file_name=file_name,
1546
+ content=content,
1547
+ mime_type=mime_type,
1548
+ parent_path=body.get("parent_path"),
1549
+ )
1550
+ except FileNotFoundError as exc:
1551
+ return 404, {"ok": False, "message": str(exc)}
1552
+ except FileExistsError as exc:
1553
+ return 409, {"ok": False, "message": str(exc)}
1554
+ except ValueError as exc:
1555
+ return 400, {"ok": False, "message": str(exc)}
1556
+
1557
+ def quest_file_rename(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
1558
+ try:
1559
+ return self._fresh_quest_service().rename_workspace_entry(
1560
+ quest_id,
1561
+ path=body.get("path"),
1562
+ new_name=body.get("new_name"),
1563
+ )
1564
+ except FileNotFoundError as exc:
1565
+ return 404, {"ok": False, "message": str(exc)}
1566
+ except FileExistsError as exc:
1567
+ return 409, {"ok": False, "message": str(exc)}
1568
+ except ValueError as exc:
1569
+ return 400, {"ok": False, "message": str(exc)}
1570
+
1571
+ def quest_file_move(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
1572
+ try:
1573
+ return self._fresh_quest_service().move_workspace_entries(
1574
+ quest_id,
1575
+ paths=body.get("paths"),
1576
+ target_parent_path=body.get("target_parent_path"),
1577
+ )
1578
+ except FileNotFoundError as exc:
1579
+ return 404, {"ok": False, "message": str(exc)}
1580
+ except FileExistsError as exc:
1581
+ return 409, {"ok": False, "message": str(exc)}
1582
+ except ValueError as exc:
1583
+ return 400, {"ok": False, "message": str(exc)}
1584
+
1585
+ def quest_file_delete(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
1586
+ try:
1587
+ return self._fresh_quest_service().delete_workspace_entries(
1588
+ quest_id,
1589
+ paths=body.get("paths"),
1590
+ )
1591
+ except FileNotFoundError as exc:
1592
+ return 404, {"ok": False, "message": str(exc)}
1593
+ except ValueError as exc:
1594
+ return 400, {"ok": False, "message": str(exc)}
1377
1595
 
1378
1596
  def document_asset(self, quest_id: str, path: str) -> tuple[int, dict, bytes]:
1379
1597
  quest_service = self._fresh_quest_service()
@@ -1390,17 +1608,17 @@ npm --prefix src/ui run build</pre>
1390
1608
  mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
1391
1609
  content = quest_service._read_git_bytes(quest_root, revision, relative)
1392
1610
  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
- )
1611
+ path, _writable, _scope, _source_kind = quest_service.resolve_document(quest_id, document_id)
1397
1612
  if not path.exists() or not path.is_file():
1398
1613
  return 404, {"Content-Type": "text/plain; charset=utf-8"}, b"Not Found"
1399
1614
  mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
1400
1615
  return 200, self._asset_headers(mime_type), path.read_bytes()
1401
1616
 
1402
1617
  def document_open(self, quest_id: str, body: dict) -> dict:
1403
- return self._fresh_quest_service().open_document(quest_id, body["document_id"])
1618
+ try:
1619
+ return self._fresh_quest_service().open_document(quest_id, body["document_id"])
1620
+ except FileNotFoundError:
1621
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1404
1622
 
1405
1623
  def document_asset_upload(self, quest_id: str, body: dict) -> dict:
1406
1624
  document_id = str(body.get("document_id") or "").strip()
@@ -1418,22 +1636,28 @@ npm --prefix src/ui run build</pre>
1418
1636
  content = base64.b64decode(content_base64, validate=True)
1419
1637
  except (ValueError, TypeError):
1420
1638
  return {"ok": False, "message": "Invalid `content_base64` payload."}
1421
- return self.app.quest_service.save_document_asset(
1422
- quest_id,
1423
- document_id,
1424
- file_name=file_name,
1425
- mime_type=mime_type or None,
1426
- content=content,
1427
- kind=kind,
1428
- )
1639
+ try:
1640
+ return self.app.quest_service.save_document_asset(
1641
+ quest_id,
1642
+ document_id,
1643
+ file_name=file_name,
1644
+ mime_type=mime_type or None,
1645
+ content=content,
1646
+ kind=kind,
1647
+ )
1648
+ except FileNotFoundError:
1649
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1429
1650
 
1430
1651
  def document_save(self, quest_id: str, document_id: str, body: dict) -> dict:
1431
- return self.app.quest_service.save_document(
1432
- quest_id,
1433
- document_id,
1434
- body["content"],
1435
- previous_revision=body.get("revision"),
1436
- )
1652
+ try:
1653
+ return self.app.quest_service.save_document(
1654
+ quest_id,
1655
+ document_id,
1656
+ body["content"],
1657
+ previous_revision=body.get("revision"),
1658
+ )
1659
+ except FileNotFoundError:
1660
+ return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
1437
1661
 
1438
1662
  def latex_init(self, project_id: str, body: dict) -> dict:
1439
1663
  return self.app.latex_service.init_project(
@@ -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"),
@@ -74,6 +78,11 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
74
78
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents$"), "documents"),
75
79
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/explorer$"), "explorer"),
76
80
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/search$"), "quest_search"),
81
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/files/folder$"), "quest_file_create_folder"),
82
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/files/upload$"), "quest_file_upload"),
83
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/files/rename$"), "quest_file_rename"),
84
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/files/move$"), "quest_file_move"),
85
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/files/delete$"), "quest_file_delete"),
77
86
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents/asset$"), "document_asset"),
78
87
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents/open$"), "document_open"),
79
88
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents/assets$"), "document_asset_upload"),