thatgfsj-code 2.2.9 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) 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 +76 -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/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.js.map +1 -1
  95. package/dist/tui/hooks/useChat.d.ts +22 -0
  96. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  97. package/dist/tui/hooks/useChat.js +82 -46
  98. package/dist/tui/hooks/useChat.js.map +1 -1
  99. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  100. package/dist/tui/hooks/useCommands.js +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -0
  178. package/tests/cache/stableStringify.test.ts +43 -0
  179. package/tests/cache/stats.test.ts +146 -0
  180. package/tests/cache/volatile.test.ts +75 -0
  181. package/tests/smoke-pollution.mjs +78 -0
  182. package/tests/smoke-thinking.mjs +110 -0
  183. package/tests/smoke-tool-stream.mjs +69 -0
  184. package/tests/smoke-tool.mjs +117 -0
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. package/src/version.ts +0 -16
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Smart-model routing (P3 of the Reasonix plan).
3
+ *
4
+ * Idea: classify the user's input as "simple" or "complex" and route
5
+ * simple queries to a cheaper / faster model while keeping the main model
6
+ * for complex work. The classification is intentionally trivial — prompt
7
+ * length + presence of recent tool calls — because the cost of a
8
+ * misclassification (a slightly worse answer on a simple query) is much
9
+ * lower than the cost of running a frontier model on every greeting.
10
+ *
11
+ * Trade-off: each routing decision adds one model swap (different
12
+ * `model` field on the wire). If the cache prefix is keyed on the model
13
+ * string, the cache miss rate can spike on every simple query. To keep
14
+ * the prefix stable, callers should pass a *family* (e.g. "anthropic" or
15
+ * "deepseek") rather than a specific model name when forwarding the
16
+ * prompt to the downstream provider — that's outside the scope of this
17
+ * hook; this module only answers the question "should I downgrade?".
18
+ *
19
+ * v3.0.0: shipped as a stub. Real decision logic is conservative
20
+ * (downgrade only when prompt is short AND no recent tool activity).
21
+ * Future iterations can add heuristic or classifier-based routing.
22
+ */
23
+
24
+ import type { ChatMessage } from '../types.js';
25
+
26
+ export interface SmartModelDecision {
27
+ /** Should we route this turn to a cheaper model? */
28
+ downgrade: boolean;
29
+ /** Why we made this call — useful for `/cache` debug output. */
30
+ reason: string;
31
+ }
32
+
33
+ /**
34
+ * Heuristic: downgrade when
35
+ * 1. The latest user message is short (< 200 chars), AND
36
+ * 2. The last 3 messages contain no assistant tool_calls.
37
+ *
38
+ * This covers greetings, one-line questions, and simple follow-ups
39
+ * ("thanks", "ok", "explain this one line") while routing anything
40
+ * that triggered a tool to the main model.
41
+ */
42
+ export function shouldDowngrade(messages: ChatMessage[], lastUserInput: string): SmartModelDecision {
43
+ if (lastUserInput.length > 200) {
44
+ return { downgrade: false, reason: 'prompt-too-long' };
45
+ }
46
+ const recent = messages.slice(-3);
47
+ const hasRecentToolCalls = recent.some(m => m.tool_calls && m.tool_calls.length > 0);
48
+ if (hasRecentToolCalls) {
49
+ return { downgrade: false, reason: 'recent-tool-call' };
50
+ }
51
+ return { downgrade: true, reason: 'short-no-tools' };
52
+ }
@@ -0,0 +1,199 @@
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
+ }
@@ -0,0 +1,47 @@
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
+ }