@researai/deepscientist 1.5.2 → 1.5.3

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 (93) hide show
  1. package/README.md +22 -0
  2. package/bin/ds.js +384 -0
  3. package/docs/en/00_QUICK_START.md +22 -0
  4. package/docs/zh/00_QUICK_START.md +22 -0
  5. package/install.sh +120 -4
  6. package/package.json +1 -1
  7. package/pyproject.toml +1 -1
  8. package/src/deepscientist/__init__.py +1 -1
  9. package/src/deepscientist/artifact/service.py +1 -1
  10. package/src/deepscientist/bash_exec/monitor.py +23 -4
  11. package/src/deepscientist/bash_exec/runtime.py +3 -0
  12. package/src/deepscientist/bash_exec/service.py +132 -4
  13. package/src/deepscientist/bridges/base.py +10 -19
  14. package/src/deepscientist/channels/discord_gateway.py +25 -2
  15. package/src/deepscientist/channels/feishu_long_connection.py +41 -3
  16. package/src/deepscientist/channels/qq.py +524 -64
  17. package/src/deepscientist/channels/qq_gateway.py +22 -3
  18. package/src/deepscientist/channels/relay.py +429 -90
  19. package/src/deepscientist/channels/slack_socket.py +29 -5
  20. package/src/deepscientist/channels/telegram_polling.py +25 -2
  21. package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
  22. package/src/deepscientist/cli.py +27 -0
  23. package/src/deepscientist/config/models.py +6 -40
  24. package/src/deepscientist/config/service.py +164 -155
  25. package/src/deepscientist/connector_profiles.py +346 -0
  26. package/src/deepscientist/connector_runtime.py +88 -43
  27. package/src/deepscientist/daemon/api/handlers.py +47 -10
  28. package/src/deepscientist/daemon/api/router.py +2 -2
  29. package/src/deepscientist/daemon/app.py +682 -218
  30. package/src/deepscientist/mcp/server.py +60 -7
  31. package/src/deepscientist/migration.py +114 -0
  32. package/src/deepscientist/prompts/builder.py +30 -3
  33. package/src/deepscientist/qq_profiles.py +186 -0
  34. package/src/prompts/connectors/qq.md +42 -2
  35. package/src/prompts/system.md +85 -5
  36. package/src/skills/analysis-campaign/SKILL.md +11 -5
  37. package/src/skills/baseline/SKILL.md +66 -31
  38. package/src/skills/decision/SKILL.md +1 -1
  39. package/src/skills/experiment/SKILL.md +11 -5
  40. package/src/skills/finalize/SKILL.md +1 -1
  41. package/src/skills/idea/SKILL.md +1 -1
  42. package/src/skills/intake-audit/SKILL.md +1 -1
  43. package/src/skills/rebuttal/SKILL.md +1 -1
  44. package/src/skills/review/SKILL.md +1 -1
  45. package/src/skills/scout/SKILL.md +1 -1
  46. package/src/skills/write/SKILL.md +1 -1
  47. package/src/tui/package.json +1 -1
  48. package/src/ui/dist/assets/{AiManusChatView-CZpg376x.js → AiManusChatView-qzChi9uh.js} +14 -37
  49. package/src/ui/dist/assets/{AnalysisPlugin-CtHA22g3.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
  50. package/src/ui/dist/assets/{AutoFigurePlugin-BSWmLMmF.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
  51. package/src/ui/dist/assets/{CliPlugin-CJ7jdm_s.js → CliPlugin-DJJFfVmW.js} +17 -110
  52. package/src/ui/dist/assets/{CodeEditorPlugin-DhInVGFf.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
  53. package/src/ui/dist/assets/{CodeViewerPlugin-D1n8S9r5.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
  54. package/src/ui/dist/assets/{DocViewerPlugin-C4XM_kqk.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
  55. package/src/ui/dist/assets/{GitDiffViewerPlugin-W6kS9r6v.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
  56. package/src/ui/dist/assets/{ImageViewerPlugin-DPeUx_Oz.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
  57. package/src/ui/dist/assets/{LabCopilotPanel-eAelUaub.js → LabCopilotPanel-dfLptQcR.js} +10 -10
  58. package/src/ui/dist/assets/{LabPlugin-BbOrBxKY.js → LabPlugin-CeGjAl3A.js} +1 -1
  59. package/src/ui/dist/assets/{LatexPlugin-C-HhkVXY.js → LatexPlugin-BBJ7kd1V.js} +7 -7
  60. package/src/ui/dist/assets/{MarkdownViewerPlugin-BDIzIBfh.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
  61. package/src/ui/dist/assets/{MarketplacePlugin-DAOJphwr.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
  62. package/src/ui/dist/assets/{NotebookEditor-BsoMvDoU.js → NotebookEditor-4R88_BMO.js} +1 -1
  63. package/src/ui/dist/assets/{PdfLoader-fiC7RtHf.js → PdfLoader-DwEFQLrw.js} +1 -1
  64. package/src/ui/dist/assets/{PdfMarkdownPlugin-C5OxZBFK.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
  65. package/src/ui/dist/assets/{PdfViewerPlugin-CAbxQebk.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
  66. package/src/ui/dist/assets/{SearchPlugin-SE33Lb9B.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
  67. package/src/ui/dist/assets/{Stepper-0Av7GfV7.js → Stepper-ClOgzWM3.js} +1 -1
  68. package/src/ui/dist/assets/{TextViewerPlugin-Daf2gJDI.js → TextViewerPlugin-DDQWxibk.js} +4 -4
  69. package/src/ui/dist/assets/{VNCViewer-BKrMUIOX.js → VNCViewer-CJXT0Nm8.js} +9 -9
  70. package/src/ui/dist/assets/{bibtex-JBdOEe45.js → bibtex-DLr4Rtk4.js} +1 -1
  71. package/src/ui/dist/assets/{code-B0TDFCZz.js → code-DgKK408Y.js} +1 -1
  72. package/src/ui/dist/assets/{file-content-3YtrSacz.js → file-content-6HBqQnvQ.js} +1 -1
  73. package/src/ui/dist/assets/{file-diff-panel-CJEg5OG1.js → file-diff-panel-Dhu0TbBM.js} +1 -1
  74. package/src/ui/dist/assets/{file-socket-CYQYdmB1.js → file-socket-CP3iwVZG.js} +1 -1
  75. package/src/ui/dist/assets/{file-utils-Cd1C9Ppl.js → file-utils-BsS-Aw68.js} +1 -1
  76. package/src/ui/dist/assets/{image-B33ctrvC.js → image-ByeK-Zcv.js} +1 -1
  77. package/src/ui/dist/assets/{index-BVXsmS7V.js → index-BLjo5--a.js} +9499 -8688
  78. package/src/ui/dist/assets/{index-BNQWqmJ2.js → index-BdsE0uRz.js} +11 -11
  79. package/src/ui/dist/assets/{index-9CLPVeZh.js → index-C-eX-N6A.js} +1 -1
  80. package/src/ui/dist/assets/{index-SwmFAld3.css → index-CuQhlrR-.css} +49 -2
  81. package/src/ui/dist/assets/{index-Buw_N1VQ.js → index-DyremSIv.js} +2 -2
  82. package/src/ui/dist/assets/{message-square-D0cUJ9yU.js → message-square-DnagiLnc.js} +1 -1
  83. package/src/ui/dist/assets/{monaco-UZLYkp2n.js → monaco-4kBFeprs.js} +1 -1
  84. package/src/ui/dist/assets/{popover-CTeiY-dK.js → popover-hRCXZzs2.js} +1 -1
  85. package/src/ui/dist/assets/{project-sync-Dbs01Xky.js → project-sync-O_85YuP6.js} +1 -1
  86. package/src/ui/dist/assets/{sigma-CM08S-xT.js → sigma-DvKopSnL.js} +1 -1
  87. package/src/ui/dist/assets/{tooltip-pDtzvU9p.js → tooltip-BmlPc6kc.js} +1 -1
  88. package/src/ui/dist/assets/{trash-YvPCP-da.js → trash-n-UvdZFR.js} +1 -1
  89. package/src/ui/dist/assets/{useCliAccess-Bavi74Ac.js → useCliAccess-WDd3_wIh.js} +1 -1
  90. package/src/ui/dist/assets/{useFileDiffOverlay-CVXY6oeg.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
  91. package/src/ui/dist/assets/{wrap-text-Cf4flRW7.js → wrap-text-qIYQ4a_W.js} +1 -1
  92. package/src/ui/dist/assets/{zoom-out-Hb0Z1YpT.js → zoom-out-fZXCEFsy.js} +1 -1
  93. package/src/ui/dist/index.html +2 -2
package/README.md CHANGED
@@ -52,6 +52,28 @@ The default DeepScientist home is:
52
52
 
53
53
  Use `ds --home <path>` if you want to place the runtime somewhere else.
54
54
 
55
+ If you want to use the current working directory directly as the DeepScientist home, use:
56
+
57
+ ```bash
58
+ ds --hero
59
+ ```
60
+
61
+ This is equivalent to launching with `ds --home "$PWD"`.
62
+
63
+ If you want to install the bundled CLI tree into another base path from a source checkout:
64
+
65
+ ```bash
66
+ bash install.sh --dir /data/DeepScientist
67
+ ```
68
+
69
+ If you already have a populated DeepScientist home and want to move it safely:
70
+
71
+ ```bash
72
+ ds migrate /data/DeepScientist
73
+ ```
74
+
75
+ `ds migrate` stops the managed daemon first, shows the absolute source and target paths, asks for a double confirmation, verifies the copied tree, updates launcher wrappers, and only then removes the old path.
76
+
55
77
  ## Troubleshooting
56
78
 
57
79
  ```bash
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',
@@ -46,6 +47,9 @@ Usage:
46
47
  ds update
47
48
  ds update --check
48
49
  ds update --yes
50
+ ds migrate /data/DeepScientist
51
+ ds --hero
52
+ ds --hero doctor
49
53
  ds --tui
50
54
  ds --both
51
55
  ds --host 0.0.0.0 --port 21000
@@ -67,6 +71,7 @@ Launcher flags:
67
71
  --stop Stop the managed daemon
68
72
  --restart Restart the managed daemon
69
73
  --home <path> Use a custom DeepScientist home
74
+ --hero Use the current working directory as DeepScientist home
70
75
  --quest-id <id> Open the TUI on one quest directly
71
76
 
72
77
  Update:
@@ -74,6 +79,10 @@ Update:
74
79
  ds update --check Print structured update status
75
80
  ds update --yes Install the latest npm release immediately
76
81
 
82
+ Migration:
83
+ ds migrate <target> Move the DeepScientist home/install root to a new absolute path
84
+ ds migrate <target> --yes --restart
85
+
77
86
  Runtime:
78
87
  DeepScientist uses uv to manage a locked local Python runtime.
79
88
  If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
@@ -384,6 +393,9 @@ function resolveHome(args) {
384
393
  if (index >= 0 && index + 1 < args.length) {
385
394
  return path.resolve(args[index + 1]);
386
395
  }
396
+ if (args.includes('--hero') || args.includes('--here')) {
397
+ return process.cwd();
398
+ }
387
399
  if (process.env.DEEPSCIENTIST_HOME) {
388
400
  return path.resolve(process.env.DEEPSCIENTIST_HOME);
389
401
  }
@@ -554,12 +566,14 @@ function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
554
566
  renderKeyValueRows([
555
567
  ['ds --port 21000', 'Change the web port'],
556
568
  ['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
569
+ ['ds --hero', 'Use the current directory as home'],
557
570
  ['ds --both', 'Start web + TUI together'],
558
571
  ['ds --tui', 'Start the terminal workspace only'],
559
572
  ['ds --no-browser', 'Do not auto-open the browser'],
560
573
  ['ds --status', 'Show daemon health as JSON'],
561
574
  ['ds --restart', 'Restart the managed daemon'],
562
575
  ['ds --stop', 'Stop the managed daemon'],
576
+ ['ds migrate /data/DeepScientist', 'Move the full home/install root safely'],
563
577
  ['ds --help', 'Show the full launcher help'],
564
578
  ]);
565
579
  console.log('');
@@ -630,6 +644,7 @@ function printLaunchCard({
630
644
  console.log(centerText(browserLine, width));
631
645
  console.log(centerText(nextStep, width));
632
646
  console.log(centerText('Run ds --stop to stop the managed daemon.', width));
647
+ console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
633
648
  console.log('');
634
649
  renderLaunchHints({ home, url, bindUrl, pythonSelection });
635
650
  }
@@ -894,6 +909,22 @@ Flags:
894
909
  `);
895
910
  }
896
911
 
912
+ function printMigrateHelp() {
913
+ console.log(`DeepScientist migrate
914
+
915
+ Usage:
916
+ ds migrate /absolute/target/path
917
+ ds migrate /absolute/target/path --yes
918
+ ds migrate /absolute/target/path --restart
919
+ ds migrate /absolute/target/path --home /current/source/path
920
+
921
+ Flags:
922
+ --yes Skip the interactive double-confirmation prompt
923
+ --restart Start the managed daemon again from the migrated home
924
+ --home <path> Override the current DeepScientist source home/root
925
+ `);
926
+ }
927
+
897
928
  function parseUpdateArgs(argv) {
898
929
  const args = [...argv];
899
930
  if (args[0] === 'update') {
@@ -950,6 +981,40 @@ function parseUpdateArgs(argv) {
950
981
  };
951
982
  }
952
983
 
984
+ function parseMigrateArgs(argv) {
985
+ const args = [...argv];
986
+ if (args[0] === 'migrate') {
987
+ args.shift();
988
+ }
989
+ let home = null;
990
+ let target = null;
991
+ let yes = false;
992
+ let restart = false;
993
+
994
+ for (let index = 0; index < args.length; index += 1) {
995
+ const arg = args[index];
996
+ if (arg === '--yes') yes = true;
997
+ else if (arg === '--restart') restart = true;
998
+ else if (arg === '--home' && args[index + 1]) home = path.resolve(expandUserPath(args[++index]));
999
+ else if (arg === '--help' || arg === '-h') return { help: true };
1000
+ else if (arg.startsWith('--')) return null;
1001
+ else if (!target) target = path.resolve(expandUserPath(arg));
1002
+ else return null;
1003
+ }
1004
+
1005
+ if (!target) {
1006
+ return null;
1007
+ }
1008
+
1009
+ return {
1010
+ help: false,
1011
+ home,
1012
+ target,
1013
+ yes,
1014
+ restart,
1015
+ };
1016
+ }
1017
+
953
1018
  function findFirstPositionalArg(args) {
954
1019
  for (let index = 0; index < args.length; index += 1) {
955
1020
  const arg = args[index];
@@ -973,6 +1038,188 @@ function realpathOrSelf(targetPath) {
973
1038
  }
974
1039
  }
975
1040
 
1041
+ function isPathEqual(left, right) {
1042
+ return realpathOrSelf(path.resolve(left)) === realpathOrSelf(path.resolve(right));
1043
+ }
1044
+
1045
+ function isPathInside(candidatePath, parentPath) {
1046
+ const candidate = realpathOrSelf(path.resolve(candidatePath));
1047
+ const parent = realpathOrSelf(path.resolve(parentPath));
1048
+ if (candidate === parent) {
1049
+ return false;
1050
+ }
1051
+ const relative = path.relative(parent, candidate);
1052
+ return Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
1053
+ }
1054
+
1055
+ function buildInstalledWrapperScript() {
1056
+ return [
1057
+ '#!/usr/bin/env bash',
1058
+ 'set -euo pipefail',
1059
+ 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
1060
+ 'HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"',
1061
+ 'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
1062
+ ' export DEEPSCIENTIST_HOME="$HOME_DIR"',
1063
+ 'fi',
1064
+ 'NODE_BIN="${DEEPSCIENTIST_NODE:-node}"',
1065
+ 'exec "$NODE_BIN" "$SCRIPT_DIR/ds.js" "$@"',
1066
+ '',
1067
+ ].join('\n');
1068
+ }
1069
+
1070
+ function buildGlobalWrapperScript({ installDir, home, commandName }) {
1071
+ return [
1072
+ '#!/usr/bin/env bash',
1073
+ 'set -euo pipefail',
1074
+ 'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
1075
+ ` export DEEPSCIENTIST_HOME="${home}"`,
1076
+ 'fi',
1077
+ `exec "${path.join(installDir, 'bin', commandName)}" "$@"`,
1078
+ '',
1079
+ ].join('\n');
1080
+ }
1081
+
1082
+ function writeExecutableScript(targetPath, content) {
1083
+ ensureDir(path.dirname(targetPath));
1084
+ fs.writeFileSync(targetPath, content, { encoding: 'utf8', mode: 0o755 });
1085
+ fs.chmodSync(targetPath, 0o755);
1086
+ }
1087
+
1088
+ function repairMigratedInstallWrappers(targetHome) {
1089
+ const installBinDir = path.join(targetHome, 'cli', 'bin');
1090
+ if (!fs.existsSync(installBinDir)) {
1091
+ return;
1092
+ }
1093
+ const content = buildInstalledWrapperScript();
1094
+ for (const commandName of launcherWrapperCommands) {
1095
+ const wrapperPath = path.join(installBinDir, commandName);
1096
+ if (!fs.existsSync(wrapperPath)) {
1097
+ continue;
1098
+ }
1099
+ writeExecutableScript(wrapperPath, content);
1100
+ }
1101
+ }
1102
+
1103
+ function candidateWrapperPathsForCommand(commandName) {
1104
+ const directories = String(process.env.PATH || '')
1105
+ .split(path.delimiter)
1106
+ .filter(Boolean);
1107
+ const candidates = [];
1108
+ for (const directory of directories) {
1109
+ candidates.push(path.join(directory, commandName));
1110
+ if (process.platform === 'win32') {
1111
+ candidates.push(path.join(directory, `${commandName}.cmd`));
1112
+ candidates.push(path.join(directory, `${commandName}.ps1`));
1113
+ }
1114
+ }
1115
+ return candidates;
1116
+ }
1117
+
1118
+ function rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome }) {
1119
+ if (process.platform === 'win32') {
1120
+ return [];
1121
+ }
1122
+ const rewritten = [];
1123
+ const sourceInstallDir = path.join(sourceHome, 'cli');
1124
+ const targetInstallDir = path.join(targetHome, 'cli');
1125
+ for (const commandName of launcherWrapperCommands) {
1126
+ for (const candidate of candidateWrapperPathsForCommand(commandName)) {
1127
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
1128
+ continue;
1129
+ }
1130
+ let text = '';
1131
+ try {
1132
+ text = fs.readFileSync(candidate, 'utf8');
1133
+ } catch {
1134
+ continue;
1135
+ }
1136
+ if (!text.includes(sourceInstallDir) && !text.includes(sourceHome)) {
1137
+ continue;
1138
+ }
1139
+ writeExecutableScript(
1140
+ candidate,
1141
+ buildGlobalWrapperScript({
1142
+ installDir: targetInstallDir,
1143
+ home: targetHome,
1144
+ commandName,
1145
+ })
1146
+ );
1147
+ rewritten.push(candidate);
1148
+ }
1149
+ }
1150
+ return rewritten;
1151
+ }
1152
+
1153
+ function scheduleDeferredSourceCleanup({ sourceHome, targetHome }) {
1154
+ const logPath = path.join(targetHome, 'logs', 'migrate-cleanup.log');
1155
+ ensureDir(path.dirname(logPath));
1156
+ const helperScript = [
1157
+ "const fs = require('node:fs');",
1158
+ "const { setTimeout: sleep } = require('node:timers/promises');",
1159
+ 'const parentPid = Number(process.argv[1]);',
1160
+ 'const sourceHome = process.argv[2];',
1161
+ 'const logPath = process.argv[3];',
1162
+ '(async () => {',
1163
+ ' for (let attempt = 0; attempt < 300; attempt += 1) {',
1164
+ ' try {',
1165
+ ' process.kill(parentPid, 0);',
1166
+ ' await sleep(100);',
1167
+ ' continue;',
1168
+ ' } catch {',
1169
+ ' break;',
1170
+ ' }',
1171
+ ' }',
1172
+ ' try {',
1173
+ ' fs.rmSync(sourceHome, { recursive: true, force: true });',
1174
+ ' } catch (error) {',
1175
+ " fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${error instanceof Error ? error.message : String(error)}\\n`, 'utf8');",
1176
+ ' process.exit(1);',
1177
+ ' }',
1178
+ '})();',
1179
+ ].join('\n');
1180
+ const child = spawn(process.execPath, ['-e', helperScript, String(process.pid), sourceHome, logPath], {
1181
+ detached: true,
1182
+ stdio: 'ignore',
1183
+ env: process.env,
1184
+ });
1185
+ child.unref();
1186
+ }
1187
+
1188
+ async function promptMigrationConfirmation({ sourceHome, targetHome }) {
1189
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1190
+ throw new Error('DeepScientist migration needs a TTY for confirmation. Re-run with `--yes` to continue non-interactively.');
1191
+ }
1192
+ console.log('');
1193
+ console.log('DeepScientist home migration');
1194
+ console.log('');
1195
+ console.log(`From: ${sourceHome}`);
1196
+ console.log(`To: ${targetHome}`);
1197
+ console.log('');
1198
+ 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.');
1199
+ const ask = (question) => new Promise((resolve) => {
1200
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1201
+ rl.question(question, (answer) => {
1202
+ rl.close();
1203
+ resolve(String(answer || '').trim());
1204
+ });
1205
+ });
1206
+ const first = await ask('Type YES to continue: ');
1207
+ if (first !== 'YES') {
1208
+ return false;
1209
+ }
1210
+ const second = await ask('Type MIGRATE to confirm old-path deletion after a successful copy: ');
1211
+ return second === 'MIGRATE';
1212
+ }
1213
+
1214
+ function printMigrationSummary({ sourceHome, targetHome, restart }) {
1215
+ console.log('');
1216
+ console.log('DeepScientist migrate');
1217
+ console.log('');
1218
+ console.log(`Source: ${sourceHome}`);
1219
+ console.log(`Target: ${targetHome}`);
1220
+ console.log(`Restart: ${restart ? 'yes' : 'no'}`);
1221
+ }
1222
+
976
1223
  function pythonMeetsMinimum(probe) {
977
1224
  if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
978
1225
  return false;
@@ -1784,6 +2031,9 @@ function normalizePythonCliArgs(args, home) {
1784
2031
  index += 1;
1785
2032
  continue;
1786
2033
  }
2034
+ if (arg === '--hero' || arg === '--here') {
2035
+ continue;
2036
+ }
1787
2037
  normalized.push(arg);
1788
2038
  }
1789
2039
  return ['--home', home, ...normalized];
@@ -2996,6 +3246,135 @@ async function updateMain(rawArgs) {
2996
3246
  process.exit(payload.ok ? 0 : 1);
2997
3247
  }
2998
3248
 
3249
+ async function migrateMain(rawArgs) {
3250
+ const options = parseMigrateArgs(rawArgs);
3251
+ if (!options) {
3252
+ printMigrateHelp();
3253
+ process.exit(1);
3254
+ }
3255
+ if (options.help) {
3256
+ printMigrateHelp();
3257
+ process.exit(0);
3258
+ }
3259
+
3260
+ const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
3261
+ const targetHome = path.resolve(options.target);
3262
+ if (!fs.existsSync(sourceHome)) {
3263
+ console.error(`DeepScientist source path does not exist: ${sourceHome}`);
3264
+ process.exit(1);
3265
+ }
3266
+ if (isPathEqual(sourceHome, targetHome)) {
3267
+ console.error('DeepScientist source and target paths are identical. Choose a different migration target.');
3268
+ process.exit(1);
3269
+ }
3270
+ if (isPathInside(targetHome, sourceHome) || isPathInside(sourceHome, targetHome)) {
3271
+ console.error('DeepScientist migration requires two separate sibling paths. Do not nest one path inside the other.');
3272
+ process.exit(1);
3273
+ }
3274
+ if (fs.existsSync(targetHome)) {
3275
+ console.error(`DeepScientist target path already exists: ${targetHome}`);
3276
+ process.exit(1);
3277
+ }
3278
+
3279
+ printMigrationSummary({ sourceHome, targetHome, restart: options.restart });
3280
+ if (!options.yes) {
3281
+ const confirmed = await promptMigrationConfirmation({ sourceHome, targetHome });
3282
+ if (!confirmed) {
3283
+ console.log('DeepScientist migration cancelled.');
3284
+ process.exit(1);
3285
+ }
3286
+ }
3287
+
3288
+ const state = readDaemonState(sourceHome);
3289
+ const configured = readConfiguredUiAddressFromFile(sourceHome);
3290
+ const url = state?.url || browserUiUrl(configured.host, configured.port);
3291
+ const health = await fetchHealth(url);
3292
+ if (state || healthMatchesHome({ health, home: sourceHome })) {
3293
+ await stopDaemon(sourceHome);
3294
+ } else if (health && health.status === 'ok') {
3295
+ console.log(`Skipping daemon stop because ${url} belongs to another DeepScientist home.`);
3296
+ }
3297
+
3298
+ const pythonRuntime = ensurePythonRuntime(sourceHome);
3299
+ const runtimePython = pythonRuntime.runtimePython;
3300
+ const result = runPythonCli(
3301
+ runtimePython,
3302
+ ['--home', sourceHome, 'migrate', targetHome],
3303
+ { capture: true, allowFailure: true }
3304
+ );
3305
+ let payload = null;
3306
+ try {
3307
+ payload = JSON.parse(String(result.stdout || '{}'));
3308
+ } catch {
3309
+ payload = null;
3310
+ }
3311
+ if (result.status !== 0 || !payload || payload.ok !== true) {
3312
+ if (result.stdout) {
3313
+ process.stdout.write(result.stdout);
3314
+ if (!String(result.stdout).endsWith('\n')) {
3315
+ process.stdout.write('\n');
3316
+ }
3317
+ }
3318
+ if (result.stderr) {
3319
+ process.stderr.write(result.stderr);
3320
+ if (!String(result.stderr).endsWith('\n')) {
3321
+ process.stderr.write('\n');
3322
+ }
3323
+ }
3324
+ console.error('DeepScientist migration failed.');
3325
+ process.exit(result.status ?? 1);
3326
+ }
3327
+
3328
+ repairMigratedInstallWrappers(targetHome);
3329
+ const rewrittenWrappers = rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome });
3330
+
3331
+ const sourceContainsCurrentInstall = isPathEqual(repoRoot, path.join(sourceHome, 'cli')) || isPathInside(repoRoot, sourceHome);
3332
+ if (sourceContainsCurrentInstall) {
3333
+ scheduleDeferredSourceCleanup({ sourceHome, targetHome });
3334
+ } else {
3335
+ fs.rmSync(sourceHome, { recursive: true, force: true });
3336
+ }
3337
+
3338
+ let restartMessage = 'Restart skipped.';
3339
+ if (options.restart) {
3340
+ const migratedLauncher = path.join(targetHome, 'cli', 'bin', 'ds.js');
3341
+ if (!fs.existsSync(migratedLauncher)) {
3342
+ restartMessage = `Migration succeeded, but restart was skipped because the migrated launcher is missing: ${migratedLauncher}`;
3343
+ } else {
3344
+ const child = spawn(
3345
+ process.execPath,
3346
+ [migratedLauncher, '--home', targetHome, '--daemon-only', '--no-browser', '--skip-update-check'],
3347
+ {
3348
+ cwd: path.join(targetHome, 'cli'),
3349
+ detached: true,
3350
+ stdio: 'ignore',
3351
+ env: process.env,
3352
+ }
3353
+ );
3354
+ child.unref();
3355
+ restartMessage = 'Managed daemon restart scheduled from the migrated home.';
3356
+ }
3357
+ }
3358
+
3359
+ console.log('');
3360
+ console.log('DeepScientist migration completed.');
3361
+ console.log(`New home: ${targetHome}`);
3362
+ if (payload.summary) {
3363
+ console.log(payload.summary);
3364
+ }
3365
+ if (rewrittenWrappers.length > 0) {
3366
+ console.log(`Updated wrappers: ${rewrittenWrappers.join(', ')}`);
3367
+ }
3368
+ console.log(restartMessage);
3369
+ if (sourceContainsCurrentInstall) {
3370
+ console.log(`Old path cleanup has been scheduled: ${sourceHome}`);
3371
+ } else {
3372
+ console.log(`Old path removed: ${sourceHome}`);
3373
+ }
3374
+ console.log(`Use \`ds --home ${targetHome}\` if you want to override the default explicitly.`);
3375
+ process.exit(0);
3376
+ }
3377
+
2999
3378
  async function launcherMain(rawArgs) {
3000
3379
  const options = parseLauncherArgs(rawArgs);
3001
3380
  if (!options) {
@@ -3097,6 +3476,10 @@ async function main() {
3097
3476
  await updateMain(args);
3098
3477
  return;
3099
3478
  }
3479
+ if (positional && positional.value === 'migrate') {
3480
+ await migrateMain(args);
3481
+ return;
3482
+ }
3100
3483
  if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
3101
3484
  await launcherMain(args);
3102
3485
  return;
@@ -3144,6 +3527,7 @@ module.exports = {
3144
3527
  legacyVenvRootPath,
3145
3528
  resolveUvBinary,
3146
3529
  resolveHome,
3530
+ parseMigrateArgs,
3147
3531
  useEditableProjectInstall,
3148
3532
  compareVersions,
3149
3533
  detectInstallMode,
@@ -39,6 +39,28 @@ If `uv` is missing on your machine, `ds` will bootstrap a local copy automatical
39
39
 
40
40
  By default, the DeepScientist home is `~/DeepScientist` on macOS and Linux, and `%USERPROFILE%\\DeepScientist` on Windows. You can override it with `ds --home <path>`.
41
41
 
42
+ If you want to use the current working directory as the DeepScientist home directly, run:
43
+
44
+ ```bash
45
+ ds --hero
46
+ ```
47
+
48
+ This is equivalent to `ds --home "$PWD"`.
49
+
50
+ If you install from a source checkout and want another default base path for the bundled CLI tree, use:
51
+
52
+ ```bash
53
+ bash install.sh --dir /data/DeepScientist
54
+ ```
55
+
56
+ If you already have a populated DeepScientist home and need to move it to another path later, use:
57
+
58
+ ```bash
59
+ ds migrate /data/DeepScientist
60
+ ```
61
+
62
+ The migration command prints the absolute source and target paths, stops the managed daemon, asks for a double confirmation, verifies the copied tree, updates launcher wrappers, and removes the old path only after the migration succeeds.
63
+
42
64
  By default, the web UI is served at:
43
65
 
44
66
  ```text
@@ -39,6 +39,28 @@ DeepScientist 现在使用 `uv` 管理锁定的本地 Python 运行时。如果
39
39
 
40
40
  默认情况下,DeepScientist home 在 macOS / Linux 上是 `~/DeepScientist`,在 Windows 上是 `%USERPROFILE%\\DeepScientist`。如果你希望放到别的路径,可以直接使用 `ds --home <path>`。
41
41
 
42
+ 如果你希望直接把“当前工作目录”作为 DeepScientist home,可以运行:
43
+
44
+ ```bash
45
+ ds --hero
46
+ ```
47
+
48
+ 它等价于 `ds --home "$PWD"`。
49
+
50
+ 如果你是从源码仓库安装,并希望把默认的 CLI 安装基路径改到别的位置,可以使用:
51
+
52
+ ```bash
53
+ bash install.sh --dir /data/DeepScientist
54
+ ```
55
+
56
+ 如果你已经有一个正在使用的 DeepScientist home,之后又想安全迁移到别的路径,可以使用:
57
+
58
+ ```bash
59
+ ds migrate /data/DeepScientist
60
+ ```
61
+
62
+ `ds migrate` 会先显示当前绝对路径和目标绝对路径,停止托管 daemon,要求二次确认,校验复制结果,并在确认迁移成功后才删除旧路径。
63
+
42
64
  默认情况下,网页会运行在:
43
65
 
44
66
  ```text