thatgfsj-code 0.2.1 → 0.2.2

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 (64) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/DEVELOPMENT.md +286 -0
  3. package/README.md +131 -30
  4. package/dist/core/ai-engine.d.ts +17 -0
  5. package/dist/core/ai-engine.d.ts.map +1 -1
  6. package/dist/core/ai-engine.js +23 -7
  7. package/dist/core/ai-engine.js.map +1 -1
  8. package/dist/index.js +18 -27
  9. package/dist/index.js.map +1 -1
  10. package/dist/repl/input.d.ts +8 -0
  11. package/dist/repl/input.d.ts.map +1 -1
  12. package/dist/repl/input.js +12 -0
  13. package/dist/repl/input.js.map +1 -1
  14. package/dist/repl/loop.d.ts +56 -0
  15. package/dist/repl/loop.d.ts.map +1 -1
  16. package/dist/repl/loop.js +333 -4
  17. package/dist/repl/loop.js.map +1 -1
  18. package/dist/repl/output.d.ts.map +1 -1
  19. package/dist/repl/output.js +3 -1
  20. package/dist/repl/output.js.map +1 -1
  21. package/dist/repl/welcome.js +1 -1
  22. package/dist/repl/welcome.js.map +1 -1
  23. package/docs/API_KEY_GUIDE.md +6 -0
  24. package/docs/FAQ.md +25 -3
  25. package/package.json +35 -3
  26. package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +0 -279
  27. package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +0 -564
  28. package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +0 -194
  29. package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +0 -24
  30. package/ROADMAP.md +0 -107
  31. package/src/agent/core.ts +0 -179
  32. package/src/agent/index.ts +0 -8
  33. package/src/agent/intent.ts +0 -181
  34. package/src/agent/streaming.ts +0 -132
  35. package/src/core/ai-engine.ts +0 -437
  36. package/src/core/cli.ts +0 -171
  37. package/src/core/config.ts +0 -147
  38. package/src/core/context-compactor.ts +0 -245
  39. package/src/core/hooks.ts +0 -196
  40. package/src/core/permissions.ts +0 -308
  41. package/src/core/session.ts +0 -165
  42. package/src/core/skills.ts +0 -208
  43. package/src/core/state.ts +0 -120
  44. package/src/core/subagent.ts +0 -195
  45. package/src/core/system-prompt.ts +0 -163
  46. package/src/core/tool-registry.ts +0 -157
  47. package/src/core/types.ts +0 -280
  48. package/src/index.ts +0 -544
  49. package/src/mcp/client.ts +0 -330
  50. package/src/repl/index.ts +0 -8
  51. package/src/repl/input.ts +0 -139
  52. package/src/repl/loop.ts +0 -280
  53. package/src/repl/output.ts +0 -222
  54. package/src/repl/welcome.ts +0 -296
  55. package/src/tools/file.ts +0 -117
  56. package/src/tools/git.ts +0 -132
  57. package/src/tools/index.ts +0 -48
  58. package/src/tools/search.ts +0 -263
  59. package/src/tools/shell.ts +0 -181
  60. package/src/utils/diff-preview.ts +0 -202
  61. package/src/utils/index.ts +0 -8
  62. package/src/utils/memory.ts +0 -223
  63. package/src/utils/project-context.ts +0 -207
  64. package/tsconfig.json +0 -19
@@ -1,132 +0,0 @@
1
- /**
2
- * Streaming Output
3
- * Handles real-time streaming output with typewriter effect
4
- */
5
-
6
- import chalk from 'chalk';
7
-
8
- export type StreamCallback = (chunk: string) => void;
9
- export type DoneCallback = (full: string) => void;
10
-
11
- export class StreamingOutput {
12
- private fullContent: string = '';
13
- private isStreaming: boolean = false;
14
- private onChunk?: StreamCallback;
15
- private onDone?: DoneCallback;
16
- private buffer: string = '';
17
- private minChunkDelay: number = 10; // ms between chunks
18
-
19
- /**
20
- * Start streaming mode
21
- */
22
- start(callbacks?: { onChunk?: StreamCallback; onDone?: DoneCallback }): void {
23
- this.fullContent = '';
24
- this.isStreaming = true;
25
- this.onChunk = callbacks?.onChunk;
26
- this.onDone = callbacks?.onDone;
27
- }
28
-
29
- /**
30
- * Process a chunk of content
31
- */
32
- processChunk(chunk: string): void {
33
- if (!this.isStreaming) return;
34
-
35
- this.fullContent += chunk;
36
- this.buffer += chunk;
37
-
38
- // Flush buffer periodically
39
- if (this.buffer.length > 0) {
40
- const toPrint = this.buffer;
41
- this.buffer = '';
42
-
43
- if (this.onChunk) {
44
- this.onChunk(toPrint);
45
- } else {
46
- process.stdout.write(toPrint);
47
- }
48
- }
49
- }
50
-
51
- /**
52
- * End streaming mode
53
- */
54
- end(): string {
55
- this.isStreaming = false;
56
-
57
- // Flush remaining buffer
58
- if (this.buffer.length > 0) {
59
- if (this.onChunk) {
60
- this.onChunk(this.buffer);
61
- } else {
62
- process.stdout.write(this.buffer);
63
- }
64
- this.buffer = '';
65
- }
66
-
67
- if (this.onDone) {
68
- this.onDone(this.fullContent);
69
- }
70
-
71
- return this.fullContent;
72
- }
73
-
74
- /**
75
- * Stop streaming (for interrupt)
76
- */
77
- stop(): string {
78
- this.isStreaming = false;
79
- return this.fullContent;
80
- }
81
-
82
- /**
83
- * Get full content so far
84
- */
85
- getContent(): string {
86
- return this.fullContent;
87
- }
88
-
89
- /**
90
- * Check if streaming
91
- */
92
- isActive(): boolean {
93
- return this.isStreaming;
94
- }
95
-
96
- /**
97
- * Print with typewriter effect (for interactive mode)
98
- */
99
- static async typewriter(
100
- text: string,
101
- delay: number = 20,
102
- onChunk?: (chunk: string) => void
103
- ): Promise<void> {
104
- for (let i = 0; i < text.length; i++) {
105
- process.stdout.write(text[i]);
106
- if (onChunk) onChunk(text[i]);
107
- await new Promise(r => setTimeout(r, delay));
108
- }
109
- }
110
-
111
- /**
112
- * Print code block with syntax highlighting hint
113
- */
114
- static printCodeBlock(code: string, language: string = ''): void {
115
- const lines = code.split('\n');
116
- const maxLen = Math.max(...lines.map(l => l.length), 60);
117
-
118
- console.log(chalk.bgBlack.gray('┌' + '─'.repeat(Math.min(maxLen, 80)) + '┐'));
119
-
120
- if (language) {
121
- console.log(chalk.bgBlack.gray('│ ') + chalk.cyan(language) + chalk.bgBlack.gray(' '.repeat(Math.max(0, maxLen - language.length - 1))) + chalk.bgBlack.gray('│'));
122
- console.log(chalk.bgBlack.gray('├' + '─'.repeat(Math.min(maxLen, 80)) + '┤'));
123
- }
124
-
125
- for (const line of lines) {
126
- const displayLine = line.length > maxLen ? line.substring(0, maxLen - 3) + '...' : line;
127
- console.log(chalk.bgBlack('│ ') + displayLine + ' '.repeat(Math.max(0, maxLen - displayLine.length)) + chalk.bgBlack(' │'));
128
- }
129
-
130
- console.log(chalk.bgBlack.gray('└' + '─'.repeat(Math.min(maxLen, 80)) + '┘'));
131
- }
132
- }
@@ -1,437 +0,0 @@
1
- /**
2
- * AI Engine - Core AI interaction module
3
- * Supports multiple providers: MiniMax, SiliconFlow, OpenAI, Anthropic
4
- *
5
- * Implements S01: Agent Loop pattern with async generator for streaming
6
- * Core pattern: while(true) { response → execute tools → append results → repeat }
7
- */
8
-
9
- import chalk from 'chalk';
10
- import { ChatMessage, AIResponse, AIConfig, Tool, ToolCall } from './types.js';
11
-
12
- export { AIResponse, ChatMessage };
13
-
14
- export class AIEngine {
15
- private config: AIConfig;
16
- private tools: Map<string, Tool> = new Map();
17
-
18
- constructor(config: AIConfig) {
19
- this.config = config;
20
- }
21
-
22
- /**
23
- * Register tools for function calling
24
- */
25
- registerTool(tool: Tool) {
26
- this.tools.set(tool.name, tool);
27
- }
28
-
29
- /**
30
- * Unregister a tool
31
- */
32
- unregisterTool(name: string) {
33
- this.tools.delete(name);
34
- }
35
-
36
- /**
37
- * Get registered tools for API (S02: uses ToolRegistry if available)
38
- */
39
- private getToolsForAPI() {
40
- // If registry is injected, use its getToolsForAPI
41
- if (this.registry) {
42
- return this.registry.getToolsForAPI();
43
- }
44
- // Fallback: build from tools map
45
- return Array.from(this.tools.values()).map(tool => ({
46
- type: 'function' as const,
47
- function: {
48
- name: tool.name,
49
- description: tool.description,
50
- parameters: {
51
- type: 'object',
52
- properties: tool.parameters.reduce((acc, p) => {
53
- acc[p.name] = { type: p.type, description: p.description };
54
- return acc;
55
- }, {} as any),
56
- required: tool.parameters.filter(p => p.required).map(p => p.name)
57
- }
58
- }
59
- }));
60
- }
61
-
62
- // S02: Inject registry for enhanced tool management
63
- setRegistry(registry: any) {
64
- this.registry = registry;
65
- }
66
- private registry?: any;
67
-
68
- // S08: Hook manager
69
- setHooks(hooks: import('./hooks.js').HookManager) {
70
- this.hooks = hooks;
71
- }
72
- private hooks?: import('./hooks.js').HookManager | null = null;
73
-
74
- // ==================== S01: Async Generator Agent Loop ====================
75
-
76
- async *chatStream(
77
- messages: ChatMessage[],
78
- maxIterations: number = 10
79
- ): AsyncGenerator<string, AIResponse, unknown> {
80
- let currentMessages = [...messages];
81
- let iterations = 0;
82
-
83
- while (iterations < maxIterations) {
84
- iterations++;
85
-
86
- // S08 Hook: beforeAgentLoop
87
- if (this.hooks) {
88
- await this.hooks.emit('beforeAgentLoop', { messages: currentMessages, iteration: iterations });
89
- }
90
-
91
- let fullResponse = '';
92
-
93
- for await (const chunk of this.streamRequest(currentMessages)) {
94
- fullResponse += chunk;
95
- yield chunk;
96
- }
97
-
98
- const response = this.parseResponse(fullResponse);
99
-
100
- if (response.tool_calls && response.tool_calls.length > 0) {
101
- currentMessages.push({
102
- role: 'assistant',
103
- content: response.content,
104
- tool_calls: response.tool_calls
105
- });
106
-
107
- // Execute each tool
108
- for (const toolCall of response.tool_calls) {
109
- // S08 Hook: beforeToolCall
110
- if (this.hooks) {
111
- await this.hooks.emit('beforeToolCall', {
112
- toolName: toolCall.function.name,
113
- toolParams: JSON.parse(toolCall.function.arguments || '{}'),
114
- toolCallId: toolCall.id
115
- });
116
- }
117
-
118
- const result = await this.executeToolCall(toolCall);
119
-
120
- // S08 Hook: afterToolCall
121
- if (this.hooks) {
122
- await this.hooks.emit('afterToolCall', {
123
- toolName: toolCall.function.name,
124
- toolParams: JSON.parse(toolCall.function.arguments || '{}'),
125
- toolResult: result,
126
- toolCallId: toolCall.id
127
- });
128
- }
129
-
130
- const toolResultMsg: ChatMessage = {
131
- role: 'tool',
132
- content: result.output || result.error || '',
133
- tool_call_id: toolCall.id,
134
- name: toolCall.function.name
135
- };
136
- currentMessages.push(toolResultMsg);
137
-
138
- yield `\n${chalk.gray(`[tool: ${toolCall.function.name}]`)}`;
139
- }
140
-
141
- continue;
142
- }
143
-
144
- // No tool calls — exit loop
145
- if (response.content) {
146
- currentMessages.push({
147
- role: 'assistant',
148
- content: response.content
149
- });
150
- }
151
-
152
- // S08 Hook: afterAgentLoop
153
- if (this.hooks) {
154
- await this.hooks.emit('afterAgentLoop', { messages: currentMessages, iteration: iterations });
155
- }
156
-
157
- return response;
158
- }
159
-
160
- // Max iterations reached
161
- return {
162
- content: '[Agent loop exceeded maximum iterations]',
163
- role: 'assistant'
164
- };
165
- }
166
-
167
- /**
168
- * S01 Core: Stream request — yields chunks as they arrive
169
- * Uses async generator for backpressure control and lazy evaluation
170
- */
171
- private async *streamRequest(
172
- messages: ChatMessage[]
173
- ): AsyncGenerator<string, void, unknown> {
174
- const apiKey = this.config.apiKey || this.getApiKey();
175
-
176
- if (!apiKey) {
177
- // Mock streaming for demo
178
- const mock = this.getMockResponse(messages);
179
- for (const char of mock) {
180
- await new Promise(r => setTimeout(r, 10));
181
- yield char;
182
- }
183
- return;
184
- }
185
-
186
- const isAnthropicFormat = this.config.provider === 'anthropic' || this.config.provider === 'minimax';
187
- const isGeminiFormat = this.config.provider === 'gemini';
188
-
189
- let url: string;
190
- let headers: Record<string, string> = {};
191
- let body: any;
192
-
193
- if (isGeminiFormat) {
194
- url = `${this.config.baseUrl}/models/${this.config.model}:generateContent`;
195
- headers = { 'Content-Type': 'application/json' };
196
- body = this.buildGeminiRequest(messages);
197
- } else if (isAnthropicFormat) {
198
- url = `${this.config.baseUrl}/messages-stream`;
199
- headers = {
200
- 'Content-Type': 'application/json',
201
- 'x-api-key': apiKey,
202
- 'anthropic-version': '2023-06-01',
203
- 'anthropic-dangerous-direct-browser-access': 'true'
204
- };
205
- body = this.buildAnthropicRequest(messages, true);
206
- } else {
207
- url = `${this.config.baseUrl}/chat/completions`;
208
- headers = {
209
- 'Content-Type': 'application/json',
210
- 'Authorization': `Bearer ${apiKey}`
211
- };
212
- body = this.buildOpenAIRequest(messages, true);
213
- }
214
-
215
- const response = await fetch(url, {
216
- method: 'POST',
217
- headers,
218
- body: JSON.stringify(body)
219
- });
220
-
221
- if (!response.ok || !response.body) {
222
- yield `\n${chalk.red(`[API Error: ${response.status}]`)}`;
223
- return;
224
- }
225
-
226
- const decoder = new TextDecoder();
227
- const reader = response.body.getReader();
228
-
229
- try {
230
- while (true) {
231
- const { done, value } = await reader.read();
232
- if (done) break;
233
-
234
- const chunk = decoder.decode(value, { stream: true });
235
- const text = this.extractChunkText(chunk, isAnthropicFormat, isGeminiFormat);
236
- if (text) {
237
- yield text;
238
- }
239
- }
240
- } finally {
241
- reader.releaseLock();
242
- }
243
- }
244
-
245
- /**
246
- * Extract text from streaming chunk based on provider format
247
- */
248
- private extractChunkText(
249
- chunk: string,
250
- isAnthropic: boolean,
251
- isGemini: boolean
252
- ): string {
253
- if (isAnthropic) {
254
- // Anthropic streaming format: data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"..."}}
255
- const match = chunk.match(/"type"\s*:\s*"text_delta"\s*,\s*"text"\s*:\s*"([^"]*)"/);
256
- return match ? match[1] : '';
257
- } else if (isGemini) {
258
- // Gemini format
259
- const match = chunk.match(/"text"\s*:\s*"([^"]*)"/);
260
- return match ? match[1] : '';
261
- } else {
262
- // OpenAI SSE format: data: {"choices":[{"delta":{"content":"..."}}]}
263
- const lines = chunk.split('\n');
264
- for (const line of lines) {
265
- if (line.startsWith('data: ') && line !== 'data: [DONE]') {
266
- try {
267
- const data = JSON.parse(line.slice(6));
268
- const content = data.choices?.[0]?.delta?.content;
269
- if (content) return content;
270
- } catch {
271
- // Skip invalid JSON
272
- }
273
- }
274
- }
275
- return '';
276
- }
277
- }
278
-
279
- /**
280
- * Parse full response text into structured AIResponse
281
- */
282
- private parseResponse(fullText: string): AIResponse {
283
- // For now return content as-is; tool call parsing happens via content patterns
284
- return {
285
- content: fullText,
286
- role: 'assistant',
287
- tool_calls: this.extractToolCalls(fullText)
288
- };
289
- }
290
-
291
- /**
292
- * Extract tool calls from response content
293
- */
294
- private extractToolCalls(content: string): ToolCall[] | undefined {
295
- // Simple pattern: look for tool_call blocks in response
296
- // In real implementation this would be parsed from structured response
297
- // For streaming, we check if content indicates a tool call
298
- if (!content.includes('tool_use')) return undefined;
299
- return undefined; // Placeholder — actual implementation depends on provider
300
- }
301
-
302
- // ==================== Legacy chat() — delegates to chatStream ====================
303
-
304
- /**
305
- * Original non-streaming chat — now implemented via chatStream
306
- */
307
- async chat(messages: ChatMessage[], maxIterations: number = 5): Promise<AIResponse> {
308
- let fullContent = '';
309
-
310
- for await (const chunk of this.chatStream(messages, maxIterations)) {
311
- fullContent += chunk;
312
- }
313
-
314
- return {
315
- content: fullContent,
316
- role: 'assistant'
317
- };
318
- }
319
-
320
- // ==================== Tool Execution ====================
321
-
322
- private async executeToolCall(toolCall: ToolCall): Promise<{ output?: string; error?: string }> {
323
- const { name, arguments: args } = toolCall.function;
324
- const tool = this.tools.get(name);
325
-
326
- if (!tool) {
327
- return { error: `Tool "${name}" not found` };
328
- }
329
-
330
- try {
331
- const params = JSON.parse(args);
332
- const result = await tool.execute(params, {
333
- confirmAction: async (msg: string): Promise<boolean> => {
334
- console.log(chalk.yellow(`\n⚠️ Tool wants to execute: ${msg}`));
335
- return false; // Default deny in CLI mode
336
- }
337
- });
338
-
339
- return {
340
- output: result.success ? (result.output || JSON.stringify(result.data)) : undefined,
341
- error: result.error
342
- };
343
- } catch (error: any) {
344
- return { error: `Tool execution failed: ${error.message}` };
345
- }
346
- }
347
-
348
- // ==================== Request Builders ====================
349
-
350
- private buildOpenAIRequest(messages: ChatMessage[], stream: boolean) {
351
- const tools = this.getToolsForAPI();
352
- return {
353
- model: this.config.model || 'Qwen/Qwen2.5-7B-Instruct',
354
- messages: messages.map(m => ({
355
- role: m.role,
356
- content: m.content,
357
- name: m.name,
358
- tool_call_id: m.tool_call_id,
359
- tool_calls: m.tool_calls
360
- })),
361
- temperature: this.config.temperature || 0.7,
362
- max_tokens: this.config.maxTokens || 4096,
363
- stream,
364
- ...(tools.length > 0 && { tools })
365
- };
366
- }
367
-
368
- private buildAnthropicRequest(messages: ChatMessage[], stream: boolean) {
369
- const anthropicMessages = messages
370
- .filter(m => m.role !== 'system')
371
- .map(m => ({
372
- role: m.role === 'assistant' ? 'assistant' : 'user',
373
- content: m.content
374
- }));
375
-
376
- return {
377
- model: this.config.model || 'claude-3-haiku-20240307',
378
- messages: anthropicMessages,
379
- max_tokens: this.config.maxTokens || 4096,
380
- temperature: this.config.temperature || 0.7,
381
- stream
382
- };
383
- }
384
-
385
- private buildGeminiRequest(messages: ChatMessage[]) {
386
- const contents = messages
387
- .filter(m => m.role !== 'system')
388
- .map(m => ({
389
- role: m.role === 'assistant' ? 'model' : 'user',
390
- parts: [{ text: m.content }]
391
- }));
392
-
393
- return {
394
- contents,
395
- generationConfig: {
396
- temperature: this.config.temperature || 0.7,
397
- maxOutputTokens: this.config.maxTokens || 4096
398
- }
399
- };
400
- }
401
-
402
- private getApiKey(): string {
403
- const provider = this.config.provider || 'siliconflow';
404
-
405
- const envKeys: Record<string, string[]> = {
406
- siliconflow: ['SILICONFLOW_API_KEY', 'OPENAI_API_KEY'],
407
- minimax: ['MINIMAX_API_KEY', 'OPENAI_API_KEY'],
408
- openai: ['OPENAI_API_KEY'],
409
- anthropic: ['ANTHROPIC_API_KEY'],
410
- ollama: [],
411
- gemini: ['GEMINI_API_KEY'],
412
- kimi: ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
413
- deepseek: ['DEEPSEEK_API_KEY']
414
- };
415
-
416
- for (const key of envKeys[provider] || []) {
417
- if (process.env[key]) return process.env[key];
418
- }
419
-
420
- return '';
421
- }
422
-
423
- private getMockResponse(messages: ChatMessage[]): string {
424
- const lastMessage = messages[messages.length - 1]?.content || '';
425
- let response = 'Hello! I am Thatgfsj Code.\n\n';
426
-
427
- if (lastMessage.toLowerCase().includes('hello') || lastMessage.toLowerCase().includes('hi')) {
428
- response = 'Hi there! How can I help you today?';
429
- } else if (lastMessage.toLowerCase().includes('who are you')) {
430
- response = 'I am Thatgfsj Code, an AI assistant built with Node.js.';
431
- } else {
432
- response = `I understand you said: "${lastMessage.slice(0, 50)}..."\n\nThis is a streaming demo. Enable an API key for full AI capabilities.`;
433
- }
434
-
435
- return response;
436
- }
437
- }