dsh-openwrite 0.2.28 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +49 -8
  3. package/THIRD_PARTY_NOTICES.md +4 -3
  4. package/companions.mjs +508 -0
  5. package/cordis.patch.yml +0 -2
  6. package/docs/CLOSED_ISSUE_FOLLOWUPS.md +1 -1
  7. package/docs/COMPANION_PLUGINS.md +142 -0
  8. package/docs/CONTEXT_SLOTS.md +1 -1
  9. package/docs/COST_MEASUREMENT.md +2 -2
  10. package/docs/DEVELOPMENT.md +6 -11
  11. package/docs/DSH_STORE.md +14 -2
  12. package/docs/EDITOR_SAFETY_REPAIRS.md +2 -2
  13. package/docs/INSTALL.md +112 -4
  14. package/docs/ISSUES_121_123.md +4 -4
  15. package/docs/ISSUES_140_142.md +58 -0
  16. package/docs/ISSUES_149_150_151.md +77 -0
  17. package/docs/ISSUE_123_FOLLOWUP.md +3 -2
  18. package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
  19. package/docs/LEARNING.md +1 -1
  20. package/docs/MOBILE_READINESS.md +62 -0
  21. package/docs/PLUGIN_MAINTENANCE.md +1 -3
  22. package/docs/RELEASE_ACCEPTANCE.md +20 -3
  23. package/docs/REVIEW_ROUND_22.md +67 -0
  24. package/docs/REVIEW_ROUND_23.md +48 -0
  25. package/docs/REVIEW_ROUND_25.md +51 -0
  26. package/docs/SELECTION_UPGRADE_REPAIRS.md +2 -2
  27. package/docs/TOOLS.md +181 -604
  28. package/docs/WORKFLOWS.md +5 -5
  29. package/package.json +17 -21
  30. package/packages/openwrite-bridge/lib/client.d.ts +0 -1
  31. package/packages/openwrite-bridge/lib/client.js +90 -9
  32. package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
  33. package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
  34. package/packages/openwrite-bridge/lib/dog-delivery.js +3 -1
  35. package/packages/openwrite-bridge/lib/dog-review.js +15 -3
  36. package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
  37. package/packages/openwrite-bridge/lib/domain.js +250 -29
  38. package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
  39. package/packages/openwrite-bridge/lib/host-llm.js +657 -0
  40. package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
  41. package/packages/openwrite-bridge/lib/host-web.js +166 -0
  42. package/packages/openwrite-bridge/lib/index.d.ts +4 -0
  43. package/packages/openwrite-bridge/lib/index.js +150 -6
  44. package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
  45. package/packages/openwrite-bridge/lib/managed-runtime.js +176 -34
  46. package/packages/openwrite-bridge/lib/tool-policy.js +37 -12
  47. package/packages/openwrite-bridge/lib/tools.js +1223 -1227
  48. package/packages/openwrite-bridge/package.json +7 -5
  49. package/packages/studio-panel/lib/client.js +123207 -121534
  50. package/packages/studio-panel/lib/client.js.map +1 -1
  51. package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
  52. package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
  53. package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
  54. package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
  55. package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
  56. package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
  57. package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
  58. package/packages/studio-panel/lib/types/client/VditorBody.d.ts +1 -1
  59. package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
  60. package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
  61. package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
  62. package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
  63. package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
  64. package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
  65. package/packages/studio-panel/package.json +5 -3
  66. package/plugin.mjs +159 -20
  67. package/preset-definition.mjs +144 -4
  68. package/presets/openwrite/agent.cordis.yml +7 -9
  69. package/presets/openwrite/ops-integration.mjs +28 -0
  70. package/presets/openwrite/preset.yml +2 -1
  71. package/presets/openwrite/skills/canon/SKILL.md +3 -3
  72. package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
  73. package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
  74. package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
  75. package/presets/openwrite/skills/export-book/SKILL.md +3 -3
  76. package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
  77. package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
  78. package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
  79. package/presets/openwrite/skills/learn/SKILL.md +3 -3
  80. package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
  81. package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
  82. package/presets/openwrite/skills/progress/SKILL.md +1 -1
  83. package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
  84. package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
  85. package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
  86. package/presets/openwrite/skills/style-system/SKILL.md +13 -6
  87. package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
  88. package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
  89. package/presets/openwrite/skills/world-query/SKILL.md +16 -11
  90. package/presets/openwrite/skills/write-next/SKILL.md +1 -1
  91. package/release/{openwrite-5.8.20-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
  92. package/release/runtime-manifest.json +4 -13
  93. package/release/source.json +21 -21
  94. package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
  95. package/scripts/maintenance.mjs +67 -7
  96. package/scripts/plugin-doctor.mjs +179 -0
  97. package/scripts/prepare.mjs +26 -4
  98. package/scripts/safe-upgrade.mjs +38 -5
  99. package/dog-tools.mjs +0 -13
  100. package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
  101. package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
  102. package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
  103. package/scripts/dog/delivery-stage.js +0 -32
  104. package/scripts/dog/import-chapter.js +0 -20
  105. package/scripts/dog/import-record.js +0 -35
  106. package/scripts/dog/review-dimension.js +0 -41
  107. package/scripts/dog/review-record.js +0 -42
  108. package/vendor/dsh-dog/LICENSE +0 -29
  109. package/vendor/dsh-dog/README.md +0 -114
  110. package/vendor/dsh-dog/UPSTREAM.md +0 -30
  111. package/vendor/dsh-dog/cordis.patch.yml +0 -14
  112. package/vendor/dsh-dog/lib/client.js +0 -2960
  113. package/vendor/dsh-dog/lib/client.js.map +0 -1
  114. package/vendor/dsh-dog/lib/core--byUPJL8.js +0 -2191
  115. package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
  116. package/vendor/dsh-dog/lib/core.d.ts +0 -2
  117. package/vendor/dsh-dog/lib/core.js +0 -2
  118. package/vendor/dsh-dog/lib/index.d.ts +0 -143
  119. package/vendor/dsh-dog/lib/index.js +0 -1230
  120. package/vendor/dsh-dog/package.json +0 -110
  121. package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
  122. package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
  123. package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
  124. package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
  125. package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright 2026 The OpenWrite Project Authors
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <p align="center">在 DeepSeek Harness 中规划、写作、审稿和管理长篇小说。</p>
11
11
 
12
12
  <p align="center">
13
- <a href="package.json"><img src="https://img.shields.io/badge/dsh-0.2.1--rc.3-2563eb" alt="dsh 0.2.1-rc.3"></a>
13
+ <a href="package.json"><img src="https://img.shields.io/badge/dsh-0.2.1--rc.5-2563eb" alt="dsh 0.2.1-rc.5"></a>
14
14
  <a href="package.json"><img src="https://img.shields.io/badge/Node-%E2%89%A522.19-15803d" alt="Node >= 22.19"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-0f766e" alt="Apache-2.0"></a>
16
16
  <a href="https://dsh-plugin.org/plugins/lipu-jpg/openwrite"><img src="https://dsh-plugin.org/badges/listed.svg" alt="Listed on dsh-plugin.org"></a>
@@ -27,9 +27,48 @@
27
27
  这是 [OpenWrite](https://github.com/LiPu-jpg/Openwrite/tree/native-core) 的 dsh 插件版本。
28
28
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供 Agent 与交互界面,OpenWrite Core 提供小说领域能力;人物、设定、大纲、正文和评审保存在本地作品目录。
29
29
 
30
- ## 开发中:0.2.28(未发布)
30
+ ## 开发中:0.2.29(未发布)
31
31
 
32
- Core 5.8.20 候选继续修复正文最后输入漏存、审稿修订越过选区,以及真实 Markdown 编辑器格式转换导致的选区拒绝,见 [正文编辑与审稿补修](docs/EDITOR_SAFETY_REPAIRS.md)。前轮修复见 [选区与升级补修](docs/SELECTION_UPGRADE_REPAIRS.md)。宿主正式发布与 Android/Termux 真机两项继续跟踪,本轮不发布;下面的 npm/GitHub 安装入口仍为已发布的 0.2.27。
32
+ 增加 `pythonPath` / `OPENWRITE_PYTHON` 和显式依赖目录配置,允许使用已准备依赖的 Python,
33
+ 跳过 uv 与解释器下载;补充 Android/Termux 的本机 external Studio 步骤,以及旧版安装选择诊断。
34
+ 配置和边界见 [安装说明](docs/INSTALL.md#bring-your-own-python)。同时补齐手机后台输入保留、独立 Studio 正文草稿恢复、触屏选区与下载,并加入 Chromium / WebKit 发布回归,见 [手机验收说明](docs/MOBILE_READINESS.md)。正式可安装版本仍为下方的 0.2.28;
35
+ 同时修复审稿证据漏填误判与机器批注污染,候选捆绑 Core 5.8.22,见 [#146 修复说明](docs/ISSUE_146_REVIEW_EVIDENCE.md)。
36
+ Android 完整工具验收与宿主升级保护发布继续在 [#123](https://github.com/LiPu-jpg/Openwrite/issues/123) 跟踪。
37
+
38
+ ## 0.2.33
39
+
40
+ - **深度研究搜索默认走宿主 DSH**:托管运行时把 `ctx.web` 的搜索/抓取经环回中继交给 Core,默认 provider `dsh`,
41
+ 不需要第三方搜索 Key;外部 Studio 仍默认 `bocha`。
42
+ - **研究页新增搜索设置**:提供方、API Key、记住 Key;`dsh` 显示无需 Key。保存与 `novel_research_settings_save`
43
+ 同路由,设置按作品作用域落盘。
44
+ - 新增宿主 `host-web.ts` 中继(`/search`、`/fetch`、Bocha 兼容 `/bocha`,仅 127.0.0.1 + 机器 token)和 Core
45
+ handshake;DeepResearch 仍可选,首次使用需在 integration 目录 `pnpm install --frozen-lockfile`。
46
+ - 搜索默认值放到左下角 **OpenWrite → 设置 → 搜索**(机器级默认,新作品生效;单个作品可在研究页覆盖)。
47
+ - 研究 `quality` 在启动子进程前校验为 `advisory` / `balanced` / `strict`。
48
+ - 修复非 ASCII `Authorization` 头返回 500 的问题,认证比较改为 UTF-8 bytes。
49
+ - 左下角设置页视觉重做:搜索 provider 卡片、胶囊 Tab、卡片化配置块与主渐变保存按钮,对齐 dsh-our-free-model。
50
+
51
+ ## 0.2.30
52
+
53
+ - **联动插件可执行接入**:`dsh-our-free-model` 接入按钮现在写 profile 并把写作/评审路由切到该模型(其余路由保留、二次确认、部分失败如实报告);`dsh-ops` 的挂载计划(文件工具/渐进发现/无 shell/不替换官方工具)显示在「联动插件」区;`dsh-dog` 新增非斜杠技能 `dog-delivery-verification`,把正文与评审证据写成 handoff 图交给 `dog_validate`/`dog_create`/`dog_run` 独立验收。详见 [联动插件说明](docs/COMPANION_PLUGINS.md)。
54
+
55
+ 本轮候选还包含两项发布阻塞修复,见 [#149/#150/#151 修复说明](docs/ISSUES_149_150_151.md):
56
+
57
+ - 兼容精确宿主 dsh `0.2.1-rc.4` 与 `0.2.1-rc.5`,修复 `plugin does not support the DSH version in use` 直接拒装([#150](https://github.com/LiPu-jpg/Openwrite/issues/150)/[#151](https://github.com/LiPu-jpg/Openwrite/issues/151)/[#142](https://github.com/LiPu-jpg/Openwrite/issues/142))。
58
+ - 新建会话选择器不再为每个已发布版本注册预设别名,只保留会话真实引用过的旧版本([#149](https://github.com/LiPu-jpg/Openwrite/issues/149));工作台入口改用稳定预设 id。
59
+
60
+ 本轮候选还加入**可选联动插件**支持,见 [联动插件说明](docs/COMPANION_PLUGINS.md):检测到 [dsh-ops](https://github.com/T-Auto/dsh-ops)
61
+ 时,用其官方预设接口挂载有界文件工具与渐进发现;检测到 [dsh-our-free-model](https://github.com/Ebony-Vinyl/dsh-our-free-model) 时,
62
+ 在工作台「联动插件」区显示状态与接入提示。两者均为可选项,未安装时行为与之前一致;任何判定失败都回退到 OpenWrite 自己的实现。
63
+ 同时**移除了内置的 DoG 引擎**(原 `vendor/dsh-dog`):包里不再有 `dog_*` 工具与「DoG 验收」入口,
64
+ 三个查询技能改为直接读 OpenWrite 自己的评审/交付/导入产物;需要 DoG 图形验证可自行安装
65
+ [@dsh-external/dsh-dog](https://github.com/Fun10165/dsh-dog)。
66
+
67
+ ## 0.2.28
68
+
69
+ 0.2.28 捆绑 Core 5.8.20,最终 22 项 CI 全部通过;修复正文最后输入漏存、审稿修订越过选区,以及真实 Markdown 编辑器格式转换导致的选区拒绝,见 [正文编辑与审稿补修](docs/EDITOR_SAFETY_REPAIRS.md)。并包含前轮 [选区与升级补修](docs/SELECTION_UPGRADE_REPAIRS.md)、[写作与审稿改进](docs/ISSUES_121_123.md)。
70
+
71
+ [GitHub 0.2.28](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.28) 与 [npm 0.2.28](https://www.npmjs.com/package/dsh-openwrite/v/0.2.28) 均已发布,npm `latest` 已更新为 0.2.28;两处公开下载包均与 CI 原始安装包逐字节一致。#123 的宿主正式发布和 Android/Termux 真机两项仍未完成,作为已知限制继续跟踪。
33
72
 
34
73
  ## 0.2.27
35
74
 
@@ -46,12 +85,12 @@ Android/ARM 外部 Studio 诊断,以及连载写作可靠性、完整上下文
46
85
 
47
86
  ## 快速安装
48
87
 
49
- **0.2.27 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2 / 0.2.1-rc.2 / 0.2.1-rc.3**。五个版本均已完成 Web 工作台渲染与 DoG 数据读取验证,Windows Electron 桌面端启动/恢复链路尚未实机复测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
88
+ **0.2.28 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2 / 0.2.1-rc.2 / 0.2.1-rc.3**。五个版本均已完成 Web 工作台渲染与 DoG 数据读取验证;三系统原生安装和 Electron 回归通过,报告者也补齐了 Windows 历史会话恢复实测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
50
89
 
51
- 正式发行状态以 [npm 版本列表](https://www.npmjs.com/package/dsh-openwrite?activeTab=versions) 和 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 为准。以下是 0.2.27 精确版本的安装命令;发布时两处使用同一份验收产物:
90
+ 正式发行状态以 [npm 版本列表](https://www.npmjs.com/package/dsh-openwrite?activeTab=versions) 和 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 为准。以下是 0.2.28 精确版本的安装命令;发布时两处使用同一份验收产物:
52
91
 
53
92
  ```sh
54
- dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.27/dsh-openwrite-0.2.27.tgz
93
+ dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.28/dsh-openwrite-0.2.28.tgz
55
94
  dsh web
56
95
  ```
57
96
 
@@ -81,6 +120,8 @@ dsh web
81
120
 
82
121
  ### dsh 0.2 桌面端升级
83
122
 
123
+ `0.2.28`(Core 5.8.20)已发布。升级前完全退出 Desktop(含托盘)及 dsh,备份 profile;宿主原生升级保护尚未正式发布,停机升级工具及限制见 [升级说明](docs/ISSUES_98_100.md#离线升级和损坏修复)。完成后重启宿主并刷新界面。
124
+
84
125
  `0.2.27`(Core 5.8.16)包含 Go 工作台认证、评审输出预算及后台重试补漏,并保留 #105–#113 与 #103/#104 的既有修复;原始场景仍待验证的条目见 [#123](https://github.com/LiPu-jpg/Openwrite/issues/123)。升级前完全退出 dsh/桌面客户端;安装后重新启动并刷新浏览器。原有作品、凭据和会话继续保留,历史版本预设用于恢复旧会话。受限平台使用外部 Studio,OpenCode Go 的服务商使用范围和配置要求见上述文档。
85
126
 
86
127
  `0.2.25`(Core 5.8.12)已在 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.25) 与 [npm](https://www.npmjs.com/package/dsh-openwrite/v/0.2.25) 发布,新增连载体检、分段续写、共享术语/范本、章组审计和漂移裁决,并修复验收与任务可观测性。范围、离线升级工具及宿主侧限制见 [#98–#100 实现说明](docs/ISSUES_98_100.md),完整验收见 [发布验收](docs/RELEASE_ACCEPTANCE.md#025)。
@@ -183,7 +224,7 @@ dsh web
183
224
  packages/openwrite-bridge/ 小说工具与后端桥接
184
225
  packages/studio-panel/ dsh 原生工作台
185
226
  presets/openwrite/ OpenWrite 创作预设与技能
186
- scripts/ 安装、启动、检查与 DoG 适配
227
+ scripts/ 安装、启动与检查
187
228
  conductor/ 连续写章、评审与修订编排
188
229
  ```
189
230
 
@@ -193,7 +234,7 @@ conductor/ 连续写章、评审与修订编排
193
234
  ## 许可与来源
194
235
 
195
236
  项目采用 [Apache-2.0](LICENSE)。`oh-story-*` 技能和随包编辑器保留各自目录中的原有许可证。
196
- Logo 基于 OpenWrite `native-core` 分支,已调整深浅主题配色;基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),图谱集成使用 [dsh-dog](https://github.com/Fun10165/dsh-dog)。
237
+ Logo 基于 OpenWrite `native-core` 分支,已调整深浅主题配色;基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),图谱验证可选装第三方 [dsh-dog](https://github.com/Fun10165/dsh-dog)(内置引擎已移除)。
197
238
 
198
239
  生态目录:[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) · [社区插件市场](https://dshtui.com/plugins/)。OpenWrite 当前面向 DeepSeek Harness 的 `web` profile,使用 `dsh web` 打开创作工作台;尚未验证 dsh-TUI 界面兼容性。
199
240
 
@@ -1,8 +1,7 @@
1
1
  # Third-party components
2
2
 
3
3
  OpenWrite is Apache-2.0. The separately maintained Studio panel retains its MIT
4
- license. DoG is based on Fun10165/dsh-dog v1.2.0, BSD-3-Clause; its full license
5
- and integration changes are in vendor/dsh-dog/LICENSE and UPSTREAM.md.
4
+ license.
6
5
 
7
6
  The editor ships Vditor and its auxiliary files with the upstream license in
8
7
  packages/studio-panel/vendor/vditor/LICENSE. OpenWrite preset skills retain
@@ -21,7 +20,9 @@ they are not inlined into the plugin's browser code.
21
20
  The release includes an OpenWrite-built cryptography 50.0.1 wheel for macOS
22
21
  x86_64 because upstream discontinued Intel Mac wheels. It is built from the
23
22
  SHA-256 pinned upstream source with statically linked OpenSSL 3.6.3. The wheel
24
- retains upstream licenses; OpenSSL's full license is in release/wheels/licenses.
23
+ retains upstream licenses; OpenSSL's full license is in
24
+ release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt (Apache License 2.0,
25
+ reproduced verbatim from the wheel with its SHA-256 recorded in that file).
25
26
  Source hash, toolchain, linkage inspection and the native CI build URL are in
26
27
  release/wheels/provenance.json. No cryptography source code was modified.
27
28
 
package/companions.mjs ADDED
@@ -0,0 +1,508 @@
1
+ // Companion-plugin detection. OpenWrite ships its own DoG engine and works
2
+ // without any companion; when a companion plugin is installed in the same
3
+ // profile AND its declared host range covers the running host, the companion's
4
+ // newest implementation is used instead of ours. Every unknown answer is
5
+ // fail-closed: detection problems keep our own implementation.
6
+ import { readFile, readdir, stat, mkdir, writeFile, realpath } from 'node:fs/promises'
7
+ import { createRequire } from 'node:module'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { dirname, join, resolve } from 'node:path'
10
+
11
+ /** Bundle names that identify each companion, with and without scope. */
12
+ const companionNames = {
13
+ dog: ['@dsh-external/dsh-dog'],
14
+ ops: ['dsh-ops'],
15
+ freeModel: ['dsh-our-free-model', '@ebony-vinyl/dsh-our-free-model'],
16
+ }
17
+
18
+ /**
19
+ * The host constraint a companion actually declares.
20
+ *
21
+ * Most plugins declare `peerDependencies["@deepseek-ai/dsh"]`, and the host reads
22
+ * exactly that. Some (dsh-dog 1.4.x) declare only `@deepseek-ai/dsh-*`
23
+ * sub-packages instead; the host needs every one of them, so their ranges are the
24
+ * real constraint. Treating a missing `@deepseek-ai/dsh` peer as "no constraint"
25
+ * would report such a plugin usable on hosts where the host itself refuses it.
26
+ */
27
+ function hostConstraint(manifest) {
28
+ const peers = manifest?.peerDependencies ?? {}
29
+ const direct = peers['@deepseek-ai/dsh']
30
+ if (typeof direct === 'string' && direct !== '') return { range: direct, source: '@deepseek-ai/dsh' }
31
+ const subpackage = Object.entries(peers)
32
+ .filter(([name]) => name.startsWith('@deepseek-ai/dsh-'))
33
+ .map(([, range]) => String(range))
34
+ if (subpackage.length > 0) return { range: subpackage.join(' '), source: '@deepseek-ai/dsh-*' }
35
+ return { range: null, source: null }
36
+ }
37
+
38
+ /**
39
+ * The public entry each companion must expose for OpenWrite to use it. A
40
+ * companion that is installed but does not expose its entry (an older release,
41
+ * or one that moved the API) is treated as absent: mounting a row whose import
42
+ * fails would take the whole preset composition down with it.
43
+ */
44
+ const companionEntries = { ops: 'dsh-ops/integration' }
45
+
46
+ /**
47
+ * The exact dsh-ops integration OpenWrite mounts. Detection builds it too, so an
48
+ * upstream release that stops accepting these options is reported instead of
49
+ * breaking the preset composition at load time.
50
+ */
51
+ /** The exact options OpenWrite passes to the dsh-ops integration factory. */
52
+ export const OPENWRITE_OPS_OPTIONS = Object.freeze({
53
+ preset: 'openwrite',
54
+ files: true,
55
+ discovery: true,
56
+ presentation: 'native',
57
+ replaceOfficial: false,
58
+ shell: false,
59
+ })
60
+
61
+ const opsIntegration = factory => factory({ ...OPENWRITE_OPS_OPTIONS })
62
+
63
+ /** The small, serializable summary of a resolved dsh-ops plan. */
64
+ export function summarizeOpsPlan(plan) {
65
+ return {
66
+ files: plan.files === true,
67
+ discovery: plan.discovery === true,
68
+ presentation: String(plan.presentation ?? ''),
69
+ shell: plan.shell === true,
70
+ replaceOfficial: plan.replaceOfficial === true,
71
+ residentTools: plan.residentTools === true,
72
+ residentNames: Array.isArray(plan.residentNames) ? plan.residentNames.length : 0,
73
+ }
74
+ }
75
+
76
+ const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
77
+
78
+ /** Compare two parsed versions, prerelease identifiers included. */
79
+ function compareVersions(a, b) {
80
+ for (const key of ['major', 'minor', 'patch']) {
81
+ if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1
82
+ }
83
+ const [x, y] = [a.pre, b.pre]
84
+ if (x.length === 0 && y.length === 0) return 0
85
+ if (x.length === 0) return 1
86
+ if (y.length === 0) return -1
87
+ for (let index = 0; index < Math.max(x.length, y.length); index += 1) {
88
+ if (index >= x.length) return -1
89
+ if (index >= y.length) return 1
90
+ const left = x[index], right = y[index]
91
+ if (left === right) continue
92
+ const numeric = /^\d+$/.test(left) && /^\d+$/.test(right)
93
+ if (numeric) return Number(left) < Number(right) ? -1 : 1
94
+ return left < right ? -1 : 1
95
+ }
96
+ return 0
97
+ }
98
+
99
+ /** Parse `1.2.3-rc.4` into comparable parts; returns null when unparseable. */
100
+ function parseVersion(value) {
101
+ const match = /^(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec((value ?? '').trim())
102
+ if (!match) return null
103
+ const wildcard = part => part === undefined || /[xX*]/.test(part)
104
+ // `1.x.3` is not a version spelling npm accepts.
105
+ if (wildcard(match[2]) && match[3] !== undefined && !wildcard(match[3])) return null
106
+ if (wildcard(match[2]) && match[4] !== undefined) return null
107
+ return { major: +match[1], minor: +(wildcard(match[2]) ? 0 : match[2]), patch: +(wildcard(match[3]) ? 0 : match[3]),
108
+ pre: match[4] ? match[4].split('.') : [], partial: 3 - [match[2], match[3]].filter(wildcard).length }
109
+ }
110
+
111
+ /** One comparator: an optional operator plus its version spelling. */
112
+ function parseComparator(comparator) {
113
+ const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(comparator.trim())
114
+ if (!match) return null
115
+ const version = parseVersion(match[2].trim().replace(/^[vV]/, ''))
116
+ if (!version) return null
117
+ return { op: match[1] ?? '=', version }
118
+ }
119
+
120
+ /** The exclusive end of a spelled version prefix (`0.3` -> 0.4.0-0, `1` -> 2.0.0-0). */
121
+ function upperBoundOf(spelling) {
122
+ const version = parseVersion(spelling.replace(/^[vV]/, ''))
123
+ if (!version) return null
124
+ if (version.partial === 3) return { ...version, pre: ['0'] }
125
+ return version.partial === 1
126
+ ? { major: version.major + 1, minor: 0, patch: 0, pre: ['0'] }
127
+ : { major: version.major, minor: version.minor + 1, patch: 0, pre: ['0'] }
128
+ }
129
+
130
+ /** Expand one comparator (`1.2.3`, `^1.2.3`, `~1.2.3`, `>=1.2.3`, `*`) into bounds. */
131
+ function expandComparator(comparator) {
132
+ const trimmed = comparator.trim()
133
+ // `x`, `*`, `x.x.x` and most operators applied to them accept any version;
134
+ // the strict `>x` / `<x` accept none.
135
+ const wildcard = /^((?:>=|<=|>|<|=|\^|~)?)\s*[xX*](?:\.[xX*]){0,2}$/.exec(trimmed)
136
+ if (trimmed === '' || wildcard) {
137
+ const strict = wildcard !== null && (wildcard[1] === '>' || wildcard[1] === '<')
138
+ return strict ? [] : [{ op: '>=', version: { major: 0, minor: 0, patch: 0, pre: [] } }]
139
+ }
140
+ const parsed = parseComparator(trimmed)
141
+ if (!parsed) return null
142
+ const { op, version } = parsed
143
+ const upper = (major, minor, patch) => ({ op: '<', version: { major, minor, patch, pre: ['0'] } })
144
+ if (op !== '^' && op !== '~') {
145
+ // npm X-range semantics for a partial spelling, taken from semver's own Range
146
+ // expansion (verified comparator by comparator):
147
+ // `0.2` -> >=0.2.0 <0.3.0-0 `<=0.2` -> <0.3.0-0
148
+ // `<0.3` -> <0.3.0-0 `<=0.3` -> <0.4.0-0
149
+ // `>0.2` -> >=0.3.0 `>=0.3` -> >=0.3.0
150
+ // `>0.3` -> >=0.4.0 `<1` -> <1.0.0-0
151
+ // `<=0` -> <1.0.0-0 `>0` -> >=1.0.0
152
+ // `<` bounds at the spelled prefix; `<=`, `>` and `>=` bound one component
153
+ // further. Zero-filling the bound instead would make `>0.2` accept 0.2.5
154
+ // (fail open) and `<=0.2` reject it (fail closed).
155
+ if (version.partial < 3) {
156
+ const start = { major: version.major, minor: version.minor, patch: version.patch, pre: [] }
157
+ const end = version.partial === 1
158
+ ? { major: version.major + 1, minor: 0, patch: 0, pre: ['0'] }
159
+ : { major: version.major, minor: version.minor + 1, patch: 0, pre: ['0'] }
160
+ const here = { major: version.major, minor: version.minor, patch: 0, pre: ['0'] }
161
+ if (op === '=') return [{ op: '>=', version: start }, { op: '<', version: end }]
162
+ if (op === '>') return [{ op: '>=', version: { ...end, pre: [] } }]
163
+ if (op === '<') return [{ op: '<', version: here }]
164
+ if (op === '<=') return [{ op: '<', version: end }]
165
+ // `>=0.3` starts AT the spelled prefix; `>0.3` starts one component later.
166
+ return [{ op: '>=', version: op === '>=' ? start : end }]
167
+ }
168
+ return [{ op, version }]
169
+ }
170
+ const floor = { op: '>=', version }
171
+ // npm X-ranges inside a caret/tilde: `^0` and `~0` both stop at 1.0.0.
172
+ if (version.partial === 1) return [floor, upper(version.major + 1, 0, 0)]
173
+ if (op === '~') return [floor, upper(version.major, version.minor + 1, 0)]
174
+ if (op === '^' && version.partial === 2 && version.major === 0) return [floor, upper(0, version.minor + 1, 0)]
175
+ if (version.major > 0) return [floor, upper(version.major + 1, 0, 0)]
176
+ if (version.minor > 0) return [floor, upper(0, version.minor + 1, 0)]
177
+ return [floor, upper(0, 0, version.patch + 1)]
178
+ }
179
+
180
+ /** Split one `||` alternative into comparators, keeping `>= 1.2.3` together. */
181
+ function expandAlternative(alternative) {
182
+ const trimmed = alternative.trim()
183
+ if (trimmed === '') return [{ op: '>=', version: { major: 0, minor: 0, patch: 0, pre: [] } }]
184
+ const comparators = []
185
+ const verbatim = []
186
+ let cursor = 0
187
+ for (const match of trimmed.matchAll(/(>=|<=|>|<|=|\^|~)?\s*(v?[0-9xX*][^\s]*)/g)) {
188
+ verbatim.push({ op: match[1] ?? '=', version: match[2].replace(/^[vV]/, '') })
189
+ // Anything but whitespace between two comparators is a spelling we refuse,
190
+ // except the lone `-` of a hyphen range (`1.2.3 - 2.3.4`).
191
+ const gap = trimmed.slice(cursor, match.index)
192
+ if (gap.trim() !== '' && gap.trim() !== '-') return null
193
+ const expanded = expandComparator(`${match[1] ?? ''} ${match[2]}`)
194
+ if (expanded === null) return null
195
+ comparators.push(...expanded)
196
+ cursor = match.index + match[0].length
197
+ }
198
+ // (hyphen operands are re-emitted verbatim below)
199
+ if (trimmed.slice(cursor).trim() !== '') return null
200
+ // A hyphen range (`1.2.3 - 2.3.4`, `0.2 - 0.3`) is two partial-or-full
201
+ // versions with a lone `-` between them; npm expands it to `>=lo <=hi` with
202
+ // both bounds verbatim, so the X-range rule must not rewrite them. The
203
+ // tokenizer drops the `-`, so detect it from the original text.
204
+ if (verbatim.length === 2 && verbatim.every(item => item.op === '=')
205
+ && /(^|\s)-(\s|$)/.test(trimmed)) {
206
+ // npm's hyphen range is the X-range of the lower bound plus the prefix end of
207
+ // the upper one: `0.2 - 0.3` is `>=0.2.0 <0.4.0-0`, `1 - 2` is
208
+ // `>=1.0.0 <3.0.0-0`. Both operands keep their own partial expansion.
209
+ const lower = parseVersion(verbatim[0].version.replace(/^[vV]/, ''))
210
+ const upper = parseVersion(verbatim[1].version.replace(/^[vV]/, ''))
211
+ // The lower operand is always inclusive; the upper one keeps its own value
212
+ // when it is a full version and bumps to its prefix end when it is partial.
213
+ return [{ op: '>=', version: lower }, upper.partial === 3
214
+ ? { op: '<=', version: upper }
215
+ : { op: '<', version: upperBoundOf(verbatim[1].version) }]
216
+ }
217
+ return comparators
218
+ }
219
+
220
+ /**
221
+ * Whether one version satisfies one range. Implements the subset the dsh host
222
+ * uses: `||` alternatives, `=`/`>`/`>=`/`<`/`<=`/`^`/`~`, X-ranges.
223
+ *
224
+ * The host decides with `semver.satisfies(version, range, { includePrerelease: true })`
225
+ * (dsh-app-boot `unmetRequirements` → `satisfies`). That flag relaxes npm's rule
226
+ * that a prerelease only matches a comparator carrying the same
227
+ * major/minor/patch, so `0.2.1-rc.5` DOES satisfy `>=0.2.0-rc.2 <0.2.2-0`.
228
+ *
229
+ * Implementing the stricter rule here made this detection more conservative than
230
+ * the host: dsh-ops and dsh-our-free-model were reported "installed but the host
231
+ * does not allow it" on 0.2.1-rc.* while the host loaded both. Detection must
232
+ * match the host, not npm's install-time resolution.
233
+ */
234
+ export function satisfiesRange(version, range) {
235
+ const parsed = parseVersion(version)
236
+ // Only a complete version is testable; partial spellings belong to ranges.
237
+ if (!parsed || parsed.partial !== 3 || typeof range !== 'string') return false
238
+ for (const alternative of range.split('||')) {
239
+ const comparators = expandAlternative(alternative)
240
+ if (comparators === null || comparators.length === 0) continue
241
+ if (comparators.every(({ op, version: bound }) => {
242
+ const order = compareVersions(parsed, bound)
243
+ return op === '=' ? order === 0
244
+ : op === '>' ? order > 0
245
+ : op === '>=' ? order >= 0
246
+ : op === '<' ? order < 0
247
+ : order <= 0
248
+ })) return true
249
+ }
250
+ return false
251
+ }
252
+
253
+ /** Walk up from one path to the version of the host package that owns it. */
254
+ async function hostVersionAbove(path) {
255
+ // Resolve symlinks first. The running host is usually started through the
256
+ // `dsh` shim in the npm global bin directory, which is a symlink into
257
+ // lib/node_modules/@deepseek-ai/dsh/lib/bin.js. `path.resolve` does not follow
258
+ // symlinks, so walking up from the shim never reaches the package and the
259
+ // version silently falls through to whatever this checkout happens to depend
260
+ // on - reported a source checkout's 0.1.2-rc.1 devDependency while the real
261
+ // host was 0.2.1-rc.5.
262
+ let directory = dirname(await realpath(path).catch(() => resolve(path)))
263
+ while (true) {
264
+ const manifest = await readJson(join(directory, 'package.json')).catch(() => null)
265
+ if (manifest?.name === '@deepseek-ai/dsh' && typeof manifest.version === 'string') return manifest.version
266
+ const parent = dirname(directory)
267
+ if (parent === directory) return null
268
+ directory = parent
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The running host version. A host installed into a profile is read from that
274
+ * profile; otherwise the host sub-packages this plugin already resolves are
275
+ * read, because the harness publishes them in lockstep with `@deepseek-ai/dsh`.
276
+ * `OPENWRITE_HOST_VERSION` overrides every source for diagnostics and tests.
277
+ * Unknown when nothing resolves, which keeps every companion decision
278
+ * fail-closed.
279
+ */
280
+ export async function resolveHostVersion(home) {
281
+ const override = process.env.OPENWRITE_HOST_VERSION
282
+ if (typeof override === 'string' && parseVersion(override)) return override
283
+ for (const profile of await readdir(join(home, 'profiles')).catch(() => [])) {
284
+ const installed = join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh')
285
+ const version = await hostVersionAbove(join(installed, 'lib', 'index.js'))
286
+ ?? (await readJson(join(installed, 'package.json')).catch(() => null))?.version
287
+ if (typeof version === 'string' && parseVersion(version)) return version
288
+ }
289
+ // The entry this process was started from is the host that is actually
290
+ // running; it outranks whatever this checkout happens to depend on.
291
+ const entry = process.argv?.[1]
292
+ if (typeof entry === 'string') {
293
+ const version = await hostVersionAbove(entry)
294
+ if (version) return version
295
+ }
296
+ // Last resort: the host packages resolvable from here. In a source checkout
297
+ // that is this repo's own pinned devDependency, never the running host, so it
298
+ // can answer wrongly in both directions — prefer it over nothing only.
299
+ for (const name of ['@deepseek-ai/dsh', '@deepseek-ai/dsh-home-paths', '@deepseek-ai/dsh-settings', '@deepseek-ai/dsh-tools']) {
300
+ const version = await readJson(createRequire(import.meta.url).resolve(`${name}/package.json`)).then(manifest => manifest?.version).catch(() => null)
301
+ if (typeof version === 'string' && parseVersion(version)) return version
302
+ }
303
+ return null
304
+ }
305
+
306
+ /** Every profile bundle list plus a fingerprint of the manifests behind them. */
307
+ async function readProfiles(home) {
308
+ const profiles = []
309
+ let fingerprint = ''
310
+ for (const name of await readdir(join(home, 'profiles')).catch(() => [])) {
311
+ const manifest = join(home, 'profiles', name, 'package.json')
312
+ let parsed, modified
313
+ try {
314
+ parsed = await readJson(manifest)
315
+ modified = (await stat(manifest)).mtimeMs
316
+ } catch { continue }
317
+ const bundles = parsed?.dsh?.profile?.bundles
318
+ if (!Array.isArray(bundles)) continue
319
+ profiles.push({ name, directory: join(home, 'profiles', name), bundles })
320
+ fingerprint += `${name}:${Math.round(modified)};`
321
+ // The companion's own manifest decides whether the running host is inside its
322
+ // declared range, so an upgrade that widens (or narrows) that range must
323
+ // invalidate the cache. Keying only on the profile manifest kept serving the
324
+ // previous verdict after a companion upgrade - the exact #150/#151/#142 class
325
+ // of bug, reachable again the moment the range changes.
326
+ for (const candidates of Object.values(companionNames)) {
327
+ for (const companion of candidates) {
328
+ const companionManifest = join(profiles[profiles.length - 1].directory, 'node_modules', ...companion.split('/'), 'package.json')
329
+ try {
330
+ fingerprint += `${companion}:${Math.round((await stat(companionManifest)).mtimeMs)};`
331
+ } catch { /* companion not installed in this profile */ }
332
+ }
333
+ }
334
+ }
335
+ return { profiles, fingerprint }
336
+ }
337
+
338
+ /** Locate one installed companion package inside a profile. */
339
+ async function installedPackage(profile, names) {
340
+ for (const name of names) {
341
+ if (!profile.bundles.some(bundle => String(bundle).toLowerCase() === name.toLowerCase())) continue
342
+ const manifest = join(profile.directory, 'node_modules', ...name.split('/'), 'package.json')
343
+ const parsed = await readJson(manifest).catch(() => null)
344
+ if (parsed) {
345
+ const constraint = hostConstraint(parsed)
346
+ return { name, directory: join(profile.directory, 'node_modules', ...name.split('/')),
347
+ version: parsed.version, range: constraint.range, rangeSource: constraint.source }
348
+ }
349
+ }
350
+ return null
351
+ }
352
+
353
+ /**
354
+ * Load a companion's entry and build the integration OpenWrite would mount, so an
355
+ * entry that no longer accepts our options is caught here instead of taking the
356
+ * preset composition down with it. Returns null when usable, or why it is not.
357
+ */
358
+ async function validateEntry(directory, entry, build) {
359
+ if (!resolvesEntry(directory, entry)) return `installed package does not expose ${entry}`
360
+ try {
361
+ const resolved = createRequire(join(directory, 'package.json')).resolve(entry)
362
+ const module = await import(pathToFileURL(resolved).href)
363
+ const factory = module.integration ?? module.default
364
+ if (typeof factory !== 'function') return `${entry} no longer exports an integration factory`
365
+ // The mounted row immediately calls `.apply` on the result, so a factory
366
+ // that returns nothing must be refused here rather than at activation.
367
+ const built = build(factory)
368
+ if (built === null || typeof built !== 'object' || typeof built.apply !== 'function') {
369
+ return `${entry} returned no plugin object`
370
+ }
371
+ return null
372
+ } catch (error) {
373
+ return `${entry} rejected the OpenWrite integration: ${error instanceof Error ? error.message : String(error)}`
374
+ }
375
+ }
376
+
377
+ /** Whether one package directory exposes the entry OpenWrite would import. */
378
+ function resolvesEntry(directory, entry) {
379
+ try {
380
+ createRequire(join(directory, 'package.json')).resolve(entry)
381
+ return true
382
+ } catch { return false }
383
+ }
384
+
385
+ /**
386
+ * Whether a companion's own bundle patch is self-consistent.
387
+ *
388
+ * The host imports each `insert` row by its `name`, resolved as a package from
389
+ * the profile. A scoped package that ships a patch naming the bare package
390
+ * (`@ebony-vinyl/dsh-our-free-model` declaring `name: dsh-our-free-model`)
391
+ * therefore never activates: the host reports
392
+ * "Cannot find package 'dsh-our-free-model'". Detecting that here turns a
393
+ * panel that shows "available" and then fails to read the gateway into an
394
+ * accurate "installed but the host cannot load it".
395
+ */
396
+ async function bundlePatchReason(directory, packageName) {
397
+ const manifest = await readJson(join(directory, 'package.json')).catch(() => null)
398
+ const patch = manifest?.dsh?.bundle?.patch
399
+ if (typeof patch !== 'string' || patch === '') return null
400
+ const text = await readFile(join(directory, patch), 'utf8').catch(() => null)
401
+ if (text === null) return `bundle patch ${patch} is missing`
402
+ // Minimal scan: every `name:` value in an insert block. A full YAML parse is
403
+ // deliberately avoided so a malformed patch cannot make detection itself fail.
404
+ const names = [...text.matchAll(/^\s*-?\s*name:\s*['"]?([^'"\n]+)['"]?\s*$/gm)].map(m => m[1].trim())
405
+ if (names.length === 0) return null
406
+ for (const name of names) {
407
+ if (name === packageName) continue
408
+ // A relative or subpath specifier is resolved against the package itself.
409
+ if (name.startsWith('.') || name.startsWith('#')) continue
410
+ try {
411
+ const resolved = createRequire(join(directory, 'package.json')).resolve(name)
412
+ if (resolved.startsWith(resolve(directory))) continue
413
+ } catch { /* fall through to the mismatch report */ }
414
+ return `bundle patch names ${name}, which is not this package (${packageName}); the host cannot load it`
415
+ }
416
+ return null
417
+ }
418
+ /**
419
+ * Why a companion is not usable because of its declared host constraint, or null
420
+ * when the constraint covers the running host.
421
+ *
422
+ * A companion may declare several constraints (dsh-dog declares one range per
423
+ * `@deepseek-ai/dsh-*` sub-package); the host needs all of them, so every one
424
+ * must cover. An unknown host version is reported rather than silently accepted,
425
+ * because "no constraint declared" and "cannot tell" must not read the same.
426
+ */
427
+ function rangeExclusionReason(host, candidate) {
428
+ const range = candidate.range
429
+ if (range === null || range === undefined) {
430
+ return host === null || host === undefined
431
+ ? 'the running host version is unknown, so its declared range cannot be checked'
432
+ : null
433
+ }
434
+ if (host === null || host === undefined) {
435
+ const shown = candidate.rangeSource === '@deepseek-ai/dsh-*'
436
+ ? `${candidate.range.split(' ').filter(Boolean).length} @deepseek-ai/dsh-* peer ranges`
437
+ : `declared range ${range}`
438
+ return `the running host version is unknown, so ${shown} cannot be checked`
439
+ }
440
+ if (satisfiesRange(host, range)) return null
441
+ const source = candidate.rangeSource === '@deepseek-ai/dsh-*'
442
+ ? 'its @deepseek-ai/dsh-* peer ranges'
443
+ : `declared host range ${range}`
444
+ return `${source} excludes dsh ${host}`
445
+ }
446
+
447
+ /**
448
+ * Describe every companion: whether it is installed and whether the running host
449
+ * would load it.
450
+ */
451
+ export async function detectCompanions(home, options = {}) {
452
+ const { profiles, fingerprint } = await readProfiles(home)
453
+ // An explicit host version wins; `undefined` means "resolve it" while an
454
+ // explicit null forces the unknown-host path.
455
+ const host = 'hostVersion' in options ? options.hostVersion : await resolveHostVersion(home)
456
+ const describe = async (names, entry, build) => {
457
+ // Every profile is examined: readdir order is unspecified, so stopping at
458
+ // the first match would make the answer depend on directory order when one
459
+ // profile carries a usable copy and another does not.
460
+ const found = []
461
+ for (const profile of profiles) {
462
+ const installed = await installedPackage(profile, names)
463
+ if (installed) found.push({ profile, ...installed })
464
+ }
465
+ if (found.length === 0) return { installed: false, usable: false, version: null, range: null, profile: null, reason: null }
466
+ const described = []
467
+ for (const candidate of found) {
468
+ const entryReason = entry === undefined ? null : await validateEntry(candidate.directory, entry, build)
469
+ // A companion whose own bundle patch cannot be loaded by the host is not
470
+ // usable however well its range matches: the panel would offer an
471
+ // integration that never activates.
472
+ const patchReason = entryReason ?? await bundlePatchReason(candidate.directory, candidate.name)
473
+ // A null range means the plugin declares no host constraint at all. That is
474
+ // not "usable everywhere": an unknown constraint is treated as covering,
475
+ // because the host would also accept it, but it is reported so the panel
476
+ // can say the range is unknown rather than asserting compatibility.
477
+ const rangeReason = patchReason ?? rangeExclusionReason(host, candidate)
478
+ const usable = rangeReason === null
479
+ described.push({ installed: true, usable, version: candidate.version ?? null, range: candidate.range ?? null,
480
+ rangeSource: candidate.rangeSource ?? null,
481
+ profile: candidate.profile.name, reason: rangeReason })
482
+ }
483
+ return described.find(candidate => candidate.usable) ?? described[0]
484
+ }
485
+ const [dog, ops, freeModel] = await Promise.all([
486
+ describe(companionNames.dog),
487
+ describe(companionNames.ops, companionEntries.ops, opsIntegration),
488
+ describe(companionNames.freeModel),
489
+ ])
490
+ const result = { host, dog, ops, freeModel }
491
+ const cache = join(home, 'openwrite', 'companions.json')
492
+ // The cache key must include this plugin's own version. Keying only on the
493
+ // profile manifests and the host meant an upgrade that changes the detection
494
+ // logic (new companion, corrected range semantics) kept serving the previous
495
+ // answer: a real 0.2.1-rc.5 host showed the old "ops excludes dsh 0.2.1-rc.5"
496
+ // verdict and no DoG row at all, until the manifests happened to change.
497
+ const build = await readJson(new URL('package.json', import.meta.url)).then(m => m?.version).catch(() => null)
498
+ const cached = await readJson(cache).catch(() => null)
499
+ if (cached?.fingerprint === fingerprint && cached.host === host && cached.build === build) {
500
+ // Serve the stored answer: recomputing on every call would re-read every
501
+ // profile and re-import the companion entry each time.
502
+ const { fingerprint: _fingerprint, build: _build, ...rest } = cached
503
+ return { ...rest, host, cached: true }
504
+ }
505
+ await mkdir(join(home, 'openwrite'), { recursive: true }).catch(() => {})
506
+ await writeFile(cache, JSON.stringify({ fingerprint, host, build, ...result }) + '\n').catch(() => {})
507
+ return { ...result, cached: false }
508
+ }
package/cordis.patch.yml CHANGED
@@ -7,5 +7,3 @@
7
7
  mode: managed
8
8
  - id: studio-panel
9
9
  name: ./packages/studio-panel/lib/index.js
10
- - id: openwrite-dog
11
- name: ./vendor/dsh-dog/lib/index.js