ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Engine — Dynamic, learnable intent pattern matcher.
|
|
3
|
+
*
|
|
4
|
+
* Replaces hardcoded regex with a JSON-driven pattern store that learns
|
|
5
|
+
* from successful interactions. Patterns are loaded from disk, matched
|
|
6
|
+
* against user messages, and new patterns are learned over time.
|
|
7
|
+
*
|
|
8
|
+
* Storage: .ai-workflow/intelligence/intent-patterns.json
|
|
9
|
+
* .ai-workflow/intelligence/learned-phrases.json
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
import { getWorkflowDir } from './pipeline.js';
|
|
14
|
+
|
|
15
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const INTEL_DIR_NAME = 'intelligence';
|
|
18
|
+
|
|
19
|
+
function getIntelDir() {
|
|
20
|
+
const dir = resolve(getWorkflowDir(), INTEL_DIR_NAME);
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getPatternsPath() {
|
|
26
|
+
return resolve(getIntelDir(), 'intent-patterns.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getLearnedPath() {
|
|
30
|
+
return resolve(getIntelDir(), 'learned-phrases.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPromptPath() {
|
|
34
|
+
return resolve(getIntelDir(), 'system-prompt.md');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Default Patterns (seed) ─────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const DEFAULT_PATTERNS = {
|
|
40
|
+
fix: {
|
|
41
|
+
description: 'User wants Copilot to fix bugs/blockers from review',
|
|
42
|
+
primary: [
|
|
43
|
+
'fix', 'repair', 'address', 'correct', 'apply.*fix',
|
|
44
|
+
'dispatch.*fix', 'copilot.*fix', 'fix.*blockers', 'fix.*issue'
|
|
45
|
+
],
|
|
46
|
+
context: [
|
|
47
|
+
'it', 'this', 'that', 'those', 'them',
|
|
48
|
+
'the (issues?|bugs?|blockers?|errors?|code|review|problems?)'
|
|
49
|
+
],
|
|
50
|
+
requiresContext: true,
|
|
51
|
+
requiresFeature: true
|
|
52
|
+
},
|
|
53
|
+
review: {
|
|
54
|
+
description: 'User wants a code review',
|
|
55
|
+
primary: [
|
|
56
|
+
'review', 're.?view', 'check.*code', 'run.*review', 'gemini.*review'
|
|
57
|
+
],
|
|
58
|
+
context: [
|
|
59
|
+
'it', 'this', 'that', 'again', 'now',
|
|
60
|
+
'the (code|changes?|impl|feature)', 'once more'
|
|
61
|
+
],
|
|
62
|
+
requiresContext: true,
|
|
63
|
+
requiresFeature: true
|
|
64
|
+
},
|
|
65
|
+
approve: {
|
|
66
|
+
description: 'User wants to approve the feature',
|
|
67
|
+
primary: ['approve', 'approved', 'lgtm', 'looks good', 'good to go', 'ship it', 'merge it'],
|
|
68
|
+
context: [],
|
|
69
|
+
requiresContext: false,
|
|
70
|
+
requiresFeature: true
|
|
71
|
+
},
|
|
72
|
+
deploy: {
|
|
73
|
+
description: 'User wants to deploy',
|
|
74
|
+
primary: ['deploy', 'push to prod', 'go live', 'release it'],
|
|
75
|
+
context: [],
|
|
76
|
+
requiresContext: false,
|
|
77
|
+
requiresFeature: true
|
|
78
|
+
},
|
|
79
|
+
implement: {
|
|
80
|
+
description: 'User wants to start/resume Copilot implementation',
|
|
81
|
+
primary: [
|
|
82
|
+
'implement', 'start copilot', 'run impl', 'run it', 'run the task',
|
|
83
|
+
'run feature', 'trigger impl'
|
|
84
|
+
],
|
|
85
|
+
combo: [
|
|
86
|
+
{ primary: ['restart', 'resume', 'start', 'run', 'trigger'],
|
|
87
|
+
context: ['impl', 'implementat', 'copilot', 'pipeline', 'feature', 'it', 'that', 'tasks'] }
|
|
88
|
+
],
|
|
89
|
+
context: [],
|
|
90
|
+
requiresContext: false,
|
|
91
|
+
requiresFeature: true,
|
|
92
|
+
stageGate: ['arch_complete', 'implementation_failed']
|
|
93
|
+
},
|
|
94
|
+
fix_and_review: {
|
|
95
|
+
description: 'User wants fix then review (combo)',
|
|
96
|
+
primary: [],
|
|
97
|
+
combo: [
|
|
98
|
+
{ primary: ['fix', 'repair', 'address', 'correct', 'copilot.*fix'],
|
|
99
|
+
context: ['review', 're.?view', 'check'] }
|
|
100
|
+
],
|
|
101
|
+
context: [],
|
|
102
|
+
requiresContext: false,
|
|
103
|
+
requiresFeature: true,
|
|
104
|
+
priority: 10 // higher priority — match before individual fix/review
|
|
105
|
+
},
|
|
106
|
+
reset: {
|
|
107
|
+
description: 'User wants to reset pipeline to idle / abandon current feature',
|
|
108
|
+
primary: [
|
|
109
|
+
'reset', 'idle', 'go idle', 'set idle', 'change.*idle', 'move.*idle',
|
|
110
|
+
'stop.*pipeline', 'abandon', 'cancel.*feature', 'clear.*pipeline',
|
|
111
|
+
'start over', 'start fresh', 'clean slate'
|
|
112
|
+
],
|
|
113
|
+
context: [],
|
|
114
|
+
requiresContext: false,
|
|
115
|
+
requiresFeature: false,
|
|
116
|
+
priority: 12
|
|
117
|
+
},
|
|
118
|
+
status: {
|
|
119
|
+
description: 'User wants to check pipeline status',
|
|
120
|
+
primary: [
|
|
121
|
+
'status', 'what.*stage', 'where.*pipeline', 'what.*happening',
|
|
122
|
+
'current.*state', 'pipeline.*state', 'show.*status'
|
|
123
|
+
],
|
|
124
|
+
context: [],
|
|
125
|
+
requiresContext: false,
|
|
126
|
+
requiresFeature: false
|
|
127
|
+
},
|
|
128
|
+
cleanup: {
|
|
129
|
+
description: 'User wants to clean up workspace',
|
|
130
|
+
primary: ['cleanup', 'clean up', 'clean.*workspace', 'tidy up'],
|
|
131
|
+
context: [],
|
|
132
|
+
requiresContext: false,
|
|
133
|
+
requiresFeature: false
|
|
134
|
+
},
|
|
135
|
+
rewrite_docs: {
|
|
136
|
+
description: 'User wants to rework/rewrite spec and/or architecture docs',
|
|
137
|
+
primary: [
|
|
138
|
+
'rework', 'rewrite', 'redo', 'redo.*spec', 'redo.*arch',
|
|
139
|
+
'update.*spec', 'update.*arch', 'revise.*spec', 'revise.*arch',
|
|
140
|
+
'rewrite.*spec', 'rewrite.*arch', 'rework.*spec', 'rework.*arch',
|
|
141
|
+
'refresh.*spec', 'refresh.*arch'
|
|
142
|
+
],
|
|
143
|
+
combo: [
|
|
144
|
+
{ primary: ['rework', 'rewrite', 'redo', 'update', 'revise', 'refresh'],
|
|
145
|
+
context: ['spec', 'arch', 'document', 'docs', 'plan', 'design', 'tasks'] }
|
|
146
|
+
],
|
|
147
|
+
context: [],
|
|
148
|
+
requiresContext: false,
|
|
149
|
+
requiresFeature: false,
|
|
150
|
+
priority: 15 // high priority — catch before generic fix/implement
|
|
151
|
+
},
|
|
152
|
+
payment: {
|
|
153
|
+
description: 'User wants to implement payment functionality',
|
|
154
|
+
primary: [
|
|
155
|
+
'payment', 'stripe', 'payos', 'momo', 'vnpay', 'zalopay',
|
|
156
|
+
'checkout', 'billing', 'subscription', 'invoice', 'transaction',
|
|
157
|
+
'pay', 'purchase', 'buy', 'cart', 'order'
|
|
158
|
+
],
|
|
159
|
+
context: [],
|
|
160
|
+
requiresContext: false,
|
|
161
|
+
skillsToInject: ['skill-payment'],
|
|
162
|
+
},
|
|
163
|
+
auth: {
|
|
164
|
+
description: 'User wants authentication/authorization',
|
|
165
|
+
primary: [
|
|
166
|
+
'login', 'logout', 'auth', 'oauth', 'jwt', 'session', 'token',
|
|
167
|
+
'register', 'signup', 'sign up', 'sign in', 'password', 'forgot password',
|
|
168
|
+
'2fa', 'mfa', 'sso'
|
|
169
|
+
],
|
|
170
|
+
context: [],
|
|
171
|
+
requiresContext: false,
|
|
172
|
+
skillsToInject: [],
|
|
173
|
+
},
|
|
174
|
+
notification: {
|
|
175
|
+
description: 'User wants notification system',
|
|
176
|
+
primary: [
|
|
177
|
+
'email', 'push notification', 'sms', 'notify', 'notification',
|
|
178
|
+
'alert', 'reminder', 'webhook'
|
|
179
|
+
],
|
|
180
|
+
context: [],
|
|
181
|
+
requiresContext: false,
|
|
182
|
+
skillsToInject: [],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// ─── Pattern Loading ─────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
let _cachedPatterns = null;
|
|
189
|
+
let _cacheTime = 0;
|
|
190
|
+
const CACHE_TTL = 30_000; // reload every 30s
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Load intent patterns from disk. Seeds default if file doesn't exist.
|
|
194
|
+
*/
|
|
195
|
+
export function loadPatterns() {
|
|
196
|
+
if (_cachedPatterns && Date.now() - _cacheTime < CACHE_TTL) return _cachedPatterns;
|
|
197
|
+
|
|
198
|
+
const path = getPatternsPath();
|
|
199
|
+
if (!existsSync(path)) {
|
|
200
|
+
// Seed with defaults
|
|
201
|
+
savePatterns(DEFAULT_PATTERNS);
|
|
202
|
+
_cachedPatterns = DEFAULT_PATTERNS;
|
|
203
|
+
_cacheTime = Date.now();
|
|
204
|
+
return DEFAULT_PATTERNS;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const loaded = JSON.parse(readFileSync(path, 'utf8'));
|
|
209
|
+
// Merge new/updated DEFAULT_PATTERNS into disk file.
|
|
210
|
+
// New intents are added; existing intents get their primary array refreshed
|
|
211
|
+
// from defaults (so code-level fixes to patterns take effect).
|
|
212
|
+
let merged = false;
|
|
213
|
+
for (const [key, value] of Object.entries(DEFAULT_PATTERNS)) {
|
|
214
|
+
if (!(key in loaded)) {
|
|
215
|
+
loaded[key] = value;
|
|
216
|
+
merged = true;
|
|
217
|
+
} else if (JSON.stringify(loaded[key].primary) !== JSON.stringify(value.primary)) {
|
|
218
|
+
loaded[key].primary = value.primary;
|
|
219
|
+
merged = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (merged) savePatterns(loaded);
|
|
223
|
+
_cachedPatterns = loaded;
|
|
224
|
+
_cacheTime = Date.now();
|
|
225
|
+
return _cachedPatterns;
|
|
226
|
+
} catch {
|
|
227
|
+
_cachedPatterns = DEFAULT_PATTERNS;
|
|
228
|
+
_cacheTime = Date.now();
|
|
229
|
+
return DEFAULT_PATTERNS;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function savePatterns(patterns) {
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync(getPatternsPath(), JSON.stringify(patterns, null, 2));
|
|
236
|
+
_cachedPatterns = patterns;
|
|
237
|
+
_cacheTime = Date.now();
|
|
238
|
+
} catch { /* non-fatal */ }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Learned Phrases ─────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Load learned phrases — maps raw user text → resolved intent.
|
|
245
|
+
* Structure: { phrases: [{ text, intent, action, count, lastUsed }], ... }
|
|
246
|
+
*/
|
|
247
|
+
function loadLearned() {
|
|
248
|
+
const path = getLearnedPath();
|
|
249
|
+
if (!existsSync(path)) return { phrases: [], stats: { totalLearned: 0, totalMatches: 0 } };
|
|
250
|
+
try {
|
|
251
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
252
|
+
} catch {
|
|
253
|
+
return { phrases: [], stats: { totalLearned: 0, totalMatches: 0 } };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function saveLearned(data) {
|
|
258
|
+
try {
|
|
259
|
+
writeFileSync(getLearnedPath(), JSON.stringify(data, null, 2));
|
|
260
|
+
} catch { /* non-fatal */ }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Record a successful intent match for learning.
|
|
265
|
+
* Next time the user says something similar, we can skip AI entirely.
|
|
266
|
+
*/
|
|
267
|
+
export function learnPhrase(rawText, intent, actionData = {}) {
|
|
268
|
+
const learned = loadLearned();
|
|
269
|
+
const normalized = rawText.toLowerCase().trim();
|
|
270
|
+
|
|
271
|
+
// Check if we already know this phrase
|
|
272
|
+
const existing = learned.phrases.find(p => p.text === normalized);
|
|
273
|
+
if (existing) {
|
|
274
|
+
existing.count = (existing.count || 1) + 1;
|
|
275
|
+
existing.lastUsed = Date.now();
|
|
276
|
+
} else {
|
|
277
|
+
learned.phrases.push({
|
|
278
|
+
text: normalized,
|
|
279
|
+
intent,
|
|
280
|
+
action: actionData,
|
|
281
|
+
count: 1,
|
|
282
|
+
firstSeen: Date.now(),
|
|
283
|
+
lastUsed: Date.now()
|
|
284
|
+
});
|
|
285
|
+
learned.stats.totalLearned = (learned.stats.totalLearned || 0) + 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Keep max 500 phrases — prune least-used ones
|
|
289
|
+
if (learned.phrases.length > 500) {
|
|
290
|
+
learned.phrases.sort((a, b) => (b.count * (b.lastUsed || 0)) - (a.count * (a.lastUsed || 0)));
|
|
291
|
+
learned.phrases = learned.phrases.slice(0, 400);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
saveLearned(learned);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if we've seen this exact (or very similar) phrase before.
|
|
299
|
+
* Returns the intent if found, null otherwise.
|
|
300
|
+
*/
|
|
301
|
+
export function matchLearnedPhrase(rawText) {
|
|
302
|
+
const learned = loadLearned();
|
|
303
|
+
const normalized = rawText.toLowerCase().trim();
|
|
304
|
+
|
|
305
|
+
// Exact match
|
|
306
|
+
const exact = learned.phrases.find(p => p.text === normalized);
|
|
307
|
+
if (exact && exact.count >= 2) {
|
|
308
|
+
// Only trust phrases we've seen at least twice
|
|
309
|
+
const data = loadLearned();
|
|
310
|
+
data.stats.totalMatches = (data.stats.totalMatches || 0) + 1;
|
|
311
|
+
saveLearned(data);
|
|
312
|
+
return { intent: exact.intent, action: exact.action, confidence: 'exact' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Fuzzy match — check if text is 80%+ similar to a known phrase
|
|
316
|
+
for (const phrase of learned.phrases) {
|
|
317
|
+
if (phrase.count < 3) continue; // need at least 3 occurrences for fuzzy
|
|
318
|
+
if (fuzzyMatch(normalized, phrase.text) > 0.8) {
|
|
319
|
+
return { intent: phrase.intent, action: phrase.action, confidence: 'fuzzy' };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Simple similarity score (Dice coefficient on bigrams).
|
|
328
|
+
*/
|
|
329
|
+
function fuzzyMatch(a, b) {
|
|
330
|
+
if (a === b) return 1;
|
|
331
|
+
if (a.length < 2 || b.length < 2) return 0;
|
|
332
|
+
|
|
333
|
+
const bigramsA = new Set();
|
|
334
|
+
for (let i = 0; i < a.length - 1; i++) bigramsA.add(a.slice(i, i + 2));
|
|
335
|
+
|
|
336
|
+
let matches = 0;
|
|
337
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
338
|
+
if (bigramsA.has(b.slice(i, i + 2))) matches++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return (2.0 * matches) / (a.length - 1 + b.length - 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Pattern Matching ────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Match user text against all intent patterns.
|
|
348
|
+
* Returns { intent, confidence } or null.
|
|
349
|
+
*/
|
|
350
|
+
export function matchIntent(text, statusData = {}) {
|
|
351
|
+
const lc = text.toLowerCase();
|
|
352
|
+
const isQuestion = /^(how|what|why|when|where|who|can you explain|could you|would you|should|is it|are you|do you|does it|did|will|shall)\b/.test(lc) || text.endsWith('?');
|
|
353
|
+
|
|
354
|
+
if (isQuestion) return null;
|
|
355
|
+
|
|
356
|
+
const patterns = loadPatterns();
|
|
357
|
+
|
|
358
|
+
// Sort by priority (higher first), default priority = 0
|
|
359
|
+
const intents = Object.entries(patterns)
|
|
360
|
+
.sort(([, a], [, b]) => (b.priority || 0) - (a.priority || 0));
|
|
361
|
+
|
|
362
|
+
for (const [name, pattern] of intents) {
|
|
363
|
+
// Gate: requires active feature?
|
|
364
|
+
if (pattern.requiresFeature && !statusData.current_feature) continue;
|
|
365
|
+
|
|
366
|
+
// Gate: stage restriction?
|
|
367
|
+
if (pattern.stageGate && !pattern.stageGate.includes(statusData.stage)) continue;
|
|
368
|
+
|
|
369
|
+
// Check combo patterns first (they need BOTH sets to match)
|
|
370
|
+
if (pattern.combo?.length) {
|
|
371
|
+
for (const combo of pattern.combo) {
|
|
372
|
+
const primaryMatch = combo.primary.some(p => new RegExp(`\\b${p}\\b`, 'i').test(lc));
|
|
373
|
+
const contextMatch = combo.context.some(c => new RegExp(`\\b${c}\\b`, 'i').test(lc));
|
|
374
|
+
if (primaryMatch && contextMatch) {
|
|
375
|
+
return { intent: name, confidence: 'combo_pattern' };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check primary patterns
|
|
381
|
+
if (pattern.primary?.length) {
|
|
382
|
+
const primaryMatch = pattern.primary.some(p => new RegExp(`\\b${p}\\b`, 'i').test(lc));
|
|
383
|
+
if (!primaryMatch) continue;
|
|
384
|
+
|
|
385
|
+
// If context is required, check context patterns too
|
|
386
|
+
if (pattern.requiresContext && pattern.context?.length) {
|
|
387
|
+
const contextMatch = pattern.context.some(c => new RegExp(`\\b${c}\\b`, 'i').test(lc));
|
|
388
|
+
if (!contextMatch) continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { intent: name, confidence: 'pattern' };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── External System Prompt ──────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Load the externalized system prompt.
|
|
402
|
+
* Falls back to null if file doesn't exist (caller uses inline prompt).
|
|
403
|
+
*/
|
|
404
|
+
export function loadExternalPrompt() {
|
|
405
|
+
const path = getPromptPath();
|
|
406
|
+
if (!existsSync(path)) return null;
|
|
407
|
+
try {
|
|
408
|
+
return readFileSync(path, 'utf8');
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Save/update the externalized system prompt.
|
|
416
|
+
*/
|
|
417
|
+
export function saveExternalPrompt(content) {
|
|
418
|
+
try {
|
|
419
|
+
getIntelDir(); // ensure dir exists
|
|
420
|
+
writeFileSync(getPromptPath(), content);
|
|
421
|
+
return true;
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Add a learned pattern to the prompt's "Learned Shortcuts" section.
|
|
429
|
+
* This enriches the AI's understanding over time.
|
|
430
|
+
*/
|
|
431
|
+
export function appendLearnedContext() {
|
|
432
|
+
const learned = loadLearned();
|
|
433
|
+
if (!learned.phrases.length) return '';
|
|
434
|
+
|
|
435
|
+
// Get top phrases (most used) to add as examples for the AI
|
|
436
|
+
const topPhrases = learned.phrases
|
|
437
|
+
.filter(p => p.count >= 2)
|
|
438
|
+
.sort((a, b) => b.count - a.count)
|
|
439
|
+
.slice(0, 20);
|
|
440
|
+
|
|
441
|
+
if (!topPhrases.length) return '';
|
|
442
|
+
|
|
443
|
+
return '\n\n## Learned User Patterns (from past interactions)\n' +
|
|
444
|
+
'These phrases have been successfully mapped to actions before:\n' +
|
|
445
|
+
topPhrases.map(p => `- "${p.text}" → ${p.intent}`).join('\n') + '\n' +
|
|
446
|
+
'Use these as hints when classifying messages with similar wording.\n';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get intelligence stats for display.
|
|
451
|
+
*/
|
|
452
|
+
export function getIntelStats() {
|
|
453
|
+
const learned = loadLearned();
|
|
454
|
+
const patterns = loadPatterns();
|
|
455
|
+
return {
|
|
456
|
+
totalIntents: Object.keys(patterns).length,
|
|
457
|
+
totalLearnedPhrases: learned.phrases.length,
|
|
458
|
+
totalMatches: learned.stats?.totalMatches || 0,
|
|
459
|
+
topIntents: learned.phrases.reduce((acc, p) => {
|
|
460
|
+
acc[p.intent] = (acc[p.intent] || 0) + p.count;
|
|
461
|
+
return acc;
|
|
462
|
+
}, {})
|
|
463
|
+
};
|
|
464
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Context Protocol (MCP) — Configuration Reader, Formatter & Client.
|
|
3
|
+
*
|
|
4
|
+
* Reads MCP server configuration from aicc.config.js and formats
|
|
5
|
+
* server metadata as context strings for AI prompts.
|
|
6
|
+
* Also provides JSON-RPC stdio transport, server lifecycle management,
|
|
7
|
+
* and tool calling capabilities.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
|
|
15
|
+
/** Example schema showing the expected MCP config structure. */
|
|
16
|
+
export const MCP_SCHEMA = {
|
|
17
|
+
mcp: {
|
|
18
|
+
servers: [
|
|
19
|
+
{
|
|
20
|
+
name: 'example-server',
|
|
21
|
+
url: 'http://localhost:3100',
|
|
22
|
+
transport: 'stdio',
|
|
23
|
+
capabilities: ['tools', 'resources'],
|
|
24
|
+
description: 'Example MCP server',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read MCP server configuration from aicc.config.js.
|
|
32
|
+
* Returns an empty array when no config is found.
|
|
33
|
+
*/
|
|
34
|
+
export async function getMCPConfig() {
|
|
35
|
+
const configPaths = [
|
|
36
|
+
resolve(process.cwd(), 'aicc.config.js'),
|
|
37
|
+
resolve(process.cwd(), 'aicc.config.mjs'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const configPath of configPaths) {
|
|
41
|
+
if (!existsSync(configPath)) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
45
|
+
const mod = await import(configUrl);
|
|
46
|
+
const config = mod.default || mod;
|
|
47
|
+
if (config?.mcp?.servers && Array.isArray(config.mcp.servers)) {
|
|
48
|
+
return config.mcp.servers;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Config exists but failed to load — fall through
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* List configured MCP servers with a basic status indicator.
|
|
60
|
+
*/
|
|
61
|
+
export async function listMCPServers() {
|
|
62
|
+
const servers = await getMCPConfig();
|
|
63
|
+
|
|
64
|
+
return servers.map(server => ({
|
|
65
|
+
name: server.name || 'unnamed',
|
|
66
|
+
url: server.url || null,
|
|
67
|
+
transport: server.transport || 'stdio',
|
|
68
|
+
capabilities: server.capabilities || [],
|
|
69
|
+
description: server.description || '',
|
|
70
|
+
command: server.command || null,
|
|
71
|
+
args: server.args || [],
|
|
72
|
+
env: server.env || {},
|
|
73
|
+
status: 'configured',
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format MCP server info as a context string suitable for AI prompts.
|
|
79
|
+
*/
|
|
80
|
+
export function formatMCPContext(servers) {
|
|
81
|
+
if (!servers || servers.length === 0) {
|
|
82
|
+
return 'No MCP servers configured.';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lines = ['Available MCP Servers:', ''];
|
|
86
|
+
|
|
87
|
+
for (const server of servers) {
|
|
88
|
+
lines.push(`• ${server.name || 'unnamed'}`);
|
|
89
|
+
if (server.url) lines.push(` URL: ${server.url}`);
|
|
90
|
+
if (server.transport) lines.push(` Transport: ${server.transport}`);
|
|
91
|
+
if (server.description) lines.push(` ${server.description}`);
|
|
92
|
+
if (server.capabilities?.length) {
|
|
93
|
+
lines.push(` Capabilities: ${server.capabilities.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── JSON-RPC Stdio Transport & Server Lifecycle ───────────────────────────────
|
|
102
|
+
|
|
103
|
+
const _servers = new Map(); // name → { process, pendingRequests, nextId, ... }
|
|
104
|
+
|
|
105
|
+
export async function startServer(serverConfig) {
|
|
106
|
+
const { name, command, args = [], env = {} } = serverConfig;
|
|
107
|
+
if (_servers.has(name)) return _servers.get(name);
|
|
108
|
+
|
|
109
|
+
const proc = spawn(command, args, {
|
|
110
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
|
+
env: { ...process.env, ...env },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const server = {
|
|
115
|
+
name,
|
|
116
|
+
process: proc,
|
|
117
|
+
pendingRequests: new Map(),
|
|
118
|
+
nextId: 1,
|
|
119
|
+
tools: [],
|
|
120
|
+
ready: false,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let buffer = '';
|
|
124
|
+
proc.stdout.on('data', (data) => {
|
|
125
|
+
buffer += data.toString();
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
buffer = lines.pop(); // keep incomplete line
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (!line.trim()) continue;
|
|
130
|
+
try {
|
|
131
|
+
const msg = JSON.parse(line);
|
|
132
|
+
if (msg.id !== undefined && server.pendingRequests.has(msg.id)) {
|
|
133
|
+
const { resolve, reject, timer } = server.pendingRequests.get(msg.id);
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
server.pendingRequests.delete(msg.id);
|
|
136
|
+
if (msg.error) reject(new Error(msg.error.message || 'MCP error'));
|
|
137
|
+
else resolve(msg.result);
|
|
138
|
+
}
|
|
139
|
+
} catch { /* ignore malformed */ }
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
proc.on('error', (err) => {
|
|
144
|
+
console.error(`[MCP] Server "${name}" error: ${err.message}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
proc.on('exit', (code) => {
|
|
148
|
+
_servers.delete(name);
|
|
149
|
+
for (const [, { reject, timer }] of server.pendingRequests) {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
reject(new Error(`MCP server "${name}" exited with code ${code}`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
_servers.set(name, server);
|
|
156
|
+
|
|
157
|
+
// Send initialize request
|
|
158
|
+
try {
|
|
159
|
+
await _sendRequest(server, 'initialize', {
|
|
160
|
+
protocolVersion: '2024-11-05',
|
|
161
|
+
capabilities: {},
|
|
162
|
+
clientInfo: { name: 'aicc', version: '1.7.0' },
|
|
163
|
+
}, 10000);
|
|
164
|
+
server.ready = true;
|
|
165
|
+
|
|
166
|
+
_sendNotification(server, 'notifications/initialized', {});
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const toolsResult = await _sendRequest(server, 'tools/list', {}, 5000);
|
|
170
|
+
server.tools = toolsResult.tools || [];
|
|
171
|
+
} catch { /* server may not support tools */ }
|
|
172
|
+
} catch (e) {
|
|
173
|
+
proc.kill();
|
|
174
|
+
_servers.delete(name);
|
|
175
|
+
throw new Error(`MCP server "${name}" initialization failed: ${e.message}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return server;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function callTool(serverName, toolName, args = {}, timeout = 30000) {
|
|
182
|
+
const server = _servers.get(serverName);
|
|
183
|
+
if (!server) throw new Error(`MCP server "${serverName}" not started. Call startServer() first.`);
|
|
184
|
+
if (!server.ready) throw new Error(`MCP server "${serverName}" not ready`);
|
|
185
|
+
|
|
186
|
+
return _sendRequest(server, 'tools/call', { name: toolName, arguments: args }, timeout);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function listTools(serverName) {
|
|
190
|
+
const server = _servers.get(serverName);
|
|
191
|
+
if (!server) throw new Error(`MCP server "${serverName}" not started`);
|
|
192
|
+
if (server.tools.length > 0) return server.tools;
|
|
193
|
+
const result = await _sendRequest(server, 'tools/list', {}, 5000);
|
|
194
|
+
server.tools = result.tools || [];
|
|
195
|
+
return server.tools;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function stopServer(serverName) {
|
|
199
|
+
const server = _servers.get(serverName);
|
|
200
|
+
if (!server) return;
|
|
201
|
+
try { server.process.kill(); } catch { /* already dead */ }
|
|
202
|
+
_servers.delete(serverName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function stopAllServers() {
|
|
206
|
+
for (const [name] of _servers) {
|
|
207
|
+
stopServer(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getRunningServers() {
|
|
212
|
+
return Array.from(_servers.entries()).map(([name, s]) => ({
|
|
213
|
+
name, ready: s.ready, tools: s.tools.length, pid: s.process.pid,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _sendRequest(server, method, params, timeout = 30000) {
|
|
218
|
+
return new Promise((resolvePromise, reject) => {
|
|
219
|
+
const id = server.nextId++;
|
|
220
|
+
const timer = setTimeout(() => {
|
|
221
|
+
server.pendingRequests.delete(id);
|
|
222
|
+
reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
|
|
223
|
+
}, timeout);
|
|
224
|
+
server.pendingRequests.set(id, { resolve: resolvePromise, reject, timer });
|
|
225
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
226
|
+
server.process.stdin.write(msg);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _sendNotification(server, method, params) {
|
|
231
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
232
|
+
server.process.stdin.write(msg);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Cleanup on process exit
|
|
236
|
+
process.on('exit', stopAllServers);
|
|
237
|
+
process.on('SIGINT', () => { stopAllServers(); process.exit(0); });
|
|
238
|
+
process.on('SIGTERM', () => { stopAllServers(); process.exit(0); });
|