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.
Files changed (56) hide show
  1. package/.claude/agents/backend-architecture-designer.md +1250 -0
  2. package/.claude/agents/backend-expert.md +1731 -0
  3. package/.claude/agents/deployment-expert.md +1126 -0
  4. package/.claude/agents/dev-environment-initiator.md +788 -0
  5. package/.claude/agents/frontend-expert.md +1343 -0
  6. package/.claude/agents/frontend-planner.md +1044 -0
  7. package/.claude/agents/git-manager.md +811 -0
  8. package/.claude/agents/project-manager.md +843 -0
  9. package/.claude/agents/security-expert.md +835 -0
  10. package/.claude/agents/tech-stack-expert.md +1357 -0
  11. package/.claude/agents/test-agent.md +1372 -0
  12. package/.claude/agents/uiux-designer.md +955 -0
  13. package/.claude/commands/build.md +5 -0
  14. package/.claude/commands/deploy.md +3 -0
  15. package/.claude/commands/feature.md +3 -0
  16. package/.claude/commands/fix.md +3 -0
  17. package/.claude/skills/pdf-reader/.skillfish.json +10 -0
  18. package/.claude/skills/pdf-reader/SKILL.md +94 -0
  19. package/.claude/skills/pdf-reader/pdf-reader +203 -0
  20. package/README.md +163 -3
  21. package/dist/.claude/agents/backend-architecture-designer.md +1250 -0
  22. package/dist/.claude/agents/backend-expert.md +1731 -0
  23. package/dist/.claude/agents/deployment-expert.md +1126 -0
  24. package/dist/.claude/agents/dev-environment-initiator.md +788 -0
  25. package/dist/.claude/agents/frontend-expert.md +1343 -0
  26. package/dist/.claude/agents/frontend-planner.md +1044 -0
  27. package/dist/.claude/agents/git-manager.md +811 -0
  28. package/dist/.claude/agents/project-manager.md +843 -0
  29. package/dist/.claude/agents/security-expert.md +835 -0
  30. package/dist/.claude/agents/tech-stack-expert.md +1357 -0
  31. package/dist/.claude/agents/test-agent.md +1372 -0
  32. package/dist/.claude/agents/uiux-designer.md +955 -0
  33. package/dist/.claude/commands/build.md +5 -0
  34. package/dist/.claude/commands/deploy.md +3 -0
  35. package/dist/.claude/commands/feature.md +3 -0
  36. package/dist/.claude/commands/fix.md +3 -0
  37. package/dist/.claude/skills/pdf-reader/.skillfish.json +10 -0
  38. package/dist/.claude/skills/pdf-reader/SKILL.md +94 -0
  39. package/dist/.claude/skills/pdf-reader/pdf-reader +203 -0
  40. package/dist/cli.js +210 -0
  41. package/dist/cli.js.map +7 -0
  42. package/dist/main.js +173 -0
  43. package/dist/main.js.map +7 -0
  44. package/package.json +90 -6
  45. package/src/agents/index.ts +94 -0
  46. package/src/cli.ts +155 -0
  47. package/src/diff/index.ts +255 -0
  48. package/src/llm/client.ts +192 -0
  49. package/src/llm/tools.ts +206 -0
  50. package/src/main.ts +427 -0
  51. package/src/memory/index.ts +130 -0
  52. package/src/tools/index.ts +371 -0
  53. package/src/types/index.ts +151 -0
  54. package/src/ui/index.ts +519 -0
  55. package/src/utils/ansi.ts +134 -0
  56. package/src/utils/config.ts +118 -0
@@ -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';