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,1015 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Auto Context Loading
|
|
5
|
+
*
|
|
6
|
+
* Intelligently loads relevant context before any task starts.
|
|
7
|
+
* Analyzes task descriptions and loads matching files from:
|
|
8
|
+
* - app-map.md (component registry)
|
|
9
|
+
* - component-index.json (auto-scanned files)
|
|
10
|
+
* - Codebase grep results
|
|
11
|
+
*
|
|
12
|
+
* Uses proactive context gathering approach.
|
|
13
|
+
*
|
|
14
|
+
* Usage as module:
|
|
15
|
+
* const { getAutoContext } = require('./flow-auto-context');
|
|
16
|
+
* const context = await getAutoContext('implement user authentication');
|
|
17
|
+
*
|
|
18
|
+
* Usage as CLI:
|
|
19
|
+
* flow auto-context "task description"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { execSync } = require('child_process');
|
|
25
|
+
const {
|
|
26
|
+
getProjectRoot,
|
|
27
|
+
getConfig,
|
|
28
|
+
PATHS,
|
|
29
|
+
colors,
|
|
30
|
+
isAstGrepAvailable,
|
|
31
|
+
astGrepSearch,
|
|
32
|
+
AST_PATTERNS,
|
|
33
|
+
findReactComponents,
|
|
34
|
+
findCustomHooks,
|
|
35
|
+
findTypeDefinitions
|
|
36
|
+
} = require('./flow-utils');
|
|
37
|
+
|
|
38
|
+
// Semantic memory search (optional - may not be initialized)
|
|
39
|
+
let searchFacts = null;
|
|
40
|
+
try {
|
|
41
|
+
const memoryDb = require('./flow-memory-db');
|
|
42
|
+
searchFacts = memoryDb.searchFacts;
|
|
43
|
+
} catch {
|
|
44
|
+
// Memory DB not available - that's ok
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Index Freshness Check
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if component index is stale and refresh if needed
|
|
55
|
+
* @param {object} config - Config object with componentIndex settings
|
|
56
|
+
* @returns {boolean} - True if index was refreshed
|
|
57
|
+
*/
|
|
58
|
+
function checkAndRefreshIndex(config) {
|
|
59
|
+
const indexPath = path.join(PATHS.state, 'component-index.json');
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(indexPath)) {
|
|
62
|
+
return false; // No index to refresh
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const staleAfterMinutes = config.componentIndex?.staleAfterMinutes || 60;
|
|
66
|
+
const scanOn = config.componentIndex?.scanOn || [];
|
|
67
|
+
|
|
68
|
+
// Only check if sessionStart is a trigger
|
|
69
|
+
if (!scanOn.includes('sessionStart')) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const stats = fs.statSync(indexPath);
|
|
75
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
76
|
+
const staleMs = staleAfterMinutes * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
if (ageMs > staleMs) {
|
|
79
|
+
// Index is stale - refresh it
|
|
80
|
+
execSync('bash scripts/flow-map-index scan --quiet', {
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
stdio: 'pipe',
|
|
83
|
+
timeout: 30000 // 30 second timeout
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore errors - stale check is best-effort
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================
|
|
95
|
+
// Keyword Extraction
|
|
96
|
+
// ============================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract keywords from task description
|
|
100
|
+
* Returns weighted keywords for context matching
|
|
101
|
+
*/
|
|
102
|
+
function extractKeywords(description) {
|
|
103
|
+
const text = description.toLowerCase();
|
|
104
|
+
const words = text.match(/[a-z]+/g) || [];
|
|
105
|
+
|
|
106
|
+
// High-value keywords (likely component/feature names)
|
|
107
|
+
const highValue = new Set([
|
|
108
|
+
'auth', 'authentication', 'login', 'logout', 'signup', 'register',
|
|
109
|
+
'user', 'profile', 'account', 'settings', 'dashboard', 'admin',
|
|
110
|
+
'form', 'modal', 'dialog', 'button', 'input', 'select', 'dropdown',
|
|
111
|
+
'table', 'list', 'grid', 'card', 'nav', 'navigation', 'menu', 'sidebar',
|
|
112
|
+
'header', 'footer', 'layout', 'page', 'view', 'screen',
|
|
113
|
+
'api', 'service', 'hook', 'context', 'provider', 'store', 'state',
|
|
114
|
+
'payment', 'checkout', 'cart', 'order', 'product', 'item',
|
|
115
|
+
'search', 'filter', 'sort', 'pagination', 'infinite',
|
|
116
|
+
'upload', 'download', 'file', 'image', 'media', 'avatar',
|
|
117
|
+
'notification', 'alert', 'toast', 'message', 'error', 'success',
|
|
118
|
+
'loading', 'spinner', 'skeleton', 'placeholder'
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Action keywords (help identify task type)
|
|
122
|
+
const actions = new Set([
|
|
123
|
+
'add', 'create', 'implement', 'build', 'make',
|
|
124
|
+
'fix', 'repair', 'resolve', 'debug', 'patch',
|
|
125
|
+
'update', 'modify', 'change', 'edit', 'refactor',
|
|
126
|
+
'remove', 'delete', 'clean', 'optimize',
|
|
127
|
+
'test', 'validate', 'check', 'verify'
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
const result = {
|
|
131
|
+
high: [], // High-value component/feature keywords
|
|
132
|
+
medium: [], // Regular keywords
|
|
133
|
+
actions: [] // Action verbs
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
for (const word of words) {
|
|
137
|
+
if (word.length < 3) continue;
|
|
138
|
+
|
|
139
|
+
if (highValue.has(word)) {
|
|
140
|
+
result.high.push(word);
|
|
141
|
+
} else if (actions.has(word)) {
|
|
142
|
+
result.actions.push(word);
|
|
143
|
+
} else if (word.length >= 4) {
|
|
144
|
+
result.medium.push(word);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Also extract PascalCase/camelCase terms (likely component names)
|
|
149
|
+
const caseTerms = description.match(/[A-Z][a-z]+(?:[A-Z][a-z]+)*/g) || [];
|
|
150
|
+
for (const term of caseTerms) {
|
|
151
|
+
if (!result.high.includes(term.toLowerCase())) {
|
|
152
|
+
result.high.push(term);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Infer task type from extracted keywords
|
|
161
|
+
* Used to customize AST-grep search patterns
|
|
162
|
+
*/
|
|
163
|
+
function inferTaskType(keywords) {
|
|
164
|
+
const allKeywords = [...keywords.high, ...keywords.medium, ...keywords.actions].map(k => k.toLowerCase());
|
|
165
|
+
|
|
166
|
+
// PRIORITY 1: Check for action keywords first (fix, refactor take precedence)
|
|
167
|
+
// These override noun-based detection since "refactor auth service" should be refactor, not create-service
|
|
168
|
+
|
|
169
|
+
// Check for fix/bug keywords
|
|
170
|
+
if (allKeywords.some(k => ['fix', 'bug', 'issue', 'error', 'broken'].includes(k))) {
|
|
171
|
+
return 'fix-bug';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for refactor keywords
|
|
175
|
+
if (allKeywords.some(k => ['refactor', 'cleanup', 'optimize', 'improve', 'reorganize'].includes(k))) {
|
|
176
|
+
return 'refactor';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// PRIORITY 2: Check for creation patterns (create/add/new + noun)
|
|
180
|
+
|
|
181
|
+
// Check for component-related keywords
|
|
182
|
+
if (allKeywords.some(k => ['component', 'button', 'form', 'modal', 'card', 'dialog', 'page', 'view', 'ui'].includes(k))) {
|
|
183
|
+
if (allKeywords.includes('create') || allKeywords.includes('add') || allKeywords.includes('new')) {
|
|
184
|
+
return 'create-component';
|
|
185
|
+
}
|
|
186
|
+
return 'modify-component';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for hook-related keywords
|
|
190
|
+
// Common React hooks (lowercase, as keywords are lowercased)
|
|
191
|
+
const reactHooks = ['usestate', 'useeffect', 'usecontext', 'usereducer', 'usecallback',
|
|
192
|
+
'usememo', 'useref', 'useimperativehandle', 'uselayouteffect', 'usedebugvalue',
|
|
193
|
+
'usetransition', 'usedeferredvalue', 'useid', 'usesyncexternalstore', 'useinsertioneffect'];
|
|
194
|
+
const isHookKeyword = (k) => {
|
|
195
|
+
if (k === 'hook' || k === 'state' || k === 'effect') return true;
|
|
196
|
+
// Match known React hooks
|
|
197
|
+
if (reactHooks.includes(k)) return true;
|
|
198
|
+
// Match custom hooks: useXyz where it's not a common word like "user", "used", "useful"
|
|
199
|
+
if (k.startsWith('use') && k.length > 4 && !['user', 'used', 'uses', 'useful'].includes(k)) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
if (allKeywords.some(isHookKeyword)) {
|
|
205
|
+
if (allKeywords.includes('create') || allKeywords.includes('add') || allKeywords.includes('new')) {
|
|
206
|
+
return 'create-hook';
|
|
207
|
+
}
|
|
208
|
+
return 'modify-hook';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for service/API keywords
|
|
212
|
+
if (allKeywords.some(k => ['api', 'service', 'fetch', 'request', 'endpoint'].includes(k))) {
|
|
213
|
+
if (allKeywords.includes('create') || allKeywords.includes('add') || allKeywords.includes('new')) {
|
|
214
|
+
return 'create-service';
|
|
215
|
+
}
|
|
216
|
+
return 'modify-service';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return 'generic';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================
|
|
223
|
+
// Context Sources
|
|
224
|
+
// ============================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Search app-map.md for matching components
|
|
228
|
+
*/
|
|
229
|
+
function searchAppMap(keywords) {
|
|
230
|
+
const results = [];
|
|
231
|
+
const appMapPath = PATHS.appMap;
|
|
232
|
+
|
|
233
|
+
if (!fs.existsSync(appMapPath)) return results;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(appMapPath, 'utf-8');
|
|
237
|
+
const lines = content.split('\n');
|
|
238
|
+
|
|
239
|
+
const allKeywords = [...keywords.high, ...keywords.medium];
|
|
240
|
+
|
|
241
|
+
for (const keyword of allKeywords) {
|
|
242
|
+
const regex = new RegExp(keyword, 'gi');
|
|
243
|
+
for (let i = 0; i < lines.length; i++) {
|
|
244
|
+
if (regex.test(lines[i])) {
|
|
245
|
+
// Extract component info from nearby lines
|
|
246
|
+
const contextStart = Math.max(0, i - 2);
|
|
247
|
+
const contextEnd = Math.min(lines.length, i + 5);
|
|
248
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
249
|
+
|
|
250
|
+
// Try to extract file path
|
|
251
|
+
const pathMatch = context.match(/`([^`]+\.(tsx?|jsx?|vue))`/);
|
|
252
|
+
if (pathMatch) {
|
|
253
|
+
results.push({
|
|
254
|
+
source: 'app-map',
|
|
255
|
+
keyword,
|
|
256
|
+
path: pathMatch[1],
|
|
257
|
+
context: context.slice(0, 200),
|
|
258
|
+
score: keywords.high.includes(keyword) ? 3 : 1
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// Ignore errors
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Search component-index.json for matching files
|
|
273
|
+
* @param {object} keywords - Extracted keywords object
|
|
274
|
+
* @param {object} config - Config object with autoContext settings
|
|
275
|
+
*/
|
|
276
|
+
function searchComponentIndex(keywords, config = null) {
|
|
277
|
+
const results = [];
|
|
278
|
+
const indexPath = path.join(PATHS.state, 'component-index.json');
|
|
279
|
+
|
|
280
|
+
if (!fs.existsSync(indexPath)) return results;
|
|
281
|
+
|
|
282
|
+
// Use config values if available
|
|
283
|
+
const cfg = config || getConfig();
|
|
284
|
+
const maxComponentMatches = cfg.autoContext?.maxComponentMatches || 15;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
288
|
+
const components = index.components || [];
|
|
289
|
+
|
|
290
|
+
const allKeywords = [...keywords.high, ...keywords.medium];
|
|
291
|
+
let totalMatches = 0;
|
|
292
|
+
|
|
293
|
+
for (const comp of components) {
|
|
294
|
+
const name = (comp.name || '').toLowerCase();
|
|
295
|
+
const filePath = comp.path || '';
|
|
296
|
+
|
|
297
|
+
for (const keyword of allKeywords) {
|
|
298
|
+
const kw = keyword.toLowerCase();
|
|
299
|
+
if (name.includes(kw) || filePath.toLowerCase().includes(kw)) {
|
|
300
|
+
totalMatches++;
|
|
301
|
+
if (results.length < maxComponentMatches) {
|
|
302
|
+
results.push({
|
|
303
|
+
source: 'component-index',
|
|
304
|
+
keyword,
|
|
305
|
+
path: filePath,
|
|
306
|
+
name: comp.name,
|
|
307
|
+
exports: comp.exports || [],
|
|
308
|
+
score: keywords.high.includes(keyword) ? 3 : 1
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
break; // Don't add same component multiple times
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add truncation notice if we limited results
|
|
317
|
+
if (totalMatches > maxComponentMatches) {
|
|
318
|
+
results.push({
|
|
319
|
+
source: 'truncation_notice',
|
|
320
|
+
message: `... and ${totalMatches - maxComponentMatches} more component matches (limited to ${maxComponentMatches})`,
|
|
321
|
+
score: 0
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Ignore errors
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Grep codebase for keyword matches
|
|
333
|
+
* @param {object} keywords - Extracted keywords object
|
|
334
|
+
* @param {number} maxResults - Maximum results to return
|
|
335
|
+
* @param {object} config - Config object with autoContext settings
|
|
336
|
+
*/
|
|
337
|
+
function grepCodebase(keywords, maxResults = 10, config = null) {
|
|
338
|
+
const results = [];
|
|
339
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
340
|
+
|
|
341
|
+
if (!fs.existsSync(srcDir)) return results;
|
|
342
|
+
|
|
343
|
+
// Use config values if available
|
|
344
|
+
const cfg = config || getConfig();
|
|
345
|
+
const effectiveMaxResults = cfg.autoContext?.maxGrepResults || maxResults;
|
|
346
|
+
const maxContentLines = cfg.autoContext?.maxContentLines || 50;
|
|
347
|
+
|
|
348
|
+
// Only grep for high-value keywords to avoid noise
|
|
349
|
+
const searchKeywords = keywords.high.slice(0, 5);
|
|
350
|
+
let totalMatches = 0;
|
|
351
|
+
|
|
352
|
+
for (const keyword of searchKeywords) {
|
|
353
|
+
try {
|
|
354
|
+
// Case-insensitive grep for the keyword
|
|
355
|
+
const output = execSync(
|
|
356
|
+
`grep -ril "${keyword}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" "${srcDir}" 2>/dev/null | head -20`,
|
|
357
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const files = output.split('\n').filter(f => f.trim());
|
|
361
|
+
totalMatches += files.length;
|
|
362
|
+
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
if (results.length >= effectiveMaxResults) break;
|
|
365
|
+
|
|
366
|
+
const relPath = path.relative(PROJECT_ROOT, file);
|
|
367
|
+
if (!results.some(r => r.path === relPath)) {
|
|
368
|
+
// Optionally read file content with truncation
|
|
369
|
+
let content = null;
|
|
370
|
+
if (cfg.autoContext?.includeContent) {
|
|
371
|
+
try {
|
|
372
|
+
const fullContent = fs.readFileSync(file, 'utf-8');
|
|
373
|
+
const lines = fullContent.split('\n');
|
|
374
|
+
if (lines.length > maxContentLines) {
|
|
375
|
+
content = [
|
|
376
|
+
...lines.slice(0, maxContentLines),
|
|
377
|
+
`\n... ${lines.length - maxContentLines} more lines truncated ...`
|
|
378
|
+
].join('\n');
|
|
379
|
+
} else {
|
|
380
|
+
content = fullContent;
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
// Ignore read errors
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
results.push({
|
|
388
|
+
source: 'grep',
|
|
389
|
+
keyword,
|
|
390
|
+
path: relPath,
|
|
391
|
+
content,
|
|
392
|
+
score: 2
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Ignore grep errors (no matches, timeout, etc.)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (results.length >= effectiveMaxResults) break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Add truncation notice if we limited results
|
|
404
|
+
if (totalMatches > effectiveMaxResults) {
|
|
405
|
+
results.push({
|
|
406
|
+
source: 'truncation_notice',
|
|
407
|
+
message: `... and ${totalMatches - effectiveMaxResults} more grep matches (limited to ${effectiveMaxResults})`,
|
|
408
|
+
score: 0
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Search ready.json for related tasks
|
|
417
|
+
*/
|
|
418
|
+
function searchRelatedTasks(keywords) {
|
|
419
|
+
const results = [];
|
|
420
|
+
|
|
421
|
+
if (!fs.existsSync(PATHS.ready)) return results;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const data = JSON.parse(fs.readFileSync(PATHS.ready, 'utf-8'));
|
|
425
|
+
const allTasks = [
|
|
426
|
+
...(data.ready || []),
|
|
427
|
+
...(data.inProgress || []),
|
|
428
|
+
...(data.recentlyCompleted || []).slice(0, 5)
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
const allKeywords = [...keywords.high, ...keywords.medium];
|
|
432
|
+
|
|
433
|
+
for (const task of allTasks) {
|
|
434
|
+
const title = typeof task === 'string' ? task : (task.title || task.id || '');
|
|
435
|
+
const titleLower = title.toLowerCase();
|
|
436
|
+
|
|
437
|
+
for (const keyword of allKeywords) {
|
|
438
|
+
if (titleLower.includes(keyword.toLowerCase())) {
|
|
439
|
+
results.push({
|
|
440
|
+
source: 'related-task',
|
|
441
|
+
keyword,
|
|
442
|
+
taskId: typeof task === 'string' ? task : task.id,
|
|
443
|
+
title,
|
|
444
|
+
score: 1
|
|
445
|
+
});
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
// Ignore errors
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return results;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Search semantic memory (SQLite facts) for relevant context
|
|
459
|
+
* Returns facts that match the task description
|
|
460
|
+
*
|
|
461
|
+
* @param {object} keywords - Extracted keywords
|
|
462
|
+
* @param {object} config - Config object
|
|
463
|
+
*/
|
|
464
|
+
async function searchSemanticMemory(keywords, config = null) {
|
|
465
|
+
// Skip if searchFacts not available or memory disabled
|
|
466
|
+
if (!searchFacts) return [];
|
|
467
|
+
|
|
468
|
+
const cfg = config || getConfig();
|
|
469
|
+
if (!cfg.memory?.enabled) return [];
|
|
470
|
+
|
|
471
|
+
const results = [];
|
|
472
|
+
const maxFacts = cfg.autoContext?.maxSemanticFacts || 5;
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// Build query from high-value keywords
|
|
476
|
+
const query = keywords.high.join(' ') || keywords.medium.slice(0, 3).join(' ');
|
|
477
|
+
if (!query) return [];
|
|
478
|
+
|
|
479
|
+
// Search for relevant facts
|
|
480
|
+
const facts = await searchFacts({
|
|
481
|
+
query,
|
|
482
|
+
limit: maxFacts,
|
|
483
|
+
trackAccess: true // Boost relevance when recalled
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
for (const fact of facts) {
|
|
487
|
+
// Only include facts with reasonable relevance (>40%)
|
|
488
|
+
if (fact.relevance >= 40) {
|
|
489
|
+
results.push({
|
|
490
|
+
source: 'semantic-memory',
|
|
491
|
+
fact: fact.fact,
|
|
492
|
+
category: fact.category,
|
|
493
|
+
relevance: fact.relevance,
|
|
494
|
+
score: Math.round(fact.relevance / 25) // Score 1-4 based on relevance
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
// Graceful fallback - memory search is optional
|
|
500
|
+
if (cfg.debug) {
|
|
501
|
+
console.warn(`Semantic memory search failed: ${err.message}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Enrich file results with LSP type information
|
|
510
|
+
* Runs AFTER initial grep/search phase completes
|
|
511
|
+
* v2.2: LSP enrichment for auto-context
|
|
512
|
+
*
|
|
513
|
+
* @param {Array} fileResults - Results with path property
|
|
514
|
+
* @param {object} config - Config object
|
|
515
|
+
*/
|
|
516
|
+
async function enrichWithLSP(fileResults, config) {
|
|
517
|
+
// Skip if disabled
|
|
518
|
+
if (!config.autoContext?.lspEnrichment?.enabled) return fileResults;
|
|
519
|
+
|
|
520
|
+
// Lazy load LSP module to avoid circular dependencies
|
|
521
|
+
let getLSP, isLSPEnabled;
|
|
522
|
+
try {
|
|
523
|
+
const lspModule = require('./flow-lsp');
|
|
524
|
+
getLSP = lspModule.getLSP;
|
|
525
|
+
isLSPEnabled = lspModule.isLSPEnabled;
|
|
526
|
+
} catch {
|
|
527
|
+
return fileResults; // LSP module not available
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!isLSPEnabled()) return fileResults;
|
|
531
|
+
|
|
532
|
+
const lsp = await getLSP();
|
|
533
|
+
if (!lsp) return fileResults;
|
|
534
|
+
|
|
535
|
+
const timeout = config.autoContext?.lspEnrichment?.timeoutMs || 2000;
|
|
536
|
+
const maxFiles = config.autoContext?.lspEnrichment?.maxFiles || 5;
|
|
537
|
+
|
|
538
|
+
// Only enrich top N JS/TS files to limit latency
|
|
539
|
+
const filesToEnrich = fileResults
|
|
540
|
+
.filter(r => r.path && /\.(ts|tsx|js|jsx)$/.test(r.path))
|
|
541
|
+
.slice(0, maxFiles);
|
|
542
|
+
|
|
543
|
+
if (filesToEnrich.length === 0) return fileResults;
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const enriched = await Promise.race([
|
|
547
|
+
Promise.all(filesToEnrich.map(async (result) => {
|
|
548
|
+
try {
|
|
549
|
+
const [symbols, diagnostics] = await Promise.all([
|
|
550
|
+
lsp.getDocumentSymbols(result.path),
|
|
551
|
+
lsp.getDiagnostics(result.path)
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
...result,
|
|
556
|
+
lsp: {
|
|
557
|
+
exports: (symbols || [])
|
|
558
|
+
.filter(s => ['function', 'class', 'interface', 'variable'].includes(s.kind))
|
|
559
|
+
.slice(0, 10)
|
|
560
|
+
.map(s => ({ name: s.name, kind: s.kind })),
|
|
561
|
+
errorCount: (diagnostics || []).filter(d => d.severity === 'error').length,
|
|
562
|
+
warningCount: (diagnostics || []).filter(d => d.severity === 'warning').length
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
} catch {
|
|
566
|
+
return result; // Graceful fallback for individual files
|
|
567
|
+
}
|
|
568
|
+
})),
|
|
569
|
+
// Timeout fallback - return original results if LSP takes too long
|
|
570
|
+
new Promise(resolve => setTimeout(() => resolve(filesToEnrich), timeout))
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
// Merge enriched results back into full list
|
|
574
|
+
const enrichedMap = new Map(enriched.map(r => [r.path, r]));
|
|
575
|
+
return fileResults.map(r => enrichedMap.get(r.path) || r);
|
|
576
|
+
} catch {
|
|
577
|
+
// If anything fails, return original results
|
|
578
|
+
return fileResults;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Search codebase using AST-grep for structural patterns
|
|
584
|
+
* Falls back gracefully if ast-grep is not installed
|
|
585
|
+
*
|
|
586
|
+
* @param {object} keywords - Extracted keywords
|
|
587
|
+
* @param {string} taskType - Type of task (create-component, create-hook, etc.)
|
|
588
|
+
* @param {object} config - Config object
|
|
589
|
+
*/
|
|
590
|
+
function searchWithAstGrep(keywords, taskType = null, config = null) {
|
|
591
|
+
const cfg = config || getConfig();
|
|
592
|
+
|
|
593
|
+
// Skip if disabled or ast-grep not available
|
|
594
|
+
if (!cfg.autoContext?.useAstGrep || !isAstGrepAvailable()) {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const results = [];
|
|
599
|
+
const maxResults = cfg.autoContext?.maxAstGrepResults || 5;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
// Determine search strategy based on task type
|
|
603
|
+
if (taskType === 'create-component' || keywords.high.some(k =>
|
|
604
|
+
['component', 'button', 'form', 'modal', 'card', 'list'].includes(k.toLowerCase())
|
|
605
|
+
)) {
|
|
606
|
+
// Find similar React components for reference
|
|
607
|
+
const components = findReactComponents({ maxResults: maxResults * 2 });
|
|
608
|
+
if (components) {
|
|
609
|
+
for (const comp of components.slice(0, maxResults)) {
|
|
610
|
+
results.push({
|
|
611
|
+
source: 'ast-grep',
|
|
612
|
+
type: 'component',
|
|
613
|
+
path: comp.file,
|
|
614
|
+
line: comp.line,
|
|
615
|
+
preview: comp.content?.slice(0, 100),
|
|
616
|
+
score: 2.5 // Higher than grep, lower than app-map
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (taskType === 'create-hook' || keywords.high.some(k =>
|
|
623
|
+
['hook', 'usestate', 'useeffect', 'usememo'].includes(k.toLowerCase())
|
|
624
|
+
)) {
|
|
625
|
+
// Find existing hooks for patterns
|
|
626
|
+
const hooks = findCustomHooks({ maxResults });
|
|
627
|
+
if (hooks) {
|
|
628
|
+
for (const hook of hooks.slice(0, Math.ceil(maxResults / 2))) {
|
|
629
|
+
results.push({
|
|
630
|
+
source: 'ast-grep',
|
|
631
|
+
type: 'hook',
|
|
632
|
+
path: hook.file,
|
|
633
|
+
line: hook.line,
|
|
634
|
+
preview: hook.content?.slice(0, 100),
|
|
635
|
+
score: 2.5
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Search for type definitions matching keywords
|
|
642
|
+
for (const keyword of keywords.high.slice(0, 3)) {
|
|
643
|
+
const types = findTypeDefinitions(keyword, { maxResults: 2 });
|
|
644
|
+
if (types && types.length > 0) {
|
|
645
|
+
for (const type of types) {
|
|
646
|
+
if (!results.some(r => r.path === type.file)) {
|
|
647
|
+
results.push({
|
|
648
|
+
source: 'ast-grep',
|
|
649
|
+
type: 'type-definition',
|
|
650
|
+
keyword,
|
|
651
|
+
path: type.file,
|
|
652
|
+
line: type.line,
|
|
653
|
+
preview: type.content?.slice(0, 100),
|
|
654
|
+
score: 2.5
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Generic pattern search for high-value keywords
|
|
662
|
+
for (const keyword of keywords.high.slice(0, 2)) {
|
|
663
|
+
// Search for exported functions/consts with this name
|
|
664
|
+
const pattern = `export $_ ${keyword}$_`;
|
|
665
|
+
const matches = astGrepSearch(pattern, { maxResults: 2 });
|
|
666
|
+
if (matches) {
|
|
667
|
+
for (const match of matches) {
|
|
668
|
+
if (!results.some(r => r.path === match.file)) {
|
|
669
|
+
results.push({
|
|
670
|
+
source: 'ast-grep',
|
|
671
|
+
type: 'export',
|
|
672
|
+
keyword,
|
|
673
|
+
path: match.file,
|
|
674
|
+
line: match.line,
|
|
675
|
+
score: 2
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
// Graceful fallback - AST-grep is optional
|
|
683
|
+
if (cfg.debug) {
|
|
684
|
+
console.warn(`AST-grep search failed: ${err.message}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Truncate if too many results
|
|
689
|
+
if (results.length > maxResults * 2) {
|
|
690
|
+
const truncated = results.slice(0, maxResults * 2);
|
|
691
|
+
truncated.push({
|
|
692
|
+
source: 'truncation_notice',
|
|
693
|
+
message: `... and ${results.length - maxResults * 2} more AST matches`,
|
|
694
|
+
score: 0
|
|
695
|
+
});
|
|
696
|
+
return truncated;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ============================================================
|
|
703
|
+
// Main Context Loading
|
|
704
|
+
// ============================================================
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get auto-context for a task description
|
|
708
|
+
* Returns prioritized list of relevant files and context
|
|
709
|
+
* Now async to support semantic memory search
|
|
710
|
+
*/
|
|
711
|
+
async function getAutoContext(description, options = {}) {
|
|
712
|
+
const config = getConfig();
|
|
713
|
+
|
|
714
|
+
// Check if auto-context is enabled
|
|
715
|
+
if (config.autoContext?.enabled === false) {
|
|
716
|
+
return { enabled: false, files: [], context: [] };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// v2.0: Check and refresh stale component index
|
|
720
|
+
if (config.componentIndex?.autoScan !== false) {
|
|
721
|
+
checkAndRefreshIndex(config);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const maxFiles = options.maxFiles || config.autoContext?.maxFilesToLoad || 10;
|
|
725
|
+
const showFiles = options.showFiles ?? config.autoContext?.showLoadedFiles ?? true;
|
|
726
|
+
|
|
727
|
+
// Extract keywords
|
|
728
|
+
const keywords = extractKeywords(description);
|
|
729
|
+
|
|
730
|
+
if (keywords.high.length === 0 && keywords.medium.length === 0) {
|
|
731
|
+
return {
|
|
732
|
+
enabled: true,
|
|
733
|
+
files: [],
|
|
734
|
+
context: [],
|
|
735
|
+
message: 'No specific keywords found in task description'
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Determine task type from keywords/options for ast-grep
|
|
740
|
+
const taskType = options.taskType || inferTaskType(keywords);
|
|
741
|
+
|
|
742
|
+
// v2.2: Search semantic memory (async)
|
|
743
|
+
const semanticResults = await searchSemanticMemory(keywords, config);
|
|
744
|
+
|
|
745
|
+
// Gather context from all sources (pass config for truncation settings)
|
|
746
|
+
const allResults = [
|
|
747
|
+
...searchAppMap(keywords),
|
|
748
|
+
...searchComponentIndex(keywords, config),
|
|
749
|
+
...searchWithAstGrep(keywords, taskType, config), // AST-grep search (if enabled)
|
|
750
|
+
...grepCodebase(keywords, 10, config),
|
|
751
|
+
...searchRelatedTasks(keywords),
|
|
752
|
+
...semanticResults // v2.2: Include semantic memory
|
|
753
|
+
];
|
|
754
|
+
|
|
755
|
+
// Collect truncation notices separately
|
|
756
|
+
const truncationNotices = allResults.filter(r => r.source === 'truncation_notice');
|
|
757
|
+
const actualResults = allResults.filter(r => r.source !== 'truncation_notice');
|
|
758
|
+
|
|
759
|
+
// Dedupe by path and sort by score
|
|
760
|
+
const seen = new Set();
|
|
761
|
+
const unique = [];
|
|
762
|
+
|
|
763
|
+
for (const result of actualResults) {
|
|
764
|
+
const key = result.path || result.taskId || result.keyword;
|
|
765
|
+
if (!seen.has(key)) {
|
|
766
|
+
seen.add(key);
|
|
767
|
+
unique.push(result);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Sort by score (higher first)
|
|
772
|
+
unique.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
773
|
+
|
|
774
|
+
// v2.2: LSP enrichment (async, with timeout)
|
|
775
|
+
const enrichedUnique = await enrichWithLSP(unique, config);
|
|
776
|
+
|
|
777
|
+
// Re-sort: prioritize files without errors (if LSP enrichment added data)
|
|
778
|
+
if (config.autoContext?.lspEnrichment?.prioritizeHealthyFiles !== false) {
|
|
779
|
+
enrichedUnique.sort((a, b) => {
|
|
780
|
+
// Files with LSP errors go to bottom
|
|
781
|
+
const aErrors = a.lsp?.errorCount || 0;
|
|
782
|
+
const bErrors = b.lsp?.errorCount || 0;
|
|
783
|
+
if (aErrors !== bErrors) return aErrors - bErrors;
|
|
784
|
+
// Then by original score
|
|
785
|
+
return (b.score || 0) - (a.score || 0);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Take top results
|
|
790
|
+
const topResults = enrichedUnique.slice(0, maxFiles);
|
|
791
|
+
|
|
792
|
+
// Extract unique file paths
|
|
793
|
+
const files = topResults
|
|
794
|
+
.filter(r => r.path)
|
|
795
|
+
.map(r => r.path);
|
|
796
|
+
|
|
797
|
+
// Extract semantic memory facts (v2.2)
|
|
798
|
+
const semanticFacts = topResults
|
|
799
|
+
.filter(r => r.source === 'semantic-memory')
|
|
800
|
+
.map(r => ({
|
|
801
|
+
fact: r.fact,
|
|
802
|
+
category: r.category,
|
|
803
|
+
relevance: r.relevance
|
|
804
|
+
}));
|
|
805
|
+
|
|
806
|
+
// Build context summary
|
|
807
|
+
const context = {
|
|
808
|
+
keywords: {
|
|
809
|
+
high: keywords.high,
|
|
810
|
+
medium: keywords.medium.slice(0, 5),
|
|
811
|
+
actions: keywords.actions
|
|
812
|
+
},
|
|
813
|
+
sources: {
|
|
814
|
+
appMap: topResults.filter(r => r.source === 'app-map').length,
|
|
815
|
+
componentIndex: topResults.filter(r => r.source === 'component-index').length,
|
|
816
|
+
grep: topResults.filter(r => r.source === 'grep').length,
|
|
817
|
+
relatedTasks: topResults.filter(r => r.source === 'related-task').length,
|
|
818
|
+
semanticMemory: semanticFacts.length // v2.2
|
|
819
|
+
},
|
|
820
|
+
relatedTasks: topResults
|
|
821
|
+
.filter(r => r.source === 'related-task')
|
|
822
|
+
.map(r => ({ id: r.taskId, title: r.title })),
|
|
823
|
+
semanticFacts, // v2.2: Include learned facts
|
|
824
|
+
truncated: truncationNotices.length > 0,
|
|
825
|
+
truncationNotices: truncationNotices.map(t => t.message)
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
enabled: true,
|
|
830
|
+
files,
|
|
831
|
+
results: topResults,
|
|
832
|
+
context,
|
|
833
|
+
semanticFacts, // v2.2: Top-level for easy access
|
|
834
|
+
truncationNotices,
|
|
835
|
+
message: files.length > 0
|
|
836
|
+
? `Found ${files.length} relevant file(s)${semanticFacts.length > 0 ? `, ${semanticFacts.length} learned facts` : ''}${truncationNotices.length > 0 ? ' (results truncated)' : ''}`
|
|
837
|
+
: (semanticFacts.length > 0 ? `Found ${semanticFacts.length} learned fact(s)` : 'No directly relevant files found')
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Format auto-context results for display
|
|
843
|
+
*/
|
|
844
|
+
function formatAutoContext(result) {
|
|
845
|
+
if (!result.enabled) {
|
|
846
|
+
return `${colors.dim}Auto-context disabled${colors.reset}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
let output = '';
|
|
850
|
+
|
|
851
|
+
if (result.files.length > 0) {
|
|
852
|
+
output += `${colors.cyan}đ Auto-loaded context:${colors.reset}\n`;
|
|
853
|
+
for (const file of result.files.slice(0, 8)) {
|
|
854
|
+
// v2.2: Look up LSP enrichment data for this file
|
|
855
|
+
const fileResult = result.results?.find(r => r.path === file);
|
|
856
|
+
const lsp = fileResult?.lsp;
|
|
857
|
+
|
|
858
|
+
let icon = 'âĸ';
|
|
859
|
+
let suffix = '';
|
|
860
|
+
if (lsp) {
|
|
861
|
+
if (lsp.errorCount > 0) {
|
|
862
|
+
icon = 'â';
|
|
863
|
+
suffix = ` ${colors.red}(${lsp.errorCount} error${lsp.errorCount > 1 ? 's' : ''})${colors.reset}`;
|
|
864
|
+
} else if (lsp.warningCount > 0) {
|
|
865
|
+
icon = 'â ī¸';
|
|
866
|
+
suffix = ` ${colors.yellow}(${lsp.warningCount} warning${lsp.warningCount > 1 ? 's' : ''})${colors.reset}`;
|
|
867
|
+
} else {
|
|
868
|
+
icon = 'â';
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
output += ` ${colors.dim}${icon}${colors.reset} ${file}${suffix}\n`;
|
|
872
|
+
}
|
|
873
|
+
if (result.files.length > 8) {
|
|
874
|
+
output += ` ${colors.dim}... and ${result.files.length - 8} more${colors.reset}\n`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// v2.2: Show key exports from LSP-enriched files
|
|
878
|
+
const filesWithExports = result.results?.filter(r => r.lsp?.exports?.length > 0) || [];
|
|
879
|
+
if (filesWithExports.length > 0) {
|
|
880
|
+
output += `\n${colors.cyan}đĻ Key exports:${colors.reset}\n`;
|
|
881
|
+
for (const fileInfo of filesWithExports.slice(0, 3)) {
|
|
882
|
+
const fileName = path.basename(fileInfo.path);
|
|
883
|
+
const exportNames = fileInfo.lsp.exports.slice(0, 5).map(e => e.name).join(', ');
|
|
884
|
+
const more = fileInfo.lsp.exports.length > 5 ? ` +${fileInfo.lsp.exports.length - 5}` : '';
|
|
885
|
+
output += ` ${colors.dim}${fileName}:${colors.reset} ${exportNames}${more}\n`;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
output += `${colors.dim}No specific files matched. Proceeding with general context.${colors.reset}\n`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (result.context?.relatedTasks?.length > 0) {
|
|
893
|
+
output += `\n${colors.cyan}đ Related tasks:${colors.reset}\n`;
|
|
894
|
+
for (const task of result.context.relatedTasks.slice(0, 3)) {
|
|
895
|
+
output += ` ${colors.dim}âĸ${colors.reset} ${task.id}: ${task.title}\n`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// v2.2: Show semantic memory facts
|
|
900
|
+
if (result.semanticFacts?.length > 0) {
|
|
901
|
+
output += `\n${colors.cyan}đ§ Learned facts:${colors.reset}\n`;
|
|
902
|
+
for (const fact of result.semanticFacts.slice(0, 5)) {
|
|
903
|
+
const relevanceIcon = fact.relevance >= 70 ? 'â' : fact.relevance >= 50 ? 'â' : 'â';
|
|
904
|
+
output += ` ${colors.dim}${relevanceIcon}${colors.reset} ${fact.fact.slice(0, 100)}${fact.fact.length > 100 ? '...' : ''}\n`;
|
|
905
|
+
}
|
|
906
|
+
if (result.semanticFacts.length > 5) {
|
|
907
|
+
output += ` ${colors.dim}... and ${result.semanticFacts.length - 5} more${colors.reset}\n`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Show truncation notices if any
|
|
912
|
+
if (result.truncationNotices?.length > 0) {
|
|
913
|
+
output += `\n${colors.dim}âšī¸ Results truncated:${colors.reset}\n`;
|
|
914
|
+
for (const notice of result.truncationNotices) {
|
|
915
|
+
output += ` ${colors.dim}${notice}${colors.reset}\n`;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return output;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ============================================================
|
|
923
|
+
// CLI
|
|
924
|
+
// ============================================================
|
|
925
|
+
|
|
926
|
+
function showHelp() {
|
|
927
|
+
console.log(`
|
|
928
|
+
Wogi Flow - Auto Context Loading
|
|
929
|
+
|
|
930
|
+
Analyzes task descriptions and automatically loads relevant context.
|
|
931
|
+
|
|
932
|
+
Usage:
|
|
933
|
+
flow auto-context "task description"
|
|
934
|
+
flow auto-context --json "task description"
|
|
935
|
+
|
|
936
|
+
Options:
|
|
937
|
+
--json Output as JSON
|
|
938
|
+
--verbose Show all matched results
|
|
939
|
+
--max N Maximum files to load (default: 10)
|
|
940
|
+
--help, -h Show this help
|
|
941
|
+
|
|
942
|
+
Examples:
|
|
943
|
+
flow auto-context "implement user authentication"
|
|
944
|
+
flow auto-context "fix the login form validation"
|
|
945
|
+
flow auto-context "add a new Button variant"
|
|
946
|
+
`);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async function main() {
|
|
950
|
+
const args = process.argv.slice(2);
|
|
951
|
+
|
|
952
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
953
|
+
showHelp();
|
|
954
|
+
process.exit(0);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const jsonOutput = args.includes('--json');
|
|
958
|
+
const verbose = args.includes('--verbose');
|
|
959
|
+
|
|
960
|
+
// Extract max files option
|
|
961
|
+
const maxIndex = args.indexOf('--max');
|
|
962
|
+
const maxFiles = maxIndex >= 0 ? parseInt(args[maxIndex + 1]) || 10 : 10;
|
|
963
|
+
|
|
964
|
+
// Get description (everything that's not a flag)
|
|
965
|
+
const description = args
|
|
966
|
+
.filter(a => !a.startsWith('--') && !(maxIndex >= 0 && args[maxIndex + 1] === a))
|
|
967
|
+
.join(' ');
|
|
968
|
+
|
|
969
|
+
if (!description) {
|
|
970
|
+
console.log(`${colors.red}Error: Please provide a task description${colors.reset}`);
|
|
971
|
+
showHelp();
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const result = await getAutoContext(description, { maxFiles });
|
|
976
|
+
|
|
977
|
+
if (jsonOutput) {
|
|
978
|
+
console.log(JSON.stringify(result, null, 2));
|
|
979
|
+
} else {
|
|
980
|
+
console.log(formatAutoContext(result));
|
|
981
|
+
|
|
982
|
+
if (verbose && result.results) {
|
|
983
|
+
console.log(`\n${colors.bold}All matches:${colors.reset}`);
|
|
984
|
+
for (const r of result.results) {
|
|
985
|
+
console.log(` [${r.source}] ${r.path || r.taskId || r.keyword} (score: ${r.score})`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ============================================================
|
|
992
|
+
// Exports
|
|
993
|
+
// ============================================================
|
|
994
|
+
|
|
995
|
+
module.exports = {
|
|
996
|
+
extractKeywords,
|
|
997
|
+
searchAppMap,
|
|
998
|
+
searchComponentIndex,
|
|
999
|
+
grepCodebase,
|
|
1000
|
+
searchRelatedTasks,
|
|
1001
|
+
searchWithAstGrep,
|
|
1002
|
+
searchSemanticMemory, // v2.2
|
|
1003
|
+
enrichWithLSP, // v2.2: LSP enrichment
|
|
1004
|
+
inferTaskType,
|
|
1005
|
+
checkAndRefreshIndex,
|
|
1006
|
+
getAutoContext,
|
|
1007
|
+
formatAutoContext
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (require.main === module) {
|
|
1011
|
+
main().catch(err => {
|
|
1012
|
+
console.error(`Error: ${err.message}`);
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
});
|
|
1015
|
+
}
|