@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
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Demo - CLI Interface
|
|
4
|
+
*
|
|
5
|
+
* Simple readline-based REPL for testing the agent.
|
|
6
|
+
*/
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { marked } from 'marked';
|
|
9
|
+
import { markedTerminal } from 'marked-terminal';
|
|
10
|
+
import { createAgent } from './agent.js';
|
|
11
|
+
import { selectToolNamesByIntent, estimateTokenSavings } from './tool-selector.js';
|
|
12
|
+
import { createInteractiveInput } from './slash-autocomplete.js';
|
|
13
|
+
import { showHelpMenu } from './tabbed-menu.js';
|
|
14
|
+
// Configure marked for terminal output
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
marked.use(markedTerminal({
|
|
17
|
+
reflowText: true,
|
|
18
|
+
width: 80,
|
|
19
|
+
}));
|
|
20
|
+
/**
|
|
21
|
+
* Render markdown text for terminal display
|
|
22
|
+
*/
|
|
23
|
+
function renderMarkdown(text) {
|
|
24
|
+
try {
|
|
25
|
+
// Preprocess: remove common leading indentation that breaks markdown parsing
|
|
26
|
+
// (4 spaces makes it a code block, which preserves raw **bold** markers)
|
|
27
|
+
const lines = text.split('\n');
|
|
28
|
+
const processedLines = lines.map(line => {
|
|
29
|
+
// Remove leading 4 spaces that would make it a code block
|
|
30
|
+
// But preserve intentional code blocks (``` fenced)
|
|
31
|
+
if (line.startsWith(' ') && !line.startsWith(' ```')) {
|
|
32
|
+
return line.slice(4);
|
|
33
|
+
}
|
|
34
|
+
return line;
|
|
35
|
+
});
|
|
36
|
+
const processed = processedLines.join('\n');
|
|
37
|
+
// marked.parse returns string synchronously with these options
|
|
38
|
+
let rendered = marked.parse(processed, { async: false });
|
|
39
|
+
// Post-process: marked-terminal doesn't render bold/italic inside list items
|
|
40
|
+
// Convert remaining **bold** and *italic* to ANSI codes
|
|
41
|
+
rendered = rendered
|
|
42
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, text) => pc.bold(text)) // **bold**
|
|
43
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, text) => pc.italic(text)) // *italic* (not **)
|
|
44
|
+
.replace(/`([^`]+)`/g, (_, text) => pc.cyan(text)); // `code`
|
|
45
|
+
// Remove trailing newlines that marked adds
|
|
46
|
+
return rendered.replace(/\n+$/, '');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return text; // Fallback to raw text
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Funny thinking words (inspired by Claude Code)
|
|
53
|
+
const THINKING_WORDS = [
|
|
54
|
+
'Thinking',
|
|
55
|
+
'Pondering',
|
|
56
|
+
'Contemplating',
|
|
57
|
+
'Ruminating',
|
|
58
|
+
'Cogitating',
|
|
59
|
+
'Deliberating',
|
|
60
|
+
'Musing',
|
|
61
|
+
'Reflecting',
|
|
62
|
+
'Considering',
|
|
63
|
+
'Analyzing',
|
|
64
|
+
'Processing',
|
|
65
|
+
'Computing',
|
|
66
|
+
'Synthesizing',
|
|
67
|
+
'Extrapolating',
|
|
68
|
+
'Hypothesizing',
|
|
69
|
+
'Discombobulating',
|
|
70
|
+
'Nebulizing',
|
|
71
|
+
'Percolating',
|
|
72
|
+
'Marinating',
|
|
73
|
+
'Simmering',
|
|
74
|
+
'Brewing',
|
|
75
|
+
'Fermenting',
|
|
76
|
+
'Distilling',
|
|
77
|
+
'Crystallizing',
|
|
78
|
+
'Transmuting',
|
|
79
|
+
];
|
|
80
|
+
// Global spinner instance for Esc handling
|
|
81
|
+
let spinnerInstance = null;
|
|
82
|
+
/**
|
|
83
|
+
* Format duration in human-readable format (e.g., "1m 23s", "45s")
|
|
84
|
+
*/
|
|
85
|
+
function formatDuration(ms) {
|
|
86
|
+
const seconds = Math.floor(ms / 1000);
|
|
87
|
+
if (seconds < 60) {
|
|
88
|
+
return `${seconds}s`;
|
|
89
|
+
}
|
|
90
|
+
const minutes = Math.floor(seconds / 60);
|
|
91
|
+
const remainingSeconds = seconds % 60;
|
|
92
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format token count (e.g., "1.2k", "500")
|
|
96
|
+
*/
|
|
97
|
+
function formatTokens(tokens) {
|
|
98
|
+
if (tokens >= 1000) {
|
|
99
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
100
|
+
}
|
|
101
|
+
return tokens.toString();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Current todo list (stored globally for display during agent execution)
|
|
105
|
+
*/
|
|
106
|
+
let currentTodos = [];
|
|
107
|
+
/**
|
|
108
|
+
* Render todo list with visual styling like Claude Code
|
|
109
|
+
*/
|
|
110
|
+
function renderTodoList(todos) {
|
|
111
|
+
if (!todos || todos.length === 0) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
const lines = [];
|
|
115
|
+
for (const todo of todos) {
|
|
116
|
+
let checkbox;
|
|
117
|
+
let text;
|
|
118
|
+
switch (todo.status) {
|
|
119
|
+
case 'completed':
|
|
120
|
+
checkbox = pc.dim('☒');
|
|
121
|
+
text = pc.strikethrough(pc.dim(todo.content));
|
|
122
|
+
break;
|
|
123
|
+
case 'in_progress':
|
|
124
|
+
checkbox = pc.cyan('☐');
|
|
125
|
+
text = pc.bold(todo.content);
|
|
126
|
+
break;
|
|
127
|
+
case 'pending':
|
|
128
|
+
default:
|
|
129
|
+
checkbox = pc.dim('☐');
|
|
130
|
+
text = pc.dim(todo.content);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
lines.push(`${checkbox} ${text}`);
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the currently active todo (in_progress)
|
|
139
|
+
*/
|
|
140
|
+
function getActiveTodo() {
|
|
141
|
+
return currentTodos.find(t => t.status === 'in_progress');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Format tool result for display - handles strings, objects, arrays
|
|
145
|
+
*/
|
|
146
|
+
function formatToolResult(result) {
|
|
147
|
+
if (result === null || result === undefined) {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
// String result - count lines or truncate
|
|
151
|
+
if (typeof result === 'string') {
|
|
152
|
+
const lines = result.split('\n').filter(l => l.trim()).length;
|
|
153
|
+
if (lines > 3) {
|
|
154
|
+
return pc.dim(`${lines} lines`);
|
|
155
|
+
}
|
|
156
|
+
const truncated = result.slice(0, 80).replace(/\n/g, ' ');
|
|
157
|
+
return pc.dim(truncated + (result.length > 80 ? '...' : ''));
|
|
158
|
+
}
|
|
159
|
+
// Array result - show count and type hint
|
|
160
|
+
if (Array.isArray(result)) {
|
|
161
|
+
const len = result.length;
|
|
162
|
+
if (len === 0)
|
|
163
|
+
return pc.dim('empty array');
|
|
164
|
+
// Try to describe contents
|
|
165
|
+
const first = result[0];
|
|
166
|
+
if (typeof first === 'string') {
|
|
167
|
+
return pc.dim(`${len} items`);
|
|
168
|
+
}
|
|
169
|
+
else if (typeof first === 'object' && first !== null) {
|
|
170
|
+
// Check for common patterns like todos, files, branches
|
|
171
|
+
if ('content' in first && 'status' in first) {
|
|
172
|
+
return pc.dim(`${len} todo items`);
|
|
173
|
+
}
|
|
174
|
+
if ('name' in first || 'path' in first) {
|
|
175
|
+
return pc.dim(`${len} items`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return pc.dim(`${len} items`);
|
|
179
|
+
}
|
|
180
|
+
// Object result - try to extract meaningful summary
|
|
181
|
+
if (typeof result === 'object') {
|
|
182
|
+
const obj = result;
|
|
183
|
+
// Git status pattern
|
|
184
|
+
if ('branch' in obj && 'clean' in obj) {
|
|
185
|
+
const branch = obj.branch;
|
|
186
|
+
const clean = obj.clean;
|
|
187
|
+
return pc.dim(`${branch}${clean ? ' (clean)' : ' (changes)'}`);
|
|
188
|
+
}
|
|
189
|
+
// Git diff pattern
|
|
190
|
+
if ('files' in obj && Array.isArray(obj.files)) {
|
|
191
|
+
const files = obj.files;
|
|
192
|
+
if (files.length === 0)
|
|
193
|
+
return pc.dim('no changes');
|
|
194
|
+
return pc.dim(`${files.length} file(s) changed`);
|
|
195
|
+
}
|
|
196
|
+
// Git branch pattern
|
|
197
|
+
if ('current' in obj && 'branches' in obj) {
|
|
198
|
+
const branches = obj.branches;
|
|
199
|
+
return pc.dim(`${branches?.length || 0} branches`);
|
|
200
|
+
}
|
|
201
|
+
// File read pattern
|
|
202
|
+
if ('content' in obj) {
|
|
203
|
+
const content = String(obj.content);
|
|
204
|
+
const lines = content.split('\n').length;
|
|
205
|
+
return pc.dim(`${lines} lines`);
|
|
206
|
+
}
|
|
207
|
+
// Bash/command pattern
|
|
208
|
+
if ('stdout' in obj || 'output' in obj) {
|
|
209
|
+
const out = String(obj.stdout || obj.output || '');
|
|
210
|
+
const lines = out.split('\n').filter(l => l.trim()).length;
|
|
211
|
+
if (lines > 0)
|
|
212
|
+
return pc.dim(`${lines} lines`);
|
|
213
|
+
return pc.dim('done');
|
|
214
|
+
}
|
|
215
|
+
// Success message pattern
|
|
216
|
+
if ('success' in obj && typeof obj.message === 'string') {
|
|
217
|
+
return pc.dim(obj.message.slice(0, 60));
|
|
218
|
+
}
|
|
219
|
+
// Fallback - show key count
|
|
220
|
+
const keys = Object.keys(obj);
|
|
221
|
+
if (keys.length <= 3) {
|
|
222
|
+
return pc.dim(keys.join(', '));
|
|
223
|
+
}
|
|
224
|
+
return pc.dim(`${keys.length} fields`);
|
|
225
|
+
}
|
|
226
|
+
// Primitive - just stringify
|
|
227
|
+
return pc.dim(String(result).slice(0, 60));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Animated thinking spinner with Knight Rider scanning effect
|
|
231
|
+
* Shows elapsed time, token usage, and todo list
|
|
232
|
+
*/
|
|
233
|
+
class ThinkingSpinner {
|
|
234
|
+
word = '';
|
|
235
|
+
position = 0;
|
|
236
|
+
direction = 1; // 1 = right, -1 = left
|
|
237
|
+
interval = null;
|
|
238
|
+
abortController = null;
|
|
239
|
+
startTime = 0;
|
|
240
|
+
tokens = 0;
|
|
241
|
+
currentTool = null;
|
|
242
|
+
renderedLines = 0; // Track how many lines we've rendered
|
|
243
|
+
start() {
|
|
244
|
+
this.abortController = new AbortController();
|
|
245
|
+
spinnerInstance = this;
|
|
246
|
+
this.startTime = Date.now();
|
|
247
|
+
this.tokens = 0;
|
|
248
|
+
this.currentTool = null;
|
|
249
|
+
this.renderedLines = 0;
|
|
250
|
+
// Pick a random word
|
|
251
|
+
this.word = THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] + '...';
|
|
252
|
+
this.position = 0;
|
|
253
|
+
this.direction = 1;
|
|
254
|
+
// Start animation
|
|
255
|
+
this.render();
|
|
256
|
+
this.interval = setInterval(() => {
|
|
257
|
+
// Move position
|
|
258
|
+
this.position += this.direction;
|
|
259
|
+
// Bounce at edges
|
|
260
|
+
if (this.position >= this.word.length - 1) {
|
|
261
|
+
this.direction = -1;
|
|
262
|
+
}
|
|
263
|
+
else if (this.position <= 0) {
|
|
264
|
+
this.direction = 1;
|
|
265
|
+
}
|
|
266
|
+
this.render();
|
|
267
|
+
}, 150); // Smooth scanning speed
|
|
268
|
+
return this.abortController;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Update token count (called when tokens are received)
|
|
272
|
+
*/
|
|
273
|
+
addTokens(count) {
|
|
274
|
+
this.tokens += count;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Set current tool being executed
|
|
278
|
+
*/
|
|
279
|
+
setTool(toolName) {
|
|
280
|
+
this.currentTool = toolName;
|
|
281
|
+
// Re-render immediately to show tool
|
|
282
|
+
if (this.interval) {
|
|
283
|
+
this.render();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Clear the spinner area (all rendered lines)
|
|
288
|
+
*/
|
|
289
|
+
clearArea() {
|
|
290
|
+
if (this.renderedLines > 0) {
|
|
291
|
+
// Move to start of current line
|
|
292
|
+
process.stdout.write('\r');
|
|
293
|
+
// Move up to first line
|
|
294
|
+
if (this.renderedLines > 1) {
|
|
295
|
+
process.stdout.write(`\x1b[${this.renderedLines - 1}A`);
|
|
296
|
+
}
|
|
297
|
+
// Clear from cursor to end of screen
|
|
298
|
+
process.stdout.write('\x1b[J');
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
process.stdout.write('\r\x1b[K');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
render() {
|
|
305
|
+
const elapsed = formatDuration(Date.now() - this.startTime);
|
|
306
|
+
const tokenStr = formatTokens(this.tokens);
|
|
307
|
+
// Clear previous render
|
|
308
|
+
this.clearArea();
|
|
309
|
+
// Build stats string
|
|
310
|
+
const stats = pc.dim(` (Esc to interrupt · ${elapsed} · ↓ ${tokenStr} tokens)`);
|
|
311
|
+
// If a tool is running, show that instead of the thinking word
|
|
312
|
+
if (this.currentTool) {
|
|
313
|
+
const toolDisplay = pc.yellow(`● ${this.currentTool}`);
|
|
314
|
+
process.stdout.write(`${toolDisplay}${stats}`);
|
|
315
|
+
// Render todo list below if we have todos
|
|
316
|
+
if (currentTodos.length > 0) {
|
|
317
|
+
process.stdout.write('\n' + renderTodoList(currentTodos));
|
|
318
|
+
this.renderedLines = 1 + currentTodos.length; // status + todos
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.renderedLines = 1;
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Check if there's an active todo - show that with animation
|
|
326
|
+
const activeTodo = getActiveTodo();
|
|
327
|
+
const displayText = activeTodo
|
|
328
|
+
? (activeTodo.activeForm || activeTodo.content) + '...'
|
|
329
|
+
: this.word;
|
|
330
|
+
// Build the text with scanning highlight (knight rider effect)
|
|
331
|
+
let result = '';
|
|
332
|
+
for (let i = 0; i < displayText.length; i++) {
|
|
333
|
+
const distance = Math.abs(i - (this.position % displayText.length));
|
|
334
|
+
const char = displayText[i];
|
|
335
|
+
if (distance === 0) {
|
|
336
|
+
result += pc.bold(pc.magenta(char));
|
|
337
|
+
}
|
|
338
|
+
else if (distance === 1) {
|
|
339
|
+
result += pc.magenta(char);
|
|
340
|
+
}
|
|
341
|
+
else if (distance === 2) {
|
|
342
|
+
result += pc.red(char);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
result += pc.dim(pc.red(char));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// If we have an active todo, show the indicator
|
|
349
|
+
if (activeTodo) {
|
|
350
|
+
result = pc.red('✱ ') + result;
|
|
351
|
+
}
|
|
352
|
+
process.stdout.write(`${result}${stats}`);
|
|
353
|
+
// Render todo list below if we have todos
|
|
354
|
+
if (currentTodos.length > 0) {
|
|
355
|
+
process.stdout.write('\n' + renderTodoList(currentTodos));
|
|
356
|
+
this.renderedLines = 1 + currentTodos.length; // status + todos
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
this.renderedLines = 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
stop(wasAborted = false) {
|
|
363
|
+
if (this.interval) {
|
|
364
|
+
clearInterval(this.interval);
|
|
365
|
+
this.interval = null;
|
|
366
|
+
}
|
|
367
|
+
spinnerInstance = null;
|
|
368
|
+
// Clear all spinner lines
|
|
369
|
+
this.clearArea();
|
|
370
|
+
this.renderedLines = 0;
|
|
371
|
+
if (wasAborted) {
|
|
372
|
+
console.log(pc.yellow('Interrupted by user.\n'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Pause the spinner animation (for permission prompts)
|
|
377
|
+
*/
|
|
378
|
+
pause() {
|
|
379
|
+
if (this.interval) {
|
|
380
|
+
clearInterval(this.interval);
|
|
381
|
+
this.interval = null;
|
|
382
|
+
}
|
|
383
|
+
// Clear the spinner line
|
|
384
|
+
process.stdout.write('\r\x1b[K');
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Clear all spinner lines for external output (tool results, etc.)
|
|
388
|
+
* After calling this, call render() to redraw if spinner should continue.
|
|
389
|
+
*/
|
|
390
|
+
clearForOutput() {
|
|
391
|
+
this.clearArea();
|
|
392
|
+
this.renderedLines = 0;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Force an immediate render (e.g., after todos are updated)
|
|
396
|
+
*/
|
|
397
|
+
forceRender() {
|
|
398
|
+
if (spinnerInstance === this) {
|
|
399
|
+
this.render();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Resume the spinner animation after pause
|
|
404
|
+
*/
|
|
405
|
+
resume() {
|
|
406
|
+
if (!this.interval && spinnerInstance === this) {
|
|
407
|
+
this.render();
|
|
408
|
+
this.interval = setInterval(() => {
|
|
409
|
+
this.position += this.direction;
|
|
410
|
+
if (this.position >= this.word.length - 1) {
|
|
411
|
+
this.direction = -1;
|
|
412
|
+
}
|
|
413
|
+
else if (this.position <= 0) {
|
|
414
|
+
this.direction = 1;
|
|
415
|
+
}
|
|
416
|
+
this.render();
|
|
417
|
+
}, 150);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
abort() {
|
|
421
|
+
if (this.abortController) {
|
|
422
|
+
this.abortController.abort();
|
|
423
|
+
}
|
|
424
|
+
this.stop(true);
|
|
425
|
+
}
|
|
426
|
+
isAborted() {
|
|
427
|
+
return this.abortController?.signal.aborted ?? false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Set up global Esc key handler (called once at startup)
|
|
432
|
+
*/
|
|
433
|
+
function setupEscHandler() {
|
|
434
|
+
// Listen for escape key (0x1b by itself, not part of arrow key sequence)
|
|
435
|
+
process.stdin.on('data', (data) => {
|
|
436
|
+
if (data.length === 1 && data[0] === 0x1b && spinnerInstance) {
|
|
437
|
+
spinnerInstance.abort();
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Parse command line arguments
|
|
442
|
+
const args = process.argv.slice(2);
|
|
443
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
444
|
+
const minimal = args.includes('--minimal') || args.includes('-m');
|
|
445
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
446
|
+
const showFiltering = args.includes('--show-filtering') || args.includes('-f');
|
|
447
|
+
// Parse --provider flag
|
|
448
|
+
let provider = 'claude';
|
|
449
|
+
const providerIdx = args.findIndex((a) => a === '--provider' || a === '-p');
|
|
450
|
+
if (providerIdx !== -1 && args[providerIdx + 1]) {
|
|
451
|
+
const p = args[providerIdx + 1].toLowerCase();
|
|
452
|
+
if (p === 'claude' || p === 'ollama' || p === 'openai' || p === 'gemini') {
|
|
453
|
+
provider = p;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.error(`Unknown provider: ${p}. Use 'claude', 'ollama', 'openai', or 'gemini'.`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Parse --model flag
|
|
461
|
+
let model;
|
|
462
|
+
const modelIdx = args.findIndex((a) => a === '--model');
|
|
463
|
+
if (modelIdx !== -1 && args[modelIdx + 1]) {
|
|
464
|
+
model = args[modelIdx + 1];
|
|
465
|
+
}
|
|
466
|
+
if (help) {
|
|
467
|
+
console.log(`
|
|
468
|
+
Agent Demo - PoC for @compilr-dev/agents
|
|
469
|
+
|
|
470
|
+
Usage:
|
|
471
|
+
npm run dev [options]
|
|
472
|
+
|
|
473
|
+
Options:
|
|
474
|
+
-v, --verbose Show tool execution events
|
|
475
|
+
-m, --minimal Use minimal tool set (faster startup)
|
|
476
|
+
-f, --show-filtering Show tool filtering analysis (token savings)
|
|
477
|
+
-p, --provider <name> LLM provider: claude (default), ollama, openai, gemini
|
|
478
|
+
--model <name> Model to use (provider-specific, see examples)
|
|
479
|
+
-h, --help Show this help message
|
|
480
|
+
|
|
481
|
+
Commands (in REPL):
|
|
482
|
+
exit Quit the demo
|
|
483
|
+
clear Clear conversation history
|
|
484
|
+
tools List available tools
|
|
485
|
+
|
|
486
|
+
Environment Variables:
|
|
487
|
+
ANTHROPIC_API_KEY Required for Claude provider
|
|
488
|
+
OPENAI_API_KEY Required for OpenAI provider
|
|
489
|
+
GOOGLE_AI_API_KEY Required for Gemini provider (or GEMINI_API_KEY)
|
|
490
|
+
OLLAMA_BASE_URL Ollama server URL (default: http://localhost:11434)
|
|
491
|
+
|
|
492
|
+
Examples:
|
|
493
|
+
npm run dev -- --provider claude # Claude with claude-sonnet-4
|
|
494
|
+
npm run dev -- --provider ollama # Ollama with llama3.1
|
|
495
|
+
npm run dev -- -p ollama --model mistral # Ollama with mistral
|
|
496
|
+
npm run dev -- --provider openai # OpenAI with gpt-4o
|
|
497
|
+
npm run dev -- -p openai --model gpt-4o-mini # OpenAI with gpt-4o-mini
|
|
498
|
+
npm run dev -- --provider gemini # Gemini with gemini-2.0-flash
|
|
499
|
+
npm run dev -- -p gemini --model gemini-1.5-pro # Gemini with gemini-1.5-pro
|
|
500
|
+
`);
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
async function main() {
|
|
504
|
+
// Clear screen and move cursor to top
|
|
505
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
506
|
+
// ASCII Logo (from compilr-dev-cli)
|
|
507
|
+
console.log(pc.cyan(` ___ ___ _ __ ___ _ __ (_) |_ __
|
|
508
|
+
/ __|/ _ \\| '_ \` _ \\| '_ \\| | | '__|
|
|
509
|
+
| (__| (_) | | | | | | |_) | | | |
|
|
510
|
+
\\___|\\___/|_| |_| |_| .__/|_|_|_|
|
|
511
|
+
| | .dev
|
|
512
|
+
|_|`));
|
|
513
|
+
console.log('');
|
|
514
|
+
console.log(pc.bold(pc.cyan('compilr-dev-cli')) + pc.dim(' - AI-powered terminal assistant'));
|
|
515
|
+
console.log('');
|
|
516
|
+
// Interactive input handler (will be initialized later)
|
|
517
|
+
let inputHandler = null;
|
|
518
|
+
/**
|
|
519
|
+
* Simple single-key input for permission prompts
|
|
520
|
+
*/
|
|
521
|
+
const askYesNo = (prompt) => {
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
process.stdout.write(prompt);
|
|
524
|
+
// Ensure stdin is in the right state for reading
|
|
525
|
+
if (process.stdin.isTTY) {
|
|
526
|
+
process.stdin.setRawMode(true);
|
|
527
|
+
}
|
|
528
|
+
process.stdin.resume();
|
|
529
|
+
const onData = (data) => {
|
|
530
|
+
const key = data.toString().toLowerCase();
|
|
531
|
+
process.stdin.removeListener('data', onData);
|
|
532
|
+
// Restore stdin state
|
|
533
|
+
if (process.stdin.isTTY) {
|
|
534
|
+
process.stdin.setRawMode(false);
|
|
535
|
+
}
|
|
536
|
+
process.stdin.pause();
|
|
537
|
+
if (key === 'y') {
|
|
538
|
+
process.stdout.write('y\n');
|
|
539
|
+
resolve(true);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
process.stdout.write('n\n');
|
|
543
|
+
resolve(false);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
process.stdin.once('data', onData);
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* Permission handler - prompts user for approval
|
|
551
|
+
*/
|
|
552
|
+
const handlePermissionRequest = async (request) => {
|
|
553
|
+
// Pause spinner if active (stop the interval, clear the line)
|
|
554
|
+
let wasSpinnerActive = false;
|
|
555
|
+
if (spinnerInstance) {
|
|
556
|
+
wasSpinnerActive = true;
|
|
557
|
+
spinnerInstance.pause();
|
|
558
|
+
}
|
|
559
|
+
// Show permission request
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log(pc.yellow('⚠ Permission Required'));
|
|
562
|
+
console.log(pc.bold(` Tool: ${request.toolName}`));
|
|
563
|
+
if (request.description) {
|
|
564
|
+
console.log(pc.dim(` ${request.description}`));
|
|
565
|
+
}
|
|
566
|
+
// Show preview of what will happen
|
|
567
|
+
const input = request.input;
|
|
568
|
+
if (input.command) {
|
|
569
|
+
console.log(pc.dim(` Command: ${String(input.command).slice(0, 60)}${String(input.command).length > 60 ? '...' : ''}`));
|
|
570
|
+
}
|
|
571
|
+
else if (input.path) {
|
|
572
|
+
console.log(pc.dim(` Path: ${input.path}`));
|
|
573
|
+
}
|
|
574
|
+
else if (input.message) {
|
|
575
|
+
console.log(pc.dim(` Message: ${String(input.message).slice(0, 60)}`));
|
|
576
|
+
}
|
|
577
|
+
console.log('');
|
|
578
|
+
const allowed = await askYesNo(pc.cyan(' Allow? [y/N]: '));
|
|
579
|
+
if (allowed) {
|
|
580
|
+
console.log(pc.green(' ✓ Allowed\n'));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
console.log(pc.red(' ✗ Denied\n'));
|
|
584
|
+
}
|
|
585
|
+
// Resume spinner if it was active
|
|
586
|
+
if (wasSpinnerActive && spinnerInstance) {
|
|
587
|
+
spinnerInstance.resume();
|
|
588
|
+
}
|
|
589
|
+
return allowed;
|
|
590
|
+
};
|
|
591
|
+
// Create agent with permission handler
|
|
592
|
+
let agent;
|
|
593
|
+
try {
|
|
594
|
+
agent = createAgent({
|
|
595
|
+
verbose,
|
|
596
|
+
minimal,
|
|
597
|
+
provider,
|
|
598
|
+
model,
|
|
599
|
+
onPermissionRequest: handlePermissionRequest,
|
|
600
|
+
});
|
|
601
|
+
console.log(pc.green('✓') + ` Agent initialized with ${minimal ? 'minimal' : 'full'} tool set.`);
|
|
602
|
+
console.log(pc.green('✓') + ` Permissions enabled for: bash, write_file, edit, git_commit`);
|
|
603
|
+
console.log(pc.dim('Type / for commands, /exit to quit.\n'));
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
console.error('Failed to create agent:', error.message);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
// Session-wide token tracking
|
|
610
|
+
let sessionInputTokens = 0;
|
|
611
|
+
let sessionOutputTokens = 0;
|
|
612
|
+
let sessionRequests = 0;
|
|
613
|
+
// Set up Esc key handler for interrupting agent (once at startup)
|
|
614
|
+
setupEscHandler();
|
|
615
|
+
// Handle slash commands
|
|
616
|
+
function handleCommand(input) {
|
|
617
|
+
const trimmed = input.trim();
|
|
618
|
+
// Commands must start with /
|
|
619
|
+
if (!trimmed.startsWith('/')) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const cmd = trimmed.slice(1).toLowerCase();
|
|
623
|
+
switch (cmd) {
|
|
624
|
+
case 'exit':
|
|
625
|
+
case 'quit':
|
|
626
|
+
case 'q':
|
|
627
|
+
console.log('\nGoodbye!');
|
|
628
|
+
inputHandler?.stop();
|
|
629
|
+
process.exit(0);
|
|
630
|
+
case 'clear':
|
|
631
|
+
agent.clearHistory();
|
|
632
|
+
console.log(pc.green('✓') + ' Conversation history cleared.\n');
|
|
633
|
+
return true;
|
|
634
|
+
case 'tools': {
|
|
635
|
+
const tools = agent.getToolDefinitions();
|
|
636
|
+
console.log(pc.bold(`\nAvailable tools (${tools.length}):`));
|
|
637
|
+
for (const tool of tools) {
|
|
638
|
+
console.log(pc.dim(' •') + ` ${tool.name}`);
|
|
639
|
+
}
|
|
640
|
+
console.log('');
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
case 'tokens': {
|
|
644
|
+
const sessionTotal = sessionInputTokens + sessionOutputTokens;
|
|
645
|
+
console.log(pc.bold(`\nSession Token Usage:`));
|
|
646
|
+
console.log(` Total: ${pc.cyan(sessionTotal.toLocaleString())} tokens`);
|
|
647
|
+
console.log(` Input: ${pc.dim(sessionInputTokens.toLocaleString())} tokens`);
|
|
648
|
+
console.log(` Output: ${pc.dim(sessionOutputTokens.toLocaleString())} tokens`);
|
|
649
|
+
console.log(` Requests: ${sessionRequests}`);
|
|
650
|
+
if (sessionRequests > 0) {
|
|
651
|
+
console.log(` Average: ${Math.round(sessionTotal / sessionRequests).toLocaleString()} tokens/req`);
|
|
652
|
+
}
|
|
653
|
+
console.log('');
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
case 'context': {
|
|
657
|
+
const stats = agent.getContextStats?.();
|
|
658
|
+
if (stats) {
|
|
659
|
+
const pct = (stats.utilization * 100).toFixed(2);
|
|
660
|
+
const sessionTotal = sessionInputTokens + sessionOutputTokens;
|
|
661
|
+
// Color utilization based on level
|
|
662
|
+
const pctNum = parseFloat(pct);
|
|
663
|
+
const pctColor = pctNum > 80 ? pc.red : pctNum > 50 ? pc.yellow : pc.green;
|
|
664
|
+
console.log(pc.bold(`\nContext Window:`));
|
|
665
|
+
console.log(` History: ${pc.cyan(stats.currentTokens.toLocaleString())} tokens ${pc.dim(`(${stats.messageCount} msgs, ${stats.turnCount} turns)`)}`);
|
|
666
|
+
console.log(` Limit: ${pc.dim(stats.maxTokens.toLocaleString())} tokens`);
|
|
667
|
+
console.log(` Utilization: ${pctColor(pct + '%')}`);
|
|
668
|
+
console.log(pc.bold(`\nSession Usage:`));
|
|
669
|
+
console.log(` API tokens: ${pc.cyan(sessionTotal.toLocaleString())} ${pc.dim(`(${sessionInputTokens.toLocaleString()} in / ${sessionOutputTokens.toLocaleString()} out)`)}`);
|
|
670
|
+
console.log(` Requests: ${sessionRequests}`);
|
|
671
|
+
if (stats.compactionCount > 0 || stats.summarizationCount > 0) {
|
|
672
|
+
console.log(pc.bold(`\nContext Management:`));
|
|
673
|
+
if (stats.compactionCount > 0) {
|
|
674
|
+
console.log(` Compactions: ${stats.compactionCount}`);
|
|
675
|
+
}
|
|
676
|
+
if (stats.summarizationCount > 0) {
|
|
677
|
+
console.log(` Summarizations: ${stats.summarizationCount}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
console.log(pc.yellow('\nContext manager not enabled or no stats available.'));
|
|
683
|
+
}
|
|
684
|
+
console.log('');
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
case 'compact': {
|
|
688
|
+
// Mark as async command - will be handled separately
|
|
689
|
+
return 'async';
|
|
690
|
+
}
|
|
691
|
+
case 'help':
|
|
692
|
+
case '?':
|
|
693
|
+
// Mark as async command - will show tabbed help menu
|
|
694
|
+
return 'async';
|
|
695
|
+
default:
|
|
696
|
+
if (trimmed.startsWith('/')) {
|
|
697
|
+
console.log(pc.yellow(`Unknown command: ${trimmed}`) + pc.dim(` - Type /help for available commands.\n`));
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Handle async /compact command
|
|
704
|
+
async function handleCompact() {
|
|
705
|
+
const statsBefore = agent.getContextStats?.();
|
|
706
|
+
if (!statsBefore || statsBefore.currentTokens === 0) {
|
|
707
|
+
console.log(pc.yellow('\nNothing to compact (history is empty).\n'));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const history = agent.getHistory();
|
|
711
|
+
if (history.length < 6) {
|
|
712
|
+
console.log(pc.yellow('\nNot enough history to compact (need at least 6 messages).\n'));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
console.log(pc.bold('\nCompacting context...'));
|
|
716
|
+
console.log(` Before: ${pc.cyan(statsBefore.currentTokens.toLocaleString())} tokens (${history.length} messages)`);
|
|
717
|
+
try {
|
|
718
|
+
// Keep the last 4 messages (2 turns), but only text-only messages
|
|
719
|
+
// Tool call messages can cause issues when restored without proper context
|
|
720
|
+
const keepCount = 4;
|
|
721
|
+
// Filter to get only "clean" messages (text-only, no tool calls)
|
|
722
|
+
const cleanMessages = history.filter(m => {
|
|
723
|
+
if (typeof m.content === 'string')
|
|
724
|
+
return true;
|
|
725
|
+
// Check if any content block is a tool_use or tool_result
|
|
726
|
+
const hasToolContent = m.content.some(b => b.type === 'tool_use' || b.type === 'tool_result');
|
|
727
|
+
return !hasToolContent;
|
|
728
|
+
});
|
|
729
|
+
if (cleanMessages.length < 4) {
|
|
730
|
+
console.log(pc.yellow(' Not enough text messages to compact (mostly tool calls).\n'));
|
|
731
|
+
console.log(pc.dim(' Use /clear to reset history completely.\n'));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const recentMessages = cleanMessages.slice(-keepCount);
|
|
735
|
+
const oldMessages = cleanMessages.slice(0, -keepCount);
|
|
736
|
+
if (oldMessages.length === 0) {
|
|
737
|
+
console.log(pc.yellow(' Nothing to summarize (only recent messages remain).\n'));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
// Generate a summary of the old messages using the agent
|
|
741
|
+
console.log(pc.dim(` Summarizing ${oldMessages.length} old messages...`));
|
|
742
|
+
// Build a summary prompt from old messages
|
|
743
|
+
const oldContent = oldMessages
|
|
744
|
+
.filter(m => m.role !== 'system')
|
|
745
|
+
.map(m => {
|
|
746
|
+
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
747
|
+
const text = typeof m.content === 'string'
|
|
748
|
+
? m.content
|
|
749
|
+
: m.content
|
|
750
|
+
.filter((b) => b.type === 'text')
|
|
751
|
+
.map(b => b.text)
|
|
752
|
+
.join('\n');
|
|
753
|
+
return `${role}: ${text.slice(0, 500)}${text.length > 500 ? '...' : ''}`;
|
|
754
|
+
})
|
|
755
|
+
.join('\n\n');
|
|
756
|
+
// Use a temporary clear to generate summary without old context
|
|
757
|
+
agent.clearHistory();
|
|
758
|
+
// Generate summary
|
|
759
|
+
let summary = '';
|
|
760
|
+
const summaryPrompt = `Summarize this conversation in 2-3 sentences, focusing on key topics discussed and any important outcomes:\n\n${oldContent}`;
|
|
761
|
+
for await (const event of agent.stream(summaryPrompt)) {
|
|
762
|
+
if (event.type === 'llm_chunk' && event.chunk.type === 'text' && event.chunk.text) {
|
|
763
|
+
summary += event.chunk.text;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Clear again and set new history with summary + recent messages
|
|
767
|
+
agent.clearHistory();
|
|
768
|
+
// Create a system-style message with the summary
|
|
769
|
+
const summaryMessage = {
|
|
770
|
+
role: 'user',
|
|
771
|
+
content: `[Previous conversation summary: ${summary.trim()}]`,
|
|
772
|
+
};
|
|
773
|
+
const assistantAck = {
|
|
774
|
+
role: 'assistant',
|
|
775
|
+
content: 'I understand. I have context from our previous conversation. How can I help you continue?',
|
|
776
|
+
};
|
|
777
|
+
// Set new compacted history
|
|
778
|
+
await agent.setHistory([summaryMessage, assistantAck, ...recentMessages]);
|
|
779
|
+
const statsAfter = agent.getContextStats?.();
|
|
780
|
+
const saved = statsBefore.currentTokens - (statsAfter?.currentTokens ?? 0);
|
|
781
|
+
const savedPct = ((saved / statsBefore.currentTokens) * 100).toFixed(1);
|
|
782
|
+
console.log(` After: ${pc.cyan(statsAfter?.currentTokens.toLocaleString() ?? '0')} tokens (${(statsAfter?.messageCount ?? 0)} messages)`);
|
|
783
|
+
console.log(pc.green('✓') + ` Compacted! Saved ${pc.cyan(saved.toLocaleString())} tokens (${savedPct}% reduction)`);
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
console.log(pc.red(' Error:'), error.message);
|
|
787
|
+
}
|
|
788
|
+
console.log('');
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Process user input (command or message to agent)
|
|
792
|
+
*/
|
|
793
|
+
async function processInput(input) {
|
|
794
|
+
// Handle empty input
|
|
795
|
+
if (!input.trim()) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// Check for slash commands
|
|
799
|
+
const cmdResult = handleCommand(input);
|
|
800
|
+
if (cmdResult === 'async') {
|
|
801
|
+
// Handle async commands
|
|
802
|
+
const cmd = input.trim().toLowerCase();
|
|
803
|
+
if (cmd === '/compact') {
|
|
804
|
+
await handleCompact();
|
|
805
|
+
}
|
|
806
|
+
else if (cmd === '/help' || cmd === '/?') {
|
|
807
|
+
await showHelpMenu();
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (cmdResult === true) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// Apply tool filtering - select relevant tools based on user intent
|
|
815
|
+
const totalTools = agent.getToolDefinitions().length;
|
|
816
|
+
const selectedNames = selectToolNamesByIntent(input);
|
|
817
|
+
// Show tool filtering analysis if enabled
|
|
818
|
+
if (showFiltering) {
|
|
819
|
+
const savings = estimateTokenSavings(selectedNames.length, totalTools);
|
|
820
|
+
console.log(`\n[Tool Filtering] Selecting ${selectedNames.length}/${totalTools} tools:`);
|
|
821
|
+
console.log(` Selected: ${selectedNames.join(', ')}`);
|
|
822
|
+
console.log(` Estimated savings: ~${savings.saved} tokens (${savings.percentage}% reduction)`);
|
|
823
|
+
}
|
|
824
|
+
// Send to agent using stream() for real-time output
|
|
825
|
+
// Pass toolFilter to reduce token usage by only sending relevant tools
|
|
826
|
+
const spinner = new ThinkingSpinner();
|
|
827
|
+
try {
|
|
828
|
+
// Start the thinking spinner
|
|
829
|
+
const abortController = spinner.start();
|
|
830
|
+
let totalInputTokens = 0;
|
|
831
|
+
let totalOutputTokens = 0;
|
|
832
|
+
let llmCalls = 0;
|
|
833
|
+
let hasTextOutput = false;
|
|
834
|
+
let usedTools = false;
|
|
835
|
+
let spinnerStopped = false;
|
|
836
|
+
let lastToolInput = null;
|
|
837
|
+
let textBuffer = ''; // Buffer text for markdown rendering
|
|
838
|
+
for await (const event of agent.stream(input, {
|
|
839
|
+
toolFilter: selectedNames,
|
|
840
|
+
signal: abortController.signal,
|
|
841
|
+
})) {
|
|
842
|
+
// Check if aborted
|
|
843
|
+
if (spinner.isAborted()) {
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
if (event.type === 'llm_chunk') {
|
|
847
|
+
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
848
|
+
// Stop spinner on first text output
|
|
849
|
+
if (!spinnerStopped) {
|
|
850
|
+
spinner.stop();
|
|
851
|
+
spinnerStopped = true;
|
|
852
|
+
}
|
|
853
|
+
// Buffer text for markdown rendering at the end
|
|
854
|
+
textBuffer += event.chunk.text;
|
|
855
|
+
hasTextOutput = true;
|
|
856
|
+
}
|
|
857
|
+
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
858
|
+
// Update token tracking
|
|
859
|
+
const tokens = event.chunk.usage.inputTokens + event.chunk.usage.outputTokens;
|
|
860
|
+
spinner.addTokens(tokens);
|
|
861
|
+
totalInputTokens += event.chunk.usage.inputTokens;
|
|
862
|
+
totalOutputTokens += event.chunk.usage.outputTokens;
|
|
863
|
+
llmCalls++;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else if (event.type === 'tool_start') {
|
|
867
|
+
usedTools = true;
|
|
868
|
+
// Store input for when tool ends
|
|
869
|
+
lastToolInput = event.input;
|
|
870
|
+
// Show tool name in spinner while running (but not for todo tools - they're silent)
|
|
871
|
+
if (event.name !== 'todo_read' && event.name !== 'todo_write') {
|
|
872
|
+
spinner.setTool(event.name);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
else if (event.type === 'tool_end') {
|
|
876
|
+
// Clear all spinner lines (including todos) before printing tool result
|
|
877
|
+
spinner.clearForOutput();
|
|
878
|
+
// Format: ● ToolName(args)
|
|
879
|
+
// ⎿ Result summary
|
|
880
|
+
const toolName = event.name;
|
|
881
|
+
const toolInput = lastToolInput;
|
|
882
|
+
const result = event.result;
|
|
883
|
+
lastToolInput = null;
|
|
884
|
+
// Build argument summary (first string arg or key info)
|
|
885
|
+
let argSummary = '';
|
|
886
|
+
if (toolInput) {
|
|
887
|
+
// Common patterns: path, file_path, command, pattern
|
|
888
|
+
const path = toolInput.path || toolInput.file_path || toolInput.filePath;
|
|
889
|
+
const command = toolInput.command;
|
|
890
|
+
const pattern = toolInput.pattern;
|
|
891
|
+
if (typeof path === 'string') {
|
|
892
|
+
// Show just filename, not full path
|
|
893
|
+
const filename = path.split('/').pop() || path;
|
|
894
|
+
argSummary = filename;
|
|
895
|
+
}
|
|
896
|
+
else if (typeof command === 'string') {
|
|
897
|
+
// Truncate long commands
|
|
898
|
+
argSummary = command.length > 40 ? command.slice(0, 40) + '...' : command;
|
|
899
|
+
}
|
|
900
|
+
else if (typeof pattern === 'string') {
|
|
901
|
+
argSummary = pattern;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// Special handling for todo tools - silent, rendered with spinner
|
|
905
|
+
if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
|
|
906
|
+
// Normalize todos - handle different property names the agent might use
|
|
907
|
+
currentTodos = toolInput.todos.map((t) => ({
|
|
908
|
+
content: String(t.content || t.title || t.task || t.description || t.text || 'Untitled task'),
|
|
909
|
+
status: (['pending', 'in_progress', 'completed'].includes(String(t.status))
|
|
910
|
+
? t.status
|
|
911
|
+
: 'pending'),
|
|
912
|
+
activeForm: t.activeForm ? String(t.activeForm) : undefined,
|
|
913
|
+
}));
|
|
914
|
+
// Immediately render updated todos
|
|
915
|
+
spinner.setTool(null);
|
|
916
|
+
spinner.forceRender();
|
|
917
|
+
continue; // Skip normal tool result display
|
|
918
|
+
}
|
|
919
|
+
// Skip todo_read display - silent like todo_write
|
|
920
|
+
if (toolName === 'todo_read') {
|
|
921
|
+
spinner.setTool(null);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
// Format tool header
|
|
925
|
+
const toolHeader = argSummary
|
|
926
|
+
? pc.yellow(`● ${toolName}`) + pc.dim(`(${argSummary})`)
|
|
927
|
+
: pc.yellow(`● ${toolName}`);
|
|
928
|
+
// Format result summary
|
|
929
|
+
let resultSummary = '';
|
|
930
|
+
if (result) {
|
|
931
|
+
if (result.error) {
|
|
932
|
+
resultSummary = pc.red(`Error: ${String(result.error).slice(0, 60)}`);
|
|
933
|
+
}
|
|
934
|
+
else if (result.result !== undefined) {
|
|
935
|
+
resultSummary = formatToolResult(result.result);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// Print tool log
|
|
939
|
+
console.log(toolHeader);
|
|
940
|
+
if (resultSummary) {
|
|
941
|
+
console.log(pc.dim(' ⎿ ') + resultSummary);
|
|
942
|
+
}
|
|
943
|
+
console.log(''); // Blank line between tools
|
|
944
|
+
// Clear tool from spinner and immediately re-render with todos
|
|
945
|
+
spinner.setTool(null);
|
|
946
|
+
spinner.forceRender();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Make sure spinner is stopped
|
|
950
|
+
if (!spinnerStopped && !spinner.isAborted()) {
|
|
951
|
+
spinner.stop();
|
|
952
|
+
}
|
|
953
|
+
// Skip stats if aborted
|
|
954
|
+
if (spinner.isAborted()) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
// Output buffered text with markdown rendering
|
|
958
|
+
if (textBuffer) {
|
|
959
|
+
process.stdout.write(pc.bold(pc.green('Agent: ')));
|
|
960
|
+
console.log(renderMarkdown(textBuffer));
|
|
961
|
+
}
|
|
962
|
+
// Warn if tools were used but no text output (common with lite models)
|
|
963
|
+
if (usedTools && !hasTextOutput) {
|
|
964
|
+
console.log(pc.yellow('(No text response from model - try a more capable model like gemini-2.0-flash)'));
|
|
965
|
+
}
|
|
966
|
+
// Update session totals
|
|
967
|
+
sessionInputTokens += totalInputTokens;
|
|
968
|
+
sessionOutputTokens += totalOutputTokens;
|
|
969
|
+
sessionRequests++;
|
|
970
|
+
// Show token usage summary
|
|
971
|
+
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
|
972
|
+
const total = totalInputTokens + totalOutputTokens;
|
|
973
|
+
const sessionTotal = sessionInputTokens + sessionOutputTokens;
|
|
974
|
+
console.log(pc.dim(`\n[Tokens: ${total.toLocaleString()} (${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out) - ${llmCalls} LLM call(s)]`));
|
|
975
|
+
console.log(pc.dim(`[Session: ${sessionTotal.toLocaleString()} total (${sessionRequests} requests, avg ${Math.round(sessionTotal / sessionRequests).toLocaleString()}/req)]`));
|
|
976
|
+
}
|
|
977
|
+
console.log('');
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
spinner.stop();
|
|
981
|
+
// Don't show error for abort
|
|
982
|
+
if (error.name !== 'AbortError') {
|
|
983
|
+
console.error('\n' + pc.red('Error:'), error.message);
|
|
984
|
+
}
|
|
985
|
+
console.log('');
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// Create interactive input with slash command autocomplete
|
|
989
|
+
const promptText = pc.bold(pc.cyan('compilr>')) + ' ';
|
|
990
|
+
const handleInput = async (input) => {
|
|
991
|
+
// Stop input handler while processing
|
|
992
|
+
inputHandler?.stop();
|
|
993
|
+
// Process the input
|
|
994
|
+
await processInput(input);
|
|
995
|
+
// Restart input handler for next prompt
|
|
996
|
+
inputHandler?.start();
|
|
997
|
+
};
|
|
998
|
+
inputHandler = createInteractiveInput(promptText, (input) => {
|
|
999
|
+
handleInput(input);
|
|
1000
|
+
}, true, () => currentTodos);
|
|
1001
|
+
// Handle Ctrl+C gracefully
|
|
1002
|
+
process.on('SIGINT', () => {
|
|
1003
|
+
console.log('\n\nGoodbye!');
|
|
1004
|
+
inputHandler?.stop();
|
|
1005
|
+
process.exit(0);
|
|
1006
|
+
});
|
|
1007
|
+
// Start the interactive input
|
|
1008
|
+
inputHandler.start();
|
|
1009
|
+
}
|
|
1010
|
+
// Run
|
|
1011
|
+
main().catch((error) => {
|
|
1012
|
+
console.error('Fatal error:', error);
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
});
|