@researai/deepscientist 1.5.15 → 1.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
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') {
@@ -112,6 +139,7 @@ Usage:
112
139
  ds update
113
140
  ds update --check
114
141
  ds update --yes
142
+ ds uninstall
115
143
  ds migrate /data/DeepScientist
116
144
  ds --here
117
145
  ds --yolo --port 20999 --here
@@ -130,6 +158,7 @@ Usage:
130
158
  Launcher flags:
131
159
  --host <host> Bind host for the local web daemon
132
160
  --port <port> Bind port for the local web daemon
161
+ --auth [true|false] Require a 16-character local browser password. Default is false
133
162
  --tui Start the terminal workspace only
134
163
  --both Start web + terminal workspace together
135
164
  --no-browser Do not auto-open the browser
@@ -154,6 +183,9 @@ Migration:
154
183
  ds migrate <target> Move the DeepScientist home/install root to a new absolute path
155
184
  ds migrate <target> --yes --restart
156
185
 
186
+ Uninstall:
187
+ ds uninstall Remove code/runtime only and preserve local data
188
+
157
189
  Runtime:
158
190
  DeepScientist uses uv to manage a locked local Python runtime.
159
191
  If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
@@ -192,6 +224,47 @@ function normalizeProxyUrl(rawValue) {
192
224
  return value || null;
193
225
  }
194
226
 
227
+ function normalizeLegacyHostFlagArgs(argv) {
228
+ const args = [];
229
+ let warned = false;
230
+ let legacyValue = null;
231
+
232
+ for (let index = 0; index < argv.length; index += 1) {
233
+ const arg = argv[index];
234
+ if (arg === '--ip') {
235
+ warned = true;
236
+ legacyValue = argv[index + 1] || legacyValue;
237
+ args.push('--host');
238
+ if (argv[index + 1]) {
239
+ args.push(argv[index + 1]);
240
+ index += 1;
241
+ }
242
+ continue;
243
+ }
244
+ if (typeof arg === 'string' && arg.startsWith('--ip=')) {
245
+ warned = true;
246
+ legacyValue = arg.slice('--ip='.length) || legacyValue;
247
+ args.push('--host', arg.slice('--ip='.length));
248
+ continue;
249
+ }
250
+ args.push(arg);
251
+ }
252
+
253
+ if (!warned) {
254
+ return { args, warnings: [] };
255
+ }
256
+
257
+ const normalizedValue = String(legacyValue || '').trim();
258
+ const bindHint =
259
+ normalizedValue && ['0.0.0.0', '::', '[::]'].includes(normalizedValue)
260
+ ? ' Note: bind-all addresses such as 0.0.0.0 are valid for `--host`, but local browser access still uses 127.0.0.1.'
261
+ : '';
262
+ return {
263
+ args,
264
+ warnings: [`Launcher note: \`--ip\` is deprecated. Use \`--host\` instead.${bindHint}`],
265
+ };
266
+ }
267
+
195
268
  function applyLauncherProxy(proxyUrl) {
196
269
  const normalized = normalizeProxyUrl(proxyUrl);
197
270
  if (!normalized) {
@@ -562,6 +635,20 @@ function bindUiUrl(host, port) {
562
635
  return `http://${formatHttpHost(normalized)}:${port}`;
563
636
  }
564
637
 
638
+ function generateBrowserAuthToken() {
639
+ return crypto.randomBytes(8).toString('hex');
640
+ }
641
+
642
+ function appendBrowserAuthToken(url, authToken) {
643
+ const normalized = typeof authToken === 'string' ? authToken.trim() : '';
644
+ if (!normalized) {
645
+ return url;
646
+ }
647
+ const target = new URL(url);
648
+ target.searchParams.set('token', normalized);
649
+ return target.toString();
650
+ }
651
+
565
652
  function normalizeMode(value) {
566
653
  const normalized = String(value || '')
567
654
  .trim()
@@ -588,6 +675,66 @@ function parseBooleanSetting(rawValue, fallback = false) {
588
675
  return fallback;
589
676
  }
590
677
 
678
+ function shouldCompileRuntimeBytecode() {
679
+ return parseBooleanSetting(process.env.DEEPSCIENTIST_RUNTIME_COMPILE_BYTECODE, false);
680
+ }
681
+
682
+ function readRequiredOptionValue(args, index, optionName) {
683
+ const value = args[index + 1];
684
+ if (!value || String(value).startsWith('--')) {
685
+ return {
686
+ ok: false,
687
+ error: `Missing value for ${optionName}.`,
688
+ };
689
+ }
690
+ return {
691
+ ok: true,
692
+ value,
693
+ };
694
+ }
695
+
696
+ function parseStrictBooleanOption(rawValue, optionName) {
697
+ const parsed = parseBooleanFlagValue(rawValue);
698
+ if (parsed === null) {
699
+ return {
700
+ ok: false,
701
+ error: `Invalid value for ${optionName}: ${rawValue}. Use true or false.`,
702
+ };
703
+ }
704
+ return {
705
+ ok: true,
706
+ value: parsed,
707
+ };
708
+ }
709
+
710
+ function parseStrictPortOption(rawValue, optionName) {
711
+ const port = Number(rawValue);
712
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
713
+ return {
714
+ ok: false,
715
+ error: `Invalid value for ${optionName}: ${rawValue}. Expected an integer between 1 and 65535.`,
716
+ };
717
+ }
718
+ return {
719
+ ok: true,
720
+ value: port,
721
+ };
722
+ }
723
+
724
+ function parseStrictModeOption(rawValue, optionName) {
725
+ const normalized = String(rawValue || '').trim().toLowerCase();
726
+ if (!['web', 'tui', 'both'].includes(normalized)) {
727
+ return {
728
+ ok: false,
729
+ error: `Invalid value for ${optionName}: ${rawValue}. Expected one of: web, tui, both.`,
730
+ };
731
+ }
732
+ return {
733
+ ok: true,
734
+ value: normalized,
735
+ };
736
+ }
737
+
591
738
  function supportsAnsi() {
592
739
  return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb');
593
740
  }
@@ -623,6 +770,64 @@ function colorize(code, text) {
623
770
  return `${code}${text}\u001B[0m`;
624
771
  }
625
772
 
773
+ function readCodexProviderMetadata(configDir, profile) {
774
+ const normalizedProfile = String(profile || '').trim();
775
+ const expandedDir = expandUserPath(configDir || path.join(os.homedir(), '.codex'));
776
+ const configPath = path.join(expandedDir, 'config.toml');
777
+ if (!normalizedProfile || !fs.existsSync(configPath)) {
778
+ return {
779
+ provider: null,
780
+ model: null,
781
+ envKey: null,
782
+ baseUrl: null,
783
+ wireApi: null,
784
+ requiresOpenAiAuth: null,
785
+ };
786
+ }
787
+ const text = fs.readFileSync(configPath, 'utf8');
788
+ const profileBlock = text.match(new RegExp(`\\[profiles\\.${normalizedProfile.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]([\\s\\S]*?)(?:\\n\\[|$)`));
789
+ const provider = profileBlock?.[1]?.match(/^\s*model_provider\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || text.match(/^\s*model_provider\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
790
+ const model = profileBlock?.[1]?.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || text.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
791
+ if (!provider) {
792
+ return {
793
+ provider: null,
794
+ model,
795
+ envKey: null,
796
+ baseUrl: null,
797
+ wireApi: null,
798
+ requiresOpenAiAuth: null,
799
+ };
800
+ }
801
+ const providerBlock = text.match(new RegExp(`\\[model_providers\\.${provider.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]([\\s\\S]*?)(?:\\n\\[|$)`));
802
+ const providerText = providerBlock?.[1] || '';
803
+ const envKey = providerText.match(/^\s*env_key\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
804
+ const baseUrl = providerText.match(/^\s*base_url\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
805
+ const wireApi = providerText.match(/^\s*wire_api\s*=\s*["']([^"']+)["']/m)?.[1]?.trim() || null;
806
+ const requiresOpenAiAuthRaw = providerText.match(/^\s*requires_openai_auth\s*=\s*(true|false)\s*$/m)?.[1] || null;
807
+ const requiresOpenAiAuth = requiresOpenAiAuthRaw === null ? null : requiresOpenAiAuthRaw === 'true';
808
+ return {
809
+ provider,
810
+ model,
811
+ envKey,
812
+ baseUrl,
813
+ wireApi,
814
+ requiresOpenAiAuth,
815
+ };
816
+ }
817
+
818
+ function installedCodexCliVersion(binaryPath) {
819
+ const resolved = resolveExecutableOnPath(binaryPath || 'codex') || binaryPath || 'codex';
820
+ try {
821
+ const result = spawnSync(resolved, ['--version'], syncSpawnOptions({ encoding: 'utf8' }));
822
+ if (result.status !== 0) {
823
+ return null;
824
+ }
825
+ return parseCodexCliVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+
626
831
  const OFFICIAL_REPOSITORY_URL = 'https://github.com/ResearAI/DeepScientist';
627
832
 
628
833
  function officialRepositoryLine() {
@@ -685,7 +890,7 @@ function pythonVersionText(probe) {
685
890
  return version;
686
891
  }
687
892
 
688
- function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
893
+ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken }) {
689
894
  const runtimeRows = [
690
895
  ['Version', packageJson.version],
691
896
  ['Home', truncateMiddle(home)],
@@ -694,6 +899,9 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
694
899
  ['Python', truncateMiddle(pythonVersionText(pythonSelection))],
695
900
  ['Codex mode', yolo ? 'YOLO (never + danger-full-access)' : 'Default (on-request + workspace-write)'],
696
901
  ];
902
+ if (authEnabled && authToken) {
903
+ runtimeRows.splice(4, 0, ['Auth token', authToken]);
904
+ }
697
905
  if (pythonSelection && pythonSelection.sourceLabel) {
698
906
  runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
699
907
  }
@@ -706,6 +914,7 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo }) {
706
914
  ['ds --yolo --port 20999 --here', 'Start in ./DeepScientist under the current directory with YOLO Codex access'],
707
915
  ['ds --port 21000', 'Change the web port'],
708
916
  ['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
917
+ ['ds --auth true', 'Enable the local browser password for this launch'],
709
918
  ['ds --here', 'Use ./DeepScientist under the current directory as home'],
710
919
  ['ds --both', 'Start web + TUI together'],
711
920
  ['ds --tui', 'Start the terminal workspace only'],
@@ -729,6 +938,8 @@ function printLaunchCard({
729
938
  home,
730
939
  pythonSelection,
731
940
  yolo,
941
+ authEnabled,
942
+ authToken,
732
943
  }) {
733
944
  const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
734
945
  const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
@@ -787,13 +998,18 @@ function printLaunchCard({
787
998
  console.log(centerText(colorize('\u001B[1m', workspaceMode), width));
788
999
  console.log(centerText(urlLabel, width));
789
1000
  console.log(centerText(divider, width));
1001
+ if (authEnabled && authToken) {
1002
+ console.log('');
1003
+ console.log(centerText(colorize('\u001B[1;38;5;214m', authToken), width));
1004
+ console.log('');
1005
+ }
790
1006
  console.log(centerText(browserLine, width));
791
1007
  console.log(centerText(nextStep, width));
792
1008
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
793
1009
  console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
794
1010
  console.log(centerText(officialRepositoryLine(), width));
795
1011
  console.log('');
796
- renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo });
1012
+ renderLaunchHints({ home, url, bindUrl, pythonSelection, yolo, authEnabled, authToken });
797
1013
  }
798
1014
 
799
1015
  function escapeHtml(value) {
@@ -816,10 +1032,10 @@ function writeCodexPreflightReport(home, probe) {
816
1032
  const profile = typeof details.profile === 'string' ? details.profile.trim() : '';
817
1033
  const intro = profile
818
1034
  ? `DeepScientist blocked startup because the Codex hello probe did not pass for profile \`${profile}\`. Verify that \`codex --profile ${profile}\` works on this machine and that the profile's provider-specific API key, Base URL, and model configuration are already set up.`
819
- : 'DeepScientist blocked startup because the Codex hello probe did not pass. In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency. If `codex` is still missing, repair it with `npm install -g @openai/codex`. Then run `codex --login` (or `codex`), finish authentication, run `ds doctor`, and launch `ds` again.';
1035
+ : 'DeepScientist blocked startup because the Codex hello probe did not pass. In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency. If `codex` is still missing, repair it with `npm install -g @openai/codex`. Then run `codex login` (or just `codex`), finish authentication, run `ds doctor`, and launch `ds` again.';
820
1036
  const introZh = profile
821
1037
  ? `DeepScientist 启动前进行了 Codex 可用性检查,但 profile \`${profile}\` 的 hello 探测没有通过。请先确认 \`codex --profile ${profile}\` 在当前机器上可以正常启动,并确保该 profile 依赖的 provider API Key、Base URL 和模型配置都已经在 Codex 中配置好。`
822
- : 'DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,`npm install -g @researai/deepscientist` 也会一并安装 bundled Codex 依赖;如果此后 `codex` 仍不可用,请再执行 `npm install -g @openai/codex` 修复。然后运行 `codex --login`(或 `codex`)完成认证,再执行 `ds doctor`,最后重新启动 `ds`。';
1038
+ : 'DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,`npm install -g @researai/deepscientist` 也会一并安装 bundled Codex 依赖;如果此后 `codex` 仍不可用,请再执行 `npm install -g @openai/codex` 修复。然后运行 `codex login`(或直接运行 `codex`)完成认证,再执行 `ds doctor`,最后重新启动 `ds`。';
823
1039
  const renderItems = (items, tone) =>
824
1040
  items
825
1041
  .map(
@@ -1020,6 +1236,7 @@ function parseLauncherArgs(argv) {
1020
1236
  let daemonOnly = false;
1021
1237
  let skipUpdateCheck = false;
1022
1238
  let yolo = true;
1239
+ let auth = null;
1023
1240
  let codexProfile = null;
1024
1241
  let codexBinary = null;
1025
1242
 
@@ -1040,21 +1257,71 @@ function parseLauncherArgs(argv) {
1040
1257
  else if (arg === '--open-browser') openBrowser = true;
1041
1258
  else if (arg === '--daemon-only') daemonOnly = true;
1042
1259
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
1260
+ else if (arg === '--here') continue;
1043
1261
  else {
1044
1262
  const parsedYolo = parseYoloArg(args, index, yolo);
1045
1263
  if (parsedYolo.matched) {
1046
1264
  yolo = parsedYolo.value;
1047
1265
  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]);
1266
+ } else if (arg === '--auth') {
1267
+ const next = readRequiredOptionValue(args, index, '--auth');
1268
+ if (!next.ok) return { help: false, error: next.error };
1269
+ const parsed = parseStrictBooleanOption(next.value, '--auth');
1270
+ if (!parsed.ok) return { help: false, error: parsed.error };
1271
+ auth = parsed.value;
1272
+ index += 1;
1273
+ } else if (typeof arg === 'string' && arg.startsWith('--auth=')) {
1274
+ const parsed = parseStrictBooleanOption(arg.slice('--auth='.length), '--auth');
1275
+ if (!parsed.ok) return { help: false, error: parsed.error };
1276
+ auth = parsed.value;
1277
+ } else if (arg === '--codex-profile') {
1278
+ const next = readRequiredOptionValue(args, index, '--codex-profile');
1279
+ if (!next.ok) return { help: false, error: next.error };
1280
+ codexProfile = next.value;
1281
+ index += 1;
1282
+ } else if (arg === '--codex') {
1283
+ const next = readRequiredOptionValue(args, index, '--codex');
1284
+ if (!next.ok) return { help: false, error: next.error };
1285
+ codexBinary = next.value;
1286
+ index += 1;
1287
+ } else if (arg === '--host') {
1288
+ const next = readRequiredOptionValue(args, index, '--host');
1289
+ if (!next.ok) return { help: false, error: next.error };
1290
+ host = next.value;
1291
+ index += 1;
1292
+ } else if (arg === '--port') {
1293
+ const next = readRequiredOptionValue(args, index, '--port');
1294
+ if (!next.ok) return { help: false, error: next.error };
1295
+ const parsed = parseStrictPortOption(next.value, '--port');
1296
+ if (!parsed.ok) return { help: false, error: parsed.error };
1297
+ port = parsed.value;
1298
+ index += 1;
1299
+ } else if (arg === '--home') {
1300
+ const next = readRequiredOptionValue(args, index, '--home');
1301
+ if (!next.ok) return { help: false, error: next.error };
1302
+ home = path.resolve(next.value);
1303
+ index += 1;
1304
+ } else if (arg === '--proxy') {
1305
+ const next = readRequiredOptionValue(args, index, '--proxy');
1306
+ if (!next.ok) return { help: false, error: next.error };
1307
+ proxy = next.value;
1308
+ index += 1;
1309
+ } else if (arg === '--quest-id') {
1310
+ const next = readRequiredOptionValue(args, index, '--quest-id');
1311
+ if (!next.ok) return { help: false, error: next.error };
1312
+ questId = next.value;
1313
+ index += 1;
1314
+ } else if (arg === '--mode') {
1315
+ const next = readRequiredOptionValue(args, index, '--mode');
1316
+ if (!next.ok) return { help: false, error: next.error };
1317
+ const parsed = parseStrictModeOption(next.value, '--mode');
1318
+ if (!parsed.ok) return { help: false, error: parsed.error };
1319
+ mode = parsed.value;
1320
+ index += 1;
1321
+ }
1056
1322
  else if (arg === '--help' || arg === '-h') return { help: true };
1057
- else if (!arg.startsWith('--')) return null;
1323
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown launcher flag: ${arg}` };
1324
+ else return { help: false, error: `Unexpected launcher argument: ${arg}` };
1058
1325
  }
1059
1326
  }
1060
1327
 
@@ -1073,8 +1340,10 @@ function parseLauncherArgs(argv) {
1073
1340
  daemonOnly,
1074
1341
  skipUpdateCheck,
1075
1342
  yolo,
1343
+ auth,
1076
1344
  codexProfile,
1077
1345
  codexBinary,
1346
+ error: null,
1078
1347
  };
1079
1348
  }
1080
1349
 
@@ -1116,6 +1385,26 @@ Flags:
1116
1385
  `);
1117
1386
  }
1118
1387
 
1388
+ function printUninstallHelp() {
1389
+ console.log(`DeepScientist uninstall
1390
+
1391
+ Usage:
1392
+ ds uninstall
1393
+ ds uninstall --home /absolute/home/path
1394
+ ds uninstall --yes
1395
+
1396
+ Behavior:
1397
+ - removes DeepScientist code, launcher wrappers, and local runtime code
1398
+ - preserves local data such as quests, memory, config, logs, plugins, and cache
1399
+ - if this command is run from the globally installed npm package, it also removes the npm package itself
1400
+
1401
+ Flags:
1402
+ --yes Skip the interactive confirmation prompt
1403
+ --home <path> Override the target DeepScientist home/root
1404
+ --origin <value> Internal use for npm uninstall integration
1405
+ `);
1406
+ }
1407
+
1119
1408
  function parseUpdateArgs(argv) {
1120
1409
  const args = [...argv];
1121
1410
  if (args[0] === 'update') {
@@ -1148,12 +1437,32 @@ function parseUpdateArgs(argv) {
1148
1437
  else if (arg === '--worker') worker = true;
1149
1438
  else if (arg === '--restart-daemon') restartDaemon = true;
1150
1439
  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];
1440
+ else if (arg === '--home') {
1441
+ const next = readRequiredOptionValue(args, index, '--home');
1442
+ if (!next.ok) return { help: false, error: next.error };
1443
+ home = path.resolve(next.value);
1444
+ index += 1;
1445
+ } else if (arg === '--host') {
1446
+ const next = readRequiredOptionValue(args, index, '--host');
1447
+ if (!next.ok) return { help: false, error: next.error };
1448
+ host = next.value;
1449
+ index += 1;
1450
+ } else if (arg === '--port') {
1451
+ const next = readRequiredOptionValue(args, index, '--port');
1452
+ if (!next.ok) return { help: false, error: next.error };
1453
+ const parsed = parseStrictPortOption(next.value, '--port');
1454
+ if (!parsed.ok) return { help: false, error: parsed.error };
1455
+ port = parsed.value;
1456
+ index += 1;
1457
+ } else if (arg === '--proxy') {
1458
+ const next = readRequiredOptionValue(args, index, '--proxy');
1459
+ if (!next.ok) return { help: false, error: next.error };
1460
+ proxy = next.value;
1461
+ index += 1;
1462
+ }
1155
1463
  else if (arg === '--help' || arg === '-h') return { help: true };
1156
- else if (!arg.startsWith('--')) return null;
1464
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown update flag: ${arg}` };
1465
+ else return { help: false, error: `Unexpected update argument: ${arg}` };
1157
1466
  }
1158
1467
 
1159
1468
  return {
@@ -1172,6 +1481,7 @@ function parseUpdateArgs(argv) {
1172
1481
  proxy,
1173
1482
  restartDaemon,
1174
1483
  skipUpdateCheck,
1484
+ error: null,
1175
1485
  };
1176
1486
  }
1177
1487
 
@@ -1189,15 +1499,23 @@ function parseMigrateArgs(argv) {
1189
1499
  const arg = args[index];
1190
1500
  if (arg === '--yes') yes = true;
1191
1501
  else if (arg === '--restart') restart = true;
1192
- else if (arg === '--home' && args[index + 1]) home = path.resolve(expandUserPath(args[++index]));
1502
+ else if (arg === '--home') {
1503
+ const next = readRequiredOptionValue(args, index, '--home');
1504
+ if (!next.ok) return { help: false, error: next.error };
1505
+ home = path.resolve(expandUserPath(next.value));
1506
+ index += 1;
1507
+ }
1193
1508
  else if (arg === '--help' || arg === '-h') return { help: true };
1194
- else if (arg.startsWith('--')) return null;
1509
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown migrate flag: ${arg}` };
1195
1510
  else if (!target) target = path.resolve(expandUserPath(arg));
1196
- else return null;
1511
+ else return { help: false, error: `Unexpected migrate argument: ${arg}` };
1197
1512
  }
1198
1513
 
1199
1514
  if (!target) {
1200
- return null;
1515
+ return {
1516
+ help: false,
1517
+ error: 'Missing migration target path.',
1518
+ };
1201
1519
  }
1202
1520
 
1203
1521
  return {
@@ -1206,6 +1524,44 @@ function parseMigrateArgs(argv) {
1206
1524
  target,
1207
1525
  yes,
1208
1526
  restart,
1527
+ error: null,
1528
+ };
1529
+ }
1530
+
1531
+ function parseUninstallArgs(argv) {
1532
+ const args = [...argv];
1533
+ if (args[0] === 'uninstall') {
1534
+ args.shift();
1535
+ }
1536
+ let home = null;
1537
+ let yes = false;
1538
+ let origin = null;
1539
+
1540
+ for (let index = 0; index < args.length; index += 1) {
1541
+ const arg = args[index];
1542
+ if (arg === '--yes') yes = true;
1543
+ else if (arg === '--home') {
1544
+ const next = readRequiredOptionValue(args, index, '--home');
1545
+ if (!next.ok) return { help: false, error: next.error };
1546
+ home = path.resolve(expandUserPath(next.value));
1547
+ index += 1;
1548
+ } else if (arg === '--origin') {
1549
+ const next = readRequiredOptionValue(args, index, '--origin');
1550
+ if (!next.ok) return { help: false, error: next.error };
1551
+ origin = String(next.value || '').trim().toLowerCase() || null;
1552
+ index += 1;
1553
+ }
1554
+ else if (arg === '--help' || arg === '-h') return { help: true };
1555
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown uninstall flag: ${arg}` };
1556
+ else return { help: false, error: `Unexpected uninstall argument: ${arg}` };
1557
+ }
1558
+
1559
+ return {
1560
+ help: false,
1561
+ home,
1562
+ yes,
1563
+ origin,
1564
+ error: null,
1209
1565
  };
1210
1566
  }
1211
1567
 
@@ -2152,13 +2508,34 @@ function ensureUvBinary(home) {
2152
2508
  }
2153
2509
 
2154
2510
  function buildUvRuntimeEnv(home, extraEnv = {}) {
2155
- return {
2511
+ const env = {
2156
2512
  ...process.env,
2157
2513
  UV_CACHE_DIR: runtimeUvCachePath(home),
2158
2514
  UV_PROJECT_ENVIRONMENT: runtimePythonEnvPath(home),
2159
2515
  UV_PYTHON_INSTALL_DIR: runtimeUvPythonInstallPath(home),
2160
2516
  ...extraEnv,
2161
2517
  };
2518
+ for (const key of ['PYTHONPATH', 'PYTHONHOME', 'VIRTUAL_ENV', '__PYVENV_LAUNCHER__']) {
2519
+ delete env[key];
2520
+ }
2521
+ for (const key of Object.keys(env)) {
2522
+ if (key === 'CONDA_EXE' || key === 'CONDA_PYTHON_EXE' || key === '_CE_CONDA' || key === '_CE_M') {
2523
+ delete env[key];
2524
+ continue;
2525
+ }
2526
+ if (key === 'MAMBA_EXE' || key === 'MAMBA_ROOT_PREFIX') {
2527
+ delete env[key];
2528
+ continue;
2529
+ }
2530
+ if (key === 'CONDA_PREFIX' || key === 'CONDA_DEFAULT_ENV' || key === 'CONDA_PROMPT_MODIFIER' || key === 'CONDA_SHLVL') {
2531
+ delete env[key];
2532
+ continue;
2533
+ }
2534
+ if (/^CONDA_PREFIX_\d+$/.test(key)) {
2535
+ delete env[key];
2536
+ }
2537
+ }
2538
+ return env;
2162
2539
  }
2163
2540
 
2164
2541
  function ensureUvLockPresent() {
@@ -2171,6 +2548,43 @@ function ensureUvLockPresent() {
2171
2548
  process.exit(1);
2172
2549
  }
2173
2550
 
2551
+ function buildUvSyncFailureGuidance({ installMode = detectInstallMode(repoRoot), env = process.env } = {}) {
2552
+ const guidance = [];
2553
+ if (installMode === 'source-checkout') {
2554
+ guidance.push('If you changed Python dependencies in a source checkout, run `uv lock` and try again.');
2555
+ } else {
2556
+ 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.');
2557
+ guidance.push('Re-run `ds` in a clean shell first. If you have an active conda or virtualenv, try deactivating it before starting DeepScientist.');
2558
+ }
2559
+
2560
+ const hasPythonEnv =
2561
+ Boolean(String(env.VIRTUAL_ENV || '').trim())
2562
+ || Boolean(String(env.CONDA_PREFIX || '').trim())
2563
+ || Boolean(String(env.PYTHONPATH || '').trim())
2564
+ || Boolean(String(env.PYTHONHOME || '').trim());
2565
+ if (hasPythonEnv) {
2566
+ guidance.push('An active Python environment was detected. `VIRTUAL_ENV`, `CONDA_PREFIX`, `PYTHONPATH`, or `PYTHONHOME` can interfere with uv runtime bootstrap.');
2567
+ }
2568
+
2569
+ const hasCustomIndex =
2570
+ Object.keys(env).some((key) => /^PIP_/i.test(key))
2571
+ || Boolean(String(env.UV_INDEX_URL || '').trim())
2572
+ || Boolean(String(env.UV_EXTRA_INDEX_URL || '').trim());
2573
+ if (hasCustomIndex) {
2574
+ guidance.push('Custom package index settings were detected. Check `PIP_*`, `UV_INDEX_URL`, or `UV_EXTRA_INDEX_URL` if uv could not download packages.');
2575
+ }
2576
+
2577
+ const hasProxyOrCert =
2578
+ ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'SSL_CERT_FILE', 'REQUESTS_CA_BUNDLE']
2579
+ .some((key) => Boolean(String(env[key] || '').trim()));
2580
+ if (hasProxyOrCert) {
2581
+ guidance.push('Proxy or certificate overrides were detected. If uv reported TLS, certificate, or download errors above, verify those settings and try again.');
2582
+ }
2583
+
2584
+ guidance.push('Look at the uv error printed above this message. That original uv output is the real failure reason.');
2585
+ return guidance;
2586
+ }
2587
+
2174
2588
  function resolveUvVersion(uvBinary) {
2175
2589
  const result = runSync(uvBinary, ['--version'], { capture: true, allowFailure: true });
2176
2590
  if (result.status !== 0) {
@@ -2237,7 +2651,10 @@ function resolveBackgroundPythonExecutable(runtimePython) {
2237
2651
  }
2238
2652
 
2239
2653
  function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2240
- const args = ['sync', '--frozen', '--no-dev', '--compile-bytecode', '--python', pythonTarget];
2654
+ const args = ['sync', '--frozen', '--no-dev', '--python', pythonTarget];
2655
+ if (shouldCompileRuntimeBytecode()) {
2656
+ args.splice(3, 0, '--compile-bytecode');
2657
+ }
2241
2658
  if (!editable) {
2242
2659
  args.push('--no-editable');
2243
2660
  }
@@ -2250,7 +2667,9 @@ function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
2250
2667
  return;
2251
2668
  }
2252
2669
  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.');
2670
+ for (const line of buildUvSyncFailureGuidance()) {
2671
+ console.error(line);
2672
+ }
2254
2673
  process.exit(result.status ?? 1);
2255
2674
  }
2256
2675
 
@@ -2629,6 +3048,374 @@ function removeDaemonState(home) {
2629
3048
  }
2630
3049
  }
2631
3050
 
3051
+ function installIndexPath() {
3052
+ return path.join(os.homedir(), '.deepscientist', 'install-index.json');
3053
+ }
3054
+
3055
+ function readInstallIndex() {
3056
+ const indexPath = installIndexPath();
3057
+ if (!fs.existsSync(indexPath)) {
3058
+ return { installs: [] };
3059
+ }
3060
+ try {
3061
+ const payload = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3062
+ const installs = Array.isArray(payload?.installs) ? payload.installs.filter((item) => item && typeof item === 'object') : [];
3063
+ return { installs };
3064
+ } catch {
3065
+ return { installs: [] };
3066
+ }
3067
+ }
3068
+
3069
+ function writeInstallIndex(payload) {
3070
+ const normalized = {
3071
+ installs: Array.isArray(payload?.installs) ? payload.installs : [],
3072
+ };
3073
+ const targetPath = installIndexPath();
3074
+ ensureDir(path.dirname(targetPath));
3075
+ fs.writeFileSync(targetPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
3076
+ }
3077
+
3078
+ function normalizeInstallRecord(record) {
3079
+ const normalizedHome = normalizeHomePath(record?.home || '');
3080
+ if (!normalizedHome) {
3081
+ return null;
3082
+ }
3083
+ const installMode = String(record?.install_mode || '').trim() || null;
3084
+ const installDir = record?.install_dir ? normalizeHomePath(record.install_dir) : null;
3085
+ const packageRoot = record?.package_root ? normalizeHomePath(record.package_root) : null;
3086
+ const launcherPath = record?.launcher_path ? path.resolve(String(record.launcher_path)) : null;
3087
+ const wrapperPaths = Array.isArray(record?.wrapper_paths)
3088
+ ? [...new Set(record.wrapper_paths.map((item) => String(item || '').trim()).filter(Boolean).map((item) => path.resolve(item)))]
3089
+ : [];
3090
+ const createdAt = String(record?.created_at || '').trim() || new Date().toISOString();
3091
+ return {
3092
+ home: normalizedHome,
3093
+ install_mode: installMode,
3094
+ install_dir: installDir,
3095
+ package_root: packageRoot,
3096
+ launcher_path: launcherPath,
3097
+ wrapper_paths: wrapperPaths,
3098
+ created_at: createdAt,
3099
+ updated_at: new Date().toISOString(),
3100
+ };
3101
+ }
3102
+
3103
+ function installRecordMatches(left, right) {
3104
+ return left.home === right.home
3105
+ && (left.install_dir || null) === (right.install_dir || null)
3106
+ && (left.package_root || null) === (right.package_root || null)
3107
+ && (left.install_mode || null) === (right.install_mode || null);
3108
+ }
3109
+
3110
+ function upsertInstallRecord(record) {
3111
+ const normalized = normalizeInstallRecord(record);
3112
+ if (!normalized) {
3113
+ return null;
3114
+ }
3115
+ const index = readInstallIndex();
3116
+ const installs = index.installs
3117
+ .map((item) => normalizeInstallRecord(item))
3118
+ .filter(Boolean);
3119
+ const nextInstalls = installs.filter((item) => !installRecordMatches(item, normalized));
3120
+ nextInstalls.push(normalized);
3121
+ nextInstalls.sort((left, right) => String(left.updated_at || '').localeCompare(String(right.updated_at || '')));
3122
+ writeInstallIndex({ installs: nextInstalls });
3123
+ return normalized;
3124
+ }
3125
+
3126
+ function removeInstallRecords(predicate) {
3127
+ const index = readInstallIndex();
3128
+ const installs = index.installs
3129
+ .map((item) => normalizeInstallRecord(item))
3130
+ .filter(Boolean);
3131
+ const nextInstalls = installs.filter((item) => !predicate(item));
3132
+ writeInstallIndex({ installs: nextInstalls });
3133
+ return nextInstalls;
3134
+ }
3135
+
3136
+ function parseManagedWrapperCandidate(candidatePath) {
3137
+ let stat = null;
3138
+ try {
3139
+ stat = fs.lstatSync(candidatePath);
3140
+ } catch {
3141
+ return null;
3142
+ }
3143
+
3144
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
3145
+ return null;
3146
+ }
3147
+
3148
+ let text = '';
3149
+ try {
3150
+ text = fs.readFileSync(candidatePath, 'utf8');
3151
+ } catch {
3152
+ return null;
3153
+ }
3154
+
3155
+ const homeMatch = text.match(/export DEEPSCIENTIST_HOME="([^"\n]+)"/);
3156
+ const execMatch = text.match(/exec "([^"\n]+)" "\$@"/);
3157
+ return {
3158
+ path: path.resolve(candidatePath),
3159
+ home: homeMatch ? normalizeHomePath(homeMatch[1]) : null,
3160
+ execPath: execMatch ? path.resolve(execMatch[1]) : null,
3161
+ };
3162
+ }
3163
+
3164
+ function collectManagedWrapperPaths({ home, installDir = null, explicitWrapperPaths = [] }) {
3165
+ const normalizedHome = normalizeHomePath(home);
3166
+ const normalizedInstallDir = installDir ? normalizeHomePath(installDir) : null;
3167
+ const candidates = new Set(explicitWrapperPaths.map((item) => path.resolve(String(item))));
3168
+ for (const commandName of launcherWrapperCommands) {
3169
+ for (const candidate of candidateWrapperPathsForCommand(commandName)) {
3170
+ candidates.add(candidate);
3171
+ }
3172
+ }
3173
+ const matched = [];
3174
+ for (const candidate of candidates) {
3175
+ const parsed = parseManagedWrapperCandidate(candidate);
3176
+ if (!parsed) {
3177
+ continue;
3178
+ }
3179
+ if (parsed.home && parsed.home === normalizedHome) {
3180
+ matched.push(parsed.path);
3181
+ continue;
3182
+ }
3183
+ if (normalizedInstallDir && parsed.execPath && parsed.execPath.startsWith(path.join(normalizedInstallDir, 'bin'))) {
3184
+ matched.push(parsed.path);
3185
+ }
3186
+ }
3187
+ return [...new Set(matched)].sort();
3188
+ }
3189
+
3190
+ function buildCodeOnlyUninstallPlan({ home, installDir = null, wrapperPaths = [] }) {
3191
+ const normalizedHome = normalizeHomePath(home);
3192
+ const normalizedInstallDir = installDir ? normalizeHomePath(installDir) : null;
3193
+ const removePaths = [
3194
+ path.join(normalizedHome, 'runtime', 'python-env'),
3195
+ path.join(normalizedHome, 'runtime', 'python'),
3196
+ path.join(normalizedHome, 'runtime', 'tools'),
3197
+ path.join(normalizedHome, 'runtime', 'bundle'),
3198
+ path.join(normalizedHome, 'runtime', 'daemon.json'),
3199
+ ];
3200
+ if (normalizedInstallDir && normalizedInstallDir !== normalizeHomePath(repoRoot)) {
3201
+ removePaths.push(normalizedInstallDir);
3202
+ }
3203
+ return {
3204
+ remove_paths: [...new Set(removePaths.map((item) => path.resolve(item)))].sort(),
3205
+ preserve_paths: [
3206
+ path.join(normalizedHome, 'quests'),
3207
+ path.join(normalizedHome, 'memory'),
3208
+ path.join(normalizedHome, 'config'),
3209
+ path.join(normalizedHome, 'logs'),
3210
+ path.join(normalizedHome, 'plugins'),
3211
+ path.join(normalizedHome, 'cache'),
3212
+ ].sort(),
3213
+ wrapper_paths: [...new Set(wrapperPaths.map((item) => path.resolve(item)))].sort(),
3214
+ };
3215
+ }
3216
+
3217
+ function removePathEntry(targetPath) {
3218
+ if (!targetPath || !fs.existsSync(targetPath)) {
3219
+ return false;
3220
+ }
3221
+ const stat = fs.lstatSync(targetPath);
3222
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
3223
+ fs.rmSync(targetPath, { recursive: true, force: true });
3224
+ return true;
3225
+ }
3226
+ fs.rmSync(targetPath, { force: true });
3227
+ return true;
3228
+ }
3229
+
3230
+ function currentInstallRecord(home) {
3231
+ const installMode = detectInstallMode(repoRoot);
3232
+ return normalizeInstallRecord({
3233
+ home,
3234
+ install_mode: installMode,
3235
+ install_dir: installMode === 'source-checkout' ? null : null,
3236
+ package_root: normalizeHomePath(repoRoot),
3237
+ launcher_path: resolveLauncherPath() || path.join(repoRoot, 'bin', 'ds.js'),
3238
+ wrapper_paths: [],
3239
+ });
3240
+ }
3241
+
3242
+ function registerCurrentInstall(home) {
3243
+ const record = currentInstallRecord(home);
3244
+ if (!record) {
3245
+ return null;
3246
+ }
3247
+ return upsertInstallRecord(record);
3248
+ }
3249
+
3250
+ function dedupeUninstallRecords(records) {
3251
+ const seen = new Set();
3252
+ const deduped = [];
3253
+ for (const record of records) {
3254
+ const normalized = normalizeInstallRecord(record);
3255
+ if (!normalized) {
3256
+ continue;
3257
+ }
3258
+ const key = JSON.stringify([
3259
+ normalized.home,
3260
+ normalized.install_mode || null,
3261
+ normalized.install_dir || null,
3262
+ normalized.package_root || null,
3263
+ ]);
3264
+ if (seen.has(key)) {
3265
+ continue;
3266
+ }
3267
+ seen.add(key);
3268
+ deduped.push(normalized);
3269
+ }
3270
+ return deduped;
3271
+ }
3272
+
3273
+ function resolveUninstallRecords({ home, origin }) {
3274
+ const normalizedHome = normalizeHomePath(home);
3275
+ const currentPackageRoot = normalizeHomePath(repoRoot);
3276
+ const index = readInstallIndex();
3277
+ const installs = index.installs.map((item) => normalizeInstallRecord(item)).filter(Boolean);
3278
+ if (origin === 'npm') {
3279
+ const matching = installs.filter((item) => item.package_root === currentPackageRoot);
3280
+ if (matching.length > 0) {
3281
+ return dedupeUninstallRecords(matching);
3282
+ }
3283
+ return dedupeUninstallRecords([currentInstallRecord(normalizedHome)]);
3284
+ }
3285
+ const matching = installs.filter((item) => item.home === normalizedHome);
3286
+ if (matching.length > 0) {
3287
+ return dedupeUninstallRecords(matching);
3288
+ }
3289
+ const inferredInstallDir = fs.existsSync(path.join(normalizedHome, 'cli')) ? path.join(normalizedHome, 'cli') : null;
3290
+ return dedupeUninstallRecords([
3291
+ {
3292
+ ...currentInstallRecord(normalizedHome),
3293
+ install_dir: inferredInstallDir,
3294
+ },
3295
+ ]);
3296
+ }
3297
+
3298
+ function aggregateCodeOnlyUninstallPlan(records) {
3299
+ const removePaths = new Set();
3300
+ const preservePaths = new Set();
3301
+ const wrapperPaths = new Set();
3302
+ for (const record of records) {
3303
+ const installDir = record.install_dir || (fs.existsSync(path.join(record.home, 'cli')) ? path.join(record.home, 'cli') : null);
3304
+ const matchedWrappers = collectManagedWrapperPaths({
3305
+ home: record.home,
3306
+ installDir,
3307
+ explicitWrapperPaths: record.wrapper_paths || [],
3308
+ });
3309
+ const plan = buildCodeOnlyUninstallPlan({
3310
+ home: record.home,
3311
+ installDir,
3312
+ wrapperPaths: matchedWrappers,
3313
+ });
3314
+ for (const targetPath of plan.remove_paths) removePaths.add(targetPath);
3315
+ for (const targetPath of plan.preserve_paths) preservePaths.add(targetPath);
3316
+ for (const targetPath of plan.wrapper_paths) wrapperPaths.add(targetPath);
3317
+ }
3318
+ return {
3319
+ remove_paths: [...removePaths].sort(),
3320
+ preserve_paths: [...preservePaths].sort(),
3321
+ wrapper_paths: [...wrapperPaths].sort(),
3322
+ };
3323
+ }
3324
+
3325
+ async function promptUninstallConfirmation({ records, plan }) {
3326
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3327
+ throw new Error('DeepScientist uninstall needs a TTY for confirmation. Re-run with `--yes` to continue non-interactively.');
3328
+ }
3329
+ console.log('');
3330
+ console.log('DeepScientist uninstall');
3331
+ console.log('');
3332
+ console.log('This removes code and runtime directories, but preserves local data.');
3333
+ console.log('');
3334
+ for (const record of records) {
3335
+ console.log(`Home: ${record.home}`);
3336
+ }
3337
+ console.log('');
3338
+ console.log('Code/runtime paths to remove:');
3339
+ for (const targetPath of plan.remove_paths) {
3340
+ console.log(`- ${targetPath}`);
3341
+ }
3342
+ console.log('');
3343
+ console.log('Preserved data paths:');
3344
+ for (const targetPath of plan.preserve_paths) {
3345
+ console.log(`- ${targetPath}`);
3346
+ }
3347
+ const answer = await new Promise((resolve) => {
3348
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3349
+ rl.question('Type UNINSTALL to continue: ', (value) => {
3350
+ rl.close();
3351
+ resolve(String(value || '').trim());
3352
+ });
3353
+ });
3354
+ return answer === 'UNINSTALL';
3355
+ }
3356
+
3357
+ function runGlobalNpmUninstall() {
3358
+ const configuredPrefix = String(process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || '').trim();
3359
+ let uninstallPrefix = configuredPrefix || null;
3360
+ if (!uninstallPrefix && detectInstallMode(repoRoot) === 'npm-package') {
3361
+ let cursor = path.resolve(repoRoot);
3362
+ while (cursor && cursor !== path.dirname(cursor)) {
3363
+ if (path.basename(cursor) === 'node_modules') {
3364
+ const container = path.dirname(cursor);
3365
+ uninstallPrefix =
3366
+ path.basename(container) === 'lib'
3367
+ ? path.dirname(container)
3368
+ : container;
3369
+ break;
3370
+ }
3371
+ cursor = path.dirname(cursor);
3372
+ }
3373
+ }
3374
+ const npmBinary =
3375
+ resolveExecutableOnPath(process.platform === 'win32' ? 'npm.cmd' : 'npm')
3376
+ || resolveExecutableOnPath('npm');
3377
+ if (!npmBinary) {
3378
+ return {
3379
+ ok: false,
3380
+ message: 'Global npm package removal was skipped because `npm` is not available on PATH.',
3381
+ };
3382
+ }
3383
+ const result = spawnSync(
3384
+ npmBinary,
3385
+ ['uninstall', '-g', UPDATE_PACKAGE_NAME, ...(uninstallPrefix ? ['--prefix', uninstallPrefix] : [])],
3386
+ syncSpawnOptions({
3387
+ stdio: 'inherit',
3388
+ env: process.env,
3389
+ })
3390
+ );
3391
+ if (result.error) {
3392
+ return {
3393
+ ok: false,
3394
+ message: result.error.message,
3395
+ };
3396
+ }
3397
+ return {
3398
+ ok: result.status === 0,
3399
+ message: result.status === 0 ? null : `npm uninstall exited with status ${result.status ?? 1}.`,
3400
+ };
3401
+ }
3402
+
3403
+ function buildDaemonStatusPayload({ home, url, state, health, launcherPath = null }) {
3404
+ const healthy = Boolean(health && health.status === 'ok');
3405
+ const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
3406
+ return {
3407
+ healthy,
3408
+ identity_match: identityMatch,
3409
+ managed: Boolean(state),
3410
+ home,
3411
+ url,
3412
+ daemon_state_path: daemonStatePath(home),
3413
+ launcher_path: launcherPath || resolveLauncherPath() || null,
3414
+ daemon: state,
3415
+ health,
3416
+ };
3417
+ }
3418
+
2632
3419
  function daemonSupervisorLogPath(home) {
2633
3420
  return path.join(home, 'logs', 'daemon-supervisor.log');
2634
3421
  }
@@ -2678,9 +3465,25 @@ function decodeSupervisorEnvPayload(rawValue) {
2678
3465
  }
2679
3466
  }
2680
3467
 
2681
- function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = null, envOverrides = {}, daemonId = null }) {
3468
+ function spawnManagedDaemonProcess({
3469
+ home,
3470
+ runtimePython,
3471
+ host,
3472
+ port,
3473
+ proxy = null,
3474
+ envOverrides = {},
3475
+ daemonId = null,
3476
+ authEnabled = false,
3477
+ authToken = null,
3478
+ }) {
2682
3479
  const browserUrl = browserUiUrl(host, port);
2683
3480
  const daemonBindUrl = bindUiUrl(host, port);
3481
+ const resolvedAuthEnabled = authEnabled !== false;
3482
+ const resolvedAuthToken = resolvedAuthEnabled
3483
+ ? (typeof authToken === 'string' && authToken.trim() ? authToken.trim() : generateBrowserAuthToken())
3484
+ : null;
3485
+ const launchUrl = browserUrl;
3486
+ const bindLaunchUrl = daemonBindUrl;
2684
3487
  const logPath = path.join(home, 'logs', 'daemon.log');
2685
3488
  ensureDir(path.dirname(logPath));
2686
3489
  const out = fs.openSync(logPath, 'a');
@@ -2700,6 +3503,9 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2700
3503
  host,
2701
3504
  '--port',
2702
3505
  String(port),
3506
+ '--auth',
3507
+ resolvedAuthEnabled ? 'true' : 'false',
3508
+ ...(resolvedAuthEnabled && resolvedAuthToken ? ['--auth-token', resolvedAuthToken] : []),
2703
3509
  ],
2704
3510
  detachedSpawnOptions({
2705
3511
  cwd: repoRoot,
@@ -2712,6 +3518,8 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2712
3518
  DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2713
3519
  DS_DAEMON_ID: resolvedDaemonId,
2714
3520
  DS_DAEMON_MANAGED_BY: 'ds-launcher',
3521
+ DS_UI_AUTH_ENABLED: resolvedAuthEnabled ? '1' : '0',
3522
+ ...(resolvedAuthEnabled && resolvedAuthToken ? { DS_UI_AUTH_TOKEN: resolvedAuthToken } : {}),
2715
3523
  },
2716
3524
  })
2717
3525
  );
@@ -2722,10 +3530,14 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2722
3530
  port,
2723
3531
  url: browserUrl,
2724
3532
  bind_url: daemonBindUrl,
3533
+ launch_url: launchUrl,
3534
+ bind_launch_url: bindLaunchUrl,
2725
3535
  log_path: logPath,
2726
3536
  started_at: new Date().toISOString(),
2727
3537
  home: normalizeHomePath(home),
2728
3538
  daemon_id: resolvedDaemonId,
3539
+ auth_enabled: resolvedAuthEnabled,
3540
+ auth_token: resolvedAuthToken,
2729
3541
  };
2730
3542
  writeDaemonState(home, statePayload);
2731
3543
  return {
@@ -2733,6 +3545,8 @@ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = nu
2733
3545
  statePayload,
2734
3546
  browserUrl,
2735
3547
  bindUrl: daemonBindUrl,
3548
+ launchUrl,
3549
+ bindLaunchUrl,
2736
3550
  logPath,
2737
3551
  };
2738
3552
  }
@@ -2787,19 +3601,54 @@ function parseDaemonSupervisorArgs(argv) {
2787
3601
 
2788
3602
  for (let index = 0; index < args.length; index += 1) {
2789
3603
  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];
3604
+ if (arg === '--home') {
3605
+ const next = readRequiredOptionValue(args, index, '--home');
3606
+ if (!next.ok) return { help: false, error: next.error };
3607
+ home = path.resolve(next.value);
3608
+ index += 1;
3609
+ } else if (arg === '--runtime-python') {
3610
+ const next = readRequiredOptionValue(args, index, '--runtime-python');
3611
+ if (!next.ok) return { help: false, error: next.error };
3612
+ runtimePython = next.value;
3613
+ index += 1;
3614
+ } else if (arg === '--host') {
3615
+ const next = readRequiredOptionValue(args, index, '--host');
3616
+ if (!next.ok) return { help: false, error: next.error };
3617
+ host = next.value;
3618
+ index += 1;
3619
+ } else if (arg === '--port') {
3620
+ const next = readRequiredOptionValue(args, index, '--port');
3621
+ if (!next.ok) return { help: false, error: next.error };
3622
+ const parsed = parseStrictPortOption(next.value, '--port');
3623
+ if (!parsed.ok) return { help: false, error: parsed.error };
3624
+ port = parsed.value;
3625
+ index += 1;
3626
+ } else if (arg === '--proxy') {
3627
+ const next = readRequiredOptionValue(args, index, '--proxy');
3628
+ if (!next.ok) return { help: false, error: next.error };
3629
+ proxy = next.value;
3630
+ index += 1;
3631
+ } else if (arg === '--daemon-id') {
3632
+ const next = readRequiredOptionValue(args, index, '--daemon-id');
3633
+ if (!next.ok) return { help: false, error: next.error };
3634
+ daemonId = next.value;
3635
+ index += 1;
3636
+ } else if (arg === '--env-json') {
3637
+ const next = readRequiredOptionValue(args, index, '--env-json');
3638
+ if (!next.ok) return { help: false, error: next.error };
3639
+ envJson = next.value;
3640
+ index += 1;
3641
+ }
2797
3642
  else if (arg === '--help' || arg === '-h') return { help: true };
2798
- else return null;
3643
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown daemon supervisor flag: ${arg}` };
3644
+ else return { help: false, error: `Unexpected daemon supervisor argument: ${arg}` };
2799
3645
  }
2800
3646
 
2801
3647
  if (!home || !runtimePython || !daemonId || !Number.isFinite(port) || port <= 0) {
2802
- return null;
3648
+ return {
3649
+ help: false,
3650
+ error: 'Daemon supervisor requires --home, --runtime-python, --daemon-id, and a valid --port.',
3651
+ };
2803
3652
  }
2804
3653
 
2805
3654
  return {
@@ -2811,18 +3660,19 @@ function parseDaemonSupervisorArgs(argv) {
2811
3660
  proxy,
2812
3661
  daemonId,
2813
3662
  envOverrides: decodeSupervisorEnvPayload(envJson),
3663
+ error: null,
2814
3664
  };
2815
3665
  }
2816
3666
 
2817
3667
  async function daemonSupervisorMain(rawArgs) {
2818
3668
  const options = parseDaemonSupervisorArgs(rawArgs);
2819
- if (!options) {
2820
- console.error('Invalid daemon supervisor arguments.');
2821
- process.exit(1);
2822
- }
2823
3669
  if (options.help) {
2824
3670
  process.exit(0);
2825
3671
  }
3672
+ if (options.error) {
3673
+ console.error(options.error);
3674
+ process.exit(1);
3675
+ }
2826
3676
 
2827
3677
  const home = options.home;
2828
3678
  let trackedDaemonId = String(options.daemonId || '').trim();
@@ -2849,7 +3699,8 @@ async function daemonSupervisorMain(rawArgs) {
2849
3699
  appendDaemonSupervisorLog(home, `daemon id changed to ${stateDaemonId}; supervisor exiting`);
2850
3700
  return;
2851
3701
  }
2852
- const health = await fetchHealth(state.url || browserUiUrl(options.host, options.port));
3702
+ const authToken = typeof state.auth_token === 'string' ? state.auth_token.trim() : '';
3703
+ const health = await fetchHealth(state.url || browserUiUrl(options.host, options.port), authToken);
2853
3704
  if (health && health.status === 'ok' && healthMatchesManagedState({ health, state, home })) {
2854
3705
  restartBackoffMs = 1000;
2855
3706
  await sleep(2500);
@@ -2872,6 +3723,8 @@ async function daemonSupervisorMain(rawArgs) {
2872
3723
  port: options.port,
2873
3724
  proxy: options.proxy,
2874
3725
  envOverrides: options.envOverrides,
3726
+ authEnabled: state.auth_enabled !== false,
3727
+ authToken,
2875
3728
  });
2876
3729
  trackedDaemonId = String(restarted.statePayload.daemon_id || '').trim();
2877
3730
  observeManagedDaemonChild(home, restarted.child, trackedDaemonId);
@@ -2896,14 +3749,19 @@ function sleep(ms) {
2896
3749
  return new Promise((resolve) => setTimeout(resolve, ms));
2897
3750
  }
2898
3751
 
2899
- async function isHealthy(url) {
2900
- const payload = await fetchHealth(url);
3752
+ async function isHealthy(url, authToken = null) {
3753
+ const payload = await fetchHealth(url, authToken);
2901
3754
  return Boolean(payload && payload.status === 'ok');
2902
3755
  }
2903
3756
 
2904
- async function fetchHealth(url) {
3757
+ async function fetchHealth(url, authToken = null) {
2905
3758
  try {
2906
- const response = await fetch(`${url}/api/health`);
3759
+ const headers = {};
3760
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3761
+ if (normalizedAuthToken) {
3762
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3763
+ }
3764
+ const response = await fetch(`${url}/api/health`, { headers });
2907
3765
  if (!response.ok) {
2908
3766
  return null;
2909
3767
  }
@@ -2953,13 +3811,18 @@ function daemonIdentityError({ url, home, health, state }) {
2953
3811
  ].join('\n');
2954
3812
  }
2955
3813
 
2956
- async function requestDaemonShutdown(url, daemonId) {
3814
+ async function requestDaemonShutdown(url, daemonId, authToken = null) {
2957
3815
  try {
3816
+ const headers = {
3817
+ 'Content-Type': 'application/json',
3818
+ };
3819
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3820
+ if (normalizedAuthToken) {
3821
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3822
+ }
2958
3823
  const response = await fetch(`${url}/api/admin/shutdown`, {
2959
3824
  method: 'POST',
2960
- headers: {
2961
- 'Content-Type': 'application/json',
2962
- },
3825
+ headers,
2963
3826
  body: JSON.stringify({ source: 'ds-launcher', daemon_id: daemonId || null }),
2964
3827
  });
2965
3828
  if (!response.ok) {
@@ -2972,6 +3835,31 @@ async function requestDaemonShutdown(url, daemonId) {
2972
3835
  }
2973
3836
  }
2974
3837
 
3838
+ async function requestDaemonAuthRotate(url, authToken = null) {
3839
+ try {
3840
+ const headers = {
3841
+ 'Content-Type': 'application/json',
3842
+ };
3843
+ const normalizedAuthToken = typeof authToken === 'string' ? authToken.trim() : '';
3844
+ if (normalizedAuthToken) {
3845
+ headers.Authorization = `Bearer ${normalizedAuthToken}`;
3846
+ }
3847
+ const response = await fetch(`${url}/api/auth/rotate`, {
3848
+ method: 'POST',
3849
+ headers,
3850
+ body: '{}',
3851
+ });
3852
+ if (!response.ok) {
3853
+ return null;
3854
+ }
3855
+ const payload = await response.json().catch(() => ({}));
3856
+ const token = typeof payload?.token === 'string' ? payload.token.trim() : '';
3857
+ return token || null;
3858
+ } catch {
3859
+ return null;
3860
+ }
3861
+ }
3862
+
2975
3863
  function isPidAlive(pid) {
2976
3864
  if (!pid) return false;
2977
3865
  try {
@@ -3005,9 +3893,9 @@ function killManagedProcess(pid, signal) {
3005
3893
  }
3006
3894
  }
3007
3895
 
3008
- async function waitForDaemonStop({ url, pid, attempts = 20, delayMs = 200 }) {
3896
+ async function waitForDaemonStop({ url, pid, authToken = null, attempts = 20, delayMs = 200 }) {
3009
3897
  for (let attempt = 0; attempt < attempts; attempt += 1) {
3010
- const healthy = url ? await isHealthy(url) : false;
3898
+ const healthy = url ? await isHealthy(url, authToken) : false;
3011
3899
  const alive = pid ? isPidAlive(pid) : false;
3012
3900
  if (!healthy && !alive) {
3013
3901
  return true;
@@ -3032,7 +3920,8 @@ async function stopDaemon(home) {
3032
3920
  const state = readDaemonState(home);
3033
3921
  const configured = readConfiguredUiAddressFromFile(home);
3034
3922
  const url = state?.url || browserUiUrl(state?.host || configured.host, state?.port || configured.port);
3035
- const healthBefore = await fetchHealth(url);
3923
+ const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
3924
+ const healthBefore = await fetchHealth(url, authToken);
3036
3925
  const healthyBefore = Boolean(healthBefore && healthBefore.status === 'ok');
3037
3926
  const sameHomeHealthy = healthMatchesHome({ health: healthBefore, home });
3038
3927
  const pid = state?.pid || (sameHomeHealthy ? healthBefore?.pid : null);
@@ -3073,21 +3962,21 @@ async function stopDaemon(home) {
3073
3962
  let stopped = false;
3074
3963
 
3075
3964
  if (healthyBefore) {
3076
- await requestDaemonShutdown(url, shutdownDaemonId || null);
3077
- stopped = await waitForDaemonStop({ url, pid, attempts: 20, delayMs: 200 });
3965
+ await requestDaemonShutdown(url, shutdownDaemonId || null, authToken);
3966
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 20, delayMs: 200 });
3078
3967
  }
3079
3968
 
3080
3969
  if (!stopped && pid && isPidAlive(pid)) {
3081
3970
  killManagedProcess(pid, 'SIGTERM');
3082
- stopped = await waitForDaemonStop({ url, pid, attempts: 30, delayMs: 200 });
3971
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 30, delayMs: 200 });
3083
3972
  }
3084
3973
 
3085
3974
  if (!stopped && pid && isPidAlive(pid)) {
3086
3975
  killManagedProcess(pid, 'SIGKILL');
3087
- stopped = await waitForDaemonStop({ url, pid, attempts: 20, delayMs: 150 });
3976
+ stopped = await waitForDaemonStop({ url, pid, authToken, attempts: 20, delayMs: 150 });
3088
3977
  }
3089
3978
 
3090
- const stillHealthy = await isHealthy(url);
3979
+ const stillHealthy = await isHealthy(url, authToken);
3091
3980
  if (!stopped && (stillHealthy || (pid && isPidAlive(pid)))) {
3092
3981
  console.error('DeepScientist daemon is still running after shutdown attempts.');
3093
3982
  process.exit(1);
@@ -3097,6 +3986,86 @@ async function stopDaemon(home) {
3097
3986
  console.log('DeepScientist daemon stopped.');
3098
3987
  }
3099
3988
 
3989
+ async function uninstallMain(rawArgs) {
3990
+ const options = parseUninstallArgs(rawArgs);
3991
+ if (options.help) {
3992
+ printUninstallHelp();
3993
+ process.exit(0);
3994
+ }
3995
+ if (options.error) {
3996
+ console.error(options.error);
3997
+ console.error('Run `ds uninstall --help` for usage.');
3998
+ process.exit(1);
3999
+ }
4000
+
4001
+ const home = normalizeHomePath(options.home || resolveHome(rawArgs));
4002
+ const records = resolveUninstallRecords({ home, origin: options.origin });
4003
+ const plan = aggregateCodeOnlyUninstallPlan(records);
4004
+
4005
+ if (!options.yes && options.origin !== 'npm') {
4006
+ const confirmed = await promptUninstallConfirmation({ records, plan });
4007
+ if (!confirmed) {
4008
+ console.log('DeepScientist uninstall cancelled.');
4009
+ process.exit(1);
4010
+ }
4011
+ }
4012
+
4013
+ for (const record of records) {
4014
+ try {
4015
+ await stopDaemon(record.home);
4016
+ } catch (error) {
4017
+ console.warn(`DeepScientist could not fully stop the daemon for ${record.home}: ${error instanceof Error ? error.message : String(error)}`);
4018
+ }
4019
+ }
4020
+
4021
+ const removed = [];
4022
+ for (const targetPath of plan.remove_paths) {
4023
+ if (removePathEntry(targetPath)) {
4024
+ removed.push(targetPath);
4025
+ }
4026
+ }
4027
+ for (const targetPath of plan.wrapper_paths) {
4028
+ if (removePathEntry(targetPath)) {
4029
+ removed.push(targetPath);
4030
+ }
4031
+ }
4032
+
4033
+ removeInstallRecords((record) => records.some((candidate) => installRecordMatches(candidate, record)));
4034
+
4035
+ let npmRemovalMessage = null;
4036
+ if (options.origin !== 'npm' && detectInstallMode(repoRoot) === 'npm-package') {
4037
+ const npmRemoval = runGlobalNpmUninstall();
4038
+ if (!npmRemoval.ok) {
4039
+ npmRemovalMessage = npmRemoval.message;
4040
+ }
4041
+ }
4042
+
4043
+ console.log('');
4044
+ console.log('DeepScientist code uninstall completed.');
4045
+ if (removed.length > 0) {
4046
+ console.log('');
4047
+ console.log('Removed:');
4048
+ for (const targetPath of removed) {
4049
+ console.log(`- ${targetPath}`);
4050
+ }
4051
+ }
4052
+ console.log('');
4053
+ console.log('Preserved local data:');
4054
+ for (const targetPath of plan.preserve_paths) {
4055
+ console.log(`- ${targetPath}`);
4056
+ }
4057
+ console.log('');
4058
+ for (const record of records) {
4059
+ console.log(`If you also want to delete local data manually: rm -rf ${record.home}`);
4060
+ }
4061
+ if (npmRemovalMessage) {
4062
+ console.log('');
4063
+ console.warn(`Global npm package removal did not complete automatically: ${npmRemovalMessage}`);
4064
+ console.warn(`Run: npm uninstall -g ${UPDATE_PACKAGE_NAME}`);
4065
+ }
4066
+ process.exit(0);
4067
+ }
4068
+
3100
4069
  function writeUpdateLog(home, content) {
3101
4070
  const logPath = path.join(home, 'logs', 'update.log');
3102
4071
  ensureDir(path.dirname(logPath));
@@ -3362,19 +4331,23 @@ async function performSelfUpdate(home, options = {}) {
3362
4331
  log_path: installResult.logPath,
3363
4332
  };
3364
4333
  }
4334
+ const restartArgs = [
4335
+ launcherPath,
4336
+ '--home',
4337
+ home,
4338
+ '--host',
4339
+ String(host),
4340
+ '--port',
4341
+ String(port),
4342
+ '--daemon-only',
4343
+ '--no-browser',
4344
+ '--skip-update-check',
4345
+ ];
4346
+ if (daemonState && daemonState.auth_enabled === false) {
4347
+ restartArgs.push('--auth', 'false');
4348
+ }
3365
4349
  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
- ],
4350
+ restartArgs,
3378
4351
  {
3379
4352
  cwd: repoRoot,
3380
4353
  env: process.env,
@@ -3569,17 +4542,88 @@ async function startBackgroundUpdateWorker(home, options = {}) {
3569
4542
  };
3570
4543
  }
3571
4544
 
4545
+ async function maybeHandleMiniMaxCodexVersion(home, runtimePython, options = {}) {
4546
+ const configuredRunners = (() => {
4547
+ try {
4548
+ const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'runners'], {
4549
+ capture: true,
4550
+ allowFailure: true,
4551
+ });
4552
+ return String(result.stdout || '');
4553
+ } catch {
4554
+ return '';
4555
+ }
4556
+ })();
4557
+ const profileFromConfig =
4558
+ configuredRunners.match(/^\s*profile:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
4559
+ const binaryFromConfig =
4560
+ configuredRunners.match(/^\s*binary:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || 'codex';
4561
+ const configDirFromConfig =
4562
+ configuredRunners.match(/^\s*config_dir:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '~/.codex';
4563
+
4564
+ const effectiveProfile = String(options.codexProfile || profileFromConfig || '').trim();
4565
+ if (!effectiveProfile) {
4566
+ return false;
4567
+ }
4568
+ const metadata = readCodexProviderMetadata(configDirFromConfig, effectiveProfile);
4569
+ if (String(metadata.provider || '').trim().toLowerCase() !== 'minimax') {
4570
+ return false;
4571
+ }
4572
+ const version = installedCodexCliVersion(options.codexBinary || binaryFromConfig || 'codex');
4573
+ const expected = [0, 57, 0];
4574
+ if (!version || compareCodexCliVersion(version, expected) === 0) {
4575
+ return false;
4576
+ }
4577
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
4578
+ console.log(
4579
+ `MiniMax profile \`${effectiveProfile}\` is configured, but installed Codex CLI is ${formatCodexCliVersion(version)}. MiniMax currently requires Codex CLI 0.57.0 for the documented path.`
4580
+ );
4581
+ console.log('Install it manually with `npm install -g @openai/codex@0.57.0` before continuing.');
4582
+ return false;
4583
+ }
4584
+
4585
+ console.log('');
4586
+ console.log(colorize('\u001B[1;38;5;214m', 'MiniMax compatibility check'));
4587
+ console.log(
4588
+ `DeepScientist detected MiniMax profile \`${effectiveProfile}\`, but installed Codex CLI is ${formatCodexCliVersion(version)}.`
4589
+ );
4590
+ console.log('MiniMax currently requires Codex CLI 0.57.0 for the documented DeepScientist path.');
4591
+ const confirmed = await promptYesNo('Reinstall Codex CLI to 0.57.0 now? [y/N]: ', {
4592
+ defaultValue: false,
4593
+ });
4594
+ if (!confirmed) {
4595
+ return false;
4596
+ }
4597
+ const npmBinary = resolveNpmBinary();
4598
+ if (!npmBinary) {
4599
+ console.error('`npm` is unavailable; cannot reinstall Codex CLI automatically.');
4600
+ process.exit(1);
4601
+ }
4602
+ const result = spawnSync(
4603
+ npmBinary,
4604
+ ['install', '-g', '@openai/codex@0.57.0'],
4605
+ syncSpawnOptions({ stdio: 'inherit' })
4606
+ );
4607
+ if (result.status !== 0) {
4608
+ console.error('Failed to reinstall Codex CLI 0.57.0 automatically.');
4609
+ process.exit(result.status ?? 1);
4610
+ }
4611
+ return true;
4612
+ }
4613
+
3572
4614
  async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallbackPort) {
3573
4615
  try {
3574
4616
  const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'config'], { capture: true, allowFailure: true });
3575
4617
  const text = result.stdout || '';
3576
4618
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3577
4619
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4620
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3578
4621
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3579
4622
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3580
4623
  return {
3581
4624
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3582
4625
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4626
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3583
4627
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3584
4628
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3585
4629
  };
@@ -3587,6 +4631,7 @@ async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallba
3587
4631
  return {
3588
4632
  host: fallbackHost || '0.0.0.0',
3589
4633
  port: fallbackPort || 20999,
4634
+ authEnabled: false,
3590
4635
  defaultMode: 'web',
3591
4636
  autoOpenBrowser: true,
3592
4637
  };
@@ -3599,6 +4644,7 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3599
4644
  return {
3600
4645
  host: fallbackHost || '0.0.0.0',
3601
4646
  port: fallbackPort || 20999,
4647
+ authEnabled: false,
3602
4648
  defaultMode: 'web',
3603
4649
  autoOpenBrowser: true,
3604
4650
  };
@@ -3607,11 +4653,13 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3607
4653
  const text = fs.readFileSync(configPath, 'utf8');
3608
4654
  const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
3609
4655
  const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
4656
+ const authMatch = text.match(/^\s*auth_enabled:\s*([^\n]+)\s*$/m);
3610
4657
  const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
3611
4658
  const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
3612
4659
  return {
3613
4660
  host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
3614
4661
  port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
4662
+ authEnabled: parseBooleanSetting(authMatch ? authMatch[1].trim() : false, false),
3615
4663
  defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
3616
4664
  autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
3617
4665
  };
@@ -3619,20 +4667,51 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
3619
4667
  return {
3620
4668
  host: fallbackHost || '0.0.0.0',
3621
4669
  port: fallbackPort || 20999,
4670
+ authEnabled: false,
3622
4671
  defaultMode: 'web',
3623
4672
  autoOpenBrowser: true,
3624
4673
  };
3625
4674
  }
3626
4675
  }
3627
4676
 
3628
- async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}) {
4677
+ async function startDaemon(home, runtimePython, host, port, proxy = null, envOverrides = {}, authEnabled = false) {
3629
4678
  const browserUrl = browserUiUrl(host, port);
3630
4679
  const daemonBindUrl = bindUiUrl(host, port);
3631
4680
  const state = readDaemonState(home);
3632
- const existingHealth = await fetchHealth(browserUrl);
4681
+ const desiredAuthToken = authEnabled ? generateBrowserAuthToken() : null;
4682
+ const launchUrl = browserUrl;
4683
+ const bindLaunchUrl = daemonBindUrl;
4684
+ const existingHealth = await fetchHealth(browserUrl, typeof state?.auth_token === 'string' ? state.auth_token.trim() : '');
3633
4685
  if (existingHealth && existingHealth.status === 'ok') {
3634
4686
  if (state && healthMatchesManagedState({ health: existingHealth, state, home })) {
3635
- return { url: browserUrl, bindUrl: daemonBindUrl, reused: true };
4687
+ const stateAuthEnabled = state.auth_enabled !== false;
4688
+ const stateAuthToken = typeof state.auth_token === 'string' ? state.auth_token.trim() : '';
4689
+ let resolvedAuthToken = stateAuthToken || null;
4690
+ if (stateAuthEnabled) {
4691
+ const rotatedAuthToken = await requestDaemonAuthRotate(browserUrl, stateAuthToken);
4692
+ if (!rotatedAuthToken) {
4693
+ console.error('Managed daemon is healthy, but the browser auth token could not be rotated.');
4694
+ console.error('Restart the daemon with `ds --restart` if this keeps happening.');
4695
+ process.exit(1);
4696
+ }
4697
+ resolvedAuthToken = rotatedAuthToken;
4698
+ writeDaemonState(home, {
4699
+ ...state,
4700
+ auth_enabled: true,
4701
+ auth_token: rotatedAuthToken,
4702
+ url: browserUrl,
4703
+ bind_url: daemonBindUrl,
4704
+ launch_url: launchUrl,
4705
+ bind_launch_url: bindLaunchUrl,
4706
+ });
4707
+ }
4708
+ return {
4709
+ url: browserUrl,
4710
+ bindUrl: daemonBindUrl,
4711
+ reused: true,
4712
+ authEnabled: stateAuthEnabled,
4713
+ authToken: resolvedAuthToken,
4714
+ };
3636
4715
  }
3637
4716
  console.error(
3638
4717
  state
@@ -3666,11 +4745,13 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3666
4745
  port,
3667
4746
  proxy,
3668
4747
  envOverrides,
4748
+ authEnabled,
4749
+ authToken: desiredAuthToken,
3669
4750
  });
3670
4751
  const logPath = startedProcess.logPath;
3671
4752
 
3672
4753
  for (let attempt = 0; attempt < 60; attempt += 1) {
3673
- const health = await fetchHealth(browserUrl);
4754
+ const health = await fetchHealth(browserUrl, desiredAuthToken);
3674
4755
  if (health && health.status === 'ok') {
3675
4756
  const liveState = readDaemonState(home);
3676
4757
  if (!healthMatchesManagedState({ health, state: liveState, home })) {
@@ -3689,12 +4770,26 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3689
4770
  if (supervisorPid) {
3690
4771
  appendDaemonSupervisorLog(home, `supervisor started with pid ${supervisorPid}`);
3691
4772
  }
3692
- return { url: browserUrl, bindUrl: daemonBindUrl, reused: false };
4773
+ return {
4774
+ url: launchUrl,
4775
+ bindUrl: bindLaunchUrl,
4776
+ reused: false,
4777
+ authEnabled,
4778
+ authToken: desiredAuthToken,
4779
+ };
3693
4780
  }
3694
4781
  await sleep(250);
3695
4782
  }
3696
4783
 
3697
4784
  console.error('DeepScientist daemon failed to become healthy.');
4785
+ console.error(`Expected local URL: ${launchUrl}`);
4786
+ console.error(`Daemon bind URL: ${bindLaunchUrl}`);
4787
+ if (authEnabled && desiredAuthToken) {
4788
+ console.error(`Auth token: ${desiredAuthToken}`);
4789
+ }
4790
+ if (['0.0.0.0', '::', '[::]'].includes(String(host || '').trim())) {
4791
+ console.error(`Hint: ${String(host || '').trim() || '0.0.0.0'} is a bind address. Local browser and health probes use ${browserUrl}.`);
4792
+ }
3698
4793
  const logTail = tailLog(logPath);
3699
4794
  if (logTail) {
3700
4795
  console.error(logTail);
@@ -3766,7 +4861,7 @@ function handleCodexPreflightFailure(error) {
3766
4861
  : [
3767
4862
  'In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency.',
3768
4863
  'If `codex` is still missing, run `npm install -g @openai/codex`.',
3769
- 'Run `codex --login` (or `codex`) and finish authentication.',
4864
+ 'Run `codex login` (or just `codex`) and finish authentication.',
3770
4865
  'Run `ds doctor` and confirm the Codex check passes.',
3771
4866
  'Run `ds` again.',
3772
4867
  ];
@@ -3778,12 +4873,15 @@ function handleCodexPreflightFailure(error) {
3778
4873
  return true;
3779
4874
  }
3780
4875
 
3781
- function launchTui(url, questId, home, runtimePython) {
4876
+ function launchTui(url, questId, home, runtimePython, authToken = null) {
3782
4877
  const entry = ensureNodeBundle('src/tui', 'dist/index.js');
3783
4878
  const args = [entry, '--base-url', url];
3784
4879
  if (questId) {
3785
4880
  args.push('--quest-id', questId);
3786
4881
  }
4882
+ if (typeof authToken === 'string' && authToken.trim()) {
4883
+ args.push('--auth-token', authToken.trim());
4884
+ }
3787
4885
  const child = spawn(process.execPath, args, {
3788
4886
  cwd: repoRoot,
3789
4887
  stdio: 'inherit',
@@ -3801,14 +4899,15 @@ function launchTui(url, questId, home, runtimePython) {
3801
4899
 
3802
4900
  async function updateMain(rawArgs) {
3803
4901
  const options = parseUpdateArgs(rawArgs);
3804
- if (!options) {
3805
- printUpdateHelp();
3806
- process.exit(1);
3807
- }
3808
4902
  if (options.help) {
3809
4903
  printUpdateHelp();
3810
4904
  process.exit(0);
3811
4905
  }
4906
+ if (options.error) {
4907
+ console.error(options.error);
4908
+ console.error('Run `ds update --help` for update usage.');
4909
+ process.exit(1);
4910
+ }
3812
4911
 
3813
4912
  const home = options.home || resolveHome(rawArgs);
3814
4913
  applyLauncherProxy(options.proxy);
@@ -3923,14 +5022,15 @@ async function updateMain(rawArgs) {
3923
5022
 
3924
5023
  async function migrateMain(rawArgs) {
3925
5024
  const options = parseMigrateArgs(rawArgs);
3926
- if (!options) {
3927
- printMigrateHelp();
3928
- process.exit(1);
3929
- }
3930
5025
  if (options.help) {
3931
5026
  printMigrateHelp();
3932
5027
  process.exit(0);
3933
5028
  }
5029
+ if (options.error) {
5030
+ console.error(options.error);
5031
+ console.error('Run `ds migrate --help` for migration usage.');
5032
+ process.exit(1);
5033
+ }
3934
5034
 
3935
5035
  const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
3936
5036
  const targetHome = path.resolve(options.target);
@@ -4051,20 +5151,27 @@ async function migrateMain(rawArgs) {
4051
5151
 
4052
5152
  async function launcherMain(rawArgs) {
4053
5153
  const options = parseLauncherArgs(rawArgs);
4054
- if (!options) {
4055
- return false;
4056
- }
4057
5154
  if (options.help) {
4058
5155
  printLauncherHelp();
4059
5156
  process.exit(0);
4060
5157
  }
5158
+ if (options.error) {
5159
+ console.error(options.error);
5160
+ console.error('Run `ds --help` for launcher usage.');
5161
+ process.exit(1);
5162
+ }
4061
5163
 
4062
5164
  const home = options.home || resolveHome(rawArgs);
4063
5165
  applyLauncherProxy(options.proxy);
4064
5166
  ensureDir(home);
5167
+ registerCurrentInstall(home);
5168
+ const forceWrapperRepair =
5169
+ detectInstallMode(repoRoot) !== 'npm-package'
5170
+ && Boolean(options.home || process.env.DEEPSCIENTIST_HOME);
4065
5171
  repairLegacyPathWrappers({
4066
5172
  home,
4067
5173
  launcherPath: resolveLauncherPath(),
5174
+ force: forceWrapperRepair,
4068
5175
  });
4069
5176
 
4070
5177
  if (options.stop) {
@@ -4075,26 +5182,21 @@ async function launcherMain(rawArgs) {
4075
5182
  if (options.status) {
4076
5183
  const state = readDaemonState(home);
4077
5184
  const configured = readConfiguredUiAddressFromFile(home, options.host, options.port);
4078
- const url = state?.url || browserUiUrl(configured.host, configured.port);
4079
- const health = await fetchHealth(url);
4080
- const healthy = Boolean(health && health.status === 'ok');
4081
- const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
5185
+ const url = state?.launch_url || state?.url || browserUiUrl(configured.host, configured.port);
5186
+ const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
5187
+ const probeUrl = state?.url || browserUiUrl(configured.host, configured.port);
5188
+ const health = await fetchHealth(probeUrl, authToken);
5189
+ const statusPayload = buildDaemonStatusPayload({
5190
+ home,
5191
+ url,
5192
+ state,
5193
+ health,
5194
+ launcherPath: resolveLauncherPath(),
5195
+ });
4082
5196
  console.log(
4083
- JSON.stringify(
4084
- {
4085
- healthy,
4086
- identity_match: identityMatch,
4087
- managed: Boolean(state),
4088
- home,
4089
- url,
4090
- daemon: state,
4091
- health,
4092
- },
4093
- null,
4094
- 2
4095
- )
5197
+ JSON.stringify(statusPayload, null, 2)
4096
5198
  );
4097
- process.exit(healthy && (!state || identityMatch) ? 0 : 1);
5199
+ process.exit(statusPayload.healthy && (!state || statusPayload.identity_match) ? 0 : 1);
4098
5200
  }
4099
5201
 
4100
5202
  const pythonRuntime = ensurePythonRuntime(home);
@@ -4105,6 +5207,7 @@ async function launcherMain(rawArgs) {
4105
5207
  binary: options.codexBinary,
4106
5208
  });
4107
5209
  ensureInitialized(home, runtimePython);
5210
+ await maybeHandleMiniMaxCodexVersion(home, runtimePython, options);
4108
5211
  if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
4109
5212
  return true;
4110
5213
  }
@@ -4113,20 +5216,27 @@ async function launcherMain(rawArgs) {
4113
5216
  const configuredUi = await readConfiguredUiAddress(home, runtimePython, options.host, options.port);
4114
5217
  const host = configuredUi.host;
4115
5218
  const port = configuredUi.port;
5219
+ const authEnabled = options.auth === null ? false : options.auth !== false;
4116
5220
  const mode = normalizeMode(options.mode ?? 'web');
4117
5221
  const shouldOpenBrowser = options.daemonOnly
4118
5222
  ? false
4119
5223
  : options.openBrowser === null
4120
5224
  ? configuredUi.autoOpenBrowser !== false && mode !== 'tui'
4121
5225
  : options.openBrowser;
4122
- if (options.restart) {
5226
+ const existingState = readDaemonState(home);
5227
+ const existingAuthEnabled = existingState ? existingState.auth_enabled !== false : null;
5228
+ const existingAuthToken = typeof existingState?.auth_token === 'string' ? existingState.auth_token.trim() : '';
5229
+ const authStateMismatch = existingState && (
5230
+ existingAuthEnabled !== authEnabled || (authEnabled && !existingAuthToken)
5231
+ );
5232
+ if (options.restart || authStateMismatch) {
4123
5233
  await stopDaemon(home);
4124
5234
  }
4125
5235
 
4126
5236
  step(4, 4, 'Starting local daemon and UI surfaces');
4127
5237
  let started;
4128
5238
  try {
4129
- started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv);
5239
+ started = await startDaemon(home, runtimePython, host, port, options.proxy, codexOverrideEnv, authEnabled);
4130
5240
  } catch (error) {
4131
5241
  if (handleCodexPreflightFailure(error)) return true;
4132
5242
  throw error;
@@ -4142,6 +5252,8 @@ async function launcherMain(rawArgs) {
4142
5252
  home,
4143
5253
  pythonSelection: pythonRuntime.runtimeProbe,
4144
5254
  yolo: options.yolo,
5255
+ authEnabled: started.authEnabled,
5256
+ authToken: started.authToken,
4145
5257
  });
4146
5258
 
4147
5259
  if (options.daemonOnly) {
@@ -4150,12 +5262,16 @@ async function launcherMain(rawArgs) {
4150
5262
  if (mode === 'web') {
4151
5263
  process.exit(0);
4152
5264
  }
4153
- launchTui(started.url, options.questId, home, runtimePython);
5265
+ launchTui(browserUiUrl(host, port), options.questId, home, runtimePython, started.authToken);
4154
5266
  return true;
4155
5267
  }
4156
5268
 
4157
5269
  async function main() {
4158
- const args = process.argv.slice(2);
5270
+ const normalizedArgState = normalizeLegacyHostFlagArgs(process.argv.slice(2));
5271
+ const args = normalizedArgState.args;
5272
+ for (const warning of normalizedArgState.warnings) {
5273
+ console.warn(warning);
5274
+ }
4159
5275
  if (args[0] === '--daemon-supervisor') {
4160
5276
  await daemonSupervisorMain(args.slice(1));
4161
5277
  return;
@@ -4169,7 +5285,15 @@ async function main() {
4169
5285
  await migrateMain(args);
4170
5286
  return;
4171
5287
  }
4172
- if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
5288
+ if (positional && positional.value === 'uninstall') {
5289
+ await uninstallMain(args);
5290
+ return;
5291
+ }
5292
+ if (
5293
+ args.length === 0
5294
+ || args[0] === 'ui'
5295
+ || (args[0]?.startsWith('--') && (!positional || !pythonCommands.has(positional.value)))
5296
+ ) {
4173
5297
  await launcherMain(args);
4174
5298
  return;
4175
5299
  }
@@ -4225,19 +5349,26 @@ module.exports = {
4225
5349
  resolveUvBinary,
4226
5350
  resolveHome,
4227
5351
  parseLauncherArgs,
5352
+ generateBrowserAuthToken,
5353
+ appendBrowserAuthToken,
4228
5354
  normalizeProxyUrl,
5355
+ buildCodeOnlyUninstallPlan,
4229
5356
  parseMigrateArgs,
4230
5357
  parseLegacyWrapperCandidate,
4231
5358
  repairLegacyPathWrappers,
4232
5359
  useEditableProjectInstall,
4233
5360
  compareVersions,
4234
5361
  detectInstallMode,
5362
+ buildUvSyncFailureGuidance,
4235
5363
  updateManualCommand,
4236
5364
  buildUpdateStatus,
5365
+ buildDaemonStatusPayload,
4237
5366
  parseYesNoAnswer,
4238
5367
  normalizeLauncherRelaunchArgs,
4239
5368
  officialRepositoryLine,
4240
5369
  stripAnsi,
5370
+ normalizeLegacyHostFlagArgs,
5371
+ runGlobalNpmUninstall,
4241
5372
  },
4242
5373
  };
4243
5374