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,381 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname, relative } from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ const WORKFLOW_DIR = '.ai-workflow';
6
+ const INDEX_FILE = join(WORKFLOW_DIR, 'codebase-index.json');
7
+ const MAX_DEPTH = 4;
8
+ const MAX_FILE_SIZE = 50000; // 50KB max per file for indexing
9
+
10
+ // File extensions to analyze
11
+ const CODE_EXTENSIONS = new Set([
12
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.rs', '.java',
13
+ '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.swift', '.kt',
14
+ '.vue', '.svelte', '.html', '.css', '.scss', '.less',
15
+ '.json', '.yaml', '.yml', '.toml', '.xml',
16
+ '.md', '.mdx', '.txt',
17
+ '.sql', '.graphql', '.proto',
18
+ '.sh', '.bash', '.zsh', '.fish',
19
+ '.dockerfile', '.env',
20
+ ]);
21
+
22
+ const IGNORE_DIRS = new Set([
23
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt',
24
+ 'coverage', '.nyc_output', '__pycache__', '.pytest_cache',
25
+ '.ai-workflow', '.vscode', '.idea', 'vendor', 'target',
26
+ ]);
27
+
28
+ export function getFileTree(dir, depth = 0, maxDepth = MAX_DEPTH) {
29
+ if (depth > maxDepth) return [];
30
+ const entries = [];
31
+ try {
32
+ const items = readdirSync(dir);
33
+ for (const item of items) {
34
+ if (item.startsWith('.') && depth === 0 && item !== '.env.example') continue;
35
+ if (IGNORE_DIRS.has(item)) continue;
36
+
37
+ const fullPath = join(dir, item);
38
+ try {
39
+ const stat = statSync(fullPath);
40
+ if (stat.isDirectory()) {
41
+ const children = getFileTree(fullPath, depth + 1, maxDepth);
42
+ entries.push({ name: item, type: 'dir', children, path: relative(process.cwd(), fullPath) });
43
+ } else if (stat.isFile()) {
44
+ const ext = extname(item).toLowerCase();
45
+ entries.push({
46
+ name: item,
47
+ type: 'file',
48
+ size: stat.size,
49
+ ext,
50
+ path: relative(process.cwd(), fullPath),
51
+ });
52
+ }
53
+ } catch { /* permission error, skip */ }
54
+ }
55
+ } catch { /* directory read error */ }
56
+ return entries;
57
+ }
58
+
59
+ export function readPackageJson(dir) {
60
+ const pkgPath = join(dir, 'package.json');
61
+ if (!existsSync(pkgPath)) return null;
62
+ try {
63
+ return JSON.parse(readFileSync(pkgPath, 'utf8'));
64
+ } catch { return null; }
65
+ }
66
+
67
+ export function getRecentCommits(count = 20) {
68
+ try {
69
+ const log = execSync(`git log --oneline -${count} --no-color 2>/dev/null`, {
70
+ encoding: 'utf8', timeout: 5000,
71
+ });
72
+ return log.trim().split('\n').filter(Boolean);
73
+ } catch { return []; }
74
+ }
75
+
76
+ export function detectEntryPoints(tree) {
77
+ const entryNames = ['index.js', 'index.ts', 'main.js', 'main.ts', 'app.js', 'app.ts',
78
+ 'server.js', 'server.ts', 'cli.js', 'cli.ts'];
79
+ const entries = [];
80
+
81
+ function walk(items, prefix = '') {
82
+ for (const item of items) {
83
+ if (item.type === 'file' && entryNames.includes(item.name)) {
84
+ entries.push(item.path);
85
+ }
86
+ if (item.children) walk(item.children, join(prefix, item.name));
87
+ }
88
+ }
89
+ walk(tree);
90
+ return entries;
91
+ }
92
+
93
+ export function extractDependencies(packageJson) {
94
+ if (!packageJson) return { production: [], dev: [] };
95
+ return {
96
+ production: Object.keys(packageJson.dependencies || {}),
97
+ dev: Object.keys(packageJson.devDependencies || {}),
98
+ };
99
+ }
100
+
101
+ export function detectModules(tree) {
102
+ const modules = [];
103
+ function walk(items) {
104
+ for (const item of items) {
105
+ if (item.type === 'dir' && item.children && item.children.length > 0) {
106
+ const files = item.children.filter(c => c.type === 'file');
107
+ const hasIndex = files.some(f => f.name.startsWith('index.'));
108
+ if (hasIndex || files.length >= 2) {
109
+ modules.push({
110
+ name: item.name,
111
+ path: item.path,
112
+ files: files.map(f => f.name),
113
+ fileCount: files.length,
114
+ });
115
+ }
116
+ walk(item.children);
117
+ }
118
+ }
119
+ }
120
+ walk(tree);
121
+ return modules;
122
+ }
123
+
124
+ export function detectAPIEndpoints(tree) {
125
+ const apiFiles = [];
126
+ function walk(items) {
127
+ for (const item of items) {
128
+ if (item.type === 'file' && (
129
+ item.name.includes('route') || item.name.includes('api') ||
130
+ item.name.includes('controller') || item.name.includes('endpoint')
131
+ )) {
132
+ apiFiles.push(item.path);
133
+ }
134
+ if (item.children) walk(item.children);
135
+ }
136
+ }
137
+ walk(tree);
138
+
139
+ const endpoints = [];
140
+ for (const filePath of apiFiles.slice(0, 10)) {
141
+ try {
142
+ const content = readFileSync(filePath, 'utf8');
143
+ const routePatterns = [
144
+ /\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
145
+ /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
146
+ /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/gi,
147
+ ];
148
+ for (const pattern of routePatterns) {
149
+ let match;
150
+ while ((match = pattern.exec(content)) !== null) {
151
+ endpoints.push({ method: match[1].toUpperCase(), path: match[2], file: filePath });
152
+ }
153
+ }
154
+ } catch { /* skip unreadable */ }
155
+ }
156
+ return endpoints;
157
+ }
158
+
159
+ export function extractFileExports(filePath) {
160
+ try {
161
+ const content = readFileSync(filePath, 'utf8');
162
+ if (content.length > MAX_FILE_SIZE) return [];
163
+ const exports = [];
164
+ const exportPatterns = [
165
+ /export\s+(async\s+)?function\s+(\w+)/g,
166
+ /export\s+(const|let|var)\s+(\w+)/g,
167
+ /export\s+class\s+(\w+)/g,
168
+ /export\s+default\s+(class|function)\s*(\w*)/g,
169
+ ];
170
+ for (const pattern of exportPatterns) {
171
+ let match;
172
+ while ((match = pattern.exec(content)) !== null) {
173
+ const name = match[2] || match[1];
174
+ if (name && name !== 'async') exports.push(name);
175
+ }
176
+ }
177
+ return exports;
178
+ } catch { return []; }
179
+ }
180
+
181
+ export function generateSummary(tree, packageJson) {
182
+ const flatFiles = [];
183
+ function walk(items) {
184
+ for (const item of items) {
185
+ if (item.type === 'file') flatFiles.push(item);
186
+ if (item.children) walk(item.children);
187
+ }
188
+ }
189
+ walk(tree);
190
+
191
+ const byExt = {};
192
+ for (const f of flatFiles) {
193
+ const ext = f.ext || 'other';
194
+ byExt[ext] = (byExt[ext] || 0) + 1;
195
+ }
196
+
197
+ return {
198
+ totalFiles: flatFiles.length,
199
+ totalSize: flatFiles.reduce((s, f) => s + (f.size || 0), 0),
200
+ byExtension: byExt,
201
+ languages: detectLanguages(byExt),
202
+ framework: detectFramework(packageJson),
203
+ };
204
+ }
205
+
206
+ function detectLanguages(byExt) {
207
+ const langs = [];
208
+ if (byExt['.js'] || byExt['.jsx']) langs.push('JavaScript');
209
+ if (byExt['.ts'] || byExt['.tsx']) langs.push('TypeScript');
210
+ if (byExt['.py']) langs.push('Python');
211
+ if (byExt['.go']) langs.push('Go');
212
+ if (byExt['.rs']) langs.push('Rust');
213
+ if (byExt['.java'] || byExt['.kt']) langs.push('Java/Kotlin');
214
+ if (byExt['.rb']) langs.push('Ruby');
215
+ if (byExt['.php']) langs.push('PHP');
216
+ return langs;
217
+ }
218
+
219
+ function detectFramework(pkg) {
220
+ if (!pkg) return 'unknown';
221
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
222
+ if (deps['next']) return 'Next.js';
223
+ if (deps['nuxt']) return 'Nuxt';
224
+ if (deps['@angular/core']) return 'Angular';
225
+ if (deps['react']) return 'React';
226
+ if (deps['vue']) return 'Vue';
227
+ if (deps['svelte']) return 'Svelte';
228
+ if (deps['express']) return 'Express';
229
+ if (deps['fastify']) return 'Fastify';
230
+ if (deps['django']) return 'Django';
231
+ if (deps['flask']) return 'Flask';
232
+ return 'Node.js';
233
+ }
234
+
235
+ export async function indexCodebase(projectDir = process.cwd()) {
236
+ const tree = getFileTree(projectDir);
237
+ const packageJson = readPackageJson(projectDir);
238
+ const gitLog = getRecentCommits(20);
239
+ const entryPoints = detectEntryPoints(tree);
240
+ const dependencies = extractDependencies(packageJson);
241
+ const modules = detectModules(tree);
242
+ const apis = detectAPIEndpoints(tree);
243
+ const summary = generateSummary(tree, packageJson);
244
+
245
+ const index = {
246
+ generatedAt: new Date().toISOString(),
247
+ projectDir: relative(process.cwd(), projectDir) || '.',
248
+ summary,
249
+ tree,
250
+ packageJson: packageJson ? {
251
+ name: packageJson.name,
252
+ version: packageJson.version,
253
+ description: packageJson.description,
254
+ scripts: Object.keys(packageJson.scripts || {}),
255
+ } : null,
256
+ gitLog,
257
+ entryPoints,
258
+ dependencies,
259
+ modules,
260
+ apis,
261
+ };
262
+
263
+ if (!existsSync(WORKFLOW_DIR)) mkdirSync(WORKFLOW_DIR, { recursive: true });
264
+ writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
265
+ return index;
266
+ }
267
+
268
+ export function loadIndex() {
269
+ if (!existsSync(INDEX_FILE)) return null;
270
+ try {
271
+ return JSON.parse(readFileSync(INDEX_FILE, 'utf8'));
272
+ } catch { return null; }
273
+ }
274
+
275
+ export function isIndexStale(maxAgeMs = 3600000) {
276
+ if (!existsSync(INDEX_FILE)) return true;
277
+ try {
278
+ const index = JSON.parse(readFileSync(INDEX_FILE, 'utf8'));
279
+ const age = Date.now() - new Date(index.generatedAt).getTime();
280
+ return age > maxAgeMs;
281
+ } catch { return true; }
282
+ }
283
+
284
+ export function searchIndex(query, index = null) {
285
+ if (!index) index = loadIndex();
286
+ if (!index) return { results: [], message: 'No index found. Run: aicc index' };
287
+
288
+ const q = query.toLowerCase();
289
+ const results = [];
290
+
291
+ function walkTree(items) {
292
+ for (const item of items) {
293
+ if (item.name && item.name.toLowerCase().includes(q)) {
294
+ results.push({ type: 'file', name: item.name, path: item.path });
295
+ }
296
+ if (item.children) walkTree(item.children);
297
+ }
298
+ }
299
+ walkTree(index.tree || []);
300
+
301
+ for (const mod of (index.modules || [])) {
302
+ if (mod.name.toLowerCase().includes(q)) {
303
+ results.push({ type: 'module', name: mod.name, path: mod.path, files: mod.files });
304
+ }
305
+ }
306
+
307
+ for (const api of (index.apis || [])) {
308
+ if (api.path.toLowerCase().includes(q)) {
309
+ results.push({ type: 'api', method: api.method, path: api.path, file: api.file });
310
+ }
311
+ }
312
+
313
+ const allDeps = [...(index.dependencies?.production || []), ...(index.dependencies?.dev || [])];
314
+ for (const dep of allDeps) {
315
+ if (dep.toLowerCase().includes(q)) {
316
+ results.push({ type: 'dependency', name: dep });
317
+ }
318
+ }
319
+
320
+ return { results, total: results.length };
321
+ }
322
+
323
+ export function formatIndexSummary(index) {
324
+ if (!index) return 'No codebase index found. Run: aicc index';
325
+ const s = index.summary || {};
326
+ const lines = [
327
+ `📊 Codebase Index (${index.generatedAt})`,
328
+ ` Project: ${index.packageJson?.name || 'unknown'} v${index.packageJson?.version || '?'}`,
329
+ ` Framework: ${s.framework || 'unknown'}`,
330
+ ` Languages: ${(s.languages || []).join(', ') || 'unknown'}`,
331
+ ` Files: ${s.totalFiles || 0} (${((s.totalSize || 0) / 1024).toFixed(0)} KB)`,
332
+ ` Entry points: ${(index.entryPoints || []).join(', ') || 'none'}`,
333
+ ` Modules: ${(index.modules || []).length}`,
334
+ ` API endpoints: ${(index.apis || []).length}`,
335
+ ` Dependencies: ${(index.dependencies?.production || []).length} prod, ${(index.dependencies?.dev || []).length} dev`,
336
+ ` Recent commits: ${(index.gitLog || []).length}`,
337
+ ];
338
+ return lines.join('\n');
339
+ }
340
+
341
+ export function getCompactContext(index = null) {
342
+ if (!index) index = loadIndex();
343
+ if (!index) return '';
344
+
345
+ const sections = [];
346
+
347
+ if (index.packageJson) {
348
+ sections.push(`Project: ${index.packageJson.name} v${index.packageJson.version}`);
349
+ if (index.packageJson.description) sections.push(`Description: ${index.packageJson.description}`);
350
+ sections.push(`Scripts: ${(index.packageJson.scripts || []).join(', ')}`);
351
+ }
352
+
353
+ function treeToString(items, indent = '') {
354
+ return items.map(item => {
355
+ if (item.type === 'dir') {
356
+ const children = item.children ? treeToString(item.children, indent + ' ') : '';
357
+ return `${indent}${item.name}/\n${children}`;
358
+ }
359
+ return `${indent}${item.name}`;
360
+ }).join('\n');
361
+ }
362
+ sections.push('File Structure:\n' + treeToString(index.tree));
363
+
364
+ if (index.modules?.length > 0) {
365
+ sections.push('Modules: ' + index.modules.map(m => `${m.name} (${m.fileCount} files)`).join(', '));
366
+ }
367
+
368
+ if (index.apis?.length > 0) {
369
+ sections.push('API Endpoints:\n' + index.apis.map(a => ` ${a.method} ${a.path} → ${a.file}`).join('\n'));
370
+ }
371
+
372
+ if (index.dependencies?.production?.length > 0) {
373
+ sections.push('Dependencies: ' + index.dependencies.production.slice(0, 20).join(', '));
374
+ }
375
+
376
+ if (index.gitLog?.length > 0) {
377
+ sections.push('Recent commits:\n' + index.gitLog.slice(0, 10).map(c => ` ${c}`).join('\n'));
378
+ }
379
+
380
+ return sections.join('\n\n');
381
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * JSON Schema validation for aicc.config.js
3
+ */
4
+
5
+ export const CONFIG_SCHEMA = {
6
+ name: { type: 'string', description: 'Project name' },
7
+ sourceDir: { type: 'string', description: 'Source directory' },
8
+ framework: { type: 'string', description: 'Project framework' },
9
+ deployCommand: { type: 'string', description: 'Deploy shell command' },
10
+ testCommand: { type: 'string', description: 'Test shell command' },
11
+ models: {
12
+ type: 'object',
13
+ description: 'AI model configuration',
14
+ properties: {
15
+ gemini: { type: 'string', description: 'Gemini model identifier' },
16
+ claude: { type: 'string', description: 'Claude model identifier' },
17
+ copilot: { type: 'string', description: 'Copilot model identifier' },
18
+ ollama: { type: 'string', description: 'Ollama model identifier' },
19
+ tokenLimits: {
20
+ type: 'object',
21
+ description: 'Token limits per persona',
22
+ properties: {
23
+ pm: { type: 'number', description: 'Token limit for PM persona' },
24
+ explore: { type: 'number', description: 'Token limit for explore persona' },
25
+ arch: { type: 'number', description: 'Token limit for architect persona' },
26
+ impl: { type: 'number', description: 'Token limit for implementation persona' },
27
+ review: { type: 'number', description: 'Token limit for review persona' },
28
+ chat: { type: 'number', description: 'Token limit for chat persona' },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ pipeline: {
34
+ type: 'object',
35
+ description: 'Pipeline configuration',
36
+ properties: {
37
+ stages: { type: 'array', items: 'string', description: 'Pipeline stage names' },
38
+ autoReview: { type: 'boolean', description: 'Enable automatic code review' },
39
+ maxReviewCycles: { type: 'number', description: 'Maximum review iterations' },
40
+ },
41
+ },
42
+ hub: {
43
+ type: 'object',
44
+ description: 'Hub configuration for multi-project management',
45
+ properties: {
46
+ enabled: { type: 'boolean', description: 'Enable hub mode' },
47
+ port: { type: 'number', description: 'Hub server port' },
48
+ pollInterval: { type: 'number', description: 'Polling interval in milliseconds' },
49
+ projects: {
50
+ type: 'array',
51
+ description: 'Hub project entries',
52
+ items: {
53
+ type: 'object',
54
+ properties: {
55
+ name: { type: 'string', description: 'Project name' },
56
+ url: { type: 'string', description: 'Project URL' },
57
+ color: { type: 'string', description: 'Display color' },
58
+ icon: { type: 'string', description: 'Display icon' },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ rbac: {
65
+ type: 'object',
66
+ description: 'Role-based access control',
67
+ properties: {
68
+ users: { type: 'object', description: 'Map of userId to role' },
69
+ apiKeys: { type: 'object', description: 'Map of API key to role' },
70
+ defaultRole: { type: 'string', description: 'Default role for unauthenticated users' },
71
+ },
72
+ },
73
+ webhooks: {
74
+ type: 'array',
75
+ description: 'Webhook configurations',
76
+ items: {
77
+ type: 'object',
78
+ properties: {
79
+ url: { type: 'string', description: 'Webhook endpoint URL' },
80
+ events: { type: 'array', items: 'string', description: 'Events to trigger on' },
81
+ secret: { type: 'string', description: 'Webhook signing secret' },
82
+ },
83
+ },
84
+ },
85
+ mcp: {
86
+ type: 'object',
87
+ description: 'MCP server configuration',
88
+ properties: {
89
+ servers: {
90
+ type: 'array',
91
+ description: 'MCP server entries',
92
+ items: {
93
+ type: 'object',
94
+ properties: {
95
+ name: { type: 'string', description: 'Server name' },
96
+ command: { type: 'string', description: 'Server command' },
97
+ args: { type: 'array', items: 'string', description: 'Command arguments' },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ customQueries: {
104
+ type: 'object',
105
+ description: 'Custom query definitions',
106
+ properties: {},
107
+ additionalProperties: {
108
+ type: 'object',
109
+ properties: {
110
+ label: { type: 'string', description: 'Display label' },
111
+ command: { type: 'string', description: 'Shell command to execute' },
112
+ },
113
+ },
114
+ },
115
+ };
116
+
117
+ function validateValue(value, schema, path) {
118
+ const errors = [];
119
+
120
+ if (schema.type === 'string') {
121
+ if (typeof value !== 'string') {
122
+ errors.push(`Invalid config at '${path}': expected string, got ${typeof value}`);
123
+ }
124
+ } else if (schema.type === 'number') {
125
+ if (typeof value !== 'number') {
126
+ errors.push(`Invalid config at '${path}': expected number, got ${typeof value}`);
127
+ }
128
+ } else if (schema.type === 'boolean') {
129
+ if (typeof value !== 'boolean') {
130
+ errors.push(`Invalid config at '${path}': expected boolean, got ${typeof value}`);
131
+ }
132
+ } else if (schema.type === 'array') {
133
+ if (!Array.isArray(value)) {
134
+ errors.push(`Invalid config at '${path}': expected array, got ${typeof value}`);
135
+ } else if (schema.items) {
136
+ for (let i = 0; i < value.length; i++) {
137
+ if (typeof schema.items === 'string') {
138
+ if (typeof value[i] !== schema.items) {
139
+ errors.push(`Invalid config at '${path}[${i}]': expected ${schema.items}, got ${typeof value[i]}`);
140
+ }
141
+ } else {
142
+ errors.push(...validateValue(value[i], schema.items, `${path}[${i}]`));
143
+ }
144
+ }
145
+ }
146
+ } else if (schema.type === 'object') {
147
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
148
+ errors.push(`Invalid config at '${path}': expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
149
+ } else if (schema.properties) {
150
+ for (const [key, val] of Object.entries(value)) {
151
+ if (schema.properties[key]) {
152
+ errors.push(...validateValue(val, schema.properties[key], `${path}.${key}`));
153
+ } else if (schema.additionalProperties) {
154
+ errors.push(...validateValue(val, schema.additionalProperties, `${path}.${key}`));
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ return errors;
161
+ }
162
+
163
+ export function validateConfig(config) {
164
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) {
165
+ return { valid: false, errors: ['Config must be a plain object'] };
166
+ }
167
+
168
+ const errors = [];
169
+
170
+ for (const [key, value] of Object.entries(config)) {
171
+ const fieldSchema = CONFIG_SCHEMA[key];
172
+ if (!fieldSchema) {
173
+ continue; // allow unknown top-level fields
174
+ }
175
+
176
+ if (fieldSchema.type) {
177
+ errors.push(...validateValue(value, fieldSchema, key));
178
+ } else {
179
+ // top-level simple field
180
+ if (typeof value !== fieldSchema.type) {
181
+ errors.push(`Invalid config at '${key}': expected ${fieldSchema.type}, got ${typeof value}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ return { valid: errors.length === 0, errors };
187
+ }
188
+
189
+ export function getDefaultConfig() {
190
+ return {
191
+ name: 'my-project',
192
+ sourceDir: 'src',
193
+ framework: 'node',
194
+ testCommand: 'npm test',
195
+ deployCommand: 'npm run build',
196
+ models: {
197
+ gemini: 'gemini-2.5-pro',
198
+ claude: 'claude-sonnet-4-6',
199
+ },
200
+ pipeline: {
201
+ stages: ['inbox', 'spec', 'architecture', 'implementation', 'review', 'approved', 'deployed'],
202
+ autoReview: true,
203
+ maxReviewCycles: 3,
204
+ },
205
+ };
206
+ }
207
+
208
+ export function describeField(path) {
209
+ const parts = path.split('.');
210
+ let current = CONFIG_SCHEMA;
211
+
212
+ for (const part of parts) {
213
+ if (!current) return null;
214
+
215
+ if (current.properties && current.properties[part]) {
216
+ current = current.properties[part];
217
+ } else if (current[part]) {
218
+ current = current[part];
219
+ } else {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ if (!current) return null;
225
+
226
+ return {
227
+ type: current.type || 'unknown',
228
+ description: current.description || '',
229
+ };
230
+ }