@researai/deepscientist 1.5.15 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +336 -98
  2. package/bin/ds.js +691 -91
  3. package/docs/en/00_QUICK_START.md +36 -15
  4. package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +11 -5
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/README.md +18 -0
  15. package/docs/zh/00_QUICK_START.md +36 -15
  16. package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
  17. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  18. package/docs/zh/05_TUI_GUIDE.md +6 -0
  19. package/docs/zh/09_DOCTOR.md +11 -5
  20. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  21. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  22. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  23. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  24. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  25. package/docs/zh/README.md +18 -0
  26. package/package.json +1 -1
  27. package/pyproject.toml +1 -1
  28. package/src/deepscientist/__init__.py +1 -1
  29. package/src/deepscientist/acp/envelope.py +6 -0
  30. package/src/deepscientist/artifact/service.py +647 -22
  31. package/src/deepscientist/bash_exec/service.py +234 -9
  32. package/src/deepscientist/cli.py +115 -19
  33. package/src/deepscientist/codex_cli_compat.py +232 -0
  34. package/src/deepscientist/config/models.py +2 -1
  35. package/src/deepscientist/config/service.py +31 -9
  36. package/src/deepscientist/daemon/api/handlers.py +125 -6
  37. package/src/deepscientist/daemon/api/router.py +4 -0
  38. package/src/deepscientist/daemon/app.py +715 -98
  39. package/src/deepscientist/gitops/__init__.py +10 -1
  40. package/src/deepscientist/gitops/diff.py +129 -0
  41. package/src/deepscientist/gitops/service.py +4 -1
  42. package/src/deepscientist/mcp/server.py +39 -0
  43. package/src/deepscientist/prompts/builder.py +255 -32
  44. package/src/deepscientist/quest/layout.py +15 -2
  45. package/src/deepscientist/quest/service.py +295 -43
  46. package/src/deepscientist/quest/stage_views.py +6 -1
  47. package/src/deepscientist/runners/codex.py +86 -31
  48. package/src/deepscientist/skills/__init__.py +2 -2
  49. package/src/deepscientist/skills/installer.py +196 -5
  50. package/src/deepscientist/skills/registry.py +66 -0
  51. package/src/prompts/connectors/qq.md +18 -8
  52. package/src/prompts/connectors/weixin.md +16 -6
  53. package/src/prompts/contracts/shared_interaction.md +12 -1
  54. package/src/prompts/system.md +10 -5
  55. package/src/prompts/system_copilot.md +43 -0
  56. package/src/skills/analysis-campaign/SKILL.md +1 -0
  57. package/src/skills/baseline/SKILL.md +8 -0
  58. package/src/skills/decision/SKILL.md +8 -0
  59. package/src/skills/experiment/SKILL.md +8 -0
  60. package/src/skills/figure-polish/SKILL.md +1 -0
  61. package/src/skills/finalize/SKILL.md +1 -0
  62. package/src/skills/idea/SKILL.md +1 -0
  63. package/src/skills/intake-audit/SKILL.md +8 -0
  64. package/src/skills/mentor/SKILL.md +217 -0
  65. package/src/skills/mentor/references/correction-rules.md +210 -0
  66. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  67. package/src/skills/mentor/references/persona-profile.md +138 -0
  68. package/src/skills/mentor/references/taste-profile.md +128 -0
  69. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  70. package/src/skills/mentor/references/work-profile.md +289 -0
  71. package/src/skills/mentor/references/workflow-profile.md +240 -0
  72. package/src/skills/optimize/SKILL.md +1 -0
  73. package/src/skills/rebuttal/SKILL.md +1 -0
  74. package/src/skills/review/SKILL.md +1 -0
  75. package/src/skills/scout/SKILL.md +8 -0
  76. package/src/skills/write/SKILL.md +1 -0
  77. package/src/tui/dist/app/AppContainer.js +19 -11
  78. package/src/tui/dist/index.js +4 -1
  79. package/src/tui/dist/lib/api.js +33 -3
  80. package/src/tui/package.json +1 -1
  81. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  82. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  83. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  84. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  85. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  86. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  87. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  88. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  89. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  90. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  91. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  92. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  93. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  94. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  95. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  96. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  97. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  98. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  99. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  100. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  101. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  102. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  103. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  104. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  105. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  106. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  107. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  108. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  109. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  110. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  111. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  112. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  113. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  114. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  115. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  116. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  117. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  118. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  119. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  120. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  121. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  122. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  123. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  124. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  125. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  126. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  127. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  128. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  129. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  130. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  131. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  132. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  133. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  134. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  135. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  136. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  137. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  138. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  139. package/src/ui/dist/index.html +5 -2
  140. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  141. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  142. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  143. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  144. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  145. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  146. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  147. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  148. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  149. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  150. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  151. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  152. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  153. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  154. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  155. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  156. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  157. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  158. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  159. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  160. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  161. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  162. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  163. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  164. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  165. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  166. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  167. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  168. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  169. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  170. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  171. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  172. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  173. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  174. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  175. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  176. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  177. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  178. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  179. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  180. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  181. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  182. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  183. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  184. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  185. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  186. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  187. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  188. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  189. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  190. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  191. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  192. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  193. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
package/bin/ds.js CHANGED
@@ -36,7 +36,7 @@ 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
41
  function buildCodexOverrideEnv({ yolo = true, profile = null, binary = null } = {}) {
42
42
  const normalizedProfile = typeof profile === 'string' ? profile.trim() : '';
@@ -77,6 +77,33 @@ function parseBooleanFlagValue(rawValue) {
77
77
  return null;
78
78
  }
79
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
+
80
107
  function parseYoloArg(args, index, currentValue = true) {
81
108
  const arg = args[index];
82
109
  if (arg === '--yolo') {
@@ -130,6 +157,7 @@ Usage:
130
157
  Launcher flags:
131
158
  --host <host> Bind host for the local web daemon
132
159
  --port <port> Bind port for the local web daemon
160
+ --auth [true|false] Require a 16-character local browser password. Default is false
133
161
  --tui Start the terminal workspace only
134
162
  --both Start web + terminal workspace together
135
163
  --no-browser Do not auto-open the browser
@@ -192,6 +220,47 @@ function normalizeProxyUrl(rawValue) {
192
220
  return value || null;
193
221
  }
194
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
+
195
264
  function applyLauncherProxy(proxyUrl) {
196
265
  const normalized = normalizeProxyUrl(proxyUrl);
197
266
  if (!normalized) {
@@ -562,6 +631,20 @@ function bindUiUrl(host, port) {
562
631
  return `http://${formatHttpHost(normalized)}:${port}`;
563
632
  }
564
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
+
565
648
  function normalizeMode(value) {
566
649
  const normalized = String(value || '')
567
650
  .trim()
@@ -588,6 +671,66 @@ function parseBooleanSetting(rawValue, fallback = false) {
588
671
  return fallback;
589
672
  }
590
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
+
591
734
  function supportsAnsi() {
592
735
  return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb');
593
736
  }
@@ -623,6 +766,64 @@ function colorize(code, text) {
623
766
  return `${code}${text}\u001B[0m`;
624
767
  }
625
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
+
626
827
  const OFFICIAL_REPOSITORY_URL = 'https://github.com/ResearAI/DeepScientist';
627
828
 
628
829
  function officialRepositoryLine() {
@@ -685,7 +886,7 @@ function pythonVersionText(probe) {
685
886
  return version;
686
887
  }
687
888
 
688
- function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
889
+ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken }) {
689
890
  const runtimeRows = [
690
891
  ['Version', packageJson.version],
691
892
  ['Home', truncateMiddle(home)],
@@ -694,6 +895,9 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
694
895
  ['Python', truncateMiddle(pythonVersionText(pythonSelection))],
695
896
  ['Codex mode', yolo ? 'YOLO (never + danger-full-access)' : 'Default (on-request + workspace-write)'],
696
897
  ];
898
+ if (authEnabled && authToken) {
899
+ runtimeRows.splice(4, 0, ['Auth token', authToken]);
900
+ }
697
901
  if (pythonSelection && pythonSelection.sourceLabel) {
698
902
  runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
699
903
  }
@@ -706,6 +910,7 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
706
910
  ['ds --yolo --port 20999 --here', 'Start in ./DeepScientist under the current directory with YOLO Codex access'],
707
911
  ['ds --port 21000', 'Change the web port'],
708
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'],
709
914
  ['ds --here', 'Use ./DeepScientist under the current directory as home'],
710
915
  ['ds --both', 'Start web + TUI together'],
711
916
  ['ds --tui', 'Start the terminal workspace only'],
@@ -729,6 +934,8 @@ function printLaunchCard({
729
934
  home,
730
935
  pythonSelection,
731
936
  yolo,
937
+ authEnabled,
938
+ authToken,
732
939
  }) {
733
940
  const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
734
941
  const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
@@ -787,13 +994,18 @@ function printLaunchCard({
787
994
  console.log(centerText(colorize('\u001B[1m', workspaceMode), width));
788
995
  console.log(centerText(urlLabel, width));
789
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
+ }
790
1002
  console.log(centerText(browserLine, width));
791
1003
  console.log(centerText(nextStep, width));
792
1004
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
793
1005
  console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
794
1006
  console.log(centerText(officialRepositoryLine(), width));
795
1007
  console.log('');
796
- renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo });
1008
+ renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken });
797
1009
  }
798
1010
 
799
1011
  function escapeHtml(value) {
@@ -1020,6 +1232,7 @@ function parseLauncherArgs(argv) {
1020
1232
  let daemonOnly = false;
1021
1233
  let skipUpdateCheck = false;
1022
1234
  let yolo = true;
1235
+ let auth = null;
1023
1236
  let codexProfile = null;
1024
1237
  let codexBinary = null;
1025
1238
 
@@ -1040,21 +1253,71 @@ function parseLauncherArgs(argv) {
1040
1253
  else if (arg === '--open-browser') openBrowser = true;
1041
1254
  else if (arg === '--daemon-only') daemonOnly = true;
1042
1255
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
1256
+ else if (arg === '--here') continue;
1043
1257
  else {
1044
1258
  const parsedYolo = parseYoloArg(args, index, yolo);
1045
1259
  if (parsedYolo.matched) {
1046
1260
  yolo = parsedYolo.value;
1047
1261
  index += Math.max(0, parsedYolo.consumed - 1);
1048
- } else if (arg === '--codex-profile' && args[index + 1]) codexProfile = args[++index];
1049
- else if (arg === '--codex' && args[index + 1]) codexBinary = args[++index];
1050
- else if (arg === '--host' && args[index + 1]) host = args[++index];
1051
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
1052
- else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
1053
- else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
1054
- else if (arg === '--quest-id' && args[index + 1]) questId = args[++index];
1055
- else if (arg === '--mode' && args[index + 1]) mode = normalizeMode(args[++index]);
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
+ }
1056
1318
  else if (arg === '--help' || arg === '-h') return { help: true };
1057
- else if (!arg.startsWith('--')) return null;
1319
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown launcher flag: ${arg}` };
1320
+ else return { help: false, error: `Unexpected launcher argument: ${arg}` };
1058
1321
  }
1059
1322
  }
1060
1323
 
@@ -1073,8 +1336,10 @@ function parseLauncherArgs(argv) {
1073
1336
  daemonOnly,
1074
1337
  skipUpdateCheck,
1075
1338
  yolo,
1339
+ auth,
1076
1340
  codexProfile,
1077
1341
  codexBinary,
1342
+ error: null,
1078
1343
  };
1079
1344
  }
1080
1345
 
@@ -1148,12 +1413,32 @@ function parseUpdateArgs(argv) {
1148
1413
  else if (arg === '--worker') worker = true;
1149
1414
  else if (arg === '--restart-daemon') restartDaemon = true;
1150
1415
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
1151
- else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
1152
- else if (arg === '--host' && args[index + 1]) host = args[++index];
1153
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
1154
- 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
+ }
1155
1439
  else if (arg === '--help' || arg === '-h') return { help: true };
1156
- 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}` };
1157
1442
  }
1158
1443
 
1159
1444
  return {
@@ -1172,6 +1457,7 @@ function parseUpdateArgs(argv) {
1172
1457
  proxy,
1173
1458
  restartDaemon,
1174
1459
  skipUpdateCheck,
1460
+ error: null,
1175
1461
  };
1176
1462
  }
1177
1463
 
@@ -1189,15 +1475,23 @@ function parseMigrateArgs(argv) {
1189
1475
  const arg = args[index];
1190
1476
  if (arg === '--yes') yes = true;
1191
1477
  else if (arg === '--restart') restart = true;
1192
- 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
+ }
1193
1484
  else if (arg === '--help' || arg === '-h') return { help: true };
1194
- else if (arg.startsWith('--')) return null;
1485
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown migrate flag: ${arg}` };
1195
1486
  else if (!target) target = path.resolve(expandUserPath(arg));
1196
- else return null;
1487
+ else return { help: false, error: `Unexpected migrate argument: ${arg}` };
1197
1488
  }
1198
1489
 
1199
1490
  if (!target) {
1200
- return null;
1491
+ return {
1492
+ help: false,
1493
+ error: 'Missing migration target path.',
1494
+ };
1201
1495
  }
1202
1496
 
1203
1497
  return {
@@ -1206,6 +1500,7 @@ function parseMigrateArgs(argv) {
1206
1500
  target,
1207
1501
  yes,
1208
1502
  restart,
1503
+ error: null,
1209
1504
  };
1210
1505
  }
1211
1506
 
@@ -2152,13 +2447,17 @@ function ensureUvBinary(home) {
2152
2447
  }
2153
2448
 
2154
2449
  function buildUvRuntimeEnv(home, extraEnv = {}) {
2155
- return {
2450
+ const env = {
2156
2451
  ...process.env,
2157
2452
  UV_CACHE_DIR: runtimeUvCachePath(home),
2158
2453
  UV_PROJECT_ENVIRONMENT: runtimePythonEnvPath(home),
2159
2454
  UV_PYTHON_INSTALL_DIR: runtimeUvPythonInstallPath(home),
2160
2455
  ...extraEnv,
2161
2456
  };
2457
+ for (const key of ['PYTHONPATH', 'PYTHONHOME', 'VIRTUAL_ENV', '__PYVENV_LAUNCHER__']) {
2458
+ delete env[key];
2459
+ }
2460
+ return env;
2162
2461
  }
2163
2462
 
2164
2463
  function ensureUvLockPresent() {
@@ -2171,6 +2470,43 @@ function ensureUvLockPresent() {
2171
2470
  process.exit(1);
2172
2471
  }
2173
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
+
2174
2510
  function resolveUvVersion(uvBinary) {
2175
2511
  const result = runSync(uvBinary, ['--version'], { capture: true, allowFailure: true });
2176
2512
  if (result.status !== 0) {
@@ -2237,7 +2573,10 @@ function resolveBackgroundPythonExecutable(runtimePython) {
2237
2573
  }
2238
2574
 
2239
2575
  function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2240
- 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
+ }
2241
2580
  if (!editable) {
2242
2581
  args.push('--no-editable');
2243
2582
  }
@@ -2250,7 +2589,9 @@ function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2250
2589
  return;
2251
2590
  }
2252
2591
  console.error('DeepScientist could not sync the locked Python environment with uv.');
2253
- 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
+ }
2254
2595
  process.exit(result.status ?? 1);
2255
2596
  }
2256
2597
 
@@ -2678,9 +3019,25 @@ function decodeSupervisorEnvPayload(rawValue) {
2678
3019
  }
2679
3020
  }
2680
3021
 
2681
- 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
+ }) {
2682
3033
  const browserUrl = browserUiUrl(host, port);
2683
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;
2684
3041
  const logPath = path.join(home, 'logs', 'daemon.log');
2685
3042
  ensureDir(path.dirname(logPath));
2686
3043
  const out = fs.openSync(logPath, 'a');
@@ -2700,6 +3057,9 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2700
3057
  host,
2701
3058
  '--port',
2702
3059
  String(port),
3060
+ '--auth',
3061
+ resolvedAuthEnabled ? 'true' : 'false',
3062
+ ...(resolvedAuthEnabled && resolvedAuthToken ? ['--auth-token', resolvedAuthToken] : []),
2703
3063
  ],
2704
3064
  detachedSpawnOptions({
2705
3065
  cwd: repoRoot,
@@ -2712,6 +3072,8 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2712
3072
  DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2713
3073
  DS_DAEMON_ID: resolvedDaemonId,
2714
3074
  DS_DAEMON_MANAGED_BY: 'ds-launcher',
3075
+ DS_UI_AUTH_ENABLED: resolvedAuthEnabled ? '1' : '0',
3076
+ ...(resolvedAuthEnabled && resolvedAuthToken ? { DS_UI_AUTH_TOKEN: resolvedAuthToken } : {}),
2715
3077
  },
2716
3078
  })
2717
3079
  );
@@ -2722,10 +3084,14 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2722
3084
  port,
2723
3085
  url: browserUrl,
2724
3086
  bind_url: daemonBindUrl,
3087
+ launch_url: launchUrl,
3088
+ bind_launch_url: bindLaunchUrl,
2725
3089
  log_path: logPath,
2726
3090
  started_at: new Date().toISOString(),
2727
3091
  home: normalizeHomePath(home),
2728
3092
  daemon_id: resolvedDaemonId,
3093
+ auth_enabled: resolvedAuthEnabled,
3094
+ auth_token: resolvedAuthToken,
2729
3095
  };
2730
3096
  writeDaemonState(home, statePayload);
2731
3097
  return {
@@ -2733,6 +3099,8 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2733
3099
  statePayload,
2734
3100
  browserUrl,
2735
3101
  bindUrl: daemonBindUrl,
3102
+ launchUrl,
3103
+ bindLaunchUrl,
2736
3104
  logPath,
2737
3105
  };
2738
3106
  }
@@ -2787,19 +3155,54 @@ function parseDaemonSupervisorArgs(argv) {
2787
3155
 
2788
3156
  for (let index = 0; index < args.length; index += 1) {
2789
3157
  const arg = args[index];
2790
- if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
2791
- else if (arg === '--runtime-python' && args[index + 1]) runtimePython = args[++index];
2792
- else if (arg === '--host' && args[index + 1]) host = args[++index];
2793
- else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
2794
- else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
2795
- else if (arg === '--daemon-id' && args[index + 1]) daemonId = args[++index];
2796
- 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
+ }
2797
3196
  else if (arg === '--help' || arg === '-h') return { help: true };
2798
- 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}` };
2799
3199
  }
2800
3200
 
2801
3201
  if (!home || !runtimePython || !daemonId || !Number.isFinite(port) || port <= 0) {
2802
- return null;
3202
+ return {
3203
+ help: false,
3204
+ error: 'Daemon supervisor requires --home, --runtime-python, --daemon-id, and a valid --port.',
3205
+ };
2803
3206
  }
2804
3207
 
2805
3208
  return {
@@ -2811,18 +3214,19 @@ function parseDaemonSupervisorArgs(argv) {
2811
3214
  proxy,
2812
3215
  daemonId,
2813
3216
  envOverrides: decodeSupervisorEnvPayload(envJson),
3217
+ error: null,
2814
3218
  };
2815
3219
  }
2816
3220
 
2817
3221
  async function daemonSupervisorMain(rawArgs) {
2818
3222
  const options = parseDaemonSupervisorArgs(rawArgs);
2819
- if (!options) {
2820
- console.error('Invalid daemon supervisor arguments.');
2821
- process.exit(1);
2822
- }
2823
3223
  if (options.help) {
2824
3224
  process.exit(0);
2825
3225
  }
3226
+ if (options.error) {
3227
+ console.error(options.error);
3228
+ process.exit(1);
3229
+ }
2826
3230
 
2827
3231
  const home = options.home;
2828
3232
  let trackedDaemonId = String(options.daemonId || '').trim();
@@ -2849,7 +3253,8 @@ async function daemonSupervisorMain(rawArgs) {
2849
3253
  appendDaemonSupervisorLog(home, `daemon id changed to ${stateDaemonId}; supervisor exiting`);
2850
3254
  return;
2851
3255
  }
2852
- 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);
2853
3258
  if (health && health.status === 'ok' && healthMatchesManagedState({ health, state, home })) {
2854
3259
  restartBackoffMs = 1000;
2855
3260
  await sleep(2500);
@@ -2872,6 +3277,8 @@ async function daemonSupervisorMain(rawArgs) {
2872
3277
  port: options.port,
2873
3278
  proxy: options.proxy,
2874
3279
  envOverrides: options.envOverrides,
3280
+ authEnabled: state.auth_enabled !== false,
3281
+ authToken,
2875
3282
  });
2876
3283
  trackedDaemonId = String(restarted.statePayload.daemon_id || '').trim();
2877
3284
  observeManagedDaemonChild(home, restarted.child, trackedDaemonId);
@@ -2896,14 +3303,19 @@ function sleep(ms) {
2896
3303
  return new Promise((resolve) => setTimeout(resolve, ms));
2897
3304
  }
2898
3305
 
2899
- async function isHealthy(url) {
2900
- const payload = await fetchHealth(url);
3306
+ async function isHealthy(url, authToken = null) {
3307
+ const payload = await fetchHealth(url, authToken);
2901
3308
  return Boolean(payload && payload.status === 'ok');
2902
3309
  }
2903
3310
 
2904
- async function fetchHealth(url) {
3311
+ async function fetchHealth(url, authToken = null) {
2905
3312
  try {
2906
- 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 });
2907
3319
  if (!response.ok) {
2908
3320
  return null;
2909
3321
  }
@@ -2953,13 +3365,18 @@ function daemonIdentityError({ url, home, health, state }) {
2953
3365
  ].join('\n');
2954
3366
  }
2955
3367
 
2956
- async function requestDaemonShutdown(url, daemonId) {
3368
+ async function requestDaemonShutdown(url, daemonId, authToken = null) {
2957
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
+ }
2958
3377
  const response = await fetch(`${url}/api/admin/shutdown`, {
2959
3378
  method: 'POST',
2960
- headers: {
2961
- 'Content-Type': 'application/json',
2962
- },
3379
+ headers,
2963
3380
  body: JSON.stringify({ source: 'ds-launcher', daemon_id: daemonId || null }),
2964
3381
  });
2965
3382
  if (!response.ok) {
@@ -2972,6 +3389,31 @@ async function requestDaemonShutdown(url, daemonId) {
2972
3389
  }
2973
3390
  }
2974
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
+
2975
3417
  function isPidAlive(pid) {
2976
3418
  if (!pid) return false;
2977
3419
  try {
@@ -3005,9 +3447,9 @@ function killManagedProcess(pid, signal) {
3005
3447
  }
3006
3448
  }
3007
3449
 
3008
- async function waitForDaemonStop({ url, pid, attempts = 20, delayMs = 200 }) {
3450
+ async function waitForDaemonStop({ url, pid, authToken = null, attempts = 20, delayMs = 200 }) {
3009
3451
  for (let attempt = 0; attempt < attempts; attempt += 1) {
3010
- const healthy = url ? await isHealthy(url) : false;
3452
+ const healthy = url ? await isHealthy(url, authToken) : false;
3011
3453
  const alive = pid ? isPidAlive(pid) : false;
3012
3454
  if (!healthy && !alive) {
3013
3455
  return true;
@@ -3032,7 +3474,8 @@ async function stopDaemon(home) {
3032
3474
  const state = readDaemonState(home);
3033
3475
  const configured = readConfiguredUiAddressFromFile(home);
3034
3476
  const url = state?.url || browserUiUrl(state?.host || configured.host, state?.port || configured.port);
3035
- const healthBefore = await fetchHealth(url);
3477
+ const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
3478
+ const healthBefore = await fetchHealth(url, authToken);
3036
3479
  const healthyBefore = Boolean(healthBefore && healthBefore.status === 'ok');
3037
3480
  const sameHomeHealthy = healthMatchesHome({ health: healthBefore, home });
3038
3481
  const pid = state?.pid || (sameHomeHealthy ? healthBefore?.pid : null);
@@ -3073,21 +3516,21 @@ async function stopDaemon(home) {
3073
3516
  let stopped = false;
3074
3517
 
3075
3518
  if (healthyBefore) {
3076
- await requestDaemonShutdown(url, shutdownDaemonId || null);
3077
- 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 });
3078
3521
  }
3079
3522
 
3080
3523
  if (!stopped && pid && isPidAlive(pid)) {
3081
3524
  killManagedProcess(pid, 'SIGTERM');
3082
- stopped = await waitForDaemonStop({ url, pid, attempts: 30, delayMs: 200 });
3525
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 30, delayMs: 200 });
3083
3526
  }
3084
3527
 
3085
3528
  if (!stopped && pid && isPidAlive(pid)) {
3086
3529
  killManagedProcess(pid, 'SIGKILL');
3087
- stopped = await waitForDaemonStop({ url, pid, attempts: 20, delayMs: 150 });
3530
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 20, delayMs: 150 });
3088
3531
  }
3089
3532
 
3090
- const stillHealthy = await isHealthy(url);
3533
+ const stillHealthy = await isHealthy(url, authToken);
3091
3534
  if (!stopped && (stillHealthy || (pid && isPidAlive(pid)))) {
3092
3535
  console.error('DeepScientist daemon is still running after shutdown attempts.');
3093
3536
  process.exit(1);
@@ -3362,19 +3805,23 @@ async function performSelfUpdate(home, options = {}) {
3362
3805
  log_path: installResult.logPath,
3363
3806
  };
3364
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
+ }
3365
3823
  spawnDetachedNode(
3366
- [
3367
- launcherPath,
3368
- '--home',
3369
- home,
3370
- '--host',
3371
- String(host),
3372
- '--port',
3373
- String(port),
3374
- '--daemon-only',
3375
- '--no-browser',
3376
- '--skip-update-check',
3377
- ],
3824
+ restartArgs,
3378
3825
  {
3379
3826
  cwd: repoRoot,
3380
3827
  env: process.env,
@@ -3569,17 +4016,88 @@ async function startBackgroundUpdateWorker(home, options = {}) {
3569
4016
  };
3570
4017
  }
3571
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
+
3572
4088
  async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallbackPort) {
3573
4089
  try {
3574
4090
  const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'config'], { capture: true, allowFailure: true });
3575
4091
  const text = result.stdout || '';
3576
4092
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3577
4093
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4094
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3578
4095
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3579
4096
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3580
4097
  return {
3581
4098
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3582
4099
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4100
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3583
4101
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3584
4102
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3585
4103
  };
@@ -3587,6 +4105,7 @@ async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallba
3587
4105
  return {
3588
4106
  host: fallbackHost || '0.0.0.0',
3589
4107
  port: fallbackPort || 20999,
4108
+ authEnabled: false,
3590
4109
  defaultMode: 'web',
3591
4110
  autoOpenBrowser: true,
3592
4111
  };
@@ -3599,6 +4118,7 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3599
4118
  return {
3600
4119
  host: fallbackHost || '0.0.0.0',
3601
4120
  port: fallbackPort || 20999,
4121
+ authEnabled: false,
3602
4122
  defaultMode: 'web',
3603
4123
  autoOpenBrowser: true,
3604
4124
  };
@@ -3607,11 +4127,13 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3607
4127
  const text = fs.readFileSync(configPath, 'utf8');
3608
4128
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3609
4129
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4130
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3610
4131
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3611
4132
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3612
4133
  return {
3613
4134
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3614
4135
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4136
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3615
4137
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3616
4138
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3617
4139
  };
@@ -3619,20 +4141,51 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3619
4141
  return {
3620
4142
  host: fallbackHost || '0.0.0.0',
3621
4143
  port: fallbackPort || 20999,
4144
+ authEnabled: false,
3622
4145
  defaultMode: 'web',
3623
4146
  autoOpenBrowser: true,
3624
4147
  };
3625
4148
  }
3626
4149
  }
3627
4150
 
3628
- async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}) {
4151
+ async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}, authEnabled = false) {
3629
4152
  const browserUrl = browserUiUrl(host, port);
3630
4153
  const daemonBindUrl = bindUiUrl(host, port);
3631
4154
  const state = readDaemonState(home);
3632
- 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() : '');
3633
4159
  if (existingHealth && existingHealth.status === 'ok') {
3634
4160
  if (state && healthMatchesManagedState({ health: existingHealth, state, home })) {
3635
- 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
+ };
3636
4189
  }
3637
4190
  console.error(
3638
4191
  state
@@ -3666,11 +4219,13 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3666
4219
  port,
3667
4220
  proxy,
3668
4221
  envOverrides,
4222
+ authEnabled,
4223
+ authToken: desiredAuthToken,
3669
4224
  });
3670
4225
  const logPath = startedProcess.logPath;
3671
4226
 
3672
4227
  for (let attempt = 0; attempt < 60; attempt += 1) {
3673
- const health = await fetchHealth(browserUrl);
4228
+ const health = await fetchHealth(browserUrl, desiredAuthToken);
3674
4229
  if (health && health.status === 'ok') {
3675
4230
  const liveState = readDaemonState(home);
3676
4231
  if (!healthMatchesManagedState({ health, state: liveState, home })) {
@@ -3689,12 +4244,26 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3689
4244
  if (supervisorPid) {
3690
4245
  appendDaemonSupervisorLog(home, `supervisor started with pid ${supervisorPid}`);
3691
4246
  }
3692
- return { url: browserUrl, bindUrl: daemonBindUrl, reused: false };
4247
+ return {
4248
+ url: launchUrl,
4249
+ bindUrl: bindLaunchUrl,
4250
+ reused: false,
4251
+ authEnabled,
4252
+ authToken: desiredAuthToken,
4253
+ };
3693
4254
  }
3694
4255
  await sleep(250);
3695
4256
  }
3696
4257
 
3697
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
+ }
3698
4267
  const logTail = tailLog(logPath);
3699
4268
  if (logTail) {
3700
4269
  console.error(logTail);
@@ -3778,12 +4347,15 @@ function handleCodexPreflightFailure(error) {
3778
4347
  return true;
3779
4348
  }
3780
4349
 
3781
- function launchTui(url, questId, home, runtimePython) {
4350
+ function launchTui(url, questId, home, runtimePython, authToken = null) {
3782
4351
  const entry = ensureNodeBundle('src/tui', 'dist/index.js');
3783
4352
  const args = [entry, '--base-url', url];
3784
4353
  if (questId) {
3785
4354
  args.push('--quest-id', questId);
3786
4355
  }
4356
+ if (typeof authToken === 'string' && authToken.trim()) {
4357
+ args.push('--auth-token', authToken.trim());
4358
+ }
3787
4359
  const child = spawn(process.execPath, args, {
3788
4360
  cwd: repoRoot,
3789
4361
  stdio: 'inherit',
@@ -3801,14 +4373,15 @@ function launchTui(url, questId, home, runtimePython) {
3801
4373
 
3802
4374
  async function updateMain(rawArgs) {
3803
4375
  const options = parseUpdateArgs(rawArgs);
3804
- if (!options) {
3805
- printUpdateHelp();
3806
- process.exit(1);
3807
- }
3808
4376
  if (options.help) {
3809
4377
  printUpdateHelp();
3810
4378
  process.exit(0);
3811
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
+ }
3812
4385
 
3813
4386
  const home = options.home || resolveHome(rawArgs);
3814
4387
  applyLauncherProxy(options.proxy);
@@ -3923,14 +4496,15 @@ async function updateMain(rawArgs) {
3923
4496
 
3924
4497
  async function migrateMain(rawArgs) {
3925
4498
  const options = parseMigrateArgs(rawArgs);
3926
- if (!options) {
3927
- printMigrateHelp();
3928
- process.exit(1);
3929
- }
3930
4499
  if (options.help) {
3931
4500
  printMigrateHelp();
3932
4501
  process.exit(0);
3933
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
+ }
3934
4508
 
3935
4509
  const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
3936
4510
  const targetHome = path.resolve(options.target);
@@ -4051,13 +4625,15 @@ async function migrateMain(rawArgs) {
4051
4625
 
4052
4626
  async function launcherMain(rawArgs) {
4053
4627
  const options = parseLauncherArgs(rawArgs);
4054
- if (!options) {
4055
- return false;
4056
- }
4057
4628
  if (options.help) {
4058
4629
  printLauncherHelp();
4059
4630
  process.exit(0);
4060
4631
  }
4632
+ if (options.error) {
4633
+ console.error(options.error);
4634
+ console.error('Run `ds --help` for launcher usage.');
4635
+ process.exit(1);
4636
+ }
4061
4637
 
4062
4638
  const home = options.home || resolveHome(rawArgs);
4063
4639
  applyLauncherProxy(options.proxy);
@@ -4075,8 +4651,10 @@ async function launcherMain(rawArgs) {
4075
4651
  if (options.status) {
4076
4652
  const state = readDaemonState(home);
4077
4653
  const configured = readConfiguredUiAddressFromFile(home, options.host, options.port);
4078
- const url = state?.url || browserUiUrl(configured.host, configured.port);
4079
- 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);
4080
4658
  const healthy = Boolean(health && health.status === 'ok');
4081
4659
  const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
4082
4660
  console.log(
@@ -4105,6 +4683,7 @@ async function launcherMain(rawArgs) {
4105
4683
  binary: options.codexBinary,
4106
4684
  });
4107
4685
  ensureInitialized(home, runtimePython);
4686
+ await maybeHandleMiniMaxCodexVersion(home, runtimePython, options);
4108
4687
  if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
4109
4688
  return true;
4110
4689
  }
@@ -4113,20 +4692,27 @@ async function launcherMain(rawArgs) {
4113
4692
  const configuredUi = await readConfiguredUiAddress(home, runtimePython, options.host, options.port);
4114
4693
  const host = configuredUi.host;
4115
4694
  const port = configuredUi.port;
4695
+ const authEnabled = options.auth === null ? false : options.auth !== false;
4116
4696
  const mode = normalizeMode(options.mode ?? 'web');
4117
4697
  const shouldOpenBrowser = options.daemonOnly
4118
4698
  ? false
4119
4699
  : options.openBrowser === null
4120
4700
  ? configuredUi.autoOpenBrowser !== false && mode !== 'tui'
4121
4701
  : options.openBrowser;
4122
- 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) {
4123
4709
  await stopDaemon(home);
4124
4710
  }
4125
4711
 
4126
4712
  step(4, 4, 'Starting local daemon and UI surfaces');
4127
4713
  let started;
4128
4714
  try {
4129
- started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv);
4715
+ started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv, authEnabled);
4130
4716
  } catch (error) {
4131
4717
  if (handleCodexPreflightFailure(error)) return true;
4132
4718
  throw error;
@@ -4142,6 +4728,8 @@ async function launcherMain(rawArgs) {
4142
4728
  home,
4143
4729
  pythonSelection: pythonRuntime.runtimeProbe,
4144
4730
  yolo: options.yolo,
4731
+ authEnabled: started.authEnabled,
4732
+ authToken: started.authToken,
4145
4733
  });
4146
4734
 
4147
4735
  if (options.daemonOnly) {
@@ -4150,12 +4738,16 @@ async function launcherMain(rawArgs) {
4150
4738
  if (mode === 'web') {
4151
4739
  process.exit(0);
4152
4740
  }
4153
- launchTui(started.url, options.questId, home, runtimePython);
4741
+ launchTui(browserUiUrl(host, port), options.questId, home, runtimePython, started.authToken);
4154
4742
  return true;
4155
4743
  }
4156
4744
 
4157
4745
  async function main() {
4158
- 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
+ }
4159
4751
  if (args[0] === '--daemon-supervisor') {
4160
4752
  await daemonSupervisorMain(args.slice(1));
4161
4753
  return;
@@ -4169,7 +4761,11 @@ async function main() {
4169
4761
  await migrateMain(args);
4170
4762
  return;
4171
4763
  }
4172
- 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
+ ) {
4173
4769
  await launcherMain(args);
4174
4770
  return;
4175
4771
  }
@@ -4225,6 +4821,8 @@ module.exports = {
4225
4821
  resolveUvBinary,
4226
4822
  resolveHome,
4227
4823
  parseLauncherArgs,
4824
+ generateBrowserAuthToken,
4825
+ appendBrowserAuthToken,
4228
4826
  normalizeProxyUrl,
4229
4827
  parseMigrateArgs,
4230
4828
  parseLegacyWrapperCandidate,
@@ -4232,12 +4830,14 @@ module.exports = {
4232
4830
  useEditableProjectInstall,
4233
4831
  compareVersions,
4234
4832
  detectInstallMode,
4833
+ buildUvSyncFailureGuidance,
4235
4834
  updateManualCommand,
4236
4835
  buildUpdateStatus,
4237
4836
  parseYesNoAnswer,
4238
4837
  normalizeLauncherRelaunchArgs,
4239
4838
  officialRepositoryLine,
4240
4839
  stripAnsi,
4840
+ normalizeLegacyHostFlagArgs,
4241
4841
  },
4242
4842
  };
4243
4843