@tacuchi/agent-factory 0.1.1 → 0.3.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.
@@ -39,6 +39,10 @@ program
39
39
  .option('-o, --output <path>', 'Output directory (default: current dir)')
40
40
  .option('-t, --target <target>', 'Target: claude, codex, all', 'all')
41
41
  .option('--tools <tools>', 'Comma-separated tools: Read,Write,Edit,Bash')
42
+ .option('--specialists <list>', 'CSV list of specialist agent names (for coordinator role)')
43
+ .option('--repo-count <n>', 'Number of repos in workspace (for coordinator role)', parseInt)
44
+ .option('--description <text>', 'Short description for the agent (for custom role)')
45
+ .option('--instructions <text>', 'Agent body: inline text or path to .md file (for custom role)')
42
46
  .option('-y, --yes', 'Skip confirmations')
43
47
  .action(async (options) => {
44
48
  const { runCreate } = require('../src/commands/create');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacuchi/agent-factory",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI to create AI agents for Claude Code, Codex, Gemini CLI and more",
5
5
  "bin": {
6
6
  "agent-factory": "bin/agent-factory.js"
@@ -10,37 +10,51 @@ async function runCreate(options = {}) {
10
10
  const isInteractive = !options.name;
11
11
  const config = isInteractive ? await askCreateOptions() : normalizeFlags(options);
12
12
 
13
- const { name, role, model, scope, output, target, tools } = config;
13
+ const { name, role, model, scope, output, target, tools, specialists, repoCount, description, instructions } = config;
14
14
 
15
15
  const spin = spinner('Generating agent...').start();
16
16
 
17
- let stackResult = { primaryTech: 'Generic', framework: '', verifyCommands: '', stackParts: [], stackCsv: 'Generic' };
18
- if (scope) {
19
- stackResult = await detect(scope);
20
- }
17
+ let body;
21
18
 
22
- const templateData = {
23
- name,
24
- primary_tech: stackResult.primaryTech,
25
- framework: stackResult.framework,
26
- scope: scope || '.',
27
- stack_list: stackResult.stackParts.length > 0
28
- ? stackResult.stackParts.map((p) => `- ${p}`).join('\n')
29
- : `- ${stackResult.primaryTech}`,
30
- verify_cmds: stackResult.verifyCommands || 'N/A',
31
- stack_csv: stackResult.stackCsv,
32
- };
19
+ if (role === 'custom') {
20
+ body = await resolveCustomBody(instructions, name, description);
21
+ spin.succeed('Custom agent generated');
22
+ } else {
23
+ let stackResult = { primaryTech: 'Generic', framework: '', verifyCommands: '', stackParts: [], stackCsv: 'Generic' };
24
+ if (scope) {
25
+ stackResult = await detect(scope);
26
+ }
33
27
 
34
- const templateFile = `${role}.md.tmpl`;
35
- let body;
36
- try {
37
- body = await renderFile(templateFile, templateData);
38
- } catch {
39
- spin.fail(`Template not found: ${templateFile}`);
40
- process.exit(1);
41
- }
28
+ const techLabel = stackResult.framework
29
+ ? `${stackResult.framework} (${stackResult.primaryTech})`
30
+ : stackResult.primaryTech;
31
+
32
+ const templateData = {
33
+ name,
34
+ primary_tech: stackResult.primaryTech,
35
+ tech_label: techLabel,
36
+ framework: stackResult.framework,
37
+ scope: scope || '.',
38
+ stack_list: stackResult.stackParts.length > 0
39
+ ? stackResult.stackParts.map((p) => `- ${p}`).join('\n')
40
+ : `- ${stackResult.primaryTech}`,
41
+ verify_cmds: stackResult.verifyCommands || 'N/A',
42
+ stack_csv: stackResult.stackCsv,
43
+ specialist_list: formatSpecialistList(specialists),
44
+ N: repoCount ? String(repoCount) : '',
45
+ repos_word: repoCount === 1 ? 'repositorio' : 'repositorios',
46
+ };
42
47
 
43
- spin.succeed('Agent generated');
48
+ const templateFile = `${role}.md.tmpl`;
49
+ try {
50
+ body = await renderFile(templateFile, templateData);
51
+ } catch {
52
+ spin.fail(`Template not found: ${templateFile}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ spin.succeed('Agent generated');
57
+ }
44
58
 
45
59
  const outputDir = output || process.cwd();
46
60
  const targetDirs = [];
@@ -64,6 +78,7 @@ async function runCreate(options = {}) {
64
78
  body,
65
79
  outputDir,
66
80
  target,
81
+ description: description || undefined,
67
82
  });
68
83
 
69
84
  const validator = new AgentValidator();
@@ -72,14 +87,18 @@ async function runCreate(options = {}) {
72
87
  const content = await fs.readFile(results.claude, 'utf8');
73
88
  const validation = validator.validate(content, `${name}.md`);
74
89
  if (validation.valid) {
75
- log.success(`Claude: ${results.claude} (score: ${validation.score}/100)`);
90
+ log.success(`Claude: ${results.claude} (score: ${validation.score}/100)`);
76
91
  } else {
77
- log.warn(`Claude: ${results.claude} (score: ${validation.score}/100, ${validation.errorCount} errors)`);
92
+ log.warn(`Claude: ${results.claude} (score: ${validation.score}/100, ${validation.errorCount} errors)`);
78
93
  }
79
94
  }
80
95
 
81
96
  if (results.codex) {
82
- log.success(`Codex: ${results.codex}`);
97
+ log.success(`Codex: ${results.codex}`);
98
+ }
99
+
100
+ if (results.skills) {
101
+ log.success(`Skills: ${results.skills}`);
83
102
  }
84
103
  }
85
104
 
@@ -93,7 +112,33 @@ function normalizeFlags(options) {
93
112
  target: options.target || 'all',
94
113
  tools: options.tools || '',
95
114
  yes: options.yes || false,
115
+ specialists: options.specialists || '',
116
+ repoCount: options.repoCount || 0,
117
+ description: options.description || '',
118
+ instructions: options.instructions || '',
96
119
  };
97
120
  }
98
121
 
122
+ function formatSpecialistList(csv) {
123
+ if (!csv) return '';
124
+ const names = csv.split(',').map((n) => n.trim()).filter(Boolean);
125
+ if (names.length === 0) return '';
126
+ if (names.length === 1) return `\`${names[0]}\``;
127
+ const last = names.pop();
128
+ return names.map((n) => `\`${n}\``).join(', ') + ` y \`${last}\``;
129
+ }
130
+
131
+ async function resolveCustomBody(instructions, name, description) {
132
+ if (!instructions) {
133
+ return `# ${name}\n\n${description || 'Custom agent.'}\n`;
134
+ }
135
+
136
+ const fs = require('fs-extra');
137
+ if (instructions.endsWith('.md') && await fs.pathExists(instructions)) {
138
+ return await fs.readFile(instructions, 'utf8');
139
+ }
140
+
141
+ return `# ${name}\n\n${instructions}\n`;
142
+ }
143
+
99
144
  module.exports = { runCreate };
@@ -32,6 +32,24 @@ async function runList(dirPath, options = {}) {
32
32
  }
33
33
  }
34
34
 
35
+ const skillsDir = path.join(abs, '.agents', 'skills');
36
+ if (await fs.pathExists(skillsDir)) {
37
+ const skillDirs = (await fs.readdir(skillsDir, { withFileTypes: true })).filter((d) => d.isDirectory());
38
+ for (const dir of skillDirs) {
39
+ const skillFile = path.join(skillsDir, dir.name, 'SKILL.md');
40
+ if (await fs.pathExists(skillFile)) {
41
+ const file = `${dir.name}.md`;
42
+ const existing = agents.find((a) => a.file === file);
43
+ if (existing) {
44
+ existing.skillsToo = true;
45
+ continue;
46
+ }
47
+ const content = await fs.readFile(skillFile, 'utf8');
48
+ agents.push({ file, source: '.agents/skills', content, format: 'skills' });
49
+ }
50
+ }
51
+ }
52
+
35
53
  if (agents.length === 0) {
36
54
  if (options.json) {
37
55
  console.log(JSON.stringify([]));
@@ -51,7 +69,7 @@ async function runList(dirPath, options = {}) {
51
69
  return {
52
70
  name: fm?.name || agent.file.replace('.md', ''),
53
71
  model: fm?.model || null,
54
- source: agent.codexToo ? 'both' : agent.source,
72
+ source: resolveSource(agent),
55
73
  score: validation.score,
56
74
  valid: validation.valid,
57
75
  };
@@ -72,7 +90,7 @@ async function runList(dirPath, options = {}) {
72
90
  const fm = extractFrontmatter(agent.content);
73
91
  const name = fm?.name || agent.file.replace('.md', '');
74
92
  const model = fm?.model || '—';
75
- const source = agent.codexToo ? 'both' : agent.source;
93
+ const source = resolveSource(agent);
76
94
 
77
95
  const validation = validator.validate(agent.content, agent.file);
78
96
  const scoreStr = `${validation.score}/100`;
@@ -100,6 +118,16 @@ async function runList(dirPath, options = {}) {
100
118
  console.log('');
101
119
  }
102
120
 
121
+ function resolveSource(agent) {
122
+ const parts = [];
123
+ if (agent.source === '.claude/agents' || agent.codexToo) parts.push('claude');
124
+ if (agent.source === '.agents' || agent.codexToo) parts.push('codex');
125
+ if (agent.skillsToo || agent.source === '.agents/skills') parts.push('skills');
126
+ if (parts.length === 3) return 'all';
127
+ if (parts.length === 0) return agent.source;
128
+ return parts.join('+');
129
+ }
130
+
103
131
  function padRow(name, model, source, score) {
104
132
  return ` ${name.padEnd(30)} ${model.padEnd(10)} ${source.padEnd(18)} ${score}`;
105
133
  }
@@ -4,9 +4,10 @@ const yaml = require('js-yaml');
4
4
 
5
5
  const TOOLS_BY_ROLE = {
6
6
  specialist: 'Read, Write, Edit, Bash',
7
- coordinator: 'Read, Bash, Task',
7
+ coordinator: 'Read, Glob, Grep, Task, Bash',
8
8
  reviewer: 'Read, Grep, Glob, Bash',
9
9
  architect: 'Read, Grep, Glob, Bash',
10
+ custom: 'Read, Bash',
10
11
  };
11
12
 
12
13
  function buildClaudeFormat(name, description, model, tools, body) {
@@ -32,9 +33,18 @@ function buildDescription(role, primaryTech, framework) {
32
33
  return descriptions[role] || `Agent for ${tech}`;
33
34
  }
34
35
 
35
- async function writeAgent({ name, role, model, tools, body, outputDir, target }) {
36
- const results = { claude: null, codex: null };
37
- const description = body.split('\n').find((l) => l.trim() && !l.startsWith('#'))?.trim() || name;
36
+ function buildSkillsFormat(name, description, body) {
37
+ return `---\nname: ${name}\ndescription: "${description}"\n---\n\n${body}\n`;
38
+ }
39
+
40
+ function ensureAgentSuffix(name) {
41
+ return name.endsWith('-agent') ? name : `${name}-agent`;
42
+ }
43
+
44
+ async function writeAgent({ name: rawName, role, model, tools, body, outputDir, target, description: customDesc }) {
45
+ const name = ensureAgentSuffix(rawName);
46
+ const results = { claude: null, codex: null, skills: null };
47
+ const description = customDesc || body.split('\n').find((l) => l.trim() && !l.startsWith('#'))?.trim() || name;
38
48
 
39
49
  const resolvedTools = tools || TOOLS_BY_ROLE[role] || 'Read, Write, Edit, Bash';
40
50
 
@@ -56,7 +66,16 @@ async function writeAgent({ name, role, model, tools, body, outputDir, target })
56
66
  results.codex = codexPath;
57
67
  }
58
68
 
69
+ if (target === 'codex' || target === 'all') {
70
+ const skillsDir = path.join(outputDir, '.agents', 'skills', name);
71
+ await fs.ensureDir(skillsDir);
72
+ const skillsContent = buildSkillsFormat(name, description, body);
73
+ const skillsPath = path.join(skillsDir, 'SKILL.md');
74
+ await fs.writeFile(skillsPath, skillsContent, 'utf8');
75
+ results.skills = skillsPath;
76
+ }
77
+
59
78
  return results;
60
79
  }
61
80
 
62
- module.exports = { writeAgent, buildClaudeFormat, buildCodexFormat, buildDescription, TOOLS_BY_ROLE };
81
+ module.exports = { writeAgent, buildClaudeFormat, buildCodexFormat, buildSkillsFormat, buildDescription, ensureAgentSuffix, TOOLS_BY_ROLE };
@@ -6,6 +6,7 @@ const ROLES = [
6
6
  { name: 'coordinator — Orchestrates across repos', value: 'coordinator' },
7
7
  { name: 'reviewer — Code review and quality analysis', value: 'reviewer' },
8
8
  { name: 'architect — System design and technical decisions', value: 'architect' },
9
+ { name: 'custom — Free-form agent with custom instructions', value: 'custom' },
9
10
  ];
10
11
 
11
12
  const MODELS = [
@@ -21,7 +22,7 @@ const TARGETS = [
21
22
  ];
22
23
 
23
24
  async function askCreateOptions() {
24
- const answers = await inquirer.prompt([
25
+ const baseQuestions = [
25
26
  {
26
27
  type: 'input',
27
28
  name: 'name',
@@ -40,13 +41,39 @@ async function askCreateOptions() {
40
41
  message: 'Model:',
41
42
  choices: MODELS,
42
43
  },
43
- {
44
- type: 'input',
45
- name: 'scope',
46
- message: 'Repository path (for stack detection, optional):',
47
- default: '',
48
- filter: (v) => (v ? path.resolve(v) : ''),
49
- },
44
+ ];
45
+
46
+ const answers = await inquirer.prompt(baseQuestions);
47
+
48
+ if (answers.role === 'custom') {
49
+ const customAnswers = await inquirer.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'description',
53
+ message: 'Short description for the agent:',
54
+ validate: (v) => v.length >= 5 || 'Description must be at least 5 characters',
55
+ },
56
+ {
57
+ type: 'editor',
58
+ name: 'instructions',
59
+ message: 'Agent instructions (opens editor):',
60
+ },
61
+ ]);
62
+ Object.assign(answers, customAnswers);
63
+ } else {
64
+ const scopeAnswers = await inquirer.prompt([
65
+ {
66
+ type: 'input',
67
+ name: 'scope',
68
+ message: 'Repository path (for stack detection, optional):',
69
+ default: '',
70
+ filter: (v) => (v ? path.resolve(v) : ''),
71
+ },
72
+ ]);
73
+ Object.assign(answers, scopeAnswers);
74
+ }
75
+
76
+ const outputAnswers = await inquirer.prompt([
50
77
  {
51
78
  type: 'list',
52
79
  name: 'target',
@@ -62,7 +89,7 @@ async function askCreateOptions() {
62
89
  },
63
90
  ]);
64
91
 
65
- return answers;
92
+ return { ...answers, ...outputAnswers };
66
93
  }
67
94
 
68
95
  async function confirmGeneration(agentName, files) {
@@ -1,17 +1,25 @@
1
1
  # {{name}}
2
2
 
3
- Coordinador del workspace. Proporciona vision global, orienta sobre que especialista usar y consolida resultados.
3
+ Guia multi-repo del workspace. Proporciona vision global de {{N}} {{repos_word}} externos, orienta sobre que especialista usar y consolida resultados.
4
+
5
+ ## Agentes especialistas disponibles
6
+
7
+ {{specialist_list}}
8
+
9
+ Cada especialista opera exclusivamente dentro de su repo asignado. Para ejecutar cambios en un repo especifico, invoca al especialista correspondiente usando `/agents` o `Task`.
4
10
 
5
11
  ## Reglas
6
12
 
7
- - **Vision global**: este agente conoce la arquitectura completa del workspace.
13
+ - **Vision global**: este agente conoce la arquitectura completa del workspace y la relacion entre los repos.
8
14
  - Puede **leer y buscar** en cualquier repo del workspace para analisis cross-repo.
9
- - **No implementar cambios directamente** en repos externos. Indicar al usuario que invoque al especialista correspondiente.
15
+ - **No implementar cambios directamente** en repos externos. Indicar al usuario que invoque al especialista correspondiente, o usar la herramienta Task para delegarle la subtarea.
16
+ - Cuando haya subtareas independientes en repos distintos, sugerir al usuario ejecutarlas en paralelo.
10
17
  - Siempre producir un **resumen** con:
11
18
  - Plan de accion por repo
12
19
  - Especialista recomendado para cada subtarea
13
20
  - Comandos de verificacion pendientes
14
- - Consolidar resultados y documentacion en `./docs/`.
21
+ - Consolidar resultados y documentacion en `./docs/<tema>/`.
22
+ - Seguir el protocolo cross-repo definido en AGENTS.md (o CLAUDE.md): planificar primero, cambios minimos, reportar archivos tocados.
15
23
 
16
24
  ## Flujo de trabajo
17
25
 
@@ -20,3 +28,7 @@ Coordinador del workspace. Proporciona vision global, orienta sobre que especial
20
28
  3. Si es solo un repo: orientar al usuario hacia el especialista correspondiente.
21
29
  4. Si es cross-repo: planificar la secuencia, indicar el orden de invocacion de especialistas.
22
30
  5. Consolidar documentacion y reportar al usuario.
31
+
32
+ ## Agentes globales (gestionados por el usuario)
33
+
34
+ Si el usuario ha creado agentes globales/personales en el workspace (por ejemplo, agentes transversales de arquitectura, estilo o revision de codigo), puedes invocarlos para obtener analisis cross-repo. Estos agentes son gestionados directamente por el usuario y pueden no estar presentes en todos los workspaces.
@@ -1,6 +1,6 @@
1
1
  # {{name}}
2
2
 
3
- Especialista {{primary_tech}} — scope exclusivo: `{{scope}}`
3
+ Especialista {{tech_label}} — scope exclusivo: `{{scope}}`
4
4
 
5
5
  ## Stack
6
6