@researai/deepscientist 1.5.11 → 1.5.13

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 (107) hide show
  1. package/README.md +8 -8
  2. package/bin/ds.js +375 -61
  3. package/docs/en/00_QUICK_START.md +55 -4
  4. package/docs/en/01_SETTINGS_REFERENCE.md +15 -0
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +68 -4
  6. package/docs/en/09_DOCTOR.md +48 -4
  7. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +21 -2
  8. package/docs/en/15_CODEX_PROVIDER_SETUP.md +382 -0
  9. package/docs/en/README.md +4 -0
  10. package/docs/zh/00_QUICK_START.md +54 -3
  11. package/docs/zh/01_SETTINGS_REFERENCE.md +15 -0
  12. package/docs/zh/02_START_RESEARCH_GUIDE.md +69 -3
  13. package/docs/zh/09_DOCTOR.md +48 -2
  14. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +21 -2
  15. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +383 -0
  16. package/docs/zh/README.md +4 -1
  17. package/package.json +2 -1
  18. package/pyproject.toml +1 -1
  19. package/src/deepscientist/__init__.py +1 -1
  20. package/src/deepscientist/bash_exec/monitor.py +7 -5
  21. package/src/deepscientist/bash_exec/service.py +84 -21
  22. package/src/deepscientist/channels/local.py +3 -3
  23. package/src/deepscientist/channels/qq.py +7 -7
  24. package/src/deepscientist/channels/relay.py +7 -7
  25. package/src/deepscientist/channels/weixin_ilink.py +90 -19
  26. package/src/deepscientist/cli.py +3 -0
  27. package/src/deepscientist/codex_cli_compat.py +117 -0
  28. package/src/deepscientist/config/models.py +1 -0
  29. package/src/deepscientist/config/service.py +173 -25
  30. package/src/deepscientist/daemon/app.py +314 -6
  31. package/src/deepscientist/doctor.py +1 -5
  32. package/src/deepscientist/mcp/server.py +124 -3
  33. package/src/deepscientist/prompts/builder.py +113 -11
  34. package/src/deepscientist/quest/service.py +247 -31
  35. package/src/deepscientist/runners/codex.py +132 -24
  36. package/src/deepscientist/runners/runtime_overrides.py +9 -0
  37. package/src/deepscientist/shared.py +33 -14
  38. package/src/prompts/connectors/qq.md +2 -1
  39. package/src/prompts/connectors/weixin.md +2 -1
  40. package/src/prompts/contracts/shared_interaction.md +4 -1
  41. package/src/prompts/system.md +59 -9
  42. package/src/skills/analysis-campaign/SKILL.md +46 -6
  43. package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
  44. package/src/skills/baseline/SKILL.md +1 -1
  45. package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
  46. package/src/skills/decision/SKILL.md +1 -1
  47. package/src/skills/experiment/SKILL.md +1 -1
  48. package/src/skills/finalize/SKILL.md +1 -1
  49. package/src/skills/idea/SKILL.md +1 -1
  50. package/src/skills/intake-audit/SKILL.md +1 -1
  51. package/src/skills/rebuttal/SKILL.md +74 -1
  52. package/src/skills/rebuttal/references/response-letter-template.md +55 -11
  53. package/src/skills/review/SKILL.md +118 -1
  54. package/src/skills/review/references/experiment-todo-template.md +23 -0
  55. package/src/skills/review/references/review-report-template.md +16 -0
  56. package/src/skills/review/references/revision-log-template.md +4 -0
  57. package/src/skills/scout/SKILL.md +1 -1
  58. package/src/skills/write/SKILL.md +168 -7
  59. package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
  60. package/src/tui/dist/lib/connectorConfig.js +90 -0
  61. package/src/tui/dist/lib/qr.js +21 -0
  62. package/src/tui/package.json +2 -1
  63. package/src/ui/dist/assets/{AiManusChatView-D0mTXG4-.js → AiManusChatView-CnJcXynW.js} +12 -12
  64. package/src/ui/dist/assets/{AnalysisPlugin-Db0cTXxm.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
  65. package/src/ui/dist/assets/{CliPlugin-DrV8je02.js → CliPlugin-CB1YODQn.js} +9 -9
  66. package/src/ui/dist/assets/{CodeEditorPlugin-QXMSCH71.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
  67. package/src/ui/dist/assets/{CodeViewerPlugin-7hhtWj_E.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
  68. package/src/ui/dist/assets/{DocViewerPlugin-BWMSnRJe.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
  69. package/src/ui/dist/assets/{GitDiffViewerPlugin-7J9h9Vy_.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -20
  70. package/src/ui/dist/assets/{ImageViewerPlugin-CHJl_0lr.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
  71. package/src/ui/dist/assets/{LabCopilotPanel-1qSow1es.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
  72. package/src/ui/dist/assets/{LabPlugin-eQpPPCEp.js → LabPlugin-Ciz1gDaX.js} +2 -2
  73. package/src/ui/dist/assets/{LatexPlugin-BwRfi89Z.js → LatexPlugin-BhmjNQRC.js} +37 -11
  74. package/src/ui/dist/assets/{MarkdownViewerPlugin-836PVQWV.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
  75. package/src/ui/dist/assets/{MarketplacePlugin-C2y_556i.js → MarketplacePlugin-DmyHspXt.js} +3 -3
  76. package/src/ui/dist/assets/{NotebookEditor-DIX7Mlzu.js → NotebookEditor-BMXKrDRk.js} +1 -1
  77. package/src/ui/dist/assets/{NotebookEditor-BRzJbGsn.js → NotebookEditor-BTVYRGkm.js} +11 -11
  78. package/src/ui/dist/assets/{PdfLoader-DzRaTAlq.js → PdfLoader-CvcjJHXv.js} +1 -1
  79. package/src/ui/dist/assets/{PdfMarkdownPlugin-DZUfIUnp.js → PdfMarkdownPlugin-DW2ej8Vk.js} +2 -2
  80. package/src/ui/dist/assets/{PdfViewerPlugin-BwtICzue.js → PdfViewerPlugin-CmlDxbhU.js} +10 -10
  81. package/src/ui/dist/assets/{SearchPlugin-DHeIAMsx.js → SearchPlugin-DAjQZPSv.js} +1 -1
  82. package/src/ui/dist/assets/{TextViewerPlugin-C3tCmFox.js → TextViewerPlugin-C-nVAZb_.js} +5 -5
  83. package/src/ui/dist/assets/{VNCViewer-CQsKVm3t.js → VNCViewer-D7-dIYon.js} +10 -10
  84. package/src/ui/dist/assets/{bot-BEA2vWuK.js → bot-C_G4WtNI.js} +1 -1
  85. package/src/ui/dist/assets/{code-XfbSR8K2.js → code-Cd7WfiWq.js} +1 -1
  86. package/src/ui/dist/assets/{file-content-BjxNaIfy.js → file-content-B57zsL9y.js} +1 -1
  87. package/src/ui/dist/assets/{file-diff-panel-D_lLVQk0.js → file-diff-panel-DVoheLFq.js} +1 -1
  88. package/src/ui/dist/assets/{file-socket-D9x_5vlY.js → file-socket-B5kXFxZP.js} +1 -1
  89. package/src/ui/dist/assets/{image-BhWT33W1.js → image-LLOjkMHF.js} +1 -1
  90. package/src/ui/dist/assets/{index-Dqj-Mjb4.css → index-BQG-1s2o.css} +40 -2
  91. package/src/ui/dist/assets/{index--c4iXtuy.js → index-C3r2iGrp.js} +12 -12
  92. package/src/ui/dist/assets/{index-DZTZ8mWP.js → index-CLQauncb.js} +911 -120
  93. package/src/ui/dist/assets/{index-PJbSbPTy.js → index-Dxa2eYMY.js} +1 -1
  94. package/src/ui/dist/assets/{index-BDxipwrC.js → index-hOUOWbW2.js} +2 -2
  95. package/src/ui/dist/assets/{monaco-K8izTGgo.js → monaco-BGGAEii3.js} +1 -1
  96. package/src/ui/dist/assets/{pdf-effect-queue-DfBors6y.js → pdf-effect-queue-DlEr1_y5.js} +1 -1
  97. package/src/ui/dist/assets/{popover-yFK1J4fL.js → popover-CWJbJuYY.js} +1 -1
  98. package/src/ui/dist/assets/{project-sync-PENr2zcz.js → project-sync-CRJiucYO.js} +18 -4
  99. package/src/ui/dist/assets/{select-CAbJDfYv.js → select-CoHB7pvH.js} +2 -2
  100. package/src/ui/dist/assets/{sigma-DEuYJqTl.js → sigma-D5aJWR8J.js} +1 -1
  101. package/src/ui/dist/assets/{square-check-big-omoSUmcd.js → square-check-big-DUK_mnkS.js} +1 -1
  102. package/src/ui/dist/assets/{trash--F119N47.js → trash-ChU3SEE3.js} +1 -1
  103. package/src/ui/dist/assets/{useCliAccess-D31UR23I.js → useCliAccess-BrJBV3tY.js} +1 -1
  104. package/src/ui/dist/assets/{useFileDiffOverlay-BH6KcMzq.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
  105. package/src/ui/dist/assets/{wrap-text-CZ613PM5.js → wrap-text-C7Qqh-om.js} +1 -1
  106. package/src/ui/dist/assets/{zoom-out-BgDLAv3z.js → zoom-out-rtX0FKya.js} +1 -1
  107. package/src/ui/dist/index.html +2 -2
package/README.md CHANGED
@@ -1,14 +1,13 @@
1
- # DeepScientist
1
+ <h1 align="center">
2
+ <img src="assets/branding/logo.svg" alt="DeepScientist logo" width="84" />
3
+ DeepScientist
4
+ </h1>
2
5
 
3
- <p align="center">
4
- <img src="assets/branding/logo.svg" alt="DeepScientist logo" width="120" />
5
- </p>
6
-
7
- <p align="center">
6
+ <p>
8
7
  <strong>DeepScientist is not just a long-running autonomous scientific discovery system. It is also a persistent research map that lives on your own machine.</strong>
9
8
  </p>
10
9
 
11
- <p align="center">
10
+ <p>
12
11
  Local-first. Open-source. Git-backed. Built for verifiable computational research.
13
12
  </p>
14
13
 
@@ -99,6 +98,7 @@ If `codex --login` is unavailable, run `codex` once and finish authentication th
99
98
  For detailed install, troubleshooting, PDF compile, and other launch modes, use:
100
99
 
101
100
  - [Quick Start](docs/en/00_QUICK_START.md)
101
+ - [Codex Provider Setup](docs/en/15_CODEX_PROVIDER_SETUP.md)
102
102
  - [Doctor](docs/en/09_DOCTOR.md)
103
103
 
104
104
  ## Documentation
@@ -112,6 +112,7 @@ For detailed install, troubleshooting, PDF compile, and other launch modes, use:
112
112
  - [Lingzhu / Rokid Guide (English)](docs/en/04_LINGZHU_CONNECTOR_GUIDE.md)
113
113
  - [Memory and MCP Guide (English)](docs/en/07_MEMORY_AND_MCP.md)
114
114
  - [Settings Reference (English)](docs/en/01_SETTINGS_REFERENCE.md)
115
+ - [Codex Provider Setup (English)](docs/en/15_CODEX_PROVIDER_SETUP.md)
115
116
 
116
117
  ## Maintainers
117
118
 
@@ -147,7 +148,6 @@ url={https://openreview.net/forum?id=cZFgsLq8Gs}
147
148
  | [Dr. Claw](https://github.com/OpenLAIR/dr-claw) | Open-source | ✓ | | ✓ | | ✓ | |
148
149
  | [FARS](https://analemma.ai/fars/) | Closed-source | ✓ | | | | | |
149
150
  | [EvoScientist](https://github.com/EvoScientist/EvoScientist) | Open-source | ✓ | | ✓ | ✓ | ✓ | |
150
- | [PaperClaw](https://github.com/meowscles69/PaperClaw) | Open-source | | | | | | ✓ |
151
151
  | [ScienceClaw](https://github.com/beita6969/ScienceClaw) | Open-source | | | | ✓ | ✓ | |
152
152
  | [claude-scholar](https://github.com/Galaxy-Dawn/claude-scholar) | Open-source | ✓ | | ✓ | ✓ | | |
153
153
  | [Research-Claw](https://github.com/wentorai/Research-Claw) | Open-source | ✓ | | ✓ | ✓ | ✓ | |
package/bin/ds.js CHANGED
@@ -36,15 +36,37 @@ 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']);
39
+ const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode', '--proxy', '--codex-profile', '--codex']);
40
40
 
41
- function buildCodexOverrideEnv({ yolo = false } = {}) {
41
+ function buildCodexOverrideEnv({ yolo = false, profile = null, binary = null } = {}) {
42
+ const normalizedProfile = typeof profile === 'string' ? profile.trim() : '';
43
+ const normalizedBinary = typeof binary === 'string' ? binary.trim() : '';
44
+ const overrides = {};
45
+ if (normalizedBinary) {
46
+ overrides.DEEPSCIENTIST_CODEX_BINARY = normalizedBinary;
47
+ }
42
48
  if (!yolo) {
43
- return {};
49
+ if (normalizedProfile) {
50
+ overrides.DEEPSCIENTIST_CODEX_PROFILE = normalizedProfile;
51
+ overrides.DEEPSCIENTIST_CODEX_MODEL = 'inherit';
52
+ }
53
+ return overrides;
44
54
  }
45
- return {
46
- DEEPSCIENTIST_CODEX_YOLO: '1',
47
- };
55
+ overrides.DEEPSCIENTIST_CODEX_YOLO = '1';
56
+ if (normalizedProfile) {
57
+ overrides.DEEPSCIENTIST_CODEX_PROFILE = normalizedProfile;
58
+ overrides.DEEPSCIENTIST_CODEX_MODEL = 'inherit';
59
+ }
60
+ return overrides;
61
+ }
62
+
63
+ function readOptionValue(argv, optionName) {
64
+ for (let index = 0; index < argv.length; index += 1) {
65
+ if (argv[index] === optionName && argv[index + 1]) {
66
+ return argv[index + 1];
67
+ }
68
+ }
69
+ return null;
48
70
  }
49
71
 
50
72
  function printLauncherHelp() {
@@ -84,6 +106,8 @@ Launcher flags:
84
106
  --here Create/use ./DeepScientist under the current working directory as home
85
107
  --proxy <url> Use an outbound HTTP/WS proxy for npm and Python runtime traffic
86
108
  --yolo Run Codex in YOLO mode: approval_policy=never and sandbox_mode=danger-full-access
109
+ --codex-profile <id> Run DeepScientist with a specific Codex profile, for example \`m27\`
110
+ --codex <path> Run DeepScientist with a specific Codex executable path for this launch
87
111
  --quest-id <id> Open the TUI on one quest directly
88
112
 
89
113
  Update:
@@ -754,6 +778,13 @@ function writeCodexPreflightReport(home, probe) {
754
778
  const errors = Array.isArray(probe?.errors) ? probe.errors : [];
755
779
  const guidance = Array.isArray(probe?.guidance) ? probe.guidance : [];
756
780
  const details = probe && typeof probe.details === 'object' ? probe.details : {};
781
+ const profile = typeof details.profile === 'string' ? details.profile.trim() : '';
782
+ const intro = profile
783
+ ? `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.`
784
+ : '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.';
785
+ const introZh = profile
786
+ ? `DeepScientist 启动前进行了 Codex 可用性检查,但 profile \`${profile}\` 的 hello 探测没有通过。请先确认 \`codex --profile ${profile}\` 在当前机器上可以正常启动,并确保该 profile 依赖的 provider API Key、Base URL 和模型配置都已经在 Codex 中配置好。`
787
+ : 'DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,`npm install -g @researai/deepscientist` 也会一并安装 bundled Codex 依赖;如果此后 `codex` 仍不可用,请再执行 `npm install -g @openai/codex` 修复。然后运行 `codex --login`(或 `codex`)完成认证,再执行 `ds doctor`,最后重新启动 `ds`。';
757
788
  const renderItems = (items, tone) =>
758
789
  items
759
790
  .map(
@@ -814,8 +845,8 @@ function writeCodexPreflightReport(home, probe) {
814
845
  <main class="page">
815
846
  <section class="panel">
816
847
  <h1>DeepScientist could not start Codex</h1>
817
- <p class="meta">DeepScientist blocked startup because the Codex hello probe did not pass. In most installs, <code>npm install -g @researai/deepscientist</code> also installs the bundled Codex dependency. If <code>codex</code> is still missing, repair it with <code>npm install -g @openai/codex</code>. Then run <code>codex --login</code> (or <code>codex</code>), finish authentication, run <code>ds doctor</code>, and launch <code>ds</code> again.</p>
818
- <p class="meta">DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,<code>npm install -g @researai/deepscientist</code> 也会一并安装 bundled Codex 依赖;如果此后 <code>codex</code> 仍不可用,请再执行 <code>npm install -g @openai/codex</code> 修复。然后运行 <code>codex --login</code>(或 <code>codex</code>)完成认证,再执行 <code>ds doctor</code>,最后重新启动 <code>ds</code>。</p>
848
+ <p class="meta">${escapeHtml(intro)}</p>
849
+ <p class="meta">${escapeHtml(introZh)}</p>
819
850
 
820
851
  <h2>Summary</h2>
821
852
  <p>${escapeHtml(probe?.summary || 'Codex startup probe failed.')}</p>
@@ -838,6 +869,10 @@ function writeCodexPreflightReport(home, probe) {
838
869
  <dt>Model</dt>
839
870
  <dd>${escapeHtml(details.model || '')}</dd>
840
871
  </dl>
872
+ <dl class="kv">
873
+ <dt>Profile</dt>
874
+ <dd>${escapeHtml(details.profile || '')}</dd>
875
+ </dl>
841
876
  <dl class="kv">
842
877
  <dt>Exit code</dt>
843
878
  <dd>${escapeHtml(details.exit_code ?? '')}</dd>
@@ -950,6 +985,8 @@ function parseLauncherArgs(argv) {
950
985
  let daemonOnly = false;
951
986
  let skipUpdateCheck = false;
952
987
  let yolo = false;
988
+ let codexProfile = null;
989
+ let codexBinary = null;
953
990
 
954
991
  if (args[0] === 'ui') {
955
992
  args.shift();
@@ -969,6 +1006,8 @@ function parseLauncherArgs(argv) {
969
1006
  else if (arg === '--daemon-only') daemonOnly = true;
970
1007
  else if (arg === '--skip-update-check') skipUpdateCheck = true;
971
1008
  else if (arg === '--yolo') yolo = true;
1009
+ else if (arg === '--codex-profile' && args[index + 1]) codexProfile = args[++index];
1010
+ else if (arg === '--codex' && args[index + 1]) codexBinary = args[++index];
972
1011
  else if (arg === '--host' && args[index + 1]) host = args[++index];
973
1012
  else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
974
1013
  else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
@@ -994,6 +1033,8 @@ function parseLauncherArgs(argv) {
994
1033
  daemonOnly,
995
1034
  skipUpdateCheck,
996
1035
  yolo,
1036
+ codexProfile,
1037
+ codexBinary,
997
1038
  };
998
1039
  }
999
1040
 
@@ -2284,6 +2325,14 @@ function normalizePythonCliArgs(args, home) {
2284
2325
  if (arg === '--yolo') {
2285
2326
  continue;
2286
2327
  }
2328
+ if (arg === '--codex-profile') {
2329
+ index += 1;
2330
+ continue;
2331
+ }
2332
+ if (arg === '--codex') {
2333
+ index += 1;
2334
+ continue;
2335
+ }
2287
2336
  normalized.push(arg);
2288
2337
  }
2289
2338
  return ['--home', home, ...normalized];
@@ -2492,6 +2541,270 @@ function removeDaemonState(home) {
2492
2541
  }
2493
2542
  }
2494
2543
 
2544
+ function daemonSupervisorLogPath(home) {
2545
+ return path.join(home, 'logs', 'daemon-supervisor.log');
2546
+ }
2547
+
2548
+ function appendDaemonSupervisorLog(home, message) {
2549
+ try {
2550
+ const logPath = daemonSupervisorLogPath(home);
2551
+ ensureDir(path.dirname(logPath));
2552
+ fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${String(message || '').trim()}\n`, 'utf8');
2553
+ } catch {}
2554
+ }
2555
+
2556
+ function observeManagedDaemonChild(home, child, daemonId) {
2557
+ if (!child || typeof child.once !== 'function') {
2558
+ return;
2559
+ }
2560
+ const normalizedDaemonId = String(daemonId || '').trim() || 'unknown';
2561
+ child.once('exit', (code, signal) => {
2562
+ appendDaemonSupervisorLog(
2563
+ home,
2564
+ `daemon ${normalizedDaemonId} exited with code=${code === null ? 'null' : code} signal=${signal || 'null'}`
2565
+ );
2566
+ });
2567
+ child.once('error', (error) => {
2568
+ appendDaemonSupervisorLog(
2569
+ home,
2570
+ `daemon ${normalizedDaemonId} child process error: ${error instanceof Error ? error.message : String(error)}`
2571
+ );
2572
+ });
2573
+ }
2574
+
2575
+ function encodeSupervisorEnvPayload(envOverrides) {
2576
+ const payload = envOverrides && typeof envOverrides === 'object' && !Array.isArray(envOverrides) ? envOverrides : {};
2577
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
2578
+ }
2579
+
2580
+ function decodeSupervisorEnvPayload(rawValue) {
2581
+ const normalized = String(rawValue || '').trim();
2582
+ if (!normalized) {
2583
+ return {};
2584
+ }
2585
+ try {
2586
+ const parsed = JSON.parse(Buffer.from(normalized, 'base64').toString('utf8'));
2587
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
2588
+ } catch {
2589
+ return {};
2590
+ }
2591
+ }
2592
+
2593
+ function spawnManagedDaemonProcess({ home, runtimePython, host, port, proxy = null, envOverrides = {}, daemonId = null }) {
2594
+ const browserUrl = browserUiUrl(host, port);
2595
+ const daemonBindUrl = bindUiUrl(host, port);
2596
+ const logPath = path.join(home, 'logs', 'daemon.log');
2597
+ ensureDir(path.dirname(logPath));
2598
+ const out = fs.openSync(logPath, 'a');
2599
+ const resolvedDaemonId = String(daemonId || crypto.randomUUID()).trim();
2600
+ const launcherPath = path.join(repoRoot, 'bin', 'ds.js');
2601
+ const child = spawn(
2602
+ runtimePython,
2603
+ [
2604
+ '-m',
2605
+ 'deepscientist.cli',
2606
+ '--home',
2607
+ home,
2608
+ ...(normalizeProxyUrl(proxy) ? ['--proxy', normalizeProxyUrl(proxy)] : []),
2609
+ 'daemon',
2610
+ '--host',
2611
+ host,
2612
+ '--port',
2613
+ String(port),
2614
+ ],
2615
+ {
2616
+ cwd: repoRoot,
2617
+ detached: true,
2618
+ stdio: ['ignore', out, out],
2619
+ env: {
2620
+ ...process.env,
2621
+ ...envOverrides,
2622
+ DEEPSCIENTIST_REPO_ROOT: repoRoot,
2623
+ DEEPSCIENTIST_NODE_BINARY: process.execPath,
2624
+ DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2625
+ DS_DAEMON_ID: resolvedDaemonId,
2626
+ DS_DAEMON_MANAGED_BY: 'ds-launcher',
2627
+ },
2628
+ }
2629
+ );
2630
+ child.unref();
2631
+ const statePayload = {
2632
+ pid: child.pid,
2633
+ host,
2634
+ port,
2635
+ url: browserUrl,
2636
+ bind_url: daemonBindUrl,
2637
+ log_path: logPath,
2638
+ started_at: new Date().toISOString(),
2639
+ home: normalizeHomePath(home),
2640
+ daemon_id: resolvedDaemonId,
2641
+ };
2642
+ writeDaemonState(home, statePayload);
2643
+ return {
2644
+ child,
2645
+ statePayload,
2646
+ browserUrl,
2647
+ bindUrl: daemonBindUrl,
2648
+ logPath,
2649
+ };
2650
+ }
2651
+
2652
+ function spawnDaemonSupervisor({ home, runtimePython, host, port, proxy = null, envOverrides = {}, daemonId }) {
2653
+ const launcherPath = resolveLauncherPath() || path.join(repoRoot, 'bin', 'ds.js');
2654
+ const args = [
2655
+ launcherPath,
2656
+ '--daemon-supervisor',
2657
+ '--home',
2658
+ home,
2659
+ '--runtime-python',
2660
+ runtimePython,
2661
+ '--host',
2662
+ host,
2663
+ '--port',
2664
+ String(port),
2665
+ '--daemon-id',
2666
+ String(daemonId || '').trim(),
2667
+ ];
2668
+ const normalizedProxy = normalizeProxyUrl(proxy);
2669
+ if (normalizedProxy) {
2670
+ args.push('--proxy', normalizedProxy);
2671
+ }
2672
+ const envPayload = encodeSupervisorEnvPayload(envOverrides);
2673
+ if (envPayload) {
2674
+ args.push('--env-json', envPayload);
2675
+ }
2676
+ const child = spawn(process.execPath, args, {
2677
+ cwd: repoRoot,
2678
+ detached: true,
2679
+ stdio: 'ignore',
2680
+ env: {
2681
+ ...process.env,
2682
+ DEEPSCIENTIST_REPO_ROOT: repoRoot,
2683
+ DEEPSCIENTIST_NODE_BINARY: process.execPath,
2684
+ DEEPSCIENTIST_LAUNCHER_PATH: launcherPath,
2685
+ },
2686
+ });
2687
+ child.unref();
2688
+ return child.pid || null;
2689
+ }
2690
+
2691
+ function parseDaemonSupervisorArgs(argv) {
2692
+ const args = [...argv];
2693
+ let home = null;
2694
+ let runtimePython = null;
2695
+ let host = '0.0.0.0';
2696
+ let port = 20999;
2697
+ let proxy = null;
2698
+ let daemonId = null;
2699
+ let envJson = '';
2700
+
2701
+ for (let index = 0; index < args.length; index += 1) {
2702
+ const arg = args[index];
2703
+ if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
2704
+ else if (arg === '--runtime-python' && args[index + 1]) runtimePython = args[++index];
2705
+ else if (arg === '--host' && args[index + 1]) host = args[++index];
2706
+ else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
2707
+ else if (arg === '--proxy' && args[index + 1]) proxy = args[++index];
2708
+ else if (arg === '--daemon-id' && args[index + 1]) daemonId = args[++index];
2709
+ else if (arg === '--env-json' && args[index + 1]) envJson = args[++index];
2710
+ else if (arg === '--help' || arg === '-h') return { help: true };
2711
+ else return null;
2712
+ }
2713
+
2714
+ if (!home || !runtimePython || !daemonId || !Number.isFinite(port) || port <= 0) {
2715
+ return null;
2716
+ }
2717
+
2718
+ return {
2719
+ help: false,
2720
+ home,
2721
+ runtimePython,
2722
+ host,
2723
+ port,
2724
+ proxy,
2725
+ daemonId,
2726
+ envOverrides: decodeSupervisorEnvPayload(envJson),
2727
+ };
2728
+ }
2729
+
2730
+ async function daemonSupervisorMain(rawArgs) {
2731
+ const options = parseDaemonSupervisorArgs(rawArgs);
2732
+ if (!options) {
2733
+ console.error('Invalid daemon supervisor arguments.');
2734
+ process.exit(1);
2735
+ }
2736
+ if (options.help) {
2737
+ process.exit(0);
2738
+ }
2739
+
2740
+ const home = options.home;
2741
+ let trackedDaemonId = String(options.daemonId || '').trim();
2742
+ let restartBackoffMs = 1000;
2743
+ appendDaemonSupervisorLog(home, `supervisor started for daemon ${trackedDaemonId}`);
2744
+
2745
+ while (true) {
2746
+ const state = readDaemonState(home);
2747
+ if (!state) {
2748
+ appendDaemonSupervisorLog(home, 'daemon state removed; supervisor exiting');
2749
+ return;
2750
+ }
2751
+ if (state.shutdown_requested_at) {
2752
+ appendDaemonSupervisorLog(home, 'managed shutdown requested; supervisor exiting');
2753
+ return;
2754
+ }
2755
+ const stateHome = normalizeHomePath(state.home || home);
2756
+ if (stateHome !== normalizeHomePath(home)) {
2757
+ appendDaemonSupervisorLog(home, `daemon state home changed to ${stateHome}; supervisor exiting`);
2758
+ return;
2759
+ }
2760
+ const stateDaemonId = String(state.daemon_id || '').trim();
2761
+ if (trackedDaemonId && stateDaemonId && stateDaemonId !== trackedDaemonId) {
2762
+ appendDaemonSupervisorLog(home, `daemon id changed to ${stateDaemonId}; supervisor exiting`);
2763
+ return;
2764
+ }
2765
+ const health = await fetchHealth(state.url || browserUiUrl(options.host, options.port));
2766
+ if (health && health.status === 'ok' && healthMatchesManagedState({ health, state, home })) {
2767
+ restartBackoffMs = 1000;
2768
+ await sleep(2500);
2769
+ continue;
2770
+ }
2771
+ if (state.pid && isPidAlive(state.pid)) {
2772
+ await sleep(2500);
2773
+ continue;
2774
+ }
2775
+
2776
+ appendDaemonSupervisorLog(
2777
+ home,
2778
+ `daemon ${stateDaemonId || trackedDaemonId || 'unknown'} is not healthy; attempting restart`
2779
+ );
2780
+ try {
2781
+ const restarted = spawnManagedDaemonProcess({
2782
+ home,
2783
+ runtimePython: options.runtimePython,
2784
+ host: options.host,
2785
+ port: options.port,
2786
+ proxy: options.proxy,
2787
+ envOverrides: options.envOverrides,
2788
+ });
2789
+ trackedDaemonId = String(restarted.statePayload.daemon_id || '').trim();
2790
+ observeManagedDaemonChild(home, restarted.child, trackedDaemonId);
2791
+ appendDaemonSupervisorLog(
2792
+ home,
2793
+ `restarted daemon ${trackedDaemonId} with pid ${restarted.statePayload.pid}`
2794
+ );
2795
+ restartBackoffMs = 1000;
2796
+ await sleep(2500);
2797
+ } catch (error) {
2798
+ appendDaemonSupervisorLog(
2799
+ home,
2800
+ `restart failed: ${error instanceof Error ? error.message : String(error)}`
2801
+ );
2802
+ await sleep(restartBackoffMs);
2803
+ restartBackoffMs = Math.min(restartBackoffMs * 2, 30000);
2804
+ }
2805
+ }
2806
+ }
2807
+
2495
2808
  function sleep(ms) {
2496
2809
  return new Promise((resolve) => setTimeout(resolve, ms));
2497
2810
  }
@@ -2663,6 +2976,13 @@ async function stopDaemon(home) {
2663
2976
  }
2664
2977
  }
2665
2978
 
2979
+ if (state) {
2980
+ writeDaemonState(home, {
2981
+ ...state,
2982
+ shutdown_requested_at: new Date().toISOString(),
2983
+ });
2984
+ }
2985
+
2666
2986
  let stopped = false;
2667
2987
 
2668
2988
  if (healthyBefore) {
@@ -3253,61 +3573,36 @@ async function startDaemon(home, runtimePython, host, port, proxy = null, envOve
3253
3573
  }
3254
3574
 
3255
3575
  ensureNodeBundle('src/ui', 'dist/index.html');
3256
-
3257
- const logPath = path.join(home, 'logs', 'daemon.log');
3258
- ensureDir(path.dirname(logPath));
3259
- const out = fs.openSync(logPath, 'a');
3260
- const daemonId = crypto.randomUUID();
3261
- const child = spawn(
3576
+ const startedProcess = spawnManagedDaemonProcess({
3577
+ home,
3262
3578
  runtimePython,
3263
- [
3264
- '-m',
3265
- 'deepscientist.cli',
3266
- '--home',
3267
- home,
3268
- ...(normalizeProxyUrl(proxy) ? ['--proxy', normalizeProxyUrl(proxy)] : []),
3269
- 'daemon',
3270
- '--host',
3271
- host,
3272
- '--port',
3273
- String(port),
3274
- ],
3275
- {
3276
- cwd: repoRoot,
3277
- detached: true,
3278
- stdio: ['ignore', out, out],
3279
- env: {
3280
- ...process.env,
3281
- ...envOverrides,
3282
- DEEPSCIENTIST_REPO_ROOT: repoRoot,
3283
- DEEPSCIENTIST_NODE_BINARY: process.execPath,
3284
- DEEPSCIENTIST_LAUNCHER_PATH: path.join(repoRoot, 'bin', 'ds.js'),
3285
- DS_DAEMON_ID: daemonId,
3286
- DS_DAEMON_MANAGED_BY: 'ds-launcher',
3287
- },
3288
- }
3289
- );
3290
- child.unref();
3291
- const statePayload = {
3292
- pid: child.pid,
3293
3579
  host,
3294
3580
  port,
3295
- url: browserUrl,
3296
- bind_url: daemonBindUrl,
3297
- log_path: logPath,
3298
- started_at: new Date().toISOString(),
3299
- home: normalizeHomePath(home),
3300
- daemon_id: daemonId,
3301
- };
3302
- writeDaemonState(home, statePayload);
3581
+ proxy,
3582
+ envOverrides,
3583
+ });
3584
+ const logPath = startedProcess.logPath;
3303
3585
 
3304
3586
  for (let attempt = 0; attempt < 60; attempt += 1) {
3305
3587
  const health = await fetchHealth(browserUrl);
3306
3588
  if (health && health.status === 'ok') {
3307
- if (!healthMatchesManagedState({ health, state: readDaemonState(home), home })) {
3308
- console.error(daemonIdentityError({ url: browserUrl, home, health, state: readDaemonState(home) }));
3589
+ const liveState = readDaemonState(home);
3590
+ if (!healthMatchesManagedState({ health, state: liveState, home })) {
3591
+ console.error(daemonIdentityError({ url: browserUrl, home, health, state: liveState }));
3309
3592
  process.exit(1);
3310
3593
  }
3594
+ const supervisorPid = spawnDaemonSupervisor({
3595
+ home,
3596
+ runtimePython,
3597
+ host,
3598
+ port,
3599
+ proxy,
3600
+ envOverrides,
3601
+ daemonId: String((liveState || {}).daemon_id || ''),
3602
+ });
3603
+ if (supervisorPid) {
3604
+ appendDaemonSupervisorLog(home, `supervisor started with pid ${supervisorPid}`);
3605
+ }
3311
3606
  return { url: browserUrl, bindUrl: daemonBindUrl, reused: false };
3312
3607
  }
3313
3608
  await sleep(250);
@@ -3380,11 +3675,18 @@ function handleCodexPreflightFailure(error) {
3380
3675
  }
3381
3676
  }
3382
3677
  console.error(`${warningLabel} Recommended fix:`);
3383
- console.error(`${warningLabel} 1. In most installs, \`npm install -g @researai/deepscientist\` also installs the bundled Codex dependency.`);
3384
- console.error(`${warningLabel} 2. If \`codex\` is still missing, run \`npm install -g @openai/codex\`.`);
3385
- console.error(`${warningLabel} 3. Run \`codex --login\` (or \`codex\`) and finish authentication.`);
3386
- console.error(`${warningLabel} 4. Run \`ds doctor\` and confirm the Codex check passes.`);
3387
- console.error(`${warningLabel} 5. Run \`ds\` again.`);
3678
+ const guidance = Array.isArray(error.probe?.guidance) && error.probe.guidance.length > 0
3679
+ ? error.probe.guidance
3680
+ : [
3681
+ 'In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency.',
3682
+ 'If `codex` is still missing, run `npm install -g @openai/codex`.',
3683
+ 'Run `codex --login` (or `codex`) and finish authentication.',
3684
+ 'Run `ds doctor` and confirm the Codex check passes.',
3685
+ 'Run `ds` again.',
3686
+ ];
3687
+ guidance.forEach((item, index) => {
3688
+ console.error(`${warningLabel} ${index + 1}. ${item}`);
3689
+ });
3388
3690
  openBrowser(error.reportUrl);
3389
3691
  process.exit(1);
3390
3692
  return true;
@@ -3712,7 +4014,11 @@ async function launcherMain(rawArgs) {
3712
4014
 
3713
4015
  const pythonRuntime = ensurePythonRuntime(home);
3714
4016
  const runtimePython = pythonRuntime.runtimePython;
3715
- const codexOverrideEnv = buildCodexOverrideEnv({ yolo: options.yolo });
4017
+ const codexOverrideEnv = buildCodexOverrideEnv({
4018
+ yolo: options.yolo,
4019
+ profile: options.codexProfile,
4020
+ binary: options.codexBinary,
4021
+ });
3716
4022
  ensureInitialized(home, runtimePython);
3717
4023
  if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
3718
4024
  return true;
@@ -3765,6 +4071,10 @@ async function launcherMain(rawArgs) {
3765
4071
 
3766
4072
  async function main() {
3767
4073
  const args = process.argv.slice(2);
4074
+ if (args[0] === '--daemon-supervisor') {
4075
+ await daemonSupervisorMain(args.slice(1));
4076
+ return;
4077
+ }
3768
4078
  const positional = findFirstPositionalArg(args);
3769
4079
  if (positional && positional.value === 'update') {
3770
4080
  await updateMain(args);
@@ -3786,7 +4096,11 @@ async function main() {
3786
4096
  const home = resolveHome(args);
3787
4097
  const pythonRuntime = ensurePythonRuntime(home);
3788
4098
  const runtimePython = pythonRuntime.runtimePython;
3789
- const codexOverrideEnv = buildCodexOverrideEnv({ yolo: args.includes('--yolo') });
4099
+ const codexOverrideEnv = buildCodexOverrideEnv({
4100
+ yolo: args.includes('--yolo'),
4101
+ profile: readOptionValue(args, '--codex-profile'),
4102
+ binary: readOptionValue(args, '--codex'),
4103
+ });
3790
4104
  if (positional.value === 'run' || positional.value === 'daemon') {
3791
4105
  maybePrintOptionalLatexNotice(home);
3792
4106
  }