@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
package/bin/ds.js CHANGED
@@ -36,15 +36,16 @@ const pythonCommands = new Set([
36
36
  const UPDATE_PACKAGE_NAME = String(packageJson.name || '@researai/deepscientist').trim() || '@researai/deepscientist';
37
37
  const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
38
38
 
39
- const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode', '--proxy', '--codex-profile', '--codex']);
39
+ const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode', '--proxy', '--codex-profile', '--codex', '--auth']);
40
40
 
41
- function buildCodexOverrideEnv({ yolo = false, profile = null, binary = null } = {}) {
41
+ function buildCodexOverrideEnv({ yolo = true, profile = null, binary = null } = {}) {
42
42
  const normalizedProfile = typeof profile === 'string' ? profile.trim() : '';
43
43
  const normalizedBinary = typeof binary === 'string' ? binary.trim() : '';
44
44
  const overrides = {};
45
45
  if (normalizedBinary) {
46
46
  overrides.DEEPSCIENTIST_CODEX_BINARY = normalizedBinary;
47
47
  }
48
+ overrides.DEEPSCIENTIST_CODEX_YOLO = yolo ? '1' : '0';
48
49
  if (!yolo) {
49
50
  if (normalizedProfile) {
50
51
  overrides.DEEPSCIENTIST_CODEX_PROFILE = normalizedProfile;
@@ -52,7 +53,6 @@ function buildCodexOverrideEnv({ yolo = false, profile = null, binary = null } =
52
53
  }
53
54
  return overrides;
54
55
  }
55
- overrides.DEEPSCIENTIST_CODEX_YOLO = '1';
56
56
  if (normalizedProfile) {
57
57
  overrides.DEEPSCIENTIST_CODEX_PROFILE = normalizedProfile;
58
58
  overrides.DEEPSCIENTIST_CODEX_MODEL = 'inherit';
@@ -69,6 +69,68 @@ function readOptionValue(argv, optionName) {
69
69
  return null;
70
70
  }
71
71
 
72
+ function parseBooleanFlagValue(rawValue) {
73
+ const normalized = String(rawValue || '').trim().toLowerCase();
74
+ if (!normalized) return null;
75
+ if (['1', 'true', 'yes', 'on', 'y'].includes(normalized)) return true;
76
+ if (['0', 'false', 'no', 'off', 'n'].includes(normalized)) return false;
77
+ return null;
78
+ }
79
+
80
+ function parseCodexCliVersion(text) {
81
+ const match = String(text || '').match(/codex-cli\s+(\d+)\.(\d+)\.(\d+)/i);
82
+ if (!match) {
83
+ return null;
84
+ }
85
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
86
+ }
87
+
88
+ function formatCodexCliVersion(version) {
89
+ if (!Array.isArray(version) || version.length !== 3) {
90
+ return '';
91
+ }
92
+ return version.join('.');
93
+ }
94
+
95
+ function compareCodexCliVersion(left, right) {
96
+ const leftParts = Array.isArray(left) ? left : [0, 0, 0];
97
+ const rightParts = Array.isArray(right) ? right : [0, 0, 0];
98
+ for (let index = 0; index < 3; index += 1) {
99
+ const delta = Number(leftParts[index] || 0) - Number(rightParts[index] || 0);
100
+ if (delta !== 0) {
101
+ return delta;
102
+ }
103
+ }
104
+ return 0;
105
+ }
106
+
107
+ function parseYoloArg(args, index, currentValue = true) {
108
+ const arg = args[index];
109
+ if (arg === '--yolo') {
110
+ const parsed = parseBooleanFlagValue(args[index + 1]);
111
+ if (parsed === null) {
112
+ return { matched: true, value: true, consumed: 1 };
113
+ }
114
+ return { matched: true, value: parsed, consumed: 2 };
115
+ }
116
+ if (typeof arg === 'string' && arg.startsWith('--yolo=')) {
117
+ const parsed = parseBooleanFlagValue(arg.slice('--yolo='.length));
118
+ return { matched: true, value: parsed === null ? true : parsed, consumed: 1 };
119
+ }
120
+ return { matched: false, value: currentValue, consumed: 0 };
121
+ }
122
+
123
+ function resolveYoloFlag(args, defaultValue = true) {
124
+ let value = defaultValue;
125
+ for (let index = 0; index < args.length; index += 1) {
126
+ const parsed = parseYoloArg(args, index, value);
127
+ if (!parsed.matched) continue;
128
+ value = parsed.value;
129
+ index += Math.max(0, parsed.consumed - 1);
130
+ }
131
+ return value;
132
+ }
133
+
72
134
  function printLauncherHelp() {
73
135
  console.log(`DeepScientist launcher
74
136
 
@@ -95,6 +157,7 @@ Usage:
95
157
  Launcher flags:
96
158
  --host <host> Bind host for the local web daemon
97
159
  --port <port> Bind port for the local web daemon
160
+ --auth [true|false] Require a 16-character local browser password. Default is false
98
161
  --tui Start the terminal workspace only
99
162
  --both Start web + terminal workspace together
100
163
  --no-browser Do not auto-open the browser
@@ -105,7 +168,7 @@ Launcher flags:
105
168
  --home <path> Use a custom DeepScientist home
106
169
  --here Create/use ./DeepScientist under the current working directory as home
107
170
  --proxy <url> Use an outbound HTTP/WS proxy for npm and Python runtime traffic
108
- --yolo Run Codex in YOLO mode: approval_policy=never and sandbox_mode=danger-full-access
171
+ --yolo [true|false] Control Codex YOLO mode. Default is true; pass false to restore on-request + workspace-write
109
172
  --codex-profile <id> Run DeepScientist with a specific Codex profile, for example \`m27\`
110
173
  --codex <path> Run DeepScientist with a specific Codex executable path for this launch
111
174
  --quest-id <id> Open the TUI on one quest directly
@@ -157,6 +220,47 @@ function normalizeProxyUrl(rawValue) {
157
220
  return value || null;
158
221
  }
159
222
 
223
+ function normalizeLegacyHostFlagArgs(argv) {
224
+ const args = [];
225
+ let warned = false;
226
+ let legacyValue = null;
227
+
228
+ for (let index = 0; index < argv.length; index += 1) {
229
+ const arg = argv[index];
230
+ if (arg === '--ip') {
231
+ warned = true;
232
+ legacyValue = argv[index + 1] || legacyValue;
233
+ args.push('--host');
234
+ if (argv[index + 1]) {
235
+ args.push(argv[index + 1]);
236
+ index += 1;
237
+ }
238
+ continue;
239
+ }
240
+ if (typeof arg === 'string' && arg.startsWith('--ip=')) {
241
+ warned = true;
242
+ legacyValue = arg.slice('--ip='.length) || legacyValue;
243
+ args.push('--host', arg.slice('--ip='.length));
244
+ continue;
245
+ }
246
+ args.push(arg);
247
+ }
248
+
249
+ if (!warned) {
250
+ return { args, warnings: [] };
251
+ }
252
+
253
+ const normalizedValue = String(legacyValue || '').trim();
254
+ const bindHint =
255
+ normalizedValue && ['0.0.0.0', '::', '[::]'].includes(normalizedValue)
256
+ ? ' Note: bind-all addresses such as 0.0.0.0 are valid for `--host`, but local browser access still uses 127.0.0.1.'
257
+ : '';
258
+ return {
259
+ args,
260
+ warnings: [`Launcher note: \`--ip\` is deprecated. Use \`--host\` instead.${bindHint}`],
261
+ };
262
+ }
263
+
160
264
  function applyLauncherProxy(proxyUrl) {
161
265
  const normalized = normalizeProxyUrl(proxyUrl);
162
266
  if (!normalized) {
@@ -314,11 +418,11 @@ function fetchLatestPublishedVersion({ npmBinary, timeoutMs = 3500 }) {
314
418
  latestVersion: null,
315
419
  };
316
420
  }
317
- const result = spawnSync(npmBinary, ['view', UPDATE_PACKAGE_NAME, 'version', '--json'], {
421
+ const result = spawnSync(npmBinary, ['view', UPDATE_PACKAGE_NAME, 'version', '--json'], syncSpawnOptions({
318
422
  encoding: 'utf8',
319
423
  env: process.env,
320
424
  timeout: timeoutMs,
321
- });
425
+ }));
322
426
  if (result.error) {
323
427
  return {
324
428
  ok: false,
@@ -527,6 +631,20 @@ function bindUiUrl(host, port) {
527
631
  return `http://${formatHttpHost(normalized)}:${port}`;
528
632
  }
529
633
 
634
+ function generateBrowserAuthToken() {
635
+ return crypto.randomBytes(8).toString('hex');
636
+ }
637
+
638
+ function appendBrowserAuthToken(url, authToken) {
639
+ const normalized = typeof authToken === 'string' ? authToken.trim() : '';
640
+ if (!normalized) {
641
+ return url;
642
+ }
643
+ const target = new URL(url);
644
+ target.searchParams.set('token', normalized);
645
+ return target.toString();
646
+ }
647
+
530
648
  function normalizeMode(value) {
531
649
  const normalized = String(value || '')
532
650
  .trim()
@@ -553,6 +671,66 @@ function parseBooleanSetting(rawValue, fallback = false) {
553
671
  return fallback;
554
672
  }
555
673
 
674
+ function shouldCompileRuntimeBytecode() {
675
+ return parseBooleanSetting(process.env.DEEPSCIENTIST_RUNTIME_COMPILE_BYTECODE, false);
676
+ }
677
+
678
+ function readRequiredOptionValue(args, index, optionName) {
679
+ const value = args[index + 1];
680
+ if (!value || String(value).startsWith('--')) {
681
+ return {
682
+ ok: false,
683
+ error: `Missing value for ${optionName}.`,
684
+ };
685
+ }
686
+ return {
687
+ ok: true,
688
+ value,
689
+ };
690
+ }
691
+
692
+ function parseStrictBooleanOption(rawValue, optionName) {
693
+ const parsed = parseBooleanFlagValue(rawValue);
694
+ if (parsed === null) {
695
+ return {
696
+ ok: false,
697
+ error: `Invalid value for ${optionName}: ${rawValue}. Use true or false.`,
698
+ };
699
+ }
700
+ return {
701
+ ok: true,
702
+ value: parsed,
703
+ };
704
+ }
705
+
706
+ function parseStrictPortOption(rawValue, optionName) {
707
+ const port = Number(rawValue);
708
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
709
+ return {
710
+ ok: false,
711
+ error: `Invalid value for ${optionName}: ${rawValue}. Expected an integer between 1 and 65535.`,
712
+ };
713
+ }
714
+ return {
715
+ ok: true,
716
+ value: port,
717
+ };
718
+ }
719
+
720
+ function parseStrictModeOption(rawValue, optionName) {
721
+ const normalized = String(rawValue || '').trim().toLowerCase();
722
+ if (!['web', 'tui', 'both'].includes(normalized)) {
723
+ return {
724
+ ok: false,
725
+ error: `Invalid value for ${optionName}: ${rawValue}. Expected one of: web, tui, both.`,
726
+ };
727
+ }
728
+ return {
729
+ ok: true,
730
+ value: normalized,
731
+ };
732
+ }
733
+
556
734
  function supportsAnsi() {
557
735
  return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb');
558
736
  }
@@ -588,6 +766,64 @@ function colorize(code, text) {
588
766
  return `${code}${text}\u001B[0m`;
589
767
  }
590
768
 
769
+ function readCodexProviderMetadata(configDir, profile) {
770
+ const normalizedProfile = String(profile || '').trim();
771
+ const expandedDir = expandUserPath(configDir || path.join(os.homedir(), '.codex'));
772
+ const configPath = path.join(expandedDir, 'config.toml');
773
+ if (!normalizedProfile || !fs.existsSync(configPath)) {
774
+ return {
775
+ provider: null,
776
+ model: null,
777
+ envKey: null,
778
+ baseUrl: null,
779
+ wireApi: null,
780
+ requiresOpenAiAuth: null,
781
+ };
782
+ }
783
+ const text = fs.readFileSync(configPath, 'utf8');
784
+ const profileBlock = text.match(new RegExp(`\\[profiles\\.${normalizedProfile.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]([\\s\\S]*?)(?:\\n\\[|$)`));
785
+ const provider = profileBlock?.[1]?.match(/^\s*model_provider\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || text.match(/^\s*model_provider\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
786
+ const model = profileBlock?.[1]?.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || text.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
787
+ if (!provider) {
788
+ return {
789
+ provider: null,
790
+ model,
791
+ envKey: null,
792
+ baseUrl: null,
793
+ wireApi: null,
794
+ requiresOpenAiAuth: null,
795
+ };
796
+ }
797
+ const providerBlock = text.match(new RegExp(`\\[model_providers\\.${provider.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]([\\s\\S]*?)(?:\\n\\[|$)`));
798
+ const providerText = providerBlock?.[1] || '';
799
+ const envKey = providerText.match(/^\s*env_key\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
800
+ const baseUrl = providerText.match(/^\s*base_url\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
801
+ const wireApi = providerText.match(/^\s*wire_api\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
802
+ const requiresOpenAiAuthRaw = providerText.match(/^\s*requires_openai_auth\s*=\s*(true|false)\s*$/m)?.[1] || null;
803
+ const requiresOpenAiAuth = requiresOpenAiAuthRaw === null ? null : requiresOpenAiAuthRaw === 'true';
804
+ return {
805
+ provider,
806
+ model,
807
+ envKey,
808
+ baseUrl,
809
+ wireApi,
810
+ requiresOpenAiAuth,
811
+ };
812
+ }
813
+
814
+ function installedCodexCliVersion(binaryPath) {
815
+ const resolved = resolveExecutableOnPath(binaryPath || 'codex') || binaryPath || 'codex';
816
+ try {
817
+ const result = spawnSync(resolved, ['--version'], syncSpawnOptions({ encoding: 'utf8' }));
818
+ if (result.status !== 0) {
819
+ return null;
820
+ }
821
+ return parseCodexCliVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
822
+ } catch {
823
+ return null;
824
+ }
825
+ }
826
+
591
827
  const OFFICIAL_REPOSITORY_URL = 'https://github.com/ResearAI/DeepScientist';
592
828
 
593
829
  function officialRepositoryLine() {
@@ -606,7 +842,7 @@ function renderBrandArtwork() {
606
842
  const result = spawnSync(
607
843
  chafa,
608
844
  ['--size', `${width}x${height}`, '--format', 'symbols', '--colors', '16', brandPath],
609
- { encoding: 'utf8' }
845
+ syncSpawnOptions({ encoding: 'utf8' })
610
846
  );
611
847
  if (result.status === 0 && result.stdout && result.stdout.trim()) {
612
848
  return result.stdout.replace(/\s+$/, '').split(/\r?\n/);
@@ -650,7 +886,7 @@ function pythonVersionText(probe) {
650
886
  return version;
651
887
  }
652
888
 
653
- function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
889
+ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken }) {
654
890
  const runtimeRows = [
655
891
  ['Version', packageJson.version],
656
892
  ['Home', truncateMiddle(home)],
@@ -659,6 +895,9 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
659
895
  ['Python', truncateMiddle(pythonVersionText(pythonSelection))],
660
896
  ['Codex mode', yolo ? 'YOLO (never + danger-full-access)' : 'Default (on-request + workspace-write)'],
661
897
  ];
898
+ if (authEnabled && authToken) {
899
+ runtimeRows.splice(4, 0, ['Auth token', authToken]);
900
+ }
662
901
  if (pythonSelection && pythonSelection.sourceLabel) {
663
902
  runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
664
903
  }
@@ -671,6 +910,7 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
671
910
  ['ds --yolo --port 20999 --here', 'Start in ./DeepScientist under the current directory with YOLO Codex access'],
672
911
  ['ds --port 21000', 'Change the web port'],
673
912
  ['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
913
+ ['ds --auth true', 'Enable the local browser password for this launch'],
674
914
  ['ds --here', 'Use ./DeepScientist under the current directory as home'],
675
915
  ['ds --both', 'Start web + TUI together'],
676
916
  ['ds --tui', 'Start the terminal workspace only'],
@@ -694,6 +934,8 @@ function printLaunchCard({
694
934
  home,
695
935
  pythonSelection,
696
936
  yolo,
937
+ authEnabled,
938
+ authToken,
697
939
  }) {
698
940
  const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
699
941
  const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
@@ -752,13 +994,18 @@ function printLaunchCard({
752
994
  console.log(centerText(colorize('\u001B[1m', workspaceMode), width));
753
995
  console.log(centerText(urlLabel, width));
754
996
  console.log(centerText(divider, width));
997
+ if (authEnabled && authToken) {
998
+ console.log('');
999
+ console.log(centerText(colorize('\u001B[1;38;5;214m', authToken), width));
1000
+ console.log('');
1001
+ }
755
1002
  console.log(centerText(browserLine, width));
756
1003
  console.log(centerText(nextStep, width));
757
1004
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
758
1005
  console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
759
1006
  console.log(centerText(officialRepositoryLine(), width));
760
1007
  console.log('');
761
- renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo });
1008
+ renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken });
762
1009
  }
763
1010
 
764
1011
  function escapeHtml(value) {
@@ -984,7 +1231,8 @@ function parseLauncherArgs(argv) {
984
1231
  let status = false;
985
1232
  let daemonOnly = false;
986
1233
  let skipUpdateCheck = false;
987
- let yolo = false;
1234
+ let yolo = true;
1235
+ let auth = null;
988
1236
  let codexProfile = null;
989
1237
  let codexBinary = null;
990
1238
 
@@ -1005,17 +1253,72 @@ function parseLauncherArgs(argv) {
1005
1253
  else if (arg === '--open-browser') openBrowser = true;
1006
1254
  else if (arg === '--daemon-only') daemonOnly = true;
1007
1255
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
1008
- else if (arg === '--yolo') yolo = true;
1009
- else if (arg === '--codex-profile' && args[index + 1]) codexProfile = args[++index];
1010
- else if (arg === '--codex' && args[index + 1]) codexBinary = args[++index];
1011
- else if (arg === '--host' && args[index + 1]) host = args[++index];
1012
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
1013
- else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
1014
- else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
1015
- else if (arg === '--quest-id' && args[index + 1]) questId = args[++index];
1016
- else if (arg === '--mode' && args[index + 1]) mode = normalizeMode(args[++index]);
1017
- else if (arg === '--help' || arg === '-h') return { help: true };
1018
- else if (!arg.startsWith('--')) return null;
1256
+ else if (arg === '--here') continue;
1257
+ else {
1258
+ const parsedYolo = parseYoloArg(args, index, yolo);
1259
+ if (parsedYolo.matched) {
1260
+ yolo = parsedYolo.value;
1261
+ index += Math.max(0, parsedYolo.consumed - 1);
1262
+ } else if (arg === '--auth') {
1263
+ const next = readRequiredOptionValue(args, index, '--auth');
1264
+ if (!next.ok) return { help: false, error: next.error };
1265
+ const parsed = parseStrictBooleanOption(next.value, '--auth');
1266
+ if (!parsed.ok) return { help: false, error: parsed.error };
1267
+ auth = parsed.value;
1268
+ index += 1;
1269
+ } else if (typeof arg === 'string' && arg.startsWith('--auth=')) {
1270
+ const parsed = parseStrictBooleanOption(arg.slice('--auth='.length), '--auth');
1271
+ if (!parsed.ok) return { help: false, error: parsed.error };
1272
+ auth = parsed.value;
1273
+ } else if (arg === '--codex-profile') {
1274
+ const next = readRequiredOptionValue(args, index, '--codex-profile');
1275
+ if (!next.ok) return { help: false, error: next.error };
1276
+ codexProfile = next.value;
1277
+ index += 1;
1278
+ } else if (arg === '--codex') {
1279
+ const next = readRequiredOptionValue(args, index, '--codex');
1280
+ if (!next.ok) return { help: false, error: next.error };
1281
+ codexBinary = next.value;
1282
+ index += 1;
1283
+ } else if (arg === '--host') {
1284
+ const next = readRequiredOptionValue(args, index, '--host');
1285
+ if (!next.ok) return { help: false, error: next.error };
1286
+ host = next.value;
1287
+ index += 1;
1288
+ } else if (arg === '--port') {
1289
+ const next = readRequiredOptionValue(args, index, '--port');
1290
+ if (!next.ok) return { help: false, error: next.error };
1291
+ const parsed = parseStrictPortOption(next.value, '--port');
1292
+ if (!parsed.ok) return { help: false, error: parsed.error };
1293
+ port = parsed.value;
1294
+ index += 1;
1295
+ } else if (arg === '--home') {
1296
+ const next = readRequiredOptionValue(args, index, '--home');
1297
+ if (!next.ok) return { help: false, error: next.error };
1298
+ home = path.resolve(next.value);
1299
+ index += 1;
1300
+ } else if (arg === '--proxy') {
1301
+ const next = readRequiredOptionValue(args, index, '--proxy');
1302
+ if (!next.ok) return { help: false, error: next.error };
1303
+ proxy = next.value;
1304
+ index += 1;
1305
+ } else if (arg === '--quest-id') {
1306
+ const next = readRequiredOptionValue(args, index, '--quest-id');
1307
+ if (!next.ok) return { help: false, error: next.error };
1308
+ questId = next.value;
1309
+ index += 1;
1310
+ } else if (arg === '--mode') {
1311
+ const next = readRequiredOptionValue(args, index, '--mode');
1312
+ if (!next.ok) return { help: false, error: next.error };
1313
+ const parsed = parseStrictModeOption(next.value, '--mode');
1314
+ if (!parsed.ok) return { help: false, error: parsed.error };
1315
+ mode = parsed.value;
1316
+ index += 1;
1317
+ }
1318
+ else if (arg === '--help' || arg === '-h') return { help: true };
1319
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown launcher flag: ${arg}` };
1320
+ else return { help: false, error: `Unexpected launcher argument: ${arg}` };
1321
+ }
1019
1322
  }
1020
1323
 
1021
1324
  return {
@@ -1033,8 +1336,10 @@ function parseLauncherArgs(argv) {
1033
1336
  daemonOnly,
1034
1337
  skipUpdateCheck,
1035
1338
  yolo,
1339
+ auth,
1036
1340
  codexProfile,
1037
1341
  codexBinary,
1342
+ error: null,
1038
1343
  };
1039
1344
  }
1040
1345
 
@@ -1108,12 +1413,32 @@ function parseUpdateArgs(argv) {
1108
1413
  else if (arg === '--worker') worker = true;
1109
1414
  else if (arg === '--restart-daemon') restartDaemon = true;
1110
1415
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
1111
- else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
1112
- else if (arg === '--host' && args[index + 1]) host = args[++index];
1113
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
1114
- else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
1416
+ else if (arg === '--home') {
1417
+ const next = readRequiredOptionValue(args, index, '--home');
1418
+ if (!next.ok) return { help: false, error: next.error };
1419
+ home = path.resolve(next.value);
1420
+ index += 1;
1421
+ } else if (arg === '--host') {
1422
+ const next = readRequiredOptionValue(args, index, '--host');
1423
+ if (!next.ok) return { help: false, error: next.error };
1424
+ host = next.value;
1425
+ index += 1;
1426
+ } else if (arg === '--port') {
1427
+ const next = readRequiredOptionValue(args, index, '--port');
1428
+ if (!next.ok) return { help: false, error: next.error };
1429
+ const parsed = parseStrictPortOption(next.value, '--port');
1430
+ if (!parsed.ok) return { help: false, error: parsed.error };
1431
+ port = parsed.value;
1432
+ index += 1;
1433
+ } else if (arg === '--proxy') {
1434
+ const next = readRequiredOptionValue(args, index, '--proxy');
1435
+ if (!next.ok) return { help: false, error: next.error };
1436
+ proxy = next.value;
1437
+ index += 1;
1438
+ }
1115
1439
  else if (arg === '--help' || arg === '-h') return { help: true };
1116
- else if (!arg.startsWith('--')) return null;
1440
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown update flag: ${arg}` };
1441
+ else return { help: false, error: `Unexpected update argument: ${arg}` };
1117
1442
  }
1118
1443
 
1119
1444
  return {
@@ -1132,6 +1457,7 @@ function parseUpdateArgs(argv) {
1132
1457
  proxy,
1133
1458
  restartDaemon,
1134
1459
  skipUpdateCheck,
1460
+ error: null,
1135
1461
  };
1136
1462
  }
1137
1463
 
@@ -1149,15 +1475,23 @@ function parseMigrateArgs(argv) {
1149
1475
  const arg = args[index];
1150
1476
  if (arg === '--yes') yes = true;
1151
1477
  else if (arg === '--restart') restart = true;
1152
- else if (arg === '--home' && args[index + 1]) home = path.resolve(expandUserPath(args[++index]));
1478
+ else if (arg === '--home') {
1479
+ const next = readRequiredOptionValue(args, index, '--home');
1480
+ if (!next.ok) return { help: false, error: next.error };
1481
+ home = path.resolve(expandUserPath(next.value));
1482
+ index += 1;
1483
+ }
1153
1484
  else if (arg === '--help' || arg === '-h') return { help: true };
1154
- else if (arg.startsWith('--')) return null;
1485
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown migrate flag: ${arg}` };
1155
1486
  else if (!target) target = path.resolve(expandUserPath(arg));
1156
- else return null;
1487
+ else return { help: false, error: `Unexpected migrate argument: ${arg}` };
1157
1488
  }
1158
1489
 
1159
1490
  if (!target) {
1160
- return null;
1491
+ return {
1492
+ help: false,
1493
+ error: 'Missing migration target path.',
1494
+ };
1161
1495
  }
1162
1496
 
1163
1497
  return {
@@ -1166,12 +1500,18 @@ function parseMigrateArgs(argv) {
1166
1500
  target,
1167
1501
  yes,
1168
1502
  restart,
1503
+ error: null,
1169
1504
  };
1170
1505
  }
1171
1506
 
1172
1507
  function findFirstPositionalArg(args) {
1173
1508
  for (let index = 0; index < args.length; index += 1) {
1174
1509
  const arg = args[index];
1510
+ const parsedYolo = parseYoloArg(args, index);
1511
+ if (parsedYolo.matched) {
1512
+ index += Math.max(0, parsedYolo.consumed - 1);
1513
+ continue;
1514
+ }
1175
1515
  if (optionsWithValues.has(arg)) {
1176
1516
  index += 1;
1177
1517
  continue;
@@ -1465,11 +1805,14 @@ function scheduleDeferredSourceCleanup({ sourceHome, targetHome }) {
1465
1805
  ' }',
1466
1806
  '})();',
1467
1807
  ].join('\n');
1468
- const child = spawn(process.execPath, ['-e', helperScript, String(process.pid), sourceHome, logPath], {
1469
- detached: true,
1470
- stdio: 'ignore',
1471
- env: process.env,
1472
- });
1808
+ const child = spawn(
1809
+ process.execPath,
1810
+ ['-e', helperScript, String(process.pid), sourceHome, logPath],
1811
+ detachedSpawnOptions({
1812
+ stdio: 'ignore',
1813
+ env: process.env,
1814
+ })
1815
+ );
1473
1816
  child.unref();
1474
1817
  }
1475
1818
 
@@ -1554,10 +1897,10 @@ function probePython(binary) {
1554
1897
  ' "patch": sys.version_info[2],',
1555
1898
  '}, ensure_ascii=False))',
1556
1899
  ].join('\n');
1557
- const result = spawnSync(binary, ['-c', snippet], {
1900
+ const result = spawnSync(binary, ['-c', snippet], syncSpawnOptions({
1558
1901
  encoding: 'utf8',
1559
1902
  env: process.env,
1560
- });
1903
+ }));
1561
1904
  if (result.error) {
1562
1905
  return {
1563
1906
  ok: false,
@@ -1830,6 +2173,7 @@ function runSync(binary, args, options = {}) {
1830
2173
  env: options.env || process.env,
1831
2174
  encoding: 'utf8',
1832
2175
  input: options.input,
2176
+ windowsHide: process.platform === 'win32',
1833
2177
  });
1834
2178
  if (result.error) {
1835
2179
  throw result.error;
@@ -1847,12 +2191,30 @@ function step(index, total, message) {
1847
2191
  console.log(`[${index}/${total}] ${message}`);
1848
2192
  }
1849
2193
 
2194
+ function detachedSpawnOptions(options = {}) {
2195
+ return {
2196
+ ...options,
2197
+ detached: true,
2198
+ windowsHide: process.platform === 'win32',
2199
+ };
2200
+ }
2201
+
2202
+ function syncSpawnOptions(options = {}) {
2203
+ return {
2204
+ ...options,
2205
+ windowsHide: process.platform === 'win32',
2206
+ };
2207
+ }
2208
+
1850
2209
  function verifyPythonRuntime(runtimePython) {
1851
2210
  const result = runSync(
1852
2211
  runtimePython,
1853
2212
  ['-c', 'import deepscientist.cli; import cryptography; import _cffi_backend; print("ok")'],
1854
2213
  { capture: true, allowFailure: true }
1855
2214
  );
2215
+ if (result.status !== 0 && result.stderr) {
2216
+ process.stderr.write(result.stderr);
2217
+ }
1856
2218
  return result.status === 0;
1857
2219
  }
1858
2220
 
@@ -1999,11 +2361,11 @@ function downloadFileWithNode(url, destinationPath) {
1999
2361
  ' process.exit(1);',
2000
2362
  '});',
2001
2363
  ].join('\n');
2002
- const result = spawnSync(process.execPath, ['-e', downloader, url, destinationPath, '45000'], {
2364
+ const result = spawnSync(process.execPath, ['-e', downloader, url, destinationPath, '45000'], syncSpawnOptions({
2003
2365
  cwd: repoRoot,
2004
2366
  stdio: 'inherit',
2005
2367
  env: process.env,
2006
- });
2368
+ }));
2007
2369
  if (result.error) {
2008
2370
  throw result.error;
2009
2371
  }
@@ -2054,11 +2416,11 @@ function installLocalUv(home) {
2054
2416
  shellArgs = [installerPath];
2055
2417
  }
2056
2418
 
2057
- const installResult = spawnSync(shellBinary, shellArgs, {
2419
+ const installResult = spawnSync(shellBinary, shellArgs, syncSpawnOptions({
2058
2420
  cwd: repoRoot,
2059
2421
  stdio: 'inherit',
2060
2422
  env: installEnv,
2061
- });
2423
+ }));
2062
2424
  if (installResult.error) {
2063
2425
  throw installResult.error;
2064
2426
  }
@@ -2085,13 +2447,17 @@ function ensureUvBinary(home) {
2085
2447
  }
2086
2448
 
2087
2449
  function buildUvRuntimeEnv(home, extraEnv = {}) {
2088
- return {
2450
+ const env = {
2089
2451
  ...process.env,
2090
2452
  UV_CACHE_DIR: runtimeUvCachePath(home),
2091
2453
  UV_PROJECT_ENVIRONMENT: runtimePythonEnvPath(home),
2092
2454
  UV_PYTHON_INSTALL_DIR: runtimeUvPythonInstallPath(home),
2093
2455
  ...extraEnv,
2094
2456
  };
2457
+ for (const key of ['PYTHONPATH', 'PYTHONHOME', 'VIRTUAL_ENV', '__PYVENV_LAUNCHER__']) {
2458
+ delete env[key];
2459
+ }
2460
+ return env;
2095
2461
  }
2096
2462
 
2097
2463
  function ensureUvLockPresent() {
@@ -2104,6 +2470,43 @@ function ensureUvLockPresent() {
2104
2470
  process.exit(1);
2105
2471
  }
2106
2472
 
2473
+ function buildUvSyncFailureGuidance({ installMode = detectInstallMode(repoRoot), env = process.env } = {}) {
2474
+ const guidance = [];
2475
+ if (installMode === 'source-checkout') {
2476
+ guidance.push('If you changed Python dependencies in a source checkout, run `uv lock` and try again.');
2477
+ } else {
2478
+ guidance.push('This npm install already includes a locked `uv.lock`, so this is usually a local Python or network environment issue rather than a missing lockfile.');
2479
+ guidance.push('Re-run `ds` in a clean shell first. If you have an active conda or virtualenv, try deactivating it before starting DeepScientist.');
2480
+ }
2481
+
2482
+ const hasPythonEnv =
2483
+ Boolean(String(env.VIRTUAL_ENV || '').trim())
2484
+ || Boolean(String(env.CONDA_PREFIX || '').trim())
2485
+ || Boolean(String(env.PYTHONPATH || '').trim())
2486
+ || Boolean(String(env.PYTHONHOME || '').trim());
2487
+ if (hasPythonEnv) {
2488
+ guidance.push('An active Python environment was detected. `VIRTUAL_ENV`, `CONDA_PREFIX`, `PYTHONPATH`, or `PYTHONHOME` can interfere with uv runtime bootstrap.');
2489
+ }
2490
+
2491
+ const hasCustomIndex =
2492
+ Object.keys(env).some((key) => /^PIP_/i.test(key))
2493
+ || Boolean(String(env.UV_INDEX_URL || '').trim())
2494
+ || Boolean(String(env.UV_EXTRA_INDEX_URL || '').trim());
2495
+ if (hasCustomIndex) {
2496
+ guidance.push('Custom package index settings were detected. Check `PIP_*`, `UV_INDEX_URL`, or `UV_EXTRA_INDEX_URL` if uv could not download packages.');
2497
+ }
2498
+
2499
+ const hasProxyOrCert =
2500
+ ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'SSL_CERT_FILE', 'REQUESTS_CA_BUNDLE']
2501
+ .some((key) => Boolean(String(env[key] || '').trim()));
2502
+ if (hasProxyOrCert) {
2503
+ guidance.push('Proxy or certificate overrides were detected. If uv reported TLS, certificate, or download errors above, verify those settings and try again.');
2504
+ }
2505
+
2506
+ guidance.push('Look at the uv error printed above this message. That original uv output is the real failure reason.');
2507
+ return guidance;
2508
+ }
2509
+
2107
2510
  function resolveUvVersion(uvBinary) {
2108
2511
  const result = runSync(uvBinary, ['--version'], { capture: true, allowFailure: true });
2109
2512
  if (result.status !== 0) {
@@ -2155,8 +2558,25 @@ function ensureUvManagedPython(home, uvBinary, minimumVersionRequest) {
2155
2558
  return probe;
2156
2559
  }
2157
2560
 
2561
+ function resolveBackgroundPythonExecutable(runtimePython) {
2562
+ const normalized = String(runtimePython || '').trim();
2563
+ if (process.platform !== 'win32' || !normalized) {
2564
+ return normalized;
2565
+ }
2566
+ const runtimePath = path.resolve(normalized);
2567
+ const runtimeDir = path.dirname(runtimePath);
2568
+ const pythonwCandidate = path.join(runtimeDir, 'pythonw.exe');
2569
+ if (fs.existsSync(pythonwCandidate)) {
2570
+ return pythonwCandidate;
2571
+ }
2572
+ return runtimePath;
2573
+ }
2574
+
2158
2575
  function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2159
- const args = ['sync', '--frozen', '--no-dev', '--compile-bytecode', '--python', pythonTarget];
2576
+ const args = ['sync', '--frozen', '--no-dev', '--python', pythonTarget];
2577
+ if (shouldCompileRuntimeBytecode()) {
2578
+ args.splice(3, 0, '--compile-bytecode');
2579
+ }
2160
2580
  if (!editable) {
2161
2581
  args.push('--no-editable');
2162
2582
  }
@@ -2169,7 +2589,9 @@ function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2169
2589
  return;
2170
2590
  }
2171
2591
  console.error('DeepScientist could not sync the locked Python environment with uv.');
2172
- console.error('If you are working from a source checkout, run `uv lock` after dependency changes and try again.');
2592
+ for (const line of buildUvSyncFailureGuidance()) {
2593
+ console.error(line);
2594
+ }
2173
2595
  process.exit(result.status ?? 1);
2174
2596
  }
2175
2597
 
@@ -2323,6 +2745,13 @@ function normalizePythonCliArgs(args, home) {
2323
2745
  continue;
2324
2746
  }
2325
2747
  if (arg === '--yolo') {
2748
+ const parsed = parseBooleanFlagValue(args[index + 1]);
2749
+ if (parsed !== null) {
2750
+ index += 1;
2751
+ }
2752
+ continue;
2753
+ }
2754
+ if (typeof arg === 'string' && arg.startsWith('--yolo=')) {
2326
2755
  continue;
2327
2756
  }
2328
2757
  if (arg === '--codex-profile') {
@@ -2590,16 +3019,33 @@ function decodeSupervisorEnvPayload(rawValue) {
2590
3019
  }
2591
3020
  }
2592
3021
 
2593
- function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = null, envOverrides = {}, daemonId = null }) {
3022
+ function spawnManagedDaemonProcess({
3023
+ home,
3024
+ runtimePython,
3025
+ host,
3026
+ port,
3027
+ proxy = null,
3028
+ envOverrides = {},
3029
+ daemonId = null,
3030
+ authEnabled = false,
3031
+ authToken = null,
3032
+ }) {
2594
3033
  const browserUrl = browserUiUrl(host, port);
2595
3034
  const daemonBindUrl = bindUiUrl(host, port);
3035
+ const resolvedAuthEnabled = authEnabled !== false;
3036
+ const resolvedAuthToken = resolvedAuthEnabled
3037
+ ? (typeof authToken === 'string' && authToken.trim() ? authToken.trim() : generateBrowserAuthToken())
3038
+ : null;
3039
+ const launchUrl = browserUrl;
3040
+ const bindLaunchUrl = daemonBindUrl;
2596
3041
  const logPath = path.join(home, 'logs', 'daemon.log');
2597
3042
  ensureDir(path.dirname(logPath));
2598
3043
  const out = fs.openSync(logPath, 'a');
2599
3044
  const resolvedDaemonId = String(daemonId || crypto.randomUUID()).trim();
2600
3045
  const launcherPath = path.join(repoRoot, 'bin', 'ds.js');
3046
+ const backgroundPython = resolveBackgroundPythonExecutable(runtimePython);
2601
3047
  const child = spawn(
2602
- runtimePython,
3048
+ backgroundPython,
2603
3049
  [
2604
3050
  '-m',
2605
3051
  'deepscientist.cli',
@@ -2611,10 +3057,12 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2611
3057
  host,
2612
3058
  '--port',
2613
3059
  String(port),
3060
+ '--auth',
3061
+ resolvedAuthEnabled ? 'true' : 'false',
3062
+ ...(resolvedAuthEnabled && resolvedAuthToken ? ['--auth-token', resolvedAuthToken] : []),
2614
3063
  ],
2615
- {
3064
+ detachedSpawnOptions({
2616
3065
  cwd: repoRoot,
2617
- detached: true,
2618
3066
  stdio: ['ignore', out, out],
2619
3067
  env: {
2620
3068
  ...process.env,
@@ -2624,8 +3072,10 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2624
3072
  DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2625
3073
  DS_DAEMON_ID: resolvedDaemonId,
2626
3074
  DS_DAEMON_MANAGED_BY: 'ds-launcher',
3075
+ DS_UI_AUTH_ENABLED: resolvedAuthEnabled ? '1' : '0',
3076
+ ...(resolvedAuthEnabled && resolvedAuthToken ? { DS_UI_AUTH_TOKEN: resolvedAuthToken } : {}),
2627
3077
  },
2628
- }
3078
+ })
2629
3079
  );
2630
3080
  child.unref();
2631
3081
  const statePayload = {
@@ -2634,10 +3084,14 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2634
3084
  port,
2635
3085
  url: browserUrl,
2636
3086
  bind_url: daemonBindUrl,
3087
+ launch_url: launchUrl,
3088
+ bind_launch_url: bindLaunchUrl,
2637
3089
  log_path: logPath,
2638
3090
  started_at: new Date().toISOString(),
2639
3091
  home: normalizeHomePath(home),
2640
3092
  daemon_id: resolvedDaemonId,
3093
+ auth_enabled: resolvedAuthEnabled,
3094
+ auth_token: resolvedAuthToken,
2641
3095
  };
2642
3096
  writeDaemonState(home, statePayload);
2643
3097
  return {
@@ -2645,6 +3099,8 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2645
3099
  statePayload,
2646
3100
  browserUrl,
2647
3101
  bindUrl: daemonBindUrl,
3102
+ launchUrl,
3103
+ bindLaunchUrl,
2648
3104
  logPath,
2649
3105
  };
2650
3106
  }
@@ -2673,9 +3129,8 @@ function spawnDaemonSupervisor({ home, runtimePython, host, port, proxy = null,
2673
3129
  if (envPayload) {
2674
3130
  args.push('--env-json', envPayload);
2675
3131
  }
2676
- const child = spawn(process.execPath, args, {
3132
+ const child = spawn(process.execPath, args, detachedSpawnOptions({
2677
3133
  cwd: repoRoot,
2678
- detached: true,
2679
3134
  stdio: 'ignore',
2680
3135
  env: {
2681
3136
  ...process.env,
@@ -2683,7 +3138,7 @@ function spawnDaemonSupervisor({ home, runtimePython, host, port, proxy = null,
2683
3138
  DEEPSCIENTIST_NODE_BINARY: process.execPath,
2684
3139
  DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2685
3140
  },
2686
- });
3141
+ }));
2687
3142
  child.unref();
2688
3143
  return child.pid || null;
2689
3144
  }
@@ -2700,19 +3155,54 @@ function parseDaemonSupervisorArgs(argv) {
2700
3155
 
2701
3156
  for (let index = 0; index < args.length; index += 1) {
2702
3157
  const arg = args[index];
2703
- if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
2704
- else if (arg === '--runtime-python' && args[index + 1]) runtimePython = args[++index];
2705
- else if (arg === '--host' && args[index + 1]) host = args[++index];
2706
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
2707
- else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
2708
- else if (arg === '--daemon-id' && args[index + 1]) daemonId = args[++index];
2709
- else if (arg === '--env-json' && args[index + 1]) envJson = args[++index];
3158
+ if (arg === '--home') {
3159
+ const next = readRequiredOptionValue(args, index, '--home');
3160
+ if (!next.ok) return { help: false, error: next.error };
3161
+ home = path.resolve(next.value);
3162
+ index += 1;
3163
+ } else if (arg === '--runtime-python') {
3164
+ const next = readRequiredOptionValue(args, index, '--runtime-python');
3165
+ if (!next.ok) return { help: false, error: next.error };
3166
+ runtimePython = next.value;
3167
+ index += 1;
3168
+ } else if (arg === '--host') {
3169
+ const next = readRequiredOptionValue(args, index, '--host');
3170
+ if (!next.ok) return { help: false, error: next.error };
3171
+ host = next.value;
3172
+ index += 1;
3173
+ } else if (arg === '--port') {
3174
+ const next = readRequiredOptionValue(args, index, '--port');
3175
+ if (!next.ok) return { help: false, error: next.error };
3176
+ const parsed = parseStrictPortOption(next.value, '--port');
3177
+ if (!parsed.ok) return { help: false, error: parsed.error };
3178
+ port = parsed.value;
3179
+ index += 1;
3180
+ } else if (arg === '--proxy') {
3181
+ const next = readRequiredOptionValue(args, index, '--proxy');
3182
+ if (!next.ok) return { help: false, error: next.error };
3183
+ proxy = next.value;
3184
+ index += 1;
3185
+ } else if (arg === '--daemon-id') {
3186
+ const next = readRequiredOptionValue(args, index, '--daemon-id');
3187
+ if (!next.ok) return { help: false, error: next.error };
3188
+ daemonId = next.value;
3189
+ index += 1;
3190
+ } else if (arg === '--env-json') {
3191
+ const next = readRequiredOptionValue(args, index, '--env-json');
3192
+ if (!next.ok) return { help: false, error: next.error };
3193
+ envJson = next.value;
3194
+ index += 1;
3195
+ }
2710
3196
  else if (arg === '--help' || arg === '-h') return { help: true };
2711
- else return null;
3197
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown daemon supervisor flag: ${arg}` };
3198
+ else return { help: false, error: `Unexpected daemon supervisor argument: ${arg}` };
2712
3199
  }
2713
3200
 
2714
3201
  if (!home || !runtimePython || !daemonId || !Number.isFinite(port) || port <= 0) {
2715
- return null;
3202
+ return {
3203
+ help: false,
3204
+ error: 'Daemon supervisor requires --home, --runtime-python, --daemon-id, and a valid --port.',
3205
+ };
2716
3206
  }
2717
3207
 
2718
3208
  return {
@@ -2724,18 +3214,19 @@ function parseDaemonSupervisorArgs(argv) {
2724
3214
  proxy,
2725
3215
  daemonId,
2726
3216
  envOverrides: decodeSupervisorEnvPayload(envJson),
3217
+ error: null,
2727
3218
  };
2728
3219
  }
2729
3220
 
2730
3221
  async function daemonSupervisorMain(rawArgs) {
2731
3222
  const options = parseDaemonSupervisorArgs(rawArgs);
2732
- if (!options) {
2733
- console.error('Invalid daemon supervisor arguments.');
2734
- process.exit(1);
2735
- }
2736
3223
  if (options.help) {
2737
3224
  process.exit(0);
2738
3225
  }
3226
+ if (options.error) {
3227
+ console.error(options.error);
3228
+ process.exit(1);
3229
+ }
2739
3230
 
2740
3231
  const home = options.home;
2741
3232
  let trackedDaemonId = String(options.daemonId || '').trim();
@@ -2762,7 +3253,8 @@ async function daemonSupervisorMain(rawArgs) {
2762
3253
  appendDaemonSupervisorLog(home, `daemon id changed to ${stateDaemonId}; supervisor exiting`);
2763
3254
  return;
2764
3255
  }
2765
- const health = await fetchHealth(state.url || browserUiUrl(options.host, options.port));
3256
+ const authToken = typeof state.auth_token === 'string' ? state.auth_token.trim() : '';
3257
+ const health = await fetchHealth(state.url || browserUiUrl(options.host, options.port), authToken);
2766
3258
  if (health && health.status === 'ok' && healthMatchesManagedState({ health, state, home })) {
2767
3259
  restartBackoffMs = 1000;
2768
3260
  await sleep(2500);
@@ -2785,6 +3277,8 @@ async function daemonSupervisorMain(rawArgs) {
2785
3277
  port: options.port,
2786
3278
  proxy: options.proxy,
2787
3279
  envOverrides: options.envOverrides,
3280
+ authEnabled: state.auth_enabled !== false,
3281
+ authToken,
2788
3282
  });
2789
3283
  trackedDaemonId = String(restarted.statePayload.daemon_id || '').trim();
2790
3284
  observeManagedDaemonChild(home, restarted.child, trackedDaemonId);
@@ -2809,14 +3303,19 @@ function sleep(ms) {
2809
3303
  return new Promise((resolve) => setTimeout(resolve, ms));
2810
3304
  }
2811
3305
 
2812
- async function isHealthy(url) {
2813
- const payload = await fetchHealth(url);
3306
+ async function isHealthy(url, authToken = null) {
3307
+ const payload = await fetchHealth(url, authToken);
2814
3308
  return Boolean(payload && payload.status === 'ok');
2815
3309
  }
2816
3310
 
2817
- async function fetchHealth(url) {
3311
+ async function fetchHealth(url, authToken = null) {
2818
3312
  try {
2819
- const response = await fetch(`${url}/api/health`);
3313
+ const headers = {};
3314
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3315
+ if (normalizedAuthToken) {
3316
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3317
+ }
3318
+ const response = await fetch(`${url}/api/health`, { headers });
2820
3319
  if (!response.ok) {
2821
3320
  return null;
2822
3321
  }
@@ -2866,13 +3365,18 @@ function daemonIdentityError({ url, home, health, state }) {
2866
3365
  ].join('\n');
2867
3366
  }
2868
3367
 
2869
- async function requestDaemonShutdown(url, daemonId) {
3368
+ async function requestDaemonShutdown(url, daemonId, authToken = null) {
2870
3369
  try {
3370
+ const headers = {
3371
+ 'Content-Type': 'application/json',
3372
+ };
3373
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3374
+ if (normalizedAuthToken) {
3375
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3376
+ }
2871
3377
  const response = await fetch(`${url}/api/admin/shutdown`, {
2872
3378
  method: 'POST',
2873
- headers: {
2874
- 'Content-Type': 'application/json',
2875
- },
3379
+ headers,
2876
3380
  body: JSON.stringify({ source: 'ds-launcher', daemon_id: daemonId || null }),
2877
3381
  });
2878
3382
  if (!response.ok) {
@@ -2885,6 +3389,31 @@ async function requestDaemonShutdown(url, daemonId) {
2885
3389
  }
2886
3390
  }
2887
3391
 
3392
+ async function requestDaemonAuthRotate(url, authToken = null) {
3393
+ try {
3394
+ const headers = {
3395
+ 'Content-Type': 'application/json',
3396
+ };
3397
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3398
+ if (normalizedAuthToken) {
3399
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3400
+ }
3401
+ const response = await fetch(`${url}/api/auth/rotate`, {
3402
+ method: 'POST',
3403
+ headers,
3404
+ body: '{}',
3405
+ });
3406
+ if (!response.ok) {
3407
+ return null;
3408
+ }
3409
+ const payload = await response.json().catch(() => ({}));
3410
+ const token = typeof payload?.token === 'string' ? payload.token.trim() : '';
3411
+ return token || null;
3412
+ } catch {
3413
+ return null;
3414
+ }
3415
+ }
3416
+
2888
3417
  function isPidAlive(pid) {
2889
3418
  if (!pid) return false;
2890
3419
  try {
@@ -2902,7 +3431,7 @@ function killManagedProcess(pid, signal) {
2902
3431
  if (signal === 'SIGKILL') {
2903
3432
  taskkillArgs.push('/T', '/F');
2904
3433
  }
2905
- const result = spawnSync('taskkill', taskkillArgs, { stdio: 'ignore' });
3434
+ const result = spawnSync('taskkill', taskkillArgs, syncSpawnOptions({ stdio: 'ignore' }));
2906
3435
  return result.status === 0;
2907
3436
  }
2908
3437
  try {
@@ -2918,9 +3447,9 @@ function killManagedProcess(pid, signal) {
2918
3447
  }
2919
3448
  }
2920
3449
 
2921
- async function waitForDaemonStop({ url, pid, attempts = 20, delayMs = 200 }) {
3450
+ async function waitForDaemonStop({ url, pid, authToken = null, attempts = 20, delayMs = 200 }) {
2922
3451
  for (let attempt = 0; attempt < attempts; attempt += 1) {
2923
- const healthy = url ? await isHealthy(url) : false;
3452
+ const healthy = url ? await isHealthy(url, authToken) : false;
2924
3453
  const alive = pid ? isPidAlive(pid) : false;
2925
3454
  if (!healthy && !alive) {
2926
3455
  return true;
@@ -2945,7 +3474,8 @@ async function stopDaemon(home) {
2945
3474
  const state = readDaemonState(home);
2946
3475
  const configured = readConfiguredUiAddressFromFile(home);
2947
3476
  const url = state?.url || browserUiUrl(state?.host || configured.host, state?.port || configured.port);
2948
- const healthBefore = await fetchHealth(url);
3477
+ const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
3478
+ const healthBefore = await fetchHealth(url, authToken);
2949
3479
  const healthyBefore = Boolean(healthBefore && healthBefore.status === 'ok');
2950
3480
  const sameHomeHealthy = healthMatchesHome({ health: healthBefore, home });
2951
3481
  const pid = state?.pid || (sameHomeHealthy ? healthBefore?.pid : null);
@@ -2986,21 +3516,21 @@ async function stopDaemon(home) {
2986
3516
  let stopped = false;
2987
3517
 
2988
3518
  if (healthyBefore) {
2989
- await requestDaemonShutdown(url, shutdownDaemonId || null);
2990
- stopped = await waitForDaemonStop({ url, pid, attempts: 20, delayMs: 200 });
3519
+ await requestDaemonShutdown(url, shutdownDaemonId || null, authToken);
3520
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 20, delayMs: 200 });
2991
3521
  }
2992
3522
 
2993
3523
  if (!stopped && pid && isPidAlive(pid)) {
2994
3524
  killManagedProcess(pid, 'SIGTERM');
2995
- stopped = await waitForDaemonStop({ url, pid, attempts: 30, delayMs: 200 });
3525
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 30, delayMs: 200 });
2996
3526
  }
2997
3527
 
2998
3528
  if (!stopped && pid && isPidAlive(pid)) {
2999
3529
  killManagedProcess(pid, 'SIGKILL');
3000
- stopped = await waitForDaemonStop({ url, pid, attempts: 20, delayMs: 150 });
3530
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 20, delayMs: 150 });
3001
3531
  }
3002
3532
 
3003
- const stillHealthy = await isHealthy(url);
3533
+ const stillHealthy = await isHealthy(url, authToken);
3004
3534
  if (!stopped && (stillHealthy || (pid && isPidAlive(pid)))) {
3005
3535
  console.error('DeepScientist daemon is still running after shutdown attempts.');
3006
3536
  process.exit(1);
@@ -3034,11 +3564,11 @@ function summarizeUpdateFailure(result) {
3034
3564
  function runNpmInstallLatest(home, npmBinary) {
3035
3565
  const args = ['install', '-g', `${UPDATE_PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'];
3036
3566
  const startedAt = new Date().toISOString();
3037
- const result = spawnSync(npmBinary, args, {
3567
+ const result = spawnSync(npmBinary, args, syncSpawnOptions({
3038
3568
  encoding: 'utf8',
3039
3569
  env: process.env,
3040
3570
  timeout: 15 * 60 * 1000,
3041
- });
3571
+ }));
3042
3572
  const finishedAt = new Date().toISOString();
3043
3573
  const logPath = writeUpdateLog(
3044
3574
  home,
@@ -3132,12 +3662,11 @@ async function promptYesNo(question, { defaultValue = false } = {}) {
3132
3662
 
3133
3663
  function spawnDetachedNode(args, options = {}) {
3134
3664
  const out = options.logPath ? fs.openSync(options.logPath, 'a') : 'ignore';
3135
- const child = spawn(process.execPath, args, {
3665
+ const child = spawn(process.execPath, args, detachedSpawnOptions({
3136
3666
  cwd: options.cwd || repoRoot,
3137
- detached: true,
3138
3667
  stdio: ['ignore', out, out],
3139
3668
  env: options.env || process.env,
3140
- });
3669
+ }));
3141
3670
  child.unref();
3142
3671
  return child;
3143
3672
  }
@@ -3276,19 +3805,23 @@ async function performSelfUpdate(home, options = {}) {
3276
3805
  log_path: installResult.logPath,
3277
3806
  };
3278
3807
  }
3808
+ const restartArgs = [
3809
+ launcherPath,
3810
+ '--home',
3811
+ home,
3812
+ '--host',
3813
+ String(host),
3814
+ '--port',
3815
+ String(port),
3816
+ '--daemon-only',
3817
+ '--no-browser',
3818
+ '--skip-update-check',
3819
+ ];
3820
+ if (daemonState && daemonState.auth_enabled === false) {
3821
+ restartArgs.push('--auth', 'false');
3822
+ }
3279
3823
  spawnDetachedNode(
3280
- [
3281
- launcherPath,
3282
- '--home',
3283
- home,
3284
- '--host',
3285
- String(host),
3286
- '--port',
3287
- String(port),
3288
- '--daemon-only',
3289
- '--no-browser',
3290
- '--skip-update-check',
3291
- ],
3824
+ restartArgs,
3292
3825
  {
3293
3826
  cwd: repoRoot,
3294
3827
  env: process.env,
@@ -3351,11 +3884,11 @@ function relaunchLauncherAfterUpdate(rawArgs, home) {
3351
3884
  message: 'DeepScientist was updated, but the new launcher path could not be resolved for relaunch.',
3352
3885
  };
3353
3886
  }
3354
- const result = spawnSync(process.execPath, [launcherPath, ...normalizeLauncherRelaunchArgs(rawArgs, home)], {
3887
+ const result = spawnSync(process.execPath, [launcherPath, ...normalizeLauncherRelaunchArgs(rawArgs, home)], syncSpawnOptions({
3355
3888
  cwd: repoRoot,
3356
3889
  stdio: 'inherit',
3357
3890
  env: process.env,
3358
- });
3891
+ }));
3359
3892
  if (result.error) {
3360
3893
  return {
3361
3894
  ok: false,
@@ -3483,17 +4016,88 @@ async function startBackgroundUpdateWorker(home, options = {}) {
3483
4016
  };
3484
4017
  }
3485
4018
 
4019
+ async function maybeHandleMiniMaxCodexVersion(home, runtimePython, options = {}) {
4020
+ const configuredRunners = (() => {
4021
+ try {
4022
+ const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'runners'], {
4023
+ capture: true,
4024
+ allowFailure: true,
4025
+ });
4026
+ return String(result.stdout || '');
4027
+ } catch {
4028
+ return '';
4029
+ }
4030
+ })();
4031
+ const profileFromConfig =
4032
+ configuredRunners.match(/^\s*profile:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
4033
+ const binaryFromConfig =
4034
+ configuredRunners.match(/^\s*binary:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || 'codex';
4035
+ const configDirFromConfig =
4036
+ configuredRunners.match(/^\s*config_dir:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '~/.codex';
4037
+
4038
+ const effectiveProfile = String(options.codexProfile || profileFromConfig || '').trim();
4039
+ if (!effectiveProfile) {
4040
+ return false;
4041
+ }
4042
+ const metadata = readCodexProviderMetadata(configDirFromConfig, effectiveProfile);
4043
+ if (String(metadata.provider || '').trim().toLowerCase() !== 'minimax') {
4044
+ return false;
4045
+ }
4046
+ const version = installedCodexCliVersion(options.codexBinary || binaryFromConfig || 'codex');
4047
+ const expected = [0, 57, 0];
4048
+ if (!version || compareCodexCliVersion(version, expected) === 0) {
4049
+ return false;
4050
+ }
4051
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
4052
+ console.log(
4053
+ `MiniMax profile \`${effectiveProfile}\` is configured, but installed Codex CLI is ${formatCodexCliVersion(version)}. MiniMax currently requires Codex CLI 0.57.0 for the documented path.`
4054
+ );
4055
+ console.log('Install it manually with `npm install -g @openai/codex@0.57.0` before continuing.');
4056
+ return false;
4057
+ }
4058
+
4059
+ console.log('');
4060
+ console.log(colorize('\u001B[1;38;5;214m', 'MiniMax compatibility check'));
4061
+ console.log(
4062
+ `DeepScientist detected MiniMax profile \`${effectiveProfile}\`, but installed Codex CLI is ${formatCodexCliVersion(version)}.`
4063
+ );
4064
+ console.log('MiniMax currently requires Codex CLI 0.57.0 for the documented DeepScientist path.');
4065
+ const confirmed = await promptYesNo('Reinstall Codex CLI to 0.57.0 now? [y/N]: ', {
4066
+ defaultValue: false,
4067
+ });
4068
+ if (!confirmed) {
4069
+ return false;
4070
+ }
4071
+ const npmBinary = resolveNpmBinary();
4072
+ if (!npmBinary) {
4073
+ console.error('`npm` is unavailable; cannot reinstall Codex CLI automatically.');
4074
+ process.exit(1);
4075
+ }
4076
+ const result = spawnSync(
4077
+ npmBinary,
4078
+ ['install', '-g', '@openai/codex@0.57.0'],
4079
+ syncSpawnOptions({ stdio: 'inherit' })
4080
+ );
4081
+ if (result.status !== 0) {
4082
+ console.error('Failed to reinstall Codex CLI 0.57.0 automatically.');
4083
+ process.exit(result.status ?? 1);
4084
+ }
4085
+ return true;
4086
+ }
4087
+
3486
4088
  async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallbackPort) {
3487
4089
  try {
3488
4090
  const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'config'], { capture: true, allowFailure: true });
3489
4091
  const text = result.stdout || '';
3490
4092
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3491
4093
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4094
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3492
4095
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3493
4096
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3494
4097
  return {
3495
4098
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3496
4099
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4100
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3497
4101
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3498
4102
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3499
4103
  };
@@ -3501,6 +4105,7 @@ async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallba
3501
4105
  return {
3502
4106
  host: fallbackHost || '0.0.0.0',
3503
4107
  port: fallbackPort || 20999,
4108
+ authEnabled: false,
3504
4109
  defaultMode: 'web',
3505
4110
  autoOpenBrowser: true,
3506
4111
  };
@@ -3513,6 +4118,7 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3513
4118
  return {
3514
4119
  host: fallbackHost || '0.0.0.0',
3515
4120
  port: fallbackPort || 20999,
4121
+ authEnabled: false,
3516
4122
  defaultMode: 'web',
3517
4123
  autoOpenBrowser: true,
3518
4124
  };
@@ -3521,11 +4127,13 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3521
4127
  const text = fs.readFileSync(configPath, 'utf8');
3522
4128
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3523
4129
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4130
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3524
4131
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3525
4132
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3526
4133
  return {
3527
4134
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3528
4135
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4136
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3529
4137
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3530
4138
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3531
4139
  };
@@ -3533,20 +4141,51 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3533
4141
  return {
3534
4142
  host: fallbackHost || '0.0.0.0',
3535
4143
  port: fallbackPort || 20999,
4144
+ authEnabled: false,
3536
4145
  defaultMode: 'web',
3537
4146
  autoOpenBrowser: true,
3538
4147
  };
3539
4148
  }
3540
4149
  }
3541
4150
 
3542
- async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}) {
4151
+ async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}, authEnabled = false) {
3543
4152
  const browserUrl = browserUiUrl(host, port);
3544
4153
  const daemonBindUrl = bindUiUrl(host, port);
3545
4154
  const state = readDaemonState(home);
3546
- const existingHealth = await fetchHealth(browserUrl);
4155
+ const desiredAuthToken = authEnabled ? generateBrowserAuthToken() : null;
4156
+ const launchUrl = browserUrl;
4157
+ const bindLaunchUrl = daemonBindUrl;
4158
+ const existingHealth = await fetchHealth(browserUrl, typeof state?.auth_token === 'string' ? state.auth_token.trim() : '');
3547
4159
  if (existingHealth && existingHealth.status === 'ok') {
3548
4160
  if (state && healthMatchesManagedState({ health: existingHealth, state, home })) {
3549
- return { url: browserUrl, bindUrl: daemonBindUrl, reused: true };
4161
+ const stateAuthEnabled = state.auth_enabled !== false;
4162
+ const stateAuthToken = typeof state.auth_token === 'string' ? state.auth_token.trim() : '';
4163
+ let resolvedAuthToken = stateAuthToken || null;
4164
+ if (stateAuthEnabled) {
4165
+ const rotatedAuthToken = await requestDaemonAuthRotate(browserUrl, stateAuthToken);
4166
+ if (!rotatedAuthToken) {
4167
+ console.error('Managed daemon is healthy, but the browser auth token could not be rotated.');
4168
+ console.error('Restart the daemon with `ds --restart` if this keeps happening.');
4169
+ process.exit(1);
4170
+ }
4171
+ resolvedAuthToken = rotatedAuthToken;
4172
+ writeDaemonState(home, {
4173
+ ...state,
4174
+ auth_enabled: true,
4175
+ auth_token: rotatedAuthToken,
4176
+ url: browserUrl,
4177
+ bind_url: daemonBindUrl,
4178
+ launch_url: launchUrl,
4179
+ bind_launch_url: bindLaunchUrl,
4180
+ });
4181
+ }
4182
+ return {
4183
+ url: browserUrl,
4184
+ bindUrl: daemonBindUrl,
4185
+ reused: true,
4186
+ authEnabled: stateAuthEnabled,
4187
+ authToken: resolvedAuthToken,
4188
+ };
3550
4189
  }
3551
4190
  console.error(
3552
4191
  state
@@ -3580,11 +4219,13 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3580
4219
  port,
3581
4220
  proxy,
3582
4221
  envOverrides,
4222
+ authEnabled,
4223
+ authToken: desiredAuthToken,
3583
4224
  });
3584
4225
  const logPath = startedProcess.logPath;
3585
4226
 
3586
4227
  for (let attempt = 0; attempt < 60; attempt += 1) {
3587
- const health = await fetchHealth(browserUrl);
4228
+ const health = await fetchHealth(browserUrl, desiredAuthToken);
3588
4229
  if (health && health.status === 'ok') {
3589
4230
  const liveState = readDaemonState(home);
3590
4231
  if (!healthMatchesManagedState({ health, state: liveState, home })) {
@@ -3603,12 +4244,26 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3603
4244
  if (supervisorPid) {
3604
4245
  appendDaemonSupervisorLog(home, `supervisor started with pid ${supervisorPid}`);
3605
4246
  }
3606
- return { url: browserUrl, bindUrl: daemonBindUrl, reused: false };
4247
+ return {
4248
+ url: launchUrl,
4249
+ bindUrl: bindLaunchUrl,
4250
+ reused: false,
4251
+ authEnabled,
4252
+ authToken: desiredAuthToken,
4253
+ };
3607
4254
  }
3608
4255
  await sleep(250);
3609
4256
  }
3610
4257
 
3611
4258
  console.error('DeepScientist daemon failed to become healthy.');
4259
+ console.error(`Expected local URL: ${launchUrl}`);
4260
+ console.error(`Daemon bind URL: ${bindLaunchUrl}`);
4261
+ if (authEnabled && desiredAuthToken) {
4262
+ console.error(`Auth token: ${desiredAuthToken}`);
4263
+ }
4264
+ if (['0.0.0.0', '::', '[::]'].includes(String(host || '').trim())) {
4265
+ console.error(`Hint: ${String(host || '').trim() || '0.0.0.0'} is a bind address. Local browser and health probes use ${browserUrl}.`);
4266
+ }
3612
4267
  const logTail = tailLog(logPath);
3613
4268
  if (logTail) {
3614
4269
  console.error(logTail);
@@ -3619,7 +4274,7 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3619
4274
  function openBrowser(url) {
3620
4275
  const spawnDetached = (command, args) => {
3621
4276
  try {
3622
- const child = spawn(command, args, { detached: true, stdio: 'ignore' });
4277
+ const child = spawn(command, args, detachedSpawnOptions({ stdio: 'ignore' }));
3623
4278
  child.unref();
3624
4279
  return true;
3625
4280
  } catch {
@@ -3692,12 +4347,15 @@ function handleCodexPreflightFailure(error) {
3692
4347
  return true;
3693
4348
  }
3694
4349
 
3695
- function launchTui(url, questId, home, runtimePython) {
4350
+ function launchTui(url, questId, home, runtimePython, authToken = null) {
3696
4351
  const entry = ensureNodeBundle('src/tui', 'dist/index.js');
3697
4352
  const args = [entry, '--base-url', url];
3698
4353
  if (questId) {
3699
4354
  args.push('--quest-id', questId);
3700
4355
  }
4356
+ if (typeof authToken === 'string' && authToken.trim()) {
4357
+ args.push('--auth-token', authToken.trim());
4358
+ }
3701
4359
  const child = spawn(process.execPath, args, {
3702
4360
  cwd: repoRoot,
3703
4361
  stdio: 'inherit',
@@ -3715,14 +4373,15 @@ function launchTui(url, questId, home, runtimePython) {
3715
4373
 
3716
4374
  async function updateMain(rawArgs) {
3717
4375
  const options = parseUpdateArgs(rawArgs);
3718
- if (!options) {
3719
- printUpdateHelp();
3720
- process.exit(1);
3721
- }
3722
4376
  if (options.help) {
3723
4377
  printUpdateHelp();
3724
4378
  process.exit(0);
3725
4379
  }
4380
+ if (options.error) {
4381
+ console.error(options.error);
4382
+ console.error('Run `ds update --help` for update usage.');
4383
+ process.exit(1);
4384
+ }
3726
4385
 
3727
4386
  const home = options.home || resolveHome(rawArgs);
3728
4387
  applyLauncherProxy(options.proxy);
@@ -3837,14 +4496,15 @@ async function updateMain(rawArgs) {
3837
4496
 
3838
4497
  async function migrateMain(rawArgs) {
3839
4498
  const options = parseMigrateArgs(rawArgs);
3840
- if (!options) {
3841
- printMigrateHelp();
3842
- process.exit(1);
3843
- }
3844
4499
  if (options.help) {
3845
4500
  printMigrateHelp();
3846
4501
  process.exit(0);
3847
4502
  }
4503
+ if (options.error) {
4504
+ console.error(options.error);
4505
+ console.error('Run `ds migrate --help` for migration usage.');
4506
+ process.exit(1);
4507
+ }
3848
4508
 
3849
4509
  const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
3850
4510
  const targetHome = path.resolve(options.target);
@@ -3933,12 +4593,11 @@ async function migrateMain(rawArgs) {
3933
4593
  const child = spawn(
3934
4594
  process.execPath,
3935
4595
  [migratedLauncher, '--home', targetHome, '--daemon-only', '--no-browser', '--skip-update-check'],
3936
- {
4596
+ detachedSpawnOptions({
3937
4597
  cwd: path.join(targetHome, 'cli'),
3938
- detached: true,
3939
4598
  stdio: 'ignore',
3940
4599
  env: process.env,
3941
- }
4600
+ })
3942
4601
  );
3943
4602
  child.unref();
3944
4603
  restartMessage = 'Managed daemon restart scheduled from the migrated home.';
@@ -3966,13 +4625,15 @@ async function migrateMain(rawArgs) {
3966
4625
 
3967
4626
  async function launcherMain(rawArgs) {
3968
4627
  const options = parseLauncherArgs(rawArgs);
3969
- if (!options) {
3970
- return false;
3971
- }
3972
4628
  if (options.help) {
3973
4629
  printLauncherHelp();
3974
4630
  process.exit(0);
3975
4631
  }
4632
+ if (options.error) {
4633
+ console.error(options.error);
4634
+ console.error('Run `ds --help` for launcher usage.');
4635
+ process.exit(1);
4636
+ }
3976
4637
 
3977
4638
  const home = options.home || resolveHome(rawArgs);
3978
4639
  applyLauncherProxy(options.proxy);
@@ -3990,8 +4651,10 @@ async function launcherMain(rawArgs) {
3990
4651
  if (options.status) {
3991
4652
  const state = readDaemonState(home);
3992
4653
  const configured = readConfiguredUiAddressFromFile(home, options.host, options.port);
3993
- const url = state?.url || browserUiUrl(configured.host, configured.port);
3994
- const health = await fetchHealth(url);
4654
+ const url = state?.launch_url || state?.url || browserUiUrl(configured.host, configured.port);
4655
+ const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
4656
+ const probeUrl = state?.url || browserUiUrl(configured.host, configured.port);
4657
+ const health = await fetchHealth(probeUrl, authToken);
3995
4658
  const healthy = Boolean(health && health.status === 'ok');
3996
4659
  const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
3997
4660
  console.log(
@@ -4020,6 +4683,7 @@ async function launcherMain(rawArgs) {
4020
4683
  binary: options.codexBinary,
4021
4684
  });
4022
4685
  ensureInitialized(home, runtimePython);
4686
+ await maybeHandleMiniMaxCodexVersion(home, runtimePython, options);
4023
4687
  if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
4024
4688
  return true;
4025
4689
  }
@@ -4028,20 +4692,27 @@ async function launcherMain(rawArgs) {
4028
4692
  const configuredUi = await readConfiguredUiAddress(home, runtimePython, options.host, options.port);
4029
4693
  const host = configuredUi.host;
4030
4694
  const port = configuredUi.port;
4695
+ const authEnabled = options.auth === null ? false : options.auth !== false;
4031
4696
  const mode = normalizeMode(options.mode ?? 'web');
4032
4697
  const shouldOpenBrowser = options.daemonOnly
4033
4698
  ? false
4034
4699
  : options.openBrowser === null
4035
4700
  ? configuredUi.autoOpenBrowser !== false && mode !== 'tui'
4036
4701
  : options.openBrowser;
4037
- if (options.restart) {
4702
+ const existingState = readDaemonState(home);
4703
+ const existingAuthEnabled = existingState ? existingState.auth_enabled !== false : null;
4704
+ const existingAuthToken = typeof existingState?.auth_token === 'string' ? existingState.auth_token.trim() : '';
4705
+ const authStateMismatch = existingState && (
4706
+ existingAuthEnabled !== authEnabled || (authEnabled && !existingAuthToken)
4707
+ );
4708
+ if (options.restart || authStateMismatch) {
4038
4709
  await stopDaemon(home);
4039
4710
  }
4040
4711
 
4041
4712
  step(4, 4, 'Starting local daemon and UI surfaces');
4042
4713
  let started;
4043
4714
  try {
4044
- started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv);
4715
+ started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv, authEnabled);
4045
4716
  } catch (error) {
4046
4717
  if (handleCodexPreflightFailure(error)) return true;
4047
4718
  throw error;
@@ -4057,6 +4728,8 @@ async function launcherMain(rawArgs) {
4057
4728
  home,
4058
4729
  pythonSelection: pythonRuntime.runtimeProbe,
4059
4730
  yolo: options.yolo,
4731
+ authEnabled: started.authEnabled,
4732
+ authToken: started.authToken,
4060
4733
  });
4061
4734
 
4062
4735
  if (options.daemonOnly) {
@@ -4065,12 +4738,16 @@ async function launcherMain(rawArgs) {
4065
4738
  if (mode === 'web') {
4066
4739
  process.exit(0);
4067
4740
  }
4068
- launchTui(started.url, options.questId, home, runtimePython);
4741
+ launchTui(browserUiUrl(host, port), options.questId, home, runtimePython, started.authToken);
4069
4742
  return true;
4070
4743
  }
4071
4744
 
4072
4745
  async function main() {
4073
- const args = process.argv.slice(2);
4746
+ const normalizedArgState = normalizeLegacyHostFlagArgs(process.argv.slice(2));
4747
+ const args = normalizedArgState.args;
4748
+ for (const warning of normalizedArgState.warnings) {
4749
+ console.warn(warning);
4750
+ }
4074
4751
  if (args[0] === '--daemon-supervisor') {
4075
4752
  await daemonSupervisorMain(args.slice(1));
4076
4753
  return;
@@ -4084,7 +4761,11 @@ async function main() {
4084
4761
  await migrateMain(args);
4085
4762
  return;
4086
4763
  }
4087
- if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
4764
+ if (
4765
+ args.length === 0
4766
+ || args[0] === 'ui'
4767
+ || (args[0]?.startsWith('--') && (!positional || !pythonCommands.has(positional.value)))
4768
+ ) {
4088
4769
  await launcherMain(args);
4089
4770
  return;
4090
4771
  }
@@ -4097,7 +4778,7 @@ async function main() {
4097
4778
  const pythonRuntime = ensurePythonRuntime(home);
4098
4779
  const runtimePython = pythonRuntime.runtimePython;
4099
4780
  const codexOverrideEnv = buildCodexOverrideEnv({
4100
- yolo: args.includes('--yolo'),
4781
+ yolo: resolveYoloFlag(args, true),
4101
4782
  profile: readOptionValue(args, '--codex-profile'),
4102
4783
  binary: readOptionValue(args, '--codex'),
4103
4784
  });
@@ -4140,6 +4821,8 @@ module.exports = {
4140
4821
  resolveUvBinary,
4141
4822
  resolveHome,
4142
4823
  parseLauncherArgs,
4824
+ generateBrowserAuthToken,
4825
+ appendBrowserAuthToken,
4143
4826
  normalizeProxyUrl,
4144
4827
  parseMigrateArgs,
4145
4828
  parseLegacyWrapperCandidate,
@@ -4147,12 +4830,14 @@ module.exports = {
4147
4830
  useEditableProjectInstall,
4148
4831
  compareVersions,
4149
4832
  detectInstallMode,
4833
+ buildUvSyncFailureGuidance,
4150
4834
  updateManualCommand,
4151
4835
  buildUpdateStatus,
4152
4836
  parseYesNoAnswer,
4153
4837
  normalizeLauncherRelaunchArgs,
4154
4838
  officialRepositoryLine,
4155
4839
  stripAnsi,
4840
+ normalizeLegacyHostFlagArgs,
4156
4841
  },
4157
4842
  };
4158
4843