wogiflow 1.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Prompt Composer
|
|
5
|
+
*
|
|
6
|
+
* Assembles prompt fragments into complete prompts tailored
|
|
7
|
+
* to specific models and CLIs.
|
|
8
|
+
*
|
|
9
|
+
* Part of Phase 2: Multi-Model Core
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* flow prompt-compose --model claude-sonnet-4 --task-type feature
|
|
13
|
+
* flow prompt-compose --model gemini-2-flash --domain api
|
|
14
|
+
* flow prompt-compose --list-fragments
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const {
|
|
20
|
+
PROJECT_ROOT,
|
|
21
|
+
parseFlags,
|
|
22
|
+
outputJson,
|
|
23
|
+
color,
|
|
24
|
+
info,
|
|
25
|
+
warn,
|
|
26
|
+
error,
|
|
27
|
+
fileExists,
|
|
28
|
+
dirExists,
|
|
29
|
+
printHeader,
|
|
30
|
+
printSection,
|
|
31
|
+
isPathWithinProject
|
|
32
|
+
} = require('./flow-utils');
|
|
33
|
+
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Constants
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
const FRAGMENTS_DIR = path.join(PROJECT_ROOT, '.workflow', 'prompts', 'fragments');
|
|
39
|
+
const COMPOSED_DIR = path.join(PROJECT_ROOT, '.workflow', 'prompts', 'composed');
|
|
40
|
+
|
|
41
|
+
// Model to CLI mapping
|
|
42
|
+
const MODEL_CLI_MAP = {
|
|
43
|
+
'claude-opus-4-5': 'claude-code',
|
|
44
|
+
'claude-sonnet-4': 'claude-code',
|
|
45
|
+
'claude-haiku-3-5': 'claude-code',
|
|
46
|
+
'gpt-4o': null,
|
|
47
|
+
'gemini-2-flash': 'gemini-cli'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Fragment Loading
|
|
52
|
+
// ============================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse fragment front matter
|
|
56
|
+
* @param {string} content - Fragment file content
|
|
57
|
+
* @returns {Object} Parsed fragment with metadata and content
|
|
58
|
+
*/
|
|
59
|
+
function parseFragment(content) {
|
|
60
|
+
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
61
|
+
|
|
62
|
+
if (!frontMatterMatch) {
|
|
63
|
+
return {
|
|
64
|
+
metadata: {},
|
|
65
|
+
content: content.trim(),
|
|
66
|
+
_parseErrors: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const frontMatter = frontMatterMatch[1];
|
|
71
|
+
const body = frontMatterMatch[2].trim();
|
|
72
|
+
|
|
73
|
+
// Parse YAML-like front matter with error tracking
|
|
74
|
+
const metadata = {};
|
|
75
|
+
const parseErrors = [];
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
for (const line of frontMatter.split('\n')) {
|
|
79
|
+
const colonIndex = line.indexOf(':');
|
|
80
|
+
if (colonIndex === -1) {
|
|
81
|
+
// Skip empty lines silently, warn about malformed lines
|
|
82
|
+
if (line.trim() && !line.trim().startsWith('#')) {
|
|
83
|
+
parseErrors.push(`Malformed line (no colon): "${line.slice(0, 50)}"`);
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const key = line.slice(0, colonIndex).trim();
|
|
89
|
+
if (!key) {
|
|
90
|
+
parseErrors.push(`Empty key in line: "${line.slice(0, 50)}"`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
95
|
+
|
|
96
|
+
// Parse arrays
|
|
97
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
98
|
+
try {
|
|
99
|
+
value = value.slice(1, -1).split(',').map(v => v.trim());
|
|
100
|
+
} catch {
|
|
101
|
+
parseErrors.push(`Failed to parse array for key "${key}"`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Parse numbers
|
|
106
|
+
else if (/^\d+$/.test(value)) {
|
|
107
|
+
value = parseInt(value, 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
metadata[key] = value;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
parseErrors.push(`Unexpected error: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Log warnings for parse errors
|
|
117
|
+
if (parseErrors.length > 0) {
|
|
118
|
+
warn(`Fragment parse warnings: ${parseErrors.join('; ')}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { metadata, content: body, _parseErrors: parseErrors };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load all fragments from directory
|
|
126
|
+
* @returns {Object[]} Array of loaded fragments
|
|
127
|
+
*/
|
|
128
|
+
function loadFragments() {
|
|
129
|
+
if (!dirExists(FRAGMENTS_DIR)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fragments = [];
|
|
134
|
+
|
|
135
|
+
let files;
|
|
136
|
+
try {
|
|
137
|
+
files = fs.readdirSync(FRAGMENTS_DIR).filter(f => f.endsWith('.md'));
|
|
138
|
+
} catch (err) {
|
|
139
|
+
warn(`Could not read fragments directory: ${err.message}`);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const filePath = path.join(FRAGMENTS_DIR, file);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
148
|
+
const parsed = parseFragment(content);
|
|
149
|
+
|
|
150
|
+
fragments.push({
|
|
151
|
+
file,
|
|
152
|
+
path: filePath,
|
|
153
|
+
...parsed
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
warn(`Could not read fragment ${file}: ${err.message}`);
|
|
157
|
+
// Continue with other fragments
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return fragments;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Filter fragments for a specific model and context
|
|
166
|
+
* @param {Object[]} fragments - All fragments
|
|
167
|
+
* @param {Object} filter - Filter criteria
|
|
168
|
+
* @returns {Object[]} Filtered and sorted fragments
|
|
169
|
+
*/
|
|
170
|
+
function filterFragments(fragments, filter) {
|
|
171
|
+
const { model, cli, domain, purpose } = filter;
|
|
172
|
+
|
|
173
|
+
return fragments.filter(f => {
|
|
174
|
+
const meta = f.metadata;
|
|
175
|
+
|
|
176
|
+
// Check model compatibility
|
|
177
|
+
if (meta.models && meta.models !== 'all') {
|
|
178
|
+
const models = Array.isArray(meta.models) ? meta.models : [meta.models];
|
|
179
|
+
if (!models.includes(model) && !models.includes('all')) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check CLI compatibility
|
|
185
|
+
if (meta.cli && meta.cli !== 'all') {
|
|
186
|
+
const clis = Array.isArray(meta.cli) ? meta.cli : [meta.cli];
|
|
187
|
+
if (!clis.includes(cli) && !clis.includes('all')) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check domain if specified
|
|
193
|
+
if (domain && meta.domain && meta.domain !== domain) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check purpose if specified
|
|
198
|
+
if (purpose && meta.purpose && meta.purpose !== purpose) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true;
|
|
203
|
+
}).sort((a, b) => {
|
|
204
|
+
// Sort by order (lower first)
|
|
205
|
+
const orderA = a.metadata.order || 50;
|
|
206
|
+
const orderB = b.metadata.order || 50;
|
|
207
|
+
return orderA - orderB;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// Prompt Composition
|
|
213
|
+
// ============================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Compose prompt from fragments
|
|
217
|
+
* @param {Object} params - Composition parameters
|
|
218
|
+
* @returns {Object} Composed prompt
|
|
219
|
+
*/
|
|
220
|
+
function composePrompt(params) {
|
|
221
|
+
const {
|
|
222
|
+
model,
|
|
223
|
+
taskType = 'feature',
|
|
224
|
+
domain = null,
|
|
225
|
+
taskData = null,
|
|
226
|
+
includeCore = true
|
|
227
|
+
} = params;
|
|
228
|
+
|
|
229
|
+
// Get CLI for model
|
|
230
|
+
const cli = MODEL_CLI_MAP[model] || 'claude-code';
|
|
231
|
+
|
|
232
|
+
// Load and filter fragments
|
|
233
|
+
const allFragments = loadFragments();
|
|
234
|
+
const filtered = filterFragments(allFragments, {
|
|
235
|
+
model,
|
|
236
|
+
cli,
|
|
237
|
+
domain
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Separate by purpose
|
|
241
|
+
const coreFragments = filtered.filter(f => f.metadata.purpose === 'core');
|
|
242
|
+
const qualityFragments = filtered.filter(f => f.metadata.purpose === 'quality');
|
|
243
|
+
const domainFragments = filtered.filter(f => f.metadata.purpose === 'domain');
|
|
244
|
+
const formatFragments = filtered.filter(f => f.metadata.purpose === 'formatting');
|
|
245
|
+
|
|
246
|
+
// Build sections
|
|
247
|
+
const sections = [];
|
|
248
|
+
|
|
249
|
+
// Core context
|
|
250
|
+
if (includeCore && coreFragments.length > 0) {
|
|
251
|
+
sections.push({
|
|
252
|
+
name: 'Task Context',
|
|
253
|
+
fragments: coreFragments
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Quality guidelines
|
|
258
|
+
if (qualityFragments.length > 0) {
|
|
259
|
+
sections.push({
|
|
260
|
+
name: 'Quality Guidelines',
|
|
261
|
+
fragments: qualityFragments
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Domain-specific
|
|
266
|
+
if (domainFragments.length > 0) {
|
|
267
|
+
sections.push({
|
|
268
|
+
name: 'Domain Guidelines',
|
|
269
|
+
fragments: domainFragments
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Output format (model-specific)
|
|
274
|
+
if (formatFragments.length > 0) {
|
|
275
|
+
sections.push({
|
|
276
|
+
name: 'Output Format',
|
|
277
|
+
fragments: formatFragments
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Compose full prompt
|
|
282
|
+
let fullPrompt = '';
|
|
283
|
+
|
|
284
|
+
for (const section of sections) {
|
|
285
|
+
for (const fragment of section.fragments) {
|
|
286
|
+
fullPrompt += fragment.content + '\n\n';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Apply template substitution if task data provided
|
|
291
|
+
if (taskData) {
|
|
292
|
+
fullPrompt = applyTemplate(fullPrompt, taskData);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
model,
|
|
297
|
+
cli,
|
|
298
|
+
domain,
|
|
299
|
+
taskType,
|
|
300
|
+
sections: sections.map(s => ({
|
|
301
|
+
name: s.name,
|
|
302
|
+
fragments: s.fragments.map(f => f.metadata.id || f.file)
|
|
303
|
+
})),
|
|
304
|
+
fragmentCount: filtered.length,
|
|
305
|
+
prompt: fullPrompt.trim(),
|
|
306
|
+
tokenEstimate: Math.ceil(fullPrompt.length / 4) // Rough estimate
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Apply handlebars-like template substitution
|
|
312
|
+
* @param {string} template - Template string
|
|
313
|
+
* @param {Object} data - Data to substitute
|
|
314
|
+
* @returns {string} Processed string
|
|
315
|
+
*/
|
|
316
|
+
function applyTemplate(template, data) {
|
|
317
|
+
// Forbidden keys to prevent prototype pollution (case-insensitive)
|
|
318
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
319
|
+
|
|
320
|
+
// Simple substitution: {{key}} or {{object.key}}
|
|
321
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
322
|
+
const keys = path.trim().split('.');
|
|
323
|
+
let value = data;
|
|
324
|
+
|
|
325
|
+
for (const key of keys) {
|
|
326
|
+
// Prevent prototype pollution attacks (case-insensitive check)
|
|
327
|
+
const keyLower = key.toLowerCase();
|
|
328
|
+
if (FORBIDDEN_KEYS.has(keyLower)) return match;
|
|
329
|
+
if (value === undefined || value === null) return match;
|
|
330
|
+
// Only access own properties
|
|
331
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) return match;
|
|
332
|
+
value = value[key];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
return value.join('\n');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return value !== undefined ? String(value) : match;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ============================================================
|
|
344
|
+
// CLI Output
|
|
345
|
+
// ============================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* List all available fragments
|
|
349
|
+
*/
|
|
350
|
+
function listFragments() {
|
|
351
|
+
const fragments = loadFragments();
|
|
352
|
+
|
|
353
|
+
printHeader('PROMPT FRAGMENTS');
|
|
354
|
+
|
|
355
|
+
if (fragments.length === 0) {
|
|
356
|
+
info('No fragments found in ' + FRAGMENTS_DIR);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Group by purpose
|
|
361
|
+
const byPurpose = {};
|
|
362
|
+
for (const f of fragments) {
|
|
363
|
+
const purpose = f.metadata.purpose || 'other';
|
|
364
|
+
if (!byPurpose[purpose]) byPurpose[purpose] = [];
|
|
365
|
+
byPurpose[purpose].push(f);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [purpose, frags] of Object.entries(byPurpose)) {
|
|
369
|
+
printSection(purpose.charAt(0).toUpperCase() + purpose.slice(1));
|
|
370
|
+
|
|
371
|
+
for (const f of frags) {
|
|
372
|
+
const models = f.metadata.models === 'all' ? 'all' :
|
|
373
|
+
(Array.isArray(f.metadata.models) ? f.metadata.models.join(', ') : f.metadata.models);
|
|
374
|
+
console.log(` ${color('cyan', f.metadata.id || f.file)}`);
|
|
375
|
+
console.log(` Models: ${models}`);
|
|
376
|
+
console.log(` Order: ${f.metadata.order || 50}`);
|
|
377
|
+
if (f.metadata.description) {
|
|
378
|
+
console.log(` ${f.metadata.description}`);
|
|
379
|
+
}
|
|
380
|
+
console.log('');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Print composed prompt summary
|
|
387
|
+
* @param {Object} composed - Composed prompt result
|
|
388
|
+
*/
|
|
389
|
+
function printComposed(composed) {
|
|
390
|
+
printHeader('COMPOSED PROMPT');
|
|
391
|
+
|
|
392
|
+
printSection('Configuration');
|
|
393
|
+
console.log(` Model: ${color('cyan', composed.model)}`);
|
|
394
|
+
console.log(` CLI: ${composed.cli}`);
|
|
395
|
+
if (composed.domain) {
|
|
396
|
+
console.log(` Domain: ${composed.domain}`);
|
|
397
|
+
}
|
|
398
|
+
console.log(` Task Type: ${composed.taskType}`);
|
|
399
|
+
|
|
400
|
+
printSection('Sections');
|
|
401
|
+
for (const section of composed.sections) {
|
|
402
|
+
console.log(` ${section.name}:`);
|
|
403
|
+
for (const frag of section.fragments) {
|
|
404
|
+
console.log(` - ${frag}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
printSection('Stats');
|
|
409
|
+
console.log(` Fragments: ${composed.fragmentCount}`);
|
|
410
|
+
console.log(` Estimated tokens: ~${composed.tokenEstimate.toLocaleString()}`);
|
|
411
|
+
|
|
412
|
+
if (composed.prompt) {
|
|
413
|
+
printSection('Preview (first 500 chars)');
|
|
414
|
+
console.log(` ${composed.prompt.slice(0, 500)}...`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log('');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================
|
|
421
|
+
// Main
|
|
422
|
+
// ============================================================
|
|
423
|
+
|
|
424
|
+
async function main() {
|
|
425
|
+
const { positional, flags } = parseFlags(process.argv.slice(2));
|
|
426
|
+
|
|
427
|
+
// List fragments mode
|
|
428
|
+
if (flags['list-fragments'] || positional[0] === 'list') {
|
|
429
|
+
listFragments();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Compose prompt
|
|
434
|
+
const model = flags.model || 'claude-sonnet-4';
|
|
435
|
+
const taskType = flags['task-type'] || flags.type || 'feature';
|
|
436
|
+
const domain = flags.domain || null;
|
|
437
|
+
|
|
438
|
+
const composed = composePrompt({
|
|
439
|
+
model,
|
|
440
|
+
taskType,
|
|
441
|
+
domain,
|
|
442
|
+
includeCore: true
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Output
|
|
446
|
+
if (flags.json) {
|
|
447
|
+
outputJson({
|
|
448
|
+
success: true,
|
|
449
|
+
...composed
|
|
450
|
+
});
|
|
451
|
+
} else {
|
|
452
|
+
printComposed(composed);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Optionally save to file
|
|
456
|
+
if (flags.output) {
|
|
457
|
+
const outputPath = path.isAbsolute(flags.output)
|
|
458
|
+
? flags.output
|
|
459
|
+
: path.join(PROJECT_ROOT, flags.output);
|
|
460
|
+
|
|
461
|
+
// Validate path is within project to prevent path traversal
|
|
462
|
+
if (!isPathWithinProject(outputPath)) {
|
|
463
|
+
error('Output path must be within project directory');
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
fs.writeFileSync(outputPath, composed.prompt);
|
|
468
|
+
info(`Saved to: ${outputPath}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Export for use by other scripts
|
|
473
|
+
module.exports = {
|
|
474
|
+
composePrompt,
|
|
475
|
+
loadFragments,
|
|
476
|
+
filterFragments,
|
|
477
|
+
parseFragment,
|
|
478
|
+
applyTemplate,
|
|
479
|
+
MODEL_CLI_MAP
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (require.main === module) {
|
|
483
|
+
main().catch(err => {
|
|
484
|
+
error(err.message);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
});
|
|
487
|
+
}
|