@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/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