@polymorphism-tech/morph-spec 1.0.4 → 2.0.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/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +89 -6
- package/bin/detect-agents.js +225 -0
- package/bin/morph-spec.js +120 -0
- package/bin/render-template.js +302 -0
- package/bin/semantic-detect-agents.js +246 -0
- package/bin/validate-agents-skills.js +239 -0
- package/bin/validate-agents.js +69 -0
- package/bin/validate-phase.js +263 -0
- package/content/.azure/README.md +293 -0
- package/content/.azure/docs/azure-devops-setup.md +454 -0
- package/content/.azure/docs/branch-strategy.md +398 -0
- package/content/.azure/docs/local-development.md +515 -0
- package/content/.azure/pipelines/pipeline-variables.yml +34 -0
- package/content/.azure/pipelines/prod-pipeline.yml +319 -0
- package/content/.azure/pipelines/staging-pipeline.yml +234 -0
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
- package/content/.claude/commands/morph-apply.md +118 -26
- package/content/.claude/commands/morph-archive.md +9 -9
- package/content/.claude/commands/morph-clarify.md +184 -0
- package/content/.claude/commands/morph-design.md +275 -0
- package/content/.claude/commands/morph-proposal.md +56 -15
- package/content/.claude/commands/morph-setup.md +100 -0
- package/content/.claude/commands/morph-status.md +47 -32
- package/content/.claude/commands/morph-tasks.md +319 -0
- package/content/.claude/commands/morph-uiux.md +211 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
- package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
- package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
- package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
- package/content/.morph/.morphversion +5 -0
- package/content/.morph/config/agents.json +101 -8
- package/content/.morph/config/azure-pricing.json +70 -0
- package/content/.morph/config/azure-pricing.schema.json +50 -0
- package/content/.morph/config/config.template.json +15 -3
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
- package/content/.morph/hooks/README.md +239 -0
- package/content/.morph/hooks/pre-commit-agents.sh +24 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -0
- package/content/.morph/hooks/pre-commit-costs.sh +91 -0
- package/content/.morph/hooks/pre-commit-specs.sh +49 -0
- package/content/.morph/hooks/pre-commit-tests.sh +60 -0
- package/content/.morph/project.md +5 -4
- package/content/.morph/schemas/agent.schema.json +296 -0
- package/content/.morph/standards/agent-framework-setup.md +453 -0
- package/content/.morph/standards/architecture.md +142 -7
- package/content/.morph/standards/azure.md +218 -23
- package/content/.morph/standards/coding.md +47 -12
- package/content/.morph/standards/dotnet10-migration.md +494 -0
- package/content/.morph/standards/fluent-ui-setup.md +590 -0
- package/content/.morph/standards/migration-guide.md +514 -0
- package/content/.morph/standards/passkeys-auth.md +423 -0
- package/content/.morph/standards/vector-search-rag.md +536 -0
- package/content/.morph/state.json +18 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -0
- package/content/.morph/templates/MudTheme.cs +281 -0
- package/content/.morph/templates/contracts.cs +55 -55
- package/content/.morph/templates/decisions.md +4 -4
- package/content/.morph/templates/design-system.css +226 -0
- package/content/.morph/templates/infra/.dockerignore.example +89 -0
- package/content/.morph/templates/infra/Dockerfile.example +82 -0
- package/content/.morph/templates/infra/README.md +286 -0
- package/content/.morph/templates/infra/app-service.bicep +164 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -0
- package/content/.morph/templates/infra/deploy.sh +208 -0
- package/content/.morph/templates/infra/main.bicep +41 -7
- package/content/.morph/templates/infra/parameters.dev.json +6 -0
- package/content/.morph/templates/infra/parameters.prod.json +6 -0
- package/content/.morph/templates/infra/parameters.staging.json +29 -0
- package/content/.morph/templates/proposal.md +3 -3
- package/content/.morph/templates/recap.md +3 -3
- package/content/.morph/templates/spec.md +9 -8
- package/content/.morph/templates/sprint-status.yaml +68 -0
- package/content/.morph/templates/state.template.json +222 -0
- package/content/.morph/templates/story.md +143 -0
- package/content/.morph/templates/tasks.md +1 -1
- package/content/.morph/templates/ui-components.md +276 -0
- package/content/.morph/templates/ui-design-system.md +286 -0
- package/content/.morph/templates/ui-flows.md +336 -0
- package/content/.morph/templates/ui-mockups.md +133 -0
- package/content/.morph/test-infra/example.bicep +59 -0
- package/content/CLAUDE.md +124 -0
- package/content/README.md +79 -0
- package/detectors/config-detector.js +223 -0
- package/detectors/conversation-analyzer.js +163 -0
- package/detectors/index.js +84 -0
- package/detectors/standards-generator.js +275 -0
- package/detectors/structure-detector.js +221 -0
- package/docs/README.md +149 -0
- package/docs/api/cost-calculator.js.html +513 -0
- package/docs/api/design-system-generator.js.html +382 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/api/global.html +5263 -0
- package/docs/api/index.html +96 -0
- package/docs/api/scripts/collapse.js +39 -0
- package/docs/api/scripts/commonNav.js +28 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/nav.js +12 -0
- package/docs/api/scripts/polyfill.js +4 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/scripts/search.js +99 -0
- package/docs/api/state-manager.js.html +423 -0
- package/docs/api/styles/jsdoc.css +776 -0
- package/docs/api/styles/prettify.css +80 -0
- package/docs/examples.md +328 -0
- package/docs/getting-started.md +302 -0
- package/docs/installation.md +361 -0
- package/docs/templates.md +418 -0
- package/docs/validation-checklist.md +266 -0
- package/package.json +39 -12
- package/src/commands/cost.js +181 -0
- package/src/commands/create-story.js +283 -0
- package/src/commands/detect.js +104 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/generate.js +149 -0
- package/src/commands/init.js +69 -45
- package/src/commands/shard-spec.js +224 -0
- package/src/commands/sprint-status.js +250 -0
- package/src/commands/state.js +333 -0
- package/src/commands/sync.js +167 -0
- package/src/commands/update-pricing.js +206 -0
- package/src/commands/update.js +88 -13
- package/src/lib/complexity-analyzer.js +292 -0
- package/src/lib/cost-calculator.js +429 -0
- package/src/lib/design-system-generator.js +298 -0
- package/src/lib/state-manager.js +340 -0
- package/src/utils/file-copier.js +59 -0
- package/src/utils/version-checker.js +175 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MORPH-SPEC Template Renderer
|
|
5
|
+
*
|
|
6
|
+
* Renders templates by replacing {{PLACEHOLDER}} with actual values.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node bin/render-template.js \
|
|
13
|
+
* content/.morph/templates/spec.md \
|
|
14
|
+
* .morph/project/outputs/my-feature/spec.md \
|
|
15
|
+
* '{"FEATURE_NAME":"my-feature","STACK":"Blazor","DATE":"2024-01-15"}'
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// ANSI color codes
|
|
26
|
+
const colors = {
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
yellow: '\x1b[33m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
reset: '\x1b[0m'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load config to get default values
|
|
36
|
+
*/
|
|
37
|
+
function loadConfig() {
|
|
38
|
+
const configPaths = [
|
|
39
|
+
path.join(process.cwd(), '.morph/config/config.json'),
|
|
40
|
+
path.join(process.cwd(), 'content/.morph/config/config.json'),
|
|
41
|
+
path.join(__dirname, '../content/.morph/config/config.template.json')
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const configPath of configPaths) {
|
|
45
|
+
if (fs.existsSync(configPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Continue to next path
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get default variables from config and environment
|
|
60
|
+
*/
|
|
61
|
+
function getDefaultVariables() {
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
const now = new Date();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
DATE: now.toISOString().split('T')[0],
|
|
67
|
+
YEAR: now.getFullYear().toString(),
|
|
68
|
+
AUTHOR: config?.project?.author || 'MORPH-SPEC',
|
|
69
|
+
PROJECT_NAME: config?.project?.name || 'Project',
|
|
70
|
+
STACK: config?.project?.stack || 'Blazor',
|
|
71
|
+
NAMESPACE: config?.project?.namespace || 'Project'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert string to various case formats
|
|
77
|
+
*/
|
|
78
|
+
function transformCase(str) {
|
|
79
|
+
return {
|
|
80
|
+
// kebab-case → PascalCase
|
|
81
|
+
pascal: str
|
|
82
|
+
.split('-')
|
|
83
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
84
|
+
.join(''),
|
|
85
|
+
|
|
86
|
+
// kebab-case → camelCase
|
|
87
|
+
camel: str
|
|
88
|
+
.split('-')
|
|
89
|
+
.map((word, index) =>
|
|
90
|
+
index === 0
|
|
91
|
+
? word.toLowerCase()
|
|
92
|
+
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
93
|
+
)
|
|
94
|
+
.join(''),
|
|
95
|
+
|
|
96
|
+
// kebab-case → Title Case
|
|
97
|
+
title: str
|
|
98
|
+
.split('-')
|
|
99
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
100
|
+
.join(' '),
|
|
101
|
+
|
|
102
|
+
// kebab-case → UPPER_SNAKE_CASE
|
|
103
|
+
upperSnake: str.toUpperCase().replace(/-/g, '_'),
|
|
104
|
+
|
|
105
|
+
// kebab-case → lower_snake_case
|
|
106
|
+
lowerSnake: str.toLowerCase().replace(/-/g, '_')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render template by replacing placeholders
|
|
112
|
+
*/
|
|
113
|
+
function renderTemplate(templatePath, variables) {
|
|
114
|
+
if (!fs.existsSync(templatePath)) {
|
|
115
|
+
throw new Error(`Template file not found: ${templatePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
119
|
+
|
|
120
|
+
// Get default variables
|
|
121
|
+
const defaults = getDefaultVariables();
|
|
122
|
+
|
|
123
|
+
// Merge with provided variables (provided takes precedence)
|
|
124
|
+
const allVariables = { ...defaults, ...variables };
|
|
125
|
+
|
|
126
|
+
// Auto-generate case transformations for FEATURE_NAME if provided
|
|
127
|
+
if (allVariables.FEATURE_NAME) {
|
|
128
|
+
const featureName = allVariables.FEATURE_NAME;
|
|
129
|
+
const transformations = transformCase(featureName);
|
|
130
|
+
|
|
131
|
+
allVariables.FEATURE_NAME_PASCAL = transformations.pascal;
|
|
132
|
+
allVariables.FEATURE_NAME_CAMEL = transformations.camel;
|
|
133
|
+
allVariables.FEATURE_NAME_TITLE = transformations.title;
|
|
134
|
+
allVariables.FEATURE_NAME_UPPER_SNAKE = transformations.upperSnake;
|
|
135
|
+
allVariables.FEATURE_NAME_LOWER_SNAKE = transformations.lowerSnake;
|
|
136
|
+
|
|
137
|
+
// Backward compatibility: FEATURE_TITLE maps to FEATURE_NAME_TITLE
|
|
138
|
+
if (!allVariables.FEATURE_TITLE) {
|
|
139
|
+
allVariables.FEATURE_TITLE = transformations.title;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Track which placeholders were replaced
|
|
144
|
+
const replacedPlaceholders = new Set();
|
|
145
|
+
const unreplacedPlaceholders = new Set();
|
|
146
|
+
|
|
147
|
+
// Replace all placeholders
|
|
148
|
+
for (const [key, value] of Object.entries(allVariables)) {
|
|
149
|
+
const placeholder = `{{${key}}}`;
|
|
150
|
+
if (content.includes(placeholder)) {
|
|
151
|
+
content = content.replaceAll(placeholder, value);
|
|
152
|
+
replacedPlaceholders.add(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find unreplaced placeholders
|
|
157
|
+
const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = placeholderRegex.exec(content)) !== null) {
|
|
160
|
+
unreplacedPlaceholders.add(match[1]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content,
|
|
165
|
+
replacedPlaceholders: Array.from(replacedPlaceholders),
|
|
166
|
+
unreplacedPlaceholders: Array.from(unreplacedPlaceholders)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Print help message
|
|
172
|
+
*/
|
|
173
|
+
function printHelp() {
|
|
174
|
+
console.log(`
|
|
175
|
+
${colors.cyan}MORPH-SPEC Template Renderer${colors.reset}
|
|
176
|
+
|
|
177
|
+
${colors.green}Usage:${colors.reset}
|
|
178
|
+
node bin/render-template.js <template-path> <output-path> <variables-json>
|
|
179
|
+
|
|
180
|
+
${colors.green}Arguments:${colors.reset}
|
|
181
|
+
template-path Path to the template file (required)
|
|
182
|
+
output-path Path where rendered output will be written (required)
|
|
183
|
+
variables-json JSON object with variables to replace (required)
|
|
184
|
+
|
|
185
|
+
${colors.green}Standard Placeholders:${colors.reset}
|
|
186
|
+
{{FEATURE_NAME}} - kebab-case feature name (e.g., "scheduled-reports")
|
|
187
|
+
{{FEATURE_NAME_PASCAL}} - PascalCase (e.g., "ScheduledReports")
|
|
188
|
+
{{FEATURE_NAME_CAMEL}} - camelCase (e.g., "scheduledReports")
|
|
189
|
+
{{FEATURE_NAME_TITLE}} - Title Case (e.g., "Scheduled Reports")
|
|
190
|
+
{{FEATURE_NAME_UPPER_SNAKE}} - UPPER_SNAKE_CASE (e.g., "SCHEDULED_REPORTS")
|
|
191
|
+
{{FEATURE_NAME_LOWER_SNAKE}} - lower_snake_case (e.g., "scheduled_reports")
|
|
192
|
+
{{FEATURE_TITLE}} - Alias for FEATURE_NAME_TITLE
|
|
193
|
+
{{STACK}} - Project stack (e.g., "Blazor", "Next.js")
|
|
194
|
+
{{DATE}} - Current date (YYYY-MM-DD)
|
|
195
|
+
{{YEAR}} - Current year
|
|
196
|
+
{{AUTHOR}} - Author name (from config.json)
|
|
197
|
+
{{PROJECT_NAME}} - Project name (from config.json)
|
|
198
|
+
{{NAMESPACE}} - C# namespace (from config.json)
|
|
199
|
+
|
|
200
|
+
${colors.green}Example:${colors.reset}
|
|
201
|
+
node bin/render-template.js \\
|
|
202
|
+
content/.morph/templates/spec.md \\
|
|
203
|
+
.morph/project/outputs/scheduled-reports/spec.md \\
|
|
204
|
+
'{"FEATURE_NAME":"scheduled-reports","STACK":"Blazor"}'
|
|
205
|
+
|
|
206
|
+
${colors.green}Flags:${colors.reset}
|
|
207
|
+
--help, -h Show this help message
|
|
208
|
+
--verbose, -v Show detailed replacement information
|
|
209
|
+
--dry-run, -d Preview output without writing file
|
|
210
|
+
|
|
211
|
+
${colors.yellow}Note:${colors.reset} DATE, YEAR, AUTHOR, PROJECT_NAME, STACK, NAMESPACE are auto-populated from config.json
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Main CLI logic
|
|
217
|
+
*/
|
|
218
|
+
function main() {
|
|
219
|
+
const args = process.argv.slice(2);
|
|
220
|
+
|
|
221
|
+
// Check for help flag
|
|
222
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
223
|
+
printHelp();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Parse flags
|
|
228
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
229
|
+
const dryRun = args.includes('--dry-run') || args.includes('-d');
|
|
230
|
+
|
|
231
|
+
// Filter out flags
|
|
232
|
+
const flags = ['--verbose', '-v', '--dry-run', '-d', '--help', '-h'];
|
|
233
|
+
const cleanArgs = args.filter(arg => !flags.includes(arg));
|
|
234
|
+
|
|
235
|
+
// Validate arguments
|
|
236
|
+
if (cleanArgs.length < 3) {
|
|
237
|
+
console.error(`${colors.red}❌ Error: Missing required arguments${colors.reset}\n`);
|
|
238
|
+
printHelp();
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const [templatePath, outputPath, variablesJson] = cleanArgs;
|
|
243
|
+
|
|
244
|
+
// Parse variables JSON
|
|
245
|
+
let variables;
|
|
246
|
+
try {
|
|
247
|
+
variables = JSON.parse(variablesJson);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(`${colors.red}❌ Error: Invalid JSON in variables argument${colors.reset}`);
|
|
250
|
+
console.error(` ${error.message}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Render template
|
|
256
|
+
const { content, replacedPlaceholders, unreplacedPlaceholders } = renderTemplate(
|
|
257
|
+
templatePath,
|
|
258
|
+
variables
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Dry run: just preview
|
|
262
|
+
if (dryRun) {
|
|
263
|
+
console.log(`${colors.cyan}📋 DRY RUN - Preview (file will NOT be written)${colors.reset}\n`);
|
|
264
|
+
console.log(content);
|
|
265
|
+
console.log(`\n${colors.cyan}═══════════════════════════════════════════${colors.reset}`);
|
|
266
|
+
} else {
|
|
267
|
+
// Create output directory if it doesn't exist
|
|
268
|
+
const outputDir = path.dirname(outputPath);
|
|
269
|
+
if (!fs.existsSync(outputDir)) {
|
|
270
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Write rendered content
|
|
274
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
275
|
+
console.log(`${colors.green}✅ Template rendered successfully${colors.reset}`);
|
|
276
|
+
console.log(` Output: ${outputPath}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Verbose output
|
|
280
|
+
if (verbose || unreplacedPlaceholders.length > 0) {
|
|
281
|
+
console.log(`\n${colors.cyan}📊 Replacement Summary:${colors.reset}`);
|
|
282
|
+
console.log(` ${colors.green}Replaced:${colors.reset} ${replacedPlaceholders.length} placeholders`);
|
|
283
|
+
|
|
284
|
+
if (replacedPlaceholders.length > 0) {
|
|
285
|
+
console.log(` ${replacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (unreplacedPlaceholders.length > 0) {
|
|
289
|
+
console.log(`\n ${colors.yellow}⚠️ Unreplaced:${colors.reset} ${unreplacedPlaceholders.length} placeholders`);
|
|
290
|
+
console.log(` ${unreplacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
process.exit(0);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Run CLI
|
|
302
|
+
main();
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MORPH-SPEC Semantic Agent Detection (LLM-based)
|
|
5
|
+
*
|
|
6
|
+
* Uses LLM analysis to detect which agents should be activated based on user request.
|
|
7
|
+
* More accurate than keyword matching - understands context and implicit needs.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* export ANTHROPIC_API_KEY=sk-...
|
|
11
|
+
* node bin/semantic-detect-agents.js "user request here"
|
|
12
|
+
*
|
|
13
|
+
* Falls back to keyword-based detection if API key not available.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { execSync } from 'child_process';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// ANSI colors
|
|
25
|
+
const colors = {
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
red: '\x1b[31m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
gray: '\x1b[90m',
|
|
31
|
+
reset: '\x1b[0m'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Call Anthropic Claude API for semantic analysis
|
|
36
|
+
*/
|
|
37
|
+
async function callClaudeAPI(userRequest, agentDescriptions) {
|
|
38
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
39
|
+
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
return null; // Will fall back to keyword detection
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prompt = `You are an expert at analyzing software development requests and determining which specialist agents are needed.
|
|
45
|
+
|
|
46
|
+
Given this user request:
|
|
47
|
+
"${userRequest}"
|
|
48
|
+
|
|
49
|
+
And these available specialist agents:
|
|
50
|
+
${agentDescriptions}
|
|
51
|
+
|
|
52
|
+
Analyze the request and determine which specialist agents should be activated. Consider:
|
|
53
|
+
- Explicit mentions (e.g., "create a dashboard" → UI/UX Designer)
|
|
54
|
+
- Implicit needs (e.g., "show data to users" → UI/UX Designer)
|
|
55
|
+
- Technical requirements (e.g., "scheduled tasks" → Hangfire Orchestrator)
|
|
56
|
+
- Architecture patterns (e.g., "RAG pipeline" → AI System Architect)
|
|
57
|
+
|
|
58
|
+
Respond with ONLY a JSON array of agent IDs, like:
|
|
59
|
+
["uiux-designer", "hangfire-orchestrator"]
|
|
60
|
+
|
|
61
|
+
If no specialists are needed, respond with: []
|
|
62
|
+
Do not include core agents (they are always active).`;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'x-api-key': apiKey,
|
|
70
|
+
'anthropic-version': '2023-06-01'
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: 'claude-3-haiku-20240307',
|
|
74
|
+
max_tokens: 200,
|
|
75
|
+
messages: [{
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: prompt
|
|
78
|
+
}]
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const error = await response.text();
|
|
84
|
+
console.error(`${colors.yellow}⚠ Claude API error: ${error}${colors.reset}`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
const content = data.content[0].text.trim();
|
|
90
|
+
|
|
91
|
+
// Parse JSON response
|
|
92
|
+
const agentIds = JSON.parse(content);
|
|
93
|
+
return agentIds;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`${colors.yellow}⚠ Claude API failed: ${error.message}${colors.reset}`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fallback: keyword-based detection (existing behavior)
|
|
102
|
+
*/
|
|
103
|
+
function keywordBasedDetection(userRequest) {
|
|
104
|
+
console.log(`${colors.yellow}ℹ Using keyword-based detection (fallback)${colors.reset}`);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = execSync(`node bin/detect-agents.js "${userRequest}" --json`, {
|
|
108
|
+
encoding: 'utf-8',
|
|
109
|
+
cwd: process.cwd()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const parsed = JSON.parse(result);
|
|
113
|
+
return parsed.specialists || [];
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`${colors.red}✗ Fallback detection failed: ${error.message}${colors.reset}`);
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load agent descriptions for LLM context
|
|
122
|
+
*/
|
|
123
|
+
function loadAgentDescriptions() {
|
|
124
|
+
const agentsPath = path.join(process.cwd(), 'content/.morph/config/agents.json');
|
|
125
|
+
|
|
126
|
+
if (!fs.existsSync(agentsPath)) {
|
|
127
|
+
throw new Error('agents.json not found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const agents = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
131
|
+
const specialists = agents.agents.specialists || [];
|
|
132
|
+
|
|
133
|
+
return specialists.map(agent =>
|
|
134
|
+
`- ${agent.id}: ${agent.description}`
|
|
135
|
+
).join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Print detection results
|
|
140
|
+
*/
|
|
141
|
+
function printResults(agentIds, method, userRequest) {
|
|
142
|
+
console.log(`\n${colors.cyan}╔════════════════════════════════════════════════╗${colors.reset}`);
|
|
143
|
+
console.log(`${colors.cyan}║ MORPH-SPEC SEMANTIC AGENT DETECTION ║${colors.reset}`);
|
|
144
|
+
console.log(`${colors.cyan}╚════════════════════════════════════════════════╝${colors.reset}\n`);
|
|
145
|
+
|
|
146
|
+
console.log(`${colors.gray}Request:${colors.reset} "${userRequest}"`);
|
|
147
|
+
console.log(`${colors.gray}Method:${colors.reset} ${method}`);
|
|
148
|
+
console.log(`${colors.gray}Agents Detected:${colors.reset} ${agentIds.length}`);
|
|
149
|
+
|
|
150
|
+
if (agentIds.length > 0) {
|
|
151
|
+
console.log('');
|
|
152
|
+
agentIds.forEach(id => {
|
|
153
|
+
console.log(` ${colors.green}✓${colors.reset} ${id}`);
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`\n ${colors.gray}(No specialist agents needed)${colors.reset}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Print help
|
|
164
|
+
*/
|
|
165
|
+
function printHelp() {
|
|
166
|
+
console.log(`
|
|
167
|
+
${colors.cyan}MORPH-SPEC Semantic Agent Detection${colors.reset}
|
|
168
|
+
|
|
169
|
+
Uses LLM analysis to detect which agents should be activated.
|
|
170
|
+
More accurate than keyword matching - understands context.
|
|
171
|
+
|
|
172
|
+
${colors.green}Setup:${colors.reset}
|
|
173
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
174
|
+
|
|
175
|
+
${colors.green}Usage:${colors.reset}
|
|
176
|
+
node bin/semantic-detect-agents.js "user request here"
|
|
177
|
+
|
|
178
|
+
${colors.green}Examples:${colors.reset}
|
|
179
|
+
# Detects UI/UX Designer (implicit need)
|
|
180
|
+
node bin/semantic-detect-agents.js "sistema que mostra dados ao usuário"
|
|
181
|
+
|
|
182
|
+
# Detects Hangfire Orchestrator
|
|
183
|
+
node bin/semantic-detect-agents.js "processar arquivos todo dia às 3h"
|
|
184
|
+
|
|
185
|
+
# Detects AI System Architect
|
|
186
|
+
node bin/semantic-detect-agents.js "pipeline de RAG com chunking"
|
|
187
|
+
|
|
188
|
+
${colors.green}Fallback:${colors.reset}
|
|
189
|
+
If ANTHROPIC_API_KEY is not set, falls back to keyword-based detection.
|
|
190
|
+
|
|
191
|
+
${colors.green}Benefits:${colors.reset}
|
|
192
|
+
✓ Understands context ("show data to users" → UI)
|
|
193
|
+
✓ Works in any language (not just keywords)
|
|
194
|
+
✓ Eliminates false positives/negatives
|
|
195
|
+
✓ Detects implicit needs
|
|
196
|
+
|
|
197
|
+
${colors.green}Output:${colors.reset}
|
|
198
|
+
Prints detected agent IDs, one per line, for use in scripts.
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Main function
|
|
204
|
+
*/
|
|
205
|
+
async function main() {
|
|
206
|
+
const args = process.argv.slice(2);
|
|
207
|
+
|
|
208
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
209
|
+
printHelp();
|
|
210
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const userRequest = args.join(' ');
|
|
214
|
+
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
215
|
+
|
|
216
|
+
let agentIds;
|
|
217
|
+
let method;
|
|
218
|
+
|
|
219
|
+
if (hasApiKey) {
|
|
220
|
+
method = `${colors.green}LLM-based (Claude Haiku)${colors.reset}`;
|
|
221
|
+
const agentDescriptions = loadAgentDescriptions();
|
|
222
|
+
agentIds = await callClaudeAPI(userRequest, agentDescriptions);
|
|
223
|
+
|
|
224
|
+
if (!agentIds) {
|
|
225
|
+
// API failed, fall back
|
|
226
|
+
method = `${colors.yellow}Keyword-based (API fallback)${colors.reset}`;
|
|
227
|
+
agentIds = keywordBasedDetection(userRequest);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
method = `${colors.yellow}Keyword-based (no API key)${colors.reset}`;
|
|
231
|
+
agentIds = keywordBasedDetection(userRequest);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
printResults(agentIds, method, userRequest);
|
|
235
|
+
|
|
236
|
+
// Output for scripting (one ID per line)
|
|
237
|
+
if (args.includes('--quiet') || args.includes('-q')) {
|
|
238
|
+
console.clear();
|
|
239
|
+
agentIds.forEach(id => console.log(id));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main().catch(error => {
|
|
244
|
+
console.error(`${colors.red}✗ Error: ${error.message}${colors.reset}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|