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,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
+ }