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
package/src/core/cli.ts DELETED
@@ -1,171 +0,0 @@
1
- /**
2
- * S12: CLI & Architecture
3
- *
4
- * Main entry point, CLI argument parsing, and module architecture overview.
5
- */
6
-
7
- import { readFileSync } from 'fs';
8
- import { join } from 'path';
9
- import { AIEngine } from './ai-engine.js';
10
- import { ConfigManager } from './config.js';
11
- import { REPLLoop } from '../repl/loop.js';
12
- import { ToolRegistry } from './tool-registry.js';
13
- import { ShellTool, FileTool, GitTool } from '../tools/index.js';
14
- import { HookManager, auditLogHook, getHookManager } from './hooks.js';
15
- import { getStateManager } from './state.js';
16
- import { SystemPromptBuilder } from './system-prompt.js';
17
- import { BUILT_IN_SKILLS } from './skills.js';
18
-
19
- // ==================== CLI Argument Parsing ====================
20
-
21
- export interface CLIOptions {
22
- model?: string;
23
- provider?: string;
24
- prompt?: string;
25
- noStream?: boolean;
26
- verbose?: boolean;
27
- hooks?: boolean;
28
- permissionMode?: 'accept' | 'deny' | 'ask';
29
- }
30
-
31
- export function parseArgs(args: string[]): CLIOptions {
32
- const options: CLIOptions = {};
33
-
34
- for (let i = 0; i < args.length; i++) {
35
- const arg = args[i];
36
-
37
- switch (arg) {
38
- case '--model':
39
- case '-m':
40
- options.model = args[++i];
41
- break;
42
- case '--provider':
43
- case '-p':
44
- options.provider = args[++i];
45
- break;
46
- case '--prompt':
47
- case '-c':
48
- options.prompt = args[++i];
49
- break;
50
- case '--no-stream':
51
- options.noStream = true;
52
- break;
53
- case '--verbose':
54
- case '-v':
55
- options.verbose = true;
56
- break;
57
- case '--hooks':
58
- options.hooks = true;
59
- break;
60
- case '--permission':
61
- options.permissionMode = (args[++i] as 'accept' | 'deny' | 'ask') || 'ask';
62
- break;
63
- case '--help':
64
- case '-h':
65
- printHelp();
66
- process.exit(0);
67
- }
68
- }
69
-
70
- return options;
71
- }
72
-
73
- function printHelp(): void {
74
- console.log(`
75
- Thatgfsj Code - AI Coding Assistant
76
-
77
- Usage: thatgfsj [options]
78
-
79
- Options:
80
- -m, --model <model> Set AI model
81
- -p, --provider <provider> Set provider (siliconflow, minimax, openai, etc.)
82
- -c, --prompt <text> Run single prompt and exit
83
- --no-stream Disable streaming output
84
- --hooks Enable built-in hooks (audit log, etc.)
85
- --permission <mode> Set permission mode (accept, deny, ask)
86
- -v, --verbose Verbose output
87
- -h, --help Show this help
88
-
89
- Examples:
90
- thatgfsj Start REPL
91
- thatgfsj -c "Hello" Run single prompt
92
- thatgfsj --provider minimax --model MiniMax-M2.5 Use specific provider
93
- thatgfsj --permission accept Accept all tool calls
94
- `);
95
- }
96
-
97
- // ==================== Architecture ====================
98
-
99
- /**
100
- * S12: Print architecture overview
101
- */
102
- export function printArchitecture(): void {
103
- console.log(`
104
- Thatgfsj Code Architecture
105
- ============================
106
-
107
- Core:
108
- AIEngine - Async generator agent loop (S01), multi-provider
109
- ToolRegistry - Tool registration and execution (S02)
110
- PermissionChecker - 6-stage permission pipeline (S03)
111
- SystemPromptBuilder - Dynamic prompt construction (S04)
112
- ContextCompactor - 3-tier context compression (S05)
113
- SubagentManager - Parallel subagent execution (S06)
114
- HookManager - Event-driven hooks (S08)
115
- SkillsLoader - Skills discovery and catalog (S10)
116
- StateManager - Ephemeral/Persistent/Session state (S11)
117
-
118
- Tools:
119
- ShellTool - Shell command execution
120
- FileTool - File read/write/list/delete
121
- GitTool - Git operations
122
-
123
- Entry:
124
- REPLLoop - Interactive REPL
125
- CLI - Command-line mode
126
- `);
127
- }
128
-
129
- // ==================== Main ====================
130
-
131
- export async function main(argv: string[]): Promise<void> {
132
- const options = parseArgs(argv.slice(2));
133
-
134
- // Single prompt mode
135
- if (options.prompt) {
136
- const config = await ConfigManager.load();
137
- if (options.provider) config.provider = options.provider as any;
138
- if (options.model) config.model = options.model;
139
-
140
- const ai = new AIEngine(config);
141
- const tools = [new ShellTool(), new FileTool(), new GitTool()];
142
- for (const tool of tools) ai.registerTool(tool);
143
-
144
- if (options.hooks) {
145
- const hooks = getHookManager();
146
- hooks.register('afterToolCall', auditLogHook);
147
- ai.setHooks(hooks);
148
- }
149
-
150
- const promptBuilder = new SystemPromptBuilder({ tools, permissionMode: options.permissionMode });
151
- const messages = [
152
- { role: 'system' as const, content: promptBuilder.build() },
153
- { role: 'user' as const, content: options.prompt }
154
- ];
155
-
156
- if (options.noStream) {
157
- const response = await ai.chat(messages);
158
- console.log(response.content);
159
- } else {
160
- for await (const chunk of ai.chatStream(messages)) {
161
- process.stdout.write(chunk);
162
- }
163
- console.log();
164
- }
165
- return;
166
- }
167
-
168
- // REPL mode
169
- const repl = new REPLLoop();
170
- await repl.start();
171
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Config Manager - Handles configuration loading and saving
3
- * Supports multiple AI providers: MiniMax, SiliconFlow, OpenAI, Anthropic
4
- */
5
-
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
- import { join, dirname } from 'path';
8
- import { homedir } from 'os';
9
- import { Config, PROVIDERS, AIConfig } from './types.js';
10
-
11
- const DEFAULT_CONFIG: Config = {
12
- model: 'Qwen/Qwen2.5-7B-Instruct',
13
- apiKey: '',
14
- temperature: 0.7,
15
- maxTokens: 4096,
16
- provider: 'siliconflow',
17
- baseUrl: 'https://api.siliconflow.cn/v1'
18
- };
19
-
20
- export class ConfigManager {
21
- private configPath: string;
22
-
23
- constructor() {
24
- const homeDir = homedir();
25
- const configDir = join(homeDir, '.thatgfsj');
26
- this.configPath = join(configDir, 'config.json');
27
- }
28
-
29
- /**
30
- * Load configuration with environment variable support
31
- */
32
- static async load(): Promise<Config> {
33
- const manager = new ConfigManager();
34
- return manager.loadConfig();
35
- }
36
-
37
- /**
38
- * Load config from file and merge with environment variables
39
- */
40
- private loadConfig(): Config {
41
- let config = { ...DEFAULT_CONFIG };
42
-
43
- try {
44
- if (existsSync(this.configPath)) {
45
- const data = readFileSync(this.configPath, 'utf-8');
46
- const loaded = JSON.parse(data);
47
- config = { ...config, ...loaded };
48
- }
49
- } catch (error) {
50
- console.warn('Failed to load config, using defaults:', error);
51
- }
52
-
53
- // Resolve provider and get API key from environment
54
- config = this.resolveProvider(config);
55
-
56
- return config;
57
- }
58
-
59
- /**
60
- * Resolve provider and API key from config or environment
61
- */
62
- private resolveProvider(config: Config): Config {
63
- const provider = config.provider || 'siliconflow';
64
- const providerConfig = (provider !== 'custom' && provider !== undefined) ? PROVIDERS[provider] : null;
65
-
66
- if (!providerConfig) {
67
- console.warn(`Unknown provider: ${provider}, falling back to siliconflow`);
68
- return { ...config, provider: 'siliconflow', baseUrl: PROVIDERS.siliconflow.baseUrl };
69
- }
70
-
71
- // MODEL environment variable has highest priority
72
- let model = process.env.MODEL || config.model || providerConfig.defaultModel;
73
-
74
- // Handle Ollama (local, no API key needed)
75
- let apiKey = config.apiKey;
76
- let baseUrl = config.baseUrl || providerConfig.baseUrl;
77
-
78
- if (provider === 'ollama') {
79
- // Ollama doesn't need an API key
80
- apiKey = '';
81
- // Allow custom base URL via environment
82
- baseUrl = process.env.OLLAMA_BASE_URL || baseUrl;
83
- } else {
84
- // Try to get API key from environment
85
- for (const envKey of providerConfig.envKeys) {
86
- if (process.env[envKey]) {
87
- apiKey = process.env[envKey];
88
- break;
89
- }
90
- }
91
- }
92
-
93
- return {
94
- ...config,
95
- provider,
96
- baseUrl,
97
- model,
98
- apiKey: apiKey || ''
99
- };
100
- }
101
-
102
- /**
103
- * Get AIConfig for AIEngine
104
- */
105
- static async getAIConfig(): Promise<AIConfig> {
106
- const config = await ConfigManager.load();
107
- return {
108
- model: config.model,
109
- apiKey: config.apiKey,
110
- temperature: config.temperature,
111
- maxTokens: config.maxTokens,
112
- baseUrl: config.baseUrl,
113
- provider: config.provider
114
- };
115
- }
116
-
117
- /**
118
- * Save configuration
119
- */
120
- async save(config: Partial<Config>): Promise<void> {
121
- const current = this.loadConfig();
122
- const merged = { ...current, ...config };
123
-
124
- const dir = dirname(this.configPath);
125
- if (!existsSync(dir)) {
126
- mkdirSync(dir, { recursive: true });
127
- }
128
-
129
- writeFileSync(this.configPath, JSON.stringify(merged, null, 2));
130
- }
131
-
132
- /**
133
- * Get config path
134
- */
135
- getPath(): string {
136
- return this.configPath;
137
- }
138
-
139
- /**
140
- * List available providers
141
- */
142
- static listProviders(): string {
143
- return Object.entries(PROVIDERS)
144
- .map(([key, val]) => ` ${key}: ${val.name}`)
145
- .join('\n');
146
- }
147
- }
@@ -1,245 +0,0 @@
1
- /**
2
- * S05: Context Compactor - 3-tier progressive compression strategy
3
- *
4
- * Tier 1: Truncation - Remove oldest messages beyond hard limit
5
- * Tier 2: Summarize - Replace old message ranges with summary
6
- * Tier 3: Selective - Keep only recent N turns around tool executions
7
- *
8
- * Core insight: Context window is finite. Three-tier strategy enables "infinite" work.
9
- */
10
-
11
- import { ChatMessage } from './types.js';
12
-
13
- export interface CompactorConfig {
14
- /** Hard limit: messages beyond this get removed first */
15
- hardLimit?: number;
16
- /** Soft limit: messages beyond this get summarized */
17
- softLimit?: number;
18
- /** How many recent turns to always preserve */
19
- preserveRecent?: number;
20
- /** Enable summarization (requires AI API) */
21
- enableSummarize?: boolean;
22
- }
23
-
24
- export interface CompressionResult {
25
- originalCount: number;
26
- compactedCount: number;
27
- removedCount: number;
28
- method: 'none' | 'truncation' | 'summarize' | 'selective';
29
- }
30
-
31
- /**
32
- * S05: Context Compactor
33
- */
34
- export class ContextCompactor {
35
- private config: CompactorConfig;
36
- private totalTokensUsed = 0;
37
-
38
- constructor(config: CompactorConfig = {}) {
39
- this.config = {
40
- hardLimit: 50,
41
- softLimit: 30,
42
- preserveRecent: 10,
43
- enableSummarize: false,
44
- ...config
45
- };
46
- }
47
-
48
- /**
49
- * S05: Compact messages using 3-tier strategy
50
- * Called at the end of each agent turn
51
- */
52
- compact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
53
- const count = messages.length;
54
-
55
- // Tier 1: Within hard limit — no compression
56
- if (count <= (this.config.hardLimit || 50)) {
57
- return {
58
- compacted: messages,
59
- result: { originalCount: count, compactedCount: count, removedCount: 0, method: 'none' }
60
- };
61
- }
62
-
63
- // Tier 2: Beyond soft limit — summarize middle messages
64
- if (count > (this.config.softLimit || 30)) {
65
- return this.summarizeStrategy(messages);
66
- }
67
-
68
- // Tier 3: Between soft and hard — selective preservation
69
- return this.selectiveStrategy(messages);
70
- }
71
-
72
- // Tier 1: Simple truncation
73
- private truncate(messages: ChatMessage[], maxMessages: number): ChatMessage[] {
74
- const systemMsg = messages.find(m => m.role === 'system');
75
- const others = messages.filter(m => m.role !== 'system');
76
- const toRemove = others.length - maxMessages;
77
-
78
- if (toRemove <= 0 || others.length === 0) {
79
- return messages;
80
- }
81
-
82
- // Remove oldest non-system messages
83
- const keptOthers = others.slice(toRemove);
84
- return systemMsg ? [systemMsg, ...keptOthers] : keptOthers;
85
- }
86
-
87
- // Tier 2: Summarize middle messages (stub — needs AI for real summarization)
88
- private summarizeStrategy(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
89
- const preserved = this.config.preserveRecent || 10;
90
- const systemMsg = messages.find(m => m.role === 'system');
91
- const others = messages.filter(m => m.role !== 'system');
92
-
93
- if (others.length <= preserved) {
94
- return {
95
- compacted: messages,
96
- result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
97
- };
98
- }
99
-
100
- // Keep recent N turns
101
- const recent = others.slice(-preserved);
102
-
103
- // Summarize the middle (this is a placeholder — real impl would call AI)
104
- const summaryMsg: ChatMessage = {
105
- role: 'system',
106
- content: '[S05: ' + (others.length - preserved) + ' earlier messages summarized — ' +
107
- 'conversation had ' + (others.length - preserved) + ' exchanges covering ' +
108
- this.summarizeTopic(others.slice(0, -preserved)) + ']'
109
- };
110
-
111
- const compacted = systemMsg
112
- ? [systemMsg, summaryMsg, ...recent]
113
- : [summaryMsg, ...recent];
114
-
115
- return {
116
- compacted,
117
- result: {
118
- originalCount: messages.length,
119
- compactedCount: compacted.length,
120
- removedCount: messages.length - compacted.length,
121
- method: 'summarize'
122
- }
123
- };
124
- }
125
-
126
- // Extract simple topic from message content
127
- private summarizeTopic(msgs: ChatMessage[]): string {
128
- const texts = msgs
129
- .filter(m => m.role === 'user')
130
- .map(m => m.content)
131
- .slice(0, 3)
132
- .join(' ')
133
- .substring(0, 50);
134
- return texts || 'various topics';
135
- }
136
-
137
- // Tier 3: Selective — keep turns around tool executions
138
- private selectiveStrategy(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
139
- const systemMsg = messages.find(m => m.role === 'system');
140
- const others = messages.filter(m => m.role !== 'system');
141
- const max = this.config.softLimit || 30;
142
-
143
- if (others.length <= max) {
144
- return {
145
- compacted: messages,
146
- result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
147
- };
148
- }
149
-
150
- // Find indices of tool-related messages
151
- const toolIndices = new Set<number>();
152
- others.forEach((m, i) => {
153
- if (m.tool_calls || m.role === 'tool' || m.role === 'assistant') {
154
- toolIndices.add(i);
155
- }
156
- });
157
-
158
- // Also add indices around tool messages
159
- for (const idx of toolIndices) {
160
- for (let offset = -2; offset <= 2; offset++) {
161
- toolIndices.add(idx + offset);
162
- }
163
- }
164
-
165
- // Keep system message + messages near tool executions + recent messages
166
- const recentCount = Math.floor(max * 0.3); // 30% recent
167
- const toolCount = max - recentCount;
168
-
169
- const recent = others.slice(-recentCount);
170
- const toolMsgs: ChatMessage[] = [];
171
- for (const idx of [...toolIndices].sort((a, b) => a - b)) {
172
- if (toolMsgs.length >= toolCount) break;
173
- if (idx >= 0 && idx < others.length - recentCount) {
174
- toolMsgs.push(others[idx]);
175
- }
176
- }
177
-
178
- const compacted = systemMsg
179
- ? [systemMsg, ...toolMsgs, ...recent]
180
- : [...toolMsgs, ...recent];
181
-
182
- // Deduplicate while preserving order
183
- const seen = new Set<string>();
184
- const deduped = compacted.filter(m => {
185
- const key = m.role + m.content.substring(0, 30);
186
- if (seen.has(key)) return false;
187
- seen.add(key);
188
- return true;
189
- });
190
-
191
- return {
192
- compacted: deduped,
193
- result: {
194
- originalCount: messages.length,
195
- compactedCount: deduped.length,
196
- removedCount: messages.length - deduped.length,
197
- method: 'selective'
198
- }
199
- };
200
- }
201
-
202
- // S05: Check if context is approaching limit
203
- isNearLimit(messages: ChatMessage[], limit: number = 150000): boolean {
204
- // Rough estimate: 1 message ~= 200 tokens on average
205
- const estimatedTokens = messages.length * 200;
206
- return estimatedTokens > limit * 0.8; // 80% of limit
207
- }
208
-
209
- // S05: Get estimated token count
210
- estimateTokens(messages: ChatMessage[]): number {
211
- return messages.reduce((sum, m) => {
212
- // Rough: content chars / 4 tokens
213
- return sum + Math.ceil(m.content.length / 4) + 10; // +10 for role/overhead
214
- }, 0);
215
- }
216
-
217
- // S05: Auto-compact if needed (called from AIEngine or SessionManager)
218
- autoCompact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
219
- if (this.isNearLimit(messages)) {
220
- return this.compact(messages);
221
- }
222
- return {
223
- compacted: messages,
224
- result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
225
- };
226
- }
227
- }
228
-
229
- // ==================== SessionManager Integration ====================
230
-
231
- export function integrateWithSessionManager(SessionManagerClass: any) {
232
- const originalTruncate = SessionManagerClass.prototype.truncate;
233
-
234
- SessionManagerClass.prototype.truncate = function(maxMessages: number, forceCompact = false) {
235
- if (forceCompact) {
236
- const compactor = new ContextCompactor({ hardLimit: maxMessages });
237
- const { compacted, result } = compactor.autoCompact(this.messages);
238
- if (result.method !== 'none') {
239
- this.messages = compacted;
240
- return result;
241
- }
242
- }
243
- return originalTruncate.call(this, maxMessages);
244
- };
245
- }