@carljia/omd-dsh 0.1.5 → 0.1.7

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/lib/shared.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module @carljia/omd-dsh/shared
3
+ *
4
+ * Per-agent transient state shared between the omd-mode and omd-task rows.
5
+ *
6
+ * Why this module exists: cordis scoped contexts are proxies — assigning an
7
+ * undeclared property throws ("cannot set property ... without provide"), and
8
+ * two rows in one preset are sibling contexts that cannot see each other's
9
+ * declared properties either. Both rows therefore import this module, and the
10
+ * sync ships it next to them (`.omd-vendor/shared.js`), so the two vendored
11
+ * rows resolve the SAME module instance and share one WeakMap. The override is
12
+ * keyed by the top-level agent object (stable across a session's turns; a
13
+ * resumed session mints a new agent and starts clean; subagents are distinct
14
+ * objects and simply miss the map, which is exactly the documented passthrough
15
+ * semantics).
16
+ */
17
+ const modeOverrides = new WeakMap();
18
+ /** Record (or clear, with `undefined`) the user's model pick for one agent. */
19
+ export function setModeOverride(agent, override) {
20
+ if (override === undefined)
21
+ modeOverrides.delete(agent);
22
+ else
23
+ modeOverrides.set(agent, override);
24
+ }
25
+ /** The user's recorded model pick for one agent, or undefined. */
26
+ export function modeOverrideFor(agent) {
27
+ return modeOverrides.get(agent);
28
+ }
package/lib/sync.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * omd-dsh sync core — materialize the OMD presets into <DSH_HOME>/.agent-presets.
3
+ *
4
+ * Shared by the CLI (\`omd-dsh sync\`) and the bundle boot row (\`dsh plugin add\`
5
+ * + restart): locate the DSH harness node_modules, render each preset's
6
+ * omd-mode / omd-task rows from the user's model matrix, copy the presets and
7
+ * vendored row modules into .agent-presets, and rewrite the vendored modules'
8
+ * bare @deepseek-ai/* imports to absolute file:// URLs into the harness tree so
9
+ * the rows share ONE module instance with the harness (scope symbols etc.).
10
+ */
11
+ export type SyncFlags = {
12
+ harness?: string;
13
+ dryRun: boolean;
14
+ verbose: boolean;
15
+ };
16
+ export interface TierConfig {
17
+ provider: string;
18
+ model: string;
19
+ hint?: string;
20
+ persona?: string;
21
+ maxTokens?: number;
22
+ toolFilter?: {
23
+ allow?: string[];
24
+ deny?: string[];
25
+ denyShell?: boolean;
26
+ };
27
+ }
28
+ export interface ModeConfig {
29
+ provider?: string;
30
+ model?: string;
31
+ reasoningEffort?: string;
32
+ tiers?: Record<string, TierConfig>;
33
+ }
34
+ export interface Matrix {
35
+ version: number;
36
+ defaults?: {
37
+ provider?: string;
38
+ };
39
+ modes: Record<string, ModeConfig>;
40
+ }
41
+ export declare const PACKAGE_ROOT: string;
42
+ export declare const VENDOR_SOURCES: string[];
43
+ /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
44
+ export declare const MATRIX_PATH: string;
45
+ export declare function dshHome(): string;
46
+ /**
47
+ * Resolve the DSH harness node_modules:
48
+ * 1. --harness flag (and cache it for later);
49
+ * 2. auto-detect via the dsh executable on PATH;
50
+ * 3. fall back to the locally cached value.
51
+ */
52
+ export declare function resolveHarness(flags: SyncFlags): string | undefined;
53
+ /**
54
+ * Resolve the harness node_modules from THIS module's own location, walking up
55
+ * the Node resolution path for @deepseek-ai/dsh-scope. This is the reliable
56
+ * anchor when the package runs as a bundle inside a DSH profile: the profile's
57
+ * flat module fallback (or its hoisted node_modules) exposes the harness tree.
58
+ */
59
+ export declare function resolveHarnessFromSelf(): string | undefined;
60
+ export declare function loadMatrix(flags: SyncFlags, log?: (msg: string) => void): Matrix;
61
+ export declare function saveMatrix(m: Matrix): void;
62
+ export declare function runSync(flags: SyncFlags, harnessNodeModules: string, log?: (msg: string) => void): Promise<void>;
package/lib/sync.js ADDED
@@ -0,0 +1,549 @@
1
+ import { promises as fs, existsSync, mkdirSync, realpathSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ export const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs", "omd-plan.mjs", "omd-start-work.mjs", "omd-mode-switch.mjs", "shared.js"];
9
+ /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
10
+ export const MATRIX_PATH = join(dshHome(), "omd-matrix.json");
11
+ /** Pre-migration location (package root) — migrated to MATRIX_PATH once when present. */
12
+ const LEGACY_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.json");
13
+ const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
14
+ const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]" };
15
+ /** Presets that were renamed: old directory name -> new preset name. */
16
+ const RENAMED_FROM = { "omd-architect": "omd-ultraworker" };
17
+ function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
18
+ export function dshHome() { return process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== "" ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh"); }
19
+ function findNodeModules(start) {
20
+ let current = resolve(start);
21
+ for (;;) {
22
+ if (basename(current) === "node_modules")
23
+ return current;
24
+ const parent = dirname(current);
25
+ if (parent === current)
26
+ return undefined;
27
+ current = parent;
28
+ }
29
+ }
30
+ function harnessCachePath() { return join(dshHome(), "omd-dsh-harness.json"); }
31
+ /** Read the cached harness node_modules, ignoring a stale/missing entry. */
32
+ function readCachedHarness() {
33
+ try {
34
+ const p = harnessCachePath();
35
+ if (!existsSync(p))
36
+ return undefined;
37
+ const parsed = JSON.parse(readFileSync(p, "utf8"));
38
+ const nm = parsed && typeof parsed === "object" ? parsed.harnessNodeModules : undefined;
39
+ if (typeof nm !== "string" || nm === "")
40
+ return undefined;
41
+ if (!existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
42
+ return undefined;
43
+ return nm;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** Persist the resolved harness node_modules for later runs (best-effort). */
50
+ function writeCachedHarness(harnessNodeModules) {
51
+ try {
52
+ writeFileSync(harnessCachePath(), JSON.stringify({ harnessNodeModules }, null, 2) + "\n", "utf8");
53
+ }
54
+ catch { /* best-effort */ }
55
+ }
56
+ /**
57
+ * Resolve the DSH harness node_modules:
58
+ * 1. --harness flag (and cache it for later);
59
+ * 2. auto-detect via the dsh executable on PATH;
60
+ * 3. fall back to the locally cached value.
61
+ */
62
+ export function resolveHarness(flags) {
63
+ let nm;
64
+ if (flags.harness !== undefined) {
65
+ nm = findNodeModules(flags.harness);
66
+ if (nm !== undefined) {
67
+ try {
68
+ nm = realpathSync(nm);
69
+ writeCachedHarness(nm);
70
+ }
71
+ catch { /* keep nm as-is */ }
72
+ }
73
+ return nm;
74
+ }
75
+ nm = locateHarnessViaDsh() ?? locateHarnessViaNpxCache() ?? readCachedHarness();
76
+ if (nm !== undefined) {
77
+ try {
78
+ nm = realpathSync(nm);
79
+ }
80
+ catch { /* keep */ }
81
+ }
82
+ return nm;
83
+ }
84
+ /**
85
+ * Resolve the harness node_modules from THIS module's own location, walking up
86
+ * the Node resolution path for @deepseek-ai/dsh-scope. This is the reliable
87
+ * anchor when the package runs as a bundle inside a DSH profile: the profile's
88
+ * flat module fallback (or its hoisted node_modules) exposes the harness tree.
89
+ */
90
+ export function resolveHarnessFromSelf() {
91
+ let dir = dirname(fileURLToPath(import.meta.url));
92
+ for (;;) {
93
+ const nm = join(dir, "node_modules");
94
+ const scopeDir = join(nm, "@deepseek-ai", "dsh-scope");
95
+ if (existsSync(join(scopeDir, "package.json"))) {
96
+ try {
97
+ const real = realpathSync(scopeDir);
98
+ return findNodeModules(real);
99
+ }
100
+ catch {
101
+ return nm;
102
+ }
103
+ }
104
+ const parent = dirname(dir);
105
+ if (parent === dir)
106
+ return undefined;
107
+ dir = parent;
108
+ }
109
+ }
110
+ function locateHarnessViaDsh() {
111
+ const candidates = [];
112
+ try {
113
+ const probe = process.platform === "win32" ? "where.exe" : "which";
114
+ const out = execFileSync(probe, ["dsh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
115
+ for (const line of out.split(/\r?\n/)) {
116
+ const t = line.trim();
117
+ if (t !== "")
118
+ candidates.push(t);
119
+ }
120
+ }
121
+ catch { /* dsh not on PATH */ }
122
+ for (const candidate of candidates) {
123
+ let real = candidate;
124
+ try {
125
+ real = realpathSync(candidate);
126
+ }
127
+ catch { /* keep */ }
128
+ const nm = findNodeModules(real);
129
+ if (nm !== undefined && existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
130
+ return nm;
131
+ }
132
+ return undefined;
133
+ }
134
+ /** Candidate npx cache roots where a non-global DSH install may live. */
135
+ function npxCacheRoots() {
136
+ const roots = [];
137
+ if (process.platform === "win32") {
138
+ const localAppData = process.env.LOCALAPPDATA;
139
+ if (localAppData)
140
+ roots.push(join(localAppData, "npm-cache", "_npx"));
141
+ const appData = process.env.APPDATA;
142
+ if (appData)
143
+ roots.push(join(appData, "npm-cache", "_npx"));
144
+ }
145
+ else {
146
+ roots.push(join(homedir(), ".npm", "_npx"));
147
+ }
148
+ return roots;
149
+ }
150
+ /**
151
+ * Best-effort scan of the npx cache for a DSH install whose node_modules
152
+ * carries @deepseek-ai/dsh-scope. Picks the most recently touched one.
153
+ */
154
+ function locateHarnessViaNpxCache() {
155
+ const matches = [];
156
+ for (const root of npxCacheRoots()) {
157
+ let entries;
158
+ try {
159
+ entries = readdirSync(root);
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ for (const entry of entries) {
165
+ const nm = join(root, entry, "node_modules");
166
+ if (!existsSync(join(nm, "@deepseek-ai", "dsh-scope", "package.json")))
167
+ continue;
168
+ let mtime = 0;
169
+ try {
170
+ mtime = statSync(join(root, entry)).mtimeMs;
171
+ }
172
+ catch { /* keep 0 */ }
173
+ matches.push({ nm, mtime });
174
+ }
175
+ }
176
+ matches.sort((a, b) => b.mtime - a.mtime);
177
+ return matches.length > 0 ? matches[0].nm : undefined;
178
+ }
179
+ function resolveHarnessModule(harnessNodeModules, specifier) {
180
+ const segments = specifier.split("/");
181
+ const scope = segments[0].startsWith("@") ? segments[0] + "/" + segments[1] : segments[0];
182
+ const subpath = scope === specifier ? "" : specifier.slice(scope.length + 1);
183
+ const pkgDir = join(harnessNodeModules, ...scope.split("/"));
184
+ const manifestPath = join(pkgDir, "package.json");
185
+ if (!existsSync(manifestPath))
186
+ throw new Error("omd-dsh: cannot resolve \"" + specifier + "\" -- no package.json at " + manifestPath);
187
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
188
+ let entry;
189
+ const exportsMap = manifest.exports;
190
+ if (subpath === "" && exportsMap !== undefined && exportsMap["."] !== undefined) {
191
+ const dot = exportsMap["."];
192
+ if (typeof dot === "string")
193
+ entry = dot;
194
+ else if (typeof dot === "object" && dot !== null) {
195
+ entry = dot.node ?? dot.import ?? dot.default;
196
+ if (typeof entry === "object" && entry !== null)
197
+ entry = entry.node ?? entry.import ?? entry.default;
198
+ }
199
+ }
200
+ if (entry === undefined && subpath === "")
201
+ entry = manifest.module ?? manifest.main;
202
+ if (entry === undefined)
203
+ entry = subpath === "" ? "index.js" : subpath;
204
+ else if (subpath !== "")
205
+ entry = join(entry, subpath);
206
+ let resolvedPath = resolve(pkgDir, entry);
207
+ try {
208
+ resolvedPath = realpathSync(resolvedPath);
209
+ }
210
+ catch { /* keep */ }
211
+ if (!existsSync(resolvedPath))
212
+ throw new Error("omd-dsh: resolved entry \"" + entry + "\" for \"" + specifier + "\" does not exist at " + resolvedPath);
213
+ return pathToFileURL(resolvedPath).href;
214
+ }
215
+ function rewriteImports(sourceText, harnessNodeModules) {
216
+ const specifierPattern = /@deepseek-ai\/[A-Za-z0-9@._/-]+/g;
217
+ return sourceText.split(/\r?\n/).map((line) => {
218
+ if (!line.trimStart().startsWith("import"))
219
+ return line;
220
+ return line.replace(specifierPattern, (s) => resolveHarnessModule(harnessNodeModules, s));
221
+ }).join("\n");
222
+ }
223
+ function readMeta(dir) {
224
+ const metaPath = join(dir, ".omd-meta.json");
225
+ if (!existsSync(metaPath))
226
+ return undefined;
227
+ try {
228
+ const p = JSON.parse(readFileSync(metaPath, "utf8"));
229
+ if (p !== null && typeof p === "object" && p.files !== null && typeof p.files === "object")
230
+ return p;
231
+ return undefined;
232
+ }
233
+ catch {
234
+ return undefined;
235
+ }
236
+ }
237
+ async function collectSourceFiles(rootDir) {
238
+ const out = [];
239
+ const walk = async (dir) => {
240
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
241
+ const full = join(dir, entry.name);
242
+ if (entry.isDirectory())
243
+ await walk(full);
244
+ else
245
+ out.push(full);
246
+ }
247
+ };
248
+ await walk(rootDir);
249
+ return out;
250
+ }
251
+ // ── matrix ──
252
+ const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
253
+ function parseMatrix(text) {
254
+ try {
255
+ const parsed = JSON.parse(text);
256
+ if (parsed !== null && typeof parsed === "object" && parsed.modes !== null && typeof parsed.modes === "object")
257
+ return parsed;
258
+ }
259
+ catch { /* fall through */ }
260
+ return undefined;
261
+ }
262
+ function readDefaultMatrix() {
263
+ if (!existsSync(DEFAULT_MATRIX_PATH))
264
+ throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
265
+ const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
266
+ if (parsed === undefined)
267
+ throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
268
+ return parsed;
269
+ }
270
+ function writeMatrixFile(path, text) {
271
+ mkdirSync(dirname(path), { recursive: true });
272
+ writeFileSync(path, text, "utf8");
273
+ }
274
+ export function loadMatrix(flags, log = console.log) {
275
+ if (!existsSync(MATRIX_PATH)) {
276
+ if (flags.dryRun)
277
+ return JSON.parse(JSON.stringify(readDefaultMatrix()));
278
+ const legacyText = existsSync(LEGACY_MATRIX_PATH) ? readFileSync(LEGACY_MATRIX_PATH, "utf8") : undefined;
279
+ if (legacyText !== undefined && parseMatrix(legacyText) !== undefined) {
280
+ writeMatrixFile(MATRIX_PATH, legacyText);
281
+ log("omd-dsh: migrated omd-matrix.json: " + LEGACY_MATRIX_PATH + " -> " + MATRIX_PATH);
282
+ log("omd-dsh: customize the model matrix any time with \`omd-dsh setup\`.");
283
+ }
284
+ else {
285
+ const defaults = readDefaultMatrix();
286
+ writeMatrixFile(MATRIX_PATH, JSON.stringify(defaults, null, 2) + "\n");
287
+ log("omd-dsh: generated " + MATRIX_PATH + " from the shipped deepseek default matrix; customize it any time with \`omd-dsh setup\`.");
288
+ }
289
+ }
290
+ const text = readFileSync(MATRIX_PATH, "utf8");
291
+ const parsed = parseMatrix(text);
292
+ if (parsed === undefined)
293
+ throw new Error("omd-dsh: malformed " + MATRIX_PATH + " (restore it, or delete it and run omd-dsh setup)");
294
+ return parsed;
295
+ }
296
+ export function saveMatrix(m) { writeMatrixFile(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n"); }
297
+ // ── row rendering ──
298
+ function q(s) { return JSON.stringify(s); }
299
+ function renderModeRow(modeId, cfg) {
300
+ const out = ["- id: omd-mode", " name: '../.omd-vendor/omd-mode.mjs'", " config:", " mode: " + modeId];
301
+ if (cfg.provider !== undefined)
302
+ out.push(" provider: " + cfg.provider);
303
+ if (cfg.model !== undefined)
304
+ out.push(" model: " + cfg.model);
305
+ if (cfg.reasoningEffort !== undefined)
306
+ out.push(" reasoningEffort: " + cfg.reasoningEffort);
307
+ return out;
308
+ }
309
+ function renderToolFilter(tf) {
310
+ const deny = tf.deny ?? [];
311
+ const allow = tf.allow ?? [];
312
+ const out = [];
313
+ if (tf.denyShell === true) {
314
+ const arr = (names) => "[" + names.map((n) => "'" + n + "'").join(", ") + "]";
315
+ const expr = "(process.platform === 'win32') ? " + arr([...deny, "pwsh"]) + " : " + arr([...deny, "bash"]);
316
+ out.push("toolFilter:", " deny: !!js " + q(expr));
317
+ }
318
+ else {
319
+ if (allow.length > 0)
320
+ out.push("toolFilter:", " allow: [" + allow.join(", ") + "]");
321
+ if (deny.length > 0) {
322
+ if (out.length === 0)
323
+ out.push("toolFilter:");
324
+ out.push(" deny: [" + deny.join(", ") + "]");
325
+ }
326
+ }
327
+ return out;
328
+ }
329
+ function renderTaskRow(cfg) {
330
+ const tiers = cfg.tiers ?? {};
331
+ if (Object.keys(tiers).length === 0)
332
+ return [];
333
+ const out = ["- id: omd-task", " name: '../.omd-vendor/omd-task.mjs'", " config:", " provider: spawn", " toolName: omd_task", " backgroundMode: continuable", " tiers:"];
334
+ for (const [name, t] of Object.entries(tiers)) {
335
+ out.push(" " + name + ":");
336
+ out.push(" provider: " + t.provider);
337
+ out.push(" model: " + t.model);
338
+ if (t.hint !== undefined)
339
+ out.push(" hint: " + q(t.hint));
340
+ if (t.persona !== undefined)
341
+ out.push(" persona: " + q(t.persona));
342
+ if (t.maxTokens !== undefined)
343
+ out.push(" maxTokens: " + t.maxTokens);
344
+ if (t.toolFilter !== undefined)
345
+ for (const l of renderToolFilter(t.toolFilter))
346
+ out.push(" " + l);
347
+ }
348
+ return out;
349
+ }
350
+ function spliceFence(lines, fence, rendered) {
351
+ const start = lines.findIndex((l) => l.trim() === fence.start);
352
+ const end = lines.findIndex((l) => l.trim() === fence.end);
353
+ if (start === -1 || end === -1 || end < start)
354
+ throw new Error("omd-dsh: preset is missing the " + fence.start + " / " + fence.end + " markers; regenerate the preset from source");
355
+ const indent = (lines[start].match(/^ */) || [""])[0];
356
+ const renderedIndented = rendered.map((l) => indent + l);
357
+ lines.splice(start, end - start + 1, indent + fence.start, ...renderedIndented, indent + fence.end);
358
+ }
359
+ function applyMatrix(text, modeId, cfg) {
360
+ const lines = text.split("\n");
361
+ spliceFence(lines, MODE_FENCE, renderModeRow(modeId, cfg));
362
+ const taskRendered = renderTaskRow(cfg);
363
+ const hasTaskFence = lines.some((l) => l.trim() === TASK_FENCE.start);
364
+ if (taskRendered.length > 0 && !hasTaskFence) {
365
+ throw new Error("omd-dsh: mode \"" + modeId + "\" has tiers in omd-matrix.json but its preset is missing the task fence");
366
+ }
367
+ if (hasTaskFence)
368
+ spliceFence(lines, TASK_FENCE, taskRendered);
369
+ return lines.join("\n");
370
+ }
371
+ /**
372
+ * Whether one omd-dsh-managed preset directory differs from the hashes its
373
+ * .omd-meta.json recorded. Returns a description of the first discrepancy,
374
+ * or undefined when every recorded file is present and unmodified.
375
+ */
376
+ async function locallyModified(dir, meta) {
377
+ const recorded = meta.files ?? {};
378
+ const current = {};
379
+ const walk = async (d) => {
380
+ for (const entry of await fs.readdir(d, { withFileTypes: true })) {
381
+ const full = join(d, entry.name);
382
+ if (entry.isDirectory())
383
+ await walk(full);
384
+ else if (entry.name !== ".omd-meta.json") {
385
+ const rel = relative(dir, full).split("\\").join("/");
386
+ current[rel] = sha256(await fs.readFile(full, "utf8"));
387
+ }
388
+ }
389
+ };
390
+ await walk(dir);
391
+ for (const rel of new Set([...Object.keys(recorded), ...Object.keys(current)])) {
392
+ if (current[rel] === undefined)
393
+ return "missing file " + rel;
394
+ const recordedHash = recorded[rel] !== undefined && typeof recorded[rel] === "object" && recorded[rel] !== null ? recorded[rel].sha256 : undefined;
395
+ if (typeof recordedHash !== "string" || recordedHash !== current[rel])
396
+ return "modified file " + rel;
397
+ }
398
+ return undefined;
399
+ }
400
+ export async function runSync(flags, harnessNodeModules, log = console.log) {
401
+ const matrix = loadMatrix(flags, log);
402
+ const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
403
+ const sourceVersion = manifest.version;
404
+ const presetsSourceDir = join(PACKAGE_ROOT, "presets");
405
+ const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
406
+ const agentPresetsRoot = join(dshHome(), ".agent-presets");
407
+ const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
408
+ const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [], removed: [] };
409
+ const record = (kind, text) => report[kind].push(text);
410
+ for (const presetName of presetNames) {
411
+ const modeId = presetName.replace(/^omd-/, "");
412
+ const cfg = matrix.modes[modeId];
413
+ if (cfg === undefined)
414
+ throw new Error("omd-dsh sync: omd-matrix.json has no entry for mode \"" + modeId + "\" (preset " + presetName + ")");
415
+ const sourceDir = join(presetsSourceDir, presetName);
416
+ const targetDir = join(agentPresetsRoot, presetName);
417
+ const sourceFiles = await collectSourceFiles(sourceDir);
418
+ const existingMeta = existsSync(targetDir) ? readMeta(targetDir) : undefined;
419
+ if (existsSync(targetDir) && existingMeta === undefined) {
420
+ record("skipped", presetName + "/ (directory exists but is not managed by omd-dsh -- left untouched)");
421
+ continue;
422
+ }
423
+ const nextFiles = {};
424
+ for (const sourceFile of sourceFiles) {
425
+ const rel = relative(sourceDir, sourceFile).split("\\").join("/");
426
+ let sourceText = await fs.readFile(sourceFile, "utf8");
427
+ if (rel === "agent.cordis.yml")
428
+ sourceText = applyMatrix(sourceText, modeId, cfg);
429
+ const sourceHash = sha256(sourceText);
430
+ const destFile = join(targetDir, rel);
431
+ let action = "synced";
432
+ if (existsSync(destFile)) {
433
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
434
+ const prevHash = existingMeta?.files?.[rel]?.sha256;
435
+ if (destHash === sourceHash)
436
+ action = "up-to-date";
437
+ else if (prevHash !== undefined && destHash === prevHash)
438
+ action = "updated";
439
+ else
440
+ action = "conflict";
441
+ }
442
+ if (action === "conflict") {
443
+ record("conflicts", presetName + "/" + rel + " (locally modified -- keeping your version)");
444
+ if (existingMeta?.files?.[rel]?.sha256 !== undefined)
445
+ nextFiles[rel] = { sha256: existingMeta.files[rel].sha256 };
446
+ continue;
447
+ }
448
+ if (action === "up-to-date")
449
+ record("skipped", presetName + "/" + rel + " (up to date)");
450
+ else {
451
+ if (!flags.dryRun) {
452
+ await fs.mkdir(dirname(destFile), { recursive: true });
453
+ await fs.writeFile(destFile, sourceText, "utf8");
454
+ }
455
+ record(action === "synced" ? "synced" : "updated", presetName + "/" + rel + (flags.dryRun ? " (dry-run)" : ""));
456
+ }
457
+ nextFiles[rel] = { sha256: sourceHash };
458
+ }
459
+ if (!flags.dryRun) {
460
+ await fs.mkdir(targetDir, { recursive: true });
461
+ await fs.writeFile(join(targetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextFiles }, null, 2) + "\n", "utf8");
462
+ }
463
+ }
464
+ const vendorTargetDir = join(agentPresetsRoot, ".omd-vendor");
465
+ const vendorMeta = existsSync(vendorTargetDir) ? readMeta(vendorTargetDir) : undefined;
466
+ if (existsSync(vendorTargetDir) && vendorMeta === undefined) {
467
+ record("skipped", ".omd-vendor/ (directory exists but is not managed by omd-dsh -- left untouched)");
468
+ }
469
+ else {
470
+ const nextVendorFiles = {};
471
+ for (const vendorName of VENDOR_SOURCES) {
472
+ const sourceFile = join(vendorSourceDir, vendorName);
473
+ if (!existsSync(sourceFile)) {
474
+ throw new Error("omd-dsh sync: missing vendored source " + sourceFile + " -- run \`npm run build\` first");
475
+ }
476
+ let sourceText = await fs.readFile(sourceFile, "utf8");
477
+ sourceText = rewriteImports(sourceText, harnessNodeModules);
478
+ const sourceHash = sha256(sourceText);
479
+ const destFile = join(vendorTargetDir, vendorName);
480
+ let action = "synced";
481
+ if (existsSync(destFile)) {
482
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
483
+ const prevHash = vendorMeta?.files?.[vendorName]?.sha256;
484
+ if (destHash === sourceHash)
485
+ action = "up-to-date";
486
+ else if (prevHash !== undefined && destHash === prevHash)
487
+ action = "updated";
488
+ else
489
+ action = "conflict";
490
+ }
491
+ if (action === "conflict") {
492
+ record("conflicts", ".omd-vendor/" + vendorName + " (locally modified -- keeping your version)");
493
+ if (vendorMeta?.files?.[vendorName]?.sha256 !== undefined)
494
+ nextVendorFiles[vendorName] = { sha256: vendorMeta.files[vendorName].sha256 };
495
+ continue;
496
+ }
497
+ if (action === "up-to-date")
498
+ record("skipped", ".omd-vendor/" + vendorName + " (up to date)");
499
+ else {
500
+ if (!flags.dryRun) {
501
+ await fs.mkdir(vendorTargetDir, { recursive: true });
502
+ await fs.writeFile(destFile, sourceText, "utf8");
503
+ }
504
+ record(action === "synced" ? "synced" : "updated", ".omd-vendor/" + vendorName + (flags.dryRun ? " (dry-run)" : ""));
505
+ }
506
+ nextVendorFiles[vendorName] = { sha256: sourceHash };
507
+ }
508
+ if (!flags.dryRun && VENDOR_SOURCES.length > 0) {
509
+ await fs.mkdir(vendorTargetDir, { recursive: true });
510
+ await fs.writeFile(join(vendorTargetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, harnessNodeModules, files: nextVendorFiles }, null, 2) + "\n", "utf8");
511
+ }
512
+ }
513
+ if (existsSync(agentPresetsRoot)) {
514
+ for (const entry of await fs.readdir(agentPresetsRoot, { withFileTypes: true })) {
515
+ if (!entry.isDirectory() || !entry.name.startsWith("omd-"))
516
+ continue;
517
+ if (presetNames.includes(entry.name))
518
+ continue;
519
+ const orphanDir = join(agentPresetsRoot, entry.name);
520
+ const meta = readMeta(orphanDir);
521
+ if (meta === undefined)
522
+ continue;
523
+ const renamedTo = RENAMED_FROM[entry.name];
524
+ if (renamedTo !== undefined && presetNames.includes(renamedTo)) {
525
+ const dirty = await locallyModified(orphanDir, meta);
526
+ if (dirty === undefined) {
527
+ if (!flags.dryRun)
528
+ await fs.rm(orphanDir, { recursive: true, force: true });
529
+ record("removed", entry.name + "/ (renamed to " + renamedTo + " and unmodified -- removed" + (flags.dryRun ? ", dry-run" : "") + ")");
530
+ }
531
+ else {
532
+ record("conflicts", entry.name + "/ (renamed to " + renamedTo + " but locally modified -- keeping your version: " + dirty + ")");
533
+ }
534
+ }
535
+ else {
536
+ record("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
537
+ }
538
+ }
539
+ }
540
+ log("omd-dsh sync: DSH_HOME=" + dshHome());
541
+ log("omd-dsh sync: matrix=" + MATRIX_PATH + " (customize the model matrix any time with \`omd-dsh setup\`)");
542
+ log("omd-dsh sync: harness node_modules=" + harnessNodeModules);
543
+ log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
544
+ for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
545
+ for (const line of report[key])
546
+ log(" [" + key + "] " + line);
547
+ const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
548
+ log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
549
+ }
package/lib/task.js CHANGED
@@ -2,6 +2,7 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import { assertSubagentMaxDepth } from "@deepseek-ai/dsh-subagent";
4
4
  import { scopeOf } from "@deepseek-ai/dsh-scope";
5
+ import { modeOverrideFor } from "./shared.js";
5
6
  /**
6
7
  * @module @carljia/omd-dsh/task
7
8
  *
@@ -212,9 +213,9 @@ function apply(ctx, config) {
212
213
  throw new Error("omd_task requires a calling agent (exec.agent was undefined)");
213
214
  const tierName = resolveTier(config, args.tier);
214
215
  let tier = config.tiers[tierName];
215
- // 用户显式切换模型后(omd-mode 在 agent/request 让路并在作用域 ctx 上记录
216
- // omdModeOverride),deep tier 改用用户选择的模型;其余 tier 保持矩阵配置。
217
- const override = ctx.omdModeOverride;
216
+ // 用户显式切换模型后(omd-mode 在 agent/request 让路并把用户选择记入 shared.ts,
217
+ // 键为顶层 agent 对象),deep tier 改用用户选择的模型;其余 tier 保持矩阵配置。
218
+ const override = modeOverrideFor(parent);
218
219
  if (tierName === "deep" && override !== undefined
219
220
  && typeof override.provider === "string" && typeof override.model === "string") {
220
221
  tier = { ...tier, provider: override.provider, model: override.model };