@researai/deepscientist 1.5.16 → 1.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +66 -23
  2. package/bin/ds.js +550 -19
  3. package/docs/en/00_QUICK_START.md +65 -5
  4. package/docs/en/01_SETTINGS_REFERENCE.md +1 -1
  5. package/docs/en/09_DOCTOR.md +14 -3
  6. package/docs/en/15_CODEX_PROVIDER_SETUP.md +12 -3
  7. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  8. package/docs/en/91_DEVELOPMENT.md +237 -0
  9. package/docs/en/README.md +7 -3
  10. package/docs/zh/00_QUICK_START.md +54 -5
  11. package/docs/zh/01_SETTINGS_REFERENCE.md +1 -1
  12. package/docs/zh/09_DOCTOR.md +15 -4
  13. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +12 -3
  14. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  15. package/docs/zh/README.md +7 -3
  16. package/install.sh +46 -4
  17. package/package.json +2 -1
  18. package/pyproject.toml +1 -1
  19. package/src/deepscientist/__init__.py +1 -1
  20. package/src/deepscientist/bridges/connectors.py +8 -2
  21. package/src/deepscientist/codex_cli_compat.py +185 -72
  22. package/src/deepscientist/config/service.py +154 -6
  23. package/src/deepscientist/daemon/api/handlers.py +130 -25
  24. package/src/deepscientist/daemon/api/router.py +5 -0
  25. package/src/deepscientist/daemon/app.py +446 -22
  26. package/src/deepscientist/diagnostics/__init__.py +6 -0
  27. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  28. package/src/deepscientist/doctor.py +207 -3
  29. package/src/deepscientist/prompts/builder.py +22 -4
  30. package/src/deepscientist/quest/service.py +413 -13
  31. package/src/deepscientist/runners/codex.py +59 -14
  32. package/src/deepscientist/shared.py +19 -0
  33. package/src/prompts/contracts/shared_interaction.md +3 -2
  34. package/src/prompts/system.md +13 -0
  35. package/src/prompts/system_copilot.md +13 -0
  36. package/src/tui/package.json +1 -1
  37. package/src/ui/dist/assets/{AiManusChatView-COFACy7V.js → AiManusChatView-Bv-Z8YpU.js} +44 -44
  38. package/src/ui/dist/assets/{AnalysisPlugin-DnSm0GZn.js → AnalysisPlugin-BCKAfjba.js} +1 -1
  39. package/src/ui/dist/assets/{CliPlugin-CvwCmDQ5.js → CliPlugin-BCKcpc35.js} +4 -4
  40. package/src/ui/dist/assets/{CodeEditorPlugin-cOqSa0xq.js → CodeEditorPlugin-DbOfSJ8K.js} +1 -1
  41. package/src/ui/dist/assets/{CodeViewerPlugin-itb0tltR.js → CodeViewerPlugin-CbaFRrUU.js} +3 -3
  42. package/src/ui/dist/assets/{DocViewerPlugin-DqKkiCI6.js → DocViewerPlugin-DAjLVeQD.js} +3 -3
  43. package/src/ui/dist/assets/{GitCommitViewerPlugin-DVgNHBCS.js → GitCommitViewerPlugin-CIUqbUDO.js} +1 -1
  44. package/src/ui/dist/assets/{GitDiffViewerPlugin-DxL2ezFG.js → GitDiffViewerPlugin-CQACjoAA.js} +1 -1
  45. package/src/ui/dist/assets/{GitSnapshotViewer-B_RQm1YZ.js → GitSnapshotViewer-0r4nLPke.js} +1 -1
  46. package/src/ui/dist/assets/{ImageViewerPlugin-tHqlXY3n.js → ImageViewerPlugin-nBOmI2v_.js} +3 -3
  47. package/src/ui/dist/assets/{LabCopilotPanel-ClMbq5Yu.js → LabCopilotPanel-BHxOxF4z.js} +1 -1
  48. package/src/ui/dist/assets/{LabPlugin-L_SuE8ow.js → LabPlugin-BKoZGs95.js} +1 -1
  49. package/src/ui/dist/assets/{LatexPlugin-B495DTXC.js → LatexPlugin-ZwtV8pIp.js} +1 -1
  50. package/src/ui/dist/assets/{MarkdownViewerPlugin-DG28-61B.js → MarkdownViewerPlugin-DKqVfKyW.js} +3 -3
  51. package/src/ui/dist/assets/{MarketplacePlugin-BiOGT-Kj.js → MarketplacePlugin-BwxStZ9D.js} +1 -1
  52. package/src/ui/dist/assets/{NotebookEditor-C-4Kt1p9.js → NotebookEditor-BEQhaQbt.js} +1 -1
  53. package/src/ui/dist/assets/{NotebookEditor-CVsj8h_T.js → NotebookEditor-DB9N_T9q.js} +23 -23
  54. package/src/ui/dist/assets/{PdfLoader-CASDQmxJ.js → PdfLoader-eWBONbQP.js} +1 -1
  55. package/src/ui/dist/assets/{PdfMarkdownPlugin-BFhwoKsY.js → PdfMarkdownPlugin-D22YOZL3.js} +1 -1
  56. package/src/ui/dist/assets/{PdfViewerPlugin-DcOzU9vd.js → PdfViewerPlugin-c-RK9DLM.js} +3 -3
  57. package/src/ui/dist/assets/{SearchPlugin-CHj7M58O.js → SearchPlugin-CxF9ytAx.js} +1 -1
  58. package/src/ui/dist/assets/{TextViewerPlugin-CB4DYfWO.js → TextViewerPlugin-C5xqeeUH.js} +2 -2
  59. package/src/ui/dist/assets/{VNCViewer-CjlbyCB3.js → VNCViewer-BoLGLnHz.js} +1 -1
  60. package/src/ui/dist/assets/{bot-CFkZY-JP.js → bot-DREQOxzP.js} +1 -1
  61. package/src/ui/dist/assets/{chevron-up-Dq5ofbht.js → chevron-up-C9Qpx4DE.js} +1 -1
  62. package/src/ui/dist/assets/{code-DLC6G24T.js → code-WlFHE7z_.js} +1 -1
  63. package/src/ui/dist/assets/{file-content-Dv4LoZec.js → file-content-BZMz3RYp.js} +1 -1
  64. package/src/ui/dist/assets/{file-diff-panel-Denq-lC3.js → file-diff-panel-CQhw0jS2.js} +1 -1
  65. package/src/ui/dist/assets/{file-socket-Cu4Qln7Y.js → file-socket-CfQPKQKj.js} +1 -1
  66. package/src/ui/dist/assets/{git-commit-horizontal-BUh6G52n.js → git-commit-horizontal-DxZ8DCZh.js} +1 -1
  67. package/src/ui/dist/assets/{image-B9HUUddG.js → image-Bgl4VIyx.js} +1 -1
  68. package/src/ui/dist/assets/{index-Cgla8biy.css → index-BpV6lusQ.css} +1 -1
  69. package/src/ui/dist/assets/{index-Gbl53BNp.js → index-CBNVuWcP.js} +363 -363
  70. package/src/ui/dist/assets/{index-wQ7RIIRd.js → index-CwNu1aH4.js} +1 -1
  71. package/src/ui/dist/assets/{index-B2B1sg-M.js → index-DrUnlf6K.js} +1 -1
  72. package/src/ui/dist/assets/{index-DRyx7vAc.js → index-NW-h8VzN.js} +1 -1
  73. package/src/ui/dist/assets/{pdf-effect-queue-ZtnHFCAi.js → pdf-effect-queue-J8OnM0jE.js} +1 -1
  74. package/src/ui/dist/assets/{popover-DL6h35vr.js → popover-CLc0pPP8.js} +1 -1
  75. package/src/ui/dist/assets/{project-sync-CsX08Qno.js → project-sync-C9IdzdZW.js} +1 -1
  76. package/src/ui/dist/assets/{select-DvmXt1yY.js → select-Cs2PmzwL.js} +1 -1
  77. package/src/ui/dist/assets/{sigma-7jpXazui.js → sigma-ClKcHAXm.js} +1 -1
  78. package/src/ui/dist/assets/{trash-xA7kFt8i.js → trash-DwpbFr3w.js} +1 -1
  79. package/src/ui/dist/assets/{useCliAccess-DsMwDjOp.js → useCliAccess-NQ8m0Let.js} +1 -1
  80. package/src/ui/dist/assets/{wrap-text-CwMn-iqb.js → wrap-text-BC-Hltpd.js} +1 -1
  81. package/src/ui/dist/assets/{zoom-out-R-GWEhzS.js → zoom-out-E_gaeAxL.js} +1 -1
  82. package/src/ui/dist/index.html +2 -2
package/bin/ds.js CHANGED
@@ -139,6 +139,7 @@ Usage:
139
139
  ds update
140
140
  ds update --check
141
141
  ds update --yes
142
+ ds uninstall
142
143
  ds migrate /data/DeepScientist
143
144
  ds --here
144
145
  ds --yolo --port 20999 --here
@@ -182,6 +183,9 @@ Migration:
182
183
  ds migrate <target> Move the DeepScientist home/install root to a new absolute path
183
184
  ds migrate <target> --yes --restart
184
185
 
186
+ Uninstall:
187
+ ds uninstall Remove code/runtime only and preserve local data
188
+
185
189
  Runtime:
186
190
  DeepScientist uses uv to manage a locked local Python runtime.
187
191
  If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
@@ -1028,10 +1032,10 @@ function writeCodexPreflightReport(home, probe) {
1028
1032
  const profile = typeof details.profile === 'string' ? details.profile.trim() : '';
1029
1033
  const intro = profile
1030
1034
  ? `DeepScientist blocked startup because the Codex hello probe did not pass for profile \`${profile}\`. Verify that \`codex --profile ${profile}\` works on this machine and that the profile's provider-specific API key, Base URL, and model configuration are already set up.`
1031
- : 'DeepScientist blocked startup because the Codex hello probe did not pass. In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency. If `codex` is still missing, repair it with `npm install -g @openai/codex`. Then run `codex --login` (or `codex`), finish authentication, run `ds doctor`, and launch `ds` again.';
1035
+ : 'DeepScientist blocked startup because the Codex hello probe did not pass. In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency. If `codex` is still missing, repair it with `npm install -g @openai/codex`. Then run `codex login` (or just `codex`), finish authentication, run `ds doctor`, and launch `ds` again.';
1032
1036
  const introZh = profile
1033
1037
  ? `DeepScientist 启动前进行了 Codex 可用性检查,但 profile \`${profile}\` 的 hello 探测没有通过。请先确认 \`codex --profile ${profile}\` 在当前机器上可以正常启动,并确保该 profile 依赖的 provider API Key、Base URL 和模型配置都已经在 Codex 中配置好。`
1034
- : 'DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,`npm install -g @researai/deepscientist` 也会一并安装 bundled Codex 依赖;如果此后 `codex` 仍不可用,请再执行 `npm install -g @openai/codex` 修复。然后运行 `codex --login`(或 `codex`)完成认证,再执行 `ds doctor`,最后重新启动 `ds`。';
1038
+ : 'DeepScientist 启动前进行了 Codex 可用性检查,但 hello 探测没有通过。正常情况下,`npm install -g @researai/deepscientist` 也会一并安装 bundled Codex 依赖;如果此后 `codex` 仍不可用,请再执行 `npm install -g @openai/codex` 修复。然后运行 `codex login`(或直接运行 `codex`)完成认证,再执行 `ds doctor`,最后重新启动 `ds`。';
1035
1039
  const renderItems = (items, tone) =>
1036
1040
  items
1037
1041
  .map(
@@ -1381,6 +1385,26 @@ Flags:
1381
1385
  `);
1382
1386
  }
1383
1387
 
1388
+ function printUninstallHelp() {
1389
+ console.log(`DeepScientist uninstall
1390
+
1391
+ Usage:
1392
+ ds uninstall
1393
+ ds uninstall --home /absolute/home/path
1394
+ ds uninstall --yes
1395
+
1396
+ Behavior:
1397
+ - removes DeepScientist code, launcher wrappers, and local runtime code
1398
+ - preserves local data such as quests, memory, config, logs, plugins, and cache
1399
+ - if this command is run from the globally installed npm package, it also removes the npm package itself
1400
+
1401
+ Flags:
1402
+ --yes Skip the interactive confirmation prompt
1403
+ --home <path> Override the target DeepScientist home/root
1404
+ --origin <value> Internal use for npm uninstall integration
1405
+ `);
1406
+ }
1407
+
1384
1408
  function parseUpdateArgs(argv) {
1385
1409
  const args = [...argv];
1386
1410
  if (args[0] === 'update') {
@@ -1504,6 +1528,43 @@ function parseMigrateArgs(argv) {
1504
1528
  };
1505
1529
  }
1506
1530
 
1531
+ function parseUninstallArgs(argv) {
1532
+ const args = [...argv];
1533
+ if (args[0] === 'uninstall') {
1534
+ args.shift();
1535
+ }
1536
+ let home = null;
1537
+ let yes = false;
1538
+ let origin = null;
1539
+
1540
+ for (let index = 0; index < args.length; index += 1) {
1541
+ const arg = args[index];
1542
+ if (arg === '--yes') yes = true;
1543
+ else if (arg === '--home') {
1544
+ const next = readRequiredOptionValue(args, index, '--home');
1545
+ if (!next.ok) return { help: false, error: next.error };
1546
+ home = path.resolve(expandUserPath(next.value));
1547
+ index += 1;
1548
+ } else if (arg === '--origin') {
1549
+ const next = readRequiredOptionValue(args, index, '--origin');
1550
+ if (!next.ok) return { help: false, error: next.error };
1551
+ origin = String(next.value || '').trim().toLowerCase() || null;
1552
+ index += 1;
1553
+ }
1554
+ else if (arg === '--help' || arg === '-h') return { help: true };
1555
+ else if (arg.startsWith('--')) return { help: false, error: `Unknown uninstall flag: ${arg}` };
1556
+ else return { help: false, error: `Unexpected uninstall argument: ${arg}` };
1557
+ }
1558
+
1559
+ return {
1560
+ help: false,
1561
+ home,
1562
+ yes,
1563
+ origin,
1564
+ error: null,
1565
+ };
1566
+ }
1567
+
1507
1568
  function findFirstPositionalArg(args) {
1508
1569
  for (let index = 0; index < args.length; index += 1) {
1509
1570
  const arg = args[index];
@@ -2457,6 +2518,23 @@ function buildUvRuntimeEnv(home, extraEnv = {}) {
2457
2518
  for (const key of ['PYTHONPATH', 'PYTHONHOME', 'VIRTUAL_ENV', '__PYVENV_LAUNCHER__']) {
2458
2519
  delete env[key];
2459
2520
  }
2521
+ for (const key of Object.keys(env)) {
2522
+ if (key === 'CONDA_EXE' || key === 'CONDA_PYTHON_EXE' || key === '_CE_CONDA' || key === '_CE_M') {
2523
+ delete env[key];
2524
+ continue;
2525
+ }
2526
+ if (key === 'MAMBA_EXE' || key === 'MAMBA_ROOT_PREFIX') {
2527
+ delete env[key];
2528
+ continue;
2529
+ }
2530
+ if (key === 'CONDA_PREFIX' || key === 'CONDA_DEFAULT_ENV' || key === 'CONDA_PROMPT_MODIFIER' || key === 'CONDA_SHLVL') {
2531
+ delete env[key];
2532
+ continue;
2533
+ }
2534
+ if (/^CONDA_PREFIX_\d+$/.test(key)) {
2535
+ delete env[key];
2536
+ }
2537
+ }
2460
2538
  return env;
2461
2539
  }
2462
2540
 
@@ -2970,6 +3048,374 @@ function removeDaemonState(home) {
2970
3048
  }
2971
3049
  }
2972
3050
 
3051
+ function installIndexPath() {
3052
+ return path.join(os.homedir(), '.deepscientist', 'install-index.json');
3053
+ }
3054
+
3055
+ function readInstallIndex() {
3056
+ const indexPath = installIndexPath();
3057
+ if (!fs.existsSync(indexPath)) {
3058
+ return { installs: [] };
3059
+ }
3060
+ try {
3061
+ const payload = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3062
+ const installs = Array.isArray(payload?.installs) ? payload.installs.filter((item) => item && typeof item === 'object') : [];
3063
+ return { installs };
3064
+ } catch {
3065
+ return { installs: [] };
3066
+ }
3067
+ }
3068
+
3069
+ function writeInstallIndex(payload) {
3070
+ const normalized = {
3071
+ installs: Array.isArray(payload?.installs) ? payload.installs : [],
3072
+ };
3073
+ const targetPath = installIndexPath();
3074
+ ensureDir(path.dirname(targetPath));
3075
+ fs.writeFileSync(targetPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
3076
+ }
3077
+
3078
+ function normalizeInstallRecord(record) {
3079
+ const normalizedHome = normalizeHomePath(record?.home || '');
3080
+ if (!normalizedHome) {
3081
+ return null;
3082
+ }
3083
+ const installMode = String(record?.install_mode || '').trim() || null;
3084
+ const installDir = record?.install_dir ? normalizeHomePath(record.install_dir) : null;
3085
+ const packageRoot = record?.package_root ? normalizeHomePath(record.package_root) : null;
3086
+ const launcherPath = record?.launcher_path ? path.resolve(String(record.launcher_path)) : null;
3087
+ const wrapperPaths = Array.isArray(record?.wrapper_paths)
3088
+ ? [...new Set(record.wrapper_paths.map((item) => String(item || '').trim()).filter(Boolean).map((item) => path.resolve(item)))]
3089
+ : [];
3090
+ const createdAt = String(record?.created_at || '').trim() || new Date().toISOString();
3091
+ return {
3092
+ home: normalizedHome,
3093
+ install_mode: installMode,
3094
+ install_dir: installDir,
3095
+ package_root: packageRoot,
3096
+ launcher_path: launcherPath,
3097
+ wrapper_paths: wrapperPaths,
3098
+ created_at: createdAt,
3099
+ updated_at: new Date().toISOString(),
3100
+ };
3101
+ }
3102
+
3103
+ function installRecordMatches(left, right) {
3104
+ return left.home === right.home
3105
+ && (left.install_dir || null) === (right.install_dir || null)
3106
+ && (left.package_root || null) === (right.package_root || null)
3107
+ && (left.install_mode || null) === (right.install_mode || null);
3108
+ }
3109
+
3110
+ function upsertInstallRecord(record) {
3111
+ const normalized = normalizeInstallRecord(record);
3112
+ if (!normalized) {
3113
+ return null;
3114
+ }
3115
+ const index = readInstallIndex();
3116
+ const installs = index.installs
3117
+ .map((item) => normalizeInstallRecord(item))
3118
+ .filter(Boolean);
3119
+ const nextInstalls = installs.filter((item) => !installRecordMatches(item, normalized));
3120
+ nextInstalls.push(normalized);
3121
+ nextInstalls.sort((left, right) => String(left.updated_at || '').localeCompare(String(right.updated_at || '')));
3122
+ writeInstallIndex({ installs: nextInstalls });
3123
+ return normalized;
3124
+ }
3125
+
3126
+ function removeInstallRecords(predicate) {
3127
+ const index = readInstallIndex();
3128
+ const installs = index.installs
3129
+ .map((item) => normalizeInstallRecord(item))
3130
+ .filter(Boolean);
3131
+ const nextInstalls = installs.filter((item) => !predicate(item));
3132
+ writeInstallIndex({ installs: nextInstalls });
3133
+ return nextInstalls;
3134
+ }
3135
+
3136
+ function parseManagedWrapperCandidate(candidatePath) {
3137
+ let stat = null;
3138
+ try {
3139
+ stat = fs.lstatSync(candidatePath);
3140
+ } catch {
3141
+ return null;
3142
+ }
3143
+
3144
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
3145
+ return null;
3146
+ }
3147
+
3148
+ let text = '';
3149
+ try {
3150
+ text = fs.readFileSync(candidatePath, 'utf8');
3151
+ } catch {
3152
+ return null;
3153
+ }
3154
+
3155
+ const homeMatch = text.match(/export DEEPSCIENTIST_HOME="([^"\n]+)"/);
3156
+ const execMatch = text.match(/exec "([^"\n]+)" "\$@"/);
3157
+ return {
3158
+ path: path.resolve(candidatePath),
3159
+ home: homeMatch ? normalizeHomePath(homeMatch[1]) : null,
3160
+ execPath: execMatch ? path.resolve(execMatch[1]) : null,
3161
+ };
3162
+ }
3163
+
3164
+ function collectManagedWrapperPaths({ home, installDir = null, explicitWrapperPaths = [] }) {
3165
+ const normalizedHome = normalizeHomePath(home);
3166
+ const normalizedInstallDir = installDir ? normalizeHomePath(installDir) : null;
3167
+ const candidates = new Set(explicitWrapperPaths.map((item) => path.resolve(String(item))));
3168
+ for (const commandName of launcherWrapperCommands) {
3169
+ for (const candidate of candidateWrapperPathsForCommand(commandName)) {
3170
+ candidates.add(candidate);
3171
+ }
3172
+ }
3173
+ const matched = [];
3174
+ for (const candidate of candidates) {
3175
+ const parsed = parseManagedWrapperCandidate(candidate);
3176
+ if (!parsed) {
3177
+ continue;
3178
+ }
3179
+ if (parsed.home && parsed.home === normalizedHome) {
3180
+ matched.push(parsed.path);
3181
+ continue;
3182
+ }
3183
+ if (normalizedInstallDir && parsed.execPath && parsed.execPath.startsWith(path.join(normalizedInstallDir, 'bin'))) {
3184
+ matched.push(parsed.path);
3185
+ }
3186
+ }
3187
+ return [...new Set(matched)].sort();
3188
+ }
3189
+
3190
+ function buildCodeOnlyUninstallPlan({ home, installDir = null, wrapperPaths = [] }) {
3191
+ const normalizedHome = normalizeHomePath(home);
3192
+ const normalizedInstallDir = installDir ? normalizeHomePath(installDir) : null;
3193
+ const removePaths = [
3194
+ path.join(normalizedHome, 'runtime', 'python-env'),
3195
+ path.join(normalizedHome, 'runtime', 'python'),
3196
+ path.join(normalizedHome, 'runtime', 'tools'),
3197
+ path.join(normalizedHome, 'runtime', 'bundle'),
3198
+ path.join(normalizedHome, 'runtime', 'daemon.json'),
3199
+ ];
3200
+ if (normalizedInstallDir && normalizedInstallDir !== normalizeHomePath(repoRoot)) {
3201
+ removePaths.push(normalizedInstallDir);
3202
+ }
3203
+ return {
3204
+ remove_paths: [...new Set(removePaths.map((item) => path.resolve(item)))].sort(),
3205
+ preserve_paths: [
3206
+ path.join(normalizedHome, 'quests'),
3207
+ path.join(normalizedHome, 'memory'),
3208
+ path.join(normalizedHome, 'config'),
3209
+ path.join(normalizedHome, 'logs'),
3210
+ path.join(normalizedHome, 'plugins'),
3211
+ path.join(normalizedHome, 'cache'),
3212
+ ].sort(),
3213
+ wrapper_paths: [...new Set(wrapperPaths.map((item) => path.resolve(item)))].sort(),
3214
+ };
3215
+ }
3216
+
3217
+ function removePathEntry(targetPath) {
3218
+ if (!targetPath || !fs.existsSync(targetPath)) {
3219
+ return false;
3220
+ }
3221
+ const stat = fs.lstatSync(targetPath);
3222
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
3223
+ fs.rmSync(targetPath, { recursive: true, force: true });
3224
+ return true;
3225
+ }
3226
+ fs.rmSync(targetPath, { force: true });
3227
+ return true;
3228
+ }
3229
+
3230
+ function currentInstallRecord(home) {
3231
+ const installMode = detectInstallMode(repoRoot);
3232
+ return normalizeInstallRecord({
3233
+ home,
3234
+ install_mode: installMode,
3235
+ install_dir: installMode === 'source-checkout' ? null : null,
3236
+ package_root: normalizeHomePath(repoRoot),
3237
+ launcher_path: resolveLauncherPath() || path.join(repoRoot, 'bin', 'ds.js'),
3238
+ wrapper_paths: [],
3239
+ });
3240
+ }
3241
+
3242
+ function registerCurrentInstall(home) {
3243
+ const record = currentInstallRecord(home);
3244
+ if (!record) {
3245
+ return null;
3246
+ }
3247
+ return upsertInstallRecord(record);
3248
+ }
3249
+
3250
+ function dedupeUninstallRecords(records) {
3251
+ const seen = new Set();
3252
+ const deduped = [];
3253
+ for (const record of records) {
3254
+ const normalized = normalizeInstallRecord(record);
3255
+ if (!normalized) {
3256
+ continue;
3257
+ }
3258
+ const key = JSON.stringify([
3259
+ normalized.home,
3260
+ normalized.install_mode || null,
3261
+ normalized.install_dir || null,
3262
+ normalized.package_root || null,
3263
+ ]);
3264
+ if (seen.has(key)) {
3265
+ continue;
3266
+ }
3267
+ seen.add(key);
3268
+ deduped.push(normalized);
3269
+ }
3270
+ return deduped;
3271
+ }
3272
+
3273
+ function resolveUninstallRecords({ home, origin }) {
3274
+ const normalizedHome = normalizeHomePath(home);
3275
+ const currentPackageRoot = normalizeHomePath(repoRoot);
3276
+ const index = readInstallIndex();
3277
+ const installs = index.installs.map((item) => normalizeInstallRecord(item)).filter(Boolean);
3278
+ if (origin === 'npm') {
3279
+ const matching = installs.filter((item) => item.package_root === currentPackageRoot);
3280
+ if (matching.length > 0) {
3281
+ return dedupeUninstallRecords(matching);
3282
+ }
3283
+ return dedupeUninstallRecords([currentInstallRecord(normalizedHome)]);
3284
+ }
3285
+ const matching = installs.filter((item) => item.home === normalizedHome);
3286
+ if (matching.length > 0) {
3287
+ return dedupeUninstallRecords(matching);
3288
+ }
3289
+ const inferredInstallDir = fs.existsSync(path.join(normalizedHome, 'cli')) ? path.join(normalizedHome, 'cli') : null;
3290
+ return dedupeUninstallRecords([
3291
+ {
3292
+ ...currentInstallRecord(normalizedHome),
3293
+ install_dir: inferredInstallDir,
3294
+ },
3295
+ ]);
3296
+ }
3297
+
3298
+ function aggregateCodeOnlyUninstallPlan(records) {
3299
+ const removePaths = new Set();
3300
+ const preservePaths = new Set();
3301
+ const wrapperPaths = new Set();
3302
+ for (const record of records) {
3303
+ const installDir = record.install_dir || (fs.existsSync(path.join(record.home, 'cli')) ? path.join(record.home, 'cli') : null);
3304
+ const matchedWrappers = collectManagedWrapperPaths({
3305
+ home: record.home,
3306
+ installDir,
3307
+ explicitWrapperPaths: record.wrapper_paths || [],
3308
+ });
3309
+ const plan = buildCodeOnlyUninstallPlan({
3310
+ home: record.home,
3311
+ installDir,
3312
+ wrapperPaths: matchedWrappers,
3313
+ });
3314
+ for (const targetPath of plan.remove_paths) removePaths.add(targetPath);
3315
+ for (const targetPath of plan.preserve_paths) preservePaths.add(targetPath);
3316
+ for (const targetPath of plan.wrapper_paths) wrapperPaths.add(targetPath);
3317
+ }
3318
+ return {
3319
+ remove_paths: [...removePaths].sort(),
3320
+ preserve_paths: [...preservePaths].sort(),
3321
+ wrapper_paths: [...wrapperPaths].sort(),
3322
+ };
3323
+ }
3324
+
3325
+ async function promptUninstallConfirmation({ records, plan }) {
3326
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3327
+ throw new Error('DeepScientist uninstall needs a TTY for confirmation. Re-run with `--yes` to continue non-interactively.');
3328
+ }
3329
+ console.log('');
3330
+ console.log('DeepScientist uninstall');
3331
+ console.log('');
3332
+ console.log('This removes code and runtime directories, but preserves local data.');
3333
+ console.log('');
3334
+ for (const record of records) {
3335
+ console.log(`Home: ${record.home}`);
3336
+ }
3337
+ console.log('');
3338
+ console.log('Code/runtime paths to remove:');
3339
+ for (const targetPath of plan.remove_paths) {
3340
+ console.log(`- ${targetPath}`);
3341
+ }
3342
+ console.log('');
3343
+ console.log('Preserved data paths:');
3344
+ for (const targetPath of plan.preserve_paths) {
3345
+ console.log(`- ${targetPath}`);
3346
+ }
3347
+ const answer = await new Promise((resolve) => {
3348
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3349
+ rl.question('Type UNINSTALL to continue: ', (value) => {
3350
+ rl.close();
3351
+ resolve(String(value || '').trim());
3352
+ });
3353
+ });
3354
+ return answer === 'UNINSTALL';
3355
+ }
3356
+
3357
+ function runGlobalNpmUninstall() {
3358
+ const configuredPrefix = String(process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || '').trim();
3359
+ let uninstallPrefix = configuredPrefix || null;
3360
+ if (!uninstallPrefix && detectInstallMode(repoRoot) === 'npm-package') {
3361
+ let cursor = path.resolve(repoRoot);
3362
+ while (cursor && cursor !== path.dirname(cursor)) {
3363
+ if (path.basename(cursor) === 'node_modules') {
3364
+ const container = path.dirname(cursor);
3365
+ uninstallPrefix =
3366
+ path.basename(container) === 'lib'
3367
+ ? path.dirname(container)
3368
+ : container;
3369
+ break;
3370
+ }
3371
+ cursor = path.dirname(cursor);
3372
+ }
3373
+ }
3374
+ const npmBinary =
3375
+ resolveExecutableOnPath(process.platform === 'win32' ? 'npm.cmd' : 'npm')
3376
+ || resolveExecutableOnPath('npm');
3377
+ if (!npmBinary) {
3378
+ return {
3379
+ ok: false,
3380
+ message: 'Global npm package removal was skipped because `npm` is not available on PATH.',
3381
+ };
3382
+ }
3383
+ const result = spawnSync(
3384
+ npmBinary,
3385
+ ['uninstall', '-g', UPDATE_PACKAGE_NAME, ...(uninstallPrefix ? ['--prefix', uninstallPrefix] : [])],
3386
+ syncSpawnOptions({
3387
+ stdio: 'inherit',
3388
+ env: process.env,
3389
+ })
3390
+ );
3391
+ if (result.error) {
3392
+ return {
3393
+ ok: false,
3394
+ message: result.error.message,
3395
+ };
3396
+ }
3397
+ return {
3398
+ ok: result.status === 0,
3399
+ message: result.status === 0 ? null : `npm uninstall exited with status ${result.status ?? 1}.`,
3400
+ };
3401
+ }
3402
+
3403
+ function buildDaemonStatusPayload({ home, url, state, health, launcherPath = null }) {
3404
+ const healthy = Boolean(health && health.status === 'ok');
3405
+ const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
3406
+ return {
3407
+ healthy,
3408
+ identity_match: identityMatch,
3409
+ managed: Boolean(state),
3410
+ home,
3411
+ url,
3412
+ daemon_state_path: daemonStatePath(home),
3413
+ launcher_path: launcherPath || resolveLauncherPath() || null,
3414
+ daemon: state,
3415
+ health,
3416
+ };
3417
+ }
3418
+
2973
3419
  function daemonSupervisorLogPath(home) {
2974
3420
  return path.join(home, 'logs', 'daemon-supervisor.log');
2975
3421
  }
@@ -3540,6 +3986,86 @@ async function stopDaemon(home) {
3540
3986
  console.log('DeepScientist daemon stopped.');
3541
3987
  }
3542
3988
 
3989
+ async function uninstallMain(rawArgs) {
3990
+ const options = parseUninstallArgs(rawArgs);
3991
+ if (options.help) {
3992
+ printUninstallHelp();
3993
+ process.exit(0);
3994
+ }
3995
+ if (options.error) {
3996
+ console.error(options.error);
3997
+ console.error('Run `ds uninstall --help` for usage.');
3998
+ process.exit(1);
3999
+ }
4000
+
4001
+ const home = normalizeHomePath(options.home || resolveHome(rawArgs));
4002
+ const records = resolveUninstallRecords({ home, origin: options.origin });
4003
+ const plan = aggregateCodeOnlyUninstallPlan(records);
4004
+
4005
+ if (!options.yes && options.origin !== 'npm') {
4006
+ const confirmed = await promptUninstallConfirmation({ records, plan });
4007
+ if (!confirmed) {
4008
+ console.log('DeepScientist uninstall cancelled.');
4009
+ process.exit(1);
4010
+ }
4011
+ }
4012
+
4013
+ for (const record of records) {
4014
+ try {
4015
+ await stopDaemon(record.home);
4016
+ } catch (error) {
4017
+ console.warn(`DeepScientist could not fully stop the daemon for ${record.home}: ${error instanceof Error ? error.message : String(error)}`);
4018
+ }
4019
+ }
4020
+
4021
+ const removed = [];
4022
+ for (const targetPath of plan.remove_paths) {
4023
+ if (removePathEntry(targetPath)) {
4024
+ removed.push(targetPath);
4025
+ }
4026
+ }
4027
+ for (const targetPath of plan.wrapper_paths) {
4028
+ if (removePathEntry(targetPath)) {
4029
+ removed.push(targetPath);
4030
+ }
4031
+ }
4032
+
4033
+ removeInstallRecords((record) => records.some((candidate) => installRecordMatches(candidate, record)));
4034
+
4035
+ let npmRemovalMessage = null;
4036
+ if (options.origin !== 'npm' && detectInstallMode(repoRoot) === 'npm-package') {
4037
+ const npmRemoval = runGlobalNpmUninstall();
4038
+ if (!npmRemoval.ok) {
4039
+ npmRemovalMessage = npmRemoval.message;
4040
+ }
4041
+ }
4042
+
4043
+ console.log('');
4044
+ console.log('DeepScientist code uninstall completed.');
4045
+ if (removed.length > 0) {
4046
+ console.log('');
4047
+ console.log('Removed:');
4048
+ for (const targetPath of removed) {
4049
+ console.log(`- ${targetPath}`);
4050
+ }
4051
+ }
4052
+ console.log('');
4053
+ console.log('Preserved local data:');
4054
+ for (const targetPath of plan.preserve_paths) {
4055
+ console.log(`- ${targetPath}`);
4056
+ }
4057
+ console.log('');
4058
+ for (const record of records) {
4059
+ console.log(`If you also want to delete local data manually: rm -rf ${record.home}`);
4060
+ }
4061
+ if (npmRemovalMessage) {
4062
+ console.log('');
4063
+ console.warn(`Global npm package removal did not complete automatically: ${npmRemovalMessage}`);
4064
+ console.warn(`Run: npm uninstall -g ${UPDATE_PACKAGE_NAME}`);
4065
+ }
4066
+ process.exit(0);
4067
+ }
4068
+
3543
4069
  function writeUpdateLog(home, content) {
3544
4070
  const logPath = path.join(home, 'logs', 'update.log');
3545
4071
  ensureDir(path.dirname(logPath));
@@ -4335,7 +4861,7 @@ function handleCodexPreflightFailure(error) {
4335
4861
  : [
4336
4862
  'In most installs, `npm install -g @researai/deepscientist` also installs the bundled Codex dependency.',
4337
4863
  'If `codex` is still missing, run `npm install -g @openai/codex`.',
4338
- 'Run `codex --login` (or `codex`) and finish authentication.',
4864
+ 'Run `codex login` (or just `codex`) and finish authentication.',
4339
4865
  'Run `ds doctor` and confirm the Codex check passes.',
4340
4866
  'Run `ds` again.',
4341
4867
  ];
@@ -4638,9 +5164,14 @@ async function launcherMain(rawArgs) {
4638
5164
  const home = options.home || resolveHome(rawArgs);
4639
5165
  applyLauncherProxy(options.proxy);
4640
5166
  ensureDir(home);
5167
+ registerCurrentInstall(home);
5168
+ const forceWrapperRepair =
5169
+ detectInstallMode(repoRoot) !== 'npm-package'
5170
+ && Boolean(options.home || process.env.DEEPSCIENTIST_HOME);
4641
5171
  repairLegacyPathWrappers({
4642
5172
  home,
4643
5173
  launcherPath: resolveLauncherPath(),
5174
+ force: forceWrapperRepair,
4644
5175
  });
4645
5176
 
4646
5177
  if (options.stop) {
@@ -4655,24 +5186,17 @@ async function launcherMain(rawArgs) {
4655
5186
  const authToken = typeof state?.auth_token === 'string' ? state.auth_token.trim() : '';
4656
5187
  const probeUrl = state?.url || browserUiUrl(configured.host, configured.port);
4657
5188
  const health = await fetchHealth(probeUrl, authToken);
4658
- const healthy = Boolean(health && health.status === 'ok');
4659
- const identityMatch = state ? healthMatchesManagedState({ health, state, home }) : false;
5189
+ const statusPayload = buildDaemonStatusPayload({
5190
+ home,
5191
+ url,
5192
+ state,
5193
+ health,
5194
+ launcherPath: resolveLauncherPath(),
5195
+ });
4660
5196
  console.log(
4661
- JSON.stringify(
4662
- {
4663
- healthy,
4664
- identity_match: identityMatch,
4665
- managed: Boolean(state),
4666
- home,
4667
- url,
4668
- daemon: state,
4669
- health,
4670
- },
4671
- null,
4672
- 2
4673
- )
5197
+ JSON.stringify(statusPayload, null, 2)
4674
5198
  );
4675
- process.exit(healthy && (!state || identityMatch) ? 0 : 1);
5199
+ process.exit(statusPayload.healthy && (!state || statusPayload.identity_match) ? 0 : 1);
4676
5200
  }
4677
5201
 
4678
5202
  const pythonRuntime = ensurePythonRuntime(home);
@@ -4761,6 +5285,10 @@ async function main() {
4761
5285
  await migrateMain(args);
4762
5286
  return;
4763
5287
  }
5288
+ if (positional && positional.value === 'uninstall') {
5289
+ await uninstallMain(args);
5290
+ return;
5291
+ }
4764
5292
  if (
4765
5293
  args.length === 0
4766
5294
  || args[0] === 'ui'
@@ -4824,6 +5352,7 @@ module.exports = {
4824
5352
  generateBrowserAuthToken,
4825
5353
  appendBrowserAuthToken,
4826
5354
  normalizeProxyUrl,
5355
+ buildCodeOnlyUninstallPlan,
4827
5356
  parseMigrateArgs,
4828
5357
  parseLegacyWrapperCandidate,
4829
5358
  repairLegacyPathWrappers,
@@ -4833,11 +5362,13 @@ module.exports = {
4833
5362
  buildUvSyncFailureGuidance,
4834
5363
  updateManualCommand,
4835
5364
  buildUpdateStatus,
5365
+ buildDaemonStatusPayload,
4836
5366
  parseYesNoAnswer,
4837
5367
  normalizeLauncherRelaunchArgs,
4838
5368
  officialRepositoryLine,
4839
5369
  stripAnsi,
4840
5370
  normalizeLegacyHostFlagArgs,
5371
+ runGlobalNpmUninstall,
4841
5372
  },
4842
5373
  };
4843
5374