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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logger — immutable, structured event log for all actions.
|
|
3
|
+
*
|
|
4
|
+
* Records every significant action (feature creation, reviews, deploys,
|
|
5
|
+
* AI calls, plugin execution, user commands) in .ai-workflow/audit.jsonl.
|
|
6
|
+
*
|
|
7
|
+
* Each entry is timestamped, typed, and includes contextual metadata.
|
|
8
|
+
* Append-only, never modified — provides a complete audit trail.
|
|
9
|
+
*/
|
|
10
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import { getWorkflowDir } from './pipeline.js';
|
|
13
|
+
|
|
14
|
+
// ─── Event Types ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const AUDIT_EVENTS = {
|
|
17
|
+
// Pipeline actions
|
|
18
|
+
FEATURE_CREATED: 'feature_created',
|
|
19
|
+
REVIEW_STARTED: 'review_started',
|
|
20
|
+
REVIEW_COMPLETED: 'review_completed',
|
|
21
|
+
FEATURE_APPROVED: 'feature_approved',
|
|
22
|
+
FEATURE_REJECTED: 'feature_rejected',
|
|
23
|
+
DEPLOY_STARTED: 'deploy_started',
|
|
24
|
+
DEPLOY_COMPLETED: 'deploy_completed',
|
|
25
|
+
CLEANUP_EXECUTED: 'cleanup_executed',
|
|
26
|
+
PIPELINE_RESET: 'pipeline_reset',
|
|
27
|
+
BUG_FIX_STARTED: 'bug_fix_started',
|
|
28
|
+
|
|
29
|
+
// AI interactions
|
|
30
|
+
AI_CALL_STARTED: 'ai_call_started',
|
|
31
|
+
AI_CALL_COMPLETED: 'ai_call_completed',
|
|
32
|
+
AI_CALL_FAILED: 'ai_call_failed',
|
|
33
|
+
AI_RATE_LIMIT: 'ai_rate_limit',
|
|
34
|
+
AI_MODEL_BANNED: 'ai_model_banned',
|
|
35
|
+
AI_FALLBACK: 'ai_fallback',
|
|
36
|
+
|
|
37
|
+
// User interactions
|
|
38
|
+
USER_COMMAND: 'user_command',
|
|
39
|
+
USER_ASK_AI: 'user_ask_ai',
|
|
40
|
+
AUTOPILOT_TOGGLED: 'autopilot_toggled',
|
|
41
|
+
|
|
42
|
+
// Plugin system
|
|
43
|
+
PLUGIN_LOADED: 'plugin_loaded',
|
|
44
|
+
PLUGIN_EXECUTED: 'plugin_executed',
|
|
45
|
+
PLUGIN_ERROR: 'plugin_error',
|
|
46
|
+
|
|
47
|
+
// System
|
|
48
|
+
SERVICE_STARTED: 'service_started',
|
|
49
|
+
SERVICE_STOPPED: 'service_stopped',
|
|
50
|
+
CONFIG_LOADED: 'config_loaded',
|
|
51
|
+
ERROR: 'error',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ─── Core Functions ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the audit file path.
|
|
58
|
+
*/
|
|
59
|
+
function getAuditFile() {
|
|
60
|
+
const dir = getWorkflowDir();
|
|
61
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
62
|
+
return resolve(dir, 'audit.jsonl');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Write an audit entry.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} event - Event type from AUDIT_EVENTS
|
|
69
|
+
* @param {object} data - Event-specific data
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @param {string} options.source - 'cli' | 'web' | 'telegram' | 'twitch' | 'system'
|
|
72
|
+
* @param {string} options.actor - Who triggered the action (userId, 'autopilot', 'system')
|
|
73
|
+
* @param {string} options.featureId - Associated feature ID
|
|
74
|
+
* @returns {object|null} The audit entry, or null on error
|
|
75
|
+
*/
|
|
76
|
+
export function audit(event, data = {}, options = {}) {
|
|
77
|
+
try {
|
|
78
|
+
const entry = {
|
|
79
|
+
ts: new Date().toISOString(),
|
|
80
|
+
event,
|
|
81
|
+
source: options.source || 'system',
|
|
82
|
+
actor: options.actor || 'system',
|
|
83
|
+
featureId: options.featureId || data.featureId || null,
|
|
84
|
+
data,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const auditFile = getAuditFile();
|
|
88
|
+
appendFileSync(auditFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
89
|
+
|
|
90
|
+
return entry;
|
|
91
|
+
} catch {
|
|
92
|
+
// Non-fatal — never crash the pipeline for audit logging
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read audit entries with optional filters.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} filters
|
|
101
|
+
* @param {string} filters.event - Filter by event type
|
|
102
|
+
* @param {string} filters.source - Filter by source
|
|
103
|
+
* @param {string} filters.actor - Filter by actor
|
|
104
|
+
* @param {string} filters.featureId - Filter by feature
|
|
105
|
+
* @param {number} filters.last - Only last N entries
|
|
106
|
+
* @param {string} filters.since - ISO date string — entries after this time
|
|
107
|
+
* @returns {Array} Parsed audit entries
|
|
108
|
+
*/
|
|
109
|
+
export function getAuditEntries(filters = {}) {
|
|
110
|
+
try {
|
|
111
|
+
const auditFile = getAuditFile();
|
|
112
|
+
if (!existsSync(auditFile)) return [];
|
|
113
|
+
|
|
114
|
+
let entries = readFileSync(auditFile, 'utf8')
|
|
115
|
+
.split('\n')
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map(line => {
|
|
118
|
+
try { return JSON.parse(line); }
|
|
119
|
+
catch { return null; }
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
|
|
123
|
+
if (filters.event) entries = entries.filter(e => e.event === filters.event);
|
|
124
|
+
if (filters.source) entries = entries.filter(e => e.source === filters.source);
|
|
125
|
+
if (filters.actor) entries = entries.filter(e => e.actor === filters.actor);
|
|
126
|
+
if (filters.featureId) entries = entries.filter(e => e.featureId === filters.featureId);
|
|
127
|
+
if (filters.since) entries = entries.filter(e => e.ts >= filters.since);
|
|
128
|
+
if (filters.last) entries = entries.slice(-filters.last);
|
|
129
|
+
|
|
130
|
+
return entries;
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get audit summary — counts by event type, recent activity.
|
|
138
|
+
*
|
|
139
|
+
* @returns {object} Summary with counts and recent entries
|
|
140
|
+
*/
|
|
141
|
+
export function getAuditSummary() {
|
|
142
|
+
const entries = getAuditEntries();
|
|
143
|
+
|
|
144
|
+
const summary = {
|
|
145
|
+
totalEntries: entries.length,
|
|
146
|
+
byEvent: {},
|
|
147
|
+
bySource: {},
|
|
148
|
+
recentEntries: entries.slice(-20),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
summary.byEvent[entry.event] = (summary.byEvent[entry.event] || 0) + 1;
|
|
153
|
+
summary.bySource[entry.source] = (summary.bySource[entry.source] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return summary;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Format audit entries for display.
|
|
161
|
+
*
|
|
162
|
+
* @param {Array} entries - From getAuditEntries()
|
|
163
|
+
* @param {string} format - 'text' | 'html'
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
export function formatAuditEntries(entries, format = 'text') {
|
|
167
|
+
if (!entries || entries.length === 0) {
|
|
168
|
+
return format === 'html'
|
|
169
|
+
? '<i>No audit entries found.</i>'
|
|
170
|
+
: 'No audit entries found.';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (format === 'html') {
|
|
174
|
+
let html = `📋 <b>Audit Log</b> (${entries.length} entries)\n\n`;
|
|
175
|
+
for (const e of entries.slice(-15)) {
|
|
176
|
+
const time = new Date(e.ts).toLocaleTimeString();
|
|
177
|
+
const icon = getEventIcon(e.event);
|
|
178
|
+
html += `${icon} <code>${time}</code> ${e.event}`;
|
|
179
|
+
if (e.featureId) html += ` [${e.featureId}]`;
|
|
180
|
+
if (e.source !== 'system') html += ` via ${e.source}`;
|
|
181
|
+
html += '\n';
|
|
182
|
+
}
|
|
183
|
+
return html;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Plain text
|
|
187
|
+
let text = `\n📋 Audit Log (${entries.length} entries)\n${'─'.repeat(50)}\n`;
|
|
188
|
+
for (const e of entries.slice(-20)) {
|
|
189
|
+
const time = new Date(e.ts).toLocaleTimeString();
|
|
190
|
+
const icon = getEventIcon(e.event);
|
|
191
|
+
text += `${icon} ${time} ${e.event.padEnd(22)} `;
|
|
192
|
+
if (e.featureId) text += `[${e.featureId}] `;
|
|
193
|
+
if (e.source !== 'system') text += `(${e.source})`;
|
|
194
|
+
text += '\n';
|
|
195
|
+
}
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get emoji icon for event type.
|
|
201
|
+
*/
|
|
202
|
+
function getEventIcon(event) {
|
|
203
|
+
const icons = {
|
|
204
|
+
feature_created: '🆕',
|
|
205
|
+
review_started: '🔍',
|
|
206
|
+
review_completed: '✅',
|
|
207
|
+
feature_approved: '👍',
|
|
208
|
+
feature_rejected: '👎',
|
|
209
|
+
deploy_started: '🚀',
|
|
210
|
+
deploy_completed: '🎯',
|
|
211
|
+
cleanup_executed: '🧹',
|
|
212
|
+
pipeline_reset: '🔄',
|
|
213
|
+
bug_fix_started: '🐛',
|
|
214
|
+
ai_call_started: '🤖',
|
|
215
|
+
ai_call_completed: '✨',
|
|
216
|
+
ai_call_failed: '💥',
|
|
217
|
+
ai_rate_limit: '⏳',
|
|
218
|
+
ai_model_banned: '🚫',
|
|
219
|
+
ai_fallback: '🔀',
|
|
220
|
+
user_command: '👤',
|
|
221
|
+
user_ask_ai: '💬',
|
|
222
|
+
autopilot_toggled: '🤖',
|
|
223
|
+
plugin_loaded: '🔌',
|
|
224
|
+
plugin_executed: '⚡',
|
|
225
|
+
plugin_error: '⚠️',
|
|
226
|
+
service_started: '🟢',
|
|
227
|
+
service_stopped: '🔴',
|
|
228
|
+
config_loaded: '⚙️',
|
|
229
|
+
error: '❌',
|
|
230
|
+
};
|
|
231
|
+
return icons[event] || '📌';
|
|
232
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase Context Builder — creates structured context about the project
|
|
3
|
+
* for injection into AI prompts, enabling cross-session memory.
|
|
4
|
+
*
|
|
5
|
+
* Gathers: project tree, package.json summary, recent git commits,
|
|
6
|
+
* key file contents, and workflow state. Returns a structured context
|
|
7
|
+
* object that can be serialized into system prompts.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
11
|
+
import { basename, join, relative, resolve } from 'path';
|
|
12
|
+
import { getRootDir, getWorkflowDir } from './pipeline.js';
|
|
13
|
+
|
|
14
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const DEFAULT_OPTIONS = {
|
|
17
|
+
maxChars: 12000, // Max total context chars (fits in most AI windows)
|
|
18
|
+
maxFileChars: 3000, // Max chars per file content
|
|
19
|
+
maxTreeDepth: 3, // Directory tree depth
|
|
20
|
+
maxCommits: 10, // Recent git commits to include
|
|
21
|
+
includeGitHistory: true,
|
|
22
|
+
includeTree: true,
|
|
23
|
+
includePackageJson: true,
|
|
24
|
+
includeWorkflowState: true,
|
|
25
|
+
includeKeyFiles: true,
|
|
26
|
+
keyFilePatterns: [ // Files to always try to include
|
|
27
|
+
'README.md',
|
|
28
|
+
'package.json',
|
|
29
|
+
'aicc.config.js',
|
|
30
|
+
'.ai-workflow/status.json',
|
|
31
|
+
],
|
|
32
|
+
ignoreDirs: [
|
|
33
|
+
'node_modules', '.git', 'dist', 'build', 'coverage',
|
|
34
|
+
'.next', '.nuxt', '__pycache__', '.venv', 'vendor',
|
|
35
|
+
],
|
|
36
|
+
ignoreFiles: [
|
|
37
|
+
'.DS_Store', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ─── Core Functions ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a comprehensive codebase context.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} rootDir - Project root (defaults to detected root)
|
|
47
|
+
* @param {object} options - Override DEFAULT_OPTIONS
|
|
48
|
+
* @returns {object} Structured context object
|
|
49
|
+
*/
|
|
50
|
+
export function buildCodebaseContext(rootDir = null, options = {}) {
|
|
51
|
+
const root = rootDir || getRootDir();
|
|
52
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
53
|
+
const context = { root, sections: [], charCount: 0 };
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. Project identity
|
|
57
|
+
if (opts.includePackageJson) {
|
|
58
|
+
addPackageInfo(context, root, opts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Directory tree
|
|
62
|
+
if (opts.includeTree) {
|
|
63
|
+
addDirectoryTree(context, root, opts);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Workflow state
|
|
67
|
+
if (opts.includeWorkflowState) {
|
|
68
|
+
addWorkflowState(context, opts);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Git history
|
|
72
|
+
if (opts.includeGitHistory) {
|
|
73
|
+
addGitHistory(context, root, opts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. Key files
|
|
77
|
+
if (opts.includeKeyFiles) {
|
|
78
|
+
addKeyFiles(context, root, opts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return context;
|
|
82
|
+
} catch {
|
|
83
|
+
return context;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serialize context to a string for AI prompt injection.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} context - From buildCodebaseContext()
|
|
91
|
+
* @returns {string} Formatted context string
|
|
92
|
+
*/
|
|
93
|
+
export function serializeContext(context) {
|
|
94
|
+
if (!context || !context.sections || context.sections.length === 0) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let output = '=== CODEBASE CONTEXT ===\n\n';
|
|
99
|
+
|
|
100
|
+
for (const section of context.sections) {
|
|
101
|
+
output += `--- ${section.title} ---\n`;
|
|
102
|
+
output += section.content + '\n\n';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
output += '=== END CODEBASE CONTEXT ===\n';
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build and serialize context in one call — convenience method.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} rootDir
|
|
113
|
+
* @param {object} options
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function getContextString(rootDir = null, options = {}) {
|
|
117
|
+
const context = buildCodebaseContext(rootDir, options);
|
|
118
|
+
return serializeContext(context);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Section Builders ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function addSection(context, title, content, opts) {
|
|
124
|
+
if (context.charCount + content.length > opts.maxChars) {
|
|
125
|
+
// Truncate to fit
|
|
126
|
+
const available = opts.maxChars - context.charCount;
|
|
127
|
+
if (available <= 100) return; // Not enough room
|
|
128
|
+
content = content.slice(0, available - 20) + '\n... [truncated]';
|
|
129
|
+
}
|
|
130
|
+
context.sections.push({ title, content });
|
|
131
|
+
context.charCount += content.length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function addPackageInfo(context, root, opts) {
|
|
135
|
+
const pkgPath = join(root, 'package.json');
|
|
136
|
+
if (!existsSync(pkgPath)) return;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
140
|
+
const info = [
|
|
141
|
+
`Name: ${pkg.name || 'unknown'}`,
|
|
142
|
+
`Version: ${pkg.version || 'unknown'}`,
|
|
143
|
+
pkg.description ? `Description: ${pkg.description}` : null,
|
|
144
|
+
`Type: ${pkg.type || 'commonjs'}`,
|
|
145
|
+
pkg.scripts ? `Scripts: ${Object.keys(pkg.scripts).join(', ')}` : null,
|
|
146
|
+
pkg.dependencies ? `Dependencies: ${Object.keys(pkg.dependencies).join(', ')}` : null,
|
|
147
|
+
pkg.devDependencies ? `DevDependencies: ${Object.keys(pkg.devDependencies).join(', ')}` : null,
|
|
148
|
+
].filter(Boolean).join('\n');
|
|
149
|
+
|
|
150
|
+
addSection(context, 'PROJECT', info, opts);
|
|
151
|
+
} catch { /* skip */ }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function addDirectoryTree(context, root, opts) {
|
|
155
|
+
try {
|
|
156
|
+
const tree = buildTree(root, opts.maxTreeDepth, opts);
|
|
157
|
+
if (tree) addSection(context, 'FILE STRUCTURE', tree, opts);
|
|
158
|
+
} catch { /* skip */ }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildTree(dir, depth, opts, prefix = '') {
|
|
162
|
+
if (depth <= 0) return '';
|
|
163
|
+
|
|
164
|
+
let result = '';
|
|
165
|
+
try {
|
|
166
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
167
|
+
.filter(e => {
|
|
168
|
+
if (opts.ignoreDirs.includes(e.name) && e.isDirectory()) return false;
|
|
169
|
+
if (opts.ignoreFiles.includes(e.name)) return false;
|
|
170
|
+
if (e.name.startsWith('.') && e.name !== '.ai-workflow') return false;
|
|
171
|
+
return true;
|
|
172
|
+
})
|
|
173
|
+
.sort((a, b) => {
|
|
174
|
+
// Dirs first, then files
|
|
175
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
176
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
177
|
+
return a.name.localeCompare(b.name);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < entries.length; i++) {
|
|
181
|
+
const entry = entries[i];
|
|
182
|
+
const isLast = i === entries.length - 1;
|
|
183
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
184
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
185
|
+
|
|
186
|
+
if (entry.isDirectory()) {
|
|
187
|
+
result += `${prefix}${connector}${entry.name}/\n`;
|
|
188
|
+
result += buildTree(join(dir, entry.name), depth - 1, opts, prefix + childPrefix);
|
|
189
|
+
} else {
|
|
190
|
+
result += `${prefix}${connector}${entry.name}\n`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch { /* skip */ }
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function addWorkflowState(context, opts) {
|
|
199
|
+
try {
|
|
200
|
+
const wfDir = getWorkflowDir();
|
|
201
|
+
const statusFile = join(wfDir, 'status.json');
|
|
202
|
+
if (!existsSync(statusFile)) return;
|
|
203
|
+
|
|
204
|
+
const status = JSON.parse(readFileSync(statusFile, 'utf8'));
|
|
205
|
+
const lines = [
|
|
206
|
+
`Current Stage: ${status.currentStage || 'idle'}`,
|
|
207
|
+
status.featureId ? `Feature: ${status.featureId}` : null,
|
|
208
|
+
status.featureName ? `Feature Name: ${status.featureName}` : null,
|
|
209
|
+
status.lastUpdated ? `Last Updated: ${status.lastUpdated}` : null,
|
|
210
|
+
status.autopilot !== undefined ? `Autopilot: ${status.autopilot ? 'ON' : 'OFF'}` : null,
|
|
211
|
+
].filter(Boolean).join('\n');
|
|
212
|
+
|
|
213
|
+
addSection(context, 'WORKFLOW STATE', lines, opts);
|
|
214
|
+
} catch { /* skip */ }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function addGitHistory(context, root, opts) {
|
|
218
|
+
try {
|
|
219
|
+
const log = execSync(
|
|
220
|
+
`git log --oneline --no-decorate -n ${opts.maxCommits}`,
|
|
221
|
+
{ cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
222
|
+
).trim();
|
|
223
|
+
|
|
224
|
+
if (log) {
|
|
225
|
+
// Also get current branch
|
|
226
|
+
let branch = '';
|
|
227
|
+
try {
|
|
228
|
+
branch = execSync('git branch --show-current', {
|
|
229
|
+
cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
230
|
+
}).trim();
|
|
231
|
+
} catch { /* skip */ }
|
|
232
|
+
|
|
233
|
+
let content = branch ? `Branch: ${branch}\n\nRecent commits:\n` : 'Recent commits:\n';
|
|
234
|
+
content += log;
|
|
235
|
+
|
|
236
|
+
addSection(context, 'GIT HISTORY', content, opts);
|
|
237
|
+
}
|
|
238
|
+
} catch { /* skip — not a git repo */ }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function addKeyFiles(context, root, opts) {
|
|
242
|
+
for (const pattern of opts.keyFilePatterns) {
|
|
243
|
+
if (context.charCount >= opts.maxChars) break;
|
|
244
|
+
|
|
245
|
+
const filePath = resolve(root, pattern);
|
|
246
|
+
if (!existsSync(filePath)) continue;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const stat = statSync(filePath);
|
|
250
|
+
if (stat.size > opts.maxFileChars * 4) continue; // Skip very large files
|
|
251
|
+
|
|
252
|
+
let content = readFileSync(filePath, 'utf8');
|
|
253
|
+
if (content.length > opts.maxFileChars) {
|
|
254
|
+
content = content.slice(0, opts.maxFileChars) + '\n... [truncated]';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Use relative path as section title
|
|
258
|
+
const relPath = relative(root, filePath);
|
|
259
|
+
addSection(context, `FILE: ${relPath}`, content, opts);
|
|
260
|
+
} catch { /* skip */ }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get a quick project summary (lightweight version of full context).
|
|
266
|
+
*
|
|
267
|
+
* @param {string} rootDir
|
|
268
|
+
* @returns {string} One-paragraph project description
|
|
269
|
+
*/
|
|
270
|
+
export function getProjectSummary(rootDir = null) {
|
|
271
|
+
const root = rootDir || getRootDir();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const pkgPath = join(root, 'package.json');
|
|
275
|
+
if (!existsSync(pkgPath)) return `Project at ${basename(root)}`;
|
|
276
|
+
|
|
277
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
278
|
+
const parts = [
|
|
279
|
+
pkg.name ? `${pkg.name}` : basename(root),
|
|
280
|
+
pkg.version ? `v${pkg.version}` : null,
|
|
281
|
+
pkg.description ? `— ${pkg.description}` : null,
|
|
282
|
+
].filter(Boolean);
|
|
283
|
+
|
|
284
|
+
return parts.join(' ');
|
|
285
|
+
} catch {
|
|
286
|
+
return `Project at ${basename(root)}`;
|
|
287
|
+
}
|
|
288
|
+
}
|