thatgfsj-code 2.2.9 → 3.0.0

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 (182) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +69 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  86. package/dist/tui/components/ToolCall.js +1 -6
  87. package/dist/tui/components/ToolCall.js.map +1 -1
  88. package/dist/tui/hooks/useChat.d.ts +22 -0
  89. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  90. package/dist/tui/hooks/useChat.js +82 -46
  91. package/dist/tui/hooks/useChat.js.map +1 -1
  92. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  93. package/dist/tui/hooks/useCommands.js +57 -0
  94. package/dist/tui/hooks/useCommands.js.map +1 -1
  95. package/dist/tui/welcome.d.ts.map +1 -1
  96. package/dist/tui/welcome.js +2 -3
  97. package/dist/tui/welcome.js.map +1 -1
  98. package/dist/types.d.ts +79 -6
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +7 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/stableStringify.d.ts +21 -0
  103. package/dist/utils/stableStringify.d.ts.map +1 -0
  104. package/dist/utils/stableStringify.js +53 -0
  105. package/dist/utils/stableStringify.js.map +1 -0
  106. package/dist/utils/thinking.d.ts.map +1 -1
  107. package/dist/utils/thinking.js +0 -3
  108. package/dist/utils/thinking.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/app/index.ts +180 -124
  111. package/src/cache/fingerprint.ts +101 -0
  112. package/src/cache/index.ts +15 -0
  113. package/src/cache/smartModel.ts +52 -0
  114. package/src/cache/stats.ts +199 -0
  115. package/src/cache/volatile.ts +47 -0
  116. package/src/cmd/index.tsx +288 -292
  117. package/src/config/index.ts +156 -148
  118. package/src/config/providers.ts +234 -234
  119. package/src/config/types.ts +67 -53
  120. package/src/hooks/index.ts +111 -111
  121. package/src/llm/anthropic.ts +388 -243
  122. package/src/llm/gemini.ts +169 -169
  123. package/src/llm/index.ts +265 -200
  124. package/src/llm/openai.ts +243 -196
  125. package/src/llm/provider.ts +66 -34
  126. package/src/prompts/index.ts +260 -220
  127. package/src/session/compactor.ts +103 -103
  128. package/src/session/index.ts +181 -168
  129. package/src/session/message.ts +42 -42
  130. package/src/skills/brainstorming.ts +43 -43
  131. package/src/skills/code-review.ts +45 -45
  132. package/src/skills/executing-plans.ts +27 -27
  133. package/src/skills/frontend-design.ts +35 -35
  134. package/src/skills/git-workflow.ts +36 -36
  135. package/src/skills/improve-architecture.ts +38 -38
  136. package/src/skills/index.ts +136 -136
  137. package/src/skills/neuroweave.ts +47 -47
  138. package/src/skills/playwright.ts +72 -72
  139. package/src/skills/prototype.ts +30 -30
  140. package/src/skills/subagent.ts +28 -28
  141. package/src/skills/supabase.ts +44 -44
  142. package/src/skills/systematic-debugging.ts +44 -44
  143. package/src/skills/tdd.ts +39 -39
  144. package/src/skills/triage.ts +35 -35
  145. package/src/skills/verification.ts +31 -31
  146. package/src/skills/writing-plans.ts +42 -42
  147. package/src/tools/nwt.ts +598 -598
  148. package/src/tools/types.ts +122 -122
  149. package/src/tui/app.tsx +199 -186
  150. package/src/tui/components/ChatList.tsx +41 -41
  151. package/src/tui/components/ChatMessage.tsx +54 -54
  152. package/src/tui/components/Header.tsx +56 -29
  153. package/src/tui/components/InitWizard.tsx +217 -132
  154. package/src/tui/components/Markdown.tsx +35 -35
  155. package/src/tui/components/ModelSelector.tsx +87 -87
  156. package/src/tui/components/StatusBar.tsx +30 -30
  157. package/src/tui/components/Thinking.tsx +17 -17
  158. package/src/tui/components/ToolCall.tsx +102 -107
  159. package/src/tui/components/UserInput.tsx +131 -131
  160. package/src/tui/hooks/useChat.ts +287 -238
  161. package/src/tui/hooks/useCommands.ts +234 -176
  162. package/src/tui/index.ts +6 -6
  163. package/src/tui/welcome.ts +177 -178
  164. package/src/types.ts +104 -42
  165. package/src/utils/diff.ts +71 -71
  166. package/src/utils/project.ts +99 -99
  167. package/src/utils/stableStringify.ts +47 -0
  168. package/src/utils/thinking.ts +118 -121
  169. package/tests/cache/fingerprint.test.ts +75 -0
  170. package/tests/cache/stableStringify.test.ts +43 -0
  171. package/tests/cache/stats.test.ts +146 -0
  172. package/tests/cache/volatile.test.ts +75 -0
  173. package/tests/smoke-pollution.mjs +78 -0
  174. package/tests/smoke-thinking.mjs +110 -0
  175. package/tests/smoke-tool-stream.mjs +69 -0
  176. package/tests/smoke-tool.mjs +117 -0
  177. package/.nwt/meta.json +0 -5
  178. package/dist/version.d.ts +0 -16
  179. package/dist/version.d.ts.map +0 -1
  180. package/dist/version.js +0 -16
  181. package/dist/version.js.map +0 -1
  182. package/src/version.ts +0 -16
package/src/cmd/index.tsx CHANGED
@@ -1,292 +1,288 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Thatgfsj Code - CLI Entry Point
5
- *
6
- * v2.2.4 (product 0.4.1): three concrete fixes for the bugs visible
7
- * in the 2.2.3 (product 0.4.0) session log:
8
- *
9
- * 1. Encoding: chcp 65001 used to be wrapped in a silent try/catch,
10
- * and stdout never had its default encoding set. On Windows this
11
- * caused Chinese characters from tool output to render as
12
- * mojibake (ļ ʹ ϵͳ Ƿ ...).
13
- *
14
- * 2. [已中断] hallucination loop: 1.0.4's SessionManager had no
15
- * anti-pollution filter. When the user pressed Ctrl+C mid-
16
- * stream, the truncated assistant message (containing the
17
- * model's literal "[已中断]" marker) got persisted to history.
18
- * On the next turn, the model saw that and kept echoing it.
19
- * v2.1.0 had the right fix (_wasAborted flag + filter) — we
20
- * port it back here.
21
- *
22
- * 3. Tool output visual: 1.0.4 printed `@@TOOL@@{...}` markers
23
- * inline with text, no separator, no truncation. Long tool
24
- * output (registry dumps, dir listings) bled into the next
25
- * AI message. Add separator + truncation + color separation.
26
- */
27
-
28
- if (process.platform === 'win32') {
29
- // Switch Windows console to UTF-8 BEFORE any other code runs.
30
- // v2.2.3 wrapped this in `try { } catch {}` which silently swallowed
31
- // failures (e.g. when running under a non-interactive shell where
32
- // chcp is meaningless). Now we still try, but we also fall back to
33
- // setting the codepage via the parent process's stdio handles.
34
- try {
35
- require('child_process').execSync('chcp 65001 >NUL', { stdio: 'ignore', windowsHide: true });
36
- } catch {
37
- // Non-Windows shell, or chcp unavailable (e.g. git-bash on CI) —
38
- // that's fine, Node defaults to UTF-8 in that environment.
39
- }
40
- }
41
-
42
- // Force stdout/stderr to UTF-8 regardless of platform. Without this,
43
- // Node will emit GBK-encoded bytes for non-ASCII characters on
44
- // Windows even after `chcp 65001`, because the underlying file
45
- // descriptors still report a non-UTF-8 code page.
46
- if (process.stdout.setDefaultEncoding) {
47
- process.stdout.setDefaultEncoding('utf8');
48
- }
49
- if (process.stderr.setDefaultEncoding) {
50
- process.stderr.setDefaultEncoding('utf8');
51
- }
52
-
53
- import { program } from 'commander';
54
- import chalk from 'chalk';
55
- import { App } from '../app/index.js';
56
- import { WelcomeScreen } from '../tui/welcome.js';
57
- import { compressThinking, summarizeThinking, splitThinking } from '../utils/thinking.js';
58
- import { PRODUCT_VERSION } from '../version.js';
59
-
60
- process.on('uncaughtException', (error) => {
61
- console.error(chalk.red('\n Error:'), error.message);
62
- process.exit(1);
63
- });
64
-
65
- process.on('unhandledRejection', (reason) => {
66
- console.error(chalk.red('\n Error:'), reason);
67
- process.exit(1);
68
- });
69
-
70
- program
71
- .name('gfcode')
72
- .description('Thatgfsj Code - AI Coding Assistant')
73
- .version(PRODUCT_VERSION)
74
- .argument('[prompt]', 'Task to execute (omit to start interactive mode)')
75
- .option('-m, --model <model>', 'Specify model')
76
- .option('-i, --interactive', 'Force interactive mode')
77
- .option('--show-thinking', 'Show full <think>...</think> reasoning blocks (default: compress to one-line summary)')
78
- .action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean; showThinking?: boolean }) => {
79
- try {
80
- const app = await App.create();
81
-
82
- // Check if API key is configured
83
- if (!app.config.hasApiKey()) {
84
- console.log(chalk.yellow('\n ⚠ No API key configured\n'));
85
- console.log(chalk.gray(' Run ') + chalk.cyan('gfcode init') + chalk.gray(' to set up your provider.\n'));
86
- const readline = await import('readline');
87
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
88
- const answer = await new Promise<string>(resolve => {
89
- rl.question(chalk.cyan(' Run init now? (Y/n): '), resolve);
90
- });
91
- rl.close();
92
- if (answer.toLowerCase() !== 'n') {
93
- await WelcomeScreen.interactiveSetup();
94
- // Reload config after setup
95
- const newApp = await App.create();
96
- Object.assign(app, newApp);
97
- } else {
98
- process.exit(0);
99
- }
100
- }
101
-
102
- if (options.model) {
103
- await app.config.save({ model: options.model });
104
- }
105
-
106
- if (!prompt || options.interactive) {
107
- // Interactive mode - Ink TUI
108
- const { render } = await import('ink');
109
- const { TuiApp } = await import('../tui/app.js');
110
- const { unmount } = render(<TuiApp app={app} />);
111
- await new Promise<void>((resolve) => {
112
- process.on('exit', () => { unmount(); resolve(); });
113
- });
114
- } else {
115
- // Single prompt mode
116
- console.log(chalk.cyan.bold('\n ⚡ THATGFSJ CODE\n'));
117
- console.log(chalk.gray(' You'));
118
- console.log(chalk.gray(' ' + '─'.repeat(40)));
119
- console.log(' ' + prompt);
120
- console.log();
121
-
122
- app.session.addMessage('user', prompt);
123
- let fullResponse = '';
124
- // v2.2.4: track whether the stream was aborted so we don't
125
- // persist a truncated assistant message (which is what caused
126
- // the [已中断] hallucination loop in v2.2.3).
127
- const abortCtrl = new AbortController();
128
- const onSigInt = () => { abortCtrl.abort(); };
129
- process.once('SIGINT', onSigInt);
130
- // v2.2.5 (product 0.4.2): compress <think> blocks at render time
131
- // AND before persisting to history. Without this, every assistant
132
- // message balloons history with reasoning the user has already
133
- // seen compressed, and context windows blow up fast.
134
- const showThinking = !!options.showThinking;
135
-
136
- try {
137
- process.stdout.write(chalk.gray(' Thinking...'));
138
- const stream = app.streamResponse();
139
-
140
- // v2.2.5: stream-time rendering stays simple we just buffer
141
- // the full response. Post-process at the end (compress
142
- // thinking blocks once). This avoids a state machine for
143
- // "are we inside <think> right now" mid-stream which would
144
- // be flaky if the model splits the tag across chunks.
145
- for await (const chunk of stream) {
146
- if (abortCtrl.signal.aborted) break;
147
- // Clear the entire "Thinking..." line — must use enough
148
- // spaces to overwrite any longer thinking indicator.
149
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
150
-
151
- // Parse tool messages — these should NOT be compressed even
152
- // when thinking is hidden, because they carry real signal
153
- // (tool name + args + result).
154
- if (chunk.includes('@@TOOL@@')) {
155
- const parts = chunk.split('\n');
156
- for (const part of parts) {
157
- if (part.startsWith('@@TOOL@@')) {
158
- try {
159
- const data = JSON.parse(part.slice(8));
160
- if (data.action === 'call') {
161
- console.log();
162
- console.log(chalk.cyan(` ⚙ ${data.name}: ${formatArgs(data.args)}`));
163
- console.log(chalk.gray(' ' + '─'.repeat(40)));
164
- } else if (data.action === 'result') {
165
- const output = data.output || data.error || '';
166
- const lines = output.split('\n');
167
- const MAX_TOOL_LINES = 20;
168
- const truncated = lines.length > MAX_TOOL_LINES;
169
- const visible = truncated ? lines.slice(0, MAX_TOOL_LINES) : lines;
170
- for (const line of visible) {
171
- console.log(chalk.gray(' │ ') + line);
172
- }
173
- if (truncated) {
174
- console.log(chalk.gray(` │ ... (${lines.length - MAX_TOOL_LINES} more lines truncated)`));
175
- }
176
- console.log(chalk.gray(' ' + '─'.repeat(40)));
177
- }
178
- } catch {}
179
- } else if (part) {
180
- // Mid-stream text chunk — leave it raw. The post-
181
- // process step below will compress any <think>
182
- // blocks before printing them as the final rendered
183
- // output. (We can't reliably strip mid-stream
184
- // because the closing </think> might be in a later
185
- // chunk.)
186
- process.stdout.write(chalk.cyan(' │ ') + part);
187
- }
188
- }
189
- } else {
190
- // Plain AI text chunk buffer only. Same reasoning:
191
- // post-process at end avoids mid-stream delimiter
192
- // races.
193
- fullResponse += chunk;
194
- // Show the chunk as it streams, but we'll re-render the
195
- // final compressed version below. To avoid double-
196
- // printing, only echo when we're NOT going to post-
197
- // process (i.e., when showThinking is true and we want
198
- // to see everything live).
199
- if (showThinking) {
200
- process.stdout.write(chalk.cyan(' │ ') + chunk);
201
- }
202
- }
203
- }
204
-
205
- // v2.2.5: post-process. Strip <think> blocks, summarize,
206
- // and print the cleaned conclusion if we suppressed it
207
- // during streaming.
208
- if (!showThinking && !abortCtrl.signal.aborted && fullResponse) {
209
- const split = splitThinking(fullResponse);
210
- if (split.thinking) {
211
- // Clear what we already streamed (we suppressed text
212
- // chunks during streaming, so there's nothing to clear
213
- // unless showThinking was on; in that case the text
214
- // already contains the <think> block live and there's
215
- // no point double-printing).
216
- const summary = summarizeThinking(split);
217
- if (summary) {
218
- console.log(chalk.gray(` ${summary}`));
219
- }
220
- if (split.conclusion) {
221
- console.log(chalk.cyan(' │ ') + split.conclusion);
222
- }
223
- } else if (fullResponse.trim()) {
224
- // No thinking block found — print the conclusion if we
225
- // hadn't already (showThinking=false suppresses
226
- // streaming text).
227
- console.log(chalk.cyan(' │ ') + fullResponse);
228
- }
229
- }
230
-
231
- console.log();
232
- // v2.2.4: skip persistence on abort.
233
- if (!abortCtrl.signal.aborted) {
234
- // v2.2.5: persist only the conclusion (no <think> blocks)
235
- // when compression is enabled. This keeps the conversation
236
- // log readable AND keeps context-window usage low.
237
- const toPersist = showThinking
238
- ? fullResponse
239
- : compressThinking(fullResponse, false);
240
- const accepted = app.session.addMessageSafe('assistant', toPersist);
241
- if (!accepted) {
242
- console.error(chalk.yellow(
243
- ' ⚠️ Dropped assistant message containing [已中断] marker.\n' +
244
- ' (prevents hallucination loop — try your question again)'
245
- ));
246
- }
247
- } else {
248
- console.log(chalk.yellow(' ⏹ Cancelled (response not saved)'));
249
- }
250
- } catch (error: any) {
251
- const msg = error.message || String(error);
252
- if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
253
- console.error(chalk.red('\n API key invalid or expired.'));
254
- console.log(chalk.gray(' Run ') + chalk.cyan('gfcode init') + chalk.gray(' to reconfigure.\n'));
255
- } else if (msg.includes('429') || msg.includes('rate limit') || msg.includes('quota')) {
256
- console.error(chalk.red('\n Rate limit or quota exceeded.'));
257
- console.log(chalk.gray(' Wait or run ') + chalk.cyan('gfcode init') + chalk.gray(' to switch provider.\n'));
258
- } else if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
259
- console.error(chalk.red('\n ❌ Cannot connect to API server.'));
260
- console.log(chalk.gray(' Check network or run ') + chalk.cyan('gfcode init') + chalk.gray('.\n'));
261
- } else {
262
- console.error(chalk.red(`\n Error: ${msg}`));
263
- }
264
- } finally {
265
- process.removeListener('SIGINT', onSigInt);
266
- }
267
- }
268
- } catch (error: any) {
269
- console.error(chalk.red(`\n ${error.message}`));
270
- process.exit(1);
271
- }
272
- });
273
-
274
- program
275
- .command('init')
276
- .description('Configure API key and model')
277
- .action(async () => {
278
- await WelcomeScreen.interactiveSetup();
279
- });
280
-
281
- program.parse(process.argv);
282
-
283
- function formatArgs(args: string): string {
284
- try {
285
- const obj = JSON.parse(args);
286
- return Object.entries(obj)
287
- .map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 40 ? v.slice(0, 40) + '...' : JSON.stringify(v)}`)
288
- .join(' ');
289
- } catch {
290
- return args.length > 60 ? args.slice(0, 60) + '...' : args;
291
- }
292
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Thatgfsj Code - CLI Entry Point
5
+ *
6
+ * v2.2.4 (product 0.4.1): three concrete fixes for the bugs visible
7
+ * in the 2.2.3 (product 0.4.0) session log:
8
+ *
9
+ * 1. Encoding: chcp 65001 used to be wrapped in a silent try/catch,
10
+ * and stdout never had its default encoding set. On Windows this
11
+ * caused Chinese characters from tool output to render as
12
+ * mojibake (ļ ʹ ϵͳ Ƿ ...).
13
+ *
14
+ * 2. [已中断] hallucination loop: 1.0.4's SessionManager had no
15
+ * anti-pollution filter. When the user pressed Ctrl+C mid-
16
+ * stream, the truncated assistant message (containing the
17
+ * model's literal "[已中断]" marker) got persisted to history.
18
+ * On the next turn, the model saw that and kept echoing it.
19
+ * v2.1.0 had the right fix (_wasAborted flag + filter) — we
20
+ * port it back here.
21
+ *
22
+ * 3. Tool output visual: 1.0.4 printed `@@TOOL@@{...}` markers
23
+ * inline with text, no separator, no truncation. Long tool
24
+ * output (registry dumps, dir listings) bled into the next
25
+ * AI message. Add separator + truncation + color separation.
26
+ */
27
+
28
+ if (process.platform === 'win32') {
29
+ // Switch Windows console to UTF-8 BEFORE any other code runs.
30
+ // v2.2.3 wrapped this in `try { } catch {}` which silently swallowed
31
+ // failures (e.g. when running under a non-interactive shell where
32
+ // chcp is meaningless). Now we still try, but we also fall back to
33
+ // setting the codepage via the parent process's stdio handles.
34
+ try {
35
+ require('child_process').execSync('chcp 65001 >NUL', { stdio: 'ignore', windowsHide: true });
36
+ } catch {
37
+ // Non-Windows shell, or chcp unavailable (e.g. git-bash on CI) —
38
+ // that's fine, Node defaults to UTF-8 in that environment.
39
+ }
40
+ }
41
+
42
+ // Force stdout/stderr to UTF-8 regardless of platform. Without this,
43
+ // Node will emit GBK-encoded bytes for non-ASCII characters on
44
+ // Windows even after `chcp 65001`, because the underlying file
45
+ // descriptors still report a non-UTF-8 code page.
46
+ if (process.stdout.setDefaultEncoding) {
47
+ process.stdout.setDefaultEncoding('utf8');
48
+ }
49
+ if (process.stderr.setDefaultEncoding) {
50
+ process.stderr.setDefaultEncoding('utf8');
51
+ }
52
+
53
+ import { program } from 'commander';
54
+ import chalk from 'chalk';
55
+ import { App } from '../app/index.js';
56
+ import { WelcomeScreen } from '../tui/welcome.js';
57
+ import { compressThinking, summarizeThinking, splitThinking } from '../utils/thinking.js';
58
+
59
+ process.on('uncaughtException', (error) => {
60
+ console.error(chalk.red('\n Error:'), error.message);
61
+ process.exit(1);
62
+ });
63
+
64
+ process.on('unhandledRejection', (reason) => {
65
+ console.error(chalk.red('\n Error:'), reason);
66
+ process.exit(1);
67
+ });
68
+
69
+ program
70
+ .name('gfcode')
71
+ .description('Thatgfsj Code - AI Coding Assistant')
72
+ .version('0.5.0')
73
+ .argument('[prompt]', 'Task to execute (omit to start interactive mode)')
74
+ .option('-m, --model <model>', 'Specify model')
75
+ .option('-i, --interactive', 'Force interactive mode')
76
+ .option('--show-thinking', 'Show full <think>...</think> reasoning blocks (default: compress to one-line summary)')
77
+ .action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean; showThinking?: boolean }) => {
78
+ try {
79
+ const app = await App.create();
80
+
81
+ // Check if API key is configured
82
+ if (!app.config.hasApiKey()) {
83
+ console.log(chalk.yellow('\n ⚠ No API key configured\n'));
84
+ console.log(chalk.gray(' Run ') + chalk.cyan('gfcode init') + chalk.gray(' to set up your provider.\n'));
85
+ const readline = await import('readline');
86
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
87
+ const answer = await new Promise<string>(resolve => {
88
+ rl.question(chalk.cyan(' Run init now? (Y/n): '), resolve);
89
+ });
90
+ rl.close();
91
+ if (answer.toLowerCase() !== 'n') {
92
+ await WelcomeScreen.interactiveSetup();
93
+ // Reload config after setup
94
+ const newApp = await App.create();
95
+ Object.assign(app, newApp);
96
+ } else {
97
+ process.exit(0);
98
+ }
99
+ }
100
+
101
+ if (options.model) {
102
+ await app.config.save({ model: options.model });
103
+ }
104
+
105
+ if (!prompt || options.interactive) {
106
+ // Interactive mode - Ink TUI
107
+ const { render } = await import('ink');
108
+ const { TuiApp } = await import('../tui/app.js');
109
+ const { unmount } = render(<TuiApp app={app} />);
110
+ await new Promise<void>((resolve) => {
111
+ process.on('exit', () => { unmount(); resolve(); });
112
+ });
113
+ } else {
114
+ // Single prompt mode
115
+ console.log(chalk.cyan.bold('\n ⚡ THATGFSJ CODE\n'));
116
+ console.log(chalk.gray(' You'));
117
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
118
+ console.log(' ' + prompt);
119
+ console.log();
120
+
121
+ app.session.addMessage('user', prompt);
122
+ let fullResponse = '';
123
+ // v2.2.4: track whether the stream was aborted so we don't
124
+ // persist a truncated assistant message (which is what caused
125
+ // the [已中断] hallucination loop in v2.2.3).
126
+ const abortCtrl = new AbortController();
127
+ const onSigInt = () => { abortCtrl.abort(); };
128
+ process.once('SIGINT', onSigInt);
129
+ // v2.2.5 (product 0.4.2): compress <think> blocks at render time
130
+ // AND before persisting to history. Without this, every assistant
131
+ // message balloons history with reasoning the user has already
132
+ // seen compressed, and context windows blow up fast.
133
+ const showThinking = !!options.showThinking;
134
+
135
+ try {
136
+ process.stdout.write(chalk.gray(' Thinking...'));
137
+ const stream = app.streamResponse();
138
+
139
+ // v3.0.0: stream is now AsyncGenerator<StreamChunk> instead of
140
+ // string. Tool calls come as { type: 'tool_calls', toolCalls }
141
+ // with structured args; usage comes as { type: 'usage', usage }
142
+ // at end. We render text live, render tool dispatches inline, and
143
+ // capture the final response text into fullResponse for the
144
+ // post-process step below.
145
+ for await (const chunk of stream) {
146
+ if (abortCtrl.signal.aborted) break;
147
+ // Clear the entire "Thinking..." line — must use enough
148
+ // spaces to overwrite any longer thinking indicator.
149
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
150
+
151
+ switch (chunk.type) {
152
+ case 'text': {
153
+ if (chunk.content) {
154
+ fullResponse += chunk.content;
155
+ // Show the chunk as it streams, but we'll re-render the
156
+ // final compressed version below. To avoid double-
157
+ // printing, only echo when we're NOT going to post-
158
+ // process (i.e., when showThinking is true and we want
159
+ // to see everything live).
160
+ if (showThinking) {
161
+ process.stdout.write(chalk.cyan(' │ ') + chunk.content);
162
+ }
163
+ }
164
+ break;
165
+ }
166
+ case 'thinking': {
167
+ // Reasoning content. Strip from persisted content via
168
+ // compressThinking; show live if user asked for it.
169
+ if (showThinking && chunk.content) {
170
+ fullResponse += chunk.content;
171
+ process.stdout.write(chalk.cyan(' │ ') + chunk.content);
172
+ }
173
+ break;
174
+ }
175
+ case 'tool_calls': {
176
+ if (chunk.toolCalls && chunk.toolCalls.length > 0) {
177
+ for (const tc of chunk.toolCalls) {
178
+ console.log();
179
+ console.log(chalk.cyan(` ⚙ ${tc.function.name}: ${formatArgs(tc.function.arguments)}`));
180
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
181
+ }
182
+ }
183
+ break;
184
+ }
185
+ case 'usage': {
186
+ // v3.0.0: surface cache stats at the end of single-prompt
187
+ // mode so CLI users can see hit-rate without /cache command.
188
+ const u = chunk.usage;
189
+ const hit = u.prompt_cache_hit_tokens || u.cache_read_input_tokens || 0;
190
+ const miss = u.prompt_cache_miss_tokens || u.cache_creation_input_tokens || (u.prompt_tokens - hit);
191
+ if (hit > 0 || miss > 0) {
192
+ const total = hit + miss;
193
+ const rate = total > 0 ? ((hit / total) * 100).toFixed(1) : '0.0';
194
+ console.log(chalk.gray(` ⚡ cache: ${rate}% hit (${hit} / ${total} tokens)`));
195
+ }
196
+ break;
197
+ }
198
+ }
199
+ }
200
+
201
+ // v2.2.5: post-process. Strip <think> blocks, summarize,
202
+ // and print the cleaned conclusion if we suppressed it
203
+ // during streaming.
204
+ if (!showThinking && !abortCtrl.signal.aborted && fullResponse) {
205
+ const split = splitThinking(fullResponse);
206
+ if (split.thinking) {
207
+ // Clear what we already streamed (we suppressed text
208
+ // chunks during streaming, so there's nothing to clear
209
+ // unless showThinking was on; in that case the text
210
+ // already contains the <think> block live and there's
211
+ // no point double-printing).
212
+ const summary = summarizeThinking(split);
213
+ if (summary) {
214
+ console.log(chalk.gray(` ${summary}`));
215
+ }
216
+ if (split.conclusion) {
217
+ console.log(chalk.cyan(' │ ') + split.conclusion);
218
+ }
219
+ } else if (fullResponse.trim()) {
220
+ // No thinking block found — print the conclusion if we
221
+ // hadn't already (showThinking=false suppresses
222
+ // streaming text).
223
+ console.log(chalk.cyan(' │ ') + fullResponse);
224
+ }
225
+ }
226
+
227
+ console.log();
228
+ // v2.2.4: skip persistence on abort.
229
+ if (!abortCtrl.signal.aborted) {
230
+ // v2.2.5: persist only the conclusion (no <think> blocks)
231
+ // when compression is enabled. This keeps the conversation
232
+ // log readable AND keeps context-window usage low.
233
+ const toPersist = showThinking
234
+ ? fullResponse
235
+ : compressThinking(fullResponse, false);
236
+ const accepted = app.session.addMessageSafe('assistant', toPersist);
237
+ if (!accepted) {
238
+ console.error(chalk.yellow(
239
+ ' ⚠️ Dropped assistant message containing [已中断] marker.\n' +
240
+ ' (prevents hallucination loop try your question again)'
241
+ ));
242
+ }
243
+ } else {
244
+ console.log(chalk.yellow(' ⏹ Cancelled (response not saved)'));
245
+ }
246
+ } catch (error: any) {
247
+ const msg = error.message || String(error);
248
+ if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
249
+ console.error(chalk.red('\n ❌ API key invalid or expired.'));
250
+ console.log(chalk.gray(' Run ') + chalk.cyan('gfcode init') + chalk.gray(' to reconfigure.\n'));
251
+ } else if (msg.includes('429') || msg.includes('rate limit') || msg.includes('quota')) {
252
+ console.error(chalk.red('\n ❌ Rate limit or quota exceeded.'));
253
+ console.log(chalk.gray(' Wait or run ') + chalk.cyan('gfcode init') + chalk.gray(' to switch provider.\n'));
254
+ } else if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
255
+ console.error(chalk.red('\n ❌ Cannot connect to API server.'));
256
+ console.log(chalk.gray(' Check network or run ') + chalk.cyan('gfcode init') + chalk.gray('.\n'));
257
+ } else {
258
+ console.error(chalk.red(`\n Error: ${msg}`));
259
+ }
260
+ } finally {
261
+ process.removeListener('SIGINT', onSigInt);
262
+ }
263
+ }
264
+ } catch (error: any) {
265
+ console.error(chalk.red(`\n ${error.message}`));
266
+ process.exit(1);
267
+ }
268
+ });
269
+
270
+ program
271
+ .command('init')
272
+ .description('Configure API key and model')
273
+ .action(async () => {
274
+ await WelcomeScreen.interactiveSetup();
275
+ });
276
+
277
+ program.parse(process.argv);
278
+
279
+ function formatArgs(args: string): string {
280
+ try {
281
+ const obj = JSON.parse(args);
282
+ return Object.entries(obj)
283
+ .map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 40 ? v.slice(0, 40) + '...' : JSON.stringify(v)}`)
284
+ .join(' ');
285
+ } catch {
286
+ return args.length > 60 ? args.slice(0, 60) + '...' : args;
287
+ }
288
+ }