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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. 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); });