@carljia/omd-dsh 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carl Jia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @carljia/omd-dsh
2
+
3
+ OMD 理念的 DSH 插件:两个 cordis 行 + 7 个模式 preset + 同步 CLI。总览、模式矩阵与安装见仓库根 [README.md](../../README.md)。
4
+
5
+ ## 行:omd-mode
6
+
7
+ 按模式(agent preset)固定模型路由。仅限 preset 作用域内挂载(无作用域挂载直接报错)。
8
+
9
+ | 配置字段 | 含义 |
10
+ |---|---|
11
+ | mode | 模式标识(横幅/诊断语义;不影响路由),必填 |
12
+ | provider | 该模式的 provider;与 model 同时缺省时不固定路由(透传) |
13
+ | model | 该模式的模型;与 provider 同时缺省时不固定路由(透传) |
14
+ | reasoningEffort | 可选推理强度;缺省时清除继承值,交由 provider 默认 |
15
+
16
+ 机制:监听 system-prompt/assemble(注入 {{provider}}/{{model}} 提示词变量)与 agent/request(next() 后覆盖路由),两处均 prepend,保证覆盖入口的会话级选择。
17
+
18
+ 模型体验:persona 文本每次请求固定携带该模式模型声明;模式模型路由在 agent 生命周期内稳定(KV 前缀稳定)。
19
+
20
+ ## 行:omd-task
21
+
22
+ tier 差异化子代理委派(等价 OMD 的 task(category=…))。仅限 preset 作用域内挂载。
23
+
24
+ | 配置字段 | 默认 | 含义 |
25
+ |---|---|---|
26
+ | provider | spawn | 子代理提供方 |
27
+ | toolName | omd_task | 模型可见工具名(同 preset 内唯一) |
28
+ | backgroundMode | continuable | continuable(默认后台,run_in_background:false 转前台)或 foreground |
29
+ | tiers | 必填 | tier 名 → { provider(必), model(必), hint?, persona?, toolFilter?: {allow?, deny?}, maxTokens? };tier 名匹配 [a-z0-9][a-z0-9_-]* |
30
+ | defaultTier | 无 | 模型未传 tier 时的默认档(单 tier 时自动) |
31
+ | maxDepth | 3 | 委派深度上限;provider-managed 表示交提供方 |
32
+
33
+ 模型可见参数:description(展示名)、prompt、tier(枚举见工具描述)、run_in_background(仅 continuable 模式)。工具描述枚举各 tier 的 hint 与模型,模型据此选型。
34
+
35
+ ## CLI:omd-dsh
36
+
37
+ ```
38
+ omd-dsh <command> [--harness <路径>] [--dry-run]
39
+ ```
40
+
41
+ - `omd-dsh sync` — 把 presets/ 与 vendored 行模块同步到 `<DSH_HOME>/.agent-presets/`(hash 保护、orphan 报告、非管理目录零操作)。每个 preset 的 omd-mode / omd-task 行由 `omd-matrix.json` 渲染。
42
+ - `omd-dsh setup` — 交互式向导:先读取 DSH 已有模型,再引导逐模式/逐 tier 选择模型,写回 `omd-matrix.json` 并可选立即同步。
43
+ - `omd-dsh models` — 打印发现的 DSH 模型目录(非交互)。
44
+
45
+ 细节见 docs/ARCHITECTURE.md 的「vendored 分发与跨树符号风险」。
46
+
47
+ ## 集中配置:omd-matrix.json
48
+
49
+ 所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier 的模型集中在一个 `omd-matrix.json` 里:`omd-dsh setup` 生成/更新它,`omd-dsh sync` 读取它并渲染进各 preset。preset 里 `# [omd-dsh:mode:start] / [omd-dsh:mode:end]` 与 `# [omd-dsh:task:start] / [omd-dsh:task:end]` 之间的区域是自动生成的——改模型请改矩阵后跑 sync,不要手改 fence 之间的内容。
50
+
51
+ ## 开发
52
+
53
+ ```bash
54
+ npm install # 触发 build(tsc + postbuild)
55
+ npm test # vitest,39 个测试
56
+ npm pack # prepack 自动重建,产出 tgz
57
+ ```
58
+
59
+ 测试桩在 test/stubs/(dsh-scope/dsh-tools/dsh-subagent 的轻量替身),不依赖真实 harness。
60
+
61
+
62
+ ### 子代理透传语义
63
+
64
+ omd-mode 对 subagentDepth > 0 的子代理一律透传(不覆盖 provider/model/变量):
65
+ omd-task 的 tier 模型通过显式 agentOptions 生效;顶层 agent 才被钉到模式模型。
66
+ 这保证「强模型顶层 + tier 差异化子代理」的优先级正确。
67
+
68
+ ### toolFilter 注意
69
+
70
+ tiers 里的 toolFilter.deny/allow 只能点名该 preset 工具层中真实存在的工具名,
71
+ 否则子代理创建会被 DSH 以工具层错误拒绝。只读模式的 tiers 建议不配 toolFilter。
72
+
package/lib/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/lib/cli.js ADDED
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs, existsSync, realpathSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { dirname, join, relative, resolve, basename } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { execFileSync } from "node:child_process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { createInterface } from "node:readline/promises";
9
+ /**
10
+ * omd-dsh CLI
11
+ *
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 (the single source for model routing and
15
+ * tier delegation).
16
+ * omd-dsh setup interactive wizard: discover the models DSH already has,
17
+ * then guide per-mode and per-tier model selection.
18
+ * omd-dsh models print the discovered model catalog (non-interactive).
19
+ *
20
+ * Distribution model (vendored + harness-anchored imports) is unchanged from
21
+ * the original omd-dsh sync: presets/omd-* are copied into .agent-presets/;
22
+ * their omd-mode/omd-task rows reference ../.omd-vendor/omd-mode.mjs /
23
+ * omd-task.mjs by relative path; the vendored modules are copied into
24
+ * .agent-presets/.omd-vendor/ with bare @deepseek-ai/* imports rewritten to
25
+ * absolute file:// URLs into the harness node_modules tree.
26
+ */
27
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
+ const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs"];
29
+ const MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.json");
30
+ const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
31
+ const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]" };
32
+ function usage() {
33
+ return [
34
+ "omd-dsh <command> [options]",
35
+ " commands:",
36
+ " sync materialize presets into <DSH_HOME>/.agent-presets (renders omd-mode/omd-task from omd-matrix.json)",
37
+ " setup interactive: discover DSH models, then guide per-mode/tier model selection",
38
+ " models print the discovered DSH model catalog",
39
+ " options:",
40
+ " --harness <path> node_modules dir of the DSH harness install (auto-detected via the dsh executable)",
41
+ " --dry-run (sync) print planned writes without touching the filesystem",
42
+ " --verbose (sync) print per-file detail",
43
+ ].join("\n");
44
+ }
45
+ function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
46
+ function dshHome() { return process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== "" ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh"); }
47
+ function findNodeModules(start) {
48
+ let current = resolve(start);
49
+ for (;;) {
50
+ if (basename(current) === "node_modules")
51
+ return current;
52
+ const parent = dirname(current);
53
+ if (parent === current)
54
+ return undefined;
55
+ current = parent;
56
+ }
57
+ }
58
+ function locateHarnessViaDsh() {
59
+ const candidates = [];
60
+ try {
61
+ const probe = process.platform === "win32" ? "where.exe" : "which";
62
+ const out = execFileSync(probe, ["dsh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
63
+ for (const line of out.split(/\r?\n/)) {
64
+ const t = line.trim();
65
+ if (t !== "")
66
+ candidates.push(t);
67
+ }
68
+ }
69
+ catch { /* dsh not on PATH */ }
70
+ for (const candidate of candidates) {
71
+ let real = candidate;
72
+ try {
73
+ real = realpathSync(candidate);
74
+ }
75
+ catch { /* keep */ }
76
+ const nm = findNodeModules(real);
77
+ if (nm !== undefined && existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
78
+ return nm;
79
+ }
80
+ return undefined;
81
+ }
82
+ function resolveHarnessModule(harnessNodeModules, specifier) {
83
+ const segments = specifier.split("/");
84
+ const scope = segments[0].startsWith("@") ? segments[0] + "/" + segments[1] : segments[0];
85
+ const subpath = scope === specifier ? "" : specifier.slice(scope.length + 1);
86
+ const pkgDir = join(harnessNodeModules, ...scope.split("/"));
87
+ const manifestPath = join(pkgDir, "package.json");
88
+ if (!existsSync(manifestPath))
89
+ throw new Error("omd-dsh: cannot resolve \"" + specifier + "\" -- no package.json at " + manifestPath);
90
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
91
+ let entry;
92
+ const exportsMap = manifest.exports;
93
+ if (subpath === "" && exportsMap !== undefined && exportsMap["."] !== undefined) {
94
+ const dot = exportsMap["."];
95
+ if (typeof dot === "string")
96
+ entry = dot;
97
+ else if (typeof dot === "object" && dot !== null) {
98
+ entry = dot.node ?? dot.import ?? dot.default;
99
+ if (typeof entry === "object" && entry !== null)
100
+ entry = entry.node ?? entry.import ?? entry.default;
101
+ }
102
+ }
103
+ if (entry === undefined && subpath === "")
104
+ entry = manifest.module ?? manifest.main;
105
+ if (entry === undefined)
106
+ entry = subpath === "" ? "index.js" : subpath;
107
+ else if (subpath !== "")
108
+ entry = join(entry, subpath);
109
+ let resolvedPath = resolve(pkgDir, entry);
110
+ try {
111
+ resolvedPath = realpathSync(resolvedPath);
112
+ }
113
+ catch { /* keep */ }
114
+ if (!existsSync(resolvedPath))
115
+ throw new Error("omd-dsh: resolved entry \"" + entry + "\" for \"" + specifier + "\" does not exist at " + resolvedPath);
116
+ return pathToFileURL(resolvedPath).href;
117
+ }
118
+ function rewriteImports(sourceText, harnessNodeModules) {
119
+ const specifierPattern = /@deepseek-ai\/[A-Za-z0-9@._/-]+/g;
120
+ return sourceText.split(/\r?\n/).map((line) => {
121
+ if (!line.trimStart().startsWith("import"))
122
+ return line;
123
+ return line.replace(specifierPattern, (s) => resolveHarnessModule(harnessNodeModules, s));
124
+ }).join("\n");
125
+ }
126
+ function readMeta(dir) {
127
+ const metaPath = join(dir, ".omd-meta.json");
128
+ if (!existsSync(metaPath))
129
+ return undefined;
130
+ try {
131
+ const p = JSON.parse(readFileSync(metaPath, "utf8"));
132
+ if (p !== null && typeof p === "object" && p.files !== null && typeof p.files === "object")
133
+ return p;
134
+ return undefined;
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
140
+ async function collectSourceFiles(rootDir) {
141
+ const out = [];
142
+ const walk = async (dir) => {
143
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
144
+ const full = join(dir, entry.name);
145
+ if (entry.isDirectory())
146
+ await walk(full);
147
+ else
148
+ out.push(full);
149
+ }
150
+ };
151
+ await walk(rootDir);
152
+ return out;
153
+ }
154
+ // ── matrix ──
155
+ function loadMatrix() {
156
+ if (!existsSync(MATRIX_PATH))
157
+ throw new Error("omd-dsh: missing " + MATRIX_PATH + " (run `omd-dsh setup` to create it, or restore it from source control)");
158
+ const parsed = JSON.parse(readFileSync(MATRIX_PATH, "utf8"));
159
+ if (parsed === null || typeof parsed !== "object" || parsed.modes === null || typeof parsed.modes !== "object")
160
+ throw new Error("omd-dsh: malformed " + MATRIX_PATH);
161
+ return parsed;
162
+ }
163
+ function saveMatrix(m) { writeFileSync(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n", "utf8"); }
164
+ // ── row rendering (relative indents; the splice prepends the fence indent) ──
165
+ function q(s) { return JSON.stringify(s); }
166
+ function renderModeRow(modeId, cfg) {
167
+ const out = ["- id: omd-mode", " name: '../.omd-vendor/omd-mode.mjs'", " config:", " mode: " + modeId];
168
+ if (cfg.provider !== undefined)
169
+ out.push(" provider: " + cfg.provider);
170
+ if (cfg.model !== undefined)
171
+ out.push(" model: " + cfg.model);
172
+ if (cfg.reasoningEffort !== undefined)
173
+ out.push(" reasoningEffort: " + cfg.reasoningEffort);
174
+ return out;
175
+ }
176
+ function renderToolFilter(tf) {
177
+ const deny = tf.deny ?? [];
178
+ const allow = tf.allow ?? [];
179
+ const out = [];
180
+ if (tf.denyShell === true) {
181
+ const arr = (names) => "[" + names.map((n) => "'" + n + "'").join(", ") + "]";
182
+ const expr = "(process.platform === 'win32') ? " + arr([...deny, "pwsh"]) + " : " + arr([...deny, "bash"]);
183
+ out.push("toolFilter:", " deny: !!js " + q(expr));
184
+ }
185
+ else {
186
+ if (allow.length > 0)
187
+ out.push("toolFilter:", " allow: [" + allow.join(", ") + "]");
188
+ if (deny.length > 0) {
189
+ if (out.length === 0)
190
+ out.push("toolFilter:");
191
+ out.push(" deny: [" + deny.join(", ") + "]");
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ function renderTaskRow(cfg) {
197
+ const tiers = cfg.tiers ?? {};
198
+ if (Object.keys(tiers).length === 0)
199
+ return [];
200
+ const out = ["- id: omd-task", " name: '../.omd-vendor/omd-task.mjs'", " config:", " provider: spawn", " toolName: omd_task", " backgroundMode: continuable", " tiers:"];
201
+ for (const [name, t] of Object.entries(tiers)) {
202
+ out.push(" " + name + ":");
203
+ out.push(" provider: " + t.provider);
204
+ out.push(" model: " + t.model);
205
+ if (t.hint !== undefined)
206
+ out.push(" hint: " + q(t.hint));
207
+ if (t.persona !== undefined)
208
+ out.push(" persona: " + q(t.persona));
209
+ if (t.maxTokens !== undefined)
210
+ out.push(" maxTokens: " + t.maxTokens);
211
+ if (t.toolFilter !== undefined)
212
+ for (const l of renderToolFilter(t.toolFilter))
213
+ out.push(" " + l);
214
+ }
215
+ return out;
216
+ }
217
+ function spliceFence(lines, fence, rendered) {
218
+ const start = lines.findIndex((l) => l.trim() === fence.start);
219
+ const end = lines.findIndex((l) => l.trim() === fence.end);
220
+ if (start === -1 || end === -1 || end < start)
221
+ throw new Error("omd-dsh: preset is missing the " + fence.start + " / " + fence.end + " markers; regenerate the preset from source");
222
+ const indent = (lines[start].match(/^ */) || [""])[0];
223
+ const renderedIndented = rendered.map((l) => indent + l);
224
+ lines.splice(start, end - start + 1, indent + fence.start, ...renderedIndented, indent + fence.end);
225
+ }
226
+ function applyMatrix(text, modeId, cfg) {
227
+ const lines = text.split("\n");
228
+ spliceFence(lines, MODE_FENCE, renderModeRow(modeId, cfg));
229
+ const taskRendered = renderTaskRow(cfg);
230
+ const hasTaskFence = lines.some((l) => l.trim() === TASK_FENCE.start);
231
+ if (taskRendered.length > 0 && !hasTaskFence) {
232
+ throw new Error("omd-dsh: mode \"" + modeId + "\" has tiers in omd-matrix.json but its preset is missing the task fence");
233
+ }
234
+ if (hasTaskFence)
235
+ spliceFence(lines, TASK_FENCE, taskRendered);
236
+ return lines.join("\n");
237
+ }
238
+ // ── model discovery ──
239
+ function discoverModels() {
240
+ const models = [];
241
+ const seen = new Set();
242
+ const add = (provider, model) => { if (model && !seen.has(provider + "/" + model)) {
243
+ seen.add(provider + "/" + model);
244
+ models.push({ provider, model });
245
+ } };
246
+ // DSH official DeepSeek defaults (dsh-llm-deepseek)
247
+ add("deepseek-official", "deepseek-v4-flash");
248
+ add("deepseek-official", "deepseek-v4-pro");
249
+ add("deepseek-official", "deepseek-v4-flash-vision-exp");
250
+ // best-effort: scan settings.yaml for additional model ids
251
+ const settingsPath = join(dshHome(), "settings.yaml");
252
+ let currentDefault;
253
+ if (existsSync(settingsPath)) {
254
+ const text = readFileSync(settingsPath, "utf8");
255
+ const blocklist = new Set(["spawn", "fork", "continuable", "foreground", "deepseek-official", "openai-completions", "anthropic-messages", "enabled", "disabled", "high", "low", "max"]);
256
+ for (const m of text.matchAll(/(?<![-A-Za-z0-9])model:\s*['"]?([A-Za-z0-9][A-Za-z0-9._/-]*)/g)) {
257
+ const id = m[1];
258
+ if (!blocklist.has(id) && /[0-9]/.test(id))
259
+ add("deepseek-official", id);
260
+ }
261
+ for (const m of text.matchAll(/^\s*-\s+id:\s*['"]?([A-Za-z0-9][A-Za-z0-9._/-]*)/gm)) {
262
+ const id = m[1];
263
+ if (!blocklist.has(id) && /[0-9]/.test(id))
264
+ add("deepseek-official", id);
265
+ }
266
+ const dm = text.match(/agent-default-model:\s*\n\s*provider:\s*['"]?([A-Za-z0-9._/-]+)['"]?\s*\n\s*model:\s*['"]?([A-Za-z0-9._/-]+)['"]?/);
267
+ if (dm)
268
+ currentDefault = { provider: dm[1], model: dm[2] };
269
+ }
270
+ return { models, currentDefault };
271
+ }
272
+ // ── sync ──
273
+ async function runSync(flags, harnessNodeModules) {
274
+ const matrix = loadMatrix();
275
+ const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
276
+ const sourceVersion = manifest.version;
277
+ const presetsSourceDir = join(PACKAGE_ROOT, "presets");
278
+ const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
279
+ const agentPresetsRoot = join(dshHome(), ".agent-presets");
280
+ const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
281
+ const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [] };
282
+ const log = (kind, text) => report[kind].push(text);
283
+ for (const presetName of presetNames) {
284
+ const modeId = presetName.replace(/^omd-/, "");
285
+ const cfg = matrix.modes[modeId];
286
+ if (cfg === undefined)
287
+ throw new Error("omd-dsh sync: omd-matrix.json has no entry for mode \"" + modeId + "\" (preset " + presetName + ")");
288
+ const sourceDir = join(presetsSourceDir, presetName);
289
+ const targetDir = join(agentPresetsRoot, presetName);
290
+ const sourceFiles = await collectSourceFiles(sourceDir);
291
+ const existingMeta = existsSync(targetDir) ? readMeta(targetDir) : undefined;
292
+ if (existsSync(targetDir) && existingMeta === undefined) {
293
+ log("skipped", presetName + "/ (directory exists but is not managed by omd-dsh -- left untouched)");
294
+ continue;
295
+ }
296
+ const nextFiles = {};
297
+ for (const sourceFile of sourceFiles) {
298
+ const rel = relative(sourceDir, sourceFile).split("\\").join("/");
299
+ let sourceText = await fs.readFile(sourceFile, "utf8");
300
+ if (rel === "agent.cordis.yml")
301
+ sourceText = applyMatrix(sourceText, modeId, cfg);
302
+ const sourceHash = sha256(sourceText);
303
+ const destFile = join(targetDir, rel);
304
+ let action = "synced";
305
+ if (existsSync(destFile)) {
306
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
307
+ const prevHash = existingMeta?.files?.[rel]?.sha256;
308
+ if (destHash === sourceHash)
309
+ action = "up-to-date";
310
+ else if (prevHash !== undefined && destHash === prevHash)
311
+ action = "updated";
312
+ else
313
+ action = "conflict";
314
+ }
315
+ if (action === "conflict") {
316
+ log("conflicts", presetName + "/" + rel + " (locally modified -- keeping your version)");
317
+ if (existingMeta?.files?.[rel]?.sha256 !== undefined)
318
+ nextFiles[rel] = { sha256: existingMeta.files[rel].sha256 };
319
+ continue;
320
+ }
321
+ if (action === "up-to-date")
322
+ log("skipped", presetName + "/" + rel + " (up to date)");
323
+ else {
324
+ if (!flags.dryRun) {
325
+ await fs.mkdir(dirname(destFile), { recursive: true });
326
+ await fs.writeFile(destFile, sourceText, "utf8");
327
+ }
328
+ log(action === "synced" ? "synced" : "updated", presetName + "/" + rel + (flags.dryRun ? " (dry-run)" : ""));
329
+ }
330
+ nextFiles[rel] = { sha256: sourceHash };
331
+ }
332
+ if (!flags.dryRun) {
333
+ await fs.mkdir(targetDir, { recursive: true });
334
+ await fs.writeFile(join(targetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextFiles }, null, 2) + "\n", "utf8");
335
+ }
336
+ }
337
+ const vendorTargetDir = join(agentPresetsRoot, ".omd-vendor");
338
+ const vendorMeta = existsSync(vendorTargetDir) ? readMeta(vendorTargetDir) : undefined;
339
+ if (existsSync(vendorTargetDir) && vendorMeta === undefined) {
340
+ log("skipped", ".omd-vendor/ (directory exists but is not managed by omd-dsh -- left untouched)");
341
+ }
342
+ else {
343
+ const nextVendorFiles = {};
344
+ for (const vendorName of VENDOR_SOURCES) {
345
+ const sourceFile = join(vendorSourceDir, vendorName);
346
+ if (!existsSync(sourceFile)) {
347
+ throw new Error("omd-dsh sync: missing vendored source " + sourceFile + " -- run `npm run build` first");
348
+ }
349
+ let sourceText = await fs.readFile(sourceFile, "utf8");
350
+ sourceText = rewriteImports(sourceText, harnessNodeModules);
351
+ const sourceHash = sha256(sourceText);
352
+ const destFile = join(vendorTargetDir, vendorName);
353
+ let action = "synced";
354
+ if (existsSync(destFile)) {
355
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
356
+ const prevHash = vendorMeta?.files?.[vendorName]?.sha256;
357
+ if (destHash === sourceHash)
358
+ action = "up-to-date";
359
+ else if (prevHash !== undefined && destHash === prevHash)
360
+ action = "updated";
361
+ else
362
+ action = "conflict";
363
+ }
364
+ if (action === "conflict") {
365
+ log("conflicts", ".omd-vendor/" + vendorName + " (locally modified -- keeping your version)");
366
+ if (vendorMeta?.files?.[vendorName]?.sha256 !== undefined)
367
+ nextVendorFiles[vendorName] = { sha256: vendorMeta.files[vendorName].sha256 };
368
+ continue;
369
+ }
370
+ if (action === "up-to-date")
371
+ log("skipped", ".omd-vendor/" + vendorName + " (up to date)");
372
+ else {
373
+ if (!flags.dryRun) {
374
+ await fs.mkdir(vendorTargetDir, { recursive: true });
375
+ await fs.writeFile(destFile, sourceText, "utf8");
376
+ }
377
+ log(action === "synced" ? "synced" : "updated", ".omd-vendor/" + vendorName + (flags.dryRun ? " (dry-run)" : ""));
378
+ }
379
+ nextVendorFiles[vendorName] = { sha256: sourceHash };
380
+ }
381
+ if (!flags.dryRun && VENDOR_SOURCES.length > 0) {
382
+ await fs.mkdir(vendorTargetDir, { recursive: true });
383
+ await fs.writeFile(join(vendorTargetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, harnessNodeModules, files: nextVendorFiles }, null, 2) + "\n", "utf8");
384
+ }
385
+ }
386
+ if (existsSync(agentPresetsRoot)) {
387
+ for (const entry of await fs.readdir(agentPresetsRoot, { withFileTypes: true })) {
388
+ if (!entry.isDirectory() || !entry.name.startsWith("omd-"))
389
+ continue;
390
+ if (presetNames.includes(entry.name))
391
+ continue;
392
+ if (readMeta(join(agentPresetsRoot, entry.name)) !== undefined)
393
+ log("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
394
+ }
395
+ }
396
+ console.log("omd-dsh sync: DSH_HOME=" + dshHome());
397
+ console.log("omd-dsh sync: harness node_modules=" + harnessNodeModules);
398
+ console.log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
399
+ for (const key of ["synced", "updated", "skipped", "conflicts", "orphan"])
400
+ for (const line of report[key])
401
+ console.log(" [" + key + "] " + line);
402
+ const summary = ["synced", "updated", "conflicts", "orphan"].map((key) => report[key].length + " " + key).join(", ");
403
+ console.log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
404
+ }
405
+ // ── setup (interactive) ──
406
+ function splitModel(answer) {
407
+ const a = answer.trim();
408
+ if (a.includes("/")) {
409
+ const i = a.indexOf("/");
410
+ return { provider: a.slice(0, i).trim(), model: a.slice(i + 1).trim() };
411
+ }
412
+ return { provider: "deepseek-official", model: a };
413
+ }
414
+ async function runSetup(flags, harnessNodeModules) {
415
+ const matrix = loadMatrix();
416
+ const { models, currentDefault } = discoverModels();
417
+ console.log("omd-dsh setup: 发现 DSH 已有模型:");
418
+ for (const m of models)
419
+ console.log(" - " + m.provider + "/" + m.model);
420
+ if (currentDefault)
421
+ console.log("omd-dsh setup: 当前全局默认 = " + currentDefault.provider + "/" + currentDefault.model);
422
+ console.log("");
423
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
424
+ const ask = async (question) => (await rl.question(question)).trim();
425
+ const modeOrder = ["executor", "architect", "planner", "reviewer", "explorer", "librarian", "chat"];
426
+ for (const modeId of modeOrder) {
427
+ const cfg = matrix.modes[modeId] ?? {};
428
+ const cur = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
429
+ const ans = await ask("模式 " + modeId + " 顶层模型" + (cur ? " [" + cur + "]" : "") + " (回车保留,格式 provider/model): ");
430
+ if (ans !== "") {
431
+ const r = splitModel(ans);
432
+ cfg.provider = r.provider;
433
+ cfg.model = r.model;
434
+ }
435
+ if (cfg.tiers && Object.keys(cfg.tiers).length > 0) {
436
+ for (const [tierName, tier] of Object.entries(cfg.tiers)) {
437
+ const tcur = tier.provider + "/" + tier.model;
438
+ const tans = await ask(" " + modeId + " tier \"" + tierName + "\" 模型 [" + tcur + "] (回车保留): ");
439
+ if (tans !== "") {
440
+ const r = splitModel(tans);
441
+ tier.provider = r.provider;
442
+ tier.model = r.model;
443
+ }
444
+ }
445
+ }
446
+ matrix.modes[modeId] = cfg;
447
+ }
448
+ saveMatrix(matrix);
449
+ console.log("omd-dsh setup: 已写入 " + MATRIX_PATH);
450
+ const syncNow = await ask("是否立即同步到 " + join(dshHome(), ".agent-presets") + "? [Y/n] ");
451
+ rl.close();
452
+ if (syncNow === "" || /^y/i.test(syncNow)) {
453
+ if (harnessNodeModules === undefined) {
454
+ console.error("omd-dsh setup: 无法定位 DSH harness node_modules,跳过同步。稍后运行 `omd-dsh sync --harness <路径>`。");
455
+ }
456
+ else {
457
+ await runSync(flags, harnessNodeModules);
458
+ }
459
+ }
460
+ else {
461
+ console.log("omd-dsh setup: 已跳过同步;稍后运行 `omd-dsh sync` 生效。");
462
+ }
463
+ }
464
+ // ── main ──
465
+ async function main() {
466
+ const argv = process.argv.slice(2);
467
+ const flags = { dryRun: false, verbose: false };
468
+ let command = "";
469
+ for (let i = 0; i < argv.length; i++) {
470
+ const arg = argv[i];
471
+ if (arg === "--harness")
472
+ flags.harness = argv[++i];
473
+ else if (arg === "--dry-run")
474
+ flags.dryRun = true;
475
+ else if (arg === "--verbose")
476
+ flags.verbose = true;
477
+ else if (arg === "--help" || arg === "-h") {
478
+ console.log(usage());
479
+ return;
480
+ }
481
+ else if (arg === "sync" || arg === "setup" || arg === "models")
482
+ command = arg;
483
+ else {
484
+ console.error("unknown argument: " + arg + "\n\n" + usage());
485
+ process.exitCode = 2;
486
+ return;
487
+ }
488
+ }
489
+ if (command === "") {
490
+ console.error("missing command\n\n" + usage());
491
+ process.exitCode = 2;
492
+ return;
493
+ }
494
+ if (command === "models") {
495
+ const { models, currentDefault } = discoverModels();
496
+ console.log("omd-dsh models: DSH_HOME=" + dshHome());
497
+ if (currentDefault)
498
+ console.log("omd-dsh models: 当前全局默认 = " + currentDefault.provider + "/" + currentDefault.model);
499
+ for (const m of models)
500
+ console.log(" " + m.provider + "/" + m.model);
501
+ return;
502
+ }
503
+ if (command === "setup") {
504
+ let harnessNodeModules = flags.harness !== undefined ? findNodeModules(flags.harness) : locateHarnessViaDsh();
505
+ if (harnessNodeModules !== undefined)
506
+ harnessNodeModules = realpathSync(harnessNodeModules);
507
+ await runSetup(flags, harnessNodeModules);
508
+ return;
509
+ }
510
+ let harnessNodeModules = flags.harness !== undefined ? findNodeModules(flags.harness) : locateHarnessViaDsh();
511
+ 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>.\n\n" + usage());
513
+ process.exitCode = 2;
514
+ return;
515
+ }
516
+ harnessNodeModules = realpathSync(harnessNodeModules);
517
+ await runSync(flags, harnessNodeModules);
518
+ }
519
+ main().catch((error) => {
520
+ console.error("omd-dsh failed: " + (error instanceof Error ? error.message : String(error)));
521
+ process.exitCode = 1;
522
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @module @carljia/omd-dsh
3
+ *
4
+ * omd-mode: per-mode (agent preset) model routing row for DeepSeek Harness.
5
+ *
6
+ * Structurally identical to the harness built-in installModelSelection
7
+ * (dsh-agent): it listens on the system-prompt/assemble event to inject
8
+ * the provider/model prompt variables (so a persona can render the
9
+ * model and provider template variables), and overrides provider/model
10
+ * on the agent/request waterfall after next(), dropping any inherited
11
+ * reasoningEffort. Both listeners register with prepend: true so this
12
+ * row sits OUTSIDE the entry point per-session selection listener and
13
+ * its override wins deterministically -- that is the mechanism behind
14
+ * "each mode pins its own model".
15
+ *
16
+ * When provider/model are not configured the row passes everything
17
+ * through and only serves the persona banner variables (inheriting the
18
+ * entry/session selection), so it is safe to mount into any preset.
19
+ */
20
+ /** Cordis plugin name. */
21
+ declare const name = "omd-mode";
22
+ /** No service injection: this row only registers scoped event listeners. */
23
+ declare const inject: never[];
24
+ /** Runtime schema for the omd-mode row. */
25
+ declare const Config: any;
26
+ declare function apply(ctx: any, config: any): void;
27
+ export { Config, apply, inject, name };