@carljia/omd-dsh 0.1.1 → 0.1.3
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/README.md +4 -4
- package/lib/cli.js +236 -33
- package/lib/mode.d.ts +33 -0
- package/lib/mode.js +145 -0
- package/lib/plan.d.ts +22 -0
- package/lib/plan.js +146 -0
- package/lib/startwork.d.ts +17 -0
- package/lib/startwork.js +147 -0
- package/lib/vendor/omd-mode-switch.mjs +145 -0
- package/lib/vendor/omd-plan.mjs +146 -0
- package/lib/vendor/omd-start-work.mjs +147 -0
- package/{omd-matrix.json → omd-matrix.default.json} +9 -3
- package/package.json +2 -2
- package/presets/omd-chat/agent.cordis.yml +2 -0
- package/presets/omd-executor/agent.cordis.yml +28 -1
- package/presets/omd-explorer/agent.cordis.yml +2 -0
- package/presets/omd-librarian/agent.cordis.yml +2 -0
- package/presets/omd-planner/agent.cordis.yml +8 -1
- package/presets/omd-reviewer/agent.cordis.yml +2 -0
- package/presets/{omd-architect → omd-ultraworker}/agent.cordis.yml +16 -2
- package/presets/omd-ultraworker/preset.yml +3 -0
- package/presets/omd-architect/preset.yml +0 -3
package/README.md
CHANGED
|
@@ -38,15 +38,15 @@ tier 差异化子代理委派(等价 OMD 的 task(category=…))。仅限 pr
|
|
|
38
38
|
omd-dsh <command> [--harness <路径>] [--dry-run]
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
- `omd-dsh sync` — 把 presets/ 与 vendored 行模块同步到 `<DSH_HOME>/.agent-presets/`(hash 保护、orphan 报告、非管理目录零操作)。每个 preset 的 omd-mode / omd-task
|
|
42
|
-
- `omd-dsh setup` — 交互式向导:先读取 DSH 已有模型,再引导逐模式/逐 tier 选择模型,写回
|
|
41
|
+
- `omd-dsh sync` — 把 presets/ 与 vendored 行模块同步到 `<DSH_HOME>/.agent-presets/`(hash 保护、orphan 报告、非管理目录零操作)。每个 preset 的 omd-mode / omd-task 行由用户矩阵 `<DSH_HOME>/omd-matrix.json` 渲染。
|
|
42
|
+
- `omd-dsh setup` — 交互式向导:先读取 DSH 已有模型,再引导逐模式/逐 tier 选择模型,写回 `<DSH_HOME>/omd-matrix.json` 并可选立即同步。
|
|
43
43
|
- `omd-dsh models` — 打印发现的 DSH 模型目录(非交互)。
|
|
44
44
|
|
|
45
45
|
细节见 docs/ARCHITECTURE.md 的「vendored 分发与跨树符号风险」。
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## 集中配置:<DSH_HOME>/omd-matrix.json
|
|
48
48
|
|
|
49
|
-
所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier
|
|
49
|
+
所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier 的模型集中在用户矩阵 `<DSH_HOME>/omd-matrix.json`(默认 `~/.dsh/omd-matrix.json`):`omd-dsh sync` 首次运行把随包的 deepseek 默认矩阵 `omd-matrix.default.json` 复制为默认配置(或从旧版本包内位置迁移一次),`omd-dsh setup` 更新它,`omd-dsh sync` 读取它并渲染进各 preset。仓库与 npm 包只携带默认矩阵文件,**不含任何个人模型配置**——个人模型配置只留在本机。preset 里 `# [omd-dsh:mode:start] / [omd-dsh:mode:end]` 与 `# [omd-dsh:task:start] / [omd-dsh:task:end]` 之间的区域是自动生成的——改模型请改矩阵后跑 sync,不要手改 fence 之间的内容。
|
|
50
50
|
|
|
51
51
|
## 开发
|
|
52
52
|
|
package/lib/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { promises as fs, existsSync, realpathSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { promises as fs, existsSync, mkdirSync, realpathSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { dirname, join, relative, resolve, basename } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -10,25 +10,34 @@ import { createInterface } from "node:readline/promises";
|
|
|
10
10
|
* omd-dsh CLI
|
|
11
11
|
*
|
|
12
12
|
* omd-dsh sync materialize presets into <DSH_HOME>/.agent-presets,
|
|
13
|
-
* rendering each preset's omd-mode / omd-task rows from
|
|
14
|
-
* omd-matrix.json
|
|
15
|
-
*
|
|
13
|
+
* rendering each preset's omd-mode / omd-task rows from the
|
|
14
|
+
* user's model matrix at <DSH_HOME>/omd-matrix.json. On
|
|
15
|
+
* first run the shipped deepseek default matrix
|
|
16
|
+
* (omd-matrix.default.json) is copied there; personal
|
|
17
|
+
* model settings stay on the user's machine and are never
|
|
18
|
+
* shipped or uploaded.
|
|
16
19
|
* omd-dsh setup interactive wizard: discover the models DSH already has,
|
|
17
20
|
* then guide per-mode and per-tier model selection.
|
|
18
21
|
* omd-dsh models print the discovered model catalog (non-interactive).
|
|
19
22
|
*
|
|
20
23
|
* Distribution model (vendored + harness-anchored imports) is unchanged from
|
|
21
24
|
* the original omd-dsh sync: presets/omd-* are copied into .agent-presets/;
|
|
22
|
-
* their omd-mode/omd-task
|
|
23
|
-
* omd-
|
|
24
|
-
* .agent-presets/.omd-vendor/ with bare @deepseek-ai/*
|
|
25
|
-
* absolute file:// URLs into the harness node_modules
|
|
25
|
+
* their omd-mode / omd-task / omd-plan / omd-start-work / omd-mode-switch
|
|
26
|
+
* rows reference ../.omd-vendor/*.mjs by relative path; the vendored modules
|
|
27
|
+
* are copied into .agent-presets/.omd-vendor/ with bare @deepseek-ai/*
|
|
28
|
+
* imports rewritten to absolute file:// URLs into the harness node_modules
|
|
29
|
+
* tree.
|
|
26
30
|
*/
|
|
27
31
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
-
const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs"];
|
|
29
|
-
|
|
32
|
+
const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs", "omd-plan.mjs", "omd-start-work.mjs", "omd-mode-switch.mjs"];
|
|
33
|
+
/** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
|
|
34
|
+
const MATRIX_PATH = join(dshHome(), "omd-matrix.json");
|
|
35
|
+
/** Pre-migration location (package root) — migrated to MATRIX_PATH once when present. */
|
|
36
|
+
const LEGACY_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.json");
|
|
30
37
|
const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
|
|
31
38
|
const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]" };
|
|
39
|
+
/** Presets that were renamed: old directory name -> new preset name. */
|
|
40
|
+
const RENAMED_FROM = { "omd-architect": "omd-ultraworker" };
|
|
32
41
|
function usage() {
|
|
33
42
|
return [
|
|
34
43
|
"omd-dsh <command> [options]",
|
|
@@ -37,7 +46,7 @@ function usage() {
|
|
|
37
46
|
" setup interactive: discover DSH models, then guide per-mode/tier model selection",
|
|
38
47
|
" models print the discovered DSH model catalog",
|
|
39
48
|
" options:",
|
|
40
|
-
" --harness <path> node_modules dir of the DSH harness install (
|
|
49
|
+
" --harness <path> node_modules dir of the DSH harness install (saved locally after first use; otherwise auto-detected)",
|
|
41
50
|
" --dry-run (sync) print planned writes without touching the filesystem",
|
|
42
51
|
" --verbose (sync) print per-file detail",
|
|
43
52
|
].join("\n");
|
|
@@ -55,6 +64,60 @@ function findNodeModules(start) {
|
|
|
55
64
|
current = parent;
|
|
56
65
|
}
|
|
57
66
|
}
|
|
67
|
+
function harnessCachePath() { return join(dshHome(), "omd-dsh-harness.json"); }
|
|
68
|
+
/** Read the cached harness node_modules, ignoring a stale/missing entry. */
|
|
69
|
+
function readCachedHarness() {
|
|
70
|
+
try {
|
|
71
|
+
const p = harnessCachePath();
|
|
72
|
+
if (!existsSync(p))
|
|
73
|
+
return undefined;
|
|
74
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
75
|
+
const nm = parsed && typeof parsed === "object" ? parsed.harnessNodeModules : undefined;
|
|
76
|
+
if (typeof nm !== "string" || nm === "")
|
|
77
|
+
return undefined;
|
|
78
|
+
if (!existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
|
|
79
|
+
return undefined;
|
|
80
|
+
return nm;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Persist the resolved harness node_modules for later runs (best-effort). */
|
|
87
|
+
function writeCachedHarness(harnessNodeModules) {
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(harnessCachePath(), JSON.stringify({ harnessNodeModules }, null, 2) + "\n", "utf8");
|
|
90
|
+
}
|
|
91
|
+
catch { /* best-effort */ }
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the DSH harness node_modules:
|
|
95
|
+
* 1. --harness flag (and cache it for later);
|
|
96
|
+
* 2. auto-detect via the dsh executable on PATH;
|
|
97
|
+
* 3. fall back to the locally cached value.
|
|
98
|
+
*/
|
|
99
|
+
function resolveHarness(flags) {
|
|
100
|
+
let nm;
|
|
101
|
+
if (flags.harness !== undefined) {
|
|
102
|
+
nm = findNodeModules(flags.harness);
|
|
103
|
+
if (nm !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
nm = realpathSync(nm);
|
|
106
|
+
writeCachedHarness(nm);
|
|
107
|
+
}
|
|
108
|
+
catch { /* keep nm as-is */ }
|
|
109
|
+
}
|
|
110
|
+
return nm;
|
|
111
|
+
}
|
|
112
|
+
nm = locateHarnessViaDsh() ?? locateHarnessViaNpxCache() ?? readCachedHarness();
|
|
113
|
+
if (nm !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
nm = realpathSync(nm);
|
|
116
|
+
}
|
|
117
|
+
catch { /* keep */ }
|
|
118
|
+
}
|
|
119
|
+
return nm;
|
|
120
|
+
}
|
|
58
121
|
function locateHarnessViaDsh() {
|
|
59
122
|
const candidates = [];
|
|
60
123
|
try {
|
|
@@ -79,6 +142,51 @@ function locateHarnessViaDsh() {
|
|
|
79
142
|
}
|
|
80
143
|
return undefined;
|
|
81
144
|
}
|
|
145
|
+
/** Candidate npx cache roots where a non-global DSH install may live. */
|
|
146
|
+
function npxCacheRoots() {
|
|
147
|
+
const roots = [];
|
|
148
|
+
if (process.platform === "win32") {
|
|
149
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
150
|
+
if (localAppData)
|
|
151
|
+
roots.push(join(localAppData, "npm-cache", "_npx"));
|
|
152
|
+
const appData = process.env.APPDATA;
|
|
153
|
+
if (appData)
|
|
154
|
+
roots.push(join(appData, "npm-cache", "_npx"));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
roots.push(join(homedir(), ".npm", "_npx"));
|
|
158
|
+
}
|
|
159
|
+
return roots;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Best-effort scan of the npx cache for a DSH install whose node_modules
|
|
163
|
+
* carries @deepseek-ai/dsh-scope. Picks the most recently touched one.
|
|
164
|
+
*/
|
|
165
|
+
function locateHarnessViaNpxCache() {
|
|
166
|
+
const matches = [];
|
|
167
|
+
for (const root of npxCacheRoots()) {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = readdirSync(root);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const nm = join(root, entry, "node_modules");
|
|
177
|
+
if (!existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
|
|
178
|
+
continue;
|
|
179
|
+
let mtime = 0;
|
|
180
|
+
try {
|
|
181
|
+
mtime = statSync(join(root, entry)).mtimeMs;
|
|
182
|
+
}
|
|
183
|
+
catch { /* keep 0 */ }
|
|
184
|
+
matches.push({ nm, mtime });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
188
|
+
return matches.length > 0 ? matches[0].nm : undefined;
|
|
189
|
+
}
|
|
82
190
|
function resolveHarnessModule(harnessNodeModules, specifier) {
|
|
83
191
|
const segments = specifier.split("/");
|
|
84
192
|
const scope = segments[0].startsWith("@") ? segments[0] + "/" + segments[1] : segments[0];
|
|
@@ -152,15 +260,66 @@ async function collectSourceFiles(rootDir) {
|
|
|
152
260
|
return out;
|
|
153
261
|
}
|
|
154
262
|
// ── matrix ──
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
263
|
+
/**
|
|
264
|
+
* The shipped default matrix (deepseek models) copied to
|
|
265
|
+
* <DSH_HOME>/omd-matrix.json on first run. The repo and the npm package
|
|
266
|
+
* ship ONLY this defaults file — personal model settings live exclusively
|
|
267
|
+
* in the user's <DSH_HOME>/omd-matrix.json and are never uploaded.
|
|
268
|
+
*/
|
|
269
|
+
const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
|
|
270
|
+
/** Parse matrix text; undefined when it is not a usable matrix document. */
|
|
271
|
+
function parseMatrix(text) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(text);
|
|
274
|
+
if (parsed !== null && typeof parsed === "object" && parsed.modes !== null && typeof parsed.modes === "object")
|
|
275
|
+
return parsed;
|
|
276
|
+
}
|
|
277
|
+
catch { /* fall through */ }
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
/** Read the shipped default matrix, failing loud when the package is broken. */
|
|
281
|
+
function readDefaultMatrix() {
|
|
282
|
+
if (!existsSync(DEFAULT_MATRIX_PATH))
|
|
283
|
+
throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
|
|
284
|
+
const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
|
|
285
|
+
if (parsed === undefined)
|
|
286
|
+
throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
/** Write a matrix file, creating its parent directory when needed. */
|
|
290
|
+
function writeMatrixFile(path, text) {
|
|
291
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
292
|
+
writeFileSync(path, text, "utf8");
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Load the user's model matrix from <DSH_HOME>/omd-matrix.json, creating it
|
|
296
|
+
* on first run: a still-valid package-root matrix (previous versions) is
|
|
297
|
+
* migrated over, otherwise the shipped deepseek default matrix is written.
|
|
298
|
+
* Dry runs never touch the filesystem and use the shipped defaults in memory.
|
|
299
|
+
*/
|
|
300
|
+
function loadMatrix(flags) {
|
|
301
|
+
if (!existsSync(MATRIX_PATH)) {
|
|
302
|
+
if (flags.dryRun)
|
|
303
|
+
return JSON.parse(JSON.stringify(readDefaultMatrix()));
|
|
304
|
+
const legacyText = existsSync(LEGACY_MATRIX_PATH) ? readFileSync(LEGACY_MATRIX_PATH, "utf8") : undefined;
|
|
305
|
+
if (legacyText !== undefined && parseMatrix(legacyText) !== undefined) {
|
|
306
|
+
writeMatrixFile(MATRIX_PATH, legacyText);
|
|
307
|
+
console.log("omd-dsh: migrated omd-matrix.json: " + LEGACY_MATRIX_PATH + " -> " + MATRIX_PATH);
|
|
308
|
+
console.log("omd-dsh: customize the model matrix any time with `omd-dsh setup`.");
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const defaults = readDefaultMatrix();
|
|
312
|
+
writeMatrixFile(MATRIX_PATH, JSON.stringify(defaults, null, 2) + "\n");
|
|
313
|
+
console.log("omd-dsh: generated " + MATRIX_PATH + " from the shipped deepseek default matrix; customize it any time with `omd-dsh setup`.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const text = readFileSync(MATRIX_PATH, "utf8");
|
|
317
|
+
const parsed = parseMatrix(text);
|
|
318
|
+
if (parsed === undefined)
|
|
319
|
+
throw new Error("omd-dsh: malformed " + MATRIX_PATH + " (restore it, or delete it and run omd-dsh setup)");
|
|
161
320
|
return parsed;
|
|
162
321
|
}
|
|
163
|
-
function saveMatrix(m) {
|
|
322
|
+
function saveMatrix(m) { writeMatrixFile(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n"); }
|
|
164
323
|
// ── row rendering (relative indents; the splice prepends the fence indent) ──
|
|
165
324
|
function q(s) { return JSON.stringify(s); }
|
|
166
325
|
function renderModeRow(modeId, cfg) {
|
|
@@ -271,14 +430,14 @@ function discoverModels() {
|
|
|
271
430
|
}
|
|
272
431
|
// ── sync ──
|
|
273
432
|
async function runSync(flags, harnessNodeModules) {
|
|
274
|
-
const matrix = loadMatrix();
|
|
433
|
+
const matrix = loadMatrix(flags);
|
|
275
434
|
const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
276
435
|
const sourceVersion = manifest.version;
|
|
277
436
|
const presetsSourceDir = join(PACKAGE_ROOT, "presets");
|
|
278
437
|
const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
|
|
279
438
|
const agentPresetsRoot = join(dshHome(), ".agent-presets");
|
|
280
439
|
const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
281
|
-
const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [] };
|
|
440
|
+
const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [], removed: [] };
|
|
282
441
|
const log = (kind, text) => report[kind].push(text);
|
|
283
442
|
for (const presetName of presetNames) {
|
|
284
443
|
const modeId = presetName.replace(/^omd-/, "");
|
|
@@ -389,19 +548,67 @@ async function runSync(flags, harnessNodeModules) {
|
|
|
389
548
|
continue;
|
|
390
549
|
if (presetNames.includes(entry.name))
|
|
391
550
|
continue;
|
|
392
|
-
|
|
551
|
+
const orphanDir = join(agentPresetsRoot, entry.name);
|
|
552
|
+
const meta = readMeta(orphanDir);
|
|
553
|
+
if (meta === undefined)
|
|
554
|
+
continue;
|
|
555
|
+
const renamedTo = RENAMED_FROM[entry.name];
|
|
556
|
+
if (renamedTo !== undefined && presetNames.includes(renamedTo)) {
|
|
557
|
+
const dirty = await locallyModified(orphanDir, meta);
|
|
558
|
+
if (dirty === undefined) {
|
|
559
|
+
if (!flags.dryRun)
|
|
560
|
+
await fs.rm(orphanDir, { recursive: true, force: true });
|
|
561
|
+
log("removed", entry.name + "/ (renamed to " + renamedTo + " and unmodified -- removed" + (flags.dryRun ? ", dry-run" : "") + ")");
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
log("conflicts", entry.name + "/ (renamed to " + renamedTo + " but locally modified -- keeping your version: " + dirty + ")");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
393
568
|
log("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
|
|
569
|
+
}
|
|
394
570
|
}
|
|
395
571
|
}
|
|
396
572
|
console.log("omd-dsh sync: DSH_HOME=" + dshHome());
|
|
573
|
+
console.log("omd-dsh sync: matrix=" + MATRIX_PATH + " (customize the model matrix any time with `omd-dsh setup`)");
|
|
397
574
|
console.log("omd-dsh sync: harness node_modules=" + harnessNodeModules);
|
|
398
575
|
console.log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
|
|
399
|
-
for (const key of ["synced", "updated", "skipped", "conflicts", "orphan"])
|
|
576
|
+
for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
|
|
400
577
|
for (const line of report[key])
|
|
401
578
|
console.log(" [" + key + "] " + line);
|
|
402
|
-
const summary = ["synced", "updated", "conflicts", "orphan"].map((key) => report[key].length + " " + key).join(", ");
|
|
579
|
+
const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
|
|
403
580
|
console.log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
|
|
404
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Whether one omd-dsh-managed preset directory differs from the hashes its
|
|
584
|
+
* .omd-meta.json recorded. Returns a description of the first discrepancy,
|
|
585
|
+
* or undefined when every recorded file is present and unmodified and no
|
|
586
|
+
* extra files exist.
|
|
587
|
+
*/
|
|
588
|
+
async function locallyModified(dir, meta) {
|
|
589
|
+
const recorded = meta.files ?? {};
|
|
590
|
+
const current = {};
|
|
591
|
+
const walk = async (d) => {
|
|
592
|
+
for (const entry of await fs.readdir(d, { withFileTypes: true })) {
|
|
593
|
+
const full = join(d, entry.name);
|
|
594
|
+
if (entry.isDirectory())
|
|
595
|
+
await walk(full);
|
|
596
|
+
else if (entry.name !== ".omd-meta.json") {
|
|
597
|
+
const rel = relative(dir, full).split("\\").join("/");
|
|
598
|
+
current[rel] = sha256(await fs.readFile(full, "utf8"));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
await walk(dir);
|
|
603
|
+
for (const rel of new Set([...Object.keys(recorded), ...Object.keys(current)])) {
|
|
604
|
+
if (current[rel] === undefined)
|
|
605
|
+
return "missing file " + rel;
|
|
606
|
+
const recordedHash = recorded[rel] !== undefined && typeof recorded[rel] === "object" && recorded[rel] !== null ? recorded[rel].sha256 : undefined;
|
|
607
|
+
if (typeof recordedHash !== "string" || recordedHash !== current[rel])
|
|
608
|
+
return "modified file " + rel;
|
|
609
|
+
}
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
405
612
|
// ── setup (interactive) ──
|
|
406
613
|
function splitModel(answer) {
|
|
407
614
|
const a = answer.trim();
|
|
@@ -412,7 +619,7 @@ function splitModel(answer) {
|
|
|
412
619
|
return { provider: "deepseek-official", model: a };
|
|
413
620
|
}
|
|
414
621
|
async function runSetup(flags, harnessNodeModules) {
|
|
415
|
-
const matrix = loadMatrix();
|
|
622
|
+
const matrix = loadMatrix(flags);
|
|
416
623
|
const { models, currentDefault } = discoverModels();
|
|
417
624
|
console.log("omd-dsh setup: 发现 DSH 已有模型:");
|
|
418
625
|
for (const m of models)
|
|
@@ -422,7 +629,7 @@ async function runSetup(flags, harnessNodeModules) {
|
|
|
422
629
|
console.log("");
|
|
423
630
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
424
631
|
const ask = async (question) => (await rl.question(question)).trim();
|
|
425
|
-
const modeOrder = ["executor", "
|
|
632
|
+
const modeOrder = ["executor", "ultraworker", "planner", "reviewer", "explorer", "librarian", "chat"];
|
|
426
633
|
for (const modeId of modeOrder) {
|
|
427
634
|
const cfg = matrix.modes[modeId] ?? {};
|
|
428
635
|
const cur = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
|
|
@@ -451,7 +658,7 @@ async function runSetup(flags, harnessNodeModules) {
|
|
|
451
658
|
rl.close();
|
|
452
659
|
if (syncNow === "" || /^y/i.test(syncNow)) {
|
|
453
660
|
if (harnessNodeModules === undefined) {
|
|
454
|
-
console.error("omd-dsh setup: 无法定位 DSH harness node_modules
|
|
661
|
+
console.error("omd-dsh setup: 无法定位 DSH harness node_modules,跳过同步。首次请运行 `omd-dsh sync --harness <路径>`(之后会自动缓存,无需重复指定)。");
|
|
455
662
|
}
|
|
456
663
|
else {
|
|
457
664
|
await runSync(flags, harnessNodeModules);
|
|
@@ -501,19 +708,15 @@ async function main() {
|
|
|
501
708
|
return;
|
|
502
709
|
}
|
|
503
710
|
if (command === "setup") {
|
|
504
|
-
|
|
505
|
-
if (harnessNodeModules !== undefined)
|
|
506
|
-
harnessNodeModules = realpathSync(harnessNodeModules);
|
|
507
|
-
await runSetup(flags, harnessNodeModules);
|
|
711
|
+
await runSetup(flags, resolveHarness(flags));
|
|
508
712
|
return;
|
|
509
713
|
}
|
|
510
|
-
|
|
714
|
+
const harnessNodeModules = resolveHarness(flags);
|
|
511
715
|
if (harnessNodeModules === undefined || !existsSync(join(harnessNodeModules, "@deepseek-ai", "dsh-scope", "package.json"))) {
|
|
512
|
-
console.error("omd-dsh sync: cannot locate the DSH harness node_modules.\n - ensure the dsh executable is on PATH, or\n - pass --harness <path to the harness node_modules directory
|
|
716
|
+
console.error("omd-dsh sync: cannot locate the DSH harness node_modules.\n - ensure the dsh executable is on PATH, or\n - pass --harness <path to the harness node_modules directory> (saved for later runs).\n\n" + usage());
|
|
513
717
|
process.exitCode = 2;
|
|
514
718
|
return;
|
|
515
719
|
}
|
|
516
|
-
harnessNodeModules = realpathSync(harnessNodeModules);
|
|
517
720
|
await runSync(flags, harnessNodeModules);
|
|
518
721
|
}
|
|
519
722
|
main().catch((error) => {
|
package/lib/mode.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @carljia/omd-dsh/mode
|
|
3
|
+
*
|
|
4
|
+
* omd-mode-switch: human-facing `/mode` command -- switch the CURRENT
|
|
5
|
+
* session to another OMD agent preset, including mid-conversation.
|
|
6
|
+
*
|
|
7
|
+
* DSH's native `agentPreset.select` host API refuses to recompose a
|
|
8
|
+
* session that has already started (its preset is fixed at the UI level),
|
|
9
|
+
* and the AgentPresets.recompose method itself performs no history check
|
|
10
|
+
* ("the CALLER owns that check"). This row deliberately performs the
|
|
11
|
+
* in-session recompose, then keeps the log honest:
|
|
12
|
+
* - appends `agent-preset/selected`, so resume/fork rebuild the same
|
|
13
|
+
* composition ("model-visible <-> logged" rule);
|
|
14
|
+
* - appends `plan/mode { active: false }` when plan mode is still
|
|
15
|
+
* active, since the switch itself is a mode decision;
|
|
16
|
+
* - steers a notice message so the model knows the tool set changed.
|
|
17
|
+
*
|
|
18
|
+
* Mitigations for the swapped tool catalog: the omd-planner catalog is a
|
|
19
|
+
* subset of omd-executor's (the executor preset also mounts the plan-mode
|
|
20
|
+
* row), so logged planner tool calls stay renderable under the executor
|
|
21
|
+
* composition. Switching between other omd presets follows the same rule
|
|
22
|
+
* and the model simply receives the new catalog on the next request.
|
|
23
|
+
*/
|
|
24
|
+
/** Cordis plugin name. */
|
|
25
|
+
declare const name = "omd-mode-switch";
|
|
26
|
+
/**
|
|
27
|
+
* No mount-time injection: the roster service is resolved at runtime so a
|
|
28
|
+
* rosterless deployment fails only the /mode command, never the preset
|
|
29
|
+
* mount itself.
|
|
30
|
+
*/
|
|
31
|
+
declare const inject: never[];
|
|
32
|
+
declare function apply(ctx: any): void;
|
|
33
|
+
export { apply, inject, name };
|
package/lib/mode.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
3
|
+
/**
|
|
4
|
+
* @module @carljia/omd-dsh/mode
|
|
5
|
+
*
|
|
6
|
+
* omd-mode-switch: human-facing `/mode` command -- switch the CURRENT
|
|
7
|
+
* session to another OMD agent preset, including mid-conversation.
|
|
8
|
+
*
|
|
9
|
+
* DSH's native `agentPreset.select` host API refuses to recompose a
|
|
10
|
+
* session that has already started (its preset is fixed at the UI level),
|
|
11
|
+
* and the AgentPresets.recompose method itself performs no history check
|
|
12
|
+
* ("the CALLER owns that check"). This row deliberately performs the
|
|
13
|
+
* in-session recompose, then keeps the log honest:
|
|
14
|
+
* - appends `agent-preset/selected`, so resume/fork rebuild the same
|
|
15
|
+
* composition ("model-visible <-> logged" rule);
|
|
16
|
+
* - appends `plan/mode { active: false }` when plan mode is still
|
|
17
|
+
* active, since the switch itself is a mode decision;
|
|
18
|
+
* - steers a notice message so the model knows the tool set changed.
|
|
19
|
+
*
|
|
20
|
+
* Mitigations for the swapped tool catalog: the omd-planner catalog is a
|
|
21
|
+
* subset of omd-executor's (the executor preset also mounts the plan-mode
|
|
22
|
+
* row), so logged planner tool calls stay renderable under the executor
|
|
23
|
+
* composition. Switching between other omd presets follows the same rule
|
|
24
|
+
* and the model simply receives the new catalog on the next request.
|
|
25
|
+
*/
|
|
26
|
+
/** Cordis plugin name. */
|
|
27
|
+
const name = "omd-mode-switch";
|
|
28
|
+
/**
|
|
29
|
+
* No mount-time injection: the roster service is resolved at runtime so a
|
|
30
|
+
* rosterless deployment fails only the /mode command, never the preset
|
|
31
|
+
* mount itself.
|
|
32
|
+
*/
|
|
33
|
+
const inject = [];
|
|
34
|
+
/** The OMD presets /mode may switch to. */
|
|
35
|
+
const OMD_PRESET_IDS = [
|
|
36
|
+
"omd-executor",
|
|
37
|
+
"omd-ultraworker",
|
|
38
|
+
"omd-planner",
|
|
39
|
+
"omd-reviewer",
|
|
40
|
+
"omd-explorer",
|
|
41
|
+
"omd-librarian",
|
|
42
|
+
"omd-chat",
|
|
43
|
+
];
|
|
44
|
+
/** Normalize the command input to a valid omd preset id, or undefined. */
|
|
45
|
+
function normalizeTarget(rawInput) {
|
|
46
|
+
const trimmed = String(rawInput).trim().toLowerCase();
|
|
47
|
+
if (trimmed === "")
|
|
48
|
+
return undefined;
|
|
49
|
+
const candidate = trimmed.startsWith("omd-") ? trimmed : "omd-" + trimmed;
|
|
50
|
+
return OMD_PRESET_IDS.includes(candidate) ? candidate : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Fold the session's plan/mode events (last one wins). */
|
|
53
|
+
function planModeActive(events) {
|
|
54
|
+
let active = false;
|
|
55
|
+
for (const event of events ?? []) {
|
|
56
|
+
if (event !== undefined && event.type === "plan/mode") {
|
|
57
|
+
active = event.data !== undefined && event.data !== null && event.data.active === true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return active;
|
|
61
|
+
}
|
|
62
|
+
/** Execute one /mode invocation through the roster service. */
|
|
63
|
+
async function executeSwitch(ctx, invocation) {
|
|
64
|
+
const agent = invocation.agent;
|
|
65
|
+
const target = normalizeTarget(invocation.rawInput);
|
|
66
|
+
if (target === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "error",
|
|
69
|
+
text: "Usage: /mode <preset> — valid: " + OMD_PRESET_IDS.join(", "),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let presets;
|
|
73
|
+
try {
|
|
74
|
+
presets = ctx.get("agentPresets");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
presets = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (presets === undefined || presets === null) {
|
|
80
|
+
return {
|
|
81
|
+
kind: "error",
|
|
82
|
+
text: "/mode is unavailable: this deployment composes no agent presets.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let current;
|
|
86
|
+
try {
|
|
87
|
+
current = presets.composedPreset(agent.ctx);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
current = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (current === target) {
|
|
93
|
+
return { kind: "success", text: "Already running " + target + "." };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const preset = await presets.recompose(agent.ctx, target);
|
|
97
|
+
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
98
|
+
if (planModeActive(agent.session.events)) {
|
|
99
|
+
agent.session.append("plan/mode", { active: false });
|
|
100
|
+
}
|
|
101
|
+
agent.steer(createUserMessage({
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: "The session switched to the " +
|
|
106
|
+
preset.id +
|
|
107
|
+
" agent preset. Continue in this mode with its tool set, persona, and model routing.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
source: {
|
|
111
|
+
kind: "plugin",
|
|
112
|
+
plugin: "omd-mode-switch",
|
|
113
|
+
form: "notice",
|
|
114
|
+
summary: "Session mode switched to " + preset.id,
|
|
115
|
+
},
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
kind: "success",
|
|
119
|
+
text: "Session preset switched to " + preset.id + " — the next turn runs with that mode's tools and model.",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "error",
|
|
125
|
+
text: "/mode failed: " + (error instanceof Error ? error.message : String(error)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function apply(ctx) {
|
|
130
|
+
if (scopeOf(ctx) === undefined) {
|
|
131
|
+
throw new Error("omd-mode-switch: refusing to mount outside a scoped context; mount this row inside an agent preset");
|
|
132
|
+
}
|
|
133
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
134
|
+
commandCtx.commands.register({
|
|
135
|
+
name: "mode",
|
|
136
|
+
description: "switch this session to another omd agent preset (tool set + model)",
|
|
137
|
+
input: {
|
|
138
|
+
hint: "<omd-* preset id>",
|
|
139
|
+
images: false,
|
|
140
|
+
},
|
|
141
|
+
handler: (invocation) => executeSwitch(ctx, invocation),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export { apply, inject, name };
|
package/lib/plan.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @carljia/omd-dsh/plan
|
|
3
|
+
*
|
|
4
|
+
* omd-plan: plan persistence for the OMD planner mode. It wraps the
|
|
5
|
+
* `tools/post-execute` waterfall and intercepts a successful
|
|
6
|
+
* `exit_plan_mode` approval: the approved plan text is written into the
|
|
7
|
+
* workspace's plan directory (a fixed, code-level convention -- never
|
|
8
|
+
* mentioned in any persona/prompt text), and the tool result content is
|
|
9
|
+
* enriched with the saved file name so the planner's fixed Start Work
|
|
10
|
+
* final step can hand it to the user.
|
|
11
|
+
*
|
|
12
|
+
* Plan directory convention (hardcoded here and in omd-start-work only):
|
|
13
|
+
* <session cwd>/.omd/plans/<slug>-<timestamp>.md
|
|
14
|
+
* The slug derives from the plan's first markdown heading; a timestamp
|
|
15
|
+
* suffix keeps repeated interviews from overwriting each other.
|
|
16
|
+
*/
|
|
17
|
+
/** Cordis plugin name. */
|
|
18
|
+
declare const name = "omd-plan";
|
|
19
|
+
/** No service injection: this row only registers a scoped event listener. */
|
|
20
|
+
declare const inject: never[];
|
|
21
|
+
declare function apply(ctx: any): void;
|
|
22
|
+
export { apply, inject, name };
|