agent-termo 0.0.0-stage → 1.0.1
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/.claude/agents/backend-architecture-designer.md +1250 -0
- package/.claude/agents/backend-expert.md +1731 -0
- package/.claude/agents/deployment-expert.md +1126 -0
- package/.claude/agents/dev-environment-initiator.md +788 -0
- package/.claude/agents/frontend-expert.md +1343 -0
- package/.claude/agents/frontend-planner.md +1044 -0
- package/.claude/agents/git-manager.md +811 -0
- package/.claude/agents/project-manager.md +843 -0
- package/.claude/agents/security-expert.md +835 -0
- package/.claude/agents/tech-stack-expert.md +1357 -0
- package/.claude/agents/test-agent.md +1372 -0
- package/.claude/agents/uiux-designer.md +955 -0
- package/.claude/commands/build.md +5 -0
- package/.claude/commands/deploy.md +3 -0
- package/.claude/commands/feature.md +3 -0
- package/.claude/commands/fix.md +3 -0
- package/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/README.md +163 -3
- package/dist/.claude/agents/backend-architecture-designer.md +1250 -0
- package/dist/.claude/agents/backend-expert.md +1731 -0
- package/dist/.claude/agents/deployment-expert.md +1126 -0
- package/dist/.claude/agents/dev-environment-initiator.md +788 -0
- package/dist/.claude/agents/frontend-expert.md +1343 -0
- package/dist/.claude/agents/frontend-planner.md +1044 -0
- package/dist/.claude/agents/git-manager.md +811 -0
- package/dist/.claude/agents/project-manager.md +843 -0
- package/dist/.claude/agents/security-expert.md +835 -0
- package/dist/.claude/agents/tech-stack-expert.md +1357 -0
- package/dist/.claude/agents/test-agent.md +1372 -0
- package/dist/.claude/agents/uiux-designer.md +955 -0
- package/dist/.claude/commands/build.md +5 -0
- package/dist/.claude/commands/deploy.md +3 -0
- package/dist/.claude/commands/feature.md +3 -0
- package/dist/.claude/commands/fix.md +3 -0
- package/dist/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/dist/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/dist/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/dist/cli.js +210 -0
- package/dist/cli.js.map +7 -0
- package/dist/main.js +173 -0
- package/dist/main.js.map +7 -0
- package/package.json +90 -6
- package/src/agents/index.ts +94 -0
- package/src/cli.ts +155 -0
- package/src/diff/index.ts +255 -0
- package/src/llm/client.ts +192 -0
- package/src/llm/tools.ts +206 -0
- package/src/main.ts +427 -0
- package/src/memory/index.ts +130 -0
- package/src/tools/index.ts +371 -0
- package/src/types/index.ts +151 -0
- package/src/ui/index.ts +519 -0
- package/src/utils/ansi.ts +134 -0
- package/src/utils/config.ts +118 -0
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Terminal UI — chalk-based, clean CLI styling with diff viewer
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import Chalk from 'chalk';
|
|
6
|
+
import type { ProposedFileChange } from '../types/index.js';
|
|
7
|
+
import { loadAgentTermoMemory, loadAgentTermoTimeline } from '../memory/index.js';
|
|
8
|
+
import type { ConversationMessage } from '../types/index.js';
|
|
9
|
+
|
|
10
|
+
const c: typeof Chalk = Chalk as unknown as typeof Chalk;
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// WELCOME BANNER
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export function printWelcome(projectRoot: string): void {
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(c.magenta.bold(' AGENT-TERMO'));
|
|
19
|
+
console.log(c.dim(' AI-Powered Terminal Coding Agent'));
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(c.dim(' Model: ') + c.cyan.bold('Nvidia NIM (OpenAI-compatible)'));
|
|
22
|
+
console.log(c.dim(' Project: ') + c.white(projectRoot));
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(c.dim(' Commands:'));
|
|
25
|
+
console.log(
|
|
26
|
+
' ' + c.cyan('/help') + c.dim(' — show all commands') + '\n' +
|
|
27
|
+
' ' + c.cyan('/clear') + c.dim(' — clear conversation & screen') + '\n' +
|
|
28
|
+
' ' + c.cyan('/memory') + c.dim(' — view Agent-Termo memory') + '\n' +
|
|
29
|
+
' ' + c.cyan('/timeline') + c.dim(' — view recent activity') + '\n' +
|
|
30
|
+
' ' + c.cyan('/model') + c.dim(' — show current AI model') + '\n' +
|
|
31
|
+
' ' + c.cyan('/agents') + c.dim(' — list all agents') + '\n' +
|
|
32
|
+
' ' + c.cyan('/history') + c.dim(' — show message count') + '\n' +
|
|
33
|
+
' ' + c.cyan('/exit') + c.dim(' — quit'),
|
|
34
|
+
);
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(c.dim(' ────────────────────────────────────────────────────────────────'));
|
|
37
|
+
console.log('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// MESSAGE DISPLAY
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
export function printUserMessage(msg: ConversationMessage): void {
|
|
45
|
+
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
|
46
|
+
console.log('\n' + c.green('[' + time + '] You'));
|
|
47
|
+
console.log(c.dim('─'.repeat(80)));
|
|
48
|
+
console.log(c.green(msg.content) + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function printAssistantMessage(content: string): void {
|
|
52
|
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
53
|
+
console.log('\n' + c.magenta('[' + time + '] Agent-Termo'));
|
|
54
|
+
console.log(c.dim('─'.repeat(80)));
|
|
55
|
+
console.log(c.magenta(content) + '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// TOOL CALL DISPLAY
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export function printToolCall(toolName: string, args: Record<string, unknown>): void {
|
|
63
|
+
const argsSummary = Object.entries(args)
|
|
64
|
+
.map(([k, v]) => k + '=' + JSON.stringify(v))
|
|
65
|
+
.join(', ');
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(c.blue('[Tool] ') + c.cyan(toolName));
|
|
68
|
+
const truncated = argsSummary.slice(0, 200);
|
|
69
|
+
if (truncated) console.log(c.dim(' ' + truncated));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function printToolResult(
|
|
73
|
+
toolName: string,
|
|
74
|
+
result: { success: boolean; output?: string; error?: string },
|
|
75
|
+
parsedOutput?: { needsApproval?: boolean; proposedChange?: ProposedFileChange },
|
|
76
|
+
): void {
|
|
77
|
+
// If the tool result contains a proposed change, show the diff + prompt
|
|
78
|
+
if (parsedOutput?.needsApproval && parsedOutput.proposedChange) {
|
|
79
|
+
console.log('');
|
|
80
|
+
printDiffViewer(parsedOutput.proposedChange);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (result.success) {
|
|
85
|
+
const out = result.output
|
|
86
|
+
? result.output.slice(0, 200) + (result.output.length > 200 ? '...' : '')
|
|
87
|
+
: '(success)';
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(c.green('[Done] ') + c.cyan(toolName) + ' ' + c.green('OK'));
|
|
90
|
+
const short = out.length > 80 ? out.slice(0, 80) + '...' : out;
|
|
91
|
+
console.log(c.green(' ' + short));
|
|
92
|
+
} else {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(c.red('[Error] ') + c.red(toolName + ': ' + (result.error?.slice(0, 200) ?? 'unknown error')));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// DIFF VIEWER — Claude Code-style change display
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
export function printDiffViewer(change: ProposedFileChange): void {
|
|
103
|
+
const diff = change.diff;
|
|
104
|
+
|
|
105
|
+
// File header
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(c.bold(c.cyan(' ─── ' + diff.file + ' ───')));
|
|
108
|
+
|
|
109
|
+
// Stat line
|
|
110
|
+
const actionLabel = change.isDeletion
|
|
111
|
+
? c.red('[- DELETE]')
|
|
112
|
+
: change.isNewFile
|
|
113
|
+
? c.green('[+ NEW]')
|
|
114
|
+
: c.blue('[~ MODIFY]');
|
|
115
|
+
const stat = c.dim('+' + diff.additions) + ' ' + c.red('-' + diff.deletions);
|
|
116
|
+
console.log(' ' + actionLabel + ' ' + stat);
|
|
117
|
+
|
|
118
|
+
// Diff lines
|
|
119
|
+
for (const line of diff.lines) {
|
|
120
|
+
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
|
|
121
|
+
const color = line.type === 'add' ? c.green : line.type === 'remove' ? c.red : c.dim;
|
|
122
|
+
const lineNum = line.lineNumber > 0 ? String(line.lineNumber).padStart(4) : ' ';
|
|
123
|
+
console.log(' ' + color(prefix + lineNum + '│' + line.content));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Summary footer
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(c.dim(' ── ') + c.green('+' + diff.additions) + c.dim(' added, ') + c.red('-' + diff.deletions) + c.dim(' removed ') + c.dim('─'));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// ACCEPT / REJECT PROMPT — Claude Code-style keyboard interaction
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
export async function promptAcceptReject(message: string = 'Accept this change?'): Promise<'accept' | 'reject'> {
|
|
136
|
+
process.stdout.write('\n' + c.yellow(message) + ' ');
|
|
137
|
+
process.stdout.write(c.dim('[') + c.green('Y') + c.dim('/') + c.red('n') + c.dim('] '));
|
|
138
|
+
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
// Allow a flag to auto-reject (used when detecting stale REPL listeners)
|
|
141
|
+
let alreadyHandled = false;
|
|
142
|
+
|
|
143
|
+
const onData = (key: string): void => {
|
|
144
|
+
if (alreadyHandled) return;
|
|
145
|
+
alreadyHandled = true;
|
|
146
|
+
const keyName = getKeyName(key);
|
|
147
|
+
|
|
148
|
+
switch (keyName) {
|
|
149
|
+
case 'y':
|
|
150
|
+
case 'Y':
|
|
151
|
+
case 'enter':
|
|
152
|
+
case 'return':
|
|
153
|
+
process.stdout.write(c.green('yes') + '\n');
|
|
154
|
+
cleanup();
|
|
155
|
+
resolve('accept');
|
|
156
|
+
break;
|
|
157
|
+
case 'n':
|
|
158
|
+
case 'N':
|
|
159
|
+
case 'escape':
|
|
160
|
+
process.stdout.write(c.red('no') + '\n');
|
|
161
|
+
cleanup();
|
|
162
|
+
resolve('reject');
|
|
163
|
+
break;
|
|
164
|
+
case 'ctrl+c':
|
|
165
|
+
process.stdout.write(c.red('cancelled') + '\n');
|
|
166
|
+
cleanup();
|
|
167
|
+
resolve('reject');
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
// Ignore other keys, re-arm for next key
|
|
171
|
+
alreadyHandled = false;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const cleanup = (): void => {
|
|
177
|
+
process.stdin.setRawMode(false);
|
|
178
|
+
process.stdin.pause();
|
|
179
|
+
process.stdin.removeListener('data', onData);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Remove any other stale data listeners to prevent double-handling
|
|
183
|
+
const listeners = process.stdin.listenerCount('data');
|
|
184
|
+
if (listeners > 5) {
|
|
185
|
+
// Sanity: if there are too many listeners, something went wrong
|
|
186
|
+
process.stdin.removeAllListeners('data');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.stdin.setRawMode(true);
|
|
190
|
+
process.stdin.resume();
|
|
191
|
+
process.stdin.setEncoding('utf8');
|
|
192
|
+
process.stdin.on('data', onData);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// STATUS MESSAGES
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
export function printError(message: string, hint?: string): void {
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(c.red('[Error] ' + message));
|
|
203
|
+
if (hint) console.log(c.dim(' Hint: ' + hint));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function printSuccess(message: string): void {
|
|
207
|
+
console.log(c.green('Done: ' + message));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function printInfo(message: string): void {
|
|
211
|
+
console.log(c.blue('[Info] ') + message);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// AGENT-TERMO MEMORY + TIMELINE DISPLAY
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
function clearCurrentLine(): void {
|
|
219
|
+
process.stdout.write('\x1b[2K');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function printMemoryFile(): Promise<void> {
|
|
223
|
+
const mem = loadAgentTermoMemory();
|
|
224
|
+
if (!mem) {
|
|
225
|
+
process.stdout.write('\n');
|
|
226
|
+
clearCurrentLine();
|
|
227
|
+
process.stdout.write(c.blue('[Info] ') + 'Agent-Termo memory is empty. (Normal for new sessions.)\n');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write('\n');
|
|
231
|
+
clearCurrentLine();
|
|
232
|
+
process.stdout.write(c.bold('=== AGENT-TERMO MEMORY ===') + '\n');
|
|
233
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
234
|
+
process.stdout.write(mem + '\n');
|
|
235
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function printTimelineFile(): Promise<void> {
|
|
239
|
+
const tl = loadAgentTermoTimeline();
|
|
240
|
+
if (!tl || tl.trim().length === 0) {
|
|
241
|
+
process.stdout.write('\n');
|
|
242
|
+
clearCurrentLine();
|
|
243
|
+
process.stdout.write(c.blue('[Info] ') + 'Timeline is empty.\n');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const lines = tl.split('\n');
|
|
247
|
+
const totalLines = lines.length;
|
|
248
|
+
const recentCount = Math.min(40, totalLines);
|
|
249
|
+
const recent = lines.slice(-recentCount).join('\n');
|
|
250
|
+
process.stdout.write('\n');
|
|
251
|
+
clearCurrentLine();
|
|
252
|
+
process.stdout.write(c.bold('=== AGENT-TERMO TIMELINE (recent entries) ===') + '\n');
|
|
253
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
254
|
+
process.stdout.write(recent + '\n');
|
|
255
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
256
|
+
if (totalLines > recentCount) {
|
|
257
|
+
process.stdout.write(c.dim('... (' + (totalLines - recentCount) + ' earlier entries hidden)') + '\n');
|
|
258
|
+
}
|
|
259
|
+
process.stdout.write('\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// HELP + AGENTS DISPLAY
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
export function printHelp(): void {
|
|
267
|
+
process.stdout.write('\n' + c.bold('Agent-Termo ') + c.dim('— available commands') + '\n');
|
|
268
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
269
|
+
console.log(' ' + c.cyan('/help') + c.dim(' show all commands'));
|
|
270
|
+
console.log(' ' + c.cyan('/clear') + c.dim(' clear conversation & screen'));
|
|
271
|
+
console.log(' ' + c.cyan('/memory') + c.dim(' view Agent-Termo memory'));
|
|
272
|
+
console.log(' ' + c.cyan('/timeline') + c.dim(' view recent activity'));
|
|
273
|
+
console.log(' ' + c.cyan('/model') + c.dim(' show current AI model'));
|
|
274
|
+
console.log(' ' + c.cyan('/agents') + c.dim(' list all agents'));
|
|
275
|
+
console.log(' ' + c.cyan('/history') + c.dim(' show message count'));
|
|
276
|
+
console.log(' ' + c.cyan('/exit') + c.dim(' quit'));
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function printAgentsList(agents: Array<{ name: string; description: string }>): void {
|
|
281
|
+
process.stdout.write('\n' + c.bold('Available Agents:') + '\n');
|
|
282
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
283
|
+
for (const a of agents) {
|
|
284
|
+
const short = a.description.length > 40 ? a.description.slice(0, 40) + '...' : a.description;
|
|
285
|
+
process.stdout.write(' ' + c.cyan(a.name.padEnd(28)) + ' ' + c.dim(short) + '\n');
|
|
286
|
+
}
|
|
287
|
+
process.stdout.write(c.dim('─'.repeat(80)) + '\n\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// =============================================================================
|
|
291
|
+
// INPUT STATE + REPL LOOP
|
|
292
|
+
// =============================================================================
|
|
293
|
+
|
|
294
|
+
class InputState {
|
|
295
|
+
line = '';
|
|
296
|
+
cursor = 0;
|
|
297
|
+
histIdx = -1;
|
|
298
|
+
history: string[] = [];
|
|
299
|
+
boundOnData: ((key: string) => void) | null = null;
|
|
300
|
+
|
|
301
|
+
constructor() { this.history = []; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// Cursor helpers
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
export function saveCursorPosition(): void {
|
|
309
|
+
process.stdout.write('\x1b[s');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function restoreCursorPosition(): void {
|
|
313
|
+
process.stdout.write('\x1b[u');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function moveToFreshLine(): void {
|
|
317
|
+
process.stdout.write(' '.repeat(200) + '\r');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create an input handler that manages stdin raw mode for the REPL.
|
|
322
|
+
* IMPORTANT: The listener is removed when Enter/Return is pressed to
|
|
323
|
+
* prevent stale listeners from interfering with promptAcceptReject().
|
|
324
|
+
*/
|
|
325
|
+
export function createInput(): {
|
|
326
|
+
question: (prompt: string) => Promise<string>;
|
|
327
|
+
close: () => void;
|
|
328
|
+
showPrompt: () => void;
|
|
329
|
+
} {
|
|
330
|
+
let closeFn: (() => void) | null = null;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
async question(_prompt: string): Promise<string> {
|
|
334
|
+
return new Promise<string>((resolve) => {
|
|
335
|
+
process.stdin.setRawMode(true);
|
|
336
|
+
process.stdin.resume();
|
|
337
|
+
process.stdin.setEncoding('utf8');
|
|
338
|
+
process.stdin.setMaxListeners(20);
|
|
339
|
+
|
|
340
|
+
const state = new InputState();
|
|
341
|
+
const inputPrompt = c.magenta('> ');
|
|
342
|
+
|
|
343
|
+
function finalize(l: string): void {
|
|
344
|
+
if (l.trim()) {
|
|
345
|
+
state.history.unshift(l);
|
|
346
|
+
if (state.history.length > 50) state.history.pop();
|
|
347
|
+
}
|
|
348
|
+
state.histIdx = -1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
closeFn = (): void => {
|
|
352
|
+
process.stdin.setRawMode(false);
|
|
353
|
+
process.stdin.pause();
|
|
354
|
+
finalize('');
|
|
355
|
+
if (state.boundOnData) {
|
|
356
|
+
process.stdin.removeListener('data', state.boundOnData);
|
|
357
|
+
state.boundOnData = null;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
moveToFreshLine();
|
|
362
|
+
process.stdout.write(inputPrompt);
|
|
363
|
+
saveCursorPosition();
|
|
364
|
+
process.stdout.write('\x1b[?25h');
|
|
365
|
+
|
|
366
|
+
function onData(key: string): void {
|
|
367
|
+
const keyName = getKeyName(key);
|
|
368
|
+
|
|
369
|
+
if (keyName === 'return' || keyName === 'enter') {
|
|
370
|
+
finalize(state.line);
|
|
371
|
+
process.stdout.write('\n');
|
|
372
|
+
// CRITICAL: Remove listener so promptAcceptReject doesn't get double-handled
|
|
373
|
+
process.stdin.removeListener('data', state.boundOnData!);
|
|
374
|
+
state.boundOnData = null;
|
|
375
|
+
process.stdin.setRawMode(false);
|
|
376
|
+
process.stdin.pause();
|
|
377
|
+
resolve(state.line);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (keyName === 'ctrl+c') {
|
|
382
|
+
finalize(state.line);
|
|
383
|
+
process.stdout.write('\n');
|
|
384
|
+
process.stdin.removeListener('data', state.boundOnData!);
|
|
385
|
+
state.boundOnData = null;
|
|
386
|
+
process.stdin.setRawMode(false);
|
|
387
|
+
process.stdin.pause();
|
|
388
|
+
process.exit(0);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (keyName === 'up') {
|
|
393
|
+
if (state.history.length > 0 && state.histIdx < state.history.length - 1) {
|
|
394
|
+
state.histIdx++;
|
|
395
|
+
state.line = state.history[state.histIdx] ?? '';
|
|
396
|
+
state.cursor = state.line.length;
|
|
397
|
+
restoreCursorPosition();
|
|
398
|
+
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (keyName === 'down') {
|
|
404
|
+
if (state.histIdx > 0) {
|
|
405
|
+
state.histIdx--;
|
|
406
|
+
state.line = state.history[state.histIdx] ?? '';
|
|
407
|
+
} else {
|
|
408
|
+
state.histIdx = -1;
|
|
409
|
+
state.line = '';
|
|
410
|
+
}
|
|
411
|
+
state.cursor = state.line.length;
|
|
412
|
+
restoreCursorPosition();
|
|
413
|
+
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (keyName === 'backspace') {
|
|
418
|
+
if (state.cursor > 0) {
|
|
419
|
+
state.line = state.line.slice(0, state.cursor - 1) + state.line.slice(state.cursor);
|
|
420
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
421
|
+
restoreCursorPosition();
|
|
422
|
+
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (keyName === 'left') {
|
|
428
|
+
if (state.cursor > 0) {
|
|
429
|
+
state.cursor--;
|
|
430
|
+
restoreCursorPosition();
|
|
431
|
+
process.stdout.write('\r' + inputPrompt + state.line);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (keyName === 'right') {
|
|
437
|
+
if (state.cursor < state.line.length) {
|
|
438
|
+
state.cursor++;
|
|
439
|
+
restoreCursorPosition();
|
|
440
|
+
process.stdout.write('\r' + inputPrompt + state.line);
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (key.length === 1 && key.charCodeAt(0) >= 0x20) {
|
|
446
|
+
state.line = state.line.slice(0, state.cursor) + key + state.line.slice(state.cursor);
|
|
447
|
+
state.cursor++;
|
|
448
|
+
restoreCursorPosition();
|
|
449
|
+
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
state.boundOnData = (key: string) => onData(key);
|
|
454
|
+
process.stdin.on('data', state.boundOnData);
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
close(): void { if (closeFn) closeFn(); },
|
|
459
|
+
|
|
460
|
+
showPrompt(): void {
|
|
461
|
+
moveToFreshLine();
|
|
462
|
+
process.stdout.write(c.magenta('> '));
|
|
463
|
+
saveCursorPosition();
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// =============================================================================
|
|
469
|
+
// SPINNER
|
|
470
|
+
// =============================================================================
|
|
471
|
+
|
|
472
|
+
let spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
473
|
+
|
|
474
|
+
export function startSpinner(message: string): void {
|
|
475
|
+
spinnerInterval = setInterval(() => {
|
|
476
|
+
process.stdout.write('\r' + ' ' + c.dim(message) + '... ');
|
|
477
|
+
}, 80);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function stopSpinner(success?: boolean): void {
|
|
481
|
+
if (spinnerInterval) {
|
|
482
|
+
clearInterval(spinnerInterval);
|
|
483
|
+
spinnerInterval = null;
|
|
484
|
+
}
|
|
485
|
+
if (success === true) {
|
|
486
|
+
process.stdout.write('\r' + c.green('done.') + '\n');
|
|
487
|
+
} else if (success === false) {
|
|
488
|
+
process.stdout.write('\r' + c.red('failed.') + '\n');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function clearScreen(): void {
|
|
493
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// =============================================================================
|
|
497
|
+
// KEY NAME PARSER
|
|
498
|
+
// =============================================================================
|
|
499
|
+
|
|
500
|
+
export function getKeyName(ch: string): string {
|
|
501
|
+
if (ch === '\r') return 'return';
|
|
502
|
+
if (ch === '\n') return 'enter';
|
|
503
|
+
if (ch === '\x03') return 'ctrl+c';
|
|
504
|
+
if (ch === '\x1b') return 'escape';
|
|
505
|
+
if (ch === '\x7f' || ch === '\b') return 'backspace';
|
|
506
|
+
if (ch === '\x04') return 'ctrl+d';
|
|
507
|
+
if (ch === '\t') return 'tab';
|
|
508
|
+
if (ch === '\x1b[A') return 'up';
|
|
509
|
+
if (ch === '\x1b[B') return 'down';
|
|
510
|
+
if (ch === '\x1b[C') return 'right';
|
|
511
|
+
if (ch === '\x1b[D') return 'left';
|
|
512
|
+
if (ch === '\x1b[3~') return 'delete';
|
|
513
|
+
if (ch === '\x1b[F') return 'end';
|
|
514
|
+
if (ch === '\x1b[H') return 'home';
|
|
515
|
+
if (ch === '\x1b[5~') return 'pageup';
|
|
516
|
+
if (ch === '\x1b[6~') return 'pagedown';
|
|
517
|
+
if (ch.startsWith('\x1b[')) return ch;
|
|
518
|
+
return ch.length === 1 ? ch : ch;
|
|
519
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// ANSI Color Utilities
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
// Design System colors (ANSI 256-color codes for cross-platform terminal)
|
|
6
|
+
export const COLORS = {
|
|
7
|
+
// Primary: #A855F7 → ANSI 135
|
|
8
|
+
primary: '\x1b[38;5;135m',
|
|
9
|
+
primaryBg: '\x1b[48;5;57m',
|
|
10
|
+
|
|
11
|
+
// Semantic
|
|
12
|
+
success: '\x1b[38;5;82m', // green-500
|
|
13
|
+
warning: '\x1b[38;5;214m', // amber-500
|
|
14
|
+
error: '\x1b[38;5;203m', // red-500
|
|
15
|
+
info: '\x1b[38;5;75m', // blue-500
|
|
16
|
+
|
|
17
|
+
// Dark theme neutrals
|
|
18
|
+
background: '\x1b[48;5;17m', // slate-900
|
|
19
|
+
surface: '\x1b[48;5;30m', // slate-800
|
|
20
|
+
elevated: '\x1b[48;5;31m', // slate-700
|
|
21
|
+
border: '\x1b[38;5;240m', // #646B7B
|
|
22
|
+
borderLight:'\x1b[38;5;241m', // #7C8796
|
|
23
|
+
|
|
24
|
+
// Text
|
|
25
|
+
text: '\x1b[38;5;255m', // slate-50
|
|
26
|
+
textSecondary:'\x1b[38;5;188m',// slate-300
|
|
27
|
+
muted: '\x1b[38;5;250m', // slate-400
|
|
28
|
+
dim: '\x1b[38;5;244m', // slate-400
|
|
29
|
+
|
|
30
|
+
// Agent badge colors
|
|
31
|
+
manager: '\x1b[38;5;135m', // purple
|
|
32
|
+
architect: '\x1b[38;5;75m', // blue
|
|
33
|
+
expert: '\x1b[38;5;82m', // green
|
|
34
|
+
security: '\x1b[38;5;214m', // amber
|
|
35
|
+
|
|
36
|
+
// Code syntax
|
|
37
|
+
codeKeyword: '\x1b[38;5;219m', // pink-400
|
|
38
|
+
codeString: '\x1b[38;5;114m', // green-400
|
|
39
|
+
codeComment: '\x1b[38;5;245m', // gray-400
|
|
40
|
+
codeFunction: '\x1b[38;5;75m', // blue-400
|
|
41
|
+
codeType: '\x1b[38;5;171m', // emerald-400
|
|
42
|
+
|
|
43
|
+
// Roles
|
|
44
|
+
userMsg: '\x1b[38;5;82m', // green for user
|
|
45
|
+
aiMsg: '\x1b[38;5;135m', // purple for AI
|
|
46
|
+
|
|
47
|
+
// Decorators
|
|
48
|
+
accent: '\x1b[38;5;75m', // blue accent
|
|
49
|
+
|
|
50
|
+
// Reset
|
|
51
|
+
reset: '\x1b[0m',
|
|
52
|
+
bright: '\x1b[1m',
|
|
53
|
+
dim_text: '\x1b[2m',
|
|
54
|
+
italic: '\x1b[3m',
|
|
55
|
+
underline:'\x1b[4m',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const BG = {
|
|
59
|
+
surface: '\x1b[48;5;30m',
|
|
60
|
+
elevated: '\x1b[48;5;31m',
|
|
61
|
+
primaryBg: '\x1b[48;5;57m',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const FMT = {
|
|
65
|
+
bold: '\x1b[1m',
|
|
66
|
+
dim: '\x1b[2m',
|
|
67
|
+
ita: '\x1b[3m',
|
|
68
|
+
ul: '\x1b[4m',
|
|
69
|
+
reset: '\x1b[0m',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Escape helpers
|
|
73
|
+
export function color(colorCode: string, text: string): string {
|
|
74
|
+
return `${colorCode}${text}${COLORS.reset}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function bg(colorCode: string, text: string): string {
|
|
78
|
+
return `${colorCode}${text}${COLORS.reset}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function bold(text: string): string {
|
|
82
|
+
return `${FMT.bold}${text}${FMT.reset}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function dim(text: string): string {
|
|
86
|
+
return `${FMT.dim}${text}${FMT.reset}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Spinner frames (80ms per frame, 12 chars cycle)
|
|
90
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
91
|
+
let spinnerIndex = 0;
|
|
92
|
+
|
|
93
|
+
export function spinnerFrame(): string {
|
|
94
|
+
const frame = SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length];
|
|
95
|
+
spinnerIndex++;
|
|
96
|
+
return frame ?? ' ';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resetSpinner(): void {
|
|
100
|
+
spinnerIndex = 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Box drawing helpers
|
|
104
|
+
export function hbar(length: number, char = '─'): string {
|
|
105
|
+
return char.repeat(length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function clamp(text: string, maxLength: number): string {
|
|
109
|
+
if (text.length <= maxLength) return text;
|
|
110
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Progress bar
|
|
114
|
+
export function progressBar(percent: number, width = 30): string {
|
|
115
|
+
const filled = Math.round((percent / 100) * width);
|
|
116
|
+
const empty = width - filled;
|
|
117
|
+
return `[${COLORS.success}${'█'.repeat(filled)}${'░'.repeat(empty)}${COLORS.reset}]`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Agent badge color
|
|
121
|
+
export function agentColor(agentName: string): string {
|
|
122
|
+
const lower = agentName.toLowerCase();
|
|
123
|
+
if (lower.includes('project-manager')) return COLORS.manager;
|
|
124
|
+
if (lower.includes('architecture') || lower.includes('stack')) return COLORS.architect;
|
|
125
|
+
if (lower.includes('security') || lower.includes('deploy')) return COLORS.security;
|
|
126
|
+
return COLORS.expert;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clear line (ANSI)
|
|
130
|
+
export const clearLine = '\x1b[2K\r';
|
|
131
|
+
export const clearScreen = '\x1b[2J';
|
|
132
|
+
export const cursorHome = '\x1b[H';
|
|
133
|
+
export const hideCursor = '\x1b[?25l';
|
|
134
|
+
export const showCursor = '\x1b[?25h';
|