bookmark-research 0.5.0-beta.12 → 0.5.0-beta.14
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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -7
- package/README.zh.md +11 -7
- package/bin/bookmark-research.js +9 -1
- package/docs/details.en.md +1 -1
- package/docs/distribution.md +1 -1
- package/docs/harness-compatibility.md +1 -1
- package/docs/installation.en.md +5 -6
- package/docs/installation.md +5 -6
- package/docs/release-notes-0.5.0-beta.13.md +38 -0
- package/docs/release-notes-0.5.0-beta.14.md +38 -0
- package/package.json +3 -2
- package/scripts/host_clients.py +17 -23
- package/scripts/host_install.py +2 -2
- package/scripts/install.py +4 -1
- package/scripts/launcher.py +9 -2
- package/src/dsh_client.py +206 -0
- package/src/onboarding.py +119 -64
package/README.md
CHANGED
|
@@ -19,19 +19,19 @@ Works in **Codex, Claude Code, Pi and DSH (DeepSeek Harness)**.
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- Python 3.9+ with SQLite FTS5 (check with `bookmark-research doctor`)
|
|
22
|
-
- The CLI of at least one client: Codex, Claude Code, Pi or DSH (DSH also needs `pnpm
|
|
22
|
+
- The CLI of at least one client: Codex, Claude Code, Pi or DSH (DSH also needs `pnpm`, unless it is the DeepSeek Harness Desktop CLI, which carries its own)
|
|
23
23
|
- Node 18+ and npm for release installation (including the GitHub convenience script)
|
|
24
24
|
- No API key is needed to install or to query bookmarks locally
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
|
-
**Current beta (`0.5.0-beta.
|
|
28
|
+
**Current beta (`0.5.0-beta.14`)**, interactive on macOS, Linux and Windows:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
npx bookmark-research@0.5.0-beta.
|
|
31
|
+
npx bookmark-research@0.5.0-beta.14 install
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
The installer pins the running package version and needs no `--source`. Re-running it upgrades an existing Claude Code, Pi or DSH installation in place, keeping preferences, keys and data; `@beta` currently points to `0.5.0-beta.14`. A Codex installation keeps the source and version it registered with, so change its version by removing that registration first (see **Update** below).
|
|
35
35
|
|
|
36
36
|
**Default-channel interactive installation:**
|
|
37
37
|
|
|
@@ -54,7 +54,7 @@ For a permanent `bookmark-research` command: `npm install -g bookmark-research`.
|
|
|
54
54
|
|
|
55
55
|
**One release source for all four hosts:** npm contains the shared runtime, Skill and host adapters. The installer downloads the release, prepares the selected host format, then invokes the host’s native commands. Updates retain an npm selector rather than an npx cache path. GitHub hosts source and installation catalogs; unpublished source uses the Python installer. See [distribution policy and official references](docs/distribution.md).
|
|
56
56
|
|
|
57
|
-
**DSH installs the release directly:** the published package declares `dsh.bundle.patch`, so `dsh plugin --profile <name> add bookmark-research@<version>` — or the DSH plugin manager — installs it with no export first, resolving Python and MCP paths from the installed package. DeepSeek Harness Desktop reserves the `desktop` profile for its own carrier, so install that one with the app's CLI. See the [installation guide](docs/installation.en.md).
|
|
57
|
+
**DSH installs the release directly:** the published package declares `dsh.bundle.patch`, so `dsh plugin --profile <name> add bookmark-research@<version>` — or the DSH plugin manager — installs it with no export first, resolving Python and MCP paths from the installed package. DeepSeek Harness Desktop reserves the `desktop` profile for its own carrier, so install that one with the app's CLI; the installer and the wizard locate that bundled CLI themselves on macOS, Windows and Linux, and fall back to it when the `dsh` on PATH has no pnpm. See the [installation guide](docs/installation.en.md).
|
|
58
58
|
|
|
59
59
|
**Windows:** the installer writes the detected Python path into each client's MCP config, because Windows often has no working `python3`. Install Codex with `bookmark-research install codex` rather than adding the repository to Codex directly. `bookmark-research status` shows the interpreter in use.
|
|
60
60
|
|
|
@@ -81,7 +81,7 @@ No reinstall is needed. Settings and keys apply to every installed client; start
|
|
|
81
81
|
|
|
82
82
|
| Task | Command |
|
|
83
83
|
| --- | --- |
|
|
84
|
-
| Menu: configure, check, install, update | `bookmark-research` |
|
|
84
|
+
| Menu: configure, check, install, update | `bookmark-research` (alias `bookmark`, or `bookmark research`) |
|
|
85
85
|
| Preferences and API keys | `bookmark-research setup` |
|
|
86
86
|
| Installed clients, preferences, storage locations, recent research, key status | `bookmark-research status` |
|
|
87
87
|
| Script preferences | `bookmark-research config show` · `bookmark-research config set --input prefs.json` |
|
|
@@ -89,6 +89,10 @@ No reinstall is needed. Settings and keys apply to every installed client; start
|
|
|
89
89
|
|
|
90
90
|
Use `npx bookmark-research@latest …` if the command is not installed globally.
|
|
91
91
|
|
|
92
|
+
**Open the configuration page.** In a terminal, `bookmark-research` (or its alias `bookmark`) with no arguments opens the menu: change preferences and API keys, check services, install into another client, update, or print status. `bookmark-research setup` goes straight to the guided wizard, and adding `--lang zh` shows the wizard and menu in Chinese. Preferences and keys are user-wide, so one change applies to every installed client; start a new client session afterwards.
|
|
93
|
+
|
|
94
|
+
**Update.** `bookmark-research update` refreshes every installed client from its registered source and keeps preferences, keys and data. Re-running `npx bookmark-research@latest install` also upgrades an existing Claude Code, Pi or DSH installation in place. A Codex installation keeps the source and version it was registered with; to move it to another version, remove that registration first (`codex plugin marketplace remove bookmark-research`) and install again.
|
|
95
|
+
|
|
92
96
|
**API keys are optional.** Enter them in `bookmark-research setup` (hidden input, checked immediately, saved in a private local file) or set environment variables, which take precedence. Keys are never accepted in chat, command arguments or JSON files.
|
|
93
97
|
|
|
94
98
|
| Variable | Service | Without it |
|
|
@@ -139,7 +143,7 @@ A bundle of one Skill, one local MCP server, and the remote MCPs and services th
|
|
|
139
143
|
|
|
140
144
|
Local MCP tools:
|
|
141
145
|
|
|
142
|
-
This list describes the current source tree
|
|
146
|
+
This list describes the current source tree; `wiki_refresh` and `wiki_work` have shipped in every release since `0.5.0-beta.10`. Implementation status and remaining work: [upgrade checklist](docs/upgrade-status-2026-10-11.md).
|
|
143
147
|
|
|
144
148
|
| Group | Tools |
|
|
145
149
|
| --- | --- |
|
package/README.zh.md
CHANGED
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
## 环境要求
|
|
20
20
|
|
|
21
21
|
- Python 3.9+,含 SQLite FTS5(可用 `bookmark-research doctor` 检查)
|
|
22
|
-
- 至少一个宿主的 CLI:Codex、Claude Code、Pi 或 DSH(DSH 另需 `pnpm
|
|
22
|
+
- 至少一个宿主的 CLI:Codex、Claude Code、Pi 或 DSH(DSH 另需 `pnpm`;若用 DeepSeek Harness 桌面版自带 CLI,则它自带 pnpm,无需另装)
|
|
23
23
|
- 发布版安装需要 Node 18+ 和 npm(GitHub 便捷脚本也需要)
|
|
24
24
|
- 安装和本地查书签都不需要 API Key
|
|
25
25
|
|
|
26
26
|
## 安装
|
|
27
27
|
|
|
28
|
-
**当前测试版(`0.5.0-beta.
|
|
28
|
+
**当前测试版(`0.5.0-beta.14`)**,支持 macOS、Linux、Windows 交互安装:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
npx bookmark-research@0.5.0-beta.
|
|
31
|
+
npx bookmark-research@0.5.0-beta.14 install
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
安装器会锁定当前运行的版本,无需再手动指定 `--source`。重跑该命令可就地升级已装的 Claude Code、Pi 或 DSH,偏好、Key 和数据都保留;`@beta` 标签当前指向 `0.5.0-beta.14`。Codex 会保留登记时的来源与版本,要换版本需先移除该登记(见下文 **更新**)。
|
|
35
35
|
|
|
36
36
|
**默认渠道的交互安装:**
|
|
37
37
|
|
|
@@ -54,7 +54,7 @@ npx bookmark-research@latest install
|
|
|
54
54
|
|
|
55
55
|
**四个宿主统一发布来源:** npm 包包含共享运行时、Skill 和宿主适配。安装器下载发布包,生成所选宿主格式,再调用宿主原生命令;更新记录 npm 来源,不依赖 npx 临时缓存。GitHub 提供源码和安装清单,未发布源码用 Python 安装器。见[发布约定与官方依据](docs/distribution.md)。
|
|
56
56
|
|
|
57
|
-
**DSH 可直接安装发布包:** 发布包声明 `dsh.bundle.patch`,`dsh plugin --profile <名称> add bookmark-research@<版本>`(或 DSH 插件管理界面)无需先导出即可安装,Python 与 MCP 路径按安装位置解析。DeepSeek Harness 桌面版把 `desktop` profile 保留给自身载体,该 profile 请用桌面版自带 CLI
|
|
57
|
+
**DSH 可直接安装发布包:** 发布包声明 `dsh.bundle.patch`,`dsh plugin --profile <名称> add bookmark-research@<版本>`(或 DSH 插件管理界面)无需先导出即可安装,Python 与 MCP 路径按安装位置解析。DeepSeek Harness 桌面版把 `desktop` profile 保留给自身载体,该 profile 请用桌面版自带 CLI 安装;安装器与向导会在 macOS、Windows、Linux 上自行定位该 CLI,并在 PATH 中的 `dsh` 缺少 pnpm 时改用它。详见[安装说明](docs/installation.md)。
|
|
58
58
|
|
|
59
59
|
**Windows:** Windows 上常常没有能用的 `python3`,所以安装器会把检测到的 Python 路径写进各宿主的 MCP 配置。Codex 请用 `bookmark-research install codex` 安装,不要直接把仓库加进 Codex。`bookmark-research status` 可以查看实际使用的 Python。
|
|
60
60
|
|
|
@@ -81,7 +81,7 @@ npx bookmark-research@latest install
|
|
|
81
81
|
|
|
82
82
|
| 要做什么 | 命令 |
|
|
83
83
|
| --- | --- |
|
|
84
|
-
| 主菜单:配置、检查、安装、更新 | `bookmark-research` |
|
|
84
|
+
| 主菜单:配置、检查、安装、更新 | `bookmark-research`(别名 `bookmark`,或 `bookmark research`) |
|
|
85
85
|
| 修改偏好和 API Key | `bookmark-research setup` |
|
|
86
86
|
| 查看已装宿主、偏好、保存位置、最近研究、Key 状态 | `bookmark-research status` |
|
|
87
87
|
| 用脚本改偏好 | `bookmark-research config show` · `bookmark-research config set --input prefs.json` |
|
|
@@ -89,6 +89,10 @@ npx bookmark-research@latest install
|
|
|
89
89
|
|
|
90
90
|
没有全局安装时,在前面加 `npx`,例如 `npx bookmark-research@latest setup`。
|
|
91
91
|
|
|
92
|
+
**进入配置页。** 在终端里不带参数运行 `bookmark-research`(或别名 `bookmark`)即可打开菜单:修改配置与 API Key、检查服务、安装到其他宿主、更新、查看状态。`bookmark-research setup` 直接进入引导向导,加上 `--lang zh` 可让向导和菜单显示中文。偏好和 Key 是用户级的,改一次就对所有已安装宿主生效;改完新建宿主会话即可。
|
|
93
|
+
|
|
94
|
+
**更新。** `bookmark-research update` 会按各自登记的来源刷新所有已安装宿主,并保留偏好、Key 和数据。重跑 `npx bookmark-research@latest install` 也会就地升级已装的 Claude Code、Pi 或 DSH。Codex 安装会保留登记时的来源与版本,要换版本需先移除该登记(`codex plugin marketplace remove bookmark-research`)再重新安装。
|
|
95
|
+
|
|
92
96
|
**API Key 都是可选的。** 在 `bookmark-research setup` 里输入(隐藏显示,输完当场检查,保存在仅当前用户可读的本地文件),或者设置环境变量(优先于已保存的 Key)。Key 不接受对话、命令参数或 JSON 文件。
|
|
93
97
|
|
|
94
98
|
| 环境变量 | 服务 | 不配置时 |
|
|
@@ -139,7 +143,7 @@ setup 还会问研究报告和证据放在哪里:统一目录(默认 `<数
|
|
|
139
143
|
|
|
140
144
|
本地 MCP 的工具:
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
下表对应当前源码;`wiki_refresh` 和 `wiki_work` 自 `0.5.0-beta.10` 起已包含在每个发布包中。已实现与剩余事项见[升级核对记录](docs/upgrade-status-2026-10-11.md)。
|
|
143
147
|
|
|
144
148
|
| 分类 | 工具 |
|
|
145
149
|
| --- | --- |
|
package/bin/bookmark-research.js
CHANGED
|
@@ -22,8 +22,16 @@ if (!python) {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// The same file is installed as `bookmark-research` and as the short alias
|
|
26
|
+
// `bookmark`. A leading literal `research` is accepted in both spellings, so
|
|
27
|
+
// `bookmark status`, `bookmark research status` and `bookmark-research status`
|
|
28
|
+
// all run the same command, while `bookmark` or `bookmark research` alone
|
|
29
|
+
// opens the menu.
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
if (args[0] === "research") args.shift();
|
|
32
|
+
|
|
25
33
|
const [command, ...prefix] = python;
|
|
26
|
-
const child = spawn(command, [...prefix, launcher, ...
|
|
34
|
+
const child = spawn(command, [...prefix, launcher, ...args], { stdio: "inherit", windowsHide: true });
|
|
27
35
|
// The terminal delivers Ctrl+C to the whole process group; let Python handle it.
|
|
28
36
|
process.on("SIGINT", () => {});
|
|
29
37
|
child.on("exit", (code, signal) => process.exit(signal ? 130 : code ?? 1));
|
package/docs/details.en.md
CHANGED
|
@@ -16,7 +16,7 @@ Reference material moved from the README: components, data model, research modes
|
|
|
16
16
|
| **Host workflows** | [`hosts/`](../hosts/) and [`src/routing.py`](../src/routing.py) | Native Codex delegation, Claude workflow, Pi extension recipe and DSH workflow adapter |
|
|
17
17
|
| **Professional research** | [`src/research_services.py`](../src/research_services.py) | Optional provider-owned background tasks, saved run IDs and secondary-report import |
|
|
18
18
|
| **Wiki and evaluation** | [`src/wiki.py`](../src/wiki.py), [`src/evaluation.py`](../src/evaluation.py) | Reviewed knowledge, immutable revisions, source/link lint and metrics from explicit labels |
|
|
19
|
-
| **Client integrations** | Native manifests, [`bin/bookmark-research.js`](../bin/bookmark-research.js), [`scripts/launcher.py`](../scripts/launcher.py), [`install.sh`](../install.sh), [`scripts/install.py`](../scripts/install.py) and [`scripts/export_bundle.py`](../scripts/export_bundle.py) | The `bookmark-research` command, guided installation and native install/update/verify for all four hosts |
|
|
19
|
+
| **Client integrations** | Native manifests, [`bin/bookmark-research.js`](../bin/bookmark-research.js), [`scripts/launcher.py`](../scripts/launcher.py), [`install.sh`](../install.sh), [`scripts/install.py`](../scripts/install.py) and [`scripts/export_bundle.py`](../scripts/export_bundle.py) | The `bookmark-research` command (alias `bookmark`, or `bookmark research`), guided installation and native install/update/verify for all four hosts |
|
|
20
20
|
|
|
21
21
|
The runtime requires Python 3.9+ and SQLite with FTS5; `doctor` checks these requirements. It uses the Python standard library and does not require a separate database service.
|
|
22
22
|
|
package/docs/distribution.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
候选版本在 `release/` 分支同步三个版本清单并运行 CI。该分支的 registry 集成测试使用当前已发布渠道版本,源码测试、原生安装测试和 npm 打包检查仍验证候选内容;主分支的 registry 测试始终验证 marketplace 固定版本。发布通过 CI 的 npm 包并核验 registry 后,再将候选分支合入主分支,避免默认安装清单提前指向未发布版本。
|
|
23
23
|
|
|
24
|
-
测试版使用 npm 的 `beta` 标签;若创建 GitHub
|
|
24
|
+
测试版使用 npm 的 `beta` 标签;若创建 GitHub 版本说明,则标记为预发布。安装测试版推荐直接使用 `npx bookmark-research@0.5.0-beta.14 install` 或 `npx bookmark-research@beta install`,启动器会自动读取当前运行包版本并固定内容来源,无需重复指定 `--source`。已存在的宿主配置支持无缝覆盖升级。
|
|
25
25
|
|
|
26
26
|
## 官方资料与项目实例
|
|
27
27
|
|
|
@@ -159,7 +159,7 @@ DSH 的原生运行插件是 Cordis 模块,可导出 `name`、`inject` 和 `ap
|
|
|
159
159
|
|
|
160
160
|
发布包现在**自带** `bundle.patch.yml` 并在 `package.json` 声明 `dsh.bundle.patch`,因此 `dsh plugin --profile <name> add bookmark-research@<version>`(以及 DSH 插件管理界面)可以直接从 registry 安装;`dsh` 的 bundle 对账逻辑会把声明了 `dsh.bundle` 且 patch 存在的包自动加入 `dsh.profile.bundles`。层内不写死本机路径:`command`、`args`、`cwd` 与转发的环境变量都由入口模块 `hosts/dsh/plugin.js` 在加载时提供(`bookmarkResearchPaths.python/mcpArgs/env`),模块用与 npm 启动器相同的顺序发现 Python 3.9+/SQLite,所以导出副本、registry 安装和 Windows 都不需要重生成 patch。统一安装器仍导出带校验和的固定副本,用于可验证的托管安装;对不含该文件的旧发布包或旧 Git ref,导出器按模块的旧契约生成等价层,安装不回归。[D02][D03]
|
|
161
161
|
|
|
162
|
-
DSH 桌面版把 profile 名 `desktop` 保留给 Electron 载体:启动器对该名字拒绝一切 boot/dump(`--dump-config` 亦然),只有载体自身可以管理其插件。因此对 `desktop` 的安装须用桌面版自带 CLI
|
|
162
|
+
DSH 桌面版把 profile 名 `desktop` 保留给 Electron 载体:启动器对该名字拒绝一切 boot/dump(`--dump-config` 亦然),只有载体自身可以管理其插件。因此对 `desktop` 的安装须用桌面版自带 CLI(它自带 pnpm,无需 PATH 中的 pnpm)。安装器与向导会自行定位该 CLI——PATH 中的 `dsh`、macOS 的 `/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh`(含 `~/Applications`)、Windows 的 `%LOCALAPPDATA%\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd` 与 `ProgramFiles` 同一布局、Linux 的 `/opt/DeepSeek Harness/resources/runtime/cli/bin/dsh`——并在 PATH 中的 CLI 缺少 pnpm 时改用桌面版,所以 `--dsh "<App>/Contents/Resources/runtime/cli/bin/dsh"` 现在只在非常规安装位置才需要。安装器的验证改为读取持久层:profile 依赖指向该包、`dsh.bundle.patch` 与入口模块按包目录相对解析且文件存在、层内包含 Skill 与 MCP 行。2026-10-08 在本机(macOS、桌面运行时 0.2.0-rc.2、发布包 0.5.0-beta.5)实测:注册后 Skill 出现在宿主目录,`mcp__bookmark-research__*` 工具正常应答,运行中的 MCP 子进程解析到已安装包目录。[D02][D08]
|
|
163
163
|
|
|
164
164
|
导出及查看合并配置的形式是:
|
|
165
165
|
|
package/docs/installation.en.md
CHANGED
|
@@ -6,7 +6,7 @@ Install the same plugin for English or Chinese use. See the [user guide](user-gu
|
|
|
6
6
|
|
|
7
7
|
## The `bookmark-research` command (recommended)
|
|
8
8
|
|
|
9
|
-
After `npm install -g bookmark-research`, type `bookmark-research` to open a menu for preferences and API keys, service checks, installing into another client, updates and status. Subcommands work directly: `install [HOST...]`, `update`, `verify`, `setup`, `status`, `config show|set`, `doctor`. npm is the single release package for all four hosts and includes the Python installer, runtime, Skill and adapters, so Python 3.9+ is still required; it works on native Windows, macOS and Linux. Use `npx bookmark-research@latest install HOST` for a release, and the Python source installer for unpublished development changes. Without arguments and without a terminal (agents/CI) it prints status JSON instead of opening the menu.
|
|
9
|
+
After `npm install -g bookmark-research`, type `bookmark-research` to open a menu for preferences and API keys, service checks, installing into another client, updates and status. The short alias `bookmark` is equivalent, and `bookmark research` matches `bookmark-research` (`bookmark` alone opens the menu; `bookmark research status` equals `bookmark-research status`). Subcommands work directly: `install [HOST...]`, `update`, `verify`, `setup`, `status`, `config show|set`, `doctor`. npm is the single release package for all four hosts and includes the Python installer, runtime, Skill and adapters, so Python 3.9+ is still required; it works on native Windows, macOS and Linux. Use `npx bookmark-research@latest install HOST` for a release, and the Python source installer for unpublished development changes. Without arguments and without a terminal (agents/CI) it prints status JSON instead of opening the menu.
|
|
10
10
|
|
|
11
11
|
<a id="one-command-codex-installation"></a>
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ Requirements: Bash, Node.js/npm, Python 3.9+ with SQLite FTS5, and the selected
|
|
|
18
18
|
curl -fsSL https://raw.githubusercontent.com/Browser-bookmark-hub/Bookmark-Research/main/install.sh | bash
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The terminal wizard detects available CLIs and lets you choose one or more hosts: arrow keys move, Space toggles, Enter confirms, and detected hosts are preselected. It then asks each host's scope/project/profile and shows a summary to confirm before installing. It works with piped installation by reading `/dev/tty`; without a capable terminal, or with `BOOKMARK_RESEARCH_PLAIN=1`, it uses plain numbered prompts. The npm installer checks the selected host before native registration. `--interactive` requires a terminal; `--non-interactive` never prompts. Without a terminal or an explicit host, the legacy default is Codex. Agents should pass `--host`.
|
|
21
|
+
The terminal wizard detects available CLIs and lets you choose one or more hosts: arrow keys move, Space toggles, Enter confirms, and detected hosts are preselected. DSH counts as detected when either a usable `dsh` is on PATH or a DeepSeek Harness Desktop installation with its own pnpm is found, so the app's bundled CLI is offered without any flag; for DSH the wizard then lists the initialized profiles under `$DSH_HOME/profiles` with the source this plugin is already registered from and whether that bundle is enabled, and still accepts a new profile name. It then asks each host's scope/project/profile and shows a summary to confirm before installing. It works with piped installation by reading `/dev/tty`; without a capable terminal, or with `BOOKMARK_RESEARCH_PLAIN=1`, it uses plain numbered prompts. The npm installer checks the selected host before native registration. `--interactive` requires a terminal; `--non-interactive` never prompts. Without a terminal or an explicit host, the legacy default is Codex. Agents should pass `--host`.
|
|
22
22
|
|
|
23
23
|
The first installation selects `npm:bookmark-research` (`latest`). All four hosts receive a clean export from that release through their native registration commands. The recorded source is the npm package and selector, not the temporary download or npx cache. `update` downloads that selector again; it does not need the original cache. GitHub serves this convenience script and source catalogs, not a separate release ZIP. Persistent host exports live under `${XDG_DATA_HOME:-~/.local/share}/bookmark-research/installations/` (override with `BOOKMARK_RESEARCH_INSTALL_DIR`; other hosts also accept `--install-dir`). Temporary downloads can then be removed. Start a new host session to load the Skill and tools.
|
|
24
24
|
|
|
@@ -52,7 +52,7 @@ The Codex branch manages `bookmark-research@bookmark-research`. If already insta
|
|
|
52
52
|
|
|
53
53
|
## Guided setup and readiness
|
|
54
54
|
|
|
55
|
-
After installation
|
|
55
|
+
After installation the wizard asks only a few questions: research depth, answer language, search and page-reading services (one ordered multiselect; unselected providers become the fallback search tier), where archives and research results are stored (default data folder, one custom folder for both, or research reports beside the bookmark file/folder with fallback to the central folder; new tasks only), and optional professional APIs. Wiki follow-up, the pre-research availability check and connectivity checks are enabled by default (suggest and ask first, 15-minute cache, automatic) and can be changed later with `setup` or `config set`. Enter keeps displayed preferences. API-key input is masked; each key is checked immediately and can be re-entered if the check fails. Keys are saved separately in `credentials.json` next to settings, with mode `0600`; this local file is not encrypted. `BOOKMARK_RESEARCH_CREDENTIALS` overrides its path, and environment variables override saved keys. No keys enter preferences, packages, receipts or readiness output. A ChatGPT/Codex login is separate from an OpenAI API key.
|
|
56
56
|
|
|
57
57
|
Rerun setup at any time with `python3 <installed path>/src/cli.py setup`; the exact command (`getting_started.setup_command` in the JSON result) is printed after installation. From a checkout:
|
|
58
58
|
|
|
@@ -183,11 +183,10 @@ python3 scripts/install.py verify --host dsh --profile web
|
|
|
183
183
|
|
|
184
184
|
The installer exports a relocatable `dsh.bundle` and calls `dsh plugin --profile web add PATH`. A released package also installs straight from the registry: it declares `dsh.bundle.patch` and ships that layer, so `dsh plugin --profile web add bookmark-research@VERSION` — or the DSH plugin manager — needs no export first. The layer resolves the interpreter, the working directory and the MCP argument list from the installed module, so nothing is tied to the machine that built it; the selected profile must still provide the Skill registry and the official `@deepseek-ai/dsh-mcp-client`.
|
|
185
185
|
|
|
186
|
-
`dsh plugin` delegates to pnpm. A generic CLI therefore needs pnpm on PATH (`npm install -g pnpm` or `corepack enable pnpm`) and the installer stops before creating a profile without it; the DeepSeek Harness Desktop CLI carries its own pnpm and needs none. DeepSeek Harness Desktop reserves the `desktop` profile — its launcher refuses every boot and dump for that name — so
|
|
186
|
+
`dsh plugin` delegates to pnpm. A generic CLI therefore needs pnpm on PATH (`npm install -g pnpm` or `corepack enable pnpm`) and the installer stops before creating a profile without it; the DeepSeek Harness Desktop CLI carries its own pnpm and needs none. The installer and the guided wizard look for a usable CLI themselves, so `--dsh` is only needed when dsh and the app live somewhere unusual: `dsh` on PATH first, then the Desktop installation layout — `/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh` (also `~/Applications`) on macOS, `%LOCALAPPDATA%\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd` and the `ProgramFiles` equivalent on Windows, and the community Linux package root `/opt/DeepSeek Harness/resources/runtime/cli/bin/dsh`. Desktop can also register that CLI as `dsh` from its **Manage dsh Command…** menu (macOS `/usr/local/bin/dsh`, Windows user PATH); Linux has no such registration, and an AppImage is not discovered. A CLI found on PATH that cannot run `dsh plugin` is replaced by an installed Desktop app, and the plan prints the exact CLI that will run. DeepSeek Harness Desktop reserves the `desktop` profile — its launcher refuses every boot and dump for that name, and the npm CLI refuses plugin management for it — so that profile is installed only with the app's own CLI, which the wizard pre-selects and names `desktop` for by default. Verification then reads the persisted layer instead of `--dump-config`:
|
|
187
187
|
|
|
188
188
|
```sh
|
|
189
|
-
python3 scripts/install.py install --host dsh --profile desktop
|
|
190
|
-
--dsh "<DSH app>/Contents/Resources/runtime/cli/bin/dsh"
|
|
189
|
+
python3 scripts/install.py install --host dsh --profile desktop
|
|
191
190
|
```
|
|
192
191
|
|
|
193
192
|
The legacy `cordis.patch.yml` still contains absolute paths and requires separate Skill discovery. Workflow execution separately requires the host service/engine; `hosts/dsh/workflow-call.py --research-id RID --run-key KEY` prepares a call without launching it.
|
package/docs/installation.md
CHANGED
|
@@ -8,7 +8,7 @@ Bookmark Research 使用 Python 3.9+ 标准库和 SQLite FTS5。先安装 Python
|
|
|
8
8
|
|
|
9
9
|
## `bookmark-research` 命令(推荐)
|
|
10
10
|
|
|
11
|
-
`npm install -g bookmark-research` 之后在终端输入 `bookmark-research`,即可打开主菜单:修改配置与 API Key
|
|
11
|
+
`npm install -g bookmark-research` 之后在终端输入 `bookmark-research`,即可打开主菜单:修改配置与 API Key、检查服务、安装到其他宿主、更新、查看状态;短别名 `bookmark` 与 `bookmark research` 等价(`bookmark` 单独输入即打开菜单,`bookmark research status` 等同 `bookmark-research status`)。也可以直接带子命令:`install [宿主…]`、`update`、`verify`、`setup`、`status`、`config show|set`、`doctor`。npm 是四个宿主共用的唯一正式发布包,包含 Python 安装器、运行时、Skill 和适配文件,因此仍需 Python 3.9+;Windows 原生、macOS、Linux 都可以用。发布版使用 `npx bookmark-research@latest install HOST`;未发布源码使用 Python 源码安装器。不带参数且不在终端中运行时(agent/CI),它只输出状态 JSON,不弹菜单。
|
|
12
12
|
|
|
13
13
|
## 一条命令引导安装
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ Bookmark Research 使用 Python 3.9+ 标准库和 SQLite FTS5。先安装 Python
|
|
|
18
18
|
curl -fsSL https://raw.githubusercontent.com/Browser-bookmark-hub/Bookmark-Research/main/install.sh | bash
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
终端向导检测可用 CLI,可多选 Codex、Claude Code、Pi、DSH
|
|
21
|
+
终端向导检测可用 CLI,可多选 Codex、Claude Code、Pi、DSH:方向键移动、空格勾选、回车确认,已检测到的宿主默认勾选。DSH 在「PATH 中有可用的 `dsh`」或「找到自带 pnpm 的 DeepSeek Harness 桌面版」时都算检测到,因此桌面版自带 CLI 无需任何参数即可勾选;选中 DSH 后,向导会列出 `$DSH_HOME/profiles` 下已初始化的 profile,并标注本插件已登记的来源与是否已启用,也可另填新名称。随后逐个询问作用域/项目/profile,并显示摘要确认后再安装。`curl | bash` 从 `/dev/tty` 读取回答,不会误读脚本或 Agent 的输入;终端不支持或设置 `BOOKMARK_RESEARCH_PLAIN=1` 时改用普通编号提示。npm 安装器会在原生登记前检查所选宿主。`--interactive` 要求终端,`--non-interactive` 不询问;没有终端且未指定宿主时保留默认 Codex 的旧行为,Agent 应显式传入 `--host`。
|
|
22
22
|
|
|
23
23
|
首次安装选择 `npm:bookmark-research`(`latest`)。四个宿主都从该发布包导出所需格式,再由原生命令登记与验证。安装记录保存 npm 包名和版本选择,不保存临时下载或 npx 缓存路径;更新会重新取得同一选择器指向的发布包。各宿主的持久导出保存在 `${XDG_DATA_HOME:-~/.local/share}/bookmark-research/installations/`;可用 `BOOKMARK_RESEARCH_INSTALL_DIR` 改位置,其他宿主也支持 `--install-dir`。随后清理临时下载目录,安装后新建宿主会话。
|
|
24
24
|
|
|
@@ -69,7 +69,7 @@ Codex 分支管理 `bookmark-research@bookmark-research`。已通过 `personal`
|
|
|
69
69
|
|
|
70
70
|
## 向导与就绪检查
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
安装成功后只需回答少数问题:研究深度、答复语言、搜索与正文读取服务(一个有序多选列表;未选中的服务自动作为备用搜索层)、归档与研究结果的存放位置(默认数据目录、自定义统一目录,或研究报告放在书签文件/文件夹旁边、放不了时退回统一目录;只影响新任务),以及可选专业 API。Wiki 整理、联网前的可用性检查与连通性检查都按默认值自动启用(分别为「建议并先询问」「15 分钟缓存」和自动检查),可随时在后续 `setup` 或 `config set` 中修改。回车保留显示的偏好。API Key 掩码输入并立即检查,失败可重新输入;密钥单独保存为配置旁权限 `0600` 的 `credentials.json`,是本地明文文件;可用 `BOOKMARK_RESEARCH_CREDENTIALS` 指定位置。环境变量优先于保存的密钥,密钥不进入偏好 JSON、安装包、安装记录或就绪检查输出。ChatGPT/Codex 登录与 OpenAI API Key 分开。
|
|
73
73
|
|
|
74
74
|
以后随时运行 `python3 <安装路径>/src/cli.py setup` 重开向导,完整命令(JSON 结果中的 `getting_started.setup_command`)会在安装结束时打印;在源码中可运行:
|
|
75
75
|
|
|
@@ -246,11 +246,10 @@ python3 scripts/install.py install --host dsh --profile web
|
|
|
246
246
|
python3 scripts/install.py verify --host dsh --profile web
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
`dsh plugin` 由 pnpm 执行:通用 CLI 需要 PATH 中的 pnpm(`npm install -g pnpm` 或 `corepack enable pnpm`),缺少时安装器在创建 profile 前停止;DSH 桌面版 CLI 自带 pnpm
|
|
249
|
+
`dsh plugin` 由 pnpm 执行:通用 CLI 需要 PATH 中的 pnpm(`npm install -g pnpm` 或 `corepack enable pnpm`),缺少时安装器在创建 profile 前停止;DSH 桌面版 CLI 自带 pnpm,无需另装。安装器与终端向导会自行寻找可用 CLI,只有 dsh 与桌面版装在非常规位置时才需要 `--dsh`:先找 PATH 中的 `dsh`,再按桌面版安装布局查找——macOS 为 `/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh`(`~/Applications` 同样检查),Windows 为 `%LOCALAPPDATA%\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd` 及 `ProgramFiles` 下的同一布局,Linux 为社区打包的 `/opt/DeepSeek Harness/resources/runtime/cli/bin/dsh`。桌面版还能在 **Manage dsh Command…** 菜单里把该 CLI 登记为 `dsh`(macOS 写 `/usr/local/bin/dsh`,Windows 写入用户 PATH),Linux 没有这项登记,AppImage 也不会被探测到。PATH 中已有的 CLI 若无法执行 `dsh plugin`,会自动改用已安装的桌面版,并在安装计划中打印实际使用的 CLI。桌面版把 profile 名 `desktop` 保留给 Electron 载体,其启动器对该名字拒绝 boot/dump,npm CLI 也拒绝为它管理插件,因此该 profile 只能用桌面版自带 CLI 安装;向导会默认选中并预填 `desktop`。验证改为读取持久层而非 `--dump-config`:
|
|
250
250
|
|
|
251
251
|
```sh
|
|
252
|
-
python3 scripts/install.py install --host dsh --profile desktop
|
|
253
|
-
--dsh "<DSH 应用>/Contents/Resources/runtime/cli/bin/dsh"
|
|
252
|
+
python3 scripts/install.py install --host dsh --profile desktop
|
|
254
253
|
```
|
|
255
254
|
|
|
256
255
|
手动分发时使用 `export_bundle.py --format dsh`,再用上述原生命令安装稳定导出目录;不含该层的旧发布包或旧 Git ref 仍会按模块的旧契约生成等价层。保留的旧 `cordis.patch.yml` 仍含绝对路径,需单独配置 Skill 发现;移动后应重新生成。多组研究另需 workflow service、worker-thread engine 和 `workflow` tool。参见 [官方 MCP client 文档](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md)。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Bookmark Research 0.5.0 beta 13
|
|
2
|
+
|
|
3
|
+
**Beta / 测试版.** Published through the npm `beta` channel and synced with `latest`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Interactive multi-host installation and seamless upgrade:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx bookmark-research@0.5.0-beta.13 install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or simply:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx bookmark-research@beta install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Choose Codex, Claude Code, Pi or DSH in the interactive wizard. Existing user preferences, API credentials, research archives, and Wiki databases are strictly preserved during upgrade.
|
|
20
|
+
|
|
21
|
+
For an existing DSH profile:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
dsh plugin --profile <name> add bookmark-research@0.5.0-beta.13
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Changes
|
|
28
|
+
|
|
29
|
+
- **Installer Version Locking Bug Fix**: Fixed a critical issue in `scripts/host_install.py` where installing an npm release would erroneously overwrite the requested version with an older receipt version (`old["ref"]`), preventing Claude Code and other hosts from upgrading. The ref retention logic is now strictly scoped to Git checkouts.
|
|
30
|
+
- **Auto-Versioned Launcher Pinning**: Updated `scripts/launcher.py` to automatically detect and default `--source` to the running package's version (`npm:bookmark-research@{version}`). Users no longer need to type redundant `--source npm:bookmark-research@VERSION` flags when upgrading via `npx`.
|
|
31
|
+
- **Seamless Upgrade Experience**: Enhanced host installer to cleanly update native bundles, validate plugins, and retain all existing user settings and configurations without requiring re-entering API keys.
|
|
32
|
+
- **Comprehensive Regression Tests**: Added unit tests in `tests/test_host_install.py` and `tests/test_launcher.py` to prevent version regressions during upgrades.
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
Full test suite passing across all platforms.
|
|
37
|
+
|
|
38
|
+
Install from [npm](https://www.npmjs.com/package/bookmark-research/v/0.5.0-beta.13). GitHub provides source and release notes; npm is the shared release package for all four hosts.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Bookmark Research 0.5.0 beta 14
|
|
2
|
+
|
|
3
|
+
**Beta / 测试版.** Published through the npm `beta` channel.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Interactive multi-host installation and seamless upgrade:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx bookmark-research@0.5.0-beta.14 install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or simply:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx bookmark-research@beta install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Choose Codex, Claude Code, Pi or DSH in the interactive wizard. Existing user preferences, API credentials, research archives, and Wiki databases are strictly preserved during upgrade.
|
|
20
|
+
|
|
21
|
+
For an existing DSH profile:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
dsh plugin --profile <name> add bookmark-research@0.5.0-beta.14
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Changes
|
|
28
|
+
|
|
29
|
+
- **Streamlined Setup Workflow**: Greatly simplified `onboarding.py` prompts, reducing redundant questions during initial installation and relying on well-tested, robust defaults for search concurrency, fallback strategies, and storage directories.
|
|
30
|
+
- **DeepSeek Harness (DSH) Discovery (`src/dsh_client.py`)**: Added dedicated cross-platform client detection for DeepSeek Harness Desktop, automatically identifying bundled binaries and internal pnpm distributions across macOS, Windows, and Linux.
|
|
31
|
+
- **Short CLI Alias (`bookmark`)**: Registered `bookmark` alongside `bookmark-research` in `package.json` bin definitions for more convenient CLI invocation.
|
|
32
|
+
- **Enhanced Test Coverage**: Added comprehensive test suites in `tests/test_dsh_client.py` and updated onboarding and host install test suites (totaling 692 passing tests).
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
Full test suite passing across all platforms.
|
|
37
|
+
|
|
38
|
+
Install from [npm](https://www.npmjs.com/package/bookmark-research/v/0.5.0-beta.14). GitHub provides source and release notes; npm is the shared release package for all four hosts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bookmark-research",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.14",
|
|
4
4
|
"description": "Check bookmark URLs or Canvas packages, compare sources with host-led research, and retain verifiable reports.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"deepseek-harness"
|
|
17
17
|
],
|
|
18
18
|
"bin": {
|
|
19
|
-
"bookmark-research": "bin/bookmark-research.js"
|
|
19
|
+
"bookmark-research": "bin/bookmark-research.js",
|
|
20
|
+
"bookmark": "bin/bookmark-research.js"
|
|
20
21
|
},
|
|
21
22
|
"main": "./hosts/dsh/plugin.js",
|
|
22
23
|
"engines": {
|
package/scripts/host_clients.py
CHANGED
|
@@ -5,9 +5,15 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
|
+
import sys
|
|
8
9
|
|
|
9
10
|
from export_bundle import NAME
|
|
10
11
|
|
|
12
|
+
# The published package ships src/ beside scripts/, and the installed plugin runs
|
|
13
|
+
# the same DSH discovery from src/ alone; keep one implementation of it.
|
|
14
|
+
if str(Path(__file__).resolve().parents[1] / "src") not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
SELECTOR = NAME + "@" + NAME
|
|
13
19
|
|
|
@@ -23,28 +29,6 @@ def executable(binary):
|
|
|
23
29
|
return found
|
|
24
30
|
|
|
25
31
|
|
|
26
|
-
def bundled_pnpm(binary):
|
|
27
|
-
"""The package manager a DSH CLI carries itself, if it ships one.
|
|
28
|
-
|
|
29
|
-
DeepSeek Harness Desktop runs `dsh plugin` with its own bundled pnpm, so that
|
|
30
|
-
carrier needs no `pnpm` on PATH. A generic CLI installs profile plugins by
|
|
31
|
-
forwarding to the pnpm it finds instead.
|
|
32
|
-
"""
|
|
33
|
-
found = executable(binary)
|
|
34
|
-
if not found:
|
|
35
|
-
return None
|
|
36
|
-
try:
|
|
37
|
-
path = Path(found).resolve()
|
|
38
|
-
except OSError:
|
|
39
|
-
return None
|
|
40
|
-
# Desktop layout: <resources>/runtime/cli/bin/dsh beside <resources>/runtime/pnpm.
|
|
41
|
-
for parent in list(path.parents)[:5]:
|
|
42
|
-
candidate = parent / "pnpm" / "bin" / "pnpm.mjs"
|
|
43
|
-
if candidate.is_file():
|
|
44
|
-
return candidate
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
|
|
48
32
|
def read_json(path, default=None):
|
|
49
33
|
if not path.exists():
|
|
50
34
|
return default
|
|
@@ -76,6 +60,12 @@ class HostClient:
|
|
|
76
60
|
raise ValueError("DSH uses --profile, not --scope or --project")
|
|
77
61
|
if not profile or not all(c.isalnum() or c in "._-" for c in profile) or profile.startswith("."):
|
|
78
62
|
raise ValueError("DSH requires --profile NAME (letters, numbers, dots, underscores or hyphens)")
|
|
63
|
+
# The default name resolves to PATH first and then to an installed
|
|
64
|
+
# DeepSeek Harness Desktop, whose bundled CLI carries its own pnpm and
|
|
65
|
+
# is the only carrier that composes the reserved `desktop` profile.
|
|
66
|
+
from dsh_client import resolve
|
|
67
|
+
self.resolved = resolve(binary, profile=profile)
|
|
68
|
+
self.binary = self.resolved["binary"] or binary or "dsh"
|
|
79
69
|
self.home = Path(os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser().resolve()
|
|
80
70
|
self.settings = self.home / "profiles" / profile / "package.json"
|
|
81
71
|
else:
|
|
@@ -103,7 +93,11 @@ class HostClient:
|
|
|
103
93
|
def require(self):
|
|
104
94
|
if not executable(self.binary):
|
|
105
95
|
raise ValueError(self.host + " CLI was not found: " + self.binary)
|
|
106
|
-
if self.host == "dsh" and not
|
|
96
|
+
if self.host == "dsh" and not self.resolved["usable"]:
|
|
97
|
+
if self.resolved["reason"] == "reserved":
|
|
98
|
+
raise ValueError("The DSH profile name desktop is composed only by the DeepSeek Harness Desktop CLI, and "
|
|
99
|
+
+ self.binary + " is not it; pass the app's bundled CLI with --dsh (it carries its own pnpm), "
|
|
100
|
+
"or choose another profile name")
|
|
107
101
|
# A generic dsh forwards `plugin` to pnpm; fail before creating a profile.
|
|
108
102
|
raise ValueError("DSH installs profile plugins with pnpm, which was not found on PATH and is not bundled with "
|
|
109
103
|
+ self.binary + "; install it (npm install -g pnpm, or corepack enable pnpm) and retry, "
|
package/scripts/host_install.py
CHANGED
|
@@ -175,9 +175,9 @@ def _manage(action, client, base, source, ref, dry_run):
|
|
|
175
175
|
return result
|
|
176
176
|
if action == "install":
|
|
177
177
|
requested = _source(source if source is not None else SOURCE_ROOT, ref)
|
|
178
|
-
if receipt and ref is None:
|
|
178
|
+
if receipt and ref is None and requested["sourceType"] == "git":
|
|
179
179
|
old = receipt["source"]
|
|
180
|
-
if (
|
|
180
|
+
if old.get("sourceType") == "git" and requested["source"] == old["source"] and old.get("ref"):
|
|
181
181
|
requested["ref"] = old["ref"]
|
|
182
182
|
elif receipt:
|
|
183
183
|
if source is not None:
|
package/scripts/install.py
CHANGED
|
@@ -637,7 +637,10 @@ def main(argv=None):
|
|
|
637
637
|
"目标宿主:codex、claude、pi 或 dsh;可重复或用逗号指定多个。终端中引导选择,非交互时默认 codex"))
|
|
638
638
|
command.add_argument("--codex", default="codex", help=message("Codex CLI executable, optionally an absolute path", "Codex CLI 程序名或绝对路径"))
|
|
639
639
|
for host in ("claude", "pi", "dsh"):
|
|
640
|
-
|
|
640
|
+
detail = (" (default: a usable dsh on PATH, then a DeepSeek Harness Desktop installation)",
|
|
641
|
+
"(默认依次使用 PATH 中可用的 dsh 与 DeepSeek Harness 桌面版自带的 CLI)") if host == "dsh" else ("", "")
|
|
642
|
+
command.add_argument("--" + host, help=message("Path to the " + host + " CLI" + detail[0],
|
|
643
|
+
host + " CLI 路径" + detail[1]))
|
|
641
644
|
command.add_argument("--scope", choices=("user", "project", "local"), help=message(
|
|
642
645
|
"Claude/Pi scope; default user (local is Claude-only)", "Claude/Pi 作用域,默认 user;local 仅限 Claude"))
|
|
643
646
|
command.add_argument("--project", help=message("Project directory for project/local scope", "project/local 作用域对应的项目目录"))
|
package/scripts/launcher.py
CHANGED
|
@@ -23,6 +23,8 @@ USAGE = """usage: bookmark-research [--lang auto|en|zh] [COMMAND] [ARGS...]
|
|
|
23
23
|
|
|
24
24
|
Other options are passed through, e.g. install claude dsh --profile web --non-interactive.
|
|
25
25
|
API keys are never accepted as arguments: use setup or environment variables.
|
|
26
|
+
`bookmark` is an alias for the same command: `bookmark` opens the menu and
|
|
27
|
+
`bookmark research ...` equals `bookmark-research ...`.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
|
|
@@ -104,8 +106,13 @@ def _install_action(action, arguments, language):
|
|
|
104
106
|
import install
|
|
105
107
|
hosts, rest = _split(arguments)
|
|
106
108
|
if action == "install" and not any(arg == "--source" or arg.startswith("--source=") for arg in rest):
|
|
107
|
-
from
|
|
108
|
-
|
|
109
|
+
from export_bundle import NAME, read_plugin_manifest
|
|
110
|
+
try:
|
|
111
|
+
version = read_plugin_manifest(ROOT).get("version")
|
|
112
|
+
except Exception:
|
|
113
|
+
version = None
|
|
114
|
+
source_val = ("npm:" + NAME + "@" + version) if version else ("npm:" + NAME)
|
|
115
|
+
rest = [*rest, "--source", source_val]
|
|
109
116
|
if not hosts and action != "install" and "--host" not in rest:
|
|
110
117
|
targets = installed_targets()
|
|
111
118
|
if not targets:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Find the `dsh` command an installation should use, on every platform.
|
|
2
|
+
|
|
3
|
+
A DSH profile installs plugins through pnpm: `dsh plugin` forwards its
|
|
4
|
+
arguments to the pnpm it finds on PATH, so a CLI installed by npm alone cannot
|
|
5
|
+
manage a profile without one. DeepSeek Harness Desktop ships the same CLI
|
|
6
|
+
together with its own pnpm and runs package operations with that copy, so this
|
|
7
|
+
carrier needs none. Desktop can also register its bundled CLI as `dsh` from its
|
|
8
|
+
application menu — macOS writes `/usr/local/bin/dsh`, Windows prepends the
|
|
9
|
+
bundled directory to the user PATH — but Linux has no such registration and a
|
|
10
|
+
user may never run it, so the installation layouts are probed directly as well.
|
|
11
|
+
|
|
12
|
+
DeepSeek Harness Desktop reserves the profile name `desktop` for its own
|
|
13
|
+
carrier and rejects boot, config dump and (for the npm CLI) plugin management
|
|
14
|
+
for that name, so installations into `desktop` can only use the app's bundled
|
|
15
|
+
CLI.
|
|
16
|
+
|
|
17
|
+
Only the standard library is imported here: the published package ships `src/`
|
|
18
|
+
without `scripts/`, and the installed plugin runs `setup` from that copy.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _windows(platform):
|
|
29
|
+
return platform.startswith("win")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _accessible(path, platform):
|
|
33
|
+
"""A usable probe is a regular file, and on POSIX an executable one."""
|
|
34
|
+
try:
|
|
35
|
+
if not path.is_file():
|
|
36
|
+
return False
|
|
37
|
+
except OSError:
|
|
38
|
+
return False
|
|
39
|
+
return _windows(platform) or os.access(path, os.X_OK)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _with_extensions(path, platform, environment):
|
|
43
|
+
"""A Windows path without an extension also names its PATHEXT candidate."""
|
|
44
|
+
names = [path]
|
|
45
|
+
if _windows(platform) and not path.suffix:
|
|
46
|
+
extensions = [item for item in environment.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") if item]
|
|
47
|
+
names += [Path(str(path) + extension.lower()) for extension in extensions]
|
|
48
|
+
names += [Path(str(path) + extension.upper()) for extension in extensions]
|
|
49
|
+
return names
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def executable(binary, environment=None, platform=None):
|
|
53
|
+
"""Resolve a CLI name or path the way the invoking shell would.
|
|
54
|
+
|
|
55
|
+
Windows Python misses extensionless full paths, so those retry through
|
|
56
|
+
PATHEXT instead of being reported missing.
|
|
57
|
+
"""
|
|
58
|
+
if not binary:
|
|
59
|
+
return None
|
|
60
|
+
environment = os.environ if environment is None else environment
|
|
61
|
+
platform = sys.platform if platform is None else platform
|
|
62
|
+
text = str(binary)
|
|
63
|
+
separators = [item for item in (os.sep, os.altsep) if item]
|
|
64
|
+
path = Path(text).expanduser()
|
|
65
|
+
if not any(separator in text for separator in separators) and not path.is_absolute():
|
|
66
|
+
return shutil.which(text, path=environment.get("PATH"))
|
|
67
|
+
for candidate in _with_extensions(path, platform, environment):
|
|
68
|
+
if _accessible(candidate, platform):
|
|
69
|
+
return str(candidate)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def desktop_candidates(environment=None, platform=None):
|
|
74
|
+
"""DeepSeek Harness Desktop's bundled CLI in each platform's install layout.
|
|
75
|
+
|
|
76
|
+
The packaged app always keeps it at `<resources>/runtime/cli/bin/`, so only
|
|
77
|
+
the resources root differs; `dsh.cmd` on Windows is the name the app's own
|
|
78
|
+
command registration checks for.
|
|
79
|
+
"""
|
|
80
|
+
environment = os.environ if environment is None else environment
|
|
81
|
+
platform = sys.platform if platform is None else platform
|
|
82
|
+
if platform == "darwin":
|
|
83
|
+
roots = [Path("/Applications/DeepSeek Harness.app")]
|
|
84
|
+
home = environment.get("HOME")
|
|
85
|
+
if home:
|
|
86
|
+
roots.append(Path(home) / "Applications" / "DeepSeek Harness.app")
|
|
87
|
+
return [root / "Contents/Resources/runtime/cli/bin/dsh" for root in roots]
|
|
88
|
+
if _windows(platform):
|
|
89
|
+
bases = []
|
|
90
|
+
local = environment.get("LOCALAPPDATA")
|
|
91
|
+
if local:
|
|
92
|
+
# The official Windows installer is per-user, so this is the default.
|
|
93
|
+
bases.append(Path(local) / "Programs" / "DeepSeek Harness")
|
|
94
|
+
for key in ("ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"):
|
|
95
|
+
value = environment.get(key)
|
|
96
|
+
if value:
|
|
97
|
+
bases.append(Path(value) / "DeepSeek Harness")
|
|
98
|
+
return [base / "resources" / "runtime" / "cli" / "bin" / "dsh.cmd" for base in bases]
|
|
99
|
+
# Desktop packaging is official for macOS and Windows; the Linux builds are
|
|
100
|
+
# community repackaging, which installs under one of these roots.
|
|
101
|
+
return [Path("/opt/DeepSeek Harness/resources/runtime/cli/bin/dsh"),
|
|
102
|
+
Path("/usr/lib/deepseek-harness/resources/runtime/cli/bin/dsh")]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def bundled_pnpm(binary):
|
|
106
|
+
"""The package manager a DSH CLI carries itself, if it ships one.
|
|
107
|
+
|
|
108
|
+
Desktop places it beside the bundled CLI — `resources/runtime/pnpm` next to
|
|
109
|
+
`resources/runtime/cli/bin/dsh`, or the primary runtime's copy of the same
|
|
110
|
+
distribution. A generic CLI installs profile plugins by forwarding to the
|
|
111
|
+
pnpm it finds on PATH instead.
|
|
112
|
+
"""
|
|
113
|
+
if not binary:
|
|
114
|
+
return None
|
|
115
|
+
try:
|
|
116
|
+
path = Path(binary).resolve()
|
|
117
|
+
except OSError:
|
|
118
|
+
return None
|
|
119
|
+
relatives = (Path("pnpm/bin/pnpm.mjs"), Path("primary-runtime/dependencies/pnpm/bin/pnpm.mjs"))
|
|
120
|
+
# Desktop layout: <resources>/runtime/cli/bin/dsh beside <resources>/runtime.
|
|
121
|
+
for parent in list(path.parents)[:5]:
|
|
122
|
+
for relative in relatives:
|
|
123
|
+
candidate = parent / relative
|
|
124
|
+
if candidate.is_file():
|
|
125
|
+
return candidate
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def profiles(package="bookmark-research", home=None):
|
|
130
|
+
"""Initialized DSH profiles under `$DSH_HOME/profiles`, and this plugin in each.
|
|
131
|
+
|
|
132
|
+
`dsh plugin` initializes a profile on first use, so only a directory holding
|
|
133
|
+
a `package.json` is a profile; the pnpm store beside them is not. `dependency`
|
|
134
|
+
is the recorded spec for `package` and `enabled` says whether the composed
|
|
135
|
+
bundle list activates it, which is what tells an existing registration from a
|
|
136
|
+
fresh profile before the installer touches anything.
|
|
137
|
+
"""
|
|
138
|
+
root = Path(home or os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser() / "profiles"
|
|
139
|
+
rows = []
|
|
140
|
+
if not root.is_dir():
|
|
141
|
+
return rows
|
|
142
|
+
for directory in sorted(root.iterdir()):
|
|
143
|
+
manifest = directory / "package.json"
|
|
144
|
+
if (directory.name.startswith(".") or directory.name == "node_modules"
|
|
145
|
+
or directory.is_symlink() or not manifest.is_file()):
|
|
146
|
+
continue
|
|
147
|
+
row = {"name": directory.name, "path": str(directory), "initialized": False,
|
|
148
|
+
"dependency": None, "enabled": False}
|
|
149
|
+
try:
|
|
150
|
+
value = json.loads(manifest.read_text(encoding="utf-8"))
|
|
151
|
+
except (OSError, ValueError):
|
|
152
|
+
rows.append(row)
|
|
153
|
+
continue
|
|
154
|
+
dependencies = value.get("dependencies") if isinstance(value, dict) else None
|
|
155
|
+
bundles = value.get("dsh", {}).get("profile", {}).get("bundles") if isinstance(value, dict) else None
|
|
156
|
+
dependency = dependencies.get(package) if isinstance(dependencies, dict) else None
|
|
157
|
+
row.update(initialized=True, dependency=dependency if isinstance(dependency, str) else None,
|
|
158
|
+
enabled=isinstance(bundles, list) and package in bundles)
|
|
159
|
+
rows.append(row)
|
|
160
|
+
return rows
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _client(binary, source, pnpm, reserved=False):
|
|
164
|
+
bundled = bundled_pnpm(binary) if binary else None
|
|
165
|
+
# The reserved `desktop` profile is composable only by the Desktop carrier,
|
|
166
|
+
# the one that ships its own package manager; the npm CLI refuses it.
|
|
167
|
+
blocked = reserved and bundled is None
|
|
168
|
+
usable = bool(binary) and (bool(pnpm) or bundled is not None) and not blocked
|
|
169
|
+
if usable:
|
|
170
|
+
reason = "ok"
|
|
171
|
+
elif not binary:
|
|
172
|
+
reason = "missing"
|
|
173
|
+
elif blocked:
|
|
174
|
+
reason = "reserved"
|
|
175
|
+
else:
|
|
176
|
+
reason = "pnpm"
|
|
177
|
+
return {"binary": binary, "source": source, "pnpm": pnpm,
|
|
178
|
+
"bundled_pnpm": str(bundled) if bundled is not None else None,
|
|
179
|
+
"usable": usable, "reason": reason}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def resolve(binary=None, environment=None, platform=None, profile=None):
|
|
183
|
+
"""Choose the `dsh` a profile installation should run.
|
|
184
|
+
|
|
185
|
+
An explicit path is never substituted: it is reported as unusable rather
|
|
186
|
+
than silently switching carriers. Otherwise a usable `dsh` on PATH wins, and
|
|
187
|
+
a PATH command that cannot run `dsh plugin` is replaced by a DeepSeek
|
|
188
|
+
Harness Desktop installation carrying its own pnpm. The reserved `desktop`
|
|
189
|
+
profile accepts only that Desktop carrier.
|
|
190
|
+
"""
|
|
191
|
+
environment = os.environ if environment is None else environment
|
|
192
|
+
platform = sys.platform if platform is None else platform
|
|
193
|
+
pnpm = executable("pnpm", environment, platform)
|
|
194
|
+
if binary and str(binary) != "dsh":
|
|
195
|
+
return _client(executable(binary, environment, platform), "argument", pnpm)
|
|
196
|
+
reserved = str(profile or "").casefold() == "desktop"
|
|
197
|
+
on_path = executable("dsh", environment, platform)
|
|
198
|
+
located = [found for found in (executable(candidate, environment, platform)
|
|
199
|
+
for candidate in desktop_candidates(environment, platform)) if found]
|
|
200
|
+
available = ([_client(on_path, "path", pnpm, reserved)] if on_path else []) \
|
|
201
|
+
+ [_client(found, "desktop", pnpm, reserved) for found in located]
|
|
202
|
+
for client in available:
|
|
203
|
+
if client["usable"]:
|
|
204
|
+
return client
|
|
205
|
+
# Report whichever carrier exists, so its path and reason still reach the user.
|
|
206
|
+
return available[0] if available else _client(None, None, pnpm, reserved)
|
package/src/onboarding.py
CHANGED
|
@@ -9,6 +9,8 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
from credentials import Credentials
|
|
12
|
+
from dsh_client import profiles as dsh_profiles
|
|
13
|
+
from dsh_client import resolve as resolve_dsh
|
|
12
14
|
from readiness import Readiness
|
|
13
15
|
from settings import Settings
|
|
14
16
|
from tui import Option, Prompter
|
|
@@ -25,6 +27,29 @@ def _profile_problem(value):
|
|
|
25
27
|
return None
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def _dsh_hint(client):
|
|
31
|
+
"""Why DSH is unavailable, or which carrier the wizard selected for it."""
|
|
32
|
+
if client["usable"]:
|
|
33
|
+
if client["source"] == "desktop":
|
|
34
|
+
return ("DeepSeek Harness Desktop CLI", "DeepSeek Harness 桌面版 CLI")
|
|
35
|
+
return ("", None)
|
|
36
|
+
if client["reason"] == "pnpm":
|
|
37
|
+
return ("needs pnpm: npm install -g pnpm, or corepack enable pnpm; the DSH Desktop CLI carries its own",
|
|
38
|
+
"需要 pnpm:npm install -g pnpm 或 corepack enable pnpm;DSH 桌面版 CLI 自带 pnpm")
|
|
39
|
+
return ("needs the dsh CLI: pass --dsh PATH (DeepSeek Harness Desktop keeps one under its app resources)",
|
|
40
|
+
"需要 dsh CLI:请用 --dsh PATH 指定(DeepSeek Harness 桌面版的 CLI 位于其应用资源目录)")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _profile_hint(row):
|
|
44
|
+
"""What an initialized DSH profile already holds for this plugin."""
|
|
45
|
+
if not row["initialized"]:
|
|
46
|
+
return ("package.json is missing or unreadable", "package.json 缺失或无法读取")
|
|
47
|
+
if row["dependency"]:
|
|
48
|
+
return ("bookmark-research " + row["dependency"] + (" · enabled" if row["enabled"] else " · not enabled"),
|
|
49
|
+
"已装 bookmark-research " + row["dependency"] + (" · 已启用" if row["enabled"] else " · 未启用"))
|
|
50
|
+
return ("no bookmark-research yet", "尚未安装 bookmark-research")
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
def _directory_problem(value):
|
|
29
54
|
if not value or not Path(value).expanduser().is_dir():
|
|
30
55
|
return ("Enter an existing directory.", "请输入已存在的目录。")
|
|
@@ -88,22 +113,29 @@ class Console(Prompter):
|
|
|
88
113
|
requested = args.host
|
|
89
114
|
if isinstance(requested, str):
|
|
90
115
|
requested = [requested]
|
|
116
|
+
# Resolve the DSH carrier once, so the plan and the installation agree on
|
|
117
|
+
# which `dsh` runs: a generic CLI needs pnpm on PATH, while the DeepSeek
|
|
118
|
+
# Harness Desktop CLI bundles its own.
|
|
119
|
+
client = resolve_dsh(getattr(args, "dsh", None))
|
|
120
|
+
if client["source"] == "desktop":
|
|
121
|
+
args.dsh = client["binary"]
|
|
91
122
|
if requested:
|
|
92
123
|
hosts = list(dict.fromkeys(requested))
|
|
93
124
|
else:
|
|
94
125
|
found = {name: bool(shutil.which(getattr(args, name, None) or name)) for name, _, _ in HOSTS}
|
|
126
|
+
hints = {name: ("", None) if found[name] else ("CLI not found", "未找到 CLI") for name in found}
|
|
127
|
+
found["dsh"], hints["dsh"] = client["usable"], _dsh_hint(client)
|
|
95
128
|
if not any(found.values()):
|
|
96
129
|
self.warn("No supported host CLI (codex, claude, pi, dsh) was found on PATH.",
|
|
97
130
|
"PATH 中未找到任何受支持的宿主 CLI(codex、claude、pi、dsh)。")
|
|
98
131
|
raise ValueError("No host CLI found; install Codex, Claude Code, Pi or DSH, "
|
|
99
132
|
"or pass --codex/--claude/--pi/--dsh PATH to point at its executable")
|
|
100
|
-
hints = {name: ("", None) if found[name] else ("CLI not found", "未找到 CLI") for name in found}
|
|
101
|
-
if found["dsh"] and not shutil.which("pnpm"):
|
|
102
|
-
# dsh plugin forwards to pnpm; offering DSH without it only fails later.
|
|
103
|
-
found["dsh"], hints["dsh"] = False, ("needs pnpm: npm install -g pnpm", "需要 pnpm:npm install -g pnpm")
|
|
104
133
|
hosts = self.multiselect("Install into which hosts?", "安装到哪些宿主?", [
|
|
105
134
|
Option(name, en, zh, *hints[name], disabled=not found[name]) for name, en, zh in HOSTS],
|
|
106
135
|
[name for name, _, _ in HOSTS if found[name]])
|
|
136
|
+
# A carrier that ships its own pnpm is the Desktop CLI, however it was found:
|
|
137
|
+
# only that CLI may manage the profile name Desktop reserves for itself.
|
|
138
|
+
desktop = client["bundled_pnpm"] is not None
|
|
107
139
|
targets = []
|
|
108
140
|
for host in hosts:
|
|
109
141
|
label = next((en for name, en, _ in HOSTS if name == host), host)
|
|
@@ -121,8 +153,7 @@ class Console(Prompter):
|
|
|
121
153
|
label + ": project directory", label + ":项目目录", str(Path.cwd()), _directory_problem)
|
|
122
154
|
elif host == "dsh":
|
|
123
155
|
# Never guess a DSH profile: it selects a distinct composed runtime.
|
|
124
|
-
target["profile"] = args.profile or self.
|
|
125
|
-
"DSH profile name (for example web)", "DSH profile 名称(例如 web)", "", _profile_problem)
|
|
156
|
+
target["profile"] = args.profile or self.dsh_profile(desktop)
|
|
126
157
|
targets.append(target)
|
|
127
158
|
lines = []
|
|
128
159
|
for target in targets:
|
|
@@ -130,12 +161,51 @@ class Console(Prompter):
|
|
|
130
161
|
detail = ", ".join(key + "=" + target[key] for key in ("scope", "project", "profile") if target[key])
|
|
131
162
|
lines.append((label + (": " + detail if detail else ""), label + (":" + detail if detail else "")))
|
|
132
163
|
self.note("Installation plan", "安装计划", lines)
|
|
164
|
+
if client["source"] == "desktop" and any(target["host"] == "dsh" for target in targets):
|
|
165
|
+
self.note("DSH CLI", "DSH CLI", [(client["binary"], client["binary"])])
|
|
133
166
|
if not self.confirm("Install now?", "现在安装?", True):
|
|
134
167
|
raise KeyboardInterrupt
|
|
135
168
|
first = targets[0]
|
|
136
169
|
args.host, args.scope, args.project, args.profile = first["host"], first["scope"], first["project"], first["profile"]
|
|
137
170
|
return targets
|
|
138
171
|
|
|
172
|
+
def dsh_profile(self, desktop):
|
|
173
|
+
"""Choose an initialized DSH profile, or name a new one.
|
|
174
|
+
|
|
175
|
+
`dsh plugin` initializes a profile on first use, so the profiles on disk
|
|
176
|
+
are the real targets; each is annotated with this plugin's recorded source
|
|
177
|
+
and whether its bundle list activates it. The reserved `desktop` profile
|
|
178
|
+
is composed only by the Desktop CLI, so another carrier never offers it.
|
|
179
|
+
"""
|
|
180
|
+
check = self._profile(desktop)
|
|
181
|
+
question = ("DSH profile name (for example web)", "DSH profile 名称(例如 web)")
|
|
182
|
+
rows = dsh_profiles()
|
|
183
|
+
if not desktop:
|
|
184
|
+
rows = [row for row in rows if row["name"].casefold() != "desktop"]
|
|
185
|
+
if not rows:
|
|
186
|
+
return self.input(question[0], question[1], "desktop" if desktop else "", check)
|
|
187
|
+
options = [Option(row["name"], row["name"], row["name"], *_profile_hint(row)) for row in rows]
|
|
188
|
+
options.append(Option("", "Another profile name…", "其他 profile 名称…"))
|
|
189
|
+
default = "desktop" if any(row["name"] == "desktop" for row in rows) else None
|
|
190
|
+
chosen = self.select("Install into which DSH profile?", "安装到哪个 DSH profile?", options, default)
|
|
191
|
+
if chosen:
|
|
192
|
+
return chosen
|
|
193
|
+
return self.input(question[0], question[1], "desktop" if desktop else "", check)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _profile(desktop):
|
|
197
|
+
"""Validate a DSH profile name, rejecting the reserved name for a generic CLI."""
|
|
198
|
+
def check(value):
|
|
199
|
+
problem = _profile_problem(value)
|
|
200
|
+
if problem:
|
|
201
|
+
return problem
|
|
202
|
+
if value.casefold() == "desktop" and not desktop:
|
|
203
|
+
return ("desktop is composed only by the DeepSeek Harness Desktop CLI: choose another profile name, "
|
|
204
|
+
"or rerun with --dsh",
|
|
205
|
+
"desktop 只能由 DeepSeek Harness 桌面版 CLI 组装:请换一个 profile 名称,或用 --dsh 重新运行")
|
|
206
|
+
return None
|
|
207
|
+
return check
|
|
208
|
+
|
|
139
209
|
def install_target(self, args):
|
|
140
210
|
return self.install_targets(args)[0]
|
|
141
211
|
|
|
@@ -161,40 +231,15 @@ class Onboarding:
|
|
|
161
231
|
hints = {"exa": ("Exa search and reading", "Exa 搜索与阅读"), "parallel": ("Parallel search and extract", "Parallel 搜索与提取"),
|
|
162
232
|
"tavily": ("Tavily search and extract", "Tavily 搜索与提取"),
|
|
163
233
|
"jina": ("Jina Reader; search needs a key", "Jina Reader;搜索需要密钥")}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
providers
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
rest = [name for name in Settings.PROVIDERS if name != primary]
|
|
174
|
-
extra = console.multiselect("Fallback page readers", "备用正文读取服务", options(rest),
|
|
175
|
-
[name for name in existing if name != primary], required=False)
|
|
176
|
-
extra_ordered = [name for name in existing if name in extra] + [name for name in extra if name not in existing]
|
|
177
|
-
readers = [primary] + extra_ordered
|
|
178
|
-
archive = console.confirm("Archive ordinary page reads? Research task evidence is always retained.",
|
|
179
|
-
"自动归档普通网页读取?研究任务的证据始终保留。", current["archive"]["enabled"])
|
|
180
|
-
directory = current["archive"]["directory"]
|
|
181
|
-
if archive:
|
|
182
|
-
def check(value):
|
|
183
|
-
try:
|
|
184
|
-
Settings.external_path(value, "Archive directory")
|
|
185
|
-
except ValueError:
|
|
186
|
-
return ("Use an absolute directory outside plugins and canvas packages.", "请使用插件与画布包以外的绝对目录。")
|
|
187
|
-
return None
|
|
188
|
-
directory = console.input("Archive directory", "归档目录", directory, check)
|
|
189
|
-
output = self._output(current)
|
|
190
|
-
after = console.select("After research, update the Wiki?", "研究结束后整理进 Wiki?", [
|
|
191
|
-
Option("suggest", "Suggest", "建议", "suggest pages and ask first", "给出建议页面,先征求同意"),
|
|
192
|
-
Option("auto", "Automatic", "自动", "write reviewed claims without asking", "直接写入已审核的结论"),
|
|
193
|
-
Option("off", "Off", "关闭", "do not suggest Wiki updates", "不提示 Wiki 更新")], current["wiki"]["after_research"])
|
|
194
|
-
mode = console.select("Check availability before web research", "联网研究前的可用性检查", [
|
|
195
|
-
Option("cached", "Cached", "缓存", "refresh on first use, changes or expiry (15 minutes by default)", "首次、配置变化或过期时检查(默认 15 分钟)"),
|
|
196
|
-
Option("always", "Always", "总是", "check for every new research question", "每次开始研究问题时检查"),
|
|
197
|
-
Option("manual", "Manual", "手动", "only when explicitly requested", "仅手动检查")], current["readiness"]["mode"])
|
|
234
|
+
# One ordered list answers the former search, fallback-search and reader
|
|
235
|
+
# questions: selected providers run concurrently and also read pages,
|
|
236
|
+
# unselected providers stay as the fallback search tier.
|
|
237
|
+
options = [Option(name, name, name, *hints.get(name, ("", None))) for name in Settings.PROVIDERS]
|
|
238
|
+
primary = console.multiselect("Search and page-reading services (concurrent, in this order)",
|
|
239
|
+
"搜索与正文读取服务(并发执行,按此顺序)", options, current["search"]["providers"])
|
|
240
|
+
providers = list(primary)
|
|
241
|
+
fallbacks = [name for name in Settings.PROVIDERS if name not in providers]
|
|
242
|
+
storage = self._storage(current)
|
|
198
243
|
config = current["professional_research"]
|
|
199
244
|
service = console.select("Optional professional API (host-led research works without it)",
|
|
200
245
|
"可选专业研究 API(宿主主导的研究不需要它)", [
|
|
@@ -216,35 +261,49 @@ class Onboarding:
|
|
|
216
261
|
elif service == "parallel":
|
|
217
262
|
console.say("Parallel processor remains " + config["parallel"]["processor"] + "; change it with config set if needed.",
|
|
218
263
|
"Parallel processor 沿用 " + config["parallel"]["processor"] + ";可通过 config set 修改。")
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
# Only changed values are returned, so an unchanged answer keeps the saved
|
|
265
|
+
# Wiki follow-up, readiness policy and archive toggle exactly as they were.
|
|
266
|
+
result = {"research": {"depth": depth, "response_language": language},
|
|
267
|
+
"search": {"providers": providers, "fallback_providers": fallbacks},
|
|
268
|
+
"fetch": {"provider": providers[0], "providers": providers},
|
|
269
|
+
"professional_research": services}
|
|
270
|
+
result.update(storage)
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
def _storage(self, current):
|
|
274
|
+
"""Ask once where archives and research results live, and return only the changed keys."""
|
|
275
|
+
console, defaults = self.console, Settings.defaults()
|
|
276
|
+
archive_dir, output_dir = current["archive"]["directory"], current["output"]["directory"]
|
|
277
|
+
beside = current["output"]["mode"] == "beside_input"
|
|
227
278
|
console.say("Location changes apply to new research tasks only; existing tasks stay where they are.",
|
|
228
279
|
"位置设置只影响新的研究任务;已有任务保留在原处。")
|
|
229
|
-
choice = console.select("Where should research reports
|
|
230
|
-
|
|
231
|
-
Option("
|
|
232
|
-
|
|
280
|
+
choice = console.select("Where should archives and research reports be stored?",
|
|
281
|
+
"归档和研究报告保存在哪里?", [
|
|
282
|
+
Option("default", "Default data folder", "默认数据目录",
|
|
283
|
+
archive_dir + " · " + output_dir, archive_dir + " · " + output_dir, hint_always=True),
|
|
284
|
+
Option("custom", "One custom folder for both (knowledge/ and research/)", "自定义统一目录(其下 knowledge/ 与 research/)"),
|
|
285
|
+
Option("beside_input", "Research reports beside the bookmark file or folder", "研究报告放在书签文件或文件夹旁边",
|
|
233
286
|
"falls back to the central folder when not possible", "无法放置时退回统一目录", hint_always=True)],
|
|
234
|
-
"beside_input" if
|
|
235
|
-
directory = saved["directory"]
|
|
287
|
+
"beside_input" if beside else "default")
|
|
236
288
|
if choice == "custom":
|
|
237
289
|
def check(value):
|
|
238
290
|
try:
|
|
239
|
-
Settings.external_path(value, "
|
|
291
|
+
Settings.external_path(value, "Storage directory")
|
|
240
292
|
except ValueError:
|
|
241
293
|
return ("Use an absolute directory outside plugins and canvas packages.", "请使用插件与画布包以外的绝对目录。")
|
|
242
294
|
return None
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
295
|
+
root = Settings.external_path(console.input("Storage directory", "存放目录",
|
|
296
|
+
str(Path(archive_dir).parent), check), "Storage directory")
|
|
297
|
+
return {"archive": {"directory": str(root / "knowledge")},
|
|
298
|
+
"output": {"mode": "central", "directory": str(root / "research")}}
|
|
299
|
+
if choice == "beside_input":
|
|
300
|
+
return {"output": {"mode": "beside_input"}} if not beside else {}
|
|
301
|
+
# Default folder: reset only a previously customised location.
|
|
302
|
+
result = {}
|
|
303
|
+
if archive_dir != defaults["archive"]["directory"]:
|
|
304
|
+
result["archive"] = {"directory": defaults["archive"]["directory"]}
|
|
305
|
+
if beside or output_dir != defaults["output"]["directory"]:
|
|
306
|
+
result["output"] = {"mode": "central", "directory": defaults["output"]["directory"]}
|
|
248
307
|
return result
|
|
249
308
|
|
|
250
309
|
def _keys(self, defaults=()):
|
|
@@ -359,10 +418,6 @@ class Onboarding:
|
|
|
359
418
|
console.intro("Bookmark Research setup", "Bookmark Research 配置")
|
|
360
419
|
self.settings.update(self._preferences())
|
|
361
420
|
self._keys()
|
|
362
|
-
if checks and not test_retrieval:
|
|
363
|
-
test_retrieval = console.confirm(
|
|
364
|
-
"Also test retrieval? Each provider gets one search and one example.com read; provider quota may be used.",
|
|
365
|
-
"同时进行实际检索自检?每家服务一次搜索和一次 example.com 阅读,可能使用服务额度。", False)
|
|
366
421
|
readiness = self._check(checks, test_retrieval, host, observed_tools)
|
|
367
422
|
if console and checks:
|
|
368
423
|
failed = [row for row in readiness["checks"] if row["status"] in self.FAILED]
|