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
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { type EditorSerialization } from './selection-coordinates.ts';
|
|
2
|
+
/** Editor UTF-16 positions, plus the exact editor value which produced them. */
|
|
2
3
|
export type ManuscriptSelection = {
|
|
3
4
|
start: number;
|
|
4
5
|
end: number;
|
|
5
6
|
text: string;
|
|
7
|
+
source: string;
|
|
8
|
+
serialization?: EditorSerialization | undefined;
|
|
6
9
|
};
|
|
7
10
|
export type SelectionPolishKind = 'expand' | 'compress' | 'naturalize' | 'reviewFix';
|
|
8
11
|
export declare const SELECTION_POLISH_ACTIONS: readonly {
|
|
@@ -17,7 +20,12 @@ export declare function shouldClearManuscriptSelection(options: {
|
|
|
17
20
|
selectionInsideEditor: boolean;
|
|
18
21
|
preserveNode: EventTarget | Node | null;
|
|
19
22
|
}): boolean;
|
|
20
|
-
export declare function manuscriptSelectionFromRange(value: string, start: number, end: number): ManuscriptSelection | null;
|
|
23
|
+
export declare function manuscriptSelectionFromRange(value: string, start: number, end: number, serialization?: EditorSerialization): ManuscriptSelection | null;
|
|
24
|
+
export declare function mapManuscriptSelection(source: string, selection: ManuscriptSelection): {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
text: string;
|
|
28
|
+
} | null;
|
|
21
29
|
/** Count non-overlapping occurrences of needle in haystack. */
|
|
22
30
|
export declare function countOccurrences(haystack: string, needle: string): number;
|
|
23
31
|
/** Zero-based nth occurrence start, or -1. */
|
|
@@ -26,7 +34,7 @@ export declare function nthIndexOf(haystack: string, needle: string, occurrence:
|
|
|
26
34
|
* Map a live editor selection onto markdown offsets.
|
|
27
35
|
* Duplicate quotes without an occurrence index do not bind the first hit.
|
|
28
36
|
*/
|
|
29
|
-
export declare function locateSelectedMarkdown(value: string, selected: string, occurrence?: number): ManuscriptSelection | null;
|
|
37
|
+
export declare function locateSelectedMarkdown(value: string, selected: string, occurrence?: number, serialization?: EditorSerialization): ManuscriptSelection | null;
|
|
30
38
|
export type QuoteAnchor = {
|
|
31
39
|
state: 'attached' | 'relocated';
|
|
32
40
|
start: number;
|
|
@@ -38,20 +46,25 @@ export type QuoteAnchor = {
|
|
|
38
46
|
/** Bind a stored quote to current markdown using hints, then unique relocate. */
|
|
39
47
|
export declare function locateQuote(content: string, quote: string, startHint: number, endHint: number): QuoteAnchor;
|
|
40
48
|
/** Review issues whose evidence sits inside the current manuscript selection. */
|
|
41
|
-
export declare function reviewIssuesForSelection(issues: readonly unknown[], selection:
|
|
49
|
+
export declare function reviewIssuesForSelection(issues: readonly unknown[], selection: {
|
|
50
|
+
start: number;
|
|
51
|
+
end: number;
|
|
52
|
+
text: string;
|
|
53
|
+
}): string[];
|
|
42
54
|
export type SelectionPolishRequest = {
|
|
43
55
|
ok: true;
|
|
44
56
|
path: '/revisions/selection' | '/revisions/from-review';
|
|
45
57
|
body: Record<string, unknown>;
|
|
46
58
|
} | {
|
|
47
59
|
ok: false;
|
|
48
|
-
reason: 'empty' | 'no-review' | 'no-issues';
|
|
60
|
+
reason: 'empty' | 'no-review' | 'no-issues' | 'source-changed';
|
|
49
61
|
};
|
|
50
62
|
/** Map one author action onto the existing Studio revision-create routes. */
|
|
51
63
|
export declare function selectionPolishRequest(input: {
|
|
52
64
|
kind: SelectionPolishKind;
|
|
53
65
|
chapterId: string;
|
|
54
66
|
selection: ManuscriptSelection;
|
|
67
|
+
documentContent: string;
|
|
55
68
|
documentRevision: string;
|
|
56
69
|
reviewRevision: string;
|
|
57
70
|
reviewFresh: boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Exact mapping between an editor's UTF-16 view and the saved source's code points. */
|
|
2
|
+
export type SourceRange = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
/** A pair captured from the editor's initial serialization, before author input. */
|
|
8
|
+
export type EditorSerialization = {
|
|
9
|
+
source: string;
|
|
10
|
+
value: string;
|
|
11
|
+
};
|
|
12
|
+
/** Exact content and witnessed newline serialization; never guess among quotes. */
|
|
13
|
+
export declare function mapEditorRange(source: string, editor: string, start: number, end: number, serialization?: EditorSerialization): SourceRange | null;
|
|
14
|
+
/** Translate stored source code-point hints back into the editor's UTF-16 view. */
|
|
15
|
+
export declare function mapSourceRange(source: string, editor: string, start: number, end: number, serialization?: EditorSerialization): SourceRange | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser chrome the OpenWrite workbench owns inside the host conversation shell.
|
|
3
|
+
*
|
|
4
|
+
* The host parks its composer in a seat element (`[data-composer-seat]`) inside
|
|
5
|
+
* the conversation scroller (`[data-conversation-scroll]`). The seat is what
|
|
6
|
+
* pushed the workbench's bottom edge up: the host reserved the seat's band out
|
|
7
|
+
* of the content area, so our panel stopped short of the viewport bottom and
|
|
8
|
+
* left a bare white slab under the composer — a flat white strip, not a
|
|
9
|
+
* composer floating over the workbench.
|
|
10
|
+
*
|
|
11
|
+
* The OpenWrite workbench is a full-screen surface, not a chat transcript: it
|
|
12
|
+
* has its own navigation, its own panes and its own scrolling. A centered
|
|
13
|
+
* composer card floating over that layout covers real content (the outline
|
|
14
|
+
* card was cut in half by it). So the seat is hidden while one of our views is
|
|
15
|
+
* mounted and the workbench takes the whole content area.
|
|
16
|
+
*
|
|
17
|
+
* The host exposes no slot for "hide the composer". `conversation.composer` is
|
|
18
|
+
* a chain, but a chain selector can only read session state, not the active
|
|
19
|
+
* view, and it runs before the view mounts — so it cannot answer "is this our
|
|
20
|
+
* view?" at the moment it is asked. A `:has()` rule over the host's own stable
|
|
21
|
+
* data attributes is the one hook that reacts to the mounted view, and it is
|
|
22
|
+
* scoped to our pages: the host's Chat and Trajectory views keep their
|
|
23
|
+
* composer because they never carry the marker.
|
|
24
|
+
*/
|
|
25
|
+
/** Marker attribute carried by the root element of every OpenWrite workbench view. */
|
|
26
|
+
export declare const WORKSPACE_MARKER_ATTRIBUTE = "data-openwrite-workspace";
|
|
27
|
+
/**
|
|
28
|
+
* The rule that hides the host composer seat while an OpenWrite view is mounted.
|
|
29
|
+
*
|
|
30
|
+
* `:has()` matches the scroller that *contains* a workbench marker, and the
|
|
31
|
+
* direct-child combinator pins the rule to that scroller's own seat, so a seat
|
|
32
|
+
* belonging to some other scroller is never touched.
|
|
33
|
+
*/
|
|
34
|
+
export declare const WORKSPACE_CHROME_CSS = "[data-conversation-scroll]:has([data-openwrite-workspace]) > [data-composer-seat] { display: none; }";
|
|
35
|
+
/**
|
|
36
|
+
* Install the workbench chrome stylesheet once per document.
|
|
37
|
+
*
|
|
38
|
+
* Safe to call from every client activation: the second call finds the
|
|
39
|
+
* existing element and returns.
|
|
40
|
+
*/
|
|
41
|
+
export declare function installWorkspaceChromeStyles(): void;
|
|
@@ -35,13 +35,15 @@
|
|
|
35
35
|
"platform": "web"
|
|
36
36
|
},
|
|
37
37
|
"compatibility": {
|
|
38
|
-
"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",
|
|
38
|
+
"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 || 0.2.1-rc.4 || 0.2.1-rc.5",
|
|
39
39
|
"dshReleases": {
|
|
40
40
|
"0.1.2-rc.1": "compatible",
|
|
41
41
|
"0.1.5-alpha.1": "compatible",
|
|
42
42
|
"0.2.0-rc.2": "compatible",
|
|
43
43
|
"0.2.1-rc.2": "compatible",
|
|
44
|
-
"0.2.1-rc.3": "compatible"
|
|
44
|
+
"0.2.1-rc.3": "compatible",
|
|
45
|
+
"0.2.1-rc.4": "compatible",
|
|
46
|
+
"0.2.1-rc.5": "compatible"
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
},
|
|
@@ -71,7 +73,7 @@
|
|
|
71
73
|
},
|
|
72
74
|
"peerDependencies": {
|
|
73
75
|
"@deepseek-ai/cordis": "4.0.2 || ~4.0.4",
|
|
74
|
-
"@deepseek-ai/dsh-host-webserver": "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",
|
|
76
|
+
"@deepseek-ai/dsh-host-webserver": "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 || 0.2.1-rc.4 || 0.2.1-rc.5",
|
|
75
77
|
"react": "^18.2.0",
|
|
76
78
|
"react-dom": "^18.2.0"
|
|
77
79
|
},
|
package/plugin.mjs
CHANGED
|
@@ -3,23 +3,122 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
3
3
|
import { lock } from 'proper-lockfile'
|
|
4
4
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import {
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { legacyPresetIds, legacyAliasName, presetDefinition, referencedPresetIds } from './preset-definition.mjs'
|
|
9
|
+
import { detectCompanions, OPENWRITE_OPS_OPTIONS, summarizeOpsPlan } from './companions.mjs'
|
|
8
10
|
|
|
9
11
|
export const name = 'dsh-openwrite'
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Which companion plugins are installed and loadable here, for the workbench's
|
|
15
|
+
* companion section. Best-effort by design: an unreadable home reports nothing
|
|
16
|
+
* rather than failing the route.
|
|
17
|
+
*/
|
|
18
|
+
async function loadOpsResolver(home, entry) {
|
|
19
|
+
// The companion may live in a profile whose node_modules is not on this
|
|
20
|
+
// plugin's resolution path (dsh-openwrite can be a symlinked dev install), so
|
|
21
|
+
// resolve from the profile that actually owns dsh-ops first.
|
|
22
|
+
const candidates = []
|
|
23
|
+
if (typeof entry.profile === 'string' && entry.profile !== '') {
|
|
24
|
+
try {
|
|
25
|
+
const fromProfile = createRequire(join(home, 'profiles', entry.profile, 'package.json'))
|
|
26
|
+
candidates.push(fromProfile.resolve('dsh-ops/integration'))
|
|
27
|
+
} catch { /* fall through to the normal resolution path */ }
|
|
28
|
+
}
|
|
29
|
+
candidates.push('dsh-ops/integration')
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
try {
|
|
32
|
+
const module = await import(candidate.startsWith('/') ? pathToFileURL(candidate).href : candidate)
|
|
33
|
+
if (typeof module.resolveIntegrationConfig === 'function') return module.resolveIntegrationConfig
|
|
34
|
+
} catch { /* try the next candidate */ }
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function opsPlan(home, entry) {
|
|
40
|
+
if (!entry.usable) return null
|
|
41
|
+
try {
|
|
42
|
+
// Read-only normalization: the same options the preset row uses. A dsh-ops
|
|
43
|
+
// release without this export (or one that changed the option names) is
|
|
44
|
+
// reported as no plan rather than failing the companion inventory.
|
|
45
|
+
const resolveIntegrationConfig = await loadOpsResolver(home, entry)
|
|
46
|
+
if (resolveIntegrationConfig === null) return null
|
|
47
|
+
return summarizeOpsPlan(resolveIntegrationConfig({ ...OPENWRITE_OPS_OPTIONS }))
|
|
48
|
+
} catch { return null }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function companionSummary(home) {
|
|
52
|
+
try {
|
|
53
|
+
const companions = await detectCompanions(home)
|
|
54
|
+
const describe = entry => ({ installed: entry.installed, usable: entry.usable, version: entry.version, reason: entry.reason })
|
|
55
|
+
return {
|
|
56
|
+
host: companions.host,
|
|
57
|
+
dog: describe(companions.dog),
|
|
58
|
+
ops: { ...describe(companions.ops), plan: await opsPlan(home, companions.ops) },
|
|
59
|
+
freeModel: describe(companions.freeModel),
|
|
20
60
|
}
|
|
61
|
+
} catch { return null }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The host's own default-model selection, read from the service the host
|
|
66
|
+
* exposes to plugins (`ctx.agentDefaultModel`).
|
|
67
|
+
*
|
|
68
|
+
* The API key is deliberately NOT part of this answer. The selection is a fact
|
|
69
|
+
* the author already sees in the host's own settings; the key is a secret that
|
|
70
|
+
* must never travel to a browser, and the host keeps it in its credentials
|
|
71
|
+
* service rather than in this selection at all.
|
|
72
|
+
*
|
|
73
|
+
* A host that does not compose the service, or one whose selection is not
|
|
74
|
+
* configured yet, answers `available: false` instead of throwing: the settings
|
|
75
|
+
* page shows "not configured", which is true, rather than failing to open.
|
|
76
|
+
*/
|
|
77
|
+
function hostModelSelection(ctx) {
|
|
78
|
+
const empty = { available: false, provider: '', model: '', reasoningEffort: '' }
|
|
79
|
+
let service
|
|
80
|
+
try { service = ctx.get('agentDefaultModel') } catch { return empty }
|
|
81
|
+
if (service === undefined || service === null) return empty
|
|
82
|
+
let selection
|
|
83
|
+
try { selection = service.currentSelection() } catch { return empty }
|
|
84
|
+
const provider = typeof selection?.provider === 'string' ? selection.provider : ''
|
|
85
|
+
const model = typeof selection?.model === 'string' ? selection.model : ''
|
|
86
|
+
const reasoningEffort = typeof selection?.reasoningEffort === 'string' ? selection.reasoningEffort : ''
|
|
87
|
+
// A selection with no provider or no model names no model: report it as
|
|
88
|
+
// unconfigured rather than as a selection of two empty strings.
|
|
89
|
+
return { available: provider !== '' && model !== '', provider, model, reasoningEffort }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every non-hidden file of a tree, keyed by its path relative to the root. */
|
|
93
|
+
async function listFiles(dir, prefix = '', found = new Map()) {
|
|
94
|
+
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
95
|
+
// Editor and OS noise is not an author edit: a `.DS_Store` must not make the
|
|
96
|
+
// next activation refuse to start.
|
|
97
|
+
if (entry.name.startsWith('.')) continue
|
|
98
|
+
const key = prefix + entry.name
|
|
99
|
+
const path = join(dir, entry.name)
|
|
100
|
+
if (entry.isDirectory()) await listFiles(path, key + '/', found)
|
|
101
|
+
else found.set(key, path)
|
|
102
|
+
}
|
|
103
|
+
return found
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Digest of a managed preset directory, over the files the package ships only.
|
|
108
|
+
* A foreign file (the author's note, a skill they added, a dangling symlink) is
|
|
109
|
+
* not a modification: counting it would make one stray file brick the plugin.
|
|
110
|
+
*/
|
|
111
|
+
async function digestShipped(source, destination) {
|
|
112
|
+
const shipped = await listFiles(source)
|
|
113
|
+
const present = await listFiles(destination)
|
|
114
|
+
const hash = createHash('sha256')
|
|
115
|
+
for (const key of [...shipped.keys()].sort()) {
|
|
116
|
+
const path = present.get(key)
|
|
117
|
+
if (path === undefined) { hash.update(key + '\u0000missing'); continue }
|
|
118
|
+
hash.update(key)
|
|
119
|
+
try { hash.update(await readFile(path)) }
|
|
120
|
+
catch (error) { if (error.code !== 'ENOENT') hash.update(key + '\u0000unreadable') }
|
|
21
121
|
}
|
|
22
|
-
await visit(dir)
|
|
23
122
|
return hash.digest('hex')
|
|
24
123
|
}
|
|
25
124
|
|
|
@@ -39,12 +138,13 @@ export async function apply(ctx) {
|
|
|
39
138
|
const release = await lock(leases, { realpath: false, stale: 30_000, retries: { retries: 30, minTimeout: 100, maxTimeout: 500 } })
|
|
40
139
|
try { return await operation() } finally { await release() }
|
|
41
140
|
}
|
|
42
|
-
const expected = await digestTree(source)
|
|
43
141
|
const marker = join(destination, '.openwrite-managed.json')
|
|
44
142
|
await withLock(async () => {
|
|
45
143
|
const existing = await readFile(marker, 'utf8').then(JSON.parse).catch(() => null)
|
|
46
|
-
if (existing
|
|
47
|
-
|
|
144
|
+
if (existing) {
|
|
145
|
+
if (existing.source !== await digestShipped(source, destination)) {
|
|
146
|
+
throw new Error(`OpenWrite preset ${id} was modified; copy it to a custom preset before reinstalling this version`)
|
|
147
|
+
}
|
|
48
148
|
}
|
|
49
149
|
if (!existing) {
|
|
50
150
|
if (await lstat(destination).then(() => true).catch(error => { if (error.code === 'ENOENT') return false; throw error })) {
|
|
@@ -54,7 +154,8 @@ export async function apply(ctx) {
|
|
|
54
154
|
await mkdir(staging, { recursive: true })
|
|
55
155
|
try {
|
|
56
156
|
await cp(source, staging, { recursive: true })
|
|
57
|
-
|
|
157
|
+
const digest = await digestShipped(source, staging)
|
|
158
|
+
await writeFile(join(staging, '.openwrite-managed.json'), JSON.stringify({ source: digest, tree: digest, version }))
|
|
58
159
|
await rename(staging, destination)
|
|
59
160
|
} finally { await rm(staging, { recursive: true, force: true }) }
|
|
60
161
|
}
|
|
@@ -79,9 +180,16 @@ export async function apply(ctx) {
|
|
|
79
180
|
if (typeof presetCtx.agentPresets.register !== 'function') return
|
|
80
181
|
const definition = await presetDefinition(version, true)
|
|
81
182
|
yield await presetCtx.agentPresets.register(definition)
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
183
|
+
const discovered = (await readdir(join(home, '.agent-presets')).catch(() => []))
|
|
184
|
+
.filter(name => /^openwrite-\d+-\d+-\d+(?:-[a-z0-9-]+)?$/.test(name))
|
|
185
|
+
// An alias keeps a session written by an older release resumable. Registering
|
|
186
|
+
// every released id instead floods the new-session selector with entries no
|
|
187
|
+
// session references, so the alias set follows the session store and only
|
|
188
|
+
// falls back to the durable list when that store cannot be read. This
|
|
189
|
+
// release's own identity is excluded: it is registered as the stable preset
|
|
190
|
+
// and its directory belongs to this plugin, so an alias for it is noise.
|
|
191
|
+
const referenced = await referencedPresetIds(home)
|
|
192
|
+
const ids = new Set([...(referenced ?? legacyPresetIds), ...discovered].filter(candidate => candidate !== id))
|
|
85
193
|
for (const legacyId of ids) {
|
|
86
194
|
// An author-supplied definition wins; readDocument checks presence without
|
|
87
195
|
// waiting for host activation (list/resolve would deadlock startup).
|
|
@@ -90,8 +198,39 @@ export async function apply(ctx) {
|
|
|
90
198
|
catch (error) { if (error?.code !== 'agent-preset/not-found') throw error }
|
|
91
199
|
}
|
|
92
200
|
yield await presetCtx.agentPresets.register({ ...definition, id: legacyId,
|
|
93
|
-
order: 1000, name:
|
|
201
|
+
order: 1000, name: legacyAliasName(legacyId, definition.name),
|
|
94
202
|
})
|
|
95
203
|
}
|
|
96
204
|
})
|
|
205
|
+
// Companion status for the workbench's companion section, and the host's own
|
|
206
|
+
// default-model selection, so OpenWrite's page in the host's settings dialog
|
|
207
|
+
// can show which model dsh itself would use. The companion route reports
|
|
208
|
+
// which implementation serves DoG and whether the optional integrations are
|
|
209
|
+
// live; it never changes what the preset mounts. The model route carries no
|
|
210
|
+
// key: the selection is a browser-visible fact, the key is not. Like every
|
|
211
|
+
// other /studio-panel/* route both sit behind the host's
|
|
212
|
+
// browser-authentication fence: without it any local process could read the
|
|
213
|
+
// host version, the companion inventory and the host's model selection.
|
|
214
|
+
ctx.inject(['webServer'], webCtx => webCtx.effect(() => {
|
|
215
|
+
webCtx.webServer.register({
|
|
216
|
+
kind: 'exact', path: '/studio-panel/companions', handler: async (req, res) => {
|
|
217
|
+
const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)) }
|
|
218
|
+
const connection = webCtx.get('connection')
|
|
219
|
+
const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503
|
|
220
|
+
if (rejection !== undefined) { respond(rejection, { error: 'dsh browser authentication required', message: 'dsh browser authentication required', code: 'AUTHENTICATION_REQUIRED' }); return }
|
|
221
|
+
if (req.method !== 'GET') { respond(405, { error: 'Method not allowed', message: 'Method not allowed', code: 'METHOD_NOT_ALLOWED' }); return }
|
|
222
|
+
respond(200, await companionSummary(home) ?? { host: null, dog: { installed: false, usable: false, version: null, reason: null }, ops: { installed: false, usable: false, version: null, reason: null }, freeModel: { installed: false, usable: false, version: null, reason: null } })
|
|
223
|
+
},
|
|
224
|
+
}, 'openwrite: companion status')
|
|
225
|
+
webCtx.webServer.register({
|
|
226
|
+
kind: 'exact', path: '/studio-panel/host-model', handler: async (req, res) => {
|
|
227
|
+
const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)) }
|
|
228
|
+
const connection = webCtx.get('connection')
|
|
229
|
+
const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503
|
|
230
|
+
if (rejection !== undefined) { respond(rejection, { error: 'dsh browser authentication required', message: 'dsh browser authentication required', code: 'AUTHENTICATION_REQUIRED' }); return }
|
|
231
|
+
if (req.method !== 'GET') { respond(405, { error: 'Method not allowed', message: 'Method not allowed', code: 'METHOD_NOT_ALLOWED' }); return }
|
|
232
|
+
respond(200, hostModelSelection(ctx))
|
|
233
|
+
},
|
|
234
|
+
}, 'openwrite: host model selection')
|
|
235
|
+
}))
|
|
97
236
|
}
|
package/preset-definition.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { open, readFile, readdir, stat, mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
import { fileURLToPath } from 'node:url'
|
|
3
6
|
import { load, Type, JSON_SCHEMA } from 'js-yaml'
|
|
7
|
+
import { detectCompanions } from './companions.mjs'
|
|
8
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
4
9
|
|
|
5
10
|
// Preserve Loader expressions; evaluating them here loses the preset's baseUrl
|
|
6
11
|
// and entry-local realm. This is the host's entry-list !!js wire representation.
|
|
@@ -12,16 +17,150 @@ const schema = JSON_SCHEMA.extend(new Type('tag:yaml.org,2002:js', {
|
|
|
12
17
|
export const stablePresetId = 'openwrite'
|
|
13
18
|
|
|
14
19
|
// These released identities are durable session references, even after their
|
|
15
|
-
// package-managed directories have been removed by older plugin versions.
|
|
16
|
-
|
|
20
|
+
// package-managed directories have been removed by older plugin versions. They
|
|
21
|
+
// are the safety net used when the session store cannot be read; the live alias
|
|
22
|
+
// set follows the sessions that actually reference them (see below).
|
|
23
|
+
/**
|
|
24
|
+
* Every released identity on the current `major.minor` line.
|
|
25
|
+
*
|
|
26
|
+
* Derived from the package version rather than a hardcoded count: `{ length: 29 }`
|
|
27
|
+
* could only ever express `0.2.x`, so a move to 0.3.0 or 1.0.0 structurally could
|
|
28
|
+
* not produce the new identity and the durable-resume safety net would silently
|
|
29
|
+
* omit it.
|
|
30
|
+
*/
|
|
31
|
+
const releaseVersion = JSON.parse(await readFile(new URL('package.json', import.meta.url), 'utf8')).version
|
|
32
|
+
const releaseLine = /^(\d+)\.(\d+)\.(\d+)/.exec(String(releaseVersion || ''))
|
|
33
|
+
export const legacyPresetIds = releaseLine === null
|
|
34
|
+
? []
|
|
35
|
+
: Array.from({ length: Number(releaseLine[3]) }, (_, i) => `openwrite-${releaseLine[1]}-${releaseLine[2]}-${i + 1}`)
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
/** One released versioned preset identity, including any pre-release suffix. */
|
|
38
|
+
const versionedPresetId = /^openwrite-\d+-\d+-\d+(?:-[a-z0-9-]+)?$/
|
|
39
|
+
|
|
40
|
+
/** Session log artifacts, in both encodings the host persistence backend writes. */
|
|
41
|
+
const sessionLogSuffixes = ['.jsonl.zstd', '.jsonl']
|
|
42
|
+
|
|
43
|
+
/** A session header is the first frame of its log; a bounded prefix reads it. */
|
|
44
|
+
const headerPrefixBytes = 256 * 1024
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Name an alias after the release it restores. A bare id is an internal detail
|
|
48
|
+
* that leaves dozens of identically prefixed entries in the preset selector.
|
|
49
|
+
*/
|
|
50
|
+
export function legacyAliasName(id, name) {
|
|
51
|
+
if (!versionedPresetId.test(id)) return `${name}(历史会话 ${id})`
|
|
52
|
+
return `${name}(旧版 ${id.replace(/^openwrite-/, '').replaceAll('-', '.')} · 仅用于恢复历史会话)`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function collectSessionLogs(root) {
|
|
56
|
+
const logs = []
|
|
57
|
+
const walk = async directory => {
|
|
58
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
59
|
+
const path = join(directory, entry.name)
|
|
60
|
+
if (entry.isDirectory()) await walk(path)
|
|
61
|
+
else if (sessionLogSuffixes.some(suffix => entry.name.endsWith(suffix))) logs.push(path)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await walk(root)
|
|
65
|
+
return logs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A digest over every log's identity, not just {count, newest mtime}: deleting
|
|
70
|
+
* one session and adding another in the same second keeps that pair unchanged
|
|
71
|
+
* and would serve a stale alias set (a missing id, a lingering one). Stats only,
|
|
72
|
+
* so this stays cheap enough to re-derive on every start.
|
|
73
|
+
*/
|
|
74
|
+
async function fingerprintLogs(logs) {
|
|
75
|
+
const hash = createHash('sha256')
|
|
76
|
+
const rows = []
|
|
77
|
+
for (const path of logs) {
|
|
78
|
+
let info
|
|
79
|
+
try { info = await stat(path) } catch { continue }
|
|
80
|
+
rows.push(`${path}\u0000${Math.round(info.mtimeMs)}\u0000${info.size}\n`)
|
|
81
|
+
}
|
|
82
|
+
rows.sort()
|
|
83
|
+
for (const row of rows) hash.update(row)
|
|
84
|
+
return { files: rows.length, digest: hash.digest('hex') }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The `agentPreset` a stored session header carries, or undefined when unreadable. */
|
|
88
|
+
async function headerPresetId(path) {
|
|
89
|
+
const handle = await open(path, 'r')
|
|
90
|
+
try {
|
|
91
|
+
const buffer = Buffer.alloc(headerPrefixBytes)
|
|
92
|
+
const { bytesRead } = await handle.read(buffer, 0, headerPrefixBytes, 0)
|
|
93
|
+
const head = buffer.subarray(0, bytesRead)
|
|
94
|
+
// node:zlib decodes the leading frame and ignores the appended event frames.
|
|
95
|
+
const text = path.endsWith('.zstd') ? zstdDecompressSync(head).toString('utf8') : head.toString('utf8')
|
|
96
|
+
const header = JSON.parse(text.split('\n', 1)[0])
|
|
97
|
+
return typeof header?.agentPreset === 'string' ? header.agentPreset : undefined
|
|
98
|
+
} finally {
|
|
99
|
+
await handle.close()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The versioned preset ids stored sessions still reference. Registering every
|
|
105
|
+
* released id instead would fill the new-session selector with entries no
|
|
106
|
+
* session can resume, so the alias set is derived from the session store.
|
|
107
|
+
* Returns null when that store cannot be read at all, which keeps the durable
|
|
108
|
+
* alias list rather than silently dropping historical session resume.
|
|
109
|
+
*/
|
|
110
|
+
export async function referencedPresetIds(home) {
|
|
111
|
+
const root = join(home, 'sessions')
|
|
112
|
+
let logs
|
|
113
|
+
try {
|
|
114
|
+
logs = await collectSessionLogs(root)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code !== 'ENOENT') return null
|
|
117
|
+
logs = []
|
|
118
|
+
}
|
|
119
|
+
// fingerprintLogs skips logs it cannot stat (a dangling symlink, or one the
|
|
120
|
+
// host prunes mid-scan); a walk that cannot even be listed is the only case
|
|
121
|
+
// that falls back to the durable list.
|
|
122
|
+
const fingerprint = await fingerprintLogs(logs).catch(() => null)
|
|
123
|
+
if (!fingerprint) return null
|
|
124
|
+
const cache = join(home, 'openwrite', 'preset-aliases.json')
|
|
125
|
+
const cached = await readFile(cache, 'utf8').then(JSON.parse).catch(() => null)
|
|
126
|
+
if (cached?.fingerprint?.files === fingerprint.files && cached.fingerprint.digest === fingerprint.digest) {
|
|
127
|
+
return new Set((cached.ids ?? []).filter(id => versionedPresetId.test(id)))
|
|
128
|
+
}
|
|
129
|
+
const ids = new Set()
|
|
130
|
+
for (const path of logs) {
|
|
131
|
+
const id = await headerPresetId(path).catch(() => undefined)
|
|
132
|
+
if (id && versionedPresetId.test(id)) ids.add(id)
|
|
133
|
+
}
|
|
134
|
+
await mkdir(join(home, 'openwrite'), { recursive: true }).catch(() => {})
|
|
135
|
+
await writeFile(cache, JSON.stringify({ fingerprint, ids: [...ids] }) + '\n').catch(() => {})
|
|
136
|
+
return ids
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mount dsh-ops into this preset when the companion is installed, its declared
|
|
141
|
+
* host range covers the running host, and it exposes the integration entry.
|
|
142
|
+
*/
|
|
143
|
+
async function adaptCompanions(plugins, options) {
|
|
144
|
+
if (plugins.some(row => row.id === 'openwrite-ops')) return
|
|
145
|
+
let companions = null
|
|
146
|
+
try {
|
|
147
|
+
companions = await detectCompanions(resolveDshHome(), options)
|
|
148
|
+
} catch { /* an unreadable home leaves the preset exactly as shipped */ }
|
|
149
|
+
if (!companions?.ops?.usable) return
|
|
150
|
+
plugins.push({ id: 'openwrite-ops', name: 'dsh-openwrite/ops-integration' })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function presetDefinition(version, modern = false, options = {}) {
|
|
19
154
|
const baseUrl = new URL('./presets/openwrite/', import.meta.url)
|
|
20
155
|
const plugins = load(await readFile(new URL('agent.cordis.yml', baseUrl), 'utf8'), { schema })
|
|
21
156
|
const metadata = load(await readFile(new URL('preset.yml', baseUrl), 'utf8'))
|
|
22
157
|
if (modern) {
|
|
23
158
|
const adapt = rows => {
|
|
24
159
|
for (const row of rows) {
|
|
160
|
+
// The workflow row is renamed for hosts that ship the PTC variant. This
|
|
161
|
+
// repo's node_modules carries an older host without that package, so the
|
|
162
|
+
// resolution gate treats it as expected-absent rather than failing (see
|
|
163
|
+
// scripts/preset-smoke.mjs).
|
|
25
164
|
if (row.name === '@deepseek-ai/dsh-workflow-worker-thread') {
|
|
26
165
|
row.name = '@deepseek-ai/dsh-workflow-ptc'
|
|
27
166
|
}
|
|
@@ -33,5 +172,6 @@ export async function presetDefinition(version, modern = false) {
|
|
|
33
172
|
}
|
|
34
173
|
adapt(plugins)
|
|
35
174
|
}
|
|
175
|
+
await adaptCompanions(plugins, options)
|
|
36
176
|
return { ...metadata, id: stablePresetId, order: -1, plugins }
|
|
37
177
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
outlines and assets unless the author asks to translate them. Chinese skill instructions
|
|
21
21
|
describe procedures; they do not require Chinese output. Translate their headings and help
|
|
22
22
|
when presenting them. In English conversations, use English throughout the creative loop.
|
|
23
|
-
Writing tasks return a task_id immediately; poll
|
|
23
|
+
Writing tasks return a task_id immediately; poll novel_task(action="get") until terminal and present
|
|
24
24
|
cancellation as requested until the task reports cancelled. Resume saved drafts via run_id_v2.
|
|
25
25
|
|
|
26
26
|
你是 OpenWrite 创作 Agent,运行在 DeepSeek Harness(dsh)的统一 OpenWrite 预设中。
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
→ rereview 闭环。低质量、硬门禁失败或上下文不完整时先报告原因,不强行交付。
|
|
67
67
|
修改已有正文、人物、世界、正典、伏笔或其他项目文档时,先用 novel_search / novel_status
|
|
68
68
|
定位正式实体并读取最新版本。正文、正典和其他普通文档的每个独立修改分别调用
|
|
69
|
-
|
|
70
|
-
修改分别调用
|
|
69
|
+
novel_change_plan(action="document", step="preview");大纲、资产、创作重点、伏笔和写作目标的每个独立
|
|
70
|
+
修改分别调用 novel_change_plan(action="structured", step="preview")。向用户展示返回的 before/after 与来源版本;
|
|
71
71
|
只有用户明确确认后,才把原 preview_token 原样传给 action="apply", confirm=true,不得重新生成路径、
|
|
72
72
|
锚点或内容。用户拒绝时用 action="reject";源文件冲突时用 action="retry" 生成新预览;
|
|
73
73
|
已应用项需要撤销时,只能把返回的 undo_preview_token 传给 action="undo", confirm=true。
|
|
74
|
-
|
|
74
|
+
novel_doc(action="write") 是已获明确授权的完整文档写入兼容入口,不得用它绕过逐项预览。
|
|
75
75
|
完成写入后可用 novel_trace_list 核对来源、预算、模型调用、工具调用和领域变更。Trace 只保留摘要、
|
|
76
76
|
revision 与用量,作品正文、提示原文、模型原文、凭据和推理内容不得写入 trace。
|
|
77
77
|
普通讨论、分析和未确认建议不得写入资产。
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
或阶段仍在大纲滚动期但用户明确要求“根据现在/当前大纲写下一章”、
|
|
82
82
|
“就按这个大纲写”或等价表达,先向用户确认大纲范围,再执行章节预检与写作。
|
|
83
83
|
用户要求修改大纲时,先用 novel_outline_read 定位结构,再用
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
novel_change_plan(action="structured", step="preview", change_kind="outline") 对一个结构操作逐项预览和确认;不要用
|
|
85
|
+
novel_doc(action="write") 绕过大纲门控,也不要整篇覆盖 src/outline.md。novel_outline_edit、
|
|
86
|
+
novel_assets(action="update")、novel_focus、novel_foreshadowing 和 novel_writing_targets 仅在用户本轮已经
|
|
87
87
|
明确授权对应的精确变更时作为直接写入兼容入口使用。
|
|
88
88
|
用户要求删除已写正文或章节时,先向用户说明将删除的章节与影响并得到明确同意。
|
|
89
89
|
单章使用 novel_chapter_delete;清理连续尾段时使用 novel_chapter_delete_batch,
|
|
@@ -335,5 +335,3 @@
|
|
|
335
335
|
- id: openwrite-tools
|
|
336
336
|
name: dsh-openwrite/tools
|
|
337
337
|
|
|
338
|
-
- id: openwrite-dog-tools
|
|
339
|
-
name: dsh-openwrite/dog-tools
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// dsh-ops preset integration. The row that mounts this module is only added
|
|
2
|
+
// when dsh-ops is installed, its declared host range covers the running host,
|
|
3
|
+
// and it exposes `dsh-ops/integration`. The import is still defensive: a
|
|
4
|
+
// companion that changed its entry between detection and load must degrade to
|
|
5
|
+
// a no-op row rather than take the whole preset composition down.
|
|
6
|
+
export const name = '@dsh-novel/openwrite-ops'
|
|
7
|
+
|
|
8
|
+
export async function apply(ctx) {
|
|
9
|
+
let integration
|
|
10
|
+
try {
|
|
11
|
+
({ integration } = await import('dsh-ops/integration'))
|
|
12
|
+
} catch {
|
|
13
|
+
ctx.effect(() => () => {}, 'openwrite-ops: dsh-ops integration unavailable')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
// Bounded file tools plus progressive discovery for the OpenWrite preset.
|
|
17
|
+
// Official file tools stay available (`replaceOfficial: false`), no shell is
|
|
18
|
+
// published (`shell: false`), and the preset's own guidance is not replaced.
|
|
19
|
+
const plugin = integration({
|
|
20
|
+
preset: 'openwrite',
|
|
21
|
+
files: true,
|
|
22
|
+
discovery: true,
|
|
23
|
+
presentation: 'native',
|
|
24
|
+
replaceOfficial: false,
|
|
25
|
+
shell: false,
|
|
26
|
+
})
|
|
27
|
+
return plugin.apply(ctx)
|
|
28
|
+
}
|
|
@@ -9,9 +9,9 @@ Language: follow the author's current conversation language for responses and he
|
|
|
9
9
|
|
|
10
10
|
接到已有 `novel_*`。人物、地点、世界观以 OpenWrite 资产为准。
|
|
11
11
|
|
|
12
|
-
1. `
|
|
13
|
-
2. 指定实体用 `
|
|
12
|
+
1. `novel_assets(action="list"` 列出人物/世界资产(含名称与别名)。
|
|
13
|
+
2. 指定实体用 `novel_assets(action="read", kind="character"|"world"|"progression", id=…)`。
|
|
14
14
|
3. 按名称或别名找不到时 `novel_search`(scope 可 `assets`)。
|
|
15
15
|
4. 关系与状态漂移用 `novel_continuity`。
|
|
16
16
|
|
|
17
|
-
向作者报告规范名、别名、摘要。改设定走 `
|
|
17
|
+
向作者报告规范名、别名、摘要。改设定走 `novel_change_plan(action="structured"(change_kind="asset")`;普通讨论不得写入。
|