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.
- package/config/ndomo.config.json +60 -0
- package/package.json +1 -1
- package/src/cli/__tests__/install.test.ts +55 -0
- package/src/cli/install.ts +37 -98
package/config/ndomo.config.json
CHANGED
|
@@ -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.
|
|
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
|
+
});
|
package/src/cli/install.ts
CHANGED
|
@@ -342,125 +342,60 @@ export function stepCopySkills(
|
|
|
342
342
|
// ─── Preset application ──────────────────────────────────────────────────────
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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
|
-
|
|
413
|
-
const { frontmatter, body, startIdx, endIdx } = parseFrontmatter(content);
|
|
354
|
+
let content = readFileSync(filePath, "utf-8");
|
|
414
355
|
|
|
415
|
-
|
|
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
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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)}
|
|
395
|
+
info(`[dry-run] would update ${basename(filePath)}`);
|
|
460
396
|
} else {
|
|
461
|
-
writeFileSync(filePath,
|
|
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).");
|