@tacuchi/agent-factory 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 +37 -0
- package/README.md +142 -0
- package/bin/agent-factory.js +70 -0
- package/package.json +45 -0
- package/src/commands/create.js +99 -0
- package/src/commands/detect.js +56 -0
- package/src/commands/init.js +115 -0
- package/src/commands/list.js +117 -0
- package/src/core/agent-validator.js +162 -0
- package/src/core/agent-writer.js +62 -0
- package/src/core/stack-detector.js +319 -0
- package/src/core/template-engine.js +31 -0
- package/src/utils/logger.js +57 -0
- package/src/utils/prompts.js +96 -0
- package/templates/roles/architect.md.tmpl +26 -0
- package/templates/roles/coordinator.md.tmpl +22 -0
- package/templates/roles/reviewer.md.tmpl +28 -0
- package/templates/roles/specialist.md.tmpl +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tacuchi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Acknowledgments
|
|
26
|
+
|
|
27
|
+
This project was inspired in part by claude-code-templates
|
|
28
|
+
(https://github.com/davila7/claude-code-templates) by Daniel Γvila,
|
|
29
|
+
licensed under the MIT License. Specifically, the following concepts were
|
|
30
|
+
studied as reference:
|
|
31
|
+
|
|
32
|
+
- Stack detection patterns (detectProject utility)
|
|
33
|
+
- YAML frontmatter format for Claude Code agent definitions
|
|
34
|
+
- CLI structure using commander + inquirer + chalk + ora
|
|
35
|
+
|
|
36
|
+
All code in agent-factory is original work. No source code was copied
|
|
37
|
+
from claude-code-templates.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# π agent-factory
|
|
2
|
+
|
|
3
|
+
CLI to create AI agents for **Claude Code**, **Codex**, **Gemini CLI** and more.
|
|
4
|
+
|
|
5
|
+
Analyzes your repository's tech stack and generates role-based agents ready to use.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g agent-factory
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx agent-factory <command>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### `detect <path>`
|
|
22
|
+
|
|
23
|
+
Detect the technology stack of a repository.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agent-factory detect ./my-project
|
|
27
|
+
agent-factory detect ./my-project --json # Pure JSON output
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `init <path>`
|
|
31
|
+
|
|
32
|
+
Analyze a repository and generate suggested agents automatically.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
agent-factory init ./my-project
|
|
36
|
+
agent-factory init ./my-project -y # Skip confirmations
|
|
37
|
+
agent-factory init ./my-project --output ./workspace -y # Write agents elsewhere
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `create`
|
|
41
|
+
|
|
42
|
+
Create a single agent interactively or with flags.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
agent-factory create
|
|
46
|
+
agent-factory create -n api-expert -r specialist -s ./my-project -y
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `list [path]`
|
|
50
|
+
|
|
51
|
+
List existing agents in a directory.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agent-factory list
|
|
55
|
+
agent-factory list ./my-project --json # JSON output
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Global flags
|
|
59
|
+
|
|
60
|
+
| Flag | Description |
|
|
61
|
+
|------|-------------|
|
|
62
|
+
| `-q, --quiet` | Suppress all visual output (banners, spinners, logs). Useful for programmatic/SKILL usage. |
|
|
63
|
+
| `-V, --version` | Show version |
|
|
64
|
+
| `-h, --help` | Show help |
|
|
65
|
+
|
|
66
|
+
## Supported stacks
|
|
67
|
+
|
|
68
|
+
**Languages & frameworks:** JavaScript/TypeScript (Angular, React, Vue, Next.js, Svelte, Nuxt, Node.js), Java (Spring Boot β Maven & Gradle), Kotlin, Python (Django, FastAPI, Flask), Go, Rust, Dart/Flutter, .NET, Ruby on Rails.
|
|
69
|
+
|
|
70
|
+
**Supplementary:** Docker, SCSS, Tailwind CSS, Bootstrap, Angular Material.
|
|
71
|
+
|
|
72
|
+
## Agent roles
|
|
73
|
+
|
|
74
|
+
| Role | Purpose |
|
|
75
|
+
|------|---------|
|
|
76
|
+
| **specialist** | Deep knowledge of the detected stack, implements features and fixes |
|
|
77
|
+
| **coordinator** | Orchestrates tasks across the project, manages cross-cutting concerns |
|
|
78
|
+
| **reviewer** | Code review focused on quality, patterns and best practices |
|
|
79
|
+
| **architect** | System design, architecture decisions and technical strategy |
|
|
80
|
+
|
|
81
|
+
## Output
|
|
82
|
+
|
|
83
|
+
Agents are written in dual format:
|
|
84
|
+
|
|
85
|
+
- `.claude/agents/<name>.md` β YAML frontmatter format for Claude Code
|
|
86
|
+
- `.agents/<name>.md` β Plain markdown, compatible with other AI tools
|
|
87
|
+
|
|
88
|
+
## Programmatic usage (SKILL-friendly)
|
|
89
|
+
|
|
90
|
+
Designed to be invoked by AI agents via SKILLs:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Silent detection, JSON output
|
|
94
|
+
agent-factory detect ./repo --json
|
|
95
|
+
|
|
96
|
+
# Silent init, separate output directory
|
|
97
|
+
agent-factory -q init ./repo --output ./workspace -y
|
|
98
|
+
|
|
99
|
+
# List agents as JSON
|
|
100
|
+
agent-factory list ./workspace --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Project structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
agent-factory/
|
|
107
|
+
βββ bin/agent-factory.js # CLI entry point
|
|
108
|
+
βββ src/
|
|
109
|
+
β βββ commands/
|
|
110
|
+
β β βββ detect.js # Stack detection command
|
|
111
|
+
β β βββ create.js # Interactive agent creation
|
|
112
|
+
β β βββ init.js # Auto-suggest and generate agents
|
|
113
|
+
β β βββ list.js # List and validate existing agents
|
|
114
|
+
β βββ core/
|
|
115
|
+
β β βββ stack-detector.js # Technology stack analysis
|
|
116
|
+
β β βββ template-engine.js # Template rendering with Handlebars-like syntax
|
|
117
|
+
β β βββ agent-writer.js # Writes agents to dual formats
|
|
118
|
+
β β βββ agent-validator.js # Validates agent file structure
|
|
119
|
+
β βββ utils/
|
|
120
|
+
β βββ logger.js # Colored output, spinners, quiet mode
|
|
121
|
+
β βββ prompts.js # Inquirer prompt builders
|
|
122
|
+
βββ templates/
|
|
123
|
+
βββ roles/ # Agent role templates
|
|
124
|
+
βββ specialist.md.tmpl
|
|
125
|
+
βββ coordinator.md.tmpl
|
|
126
|
+
βββ reviewer.md.tmpl
|
|
127
|
+
βββ architect.md.tmpl
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Inspiration & acknowledgments
|
|
131
|
+
|
|
132
|
+
This project was built from scratch as an original work. The concept and architecture were inspired in part by [claude-code-templates](https://github.com/davila7/claude-code-templates) by Daniel Γvila, from which the following ideas were studied as reference:
|
|
133
|
+
|
|
134
|
+
- Stack detection patterns
|
|
135
|
+
- YAML frontmatter format for Claude Code agents
|
|
136
|
+
- CLI tooling choices (commander, inquirer, chalk, ora)
|
|
137
|
+
|
|
138
|
+
No source code was copied. All implementation is original.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { banner, setQuiet } = require('../src/utils/logger');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('agent-factory')
|
|
9
|
+
.description('CLI to create AI agents for Claude Code, Codex, Gemini CLI and more')
|
|
10
|
+
.version(pkg.version)
|
|
11
|
+
.option('-q, --quiet', 'Suppress banner and visual output (for programmatic use)')
|
|
12
|
+
.hook('preAction', (thisCommand, actionCommand) => {
|
|
13
|
+
const globalOpts = thisCommand.opts();
|
|
14
|
+
const subOpts = actionCommand.opts();
|
|
15
|
+
if (globalOpts.quiet || subOpts.json) {
|
|
16
|
+
setQuiet(true);
|
|
17
|
+
} else {
|
|
18
|
+
banner(pkg.version);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('detect <path>')
|
|
24
|
+
.description('Detect technology stack of a repository')
|
|
25
|
+
.option('-v, --verbose', 'Show detailed detection info')
|
|
26
|
+
.option('--json', 'Output as JSON (implies --quiet)')
|
|
27
|
+
.action(async (repoPath, options) => {
|
|
28
|
+
const { runDetect } = require('../src/commands/detect');
|
|
29
|
+
await runDetect(repoPath, options);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('create')
|
|
34
|
+
.description('Create an AI agent (interactive or with flags)')
|
|
35
|
+
.option('-n, --name <name>', 'Agent name (kebab-case)')
|
|
36
|
+
.option('-r, --role <role>', 'Agent role: specialist, coordinator, reviewer, architect')
|
|
37
|
+
.option('-m, --model <model>', 'Model: opus, sonnet, haiku', 'sonnet')
|
|
38
|
+
.option('-s, --scope <path>', 'Repository path (for stack detection)')
|
|
39
|
+
.option('-o, --output <path>', 'Output directory (default: current dir)')
|
|
40
|
+
.option('-t, --target <target>', 'Target: claude, codex, all', 'all')
|
|
41
|
+
.option('--tools <tools>', 'Comma-separated tools: Read,Write,Edit,Bash')
|
|
42
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
const { runCreate } = require('../src/commands/create');
|
|
45
|
+
await runCreate(options);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('init <path>')
|
|
50
|
+
.description('Analyze repository and suggest agents')
|
|
51
|
+
.option('-o, --output <path>', 'Output directory (default: same as repo path)')
|
|
52
|
+
.option('-t, --target <target>', 'Target: claude, codex, all', 'all')
|
|
53
|
+
.option('-m, --model <model>', 'Model for generated agents', 'sonnet')
|
|
54
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
55
|
+
.action(async (repoPath, options) => {
|
|
56
|
+
const { runInit } = require('../src/commands/init');
|
|
57
|
+
await runInit(repoPath, options);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('list [path]')
|
|
62
|
+
.description('List existing agents in a directory')
|
|
63
|
+
.option('-v, --verbose', 'Show validation details')
|
|
64
|
+
.option('--json', 'Output as JSON (implies --quiet)')
|
|
65
|
+
.action(async (dirPath, options) => {
|
|
66
|
+
const { runList } = require('../src/commands/list');
|
|
67
|
+
await runList(dirPath || process.cwd(), options);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tacuchi/agent-factory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to create AI agents for Claude Code, Codex, Gemini CLI and more",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agent-factory": "bin/agent-factory.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"templates/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=16"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test tests/",
|
|
20
|
+
"start": "node bin/agent-factory.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude-code",
|
|
24
|
+
"agents",
|
|
25
|
+
"codex",
|
|
26
|
+
"gemini",
|
|
27
|
+
"ai",
|
|
28
|
+
"cli",
|
|
29
|
+
"agent-factory"
|
|
30
|
+
],
|
|
31
|
+
"author": "tacuchi",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/tacuchi/agent-factory"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chalk": "^4.1.2",
|
|
39
|
+
"commander": "^11.1.0",
|
|
40
|
+
"fs-extra": "^11.2.0",
|
|
41
|
+
"inquirer": "^8.2.6",
|
|
42
|
+
"js-yaml": "^4.1.0",
|
|
43
|
+
"ora": "^5.4.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { detect } = require('../core/stack-detector');
|
|
3
|
+
const { renderFile } = require('../core/template-engine');
|
|
4
|
+
const { writeAgent, buildDescription, TOOLS_BY_ROLE } = require('../core/agent-writer');
|
|
5
|
+
const { AgentValidator } = require('../core/agent-validator');
|
|
6
|
+
const { log, spinner } = require('../utils/logger');
|
|
7
|
+
const { askCreateOptions, confirmGeneration } = require('../utils/prompts');
|
|
8
|
+
|
|
9
|
+
async function runCreate(options = {}) {
|
|
10
|
+
const isInteractive = !options.name;
|
|
11
|
+
const config = isInteractive ? await askCreateOptions() : normalizeFlags(options);
|
|
12
|
+
|
|
13
|
+
const { name, role, model, scope, output, target, tools } = config;
|
|
14
|
+
|
|
15
|
+
const spin = spinner('Generating agent...').start();
|
|
16
|
+
|
|
17
|
+
let stackResult = { primaryTech: 'Generic', framework: '', verifyCommands: '', stackParts: [], stackCsv: 'Generic' };
|
|
18
|
+
if (scope) {
|
|
19
|
+
stackResult = await detect(scope);
|
|
20
|
+
}
|
|
21
|
+
|
|
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
|
+
};
|
|
33
|
+
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
spin.succeed('Agent generated');
|
|
44
|
+
|
|
45
|
+
const outputDir = output || process.cwd();
|
|
46
|
+
const targetDirs = [];
|
|
47
|
+
if (target === 'claude' || target === 'all') targetDirs.push('.claude/agents/');
|
|
48
|
+
if (target === 'codex' || target === 'all') targetDirs.push('.agents/');
|
|
49
|
+
|
|
50
|
+
if (!options.yes && isInteractive) {
|
|
51
|
+
const confirmed = await confirmGeneration(name, targetDirs);
|
|
52
|
+
if (!confirmed) {
|
|
53
|
+
log.warn('Cancelled');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolvedTools = tools || TOOLS_BY_ROLE[role] || 'Read, Write, Edit, Bash';
|
|
59
|
+
const results = await writeAgent({
|
|
60
|
+
name,
|
|
61
|
+
role,
|
|
62
|
+
model,
|
|
63
|
+
tools: resolvedTools,
|
|
64
|
+
body,
|
|
65
|
+
outputDir,
|
|
66
|
+
target,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const validator = new AgentValidator();
|
|
70
|
+
if (results.claude) {
|
|
71
|
+
const fs = require('fs-extra');
|
|
72
|
+
const content = await fs.readFile(results.claude, 'utf8');
|
|
73
|
+
const validation = validator.validate(content, `${name}.md`);
|
|
74
|
+
if (validation.valid) {
|
|
75
|
+
log.success(`Claude: ${results.claude} (score: ${validation.score}/100)`);
|
|
76
|
+
} else {
|
|
77
|
+
log.warn(`Claude: ${results.claude} (score: ${validation.score}/100, ${validation.errorCount} errors)`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (results.codex) {
|
|
82
|
+
log.success(`Codex: ${results.codex}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeFlags(options) {
|
|
87
|
+
return {
|
|
88
|
+
name: options.name,
|
|
89
|
+
role: options.role || 'specialist',
|
|
90
|
+
model: options.model || 'sonnet',
|
|
91
|
+
scope: options.scope ? path.resolve(options.scope) : '',
|
|
92
|
+
output: options.output ? path.resolve(options.output) : process.cwd(),
|
|
93
|
+
target: options.target || 'all',
|
|
94
|
+
tools: options.tools || '',
|
|
95
|
+
yes: options.yes || false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { runCreate };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { detect, deriveAlias } = require('../core/stack-detector');
|
|
4
|
+
const { log, spinner, chalk } = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
async function runDetect(repoPath, options = {}) {
|
|
7
|
+
const abs = path.resolve(repoPath);
|
|
8
|
+
|
|
9
|
+
if (!(await fs.pathExists(abs))) {
|
|
10
|
+
if (options.json) {
|
|
11
|
+
console.log(JSON.stringify({ error: `Path does not exist: ${abs}` }));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
log.error(`Path does not exist: ${abs}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const spin = spinner('Detecting stack...').start();
|
|
19
|
+
const result = await detect(abs);
|
|
20
|
+
spin.succeed('Detection complete');
|
|
21
|
+
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ alias: deriveAlias(abs), ...result }));
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('');
|
|
28
|
+
log.label(' Path', abs);
|
|
29
|
+
log.label(' Alias', deriveAlias(abs));
|
|
30
|
+
log.label(' Primary', result.primaryTech);
|
|
31
|
+
|
|
32
|
+
if (result.framework) {
|
|
33
|
+
log.label(' Framework', result.framework);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.stackParts.length > 0) {
|
|
37
|
+
log.label(' Stack', result.stackCsv);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (result.verifyCommands) {
|
|
41
|
+
log.label(' Verify', result.verifyCommands);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.verbose && result.stackParts.length > 0) {
|
|
45
|
+
console.log('');
|
|
46
|
+
log.muted(' Stack parts:');
|
|
47
|
+
result.stackParts.forEach((part) => {
|
|
48
|
+
console.log(chalk.gray(' β’'), part);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { runDetect };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { detect, deriveAlias } = require('../core/stack-detector');
|
|
4
|
+
const { renderFile } = require('../core/template-engine');
|
|
5
|
+
const { writeAgent, TOOLS_BY_ROLE } = require('../core/agent-writer');
|
|
6
|
+
const { AgentValidator } = require('../core/agent-validator');
|
|
7
|
+
const { log, spinner, chalk } = require('../utils/logger');
|
|
8
|
+
const { askInitAgents } = require('../utils/prompts');
|
|
9
|
+
|
|
10
|
+
async function runInit(repoPath, options = {}) {
|
|
11
|
+
const abs = path.resolve(repoPath);
|
|
12
|
+
|
|
13
|
+
if (!(await fs.pathExists(abs))) {
|
|
14
|
+
log.error(`Path does not exist: ${abs}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const spin = spinner('Analyzing repository...').start();
|
|
19
|
+
const stackResult = await detect(abs);
|
|
20
|
+
const alias = deriveAlias(abs);
|
|
21
|
+
spin.succeed(`Detected: ${stackResult.primaryTech}${stackResult.framework ? ` / ${stackResult.framework}` : ''}`);
|
|
22
|
+
|
|
23
|
+
const suggestions = buildSuggestions(alias, stackResult, abs);
|
|
24
|
+
|
|
25
|
+
log.info(`Suggested agents for ${chalk.white(alias)}:`);
|
|
26
|
+
suggestions.forEach((s) => {
|
|
27
|
+
log.muted(` β’ ${s.name} (${s.role}) β ${s.description}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let selected = suggestions;
|
|
31
|
+
if (!options.yes) {
|
|
32
|
+
selected = await askInitAgents(suggestions);
|
|
33
|
+
if (selected.length === 0) {
|
|
34
|
+
log.warn('No agents selected');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const target = options.target || 'all';
|
|
40
|
+
const model = options.model || 'sonnet';
|
|
41
|
+
const outputDir = options.output ? path.resolve(options.output) : abs;
|
|
42
|
+
const validator = new AgentValidator();
|
|
43
|
+
|
|
44
|
+
for (const agent of selected) {
|
|
45
|
+
const templateData = {
|
|
46
|
+
name: agent.name,
|
|
47
|
+
primary_tech: stackResult.primaryTech,
|
|
48
|
+
framework: stackResult.framework,
|
|
49
|
+
scope: abs,
|
|
50
|
+
stack_list: stackResult.stackParts.length > 0
|
|
51
|
+
? stackResult.stackParts.map((p) => `- ${p}`).join('\n')
|
|
52
|
+
: `- ${stackResult.primaryTech}`,
|
|
53
|
+
verify_cmds: stackResult.verifyCommands || 'N/A',
|
|
54
|
+
stack_csv: stackResult.stackCsv,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = await renderFile(`${agent.role}.md.tmpl`, templateData);
|
|
60
|
+
} catch {
|
|
61
|
+
log.warn(`Template not found for role: ${agent.role}, skipping`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results = await writeAgent({
|
|
66
|
+
name: agent.name,
|
|
67
|
+
role: agent.role,
|
|
68
|
+
model,
|
|
69
|
+
tools: TOOLS_BY_ROLE[agent.role],
|
|
70
|
+
body,
|
|
71
|
+
outputDir,
|
|
72
|
+
target,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (results.claude) {
|
|
76
|
+
const content = await fs.readFile(results.claude, 'utf8');
|
|
77
|
+
const validation = validator.validate(content, `${agent.name}.md`);
|
|
78
|
+
log.success(`${agent.name} β .claude/agents/ (score: ${validation.score}/100)`);
|
|
79
|
+
}
|
|
80
|
+
if (results.codex) {
|
|
81
|
+
log.success(`${agent.name} β .agents/`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
log.success(`${selected.length} agent(s) generated in ${outputDir}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildSuggestions(alias, stackResult, repoPath) {
|
|
89
|
+
const suggestions = [];
|
|
90
|
+
const tech = stackResult.framework || stackResult.primaryTech;
|
|
91
|
+
|
|
92
|
+
suggestions.push({
|
|
93
|
+
name: `repo-${alias}`,
|
|
94
|
+
role: 'specialist',
|
|
95
|
+
description: `${tech} specialist for ${alias}`,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
suggestions.push({
|
|
99
|
+
name: `${alias}-reviewer`,
|
|
100
|
+
role: 'reviewer',
|
|
101
|
+
description: `Code reviewer for ${tech}`,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (stackResult.stackParts.length >= 3) {
|
|
105
|
+
suggestions.push({
|
|
106
|
+
name: `${alias}-architect`,
|
|
107
|
+
role: 'architect',
|
|
108
|
+
description: `Architecture advisor for ${tech}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return suggestions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { runInit };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { AgentValidator } = require('../core/agent-validator');
|
|
5
|
+
const { log, chalk } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
async function runList(dirPath, options = {}) {
|
|
8
|
+
const abs = path.resolve(dirPath);
|
|
9
|
+
const agents = [];
|
|
10
|
+
|
|
11
|
+
const claudeDir = path.join(abs, '.claude', 'agents');
|
|
12
|
+
const codexDir = path.join(abs, '.agents');
|
|
13
|
+
|
|
14
|
+
if (await fs.pathExists(claudeDir)) {
|
|
15
|
+
const files = (await fs.readdir(claudeDir)).filter((f) => f.endsWith('.md'));
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const content = await fs.readFile(path.join(claudeDir, file), 'utf8');
|
|
18
|
+
agents.push({ file, source: '.claude/agents', content, format: 'claude' });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (await fs.pathExists(codexDir)) {
|
|
23
|
+
const files = (await fs.readdir(codexDir)).filter((f) => f.endsWith('.md'));
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const existing = agents.find((a) => a.file === file);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.codexToo = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const content = await fs.readFile(path.join(codexDir, file), 'utf8');
|
|
31
|
+
agents.push({ file, source: '.agents', content, format: 'codex' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (agents.length === 0) {
|
|
36
|
+
if (options.json) {
|
|
37
|
+
console.log(JSON.stringify([]));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
log.warn(`No agents found in ${abs}`);
|
|
41
|
+
log.muted(' Run "agent-factory init <path>" or "agent-factory create" to generate agents.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const validator = new AgentValidator();
|
|
46
|
+
|
|
47
|
+
if (options.json) {
|
|
48
|
+
const jsonResult = agents.map((agent) => {
|
|
49
|
+
const fm = extractFrontmatter(agent.content);
|
|
50
|
+
const validation = validator.validate(agent.content, agent.file);
|
|
51
|
+
return {
|
|
52
|
+
name: fm?.name || agent.file.replace('.md', ''),
|
|
53
|
+
model: fm?.model || null,
|
|
54
|
+
source: agent.codexToo ? 'both' : agent.source,
|
|
55
|
+
score: validation.score,
|
|
56
|
+
valid: validation.valid,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
console.log(JSON.stringify(jsonResult));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('');
|
|
64
|
+
log.info(`${agents.length} agent(s) found in ${chalk.white(abs)}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
const header = padRow('Name', 'Model', 'Source', 'Score');
|
|
68
|
+
console.log(chalk.gray(header));
|
|
69
|
+
console.log(chalk.gray('β'.repeat(header.length)));
|
|
70
|
+
|
|
71
|
+
for (const agent of agents) {
|
|
72
|
+
const fm = extractFrontmatter(agent.content);
|
|
73
|
+
const name = fm?.name || agent.file.replace('.md', '');
|
|
74
|
+
const model = fm?.model || 'β';
|
|
75
|
+
const source = agent.codexToo ? 'both' : agent.source;
|
|
76
|
+
|
|
77
|
+
const validation = validator.validate(agent.content, agent.file);
|
|
78
|
+
const scoreStr = `${validation.score}/100`;
|
|
79
|
+
const scoreColor = validation.score >= 90 ? '#10B981' : validation.score >= 70 ? '#F59E0B' : '#EF4444';
|
|
80
|
+
|
|
81
|
+
console.log(
|
|
82
|
+
padRow(
|
|
83
|
+
chalk.white(name),
|
|
84
|
+
chalk.gray(model),
|
|
85
|
+
chalk.gray(source),
|
|
86
|
+
chalk.hex(scoreColor)(scoreStr)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (options.verbose) {
|
|
91
|
+
if (validation.errorCount > 0) {
|
|
92
|
+
validation.errors.forEach((e) => console.log(chalk.red(` β ${e.message}`)));
|
|
93
|
+
}
|
|
94
|
+
if (validation.warningCount > 0) {
|
|
95
|
+
validation.warnings.forEach((w) => console.log(chalk.yellow(` β ${w.message}`)));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function padRow(name, model, source, score) {
|
|
104
|
+
return ` ${name.padEnd(30)} ${model.padEnd(10)} ${source.padEnd(18)} ${score}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractFrontmatter(content) {
|
|
108
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
try {
|
|
111
|
+
return yaml.load(match[1]);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { runList };
|