bookmark-research 0.5.0-beta.5 → 0.5.0-beta.6

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.
@@ -9,7 +9,7 @@
9
9
  "source": {
10
10
  "source": "npm",
11
11
  "package": "bookmark-research",
12
- "version": "0.5.0-beta.4",
12
+ "version": "0.5.0-beta.5",
13
13
  "registry": "https://registry.npmjs.org"
14
14
  },
15
15
  "policy": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookmark-research",
3
- "version": "0.5.0-beta.5",
3
+ "version": "0.5.0-beta.6",
4
4
  "description": "Check bookmark URLs or Canvas packages, compare sources with host-led research, and retain verifiable reports.",
5
5
  "author": {
6
6
  "name": "kwenxu"
package/README.md CHANGED
@@ -22,13 +22,13 @@ Works in **Codex, Claude Code, Pi and DSH (DeepSeek Harness)**.
22
22
 
23
23
  ## Install
24
24
 
25
- **Current beta (`0.5.0-beta.5`)**, interactive on macOS, Linux and Windows:
25
+ **Current beta (`0.5.0-beta.6`)**, interactive on macOS, Linux and Windows:
26
26
 
27
27
  ```sh
28
- npx bookmark-research@0.5.0-beta.5 install --source npm:bookmark-research@0.5.0-beta.5
28
+ npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
29
29
  ```
30
30
 
31
- This pins both the installer and the content. The `@latest` examples below follow the default channel, which remains `0.5.0-beta.3`; `@beta` currently points to `0.5.0-beta.5`. Changing only the npx selector does not change the installer's default content source.
31
+ This pins both the installer and the content. The `@latest` examples below follow the default channel, which remains `0.5.0-beta.3`; `@beta` currently points to `0.5.0-beta.6`. Changing only the npx selector does not change the installer's default content source.
32
32
 
33
33
  **Default-channel interactive installation:**
34
34
 
@@ -51,6 +51,8 @@ For a permanent `bookmark-research` command: `npm install -g bookmark-research`.
51
51
 
52
52
  **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).
53
53
 
54
+ **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).
55
+
54
56
  **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.
55
57
 
56
58
  Several at once: `npx bookmark-research@latest install claude dsh --profile web --non-interactive`. Project scope: `--scope project --project /path/to/project` (Claude Code, Pi). All options: [installation guide](docs/installation.en.md).
package/README.zh.md CHANGED
@@ -22,13 +22,13 @@
22
22
 
23
23
  ## 安装
24
24
 
25
- **当前测试版(`0.5.0-beta.5`)**,支持 macOS、Linux、Windows 交互安装:
25
+ **当前测试版(`0.5.0-beta.6`)**,支持 macOS、Linux、Windows 交互安装:
26
26
 
27
27
  ```sh
28
- npx bookmark-research@0.5.0-beta.5 install --source npm:bookmark-research@0.5.0-beta.5
28
+ npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
29
29
  ```
30
30
 
31
- 这条命令同时固定安装器和插件内容。下面的 `@latest` 示例使用默认渠道,目前仍为 `0.5.0-beta.3`;`@beta` 当前指向 `0.5.0-beta.5`。只改 npx 的版本选择,不会改变安装器默认下载的内容来源。
31
+ 这条命令同时固定安装器和插件内容。下面的 `@latest` 示例使用默认渠道,目前仍为 `0.5.0-beta.3`;`@beta` 当前指向 `0.5.0-beta.6`。只改 npx 的版本选择,不会改变安装器默认下载的内容来源。
32
32
 
33
33
  **默认渠道的交互安装:**
34
34
 
@@ -51,6 +51,8 @@ npx bookmark-research@latest install
51
51
 
52
52
  **四个宿主统一发布来源:** npm 包包含共享运行时、Skill 和宿主适配。安装器下载发布包,生成所选宿主格式,再调用宿主原生命令;更新记录 npm 来源,不依赖 npx 临时缓存。GitHub 提供源码和安装清单,未发布源码用 Python 安装器。见[发布约定与官方依据](docs/distribution.md)。
53
53
 
54
+ **DSH 可直接安装发布包:** 发布包声明 `dsh.bundle.patch`,`dsh plugin --profile <名称> add bookmark-research@<版本>`(或 DSH 插件管理界面)无需先导出即可安装,Python 与 MCP 路径按安装位置解析。DeepSeek Harness 桌面版把 `desktop` profile 保留给自身载体,该 profile 请用桌面版自带 CLI 安装。详见[安装说明](docs/installation.md)。
55
+
54
56
  **Windows:** Windows 上常常没有能用的 `python3`,所以安装器会把检测到的 Python 路径写进各宿主的 MCP 配置。Codex 请用 `bookmark-research install codex` 安装,不要直接把仓库加进 Codex。`bookmark-research status` 可以查看实际使用的 Python。
55
57
 
56
58
  一次装多个:`npx bookmark-research@latest install claude dsh --profile web --non-interactive`。装到指定项目:`--scope project --project /path/to/project`(Claude Code、Pi)。全部参数见[安装说明](docs/installation.md)。
@@ -0,0 +1,18 @@
1
+ # DeepSeek Harness bundle layer. Paths resolve from the installed package through
2
+ # the module's context, so this file is identical in the npm package and in an
3
+ # exported copy; `python` and the forwarded environment come from the module
4
+ # (see hosts/dsh/plugin.js), not from this machine.
5
+ - insert:
6
+ - id: bookmark-research-skill
7
+ name: bookmark-research
8
+ - id: bookmark-research-mcp
9
+ name: '@deepseek-ai/dsh-mcp-client'
10
+ inject: [bookmarkResearchPaths]
11
+ config:
12
+ serverName: bookmark-research
13
+ transport: stdio
14
+ command: !!js ctx.bookmarkResearchPaths.python.command
15
+ args: !!js ctx.bookmarkResearchPaths.mcpArgs
16
+ cwd: !!js ctx.bookmarkResearchPaths.root
17
+ env: !!js ctx.bookmarkResearchPaths.env
18
+ failOnStartupError: true
@@ -19,7 +19,7 @@
19
19
 
20
20
  发布顺序:修改源码版本并完成 CI,发布已经验证的 npm 包,确认 registry 可取得该版本,再更新 GitHub marketplace 的固定版本。未发布源码不能通过 npm 入口安装;需要先用源码入口测试。
21
21
 
22
- 测试版使用 npm 的 `beta` 标签;若创建 GitHub 版本说明,则标记为预发布。不会自动移动 npm 的 `latest` 标签。安装测试版时同时固定安装器与内容来源,例如 `npx bookmark-research@0.5.0-beta.5 install --source npm:bookmark-research@0.5.0-beta.5`。仅将 npx 改为 `@beta` 不会改变默认内容来源 `latest`;已固定的旧版安装也不会自动换成测试版。
22
+ 测试版使用 npm 的 `beta` 标签;若创建 GitHub 版本说明,则标记为预发布。不会自动移动 npm 的 `latest` 标签。安装测试版时同时固定安装器与内容来源,例如 `npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6`。仅将 npx 改为 `@beta` 不会改变默认内容来源 `latest`;已固定的旧版安装也不会自动换成测试版。
23
23
 
24
24
  ## 官方资料与项目实例
25
25
 
@@ -1,8 +1,8 @@
1
1
  # 宿主兼容、工作流与打包
2
2
 
3
- 更新日期:2026-09-13。四宿主共用同一 Skill、Python CLI、stdio MCP 和研究档案。当前宿主模型负责规划、搜索/阅读、判断证据、按缺口补查及综合;原生协作支持独立分工。Deep Research 在这条流程上增加持续调查、独立核验、覆盖检查和报告。外部研究 API 可承担具体子问题,缺少它们仍能研究。
3
+ 更新日期:2026-10-08。四宿主共用同一 Skill、Python CLI、stdio MCP 和研究档案。当前宿主模型负责规划、搜索/阅读、判断证据、按缺口补查及综合;原生协作支持独立分工。Deep Research 在这条流程上增加持续调查、独立核验、覆盖检查和报告。外部研究 API 可承担具体子问题,缺少它们仍能研究。
4
4
 
5
- **正文依据、文件生成、隔离脚本检查、真实 MCP 调用和宿主端到端研究是不同验证层级。** 本轮没有安装 Pi / DSH、改全局配置或启动付费研究模型。具体脚本调用和研究规则见 [宿主工作流参考](../skills/bookmark-research/references/host-workflows.md)。
5
+ **正文依据、文件生成、隔离脚本检查、真实 MCP 调用和宿主端到端研究是不同验证层级。** 本轮在 DSH 桌面版 profile `desktop` 上完成了一次真实安装,并观察到 Skill 注册与 MCP 工具实际应答;Pi 仍未安装,未改全局配置,未启动付费研究模型。具体脚本调用和研究规则见 [宿主工作流参考](../skills/bookmark-research/references/host-workflows.md)。
6
6
 
7
7
  | 宿主 | 协作基础与本插件入口 | 生命周期与限制 |
8
8
  | --- | --- | --- |
@@ -58,7 +58,7 @@
58
58
  | Claude Code 插件 | `.claude-plugin/plugin.json`;当前文档允许省略 manifest 后按约定发现组件 | 根 `skills/`、`.mcp.json` 或 manifest 配置 | 单独保留 Claude manifest/MCP 配置 |
59
59
  | Agent Plugins 1.0.0 标准包 | **根 `plugin.json`**,必填 `$schema` 与 `name` | 固定发现根 `skills/`、`mcp.json` | 从同一源码独立导出标准包 |
60
60
  | Pi | `package.json` 的 `pi` 字段,或资源目录约定 | 原生 skills/extensions;MCP 要靠扩展 | `pi.skills`+Skill 调用 CLI;多组任务另登记项目工作流 |
61
- | DeepSeek Harness(DSH) | `--patch` 加载 Cordis 配置;可发布 bundle 另有 `package.json` 的 `dsh.bundle.patch` | 原生 Skill provider;官方 `dsh-mcp-client` 桥接 MCP tools | 导出使用绝对路径的本机 `cordis.patch.yml` 适配文件 |
61
+ | DeepSeek Harness(DSH) | 包在 `package.json` 声明 `dsh.bundle.patch`,由 `dsh plugin` 或插件管理器安装;`--patch` 仍可手工加载 Cordis 配置 | 原生 Skill provider;官方 `dsh-mcp-client` 桥接 MCP tools | 发布包自带按安装位置解析的 `bundle.patch.yml`,registry 可直接安装;绝对路径 `cordis.patch.yml` 仅作手工适配 |
62
62
 
63
63
  这些入口不是同一文件的不同别名。符合 Agent Skills 的 `SKILL.md` 和 MCP 协议是主要复用边界;某个客户端能加载 Skill,并不表示它已经支持 Agent Plugins 的根 manifest。[H01][H04][H05][P02][D02]
64
64
 
@@ -157,7 +157,9 @@ DSH 的原生运行插件是 Cordis 模块,可导出 `name`、`inject` 和 `ap
157
157
 
158
158
  `command` 与 `args` 分开传递。不要在这个 DSH 片段里假设 `${PLUGIN_ROOT}` 会像 Codex/Agent Plugins 一样展开;本轮核实的 MCP 客户端并未定义这种插件根占位符约定。绝对路径片段适合本机 checkout;搬迁或重新安装到其他位置后需要重新生成。DSH 的 patch 文件位置也不会改变模块路径解析所依据的 profile 目录。[D03][D05]
159
159
 
160
- 本次 DSH 导出是**本机配置适配文件**,不生成声明 `dsh.bundle` 的 `package.json`,也不安装 DSH 或官方 MCP client。后续如果要发布可搬迁 npm bundle,需要增加按安装位置解析源码路径的适配器,并按官方规则声明 `dsh.bundle.patch`;不能发布硬编码本机路径的配置并称其可移植。[D02][D03]
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
+
162
+ DSH 桌面版把 profile 名 `desktop` 保留给 Electron 载体:启动器对该名字拒绝一切 boot/dump(`--dump-config` 亦然),只有载体自身可以管理其插件。因此对 `desktop` 的安装须用桌面版自带 CLI(`--dsh "<App>/Contents/Resources/runtime/cli/bin/dsh"`,它自带 pnpm,无需 PATH 中的 pnpm),安装器的验证改为读取持久层: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]
161
163
 
162
164
  导出及查看合并配置的形式是:
163
165
 
@@ -166,7 +168,7 @@ python3 scripts/export_bundle.py --format dsh --output /private/tmp/bookmark-res
166
168
  dsh --profile web --patch /private/tmp/bookmark-research-dsh/cordis.patch.yml --dump-config
167
169
  ```
168
170
 
169
- 实际启动可使用 `dsh web --patch <绝对配置路径>`。这些命令要求 DSH 环境已具备对应 MCP client 依赖,属于待客户端验证的接入说明;本文没有安装或修改 DSH profile。新建任意名称的 profile 默认只有 base bundle;`web` profile 才会按官方模板包含 Web 表面,不应把“创建一个任意 profile”描述成自动创建了完整 Web 应用。[D02][D08]
171
+ 实际启动可使用 `dsh web --patch <绝对配置路径>`。该手工路线仍属待客户端验证的接入说明,且不覆盖保留的 `desktop` profile。新建任意名称的 profile 默认只有 base bundle;`web` profile 才会按官方模板包含 Web 表面,不应把“创建一个任意 profile”描述成自动创建了完整 Web 应用。[D02][D08]
170
172
 
171
173
  `dsh plugin` 在 profile 目录中通过 pnpm 管理依赖,并把声明了 `dsh.bundle` 的包加入 `dsh.profile.bundles`。没有该声明的 npm 包仍能作为普通依赖安装,但不会自动增加插件配置层。后层 patch 会替换命中行的整个 `config`,不做深度合并。[D02][D08]
172
174
 
@@ -294,5 +294,36 @@
294
294
  "Codex feature state and Claude manifest validation do not prove native tools or account access in an arbitrary session.",
295
295
  "Local correlation IDs beginning local: are not native host run IDs."
296
296
  ]
297
+ },
298
+ "host_install_review": {
299
+ "checked_on": "2026-10-08",
300
+ "host": "dsh",
301
+ "profile": "desktop",
302
+ "runtime_version": "0.2.0-rc.2",
303
+ "package_version": "0.5.0-beta.5",
304
+ "methods": [
305
+ "scripts/install.py install --host dsh --profile desktop with the managed export from this source tree",
306
+ "dsh plugin --profile <throwaway> add <packed release tarball> (registry-equivalent spec)"
307
+ ],
308
+ "observed": {
309
+ "bundle_selected_into_profile": true,
310
+ "skill_registered_in_host": true,
311
+ "mcp_tools_listed": 37,
312
+ "mcp_child_resolved_to_installed_package": true,
313
+ "composed_rows": [
314
+ "bookmark-research-skill",
315
+ "bookmark-research-mcp"
316
+ ],
317
+ "desktop_profile_dump_refused": true,
318
+ "managed_receipt_state": "verified",
319
+ "managed_export_files": 71,
320
+ "unit_tests_passed": 506,
321
+ "temporary_test_profile_removed": true
322
+ },
323
+ "not_verified": [
324
+ "a published npm release that carries bundle.patch.yml; the released 0.5.0-beta.5 predates it",
325
+ "Windows or Pi installation",
326
+ "the npm cache permission retry on a failing machine (unit-tested only; this machine used npm_config_cache instead)"
327
+ ]
297
328
  }
298
329
  }
@@ -181,7 +181,16 @@ python3 scripts/install.py install --host dsh --profile web
181
181
  python3 scripts/install.py verify --host dsh --profile web
182
182
  ```
183
183
 
184
- The installer exports a relocatable `dsh.bundle` and calls `dsh plugin --profile web add PATH`. `dsh plugin` delegates to pnpm, so install it first (`npm install -g pnpm` or `corepack enable pnpm`); without it the installer stops before creating a profile and the wizard shows DSH as unavailable. The bundle registers the shared Skill and resolves Python/MCP paths from the installed module. The selected profile must provide the Skill registry and official MCP client. For local development, export with `--format dsh` and install the stable export using that native command. 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.
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
+
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 install it with the app's own CLI, and verification reads the persisted layer instead of `--dump-config`:
187
+
188
+ ```sh
189
+ python3 scripts/install.py install --host dsh --profile desktop \
190
+ --dsh "<DSH app>/Contents/Resources/runtime/cli/bin/dsh"
191
+ ```
192
+
193
+ 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.
185
194
 
186
195
  ### Agent Plugins 1.0.0
187
196
 
@@ -237,14 +237,23 @@ pi_subagent_workflow({action:"run",name:"bookmark-research",args:{research_id:"R
237
237
 
238
238
  ### DSH / DeepSeek Harness
239
239
 
240
- 所选 DSH profile 需提供 Skill registry 和官方 `@deepseek-ai/dsh-mcp-client`。`dsh plugin` 由 pnpm 执行,请先安装(`npm install -g pnpm` 或 `corepack enable pnpm`);缺少时安装器会在创建 profile 前停止,向导中 DSH 显示为不可选。统一安装器生成可搬移的 `dsh.bundle`,调用原生 `dsh plugin --profile web add PATH`,注册 Skill 并从安装后位置解析 Python/MCP 路径:
240
+ 所选 DSH profile 需提供 Skill registry 和官方 `@deepseek-ai/dsh-mcp-client`。发布包自带 `bundle.patch.yml` 并声明 `dsh.bundle.patch`,因此除统一安装器之外,也可以直接从 registry 安装:`dsh plugin --profile <name> add bookmark-research@VERSION`,或在 DSH 插件管理界面按同一个 spec 安装;DSH 会把声明了 `dsh.bundle` 且 patch 存在的包自动加入 `dsh.profile.bundles`。层内不写死本机路径,Python 解释器、工作目录、MCP 参数与环境转发都由入口模块按安装位置解析。
241
+
242
+ 统一安装器生成可搬移的 `dsh.bundle`,调用原生 `dsh plugin --profile web add PATH`,注册 Skill 并从安装后位置解析 Python/MCP 路径:
241
243
 
242
244
  ```sh
243
245
  python3 scripts/install.py install --host dsh --profile web
244
246
  python3 scripts/install.py verify --host dsh --profile web
245
247
  ```
246
248
 
247
- 手动分发时使用 `export_bundle.py --format dsh`,再用上述原生命令安装稳定导出目录。保留的旧 `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)。
249
+ `dsh plugin` 由 pnpm 执行:通用 CLI 需要 PATH 中的 pnpm(`npm install -g pnpm` 或 `corepack enable pnpm`),缺少时安装器在创建 profile 前停止;DSH 桌面版 CLI 自带 pnpm,无需另装。桌面版把 profile 名 `desktop` 保留给 Electron 载体,其启动器对该名字拒绝 boot/dump,因此用桌面版自带 CLI 安装,验证改为读取持久层而非 `--dump-config`:
250
+
251
+ ```sh
252
+ python3 scripts/install.py install --host dsh --profile desktop \
253
+ --dsh "<DSH 应用>/Contents/Resources/runtime/cli/bin/dsh"
254
+ ```
255
+
256
+ 手动分发时使用 `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)。
248
257
 
249
258
  生成可直接传给宿主 `workflow` 工具的完整 JSON:
250
259
 
@@ -0,0 +1,30 @@
1
+ # Bookmark Research 0.5.0 beta 6
2
+
3
+ **Beta / 测试版.** Uses the npm `beta` channel. The npm `latest` tag remains unchanged.
4
+
5
+ ## Install this beta
6
+
7
+ Pin both the installer and the plugin content:
8
+
9
+ ```sh
10
+ npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
11
+ ```
12
+
13
+ Choose Codex, Claude Code, Pi or DSH in the installer. Existing installations retain their recorded source and version selection. The GitHub Codex marketplace advances to this npm version after the registry confirms publication.
14
+
15
+ ## Changes
16
+
17
+ - **DSH installs the release directly.** The package now declares `dsh.bundle.patch` and ships `bundle.patch.yml`, so `dsh plugin --profile <name> add bookmark-research@0.5.0-beta.6` — or the DSH plugin manager — installs it with no export step first, and DSH selects the bundle into `dsh.profile.bundles` on its own. Nothing in the layer is tied to the building machine: the entry module supplies the interpreter, working directory, MCP arguments and forwarded environment variables at load time.
18
+ - **No `python3` assumption in the DSH layer.** The module discovers Python 3.9+ with SQLite in the same order as the npm launcher (`BOOKMARK_RESEARCH_PYTHON`, `python3`, `python`, `py -3`) and reports a warning instead of failing silently when none is found, so Windows no longer depends on a `python3` alias.
19
+ - **DSH Desktop's reserved profile is installable.** DeepSeek Harness Desktop refuses to boot or dump the profile name `desktop`, so the installer verifies that profile from the persisted layer (dependency target, package-relative `dsh.bundle.patch`, entry module, Skill and MCP rows) instead of `--dump-config`.
20
+ - **A bundled package manager satisfies the DSH prerequisite.** A CLI that carries its own pnpm — the DeepSeek Harness Desktop CLI does — no longer requires `pnpm` on `PATH`; a generic CLI still fails before creating a profile, with instructions.
21
+ - **Unwritable npm caches are diagnosed and retried.** When npm reports a cache permission failure (for example root-owned files from a privileged install), the download retries once in a scratch cache and otherwise reports the `chown` repair command.
22
+ - **The exported DSH bundle is the released layer.** `export_bundle.py --format dsh` copies the committed `bundle.patch.yml` byte for byte; a source tree that predates the layer (an older published package or Git ref) still exports a working equivalent, so pinned installations do not regress.
23
+
24
+ ## Validation and limits
25
+
26
+ The functional diff passed 506 discovered local tests, including new coverage for the released DSH layer, the offline verification of the reserved `desktop` profile, the bundled package manager, and the npm cache retry. A real installation into the DeepSeek Harness Desktop profile (`desktop`, runtime `0.2.0-rc.2`) registered the Skill, listed 37 MCP tools, and resolved the running MCP child to the installed package; a packed release installed in a throwaway profile was selected into `dsh.profile.bundles` automatically.
27
+
28
+ Not verified in this release: Windows and Pi installation, and the cache-retry path on a machine whose npm cache is genuinely unwritable. These are covered by unit tests only. No paid research model was started.
29
+
30
+ Install from [npm](https://www.npmjs.com/package/bookmark-research/v/0.5.0-beta.6). GitHub supplies source, version notes and installation catalogs; npm remains the shared release package for all four hosts.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { fileURLToPath } from "node:url";
3
4
 
@@ -6,6 +7,7 @@ export const inject = ["skills"];
6
7
 
7
8
  // The module travels with the Python runtime; no exporting machine's path is embedded.
8
9
  const root = fileURLToPath(new URL("../../", import.meta.url));
10
+ const cli = fileURLToPath(new URL("../../src/cli.py", import.meta.url));
9
11
  const skillUrl = new URL("../../skills/bookmark-research/SKILL.md", import.meta.url);
10
12
  const resourceBase = { kind: "directory", path: fileURLToPath(new URL("./", skillUrl)) };
11
13
  const candidate = {
@@ -20,10 +22,56 @@ const candidate = {
20
22
  locator: skillUrl,
21
23
  };
22
24
 
25
+ // The MCP child inherits only these variables, so saved data/config paths and
26
+ // provider keys survive without baking values into a published patch. This list
27
+ // must equal the native manifest's mcpServers env_vars; tests assert that parity.
28
+ const FORWARDED_ENV = [
29
+ "BOOKMARK_RESEARCH_DATA_DIR",
30
+ "BOOKMARK_RESEARCH_CONFIG",
31
+ "BOOKMARK_RESEARCH_CREDENTIALS",
32
+ "XDG_DATA_HOME",
33
+ "XDG_CONFIG_HOME",
34
+ "EXA_API_KEY",
35
+ "PARALLEL_API_KEY",
36
+ "TAVILY_API_KEY",
37
+ "JINA_API_KEY",
38
+ "OPENAI_API_KEY",
39
+ "SystemRoot",
40
+ "windir",
41
+ ];
42
+
43
+ // The same discovery order as the npm launcher: an explicit interpreter, then
44
+ // the platform aliases, each accepted only with Python 3.9+ and SQLite present.
45
+ const PYTHON_PROBE = "import sys, sqlite3; sys.exit(0 if sys.version_info >= (3, 9) else 3)";
46
+
47
+ function findPython() {
48
+ const candidates = [];
49
+ if (process.env.BOOKMARK_RESEARCH_PYTHON) candidates.push([process.env.BOOKMARK_RESEARCH_PYTHON]);
50
+ candidates.push(["python3"], ["python"]);
51
+ if (process.platform === "win32") candidates.push(["py", "-3"]);
52
+ for (const [command, ...args] of candidates) {
53
+ const probe = spawnSync(command, [...args, "-c", PYTHON_PROBE], { stdio: "ignore", windowsHide: true });
54
+ if (probe.status === 0) return { command, args };
55
+ }
56
+ return undefined;
57
+ }
58
+
23
59
  export function apply(ctx) {
60
+ let python = findPython();
61
+ if (python === undefined) {
62
+ process.stderr.write("bookmark-research: no Python 3.9+ with SQLite was found; the MCP client will try python3. "
63
+ + "Set BOOKMARK_RESEARCH_PYTHON to select an interpreter.\n");
64
+ python = { command: "python3", args: [] };
65
+ }
66
+ const env = Object.fromEntries(FORWARDED_ENV
67
+ .filter((key) => process.env[key] !== undefined)
68
+ .map((key) => [key, process.env[key]]));
24
69
  ctx.provide("bookmarkResearchPaths", {
25
70
  root,
26
- cli: fileURLToPath(new URL("../../src/cli.py", import.meta.url)),
71
+ cli,
72
+ python,
73
+ mcpArgs: [...python.args, cli, "serve"],
74
+ env,
27
75
  });
28
76
  ctx.skills.registerProvider(() => ({
29
77
  name,
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "bookmark-research",
3
- "version": "0.5.0-beta.5",
3
+ "version": "0.5.0-beta.6",
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": {"type": "git", "url": "git+https://github.com/Browser-bookmark-hub/Bookmark-Research.git"},
7
7
  "keywords": ["bookmark-research", "mcp", "claude-code", "codex", "pi-package", "deepseek-harness"],
8
8
  "bin": {"bookmark-research": "bin/bookmark-research.js"},
9
+ "main": "./hosts/dsh/plugin.js",
9
10
  "engines": {"node": ">=18"},
10
- "files": ["bin", "src", "scripts", "config", "skills", "hosts", "docs", ".codex-plugin/plugin.json", ".agents/plugins/marketplace.json", "install.sh", "LICENSE", "README.md", "README.zh.md", "!**/__pycache__", "!**/*.pyc"],
11
- "pi": {"skills": ["./skills"]}
11
+ "files": ["bin", "src", "scripts", "config", "skills", "hosts", "docs", "bundle.patch.yml", ".codex-plugin/plugin.json", ".agents/plugins/marketplace.json", "install.sh", "LICENSE", "README.md", "README.zh.md", "!**/__pycache__", "!**/*.pyc"],
12
+ "pi": {"skills": ["./skills"]},
13
+ "dsh": {"bundle": {"patch": "./bundle.patch.yml"}}
12
14
  }
@@ -43,6 +43,7 @@ EXTRA_FILES = (
43
43
  "scripts/launcher.py",
44
44
  "bin/bookmark-research.js",
45
45
  "package.json",
46
+ "bundle.patch.yml",
46
47
  "docs/details.md",
47
48
  "docs/details.en.md",
48
49
  "skills/bookmark-research/agents/openai.yaml",
@@ -16,6 +16,8 @@ from host_assets import export_host_assets
16
16
  SOURCE_ROOT = Path(__file__).resolve().parents[1]
17
17
  FORMATS = ("codex", "agent-plugin", "claude", "pi", "dsh")
18
18
  NAME = "bookmark-research"
19
+ # The DSH bundle layer a released tree ships and declares in `dsh.bundle.patch`.
20
+ BUNDLE_PATCH = "bundle.patch.yml"
19
21
  SCHEMA_ROOT = "https://agent-plugins.org/schemas/1.0.0/"
20
22
  SHARED_ROOTS = ("src", "config", "skills")
21
23
  SHARED_DOCUMENTS = (
@@ -366,8 +368,34 @@ Reference: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/
366
368
  return introduction + instructions
367
369
 
368
370
 
369
- def _write_adapter(stage, format_name, output, manifest, python="python3"):
371
+ def _generated_bundle_patch(python, environment):
372
+ """The bundle layer for a source tree that does not ship `bundle.patch.yml`.
373
+
374
+ Released trees carry the committed patch, which resolves the interpreter and
375
+ the forwarded environment from the module. An older published package or Git
376
+ ref only provides `root` and `cli`, so its generated layer must stay on that
377
+ narrower contract.
378
+ """
379
+ return """- insert:
380
+ - id: bookmark-research-skill
381
+ name: bookmark-research
382
+ - id: bookmark-research-mcp
383
+ name: '@deepseek-ai/dsh-mcp-client'
384
+ inject: [bookmarkResearchPaths]
385
+ config:
386
+ serverName: bookmark-research
387
+ transport: stdio
388
+ command: %s
389
+ args: !!js "[ctx.bookmarkResearchPaths.cli, 'serve']"
390
+ cwd: !!js ctx.bookmarkResearchPaths.root
391
+ env: !!js %s
392
+ failOnStartupError: true
393
+ """ % (json.dumps(python), environment)
394
+
395
+
396
+ def _write_adapter(stage, format_name, output, manifest, python="python3", source_root=SOURCE_ROOT):
370
397
  metadata = {"name": NAME, "version": manifest["version"], "description": manifest["description"]}
398
+ adapter = {}
371
399
  if format_name == "codex":
372
400
  if python != "python3":
373
401
  # Codex registers this manifest verbatim; pin the detected interpreter.
@@ -408,27 +436,21 @@ def _write_adapter(stage, format_name, output, manifest, python="python3"):
408
436
  _write_json(stage, "package.json", {
409
437
  **metadata, "type": "module", "main": "./hosts/dsh/plugin.js",
410
438
  "exports": "./hosts/dsh/plugin.js",
411
- "files": ["src", "config", "skills", "hosts", "workflows", "docs", "bundle.patch.yml", "LICENSE"],
412
- "dsh": {"bundle": {"patch": "./bundle.patch.yml"}},
439
+ "files": ["src", "config", "skills", "hosts", "workflows", "docs", BUNDLE_PATCH, "LICENSE"],
440
+ "dsh": {"bundle": {"patch": "./" + BUNDLE_PATCH}},
413
441
  })
414
442
  forwarded = manifest["mcpServers"][NAME]["env_vars"]
415
443
  environment = json.dumps("Object.fromEntries(" + json.dumps(forwarded) +
416
444
  ".filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]]))")
417
- (stage / "bundle.patch.yml").write_text("""- insert:
418
- - id: bookmark-research-skill
419
- name: bookmark-research
420
- - id: bookmark-research-mcp
421
- name: '@deepseek-ai/dsh-mcp-client'
422
- inject: [bookmarkResearchPaths]
423
- config:
424
- serverName: bookmark-research
425
- transport: stdio
426
- command: %s
427
- args: !!js "[ctx.bookmarkResearchPaths.cli, 'serve']"
428
- cwd: !!js ctx.bookmarkResearchPaths.root
429
- env: !!js %s
430
- failOnStartupError: true
431
- """ % (json.dumps(python), environment), encoding="utf-8")
445
+ committed = Path(source_root) / BUNDLE_PATCH
446
+ if not committed.is_symlink() and committed.is_file():
447
+ # The released tree ships the layer; copying it keeps the npm package
448
+ # and the exported bundle on byte-identical patch content.
449
+ (stage / BUNDLE_PATCH).write_bytes(committed.read_bytes())
450
+ adapter["bundle_patch"] = "committed"
451
+ else:
452
+ (stage / BUNDLE_PATCH).write_text(_generated_bundle_patch(python, environment), encoding="utf-8")
453
+ adapter["bundle_patch"] = "generated"
432
454
  # JSON strings are valid YAML scalars, including paths with spaces/quotes.
433
455
  cli_path = json.dumps(str(output / "src/cli.py"), ensure_ascii=False)
434
456
  patch = """- insert:
@@ -446,6 +468,7 @@ def _write_adapter(stage, format_name, output, manifest, python="python3"):
446
468
  """ % (json.dumps(python), cli_path, environment)
447
469
  (stage / "cordis.patch.yml").write_text(patch, encoding="utf-8")
448
470
  (stage / "README.md").write_text(_readme(format_name, output), encoding="utf-8")
471
+ return adapter
449
472
 
450
473
 
451
474
  def export_bundle(format_name, output, source_root=SOURCE_ROOT, python="python3"):
@@ -473,13 +496,13 @@ def export_bundle(format_name, output, source_root=SOURCE_ROOT, python="python3"
473
496
  destination.parent.mkdir(parents=True, exist_ok=True)
474
497
  shutil.copy2(source_root / relative, destination)
475
498
  host_files = export_host_assets(source_root, stage, format_name)
476
- _write_adapter(stage, format_name, output, manifest, python)
499
+ adapter = _write_adapter(stage, format_name, output, manifest, python, source_root=source_root)
477
500
  _check_destination(output)
478
501
  if output.exists():
479
502
  output.rmdir() # Only an empty directory can be removed here.
480
503
  stage.rename(output) # Renaming a directory cannot replace nonempty data.
481
504
  return {"format": format_name, "version": manifest["version"], "output": str(output), "shared_files": len(files),
482
- "host_files": host_files, "installed": False}
505
+ "host_files": host_files, "adapter": adapter, "installed": False}
483
506
 
484
507
 
485
508
  def main(argv=None):
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  HOST_SOURCE_FILES = (
8
8
  "hosts/shared/workflow.json", "hosts/shared/research-flow.js", "hosts/shared/research-call.py",
9
9
  "hosts/claude/runtime.js", "hosts/dsh/runtime.js", "hosts/dsh/workflow-call.py", "hosts/dsh/plugin.js",
10
+ "hosts/dsh/package.json",
10
11
  "hosts/pi/runtime.js", "hosts/pi/register-workflow.py",
11
12
  "hosts/codex/delegate.md", "hosts/codex/prepare.py",
12
13
  )
@@ -59,6 +60,9 @@ def export_host_assets(source, stage, host):
59
60
  relative = "hosts/pi/register-workflow.py"
60
61
  else:
61
62
  files["hosts/dsh/plugin.js"] = read_asset(source, "hosts/dsh/plugin.js")
63
+ # Scope ESM to the entry module so a package root may stay CommonJS
64
+ # (the npm package keeps a CommonJS bin and the pi field).
65
+ files["hosts/dsh/package.json"] = read_asset(source, "hosts/dsh/package.json")
62
66
  files["workflows/bookmark-research/meta.json"] = (
63
67
  json.dumps(definition["meta"], ensure_ascii=False, indent=2) + "\n").encode("utf-8")
64
68
  relative = "hosts/dsh/workflow-call.py"
@@ -23,6 +23,28 @@ def executable(binary):
23
23
  return found
24
24
 
25
25
 
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
+
26
48
  def read_json(path, default=None):
27
49
  if not path.exists():
28
50
  return default
@@ -81,10 +103,11 @@ class HostClient:
81
103
  def require(self):
82
104
  if not executable(self.binary):
83
105
  raise ValueError(self.host + " CLI was not found: " + self.binary)
84
- if self.host == "dsh" and not shutil.which("pnpm"):
85
- # dsh plugin forwards to pnpm; fail before creating a profile.
86
- raise ValueError("DSH installs profile plugins with pnpm, which was not found on PATH; "
87
- "install it (npm install -g pnpm, or corepack enable pnpm) and retry")
106
+ if self.host == "dsh" and not shutil.which("pnpm") and bundled_pnpm(self.binary) is None:
107
+ # A generic dsh forwards `plugin` to pnpm; fail before creating a profile.
108
+ raise ValueError("DSH installs profile plugins with pnpm, which was not found on PATH and is not bundled with "
109
+ + self.binary + "; install it (npm install -g pnpm, or corepack enable pnpm) and retry, "
110
+ "or pass the DeepSeek Harness Desktop CLI with --dsh (it carries its own pnpm)")
88
111
 
89
112
  def run(self, arguments, structured=False, quiet_error=False):
90
113
  result = subprocess.run([executable(self.binary) or self.binary, *arguments], cwd=self.cwd, text=True, encoding="utf-8", errors="replace",
@@ -186,11 +209,50 @@ class HostClient:
186
209
  if self.host == "dsh":
187
210
  if not registration["enabled"]:
188
211
  raise RuntimeError("DSH installed the dependency without enabling its bundle")
189
- composition = self.run(["--profile", self.profile, "--dump-config"], quiet_error=True)
190
- if not all(name in composition for name in ("bookmark-research-skill", "bookmark-research-mcp", "bookmarkResearchPaths")):
191
- raise RuntimeError("DSH composition is missing the Bookmark Research Skill or MCP configuration")
192
212
  package = self.settings.parent / "node_modules" / NAME
193
213
  if not package.is_dir():
194
214
  raise RuntimeError("DSH did not retain the installed bundle in its profile")
215
+ if self.profile.casefold() == "desktop":
216
+ # DeepSeek Harness Desktop reserves this profile name: its launcher
217
+ # refuses every boot and dump for "desktop" (only the Electron carrier
218
+ # may manage it), so read the persisted layer the way the launcher does.
219
+ composition = self.desktop_composition(package)
220
+ else:
221
+ composition = self.run(["--profile", self.profile, "--dump-config"], quiet_error=True)
222
+ if not all(name in composition for name in ("bookmark-research-skill", "bookmark-research-mcp", "bookmarkResearchPaths")):
223
+ raise RuntimeError("DSH composition is missing the Bookmark Research Skill or MCP configuration")
195
224
  return package.resolve()
196
225
  return bundle
226
+
227
+ def desktop_composition(self, package):
228
+ """Offline evidence that the reserved desktop profile composes this bundle.
229
+
230
+ The installed layer must be exactly what the launcher loads: the profile
231
+ depends on the package, and the package declares a package-relative patch,
232
+ an entry module, and the Skill and MCP rows that patch inserts.
233
+ """
234
+ settings = read_json(self.settings, {})
235
+ if not isinstance(settings, dict):
236
+ raise ValueError("Unexpected DSH profile manifest: " + str(self.settings))
237
+ dependency = settings.get("dependencies", {}).get(NAME)
238
+ if not isinstance(dependency, str) or not dependency.strip():
239
+ raise RuntimeError("The desktop profile does not depend on " + NAME)
240
+ linked = local_path(dependency, self.settings.parent)
241
+ if linked is not None and linked != package.resolve():
242
+ raise RuntimeError("The desktop profile depends on another " + NAME + " source")
243
+ manifest = read_json(package / "package.json", {})
244
+ if not isinstance(manifest, dict) or manifest.get("name") != NAME:
245
+ raise RuntimeError("The installed bundle is not " + NAME)
246
+ declared = manifest.get("dsh", {})
247
+ patch = declared.get("bundle", {}).get("patch") if isinstance(declared, dict) else None
248
+ if not isinstance(patch, str) or not patch or Path(patch).is_absolute():
249
+ raise RuntimeError("The installed bundle declares no package-relative dsh.bundle.patch")
250
+ entry = manifest.get("main")
251
+ if not isinstance(entry, str) or not entry or Path(entry).is_absolute():
252
+ raise RuntimeError("The installed bundle declares no package-relative entry module")
253
+ for relative in (patch, entry):
254
+ resolved = (package / relative).resolve()
255
+ if package.resolve() not in resolved.parents or not resolved.is_file():
256
+ raise RuntimeError("The installed bundle is missing " + relative)
257
+ # The markers below are the same names the composed dump would contain.
258
+ return entry + "\n" + (package / patch).read_text(encoding="utf-8")
@@ -201,7 +201,7 @@ def _manage(action, client, base, source, ref, dry_run):
201
201
  export_bundle(client.host, stage, root, python)
202
202
  manifest = read_plugin_manifest(root)
203
203
  # Render path-dependent manual instructions for the persistent location.
204
- _write_adapter(stage, client.host, bundle, manifest, python)
204
+ _write_adapter(stage, client.host, bundle, manifest, python, source_root=root)
205
205
  content = _files(stage)
206
206
  digest = hashlib.sha256(json.dumps(content, sort_keys=True).encode()).hexdigest()
207
207
  version = manifest["version"]
@@ -3,6 +3,7 @@
3
3
  from contextlib import contextmanager
4
4
  import json
5
5
  from pathlib import Path, PurePosixPath
6
+ import re
6
7
  import shutil
7
8
  import subprocess
8
9
  import tarfile
@@ -13,6 +14,16 @@ from export_bundle import NAME, VERSION_PATTERN, read_plugin_manifest
13
14
 
14
15
  REGISTRY = "https://registry.npmjs.org"
15
16
  DEFAULT_SOURCE = "npm:" + NAME
17
+ CACHE_PERMISSION = re.compile(r"EACCES|EPERM")
18
+ CACHE_HINT = ("Hint: npm's cache may contain files owned by another user (a known npm state after a "
19
+ "privileged install); repair it with `sudo chown -R $(id -u):$(id -g) \"$(npm config get cache)\"` "
20
+ "or point npm_config_cache at a writable directory.")
21
+
22
+
23
+ def _cache_permission_failure(detail):
24
+ """Whether npm failed because its cache directory is not writable."""
25
+ text = detail or ""
26
+ return "cache" in text.casefold() and CACHE_PERMISSION.search(text) is not None
16
27
 
17
28
 
18
29
  def source(value, ref=None):
@@ -35,10 +46,28 @@ def checkout(requested, timeout):
35
46
  raise ValueError("npm is required to download Bookmark Research releases")
36
47
  with tempfile.TemporaryDirectory(prefix="bookmark-npm-source-") as directory:
37
48
  base = Path(directory)
38
- result = subprocess.run([
39
- npm, "pack", NAME + "@" + selected["ref"], "--ignore-scripts", "--json",
40
- "--registry", REGISTRY, "--pack-destination", str(base),
41
- ], cwd=base, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
49
+
50
+ def pack(cache=None):
51
+ command = [
52
+ npm, "pack", NAME + "@" + selected["ref"], "--ignore-scripts", "--json",
53
+ "--registry", REGISTRY, "--pack-destination", str(base),
54
+ ]
55
+ if cache is not None:
56
+ command += ["--cache", str(cache)]
57
+ return subprocess.run(command, cwd=base, capture_output=True, text=True,
58
+ encoding="utf-8", errors="replace", timeout=timeout)
59
+
60
+ result = pack()
61
+ if result.returncode and _cache_permission_failure(result.stderr):
62
+ # One retry in a scratch cache: a machine whose npm cache is unwritable
63
+ # otherwise cannot install any release, and the failure is not the release's.
64
+ with tempfile.TemporaryDirectory(prefix="bookmark-npm-cache-") as cache:
65
+ retry = pack(cache)
66
+ if retry.returncode == 0:
67
+ result = retry
68
+ else:
69
+ raise RuntimeError("Could not download the npm release: "
70
+ + result.stderr.strip()[-2000:] + "\n" + CACHE_HINT)
42
71
  if result.returncode:
43
72
  raise RuntimeError("Could not download the npm release: " + result.stderr.strip()[-2000:])
44
73
  rows = json.loads(result.stdout)