ndomo 0.2.1 → 0.2.2

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.
@@ -134,6 +134,36 @@
134
134
  "model": "opencode-go/kimi-k2.7-code",
135
135
  "temperature": 0.2,
136
136
  "reasoning_effort": "high"
137
+ },
138
+ "craftsman": {
139
+ "model": "minimax/MiniMax-M3",
140
+ "temperature": 0.3,
141
+ "reasoning_effort": "high"
142
+ },
143
+ "warden": {
144
+ "model": "minimax/MiniMax-M3",
145
+ "temperature": 0.3,
146
+ "reasoning_effort": "high"
147
+ },
148
+ "ci-smith": {
149
+ "model": "opencode-go/deepseek-v4-flash",
150
+ "temperature": 0.1,
151
+ "reasoning_effort": "high"
152
+ },
153
+ "deploy-smith": {
154
+ "model": "opencode-go/deepseek-v4-flash",
155
+ "temperature": 0.1,
156
+ "reasoning_effort": "high"
157
+ },
158
+ "release-smith": {
159
+ "model": "opencode-go/deepseek-v4-flash",
160
+ "temperature": 0.1,
161
+ "reasoning_effort": "high"
162
+ },
163
+ "ops-scout": {
164
+ "model": "opencode-go/deepseek-v4-flash",
165
+ "temperature": 0.2,
166
+ "reasoning_effort": "medium"
137
167
  }
138
168
  },
139
169
  "budget": {
@@ -216,6 +246,36 @@
216
246
  "model": "opencode-go/deepseek-v4-flash",
217
247
  "temperature": 0.2,
218
248
  "reasoning_effort": "low"
249
+ },
250
+ "craftsman": {
251
+ "model": "opencode-go/deepseek-v4-flash",
252
+ "temperature": 0.3,
253
+ "reasoning_effort": "low"
254
+ },
255
+ "warden": {
256
+ "model": "opencode-go/deepseek-v4-flash",
257
+ "temperature": 0.3,
258
+ "reasoning_effort": "low"
259
+ },
260
+ "ci-smith": {
261
+ "model": "opencode-go/deepseek-v4-flash",
262
+ "temperature": 0.1,
263
+ "reasoning_effort": "low"
264
+ },
265
+ "deploy-smith": {
266
+ "model": "opencode-go/deepseek-v4-flash",
267
+ "temperature": 0.1,
268
+ "reasoning_effort": "low"
269
+ },
270
+ "release-smith": {
271
+ "model": "opencode-go/deepseek-v4-flash",
272
+ "temperature": 0.1,
273
+ "reasoning_effort": "low"
274
+ },
275
+ "ops-scout": {
276
+ "model": "opencode-go/deepseek-v4-flash",
277
+ "temperature": 0.2,
278
+ "reasoning_effort": "low"
219
279
  }
220
280
  }
221
281
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ndomo",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "OpenCode multi-agent plugin. Taller de artesanos: foreman + 3 peer primaries (craftsman, warden, ranger) + specialists (scout, scribe, painter, smith, sage, guild, inspector, chronicler) + stack-smiths + ops (ci-smith, deploy-smith, release-smith, ops-scout). Caveman-native. opencode-mem integrated. DCP peer optional.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@ import {
25
25
  writeHttpBlock,
26
26
  applyPresetToFile,
27
27
  applyProviderPrefix,
28
+ promptHttpEnable,
28
29
  stepRegisterPlugins,
29
30
  stepCopyAgents,
30
31
  stepCopySkills,
@@ -731,3 +732,57 @@ describe("stepCopyTools", () => {
731
732
  expect(copied).toBe(0);
732
733
  });
733
734
  });
735
+
736
+ // ─── Regex-based preset preserves nested YAML ──────────────────────────────
737
+
738
+ describe("applyPresetToFile — nested permission preservation", () => {
739
+ test("preserves nested permission structure byte-for-byte", () => {
740
+ const testFile = join(tmpDir, "test-agent.md");
741
+ const original = `---
742
+ description: Test Agent
743
+ mode: subagent
744
+ model: old/model
745
+ temperature: 0.5
746
+ permission:
747
+ edit: allow
748
+ write: ask
749
+ bash:
750
+ "*": ask
751
+ "ls *": allow
752
+ ---
753
+ body content here
754
+ `;
755
+ writeFileSync(testFile, original);
756
+ const result = applyPresetToFile(
757
+ testFile,
758
+ { model: "new/model", temperature: 0.3 } as PresetEntry,
759
+ false,
760
+ );
761
+ expect(result).toBe("updated");
762
+ const updated = readFileSync(testFile, "utf-8");
763
+ expect(updated).toContain("model: new/model");
764
+ expect(updated).toContain("temperature: 0.3");
765
+ // CRITICAL: permission block intact byte-for-byte
766
+ expect(updated).toContain("permission:");
767
+ expect(updated).toContain(" edit: allow");
768
+ expect(updated).toContain(" write: ask");
769
+ expect(updated).toContain(" bash:");
770
+ expect(updated).toContain(' "*": ask');
771
+ expect(updated).toContain(' "ls *": allow');
772
+ });
773
+ });
774
+
775
+ // ─── Non-TTY promptHttpEnable ──────────────────────────────────────────────
776
+
777
+ describe("promptHttpEnable — non-TTY fallback", () => {
778
+ test("returns false immediately when stdin not TTY", async () => {
779
+ const originalIsTTY = process.stdin.isTTY;
780
+ Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
781
+ try {
782
+ const result = await promptHttpEnable();
783
+ expect(result).toBe(false);
784
+ } finally {
785
+ Object.defineProperty(process.stdin, "isTTY", { value: originalIsTTY, configurable: true });
786
+ }
787
+ });
788
+ });
@@ -342,125 +342,60 @@ export function stepCopySkills(
342
342
  // ─── Preset application ──────────────────────────────────────────────────────
343
343
 
344
344
  /**
345
- * Parse YAML frontmatter from an agent .md file.
346
- * Returns { frontmatter: Record<string, string>, body: string, raw: string }.
347
- * Frontmatter is between the first two '---' lines.
348
- */
349
- export function parseFrontmatter(content: string): {
350
- frontmatter: Record<string, string>;
351
- body: string;
352
- startIdx: number;
353
- endIdx: number;
354
- } {
355
- const lines = content.split("\n");
356
- let startIdx = -1;
357
- let endIdx = -1;
358
-
359
- for (let i = 0; i < lines.length; i++) {
360
- const line = lines[i];
361
- if (line !== undefined && line.trim() === "---") {
362
- if (startIdx === -1) {
363
- startIdx = i;
364
- } else {
365
- endIdx = i;
366
- break;
367
- }
368
- }
369
- }
370
-
371
- if (startIdx === -1 || endIdx === -1) {
372
- return { frontmatter: {}, body: content, startIdx: -1, endIdx: -1 };
373
- }
374
-
375
- const fm: Record<string, string> = {};
376
- for (let i = startIdx + 1; i < endIdx; i++) {
377
- const line = lines[i];
378
- if (line === undefined) continue;
379
- const colonIdx = line.indexOf(":");
380
- if (colonIdx > 0) {
381
- const key = line.slice(0, colonIdx).trim();
382
- const value = line.slice(colonIdx + 1).trim();
383
- fm[key] = value;
384
- }
385
- }
386
-
387
- const body = lines.slice(endIdx + 1).join("\n");
388
- return { frontmatter: fm, body, startIdx, endIdx };
389
- }
390
-
391
- /**
392
- * Serialize frontmatter + body back to a string.
393
- */
394
- export function serializeFrontmatter(frontmatter: Record<string, string>, body: string): string {
395
- const fmLines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`);
396
- return `---\n${fmLines.join("\n")}\n---\n${body}`;
397
- }
398
-
399
- /**
400
- * Apply preset values to an agent .md file's frontmatter.
401
- * Handles reasoningEffort 3-tier insert fallback:
402
- * 1. Update existing reasoningEffort line
403
- * 2. Insert after temperature (if present)
404
- * 3. Insert after model (if present)
405
- * 4. Insert after opening ---
345
+ * Apply preset values to an agent .md file's frontmatter using regex-targeted
346
+ * line replacement. Preserves YAML structure 100% (including nested permission
347
+ * blocks) by only touching top-level (0-indent) key lines.
406
348
  */
407
349
  export function applyPresetToFile(
408
350
  filePath: string,
409
351
  preset: PresetEntry,
410
352
  dryRun: boolean,
411
353
  ): "updated" | "skipped" {
412
- const content = readFileSync(filePath, "utf-8");
413
- const { frontmatter, body, startIdx, endIdx } = parseFrontmatter(content);
354
+ let content = readFileSync(filePath, "utf-8");
414
355
 
415
- if (startIdx === -1 || endIdx === -1) {
356
+ // Find frontmatter bounds (first --- block at file start)
357
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
358
+ if (!fmMatch || !fmMatch[1] || !fmMatch[2]) {
416
359
  warn(`No frontmatter found in ${basename(filePath)}, skipping`);
417
360
  return "skipped";
418
361
  }
419
-
362
+ const fmBody = fmMatch[1];
363
+ const body = fmMatch[2];
364
+ let newFmBody = fmBody;
420
365
  let changed = false;
421
366
 
422
- // Apply model
423
- if (preset.model) {
424
- frontmatter["model"] = preset.model;
425
- changed = true;
426
- }
427
-
428
- // Apply temperature
429
- if (preset.temperature !== undefined) {
430
- frontmatter["temperature"] = String(preset.temperature);
431
- changed = true;
432
- }
433
-
434
- // Apply reasoningEffort (snake_case camelCase)
435
- if (preset.reasoning_effort) {
436
- frontmatter["reasoningEffort"] = preset.reasoning_effort;
437
- changed = true;
367
+ // Update or insert each preset field at top-level (0 indent)
368
+ const fields: Array<{ yamlName: string; value: string | undefined }> = [
369
+ { yamlName: "model", value: preset.model },
370
+ { yamlName: "temperature", value: preset.temperature !== undefined ? String(preset.temperature) : undefined },
371
+ { yamlName: "reasoningEffort", value: preset.reasoning_effort },
372
+ ];
373
+
374
+ for (const field of fields) {
375
+ if (field.value === undefined) continue;
376
+ // Match line at start-of-line (no indent), so we don't touch nested keys
377
+ const lineRegex = new RegExp(`^${field.yamlName}:.*$`, "m");
378
+ if (lineRegex.test(newFmBody)) {
379
+ newFmBody = newFmBody.replace(lineRegex, `${field.yamlName}: ${field.value}`);
380
+ changed = true;
381
+ } else {
382
+ // Append at end of frontmatter (top-level, 0 indent)
383
+ const sep = newFmBody.endsWith("\n") ? "" : "\n";
384
+ newFmBody = newFmBody + sep + `${field.yamlName}: ${field.value}\n`;
385
+ changed = true;
386
+ }
438
387
  }
439
388
 
440
389
  if (!changed) {
441
390
  return "skipped";
442
391
  }
443
392
 
444
- // Serialize back preserving order: model, temperature, reasoningEffort, then others
445
- const ordered: Record<string, string> = {};
446
- const priority = ["model", "temperature", "reasoningEffort"];
447
- for (const key of priority) {
448
- if (frontmatter[key] !== undefined) {
449
- ordered[key] = frontmatter[key];
450
- }
451
- }
452
- for (const [key, value] of Object.entries(frontmatter)) {
453
- if (!(key in ordered)) {
454
- ordered[key] = value;
455
- }
456
- }
457
-
393
+ const newContent = `---\n${newFmBody}---\n${body}`;
458
394
  if (dryRun) {
459
- info(`[dry-run] would update ${basename(filePath)}: model=${preset.model ?? "(keep)"}, temp=${preset.temperature ?? "(keep)"}, effort=${preset.reasoning_effort ?? "(keep)"}`);
395
+ info(`[dry-run] would update ${basename(filePath)}`);
460
396
  } else {
461
- writeFileSync(filePath, serializeFrontmatter(ordered, body));
397
+ writeFileSync(filePath, newContent);
462
398
  }
463
-
464
399
  return "updated";
465
400
  }
466
401
 
@@ -1000,6 +935,10 @@ export function writeHttpBlock(projectRoot: string, httpConfig: HttpConfig, dryR
1000
935
  * Returns true if user accepts, false otherwise.
1001
936
  */
1002
937
  export async function promptHttpEnable(): Promise<boolean> {
938
+ if (!process.stdin.isTTY) {
939
+ info("Non-TTY stdin — skipping HTTP prompt (use --enable-http to enable)");
940
+ return false;
941
+ }
1003
942
  console.log("");
1004
943
  console.log("[?] Enable ndomo HTTP server? Allows programmatic plan/task control via API.");
1005
944
  console.log(" Recommended for users integrating ndomo with other tools (port 4097, auth required).");