@compilr-dev/cli 0.4.0
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/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- package/package.json +66 -0
package/dist/repl.js
ADDED
|
@@ -0,0 +1,1878 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL (Read-Eval-Print Loop)
|
|
3
|
+
*
|
|
4
|
+
* Main orchestration for the interactive agent session.
|
|
5
|
+
* Coordinates between input, agent execution, and output.
|
|
6
|
+
*
|
|
7
|
+
* Uses event-driven architecture with Footer component for:
|
|
8
|
+
* - Persistent todo list while agent runs
|
|
9
|
+
* - Persistent input prompt (always visible)
|
|
10
|
+
* - Input queueing during agent execution
|
|
11
|
+
* - Esc to cancel running agent
|
|
12
|
+
*/
|
|
13
|
+
import { builtinSkills } from '@compilr-dev/agents';
|
|
14
|
+
import { exec } from 'child_process';
|
|
15
|
+
import { getToolPermissionInfo } from './agent.js';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import pc from 'picocolors';
|
|
19
|
+
import { debug, debugError } from './debug.js';
|
|
20
|
+
import { getNextMode, MODE_INFO } from './ui/types.js';
|
|
21
|
+
import { createInitialState, updateTodos, setAgentRunning } from './state.js';
|
|
22
|
+
import { Footer } from './ui/footer.js';
|
|
23
|
+
import * as conversation from './ui/conversation.js';
|
|
24
|
+
import * as overlays from './ui/overlays.js';
|
|
25
|
+
import * as terminal from './ui/terminal.js';
|
|
26
|
+
import { resolveCommand } from './commands.js';
|
|
27
|
+
import { showAgentsOverlay } from './ui/agents-overlay.js';
|
|
28
|
+
import { showCommandsOverlay } from './ui/commands-overlay.js';
|
|
29
|
+
import { showConfigOverlay } from './ui/config-overlay.js';
|
|
30
|
+
import { showInitOverlay } from './ui/init-overlay.js';
|
|
31
|
+
import { showBacklogOverlay } from './ui/backlog-overlay.js';
|
|
32
|
+
import { showKeysOverlay } from './ui/keys-overlay.js';
|
|
33
|
+
import { showTutorialOverlay } from './ui/tutorial-overlay.js';
|
|
34
|
+
import { showToolsOverlay } from './ui/tools-overlay.js';
|
|
35
|
+
import { showModelWarningOverlay } from './ui/model-warning-overlay.js';
|
|
36
|
+
import { showArchTypeOverlay } from './ui/arch-type-overlay.js';
|
|
37
|
+
import { registerFooterCallbacks } from './ui/overlay-controller.js';
|
|
38
|
+
import { getCustomCommandRegistry } from './commands/index.js';
|
|
39
|
+
import { getPermissionMode } from './settings/index.js';
|
|
40
|
+
import { getModelTier, modelMeetsTier } from './utils/model-tiers.js';
|
|
41
|
+
import { findBacklogPath, parseBacklogItems } from './tools/backlog.js';
|
|
42
|
+
import { isOverlayActive } from './shared-handlers.js';
|
|
43
|
+
import { loadCompilrConfig } from './utils/project-status.js';
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Tool Intent Detection
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Keyword patterns for tool selection
|
|
48
|
+
const TOOL_KEYWORDS = {
|
|
49
|
+
read_file: ['read', 'show', 'display', 'look at', 'view', 'what is in', 'content of'],
|
|
50
|
+
write_file: ['write', 'create', 'save', 'make a file'],
|
|
51
|
+
edit: ['edit', 'modify', 'change', 'update', 'fix', 'replace'],
|
|
52
|
+
bash: ['run', 'execute', 'command', 'shell', 'npm', 'git', 'pip', 'ls', 'pwd', 'mkdir', 'build', 'install'],
|
|
53
|
+
grep: ['search', 'find', 'grep', 'look for', 'where is', 'occurrences'],
|
|
54
|
+
glob: ['list files', 'find files', 'what files', 'show files', 'directory', 'pattern'],
|
|
55
|
+
todo_write: ['todo', 'task', 'plan', 'steps'],
|
|
56
|
+
todo_read: ['todo', 'task', 'plan', 'steps'],
|
|
57
|
+
backlog_read: ['backlog', 'requirements', 'features', 'bugs', 'items'],
|
|
58
|
+
backlog_write: ['backlog', 'requirements', 'features', 'bugs', 'add item', 'add feature'],
|
|
59
|
+
git_status: ['git status', 'changes', 'modified', 'staged'],
|
|
60
|
+
git_diff: ['git diff', 'differences', 'what changed'],
|
|
61
|
+
git_log: ['git log', 'history', 'commits'],
|
|
62
|
+
git_commit: ['commit', 'save changes'],
|
|
63
|
+
git_branch: ['branch', 'branches'],
|
|
64
|
+
detect_project: ['project type', 'what kind of project', 'framework'],
|
|
65
|
+
find_project_root: ['project root', 'root directory'],
|
|
66
|
+
run_tests: ['test', 'tests', 'run tests', 'check tests'],
|
|
67
|
+
run_lint: ['lint', 'linting', 'check code', 'style'],
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Select relevant tool names based on user input intent
|
|
71
|
+
*/
|
|
72
|
+
function selectToolNamesByIntent(input, allToolNames) {
|
|
73
|
+
const lower = input.toLowerCase();
|
|
74
|
+
// If input is very short or a question, use all tools
|
|
75
|
+
if (input.length < 10 || input.includes('?')) {
|
|
76
|
+
return allToolNames;
|
|
77
|
+
}
|
|
78
|
+
const selected = new Set();
|
|
79
|
+
// Add tools based on keyword matches
|
|
80
|
+
for (const [tool, keywords] of Object.entries(TOOL_KEYWORDS)) {
|
|
81
|
+
if (keywords.some((kw) => lower.includes(kw)) && allToolNames.includes(tool)) {
|
|
82
|
+
selected.add(tool);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Always include todo tools (they're used implicitly)
|
|
86
|
+
if (allToolNames.includes('todo_write'))
|
|
87
|
+
selected.add('todo_write');
|
|
88
|
+
if (allToolNames.includes('todo_read'))
|
|
89
|
+
selected.add('todo_read');
|
|
90
|
+
// If nothing matched, use all tools
|
|
91
|
+
if (selected.size === 0) {
|
|
92
|
+
return allToolNames;
|
|
93
|
+
}
|
|
94
|
+
return Array.from(selected);
|
|
95
|
+
}
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Build Command Types and Constants
|
|
98
|
+
// =============================================================================
|
|
99
|
+
const PRIORITY_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Project Detection Helpers
|
|
102
|
+
// =============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Detect if we're in a compilr project (has .compilr folder or -docs sibling)
|
|
105
|
+
* Returns project path from config.json if available
|
|
106
|
+
*/
|
|
107
|
+
function detectCompilrProject() {
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
// First try to load config and get paths from there
|
|
110
|
+
const config = loadCompilrConfig(cwd);
|
|
111
|
+
if (config) {
|
|
112
|
+
return {
|
|
113
|
+
found: true,
|
|
114
|
+
backlogPath: findBacklogPath(cwd),
|
|
115
|
+
projectPath: config.paths.project || null,
|
|
116
|
+
docsPath: config.paths.docs,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Check for .compilr folder in current directory
|
|
120
|
+
const compilrDir = path.join(cwd, '.compilr');
|
|
121
|
+
if (fs.existsSync(compilrDir)) {
|
|
122
|
+
const backlogPath = path.join(compilrDir, 'backlog.md');
|
|
123
|
+
return {
|
|
124
|
+
found: true,
|
|
125
|
+
backlogPath: fs.existsSync(backlogPath) ? backlogPath : null,
|
|
126
|
+
projectPath: cwd,
|
|
127
|
+
docsPath: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Check for -docs sibling folder (two-repo pattern)
|
|
131
|
+
const parentDir = path.dirname(cwd);
|
|
132
|
+
const projectName = path.basename(cwd);
|
|
133
|
+
const docsDir = path.join(parentDir, `${projectName}-docs`);
|
|
134
|
+
const docsBacklogPath = path.join(docsDir, '01-planning', 'backlog.md');
|
|
135
|
+
if (fs.existsSync(docsBacklogPath)) {
|
|
136
|
+
return {
|
|
137
|
+
found: true,
|
|
138
|
+
backlogPath: docsBacklogPath,
|
|
139
|
+
projectPath: cwd,
|
|
140
|
+
docsPath: docsDir,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Check if we're in the docs repo itself
|
|
144
|
+
const inDocsBacklog = path.join(cwd, '01-planning', 'backlog.md');
|
|
145
|
+
if (fs.existsSync(inDocsBacklog)) {
|
|
146
|
+
// We're in -docs folder, try to find sibling code folder
|
|
147
|
+
const baseName = projectName.replace(/-docs$/, '');
|
|
148
|
+
const codeDir = path.join(parentDir, baseName);
|
|
149
|
+
return {
|
|
150
|
+
found: true,
|
|
151
|
+
backlogPath: inDocsBacklog,
|
|
152
|
+
projectPath: fs.existsSync(codeDir) ? codeDir : null,
|
|
153
|
+
docsPath: cwd,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Check for project subfolders (running from parent folder after /init)
|
|
157
|
+
try {
|
|
158
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (!entry.isDirectory())
|
|
161
|
+
continue;
|
|
162
|
+
// Check for .compilr in subfolder (single-repo pattern)
|
|
163
|
+
const subCompilrDir = path.join(cwd, entry.name, '.compilr');
|
|
164
|
+
if (fs.existsSync(subCompilrDir)) {
|
|
165
|
+
const backlogPath = path.join(subCompilrDir, 'backlog.md');
|
|
166
|
+
return {
|
|
167
|
+
found: true,
|
|
168
|
+
backlogPath: fs.existsSync(backlogPath) ? backlogPath : null,
|
|
169
|
+
projectPath: path.join(cwd, entry.name),
|
|
170
|
+
docsPath: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// Check for -docs subfolder (two-repo pattern)
|
|
174
|
+
if (entry.name.endsWith('-docs')) {
|
|
175
|
+
const docsBacklog = path.join(cwd, entry.name, '01-planning', 'backlog.md');
|
|
176
|
+
if (fs.existsSync(docsBacklog)) {
|
|
177
|
+
const baseName = entry.name.replace(/-docs$/, '');
|
|
178
|
+
const codeDir = path.join(cwd, baseName);
|
|
179
|
+
return {
|
|
180
|
+
found: true,
|
|
181
|
+
backlogPath: docsBacklog,
|
|
182
|
+
projectPath: fs.existsSync(codeDir) ? codeDir : null,
|
|
183
|
+
docsPath: path.join(cwd, entry.name),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Ignore read errors
|
|
191
|
+
}
|
|
192
|
+
return { found: false, backlogPath: null, projectPath: null, docsPath: null };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check if backlog has any items using the shared parser
|
|
196
|
+
*/
|
|
197
|
+
function hasBacklogItems(backlogPath) {
|
|
198
|
+
if (!backlogPath || !fs.existsSync(backlogPath)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const content = fs.readFileSync(backlogPath, 'utf-8');
|
|
203
|
+
const items = parseBacklogItems(content);
|
|
204
|
+
return items.length > 0;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get skill prompt by name from builtinSkills
|
|
212
|
+
*/
|
|
213
|
+
function getSkillPrompt(name) {
|
|
214
|
+
const skill = builtinSkills.find((s) => s.name === name);
|
|
215
|
+
return skill?.prompt ?? null;
|
|
216
|
+
}
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Build Command Helpers
|
|
219
|
+
// =============================================================================
|
|
220
|
+
/**
|
|
221
|
+
* Select the best backlog item to build.
|
|
222
|
+
* If requestedId is provided, finds that specific item.
|
|
223
|
+
* Otherwise, auto-picks the highest priority 📋 item.
|
|
224
|
+
*/
|
|
225
|
+
function selectBuildItem(items, requestedId) {
|
|
226
|
+
// If specific ID requested, find it
|
|
227
|
+
if (requestedId && requestedId.toLowerCase() !== 'scaffold') {
|
|
228
|
+
const searchId = requestedId.toUpperCase();
|
|
229
|
+
const item = items.find(i => i.id === searchId);
|
|
230
|
+
if (!item)
|
|
231
|
+
return null;
|
|
232
|
+
if (item.status !== '📋') {
|
|
233
|
+
// Item exists but not in planned status
|
|
234
|
+
return null; // Handler will show appropriate message
|
|
235
|
+
}
|
|
236
|
+
return item;
|
|
237
|
+
}
|
|
238
|
+
// Filter to only 📋 (planned) items
|
|
239
|
+
const planned = items.filter(i => i.status === '📋');
|
|
240
|
+
if (planned.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
// Sort by priority, then by ID
|
|
243
|
+
planned.sort((a, b) => {
|
|
244
|
+
const prioA = PRIORITY_ORDER.indexOf(a.priority.toLowerCase());
|
|
245
|
+
const prioB = PRIORITY_ORDER.indexOf(b.priority.toLowerCase());
|
|
246
|
+
if (prioA !== prioB)
|
|
247
|
+
return prioA - prioB;
|
|
248
|
+
return a.id.localeCompare(b.id); // REQ-001 before REQ-002
|
|
249
|
+
});
|
|
250
|
+
return planned[0];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Find dependency IDs mentioned in item title/description.
|
|
254
|
+
* Looks for patterns like "depends on REQ-001", "blocked by BUG-002", etc.
|
|
255
|
+
*/
|
|
256
|
+
function findDependencies(item) {
|
|
257
|
+
const text = `${item.title} ${item.description}`;
|
|
258
|
+
const patterns = [
|
|
259
|
+
/depends\s+on\s+([A-Z]+-\d+)/gi,
|
|
260
|
+
/blocked\s+by\s+([A-Z]+-\d+)/gi,
|
|
261
|
+
/requires\s+([A-Z]+-\d+)/gi,
|
|
262
|
+
/after\s+([A-Z]+-\d+)/gi,
|
|
263
|
+
/needs\s+([A-Z]+-\d+)/gi,
|
|
264
|
+
];
|
|
265
|
+
const deps = [];
|
|
266
|
+
for (const pattern of patterns) {
|
|
267
|
+
const matches = text.matchAll(pattern);
|
|
268
|
+
for (const match of matches) {
|
|
269
|
+
deps.push(match[1].toUpperCase());
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [...new Set(deps)];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get list of dependencies that are not yet completed.
|
|
276
|
+
*/
|
|
277
|
+
function getUnmetDependencies(item, allItems) {
|
|
278
|
+
const depIds = findDependencies(item);
|
|
279
|
+
return allItems.filter(i => depIds.includes(i.id) && i.status !== '✅');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Check if the project has a code foundation (src/, package.json, etc.)
|
|
283
|
+
*/
|
|
284
|
+
function hasProjectFoundation() {
|
|
285
|
+
const cwd = process.cwd();
|
|
286
|
+
const indicators = [
|
|
287
|
+
'src',
|
|
288
|
+
'lib',
|
|
289
|
+
'app',
|
|
290
|
+
'package.json',
|
|
291
|
+
'requirements.txt',
|
|
292
|
+
'Cargo.toml',
|
|
293
|
+
'go.mod',
|
|
294
|
+
'pom.xml',
|
|
295
|
+
'build.gradle',
|
|
296
|
+
'setup.py',
|
|
297
|
+
'pyproject.toml',
|
|
298
|
+
];
|
|
299
|
+
for (const indicator of indicators) {
|
|
300
|
+
const fullPath = path.join(cwd, indicator);
|
|
301
|
+
if (fs.existsSync(fullPath)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Also check in project subfolders (for two-repo pattern)
|
|
306
|
+
try {
|
|
307
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (!entry.isDirectory())
|
|
310
|
+
continue;
|
|
311
|
+
// Skip docs folder and hidden folders
|
|
312
|
+
if (entry.name.endsWith('-docs') || entry.name.startsWith('.'))
|
|
313
|
+
continue;
|
|
314
|
+
for (const indicator of indicators) {
|
|
315
|
+
const subPath = path.join(cwd, entry.name, indicator);
|
|
316
|
+
if (fs.existsSync(subPath)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Ignore read errors
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// REPL Class
|
|
329
|
+
// =============================================================================
|
|
330
|
+
export class REPL {
|
|
331
|
+
agent;
|
|
332
|
+
model;
|
|
333
|
+
currentModel; // Can be hot-switched
|
|
334
|
+
provider;
|
|
335
|
+
version;
|
|
336
|
+
showFiltering;
|
|
337
|
+
onModeChange;
|
|
338
|
+
onAgentFinish;
|
|
339
|
+
startTime;
|
|
340
|
+
state;
|
|
341
|
+
footer;
|
|
342
|
+
// Session stats
|
|
343
|
+
sessionInputTokens = 0;
|
|
344
|
+
sessionOutputTokens = 0;
|
|
345
|
+
sessionRequests = 0;
|
|
346
|
+
// Running state
|
|
347
|
+
isRunning = false;
|
|
348
|
+
agentRunning = false;
|
|
349
|
+
abortController = null;
|
|
350
|
+
// Promise for waiting on REPL exit
|
|
351
|
+
exitResolve = null;
|
|
352
|
+
constructor(options) {
|
|
353
|
+
this.agent = options.agent;
|
|
354
|
+
this.model = options.model;
|
|
355
|
+
this.currentModel = options.model; // Start with same model, can be hot-switched
|
|
356
|
+
this.provider = options.provider ?? 'unknown';
|
|
357
|
+
this.version = options.version ?? '0.0.1';
|
|
358
|
+
this.showFiltering = options.showFiltering ?? false;
|
|
359
|
+
this.onModeChange = options.onModeChange;
|
|
360
|
+
this.onAgentFinish = options.onAgentFinish;
|
|
361
|
+
this.startTime = new Date();
|
|
362
|
+
this.state = createInitialState(this.model);
|
|
363
|
+
// Map permission mode setting to AgentMode
|
|
364
|
+
const permissionMode = getPermissionMode();
|
|
365
|
+
const initialMode = permissionMode === 'bypass' ? 'auto-accept' :
|
|
366
|
+
permissionMode === 'plan' ? 'plan' : 'normal';
|
|
367
|
+
this.footer = new Footer({
|
|
368
|
+
showSeparators: true,
|
|
369
|
+
initialMode,
|
|
370
|
+
});
|
|
371
|
+
// Notify external state handler of initial mode
|
|
372
|
+
if (this.onModeChange && initialMode !== 'normal') {
|
|
373
|
+
this.onModeChange(initialMode);
|
|
374
|
+
}
|
|
375
|
+
// Set up event handlers
|
|
376
|
+
this.setupEventHandlers();
|
|
377
|
+
// Notify that footer is ready with pause/resume/setSuggestion functions
|
|
378
|
+
if (options.onFooterReady) {
|
|
379
|
+
options.onFooterReady(() => { this.footer.pauseAnimation(); }, () => { this.footer.resumeAnimation(); }, (action) => { this.footer.setSuggestion(action); });
|
|
380
|
+
}
|
|
381
|
+
// Register footer callbacks for overlay controller (used by tools like ask_user)
|
|
382
|
+
registerFooterCallbacks(() => { this.footer.pauseAnimation(); }, () => { this.footer.resumeAnimation(); });
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Set up Footer event handlers
|
|
386
|
+
*/
|
|
387
|
+
setupEventHandlers() {
|
|
388
|
+
// Handle submit - process message or it gets queued by Footer
|
|
389
|
+
this.footer.on('submit', (input) => {
|
|
390
|
+
if (!this.agentRunning && input.trim()) {
|
|
391
|
+
void this.handleSubmit(input);
|
|
392
|
+
}
|
|
393
|
+
// If agent is running, Footer automatically queues the input
|
|
394
|
+
});
|
|
395
|
+
// Handle slash commands
|
|
396
|
+
this.footer.on('command', (command, args) => {
|
|
397
|
+
// Pause animation for overlays
|
|
398
|
+
this.footer.pauseAnimation();
|
|
399
|
+
void this.handleCommand(command, args).then((shouldContinue) => {
|
|
400
|
+
this.footer.resumeAnimation();
|
|
401
|
+
if (!shouldContinue) {
|
|
402
|
+
this.isRunning = false;
|
|
403
|
+
if (this.exitResolve) {
|
|
404
|
+
this.exitResolve();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
// Handle Esc - abort running agent
|
|
410
|
+
this.footer.on('escape', () => {
|
|
411
|
+
if (this.agentRunning && this.abortController) {
|
|
412
|
+
this.abortController.abort();
|
|
413
|
+
conversation.printWarning('Agent execution cancelled');
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
// Handle Ctrl+C - exit REPL
|
|
417
|
+
this.footer.on('cancel', () => {
|
|
418
|
+
this.isRunning = false;
|
|
419
|
+
if (this.exitResolve) {
|
|
420
|
+
this.exitResolve();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
// Handle Shift+Tab - cycle modes
|
|
424
|
+
this.footer.on('modeChange', () => {
|
|
425
|
+
this.cycleMode();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Cycle through agent modes (normal -> auto-accept -> plan -> normal)
|
|
430
|
+
*/
|
|
431
|
+
cycleMode() {
|
|
432
|
+
const currentMode = this.footer.getMode();
|
|
433
|
+
const nextMode = getNextMode(currentMode);
|
|
434
|
+
this.footer.setMode(nextMode);
|
|
435
|
+
// Notify external state handler
|
|
436
|
+
if (this.onModeChange) {
|
|
437
|
+
this.onModeChange(nextMode);
|
|
438
|
+
}
|
|
439
|
+
// Show feedback
|
|
440
|
+
const modeInfo = MODE_INFO[nextMode];
|
|
441
|
+
this.footer.clearForOutput();
|
|
442
|
+
conversation.printInfo(`Mode: ${modeInfo.label} - ${modeInfo.description}`);
|
|
443
|
+
this.footer.forceRender();
|
|
444
|
+
}
|
|
445
|
+
// ===========================================================================
|
|
446
|
+
// Public API
|
|
447
|
+
// ===========================================================================
|
|
448
|
+
/**
|
|
449
|
+
* Start the REPL (event-driven)
|
|
450
|
+
*/
|
|
451
|
+
async run() {
|
|
452
|
+
this.isRunning = true;
|
|
453
|
+
// Set terminal title and clear screen
|
|
454
|
+
terminal.setTitle('compilr');
|
|
455
|
+
terminal.clearScreen();
|
|
456
|
+
conversation.printWelcome(this.model, this.version);
|
|
457
|
+
// Start footer (begins render loop and input capture)
|
|
458
|
+
this.footer.start();
|
|
459
|
+
// Wait for exit signal
|
|
460
|
+
await this.waitForExit();
|
|
461
|
+
// Cleanup
|
|
462
|
+
this.footer.stop();
|
|
463
|
+
terminal.writeLine('');
|
|
464
|
+
conversation.printInfo('Goodbye!');
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Wait for REPL exit
|
|
468
|
+
*/
|
|
469
|
+
waitForExit() {
|
|
470
|
+
return new Promise((resolve) => {
|
|
471
|
+
this.exitResolve = resolve;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get current model
|
|
476
|
+
*/
|
|
477
|
+
getCurrentModel() {
|
|
478
|
+
return this.currentModel;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get current provider
|
|
482
|
+
*/
|
|
483
|
+
getProvider() {
|
|
484
|
+
return this.provider;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Set model for hot-switching (within same provider)
|
|
488
|
+
*/
|
|
489
|
+
setModel(model) {
|
|
490
|
+
this.currentModel = model;
|
|
491
|
+
conversation.printInfo(`Model switched to: ${model}`);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Set the agent mode programmatically (e.g., from permission handler)
|
|
495
|
+
*/
|
|
496
|
+
setMode(mode) {
|
|
497
|
+
this.footer.setMode(mode);
|
|
498
|
+
// Notify external state handler
|
|
499
|
+
if (this.onModeChange) {
|
|
500
|
+
this.onModeChange(mode);
|
|
501
|
+
}
|
|
502
|
+
// Show feedback
|
|
503
|
+
const modeInfo = MODE_INFO[mode];
|
|
504
|
+
this.footer.clearForOutput();
|
|
505
|
+
conversation.printInfo(`Mode: ${modeInfo.label} - ${modeInfo.description}`);
|
|
506
|
+
this.footer.forceRender();
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Stop the REPL
|
|
510
|
+
*/
|
|
511
|
+
stop() {
|
|
512
|
+
this.isRunning = false;
|
|
513
|
+
this.footer.stop();
|
|
514
|
+
if (this.exitResolve) {
|
|
515
|
+
this.exitResolve();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Handle submitted input (processes message and queued inputs)
|
|
520
|
+
*/
|
|
521
|
+
async handleSubmit(input) {
|
|
522
|
+
// Clear any previous suggestion when new input is submitted
|
|
523
|
+
this.footer.setSuggestion(null);
|
|
524
|
+
await this.processMessage(input);
|
|
525
|
+
// Process queued inputs (FIFO)
|
|
526
|
+
while (this.footer.hasQueuedInput() && this.isRunning) {
|
|
527
|
+
const queued = this.footer.popQueuedInput();
|
|
528
|
+
if (queued) {
|
|
529
|
+
await this.processMessage(queued);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// ===========================================================================
|
|
534
|
+
// Command Handling
|
|
535
|
+
// ===========================================================================
|
|
536
|
+
/**
|
|
537
|
+
* Handle slash command
|
|
538
|
+
* Returns false if REPL should exit
|
|
539
|
+
*/
|
|
540
|
+
async handleCommand(command, args) {
|
|
541
|
+
// Resolve aliases to canonical command name
|
|
542
|
+
const resolved = resolveCommand(command);
|
|
543
|
+
if (!resolved) {
|
|
544
|
+
// Check if it's a custom command
|
|
545
|
+
const customRegistry = getCustomCommandRegistry();
|
|
546
|
+
if (customRegistry.has(command)) {
|
|
547
|
+
// Expand custom command and send as message
|
|
548
|
+
const customArgs = args ? args.split(' ').filter(a => a.trim()) : [];
|
|
549
|
+
const expanded = customRegistry.expand(command, customArgs);
|
|
550
|
+
if (expanded) {
|
|
551
|
+
// Resume animation before processing (it was paused for overlay commands)
|
|
552
|
+
this.footer.resumeAnimation();
|
|
553
|
+
// Process as a regular message
|
|
554
|
+
await this.processMessage(expanded);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
conversation.printWarning(`Unknown command: /${command}`);
|
|
559
|
+
conversation.printInfo('Type /help to see available commands');
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
switch (resolved) {
|
|
563
|
+
case 'agents':
|
|
564
|
+
await showAgentsOverlay();
|
|
565
|
+
return true;
|
|
566
|
+
case 'backlog': {
|
|
567
|
+
const backlogResult = await showBacklogOverlay();
|
|
568
|
+
if (backlogResult.modified) {
|
|
569
|
+
conversation.printSuccess('Backlog updated.');
|
|
570
|
+
}
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
case 'commands':
|
|
574
|
+
await showCommandsOverlay();
|
|
575
|
+
return true;
|
|
576
|
+
case 'exit':
|
|
577
|
+
return false;
|
|
578
|
+
case 'help':
|
|
579
|
+
await overlays.showHelp();
|
|
580
|
+
return true;
|
|
581
|
+
case 'init': {
|
|
582
|
+
const initResult = await showInitOverlay();
|
|
583
|
+
if (initResult.created) {
|
|
584
|
+
// Show success message with created files info
|
|
585
|
+
conversation.printSuccess(`Project created: ${initResult.projectPath ?? ''}`);
|
|
586
|
+
if (initResult.docsPath) {
|
|
587
|
+
conversation.printSuccess(`Docs repo created: ${initResult.docsPath}`);
|
|
588
|
+
}
|
|
589
|
+
conversation.printInfo('');
|
|
590
|
+
conversation.printInfo('Next steps:');
|
|
591
|
+
conversation.printInfo(` cd ${initResult.projectPath?.split('/').pop() ?? 'project'}`);
|
|
592
|
+
conversation.printInfo(' npm install');
|
|
593
|
+
conversation.printInfo(' npm run dev');
|
|
594
|
+
conversation.printInfo('');
|
|
595
|
+
// Resume footer animation before processing message
|
|
596
|
+
this.footer.resumeAnimation();
|
|
597
|
+
// Ask agent to read the COMPILR.md and understand the project
|
|
598
|
+
const compilrMdPath = initResult.docsPath
|
|
599
|
+
? `${initResult.docsPath}/COMPILR.md`
|
|
600
|
+
: `${initResult.projectPath ?? '.'}/COMPILR.md`;
|
|
601
|
+
await this.processMessage(`I just created a new project using /init. Please read ${compilrMdPath} to understand the project context and confirm you're ready to help me build it.
|
|
602
|
+
|
|
603
|
+
(Note to user: When you want to define requirements and populate the backlog, type /design)`);
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
case 'keys': {
|
|
608
|
+
const keysResult = await showKeysOverlay();
|
|
609
|
+
if (keysResult.changed) {
|
|
610
|
+
conversation.printSuccess('API keys updated.');
|
|
611
|
+
conversation.printInfo('Restart the CLI to use the new keys.');
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
case 'clear':
|
|
616
|
+
terminal.clearScreen();
|
|
617
|
+
conversation.printWelcome(this.model, this.version);
|
|
618
|
+
return true;
|
|
619
|
+
case 'compact':
|
|
620
|
+
await this.handleCompact();
|
|
621
|
+
return true;
|
|
622
|
+
case 'config': {
|
|
623
|
+
// Get context stats for usage tab
|
|
624
|
+
const contextManager = this.agent.getContextManager();
|
|
625
|
+
const history = this.agent.getHistory();
|
|
626
|
+
let contextUsed = 0;
|
|
627
|
+
let contextMax = 200000;
|
|
628
|
+
if (contextManager) {
|
|
629
|
+
const stats = contextManager.getStats(history.length);
|
|
630
|
+
contextUsed = stats.currentTokens;
|
|
631
|
+
contextMax = stats.maxTokens;
|
|
632
|
+
}
|
|
633
|
+
const configResult = await showConfigOverlay({
|
|
634
|
+
version: this.version,
|
|
635
|
+
cwd: process.cwd(),
|
|
636
|
+
model: this.currentModel,
|
|
637
|
+
provider: this.provider,
|
|
638
|
+
toolCount: this.agent.getToolDefinitions().length,
|
|
639
|
+
startTime: this.startTime,
|
|
640
|
+
// Usage stats
|
|
641
|
+
inputTokens: this.sessionInputTokens,
|
|
642
|
+
outputTokens: this.sessionOutputTokens,
|
|
643
|
+
requests: this.sessionRequests,
|
|
644
|
+
contextUsed,
|
|
645
|
+
contextMax,
|
|
646
|
+
messageCount: history.length,
|
|
647
|
+
// Model change callback for hot-switch
|
|
648
|
+
onModelChange: (model) => {
|
|
649
|
+
this.setModel(model);
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
// Handle model change result (for hot-switch)
|
|
653
|
+
if (configResult.modelChanged) {
|
|
654
|
+
this.currentModel = configResult.modelChanged;
|
|
655
|
+
}
|
|
656
|
+
// Refresh prompt in case theme changed
|
|
657
|
+
this.footer.refreshPrompt();
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
case 'tools':
|
|
661
|
+
await this.showTools();
|
|
662
|
+
return true;
|
|
663
|
+
case 'tokens':
|
|
664
|
+
this.showTokens();
|
|
665
|
+
return true;
|
|
666
|
+
case 'context':
|
|
667
|
+
this.showContext();
|
|
668
|
+
return true;
|
|
669
|
+
case 'export':
|
|
670
|
+
await this.handleExport(args);
|
|
671
|
+
return true;
|
|
672
|
+
case 'model': {
|
|
673
|
+
const modelResult = await showConfigOverlay({
|
|
674
|
+
model: this.currentModel,
|
|
675
|
+
provider: this.provider,
|
|
676
|
+
initialMode: 'model-selector',
|
|
677
|
+
// Don't print in callback - overlay cleanup would clear it
|
|
678
|
+
// We'll print after overlay closes instead
|
|
679
|
+
});
|
|
680
|
+
// Handle model change result
|
|
681
|
+
if (modelResult.modelChanged) {
|
|
682
|
+
this.currentModel = modelResult.modelChanged;
|
|
683
|
+
// Clear footer, print message, re-render footer
|
|
684
|
+
this.footer.clearForOutput();
|
|
685
|
+
conversation.printSuccess(`Model switched to: ${modelResult.modelChanged}`);
|
|
686
|
+
this.footer.forceRender();
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
case 'status':
|
|
691
|
+
this.showStatus();
|
|
692
|
+
return true;
|
|
693
|
+
case 'todos':
|
|
694
|
+
this.showTodos();
|
|
695
|
+
return true;
|
|
696
|
+
case 'plan':
|
|
697
|
+
// Switch to plan mode
|
|
698
|
+
this.footer.setMode('plan');
|
|
699
|
+
if (this.onModeChange) {
|
|
700
|
+
this.onModeChange('plan');
|
|
701
|
+
}
|
|
702
|
+
conversation.printInfo('📋 Switched to Plan mode');
|
|
703
|
+
conversation.printInfo('The agent will create execution plans without performing actions.');
|
|
704
|
+
conversation.printInfo('Use Shift+Tab to switch back to Normal or Auto-accept mode.');
|
|
705
|
+
return true;
|
|
706
|
+
case 'design': {
|
|
707
|
+
// Check if project is initialized
|
|
708
|
+
const designProject = detectCompilrProject();
|
|
709
|
+
if (!designProject.found) {
|
|
710
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
// Check model tier - /design works best with large models
|
|
714
|
+
if (!modelMeetsTier(this.currentModel, '/design')) {
|
|
715
|
+
const modelTier = getModelTier(this.currentModel);
|
|
716
|
+
// Show warning overlay
|
|
717
|
+
const choice = await showModelWarningOverlay({
|
|
718
|
+
command: '/design',
|
|
719
|
+
currentModel: this.currentModel,
|
|
720
|
+
currentTier: modelTier.tier,
|
|
721
|
+
suggestedModel: modelTier.suggestedUpgrade,
|
|
722
|
+
alternativeCommand: '/sketch',
|
|
723
|
+
});
|
|
724
|
+
switch (choice) {
|
|
725
|
+
case 'cancel':
|
|
726
|
+
return true;
|
|
727
|
+
case 'alternative':
|
|
728
|
+
// Run /sketch instead (command name without leading slash)
|
|
729
|
+
return this.handleCommand('sketch', '');
|
|
730
|
+
case 'switch':
|
|
731
|
+
// Hot-switch to the suggested model
|
|
732
|
+
if (modelTier.suggestedUpgrade) {
|
|
733
|
+
this.currentModel = modelTier.suggestedUpgrade;
|
|
734
|
+
conversation.printSuccess(`Switched to ${modelTier.suggestedUpgrade}`);
|
|
735
|
+
conversation.printInfo('');
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
conversation.printWarning('No suggested upgrade available. Continuing with current model.');
|
|
739
|
+
conversation.printInfo('');
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case 'continue':
|
|
743
|
+
// Proceed with current model
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Get design skill prompt
|
|
748
|
+
const designPrompt = getSkillPrompt('design');
|
|
749
|
+
if (!designPrompt) {
|
|
750
|
+
conversation.printError('Design skill not found.');
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
// Note: Plan mode is not yet implemented, so we run in normal mode
|
|
754
|
+
// The skill prompt guides the agent's behavior
|
|
755
|
+
conversation.printInfo('📋 Starting project design...');
|
|
756
|
+
conversation.printInfo('');
|
|
757
|
+
// Resume footer animation before processing message
|
|
758
|
+
this.footer.resumeAnimation();
|
|
759
|
+
// Inject design skill and send initial message
|
|
760
|
+
// Show short message to user, but send full skill prompt to agent
|
|
761
|
+
await this.processMessage(`I want to design my project and create the backlog.
|
|
762
|
+
|
|
763
|
+
${designPrompt}
|
|
764
|
+
|
|
765
|
+
Please start by using todo_write to track the design phases, then begin with Phase 1: Vision. Use the ask_user tool to gather information efficiently.`, {
|
|
766
|
+
displayMessage: 'Start the design process for my project.',
|
|
767
|
+
});
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
case 'sketch': {
|
|
771
|
+
// Check if project is initialized
|
|
772
|
+
const sketchProject = detectCompilrProject();
|
|
773
|
+
if (!sketchProject.found) {
|
|
774
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
// Get sketch skill prompt
|
|
778
|
+
const sketchPrompt = getSkillPrompt('sketch');
|
|
779
|
+
if (!sketchPrompt) {
|
|
780
|
+
conversation.printError('Sketch skill not found.');
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
conversation.printInfo('✏️ Starting quick project sketch...');
|
|
784
|
+
conversation.printInfo('');
|
|
785
|
+
// Resume footer animation before processing message
|
|
786
|
+
this.footer.resumeAnimation();
|
|
787
|
+
// Inject sketch skill and send initial message
|
|
788
|
+
// Uses ask_user_simple for simpler questions (one at a time)
|
|
789
|
+
await this.processMessage(`I want to quickly outline my project.
|
|
790
|
+
|
|
791
|
+
${sketchPrompt}
|
|
792
|
+
|
|
793
|
+
Please start by asking me about the type of application I'm building using the ask_user_simple tool.`, {
|
|
794
|
+
displayMessage: 'Quick project outline.',
|
|
795
|
+
});
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
case 'refine': {
|
|
799
|
+
// Check if project is initialized - use same path detection as /backlog
|
|
800
|
+
const refineBacklogPath = findBacklogPath();
|
|
801
|
+
if (!refineBacklogPath) {
|
|
802
|
+
conversation.printError('No backlog found. Run /init first to create a project.');
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
// Check if backlog has items
|
|
806
|
+
if (!hasBacklogItems(refineBacklogPath)) {
|
|
807
|
+
conversation.printError('No backlog items found. Run /design first to create initial requirements.');
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
// Check if a specific item ID was provided (e.g., /refine REQ-001)
|
|
811
|
+
const itemId = args.trim().toUpperCase();
|
|
812
|
+
const isFocusedRefine = itemId && /^[A-Z]+-\d{3}$/.test(itemId);
|
|
813
|
+
// For full refine mode (no item ID), check model tier and warn
|
|
814
|
+
if (!isFocusedRefine && !modelMeetsTier(this.currentModel, '/refine')) {
|
|
815
|
+
const modelTier = getModelTier(this.currentModel);
|
|
816
|
+
const choice = await showModelWarningOverlay({
|
|
817
|
+
command: '/refine',
|
|
818
|
+
currentModel: this.currentModel,
|
|
819
|
+
currentTier: modelTier.tier,
|
|
820
|
+
suggestedModel: modelTier.suggestedUpgrade,
|
|
821
|
+
alternativeCommand: '/refine <ITEM-ID>',
|
|
822
|
+
});
|
|
823
|
+
switch (choice) {
|
|
824
|
+
case 'cancel':
|
|
825
|
+
return true;
|
|
826
|
+
case 'alternative':
|
|
827
|
+
// Show hint about using specific item ID
|
|
828
|
+
conversation.printInfo('💡 Tip: Use /refine <ITEM-ID> for focused refinement with smaller models.');
|
|
829
|
+
conversation.printInfo(' Example: /refine REQ-001');
|
|
830
|
+
conversation.printInfo('');
|
|
831
|
+
return true;
|
|
832
|
+
case 'switch':
|
|
833
|
+
if (modelTier.suggestedUpgrade) {
|
|
834
|
+
this.currentModel = modelTier.suggestedUpgrade;
|
|
835
|
+
conversation.printSuccess(`Switched to ${this.currentModel}`);
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
case 'continue':
|
|
839
|
+
// Continue with current model
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Get appropriate skill prompt
|
|
844
|
+
const skillName = isFocusedRefine ? 'refine-item' : 'refine';
|
|
845
|
+
const refinePrompt = getSkillPrompt(skillName);
|
|
846
|
+
if (!refinePrompt) {
|
|
847
|
+
conversation.printError(`${skillName} skill not found.`);
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
if (isFocusedRefine) {
|
|
851
|
+
conversation.printInfo(`🔄 Refining item ${itemId}...`);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
conversation.printInfo('🔄 Starting requirements refinement...');
|
|
855
|
+
}
|
|
856
|
+
conversation.printInfo('');
|
|
857
|
+
// Resume footer animation before processing message
|
|
858
|
+
this.footer.resumeAnimation();
|
|
859
|
+
// Build message based on mode
|
|
860
|
+
const userIntent = isFocusedRefine
|
|
861
|
+
? `I want to refine backlog item ${itemId}.`
|
|
862
|
+
: 'I want to refine my project requirements.';
|
|
863
|
+
const agentInstructions = isFocusedRefine
|
|
864
|
+
? `Please use backlog_read with id:"${itemId}" to get the item details, then guide me through refining it.`
|
|
865
|
+
: 'Please start by using backlog_read with limit:10 to get an overview, then ask me what I\'d like to focus on using ask_user_simple.';
|
|
866
|
+
await this.processMessage(`${userIntent}
|
|
867
|
+
|
|
868
|
+
${refinePrompt}
|
|
869
|
+
|
|
870
|
+
${agentInstructions}`, {
|
|
871
|
+
displayMessage: userIntent,
|
|
872
|
+
});
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
case 'arch': {
|
|
876
|
+
// Check if project is initialized
|
|
877
|
+
const archProject = detectCompilrProject();
|
|
878
|
+
if (!archProject.found) {
|
|
879
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
// Check model tier - /arch requires large models
|
|
883
|
+
if (!modelMeetsTier(this.currentModel, '/arch')) {
|
|
884
|
+
const modelTier = getModelTier(this.currentModel);
|
|
885
|
+
const choice = await showModelWarningOverlay({
|
|
886
|
+
command: '/arch',
|
|
887
|
+
currentModel: this.currentModel,
|
|
888
|
+
currentTier: modelTier.tier,
|
|
889
|
+
suggestedModel: modelTier.suggestedUpgrade,
|
|
890
|
+
// No alternative command for /arch - it's inherently complex
|
|
891
|
+
});
|
|
892
|
+
switch (choice) {
|
|
893
|
+
case 'cancel':
|
|
894
|
+
return true;
|
|
895
|
+
case 'switch':
|
|
896
|
+
if (modelTier.suggestedUpgrade) {
|
|
897
|
+
this.currentModel = modelTier.suggestedUpgrade;
|
|
898
|
+
conversation.printSuccess(`Switched to ${modelTier.suggestedUpgrade}`);
|
|
899
|
+
conversation.printInfo('');
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
conversation.printWarning('No suggested upgrade available. Continuing with current model.');
|
|
903
|
+
conversation.printInfo('');
|
|
904
|
+
}
|
|
905
|
+
break;
|
|
906
|
+
case 'continue':
|
|
907
|
+
// Proceed with current model
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// Show type selection overlay
|
|
912
|
+
const archChoice = await showArchTypeOverlay();
|
|
913
|
+
if (!archChoice) {
|
|
914
|
+
// User cancelled
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
// Get architecture skill prompt
|
|
918
|
+
const archPrompt = getSkillPrompt('architecture');
|
|
919
|
+
if (!archPrompt) {
|
|
920
|
+
conversation.printError('Architecture skill not found.');
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
// Build the document type description for display
|
|
924
|
+
const docTypeLabels = {
|
|
925
|
+
'adr': 'Architecture Decision Record',
|
|
926
|
+
'diagram': 'System Diagram',
|
|
927
|
+
'data-model': 'Data Model',
|
|
928
|
+
'api': 'API Design',
|
|
929
|
+
'custom': 'Custom Documentation',
|
|
930
|
+
};
|
|
931
|
+
const displayType = archChoice.customTopic
|
|
932
|
+
? `Custom: ${archChoice.customTopic}`
|
|
933
|
+
: docTypeLabels[archChoice.type];
|
|
934
|
+
conversation.printInfo(`📐 Creating ${displayType}...`);
|
|
935
|
+
conversation.printInfo('');
|
|
936
|
+
// Resume footer animation before processing message
|
|
937
|
+
this.footer.resumeAnimation();
|
|
938
|
+
// Replace placeholders in skill prompt
|
|
939
|
+
const finalPrompt = archPrompt
|
|
940
|
+
.replace('{{doc_type}}', archChoice.type)
|
|
941
|
+
.replace('{{#if custom_topic}}Custom Topic: {{custom_topic}}{{/if}}', archChoice.customTopic ? `Custom Topic: ${archChoice.customTopic}` : '');
|
|
942
|
+
await this.processMessage(`I want to create architecture documentation.
|
|
943
|
+
|
|
944
|
+
${finalPrompt}
|
|
945
|
+
|
|
946
|
+
Please start by reading any existing PRD.md and using backlog_read to understand the project context. Then use ask_user to gather the information needed for this ${archChoice.type} document.`, {
|
|
947
|
+
displayMessage: `Create ${displayType} documentation.`,
|
|
948
|
+
});
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
case 'note': {
|
|
952
|
+
// Check if project is initialized
|
|
953
|
+
const noteProject = detectCompilrProject();
|
|
954
|
+
if (!noteProject.found) {
|
|
955
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
// Get session-notes skill prompt
|
|
959
|
+
const notePrompt = getSkillPrompt('session-notes');
|
|
960
|
+
if (!notePrompt) {
|
|
961
|
+
conversation.printError('Session-notes skill not found.');
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
// Check for optional title argument
|
|
965
|
+
const noteTitle = args.trim();
|
|
966
|
+
conversation.printInfo('📝 Creating session note...');
|
|
967
|
+
conversation.printInfo('');
|
|
968
|
+
// Resume footer animation before processing message
|
|
969
|
+
this.footer.resumeAnimation();
|
|
970
|
+
// Build message based on whether title was provided
|
|
971
|
+
const titleInstruction = noteTitle
|
|
972
|
+
? `The session title is: "${noteTitle}"`
|
|
973
|
+
: 'Please ask me for a title using ask_user_simple, or generate one from the session summary.';
|
|
974
|
+
await this.processMessage(`I want to create a session note capturing what we've done.
|
|
975
|
+
|
|
976
|
+
${notePrompt}
|
|
977
|
+
|
|
978
|
+
${titleInstruction}
|
|
979
|
+
|
|
980
|
+
Review the conversation context to understand what was accomplished, then create the session note file.`, {
|
|
981
|
+
displayMessage: noteTitle
|
|
982
|
+
? `Create session note: "${noteTitle}"`
|
|
983
|
+
: 'Create a session note for this session.',
|
|
984
|
+
});
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
case 'prd': {
|
|
988
|
+
// Check if project is initialized
|
|
989
|
+
const prdProject = detectCompilrProject();
|
|
990
|
+
if (!prdProject.found) {
|
|
991
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
// Get prd skill prompt
|
|
995
|
+
const prdPrompt = getSkillPrompt('prd');
|
|
996
|
+
if (!prdPrompt) {
|
|
997
|
+
conversation.printError('PRD skill not found.');
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
// Check for optional section argument
|
|
1001
|
+
const prdSection = args.trim().toLowerCase();
|
|
1002
|
+
const validSections = ['vision', 'scope', 'technical', 'success'];
|
|
1003
|
+
const sectionInstruction = validSections.includes(prdSection)
|
|
1004
|
+
? `The user wants to update the "${prdSection}" section specifically.`
|
|
1005
|
+
: 'Ask the user which section they want to update using ask_user_simple.';
|
|
1006
|
+
conversation.printInfo('📄 Opening PRD for updates...');
|
|
1007
|
+
conversation.printInfo('');
|
|
1008
|
+
// Resume footer animation before processing message
|
|
1009
|
+
this.footer.resumeAnimation();
|
|
1010
|
+
await this.processMessage(`I want to update the Product Requirements Document.
|
|
1011
|
+
|
|
1012
|
+
${prdPrompt}
|
|
1013
|
+
|
|
1014
|
+
${sectionInstruction}
|
|
1015
|
+
|
|
1016
|
+
Start by reading the existing PRD.md file to understand current state.`, {
|
|
1017
|
+
displayMessage: prdSection
|
|
1018
|
+
? `Update PRD: ${prdSection} section`
|
|
1019
|
+
: 'Update the Product Requirements Document',
|
|
1020
|
+
});
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
case 'build': {
|
|
1024
|
+
const itemId = args.trim() || undefined;
|
|
1025
|
+
// Handle /build scaffold - redirect to scaffold handler
|
|
1026
|
+
if (itemId?.toLowerCase() === 'scaffold') {
|
|
1027
|
+
return this.handleScaffoldCommand();
|
|
1028
|
+
}
|
|
1029
|
+
// Check if project is initialized
|
|
1030
|
+
const buildProject = detectCompilrProject();
|
|
1031
|
+
if (!buildProject.found) {
|
|
1032
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
// Read backlog using the shared path detection
|
|
1036
|
+
const buildBacklogPath = findBacklogPath();
|
|
1037
|
+
if (!buildBacklogPath) {
|
|
1038
|
+
conversation.printError('No backlog found. Run /design or /sketch first.');
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
// Parse backlog items
|
|
1042
|
+
let buildItems = [];
|
|
1043
|
+
try {
|
|
1044
|
+
const backlogContent = fs.readFileSync(buildBacklogPath, 'utf-8');
|
|
1045
|
+
buildItems = parseBacklogItems(backlogContent);
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
conversation.printError('Failed to read backlog file.');
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
if (buildItems.length === 0) {
|
|
1052
|
+
conversation.printError('No backlog items found. Run /design or /sketch first.');
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
// Get project path for context
|
|
1056
|
+
const projectPathForBuild = buildProject.projectPath || process.cwd();
|
|
1057
|
+
const projectPathContext = `**IMPORTANT: Project Directory**
|
|
1058
|
+
All code files MUST be created in: ${projectPathForBuild}
|
|
1059
|
+
Do NOT create files in the current working directory if it differs from the project path.
|
|
1060
|
+
`;
|
|
1061
|
+
// Check foundation
|
|
1062
|
+
const foundationExists = hasProjectFoundation();
|
|
1063
|
+
if (!foundationExists) {
|
|
1064
|
+
// Let agent handle foundation check via skill
|
|
1065
|
+
conversation.printInfo('🔍 Checking project foundation...');
|
|
1066
|
+
conversation.printInfo('');
|
|
1067
|
+
this.footer.resumeAnimation();
|
|
1068
|
+
await this.processMessage(`I want to build a feature from the backlog, but first check if the project has a foundation.
|
|
1069
|
+
|
|
1070
|
+
${projectPathContext}
|
|
1071
|
+
|
|
1072
|
+
Use detect_project to check the current state. If there's no code foundation (no src/, no package.json or equivalent), use ask_user_simple to ask:
|
|
1073
|
+
"No project foundation detected. Would you like to:"
|
|
1074
|
+
- "Create scaffold first (recommended)"
|
|
1075
|
+
- "Proceed with feature anyway"
|
|
1076
|
+
- "Cancel"
|
|
1077
|
+
|
|
1078
|
+
If user chooses scaffold, read COMPILR.md and PRD.md for tech stack info, then create the project scaffold following these guidelines:
|
|
1079
|
+
|
|
1080
|
+
${getSkillPrompt('scaffold') ?? ''}
|
|
1081
|
+
|
|
1082
|
+
After scaffold is done (or if user chose to proceed anyway), continue with building the feature.`, { displayMessage: 'Check project foundation before building.' });
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
// Select item
|
|
1086
|
+
const item = selectBuildItem(buildItems, itemId);
|
|
1087
|
+
if (!item) {
|
|
1088
|
+
if (itemId) {
|
|
1089
|
+
const existingItem = buildItems.find(i => i.id.toUpperCase() === itemId.toUpperCase());
|
|
1090
|
+
if (existingItem) {
|
|
1091
|
+
conversation.printError(`Item ${itemId} is already ${existingItem.status === '✅' ? 'completed' : 'in progress'}.`);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
conversation.printError(`Item ${itemId} not found in backlog.`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
conversation.printInfo('No pending items in backlog. All done! 🎉');
|
|
1099
|
+
}
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
// Check dependencies
|
|
1103
|
+
const unmetDeps = getUnmetDependencies(item, buildItems);
|
|
1104
|
+
const depsWarning = unmetDeps.length > 0
|
|
1105
|
+
? `\n\n**⚠️ UNMET DEPENDENCIES:**\n${unmetDeps.map(d => `- ${d.id}: ${d.title} (${d.status})`).join('\n')}\n\nThese items are not yet completed. Ask user to confirm before proceeding.`
|
|
1106
|
+
: '';
|
|
1107
|
+
// Get build skill prompt and replace placeholders
|
|
1108
|
+
const buildPromptTemplate = getSkillPrompt('build');
|
|
1109
|
+
if (!buildPromptTemplate) {
|
|
1110
|
+
conversation.printError('Build skill not found.');
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
const buildPrompt = buildPromptTemplate
|
|
1114
|
+
.replace(/\{\{item_id\}\}/g, item.id)
|
|
1115
|
+
.replace(/\{\{item_title\}\}/g, item.title)
|
|
1116
|
+
.replace(/\{\{item_description\}\}/g, item.description)
|
|
1117
|
+
.replace(/\{\{item_type\}\}/g, item.type)
|
|
1118
|
+
.replace(/\{\{item_priority\}\}/g, item.priority);
|
|
1119
|
+
conversation.printInfo(`🔨 Building ${item.id}: ${item.title}`);
|
|
1120
|
+
conversation.printInfo('');
|
|
1121
|
+
this.footer.resumeAnimation();
|
|
1122
|
+
await this.processMessage(`I want to implement backlog item ${item.id}.
|
|
1123
|
+
|
|
1124
|
+
${projectPathContext}
|
|
1125
|
+
|
|
1126
|
+
${buildPrompt}${depsWarning}`, { displayMessage: `Build ${item.id}: ${item.title}` });
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
case 'scaffold': {
|
|
1130
|
+
return this.handleScaffoldCommand();
|
|
1131
|
+
}
|
|
1132
|
+
case 'tutorial': {
|
|
1133
|
+
await showTutorialOverlay();
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
default:
|
|
1137
|
+
// This shouldn't happen if resolveCommand works correctly
|
|
1138
|
+
conversation.printWarning(`Unknown command: /${command}`);
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Handle /scaffold command (shared between /scaffold and /build scaffold)
|
|
1144
|
+
*/
|
|
1145
|
+
async handleScaffoldCommand() {
|
|
1146
|
+
// Check if project is initialized
|
|
1147
|
+
const scaffoldProject = detectCompilrProject();
|
|
1148
|
+
if (!scaffoldProject.found) {
|
|
1149
|
+
conversation.printError('No .compilr project found. Run /init first.');
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
// Get project path for context
|
|
1153
|
+
const projectPathForScaffold = scaffoldProject.projectPath || process.cwd();
|
|
1154
|
+
const scaffoldPathContext = `**IMPORTANT: Project Directory**
|
|
1155
|
+
All code files MUST be created in: ${projectPathForScaffold}
|
|
1156
|
+
Do NOT create files in the current working directory if it differs from the project path.
|
|
1157
|
+
`;
|
|
1158
|
+
// Check if foundation already exists
|
|
1159
|
+
const foundationExists = hasProjectFoundation();
|
|
1160
|
+
if (foundationExists) {
|
|
1161
|
+
// Let agent confirm with user
|
|
1162
|
+
conversation.printInfo('🔍 Project files detected...');
|
|
1163
|
+
conversation.printInfo('');
|
|
1164
|
+
this.footer.resumeAnimation();
|
|
1165
|
+
await this.processMessage(`User wants to create a project scaffold, but a foundation may already exist.
|
|
1166
|
+
|
|
1167
|
+
${scaffoldPathContext}
|
|
1168
|
+
|
|
1169
|
+
Use detect_project to analyze the current state. Then use ask_user_simple:
|
|
1170
|
+
"Project files already exist. What would you like to do?"
|
|
1171
|
+
- "Continue anyway (may overwrite)"
|
|
1172
|
+
- "Cancel"
|
|
1173
|
+
|
|
1174
|
+
If continuing, read COMPILR.md and PRD.md for tech stack info, then create the scaffold:
|
|
1175
|
+
|
|
1176
|
+
${getSkillPrompt('scaffold') ?? ''}`, { displayMessage: 'Create project scaffold (foundation exists).' });
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
// Get scaffold skill prompt
|
|
1180
|
+
const scaffoldPrompt = getSkillPrompt('scaffold');
|
|
1181
|
+
if (!scaffoldPrompt) {
|
|
1182
|
+
conversation.printError('Scaffold skill not found.');
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
conversation.printInfo('🏗️ Creating project scaffold...');
|
|
1186
|
+
conversation.printInfo('');
|
|
1187
|
+
this.footer.resumeAnimation();
|
|
1188
|
+
await this.processMessage(`I want to create the project scaffold (foundation).
|
|
1189
|
+
|
|
1190
|
+
${scaffoldPathContext}
|
|
1191
|
+
|
|
1192
|
+
${scaffoldPrompt}
|
|
1193
|
+
|
|
1194
|
+
Read COMPILR.md and PRD.md first to understand the tech stack, then create the appropriate scaffold.`, { displayMessage: 'Create project scaffold.' });
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
// ===========================================================================
|
|
1198
|
+
// Message Processing
|
|
1199
|
+
// ===========================================================================
|
|
1200
|
+
/**
|
|
1201
|
+
* Strip '@' prefix from @path mentions before sending to agent
|
|
1202
|
+
* The '@' is UI syntax for path autocomplete, not part of the actual path
|
|
1203
|
+
* Only strips @ when it's at start of a word (not in middle like email@domain.com)
|
|
1204
|
+
*/
|
|
1205
|
+
stripAtMentions(input) {
|
|
1206
|
+
// Match @ at start of string or after whitespace, followed by non-whitespace
|
|
1207
|
+
return input.replace(/(^|\s)@(\S+)/g, '$1$2');
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Process a message to the agent
|
|
1211
|
+
* @param input - The full message to send to the agent
|
|
1212
|
+
* @param options - Optional settings
|
|
1213
|
+
* @param options.displayMessage - What to show the user (defaults to input)
|
|
1214
|
+
* @param options.skipPlanModeCheck - Skip plan mode handling (for /design, /refine)
|
|
1215
|
+
*/
|
|
1216
|
+
async processMessage(input, options) {
|
|
1217
|
+
// Print user message to conversation (show displayMessage or original)
|
|
1218
|
+
this.footer.clearForOutput();
|
|
1219
|
+
conversation.printUserMessage(options?.displayMessage ?? input);
|
|
1220
|
+
this.footer.forceRender();
|
|
1221
|
+
// Strip @ prefix from path mentions before sending to agent
|
|
1222
|
+
const cleanedInput = this.stripAtMentions(input);
|
|
1223
|
+
// Check current mode (unless skipped for skill-based commands)
|
|
1224
|
+
const mode = this.footer.getMode();
|
|
1225
|
+
// Plan mode - placeholder for now (skip for /design, /refine which handle their own flow)
|
|
1226
|
+
if (mode === 'plan' && !options?.skipPlanModeCheck) {
|
|
1227
|
+
this.footer.clearForOutput();
|
|
1228
|
+
conversation.printInfo('📋 Plan Mode (coming soon)');
|
|
1229
|
+
conversation.printInfo('In plan mode, the agent will analyze your request and create');
|
|
1230
|
+
conversation.printInfo('a detailed execution plan without performing any actions.');
|
|
1231
|
+
conversation.printInfo('');
|
|
1232
|
+
conversation.printInfo('For now, switch to Normal or Auto-accept mode with Shift+Tab.');
|
|
1233
|
+
terminal.writeLine('');
|
|
1234
|
+
this.footer.forceRender();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// Get all tool names and filter based on intent (use cleaned input)
|
|
1238
|
+
const allToolNames = this.agent.getToolDefinitions().map((t) => t.name);
|
|
1239
|
+
const selectedNames = selectToolNamesByIntent(cleanedInput, allToolNames);
|
|
1240
|
+
// Show filtering analysis if enabled
|
|
1241
|
+
if (this.showFiltering) {
|
|
1242
|
+
const saved = allToolNames.length - selectedNames.length;
|
|
1243
|
+
const pct = Math.round((saved / allToolNames.length) * 100);
|
|
1244
|
+
this.footer.clearForOutput();
|
|
1245
|
+
conversation.printInfo(`[Tool Filtering] ${String(selectedNames.length)}/${String(allToolNames.length)} tools, ~${String(pct)}% reduction`);
|
|
1246
|
+
this.footer.forceRender();
|
|
1247
|
+
}
|
|
1248
|
+
// Start agent running state
|
|
1249
|
+
this.agentRunning = true;
|
|
1250
|
+
setAgentRunning(this.state, true);
|
|
1251
|
+
this.footer.setAgentRunning(true);
|
|
1252
|
+
this.abortController = new AbortController();
|
|
1253
|
+
let totalInputTokens = 0;
|
|
1254
|
+
let totalOutputTokens = 0;
|
|
1255
|
+
let llmCalls = 0;
|
|
1256
|
+
let hasTextOutput = false;
|
|
1257
|
+
let usedTools = false;
|
|
1258
|
+
let lastToolInput = null;
|
|
1259
|
+
// Event-based rendering: accumulate text, render complete blocks
|
|
1260
|
+
let textAccumulator = '';
|
|
1261
|
+
debug('processMessage', 'Starting agent stream', {
|
|
1262
|
+
inputLength: cleanedInput.length,
|
|
1263
|
+
toolCount: selectedNames.length,
|
|
1264
|
+
model: this.currentModel,
|
|
1265
|
+
});
|
|
1266
|
+
try {
|
|
1267
|
+
for await (const event of this.agent.stream(cleanedInput, {
|
|
1268
|
+
toolFilter: selectedNames,
|
|
1269
|
+
signal: this.abortController.signal,
|
|
1270
|
+
chatOptions: { model: this.currentModel },
|
|
1271
|
+
})) {
|
|
1272
|
+
debug('processMessage', `Event received: ${event.type}`, event);
|
|
1273
|
+
// Check if aborted
|
|
1274
|
+
if (this.abortController.signal.aborted) {
|
|
1275
|
+
debug('processMessage', 'Aborted');
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
if (event.type === 'llm_chunk') {
|
|
1279
|
+
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
1280
|
+
// Event-based rendering: accumulate text until tool_start or stream end
|
|
1281
|
+
// This allows complete code blocks to be rendered with syntax highlighting
|
|
1282
|
+
textAccumulator += event.chunk.text;
|
|
1283
|
+
hasTextOutput = true;
|
|
1284
|
+
}
|
|
1285
|
+
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
1286
|
+
const tokens = event.chunk.usage.inputTokens + event.chunk.usage.outputTokens;
|
|
1287
|
+
this.footer.addTokens(tokens);
|
|
1288
|
+
totalInputTokens += event.chunk.usage.inputTokens;
|
|
1289
|
+
totalOutputTokens += event.chunk.usage.outputTokens;
|
|
1290
|
+
llmCalls++;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (event.type === 'tool_start') {
|
|
1294
|
+
// Event-based rendering: flush accumulated text before tool output
|
|
1295
|
+
// Exception: For ask_user/ask_user_simple tools, we'll buffer the text and print after overlay closes
|
|
1296
|
+
// to avoid the text interfering with the overlay rendering
|
|
1297
|
+
// Also skip if an overlay is currently active (e.g., permission overlay)
|
|
1298
|
+
const isAskUserTool = event.name === 'ask_user' || event.name === 'ask_user_simple';
|
|
1299
|
+
const shouldBuffer = isAskUserTool || isOverlayActive();
|
|
1300
|
+
if (textAccumulator.trim() && !shouldBuffer) {
|
|
1301
|
+
this.footer.clearForOutput();
|
|
1302
|
+
conversation.printAssistantResponse(textAccumulator.trim());
|
|
1303
|
+
terminal.writeLine('');
|
|
1304
|
+
this.footer.forceRender();
|
|
1305
|
+
textAccumulator = '';
|
|
1306
|
+
}
|
|
1307
|
+
usedTools = true;
|
|
1308
|
+
lastToolInput = event.input;
|
|
1309
|
+
// Show tool in spinner (but not silent tools)
|
|
1310
|
+
if (event.name !== 'todo_read' && event.name !== 'todo_write' && event.name !== 'suggest') {
|
|
1311
|
+
// For task tool, show subagent type
|
|
1312
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1313
|
+
if (event.name === 'task' && event.input) {
|
|
1314
|
+
const inputObj = event.input;
|
|
1315
|
+
const subagentType = typeof inputObj.subagent_type === 'string' ? inputObj.subagent_type : 'general';
|
|
1316
|
+
const description = typeof inputObj.description === 'string' ? inputObj.description : '';
|
|
1317
|
+
this.footer.setCurrentTool(`task(${subagentType}): ${description}`);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
this.footer.setCurrentTool(event.name);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
// Note: Diff is shown in the permission handler (index.ts) BEFORE the prompt
|
|
1324
|
+
}
|
|
1325
|
+
else if (event.type === 'tool_end') {
|
|
1326
|
+
const toolName = event.name;
|
|
1327
|
+
const toolInput = lastToolInput;
|
|
1328
|
+
const result = event.result;
|
|
1329
|
+
lastToolInput = null;
|
|
1330
|
+
// Handle todo_write - update state and render
|
|
1331
|
+
if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
|
|
1332
|
+
const todos = toolInput.todos.map((t) => {
|
|
1333
|
+
const content = typeof t.content === 'string' ? t.content :
|
|
1334
|
+
typeof t.title === 'string' ? t.title :
|
|
1335
|
+
typeof t.task === 'string' ? t.task :
|
|
1336
|
+
typeof t.description === 'string' ? t.description :
|
|
1337
|
+
typeof t.text === 'string' ? t.text : 'Untitled task';
|
|
1338
|
+
const status = (typeof t.status === 'string' && ['pending', 'in_progress', 'completed'].includes(t.status)
|
|
1339
|
+
? t.status
|
|
1340
|
+
: 'pending');
|
|
1341
|
+
const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
|
|
1342
|
+
return { content, status, activeForm };
|
|
1343
|
+
});
|
|
1344
|
+
updateTodos(this.state, todos);
|
|
1345
|
+
this.footer.setTodos(todos);
|
|
1346
|
+
this.footer.setCurrentTool(null);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
// Skip todo_read display
|
|
1350
|
+
if (toolName === 'todo_read') {
|
|
1351
|
+
this.footer.setCurrentTool(null);
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
// Skip suggest tool display (suggestion is shown in input prompt)
|
|
1355
|
+
if (toolName === 'suggest') {
|
|
1356
|
+
this.footer.setCurrentTool(null);
|
|
1357
|
+
this.footer.forceRender();
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
// Handle ask_user/ask_user_simple tools - print buffered text now that overlay is done
|
|
1361
|
+
if (toolName === 'ask_user' || toolName === 'ask_user_simple') {
|
|
1362
|
+
// Print any accumulated text that was buffered before the overlay
|
|
1363
|
+
if (textAccumulator.trim()) {
|
|
1364
|
+
this.footer.clearForOutput();
|
|
1365
|
+
conversation.printAssistantResponse(textAccumulator.trim());
|
|
1366
|
+
terminal.writeLine('');
|
|
1367
|
+
textAccumulator = '';
|
|
1368
|
+
}
|
|
1369
|
+
this.footer.setCurrentTool(null);
|
|
1370
|
+
this.footer.forceRender();
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
// Clear footer for output
|
|
1374
|
+
this.footer.clearForOutput();
|
|
1375
|
+
// Handle edit tool - diff was already shown on tool_start
|
|
1376
|
+
if (toolName === 'edit') {
|
|
1377
|
+
// Just clear the tool indicator, diff was shown before permission prompt
|
|
1378
|
+
this.footer.setCurrentTool(null);
|
|
1379
|
+
this.footer.forceRender();
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
// Handle task tool (subagent) - show subagent info
|
|
1383
|
+
if (toolName === 'task' && toolInput) {
|
|
1384
|
+
const subagentType = typeof toolInput.subagent_type === 'string' ? toolInput.subagent_type : 'general';
|
|
1385
|
+
const description = typeof toolInput.description === 'string' ? toolInput.description : '';
|
|
1386
|
+
// Format result summary for subagent
|
|
1387
|
+
let resultSummary = '';
|
|
1388
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1389
|
+
if (result && typeof result === 'object') {
|
|
1390
|
+
const resultObj = result;
|
|
1391
|
+
if (resultObj.error) {
|
|
1392
|
+
const errorObj = resultObj.error;
|
|
1393
|
+
const errorMsg = typeof errorObj.message === 'string' ? errorObj.message : JSON.stringify(resultObj.error);
|
|
1394
|
+
resultSummary = pc.red(`Error: ${errorMsg.slice(0, 60)}`);
|
|
1395
|
+
}
|
|
1396
|
+
else if (resultObj.result && typeof resultObj.result === 'object') {
|
|
1397
|
+
const r = resultObj.result;
|
|
1398
|
+
const iterations = typeof r.iterations === 'number' ? r.iterations : 1;
|
|
1399
|
+
// Try to get a summary from the result
|
|
1400
|
+
if (typeof r.result === 'string') {
|
|
1401
|
+
const preview = r.result.slice(0, 60);
|
|
1402
|
+
resultSummary = pc.dim(`${String(iterations)} iteration(s) - ${preview}${r.result.length > 60 ? '...' : ''}`);
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
resultSummary = pc.dim(`${String(iterations)} iteration(s)`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
// Print as "task(explore): Search for files" format
|
|
1410
|
+
conversation.printToolExecution(`task(${pc.cyan(subagentType)})`, description, resultSummary);
|
|
1411
|
+
this.footer.setCurrentTool(null);
|
|
1412
|
+
this.footer.forceRender();
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
// Build argument summary for other tools
|
|
1416
|
+
let argSummary = '';
|
|
1417
|
+
if (toolInput) {
|
|
1418
|
+
const path = toolInput.path ?? toolInput.file_path ?? toolInput.filePath;
|
|
1419
|
+
const command = toolInput.command;
|
|
1420
|
+
const pattern = toolInput.pattern;
|
|
1421
|
+
if (typeof path === 'string') {
|
|
1422
|
+
argSummary = path.split('/').pop() ?? path;
|
|
1423
|
+
}
|
|
1424
|
+
else if (typeof command === 'string') {
|
|
1425
|
+
argSummary = command.length > 40 ? command.slice(0, 40) + '...' : command;
|
|
1426
|
+
}
|
|
1427
|
+
else if (typeof pattern === 'string') {
|
|
1428
|
+
argSummary = pattern;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
// Format result summary
|
|
1432
|
+
let resultSummary = '';
|
|
1433
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1434
|
+
if (result && typeof result === 'object') {
|
|
1435
|
+
const resultObj = result;
|
|
1436
|
+
if (resultObj.error) {
|
|
1437
|
+
const errorObj = resultObj.error;
|
|
1438
|
+
const errorMsg = typeof errorObj.message === 'string' ? errorObj.message : JSON.stringify(resultObj.error);
|
|
1439
|
+
resultSummary = pc.red(`Error: ${errorMsg.slice(0, 60)}`);
|
|
1440
|
+
}
|
|
1441
|
+
else if (resultObj.result !== undefined) {
|
|
1442
|
+
resultSummary = conversation.formatToolResult(resultObj.result);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// Print tool log
|
|
1446
|
+
conversation.printToolExecution(toolName, argSummary, resultSummary);
|
|
1447
|
+
// Re-render footer after output
|
|
1448
|
+
this.footer.setCurrentTool(null);
|
|
1449
|
+
this.footer.forceRender();
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
debug('processMessage', 'Stream completed', {
|
|
1453
|
+
hasTextOutput,
|
|
1454
|
+
usedTools,
|
|
1455
|
+
totalInputTokens,
|
|
1456
|
+
totalOutputTokens,
|
|
1457
|
+
llmCalls,
|
|
1458
|
+
textAccumulatorLength: textAccumulator.length,
|
|
1459
|
+
});
|
|
1460
|
+
// Skip stats if aborted
|
|
1461
|
+
if (this.abortController.signal.aborted) {
|
|
1462
|
+
this.agentRunning = false;
|
|
1463
|
+
setAgentRunning(this.state, false);
|
|
1464
|
+
this.footer.setAgentRunning(false);
|
|
1465
|
+
this.abortController = null;
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
// Event-based rendering: flush any remaining accumulated text
|
|
1469
|
+
if (textAccumulator.trim()) {
|
|
1470
|
+
this.footer.clearForOutput();
|
|
1471
|
+
conversation.printAssistantResponse(textAccumulator.trim());
|
|
1472
|
+
terminal.writeLine('');
|
|
1473
|
+
textAccumulator = '';
|
|
1474
|
+
}
|
|
1475
|
+
// Warn if tools used but no text
|
|
1476
|
+
if (usedTools && !hasTextOutput) {
|
|
1477
|
+
this.footer.clearForOutput();
|
|
1478
|
+
conversation.printWarning('No text response from model - try a more capable model');
|
|
1479
|
+
}
|
|
1480
|
+
// Update session stats
|
|
1481
|
+
this.sessionInputTokens += totalInputTokens;
|
|
1482
|
+
this.sessionOutputTokens += totalOutputTokens;
|
|
1483
|
+
this.sessionRequests++;
|
|
1484
|
+
// Show token usage
|
|
1485
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
1486
|
+
this.footer.clearForOutput();
|
|
1487
|
+
const total = totalInputTokens + totalOutputTokens;
|
|
1488
|
+
const sessionTotal = this.sessionInputTokens + this.sessionOutputTokens;
|
|
1489
|
+
conversation.printInfo(`[Tokens: ${total.toLocaleString()} (${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out) - ${String(llmCalls)} LLM call(s)]`);
|
|
1490
|
+
conversation.printInfo(`[Session: ${sessionTotal.toLocaleString()} total (${String(this.sessionRequests)} requests)]`);
|
|
1491
|
+
terminal.writeLine('');
|
|
1492
|
+
}
|
|
1493
|
+
// Stop spinner and re-render footer (AFTER all output is printed)
|
|
1494
|
+
this.agentRunning = false;
|
|
1495
|
+
setAgentRunning(this.state, false);
|
|
1496
|
+
this.footer.setAgentRunning(false);
|
|
1497
|
+
}
|
|
1498
|
+
catch (error) {
|
|
1499
|
+
debugError('processMessage', error);
|
|
1500
|
+
if (error.name !== 'AbortError') {
|
|
1501
|
+
this.footer.clearForOutput();
|
|
1502
|
+
conversation.printError(error.message);
|
|
1503
|
+
this.footer.forceRender();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
// Ensure agent is stopped even on error
|
|
1508
|
+
this.agentRunning = false;
|
|
1509
|
+
setAgentRunning(this.state, false);
|
|
1510
|
+
this.footer.setAgentRunning(false);
|
|
1511
|
+
this.abortController = null;
|
|
1512
|
+
// Notify that agent has finished (for applying deferred suggestions)
|
|
1513
|
+
this.onAgentFinish?.();
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
// ===========================================================================
|
|
1517
|
+
// Helper Commands
|
|
1518
|
+
// ===========================================================================
|
|
1519
|
+
async handleCompact() {
|
|
1520
|
+
const contextManager = this.agent.getContextManager();
|
|
1521
|
+
if (!contextManager) {
|
|
1522
|
+
conversation.printWarning('Context manager not available');
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const history = this.agent.getHistory();
|
|
1526
|
+
if (history.length === 0) {
|
|
1527
|
+
conversation.printInfo('No conversation history to compact');
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const stats = contextManager.getStats(history.length);
|
|
1531
|
+
this.footer.clearForOutput();
|
|
1532
|
+
conversation.printInfo(`Current: ${stats.currentTokens.toLocaleString()} tokens, ${String(history.length)} messages`);
|
|
1533
|
+
const confirmed = await overlays.showConfirmation('Compact conversation history?');
|
|
1534
|
+
if (!confirmed) {
|
|
1535
|
+
this.footer.forceRender();
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// Show spinner during compaction
|
|
1539
|
+
this.footer.setSpinnerText('Compacting context');
|
|
1540
|
+
setAgentRunning(this.state, true);
|
|
1541
|
+
this.footer.setAgentRunning(true);
|
|
1542
|
+
this.footer.forceRender(); // Render immediately before blocking operation
|
|
1543
|
+
try {
|
|
1544
|
+
// Use the agent's compact() method - handles summarization, tool pairing, and hints
|
|
1545
|
+
const result = (await this.agent.compact());
|
|
1546
|
+
// Stop spinner and clear for output
|
|
1547
|
+
setAgentRunning(this.state, false);
|
|
1548
|
+
this.footer.setAgentRunning(false);
|
|
1549
|
+
this.footer.setSpinnerText(null);
|
|
1550
|
+
this.footer.clearForOutput();
|
|
1551
|
+
if (!result.success) {
|
|
1552
|
+
conversation.printWarning('Compaction not available (no context manager)');
|
|
1553
|
+
this.footer.forceRender();
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
// Build status message
|
|
1557
|
+
const newHistory = this.agent.getHistory();
|
|
1558
|
+
const statusParts = [
|
|
1559
|
+
`Compacted: ${String(result.originalTokens ?? 0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} → ${String(result.summaryTokens ?? 0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} tokens`,
|
|
1560
|
+
`(${String(history.length)} → ${String(newHistory.length)} messages, ${String(result.messagesPreserved ?? 0)} preserved)`,
|
|
1561
|
+
];
|
|
1562
|
+
// Add smart compaction category info
|
|
1563
|
+
if (result.categoryStats) {
|
|
1564
|
+
const actions = [];
|
|
1565
|
+
if (result.categoryStats.toolResults.action === 'compacted') {
|
|
1566
|
+
actions.push('tool results → files');
|
|
1567
|
+
}
|
|
1568
|
+
if (result.categoryStats.history.action === 'summarized') {
|
|
1569
|
+
actions.push('history → summary');
|
|
1570
|
+
}
|
|
1571
|
+
if (actions.length > 0) {
|
|
1572
|
+
statusParts.push(`[${actions.join(', ')}]`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Add files created info
|
|
1576
|
+
if (result.filesCreated && result.filesCreated.length > 0) {
|
|
1577
|
+
statusParts.push(`[${String(result.filesCreated.length)} file(s) created]`);
|
|
1578
|
+
}
|
|
1579
|
+
// Add file tracking info
|
|
1580
|
+
if (result.restorationHintsInjected) {
|
|
1581
|
+
const fileTracker = this.agent.getFileTracker();
|
|
1582
|
+
const trackerStats = fileTracker?.getStats();
|
|
1583
|
+
const filesTracked = trackerStats ? trackerStats.total : 0;
|
|
1584
|
+
statusParts.push(`[${String(filesTracked)} files tracked]`);
|
|
1585
|
+
}
|
|
1586
|
+
// Add repair info if tool_results were fixed
|
|
1587
|
+
if (result.toolResultsRepaired && result.toolResultsRepaired > 0) {
|
|
1588
|
+
statusParts.push(`[${String(result.toolResultsRepaired)} orphaned tool results removed]`);
|
|
1589
|
+
}
|
|
1590
|
+
conversation.printSuccess(statusParts.join(' '));
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
// Stop spinner on error too
|
|
1594
|
+
setAgentRunning(this.state, false);
|
|
1595
|
+
this.footer.setAgentRunning(false);
|
|
1596
|
+
this.footer.setSpinnerText(null);
|
|
1597
|
+
this.footer.clearForOutput();
|
|
1598
|
+
conversation.printError(`Compaction failed: ${error.message}`);
|
|
1599
|
+
}
|
|
1600
|
+
this.footer.forceRender();
|
|
1601
|
+
}
|
|
1602
|
+
async showTools() {
|
|
1603
|
+
const tools = this.agent.getToolDefinitions().map((t) => {
|
|
1604
|
+
// Extract parameters from inputSchema
|
|
1605
|
+
const parameters = [];
|
|
1606
|
+
const props = t.inputSchema.properties;
|
|
1607
|
+
const required = t.inputSchema.required ?? [];
|
|
1608
|
+
for (const [name, schema] of Object.entries(props)) {
|
|
1609
|
+
parameters.push({
|
|
1610
|
+
name,
|
|
1611
|
+
type: schema.type ?? 'unknown',
|
|
1612
|
+
description: schema.description,
|
|
1613
|
+
required: required.includes(name),
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
// Look up permission info
|
|
1617
|
+
const permissionInfo = getToolPermissionInfo(t.name);
|
|
1618
|
+
const permission = permissionInfo
|
|
1619
|
+
? { level: permissionInfo.level, description: permissionInfo.description }
|
|
1620
|
+
: undefined;
|
|
1621
|
+
// For task tool, extract just the intro (before verbose agent types list)
|
|
1622
|
+
let description = t.description;
|
|
1623
|
+
if (t.name === 'task' && description.includes('Available agent types:')) {
|
|
1624
|
+
description = description.split('Available agent types:')[0].trim();
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
name: t.name,
|
|
1628
|
+
description,
|
|
1629
|
+
parameters,
|
|
1630
|
+
permission,
|
|
1631
|
+
};
|
|
1632
|
+
});
|
|
1633
|
+
// Pause footer during overlay
|
|
1634
|
+
this.footer.pauseAnimation();
|
|
1635
|
+
try {
|
|
1636
|
+
await showToolsOverlay(tools);
|
|
1637
|
+
}
|
|
1638
|
+
finally {
|
|
1639
|
+
this.footer.resumeAnimation();
|
|
1640
|
+
this.footer.forceRender();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
showTokens() {
|
|
1644
|
+
this.footer.clearForOutput();
|
|
1645
|
+
overlays.showTokenUsage({
|
|
1646
|
+
inputTokens: this.sessionInputTokens,
|
|
1647
|
+
outputTokens: this.sessionOutputTokens,
|
|
1648
|
+
totalTokens: this.sessionInputTokens + this.sessionOutputTokens,
|
|
1649
|
+
});
|
|
1650
|
+
this.footer.forceRender();
|
|
1651
|
+
}
|
|
1652
|
+
showContext() {
|
|
1653
|
+
const contextManager = this.agent.getContextManager();
|
|
1654
|
+
const history = this.agent.getHistory();
|
|
1655
|
+
this.footer.clearForOutput();
|
|
1656
|
+
if (contextManager) {
|
|
1657
|
+
const stats = contextManager.getStats(history.length);
|
|
1658
|
+
overlays.showContextStats({
|
|
1659
|
+
tokens: stats.currentTokens,
|
|
1660
|
+
maxTokens: stats.maxTokens,
|
|
1661
|
+
messages: stats.messageCount,
|
|
1662
|
+
turns: stats.turnCount,
|
|
1663
|
+
utilization: stats.utilization,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
// Fallback to session tracking
|
|
1668
|
+
overlays.showContextStats({
|
|
1669
|
+
tokens: this.sessionInputTokens + this.sessionOutputTokens,
|
|
1670
|
+
maxTokens: 200000,
|
|
1671
|
+
messages: history.length,
|
|
1672
|
+
turns: Math.floor(history.filter((m) => m.role === 'user').length),
|
|
1673
|
+
utilization: (this.sessionInputTokens + this.sessionOutputTokens) / 200000,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
this.footer.forceRender();
|
|
1677
|
+
}
|
|
1678
|
+
showStatus() {
|
|
1679
|
+
const contextManager = this.agent.getContextManager();
|
|
1680
|
+
const history = this.agent.getHistory();
|
|
1681
|
+
const mode = this.footer.getMode();
|
|
1682
|
+
const modeInfo = MODE_INFO[mode];
|
|
1683
|
+
this.footer.clearForOutput();
|
|
1684
|
+
terminal.writeLine('');
|
|
1685
|
+
terminal.writeLine(pc.bold('Status'));
|
|
1686
|
+
terminal.writeLine(pc.dim('─'.repeat(40)));
|
|
1687
|
+
terminal.writeLine(` Version: ${pc.cyan(this.version)}`);
|
|
1688
|
+
terminal.writeLine(` Model: ${pc.cyan(this.model)}`);
|
|
1689
|
+
terminal.writeLine(` Mode: ${pc.cyan(modeInfo.label)}`);
|
|
1690
|
+
if (contextManager) {
|
|
1691
|
+
const stats = contextManager.getStats(history.length);
|
|
1692
|
+
const pct = (stats.utilization * 100).toFixed(1);
|
|
1693
|
+
const bar = this.renderProgressBar(stats.utilization, 20);
|
|
1694
|
+
terminal.writeLine(` Context: ${bar} ${pct}%`);
|
|
1695
|
+
terminal.writeLine(` Messages: ${pc.cyan(String(stats.messageCount))}`);
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
const total = this.sessionInputTokens + this.sessionOutputTokens;
|
|
1699
|
+
terminal.writeLine(` Tokens: ${pc.cyan(total.toLocaleString())}`);
|
|
1700
|
+
terminal.writeLine(` Messages: ${pc.cyan(String(history.length))}`);
|
|
1701
|
+
}
|
|
1702
|
+
terminal.writeLine(` Requests: ${pc.cyan(String(this.sessionRequests))}`);
|
|
1703
|
+
terminal.writeLine('');
|
|
1704
|
+
this.footer.forceRender();
|
|
1705
|
+
}
|
|
1706
|
+
renderProgressBar(ratio, width) {
|
|
1707
|
+
const filled = Math.round(ratio * width);
|
|
1708
|
+
const empty = width - filled;
|
|
1709
|
+
const color = ratio > 0.9 ? pc.red : ratio > 0.7 ? pc.yellow : pc.green;
|
|
1710
|
+
return color('█'.repeat(filled)) + pc.dim('░'.repeat(empty));
|
|
1711
|
+
}
|
|
1712
|
+
showTodos() {
|
|
1713
|
+
const todos = this.state.todos;
|
|
1714
|
+
this.footer.clearForOutput();
|
|
1715
|
+
terminal.writeLine('');
|
|
1716
|
+
if (todos.length === 0) {
|
|
1717
|
+
terminal.writeLine(pc.dim('No todos. The agent will create todos when working on tasks.'));
|
|
1718
|
+
terminal.writeLine('');
|
|
1719
|
+
this.footer.forceRender();
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
terminal.writeLine(pc.bold('Todos'));
|
|
1723
|
+
terminal.writeLine(pc.dim('─'.repeat(40)));
|
|
1724
|
+
for (const todo of todos) {
|
|
1725
|
+
let icon;
|
|
1726
|
+
let style;
|
|
1727
|
+
switch (todo.status) {
|
|
1728
|
+
case 'completed':
|
|
1729
|
+
icon = pc.green('✓');
|
|
1730
|
+
style = pc.strikethrough;
|
|
1731
|
+
break;
|
|
1732
|
+
case 'in_progress':
|
|
1733
|
+
icon = pc.yellow('●');
|
|
1734
|
+
style = pc.bold;
|
|
1735
|
+
break;
|
|
1736
|
+
default:
|
|
1737
|
+
icon = pc.dim('○');
|
|
1738
|
+
style = (s) => s;
|
|
1739
|
+
}
|
|
1740
|
+
terminal.writeLine(` ${icon} ${style(todo.content)}`);
|
|
1741
|
+
}
|
|
1742
|
+
terminal.writeLine('');
|
|
1743
|
+
this.footer.forceRender();
|
|
1744
|
+
}
|
|
1745
|
+
async handleExport(args) {
|
|
1746
|
+
const history = this.agent.getHistory();
|
|
1747
|
+
if (history.length === 0) {
|
|
1748
|
+
this.footer.clearForOutput();
|
|
1749
|
+
conversation.printWarning('No conversation to export');
|
|
1750
|
+
this.footer.forceRender();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
// Format conversation as markdown
|
|
1754
|
+
const markdown = this.formatConversationAsMarkdown(history);
|
|
1755
|
+
if (args.trim()) {
|
|
1756
|
+
// Export to file
|
|
1757
|
+
const filename = args.trim();
|
|
1758
|
+
const filepath = path.isAbsolute(filename) ? filename : path.join(process.cwd(), filename);
|
|
1759
|
+
try {
|
|
1760
|
+
fs.writeFileSync(filepath, markdown, 'utf-8');
|
|
1761
|
+
this.footer.clearForOutput();
|
|
1762
|
+
conversation.printSuccess(`Exported to ${filepath}`);
|
|
1763
|
+
this.footer.forceRender();
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
this.footer.clearForOutput();
|
|
1767
|
+
conversation.printError(`Failed to export: ${error.message}`);
|
|
1768
|
+
this.footer.forceRender();
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
// Export to clipboard
|
|
1773
|
+
try {
|
|
1774
|
+
await this.copyToClipboard(markdown);
|
|
1775
|
+
this.footer.clearForOutput();
|
|
1776
|
+
conversation.printSuccess(`Copied ${String(history.length)} messages to clipboard`);
|
|
1777
|
+
this.footer.forceRender();
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
this.footer.clearForOutput();
|
|
1781
|
+
conversation.printError(`Failed to copy to clipboard: ${error.message}`);
|
|
1782
|
+
this.footer.forceRender();
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
formatConversationAsMarkdown(history) {
|
|
1787
|
+
const lines = [];
|
|
1788
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
1789
|
+
lines.push(`# Conversation Export`);
|
|
1790
|
+
lines.push('');
|
|
1791
|
+
lines.push(`**Model:** ${this.model}`);
|
|
1792
|
+
lines.push(`**Date:** ${timestamp}`);
|
|
1793
|
+
lines.push(`**Messages:** ${String(history.length)}`);
|
|
1794
|
+
lines.push('');
|
|
1795
|
+
lines.push('---');
|
|
1796
|
+
lines.push('');
|
|
1797
|
+
for (const msg of history) {
|
|
1798
|
+
const role = msg.role === 'user' ? '👤 User' : '🤖 Assistant';
|
|
1799
|
+
lines.push(`## ${role}`);
|
|
1800
|
+
lines.push('');
|
|
1801
|
+
if (typeof msg.content === 'string') {
|
|
1802
|
+
lines.push(msg.content);
|
|
1803
|
+
}
|
|
1804
|
+
else if (Array.isArray(msg.content)) {
|
|
1805
|
+
// Handle content blocks (tool calls, etc.)
|
|
1806
|
+
for (const block of msg.content) {
|
|
1807
|
+
if (typeof block === 'object' && block !== null) {
|
|
1808
|
+
const b = block;
|
|
1809
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
1810
|
+
lines.push(b.text);
|
|
1811
|
+
}
|
|
1812
|
+
else if (b.type === 'tool_use') {
|
|
1813
|
+
const toolName = typeof b.name === 'string' ? b.name : String(b.name);
|
|
1814
|
+
lines.push(`**Tool:** \`${toolName}\``);
|
|
1815
|
+
lines.push('```json');
|
|
1816
|
+
lines.push(JSON.stringify(b.input, null, 2));
|
|
1817
|
+
lines.push('```');
|
|
1818
|
+
}
|
|
1819
|
+
else if (b.type === 'tool_result') {
|
|
1820
|
+
lines.push(`**Tool Result:**`);
|
|
1821
|
+
const content = typeof b.content === 'string' ? b.content : JSON.stringify(b.content);
|
|
1822
|
+
if (content.length > 500) {
|
|
1823
|
+
lines.push('```');
|
|
1824
|
+
lines.push(content.slice(0, 500) + '...[truncated]');
|
|
1825
|
+
lines.push('```');
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
lines.push('```');
|
|
1829
|
+
lines.push(content);
|
|
1830
|
+
lines.push('```');
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
lines.push(JSON.stringify(msg.content, null, 2));
|
|
1838
|
+
}
|
|
1839
|
+
lines.push('');
|
|
1840
|
+
lines.push('---');
|
|
1841
|
+
lines.push('');
|
|
1842
|
+
}
|
|
1843
|
+
return lines.join('\n');
|
|
1844
|
+
}
|
|
1845
|
+
async copyToClipboard(text) {
|
|
1846
|
+
const platform = process.platform;
|
|
1847
|
+
let command;
|
|
1848
|
+
if (platform === 'darwin') {
|
|
1849
|
+
command = 'pbcopy';
|
|
1850
|
+
}
|
|
1851
|
+
else if (platform === 'linux') {
|
|
1852
|
+
// Try xclip first, fall back to xsel
|
|
1853
|
+
command = 'xclip -selection clipboard';
|
|
1854
|
+
}
|
|
1855
|
+
else if (platform === 'win32') {
|
|
1856
|
+
command = 'clip';
|
|
1857
|
+
}
|
|
1858
|
+
else {
|
|
1859
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
1860
|
+
}
|
|
1861
|
+
const child = exec(command);
|
|
1862
|
+
if (child.stdin) {
|
|
1863
|
+
child.stdin.write(text);
|
|
1864
|
+
child.stdin.end();
|
|
1865
|
+
}
|
|
1866
|
+
await new Promise((resolve, reject) => {
|
|
1867
|
+
child.on('exit', (code) => {
|
|
1868
|
+
if (code === 0) {
|
|
1869
|
+
resolve();
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
reject(new Error(`Clipboard command exited with code ${String(code ?? 'unknown')}`));
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
child.on('error', reject);
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|