thatgfsj-code 3.0.3 → 3.0.6

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 (212) hide show
  1. package/README.md +64 -21
  2. package/dist/app/index.d.ts +93 -22
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +232 -40
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/smartModel.d.ts +29 -22
  7. package/dist/cache/smartModel.d.ts.map +1 -1
  8. package/dist/cache/smartModel.js +33 -40
  9. package/dist/cache/smartModel.js.map +1 -1
  10. package/dist/cmd/index.d.ts +12 -20
  11. package/dist/cmd/index.d.ts.map +1 -1
  12. package/dist/cmd/index.js +192 -146
  13. package/dist/cmd/index.js.map +1 -1
  14. package/dist/config/index.d.ts +4 -0
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +9 -6
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/config/types.d.ts +5 -0
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/llm/anthropic.d.ts +1 -1
  21. package/dist/llm/anthropic.d.ts.map +1 -1
  22. package/dist/llm/anthropic.js +116 -84
  23. package/dist/llm/anthropic.js.map +1 -1
  24. package/dist/llm/gemini.d.ts +27 -3
  25. package/dist/llm/gemini.d.ts.map +1 -1
  26. package/dist/llm/gemini.js +196 -73
  27. package/dist/llm/gemini.js.map +1 -1
  28. package/dist/llm/index.d.ts +20 -1
  29. package/dist/llm/index.d.ts.map +1 -1
  30. package/dist/llm/index.js +71 -10
  31. package/dist/llm/index.js.map +1 -1
  32. package/dist/llm/openai.d.ts +5 -1
  33. package/dist/llm/openai.d.ts.map +1 -1
  34. package/dist/llm/openai.js +96 -56
  35. package/dist/llm/openai.js.map +1 -1
  36. package/dist/mcp/client.d.ts +84 -34
  37. package/dist/mcp/client.d.ts.map +1 -1
  38. package/dist/mcp/client.js +291 -126
  39. package/dist/mcp/client.js.map +1 -1
  40. package/dist/session/compactor.d.ts +27 -7
  41. package/dist/session/compactor.d.ts.map +1 -1
  42. package/dist/session/compactor.js +78 -23
  43. package/dist/session/compactor.js.map +1 -1
  44. package/dist/session/index.d.ts +96 -33
  45. package/dist/session/index.d.ts.map +1 -1
  46. package/dist/session/index.js +278 -60
  47. package/dist/session/index.js.map +1 -1
  48. package/dist/tools/file.d.ts +10 -2
  49. package/dist/tools/file.d.ts.map +1 -1
  50. package/dist/tools/file.js +54 -5
  51. package/dist/tools/file.js.map +1 -1
  52. package/dist/tools/git.d.ts +16 -2
  53. package/dist/tools/git.d.ts.map +1 -1
  54. package/dist/tools/git.js +61 -18
  55. package/dist/tools/git.js.map +1 -1
  56. package/dist/tools/nwt.d.ts.map +1 -1
  57. package/dist/tools/nwt.js +8 -1
  58. package/dist/tools/nwt.js.map +1 -1
  59. package/dist/tools/search.d.ts +8 -1
  60. package/dist/tools/search.d.ts.map +1 -1
  61. package/dist/tools/search.js +105 -34
  62. package/dist/tools/search.js.map +1 -1
  63. package/dist/tools/shell.d.ts +0 -4
  64. package/dist/tools/shell.d.ts.map +1 -1
  65. package/dist/tools/shell.js +11 -26
  66. package/dist/tools/shell.js.map +1 -1
  67. package/dist/tools/types.d.ts +7 -0
  68. package/dist/tools/types.d.ts.map +1 -1
  69. package/dist/tools/types.js.map +1 -1
  70. package/dist/tui/app.d.ts.map +1 -1
  71. package/dist/tui/app.js +90 -18
  72. package/dist/tui/app.js.map +1 -1
  73. package/dist/tui/components/ChatMessage.d.ts.map +1 -1
  74. package/dist/tui/components/ChatMessage.js +9 -2
  75. package/dist/tui/components/ChatMessage.js.map +1 -1
  76. package/dist/tui/components/ConfirmPrompt.d.ts +21 -0
  77. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  78. package/dist/tui/components/ConfirmPrompt.js +45 -0
  79. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  80. package/dist/tui/components/Header.d.ts +10 -5
  81. package/dist/tui/components/Header.d.ts.map +1 -1
  82. package/dist/tui/components/Header.js +13 -5
  83. package/dist/tui/components/Header.js.map +1 -1
  84. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  85. package/dist/tui/components/InitWizard.js +8 -13
  86. package/dist/tui/components/InitWizard.js.map +1 -1
  87. package/dist/tui/components/StatusBar.d.ts +7 -0
  88. package/dist/tui/components/StatusBar.d.ts.map +1 -1
  89. package/dist/tui/components/StatusBar.js +8 -3
  90. package/dist/tui/components/StatusBar.js.map +1 -1
  91. package/dist/tui/components/Thinking.d.ts +5 -0
  92. package/dist/tui/components/Thinking.d.ts.map +1 -1
  93. package/dist/tui/components/Thinking.js +24 -2
  94. package/dist/tui/components/Thinking.js.map +1 -1
  95. package/dist/tui/components/ToolCall.d.ts +6 -1
  96. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  97. package/dist/tui/components/ToolCall.js +50 -22
  98. package/dist/tui/components/ToolCall.js.map +1 -1
  99. package/dist/tui/components/UserInput.d.ts +5 -0
  100. package/dist/tui/components/UserInput.d.ts.map +1 -1
  101. package/dist/tui/components/UserInput.js +9 -13
  102. package/dist/tui/components/UserInput.js.map +1 -1
  103. package/dist/tui/hooks/useChat.d.ts +1 -0
  104. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  105. package/dist/tui/hooks/useChat.js +36 -16
  106. package/dist/tui/hooks/useChat.js.map +1 -1
  107. package/dist/tui/hooks/useCommands.d.ts +7 -1
  108. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  109. package/dist/tui/hooks/useCommands.js +83 -49
  110. package/dist/tui/hooks/useCommands.js.map +1 -1
  111. package/dist/tui/theme.d.ts +34 -0
  112. package/dist/tui/theme.d.ts.map +1 -0
  113. package/dist/tui/theme.js +34 -0
  114. package/dist/tui/theme.js.map +1 -0
  115. package/dist/tui/welcome.d.ts.map +1 -1
  116. package/dist/tui/welcome.js +9 -7
  117. package/dist/tui/welcome.js.map +1 -1
  118. package/dist/types.d.ts +16 -1
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/utils/diff.d.ts +5 -0
  121. package/dist/utils/diff.d.ts.map +1 -1
  122. package/dist/utils/diff.js +15 -0
  123. package/dist/utils/diff.js.map +1 -1
  124. package/dist/version.d.ts +12 -0
  125. package/dist/version.d.ts.map +1 -0
  126. package/dist/version.js +29 -0
  127. package/dist/version.js.map +1 -0
  128. package/package.json +19 -7
  129. package/CHANGELOG.md +0 -192
  130. package/DEVELOPMENT.md +0 -286
  131. package/ROADMAP.md +0 -91
  132. package/docs/API_KEY_GUIDE.md +0 -236
  133. package/docs/FAQ.md +0 -182
  134. package/install.bat +0 -63
  135. package/install.ps1 +0 -238
  136. package/install.sh +0 -113
  137. package/src/app/index.ts +0 -197
  138. package/src/cache/fingerprint.ts +0 -101
  139. package/src/cache/index.ts +0 -21
  140. package/src/cache/smartModel.ts +0 -145
  141. package/src/cache/stats.ts +0 -199
  142. package/src/cache/volatile.ts +0 -47
  143. package/src/cmd/index.tsx +0 -288
  144. package/src/config/index.ts +0 -158
  145. package/src/config/providers.ts +0 -234
  146. package/src/config/types.ts +0 -71
  147. package/src/hooks/index.ts +0 -111
  148. package/src/llm/anthropic.ts +0 -413
  149. package/src/llm/gemini.ts +0 -169
  150. package/src/llm/index.ts +0 -302
  151. package/src/llm/openai.ts +0 -243
  152. package/src/llm/provider.ts +0 -71
  153. package/src/mcp/client.ts +0 -330
  154. package/src/prompts/index.ts +0 -260
  155. package/src/session/compactor.ts +0 -103
  156. package/src/session/index.ts +0 -181
  157. package/src/session/message.ts +0 -42
  158. package/src/skills/brainstorming.ts +0 -43
  159. package/src/skills/code-review.ts +0 -45
  160. package/src/skills/executing-plans.ts +0 -27
  161. package/src/skills/frontend-design.ts +0 -35
  162. package/src/skills/git-workflow.ts +0 -36
  163. package/src/skills/improve-architecture.ts +0 -38
  164. package/src/skills/index.ts +0 -136
  165. package/src/skills/neuroweave.ts +0 -47
  166. package/src/skills/playwright.ts +0 -72
  167. package/src/skills/prototype.ts +0 -30
  168. package/src/skills/subagent.ts +0 -28
  169. package/src/skills/supabase.ts +0 -44
  170. package/src/skills/systematic-debugging.ts +0 -44
  171. package/src/skills/tdd.ts +0 -39
  172. package/src/skills/triage.ts +0 -35
  173. package/src/skills/verification.ts +0 -31
  174. package/src/skills/writing-plans.ts +0 -42
  175. package/src/tools/file.ts +0 -143
  176. package/src/tools/git.ts +0 -132
  177. package/src/tools/index.ts +0 -111
  178. package/src/tools/nwt.ts +0 -598
  179. package/src/tools/search.ts +0 -263
  180. package/src/tools/shell.ts +0 -136
  181. package/src/tools/types.ts +0 -122
  182. package/src/tui/app.tsx +0 -159
  183. package/src/tui/components/ChatList.tsx +0 -41
  184. package/src/tui/components/ChatMessage.tsx +0 -54
  185. package/src/tui/components/Header.tsx +0 -66
  186. package/src/tui/components/InitWizard.tsx +0 -251
  187. package/src/tui/components/Markdown.tsx +0 -35
  188. package/src/tui/components/ModelSelector.tsx +0 -107
  189. package/src/tui/components/StatusBar.tsx +0 -30
  190. package/src/tui/components/Thinking.tsx +0 -17
  191. package/src/tui/components/ToolCall.tsx +0 -102
  192. package/src/tui/components/UserInput.tsx +0 -151
  193. package/src/tui/hooks/useChat.ts +0 -287
  194. package/src/tui/hooks/useCommands.ts +0 -273
  195. package/src/tui/index.ts +0 -6
  196. package/src/tui/welcome.ts +0 -177
  197. package/src/types.ts +0 -104
  198. package/src/utils/diff.ts +0 -71
  199. package/src/utils/project.ts +0 -99
  200. package/src/utils/stableStringify.ts +0 -47
  201. package/src/utils/thinking.ts +0 -119
  202. package/tests/cache/fingerprint.test.ts +0 -75
  203. package/tests/cache/providerCatalog.test.ts +0 -31
  204. package/tests/cache/smartModel.test.ts +0 -112
  205. package/tests/cache/stableStringify.test.ts +0 -43
  206. package/tests/cache/stats.test.ts +0 -146
  207. package/tests/cache/volatile.test.ts +0 -75
  208. package/tests/smoke-pollution.mjs +0 -78
  209. package/tests/smoke-thinking.mjs +0 -110
  210. package/tests/smoke-tool-stream.mjs +0 -69
  211. package/tests/smoke-tool.mjs +0 -117
  212. package/tsconfig.json +0 -21
@@ -1,199 +0,0 @@
1
- /**
2
- * Cache statistics store.
3
- *
4
- * Tracks per-round cache hit/miss token counts across the user's local
5
- * sessions and persists them to `~/.thatgfsj/cache-stats.json`. The store
6
- * is consumed by:
7
- * - the TUI Header (`⚡ 命中率 87%`)
8
- * - the /cache CLI command (detailed breakdown)
9
- * - the /cache reset command (clears the file)
10
- *
11
- * Provider-agnostic. Anthropic reports cache_creation_input_tokens /
12
- * cache_read_input_tokens; DeepSeek reports prompt_cache_hit_tokens /
13
- * prompt_cache_miss_tokens; OpenAI does not surface any cache stats at
14
- * all (their automatic cache is invisible to the client). We normalize
15
- * both shapes into a single internal model.
16
- *
17
- * Cost estimation is intentionally simplified:
18
- * - cache_read = 10% of normal input price
19
- * - cache_creation = 125% of normal input price (Anthropic charges extra
20
- * on first write)
21
- * - We compare against the baseline "what would this have cost without
22
- * caching" and report the delta as estimated savings.
23
- *
24
- * Prices are in CNY per million tokens. Defaults reflect Anthropic Claude
25
- * Sonnet on the official API; users on cheaper providers may see savings
26
- * proportional to their actual price floor. The numbers are *estimates*
27
- * — the goal is to give the user a sense of order of magnitude, not a
28
- * billable invoice.
29
- */
30
-
31
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
32
- import { dirname, join } from 'path';
33
- import { homedir } from 'os';
34
- import type { Usage } from '../types.js';
35
-
36
- /** Per-million-token price in CNY. Adjust if your provider differs significantly. */
37
- const PRICE = {
38
- normalInput: 3,
39
- cacheRead: 0.3,
40
- cacheCreation: 3.75,
41
- output: 15,
42
- };
43
-
44
- /** Maximum number of recent rounds kept in the history array (rolling window). */
45
- const HISTORY_LIMIT = 50;
46
-
47
- export interface CacheStats {
48
- /** Total tokens served from cache (Anthropic cache_read / DeepSeek hit). */
49
- totalReadTokens: number;
50
- /** Total tokens written into cache (Anthropic cache_creation / DeepSeek miss prefix written). */
51
- totalCreationTokens: number;
52
- /** Total input tokens (prompt_tokens). */
53
- totalInputTokens: number;
54
- /** Total output tokens. */
55
- totalOutputTokens: number;
56
- /** Number of completed rounds. */
57
- totalRequests: number;
58
- /** Last N round snapshots (oldest first). */
59
- history: Array<{
60
- ts: number;
61
- read: number;
62
- creation: number;
63
- input: number;
64
- output: number;
65
- hitRate: number;
66
- }>;
67
- }
68
-
69
- export interface CacheSnapshot extends CacheStats {
70
- /** 0..1 ratio of cached tokens to total input tokens. */
71
- hitRate: number;
72
- /** Estimated CNY saved vs uncached baseline. */
73
- estimatedSavingsCNY: number;
74
- }
75
-
76
- const EMPTY_STATS: CacheStats = {
77
- totalReadTokens: 0,
78
- totalCreationTokens: 0,
79
- totalInputTokens: 0,
80
- totalOutputTokens: 0,
81
- totalRequests: 0,
82
- history: [],
83
- };
84
-
85
- /**
86
- * Estimate the CNY saved by cache_read vs a hypothetical uncached baseline.
87
- * Negative values are clamped to zero (e.g. a relay station that returned
88
- * bogus cache stats).
89
- */
90
- export function estimateSavingsCNY(stats: CacheStats): number {
91
- const baselineCost = (stats.totalReadTokens / 1e6) * PRICE.normalInput;
92
- const cachedCost = (stats.totalReadTokens / 1e6) * PRICE.cacheRead;
93
- return Math.max(0, baselineCost - cachedCost);
94
- }
95
-
96
- /**
97
- * Persists cache stats to disk. Safe to call from a hot path — uses atomic
98
- * write (write to .tmp, rename) to avoid corruption on crash.
99
- */
100
- export class CacheStatsStore {
101
- private stats: CacheStats;
102
- private path: string;
103
-
104
- constructor(path?: string) {
105
- this.path = path ?? join(homedir(), '.thatgfsj', 'cache-stats.json');
106
- this.stats = this.load();
107
- }
108
-
109
- /**
110
- * Record a single round's usage. Provider-agnostic: accepts both Anthropic
111
- * (cache_creation_input_tokens / cache_read_input_tokens) and DeepSeek
112
- * (prompt_cache_hit_tokens / prompt_cache_miss_tokens) shapes. If a
113
- * provider returned neither (e.g. OpenAI), the call is a no-op apart
114
- * from updating totalRequest count.
115
- */
116
- record(usage: Usage): void {
117
- const read = usage.cache_read_input_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
118
- const creation = usage.cache_creation_input_tokens
119
- ?? (usage.prompt_cache_miss_tokens ?? 0);
120
- const input = usage.prompt_tokens ?? 0;
121
- const output = usage.completion_tokens ?? 0;
122
-
123
- // Avoid double-counting: if the provider reported both cache_creation
124
- // and prompt_cache_miss_tokens we'd otherwise sum them. The ?? in the
125
- // chain above only uses prompt_cache_miss_tokens when
126
- // cache_creation_input_tokens is undefined, which is what we want.
127
-
128
- this.stats.totalReadTokens += read;
129
- this.stats.totalCreationTokens += creation;
130
- this.stats.totalInputTokens += input;
131
- this.stats.totalOutputTokens += output;
132
- this.stats.totalRequests += 1;
133
-
134
- // Per-round snapshot (for /cache command history view).
135
- const total = read + (input - read);
136
- const hitRate = total > 0 ? read / total : 0;
137
- this.stats.history.push({ ts: Date.now(), read, creation, input, output, hitRate });
138
- if (this.stats.history.length > HISTORY_LIMIT) {
139
- this.stats.history.splice(0, this.stats.history.length - HISTORY_LIMIT);
140
- }
141
-
142
- this.save();
143
- }
144
-
145
- /**
146
- * Reset the stats. Used by `/cache reset`.
147
- */
148
- reset(): void {
149
- this.stats = { ...EMPTY_STATS, history: [] };
150
- this.save();
151
- }
152
-
153
- /**
154
- * Read-only snapshot for rendering (TUI Header, /cache command).
155
- */
156
- snapshot(): CacheSnapshot {
157
- const input = this.stats.totalInputTokens;
158
- const read = this.stats.totalReadTokens;
159
- const hitRate = input > 0 ? read / input : 0;
160
- return {
161
- ...this.stats,
162
- hitRate,
163
- estimatedSavingsCNY: estimateSavingsCNY(this.stats),
164
- };
165
- }
166
-
167
- /** Direct accessor for tests. */
168
- get raw(): CacheStats { return this.stats; }
169
-
170
- // -- I/O --
171
-
172
- private load(): CacheStats {
173
- try {
174
- if (!existsSync(this.path)) return { ...EMPTY_STATS, history: [] };
175
- const txt = readFileSync(this.path, 'utf-8');
176
- const parsed = JSON.parse(txt);
177
- // Tolerate partial / older shapes.
178
- return {
179
- totalReadTokens: parsed.totalReadTokens ?? 0,
180
- totalCreationTokens: parsed.totalCreationTokens ?? 0,
181
- totalInputTokens: parsed.totalInputTokens ?? 0,
182
- totalOutputTokens: parsed.totalOutputTokens ?? 0,
183
- totalRequests: parsed.totalRequests ?? 0,
184
- history: Array.isArray(parsed.history) ? parsed.history : [],
185
- };
186
- } catch {
187
- return { ...EMPTY_STATS, history: [] };
188
- }
189
- }
190
-
191
- private save(): void {
192
- try {
193
- mkdirSync(dirname(this.path), { recursive: true });
194
- writeFileSync(this.path, JSON.stringify(this.stats, null, 2), 'utf-8');
195
- } catch {
196
- // best-effort persistence; do not crash the chat loop on disk errors
197
- }
198
- }
199
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * VolatileScratch — per-round reasoning / planning state that MUST NOT be
3
- * uploaded to the API.
4
- *
5
- * Reasonix Principle #3 (Volatile Scratch):
6
- * The model's thinking content, intermediate plans, and other
7
- * intra-round state should never be appended to the conversation
8
- * history. They are useful for the UI (so users can debug / audit
9
- * reasoning) and for cross-iteration handoff within the same round
10
- * (e.g. retry-after-tool-failure), but they are NOT part of the cache
11
- * prefix and re-uploading them in the next round would:
12
- * 1. waste tokens (potentially a lot — reasoning can dwarf the answer)
13
- * 2. cause context-window pressure
14
- * 3. risk leaking partial / contradictory reasoning back to the model
15
- *
16
- * Usage (in LLMService / useChat when receiving 'thinking' chunks):
17
- * scratch.write(chunk.content);
18
- * // ... later in the same round ...
19
- * scratch.read(); // → joined reasoning so far
20
- * scratch.reset(); // → call at end of round OR on /new
21
- */
22
- export class VolatileScratch {
23
- private current: string[] = [];
24
-
25
- reset(): void {
26
- this.current = [];
27
- }
28
-
29
- write(data: string): void {
30
- if (!data) return;
31
- this.current.push(data);
32
- }
33
-
34
- read(): string {
35
- return this.current.join('\n');
36
- }
37
-
38
- /** True when no scratch has been written since the last reset. */
39
- isEmpty(): boolean {
40
- return this.current.length === 0;
41
- }
42
-
43
- /** Number of segments written. Useful for UI display ("thought for N lines"). */
44
- size(): number {
45
- return this.current.length;
46
- }
47
- }
package/src/cmd/index.tsx DELETED
@@ -1,288 +0,0 @@
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
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * Config Manager - Handles configuration loading and saving
3
- * Supports custom providers (relay stations / 中转站)
4
- */
5
-
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
- import { join, dirname } from 'path';
8
- import { homedir } from 'os';
9
- import type { Config, AIConfig, ProviderName } from './types.js';
10
- import { PROVIDERS, getApiKeyFromEnv, isCustomProvider } from './providers.js';
11
-
12
- const DEFAULT_CONFIG: Config = {
13
- model: 'Qwen/Qwen2.5-7B-Instruct',
14
- apiKey: '',
15
- temperature: 0.7,
16
- maxTokens: 4096,
17
- contextLength: 50,
18
- provider: 'siliconflow',
19
- // v3.0.0: prompt cache policy. Default = enabled, 'auto' TTL.
20
- // v3.0.3: 'auto' TTL lets the runtime's decideTTL() pick 5m for short
21
- // sessions and 1h for long ones (>= 15 turns or >= 50k chars). The
22
- // chosen TTL is sticky across rounds within a session so the cache
23
- // prefix stays stable.
24
- cache: {
25
- enabled: true,
26
- ttl: 'auto',
27
- strategy: 'auto',
28
- },
29
- };
30
-
31
- export class ConfigManager {
32
- private configPath: string;
33
- private config: Config;
34
-
35
- private constructor(configPath: string, config: Config) {
36
- this.configPath = configPath;
37
- this.config = config;
38
- }
39
-
40
- /**
41
- * Load configuration from file + environment variables
42
- */
43
- static async load(): Promise<ConfigManager> {
44
- const configDir = join(homedir(), '.thatgfsj');
45
- const configPath = join(configDir, 'config.json');
46
-
47
- let config = { ...DEFAULT_CONFIG };
48
-
49
- // Load from file
50
- try {
51
- if (existsSync(configPath)) {
52
- const data = readFileSync(configPath, 'utf-8');
53
- config = { ...config, ...JSON.parse(data) };
54
- }
55
- } catch {
56
- // Use defaults if file is corrupted
57
- }
58
-
59
- // Resolve provider
60
- config = ConfigManager.resolveProvider(config);
61
-
62
- return new ConfigManager(configPath, config);
63
- }
64
-
65
- /**
66
- * Resolve provider settings: API key from env, base URL, model
67
- */
68
- private static resolveProvider(config: Config): Config {
69
- const provider = config.provider || 'siliconflow';
70
- const providerConfig = PROVIDERS[provider];
71
-
72
- if (!providerConfig) {
73
- return { ...config, provider: 'siliconflow', baseUrl: PROVIDERS.siliconflow.baseUrl };
74
- }
75
-
76
- // Model: env MODEL > config > provider default
77
- const model = process.env.MODEL || config.model || providerConfig.defaultModel;
78
-
79
- // API key: env > config
80
- let apiKey = config.apiKey;
81
- if (provider === 'ollama') {
82
- apiKey = '';
83
- } else if (!apiKey) {
84
- apiKey = getApiKeyFromEnv(provider);
85
- }
86
-
87
- // Base URL: config > env > provider default
88
- let baseUrl = config.baseUrl;
89
- if (!baseUrl) {
90
- // Check env for custom base URL
91
- if (provider === 'custom_openai') {
92
- baseUrl = process.env.CUSTOM_BASE_URL || '';
93
- } else if (provider === 'custom_anthropic') {
94
- baseUrl = process.env.CUSTOM_BASE_URL || '';
95
- } else {
96
- baseUrl = providerConfig.baseUrl;
97
- }
98
- }
99
-
100
- return { ...config, provider, baseUrl, model, apiKey: apiKey || '' };
101
- }
102
-
103
- /**
104
- * Get the current config
105
- */
106
- get(): Config {
107
- return { ...this.config };
108
- }
109
-
110
- /**
111
- * Get AIConfig for LLM providers
112
- */
113
- getAIConfig(): AIConfig {
114
- return {
115
- model: this.config.model,
116
- apiKey: this.config.apiKey,
117
- temperature: this.config.temperature,
118
- maxTokens: this.config.maxTokens,
119
- baseUrl: this.config.baseUrl,
120
- provider: this.config.provider,
121
- };
122
- }
123
-
124
- /**
125
- * Get provider format (openai | anthropic | gemini)
126
- */
127
- getProviderFormat(): 'openai' | 'anthropic' | 'gemini' {
128
- return PROVIDERS[this.config.provider]?.format || 'openai';
129
- }
130
-
131
- /**
132
- * Update config and save to file
133
- */
134
- async save(updates: Partial<Config>): Promise<void> {
135
- this.config = { ...this.config, ...updates };
136
-
137
- const dir = dirname(this.configPath);
138
- if (!existsSync(dir)) {
139
- mkdirSync(dir, { recursive: true });
140
- }
141
-
142
- writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
143
- }
144
-
145
- /**
146
- * Check if an API key is configured
147
- */
148
- hasApiKey(): boolean {
149
- return !!this.config.apiKey;
150
- }
151
-
152
- /**
153
- * Check if using a custom provider
154
- */
155
- isCustomProvider(): boolean {
156
- return isCustomProvider(this.config.provider);
157
- }
158
- }