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
package/dist/types.js CHANGED
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Global shared types for Thatgfsj Code
3
3
  * Only the most fundamental types used across modules
4
+ *
5
+ * v3.0.0+: Extended for Reasonix-style prompt caching:
6
+ * - ChatMessage.content now accepts ContentBlock[] (Anthropic-style)
7
+ * - cache_control field for explicit Anthropic cache breakpoints
8
+ * - usage extended with cache_creation_input_tokens / cache_read_input_tokens
9
+ * (Anthropic) and prompt_cache_hit_tokens / prompt_cache_miss_tokens (DeepSeek)
10
+ * - StreamChunk now has 'thinking' / 'usage' variants for structured streaming
4
11
  */
5
12
  export {};
6
13
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Deterministic JSON serializer.
3
+ *
4
+ * JSON.stringify in V8 iterates keys in insertion order, which means objects
5
+ * built with literal `{a, b, c}` are stable, but anything built via spread,
6
+ * Object.fromEntries, or merged maps can produce different byte sequences
7
+ * between requests. That byte-sequence instability is fatal for prompt-cache
8
+ * hit-rate: a single reordered key invalidates the cache fingerprint.
9
+ *
10
+ * stableStringify sorts object keys lexicographically before recursing, so
11
+ * any pair of structurally equal objects produces identical JSON text.
12
+ *
13
+ * Performance note: O(n log n) on key counts per object. For typical request
14
+ * bodies (<200 KB) the overhead is negligible (<5 ms). The main hot path is
15
+ * the tool/parameters tree which is small and stable in practice.
16
+ *
17
+ * Reference: Reasonix-style "stableStringify" requirement for prefix-cache
18
+ * byte-level stability (see DeepSeek Reasonix, Section 3).
19
+ */
20
+ export declare function stableStringify(value: unknown): string;
21
+ //# sourceMappingURL=stableStringify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableStringify.d.ts","sourceRoot":"","sources":["../../src/utils/stableStringify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Deterministic JSON serializer.
3
+ *
4
+ * JSON.stringify in V8 iterates keys in insertion order, which means objects
5
+ * built with literal `{a, b, c}` are stable, but anything built via spread,
6
+ * Object.fromEntries, or merged maps can produce different byte sequences
7
+ * between requests. That byte-sequence instability is fatal for prompt-cache
8
+ * hit-rate: a single reordered key invalidates the cache fingerprint.
9
+ *
10
+ * stableStringify sorts object keys lexicographically before recursing, so
11
+ * any pair of structurally equal objects produces identical JSON text.
12
+ *
13
+ * Performance note: O(n log n) on key counts per object. For typical request
14
+ * bodies (<200 KB) the overhead is negligible (<5 ms). The main hot path is
15
+ * the tool/parameters tree which is small and stable in practice.
16
+ *
17
+ * Reference: Reasonix-style "stableStringify" requirement for prefix-cache
18
+ * byte-level stability (see DeepSeek Reasonix, Section 3).
19
+ */
20
+ export function stableStringify(value) {
21
+ return stringify(value);
22
+ }
23
+ function stringify(value) {
24
+ if (value === null)
25
+ return 'null';
26
+ if (value === undefined)
27
+ return 'null'; // OpenAI wire format does not carry undefined
28
+ const t = typeof value;
29
+ if (t === 'string')
30
+ return JSON.stringify(value);
31
+ if (t === 'number')
32
+ return Number.isFinite(value) ? String(value) : 'null';
33
+ if (t === 'boolean')
34
+ return value ? 'true' : 'false';
35
+ if (Array.isArray(value)) {
36
+ return '[' + value.map((v) => stringify(v)).join(',') + ']';
37
+ }
38
+ if (t === 'object') {
39
+ const obj = value;
40
+ const keys = Object.keys(obj).sort();
41
+ const parts = [];
42
+ for (const k of keys) {
43
+ const v = obj[k];
44
+ if (v === undefined)
45
+ continue; // omit undefined to match OpenAI/Anthropic convention
46
+ parts.push(JSON.stringify(k) + ':' + stringify(v));
47
+ }
48
+ return '{' + parts.join(',') + '}';
49
+ }
50
+ // functions / symbols / bigints: drop
51
+ return 'null';
52
+ }
53
+ //# sourceMappingURL=stableStringify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableStringify.js","sourceRoot":"","sources":["../../src/utils/stableStringify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,CAAC,8CAA8C;IACtF,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC;IACvB,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAe,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACrF,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACrD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,IAAI,CAAC,KAAK,SAAS;gBAAE,SAAS,CAAC,sDAAsD;YACrF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACrC,CAAC;IACD,sCAAsC;IACtC,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../../src/utils/thinking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,QAAA,MAAM,iBAAiB,EAAE,MAAM,EAO9B,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAqB5D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAM9D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe,GAAG,MAAM,CAMvF;AAoBD,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../../src/utils/thinking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,QAAA,MAAM,iBAAiB,EAAE,MAAM,EAI9B,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAqB5D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAM9D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe,GAAG,MAAM,CAMvF;AAoBD,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -32,11 +32,8 @@
32
32
  */
33
33
  const THINKING_PATTERNS = [
34
34
  /<think>[\s\S]*?<\/think>/gi,
35
- /<thinking>[\s\S]*?<\/thinking>/gi,
36
35
  /<reasoning>[\s\S]*?<\/reasoning>/gi,
37
- /<THINK>[\s\S]*?<\/THINK>/gi, // v2.2.7 edge: case variant
38
36
  /\[THINK\][\s\S]*?\[\/THINK\]/gi,
39
- /\[\/?think\]/gi, // bare [think] / [/think] (rare)
40
37
  ];
41
38
  export function splitThinking(content) {
42
39
  if (!content) {
@@ -1 +1 @@
1
- {"version":3,"file":"thinking.js","sourceRoot":"","sources":["../../src/utils/thinking.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thinking.js","sourceRoot":"","sources":["../../src/utils/thinking.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thatgfsj-code",
3
- "version": "2.2.9",
4
- "description": "Thatgfsj Code - AI Coding Assistant",
3
+ "version": "3.0.1",
4
+ "description": "Thatgfsj Code - AI Coding Assistant with Reasonix-style prompt caching",
5
5
  "main": "dist/cmd/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -13,9 +13,8 @@
13
13
  "start": "node dist/cmd/index.js",
14
14
  "dev": "tsc && node dist/cmd/index.js",
15
15
  "link": "npm link",
16
- "test": "node tests/smoke-thinking.mjs && node tests/smoke-tool.mjs && node tests/smoke-pollution.mjs && node tests/edge-thinking.mjs && node tests/edge-tool.mjs && node tests/edge-pollution.mjs && node tests/edge-session.mjs && node tests/edge-cli.mjs",
17
- "test:single": "node tests/",
18
- "prepublishOnly": "npm run build && npm test"
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
19
18
  },
20
19
  "keywords": [
21
20
  "ai",
@@ -43,6 +42,8 @@
43
42
  "@types/marked-terminal": "^6.1.1",
44
43
  "@types/node": "^20.10.0",
45
44
  "@types/react": "^19.2.17",
46
- "typescript": "^5.3.0"
45
+ "ink-testing-library": "^4.0.0",
46
+ "typescript": "^5.3.0",
47
+ "vitest": "^4.1.10"
47
48
  }
48
49
  }
package/src/app/index.ts CHANGED
@@ -1,124 +1,189 @@
1
- /**
2
- * App - Core application singleton
3
- * Simplified: directly uses LLMService (which has built-in agent loop)
4
- */
5
-
6
- import { ConfigManager } from '../config/index.js';
7
- import { LLMService } from '../llm/index.js';
8
- import { SessionManager } from '../session/index.js';
9
- import { ToolRegistry } from '../tools/index.js';
10
- import { HookManager } from '../hooks/index.js';
11
- import { SystemPromptBuilder } from '../prompts/index.js';
12
- import { SkillRegistry } from '../skills/index.js';
13
- import { compressThinking } from '../utils/thinking.js';
14
- import type { ChatMessage, ChatResponse } from '../types.js';
15
-
16
- export class App {
17
- config: ConfigManager;
18
- llm: LLMService;
19
- session: SessionManager;
20
- tools: ToolRegistry;
21
- hooks: HookManager;
22
- prompts: SystemPromptBuilder;
23
- skills: SkillRegistry;
24
- /**
25
- * v2.2.5 (product 0.4.2): toggle <think> block compression. Default
26
- * true. Toggled by `--show-thinking` on the CLI or `/thinking on|off`
27
- * in the REPL.
28
- */
29
- showThinking: boolean = false;
30
-
31
- private constructor(
32
- config: ConfigManager,
33
- llm: LLMService,
34
- session: SessionManager,
35
- tools: ToolRegistry,
36
- hooks: HookManager,
37
- prompts: SystemPromptBuilder,
38
- skills: SkillRegistry,
39
- ) {
40
- this.config = config;
41
- this.llm = llm;
42
- this.session = session;
43
- this.tools = tools;
44
- this.hooks = hooks;
45
- this.prompts = prompts;
46
- this.skills = skills;
47
- }
48
-
49
- static async create(): Promise<App> {
50
- const config = await ConfigManager.load();
51
- const aiConfig = config.getAIConfig();
52
-
53
- const llm = LLMService.fromConfig(aiConfig);
54
- const session = new SessionManager(config.get().contextLength || 50);
55
- const tools = new ToolRegistry();
56
- const hooks = new HookManager();
57
- const skills = new SkillRegistry();
58
-
59
- // Register tools with LLM service
60
- llm.registerTools(tools.list());
61
-
62
- // Auto-init NWT timeline
63
- const nwtTool = tools.get('nwt');
64
- if (nwtTool) {
65
- await nwtTool.execute({ action: 'init' });
66
- }
67
-
68
- // Build system prompt with active skills
69
- const prompts = new SystemPromptBuilder({
70
- cwd: process.cwd(),
71
- tools: tools.list(),
72
- permissionMode: 'ask',
73
- skillsPrompt: skills.getActivePrompts(),
74
- });
75
- session.addMessage('system', prompts.build());
76
-
77
- return new App(config, llm, session, tools, hooks, prompts, skills);
78
- }
79
-
80
- /**
81
- * Stream a response for the current session messages.
82
- * The LLMService handles the full agent loop internally.
83
- */
84
- async *streamResponse(messages?: ChatMessage[]): AsyncGenerator<string, void> {
85
- const msgs = messages || this.session.getMessages();
86
- for await (const chunk of this.llm.chatStream(msgs)) {
87
- yield chunk;
88
- }
89
- }
90
-
91
- /**
92
- * Run a single prompt (non-interactive mode)
93
- *
94
- * v2.2.4 (port from v2.1.0): persistence of the assistant message
95
- * uses addMessageSafe, which drops the message if it contains
96
- * pollution markers like "[已中断]".
97
- *
98
- * v2.2.5 (product 0.4.2): persistence also strips <think> blocks
99
- * (and similar reasoning delimiters) when showThinking is false,
100
- * so the conversation log stays compact.
101
- */
102
- async runPrompt(prompt: string): Promise<string> {
103
- this.session.addMessage('user', prompt);
104
-
105
- let fullResponse = '';
106
- try {
107
- for await (const chunk of this.streamResponse()) {
108
- process.stdout.write(chunk);
109
- fullResponse += chunk;
110
- }
111
- } catch (err) {
112
- // Re-throw without persisting partial response. Persisting
113
- // truncated output here was the source of the [已中断] loop in
114
- // v2.2.3.
115
- throw err;
116
- }
117
-
118
- console.log();
119
- // v2.2.5: compress <think> blocks before persisting.
120
- const toPersist = compressThinking(fullResponse, this.showThinking);
121
- this.session.addMessageSafe('assistant', toPersist);
122
- return fullResponse;
123
- }
124
- }
1
+ /**
2
+ * App - Core application singleton
3
+ * Simplified: directly uses LLMService (which has built-in agent loop)
4
+ *
5
+ * v3.0.0+: streamResponse yields structured StreamChunk
6
+ * - runPrompt streams { type: 'text' } chunks to stdout and captures the
7
+ * final usage for cache stats recording.
8
+ */
9
+
10
+ import { ConfigManager } from '../config/index.js';
11
+ import { LLMService } from '../llm/index.js';
12
+ import { SessionManager } from '../session/index.js';
13
+ import { ToolRegistry } from '../tools/index.js';
14
+ import { HookManager } from '../hooks/index.js';
15
+ import { SystemPromptBuilder } from '../prompts/index.js';
16
+ import { SkillRegistry } from '../skills/index.js';
17
+ import { CacheStatsStore } from '../cache/stats.js';
18
+ import { compressThinking } from '../utils/thinking.js';
19
+ import type { ChatMessage, ChatResponse, StreamChunk, Usage } from '../types.js';
20
+
21
+ export class App {
22
+ config: ConfigManager;
23
+ llm: LLMService;
24
+ session: SessionManager;
25
+ tools: ToolRegistry;
26
+ hooks: HookManager;
27
+ prompts: SystemPromptBuilder;
28
+ skills: SkillRegistry;
29
+ /**
30
+ * v3.0.0: persistent cache stats store. The single source of truth for
31
+ * cache hit-rate and estimated savings, surfaced through the TUI Header
32
+ * and the /cache command.
33
+ */
34
+ cacheStats: CacheStatsStore;
35
+ /**
36
+ * v2.2.5 (product 0.4.2): toggle block compression. Default
37
+ * true. Toggled by `--show-thinking` on the CLI or `/thinking on|off`
38
+ * in the REPL.
39
+ */
40
+ showThinking: boolean = false;
41
+
42
+ private constructor(
43
+ config: ConfigManager,
44
+ llm: LLMService,
45
+ session: SessionManager,
46
+ tools: ToolRegistry,
47
+ hooks: HookManager,
48
+ prompts: SystemPromptBuilder,
49
+ skills: SkillRegistry,
50
+ cacheStats: CacheStatsStore,
51
+ ) {
52
+ this.config = config;
53
+ this.llm = llm;
54
+ this.session = session;
55
+ this.tools = tools;
56
+ this.hooks = hooks;
57
+ this.prompts = prompts;
58
+ this.skills = skills;
59
+ this.cacheStats = cacheStats;
60
+ }
61
+
62
+ static async create(): Promise<App> {
63
+ const config = await ConfigManager.load();
64
+ const aiConfig = config.getAIConfig();
65
+
66
+ const llm = LLMService.fromConfig(aiConfig);
67
+ const cacheStats = new CacheStatsStore();
68
+ const session = new SessionManager(config.get().contextLength || 50);
69
+ // v3.0.0: do not mutate messages when context grows — instead, surface
70
+ // a "consider /new" toast via onSuggestNewSession. The TUI wires this
71
+ // up in app.tsx; in CLI single-prompt mode it's a no-op (one-shot).
72
+ session.onSuggestNewSession = (info) => {
73
+ console.warn(
74
+ `\n ⚠️ 上下文较长(${info.currentLength}/${info.max})。` +
75
+ `建议调 /new 开新会话(NWT 已自动归档历史)\n`,
76
+ );
77
+ };
78
+ const tools = new ToolRegistry();
79
+ const hooks = new HookManager();
80
+ const skills = new SkillRegistry();
81
+
82
+ // Register tools with LLM service
83
+ llm.registerTools(tools.list());
84
+
85
+ // Auto-init NWT timeline
86
+ const nwtTool = tools.get('nwt');
87
+ if (nwtTool) {
88
+ await nwtTool.execute({ action: 'init' });
89
+ }
90
+
91
+ // Build system prompt with active skills
92
+ const prompts = new SystemPromptBuilder({
93
+ cwd: process.cwd(),
94
+ tools: tools.list(),
95
+ permissionMode: 'ask',
96
+ skillsPrompt: skills.getActivePrompts(),
97
+ });
98
+ session.addMessage('system', prompts.build());
99
+
100
+ return new App(config, llm, session, tools, hooks, prompts, skills, cacheStats);
101
+ }
102
+
103
+ /**
104
+ * Stream a response for the current session messages.
105
+ * The LLMService handles the full agent loop internally.
106
+ *
107
+ * Yields structured StreamChunks. Returns the final ChatResponse (with usage
108
+ * if the provider reported it) so the caller can record cache stats.
109
+ *
110
+ * Implementation note: we drain the inner stream manually so the final
111
+ * ChatResponse returned by LLMService.chatStream is propagated as this
112
+ * generator's return value. Using yield* doesn't carry the return value
113
+ * through TS's AsyncGenerator<T, R> type inference in this version of
114
+ * TypeScript, so we wrap with an inner for-await and explicit return.
115
+ */
116
+ async *streamResponse(messages?: ChatMessage[]): AsyncGenerator<StreamChunk, ChatResponse> {
117
+ const msgs = messages || this.session.getMessages();
118
+ const inner = this.llm.chatStream(msgs);
119
+ const debugUsage = !!process.env.GFCODE_DEBUG_USAGE;
120
+ let next = await inner.next();
121
+ while (!next.done) {
122
+ // Forward chunks unchanged, but capture usage into the persistent
123
+ // cache stats store so the TUI Header / /cache command can read it.
124
+ if (next.value && next.value.type === 'usage') {
125
+ try { this.cacheStats.record(next.value.usage); } catch { /* best-effort */ }
126
+ if (debugUsage) {
127
+ // v3.0.0 DEBUG: dump raw usage fields to stderr so the user can
128
+ // confirm whether the upstream provider/relay forwards cache stats.
129
+ // Enable with: GFCODE_DEBUG_USAGE=1 gfcode ...
130
+ process.stderr.write(
131
+ '[debug_usage] ' + JSON.stringify(next.value.usage) + '\n'
132
+ );
133
+ }
134
+ }
135
+ yield next.value;
136
+ next = await inner.next();
137
+ }
138
+ // The generator's return value (ChatResponse with usage) is propagated
139
+ // to callers via `for await ... await streamResponse.next()` semantics.
140
+ return next.value;
141
+ }
142
+
143
+ /**
144
+ * Run a single prompt (non-interactive mode)
145
+ *
146
+ * v2.2.4 (port from v2.1.0): persistence of the assistant message
147
+ * uses addMessageSafe, which drops the message if it contains
148
+ * pollution markers like "[已中断]".
149
+ *
150
+ * v2.2.5 (product 0.4.2): persistence also strips blocks
151
+ * (and similar reasoning delimiters) when showThinking is false,
152
+ * so the conversation log stays compact.
153
+ *
154
+ * v3.0.0: yields structured StreamChunks; final usage is captured
155
+ * into onUsage callback for cache stats persistence.
156
+ */
157
+ async runPrompt(prompt: string, onUsage?: (usage: Usage) => void): Promise<string> {
158
+ this.session.addMessage('user', prompt);
159
+
160
+ let fullResponse = '';
161
+ let finalUsage: ChatResponse['usage'] | undefined;
162
+ try {
163
+ for await (const chunk of this.streamResponse()) {
164
+ if (chunk.type === 'text' && chunk.content) {
165
+ process.stdout.write(chunk.content);
166
+ fullResponse += chunk.content;
167
+ } else if (chunk.type === 'usage') {
168
+ finalUsage = chunk.usage;
169
+ }
170
+ }
171
+ } catch (err) {
172
+ // Re-throw without persisting partial response. Persisting
173
+ // truncated output here was the source of the [已中断] loop in
174
+ // v2.2.3.
175
+ throw err;
176
+ }
177
+
178
+ console.log();
179
+ // v2.2.5: compress blocks before persisting.
180
+ const toPersist = compressThinking(fullResponse, this.showThinking);
181
+ this.session.addMessageSafe('assistant', toPersist);
182
+
183
+ // v3.0.0: forward usage to caller (CLI single-shot mode records stats too)
184
+ if (finalUsage && onUsage) {
185
+ try { onUsage(finalUsage); } catch { /* best-effort */ }
186
+ }
187
+ return fullResponse;
188
+ }
189
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Cache fingerprints — small, stable hashes used to detect when the
3
+ * "cacheable prefix" of an LLM request changes between rounds.
4
+ *
5
+ * Why this exists:
6
+ * Anthropic prompt cache and DeepSeek automatic prefix cache both break
7
+ * when the request body changes upstream of the cache breakpoint. To
8
+ * debug "why is my hit-rate 0%", we want a quick fingerprint of the
9
+ * things that *should* be cacheable (tools + system prefix) so we can
10
+ * log it on every request and compare across rounds.
11
+ *
12
+ * The fingerprint itself is NOT used to decide anything (the upstream
13
+ * provider does the actual cache lookup). It's purely a debug /
14
+ * observability tool — same role as `reasoning_content` in the message
15
+ * schema: captured for transparency, not for control flow.
16
+ *
17
+ * Implementation:
18
+ * - Uses stableStringify (sibling module) so logically equal objects
19
+ * always produce the same JSON.
20
+ * - Hashes with sha256 (Node built-in `crypto`), truncated to 16 chars.
21
+ * 16 hex chars = 64 bits, which is plenty for collision-resistance
22
+ * across a single user's session.
23
+ */
24
+
25
+ import { createHash } from 'crypto';
26
+ import type { Tool } from '../tools/types.js';
27
+ import type { SystemSegment } from '../prompts/index.js';
28
+
29
+ /**
30
+ * Hash a stable JSON representation of `value` and return a 16-char hex prefix.
31
+ * Recursively normalizes object keys (sorted) so insertion order does not
32
+ * affect the output.
33
+ */
34
+ export function fingerprint(value: unknown): string {
35
+ // stableStringify is imported lazily to avoid a circular dep — fingerprint
36
+ // is consumed by both providers (which sit above utils/) and the cache
37
+ // stats store (which sits next to fingerprint). Top-level import works
38
+ // because utils has no other consumers besides fingerprint, so we just
39
+ // import statically.
40
+ return createHash('sha256')
41
+ .update(stableStringifyLocal(value))
42
+ .digest('hex')
43
+ .slice(0, 16);
44
+ }
45
+
46
+ /**
47
+ * Compute a fingerprint of the registered tool schemas.
48
+ *
49
+ * We deliberately pick only `name`, `description`, and `inputSchema` — the
50
+ * three fields that matter for the upstream prompt cache. Other Tool
51
+ * metadata (version strings, descriptions of internal handlers, etc) is
52
+ * ignored because it never reaches the wire.
53
+ */
54
+ export function fingerprintTools(tools: Tool[]): string {
55
+ const minimal = tools.map(t => ({
56
+ name: t.name,
57
+ description: t.description,
58
+ inputSchema: t.inputSchema,
59
+ }));
60
+ return fingerprint({ tools: minimal });
61
+ }
62
+
63
+ /**
64
+ * Compute a fingerprint of the system prompt's *immutable* portion.
65
+ *
66
+ * Volatile segments (NWT history, current time) are deliberately excluded
67
+ * because they change between rounds and would defeat the fingerprint's
68
+ * purpose as a "did the cacheable prefix change?" signal.
69
+ */
70
+ export function fingerprintSystemPrefix(segments: SystemSegment[]): string {
71
+ const immutable = segments.filter(s => !s.volatile);
72
+ return fingerprint({ system: immutable.map(s => ({ name: s.name, content: s.content })) });
73
+ }
74
+
75
+ // -- Local copy of stableStringify to avoid a circular dep. This must stay
76
+ // byte-for-byte identical to src/utils/stableStringify.ts. If you change
77
+ // one, change both.
78
+
79
+ function stableStringifyLocal(value: unknown): string {
80
+ if (value === null) return 'null';
81
+ if (value === undefined) return 'null';
82
+ const t = typeof value;
83
+ if (t === 'string') return JSON.stringify(value);
84
+ if (t === 'number') return Number.isFinite(value as number) ? String(value) : 'null';
85
+ if (t === 'boolean') return value ? 'true' : 'false';
86
+ if (Array.isArray(value)) {
87
+ return '[' + value.map((v) => stableStringifyLocal(v)).join(',') + ']';
88
+ }
89
+ if (t === 'object') {
90
+ const obj = value as Record<string, unknown>;
91
+ const keys = Object.keys(obj).sort();
92
+ const parts: string[] = [];
93
+ for (const k of keys) {
94
+ const v = obj[k];
95
+ if (v === undefined) continue;
96
+ parts.push(JSON.stringify(k) + ':' + stableStringifyLocal(v));
97
+ }
98
+ return '{' + parts.join(',') + '}';
99
+ }
100
+ return 'null';
101
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cache subsystem barrel. Re-exports the public surface used by the TUI,
3
+ * the App singleton, and the LLM service.
4
+ *
5
+ * Modules:
6
+ * - fingerprint.ts: stable hashes for tools + immutable system prefix
7
+ * - stats.ts: CacheStatsStore — per-round hit/miss + cost savings
8
+ * - volatile.ts: VolatileScratch — per-round scratch that never reaches the API
9
+ * - smartModel.ts: smart-model routing hook (simple → mini, complex → main)
10
+ */
11
+
12
+ export { fingerprint, fingerprintTools, fingerprintSystemPrefix } from './fingerprint.js';
13
+ export { CacheStatsStore, estimateSavingsCNY, type CacheStats, type CacheSnapshot } from './stats.js';
14
+ export { VolatileScratch } from './volatile.js';
15
+ export { shouldDowngrade, type SmartModelDecision } from './smartModel.js';