dsh-openwrite 0.2.27 → 0.2.33
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/LICENSE +1 -1
- package/README.md +55 -6
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +5 -3
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +60 -0
- package/docs/COST_MEASUREMENT.md +2 -2
- package/docs/DEVELOPMENT.md +6 -11
- package/docs/DSH_STORE.md +14 -2
- package/docs/EDITOR_SAFETY_REPAIRS.md +17 -0
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +97 -0
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUES_98_100.md +2 -0
- package/docs/ISSUE_123_FOLLOWUP.md +55 -0
- package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
- package/docs/LEARNING.md +1 -1
- package/docs/MOBILE_READINESS.md +62 -0
- package/docs/PLUGIN_MAINTENANCE.md +1 -3
- package/docs/RELEASE_ACCEPTANCE.md +90 -3
- package/docs/REVIEW_ROUND_22.md +67 -0
- package/docs/REVIEW_ROUND_23.md +48 -0
- package/docs/REVIEW_ROUND_25.md +51 -0
- package/docs/SELECTION_UPGRADE_REPAIRS.md +16 -0
- package/docs/TOOLS.md +933 -0
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +18 -21
- package/packages/openwrite-bridge/lib/client.d.ts +0 -1
- package/packages/openwrite-bridge/lib/client.js +90 -9
- package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +10 -4
- package/packages/openwrite-bridge/lib/dog-review.js +15 -3
- package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
- package/packages/openwrite-bridge/lib/domain.js +250 -29
- package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
- package/packages/openwrite-bridge/lib/host-llm.js +657 -0
- package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
- package/packages/openwrite-bridge/lib/host-web.js +166 -0
- package/packages/openwrite-bridge/lib/index.d.ts +4 -0
- package/packages/openwrite-bridge/lib/index.js +150 -6
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
- package/packages/openwrite-bridge/lib/managed-runtime.js +179 -35
- package/packages/openwrite-bridge/lib/tool-policy.js +39 -12
- package/packages/openwrite-bridge/lib/tools.js +1238 -1219
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +116667 -114773
- package/packages/studio-panel/lib/client.js.map +1 -1
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
- package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +8 -2
- package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
- package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/manuscript-annotations.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +18 -5
- package/packages/studio-panel/lib/types/client/selection-coordinates.d.ts +15 -0
- package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
- package/packages/studio-panel/package.json +5 -3
- package/plugin.mjs +159 -20
- package/preset-definition.mjs +144 -4
- package/presets/openwrite/agent.cordis.yml +7 -9
- package/presets/openwrite/ops-integration.mjs +28 -0
- package/presets/openwrite/preset.yml +2 -1
- package/presets/openwrite/skills/canon/SKILL.md +3 -3
- package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
- package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
- package/presets/openwrite/skills/export-book/SKILL.md +3 -3
- package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
- package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
- package/presets/openwrite/skills/learn/SKILL.md +3 -3
- package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
- package/presets/openwrite/skills/progress/SKILL.md +1 -1
- package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
- package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
- package/presets/openwrite/skills/style-system/SKILL.md +13 -6
- package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
- package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
- package/presets/openwrite/skills/world-query/SKILL.md +16 -11
- package/presets/openwrite/skills/write-next/SKILL.md +1 -1
- package/release/{openwrite-5.8.16-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -12
- package/release/source.json +21 -21
- package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
- package/scripts/maintenance.mjs +67 -7
- package/scripts/plugin-doctor.mjs +179 -0
- package/scripts/prepare.mjs +11 -2
- package/scripts/safe-upgrade.mjs +141 -13
- package/dog-tools.mjs +0 -11
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
- package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
- package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
- package/scripts/dog/delivery-stage.js +0 -32
- package/scripts/dog/import-chapter.js +0 -20
- package/scripts/dog/import-record.js +0 -35
- package/scripts/dog/review-dimension.js +0 -41
- package/scripts/dog/review-record.js +0 -42
- package/vendor/dsh-dog/LICENSE +0 -29
- package/vendor/dsh-dog/README.md +0 -114
- package/vendor/dsh-dog/UPSTREAM.md +0 -30
- package/vendor/dsh-dog/cordis.patch.yml +0 -14
- package/vendor/dsh-dog/lib/client.js +0 -2960
- package/vendor/dsh-dog/lib/client.js.map +0 -1
- package/vendor/dsh-dog/lib/core-BAlgZpLq.js +0 -2074
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
- package/vendor/dsh-dog/lib/core.d.ts +0 -2
- package/vendor/dsh-dog/lib/core.js +0 -2
- package/vendor/dsh-dog/lib/index.d.ts +0 -143
- package/vendor/dsh-dog/lib/index.js +0 -1279
- package/vendor/dsh-dog/package.json +0 -110
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
- package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
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
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
- 运行时:跨平台回归发现自动恢复耗尽后的手动重试可能复用尚未清理的旧启动 promise,返回失效连接。现在等待旧启动结束后重新准备;并发重试共享新连接,后来发生的取消优先。五项可控时序回归在旧实现上失败,修复后通过。
|
|
12
12
|
- #114:本版包含 Core 5.8.15 的工作台 Console 认证、自动工作区头与请求前凭据更新。此前真实工作台连接/流式写作通过;此次真实 Go 安全评审首次请求 max_tokens=24000,流式调用 4.424 秒通过,没有写入作者正文或作品配置。
|
|
13
13
|
|
|
14
|
+
## 后续实机证据(2026-10-27)
|
|
15
|
+
|
|
16
|
+
报告者已补充[旧会话恢复记录](https://github.com/LiPu-jpg/Openwrite/issues/123#issuecomment-6422870871)和 [B-13 Windows 实测](https://github.com/LiPu-jpg/Openwrite/issues/123#issuecomment-6423904648):旧预设目录缺失时历史会话正常打开;行首/行中 `>`、混合行尾锚点预览通过。两项原场景验证据此完成,不再索要同一材料。此前“待最小复现/实机”的记录反映当时证据边界;新补修与候选状态见 [#123 回复补修](ISSUE_123_FOLLOWUP.md)。
|
|
17
|
+
|
|
14
18
|
## 保留的未完成工作
|
|
15
19
|
|
|
16
20
|
统一跟踪:[issue #123](https://github.com/LiPu-jpg/Openwrite/issues/123)。
|
|
@@ -18,11 +22,9 @@
|
|
|
18
22
|
| 来源 | 已有证据 | 尚待完成 |
|
|
19
23
|
| --- | --- | --- |
|
|
20
24
|
| #98.1 | 独立停机旁路升级工具已发布;宿主候选补丁通过 Windows/Linux 专项 CI。 | [fork PR #1](https://github.com/LiPu-jpg/deepseek-harness/pull/1) 尚未成为正式宿主;[上游讨论](https://github.com/deepseek-ai/deepseek-harness/discussions/10745) 继续跟踪。原生安装命令不受插件工具保护。 |
|
|
21
|
-
| #105 | 稳定 ID、永久别名、目录保留、原日志与自定义预设保护回归通过。 | 报告者 Windows Electron 历史会话完整恢复的实机验收。 |
|
|
22
|
-
| #109 B-13 | 字面匹配及两层传输回归通过。 | 原始失败文件片段、完整参数和行尾/空格信息的最小复现。 |
|
|
23
25
|
| #111 | 不支持平台返回明确错误并提供外部 Studio 配置;原报告接受此降级方向。 | Android/Termux 真机与外部 Studio 实测;当前没有 Android 原生运行时。 |
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
后续独立反馈 [#121](https://github.com/LiPu-jpg/Openwrite/issues/121)、[#122](https://github.com/LiPu-jpg/Openwrite/issues/122) 已修复、验收并合并关闭;修复已包含于正式 0.2.28。发行和关闭代码报告不代替上述两项外部条件。
|
|
26
28
|
|
|
27
29
|
## 后续关闭条件
|
|
28
30
|
|