@microck/canonfig 2.0.0 → 2.1.0
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 +1 -1
- package/dist/cli/cli.js +1 -1
- package/dist/harness-configuration/adapters/amp.js +87 -0
- package/dist/harness-configuration/adapters/antigravity.js +50 -0
- package/dist/harness-configuration/adapters/claude.js +43 -0
- package/dist/harness-configuration/adapters/codex.js +60 -0
- package/dist/harness-configuration/adapters/copilot.js +79 -0
- package/dist/harness-configuration/adapters/cursor.js +67 -0
- package/dist/harness-configuration/adapters/descriptor.js +10 -0
- package/dist/harness-configuration/adapters/devin.js +66 -0
- package/dist/harness-configuration/adapters/droid.js +32 -0
- package/dist/harness-configuration/adapters/grok.js +44 -0
- package/dist/harness-configuration/adapters/hermes.js +105 -0
- package/dist/harness-configuration/adapters/index.js +50 -0
- package/dist/harness-configuration/adapters/kilo.js +18 -0
- package/dist/harness-configuration/adapters/kimi.js +148 -0
- package/dist/harness-configuration/adapters/omp.js +64 -0
- package/dist/harness-configuration/adapters/open-code-family.js +94 -0
- package/dist/harness-configuration/adapters/opencode.js +18 -0
- package/dist/harness-configuration/adapters/pi.js +93 -0
- package/dist/harness-configuration/adapters/qwen.js +164 -0
- package/dist/harness-configuration/adapters/shared-common.js +66 -0
- package/dist/harness-configuration/adapters/shared-documents.js +117 -0
- package/dist/harness-configuration/adapters/shared-hooks.js +186 -0
- package/dist/harness-configuration/adapters/shared-mcp.js +214 -0
- package/dist/harness-configuration/adapters/shared.js +4 -0
- package/dist/harness-configuration/adapters/tools.js +24 -0
- package/dist/harness-configuration/cli-arguments.js +105 -0
- package/dist/harness-configuration/cli-output.js +77 -0
- package/dist/harness-configuration/cli.js +196 -0
- package/dist/harness-configuration/core/compiler.js +175 -0
- package/dist/harness-configuration/core/config.js +74 -0
- package/dist/harness-configuration/core/diff.js +60 -0
- package/dist/harness-configuration/core/doctor.js +40 -0
- package/dist/harness-configuration/core/errors.js +13 -0
- package/dist/harness-configuration/core/filesystem.js +172 -0
- package/dist/harness-configuration/core/frontmatter.js +49 -0
- package/dist/harness-configuration/core/hash.js +4 -0
- package/dist/harness-configuration/core/path.js +50 -0
- package/dist/harness-configuration/core/planner.js +255 -0
- package/dist/harness-configuration/core/render-cleanup.js +91 -0
- package/dist/harness-configuration/core/render-json.js +195 -0
- package/dist/harness-configuration/core/render-text.js +134 -0
- package/dist/harness-configuration/core/render-utils.js +202 -0
- package/dist/harness-configuration/core/render.js +54 -0
- package/dist/harness-configuration/core/scaffold.js +103 -0
- package/dist/harness-configuration/core/schema-components.js +167 -0
- package/dist/harness-configuration/core/schema-config.js +98 -0
- package/dist/harness-configuration/core/schema-runtime.js +143 -0
- package/dist/harness-configuration/core/schema-types.js +8 -0
- package/dist/harness-configuration/core/schema.js +13 -0
- package/dist/harness-configuration/core/state.js +42 -0
- package/dist/harness-configuration/core/types.js +5 -0
- package/dist/harness-configuration/core/validation.js +113 -0
- package/dist/harness-configuration/templates/runtime.js +212 -0
- package/dist/runtime/main.js +20 -14
- package/package.json +5 -5
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function deepEqual(left, right) {
|
|
5
|
+
if (Object.is(left, right))
|
|
6
|
+
return true;
|
|
7
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
8
|
+
return left.length === right.length
|
|
9
|
+
&& left.every((value, index) => deepEqual(value, right[index]));
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(left) && isRecord(right)) {
|
|
12
|
+
const leftKeys = Object.keys(left).sort();
|
|
13
|
+
const rightKeys = Object.keys(right).sort();
|
|
14
|
+
return deepEqual(leftKeys, rightKeys)
|
|
15
|
+
&& leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function stripJsonComments(text) {
|
|
20
|
+
let output = "";
|
|
21
|
+
let index = 0;
|
|
22
|
+
let inString = false;
|
|
23
|
+
let escaped = false;
|
|
24
|
+
while (index < text.length) {
|
|
25
|
+
const character = text[index];
|
|
26
|
+
const next = text[index + 1];
|
|
27
|
+
if (inString) {
|
|
28
|
+
output += character;
|
|
29
|
+
if (escaped)
|
|
30
|
+
escaped = false;
|
|
31
|
+
else if (character === "\\")
|
|
32
|
+
escaped = true;
|
|
33
|
+
else if (character === '"')
|
|
34
|
+
inString = false;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (character === '"') {
|
|
39
|
+
inString = true;
|
|
40
|
+
output += character;
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (character === "/" && next === "/") {
|
|
45
|
+
while (index < text.length && text[index] !== "\n")
|
|
46
|
+
index += 1;
|
|
47
|
+
output += "\n";
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (character === "/" && next === "*") {
|
|
52
|
+
index += 2;
|
|
53
|
+
while (index + 1 < text.length
|
|
54
|
+
&& !(text[index] === "*" && text[index + 1] === "/")) {
|
|
55
|
+
if (text[index] === "\n")
|
|
56
|
+
output += "\n";
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
index += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
output += character;
|
|
63
|
+
index += 1;
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
function removeTrailingCommas(text) {
|
|
68
|
+
let output = "";
|
|
69
|
+
let inString = false;
|
|
70
|
+
let escaped = false;
|
|
71
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
72
|
+
const character = text[index];
|
|
73
|
+
if (inString) {
|
|
74
|
+
output += character;
|
|
75
|
+
if (escaped)
|
|
76
|
+
escaped = false;
|
|
77
|
+
else if (character === "\\")
|
|
78
|
+
escaped = true;
|
|
79
|
+
else if (character === '"')
|
|
80
|
+
inString = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (character === '"') {
|
|
84
|
+
inString = true;
|
|
85
|
+
output += character;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (character === ",") {
|
|
89
|
+
let cursor = index + 1;
|
|
90
|
+
while (cursor < text.length && /\s/u.test(text[cursor]))
|
|
91
|
+
cursor += 1;
|
|
92
|
+
if (text[cursor] === "}" || text[cursor] === "]")
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
output += character;
|
|
96
|
+
}
|
|
97
|
+
return output;
|
|
98
|
+
}
|
|
99
|
+
export function parseJsonDocument(text, conflicts) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(removeTrailingCommas(stripJsonComments(text)));
|
|
102
|
+
if (!isRecord(parsed)) {
|
|
103
|
+
conflicts.push("JSON/JSONC root must be an object.");
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
conflicts.push(`Invalid JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function serializeJsonDocument(document) {
|
|
114
|
+
return `${JSON.stringify(document, undefined, 2)}\n`;
|
|
115
|
+
}
|
|
116
|
+
export function getAtPath(root, path) {
|
|
117
|
+
let current = root;
|
|
118
|
+
for (const segment of path) {
|
|
119
|
+
if (!isRecord(current))
|
|
120
|
+
return undefined;
|
|
121
|
+
current = current[segment];
|
|
122
|
+
}
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
export function setAtPath(root, path, value) {
|
|
126
|
+
if (path.length === 0) {
|
|
127
|
+
if (!isRecord(value))
|
|
128
|
+
throw new TypeError("JSON root replacement must be an object");
|
|
129
|
+
for (const key of Object.keys(root))
|
|
130
|
+
delete root[key];
|
|
131
|
+
Object.assign(root, value);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let parent = root;
|
|
135
|
+
for (const segment of path.slice(0, -1)) {
|
|
136
|
+
const current = parent[segment];
|
|
137
|
+
if (!isRecord(current))
|
|
138
|
+
parent[segment] = {};
|
|
139
|
+
parent = parent[segment];
|
|
140
|
+
}
|
|
141
|
+
const key = path[path.length - 1];
|
|
142
|
+
if (value === undefined)
|
|
143
|
+
delete parent[key];
|
|
144
|
+
else
|
|
145
|
+
parent[key] = value;
|
|
146
|
+
}
|
|
147
|
+
export function commentMarkers(marker, style) {
|
|
148
|
+
const prefix = style === "html" ? "<!-- " : style === "slash" ? "// " : "# ";
|
|
149
|
+
const suffix = style === "html" ? " -->" : "";
|
|
150
|
+
return {
|
|
151
|
+
begin: `${prefix}canonfig:begin ${marker}${suffix}`,
|
|
152
|
+
end: `${prefix}canonfig:end ${marker}${suffix}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function locateBlock(text, begin, end) {
|
|
156
|
+
const start = text.indexOf(begin);
|
|
157
|
+
if (start < 0)
|
|
158
|
+
return undefined;
|
|
159
|
+
const endStart = text.indexOf(end, start + begin.length);
|
|
160
|
+
if (endStart < 0)
|
|
161
|
+
return undefined;
|
|
162
|
+
let endOffset = endStart + end.length;
|
|
163
|
+
if (text[endOffset] === "\r" && text[endOffset + 1] === "\n")
|
|
164
|
+
endOffset += 2;
|
|
165
|
+
else if (text[endOffset] === "\n")
|
|
166
|
+
endOffset += 1;
|
|
167
|
+
return { start, end: endOffset, block: text.slice(start, endOffset) };
|
|
168
|
+
}
|
|
169
|
+
export function identityOf(value, identity) {
|
|
170
|
+
return identity === undefined || !isRecord(value) ? value : value[identity];
|
|
171
|
+
}
|
|
172
|
+
export function containsMarker(value, marker) {
|
|
173
|
+
if (typeof value === "string")
|
|
174
|
+
return value.includes(marker);
|
|
175
|
+
if (Array.isArray(value))
|
|
176
|
+
return value.some((item) => containsMarker(item, marker));
|
|
177
|
+
return isRecord(value)
|
|
178
|
+
&& Object.values(value).some((item) => containsMarker(item, marker));
|
|
179
|
+
}
|
|
180
|
+
export function tomlBlockMarkers(marker) {
|
|
181
|
+
return {
|
|
182
|
+
begin: `# canonfig:begin ${marker}`,
|
|
183
|
+
end: `# canonfig:end ${marker}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function findTomlSection(lines, section) {
|
|
187
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
188
|
+
const target = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, "u");
|
|
189
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
190
|
+
if (!target.test(lines[index] ?? ""))
|
|
191
|
+
continue;
|
|
192
|
+
let end = lines.length;
|
|
193
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
194
|
+
if (/^\s*\[\[?.+?\]\]?\s*(?:#.*)?$/u.test(lines[cursor] ?? "")) {
|
|
195
|
+
end = cursor;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { header: index, end };
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { sha256 } from "./hash.js";
|
|
2
|
+
import { unapplyPrevious } from "./render-cleanup.js";
|
|
3
|
+
import { applyJsonArtifact } from "./render-json.js";
|
|
4
|
+
import { appendManagedText, applyTomlArtifact } from "./render-text.js";
|
|
5
|
+
export function renderArtifacts(artifacts, current, previous, force = false) {
|
|
6
|
+
const conflicts = [];
|
|
7
|
+
const output = unapplyPrevious(current, previous, force, conflicts);
|
|
8
|
+
const cleanup = [];
|
|
9
|
+
if (artifacts.length === 0)
|
|
10
|
+
return { content: output, cleanup, conflicts };
|
|
11
|
+
const replacements = artifacts.filter((artifact) => artifact.kind === "replace");
|
|
12
|
+
if (replacements.length > 0) {
|
|
13
|
+
if (artifacts.length !== 1) {
|
|
14
|
+
conflicts.push("A replace artifact cannot share a path with merge artifacts.");
|
|
15
|
+
}
|
|
16
|
+
const replacement = replacements[0];
|
|
17
|
+
if (replacement === undefined)
|
|
18
|
+
return { content: output, cleanup, conflicts };
|
|
19
|
+
if (previous === undefined
|
|
20
|
+
&& current !== undefined
|
|
21
|
+
&& sha256(current) !== sha256(replacement.content)
|
|
22
|
+
&& !force) {
|
|
23
|
+
conflicts.push("File already exists and is not owned by Canonfig.");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
content: replacement.content,
|
|
27
|
+
cleanup: [{ kind: "replace" }],
|
|
28
|
+
conflicts,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (output instanceof Uint8Array) {
|
|
32
|
+
conflicts.push("Cannot merge text configuration into an existing binary file.");
|
|
33
|
+
return { content: output, cleanup, conflicts };
|
|
34
|
+
}
|
|
35
|
+
let text = output ?? "";
|
|
36
|
+
for (const artifact of artifacts) {
|
|
37
|
+
if (artifact.kind === "managed-text") {
|
|
38
|
+
const result = appendManagedText(text, artifact, force, conflicts);
|
|
39
|
+
text = result.text;
|
|
40
|
+
cleanup.push(result.cleanup);
|
|
41
|
+
}
|
|
42
|
+
else if (artifact.kind === "json") {
|
|
43
|
+
const result = applyJsonArtifact(text, artifact, force, conflicts);
|
|
44
|
+
text = result.text;
|
|
45
|
+
cleanup.push(...result.cleanup);
|
|
46
|
+
}
|
|
47
|
+
else if (artifact.kind === "toml") {
|
|
48
|
+
const result = applyTomlArtifact(text, artifact, force, conflicts);
|
|
49
|
+
text = result.text;
|
|
50
|
+
cleanup.push(...result.cleanup);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { content: text, cleanup, conflicts };
|
|
54
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { TARGET_IDS } from "./types.js";
|
|
5
|
+
import { CanonfigError } from "./errors.js";
|
|
6
|
+
import { assertRealPathInside, resolveInside } from "./path.js";
|
|
7
|
+
const ROOT_INSTRUCTIONS = `# Repository instructions
|
|
8
|
+
|
|
9
|
+
Describe the project, architecture, validation commands, constraints, and definition of done here.
|
|
10
|
+
`;
|
|
11
|
+
const EXAMPLE_RULE = `# Source-code rules
|
|
12
|
+
|
|
13
|
+
- Keep changes scoped.
|
|
14
|
+
- Run the smallest relevant validation command before finishing.
|
|
15
|
+
`;
|
|
16
|
+
const EXAMPLE_AGENT = `Review the requested change for correctness, security, regressions, and missing tests.
|
|
17
|
+
Return concrete findings before general commentary.
|
|
18
|
+
`;
|
|
19
|
+
const EXAMPLE_COMMAND = `Inspect the current changes, run relevant checks, and produce a release-readiness report.
|
|
20
|
+
`;
|
|
21
|
+
const EXAMPLE_SKILL = `---
|
|
22
|
+
name: repository-checks
|
|
23
|
+
description: Discover and run the repository's relevant validation commands.
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Repository checks
|
|
27
|
+
|
|
28
|
+
1. Inspect package and build metadata.
|
|
29
|
+
2. Select the narrowest relevant checks.
|
|
30
|
+
3. Report commands, results, and unresolved failures.
|
|
31
|
+
`;
|
|
32
|
+
async function writeNew(root, relativePath, content, force) {
|
|
33
|
+
const filePath = resolveInside(root, relativePath);
|
|
34
|
+
await assertRealPathInside(root, filePath);
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(filePath);
|
|
37
|
+
if (!force)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== "ENOENT")
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
await assertRealPathInside(root, filePath);
|
|
45
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
46
|
+
await assertRealPathInside(root, filePath);
|
|
47
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
export async function scaffoldProject(root, options = {}) {
|
|
51
|
+
const targets = [...new Set(options.targets ?? TARGET_IDS)];
|
|
52
|
+
if (targets.length === 0)
|
|
53
|
+
throw new CanonfigError("TARGET_EMPTY", "At least one target is required for init.");
|
|
54
|
+
const force = options.force ?? false;
|
|
55
|
+
const config = {
|
|
56
|
+
version: 1,
|
|
57
|
+
project: { name: path.basename(path.resolve(root)) },
|
|
58
|
+
targets: Object.fromEntries(targets.map((target) => [target, { enabled: true, options: {} }])),
|
|
59
|
+
instructions: {
|
|
60
|
+
root: "instructions/AGENTS.md",
|
|
61
|
+
rules: [{
|
|
62
|
+
id: "source",
|
|
63
|
+
file: "rules/source.md",
|
|
64
|
+
paths: ["src/**", "tests/**"],
|
|
65
|
+
activation: "path",
|
|
66
|
+
description: "Rules for source and test files",
|
|
67
|
+
}],
|
|
68
|
+
},
|
|
69
|
+
skills: { roots: ["skills"] },
|
|
70
|
+
mcp: { servers: {} },
|
|
71
|
+
hooks: [],
|
|
72
|
+
agents: [{
|
|
73
|
+
id: "reviewer",
|
|
74
|
+
file: "agents/reviewer.md",
|
|
75
|
+
description: "Reviews changes for correctness and regressions",
|
|
76
|
+
model: "inherit",
|
|
77
|
+
tools: ["read", "search", "test", "git"],
|
|
78
|
+
writable: false,
|
|
79
|
+
}],
|
|
80
|
+
commands: [{
|
|
81
|
+
id: "release-check",
|
|
82
|
+
file: "commands/release-check.md",
|
|
83
|
+
description: "Run a release-readiness review",
|
|
84
|
+
argumentHint: "[scope]",
|
|
85
|
+
}],
|
|
86
|
+
permissions: { rules: [] },
|
|
87
|
+
extensions: {},
|
|
88
|
+
};
|
|
89
|
+
const files = [
|
|
90
|
+
[".canonfig/harness.yaml", YAML.stringify(config, { lineWidth: 120 })],
|
|
91
|
+
[".canonfig/instructions/AGENTS.md", ROOT_INSTRUCTIONS],
|
|
92
|
+
[".canonfig/rules/source.md", EXAMPLE_RULE],
|
|
93
|
+
[".canonfig/agents/reviewer.md", EXAMPLE_AGENT],
|
|
94
|
+
[".canonfig/commands/release-check.md", EXAMPLE_COMMAND],
|
|
95
|
+
[".canonfig/skills/repository-checks/SKILL.md", EXAMPLE_SKILL],
|
|
96
|
+
];
|
|
97
|
+
const written = [];
|
|
98
|
+
for (const [relativePath, content] of files) {
|
|
99
|
+
if (await writeNew(root, relativePath, content, force))
|
|
100
|
+
written.push(relativePath);
|
|
101
|
+
}
|
|
102
|
+
return written;
|
|
103
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { TARGET_IDS } from "./types.js";
|
|
2
|
+
import { booleanValue, enumValue, idValue, objectValue, optionalString, positiveInteger, relativePath, secretRecord, stringArray, stringValue, } from "./schema-runtime.js";
|
|
3
|
+
import { CAPABILITIES, HOOK_EVENTS, } from "./schema-types.js";
|
|
4
|
+
export const MCP_NAME_PATTERN = /^[A-Za-z0-9._-]+$/u;
|
|
5
|
+
export function parseMcpServer(input, validator, path) {
|
|
6
|
+
const value = objectValue(input, validator, path);
|
|
7
|
+
const transport = enumValue(value.transport, ["stdio", "streamable-http", "sse"], validator, [...path, "transport"], "stdio");
|
|
8
|
+
const enabled = booleanValue(value.enabled, validator, [...path, "enabled"], true);
|
|
9
|
+
const timeoutMs = positiveInteger(value.timeoutMs, validator, [...path, "timeoutMs"]);
|
|
10
|
+
const enabledTools = value.enabledTools === undefined
|
|
11
|
+
? undefined
|
|
12
|
+
: stringArray(value.enabledTools, validator, [...path, "enabledTools"]);
|
|
13
|
+
const disabledTools = value.disabledTools === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: stringArray(value.disabledTools, validator, [...path, "disabledTools"]);
|
|
16
|
+
const common = {
|
|
17
|
+
enabled,
|
|
18
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
19
|
+
...(enabledTools === undefined ? {} : { enabledTools }),
|
|
20
|
+
...(disabledTools === undefined ? {} : { disabledTools }),
|
|
21
|
+
};
|
|
22
|
+
if (transport === "stdio") {
|
|
23
|
+
const cwd = optionalString(value.cwd, validator, [...path, "cwd"]);
|
|
24
|
+
return {
|
|
25
|
+
...common,
|
|
26
|
+
transport,
|
|
27
|
+
command: stringValue(value.command, validator, [...path, "command"], {
|
|
28
|
+
min: 1,
|
|
29
|
+
}),
|
|
30
|
+
args: stringArray(value.args, validator, [...path, "args"]),
|
|
31
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
32
|
+
env: secretRecord(value.env, validator, [...path, "env"]),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const url = stringValue(value.url, validator, [...path, "url"], { min: 1 });
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
39
|
+
validator.issue([...path, "url"], "Expected an HTTP(S) URL.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
validator.issue([...path, "url"], "Expected a valid URL.");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...common,
|
|
47
|
+
transport,
|
|
48
|
+
url,
|
|
49
|
+
headers: secretRecord(value.headers, validator, [...path, "headers"]),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function parseHook(input, validator, path) {
|
|
53
|
+
const value = objectValue(input, validator, path);
|
|
54
|
+
const matcherValue = value.matcher === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: objectValue(value.matcher, validator, [...path, "matcher"]);
|
|
57
|
+
const inputRegex = optionalString(matcherValue.inputRegex, validator, [...path, "matcher", "inputRegex"]);
|
|
58
|
+
let capabilities = [];
|
|
59
|
+
if (matcherValue.capabilities !== undefined) {
|
|
60
|
+
if (!Array.isArray(matcherValue.capabilities)) {
|
|
61
|
+
validator.issue([...path, "matcher", "capabilities"], "Expected an array.");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
capabilities = matcherValue.capabilities.map((item, index) => enumValue(item, CAPABILITIES, validator, [...path, "matcher", "capabilities", index], "read"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const matcher = {
|
|
68
|
+
capabilities,
|
|
69
|
+
tools: stringArray(matcherValue.tools, validator, [...path, "matcher", "tools"]),
|
|
70
|
+
...(inputRegex === undefined ? {} : { inputRegex }),
|
|
71
|
+
};
|
|
72
|
+
const run = stringArray(value.run, validator, [...path, "run"]);
|
|
73
|
+
if (run.length === 0) {
|
|
74
|
+
validator.issue([...path, "run"], "Expected at least one command argument.");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: idValue(value.id, validator, [...path, "id"]),
|
|
78
|
+
event: enumValue(value.event, HOOK_EVENTS, validator, [...path, "event"], "before_tool"),
|
|
79
|
+
enabled: booleanValue(value.enabled, validator, [...path, "enabled"], true),
|
|
80
|
+
matcher,
|
|
81
|
+
run,
|
|
82
|
+
timeoutMs: positiveInteger(value.timeoutMs, validator, [...path, "timeoutMs"], 600_000) ?? 10_000,
|
|
83
|
+
onFailure: value.onFailure === undefined
|
|
84
|
+
? "block"
|
|
85
|
+
: enumValue(value.onFailure, ["block", "warn", "ignore"], validator, [...path, "onFailure"], "block"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function parseRule(input, validator, path) {
|
|
89
|
+
const value = objectValue(input, validator, path);
|
|
90
|
+
const activation = value.activation === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: enumValue(value.activation, ["always", "path", "manual", "model"], validator, [...path, "activation"], "always");
|
|
93
|
+
const description = optionalString(value.description, validator, [...path, "description"]);
|
|
94
|
+
return {
|
|
95
|
+
id: idValue(value.id, validator, [...path, "id"]),
|
|
96
|
+
file: relativePath(value.file, validator, [...path, "file"]),
|
|
97
|
+
paths: stringArray(value.paths, validator, [...path, "paths"]),
|
|
98
|
+
...(activation === undefined ? {} : { activation }),
|
|
99
|
+
...(description === undefined ? {} : { description }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function parseAgent(input, validator, path) {
|
|
103
|
+
const value = objectValue(input, validator, path);
|
|
104
|
+
const toolInput = value.tools === undefined ? ["read", "search"] : value.tools;
|
|
105
|
+
let tools = [];
|
|
106
|
+
if (!Array.isArray(toolInput)) {
|
|
107
|
+
validator.issue([...path, "tools"], "Expected an array.");
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
tools = toolInput.map((item, index) => enumValue(item, CAPABILITIES, validator, [...path, "tools", index], "read"));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: idValue(value.id, validator, [...path, "id"]),
|
|
114
|
+
file: relativePath(value.file, validator, [...path, "file"]),
|
|
115
|
+
description: stringValue(value.description, validator, [...path, "description"], { min: 1 }),
|
|
116
|
+
model: value.model === undefined
|
|
117
|
+
? "inherit"
|
|
118
|
+
: stringValue(value.model, validator, [...path, "model"]),
|
|
119
|
+
tools,
|
|
120
|
+
writable: booleanValue(value.writable, validator, [...path, "writable"], false),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function parseCommand(input, validator, path) {
|
|
124
|
+
const value = objectValue(input, validator, path);
|
|
125
|
+
const argumentHint = optionalString(value.argumentHint, validator, [...path, "argumentHint"]);
|
|
126
|
+
return {
|
|
127
|
+
id: idValue(value.id, validator, [...path, "id"]),
|
|
128
|
+
file: relativePath(value.file, validator, [...path, "file"]),
|
|
129
|
+
description: stringValue(value.description, validator, [...path, "description"], { min: 1 }),
|
|
130
|
+
...(argumentHint === undefined ? {} : { argumentHint }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function parsePermissionRule(input, validator, path) {
|
|
134
|
+
const value = objectValue(input, validator, path);
|
|
135
|
+
const reason = optionalString(value.reason, validator, [...path, "reason"]);
|
|
136
|
+
return {
|
|
137
|
+
pattern: stringValue(value.pattern, validator, [...path, "pattern"], {
|
|
138
|
+
min: 1,
|
|
139
|
+
}),
|
|
140
|
+
action: enumValue(value.action, ["allow", "ask", "deny"], validator, [...path, "action"], "ask"),
|
|
141
|
+
...(reason === undefined ? {} : { reason }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function parseTargetEntry(input, validator, path) {
|
|
145
|
+
const value = objectValue(input, validator, path);
|
|
146
|
+
return {
|
|
147
|
+
enabled: booleanValue(value.enabled, validator, [...path, "enabled"], true),
|
|
148
|
+
options: value.options === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: objectValue(value.options, validator, [...path, "options"]),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function parseTargets(input, validator, path) {
|
|
154
|
+
if (Array.isArray(input)) {
|
|
155
|
+
return input.map((value, index) => enumValue(value, TARGET_IDS, validator, [...path, index], "codex"));
|
|
156
|
+
}
|
|
157
|
+
const value = objectValue(input, validator, path);
|
|
158
|
+
const output = {};
|
|
159
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
160
|
+
if (!TARGET_IDS.includes(key)) {
|
|
161
|
+
validator.issue([...path, key], `Unknown target. Expected one of: ${TARGET_IDS.join(", ")}.`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
output[key] = parseTargetEntry(entry, validator, [...path, key]);
|
|
165
|
+
}
|
|
166
|
+
return output;
|
|
167
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { TARGET_IDS } from "./types.js";
|
|
2
|
+
import { objectValue, optionalString, relativePath, } from "./schema-runtime.js";
|
|
3
|
+
import { MCP_NAME_PATTERN, parseAgent, parseCommand, parseHook, parseMcpServer, parsePermissionRule, parseRule, parseTargets, } from "./schema-components.js";
|
|
4
|
+
function parsedArray(input, validator, path, parser) {
|
|
5
|
+
if (input === undefined)
|
|
6
|
+
return [];
|
|
7
|
+
if (!Array.isArray(input)) {
|
|
8
|
+
validator.issue(path, "Expected an array.");
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return input.map((item, index) => parser(item, validator, [...path, index]));
|
|
12
|
+
}
|
|
13
|
+
function duplicateIds(validator, label, items) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
if (seen.has(item.id))
|
|
17
|
+
validator.issue([], `Duplicate ${label} id: ${item.id}`);
|
|
18
|
+
seen.add(item.id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function parseConfig(input, validator, path) {
|
|
22
|
+
const value = objectValue(input, validator, path);
|
|
23
|
+
if (value.version !== 1)
|
|
24
|
+
validator.issue(["version"], "Expected literal value 1.");
|
|
25
|
+
const projectValue = value.project === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: objectValue(value.project, validator, ["project"]);
|
|
28
|
+
const projectName = optionalString(projectValue.name, validator, ["project", "name"]);
|
|
29
|
+
const instructionsValue = value.instructions === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: objectValue(value.instructions, validator, ["instructions"]);
|
|
32
|
+
const rules = parsedArray(instructionsValue.rules, validator, ["instructions", "rules"], parseRule);
|
|
33
|
+
const skillsValue = value.skills === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: objectValue(value.skills, validator, ["skills"]);
|
|
36
|
+
const rootsInput = skillsValue.roots === undefined ? ["skills"] : skillsValue.roots;
|
|
37
|
+
let roots = [];
|
|
38
|
+
if (!Array.isArray(rootsInput)) {
|
|
39
|
+
validator.issue(["skills", "roots"], "Expected an array.");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
roots = rootsInput.map((item, index) => relativePath(item, validator, ["skills", "roots", index]));
|
|
43
|
+
}
|
|
44
|
+
const mcpValue = value.mcp === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: objectValue(value.mcp, validator, ["mcp"]);
|
|
47
|
+
const serversValue = mcpValue.servers === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: objectValue(mcpValue.servers, validator, ["mcp", "servers"]);
|
|
50
|
+
const servers = {};
|
|
51
|
+
for (const [name, server] of Object.entries(serversValue)) {
|
|
52
|
+
if (!MCP_NAME_PATTERN.test(name)) {
|
|
53
|
+
validator.issue(["mcp", "servers", name], "Invalid MCP server name.");
|
|
54
|
+
}
|
|
55
|
+
servers[name] = parseMcpServer(server, validator, ["mcp", "servers", name]);
|
|
56
|
+
}
|
|
57
|
+
const hooks = parsedArray(value.hooks, validator, ["hooks"], parseHook);
|
|
58
|
+
const agents = parsedArray(value.agents, validator, ["agents"], parseAgent);
|
|
59
|
+
const commands = parsedArray(value.commands, validator, ["commands"], parseCommand);
|
|
60
|
+
const permissionsValue = value.permissions === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: objectValue(value.permissions, validator, ["permissions"]);
|
|
63
|
+
const permissionRules = parsedArray(permissionsValue.rules, validator, ["permissions", "rules"], parsePermissionRule);
|
|
64
|
+
const extensionsValue = value.extensions === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: objectValue(value.extensions, validator, ["extensions"]);
|
|
67
|
+
const extensions = {};
|
|
68
|
+
for (const [key, extension] of Object.entries(extensionsValue)) {
|
|
69
|
+
if (!TARGET_IDS.includes(key)) {
|
|
70
|
+
validator.issue(["extensions", key], `Unknown target. Expected one of: ${TARGET_IDS.join(", ")}.`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
extensions[key] = objectValue(extension, validator, ["extensions", key]);
|
|
74
|
+
}
|
|
75
|
+
duplicateIds(validator, "rule", rules);
|
|
76
|
+
duplicateIds(validator, "hook", hooks);
|
|
77
|
+
duplicateIds(validator, "agent", agents);
|
|
78
|
+
duplicateIds(validator, "command", commands);
|
|
79
|
+
const targets = value.targets === undefined
|
|
80
|
+
? (validator.issue(["targets"], "Required."), [])
|
|
81
|
+
: parseTargets(value.targets, validator, ["targets"]);
|
|
82
|
+
return {
|
|
83
|
+
version: 1,
|
|
84
|
+
project: projectName === undefined ? {} : { name: projectName },
|
|
85
|
+
targets,
|
|
86
|
+
instructions: {
|
|
87
|
+
root: relativePath(instructionsValue.root, validator, ["instructions", "root"], "instructions/AGENTS.md"),
|
|
88
|
+
rules,
|
|
89
|
+
},
|
|
90
|
+
skills: { roots },
|
|
91
|
+
mcp: { servers },
|
|
92
|
+
hooks,
|
|
93
|
+
agents,
|
|
94
|
+
commands,
|
|
95
|
+
permissions: { rules: permissionRules },
|
|
96
|
+
extensions,
|
|
97
|
+
};
|
|
98
|
+
}
|