@carljia/omd-dsh 0.1.7 → 0.1.9

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/sync.js CHANGED
@@ -1,9 +1,9 @@
1
- import { promises as fs, existsSync, mkdirSync, realpathSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
1
+ import { promises as fs, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
- import { execFileSync } from "node:child_process";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { fileURLToPath } from "node:url";
6
+ import z from "@deepseek-ai/schemastery";
7
7
  export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
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
9
  /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
@@ -16,210 +16,6 @@ const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]
16
16
  const RENAMED_FROM = { "omd-architect": "omd-ultraworker" };
17
17
  function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
18
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
19
  function readMeta(dir) {
224
20
  const metaPath = join(dir, ".omd-meta.json");
225
21
  if (!existsSync(metaPath))
@@ -250,6 +46,32 @@ async function collectSourceFiles(rootDir) {
250
46
  }
251
47
  // ── matrix ──
252
48
  const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
49
+ /** Settings-namespace schema for the model matrix (see plan §5). Every field
50
+ * is optional: `base` (the shipped default matrix) fills whatever the user
51
+ * document omits, and extra keys (future fields) pass through untouched. */
52
+ const TierSchema = z.object({
53
+ provider: z.string(),
54
+ model: z.string(),
55
+ hint: z.string(),
56
+ persona: z.string(),
57
+ maxTokens: z.number(),
58
+ toolFilter: z.object({
59
+ allow: z.array(z.string()),
60
+ deny: z.array(z.string()),
61
+ denyShell: z.boolean(),
62
+ }),
63
+ });
64
+ const ModeSchema = z.object({
65
+ provider: z.string(),
66
+ model: z.string(),
67
+ reasoningEffort: z.string(),
68
+ tiers: z.dict(TierSchema),
69
+ });
70
+ export const MatrixSchema = z.object({
71
+ version: z.number(),
72
+ defaults: z.object({ provider: z.string() }),
73
+ modes: z.dict(ModeSchema),
74
+ });
253
75
  function parseMatrix(text) {
254
76
  try {
255
77
  const parsed = JSON.parse(text);
@@ -259,7 +81,7 @@ function parseMatrix(text) {
259
81
  catch { /* fall through */ }
260
82
  return undefined;
261
83
  }
262
- function readDefaultMatrix() {
84
+ export function readDefaultMatrix() {
263
85
  if (!existsSync(DEFAULT_MATRIX_PATH))
264
86
  throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
265
87
  const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
@@ -267,6 +89,27 @@ function readDefaultMatrix() {
267
89
  throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
268
90
  return parsed;
269
91
  }
92
+ /**
93
+ * Read the user's matrix file (<DSH_HOME>/omd-matrix.json) without touching
94
+ * the filesystem beyond the read: missing or malformed returns undefined and
95
+ * NEVER auto-generates — the caller decides what to fall back to (settings
96
+ * namespace resolved value / default matrix). This is the host row's
97
+ * "import CLI / legacy edits into the settings namespace" read.
98
+ */
99
+ export function readMatrixFileIfExists() {
100
+ if (!existsSync(MATRIX_PATH))
101
+ return undefined;
102
+ try {
103
+ return parseMatrix(readFileSync(MATRIX_PATH, "utf8"));
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ /** Structural equality over the small JSON matrix (stringify equality suffices). */
110
+ export function matrixEquals(a, b) {
111
+ return JSON.stringify(a) === JSON.stringify(b);
112
+ }
270
113
  function writeMatrixFile(path, text) {
271
114
  mkdirSync(dirname(path), { recursive: true });
272
115
  writeFileSync(path, text, "utf8");
@@ -397,8 +240,13 @@ async function locallyModified(dir, meta) {
397
240
  }
398
241
  return undefined;
399
242
  }
400
- export async function runSync(flags, harnessNodeModules, log = console.log) {
401
- const matrix = loadMatrix(flags, log);
243
+ /**
244
+ * Render and materialize the presets from an ALREADY-RESOLVED matrix. This is
245
+ * the shared core between the CLI file path (`runSync`) and the settings
246
+ * namespace path (host row): the matrix comes from the caller, everything
247
+ * after `loadMatrix(...)` is here.
248
+ */
249
+ export async function runSyncWithMatrix(matrix, flags, log = console.log) {
402
250
  const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
403
251
  const sourceVersion = manifest.version;
404
252
  const presetsSourceDir = join(PACKAGE_ROOT, "presets");
@@ -473,8 +321,9 @@ export async function runSync(flags, harnessNodeModules, log = console.log) {
473
321
  if (!existsSync(sourceFile)) {
474
322
  throw new Error("omd-dsh sync: missing vendored source " + sourceFile + " -- run \`npm run build\` first");
475
323
  }
476
- let sourceText = await fs.readFile(sourceFile, "utf8");
477
- sourceText = rewriteImports(sourceText, harnessNodeModules);
324
+ // Self-contained bundles: copied verbatim, no import rewriting, no
325
+ // harness tree knowledge (see the module doc comment).
326
+ const sourceText = await fs.readFile(sourceFile, "utf8");
478
327
  const sourceHash = sha256(sourceText);
479
328
  const destFile = join(vendorTargetDir, vendorName);
480
329
  let action = "synced";
@@ -507,7 +356,7 @@ export async function runSync(flags, harnessNodeModules, log = console.log) {
507
356
  }
508
357
  if (!flags.dryRun && VENDOR_SOURCES.length > 0) {
509
358
  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");
359
+ await fs.writeFile(join(vendorTargetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextVendorFiles }, null, 2) + "\n", "utf8");
511
360
  }
512
361
  }
513
362
  if (existsSync(agentPresetsRoot)) {
@@ -539,7 +388,7 @@ export async function runSync(flags, harnessNodeModules, log = console.log) {
539
388
  }
540
389
  log("omd-dsh sync: DSH_HOME=" + dshHome());
541
390
  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);
391
+ log("omd-dsh sync: vendored rows are self-contained bundles (no harness tree dependency)");
543
392
  log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
544
393
  for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
545
394
  for (const line of report[key])
@@ -547,3 +396,7 @@ export async function runSync(flags, harnessNodeModules, log = console.log) {
547
396
  const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
548
397
  log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
549
398
  }
399
+ /** CLI / manual-fallback path: load the matrix from <DSH_HOME>/omd-matrix.json (generating/migrating it on first run), then render. */
400
+ export async function runSync(flags, log = console.log) {
401
+ await runSyncWithMatrix(loadMatrix(flags, log), flags, log);
402
+ }
package/lib/task.js CHANGED
@@ -1,7 +1,6 @@
1
1
  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
- import { scopeOf } from "@deepseek-ai/dsh-scope";
5
4
  import { modeOverrideFor } from "./shared.js";
6
5
  /**
7
6
  * @module @carljia/omd-dsh/task
@@ -136,9 +135,7 @@ function resolveTier(config, requested) {
136
135
  throw new Error("omd_task: choose a tier for this task -- valid tiers: " + tierNames.join(", "));
137
136
  }
138
137
  function apply(ctx, config) {
139
- if (scopeOf(ctx) === undefined) {
140
- throw new Error("omd-task: refusing to mount outside a scoped context; mount this row inside an agent preset");
141
- }
138
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
142
139
  const invalid = configError(config);
143
140
  if (invalid !== undefined)
144
141
  throw new Error(invalid);