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
package/install.sh DELETED
@@ -1,113 +0,0 @@
1
- #!/bin/bash
2
- # Thatgfsj Code Installer for macOS/Linux
3
- # Usage: curl -sL https://raw.githubusercontent.com/Thatgfsj/thatgfsj-code/main/install.sh | bash
4
-
5
- set -e
6
-
7
- # Colors
8
- RED='\033[0;31m'
9
- GREEN='\033[0;32m'
10
- YELLOW='\033[1;33m'
11
- CYAN='\033[0;36m'
12
- NC='\033[0m' # No Color
13
-
14
- echo ""
15
- echo -e "${CYAN} Thatgfsj Code 安装向导${NC}"
16
- echo -e "${CYAN} =======================${NC}"
17
- echo ""
18
-
19
- # ============== Step 1: Check Node.js ==============
20
- echo -e "${YELLOW}[*] 检查 Node.js...${NC}"
21
-
22
- if command -v node &> /dev/null; then
23
- NODE_VERSION=$(node --version)
24
- MAJOR_VERSION=$(echo $NODE_VERSION | cut -d'v' -f2 | cut -d'.' -f1)
25
- if [ "$MAJOR_VERSION" -ge 18 ]; then
26
- echo -e "${GREEN}[✓] Node.js $NODE_VERSION 已安装${NC}"
27
- else
28
- echo -e "${RED}[✗] Node.js 版本过低,需要 v18+${NC}"
29
- echo " 请访问 https://nodejs.org 升级"
30
- exit 1
31
- fi
32
- else
33
- echo -e "${YELLOW}[*] 未检测到 Node.js,开始安装...${NC}"
34
-
35
- # Try Homebrew (macOS)
36
- if command -v brew &> /dev/null; then
37
- echo -e "${YELLOW}[*] 使用 Homebrew 安装...${NC}"
38
- brew install node
39
- # Try apt (Ubuntu/Debian)
40
- elif command -v apt-get &> /dev/null; then
41
- echo -e "${YELLOW}[*] 使用 apt 安装...${NC}"
42
- curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
43
- sudo apt-get install -y nodejs
44
- # Try yum (CentOS/RHEL)
45
- elif command -v yum &> /dev/null; then
46
- echo -e "${YELLOW}[*] 使用 yum 安装...${NC}"
47
- curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
48
- sudo yum install -y nodejs
49
- else
50
- echo -e "${RED}[✗] 未找到包管理器${NC}"
51
- echo " 请手动安装: https://nodejs.org"
52
- exit 1
53
- fi
54
- fi
55
-
56
- # ============== Step 2: Clone/Update ==============
57
- echo -e "${YELLOW}[*] 准备安装 Thatgfsj Code...${NC}"
58
-
59
- INSTALL_DIR="$HOME/thatgfsj-code"
60
-
61
- if [ -d "$INSTALL_DIR" ]; then
62
- echo -e "${YELLOW}[*] 检测到已有安装,正在更新...${NC}"
63
- cd "$INSTALL_DIR"
64
- git pull origin main 2>/dev/null || {
65
- echo -e "${YELLOW}[*] 更新失败,重新克隆...${NC}"
66
- rm -rf "$INSTALL_DIR"
67
- }
68
- fi
69
-
70
- if [ ! -d "$INSTALL_DIR" ]; then
71
- echo -e "${YELLOW}[*] 克隆仓库...${NC}"
72
- git clone https://github.com/Thatgfsj/thatgfsj-code.git "$INSTALL_DIR"
73
- fi
74
-
75
- cd "$INSTALL_DIR"
76
-
77
- if [ ! -f "package.json" ]; then
78
- echo -e "${RED}[✗] 安装目录无效${NC}"
79
- exit 1
80
- fi
81
-
82
- echo -e "${GREEN}[✓] 代码准备完成: $INSTALL_DIR${NC}"
83
-
84
- # ============== Step 3: Install Dependencies ==============
85
- echo -e "${YELLOW}[*] 安装依赖...${NC}"
86
-
87
- npm install
88
- npm run build
89
-
90
- echo -e "${GREEN}[✓] 依赖安装完成${NC}"
91
-
92
- # ============== Step 4: Link Command ==============
93
- echo -e "${YELLOW}[*] 设置命令...${NC}"
94
-
95
- npm link
96
-
97
- echo -e "${GREEN}[✓] 命令 'gfcode' 已可用${NC}"
98
-
99
- # ============== Done ==============
100
- echo ""
101
- echo -e "${CYAN} ======================================${NC}"
102
- echo -e "${GREEN} 安装完成!${NC}"
103
- echo -e "${CYAN} ======================================${NC}"
104
- echo ""
105
- echo -e " ${WHITE}使用方法:${NC}"
106
- echo -e " gfcode init - 重新配置"
107
- echo -e " gfcode - 启动交互模式"
108
- echo -e " gfcode '你的问题' - 直接提问"
109
- echo -e " gfcode explain '代码' - 解释代码"
110
- echo -e " gfcode debug '代码' - 调试代码"
111
- echo ""
112
- echo -e " 文档: ${CYAN}https://github.com/Thatgfsj/thatgfsj-code${NC}"
113
- echo ""
package/src/app/index.ts DELETED
@@ -1,197 +0,0 @@
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
- * v3.0.3: TTL resolved per session. null = not yet decided (auto mode
43
- * waiting for first round). After the first chatStream completes, this
44
- * is '5m' or '1h' and stays sticky.
45
- */
46
- resolvedTtl: '5m' | '1h' | null = null;
47
-
48
- private constructor(
49
- config: ConfigManager,
50
- llm: LLMService,
51
- session: SessionManager,
52
- tools: ToolRegistry,
53
- hooks: HookManager,
54
- prompts: SystemPromptBuilder,
55
- skills: SkillRegistry,
56
- cacheStats: CacheStatsStore,
57
- ) {
58
- this.config = config;
59
- this.llm = llm;
60
- this.session = session;
61
- this.tools = tools;
62
- this.hooks = hooks;
63
- this.prompts = prompts;
64
- this.skills = skills;
65
- this.cacheStats = cacheStats;
66
- }
67
-
68
- static async create(): Promise<App> {
69
- const config = await ConfigManager.load();
70
- const aiConfig = config.getAIConfig();
71
-
72
- const llm = LLMService.fromConfig(aiConfig);
73
- const cacheStats = new CacheStatsStore();
74
- const session = new SessionManager(config.get().contextLength || 50);
75
- // v3.0.0: do not mutate messages when context grows — instead, surface
76
- // a "consider /new" toast via onSuggestNewSession. The TUI wires this
77
- // up in app.tsx; in CLI single-prompt mode it's a no-op (one-shot).
78
- session.onSuggestNewSession = (info) => {
79
- console.warn(
80
- `\n ⚠️ 上下文较长(${info.currentLength}/${info.max})。` +
81
- `建议调 /new 开新会话(NWT 已自动归档历史)\n`,
82
- );
83
- };
84
- const tools = new ToolRegistry();
85
- const hooks = new HookManager();
86
- const skills = new SkillRegistry();
87
-
88
- // Register tools with LLM service
89
- llm.registerTools(tools.list());
90
-
91
- // Auto-init NWT timeline
92
- const nwtTool = tools.get('nwt');
93
- if (nwtTool) {
94
- await nwtTool.execute({ action: 'init' });
95
- }
96
-
97
- // Build system prompt with active skills
98
- const prompts = new SystemPromptBuilder({
99
- cwd: process.cwd(),
100
- tools: tools.list(),
101
- permissionMode: 'ask',
102
- skillsPrompt: skills.getActivePrompts(),
103
- });
104
- session.addMessage('system', prompts.build());
105
-
106
- return new App(config, llm, session, tools, hooks, prompts, skills, cacheStats);
107
- }
108
-
109
- /**
110
- * Stream a response for the current session messages.
111
- * The LLMService handles the full agent loop internally.
112
- *
113
- * Yields structured StreamChunks. Returns the final ChatResponse (with usage
114
- * if the provider reported it) so the caller can record cache stats.
115
- *
116
- * Implementation note: we drain the inner stream manually so the final
117
- * ChatResponse returned by LLMService.chatStream is propagated as this
118
- * generator's return value. Using yield* doesn't carry the return value
119
- * through TS's AsyncGenerator<T, R> type inference in this version of
120
- * TypeScript, so we wrap with an inner for-await and explicit return.
121
- */
122
- async *streamResponse(messages?: ChatMessage[]): AsyncGenerator<StreamChunk, ChatResponse> {
123
- const msgs = messages || this.session.getMessages();
124
- const inner = this.llm.chatStream(msgs);
125
- const debugUsage = !!process.env.GFCODE_DEBUG_USAGE;
126
- // v3.0.3: read TTL the LLMService resolved this round (sticky per session).
127
- this.resolvedTtl = this.llm.getResolvedTTL();
128
- let next = await inner.next();
129
- while (!next.done) {
130
- // Forward chunks unchanged, but capture usage into the persistent
131
- // cache stats store so the TUI Header / /cache command can read it.
132
- if (next.value && next.value.type === 'usage') {
133
- try { this.cacheStats.record(next.value.usage); } catch { /* best-effort */ }
134
- if (debugUsage) {
135
- // v3.0.0 DEBUG: dump raw usage fields to stderr so the user can
136
- // confirm whether the upstream provider/relay forwards cache stats.
137
- // Enable with: GFCODE_DEBUG_USAGE=1 gfcode ...
138
- process.stderr.write(
139
- '[debug_usage] ' + JSON.stringify(next.value.usage) + '\n'
140
- );
141
- }
142
- }
143
- yield next.value;
144
- next = await inner.next();
145
- }
146
- // The generator's return value (ChatResponse with usage) is propagated
147
- // to callers via `for await ... await streamResponse.next()` semantics.
148
- return next.value;
149
- }
150
-
151
- /**
152
- * Run a single prompt (non-interactive mode)
153
- *
154
- * v2.2.4 (port from v2.1.0): persistence of the assistant message
155
- * uses addMessageSafe, which drops the message if it contains
156
- * pollution markers like "[已中断]".
157
- *
158
- * v2.2.5 (product 0.4.2): persistence also strips blocks
159
- * (and similar reasoning delimiters) when showThinking is false,
160
- * so the conversation log stays compact.
161
- *
162
- * v3.0.0: yields structured StreamChunks; final usage is captured
163
- * into onUsage callback for cache stats persistence.
164
- */
165
- async runPrompt(prompt: string, onUsage?: (usage: Usage) => void): Promise<string> {
166
- this.session.addMessage('user', prompt);
167
-
168
- let fullResponse = '';
169
- let finalUsage: ChatResponse['usage'] | undefined;
170
- try {
171
- for await (const chunk of this.streamResponse()) {
172
- if (chunk.type === 'text' && chunk.content) {
173
- process.stdout.write(chunk.content);
174
- fullResponse += chunk.content;
175
- } else if (chunk.type === 'usage') {
176
- finalUsage = chunk.usage;
177
- }
178
- }
179
- } catch (err) {
180
- // Re-throw without persisting partial response. Persisting
181
- // truncated output here was the source of the [已中断] loop in
182
- // v2.2.3.
183
- throw err;
184
- }
185
-
186
- console.log();
187
- // v2.2.5: compress blocks before persisting.
188
- const toPersist = compressThinking(fullResponse, this.showThinking);
189
- this.session.addMessageSafe('assistant', toPersist);
190
-
191
- // v3.0.0: forward usage to caller (CLI single-shot mode records stats too)
192
- if (finalUsage && onUsage) {
193
- try { onUsage(finalUsage); } catch { /* best-effort */ }
194
- }
195
- return fullResponse;
196
- }
197
- }
@@ -1,101 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
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 {
16
- shouldDowngrade,
17
- decideTTL,
18
- totalConversationChars,
19
- type SmartModelDecision,
20
- type SmartTTLDecision,
21
- } from './smartModel.js';
@@ -1,145 +0,0 @@
1
- /**
2
- * Smart-model routing + auto TTL selection (P3 of the Reasonix plan,
3
- * v3.0.3 onwards).
4
- *
5
- * Idea: classify the conversation as "short" / "medium" / "long" and
6
- * route accordingly. Two levers:
7
- *
8
- * 1. Model downgrade
9
- * Short follow-ups ("yes", "thanks") → mini model
10
- * Tool-heavy turns → main model
11
- *
12
- * 2. Cache TTL
13
- * Short conversations → 5m (cheap write, expires fast)
14
- * Long conversations (>15 turns) → 1h (cache hits pay back the
15
- * expensive write each round)
16
- *
17
- * Why TTL is decided per-ROUND but CHANGES per-CONVERSATION:
18
- * The Anthropic cache_control marker is part of the request body.
19
- * Changing the TTL between rounds would push the wire bytes to a
20
- * different Anthropic cache bucket, blowing the cache on every round
21
- * and *costing* more in cache_creation_input_tokens than what we save.
22
- *
23
- * So we compute the TTL once per conversation (lazily, on the first
24
- * round) and never change it. Subsequent rounds get the same TTL,
25
- * which keeps the cache prefix stable and lets Anthropic match it.
26
- *
27
- * The "smart" part is that we look at the FULL conversation so far,
28
- * not just the last input. A 16-turn conversation that started with a
29
- * short prompt but grew into a long refactor still gets 1h.
30
- *
31
- * Trade-off: the first round is evaluated with 0 history, so it
32
- * defaults to 5m. If the conversation then grows long, future rounds
33
- * still use 5m for the system-token cache (because we already wrote
34
- * it with 5m). The tool definition cache gets the same TTL.
35
- */
36
-
37
- import type { ChatMessage } from '../types.js';
38
-
39
- export interface SmartModelDecision {
40
- /** Should we route this turn to a cheaper model? */
41
- downgrade: boolean;
42
- /** Why we made this call — useful for `/cache` debug output. */
43
- reason: string;
44
- }
45
-
46
- export interface SmartTTLDecision {
47
- ttl: '5m' | '1h';
48
- reason: string;
49
- /** Computed per-conversation. Once we pick a TTL, we keep it. */
50
- isFirstDecision: boolean;
51
- }
52
-
53
- /**
54
- * Heuristic: downgrade when
55
- * 1. The latest user message is short (< 200 chars), AND
56
- * 2. The last 3 messages contain no assistant tool_calls.
57
- *
58
- * This covers greetings, one-line questions, and simple follow-ups
59
- * ("thanks", "ok", "explain this one line") while routing anything
60
- * that triggered a tool to the main model.
61
- */
62
- export function shouldDowngrade(messages: ChatMessage[], lastUserInput: string): SmartModelDecision {
63
- if (lastUserInput.length > 200) {
64
- return { downgrade: false, reason: 'prompt-too-long' };
65
- }
66
- const recent = messages.slice(-3);
67
- const hasRecentToolCalls = recent.some(m => m.tool_calls && m.tool_calls.length > 0);
68
- if (hasRecentToolCalls) {
69
- return { downgrade: false, reason: 'recent-tool-call' };
70
- }
71
- return { downgrade: true, reason: 'short-no-tools' };
72
- }
73
-
74
- /**
75
- * Total character count of all message content. Used as a cheap
76
- * proxy for "is this conversation long enough to justify 1h TTL?"
77
- * (Anthropic charges cache_creation at 1.25x normal input, so we
78
- * need to be sure the conversation will hit cache ~10+ times to
79
- * break even on 1h vs 5m.)
80
- */
81
- export function totalConversationChars(messages: ChatMessage[]): number {
82
- let total = 0;
83
- for (const m of messages) {
84
- if (typeof m.content === 'string') {
85
- total += m.content.length;
86
- } else if (Array.isArray(m.content)) {
87
- for (const blk of m.content) {
88
- if (blk.type === 'text') total += (blk as any).text.length;
89
- }
90
- }
91
- }
92
- return total;
93
- }
94
-
95
- /**
96
- * Decide the cache TTL for the current round.
97
- *
98
- * Inputs:
99
- * - messages: full conversation so far (system + user + assistant + tool)
100
- * - previousTTL: TTL chosen in a prior round (null on round 0)
101
- *
102
- * Decision matrix:
103
- * - round 0 + any input length → 5m (default; user can override via init)
104
- * - round 1-14 + total chars < 50k → 5m (short sessions, 5m is enough)
105
- * - round 15+ OR total chars > 50k → 1h (long sessions, 1h amortizes the write)
106
- *
107
- * Stability rule: once a TTL is chosen, it is reused for every
108
- * subsequent round in the same session. Changing TTL between rounds
109
- * would invalidate the Anthropic cache prefix and cost more than it
110
- * saves.
111
- *
112
- * The `isFirstDecision` flag tells the caller whether to apply the
113
- * decision (first round) or reuse the previous one (later rounds).
114
- */
115
- export function decideTTL(
116
- messages: ChatMessage[],
117
- previousTTL: '5m' | '1h' | null,
118
- ): SmartTTLDecision {
119
- if (previousTTL) {
120
- return { ttl: previousTTL, reason: 'reused-from-previous-round', isFirstDecision: false };
121
- }
122
-
123
- const turnCount = messages.filter(m => m.role === 'user' || m.role === 'assistant').length;
124
- const totalChars = totalConversationChars(messages);
125
-
126
- // Round 0: empty / first input. Default to 5m.
127
- if (turnCount === 0) {
128
- return { ttl: '5m', reason: 'first-round-default', isFirstDecision: true };
129
- }
130
-
131
- // Multi-turn but small conversation → 5m
132
- if (turnCount < 15 && totalChars < 50_000) {
133
- return { ttl: '5m', reason: `short-session-${turnCount}-turns`, isFirstDecision: true };
134
- }
135
-
136
- // Long conversations → 1h
137
- if (turnCount >= 15) {
138
- return { ttl: '1h', reason: `long-session-${turnCount}-turns`, isFirstDecision: true };
139
- }
140
- if (totalChars >= 50_000) {
141
- return { ttl: '1h', reason: `long-context-${totalChars}-chars`, isFirstDecision: true };
142
- }
143
-
144
- return { ttl: '5m', reason: 'fallback', isFirstDecision: true };
145
- }