thatgfsj-code 0.3.0 → 0.4.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 (60) hide show
  1. package/dist/cmd/index.js +56 -8
  2. package/dist/cmd/index.js.map +1 -1
  3. package/dist/config/index.d.ts +5 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +22 -4
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/providers.d.ts +5 -10
  8. package/dist/config/providers.d.ts.map +1 -1
  9. package/dist/config/providers.js +152 -78
  10. package/dist/config/providers.js.map +1 -1
  11. package/dist/config/types.d.ts +1 -1
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/llm/anthropic.d.ts +24 -6
  14. package/dist/llm/anthropic.d.ts.map +1 -1
  15. package/dist/llm/anthropic.js +136 -46
  16. package/dist/llm/anthropic.js.map +1 -1
  17. package/dist/llm/gemini.d.ts +4 -4
  18. package/dist/llm/gemini.d.ts.map +1 -1
  19. package/dist/llm/gemini.js +67 -30
  20. package/dist/llm/gemini.js.map +1 -1
  21. package/dist/llm/index.d.ts +10 -23
  22. package/dist/llm/index.d.ts.map +1 -1
  23. package/dist/llm/index.js +73 -78
  24. package/dist/llm/index.js.map +1 -1
  25. package/dist/llm/openai.d.ts +19 -6
  26. package/dist/llm/openai.d.ts.map +1 -1
  27. package/dist/llm/openai.js +86 -35
  28. package/dist/llm/openai.js.map +1 -1
  29. package/dist/llm/provider.d.ts +11 -16
  30. package/dist/llm/provider.d.ts.map +1 -1
  31. package/dist/tui/input.d.ts +0 -1
  32. package/dist/tui/input.d.ts.map +1 -1
  33. package/dist/tui/input.js +2 -5
  34. package/dist/tui/input.js.map +1 -1
  35. package/dist/tui/output.d.ts +27 -12
  36. package/dist/tui/output.d.ts.map +1 -1
  37. package/dist/tui/output.js +176 -40
  38. package/dist/tui/output.js.map +1 -1
  39. package/dist/tui/repl.d.ts +0 -10
  40. package/dist/tui/repl.d.ts.map +1 -1
  41. package/dist/tui/repl.js +76 -42
  42. package/dist/tui/repl.js.map +1 -1
  43. package/dist/tui/welcome.d.ts +2 -8
  44. package/dist/tui/welcome.d.ts.map +1 -1
  45. package/dist/tui/welcome.js +101 -51
  46. package/dist/tui/welcome.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/cmd/index.ts +49 -8
  49. package/src/config/index.ts +21 -4
  50. package/src/config/providers.ts +153 -79
  51. package/src/config/types.ts +11 -5
  52. package/src/llm/anthropic.ts +148 -51
  53. package/src/llm/gemini.ts +81 -39
  54. package/src/llm/index.ts +75 -83
  55. package/src/llm/openai.ts +96 -41
  56. package/src/llm/provider.ts +12 -16
  57. package/src/tui/input.ts +2 -5
  58. package/src/tui/output.ts +193 -40
  59. package/src/tui/repl.ts +82 -43
  60. package/src/tui/welcome.ts +110 -53
@@ -3,32 +3,28 @@
3
3
  * All providers implement this interface
4
4
  */
5
5
 
6
- import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
6
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
7
7
  import type { Tool } from '../tools/types.js';
8
8
 
9
+ export interface StreamChunk {
10
+ type: 'text' | 'tool_calls';
11
+ content?: string;
12
+ toolCalls?: ToolCall[];
13
+ }
14
+
9
15
  export interface LLMProvider {
10
- /** Provider name for logging */
11
16
  readonly name: string;
12
17
 
13
- /**
14
- * Non-streaming chat completion
15
- */
16
- chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
18
+ /** Non-streaming chat with optional tools */
19
+ chat(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): Promise<ChatResponse>;
17
20
 
18
- /**
19
- * Streaming chat completion - yields text chunks
20
- */
21
- chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse>;
21
+ /** Streaming chat with optional tools - yields StreamChunks */
22
+ chatStream(messages: ChatMessage[], options?: ChatOptions, tools?: Tool[]): AsyncGenerator<StreamChunk, ChatResponse>;
22
23
 
23
- /**
24
- * Convert Tool[] to provider-specific format
25
- */
24
+ /** Convert Tool[] to provider-specific format */
26
25
  buildTools(tools: Tool[]): any[];
27
26
  }
28
27
 
29
- /**
30
- * Provider configuration passed to each provider
31
- */
32
28
  export interface ProviderConfig {
33
29
  apiKey: string;
34
30
  model: string;
package/src/tui/input.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * REPL Input Handler
3
- * Migrated from old src/repl/input.ts
4
3
  */
5
4
 
6
5
  import readline from 'readline';
@@ -20,13 +19,11 @@ export class REPLInput {
20
19
  tabSize: 4,
21
20
  });
22
21
 
23
- // Handle Ctrl+C
24
22
  this.rl.on('SIGINT', () => {
25
- process.stdout.write(chalk.gray('\n\nšŸ‘‹ Exiting...\n'));
23
+ process.stdout.write(chalk.gray('\n Goodbye! šŸ‘‹\n'));
26
24
  process.exit(0);
27
25
  });
28
26
 
29
- // Track history
30
27
  this.rl.on('line', (line) => {
31
28
  if (line.trim()) {
32
29
  this.history.push(line);
@@ -36,7 +33,7 @@ export class REPLInput {
36
33
  });
37
34
  }
38
35
 
39
- async prompt(prefix: string = chalk.green('\n> ')): Promise<string> {
36
+ async prompt(prefix: string = chalk.cyan('āÆ ')): Promise<string> {
40
37
  return new Promise((resolve) => {
41
38
  this.rl.question(prefix, (answer) => {
42
39
  resolve(answer);
package/src/tui/output.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * REPL Output Handler
3
- * Migrated from old src/repl/output.ts
2
+ * REPL Output - Beautiful terminal UI
4
3
  */
5
4
 
6
5
  import chalk from 'chalk';
@@ -8,76 +7,230 @@ import ora, { Ora } from 'ora';
8
7
 
9
8
  export class REPLOutput {
10
9
  private spinner: Ora | null = null;
10
+ private inAssistantBlock = false;
11
+ private currentLineHasContent = false;
11
12
 
12
- printError(error: string): void {
13
- console.error(chalk.red(`\nāŒ Error: ${error}`));
13
+ // ── Banner ──────────────────────────────────────────────
14
+
15
+ printBanner(): void {
16
+ console.log();
17
+ console.log(chalk.cyan.bold(' ⚔ Thatgfsj Code') + chalk.gray(' v0.4.1'));
18
+ console.log(chalk.gray(' AI Coding Assistant'));
19
+ console.log(chalk.gray(' ' + '─'.repeat(52)));
20
+ console.log();
14
21
  }
15
22
 
16
- printWarning(warning: string): void {
17
- console.warn(chalk.yellow(`\nāš ļø ${warning}`));
23
+ // ── Prompt ──────────────────────────────────────────────
24
+
25
+ getPrompt(): string {
26
+ return chalk.cyan.bold('āÆ ');
18
27
  }
19
28
 
20
- printSuccess(message: string): void {
21
- console.log(chalk.green(`\nāœ… ${message}`));
29
+ // ── User Input ──────────────────────────────────────────
30
+
31
+ printUserInput(input: string): void {
32
+ console.log();
33
+ console.log(chalk.gray(' ā”Œā”€ ') + chalk.bold('You'));
34
+ for (const line of input.split('\n')) {
35
+ console.log(chalk.gray(' │ ') + line);
36
+ }
37
+ console.log(chalk.gray(' └─'));
22
38
  }
23
39
 
24
- printInfo(message: string): void {
25
- console.log(chalk.gray(message));
40
+ // ── Assistant Streaming ─────────────────────────────────
41
+
42
+ beginAssistant(): void {
43
+ if (this.inAssistantBlock) return;
44
+ this.inAssistantBlock = true;
45
+ this.currentLineHasContent = false;
46
+ console.log();
47
+ process.stdout.write(chalk.cyan(' ā”Œā”€ ') + chalk.bold('AI') + '\n' + chalk.cyan(' │ '));
26
48
  }
27
49
 
28
- printDivider(char = '─', length = 40): void {
29
- console.log(chalk.gray(char.repeat(length)));
50
+ writeChunk(chunk: string): void {
51
+ if (!this.inAssistantBlock) this.beginAssistant();
52
+
53
+ for (const char of chunk) {
54
+ if (char === '\n') {
55
+ process.stdout.write('\n' + chalk.cyan(' │ '));
56
+ this.currentLineHasContent = false;
57
+ } else {
58
+ process.stdout.write(char);
59
+ this.currentLineHasContent = true;
60
+ }
61
+ }
30
62
  }
31
63
 
32
- printHeader(text: string): void {
33
- this.printDivider();
34
- console.log(chalk.cyan(text));
35
- this.printDivider();
64
+ endAssistant(): void {
65
+ if (!this.inAssistantBlock) return;
66
+ if (this.currentLineHasContent) {
67
+ process.stdout.write('\n');
68
+ }
69
+ process.stdout.write(chalk.cyan(' └─') + '\n\n');
70
+ this.inAssistantBlock = false;
71
+ this.currentLineHasContent = false;
36
72
  }
37
73
 
38
- startSpinner(text: string): void {
39
- this.spinner = ora({ text, color: 'cyan', spinner: 'dots' }).start();
74
+ // ── Tool Calls ──────────────────────────────────────────
75
+
76
+ printToolCall(name: string, args: string): void {
77
+ if (this.inAssistantBlock) this.endAssistant();
78
+
79
+ console.log();
80
+ console.log(chalk.yellow(' āš™ ') + chalk.bold(name) + chalk.gray(' ─────────────────────────'));
81
+
82
+ if (args) {
83
+ try {
84
+ const obj = JSON.parse(args);
85
+ for (const [key, val] of Object.entries(obj)) {
86
+ let display: string;
87
+ if (typeof val === 'string') {
88
+ display = val.length > 120 ? val.slice(0, 120) + '...' : val;
89
+ } else {
90
+ display = JSON.stringify(val);
91
+ if (display.length > 120) display = display.slice(0, 120) + '...';
92
+ }
93
+ console.log(chalk.gray(' ' + key + ': ') + chalk.white(display));
94
+ }
95
+ } catch {
96
+ const display = args.length > 150 ? args.slice(0, 150) + '...' : args;
97
+ console.log(chalk.gray(' ') + display);
98
+ }
99
+ }
100
+ }
101
+
102
+ printToolResult(output: string, isError = false): void {
103
+ if (!output) return;
104
+
105
+ const lines = output.split('\n');
106
+ const maxLines = 25;
107
+ const display = lines.slice(0, maxLines);
108
+
109
+ for (const line of display) {
110
+ if (isError) {
111
+ console.log(chalk.red(' āœ– ') + line);
112
+ } else {
113
+ console.log(chalk.gray(' │ ') + line);
114
+ }
115
+ }
116
+
117
+ if (lines.length > maxLines) {
118
+ console.log(chalk.gray(' │ ') + chalk.dim(`... +${lines.length - maxLines} more lines`));
119
+ }
120
+ }
121
+
122
+ printToolEnd(): void {
123
+ console.log(chalk.yellow(' ────────────────────────────────────'));
124
+ }
125
+
126
+ // ── Thinking ────────────────────────────────────────────
127
+
128
+ startThinking(): void {
129
+ this.spinner = ora({
130
+ text: chalk.gray('Thinking'),
131
+ color: 'cyan',
132
+ spinner: 'dots',
133
+ }).start();
40
134
  }
41
135
 
42
- stopSpinner(): void {
136
+ startExecuting(toolName: string): void {
137
+ this.spinner = ora({
138
+ text: chalk.gray(`Executing ${chalk.yellow(toolName)}`),
139
+ color: 'cyan',
140
+ spinner: 'dots',
141
+ }).start();
142
+ }
143
+
144
+ stopThinking(): void {
43
145
  if (this.spinner) {
44
146
  this.spinner.stop();
45
147
  this.spinner = null;
46
148
  }
47
149
  }
48
150
 
49
- stopSpinnerFail(text?: string): void {
151
+ stopThinkingFail(msg: string): void {
50
152
  if (this.spinner) {
51
- this.spinner.fail(text || this.spinner.text);
153
+ this.spinner.fail(chalk.red(msg));
52
154
  this.spinner = null;
53
155
  }
54
156
  }
55
157
 
158
+ // ── Messages ────────────────────────────────────────────
159
+
160
+ info(msg: string): void {
161
+ console.log(chalk.gray(' ') + msg);
162
+ }
163
+
164
+ success(msg: string): void {
165
+ console.log(chalk.green(' āœ… ') + msg);
166
+ }
167
+
168
+ error(msg: string): void {
169
+ console.log();
170
+ console.log(chalk.red(' ā”Œā”€ Error'));
171
+ for (const line of msg.split('\n')) {
172
+ console.log(chalk.red(' │ ') + line);
173
+ }
174
+ console.log(chalk.red(' └─'));
175
+ console.log();
176
+ }
177
+
178
+ warning(msg: string): void {
179
+ console.log(chalk.yellow(' ⚠ ') + msg);
180
+ }
181
+
182
+ dim(msg: string): void {
183
+ console.log(chalk.dim(' ' + msg));
184
+ }
185
+
186
+ // ── Layout ──────────────────────────────────────────────
187
+
188
+ divider(): void {
189
+ console.log(chalk.gray(' ' + '─'.repeat(52)));
190
+ }
191
+
192
+ spacer(): void {
193
+ console.log();
194
+ }
195
+
56
196
  clear(): void {
57
197
  console.clear();
58
198
  }
59
199
 
60
- printBanner(): void {
61
- console.log(chalk.cyan(`
62
- ╔═══════════════════════════════════════╗
63
- ā•‘ šŸ¤– Thatgfsj Code v0.3.0 ā•‘
64
- ā•‘ AI Coding Assistant ā•‘
65
- ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
66
- `));
200
+ // ── Sections ────────────────────────────────────────────
201
+
202
+ section(title: string, items: Array<{ label: string; value: string }>): void {
203
+ console.log();
204
+ console.log(chalk.bold(' ' + title));
205
+ this.divider();
206
+ for (const item of items) {
207
+ console.log(' ' + chalk.gray(item.label.padEnd(14)) + chalk.white(item.value));
208
+ }
209
+ console.log();
67
210
  }
68
211
 
212
+ // ── Help ────────────────────────────────────────────────
213
+
69
214
  printHelp(): void {
70
- console.log(`
71
- ${chalk.cyan('Commands:')}
72
- ${chalk.gray('exit, Ctrl+C')} - Exit
73
- ${chalk.gray('clear')} - Clear screen
74
- ${chalk.gray('help')} - Show this help
75
- ${chalk.gray('tools')} - List available tools
76
- ${chalk.gray('model')} - Show current model
77
-
78
- ${chalk.cyan('Tips:')}
79
- • Use ${chalk.gray('↑/↓')} for command history
80
- • Use ${chalk.gray('Tab')} for auto-complete
81
- `);
215
+ console.log();
216
+ console.log(chalk.bold(' Commands'));
217
+ this.divider();
218
+ const cmds: [string, string][] = [
219
+ ['help', 'Show this help'],
220
+ ['tools', 'List available tools'],
221
+ ['model', 'Show current model info'],
222
+ ['clear', 'Clear screen'],
223
+ ['exit', 'Exit'],
224
+ ];
225
+ for (const [cmd, desc] of cmds) {
226
+ console.log(' ' + chalk.cyan(cmd.padEnd(14)) + chalk.gray(desc));
227
+ }
228
+ console.log();
229
+ console.log(chalk.bold(' Keyboard'));
230
+ this.divider();
231
+ console.log(' ' + chalk.cyan('↑ / ↓'.padEnd(14)) + chalk.gray('Browse command history'));
232
+ console.log(' ' + chalk.cyan('Tab'.padEnd(14)) + chalk.gray('Auto-complete'));
233
+ console.log(' ' + chalk.cyan('Ctrl+C'.padEnd(14)) + chalk.gray('Exit'));
234
+ console.log();
82
235
  }
83
236
  }
package/src/tui/repl.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * REPL - Main interactive loop
3
- * Migrated from old src/repl/loop.ts + src/index.ts interactive mode
4
3
  */
5
4
 
6
5
  import chalk from 'chalk';
@@ -21,53 +20,45 @@ export class REPL {
21
20
  this.app = app;
22
21
  }
23
22
 
24
- /**
25
- * Start the REPL
26
- */
27
23
  async start(): Promise<void> {
28
24
  this.running = true;
29
25
 
30
- // Show banner
31
26
  this.output.clear();
32
27
  this.output.printBanner();
33
- this.output.printInfo('Type "help" for available commands\n');
34
- this.output.printDivider();
35
28
 
36
- // Show project context
37
- console.log(chalk.gray(ProjectContext.getSummary()));
38
- this.output.printDivider();
39
- console.log();
29
+ const cfg = this.app.config.get();
30
+ this.output.section('Session', [
31
+ { label: 'Provider', value: cfg.provider },
32
+ { label: 'Model', value: cfg.model },
33
+ { label: 'Project', value: ProjectContext.detectType() },
34
+ ]);
35
+
36
+ this.output.info('Type ' + chalk.cyan('help') + ' for commands');
37
+ this.output.spacer();
40
38
 
41
- // Main loop
42
39
  while (this.running) {
43
40
  try {
44
- const userInput = await this.input.prompt();
45
-
41
+ const userInput = await this.input.prompt(this.output.getPrompt());
46
42
  if (!userInput.trim()) continue;
47
43
 
48
- // Handle commands
49
44
  const handled = await this.handleCommand(userInput.trim());
50
45
  if (handled) continue;
51
46
 
52
- // Process with AI
53
47
  await this.processInput(userInput.trim());
54
48
  } catch (error: any) {
55
49
  if (error.message === 'SIGINT') continue;
56
- this.output.printError(error.message);
50
+ this.output.error(error.message);
57
51
  }
58
52
  }
59
53
  }
60
54
 
61
- /**
62
- * Handle built-in commands
63
- */
64
55
  private async handleCommand(input: string): Promise<boolean> {
65
56
  const cmd = input.toLowerCase();
66
57
 
67
58
  switch (cmd) {
68
59
  case 'exit':
69
60
  case 'quit':
70
- this.output.printInfo('\nšŸ‘‹ Goodbye!');
61
+ this.output.dim('Goodbye! šŸ‘‹');
71
62
  this.running = false;
72
63
  this.input.close();
73
64
  return true;
@@ -82,50 +73,98 @@ export class REPL {
82
73
  return true;
83
74
 
84
75
  case 'tools':
85
- this.output.printHeader('šŸ”§ Available Tools');
86
- for (const tool of this.app.tools.list()) {
87
- this.output.printInfo(` ${tool.name} - ${tool.description}`);
88
- }
89
- console.log();
76
+ this.output.section('Available Tools',
77
+ this.app.tools.list().map(t => ({
78
+ label: t.name,
79
+ value: t.description,
80
+ }))
81
+ );
90
82
  return true;
91
83
 
92
- case 'model':
93
- this.output.printHeader('šŸ¤– Current Model');
94
- const cfg = this.app.config.get();
95
- this.output.printInfo(` Provider: ${cfg.provider}`);
96
- this.output.printInfo(` Model: ${cfg.model}`);
97
- this.output.printInfo(` Base URL: ${cfg.baseUrl || 'default'}`);
98
- console.log();
84
+ case 'model': {
85
+ const c = this.app.config.get();
86
+ this.output.section('Current Model', [
87
+ { label: 'Provider', value: c.provider },
88
+ { label: 'Model', value: c.model },
89
+ { label: 'Base URL', value: c.baseUrl || 'default' },
90
+ { label: 'API Key', value: c.apiKey ? '••••' + c.apiKey.slice(-4) : 'not set' },
91
+ ]);
99
92
  return true;
93
+ }
100
94
 
101
95
  default:
102
96
  return false;
103
97
  }
104
98
  }
105
99
 
106
- /**
107
- * Process user input with AI
108
- */
109
100
  private async processInput(input: string): Promise<void> {
110
101
  this.app.session.addMessage('user', input);
102
+ this.output.printUserInput(input);
103
+ this.output.startThinking();
111
104
 
112
- this.output.startSpinner('Thinking...');
113
105
  let fullResponse = '';
106
+ let hasStartedOutput = false;
114
107
 
115
108
  try {
116
109
  const stream = this.app.getAgent().run(this.app.session.getMessages());
110
+
117
111
  for await (const chunk of stream) {
118
- this.output.stopSpinner();
119
- process.stdout.write(chunk);
112
+ this.output.stopThinking();
113
+
114
+ const parts = chunk.split('\n');
115
+ for (const part of parts) {
116
+ // Tool message: @@TOOL@@{json}
117
+ if (part.startsWith('@@TOOL@@')) {
118
+ try {
119
+ const data = JSON.parse(part.slice(8));
120
+ if (data.action === 'call') {
121
+ if (hasStartedOutput) {
122
+ this.output.endAssistant();
123
+ hasStartedOutput = false;
124
+ }
125
+ this.output.printToolCall(data.name, data.args || '');
126
+ // Show executing state
127
+ this.output.startExecuting(data.name);
128
+ } else if (data.action === 'result') {
129
+ this.output.stopThinking();
130
+ this.output.printToolResult(data.output || data.error || '', !!data.error);
131
+ this.output.printToolEnd();
132
+ }
133
+ } catch {
134
+ this.output.writeChunk(part + '\n');
135
+ }
136
+ continue;
137
+ }
138
+
139
+ // Regular text
140
+ if (part) {
141
+ if (!hasStartedOutput) {
142
+ this.output.beginAssistant();
143
+ hasStartedOutput = true;
144
+ }
145
+ this.output.writeChunk(part + '\n');
146
+ }
147
+ }
148
+
120
149
  fullResponse += chunk;
121
150
  }
122
151
 
123
- console.log(); // newline
152
+ if (hasStartedOutput) {
153
+ this.output.endAssistant();
154
+ }
155
+
156
+ // Don't save empty responses
157
+ if (fullResponse.trim()) {
158
+ this.app.session.addMessage('assistant', fullResponse);
159
+ this.app.session.truncate();
160
+ }
124
161
 
125
- this.app.session.addMessage('assistant', fullResponse);
126
- this.app.session.truncate();
127
162
  } catch (error: any) {
128
- this.output.stopSpinnerFail(chalk.red(`Error: ${error.message}`));
163
+ if (hasStartedOutput) {
164
+ this.output.endAssistant();
165
+ }
166
+ this.output.stopThinkingFail(error.message);
167
+ this.output.error(error.message);
129
168
  }
130
169
  }
131
170
  }