@researai/deepscientist 1.5.2 → 1.5.4

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 (114) hide show
  1. package/README.md +22 -0
  2. package/bin/ds.js +399 -175
  3. package/docs/en/00_QUICK_START.md +22 -0
  4. package/docs/en/01_SETTINGS_REFERENCE.md +13 -4
  5. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  6. package/docs/images/connectors/discord-setup-overview.svg +52 -0
  7. package/docs/images/connectors/feishu-setup-overview.svg +53 -0
  8. package/docs/images/connectors/slack-setup-overview.svg +51 -0
  9. package/docs/images/connectors/telegram-setup-overview.svg +55 -0
  10. package/docs/images/connectors/whatsapp-setup-overview.svg +51 -0
  11. package/docs/images/lingzhu/lingzhu-openclaw-config.svg +17 -0
  12. package/docs/images/lingzhu/lingzhu-platform-values.svg +16 -0
  13. package/docs/images/lingzhu/lingzhu-settings-overview.svg +30 -0
  14. package/docs/images/qq/tencent-cloud-qq-chat.png +0 -0
  15. package/docs/images/qq/tencent-cloud-qq-register.png +0 -0
  16. package/docs/images/quickstart/00-home.png +0 -0
  17. package/docs/images/quickstart/01-start-research.png +0 -0
  18. package/docs/images/quickstart/02-list-quest.png +0 -0
  19. package/docs/zh/00_QUICK_START.md +22 -0
  20. package/docs/zh/01_SETTINGS_REFERENCE.md +14 -5
  21. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  22. package/install.sh +120 -4
  23. package/package.json +8 -4
  24. package/pyproject.toml +1 -1
  25. package/src/deepscientist/__init__.py +1 -1
  26. package/src/deepscientist/artifact/service.py +1 -1
  27. package/src/deepscientist/bash_exec/monitor.py +23 -4
  28. package/src/deepscientist/bash_exec/runtime.py +3 -0
  29. package/src/deepscientist/bash_exec/service.py +132 -4
  30. package/src/deepscientist/bridges/base.py +12 -20
  31. package/src/deepscientist/bridges/connectors.py +2 -1
  32. package/src/deepscientist/channels/discord_gateway.py +27 -4
  33. package/src/deepscientist/channels/feishu_long_connection.py +41 -3
  34. package/src/deepscientist/channels/qq.py +524 -64
  35. package/src/deepscientist/channels/qq_gateway.py +24 -5
  36. package/src/deepscientist/channels/relay.py +429 -90
  37. package/src/deepscientist/channels/slack_socket.py +31 -7
  38. package/src/deepscientist/channels/telegram_polling.py +27 -3
  39. package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
  40. package/src/deepscientist/cli.py +31 -1
  41. package/src/deepscientist/config/models.py +13 -43
  42. package/src/deepscientist/config/service.py +216 -157
  43. package/src/deepscientist/connector_profiles.py +346 -0
  44. package/src/deepscientist/connector_runtime.py +88 -43
  45. package/src/deepscientist/daemon/api/handlers.py +53 -16
  46. package/src/deepscientist/daemon/api/router.py +2 -2
  47. package/src/deepscientist/daemon/app.py +747 -228
  48. package/src/deepscientist/mcp/server.py +60 -7
  49. package/src/deepscientist/migration.py +114 -0
  50. package/src/deepscientist/network.py +78 -0
  51. package/src/deepscientist/prompts/builder.py +50 -4
  52. package/src/deepscientist/qq_profiles.py +186 -0
  53. package/src/deepscientist/quest/service.py +1 -1
  54. package/src/deepscientist/skills/installer.py +77 -1
  55. package/src/prompts/connectors/qq.md +42 -2
  56. package/src/prompts/system.md +162 -6
  57. package/src/skills/analysis-campaign/SKILL.md +19 -5
  58. package/src/skills/baseline/SKILL.md +66 -31
  59. package/src/skills/decision/SKILL.md +1 -1
  60. package/src/skills/experiment/SKILL.md +11 -5
  61. package/src/skills/finalize/SKILL.md +1 -1
  62. package/src/skills/idea/SKILL.md +246 -4
  63. package/src/skills/intake-audit/SKILL.md +1 -1
  64. package/src/skills/rebuttal/SKILL.md +1 -1
  65. package/src/skills/review/SKILL.md +1 -1
  66. package/src/skills/scout/SKILL.md +1 -1
  67. package/src/skills/write/SKILL.md +152 -2
  68. package/src/tui/package.json +1 -1
  69. package/src/ui/dist/assets/{AiManusChatView-CZpg376x.js → AiManusChatView-BGLArZRn.js} +14 -37
  70. package/src/ui/dist/assets/{AnalysisPlugin-CtHA22g3.js → AnalysisPlugin-BgDGSigG.js} +1 -1
  71. package/src/ui/dist/assets/{AutoFigurePlugin-BSWmLMmF.js → AutoFigurePlugin-B65HD7L4.js} +5 -5
  72. package/src/ui/dist/assets/{CliPlugin-CJ7jdm_s.js → CliPlugin-CUqgsFHC.js} +17 -110
  73. package/src/ui/dist/assets/{CodeEditorPlugin-DhInVGFf.js → CodeEditorPlugin-CF5EdvaS.js} +8 -8
  74. package/src/ui/dist/assets/{CodeViewerPlugin-D1n8S9r5.js → CodeViewerPlugin-DEeU063D.js} +5 -5
  75. package/src/ui/dist/assets/{DocViewerPlugin-C4XM_kqk.js → DocViewerPlugin-Df-FuDlZ.js} +3 -3
  76. package/src/ui/dist/assets/{GitDiffViewerPlugin-W6kS9r6v.js → GitDiffViewerPlugin-RAnNaRxM.js} +1 -1
  77. package/src/ui/dist/assets/{ImageViewerPlugin-DPeUx_Oz.js → ImageViewerPlugin-DXJ0ZJGg.js} +5 -5
  78. package/src/ui/dist/assets/{LabCopilotPanel-eAelUaub.js → LabCopilotPanel-BlO-sKsj.js} +10 -10
  79. package/src/ui/dist/assets/{LabPlugin-BbOrBxKY.js → LabPlugin-BajPZW5v.js} +1 -1
  80. package/src/ui/dist/assets/{LatexPlugin-C-HhkVXY.js → LatexPlugin-F1OEol8D.js} +7 -7
  81. package/src/ui/dist/assets/{MarkdownViewerPlugin-BDIzIBfh.js → MarkdownViewerPlugin-MhUupqwT.js} +4 -4
  82. package/src/ui/dist/assets/{MarketplacePlugin-DAOJphwr.js → MarketplacePlugin-DxhIEsv0.js} +3 -3
  83. package/src/ui/dist/assets/{NotebookEditor-BsoMvDoU.js → NotebookEditor-q7TkhewC.js} +1 -1
  84. package/src/ui/dist/assets/{PdfLoader-fiC7RtHf.js → PdfLoader-B8ZOTKFc.js} +1 -1
  85. package/src/ui/dist/assets/{PdfMarkdownPlugin-C5OxZBFK.js → PdfMarkdownPlugin-xFPvzvWh.js} +3 -3
  86. package/src/ui/dist/assets/{PdfViewerPlugin-CAbxQebk.js → PdfViewerPlugin-EjEcsIB8.js} +10 -10
  87. package/src/ui/dist/assets/{SearchPlugin-SE33Lb9B.js → SearchPlugin-ixY-1lgW.js} +1 -1
  88. package/src/ui/dist/assets/{Stepper-0Av7GfV7.js → Stepper-gYFK2Pgz.js} +1 -1
  89. package/src/ui/dist/assets/{TextViewerPlugin-Daf2gJDI.js → TextViewerPlugin-Cym6pv_n.js} +4 -4
  90. package/src/ui/dist/assets/{VNCViewer-BKrMUIOX.js → VNCViewer-BPmIHcmK.js} +9 -9
  91. package/src/ui/dist/assets/{bibtex-JBdOEe45.js → bibtex-Btv6Wi7f.js} +1 -1
  92. package/src/ui/dist/assets/{code-B0TDFCZz.js → code-BlG7g85c.js} +1 -1
  93. package/src/ui/dist/assets/{file-content-3YtrSacz.js → file-content-DBT5OfTZ.js} +1 -1
  94. package/src/ui/dist/assets/{file-diff-panel-CJEg5OG1.js → file-diff-panel-BWXYzqHk.js} +1 -1
  95. package/src/ui/dist/assets/{file-socket-CYQYdmB1.js → file-socket-wDlx6byM.js} +1 -1
  96. package/src/ui/dist/assets/{file-utils-Cd1C9Ppl.js → file-utils-Ba3nJmH0.js} +1 -1
  97. package/src/ui/dist/assets/{image-B33ctrvC.js → image-BwtCyguk.js} +1 -1
  98. package/src/ui/dist/assets/{index-BNQWqmJ2.js → index-B-2scqCJ.js} +11 -11
  99. package/src/ui/dist/assets/{index-BVXsmS7V.js → index-Bz5AaWL7.js} +52383 -51440
  100. package/src/ui/dist/assets/{index-Buw_N1VQ.js → index-CfRpE209.js} +2 -2
  101. package/src/ui/dist/assets/{index-9CLPVeZh.js → index-DcqvKzeJ.js} +1 -1
  102. package/src/ui/dist/assets/{index-SwmFAld3.css → index-DpMZw8aM.css} +49 -2
  103. package/src/ui/dist/assets/{message-square-D0cUJ9yU.js → message-square-BnlyWVH0.js} +1 -1
  104. package/src/ui/dist/assets/{monaco-UZLYkp2n.js → monaco-CXe0pAVe.js} +1 -1
  105. package/src/ui/dist/assets/{popover-CTeiY-dK.js → popover-BCHmVhHj.js} +1 -1
  106. package/src/ui/dist/assets/{project-sync-Dbs01Xky.js → project-sync-Brk6kaOD.js} +1 -1
  107. package/src/ui/dist/assets/{sigma-CM08S-xT.js → sigma-D72eSUep.js} +1 -1
  108. package/src/ui/dist/assets/{tooltip-pDtzvU9p.js → tooltip-BMWd0dqX.js} +1 -1
  109. package/src/ui/dist/assets/{trash-YvPCP-da.js → trash-BIt_eWIS.js} +1 -1
  110. package/src/ui/dist/assets/{useCliAccess-Bavi74Ac.js → useCliAccess-N1hkTRrR.js} +1 -1
  111. package/src/ui/dist/assets/{useFileDiffOverlay-CVXY6oeg.js → useFileDiffOverlay-DPRPv6rv.js} +1 -1
  112. package/src/ui/dist/assets/{wrap-text-Cf4flRW7.js → wrap-text-E5-UheyP.js} +1 -1
  113. package/src/ui/dist/assets/{zoom-out-Hb0Z1YpT.js → zoom-out-D4TR-ZZ_.js} +1 -1
  114. package/src/ui/dist/index.html +2 -2
package/bin/ds.js CHANGED
@@ -13,6 +13,7 @@ const pyprojectToml = fs.readFileSync(path.join(repoRoot, 'pyproject.toml'), 'ut
13
13
  const pythonCandidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
14
14
  const requiredPythonSpec = parseRequiredPythonSpec(pyprojectToml);
15
15
  const minimumPythonVersion = parseMinimumPythonVersion(requiredPythonSpec);
16
+ const launcherWrapperCommands = ['ds', 'ds-cli', 'research', 'resear'];
16
17
  const pythonCommands = new Set([
17
18
  'init',
18
19
  'new',
@@ -34,7 +35,6 @@ const pythonCommands = new Set([
34
35
  ]);
35
36
  const UPDATE_PACKAGE_NAME = String(packageJson.name || '@researai/deepscientist').trim() || '@researai/deepscientist';
36
37
  const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
37
- const UPDATE_PROMPT_TTL_MS = 12 * 60 * 60 * 1000;
38
38
 
39
39
  const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode']);
40
40
 
@@ -46,6 +46,9 @@ Usage:
46
46
  ds update
47
47
  ds update --check
48
48
  ds update --yes
49
+ ds migrate /data/DeepScientist
50
+ ds --here
51
+ ds --here doctor
49
52
  ds --tui
50
53
  ds --both
51
54
  ds --host 0.0.0.0 --port 21000
@@ -67,6 +70,7 @@ Launcher flags:
67
70
  --stop Stop the managed daemon
68
71
  --restart Restart the managed daemon
69
72
  --home <path> Use a custom DeepScientist home
73
+ --here Use the current working directory as DeepScientist home
70
74
  --quest-id <id> Open the TUI on one quest directly
71
75
 
72
76
  Update:
@@ -74,6 +78,10 @@ Update:
74
78
  ds update --check Print structured update status
75
79
  ds update --yes Install the latest npm release immediately
76
80
 
81
+ Migration:
82
+ ds migrate <target> Move the DeepScientist home/install root to a new absolute path
83
+ ds migrate <target> --yes --restart
84
+
77
85
  Runtime:
78
86
  DeepScientist uses uv to manage a locked local Python runtime.
79
87
  If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
@@ -176,10 +184,7 @@ function detectInstallMode(rootPath = repoRoot) {
176
184
  }
177
185
 
178
186
  function updateManualCommand(installMode) {
179
- if (installMode === 'npm-package') {
180
- return `npm install -g ${UPDATE_PACKAGE_NAME}@latest`;
181
- }
182
- return 'git pull && bash install.sh';
187
+ return `npm install -g ${UPDATE_PACKAGE_NAME}@latest`;
183
188
  }
184
189
 
185
190
  function updateSupportSummary(installMode, npmBinary, launcherPath) {
@@ -194,14 +199,14 @@ function updateSupportSummary(installMode, npmBinary, launcherPath) {
194
199
  return {
195
200
  canCheck: true,
196
201
  canSelfUpdate: false,
197
- reason: 'This DeepScientist installation comes from a source checkout and should be updated from Git.',
202
+ reason: 'Self-update is disabled for this installation. Use the npm command below to install the latest release build.',
198
203
  };
199
204
  }
200
205
  if (!launcherPath || !fs.existsSync(launcherPath)) {
201
206
  return {
202
207
  canCheck: true,
203
208
  canSelfUpdate: false,
204
- reason: 'The launcher entrypoint could not be resolved.',
209
+ reason: 'Self-update is disabled because the launcher entrypoint could not be resolved. Use the npm command below instead.',
205
210
  };
206
211
  }
207
212
  return {
@@ -279,14 +284,17 @@ function buildUpdateStatus(home, statePatch = {}) {
279
284
  const support = updateSupportSummary(installMode, npmBinary, launcherPath);
280
285
  const currentVersion = normalizeVersion(state.current_version || packageJson.version);
281
286
  const latestVersion = normalizeVersion(state.latest_version || '');
287
+ const promptedVersion = normalizeVersion(state.last_prompted_version || '');
282
288
  const updateAvailable = Boolean(latestVersion) && compareVersions(latestVersion, currentVersion) > 0;
283
289
  const skippedVersion = normalizeVersion(state.last_skipped_version || '');
290
+ const promptedCurrentTarget = Boolean(updateAvailable && promptedVersion && promptedVersion === latestVersion);
284
291
  const skippedCurrentTarget = Boolean(updateAvailable && skippedVersion && skippedVersion === latestVersion);
285
292
  const promptRecommended =
286
293
  Boolean(updateAvailable)
287
294
  && !Boolean(state.busy)
295
+ && !promptedCurrentTarget
288
296
  && !skippedCurrentTarget
289
- && isExpired(state.last_prompted_at || state.last_deferred_at, UPDATE_PROMPT_TTL_MS);
297
+ ;
290
298
 
291
299
  return {
292
300
  ok: true,
@@ -302,6 +310,7 @@ function buildUpdateStatus(home, statePatch = {}) {
302
310
  last_checked_at: state.last_checked_at || null,
303
311
  last_check_error: state.last_check_error || null,
304
312
  last_prompted_at: state.last_prompted_at || null,
313
+ last_prompted_version: promptedVersion || null,
305
314
  last_deferred_at: state.last_deferred_at || null,
306
315
  last_skipped_version: skippedVersion || null,
307
316
  last_update_started_at: state.last_update_started_at || null,
@@ -345,10 +354,12 @@ function checkForUpdates(home, { force = false, timeoutMs = 3500 } = {}) {
345
354
  }
346
355
 
347
356
  function markUpdateDeferred(home, version) {
357
+ const normalizedVersion = normalizeVersion(version || readUpdateState(home).latest_version || '');
348
358
  const patched = mergeUpdateState(home, {
349
359
  last_prompted_at: new Date().toISOString(),
350
360
  last_deferred_at: new Date().toISOString(),
351
- latest_version: normalizeVersion(version || readUpdateState(home).latest_version || '') || null,
361
+ last_prompted_version: normalizedVersion || null,
362
+ latest_version: normalizedVersion || null,
352
363
  });
353
364
  return buildUpdateStatus(home, patched);
354
365
  }
@@ -357,6 +368,7 @@ function markUpdateSkipped(home, version) {
357
368
  const normalized = normalizeVersion(version);
358
369
  const patched = mergeUpdateState(home, {
359
370
  last_prompted_at: new Date().toISOString(),
371
+ last_prompted_version: normalized || null,
360
372
  last_skipped_version: normalized || null,
361
373
  });
362
374
  return buildUpdateStatus(home, patched);
@@ -384,6 +396,9 @@ function resolveHome(args) {
384
396
  if (index >= 0 && index + 1 < args.length) {
385
397
  return path.resolve(args[index + 1]);
386
398
  }
399
+ if (args.includes('--here')) {
400
+ return process.cwd();
401
+ }
387
402
  if (process.env.DEEPSCIENTIST_HOME) {
388
403
  return path.resolve(process.env.DEEPSCIENTIST_HOME);
389
404
  }
@@ -554,12 +569,14 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
554
569
  renderKeyValueRows([
555
570
  ['ds --port 21000', 'Change the web port'],
556
571
  ['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
572
+ ['ds --here', 'Use the current directory as home'],
557
573
  ['ds --both', 'Start web + TUI together'],
558
574
  ['ds --tui', 'Start the terminal workspace only'],
559
575
  ['ds --no-browser', 'Do not auto-open the browser'],
560
576
  ['ds --status', 'Show daemon health as JSON'],
561
577
  ['ds --restart', 'Restart the managed daemon'],
562
578
  ['ds --stop', 'Stop the managed daemon'],
579
+ ['ds migrate /data/DeepScientist', 'Move the full home/install root safely'],
563
580
  ['ds --help', 'Show the full launcher help'],
564
581
  ]);
565
582
  console.log('');
@@ -630,6 +647,7 @@ function printLaunchCard({
630
647
  console.log(centerText(browserLine, width));
631
648
  console.log(centerText(nextStep, width));
632
649
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
650
+ console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
633
651
  console.log('');
634
652
  renderLaunchHints({ home, url, bindUrl, pythonSelection });
635
653
  }
@@ -894,6 +912,22 @@ Flags:
894
912
  `);
895
913
  }
896
914
 
915
+ function printMigrateHelp() {
916
+ console.log(`DeepScientist migrate
917
+
918
+ Usage:
919
+ ds migrate /absolute/target/path
920
+ ds migrate /absolute/target/path --yes
921
+ ds migrate /absolute/target/path --restart
922
+ ds migrate /absolute/target/path --home /current/source/path
923
+
924
+ Flags:
925
+ --yes Skip the interactive double-confirmation prompt
926
+ --restart Start the managed daemon again from the migrated home
927
+ --home <path> Override the current DeepScientist source home/root
928
+ `);
929
+ }
930
+
897
931
  function parseUpdateArgs(argv) {
898
932
  const args = [...argv];
899
933
  if (args[0] === 'update') {
@@ -950,6 +984,40 @@ function parseUpdateArgs(argv) {
950
984
  };
951
985
  }
952
986
 
987
+ function parseMigrateArgs(argv) {
988
+ const args = [...argv];
989
+ if (args[0] === 'migrate') {
990
+ args.shift();
991
+ }
992
+ let home = null;
993
+ let target = null;
994
+ let yes = false;
995
+ let restart = false;
996
+
997
+ for (let index = 0; index < args.length; index += 1) {
998
+ const arg = args[index];
999
+ if (arg === '--yes') yes = true;
1000
+ else if (arg === '--restart') restart = true;
1001
+ else if (arg === '--home' && args[index + 1]) home = path.resolve(expandUserPath(args[++index]));
1002
+ else if (arg === '--help' || arg === '-h') return { help: true };
1003
+ else if (arg.startsWith('--')) return null;
1004
+ else if (!target) target = path.resolve(expandUserPath(arg));
1005
+ else return null;
1006
+ }
1007
+
1008
+ if (!target) {
1009
+ return null;
1010
+ }
1011
+
1012
+ return {
1013
+ help: false,
1014
+ home,
1015
+ target,
1016
+ yes,
1017
+ restart,
1018
+ };
1019
+ }
1020
+
953
1021
  function findFirstPositionalArg(args) {
954
1022
  for (let index = 0; index < args.length; index += 1) {
955
1023
  const arg = args[index];
@@ -973,6 +1041,188 @@ function realpathOrSelf(targetPath) {
973
1041
  }
974
1042
  }
975
1043
 
1044
+ function isPathEqual(left, right) {
1045
+ return realpathOrSelf(path.resolve(left)) === realpathOrSelf(path.resolve(right));
1046
+ }
1047
+
1048
+ function isPathInside(candidatePath, parentPath) {
1049
+ const candidate = realpathOrSelf(path.resolve(candidatePath));
1050
+ const parent = realpathOrSelf(path.resolve(parentPath));
1051
+ if (candidate === parent) {
1052
+ return false;
1053
+ }
1054
+ const relative = path.relative(parent, candidate);
1055
+ return Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
1056
+ }
1057
+
1058
+ function buildInstalledWrapperScript() {
1059
+ return [
1060
+ '#!/usr/bin/env bash',
1061
+ 'set -euo pipefail',
1062
+ 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
1063
+ 'HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"',
1064
+ 'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
1065
+ ' export DEEPSCIENTIST_HOME="$HOME_DIR"',
1066
+ 'fi',
1067
+ 'NODE_BIN="${DEEPSCIENTIST_NODE:-node}"',
1068
+ 'exec "$NODE_BIN" "$SCRIPT_DIR/ds.js" "$@"',
1069
+ '',
1070
+ ].join('\n');
1071
+ }
1072
+
1073
+ function buildGlobalWrapperScript({ installDir, home, commandName }) {
1074
+ return [
1075
+ '#!/usr/bin/env bash',
1076
+ 'set -euo pipefail',
1077
+ 'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
1078
+ ` export DEEPSCIENTIST_HOME="${home}"`,
1079
+ 'fi',
1080
+ `exec "${path.join(installDir, 'bin', commandName)}" "$@"`,
1081
+ '',
1082
+ ].join('\n');
1083
+ }
1084
+
1085
+ function writeExecutableScript(targetPath, content) {
1086
+ ensureDir(path.dirname(targetPath));
1087
+ fs.writeFileSync(targetPath, content, { encoding: 'utf8', mode: 0o755 });
1088
+ fs.chmodSync(targetPath, 0o755);
1089
+ }
1090
+
1091
+ function repairMigratedInstallWrappers(targetHome) {
1092
+ const installBinDir = path.join(targetHome, 'cli', 'bin');
1093
+ if (!fs.existsSync(installBinDir)) {
1094
+ return;
1095
+ }
1096
+ const content = buildInstalledWrapperScript();
1097
+ for (const commandName of launcherWrapperCommands) {
1098
+ const wrapperPath = path.join(installBinDir, commandName);
1099
+ if (!fs.existsSync(wrapperPath)) {
1100
+ continue;
1101
+ }
1102
+ writeExecutableScript(wrapperPath, content);
1103
+ }
1104
+ }
1105
+
1106
+ function candidateWrapperPathsForCommand(commandName) {
1107
+ const directories = String(process.env.PATH || '')
1108
+ .split(path.delimiter)
1109
+ .filter(Boolean);
1110
+ const candidates = [];
1111
+ for (const directory of directories) {
1112
+ candidates.push(path.join(directory, commandName));
1113
+ if (process.platform === 'win32') {
1114
+ candidates.push(path.join(directory, `${commandName}.cmd`));
1115
+ candidates.push(path.join(directory, `${commandName}.ps1`));
1116
+ }
1117
+ }
1118
+ return candidates;
1119
+ }
1120
+
1121
+ function rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome }) {
1122
+ if (process.platform === 'win32') {
1123
+ return [];
1124
+ }
1125
+ const rewritten = [];
1126
+ const sourceInstallDir = path.join(sourceHome, 'cli');
1127
+ const targetInstallDir = path.join(targetHome, 'cli');
1128
+ for (const commandName of launcherWrapperCommands) {
1129
+ for (const candidate of candidateWrapperPathsForCommand(commandName)) {
1130
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
1131
+ continue;
1132
+ }
1133
+ let text = '';
1134
+ try {
1135
+ text = fs.readFileSync(candidate, 'utf8');
1136
+ } catch {
1137
+ continue;
1138
+ }
1139
+ if (!text.includes(sourceInstallDir) && !text.includes(sourceHome)) {
1140
+ continue;
1141
+ }
1142
+ writeExecutableScript(
1143
+ candidate,
1144
+ buildGlobalWrapperScript({
1145
+ installDir: targetInstallDir,
1146
+ home: targetHome,
1147
+ commandName,
1148
+ })
1149
+ );
1150
+ rewritten.push(candidate);
1151
+ }
1152
+ }
1153
+ return rewritten;
1154
+ }
1155
+
1156
+ function scheduleDeferredSourceCleanup({ sourceHome, targetHome }) {
1157
+ const logPath = path.join(targetHome, 'logs', 'migrate-cleanup.log');
1158
+ ensureDir(path.dirname(logPath));
1159
+ const helperScript = [
1160
+ "const fs = require('node:fs');",
1161
+ "const { setTimeout: sleep } = require('node:timers/promises');",
1162
+ 'const parentPid = Number(process.argv[1]);',
1163
+ 'const sourceHome = process.argv[2];',
1164
+ 'const logPath = process.argv[3];',
1165
+ '(async () => {',
1166
+ ' for (let attempt = 0; attempt < 300; attempt += 1) {',
1167
+ ' try {',
1168
+ ' process.kill(parentPid, 0);',
1169
+ ' await sleep(100);',
1170
+ ' continue;',
1171
+ ' } catch {',
1172
+ ' break;',
1173
+ ' }',
1174
+ ' }',
1175
+ ' try {',
1176
+ ' fs.rmSync(sourceHome, { recursive: true, force: true });',
1177
+ ' } catch (error) {',
1178
+ " fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${error instanceof Error ? error.message : String(error)}\\n`, 'utf8');",
1179
+ ' process.exit(1);',
1180
+ ' }',
1181
+ '})();',
1182
+ ].join('\n');
1183
+ const child = spawn(process.execPath, ['-e', helperScript, String(process.pid), sourceHome, logPath], {
1184
+ detached: true,
1185
+ stdio: 'ignore',
1186
+ env: process.env,
1187
+ });
1188
+ child.unref();
1189
+ }
1190
+
1191
+ async function promptMigrationConfirmation({ sourceHome, targetHome }) {
1192
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1193
+ throw new Error('DeepScientist migration needs a TTY for confirmation. Re-run with `--yes` to continue non-interactively.');
1194
+ }
1195
+ console.log('');
1196
+ console.log('DeepScientist home migration');
1197
+ console.log('');
1198
+ console.log(`From: ${sourceHome}`);
1199
+ console.log(`To: ${targetHome}`);
1200
+ console.log('');
1201
+ console.log('This will stop the managed daemon, copy the full DeepScientist root, verify the copy, update launcher wrappers, and delete the old path after success.');
1202
+ const ask = (question) => new Promise((resolve) => {
1203
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1204
+ rl.question(question, (answer) => {
1205
+ rl.close();
1206
+ resolve(String(answer || '').trim());
1207
+ });
1208
+ });
1209
+ const first = await ask('Type YES to continue: ');
1210
+ if (first !== 'YES') {
1211
+ return false;
1212
+ }
1213
+ const second = await ask('Type MIGRATE to confirm old-path deletion after a successful copy: ');
1214
+ return second === 'MIGRATE';
1215
+ }
1216
+
1217
+ function printMigrationSummary({ sourceHome, targetHome, restart }) {
1218
+ console.log('');
1219
+ console.log('DeepScientist migrate');
1220
+ console.log('');
1221
+ console.log(`Source: ${sourceHome}`);
1222
+ console.log(`Target: ${targetHome}`);
1223
+ console.log(`Restart: ${restart ? 'yes' : 'no'}`);
1224
+ }
1225
+
976
1226
  function pythonMeetsMinimum(probe) {
977
1227
  if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
978
1228
  return false;
@@ -1784,6 +2034,9 @@ function normalizePythonCliArgs(args, home) {
1784
2034
  index += 1;
1785
2035
  continue;
1786
2036
  }
2037
+ if (arg === '--here') {
2038
+ continue;
2039
+ }
1787
2040
  normalized.push(arg);
1788
2041
  }
1789
2042
  return ['--home', home, ...normalized];
@@ -2242,96 +2495,11 @@ function runNpmInstallLatest(home, npmBinary) {
2242
2495
  };
2243
2496
  }
2244
2497
 
2245
- async function promptUpdateAction(status) {
2246
- const options = [
2247
- {
2248
- value: 'update',
2249
- label: status.can_self_update ? 'Update now' : 'Show manual update',
2250
- },
2251
- { value: 'later', label: 'Remind me later' },
2252
- { value: 'skip', label: 'Skip this version' },
2253
- ];
2254
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
2255
- return 'later';
2256
- }
2257
- return new Promise((resolve) => {
2258
- let selected = 1;
2259
- const lines = [
2260
- '',
2261
- 'A new DeepScientist version is available.',
2262
- '',
2263
- `Current: ${status.current_version}`,
2264
- `Latest: ${status.latest_version || 'unknown'}`,
2265
- '',
2266
- status.can_self_update
2267
- ? 'What do you want to do?'
2268
- : 'Self-update is not available for this installation. Choose an action:',
2269
- ];
2270
-
2271
- const cleanup = () => {
2272
- process.stdin.off('keypress', onKeypress);
2273
- if (process.stdin.isTTY) {
2274
- process.stdin.setRawMode(false);
2275
- }
2276
- process.stdin.pause();
2277
- console.log('');
2278
- };
2279
-
2280
- const render = () => {
2281
- console.clear();
2282
- for (const line of lines) {
2283
- console.log(line);
2284
- }
2285
- for (let index = 0; index < options.length; index += 1) {
2286
- const option = options[index];
2287
- console.log(`${index === selected ? '>' : ' '} ${option.label}`);
2288
- }
2289
- console.log('');
2290
- console.log('Use ↑/↓ and Enter.');
2291
- };
2292
-
2293
- const onKeypress = (_str, key) => {
2294
- if (key?.name === 'up') {
2295
- selected = (selected - 1 + options.length) % options.length;
2296
- render();
2297
- return;
2298
- }
2299
- if (key?.name === 'down') {
2300
- selected = (selected + 1) % options.length;
2301
- render();
2302
- return;
2303
- }
2304
- if (key?.name === 'return') {
2305
- const choice = options[selected].value;
2306
- cleanup();
2307
- resolve(choice);
2308
- return;
2309
- }
2310
- if (key?.ctrl && key?.name === 'c') {
2311
- cleanup();
2312
- resolve('later');
2313
- }
2314
- };
2315
-
2316
- readline.emitKeypressEvents(process.stdin);
2317
- process.stdin.setRawMode(true);
2318
- process.stdin.resume();
2319
- process.stdin.on('keypress', onKeypress);
2320
- render();
2321
- });
2322
- }
2323
-
2324
2498
  function printUpdateStatus(status, { compact = false } = {}) {
2325
2499
  if (compact) {
2326
2500
  if (status.update_available) {
2327
- console.log(
2328
- `DeepScientist update available: ${status.current_version} -> ${status.latest_version}`
2329
- );
2330
- if (status.can_self_update) {
2331
- console.log('Run `ds update --yes` to install it.');
2332
- } else {
2333
- console.log(`Manual update: ${status.manual_update_command}`);
2334
- }
2501
+ console.log(`DeepScientist update available: ${status.current_version} -> ${status.latest_version}`);
2502
+ console.log(`Update with: ${status.manual_update_command}`);
2335
2503
  return;
2336
2504
  }
2337
2505
  console.log(`DeepScientist is up to date (${status.current_version}).`);
@@ -2344,16 +2512,15 @@ function printUpdateStatus(status, { compact = false } = {}) {
2344
2512
  ['Latest', status.latest_version || 'unknown'],
2345
2513
  ['Available', status.update_available ? 'yes' : 'no'],
2346
2514
  ['Install mode', status.install_mode],
2347
- ['Self-update', status.can_self_update ? 'supported' : 'manual-only'],
2348
2515
  ['Last checked', status.last_checked_at || 'never'],
2349
2516
  ]);
2350
2517
  if (status.last_check_error) {
2351
2518
  console.log('');
2352
2519
  console.log(`Version check error: ${status.last_check_error}`);
2353
2520
  }
2354
- if (!status.can_self_update) {
2521
+ if (status.update_available || status.manual_update_command) {
2355
2522
  console.log('');
2356
- console.log(`Manual update command: ${status.manual_update_command}`);
2523
+ console.log(`Update command: ${status.manual_update_command}`);
2357
2524
  if (status.reason) {
2358
2525
  console.log(status.reason);
2359
2526
  }
@@ -2381,26 +2548,6 @@ function spawnDetachedNode(args, options = {}) {
2381
2548
  return child;
2382
2549
  }
2383
2550
 
2384
- async function restartIntoUpdatedLauncher(rawArgs) {
2385
- const launcherPath = resolveLauncherPath();
2386
- if (!launcherPath) {
2387
- throw new Error('Could not resolve the DeepScientist launcher after the update.');
2388
- }
2389
- const args = [launcherPath, '--skip-update-check', ...rawArgs.filter((item) => item !== '--skip-update-check')];
2390
- const child = spawn(process.execPath, args, {
2391
- cwd: repoRoot,
2392
- stdio: 'inherit',
2393
- env: process.env,
2394
- });
2395
- await new Promise((resolve, reject) => {
2396
- child.on('error', reject);
2397
- child.on('exit', (code) => {
2398
- process.exit(code ?? 0);
2399
- resolve();
2400
- });
2401
- });
2402
- }
2403
-
2404
2551
  async function performSelfUpdate(home, options = {}) {
2405
2552
  const status = checkForUpdates(home, { force: true });
2406
2553
  if (!status.update_available) {
@@ -2566,42 +2713,8 @@ async function maybeHandleStartupUpdate(home, rawArgs, options = {}) {
2566
2713
  return false;
2567
2714
  }
2568
2715
 
2569
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
2570
- printUpdateStatus(status, { compact: true });
2571
- mergeUpdateState(home, {
2572
- last_prompted_at: new Date().toISOString(),
2573
- });
2574
- return false;
2575
- }
2576
-
2577
- const action = await promptUpdateAction(status);
2578
- if (action === 'later') {
2579
- markUpdateDeferred(home, status.latest_version);
2580
- return false;
2581
- }
2582
- if (action === 'skip') {
2583
- markUpdateSkipped(home, status.latest_version);
2584
- return false;
2585
- }
2586
- if (action === 'update' && !status.can_self_update) {
2587
- console.log(`Manual update required: ${status.manual_update_command}`);
2588
- if (status.reason) {
2589
- console.log(status.reason);
2590
- }
2591
- markUpdateDeferred(home, status.latest_version);
2592
- return false;
2593
- }
2594
- if (action === 'update') {
2595
- console.log(`Updating DeepScientist ${status.current_version} -> ${status.latest_version} ...`);
2596
- const result = await performSelfUpdate(home, { restartDaemon: false });
2597
- if (!result.ok) {
2598
- console.error(result.message);
2599
- return false;
2600
- }
2601
- console.log(result.message);
2602
- await restartIntoUpdatedLauncher(rawArgs);
2603
- return true;
2604
- }
2716
+ printUpdateStatus(status, { compact: true });
2717
+ markUpdateDeferred(home, status.latest_version);
2605
2718
  return false;
2606
2719
  }
2607
2720
 
@@ -2968,32 +3081,137 @@ async function updateMain(rawArgs) {
2968
3081
  printUpdateStatus(status, { compact: true });
2969
3082
  process.exit(0);
2970
3083
  }
3084
+ printUpdateStatus(status, { compact: true });
3085
+ process.exit(0);
3086
+ }
2971
3087
 
2972
- const action = await promptUpdateAction(status);
2973
- if (action === 'later') {
2974
- markUpdateDeferred(home, status.latest_version);
2975
- console.log('Update reminder deferred.');
2976
- process.exit(0);
3088
+ async function migrateMain(rawArgs) {
3089
+ const options = parseMigrateArgs(rawArgs);
3090
+ if (!options) {
3091
+ printMigrateHelp();
3092
+ process.exit(1);
2977
3093
  }
2978
- if (action === 'skip') {
2979
- markUpdateSkipped(home, status.latest_version);
2980
- console.log(`Skipped ${status.latest_version}.`);
3094
+ if (options.help) {
3095
+ printMigrateHelp();
2981
3096
  process.exit(0);
2982
3097
  }
2983
- if (!status.can_self_update) {
2984
- console.log(`Manual update command: ${status.manual_update_command}`);
2985
- if (status.reason) {
2986
- console.log(status.reason);
3098
+
3099
+ const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
3100
+ const targetHome = path.resolve(options.target);
3101
+ if (!fs.existsSync(sourceHome)) {
3102
+ console.error(`DeepScientist source path does not exist: ${sourceHome}`);
3103
+ process.exit(1);
3104
+ }
3105
+ if (isPathEqual(sourceHome, targetHome)) {
3106
+ console.error('DeepScientist source and target paths are identical. Choose a different migration target.');
3107
+ process.exit(1);
3108
+ }
3109
+ if (isPathInside(targetHome, sourceHome) || isPathInside(sourceHome, targetHome)) {
3110
+ console.error('DeepScientist migration requires two separate sibling paths. Do not nest one path inside the other.');
3111
+ process.exit(1);
3112
+ }
3113
+ if (fs.existsSync(targetHome)) {
3114
+ console.error(`DeepScientist target path already exists: ${targetHome}`);
3115
+ process.exit(1);
3116
+ }
3117
+
3118
+ printMigrationSummary({ sourceHome, targetHome, restart: options.restart });
3119
+ if (!options.yes) {
3120
+ const confirmed = await promptMigrationConfirmation({ sourceHome, targetHome });
3121
+ if (!confirmed) {
3122
+ console.log('DeepScientist migration cancelled.');
3123
+ process.exit(1);
2987
3124
  }
2988
- process.exit(0);
2989
3125
  }
2990
- const payload = await performSelfUpdate(home, {
2991
- host: options.host,
2992
- port: options.port,
2993
- restartDaemon: options.restartDaemon,
2994
- });
2995
- console.log(payload.message);
2996
- process.exit(payload.ok ? 0 : 1);
3126
+
3127
+ const state = readDaemonState(sourceHome);
3128
+ const configured = readConfiguredUiAddressFromFile(sourceHome);
3129
+ const url = state?.url || browserUiUrl(configured.host, configured.port);
3130
+ const health = await fetchHealth(url);
3131
+ if (state || healthMatchesHome({ health, home: sourceHome })) {
3132
+ await stopDaemon(sourceHome);
3133
+ } else if (health && health.status === 'ok') {
3134
+ console.log(`Skipping daemon stop because ${url} belongs to another DeepScientist home.`);
3135
+ }
3136
+
3137
+ const pythonRuntime = ensurePythonRuntime(sourceHome);
3138
+ const runtimePython = pythonRuntime.runtimePython;
3139
+ const result = runPythonCli(
3140
+ runtimePython,
3141
+ ['--home', sourceHome, 'migrate', targetHome],
3142
+ { capture: true, allowFailure: true }
3143
+ );
3144
+ let payload = null;
3145
+ try {
3146
+ payload = JSON.parse(String(result.stdout || '{}'));
3147
+ } catch {
3148
+ payload = null;
3149
+ }
3150
+ if (result.status !== 0 || !payload || payload.ok !== true) {
3151
+ if (result.stdout) {
3152
+ process.stdout.write(result.stdout);
3153
+ if (!String(result.stdout).endsWith('\n')) {
3154
+ process.stdout.write('\n');
3155
+ }
3156
+ }
3157
+ if (result.stderr) {
3158
+ process.stderr.write(result.stderr);
3159
+ if (!String(result.stderr).endsWith('\n')) {
3160
+ process.stderr.write('\n');
3161
+ }
3162
+ }
3163
+ console.error('DeepScientist migration failed.');
3164
+ process.exit(result.status ?? 1);
3165
+ }
3166
+
3167
+ repairMigratedInstallWrappers(targetHome);
3168
+ const rewrittenWrappers = rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome });
3169
+
3170
+ const sourceContainsCurrentInstall = isPathEqual(repoRoot, path.join(sourceHome, 'cli')) || isPathInside(repoRoot, sourceHome);
3171
+ if (sourceContainsCurrentInstall) {
3172
+ scheduleDeferredSourceCleanup({ sourceHome, targetHome });
3173
+ } else {
3174
+ fs.rmSync(sourceHome, { recursive: true, force: true });
3175
+ }
3176
+
3177
+ let restartMessage = 'Restart skipped.';
3178
+ if (options.restart) {
3179
+ const migratedLauncher = path.join(targetHome, 'cli', 'bin', 'ds.js');
3180
+ if (!fs.existsSync(migratedLauncher)) {
3181
+ restartMessage = `Migration succeeded, but restart was skipped because the migrated launcher is missing: ${migratedLauncher}`;
3182
+ } else {
3183
+ const child = spawn(
3184
+ process.execPath,
3185
+ [migratedLauncher, '--home', targetHome, '--daemon-only', '--no-browser', '--skip-update-check'],
3186
+ {
3187
+ cwd: path.join(targetHome, 'cli'),
3188
+ detached: true,
3189
+ stdio: 'ignore',
3190
+ env: process.env,
3191
+ }
3192
+ );
3193
+ child.unref();
3194
+ restartMessage = 'Managed daemon restart scheduled from the migrated home.';
3195
+ }
3196
+ }
3197
+
3198
+ console.log('');
3199
+ console.log('DeepScientist migration completed.');
3200
+ console.log(`New home: ${targetHome}`);
3201
+ if (payload.summary) {
3202
+ console.log(payload.summary);
3203
+ }
3204
+ if (rewrittenWrappers.length > 0) {
3205
+ console.log(`Updated wrappers: ${rewrittenWrappers.join(', ')}`);
3206
+ }
3207
+ console.log(restartMessage);
3208
+ if (sourceContainsCurrentInstall) {
3209
+ console.log(`Old path cleanup has been scheduled: ${sourceHome}`);
3210
+ } else {
3211
+ console.log(`Old path removed: ${sourceHome}`);
3212
+ }
3213
+ console.log(`Use \`ds --home ${targetHome}\` if you want to override the default explicitly.`);
3214
+ process.exit(0);
2997
3215
  }
2998
3216
 
2999
3217
  async function launcherMain(rawArgs) {
@@ -3097,6 +3315,10 @@ async function main() {
3097
3315
  await updateMain(args);
3098
3316
  return;
3099
3317
  }
3318
+ if (positional && positional.value === 'migrate') {
3319
+ await migrateMain(args);
3320
+ return;
3321
+ }
3100
3322
  if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
3101
3323
  await launcherMain(args);
3102
3324
  return;
@@ -3144,9 +3366,11 @@ module.exports = {
3144
3366
  legacyVenvRootPath,
3145
3367
  resolveUvBinary,
3146
3368
  resolveHome,
3369
+ parseMigrateArgs,
3147
3370
  useEditableProjectInstall,
3148
3371
  compareVersions,
3149
3372
  detectInstallMode,
3373
+ updateManualCommand,
3150
3374
  buildUpdateStatus,
3151
3375
  },
3152
3376
  };