@carljia/omd-dsh 0.1.5 → 0.1.8
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 +18 -11
- package/cordis.patch.yml +10 -0
- package/lib/boot.d.ts +21 -0
- package/lib/boot.js +40 -0
- package/lib/cli.js +16 -573
- package/lib/index.d.ts +2 -2
- package/lib/index.js +11 -10
- package/lib/mode.js +1 -4
- package/lib/plan.js +1 -4
- package/lib/shared.d.ts +25 -0
- package/lib/shared.js +28 -0
- package/lib/startwork.js +1 -4
- package/lib/sync.d.ts +53 -0
- package/lib/sync.js +345 -0
- package/lib/task.js +5 -7
- package/lib/vendor/omd-mode-switch.mjs +967 -127
- package/lib/vendor/omd-mode.mjs +889 -172
- package/lib/vendor/omd-plan.mjs +95 -177
- package/lib/vendor/omd-start-work.mjs +96 -132
- package/lib/vendor/omd-task.mjs +20735 -264
- package/lib/vendor/omd-ulw.mjs +44 -49
- package/lib/vendor/shared.js +28 -0
- package/package.json +16 -5
- package/presets/omd-chat/agent.cordis.yml +1 -1
- package/presets/omd-executor/agent.cordis.yml +1 -1
- package/presets/omd-explorer/agent.cordis.yml +1 -1
- package/presets/omd-librarian/agent.cordis.yml +1 -1
- package/presets/omd-planner/agent.cordis.yml +1 -1
- package/presets/omd-reviewer/agent.cordis.yml +1 -1
- package/presets/omd-ultraworker/agent.cordis.yml +1 -1
package/lib/cli.js
CHANGED
|
@@ -1,43 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
8
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { runSync, loadMatrix, saveMatrix, dshHome, MATRIX_PATH, } from "./sync.js";
|
|
9
6
|
/**
|
|
10
7
|
* omd-dsh CLI
|
|
11
8
|
*
|
|
12
9
|
* omd-dsh sync materialize presets into <DSH_HOME>/.agent-presets,
|
|
13
10
|
* rendering each preset's omd-mode / omd-task rows from the
|
|
14
|
-
* user's model matrix at <DSH_HOME>/omd-matrix.json.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* model settings stay on the user's machine and are never
|
|
18
|
-
* shipped or uploaded.
|
|
19
|
-
* omd-dsh setup interactive wizard: discover the models DSH already has,
|
|
20
|
-
* then guide per-mode and per-tier model selection.
|
|
11
|
+
* user's model matrix at <DSH_HOME>/omd-matrix.json.
|
|
12
|
+
* omd-dsh setup interactive wizard: discover DSH models, then guide
|
|
13
|
+
* per-mode and per-tier model selection.
|
|
21
14
|
* omd-dsh models print the discovered model catalog (non-interactive).
|
|
22
15
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* imports rewritten to absolute file:// URLs into the harness node_modules
|
|
29
|
-
* tree.
|
|
16
|
+
* The sync core lives in ./sync.js and is also invoked automatically by the
|
|
17
|
+
* bundle boot row (lib/boot.js), so `dsh plugin add @carljia/omd-dsh` + restart
|
|
18
|
+
* installs the presets without this CLI. The vendored rows are self-contained
|
|
19
|
+
* bundles: no `--harness` flag exists because the sync needs no harness tree
|
|
20
|
+
* knowledge.
|
|
30
21
|
*/
|
|
31
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
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");
|
|
37
|
-
const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
|
|
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" };
|
|
41
22
|
function usage() {
|
|
42
23
|
return [
|
|
43
24
|
"omd-dsh <command> [options]",
|
|
@@ -46,354 +27,10 @@ function usage() {
|
|
|
46
27
|
" setup interactive: discover DSH models, then guide per-mode/tier model selection",
|
|
47
28
|
" models print the discovered DSH model catalog",
|
|
48
29
|
" options:",
|
|
49
|
-
" --harness <path> node_modules dir of the DSH harness install (saved locally after first use; otherwise auto-detected)",
|
|
50
30
|
" --dry-run (sync) print planned writes without touching the filesystem",
|
|
51
31
|
" --verbose (sync) print per-file detail",
|
|
52
32
|
].join("\n");
|
|
53
33
|
}
|
|
54
|
-
function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
|
|
55
|
-
function dshHome() { return process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== "" ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh"); }
|
|
56
|
-
function findNodeModules(start) {
|
|
57
|
-
let current = resolve(start);
|
|
58
|
-
for (;;) {
|
|
59
|
-
if (basename(current) === "node_modules")
|
|
60
|
-
return current;
|
|
61
|
-
const parent = dirname(current);
|
|
62
|
-
if (parent === current)
|
|
63
|
-
return undefined;
|
|
64
|
-
current = parent;
|
|
65
|
-
}
|
|
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
|
-
}
|
|
121
|
-
function locateHarnessViaDsh() {
|
|
122
|
-
const candidates = [];
|
|
123
|
-
try {
|
|
124
|
-
const probe = process.platform === "win32" ? "where.exe" : "which";
|
|
125
|
-
const out = execFileSync(probe, ["dsh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
126
|
-
for (const line of out.split(/\r?\n/)) {
|
|
127
|
-
const t = line.trim();
|
|
128
|
-
if (t !== "")
|
|
129
|
-
candidates.push(t);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch { /* dsh not on PATH */ }
|
|
133
|
-
for (const candidate of candidates) {
|
|
134
|
-
let real = candidate;
|
|
135
|
-
try {
|
|
136
|
-
real = realpathSync(candidate);
|
|
137
|
-
}
|
|
138
|
-
catch { /* keep */ }
|
|
139
|
-
const nm = findNodeModules(real);
|
|
140
|
-
if (nm !== undefined && existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
|
|
141
|
-
return nm;
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
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
|
-
}
|
|
190
|
-
function resolveHarnessModule(harnessNodeModules, specifier) {
|
|
191
|
-
const segments = specifier.split("/");
|
|
192
|
-
const scope = segments[0].startsWith("@") ? segments[0] + "/" + segments[1] : segments[0];
|
|
193
|
-
const subpath = scope === specifier ? "" : specifier.slice(scope.length + 1);
|
|
194
|
-
const pkgDir = join(harnessNodeModules, ...scope.split("/"));
|
|
195
|
-
const manifestPath = join(pkgDir, "package.json");
|
|
196
|
-
if (!existsSync(manifestPath))
|
|
197
|
-
throw new Error("omd-dsh: cannot resolve \"" + specifier + "\" -- no package.json at " + manifestPath);
|
|
198
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
199
|
-
let entry;
|
|
200
|
-
const exportsMap = manifest.exports;
|
|
201
|
-
if (subpath === "" && exportsMap !== undefined && exportsMap["."] !== undefined) {
|
|
202
|
-
const dot = exportsMap["."];
|
|
203
|
-
if (typeof dot === "string")
|
|
204
|
-
entry = dot;
|
|
205
|
-
else if (typeof dot === "object" && dot !== null) {
|
|
206
|
-
entry = dot.node ?? dot.import ?? dot.default;
|
|
207
|
-
if (typeof entry === "object" && entry !== null)
|
|
208
|
-
entry = entry.node ?? entry.import ?? entry.default;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (entry === undefined && subpath === "")
|
|
212
|
-
entry = manifest.module ?? manifest.main;
|
|
213
|
-
if (entry === undefined)
|
|
214
|
-
entry = subpath === "" ? "index.js" : subpath;
|
|
215
|
-
else if (subpath !== "")
|
|
216
|
-
entry = join(entry, subpath);
|
|
217
|
-
let resolvedPath = resolve(pkgDir, entry);
|
|
218
|
-
try {
|
|
219
|
-
resolvedPath = realpathSync(resolvedPath);
|
|
220
|
-
}
|
|
221
|
-
catch { /* keep */ }
|
|
222
|
-
if (!existsSync(resolvedPath))
|
|
223
|
-
throw new Error("omd-dsh: resolved entry \"" + entry + "\" for \"" + specifier + "\" does not exist at " + resolvedPath);
|
|
224
|
-
return pathToFileURL(resolvedPath).href;
|
|
225
|
-
}
|
|
226
|
-
function rewriteImports(sourceText, harnessNodeModules) {
|
|
227
|
-
const specifierPattern = /@deepseek-ai\/[A-Za-z0-9@._/-]+/g;
|
|
228
|
-
return sourceText.split(/\r?\n/).map((line) => {
|
|
229
|
-
if (!line.trimStart().startsWith("import"))
|
|
230
|
-
return line;
|
|
231
|
-
return line.replace(specifierPattern, (s) => resolveHarnessModule(harnessNodeModules, s));
|
|
232
|
-
}).join("\n");
|
|
233
|
-
}
|
|
234
|
-
function readMeta(dir) {
|
|
235
|
-
const metaPath = join(dir, ".omd-meta.json");
|
|
236
|
-
if (!existsSync(metaPath))
|
|
237
|
-
return undefined;
|
|
238
|
-
try {
|
|
239
|
-
const p = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
240
|
-
if (p !== null && typeof p === "object" && p.files !== null && typeof p.files === "object")
|
|
241
|
-
return p;
|
|
242
|
-
return undefined;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
async function collectSourceFiles(rootDir) {
|
|
249
|
-
const out = [];
|
|
250
|
-
const walk = async (dir) => {
|
|
251
|
-
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
252
|
-
const full = join(dir, entry.name);
|
|
253
|
-
if (entry.isDirectory())
|
|
254
|
-
await walk(full);
|
|
255
|
-
else
|
|
256
|
-
out.push(full);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
await walk(rootDir);
|
|
260
|
-
return out;
|
|
261
|
-
}
|
|
262
|
-
// ── matrix ──
|
|
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)");
|
|
320
|
-
return parsed;
|
|
321
|
-
}
|
|
322
|
-
function saveMatrix(m) { writeMatrixFile(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n"); }
|
|
323
|
-
// ── row rendering (relative indents; the splice prepends the fence indent) ──
|
|
324
|
-
function q(s) { return JSON.stringify(s); }
|
|
325
|
-
function renderModeRow(modeId, cfg) {
|
|
326
|
-
const out = ["- id: omd-mode", " name: '../.omd-vendor/omd-mode.mjs'", " config:", " mode: " + modeId];
|
|
327
|
-
if (cfg.provider !== undefined)
|
|
328
|
-
out.push(" provider: " + cfg.provider);
|
|
329
|
-
if (cfg.model !== undefined)
|
|
330
|
-
out.push(" model: " + cfg.model);
|
|
331
|
-
if (cfg.reasoningEffort !== undefined)
|
|
332
|
-
out.push(" reasoningEffort: " + cfg.reasoningEffort);
|
|
333
|
-
return out;
|
|
334
|
-
}
|
|
335
|
-
function renderToolFilter(tf) {
|
|
336
|
-
const deny = tf.deny ?? [];
|
|
337
|
-
const allow = tf.allow ?? [];
|
|
338
|
-
const out = [];
|
|
339
|
-
if (tf.denyShell === true) {
|
|
340
|
-
const arr = (names) => "[" + names.map((n) => "'" + n + "'").join(", ") + "]";
|
|
341
|
-
const expr = "(process.platform === 'win32') ? " + arr([...deny, "pwsh"]) + " : " + arr([...deny, "bash"]);
|
|
342
|
-
out.push("toolFilter:", " deny: !!js " + q(expr));
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
if (allow.length > 0)
|
|
346
|
-
out.push("toolFilter:", " allow: [" + allow.join(", ") + "]");
|
|
347
|
-
if (deny.length > 0) {
|
|
348
|
-
if (out.length === 0)
|
|
349
|
-
out.push("toolFilter:");
|
|
350
|
-
out.push(" deny: [" + deny.join(", ") + "]");
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return out;
|
|
354
|
-
}
|
|
355
|
-
function renderTaskRow(cfg) {
|
|
356
|
-
const tiers = cfg.tiers ?? {};
|
|
357
|
-
if (Object.keys(tiers).length === 0)
|
|
358
|
-
return [];
|
|
359
|
-
const out = ["- id: omd-task", " name: '../.omd-vendor/omd-task.mjs'", " config:", " provider: spawn", " toolName: omd_task", " backgroundMode: continuable", " tiers:"];
|
|
360
|
-
for (const [name, t] of Object.entries(tiers)) {
|
|
361
|
-
out.push(" " + name + ":");
|
|
362
|
-
out.push(" provider: " + t.provider);
|
|
363
|
-
out.push(" model: " + t.model);
|
|
364
|
-
if (t.hint !== undefined)
|
|
365
|
-
out.push(" hint: " + q(t.hint));
|
|
366
|
-
if (t.persona !== undefined)
|
|
367
|
-
out.push(" persona: " + q(t.persona));
|
|
368
|
-
if (t.maxTokens !== undefined)
|
|
369
|
-
out.push(" maxTokens: " + t.maxTokens);
|
|
370
|
-
if (t.toolFilter !== undefined)
|
|
371
|
-
for (const l of renderToolFilter(t.toolFilter))
|
|
372
|
-
out.push(" " + l);
|
|
373
|
-
}
|
|
374
|
-
return out;
|
|
375
|
-
}
|
|
376
|
-
function spliceFence(lines, fence, rendered) {
|
|
377
|
-
const start = lines.findIndex((l) => l.trim() === fence.start);
|
|
378
|
-
const end = lines.findIndex((l) => l.trim() === fence.end);
|
|
379
|
-
if (start === -1 || end === -1 || end < start)
|
|
380
|
-
throw new Error("omd-dsh: preset is missing the " + fence.start + " / " + fence.end + " markers; regenerate the preset from source");
|
|
381
|
-
const indent = (lines[start].match(/^ */) || [""])[0];
|
|
382
|
-
const renderedIndented = rendered.map((l) => indent + l);
|
|
383
|
-
lines.splice(start, end - start + 1, indent + fence.start, ...renderedIndented, indent + fence.end);
|
|
384
|
-
}
|
|
385
|
-
function applyMatrix(text, modeId, cfg) {
|
|
386
|
-
const lines = text.split("\n");
|
|
387
|
-
spliceFence(lines, MODE_FENCE, renderModeRow(modeId, cfg));
|
|
388
|
-
const taskRendered = renderTaskRow(cfg);
|
|
389
|
-
const hasTaskFence = lines.some((l) => l.trim() === TASK_FENCE.start);
|
|
390
|
-
if (taskRendered.length > 0 && !hasTaskFence) {
|
|
391
|
-
throw new Error("omd-dsh: mode \"" + modeId + "\" has tiers in omd-matrix.json but its preset is missing the task fence");
|
|
392
|
-
}
|
|
393
|
-
if (hasTaskFence)
|
|
394
|
-
spliceFence(lines, TASK_FENCE, taskRendered);
|
|
395
|
-
return lines.join("\n");
|
|
396
|
-
}
|
|
397
34
|
// ── model discovery ──
|
|
398
35
|
function discoverModels() {
|
|
399
36
|
const models = [];
|
|
@@ -428,187 +65,6 @@ function discoverModels() {
|
|
|
428
65
|
}
|
|
429
66
|
return { models, currentDefault };
|
|
430
67
|
}
|
|
431
|
-
// ── sync ──
|
|
432
|
-
async function runSync(flags, harnessNodeModules) {
|
|
433
|
-
const matrix = loadMatrix(flags);
|
|
434
|
-
const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
435
|
-
const sourceVersion = manifest.version;
|
|
436
|
-
const presetsSourceDir = join(PACKAGE_ROOT, "presets");
|
|
437
|
-
const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
|
|
438
|
-
const agentPresetsRoot = join(dshHome(), ".agent-presets");
|
|
439
|
-
const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
440
|
-
const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [], removed: [] };
|
|
441
|
-
const log = (kind, text) => report[kind].push(text);
|
|
442
|
-
for (const presetName of presetNames) {
|
|
443
|
-
const modeId = presetName.replace(/^omd-/, "");
|
|
444
|
-
const cfg = matrix.modes[modeId];
|
|
445
|
-
if (cfg === undefined)
|
|
446
|
-
throw new Error("omd-dsh sync: omd-matrix.json has no entry for mode \"" + modeId + "\" (preset " + presetName + ")");
|
|
447
|
-
const sourceDir = join(presetsSourceDir, presetName);
|
|
448
|
-
const targetDir = join(agentPresetsRoot, presetName);
|
|
449
|
-
const sourceFiles = await collectSourceFiles(sourceDir);
|
|
450
|
-
const existingMeta = existsSync(targetDir) ? readMeta(targetDir) : undefined;
|
|
451
|
-
if (existsSync(targetDir) && existingMeta === undefined) {
|
|
452
|
-
log("skipped", presetName + "/ (directory exists but is not managed by omd-dsh -- left untouched)");
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
const nextFiles = {};
|
|
456
|
-
for (const sourceFile of sourceFiles) {
|
|
457
|
-
const rel = relative(sourceDir, sourceFile).split("\\").join("/");
|
|
458
|
-
let sourceText = await fs.readFile(sourceFile, "utf8");
|
|
459
|
-
if (rel === "agent.cordis.yml")
|
|
460
|
-
sourceText = applyMatrix(sourceText, modeId, cfg);
|
|
461
|
-
const sourceHash = sha256(sourceText);
|
|
462
|
-
const destFile = join(targetDir, rel);
|
|
463
|
-
let action = "synced";
|
|
464
|
-
if (existsSync(destFile)) {
|
|
465
|
-
const destHash = sha256(await fs.readFile(destFile, "utf8"));
|
|
466
|
-
const prevHash = existingMeta?.files?.[rel]?.sha256;
|
|
467
|
-
if (destHash === sourceHash)
|
|
468
|
-
action = "up-to-date";
|
|
469
|
-
else if (prevHash !== undefined && destHash === prevHash)
|
|
470
|
-
action = "updated";
|
|
471
|
-
else
|
|
472
|
-
action = "conflict";
|
|
473
|
-
}
|
|
474
|
-
if (action === "conflict") {
|
|
475
|
-
log("conflicts", presetName + "/" + rel + " (locally modified -- keeping your version)");
|
|
476
|
-
if (existingMeta?.files?.[rel]?.sha256 !== undefined)
|
|
477
|
-
nextFiles[rel] = { sha256: existingMeta.files[rel].sha256 };
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (action === "up-to-date")
|
|
481
|
-
log("skipped", presetName + "/" + rel + " (up to date)");
|
|
482
|
-
else {
|
|
483
|
-
if (!flags.dryRun) {
|
|
484
|
-
await fs.mkdir(dirname(destFile), { recursive: true });
|
|
485
|
-
await fs.writeFile(destFile, sourceText, "utf8");
|
|
486
|
-
}
|
|
487
|
-
log(action === "synced" ? "synced" : "updated", presetName + "/" + rel + (flags.dryRun ? " (dry-run)" : ""));
|
|
488
|
-
}
|
|
489
|
-
nextFiles[rel] = { sha256: sourceHash };
|
|
490
|
-
}
|
|
491
|
-
if (!flags.dryRun) {
|
|
492
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
493
|
-
await fs.writeFile(join(targetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextFiles }, null, 2) + "\n", "utf8");
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
const vendorTargetDir = join(agentPresetsRoot, ".omd-vendor");
|
|
497
|
-
const vendorMeta = existsSync(vendorTargetDir) ? readMeta(vendorTargetDir) : undefined;
|
|
498
|
-
if (existsSync(vendorTargetDir) && vendorMeta === undefined) {
|
|
499
|
-
log("skipped", ".omd-vendor/ (directory exists but is not managed by omd-dsh -- left untouched)");
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
const nextVendorFiles = {};
|
|
503
|
-
for (const vendorName of VENDOR_SOURCES) {
|
|
504
|
-
const sourceFile = join(vendorSourceDir, vendorName);
|
|
505
|
-
if (!existsSync(sourceFile)) {
|
|
506
|
-
throw new Error("omd-dsh sync: missing vendored source " + sourceFile + " -- run `npm run build` first");
|
|
507
|
-
}
|
|
508
|
-
let sourceText = await fs.readFile(sourceFile, "utf8");
|
|
509
|
-
sourceText = rewriteImports(sourceText, harnessNodeModules);
|
|
510
|
-
const sourceHash = sha256(sourceText);
|
|
511
|
-
const destFile = join(vendorTargetDir, vendorName);
|
|
512
|
-
let action = "synced";
|
|
513
|
-
if (existsSync(destFile)) {
|
|
514
|
-
const destHash = sha256(await fs.readFile(destFile, "utf8"));
|
|
515
|
-
const prevHash = vendorMeta?.files?.[vendorName]?.sha256;
|
|
516
|
-
if (destHash === sourceHash)
|
|
517
|
-
action = "up-to-date";
|
|
518
|
-
else if (prevHash !== undefined && destHash === prevHash)
|
|
519
|
-
action = "updated";
|
|
520
|
-
else
|
|
521
|
-
action = "conflict";
|
|
522
|
-
}
|
|
523
|
-
if (action === "conflict") {
|
|
524
|
-
log("conflicts", ".omd-vendor/" + vendorName + " (locally modified -- keeping your version)");
|
|
525
|
-
if (vendorMeta?.files?.[vendorName]?.sha256 !== undefined)
|
|
526
|
-
nextVendorFiles[vendorName] = { sha256: vendorMeta.files[vendorName].sha256 };
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (action === "up-to-date")
|
|
530
|
-
log("skipped", ".omd-vendor/" + vendorName + " (up to date)");
|
|
531
|
-
else {
|
|
532
|
-
if (!flags.dryRun) {
|
|
533
|
-
await fs.mkdir(vendorTargetDir, { recursive: true });
|
|
534
|
-
await fs.writeFile(destFile, sourceText, "utf8");
|
|
535
|
-
}
|
|
536
|
-
log(action === "synced" ? "synced" : "updated", ".omd-vendor/" + vendorName + (flags.dryRun ? " (dry-run)" : ""));
|
|
537
|
-
}
|
|
538
|
-
nextVendorFiles[vendorName] = { sha256: sourceHash };
|
|
539
|
-
}
|
|
540
|
-
if (!flags.dryRun && VENDOR_SOURCES.length > 0) {
|
|
541
|
-
await fs.mkdir(vendorTargetDir, { recursive: true });
|
|
542
|
-
await fs.writeFile(join(vendorTargetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, harnessNodeModules, files: nextVendorFiles }, null, 2) + "\n", "utf8");
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (existsSync(agentPresetsRoot)) {
|
|
546
|
-
for (const entry of await fs.readdir(agentPresetsRoot, { withFileTypes: true })) {
|
|
547
|
-
if (!entry.isDirectory() || !entry.name.startsWith("omd-"))
|
|
548
|
-
continue;
|
|
549
|
-
if (presetNames.includes(entry.name))
|
|
550
|
-
continue;
|
|
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 {
|
|
568
|
-
log("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
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`)");
|
|
574
|
-
console.log("omd-dsh sync: harness node_modules=" + harnessNodeModules);
|
|
575
|
-
console.log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
|
|
576
|
-
for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
|
|
577
|
-
for (const line of report[key])
|
|
578
|
-
console.log(" [" + key + "] " + line);
|
|
579
|
-
const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
|
|
580
|
-
console.log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
|
|
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
|
-
}
|
|
612
68
|
// ── setup (interactive) ──
|
|
613
69
|
function splitModel(answer) {
|
|
614
70
|
const a = answer.trim();
|
|
@@ -618,7 +74,7 @@ function splitModel(answer) {
|
|
|
618
74
|
}
|
|
619
75
|
return { provider: "deepseek-official", model: a };
|
|
620
76
|
}
|
|
621
|
-
async function runSetup(flags
|
|
77
|
+
async function runSetup(flags) {
|
|
622
78
|
const matrix = loadMatrix(flags);
|
|
623
79
|
const { models, currentDefault } = discoverModels();
|
|
624
80
|
console.log("omd-dsh setup: 发现 DSH 已有模型:");
|
|
@@ -657,12 +113,7 @@ async function runSetup(flags, harnessNodeModules) {
|
|
|
657
113
|
const syncNow = await ask("是否立即同步到 " + join(dshHome(), ".agent-presets") + "? [Y/n] ");
|
|
658
114
|
rl.close();
|
|
659
115
|
if (syncNow === "" || /^y/i.test(syncNow)) {
|
|
660
|
-
|
|
661
|
-
console.error("omd-dsh setup: 无法定位 DSH harness node_modules,跳过同步。首次请运行 `omd-dsh sync --harness <路径>`(之后会自动缓存,无需重复指定)。");
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
await runSync(flags, harnessNodeModules);
|
|
665
|
-
}
|
|
116
|
+
await runSync(flags);
|
|
666
117
|
}
|
|
667
118
|
else {
|
|
668
119
|
console.log("omd-dsh setup: 已跳过同步;稍后运行 `omd-dsh sync` 生效。");
|
|
@@ -675,9 +126,7 @@ async function main() {
|
|
|
675
126
|
let command = "";
|
|
676
127
|
for (let i = 0; i < argv.length; i++) {
|
|
677
128
|
const arg = argv[i];
|
|
678
|
-
if (arg === "--
|
|
679
|
-
flags.harness = argv[++i];
|
|
680
|
-
else if (arg === "--dry-run")
|
|
129
|
+
if (arg === "--dry-run")
|
|
681
130
|
flags.dryRun = true;
|
|
682
131
|
else if (arg === "--verbose")
|
|
683
132
|
flags.verbose = true;
|
|
@@ -708,16 +157,10 @@ async function main() {
|
|
|
708
157
|
return;
|
|
709
158
|
}
|
|
710
159
|
if (command === "setup") {
|
|
711
|
-
await runSetup(flags
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const harnessNodeModules = resolveHarness(flags);
|
|
715
|
-
if (harnessNodeModules === undefined || !existsSync(join(harnessNodeModules, "@deepseek-ai", "dsh-scope", "package.json"))) {
|
|
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());
|
|
717
|
-
process.exitCode = 2;
|
|
160
|
+
await runSetup(flags);
|
|
718
161
|
return;
|
|
719
162
|
}
|
|
720
|
-
await runSync(flags
|
|
163
|
+
await runSync(flags);
|
|
721
164
|
}
|
|
722
165
|
main().catch((error) => {
|
|
723
166
|
console.error("omd-dsh failed: " + (error instanceof Error ? error.message : String(error)));
|
package/lib/index.d.ts
CHANGED
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* - otherwise the user explicitly picked a
|
|
30
30
|
* different model -> yield: the request and
|
|
31
31
|
* the persona variables keep the user's selection, and the row
|
|
32
|
-
* records it
|
|
33
|
-
*
|
|
32
|
+
* records it (shared.ts, keyed by the agent) so the omd-task row can
|
|
33
|
+
* route the "deep" tier to the user's model.
|
|
34
34
|
*
|
|
35
35
|
* When provider/model are not configured the row passes everything
|
|
36
36
|
* through and only serves the persona banner variables (inheriting the
|