awesome-agents 0.1.0 → 0.1.3

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/src/installer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { DEFAULT_AGENT, SUPPORTED_AGENTS } from "./constants.js";
4
+ import { AGENT_ALIASES, DEFAULT_AGENT, SUPPORTED_AGENTS } from "./constants.js";
5
5
  import { contentHash, isGeneratedContent, normalizeAgentList, renderForAgent, resolveTargetPath } from "./renderers.js";
6
6
  import { loadCatalog, materializeSource, splitSourceSpec } from "./source.js";
7
7
  import { readRegistry, registryPath, removeInstall, upsertInstall, writeRegistry } from "./registry.js";
@@ -20,13 +20,12 @@ export async function listAvailable(sourceInput, options = {}) {
20
20
  export async function installFromSource(sourceSpec, options = {}) {
21
21
  const split = splitSourceSpec(sourceSpec);
22
22
  const source = split.source;
23
- const optionProfiles = normalizeProfileSelectors(options.profiles ?? options.profile ?? options.skills ?? options.skill);
24
- const selectors = split.profile ? [...optionProfiles, split.profile] : optionProfiles;
25
- const scope = resolveScope(options);
26
- const harnesses = normalizeAgentList(options.agents ?? options.agent, {
23
+ const { selectors, harnessInput } = resolveInstallSelection(split, options);
24
+ const harnesses = normalizeAgentList(harnessInput, {
27
25
  all: options.all,
28
26
  defaultAgent: DEFAULT_AGENT
29
27
  });
28
+ const scope = resolveScope(options, harnesses);
30
29
 
31
30
  const materialized = await materializeSource(source, options);
32
31
  try {
@@ -72,7 +71,11 @@ export async function installFromSource(sourceSpec, options = {}) {
72
71
  await writeRegistry(registry, registryOptions);
73
72
  }
74
73
 
75
- return { operations, registryPath: registryPath(registryOptions) };
74
+ return {
75
+ operations,
76
+ registryPath: registryPath(registryOptions),
77
+ runInstructions: buildRunInstructions(operations)
78
+ };
76
79
  } finally {
77
80
  await materialized.cleanup();
78
81
  }
@@ -80,12 +83,12 @@ export async function installFromSource(sourceSpec, options = {}) {
80
83
 
81
84
  export async function useFromSource(sourceSpec, options = {}) {
82
85
  const split = splitSourceSpec(sourceSpec);
83
- const profileSelector = options.profile ?? options.skill ?? split.profile;
86
+ const { profileSelector, harnessInput } = resolveUseSelection(split, options);
84
87
  if (!profileSelector) {
85
88
  throw new Error("Specify a profile with source@profile or --profile <profile>.");
86
89
  }
87
90
 
88
- const harness = normalizeAgentList(options.agent ? [options.agent] : options.agents, {
91
+ const harness = normalizeAgentList(harnessInput, {
89
92
  defaultAgent: DEFAULT_AGENT
90
93
  })[0];
91
94
  const materialized = await materializeSource(split.source, options);
@@ -105,10 +108,11 @@ export async function useFromSource(sourceSpec, options = {}) {
105
108
  }
106
109
 
107
110
  export async function listInstalled(options = {}) {
108
- const scope = resolveScope(options);
109
- const registry = await readRegistry({ ...options, scope });
110
111
  const harnessInput = options.agents ?? options.agent;
111
- const harnessFilter = harnessInput ? new Set(normalizeAgentList(harnessInput)) : undefined;
112
+ const harnesses = harnessInput ? normalizeAgentList(harnessInput) : [];
113
+ const scope = resolveScope(options, harnesses);
114
+ const registry = await readRegistry({ ...options, scope });
115
+ const harnessFilter = harnesses.length > 0 ? new Set(harnesses) : undefined;
112
116
  const installs = registry.installs
113
117
  .filter((install) => !harnessFilter || harnessFilter.has(install.harness))
114
118
  .map((install) => ({
@@ -124,7 +128,9 @@ export async function listInstalled(options = {}) {
124
128
  }
125
129
 
126
130
  export async function removeInstalled(profileArgs = [], options = {}) {
127
- const scope = resolveScope(options);
131
+ const harnessInput = options.agents ?? options.agent;
132
+ const harnesses = harnessInput ? normalizeAgentList(harnessInput) : [];
133
+ const scope = resolveScope(options, harnesses);
128
134
  const registryOptions = { ...options, scope };
129
135
  const registry = await readRegistry(registryOptions);
130
136
  const selectors = normalizeProfileSelectors(profileArgs);
@@ -133,8 +139,7 @@ export async function removeInstalled(profileArgs = [], options = {}) {
133
139
  throw new Error("Specify at least one profile to remove, or pass --all.");
134
140
  }
135
141
 
136
- const harnessInput = options.agents ?? options.agent;
137
- const harnessFilter = harnessInput ? new Set(normalizeAgentList(harnessInput)) : undefined;
142
+ const harnessFilter = harnesses.length > 0 ? new Set(harnesses) : undefined;
138
143
  const operations = [];
139
144
  const matched = registry.installs.filter((install) => {
140
145
  const profileMatch = removeAll || selectors.includes(install.profile);
@@ -173,12 +178,13 @@ export async function removeInstalled(profileArgs = [], options = {}) {
173
178
  }
174
179
 
175
180
  export async function updateInstalled(profileArgs = [], options = {}) {
176
- const scope = resolveScope(options);
181
+ const harnessInput = options.agents ?? options.agent;
182
+ const harnesses = harnessInput ? normalizeAgentList(harnessInput) : [];
183
+ const scope = resolveScope(options, harnesses);
177
184
  const registryOptions = { ...options, scope };
178
185
  const registry = await readRegistry(registryOptions);
179
186
  const selectors = normalizeProfileSelectors(profileArgs);
180
- const harnessInput = options.agents ?? options.agent;
181
- const harnessFilter = harnessInput ? new Set(normalizeAgentList(harnessInput)) : undefined;
187
+ const harnessFilter = harnesses.length > 0 ? new Set(harnesses) : undefined;
182
188
  const candidates = registry.installs.filter((install) => {
183
189
  const profileMatch = selectors.length === 0 || selectors.includes(install.profile);
184
190
  const harnessMatch = !harnessFilter || harnessFilter.has(install.harness);
@@ -186,6 +192,7 @@ export async function updateInstalled(profileArgs = [], options = {}) {
186
192
  });
187
193
 
188
194
  const operations = [];
195
+ const runInstructions = [];
189
196
  for (const install of candidates) {
190
197
  const result = await installFromSource(install.source, {
191
198
  ...options,
@@ -199,42 +206,126 @@ export async function updateInstalled(profileArgs = [], options = {}) {
199
206
  ...operation,
200
207
  action: options.dryRun ? "would-update" : "updated"
201
208
  })));
209
+ runInstructions.push(...result.runInstructions);
202
210
  }
203
211
 
204
- return { operations, registryPath: registryPath(registryOptions) };
212
+ return { operations, registryPath: registryPath(registryOptions), runInstructions };
205
213
  }
206
214
 
207
215
  export async function initProfile(name, options = {}) {
208
216
  const slug = slugify(name || "new-agent-profile");
209
217
  const root = options.cwd ?? process.cwd();
210
- const profilePath = path.join(root, "agents", "profiles", `${slug}.md`);
211
- const adapterPath = path.join(root, "agents", "adapters", "codex", `${slug}.md`);
218
+ const profilePath = path.join(root, "agents", "profiles", `${slug}.agent.yaml`);
212
219
 
213
220
  if (!options.force) {
214
- for (const target of [profilePath, adapterPath]) {
215
- if (existsSync(target)) {
216
- throw new Error(`${target} already exists. Pass --force to overwrite.`);
217
- }
221
+ if (existsSync(profilePath)) {
222
+ throw new Error(`${profilePath} already exists. Pass --force to overwrite.`);
218
223
  }
219
224
  }
220
225
 
221
226
  const title = titleCase(slug);
222
- const profileContent = `---\nslug: ${slug}\nname: ${title}\nkind: operational-agent-profile\nsummary: Describe when to use this agent profile.\nrecommended_model: inherit\nrecommended_reasoning_effort: medium\nhome_notes_template: "~/.agents/homes/${slug}/<project>/notes"\n---\n\n# ${title}\n\nDescribe the agent's mission, boundaries, tools, coordination model, notes, and reporting style.\n`;
223
- const adapterContent = `---\nslug: ${slug}\nprofile: ../../profiles/${slug}.md\nharness: codex\nmodel: inherit\nreasoning_effort: medium\nrequired_skills: []\n---\n\n# Codex Adapter: ${title}\n\nLoad the canonical profile at \`agents/profiles/${slug}.md\`.\n`;
227
+ const profileContent = `schema: awesome-agents/v1\nid: ${slug}\nname: ${title}\ndescription: Describe when to use this agent profile.\nmodel: inherit\nreasoning_effort: medium\nhome_notes_template: "~/.agents/homes/${slug}/<project>/notes"\ninstructions: |\n Describe the agent's mission, boundaries, tools, coordination model, notes,\n and reporting style.\n`;
224
228
 
225
229
  if (!options.dryRun) {
226
230
  await fs.mkdir(path.dirname(profilePath), { recursive: true });
227
- await fs.mkdir(path.dirname(adapterPath), { recursive: true });
228
231
  await fs.writeFile(profilePath, profileContent);
229
- await fs.writeFile(adapterPath, adapterContent);
230
232
  }
231
233
 
232
234
  return {
233
235
  action: options.dryRun ? "would-init" : "initialized",
234
- files: [profilePath, adapterPath]
236
+ files: [profilePath]
235
237
  };
236
238
  }
237
239
 
240
+ function buildRunInstructions(operations, env = process.env) {
241
+ const instructions = [];
242
+ const seen = new Set();
243
+
244
+ for (const operation of operations) {
245
+ if (String(operation.action).startsWith("would-")) {
246
+ continue;
247
+ }
248
+
249
+ const instruction = runInstructionForOperation(operation, env);
250
+ if (!instruction) {
251
+ continue;
252
+ }
253
+
254
+ const key = `${instruction.harness}:${instruction.profile}:${instruction.command}`;
255
+ if (seen.has(key)) {
256
+ continue;
257
+ }
258
+ seen.add(key);
259
+ instructions.push(instruction);
260
+ }
261
+
262
+ return instructions;
263
+ }
264
+
265
+ function runInstructionForOperation(operation, env) {
266
+ if (operation.harness === "codex") {
267
+ if (!commandExists("codex", env)) {
268
+ return undefined;
269
+ }
270
+ return {
271
+ profile: operation.profile,
272
+ harness: operation.harness,
273
+ command: `codex --profile ${shellWord(operation.profile)}`,
274
+ note: "Starts Codex with this installed profile."
275
+ };
276
+ }
277
+
278
+ if (operation.harness === "claude-code") {
279
+ if (!commandExists("claude", env)) {
280
+ return undefined;
281
+ }
282
+ return {
283
+ profile: operation.profile,
284
+ harness: operation.harness,
285
+ command: `claude --agent ${shellWord(operation.profile)}`,
286
+ note: "Starts Claude Code with this installed agent profile."
287
+ };
288
+ }
289
+
290
+ if (operation.harness === "opencode") {
291
+ if (!commandExists("opencode", env)) {
292
+ return undefined;
293
+ }
294
+ return {
295
+ profile: operation.profile,
296
+ harness: operation.harness,
297
+ command: "opencode",
298
+ note: `Start OpenCode, then invoke @${operation.profile} in the session.`
299
+ };
300
+ }
301
+
302
+ return undefined;
303
+ }
304
+
305
+ function commandExists(command, env) {
306
+ const pathValue = env.PATH ?? "";
307
+ if (!pathValue) {
308
+ return false;
309
+ }
310
+
311
+ const extensions = process.platform === "win32"
312
+ ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
313
+ : [""];
314
+
315
+ return pathValue
316
+ .split(path.delimiter)
317
+ .filter(Boolean)
318
+ .some((directory) => extensions.some((extension) => existsSync(path.join(directory, `${command}${extension}`))));
319
+ }
320
+
321
+ function shellWord(value) {
322
+ const text = String(value);
323
+ if (/^[A-Za-z0-9._/@:-]+$/.test(text)) {
324
+ return text;
325
+ }
326
+ return `'${text.replaceAll("'", "'\\''")}'`;
327
+ }
328
+
238
329
  function selectProfiles(profiles, selectors, all = false) {
239
330
  if (all || selectors.length === 0 || selectors.includes("*")) {
240
331
  return profiles;
@@ -269,11 +360,23 @@ async function writeManagedFile(target, content, options = {}) {
269
360
  await fs.writeFile(target, content);
270
361
  }
271
362
 
272
- function resolveScope(options = {}) {
363
+ function resolveScope(options = {}, harnesses = []) {
273
364
  if (options.project && options.global) {
274
365
  throw new Error("Use either --global or --project, not both.");
275
366
  }
276
- return options.global ? "global" : "project";
367
+ if (options.project && harnesses.includes("codex")) {
368
+ throw new Error("Codex profiles are loaded from $CODEX_HOME/<name>.config.toml and cannot be installed project-locally. Use --global or choose a different harness.");
369
+ }
370
+ if (options.global) {
371
+ return "global";
372
+ }
373
+ if (options.project) {
374
+ return "project";
375
+ }
376
+ if (harnesses.includes("codex")) {
377
+ return "global";
378
+ }
379
+ return "project";
277
380
  }
278
381
 
279
382
  function normalizeProfileSelectors(values) {
@@ -285,6 +388,72 @@ function normalizeProfileSelectors(values) {
285
388
  .filter(Boolean);
286
389
  }
287
390
 
391
+ function resolveInstallSelection(split, options = {}) {
392
+ const selectors = [
393
+ ...normalizeProfileSelectors(options.profiles ?? options.profile ?? options.skills ?? options.skill)
394
+ ];
395
+ const explicitHarnessInput = firstDefined(options.harnesses, options.harness, options.targets, options.target);
396
+ const agentValues = normalizeProfileSelectors(options.agents ?? options.agent);
397
+ const agentHarnesses = [];
398
+
399
+ for (const value of agentValues) {
400
+ if (isHarnessSelector(value)) {
401
+ agentHarnesses.push(value);
402
+ } else {
403
+ selectors.push(value);
404
+ }
405
+ }
406
+
407
+ if (split.profile) {
408
+ selectors.push(split.profile);
409
+ }
410
+
411
+ return {
412
+ selectors,
413
+ harnessInput: explicitHarnessInput ?? (agentHarnesses.length > 0 ? agentHarnesses : undefined)
414
+ };
415
+ }
416
+
417
+ function resolveUseSelection(split, options = {}) {
418
+ const selectors = [
419
+ ...normalizeProfileSelectors(options.profiles ?? options.profile ?? options.skills ?? options.skill)
420
+ ];
421
+ const explicitHarnessInput = firstDefined(options.harnesses, options.harness, options.targets, options.target);
422
+ const agentValues = normalizeProfileSelectors(options.agents ?? options.agent);
423
+ const agentHarnesses = [];
424
+
425
+ for (const value of agentValues) {
426
+ if (isHarnessSelector(value)) {
427
+ agentHarnesses.push(value);
428
+ } else {
429
+ selectors.push(value);
430
+ }
431
+ }
432
+
433
+ if (split.profile) {
434
+ selectors.push(split.profile);
435
+ }
436
+
437
+ const uniqueSelectors = [...new Set(selectors)];
438
+ if (uniqueSelectors.length > 1) {
439
+ throw new Error(`Use renders one profile at a time. Received: ${uniqueSelectors.join(", ")}`);
440
+ }
441
+
442
+ return {
443
+ profileSelector: uniqueSelectors[0],
444
+ harnessInput: explicitHarnessInput ?? (agentHarnesses.length > 0 ? agentHarnesses : undefined)
445
+ };
446
+ }
447
+
448
+ function isHarnessSelector(value) {
449
+ const normalized = String(value).toLowerCase();
450
+ return normalized === "*" || AGENT_ALIASES.has(normalized);
451
+ }
452
+
453
+ function firstDefined(...values) {
454
+ return values.find((value) => value !== undefined);
455
+ }
456
+
288
457
  function profileSummary(profile, source) {
289
458
  return {
290
459
  slug: profile.slug,
package/src/renderers.js CHANGED
@@ -56,12 +56,7 @@ export function resolveTargetPath(profile, agent, options = {}) {
56
56
  : process.env.CODEX_HOME
57
57
  ? path.resolve(expandHome(process.env.CODEX_HOME, home))
58
58
  : path.join(home, ".codex");
59
-
60
- if (scope === "project") {
61
- return path.join(cwd, ".codex", "agents", `${profile.slug}.toml`);
62
- }
63
-
64
- return path.join(codexHome, "agents", `${profile.slug}.toml`);
59
+ return path.join(codexHome, `${profile.slug}.config.toml`);
65
60
  }
66
61
 
67
62
  if (normalized === "claude-code") {
@@ -170,9 +165,11 @@ function buildInstructionBody(profile, adapter, harness) {
170
165
  "",
171
166
  "## Installed Profile Context",
172
167
  "",
173
- `- Canonical profile slug: \`${profile.slug}\``,
168
+ `- Installed identity: \`${profile.slug}\``,
169
+ `- Role/name: \`${profile.name}\``,
174
170
  `- Installed for: \`${harness}\``,
175
- "- This profile is a reusable operational agent profile, not local machine setup.",
171
+ "- When asked who you are, what agent is running, or what role you are acting as, answer with this identity and role.",
172
+ "- This profile is a reusable operational agent profile, not a skill or local machine setup.",
176
173
  "- The runtime agent manages any profile-specific home directory and notes at task time."
177
174
  ];
178
175
 
package/src/source.js CHANGED
@@ -4,14 +4,24 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { DEFAULT_SOURCE } from "./constants.js";
7
+ import YAML from "yaml";
8
8
  import { parseFrontmatter } from "./frontmatter.js";
9
9
 
10
10
  const GITHUB_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
11
+ const PROFILE_SUFFIXES = [
12
+ ".agent.yaml",
13
+ ".agent.yml",
14
+ ".agf.yaml",
15
+ ".agf.yml",
16
+ ".yaml",
17
+ ".yml",
18
+ ".agent.md",
19
+ ".md"
20
+ ];
11
21
 
12
22
  export function splitSourceSpec(spec) {
13
23
  if (!spec) {
14
- return { source: DEFAULT_SOURCE, profile: undefined };
24
+ return { source: undefined, profile: undefined };
15
25
  }
16
26
 
17
27
  const atIndex = spec.lastIndexOf("@");
@@ -57,13 +67,17 @@ export function normalizeGithubUrl(source) {
57
67
  return undefined;
58
68
  }
59
69
 
60
- export async function materializeSource(sourceInput = DEFAULT_SOURCE, options = {}) {
70
+ export async function materializeSource(sourceInput, options = {}) {
61
71
  const home = options.home ?? os.homedir();
62
- const source = sourceInput || DEFAULT_SOURCE;
72
+ const source = sourceInput;
73
+
74
+ if (!source) {
75
+ throw new Error("Specify a source. Use a local path, owner/repo, or GitHub URL.");
76
+ }
63
77
 
64
78
  if (isLocalSource(source, home)) {
65
79
  const sourcePath = path.resolve(expandHome(source, home));
66
- await assertTouchGrassLayout(sourcePath);
80
+ await assertAgentProfileLayout(sourcePath);
67
81
  return {
68
82
  kind: "local",
69
83
  source: sourcePath,
@@ -89,7 +103,7 @@ export async function materializeSource(sourceInput = DEFAULT_SOURCE, options =
89
103
  throw new Error(`Could not clone ${source}: ${detail}`);
90
104
  }
91
105
 
92
- await assertTouchGrassLayout(cloneDir);
106
+ await assertAgentProfileLayout(cloneDir);
93
107
 
94
108
  return {
95
109
  kind: "git",
@@ -102,7 +116,7 @@ export async function materializeSource(sourceInput = DEFAULT_SOURCE, options =
102
116
  };
103
117
  }
104
118
 
105
- async function assertTouchGrassLayout(sourcePath) {
119
+ async function assertAgentProfileLayout(sourcePath) {
106
120
  const profilesDir = path.join(sourcePath, "agents", "profiles");
107
121
  if (!existsSync(profilesDir)) {
108
122
  throw new Error(`No agents/profiles directory found in ${sourcePath}`);
@@ -116,25 +130,15 @@ export async function loadCatalog(sourcePath) {
116
130
  const profiles = [];
117
131
 
118
132
  for (const file of files) {
119
- if (!file.isFile() || !file.name.endsWith(".md")) {
133
+ if (!file.isFile() || !isProfileFile(file.name)) {
120
134
  continue;
121
135
  }
122
136
 
123
137
  const filePath = path.join(profilesDir, file.name);
124
- const raw = await fs.readFile(filePath, "utf8");
125
- const parsed = parseFrontmatter(raw, filePath);
126
- const slug = parsed.attributes.slug ?? path.basename(file.name, ".md");
127
-
138
+ const profile = await loadProfileFile(filePath, sourcePath);
128
139
  profiles.push({
129
- slug,
130
- name: parsed.attributes.name ?? slug,
131
- summary: parsed.attributes.summary ?? "",
132
- attributes: parsed.attributes,
133
- body: parsed.body,
134
- raw,
135
- filePath,
136
- relativePath: path.relative(sourcePath, filePath),
137
- adapters: await loadAdapters(adapterRoot, slug, sourcePath)
140
+ ...profile,
141
+ adapters: await loadAdapters(adapterRoot, profile.slug, sourcePath)
138
142
  });
139
143
  }
140
144
 
@@ -154,13 +158,13 @@ async function loadAdapters(adapterRoot, slug, sourcePath) {
154
158
  continue;
155
159
  }
156
160
 
157
- const adapterPath = path.join(adapterRoot, harnessDir.name, `${slug}.md`);
161
+ const adapterPath = await findProfileLikeFile(path.join(adapterRoot, harnessDir.name), slug);
158
162
  if (!existsSync(adapterPath)) {
159
163
  continue;
160
164
  }
161
165
 
162
166
  const raw = await fs.readFile(adapterPath, "utf8");
163
- const parsed = parseFrontmatter(raw, adapterPath);
167
+ const parsed = await loadDefinitionFile(adapterPath, raw);
164
168
  adapters[harnessDir.name] = {
165
169
  harness: harnessDir.name,
166
170
  attributes: parsed.attributes,
@@ -174,6 +178,179 @@ async function loadAdapters(adapterRoot, slug, sourcePath) {
174
178
  return adapters;
175
179
  }
176
180
 
181
+ async function loadProfileFile(filePath, sourcePath) {
182
+ const raw = await fs.readFile(filePath, "utf8");
183
+ const parsed = await loadDefinitionFile(filePath, raw);
184
+ const fallbackSlug = profileSlugFromFilename(path.basename(filePath));
185
+ const slug = parsed.attributes.slug ?? parsed.attributes.id ?? fallbackSlug;
186
+ const name = parsed.attributes.name ?? titleize(slug);
187
+ const summary = parsed.attributes.summary ?? parsed.attributes.description ?? "";
188
+
189
+ return {
190
+ slug,
191
+ name,
192
+ summary,
193
+ attributes: {
194
+ ...parsed.attributes,
195
+ slug,
196
+ name,
197
+ summary
198
+ },
199
+ body: parsed.body,
200
+ raw,
201
+ filePath,
202
+ relativePath: path.relative(sourcePath, filePath),
203
+ format: parsed.format
204
+ };
205
+ }
206
+
207
+ async function loadDefinitionFile(filePath, rawInput) {
208
+ const raw = rawInput ?? await fs.readFile(filePath, "utf8");
209
+ if (isMarkdownProfile(filePath)) {
210
+ const parsed = parseFrontmatter(raw, filePath);
211
+ return {
212
+ attributes: normalizeFlatAttributes(parsed.attributes),
213
+ body: parsed.body,
214
+ format: "markdown-frontmatter"
215
+ };
216
+ }
217
+
218
+ let document;
219
+ try {
220
+ document = YAML.parse(raw) ?? {};
221
+ } catch (error) {
222
+ throw new Error(`Could not parse YAML profile in ${filePath}: ${error.message}`);
223
+ }
224
+ if (!isObject(document)) {
225
+ throw new Error(`YAML profile in ${filePath} must be a mapping/object.`);
226
+ }
227
+
228
+ const normalized = normalizeYamlProfile(document, filePath);
229
+ return {
230
+ attributes: normalized.attributes,
231
+ body: normalized.body,
232
+ format: normalized.format
233
+ };
234
+ }
235
+
236
+ function normalizeYamlProfile(document, filePath) {
237
+ const metadata = isObject(document.metadata) ? document.metadata : {};
238
+ const executionPolicy = isObject(document.execution_policy) ? document.execution_policy : {};
239
+ const executionConfig = isObject(executionPolicy.config) ? executionPolicy.config : {};
240
+
241
+ const id = firstString(
242
+ document.slug,
243
+ document.id,
244
+ metadata.id,
245
+ profileSlugFromFilename(path.basename(filePath))
246
+ );
247
+ const name = firstString(document.name, metadata.name, titleize(id));
248
+ const description = firstString(document.summary, document.description, metadata.description, "");
249
+ const instructions = firstString(
250
+ document.instructions,
251
+ document.instruction,
252
+ document.prompt,
253
+ document.system_prompt,
254
+ executionConfig.instructions,
255
+ ""
256
+ );
257
+ const model = firstString(document.recommended_model, document.model, executionConfig.model, "");
258
+ const reasoningEffort = firstString(
259
+ document.recommended_reasoning_effort,
260
+ document.reasoning_effort,
261
+ document.model_reasoning_effort,
262
+ executionConfig.reasoning_effort,
263
+ ""
264
+ );
265
+ const recommendedModels = document.recommended_models ?? document.models ?? undefined;
266
+
267
+ const attributes = {
268
+ ...document,
269
+ slug: id,
270
+ id,
271
+ name,
272
+ summary: description,
273
+ description,
274
+ kind: document.kind ?? document.type ?? "operational-agent-profile"
275
+ };
276
+
277
+ if (model && !attributes.recommended_model) {
278
+ attributes.recommended_model = model;
279
+ }
280
+ if (reasoningEffort && !attributes.recommended_reasoning_effort) {
281
+ attributes.recommended_reasoning_effort = reasoningEffort;
282
+ }
283
+ if (recommendedModels && !attributes.recommended_models) {
284
+ attributes.recommended_models = recommendedModels;
285
+ }
286
+
287
+ return {
288
+ attributes,
289
+ body: renderYamlProfileBody(name, description, instructions),
290
+ format: isAgentFormat(document) ? "agent-format-yaml" : "yaml"
291
+ };
292
+ }
293
+
294
+ function normalizeFlatAttributes(attributes) {
295
+ return {
296
+ ...attributes,
297
+ summary: attributes.summary ?? attributes.description ?? ""
298
+ };
299
+ }
300
+
301
+ function renderYamlProfileBody(name, description, instructions) {
302
+ const body = instructions || description || "No instructions provided.";
303
+ return `# ${name}\n\n${body}`.trimEnd();
304
+ }
305
+
306
+ async function findProfileLikeFile(directory, slug) {
307
+ for (const suffix of PROFILE_SUFFIXES) {
308
+ const candidate = path.join(directory, `${slug}${suffix}`);
309
+ if (existsSync(candidate)) {
310
+ return candidate;
311
+ }
312
+ }
313
+ return path.join(directory, `${slug}.md`);
314
+ }
315
+
316
+ function isProfileFile(filename) {
317
+ return PROFILE_SUFFIXES.some((suffix) => filename.endsWith(suffix));
318
+ }
319
+
320
+ function isMarkdownProfile(filePath) {
321
+ return filePath.endsWith(".md");
322
+ }
323
+
324
+ function profileSlugFromFilename(filename) {
325
+ const suffix = PROFILE_SUFFIXES.find((value) => filename.endsWith(value));
326
+ return suffix ? filename.slice(0, -suffix.length) : path.basename(filename, path.extname(filename));
327
+ }
328
+
329
+ function isAgentFormat(document) {
330
+ return isObject(document.metadata) && isObject(document.execution_policy);
331
+ }
332
+
333
+ function isObject(value) {
334
+ return value !== null && typeof value === "object" && !Array.isArray(value);
335
+ }
336
+
337
+ function firstString(...values) {
338
+ for (const value of values) {
339
+ if (typeof value === "string" && value.trim()) {
340
+ return value.trim();
341
+ }
342
+ }
343
+ return "";
344
+ }
345
+
346
+ function titleize(slug) {
347
+ return String(slug)
348
+ .split(/[-_]+/)
349
+ .filter(Boolean)
350
+ .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
351
+ .join(" ");
352
+ }
353
+
177
354
  export function resolvePackageRoot() {
178
355
  return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
179
356
  }