@agentyx/core 0.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.d.mts +541 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +981 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
- package/schema/agentyx.schema.json +39 -0
- package/skills/angular-modern/SKILL.md +47 -0
- package/skills/planning/SKILL.md +39 -0
- package/skills/systematic-debugging/SKILL.md +43 -0
- package/skills/typescript-modern/SKILL.md +41 -0
- package/skills/verification/SKILL.md +38 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/errors.ts
|
|
7
|
+
/**
|
|
8
|
+
* Base class for every failure Agentyx raises deliberately.
|
|
9
|
+
*
|
|
10
|
+
* Consumers can catch `AgentyxError` to separate expected domain failures from
|
|
11
|
+
* unexpected runtime errors, and branch on the stable `code` when they need to
|
|
12
|
+
* handle a specific failure.
|
|
13
|
+
*/
|
|
14
|
+
var AgentyxError = class extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "AgentyxError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/config/errors.ts
|
|
24
|
+
/** Raised when the supplied directory has no `.agentyx.json`. */
|
|
25
|
+
var AgentyxConfigNotFoundError = class extends AgentyxError {
|
|
26
|
+
filePath;
|
|
27
|
+
constructor(filePath, options) {
|
|
28
|
+
super("agentyx_config_not_found", `Agentyx configuration file not found: ${filePath}`, options);
|
|
29
|
+
this.name = "AgentyxConfigNotFoundError";
|
|
30
|
+
this.filePath = filePath;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Raised when `.agentyx.json` is not valid JSON. */
|
|
34
|
+
var AgentyxConfigParseError = class extends AgentyxError {
|
|
35
|
+
filePath;
|
|
36
|
+
constructor(filePath, reason, options) {
|
|
37
|
+
super("agentyx_config_parse_error", `${filePath} is not valid JSON: ${reason}`, options);
|
|
38
|
+
this.name = "AgentyxConfigParseError";
|
|
39
|
+
this.filePath = filePath;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Raised when the document parses as JSON but violates the Agentyx schema. */
|
|
43
|
+
var AgentyxConfigValidationError = class extends AgentyxError {
|
|
44
|
+
issues;
|
|
45
|
+
/** The file the configuration came from, when it was read from disk. */
|
|
46
|
+
filePath;
|
|
47
|
+
constructor(error, filePath) {
|
|
48
|
+
const issues = toConfigIssues(error);
|
|
49
|
+
const location = filePath === void 0 ? "" : ` in ${filePath}`;
|
|
50
|
+
const details = issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n");
|
|
51
|
+
super("agentyx_config_invalid", `Invalid Agentyx configuration${location}:\n${details}`);
|
|
52
|
+
this.name = "AgentyxConfigValidationError";
|
|
53
|
+
this.issues = issues;
|
|
54
|
+
this.filePath = filePath;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function toConfigIssues(error) {
|
|
58
|
+
return error.issues.map((issue) => ({
|
|
59
|
+
path: formatIssuePath(issue.path),
|
|
60
|
+
message: issue.message
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function formatIssuePath(path) {
|
|
64
|
+
if (path.length === 0) return "(root)";
|
|
65
|
+
return path.reduce((formatted, segment) => {
|
|
66
|
+
if (typeof segment === "number") return `${formatted}[${segment}]`;
|
|
67
|
+
return formatted === "" ? String(segment) : `${formatted}.${String(segment)}`;
|
|
68
|
+
}, "");
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/mcp/schema.ts
|
|
72
|
+
const MCP_CAPABILITY_LEVELS = [
|
|
73
|
+
"essential",
|
|
74
|
+
"recommended",
|
|
75
|
+
"optional"
|
|
76
|
+
];
|
|
77
|
+
const mcpCapabilityLevelSchema = z.enum(MCP_CAPABILITY_LEVELS);
|
|
78
|
+
const MCP_CONTEXT_COSTS = [
|
|
79
|
+
"low",
|
|
80
|
+
"medium",
|
|
81
|
+
"high"
|
|
82
|
+
];
|
|
83
|
+
const mcpContextCostSchema = z.enum(MCP_CONTEXT_COSTS);
|
|
84
|
+
const mcpServerNameSchema = z.string().min(1, "MCP server names must be non-empty strings.").regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "MCP server names must be lowercase kebab-case, for example \"context7\".");
|
|
85
|
+
const mcpEnvReferenceSchema = z.strictObject({ fromEnv: z.string().trim().min(1, "Environment variable references must be non-empty strings.").regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "Environment variable names must be valid shell names.") });
|
|
86
|
+
const commonMcpServerDefinitionSchema = z.strictObject({
|
|
87
|
+
name: mcpServerNameSchema.describe("Unique MCP server identifier."),
|
|
88
|
+
description: z.string().trim().min(1, "MCP server descriptions must be non-empty strings."),
|
|
89
|
+
transport: z.enum(["stdio", "http"]),
|
|
90
|
+
contextCost: mcpContextCostSchema.describe("Qualitative context/tool-schema overhead classification; not token accounting.").optional()
|
|
91
|
+
});
|
|
92
|
+
const explicitMcpServerReferenceSchema = z.strictObject({
|
|
93
|
+
name: mcpServerNameSchema.describe("MCP server identifier."),
|
|
94
|
+
level: mcpCapabilityLevelSchema.describe("How important this MCP capability is to the declaring stack.").default("recommended")
|
|
95
|
+
});
|
|
96
|
+
const mcpServerReferenceSchema = z.union([mcpServerNameSchema, explicitMcpServerReferenceSchema]).transform((value) => typeof value === "string" ? {
|
|
97
|
+
name: value,
|
|
98
|
+
level: "recommended"
|
|
99
|
+
} : value).pipe(z.strictObject({
|
|
100
|
+
name: mcpServerNameSchema.describe("MCP server identifier."),
|
|
101
|
+
level: mcpCapabilityLevelSchema.describe("How important this MCP capability is to the declaring stack.")
|
|
102
|
+
}));
|
|
103
|
+
const stdioMcpServerDefinitionSchema = commonMcpServerDefinitionSchema.extend({
|
|
104
|
+
transport: z.literal("stdio"),
|
|
105
|
+
command: z.string().trim().min(1, "MCP stdio command must be a non-empty string."),
|
|
106
|
+
args: z.array(z.string()).default([]),
|
|
107
|
+
env: z.record(z.string().min(1), mcpEnvReferenceSchema).default({})
|
|
108
|
+
});
|
|
109
|
+
const httpMcpServerDefinitionSchema = commonMcpServerDefinitionSchema.extend({
|
|
110
|
+
transport: z.literal("http"),
|
|
111
|
+
url: z.url("MCP HTTP URL must be a valid URL."),
|
|
112
|
+
headers: z.record(z.string().min(1), mcpEnvReferenceSchema).default({})
|
|
113
|
+
});
|
|
114
|
+
const mcpServerDefinitionSchema = z.discriminatedUnion("transport", [stdioMcpServerDefinitionSchema, httpMcpServerDefinitionSchema]);
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/skill/schema.ts
|
|
117
|
+
/**
|
|
118
|
+
* Skill names double as directory names for `SKILL.md` files, so they are
|
|
119
|
+
* constrained to a lowercase slug. That keeps them stable across filesystems
|
|
120
|
+
* and makes them safe to join onto a path.
|
|
121
|
+
*/
|
|
122
|
+
const skillNameSchema = z.string().min(1, "Skill names must be non-empty strings.").regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Skill names must be lowercase kebab-case, for example \"typescript-modern\".");
|
|
123
|
+
/**
|
|
124
|
+
* The metadata a Skill carries, and exactly what a `SKILL.md` frontmatter block
|
|
125
|
+
* may declare. Kept separate from the full definition so the parser can reject
|
|
126
|
+
* a frontmatter key that tries to supply the body.
|
|
127
|
+
*/
|
|
128
|
+
const skillMetadataSchema = z.strictObject({
|
|
129
|
+
name: skillNameSchema.describe("Unique skill identifier."),
|
|
130
|
+
description: z.string().trim().min(1, "Skill descriptions must be non-empty strings.").describe("One-line summary of what the skill tells an agent to do.")
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* A Skill is reusable instruction text for a coding agent. It is deliberately
|
|
134
|
+
* provider-independent: nothing here says how or where a provider installs it.
|
|
135
|
+
*/
|
|
136
|
+
const skillDefinitionSchema = skillMetadataSchema.extend({ content: z.string().trim().min(1, "Skill content must be non-empty.").describe("The Markdown instructions handed to an agent.") });
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/stack/schema.ts
|
|
139
|
+
/**
|
|
140
|
+
* Stack names are plain identifiers. They are intentionally not constrained to
|
|
141
|
+
* a closed list so that external registries can contribute stacks later.
|
|
142
|
+
*/
|
|
143
|
+
const stackNameSchema = z.string().min(1, "Stack names must be non-empty strings.");
|
|
144
|
+
/**
|
|
145
|
+
* A stack describes a development environment that can build on other stacks
|
|
146
|
+
* and contribute provider-agnostic capabilities to them.
|
|
147
|
+
*/
|
|
148
|
+
const stackDefinitionSchema = z.strictObject({
|
|
149
|
+
name: stackNameSchema.describe("Unique stack identifier."),
|
|
150
|
+
description: z.string().min(1, "Stack descriptions must be non-empty when provided.").describe("Short human-readable summary of the stack.").optional(),
|
|
151
|
+
extends: z.array(stackNameSchema).describe("Stacks this stack builds on, in dependency-first order.").default([]),
|
|
152
|
+
skills: z.array(skillNameSchema).describe("Skills this stack contributes, in declaration order.").default([]),
|
|
153
|
+
mcpServers: z.array(mcpServerReferenceSchema).describe("MCP servers this stack contributes, in declaration order.").default([])
|
|
154
|
+
});
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/config/schema.ts
|
|
157
|
+
/** How much autonomy the generated environment should grant an agent. */
|
|
158
|
+
const AGENTYX_PROFILES = [
|
|
159
|
+
"lean",
|
|
160
|
+
"balanced",
|
|
161
|
+
"autonomous"
|
|
162
|
+
];
|
|
163
|
+
const agentyxProfileSchema = z.enum(AGENTYX_PROFILES, { error: `Profile must be one of: ${AGENTYX_PROFILES.join(", ")}.` });
|
|
164
|
+
/** Applied when a configuration omits `profile`. */
|
|
165
|
+
const DEFAULT_AGENTYX_PROFILE = "balanced";
|
|
166
|
+
/**
|
|
167
|
+
* Targets stay free-form strings on purpose: third-party adapters should be
|
|
168
|
+
* able to register providers without a change to this schema.
|
|
169
|
+
*/
|
|
170
|
+
const agentyxTargetSchema = z.string().min(1, "Targets must be non-empty strings.");
|
|
171
|
+
/** The `.agentyx.json` project configuration. */
|
|
172
|
+
const agentyxConfigSchema = z.strictObject({
|
|
173
|
+
$schema: z.string().min(1, "$schema must be a non-empty string.").describe("Optional path or URL to the Agentyx JSON Schema.").optional(),
|
|
174
|
+
extends: z.array(stackNameSchema).describe("Stacks this project builds on.").default([]),
|
|
175
|
+
profile: agentyxProfileSchema.describe("How much autonomy the generated environment grants.").default(DEFAULT_AGENTYX_PROFILE),
|
|
176
|
+
targets: z.array(agentyxTargetSchema).describe("Coding-agent providers this project targets.").default([])
|
|
177
|
+
});
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/config/json-schema.ts
|
|
180
|
+
/** Canonical identifier of the published `.agentyx.json` schema. */
|
|
181
|
+
const AGENTYX_CONFIG_SCHEMA_ID = "https://agentyx.dev/schema/agentyx.schema.json";
|
|
182
|
+
/**
|
|
183
|
+
* Derives the JSON Schema for `.agentyx.json` from the Zod model, which stays the
|
|
184
|
+
* single source of truth. The committed `schema/agentyx.schema.json` is generated
|
|
185
|
+
* from this function and verified by tests.
|
|
186
|
+
*
|
|
187
|
+
* The `input` view is used so that fields with defaults stay optional, which is
|
|
188
|
+
* what an author writing the file cares about.
|
|
189
|
+
*/
|
|
190
|
+
function buildAgentyxConfigJsonSchema() {
|
|
191
|
+
return {
|
|
192
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
193
|
+
$id: AGENTYX_CONFIG_SCHEMA_ID,
|
|
194
|
+
title: "Agentyx project configuration",
|
|
195
|
+
description: "Configuration for an Agentyx project (.agentyx.json).",
|
|
196
|
+
...z.toJSONSchema(agentyxConfigSchema, { io: "input" })
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/config/loader.ts
|
|
201
|
+
const AGENTYX_CONFIG_FILENAME = ".agentyx.json";
|
|
202
|
+
/** The absolute path `.agentyx.json` is read from for a given project. */
|
|
203
|
+
function agentyxConfigPath(projectPath = process.cwd()) {
|
|
204
|
+
return resolve(projectPath, AGENTYX_CONFIG_FILENAME);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Validates an already-parsed configuration document.
|
|
208
|
+
*
|
|
209
|
+
* @throws {AgentyxConfigValidationError} when the document violates the schema.
|
|
210
|
+
*/
|
|
211
|
+
function parseAgentyxConfig(value, filePath) {
|
|
212
|
+
const result = agentyxConfigSchema.safeParse(value);
|
|
213
|
+
if (!result.success) throw new AgentyxConfigValidationError(result.error, filePath);
|
|
214
|
+
return result.data;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Reads and validates `.agentyx.json` from `projectPath`.
|
|
218
|
+
*
|
|
219
|
+
* Parent directories are not searched, and invalid configurations are never
|
|
220
|
+
* silently repaired.
|
|
221
|
+
*
|
|
222
|
+
* @throws {AgentyxConfigNotFoundError} when the file does not exist.
|
|
223
|
+
* @throws {AgentyxConfigParseError} when the file is not valid JSON.
|
|
224
|
+
* @throws {AgentyxConfigValidationError} when the document violates the schema.
|
|
225
|
+
*/
|
|
226
|
+
async function loadAgentyxConfig(projectPath = process.cwd()) {
|
|
227
|
+
const filePath = agentyxConfigPath(projectPath);
|
|
228
|
+
let contents;
|
|
229
|
+
try {
|
|
230
|
+
contents = await readFile(filePath, "utf8");
|
|
231
|
+
} catch (cause) {
|
|
232
|
+
if (isFileNotFound(cause)) throw new AgentyxConfigNotFoundError(filePath, { cause });
|
|
233
|
+
throw cause;
|
|
234
|
+
}
|
|
235
|
+
let document;
|
|
236
|
+
try {
|
|
237
|
+
document = JSON.parse(contents);
|
|
238
|
+
} catch (cause) {
|
|
239
|
+
throw new AgentyxConfigParseError(filePath, cause instanceof Error ? cause.message : String(cause), { cause });
|
|
240
|
+
}
|
|
241
|
+
return parseAgentyxConfig(document, filePath);
|
|
242
|
+
}
|
|
243
|
+
function isFileNotFound(cause) {
|
|
244
|
+
return cause instanceof Error && cause.code === "ENOENT";
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/mcp/errors.ts
|
|
248
|
+
var UnknownMcpServerError = class extends AgentyxError {
|
|
249
|
+
serverName;
|
|
250
|
+
requiredBy;
|
|
251
|
+
knownServers;
|
|
252
|
+
constructor(serverName, requiredBy, knownServers) {
|
|
253
|
+
const origin = requiredBy === void 0 ? "" : ` (required by stack "${requiredBy}")`;
|
|
254
|
+
const known = knownServers.length > 0 ? [...knownServers].sort().join(", ") : "none";
|
|
255
|
+
super("unknown_mcp_server", `Unknown MCP server "${serverName}"${origin}. Known MCP servers: ${known}.`);
|
|
256
|
+
this.name = "UnknownMcpServerError";
|
|
257
|
+
this.serverName = serverName;
|
|
258
|
+
this.requiredBy = requiredBy;
|
|
259
|
+
this.knownServers = knownServers;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var DuplicateMcpServerError = class extends AgentyxError {
|
|
263
|
+
serverName;
|
|
264
|
+
constructor(serverName) {
|
|
265
|
+
super("duplicate_mcp_server", `Duplicate MCP server definition: "${serverName}".`);
|
|
266
|
+
this.name = "DuplicateMcpServerError";
|
|
267
|
+
this.serverName = serverName;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var InvalidMcpServerError = class extends AgentyxError {
|
|
271
|
+
origin;
|
|
272
|
+
reason;
|
|
273
|
+
constructor(origin, reason, options) {
|
|
274
|
+
const detail = typeof reason === "string" ? reason : formatIssues$1(reason);
|
|
275
|
+
super("invalid_mcp_server", `Invalid MCP server in ${origin}: ${detail}`, options);
|
|
276
|
+
this.name = "InvalidMcpServerError";
|
|
277
|
+
this.origin = origin;
|
|
278
|
+
this.reason = detail;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function formatIssues$1(error) {
|
|
282
|
+
return error.issues.map((issue) => {
|
|
283
|
+
const path = issue.path.map(String).join(".");
|
|
284
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
285
|
+
}).join("; ");
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/mcp/registry.ts
|
|
289
|
+
function createMcpServerRegistry(sources) {
|
|
290
|
+
const byName = /* @__PURE__ */ new Map();
|
|
291
|
+
for (const source of sources) {
|
|
292
|
+
const name = mcpServerNameSchema.safeParse(source.name);
|
|
293
|
+
if (!name.success) throw new InvalidMcpServerError(`MCP server source "${source.name}"`, name.error);
|
|
294
|
+
if (byName.has(name.data)) throw new DuplicateMcpServerError(name.data);
|
|
295
|
+
byName.set(name.data, source);
|
|
296
|
+
}
|
|
297
|
+
const names = [...byName.keys()];
|
|
298
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
299
|
+
const get = (name) => {
|
|
300
|
+
const cached = loaded.get(name);
|
|
301
|
+
if (cached !== void 0) return cached;
|
|
302
|
+
const source = byName.get(name);
|
|
303
|
+
if (source === void 0) throw new UnknownMcpServerError(name, void 0, names);
|
|
304
|
+
const origin = `MCP server "${name}"`;
|
|
305
|
+
const server = mcpServerDefinitionSchema.safeParse(source.load());
|
|
306
|
+
if (!server.success) throw new InvalidMcpServerError(origin, server.error);
|
|
307
|
+
if (server.data.name !== name) throw new InvalidMcpServerError(origin, `it declares the name "${server.data.name}"`);
|
|
308
|
+
loaded.set(name, server.data);
|
|
309
|
+
return server.data;
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
names,
|
|
313
|
+
has: (name) => byName.has(name),
|
|
314
|
+
listMetadata: () => names.map((name) => {
|
|
315
|
+
const server = get(name);
|
|
316
|
+
return {
|
|
317
|
+
name: server.name,
|
|
318
|
+
description: server.description,
|
|
319
|
+
transport: server.transport,
|
|
320
|
+
contextCost: server.contextCost
|
|
321
|
+
};
|
|
322
|
+
}),
|
|
323
|
+
get
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const builtInMcpServerRegistry = createMcpServerRegistry([{
|
|
327
|
+
name: "context7",
|
|
328
|
+
load: () => ({
|
|
329
|
+
name: "context7",
|
|
330
|
+
description: "Fetch up-to-date library documentation from Context7.",
|
|
331
|
+
transport: "http",
|
|
332
|
+
contextCost: "medium",
|
|
333
|
+
url: "https://mcp.context7.com/mcp"
|
|
334
|
+
})
|
|
335
|
+
}, {
|
|
336
|
+
name: "playwright",
|
|
337
|
+
load: () => ({
|
|
338
|
+
name: "playwright",
|
|
339
|
+
description: "Automate and inspect browsers through Playwright MCP.",
|
|
340
|
+
transport: "stdio",
|
|
341
|
+
contextCost: "high",
|
|
342
|
+
command: "npx",
|
|
343
|
+
args: ["@playwright/mcp@latest"]
|
|
344
|
+
})
|
|
345
|
+
}]);
|
|
346
|
+
const builtInMcpServerNames = builtInMcpServerRegistry.names;
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/optimization/profile.ts
|
|
349
|
+
const optimizationProfiles = [
|
|
350
|
+
{
|
|
351
|
+
name: "lean",
|
|
352
|
+
goal: "Minimum context and tool overhead.",
|
|
353
|
+
mcpLevels: ["essential"],
|
|
354
|
+
notes: [
|
|
355
|
+
"Installs all resolved Skills.",
|
|
356
|
+
"Keeps MCP exposure to essential capabilities.",
|
|
357
|
+
"Prefers local CLI or native tools when they cover the same workflow."
|
|
358
|
+
]
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "balanced",
|
|
362
|
+
goal: "Good default developer experience with conservative autonomy.",
|
|
363
|
+
mcpLevels: ["essential", "recommended"],
|
|
364
|
+
notes: ["Installs all resolved Skills.", "Includes default and recommended MCP capabilities."]
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "autonomous",
|
|
368
|
+
goal: "Maximum agent capability from declared stack capabilities.",
|
|
369
|
+
mcpLevels: [
|
|
370
|
+
"essential",
|
|
371
|
+
"recommended",
|
|
372
|
+
"optional"
|
|
373
|
+
],
|
|
374
|
+
notes: ["Installs all resolved Skills.", "Includes optional MCP capabilities declared by the selected stacks."]
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
const optimizationProfileNames = optimizationProfiles.map((profile) => profile.name);
|
|
378
|
+
function getOptimizationProfile(name) {
|
|
379
|
+
const profile = optimizationProfiles.find((candidate) => candidate.name === name);
|
|
380
|
+
if (profile === void 0) throw new Error(`Unknown optimization profile: ${name}`);
|
|
381
|
+
return profile;
|
|
382
|
+
}
|
|
383
|
+
function isMcpLevelEnabled(profile, level) {
|
|
384
|
+
return getOptimizationProfile(profile).mcpLevels.includes(level);
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/stack/errors.ts
|
|
388
|
+
/**
|
|
389
|
+
* Raised when a requested or inherited stack is not present in the registry.
|
|
390
|
+
*
|
|
391
|
+
* The offending name is exposed as `stackName`; `stack` stays the JS stack
|
|
392
|
+
* trace inherited from `Error`.
|
|
393
|
+
*/
|
|
394
|
+
var UnknownStackError = class extends AgentyxError {
|
|
395
|
+
stackName;
|
|
396
|
+
requiredBy;
|
|
397
|
+
knownStacks;
|
|
398
|
+
constructor(stackName, requiredBy, knownStacks) {
|
|
399
|
+
const origin = requiredBy === void 0 ? "" : ` (required by "${requiredBy}")`;
|
|
400
|
+
const known = knownStacks.length > 0 ? [...knownStacks].sort().join(", ") : "none";
|
|
401
|
+
super("unknown_stack", `Unknown stack "${stackName}"${origin}. Known stacks: ${known}.`);
|
|
402
|
+
this.name = "UnknownStackError";
|
|
403
|
+
this.stackName = stackName;
|
|
404
|
+
this.requiredBy = requiredBy;
|
|
405
|
+
this.knownStacks = knownStacks;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
/** Raised when stack inheritance forms a cycle. */
|
|
409
|
+
var CircularStackDependencyError = class extends AgentyxError {
|
|
410
|
+
/** The cycle path, starting and ending with the same stack. */
|
|
411
|
+
cycle;
|
|
412
|
+
constructor(cycle) {
|
|
413
|
+
super("circular_stack_dependency", `Circular stack dependency: ${cycle.join(" -> ")}.`);
|
|
414
|
+
this.name = "CircularStackDependencyError";
|
|
415
|
+
this.cycle = cycle;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
/** Raised when a registry is built from definitions that reuse a stack name. */
|
|
419
|
+
var DuplicateStackError = class extends AgentyxError {
|
|
420
|
+
stackName;
|
|
421
|
+
constructor(stackName) {
|
|
422
|
+
super("duplicate_stack", `Duplicate stack definition: "${stackName}".`);
|
|
423
|
+
this.name = "DuplicateStackError";
|
|
424
|
+
this.stackName = stackName;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/stack/registry.ts
|
|
429
|
+
/** Validates definitions and indexes them by name. */
|
|
430
|
+
function createStackRegistry(definitions) {
|
|
431
|
+
const registry = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const definition of definitions) {
|
|
433
|
+
const stack = stackDefinitionSchema.parse(definition);
|
|
434
|
+
if (registry.has(stack.name)) throw new DuplicateStackError(stack.name);
|
|
435
|
+
registry.set(stack.name, stack);
|
|
436
|
+
}
|
|
437
|
+
return registry;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The stacks Agentyx ships with, expressed as data. Relationships live here, not
|
|
441
|
+
* in the resolver.
|
|
442
|
+
*/
|
|
443
|
+
const builtInStacks = [
|
|
444
|
+
{
|
|
445
|
+
name: "core",
|
|
446
|
+
description: "Baseline development environment shared by every Agentyx stack.",
|
|
447
|
+
skills: [
|
|
448
|
+
"planning",
|
|
449
|
+
"systematic-debugging",
|
|
450
|
+
"verification"
|
|
451
|
+
]
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "typescript",
|
|
455
|
+
description: "TypeScript development environment.",
|
|
456
|
+
extends: ["core"],
|
|
457
|
+
skills: ["typescript-modern"]
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "angular",
|
|
461
|
+
description: "Modern Angular development environment.",
|
|
462
|
+
extends: ["typescript"],
|
|
463
|
+
skills: ["angular-modern"],
|
|
464
|
+
mcpServers: [{
|
|
465
|
+
name: "context7",
|
|
466
|
+
level: "recommended"
|
|
467
|
+
}]
|
|
468
|
+
}
|
|
469
|
+
];
|
|
470
|
+
/** The registry used by default when no explicit registry is supplied. */
|
|
471
|
+
const builtInStackRegistry = createStackRegistry(builtInStacks);
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/stack/resolver.ts
|
|
474
|
+
/**
|
|
475
|
+
* Expands the requested stacks into the full inheritance chain.
|
|
476
|
+
*
|
|
477
|
+
* The result is dependency-first, de-duplicated and deterministic: stacks are
|
|
478
|
+
* visited in the order they are requested, and each stack's parents are visited
|
|
479
|
+
* in declaration order before the stack itself.
|
|
480
|
+
*
|
|
481
|
+
* @throws {UnknownStackError} when a requested or inherited stack is missing.
|
|
482
|
+
* @throws {CircularStackDependencyError} when inheritance forms a cycle.
|
|
483
|
+
*/
|
|
484
|
+
function resolveStacks(requestedStacks, registry = builtInStackRegistry) {
|
|
485
|
+
const resolved = [];
|
|
486
|
+
const settled = /* @__PURE__ */ new Set();
|
|
487
|
+
const path = [];
|
|
488
|
+
const visit = (name, requiredBy) => {
|
|
489
|
+
if (settled.has(name)) return;
|
|
490
|
+
const cycleStart = path.indexOf(name);
|
|
491
|
+
if (cycleStart !== -1) throw new CircularStackDependencyError([...path.slice(cycleStart), name]);
|
|
492
|
+
const definition = registry.get(name);
|
|
493
|
+
if (definition === void 0) throw new UnknownStackError(name, requiredBy, [...registry.keys()]);
|
|
494
|
+
path.push(name);
|
|
495
|
+
for (const parent of definition.extends) visit(parent, name);
|
|
496
|
+
path.pop();
|
|
497
|
+
settled.add(name);
|
|
498
|
+
resolved.push(name);
|
|
499
|
+
};
|
|
500
|
+
for (const name of requestedStacks) visit(name, void 0);
|
|
501
|
+
return resolved;
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/mcp/resolver.ts
|
|
505
|
+
function collectStackMcpServerReferences(resolvedStacks, stackRegistry, mcpRegistry) {
|
|
506
|
+
const servers = [];
|
|
507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
508
|
+
for (const stackName of resolvedStacks) for (const server of stackRegistry.get(stackName)?.mcpServers ?? []) {
|
|
509
|
+
const serverName = server.name;
|
|
510
|
+
if (seen.has(serverName)) continue;
|
|
511
|
+
if (!mcpRegistry.has(serverName)) throw new UnknownMcpServerError(serverName, stackName, mcpRegistry.names);
|
|
512
|
+
seen.add(serverName);
|
|
513
|
+
servers.push(server);
|
|
514
|
+
}
|
|
515
|
+
return servers;
|
|
516
|
+
}
|
|
517
|
+
function filterEffectiveMcpServers(servers, profile) {
|
|
518
|
+
return servers.filter((server) => isMcpLevelEnabled(profile, server.level)).map((server) => server.name);
|
|
519
|
+
}
|
|
520
|
+
function collectStackMcpServers(resolvedStacks, stackRegistry, mcpRegistry, profile = DEFAULT_AGENTYX_PROFILE) {
|
|
521
|
+
return filterEffectiveMcpServers(collectStackMcpServerReferences(resolvedStacks, stackRegistry, mcpRegistry), profile);
|
|
522
|
+
}
|
|
523
|
+
function resolveStackMcpServerReferences(requestedStacks, stackRegistry = builtInStackRegistry, mcpRegistry = builtInMcpServerRegistry) {
|
|
524
|
+
return collectStackMcpServerReferences(resolveStacks(requestedStacks, stackRegistry), stackRegistry, mcpRegistry);
|
|
525
|
+
}
|
|
526
|
+
function resolveStackMcpServers(requestedStacks, stackRegistry = builtInStackRegistry, mcpRegistry = builtInMcpServerRegistry, profile = DEFAULT_AGENTYX_PROFILE) {
|
|
527
|
+
return collectStackMcpServers(resolveStacks(requestedStacks, stackRegistry), stackRegistry, mcpRegistry, profile);
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/assets.ts
|
|
531
|
+
/**
|
|
532
|
+
* Location of the non-code assets that ship with `@agentyx/core`.
|
|
533
|
+
*
|
|
534
|
+
* **This module must stay directly under `src/`.** The path is derived from
|
|
535
|
+
* `import.meta.url` rather than the working directory, and `src/assets.ts` and
|
|
536
|
+
* the bundled `dist/index.mjs` sit at the same depth below the package root, so
|
|
537
|
+
* one relative path is correct both in development and in the published
|
|
538
|
+
* package. Moving this file into a subdirectory breaks built-in skill loading;
|
|
539
|
+
* `packages/core/test/built-in-skills.test.ts` guards against that.
|
|
540
|
+
*
|
|
541
|
+
* `skills/` is listed in the package's `files`, so it is published alongside
|
|
542
|
+
* `dist/`.
|
|
543
|
+
*/
|
|
544
|
+
const BUILT_IN_SKILLS_PATH = fileURLToPath(new URL("../skills", import.meta.url));
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/skill/errors.ts
|
|
547
|
+
/**
|
|
548
|
+
* Raised when a stack references a skill the registry does not know.
|
|
549
|
+
*
|
|
550
|
+
* The offending name is exposed as `skillName`; `stack` stays the JS stack
|
|
551
|
+
* trace inherited from `Error`.
|
|
552
|
+
*/
|
|
553
|
+
var UnknownSkillError = class extends AgentyxError {
|
|
554
|
+
skillName;
|
|
555
|
+
/** The stack that declared the skill, when the failure came from resolution. */
|
|
556
|
+
requiredBy;
|
|
557
|
+
knownSkills;
|
|
558
|
+
constructor(skillName, requiredBy, knownSkills) {
|
|
559
|
+
const origin = requiredBy === void 0 ? "" : ` (required by stack "${requiredBy}")`;
|
|
560
|
+
const known = knownSkills.length > 0 ? [...knownSkills].sort().join(", ") : "none";
|
|
561
|
+
super("unknown_skill", `Unknown skill "${skillName}"${origin}. Known skills: ${known}.`);
|
|
562
|
+
this.name = "UnknownSkillError";
|
|
563
|
+
this.skillName = skillName;
|
|
564
|
+
this.requiredBy = requiredBy;
|
|
565
|
+
this.knownSkills = knownSkills;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
/** Raised when a registry is built from sources that reuse a skill name. */
|
|
569
|
+
var DuplicateSkillError = class extends AgentyxError {
|
|
570
|
+
skillName;
|
|
571
|
+
constructor(skillName) {
|
|
572
|
+
super("duplicate_skill", `Duplicate skill definition: "${skillName}".`);
|
|
573
|
+
this.name = "DuplicateSkillError";
|
|
574
|
+
this.skillName = skillName;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Raised when a skill exists but cannot be turned into a valid definition —
|
|
579
|
+
* malformed `SKILL.md` frontmatter, a missing field, an unreadable file.
|
|
580
|
+
*
|
|
581
|
+
* Built-in skills ship with Agentyx, so this is a programmer error rather than
|
|
582
|
+
* something a user can fix in their project.
|
|
583
|
+
*/
|
|
584
|
+
var InvalidSkillError = class extends AgentyxError {
|
|
585
|
+
/** Where the skill came from: a file path, or a label for an in-memory source. */
|
|
586
|
+
origin;
|
|
587
|
+
reason;
|
|
588
|
+
constructor(origin, reason, options) {
|
|
589
|
+
const detail = typeof reason === "string" ? reason : formatIssues(reason);
|
|
590
|
+
super("invalid_skill", `Invalid skill in ${origin}: ${detail}`, options);
|
|
591
|
+
this.name = "InvalidSkillError";
|
|
592
|
+
this.origin = origin;
|
|
593
|
+
this.reason = detail;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function formatIssues(error) {
|
|
597
|
+
return error.issues.map((issue) => {
|
|
598
|
+
const path = issue.path.map(String).join(".");
|
|
599
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
600
|
+
}).join("; ");
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/skill/markdown.ts
|
|
604
|
+
const DELIMITER = "---";
|
|
605
|
+
const BYTE_ORDER_MARK = /^\uFEFF/;
|
|
606
|
+
const LINE_BREAK = /[\r\n]/;
|
|
607
|
+
/**
|
|
608
|
+
* A `SKILL.md` file is a frontmatter block followed by the Markdown body:
|
|
609
|
+
*
|
|
610
|
+
* ```md
|
|
611
|
+
* ---
|
|
612
|
+
* name: planning
|
|
613
|
+
* description: Plan non-trivial work before editing.
|
|
614
|
+
* ---
|
|
615
|
+
*
|
|
616
|
+
* Instructions...
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* The frontmatter is a deliberately small YAML subset — a flat block of
|
|
620
|
+
* `key: value` pairs, values optionally quoted — so that core keeps depending
|
|
621
|
+
* on Zod alone. Anything richer is rejected rather than guessed at.
|
|
622
|
+
*
|
|
623
|
+
* The body is preserved as written apart from trimming the blank lines around
|
|
624
|
+
* it, and `origin` is only used to make errors readable.
|
|
625
|
+
*
|
|
626
|
+
* @throws {InvalidSkillError} when the frontmatter or the resulting skill is invalid.
|
|
627
|
+
*/
|
|
628
|
+
function parseSkillMarkdown(markdown, origin) {
|
|
629
|
+
const lines = markdown.replace(BYTE_ORDER_MARK, "").split(/\r?\n/);
|
|
630
|
+
if (lines[0]?.trim() !== DELIMITER) throw new InvalidSkillError(origin, `the file must start with a "${DELIMITER}" frontmatter line`);
|
|
631
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === DELIMITER);
|
|
632
|
+
if (closing === -1) throw new InvalidSkillError(origin, `the frontmatter block is never closed with "${DELIMITER}"`);
|
|
633
|
+
const metadata = skillMetadataSchema.safeParse(parseFrontmatter(lines.slice(1, closing), origin));
|
|
634
|
+
if (!metadata.success) throw new InvalidSkillError(origin, metadata.error);
|
|
635
|
+
const skill = skillDefinitionSchema.safeParse({
|
|
636
|
+
...metadata.data,
|
|
637
|
+
content: lines.slice(closing + 1).join("\n")
|
|
638
|
+
});
|
|
639
|
+
if (!skill.success) throw new InvalidSkillError(origin, skill.error);
|
|
640
|
+
return skill.data;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Renders a skill as canonical `SKILL.md` text — the inverse of
|
|
644
|
+
* `parseSkillMarkdown`, and the single representation every consumer installs.
|
|
645
|
+
*
|
|
646
|
+
* Keeping this in core is what stops a provider from owning a copy of a skill
|
|
647
|
+
* body: an adapter decides *where* a skill goes, never what it says.
|
|
648
|
+
*
|
|
649
|
+
* The output is deterministic. Frontmatter is always `name` then `description`,
|
|
650
|
+
* the body is the trimmed content, and the file ends with exactly one newline,
|
|
651
|
+
* so re-rendering an unchanged skill produces a byte-identical file and an
|
|
652
|
+
* installer can compare content instead of guessing.
|
|
653
|
+
*
|
|
654
|
+
* @throws {InvalidSkillError} when the definition is invalid, or when the
|
|
655
|
+
* description cannot be represented in the flat frontmatter subset the parser
|
|
656
|
+
* accepts.
|
|
657
|
+
*/
|
|
658
|
+
function formatSkillMarkdown(skill) {
|
|
659
|
+
const origin = typeof skill.name === "string" ? `skill "${skill.name}"` : "skill definition";
|
|
660
|
+
const parsed = skillDefinitionSchema.safeParse(skill);
|
|
661
|
+
if (!parsed.success) throw new InvalidSkillError(origin, parsed.error);
|
|
662
|
+
const { name, description, content } = parsed.data;
|
|
663
|
+
if (LINE_BREAK.test(description)) throw new InvalidSkillError(origin, "the description must not contain a line break");
|
|
664
|
+
return `${DELIMITER}\nname: ${name}\ndescription: ${description}\n${DELIMITER}\n\n${content}\n`;
|
|
665
|
+
}
|
|
666
|
+
function parseFrontmatter(lines, origin) {
|
|
667
|
+
const entries = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const [index, line] of lines.entries()) {
|
|
669
|
+
if (line.trim() === "") continue;
|
|
670
|
+
const separator = line.indexOf(":");
|
|
671
|
+
const lineNumber = index + 2;
|
|
672
|
+
if (separator === -1) throw new InvalidSkillError(origin, `frontmatter line ${lineNumber} is not "key: value": ${line}`);
|
|
673
|
+
const key = line.slice(0, separator).trim();
|
|
674
|
+
if (key === "") throw new InvalidSkillError(origin, `frontmatter line ${lineNumber} has an empty key`);
|
|
675
|
+
if (entries.has(key)) throw new InvalidSkillError(origin, `duplicate frontmatter key "${key}" on line ${lineNumber}`);
|
|
676
|
+
entries.set(key, unquote(line.slice(separator + 1).trim()));
|
|
677
|
+
}
|
|
678
|
+
return Object.fromEntries(entries);
|
|
679
|
+
}
|
|
680
|
+
function unquote(value) {
|
|
681
|
+
const quoted = /^"(.*)"$|^'(.*)'$/.exec(value);
|
|
682
|
+
return quoted?.[1] ?? quoted?.[2] ?? value;
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/skill/registry.ts
|
|
686
|
+
/**
|
|
687
|
+
* Indexes skill sources by name.
|
|
688
|
+
*
|
|
689
|
+
* Names and duplicates are validated here; the definitions themselves are
|
|
690
|
+
* validated the first time each skill is loaded.
|
|
691
|
+
*
|
|
692
|
+
* @throws {DuplicateSkillError} when two sources share a name.
|
|
693
|
+
* @throws {InvalidSkillError} when a source name is not a valid skill name.
|
|
694
|
+
*/
|
|
695
|
+
function createSkillRegistry(sources) {
|
|
696
|
+
const byName = /* @__PURE__ */ new Map();
|
|
697
|
+
for (const source of sources) {
|
|
698
|
+
const name = skillNameSchema.safeParse(source.name);
|
|
699
|
+
if (!name.success) throw new InvalidSkillError(`skill source "${source.name}"`, name.error);
|
|
700
|
+
if (byName.has(name.data)) throw new DuplicateSkillError(name.data);
|
|
701
|
+
byName.set(name.data, source);
|
|
702
|
+
}
|
|
703
|
+
const names = [...byName.keys()];
|
|
704
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
705
|
+
return {
|
|
706
|
+
names,
|
|
707
|
+
has: (name) => byName.has(name),
|
|
708
|
+
get: (name) => {
|
|
709
|
+
const cached = loaded.get(name);
|
|
710
|
+
if (cached !== void 0) return cached;
|
|
711
|
+
const source = byName.get(name);
|
|
712
|
+
if (source === void 0) throw new UnknownSkillError(name, void 0, names);
|
|
713
|
+
const origin = `skill "${name}"`;
|
|
714
|
+
const skill = skillDefinitionSchema.safeParse(source.load());
|
|
715
|
+
if (!skill.success) throw new InvalidSkillError(origin, skill.error);
|
|
716
|
+
if (skill.data.name !== name) throw new InvalidSkillError(origin, `it declares the name "${skill.data.name}"`);
|
|
717
|
+
loaded.set(name, skill.data);
|
|
718
|
+
return skill.data;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/skill/built-in.ts
|
|
724
|
+
/**
|
|
725
|
+
* The skills Agentyx ships with. The names are the directory names under
|
|
726
|
+
* `packages/core/skills`, and knowing them without touching the filesystem is
|
|
727
|
+
* what lets stack resolution validate references without reading any body.
|
|
728
|
+
*/
|
|
729
|
+
const builtInSkillNames = [
|
|
730
|
+
"planning",
|
|
731
|
+
"systematic-debugging",
|
|
732
|
+
"verification",
|
|
733
|
+
"typescript-modern",
|
|
734
|
+
"angular-modern"
|
|
735
|
+
];
|
|
736
|
+
/** The absolute path of a built-in skill's `SKILL.md`. */
|
|
737
|
+
function builtInSkillPath(name) {
|
|
738
|
+
return join(BUILT_IN_SKILLS_PATH, name, "SKILL.md");
|
|
739
|
+
}
|
|
740
|
+
/** The registry used by default when no explicit registry is supplied. */
|
|
741
|
+
const builtInSkillRegistry = createSkillRegistry(builtInSkillNames.map((name) => ({
|
|
742
|
+
name,
|
|
743
|
+
load: () => loadBuiltInSkill(name)
|
|
744
|
+
})));
|
|
745
|
+
/**
|
|
746
|
+
* Reads one built-in `SKILL.md`.
|
|
747
|
+
*
|
|
748
|
+
* Reading is synchronous on purpose: the files are small package assets, they
|
|
749
|
+
* are only touched when a command asks for instructions, and a synchronous read
|
|
750
|
+
* keeps `SkillRegistry.get` a plain lookup instead of infecting every caller
|
|
751
|
+
* with a promise.
|
|
752
|
+
*/
|
|
753
|
+
function loadBuiltInSkill(name) {
|
|
754
|
+
const filePath = builtInSkillPath(name);
|
|
755
|
+
let markdown;
|
|
756
|
+
try {
|
|
757
|
+
markdown = readFileSync(filePath, "utf8");
|
|
758
|
+
} catch (cause) {
|
|
759
|
+
throw new InvalidSkillError(filePath, `the file could not be read: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
760
|
+
}
|
|
761
|
+
return parseSkillMarkdown(markdown, filePath);
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/skill/resolver.ts
|
|
765
|
+
/**
|
|
766
|
+
* Collects the skills contributed by an already-resolved stack chain.
|
|
767
|
+
*
|
|
768
|
+
* Only identifiers are produced — no skill body is read — so this stays cheap
|
|
769
|
+
* enough to run on every `resolve`.
|
|
770
|
+
*
|
|
771
|
+
* @throws {UnknownSkillError} when a stack references a skill the registry lacks.
|
|
772
|
+
*/
|
|
773
|
+
function collectStackSkills(resolvedStacks, stackRegistry, skillRegistry) {
|
|
774
|
+
const skills = [];
|
|
775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
776
|
+
for (const stackName of resolvedStacks) for (const skillName of stackRegistry.get(stackName)?.skills ?? []) {
|
|
777
|
+
if (seen.has(skillName)) continue;
|
|
778
|
+
if (!skillRegistry.has(skillName)) throw new UnknownSkillError(skillName, stackName, skillRegistry.names);
|
|
779
|
+
seen.add(skillName);
|
|
780
|
+
skills.push(skillName);
|
|
781
|
+
}
|
|
782
|
+
return skills;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Expands the requested stacks and returns the skills they contribute.
|
|
786
|
+
*
|
|
787
|
+
* The result follows the resolved stack order, then each stack's declaration
|
|
788
|
+
* order, and is de-duplicated by first occurrence. Skills do not extend other
|
|
789
|
+
* skills, so there is nothing further to expand.
|
|
790
|
+
*
|
|
791
|
+
* @throws {UnknownStackError} when a requested or inherited stack is missing.
|
|
792
|
+
* @throws {CircularStackDependencyError} when stack inheritance forms a cycle.
|
|
793
|
+
* @throws {UnknownSkillError} when a stack references a skill the registry lacks.
|
|
794
|
+
*/
|
|
795
|
+
function resolveStackSkills(requestedStacks, stackRegistry = builtInStackRegistry, skillRegistry = builtInSkillRegistry) {
|
|
796
|
+
return collectStackSkills(resolveStacks(requestedStacks, stackRegistry), stackRegistry, skillRegistry);
|
|
797
|
+
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/config/resolver.ts
|
|
800
|
+
/**
|
|
801
|
+
* Combines a validated project configuration with stack and skill resolution.
|
|
802
|
+
* The input is never mutated.
|
|
803
|
+
*
|
|
804
|
+
* @throws {UnknownStackError} when a configured stack is missing.
|
|
805
|
+
* @throws {CircularStackDependencyError} when inheritance forms a cycle.
|
|
806
|
+
* @throws {UnknownSkillError} when a stack references an unknown skill.
|
|
807
|
+
*/
|
|
808
|
+
function resolveAgentyxConfig(config, registry = builtInStackRegistry, skillRegistry = builtInSkillRegistry, mcpRegistry = builtInMcpServerRegistry) {
|
|
809
|
+
const requestedStacks = [...config.extends];
|
|
810
|
+
const resolvedStacks = resolveStacks(requestedStacks, registry);
|
|
811
|
+
const declaredMcpServers = collectStackMcpServerReferences(resolvedStacks, registry, mcpRegistry);
|
|
812
|
+
return {
|
|
813
|
+
requestedStacks,
|
|
814
|
+
resolvedStacks,
|
|
815
|
+
skills: collectStackSkills(resolvedStacks, registry, skillRegistry),
|
|
816
|
+
declaredMcpServers,
|
|
817
|
+
mcpServers: filterEffectiveMcpServers(declaredMcpServers, config.profile),
|
|
818
|
+
profile: config.profile,
|
|
819
|
+
targets: [...config.targets]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/meta.ts
|
|
824
|
+
const agentyxCoreName = "agentyx-core";
|
|
825
|
+
function getCoreStatus() {
|
|
826
|
+
return "ready";
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/project/detector.ts
|
|
830
|
+
const PACKAGE_MANAGERS = [
|
|
831
|
+
"pnpm",
|
|
832
|
+
"npm",
|
|
833
|
+
"yarn",
|
|
834
|
+
"bun"
|
|
835
|
+
];
|
|
836
|
+
const lockfileManagers = /* @__PURE__ */ new Map([
|
|
837
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
838
|
+
["package-lock.json", "npm"],
|
|
839
|
+
["yarn.lock", "yarn"],
|
|
840
|
+
["bun.lock", "bun"],
|
|
841
|
+
["bun.lockb", "bun"]
|
|
842
|
+
]);
|
|
843
|
+
async function detectProject(projectDir) {
|
|
844
|
+
const packageJsonPath = join(projectDir, "package.json");
|
|
845
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
846
|
+
const packageManager = await detectPackageManager(projectDir, packageJson.data);
|
|
847
|
+
const detectedStacks = await detectStacks(projectDir, packageJson.data);
|
|
848
|
+
return {
|
|
849
|
+
projectDir,
|
|
850
|
+
packageJson: {
|
|
851
|
+
present: packageJson.present,
|
|
852
|
+
valid: packageJson.valid,
|
|
853
|
+
path: packageJsonPath,
|
|
854
|
+
name: getString(packageJson.data?.name),
|
|
855
|
+
packageManager: getString(packageJson.data?.packageManager),
|
|
856
|
+
error: packageJson.error
|
|
857
|
+
},
|
|
858
|
+
packageManager,
|
|
859
|
+
detectedStacks,
|
|
860
|
+
recommendedStack: recommendStack(detectedStacks)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function buildAgentyxConfig(input) {
|
|
864
|
+
return {
|
|
865
|
+
extends: [input.stack],
|
|
866
|
+
profile: input.profile,
|
|
867
|
+
targets: [...input.targets]
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function formatAgentyxConfig(config) {
|
|
871
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
872
|
+
}
|
|
873
|
+
function recommendStack(detectedStacks) {
|
|
874
|
+
if (detectedStacks.includes("angular")) return "angular";
|
|
875
|
+
if (detectedStacks.includes("typescript")) return "typescript";
|
|
876
|
+
}
|
|
877
|
+
async function detectStacks(projectDir, packageJson) {
|
|
878
|
+
const stacks = [];
|
|
879
|
+
const dependencies = collectDependencies(packageJson);
|
|
880
|
+
const hasAngular = dependencies.has("@angular/core");
|
|
881
|
+
if (dependencies.has("typescript") || await exists(join(projectDir, "tsconfig.json")) || hasAngular) stacks.push("typescript");
|
|
882
|
+
if (hasAngular) stacks.push("angular");
|
|
883
|
+
return stacks;
|
|
884
|
+
}
|
|
885
|
+
async function detectPackageManager(projectDir, packageJson) {
|
|
886
|
+
const lockfiles = [];
|
|
887
|
+
const managers = /* @__PURE__ */ new Set();
|
|
888
|
+
for (const [file, manager] of lockfileManagers) if (await exists(join(projectDir, file))) {
|
|
889
|
+
lockfiles.push(file);
|
|
890
|
+
managers.add(manager);
|
|
891
|
+
}
|
|
892
|
+
if (managers.size === 1) return {
|
|
893
|
+
name: [...managers][0],
|
|
894
|
+
source: "lockfile",
|
|
895
|
+
lockfiles,
|
|
896
|
+
ambiguous: false
|
|
897
|
+
};
|
|
898
|
+
if (managers.size > 1) return {
|
|
899
|
+
name: void 0,
|
|
900
|
+
source: "lockfile",
|
|
901
|
+
lockfiles,
|
|
902
|
+
ambiguous: true
|
|
903
|
+
};
|
|
904
|
+
const declared = parsePackageManagerName(getString(packageJson?.packageManager));
|
|
905
|
+
return {
|
|
906
|
+
name: declared,
|
|
907
|
+
source: declared === void 0 ? void 0 : "package-json",
|
|
908
|
+
lockfiles,
|
|
909
|
+
ambiguous: false
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
async function readPackageJson(path) {
|
|
913
|
+
let contents;
|
|
914
|
+
try {
|
|
915
|
+
contents = await readFile(path, "utf8");
|
|
916
|
+
} catch (cause) {
|
|
917
|
+
if (isNotFound(cause)) return {
|
|
918
|
+
present: false,
|
|
919
|
+
valid: false,
|
|
920
|
+
data: void 0,
|
|
921
|
+
error: void 0
|
|
922
|
+
};
|
|
923
|
+
return {
|
|
924
|
+
present: true,
|
|
925
|
+
valid: false,
|
|
926
|
+
data: void 0,
|
|
927
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
return {
|
|
932
|
+
present: true,
|
|
933
|
+
valid: true,
|
|
934
|
+
data: JSON.parse(contents),
|
|
935
|
+
error: void 0
|
|
936
|
+
};
|
|
937
|
+
} catch (cause) {
|
|
938
|
+
return {
|
|
939
|
+
present: true,
|
|
940
|
+
valid: false,
|
|
941
|
+
data: void 0,
|
|
942
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function collectDependencies(packageJson) {
|
|
947
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
948
|
+
for (const field of [
|
|
949
|
+
packageJson?.dependencies,
|
|
950
|
+
packageJson?.devDependencies,
|
|
951
|
+
packageJson?.peerDependencies,
|
|
952
|
+
packageJson?.optionalDependencies
|
|
953
|
+
]) if (isRecord(field)) for (const name of Object.keys(field)) dependencies.add(name);
|
|
954
|
+
return dependencies;
|
|
955
|
+
}
|
|
956
|
+
function parsePackageManagerName(value) {
|
|
957
|
+
const name = value?.split("@")[0];
|
|
958
|
+
return PACKAGE_MANAGERS.includes(name) ? name : void 0;
|
|
959
|
+
}
|
|
960
|
+
function getString(value) {
|
|
961
|
+
return typeof value === "string" ? value : void 0;
|
|
962
|
+
}
|
|
963
|
+
function isRecord(value) {
|
|
964
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
965
|
+
}
|
|
966
|
+
async function exists(path) {
|
|
967
|
+
try {
|
|
968
|
+
await access(path);
|
|
969
|
+
return true;
|
|
970
|
+
} catch (cause) {
|
|
971
|
+
if (isNotFound(cause)) return false;
|
|
972
|
+
throw cause;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function isNotFound(cause) {
|
|
976
|
+
return cause instanceof Error && cause.code === "ENOENT";
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
export { AGENTYX_CONFIG_FILENAME, AGENTYX_CONFIG_SCHEMA_ID, AGENTYX_PROFILES, AgentyxConfigNotFoundError, AgentyxConfigParseError, AgentyxConfigValidationError, AgentyxError, CircularStackDependencyError, DEFAULT_AGENTYX_PROFILE, DuplicateMcpServerError, DuplicateSkillError, DuplicateStackError, InvalidMcpServerError, InvalidSkillError, MCP_CAPABILITY_LEVELS, MCP_CONTEXT_COSTS, PACKAGE_MANAGERS, UnknownMcpServerError, UnknownSkillError, UnknownStackError, agentyxConfigPath, agentyxConfigSchema, agentyxCoreName, agentyxProfileSchema, buildAgentyxConfig, buildAgentyxConfigJsonSchema, builtInMcpServerNames, builtInMcpServerRegistry, builtInSkillNames, builtInSkillRegistry, builtInStackRegistry, builtInStacks, collectStackMcpServerReferences, collectStackMcpServers, createMcpServerRegistry, createSkillRegistry, createStackRegistry, detectProject, filterEffectiveMcpServers, formatAgentyxConfig, formatSkillMarkdown, getCoreStatus, getOptimizationProfile, isMcpLevelEnabled, loadAgentyxConfig, mcpCapabilityLevelSchema, mcpContextCostSchema, mcpEnvReferenceSchema, mcpServerDefinitionSchema, mcpServerReferenceSchema, optimizationProfileNames, optimizationProfiles, parseAgentyxConfig, parseSkillMarkdown, resolveAgentyxConfig, resolveStackMcpServerReferences, resolveStackMcpServers, resolveStackSkills, resolveStacks, skillDefinitionSchema, stackDefinitionSchema };
|
|
980
|
+
|
|
981
|
+
//# sourceMappingURL=index.mjs.map
|