@researai/deepscientist 1.5.14 → 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 (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -18,8 +18,11 @@ from ..connector.weixin_support import (
18
18
  WEIXIN_UPLOAD_MEDIA_FILE,
19
19
  WEIXIN_UPLOAD_MEDIA_IMAGE,
20
20
  WEIXIN_UPLOAD_MEDIA_VIDEO,
21
+ clear_weixin_context_send_state,
21
22
  download_weixin_remote_attachment,
23
+ get_weixin_context_entry,
22
24
  get_weixin_context_token,
25
+ mark_weixin_context_stale,
23
26
  normalize_weixin_base_url,
24
27
  normalize_weixin_cdn_base_url,
25
28
  send_weixin_message,
@@ -558,10 +561,11 @@ class QQConnectorBridge(BaseConnectorBridge):
558
561
  connector_delivery = item.get("connector_delivery") if isinstance(item.get("connector_delivery"), dict) else {}
559
562
  qq_delivery = connector_delivery.get("qq") if isinstance(connector_delivery.get("qq"), dict) else {}
560
563
  media_kind = str(qq_delivery.get("media_kind") or "").strip().lower()
564
+ allow_internal_auto_media = bool(qq_delivery.get("allow_internal_auto_media"))
561
565
  if media_kind not in {"image", "file"}:
562
566
  residual_items.append(item)
563
567
  continue
564
- if not native_enabled:
568
+ if not native_enabled and not allow_internal_auto_media:
565
569
  issues.append(
566
570
  {
567
571
  "attachment_index": index,
@@ -781,7 +785,9 @@ class WeixinConnectorBridge(BaseConnectorBridge):
781
785
  name = "weixin"
782
786
  _MEDIA_ITEM_TYPES = {2, 4, 5}
783
787
  _MEDIA_SEND_INITIAL_DELAY_SECONDS = 0.8
784
- _MEDIA_SEND_RETRY_DELAYS_SECONDS = (1.5, 3.0)
788
+ _TEXT_SEND_RETRY_DELAYS_SECONDS = (0.8, 1.5, 3.0)
789
+ _MEDIA_SEND_RETRY_DELAYS_SECONDS = (1.5, 3.0, 5.0)
790
+ _STALE_CONTEXT_SUPPRESSED_KINDS = {"progress", "milestone", "assistant", "summary", "ack"}
785
791
 
786
792
  def deliver(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
787
793
  return self.deliver_direct(payload, config)
@@ -800,14 +806,26 @@ class WeixinConnectorBridge(BaseConnectorBridge):
800
806
  "error": "Weixin outbound target is empty.",
801
807
  }
802
808
  connector_root = self._connector_root(config)
809
+ kind = str(payload.get("kind") or "").strip().lower()
810
+ context_entry = get_weixin_context_entry(connector_root, to_user_id)
803
811
  context_token = get_weixin_context_token(connector_root, to_user_id)
804
812
  if not context_token:
813
+ if kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
814
+ return self._queued_context_wait_response(
815
+ reason=f"Weixin context_token is missing for `{to_user_id}`. Waiting for the next inbound message.",
816
+ )
805
817
  return {
806
818
  "ok": False,
807
819
  "queued": False,
808
820
  "transport": "weixin-ilink",
809
821
  "error": f"Weixin context_token is missing for `{to_user_id}`. Wait for one inbound message first.",
810
822
  }
823
+ if bool(context_entry.get("stale_context")) and kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
824
+ stale_since = str(context_entry.get("stale_since") or "").strip()
825
+ warning = "Weixin outbound is paused until the next inbound message refreshes context_token."
826
+ if stale_since:
827
+ warning = f"{warning} stale_since={stale_since}"
828
+ return self._queued_context_wait_response(reason=warning)
811
829
 
812
830
  native_attachments, residual_attachments, warnings = self._partition_native_attachments(payload.get("attachments"))
813
831
  rendered_text = self.render_text(payload.get("text"), residual_attachments)
@@ -895,6 +913,20 @@ class WeixinConnectorBridge(BaseConnectorBridge):
895
913
  else:
896
914
  warnings.append("Weixin outbound payload contained neither text nor sendable attachments.")
897
915
  except Exception as exc:
916
+ if "ret=-2" in str(exc or "").lower():
917
+ mark_weixin_context_stale(
918
+ connector_root,
919
+ user_id=to_user_id,
920
+ error=str(exc),
921
+ kind=kind or None,
922
+ )
923
+ if kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
924
+ queued = self._queued_context_wait_response(
925
+ reason="Weixin send hit stale context and was deferred until the next inbound refresh.",
926
+ )
927
+ queued["parts"] = parts
928
+ queued["warnings"] = [*warnings, *(queued.get("warnings") or [])]
929
+ return queued
898
930
  return {
899
931
  "ok": False,
900
932
  "queued": False,
@@ -909,6 +941,12 @@ class WeixinConnectorBridge(BaseConnectorBridge):
909
941
  error_messages = [str(item.get("error") or "").strip() for item in failed if str(item.get("error") or "").strip()]
910
942
  error_messages.extend(warnings)
911
943
  last_success = succeeded[-1] if succeeded else {}
944
+ if succeeded:
945
+ clear_weixin_context_send_state(
946
+ connector_root,
947
+ user_id=to_user_id,
948
+ kind=kind or None,
949
+ )
912
950
  return {
913
951
  "ok": bool(succeeded),
914
952
  "queued": False,
@@ -920,6 +958,17 @@ class WeixinConnectorBridge(BaseConnectorBridge):
920
958
  "error": "; ".join(error_messages) if error_messages else None,
921
959
  }
922
960
 
961
+ @staticmethod
962
+ def _queued_context_wait_response(*, reason: str) -> dict[str, Any]:
963
+ return {
964
+ "ok": False,
965
+ "queued": True,
966
+ "transport": "weixin-ilink",
967
+ "parts": [],
968
+ "warnings": [str(reason or "").strip()],
969
+ "error": None,
970
+ }
971
+
923
972
  @staticmethod
924
973
  def _partition_native_attachments(
925
974
  attachments: Any,
@@ -25,23 +25,78 @@ from .registries import BaselineRegistry
25
25
  from .runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
26
26
  from .runtime_tools import RuntimeToolService
27
27
  from .runtime_logs import JsonlLogger
28
- from .shared import ensure_dir, read_yaml
28
+ from .shared import ensure_dir, read_json, read_yaml
29
29
  from .skills import SkillInstaller
30
30
  from .tui import watch_tui
31
31
 
32
32
 
33
+ class DeepScientistArgumentParser(argparse.ArgumentParser):
34
+ def error(self, message: str) -> None:
35
+ self.print_usage(sys.stderr)
36
+ self.exit(2, f"DeepScientist argument error: {message}\nRun `{self.prog} --help` for usage.\n")
37
+
38
+
33
39
  def _local_ui_url(host: str, port: int) -> str:
34
- connect_host = "0.0.0.0" if host in {"0.0.0.0", "::", ""} else host
35
- return f"http://{connect_host}:{port}"
40
+ normalized = str(host or "").strip()
41
+ connect_host = "127.0.0.1" if normalized in {"0.0.0.0", "::", "[::]", ""} else normalized
42
+ if connect_host.startswith("[") and connect_host.endswith("]"):
43
+ rendered_host = connect_host
44
+ elif ":" in connect_host:
45
+ rendered_host = f"[{connect_host}]"
46
+ else:
47
+ rendered_host = connect_host
48
+ return f"http://{rendered_host}:{port}"
49
+
50
+
51
+ def _parse_optional_bool(value: object) -> bool | None:
52
+ if isinstance(value, bool):
53
+ return value
54
+ normalized = str(value or "").strip().lower()
55
+ if not normalized:
56
+ return None
57
+ if normalized in {"1", "true", "yes", "on"}:
58
+ return True
59
+ if normalized in {"0", "false", "no", "off"}:
60
+ return False
61
+ return None
62
+
63
+
64
+ def _daemon_request_headers(home: Path) -> dict[str, str]:
65
+ headers = {"Content-Type": "application/json"}
66
+ state = read_json(home / "runtime" / "daemon.json", {})
67
+ if not isinstance(state, dict):
68
+ return headers
69
+ if bool(state.get("auth_enabled")):
70
+ token = str(state.get("auth_token") or "").strip()
71
+ if token:
72
+ headers["Authorization"] = f"Bearer {token}"
73
+ return headers
74
+
75
+
76
+ def _daemon_launch_url(home: Path, *, host: str, port: int) -> str:
77
+ state = read_json(home / "runtime" / "daemon.json", {})
78
+ if isinstance(state, dict):
79
+ launch_url = str(state.get("launch_url") or "").strip()
80
+ if launch_url:
81
+ return launch_url
82
+ return _local_ui_url(host, port)
36
83
 
37
84
 
38
85
  def build_parser() -> argparse.ArgumentParser:
39
- parser = argparse.ArgumentParser(prog="ds", description="DeepScientist Core skeleton")
86
+ parser = DeepScientistArgumentParser(
87
+ prog="ds",
88
+ description="DeepScientist Core skeleton",
89
+ allow_abbrev=False,
90
+ )
40
91
  parser.add_argument("--home", default=None, help="Override DeepScientist home")
41
92
  parser.add_argument("--proxy", default=None, help="Explicit outbound HTTP/WS proxy, for example `http://127.0.0.1:7890`.")
42
93
  parser.add_argument("--codex", default=None, help="Override the Codex executable path for this invocation.")
43
94
 
44
- subparsers = parser.add_subparsers(dest="command", required=True)
95
+ subparsers = parser.add_subparsers(
96
+ dest="command",
97
+ required=True,
98
+ parser_class=DeepScientistArgumentParser,
99
+ )
45
100
 
46
101
  subparsers.add_parser("init")
47
102
 
@@ -60,12 +115,24 @@ def build_parser() -> argparse.ArgumentParser:
60
115
  daemon_parser = subparsers.add_parser("daemon")
61
116
  daemon_parser.add_argument("--host", default=None)
62
117
  daemon_parser.add_argument("--port", type=int, default=None)
118
+ daemon_parser.add_argument("--auth", default=None)
119
+ daemon_parser.add_argument("--auth-token", default=None)
120
+ daemon_parser.add_argument(
121
+ "--prompt-version",
122
+ default=None,
123
+ help="Use `latest` managed prompts, an official historical prompt version such as `1.5.13`, or an exact backup id from `.codex/prompt_versions/` for this daemon session.",
124
+ )
63
125
 
64
126
  run_parser = subparsers.add_parser("run")
65
127
  run_parser.add_argument("skill_id")
66
128
  run_parser.add_argument("--quest-id", required=True)
67
129
  run_parser.add_argument("--message", required=True)
68
130
  run_parser.add_argument("--model", default=None)
131
+ run_parser.add_argument(
132
+ "--prompt-version",
133
+ default=None,
134
+ help="Use `latest` managed prompts, an official historical prompt version such as `1.5.13`, or an exact backup id from `.codex/prompt_versions/` for this one-off run.",
135
+ )
69
136
 
70
137
  ui_parser = subparsers.add_parser("ui")
71
138
  ui_parser.add_argument("--mode", choices=("web", "tui", "both"), default="web")
@@ -186,7 +253,6 @@ def resume_command(home: Path, quest_id: str) -> int:
186
253
  print(json.dumps(snapshot, ensure_ascii=False, indent=2))
187
254
  return 0
188
255
 
189
-
190
256
  def _daemon_control_quest(home: Path, quest_id: str, *, action: str) -> dict | None:
191
257
  config = ConfigManager(home).load_named("config", create_optional=False)
192
258
  ui_config = config.get("ui", {})
@@ -194,7 +260,7 @@ def _daemon_control_quest(home: Path, quest_id: str, *, action: str) -> dict | N
194
260
  request = Request(
195
261
  url,
196
262
  data=json.dumps({"action": action, "source": "cli"}).encode("utf-8"),
197
- headers={"Content-Type": "application/json"},
263
+ headers=_daemon_request_headers(home),
198
264
  method="POST",
199
265
  )
200
266
  try:
@@ -211,7 +277,7 @@ def _daemon_create_quest(home: Path, *, goal: str, quest_id: str | None) -> dict
211
277
  request = Request(
212
278
  url,
213
279
  data=json.dumps({"goal": goal, "quest_id": quest_id, "source": "cli"}).encode("utf-8"),
214
- headers={"Content-Type": "application/json"},
280
+ headers=_daemon_request_headers(home),
215
281
  method="POST",
216
282
  )
217
283
  try:
@@ -221,18 +287,37 @@ def _daemon_create_quest(home: Path, *, goal: str, quest_id: str | None) -> dict
221
287
  return None
222
288
 
223
289
 
224
- def daemon_command(home: Path, host: str | None, port: int | None) -> int:
290
+ def daemon_command(
291
+ home: Path,
292
+ host: str | None,
293
+ port: int | None,
294
+ auth: str | None,
295
+ auth_token: str | None,
296
+ prompt_version: str | None,
297
+ ) -> int:
225
298
  ensure_home_layout(home)
226
299
  config_manager = ConfigManager(home)
227
300
  config_manager.ensure_files()
228
301
  config = config_manager.load_named("config")
229
302
  ui_config = config.get("ui", {})
230
- daemon = DaemonApp(home)
303
+ daemon = DaemonApp(
304
+ home,
305
+ browser_auth_enabled=_parse_optional_bool(auth),
306
+ browser_auth_token=str(auth_token or "").strip() or None,
307
+ prompt_version_selection=str(prompt_version or "").strip() or None,
308
+ )
231
309
  daemon.serve(host or ui_config.get("host", "0.0.0.0"), port or ui_config.get("port", 20999))
232
310
  return 0
233
311
 
234
312
 
235
- def run_command(home: Path, quest_id: str, skill_id: str, message: str, model: str | None) -> int:
313
+ def run_command(
314
+ home: Path,
315
+ quest_id: str,
316
+ skill_id: str,
317
+ message: str,
318
+ model: str | None,
319
+ prompt_version: str | None,
320
+ ) -> int:
236
321
  ensure_home_layout(home)
237
322
  config_manager = ConfigManager(home)
238
323
  config_manager.ensure_files()
@@ -246,7 +331,11 @@ def run_command(home: Path, quest_id: str, skill_id: str, message: str, model: s
246
331
  repo_root=repo_root(),
247
332
  binary=codex_cfg.get("binary", "codex"),
248
333
  logger=logger,
249
- prompt_builder=PromptBuilder(repo_root(), home),
334
+ prompt_builder=PromptBuilder(
335
+ repo_root(),
336
+ home,
337
+ prompt_version_selection=str(prompt_version or "").strip() or None,
338
+ ),
250
339
  artifact_service=ArtifactService(home),
251
340
  )
252
341
  register_builtin_runners(codex_runner=runner)
@@ -322,19 +411,26 @@ def launch_ink_tui(home: Path, url: str) -> int:
322
411
  )
323
412
  )
324
413
  return 1
325
- return subprocess.call([node_binary, str(entry), "--base-url", url])
414
+ state = read_json(home / "runtime" / "daemon.json", {})
415
+ args = [node_binary, str(entry), "--base-url", url]
416
+ if isinstance(state, dict) and bool(state.get("auth_enabled")):
417
+ token = str(state.get("auth_token") or "").strip()
418
+ if token:
419
+ args.extend(["--auth-token", token])
420
+ return subprocess.call(args)
326
421
 
327
422
 
328
423
  def ui_command(home: Path, mode: str) -> int:
329
424
  config = ConfigManager(home).load_named("config", create_optional=False)
330
425
  host = config.get("ui", {}).get("host", "0.0.0.0")
331
426
  port = config.get("ui", {}).get("port", 20999)
332
- url = _local_ui_url(host, port)
427
+ base_url = _local_ui_url(str(host), int(port))
428
+ launch_url = _daemon_launch_url(home, host=str(host), port=int(port))
333
429
  if mode in {"web", "both"}:
334
- webbrowser.open(url)
335
- print(f"Opened {url}")
430
+ webbrowser.open(launch_url)
431
+ print(f"Opened {launch_url}")
336
432
  if mode in {"tui", "both"}:
337
- return launch_ink_tui(home, url)
433
+ return launch_ink_tui(home, base_url)
338
434
  return 0
339
435
 
340
436
 
@@ -492,9 +588,9 @@ def main(argv: list[str] | None = None) -> int:
492
588
  if args.command == "resume":
493
589
  return resume_command(home, args.quest_id)
494
590
  if args.command == "daemon":
495
- return daemon_command(home, args.host, args.port)
591
+ return daemon_command(home, args.host, args.port, args.auth, args.auth_token, args.prompt_version)
496
592
  if args.command == "run":
497
- return run_command(home, args.quest_id, args.skill_id, args.message, args.model)
593
+ return run_command(home, args.quest_id, args.skill_id, args.message, args.model, args.prompt_version)
498
594
  if args.command == "ui":
499
595
  return ui_command(home, args.mode)
500
596
  if args.command == "note":
@@ -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)
@@ -22,7 +22,7 @@ def config_filename(name: str) -> str:
22
22
 
23
23
 
24
24
  def default_system_enabled_connectors() -> dict[str, bool]:
25
- return {name: name in {"qq", "weixin", "lingzhu"} for name in SYSTEM_CONNECTOR_NAMES}
25
+ return {name: name in {"qq", "weixin", "telegram", "feishu", "whatsapp", "lingzhu"} for name in SYSTEM_CONNECTOR_NAMES}
26
26
 
27
27
 
28
28
  def default_config(home: Path) -> dict:
@@ -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,10 +97,10 @@ 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
- "approval_policy": "on-request",
102
- "sandbox_mode": "workspace-write",
102
+ "approval_policy": "never",
103
+ "sandbox_mode": "danger-full-access",
103
104
  "retry_on_failure": True,
104
105
  "retry_max_attempts": 5,
105
106
  "retry_initial_backoff_sec": 10.0,
@@ -154,6 +155,9 @@ def default_connectors() -> dict:
154
155
  "transport": "ilink_long_poll",
155
156
  "bot_name": "DeepScientist",
156
157
  "command_prefix": "/",
158
+ "auto_send_main_experiment_png": True,
159
+ "stale_replay_latest_limit": 5,
160
+ "stale_replay_interval_seconds": 2.0,
157
161
  "base_url": "https://ilinkai.weixin.qq.com",
158
162
  "cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c",
159
163
  "bot_type": "3",