chatccc 0.2.252 → 0.2.254

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 (296) hide show
  1. package/README.md +10 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/config.sample.json +2 -1
  5. package/deepccc-agent/README.md +40 -35
  6. package/deepccc-agent/os-prompts/darwin.md +8 -8
  7. package/deepccc-agent/os-prompts/linux.md +8 -8
  8. package/deepccc-agent/os-prompts/win32.md +11 -11
  9. package/deepccc-agent/package.json +65 -65
  10. package/dist/deepccc-agent/src/cli.js +634 -0
  11. package/dist/deepccc-agent/src/config.js +81 -0
  12. package/dist/deepccc-agent/src/context.js +348 -0
  13. package/dist/deepccc-agent/src/file-log.js +34 -0
  14. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  15. package/dist/deepccc-agent/src/index.js +601 -0
  16. package/dist/deepccc-agent/src/permissions.js +170 -0
  17. package/dist/deepccc-agent/src/privacy.js +124 -0
  18. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  19. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  20. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  21. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  22. package/dist/deepccc-agent/src/progress/view.js +30 -0
  23. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  24. package/dist/deepccc-agent/src/session-search.js +276 -0
  25. package/dist/deepccc-agent/src/session-select.js +23 -0
  26. package/dist/deepccc-agent/src/sigint.js +26 -0
  27. package/dist/deepccc-agent/src/skills.js +178 -0
  28. package/dist/deepccc-agent/src/web-tools.js +246 -0
  29. package/dist/src/adapters/adapter-interface.js +19 -0
  30. package/dist/src/adapters/ccc-adapter.js +112 -0
  31. package/dist/src/adapters/claude-adapter.js +497 -0
  32. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  33. package/dist/src/adapters/codex-adapter.js +279 -0
  34. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  35. package/dist/src/adapters/cursor-adapter.js +491 -0
  36. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  37. package/dist/src/adapters/jsonl-stream.js +104 -0
  38. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  39. package/dist/src/adapters/raw-stream-log.js +106 -0
  40. package/dist/src/adapters/resource-monitor.js +113 -0
  41. package/dist/src/agent-activity.js +133 -0
  42. package/dist/src/agent-delegate-task-rpc.js +129 -0
  43. package/dist/src/agent-delegate-task.js +48 -0
  44. package/dist/src/agent-file-rpc.js +152 -0
  45. package/dist/src/agent-image-rpc.js +148 -0
  46. package/dist/src/agent-platform-routing.js +13 -0
  47. package/dist/src/agent-reload-config-rpc.js +23 -0
  48. package/dist/src/agent-rpc-body.js +87 -0
  49. package/dist/src/agent-stop-stuck.js +110 -0
  50. package/dist/src/agent-team/application/board-service.js +142 -0
  51. package/dist/src/agent-team/application/main-agent-service.js +151 -0
  52. package/dist/src/agent-team/domain/board.js +69 -0
  53. package/dist/src/agent-team/http/board-routes.js +213 -0
  54. package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
  55. package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
  56. package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
  57. package/dist/src/agent-team/repositories/board-repository.js +10 -0
  58. package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
  59. package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
  60. package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
  61. package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
  62. package/dist/src/agent-team/web/agent-team-page.js +219 -0
  63. package/dist/src/agent-tool.js +10 -0
  64. package/dist/src/card-action-routing.js +7 -0
  65. package/dist/src/card-plain-text.js +101 -0
  66. package/dist/src/cardkit.js +158 -0
  67. package/dist/src/cards.js +573 -0
  68. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  69. package/dist/src/chatgpt-subscription.js +199 -0
  70. package/dist/src/chrome-devtools-guard.js +238 -0
  71. package/dist/src/claude-sdk-installer.js +249 -0
  72. package/dist/src/codex-reset-actions.js +143 -0
  73. package/dist/src/config-utils.js +149 -0
  74. package/dist/src/config.js +808 -0
  75. package/dist/src/cursor-usage.js +77 -0
  76. package/dist/src/exit-banner.js +28 -0
  77. package/dist/src/feishu-api.js +1404 -0
  78. package/dist/src/feishu-message-ingress.js +137 -0
  79. package/dist/src/feishu-platform.js +97 -0
  80. package/dist/src/format-message.js +252 -0
  81. package/dist/src/git-command.js +155 -0
  82. package/dist/src/im-skills.js +121 -0
  83. package/dist/src/index.js +835 -0
  84. package/dist/src/litellm-proxy.js +300 -0
  85. package/dist/src/orchestrator.js +2096 -0
  86. package/dist/src/package-root.js +26 -0
  87. package/dist/src/platform-adapter.js +7 -0
  88. package/dist/src/platform-startup.js +6 -0
  89. package/dist/src/privacy.js +100 -0
  90. package/dist/src/progress/reducer.js +102 -0
  91. package/dist/src/progress/terminal-renderer.js +264 -0
  92. package/dist/src/progress/view.js +30 -0
  93. package/dist/src/response-stall.js +14 -0
  94. package/dist/src/runtime-entry.js +13 -0
  95. package/dist/src/runtime-reload.js +19 -0
  96. package/dist/src/session-chat-binding.js +183 -0
  97. package/dist/src/session-name.js +7 -0
  98. package/dist/src/session.js +2147 -0
  99. package/dist/src/shared-prefix.js +16 -0
  100. package/dist/src/shared.js +493 -0
  101. package/dist/src/sim-agent.js +105 -0
  102. package/dist/src/sim-platform.js +142 -0
  103. package/dist/src/sim-store.js +231 -0
  104. package/dist/src/simplify.js +99 -0
  105. package/dist/src/startup-lifecycle.js +209 -0
  106. package/dist/src/stream-state.js +141 -0
  107. package/dist/src/terminal-error.js +100 -0
  108. package/dist/src/trace.js +50 -0
  109. package/dist/src/turn-cards.js +92 -0
  110. package/dist/src/update-command-guard.js +114 -0
  111. package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
  112. package/dist/src/wechat-platform.js +545 -0
  113. package/package.json +76 -74
  114. package/deepccc-agent/LICENSE +0 -201
  115. package/deepccc-agent/bin/deepccc.mjs +0 -26
  116. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  117. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  118. package/deepccc-agent/package-lock.json +0 -2027
  119. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  120. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  121. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  122. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  123. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  124. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  125. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  126. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  127. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  128. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  129. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  130. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  131. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  132. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  133. package/deepccc-agent/src/cli.ts +0 -682
  134. package/deepccc-agent/src/config.ts +0 -101
  135. package/deepccc-agent/src/context.ts +0 -465
  136. package/deepccc-agent/src/file-log.ts +0 -38
  137. package/deepccc-agent/src/file-tools.ts +0 -1493
  138. package/deepccc-agent/src/index.ts +0 -710
  139. package/deepccc-agent/src/permissions.ts +0 -226
  140. package/deepccc-agent/src/privacy.ts +0 -141
  141. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  142. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  143. package/deepccc-agent/src/progress/reducer.ts +0 -113
  144. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  145. package/deepccc-agent/src/progress/view.ts +0 -77
  146. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  147. package/deepccc-agent/src/session-search.ts +0 -370
  148. package/deepccc-agent/src/session-select.ts +0 -48
  149. package/deepccc-agent/src/sigint.ts +0 -50
  150. package/deepccc-agent/src/skills.ts +0 -205
  151. package/deepccc-agent/src/web-tools.ts +0 -313
  152. package/deepccc-agent/tsconfig.build.json +0 -13
  153. package/deepccc-agent/tsconfig.json +0 -13
  154. package/deepccc-agent/vitest.config.ts +0 -7
  155. package/src/__tests__/adapter-interface.test.ts +0 -152
  156. package/src/__tests__/agent-activity.test.ts +0 -86
  157. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  158. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  159. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  160. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  161. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  162. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  163. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  164. package/src/__tests__/builtin-config.test.ts +0 -26
  165. package/src/__tests__/builtin-context.test.ts +0 -319
  166. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  167. package/src/__tests__/builtin-permissions.test.ts +0 -219
  168. package/src/__tests__/builtin-session-search.test.ts +0 -262
  169. package/src/__tests__/builtin-session-select.test.ts +0 -116
  170. package/src/__tests__/builtin-sigint.test.ts +0 -56
  171. package/src/__tests__/builtin-skills.test.ts +0 -284
  172. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  173. package/src/__tests__/card-action-routing.test.ts +0 -18
  174. package/src/__tests__/card-plain-text.test.ts +0 -45
  175. package/src/__tests__/cardkit.test.ts +0 -60
  176. package/src/__tests__/cards.test.ts +0 -607
  177. package/src/__tests__/ccc-adapter.test.ts +0 -194
  178. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  179. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  180. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  181. package/src/__tests__/claude-adapter.test.ts +0 -614
  182. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  183. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  184. package/src/__tests__/codex-adapter.test.ts +0 -331
  185. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  186. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  187. package/src/__tests__/config-reload.test.ts +0 -284
  188. package/src/__tests__/config-sample.test.ts +0 -97
  189. package/src/__tests__/config-utils.test.ts +0 -40
  190. package/src/__tests__/config.test.ts +0 -395
  191. package/src/__tests__/crash-logging.test.ts +0 -360
  192. package/src/__tests__/cursor-adapter.test.ts +0 -890
  193. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  194. package/src/__tests__/feishu-api.test.ts +0 -60
  195. package/src/__tests__/feishu-avatar.test.ts +0 -504
  196. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  197. package/src/__tests__/feishu-platform.test.ts +0 -75
  198. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  199. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  200. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  201. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  202. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  203. package/src/__tests__/format-message.test.ts +0 -316
  204. package/src/__tests__/git-command.test.ts +0 -288
  205. package/src/__tests__/im-skills.test.ts +0 -125
  206. package/src/__tests__/jsonl-stream.test.ts +0 -79
  207. package/src/__tests__/orchestrator.test.ts +0 -1268
  208. package/src/__tests__/package-files.test.ts +0 -24
  209. package/src/__tests__/platform-startup.test.ts +0 -19
  210. package/src/__tests__/privacy.test.ts +0 -198
  211. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  212. package/src/__tests__/progress-reducer.test.ts +0 -121
  213. package/src/__tests__/raw-stream-log.test.ts +0 -106
  214. package/src/__tests__/response-stall.test.ts +0 -49
  215. package/src/__tests__/restart.test.ts +0 -232
  216. package/src/__tests__/session-ccc-config.test.ts +0 -66
  217. package/src/__tests__/session.test.ts +0 -3004
  218. package/src/__tests__/shared-prefix.test.ts +0 -36
  219. package/src/__tests__/sim-agent.test.ts +0 -174
  220. package/src/__tests__/sim-platform.test.ts +0 -93
  221. package/src/__tests__/sim-store.test.ts +0 -214
  222. package/src/__tests__/simplify.test.ts +0 -283
  223. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  224. package/src/__tests__/stop-session.test.ts +0 -162
  225. package/src/__tests__/stream-state.test.ts +0 -164
  226. package/src/__tests__/terminal-error.test.ts +0 -54
  227. package/src/__tests__/terminal-renderer.test.ts +0 -247
  228. package/src/__tests__/update-command-guard.test.ts +0 -144
  229. package/src/__tests__/web-ui.test.ts +0 -438
  230. package/src/__tests__/wechat-platform.test.ts +0 -111
  231. package/src/adapters/adapter-interface.ts +0 -217
  232. package/src/adapters/ccc-adapter.ts +0 -150
  233. package/src/adapters/claude-adapter.ts +0 -673
  234. package/src/adapters/claude-session-meta-store.ts +0 -120
  235. package/src/adapters/codex-adapter.ts +0 -426
  236. package/src/adapters/codex-session-meta-store.ts +0 -131
  237. package/src/adapters/cursor-adapter.ts +0 -681
  238. package/src/adapters/cursor-session-meta-store.ts +0 -154
  239. package/src/adapters/jsonl-stream.ts +0 -157
  240. package/src/adapters/raw-stream-log.ts +0 -124
  241. package/src/adapters/resource-monitor.ts +0 -141
  242. package/src/agent-activity.ts +0 -175
  243. package/src/agent-delegate-task-rpc.ts +0 -153
  244. package/src/agent-delegate-task.ts +0 -91
  245. package/src/agent-file-rpc.ts +0 -172
  246. package/src/agent-image-rpc.ts +0 -168
  247. package/src/agent-platform-routing.ts +0 -28
  248. package/src/agent-reload-config-rpc.ts +0 -34
  249. package/src/agent-rpc-body.ts +0 -92
  250. package/src/agent-stop-stuck.ts +0 -129
  251. package/src/card-action-routing.ts +0 -14
  252. package/src/card-plain-text.ts +0 -108
  253. package/src/cardkit.ts +0 -179
  254. package/src/cards.ts +0 -684
  255. package/src/chatgpt-subscription-rpc.ts +0 -27
  256. package/src/chatgpt-subscription.ts +0 -299
  257. package/src/chrome-devtools-guard.ts +0 -318
  258. package/src/claude-sdk-installer.ts +0 -324
  259. package/src/codex-reset-actions.ts +0 -184
  260. package/src/config-utils.ts +0 -211
  261. package/src/config.ts +0 -1063
  262. package/src/cursor-usage.ts +0 -128
  263. package/src/exit-banner.ts +0 -33
  264. package/src/feishu-api.ts +0 -1616
  265. package/src/feishu-message-ingress.ts +0 -195
  266. package/src/feishu-platform.ts +0 -159
  267. package/src/format-message.ts +0 -293
  268. package/src/git-command.ts +0 -202
  269. package/src/im-skills.ts +0 -149
  270. package/src/index.ts +0 -1089
  271. package/src/litellm-proxy.ts +0 -374
  272. package/src/orchestrator.ts +0 -2543
  273. package/src/platform-adapter.ts +0 -70
  274. package/src/platform-startup.ts +0 -16
  275. package/src/privacy.ts +0 -118
  276. package/src/progress/reducer.ts +0 -113
  277. package/src/progress/terminal-renderer.ts +0 -294
  278. package/src/progress/view.ts +0 -77
  279. package/src/response-stall.ts +0 -28
  280. package/src/runtime-reload.ts +0 -34
  281. package/src/session-chat-binding.ts +0 -292
  282. package/src/session-name.ts +0 -8
  283. package/src/session.ts +0 -2659
  284. package/src/shared-prefix.ts +0 -29
  285. package/src/shared.ts +0 -552
  286. package/src/sim-agent.ts +0 -167
  287. package/src/sim-platform.ts +0 -177
  288. package/src/sim-store.ts +0 -317
  289. package/src/simplify.ts +0 -120
  290. package/src/startup-lifecycle.ts +0 -250
  291. package/src/stream-state.ts +0 -177
  292. package/src/terminal-error.ts +0 -129
  293. package/src/trace.ts +0 -51
  294. package/src/turn-cards.ts +0 -118
  295. package/src/update-command-guard.ts +0 -165
  296. package/src/wechat-platform.ts +0 -680
@@ -1,1493 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { createReadStream } from "node:fs";
4
- import { copyFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
5
- import { createRequire } from "node:module";
6
- import { homedir } from "node:os";
7
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
- import { createInterface } from "node:readline";
9
-
10
- import { jsonSchema, tool, type ToolSet } from "ai";
11
-
12
- import { isDangerousCommand, type PermissionGate, type PermissionRequest } from "./permissions.js";
13
- import { killProcessTree } from "./proc-tree-kill.js";
14
- import {
15
- searchBuiltinSessions,
16
- type SessionSearchInput,
17
- type SessionSearchOutput,
18
- } from "./session-search.js";
19
- import {
20
- webFetchForTool,
21
- webSearchForTool,
22
- type WebFetchInput,
23
- type WebFetchOutput,
24
- type WebSearchInput,
25
- type WebSearchOutput,
26
- } from "./web-tools.js";
27
-
28
- const MAX_READ_BYTES = 1024 * 1024;
29
- const MAX_LIST_ENTRIES = 200;
30
- const MAX_SEARCH_RESULTS = 100;
31
- const MAX_SEARCH_BYTES = 256 * 1024;
32
- const MAX_EDIT_BYTES = 2 * 1024 * 1024;
33
- const MAX_CREATE_BYTES = 2 * 1024 * 1024;
34
- const MAX_PATCH_BYTES = 512 * 1024;
35
- const SEARCH_TIMEOUT_MS = 15_000;
36
- const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
37
- const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
38
- const MAX_COMMAND_TIMEOUT_MS = 900_000;
39
- const requireFromHere = createRequire(import.meta.url);
40
- const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
41
-
42
- export interface ReadFileInput {
43
- path: string;
44
- startLine?: number;
45
- endLine?: number;
46
- }
47
-
48
- export interface ReadFileOutput {
49
- path: string;
50
- size: number;
51
- sha256: string;
52
- isBinary: boolean;
53
- truncated: boolean;
54
- startLine?: number;
55
- endLine?: number;
56
- totalLines?: number;
57
- content: string;
58
- }
59
-
60
- export interface ListDirInput {
61
- path?: string;
62
- }
63
-
64
- export interface ListDirEntry {
65
- name: string;
66
- path: string;
67
- type: "file" | "directory" | "symlink" | "other";
68
- size?: number;
69
- }
70
-
71
- export interface ListDirOutput {
72
- path: string;
73
- entries: ListDirEntry[];
74
- truncated: boolean;
75
- }
76
-
77
- export interface SearchCodeInput {
78
- query: string;
79
- path?: string;
80
- glob?: string;
81
- maxResults?: number;
82
- }
83
-
84
- export interface SearchCodeMatch {
85
- path: string;
86
- line: number;
87
- column: number;
88
- text: string;
89
- }
90
-
91
- export interface SearchCodeOutput {
92
- query: string;
93
- path: string;
94
- glob?: string;
95
- matches: SearchCodeMatch[];
96
- truncated: boolean;
97
- }
98
-
99
- /** @internal Allows tests to force the dependency-free fallback path. */
100
- export interface SearchCodeRuntimeOptions {
101
- ripgrepCommands?: readonly string[];
102
- }
103
-
104
- export interface RunCommandInput {
105
- command: string;
106
- cwd?: string;
107
- timeoutMs?: number;
108
- }
109
-
110
- export interface RunCommandOutput {
111
- command: string;
112
- cwd: string;
113
- exitCode: number | null;
114
- signal: string | null;
115
- stdout: string;
116
- stderr: string;
117
- timedOut: boolean;
118
- truncated: boolean;
119
- durationMs: number;
120
- }
121
-
122
- export interface FileEdit {
123
- oldText: string;
124
- newText: string;
125
- replaceAll?: boolean;
126
- }
127
-
128
- export interface EditFileInput {
129
- path: string;
130
- expectedSha256?: string;
131
- edits: FileEdit[];
132
- }
133
-
134
- export interface FileWriteOutput {
135
- path: string;
136
- beforeSha256?: string;
137
- afterSha256?: string;
138
- bytesWritten?: number;
139
- changed: boolean;
140
- }
141
-
142
- export interface EditFileOutput extends FileWriteOutput {
143
- editsApplied: number;
144
- }
145
-
146
- export interface CreateFileInput {
147
- path: string;
148
- content: string;
149
- overwrite?: boolean;
150
- expectedSha256?: string;
151
- }
152
-
153
- export interface DeleteFileInput {
154
- path: string;
155
- expectedSha256?: string;
156
- }
157
-
158
- export interface DeleteFileOutput {
159
- path: string;
160
- beforeSha256: string;
161
- deleted: true;
162
- }
163
-
164
- export interface MoveFileInput {
165
- sourcePath: string;
166
- destinationPath: string;
167
- overwrite?: boolean;
168
- expectedSourceSha256?: string;
169
- expectedDestinationSha256?: string;
170
- }
171
-
172
- export interface MoveFileOutput {
173
- sourcePath: string;
174
- destinationPath: string;
175
- sourceSha256: string;
176
- overwrittenDestinationSha256?: string;
177
- moved: true;
178
- }
179
-
180
- export interface ApplyPatchInput {
181
- patch: string;
182
- expectedSha256ByPath?: Record<string, string>;
183
- }
184
-
185
- export interface ApplyPatchFileChange {
186
- path: string;
187
- action: "create" | "edit" | "delete";
188
- beforeSha256?: string;
189
- afterSha256?: string;
190
- bytesWritten?: number;
191
- }
192
-
193
- export interface ApplyPatchOutput {
194
- changedFiles: ApplyPatchFileChange[];
195
- }
196
-
197
- /**
198
- * 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
199
- * `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
200
- * 供路径工具共用:技能索引里 `~/...` 路径可直接传给 read_file 等工具。
201
- */
202
- export function expandHomePath(value: string): string {
203
- if (value === "~") return homedir();
204
- if (value.startsWith("~/") || value.startsWith("~\\")) {
205
- return join(homedir(), value.slice(2));
206
- }
207
- return value;
208
- }
209
-
210
- function resolveToolPath(cwd: string, value: string | undefined): string {
211
- const raw = value?.trim();
212
- if (!raw) return resolve(cwd);
213
- const expanded = expandHomePath(raw);
214
- return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
215
- }
216
-
217
- function toPositiveInt(value: number | undefined): number | undefined {
218
- if (value === undefined) return undefined;
219
- if (!Number.isFinite(value)) return undefined;
220
- const rounded = Math.floor(value);
221
- return rounded > 0 ? rounded : undefined;
222
- }
223
-
224
- function isBinaryBuffer(buffer: Buffer): boolean {
225
- const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
226
- return sample.includes(0);
227
- }
228
-
229
- async function pathExists(path: string): Promise<boolean> {
230
- try {
231
- await stat(path);
232
- return true;
233
- } catch {
234
- return false;
235
- }
236
- }
237
-
238
- function sha256(buffer: Buffer | string): string {
239
- return createHash("sha256").update(buffer).digest("hex");
240
- }
241
-
242
- async function sha256File(path: string): Promise<string> {
243
- const hash = createHash("sha256");
244
- for await (const chunk of createReadStream(path)) {
245
- hash.update(chunk);
246
- }
247
- return hash.digest("hex");
248
- }
249
-
250
- function assertExpectedSha256(path: string, actual: string, expected: string | undefined): void {
251
- if (expected && expected !== actual) {
252
- throw new Error(`SHA-256 mismatch for ${path}: expected ${expected}, got ${actual}`);
253
- }
254
- }
255
-
256
- function assertTextSize(path: string, text: string, maxBytes: number): void {
257
- const bytes = Buffer.byteLength(text, "utf8");
258
- if (bytes > maxBytes) {
259
- throw new Error(`Content for ${path} is too large: ${bytes} bytes, max ${maxBytes}`);
260
- }
261
- }
262
-
263
- async function readEditableTextFile(path: string): Promise<{ text: string; buffer: Buffer; sha: string }> {
264
- const info = await stat(path);
265
- if (info.isDirectory()) {
266
- throw new Error(`Path is a directory: ${path}`);
267
- }
268
- if (info.size > MAX_EDIT_BYTES) {
269
- throw new Error(`File is too large to edit: ${path} (${info.size} bytes, max ${MAX_EDIT_BYTES})`);
270
- }
271
-
272
- const buffer = await readFile(path);
273
- if (isBinaryBuffer(buffer)) {
274
- throw new Error(`Refusing to edit binary file: ${path}`);
275
- }
276
-
277
- return {
278
- text: buffer.toString("utf8"),
279
- buffer,
280
- sha: sha256(buffer),
281
- };
282
- }
283
-
284
- async function atomicWriteTextFile(path: string, text: string): Promise<void> {
285
- await mkdir(dirname(path), { recursive: true });
286
- const tempPath = resolve(
287
- dirname(path),
288
- `.chatccc-${basename(path)}-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.tmp`,
289
- );
290
- try {
291
- await writeFile(tempPath, text, "utf8");
292
- await rename(tempPath, path);
293
- } catch (err) {
294
- try {
295
- await unlink(tempPath);
296
- } catch {
297
- // Best-effort cleanup.
298
- }
299
- throw err;
300
- }
301
- }
302
-
303
- function countOccurrences(text: string, needle: string): number {
304
- if (!needle) return 0;
305
- let count = 0;
306
- let index = 0;
307
- while (true) {
308
- const found = text.indexOf(needle, index);
309
- if (found === -1) return count;
310
- count++;
311
- index = found + needle.length;
312
- }
313
- }
314
-
315
- function replaceAllLiteral(text: string, oldText: string, newText: string): string {
316
- return text.split(oldText).join(newText);
317
- }
318
-
319
- function detectEol(text: string): "\r\n" | "\n" {
320
- return text.includes("\r\n") ? "\r\n" : "\n";
321
- }
322
-
323
- function normalizeCommandTimeoutMs(value: number | undefined): number {
324
- if (value === undefined || !Number.isFinite(value)) return DEFAULT_COMMAND_TIMEOUT_MS;
325
- return Math.min(Math.max(Math.floor(value), 1_000), MAX_COMMAND_TIMEOUT_MS);
326
- }
327
-
328
- function appendLimitedOutput(
329
- target: { chunks: string[]; bytes: number; truncated: boolean },
330
- chunk: Buffer,
331
- ): void {
332
- const remaining = MAX_COMMAND_OUTPUT_BYTES - target.bytes;
333
- if (remaining <= 0) {
334
- target.truncated = true;
335
- return;
336
- }
337
-
338
- if (chunk.byteLength <= remaining) {
339
- target.chunks.push(chunk.toString("utf8"));
340
- target.bytes += chunk.byteLength;
341
- return;
342
- }
343
-
344
- target.chunks.push(chunk.subarray(0, remaining).toString("utf8"));
345
- target.bytes += remaining;
346
- target.truncated = true;
347
- }
348
-
349
- function splitPatchPath(value: string): string | null {
350
- const token = value.trim().split(/\s+/)[0];
351
- if (!token || token === "/dev/null") return null;
352
- if ((token.startsWith("a/") || token.startsWith("b/")) && token.length > 2) {
353
- return token.slice(2);
354
- }
355
- return token;
356
- }
357
-
358
- interface ParsedPatchLine {
359
- kind: "context" | "add" | "remove";
360
- text: string;
361
- }
362
-
363
- interface ParsedPatchHunk {
364
- oldStart: number;
365
- lines: ParsedPatchLine[];
366
- }
367
-
368
- interface ParsedPatchFile {
369
- oldPath: string | null;
370
- newPath: string | null;
371
- hunks: ParsedPatchHunk[];
372
- }
373
-
374
- function parseHunkHeader(line: string): number {
375
- const match = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/.exec(line);
376
- if (!match) {
377
- throw new Error(`Invalid hunk header: ${line}`);
378
- }
379
- return Number(match[1]);
380
- }
381
-
382
- function parseUnifiedPatch(patch: string): ParsedPatchFile[] {
383
- assertTextSize("patch", patch, MAX_PATCH_BYTES);
384
- const normalizedPatch = patch.replace(/\r\n/g, "\n");
385
- const lines = (normalizedPatch.endsWith("\n") ? normalizedPatch.slice(0, -1) : normalizedPatch).split("\n");
386
- const files: ParsedPatchFile[] = [];
387
- let current: ParsedPatchFile | null = null;
388
- let currentHunk: ParsedPatchHunk | null = null;
389
-
390
- const finishFile = () => {
391
- if (!current) return;
392
- if (!current.oldPath && !current.newPath) {
393
- current = null;
394
- currentHunk = null;
395
- return;
396
- }
397
- files.push(current);
398
- current = null;
399
- currentHunk = null;
400
- };
401
-
402
- for (const line of lines) {
403
- if (line.startsWith("diff --git ")) {
404
- finishFile();
405
- current = { oldPath: null, newPath: null, hunks: [] };
406
- continue;
407
- }
408
-
409
- if (line.startsWith("--- ")) {
410
- if (current?.hunks.length) finishFile();
411
- current ??= { oldPath: null, newPath: null, hunks: [] };
412
- current.oldPath = splitPatchPath(line.slice(4));
413
- currentHunk = null;
414
- continue;
415
- }
416
-
417
- if (line.startsWith("+++ ")) {
418
- current ??= { oldPath: null, newPath: null, hunks: [] };
419
- current.newPath = splitPatchPath(line.slice(4));
420
- currentHunk = null;
421
- continue;
422
- }
423
-
424
- if (line.startsWith("@@ ")) {
425
- if (!current) throw new Error(`Hunk without file header: ${line}`);
426
- currentHunk = { oldStart: parseHunkHeader(line), lines: [] };
427
- current.hunks.push(currentHunk);
428
- continue;
429
- }
430
-
431
- if (line === "\") continue;
432
- if (!currentHunk) continue;
433
-
434
- if (line.startsWith(" ")) {
435
- currentHunk.lines.push({ kind: "context", text: line.slice(1) });
436
- } else if (line.startsWith("+")) {
437
- currentHunk.lines.push({ kind: "add", text: line.slice(1) });
438
- } else if (line.startsWith("-")) {
439
- currentHunk.lines.push({ kind: "remove", text: line.slice(1) });
440
- } else {
441
- throw new Error(`Invalid patch line: ${line}`);
442
- }
443
- }
444
-
445
- finishFile();
446
- if (files.length === 0) throw new Error("Patch does not contain any file changes");
447
- return files;
448
- }
449
-
450
- function splitContentLines(text: string): string[] {
451
- if (text.length === 0) return [];
452
- return text.replace(/\r\n/g, "\n").split("\n");
453
- }
454
-
455
- function applyParsedHunks(path: string, text: string, hunks: ParsedPatchHunk[]): string {
456
- const eol = detectEol(text);
457
- const original = splitContentLines(text);
458
- const output: string[] = [];
459
- let oldIndex = 0;
460
-
461
- for (const hunk of hunks) {
462
- const targetIndex = Math.max(0, hunk.oldStart - 1);
463
- if (targetIndex < oldIndex) {
464
- throw new Error(`Overlapping hunk in patch for ${path}`);
465
- }
466
- output.push(...original.slice(oldIndex, targetIndex));
467
- oldIndex = targetIndex;
468
-
469
- for (const line of hunk.lines) {
470
- if (line.kind === "add") {
471
- output.push(line.text);
472
- continue;
473
- }
474
-
475
- if (oldIndex >= original.length || original[oldIndex] !== line.text) {
476
- throw new Error(`Patch context mismatch in ${path} near line ${oldIndex + 1}`);
477
- }
478
-
479
- if (line.kind === "context") {
480
- output.push(original[oldIndex]);
481
- }
482
- oldIndex++;
483
- }
484
- }
485
-
486
- output.push(...original.slice(oldIndex));
487
- return output.join(eol);
488
- }
489
-
490
- export async function readFileForTool(cwd: string, input: ReadFileInput): Promise<ReadFileOutput> {
491
- const filePath = resolveToolPath(cwd, input.path);
492
- const info = await stat(filePath);
493
- if (info.isDirectory()) {
494
- throw new Error(`Path is a directory: ${filePath}`);
495
- }
496
-
497
- const bytesToRead = Math.min(info.size, MAX_READ_BYTES);
498
- const buffer = Buffer.alloc(bytesToRead);
499
- const handle = await open(filePath, "r");
500
- try {
501
- await handle.read(buffer, 0, bytesToRead, 0);
502
- } finally {
503
- await handle.close();
504
- }
505
- const fileSha256 = await sha256File(filePath);
506
- const isBinary = isBinaryBuffer(buffer);
507
- if (isBinary) {
508
- return {
509
- path: filePath,
510
- size: info.size,
511
- sha256: fileSha256,
512
- isBinary: true,
513
- truncated: info.size > bytesToRead,
514
- content: "",
515
- };
516
- }
517
-
518
- const text = buffer.toString("utf8");
519
- const lines = text.split(/\r?\n/);
520
- const totalLines = lines.length;
521
- const startLine = toPositiveInt(input.startLine) ?? 1;
522
- const endLine = toPositiveInt(input.endLine) ?? totalLines;
523
- const normalizedEnd = Math.max(startLine, Math.min(endLine, totalLines));
524
- const content = lines.slice(startLine - 1, normalizedEnd).join("\n");
525
-
526
- return {
527
- path: filePath,
528
- size: info.size,
529
- sha256: fileSha256,
530
- isBinary: false,
531
- truncated: info.size > bytesToRead || normalizedEnd < totalLines || startLine > 1,
532
- startLine,
533
- endLine: normalizedEnd,
534
- totalLines,
535
- content,
536
- };
537
- }
538
-
539
- export async function listDirForTool(cwd: string, input: ListDirInput = {}): Promise<ListDirOutput> {
540
- const dirPath = resolveToolPath(cwd, input.path);
541
- const entries = await readdir(dirPath, { withFileTypes: true });
542
- const selected = entries.slice(0, MAX_LIST_ENTRIES);
543
- const result: ListDirEntry[] = [];
544
-
545
- for (const entry of selected) {
546
- const entryPath = resolve(dirPath, entry.name);
547
- let size: number | undefined;
548
- if (entry.isFile()) {
549
- try {
550
- size = (await stat(entryPath)).size;
551
- } catch {
552
- size = undefined;
553
- }
554
- }
555
- result.push({
556
- name: entry.name,
557
- path: entryPath,
558
- type: entry.isDirectory()
559
- ? "directory"
560
- : entry.isFile()
561
- ? "file"
562
- : entry.isSymbolicLink()
563
- ? "symlink"
564
- : "other",
565
- ...(size !== undefined ? { size } : {}),
566
- });
567
- }
568
-
569
- return {
570
- path: dirPath,
571
- entries: result,
572
- truncated: entries.length > selected.length,
573
- };
574
- }
575
-
576
- function parseRgLine(line: string): SearchCodeMatch | null {
577
- const match = /^(.*?):(\d+):(\d+):(.*)$/.exec(line);
578
- if (!match) return null;
579
- return {
580
- path: match[1],
581
- line: Number(match[2]),
582
- column: Number(match[3]),
583
- text: match[4],
584
- };
585
- }
586
-
587
- interface RipgrepOutput {
588
- stdout: string;
589
- stderr: string;
590
- truncated: boolean;
591
- }
592
-
593
- function resolveBundledRipgrepPath(): string | undefined {
594
- try {
595
- const bundled = requireFromHere("@vscode/ripgrep") as { rgPath?: unknown };
596
- return typeof bundled.rgPath === "string" && bundled.rgPath.trim()
597
- ? bundled.rgPath
598
- : undefined;
599
- } catch {
600
- // Unsupported platforms or damaged optional platform packages must not
601
- // prevent ChatCCC itself from starting; system rg / Node fallback remain.
602
- return undefined;
603
- }
604
- }
605
-
606
- function defaultRipgrepCommands(): string[] {
607
- const bundled = resolveBundledRipgrepPath();
608
- return [...new Set([bundled, "rg"].filter((value): value is string => !!value))];
609
- }
610
-
611
- function isUnavailableExecutableError(err: unknown): boolean {
612
- const code = (err as NodeJS.ErrnoException | undefined)?.code;
613
- return code === "ENOENT" || code === "EACCES" || code === "EPERM";
614
- }
615
-
616
- async function runRipgrep(
617
- command: string,
618
- args: string[],
619
- cwd: string,
620
- signal?: AbortSignal,
621
- ): Promise<RipgrepOutput> {
622
- return new Promise<RipgrepOutput>((resolvePromise, reject) => {
623
- let settled = false;
624
- const child = spawn(command, args, {
625
- cwd,
626
- shell: false,
627
- windowsHide: true,
628
- stdio: ["ignore", "pipe", "pipe"],
629
- });
630
-
631
- let stdout = "";
632
- let stderr = "";
633
- let truncated = false;
634
- const cleanup = () => {
635
- clearTimeout(timeout);
636
- signal?.removeEventListener("abort", abort);
637
- };
638
- const rejectOnce = (err: Error) => {
639
- if (settled) return;
640
- settled = true;
641
- cleanup();
642
- reject(err);
643
- };
644
- const timeout = setTimeout(() => {
645
- child.kill();
646
- rejectOnce(new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`));
647
- }, SEARCH_TIMEOUT_MS);
648
- const abort = () => {
649
- child.kill();
650
- rejectOnce(new Error("search_code aborted"));
651
- };
652
- signal?.addEventListener("abort", abort, { once: true });
653
-
654
- child.stdout?.on("data", (chunk: Buffer) => {
655
- if (stdout.length >= MAX_SEARCH_BYTES) {
656
- truncated = true;
657
- return;
658
- }
659
- stdout += chunk.toString("utf8");
660
- if (stdout.length > MAX_SEARCH_BYTES) {
661
- stdout = stdout.slice(0, MAX_SEARCH_BYTES);
662
- truncated = true;
663
- }
664
- });
665
- child.stderr?.on("data", (chunk: Buffer) => {
666
- stderr += chunk.toString("utf8");
667
- });
668
- child.on("error", (err) => rejectOnce(err));
669
- child.on("close", (code) => {
670
- if (settled) return;
671
- settled = true;
672
- cleanup();
673
- if (code !== 0 && code !== 1) {
674
- reject(new Error(stderr.trim() || `rg exited with code ${code}`));
675
- return;
676
- }
677
- resolvePromise({ stdout, stderr, truncated });
678
- });
679
- });
680
- }
681
-
682
- function expandGlobBraces(pattern: string): string[] {
683
- const openIndex = pattern.indexOf("{");
684
- if (openIndex < 0) return [pattern];
685
- const closeIndex = pattern.indexOf("}", openIndex + 1);
686
- if (closeIndex < 0) return [pattern];
687
- const alternatives = pattern.slice(openIndex + 1, closeIndex).split(",");
688
- if (alternatives.length < 2) return [pattern];
689
- return alternatives.flatMap((alternative) => expandGlobBraces(
690
- pattern.slice(0, openIndex) + alternative + pattern.slice(closeIndex + 1),
691
- ));
692
- }
693
-
694
- function globToRegExp(pattern: string): RegExp {
695
- let source = "";
696
- for (let index = 0; index < pattern.length; index += 1) {
697
- const char = pattern[index];
698
- if (char === "*") {
699
- if (pattern[index + 1] === "*") {
700
- index += 1;
701
- if (pattern[index + 1] === "/") {
702
- index += 1;
703
- source += "(?:.*/)?";
704
- } else {
705
- source += ".*";
706
- }
707
- } else {
708
- source += "[^/]*";
709
- }
710
- } else if (char === "?") {
711
- source += "[^/]";
712
- } else {
713
- source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
714
- }
715
- }
716
- return new RegExp(`^${source}$`);
717
- }
718
-
719
- function createGlobMatchers(glob: string | undefined): Array<{ regex: RegExp; basenameOnly: boolean }> {
720
- if (!glob?.trim()) return [];
721
- return expandGlobBraces(glob.trim().replaceAll("\\", "/")).map((pattern) => ({
722
- regex: globToRegExp(pattern),
723
- basenameOnly: !pattern.includes("/"),
724
- }));
725
- }
726
-
727
- function matchesFallbackGlob(
728
- filePath: string,
729
- searchRoot: string,
730
- matchers: Array<{ regex: RegExp; basenameOnly: boolean }>,
731
- ): boolean {
732
- if (matchers.length === 0) return true;
733
- const relativePath = relative(searchRoot, filePath).split(sep).join("/");
734
- return matchers.some(({ regex, basenameOnly }) => regex.test(
735
- basenameOnly ? basename(filePath) : relativePath,
736
- ));
737
- }
738
-
739
- async function searchCodeWithNode(
740
- query: string,
741
- searchPath: string,
742
- glob: string | undefined,
743
- maxResults: number,
744
- signal?: AbortSignal,
745
- ): Promise<{ matches: SearchCodeMatch[]; truncated: boolean }> {
746
- let queryRegex: RegExp;
747
- try {
748
- queryRegex = new RegExp(query);
749
- } catch (err) {
750
- throw new Error(`invalid search regex: ${(err as Error).message}`);
751
- }
752
-
753
- const startedAt = Date.now();
754
- const matches: SearchCodeMatch[] = [];
755
- const rootInfo = await stat(searchPath);
756
- const searchRoot = rootInfo.isDirectory() ? searchPath : dirname(searchPath);
757
- const globMatchers = createGlobMatchers(glob);
758
- let truncated = false;
759
- let outputBytes = 0;
760
-
761
- const ensureActive = () => {
762
- if (signal?.aborted) throw new Error("search_code aborted");
763
- if (Date.now() - startedAt >= SEARCH_TIMEOUT_MS) {
764
- throw new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`);
765
- }
766
- };
767
-
768
- const searchFile = async (filePath: string) => {
769
- if (!matchesFallbackGlob(filePath, searchRoot, globMatchers)) return;
770
- ensureActive();
771
- const input = createReadStream(filePath, { encoding: "utf8" });
772
- const lines = createInterface({ input, crlfDelay: Infinity });
773
- let lineNumber = 0;
774
- try {
775
- for await (const line of lines) {
776
- ensureActive();
777
- lineNumber += 1;
778
- if (line.includes("\0")) break;
779
- const match = queryRegex.exec(line);
780
- queryRegex.lastIndex = 0;
781
- if (!match) continue;
782
- const lineBytes = Buffer.byteLength(line, "utf8");
783
- if (outputBytes + lineBytes > MAX_SEARCH_BYTES) {
784
- truncated = true;
785
- break;
786
- }
787
- outputBytes += lineBytes;
788
- matches.push({
789
- path: filePath,
790
- line: lineNumber,
791
- column: match.index + 1,
792
- text: line,
793
- });
794
- if (matches.length >= maxResults) {
795
- truncated = true;
796
- break;
797
- }
798
- }
799
- } catch (err) {
800
- if (signal?.aborted) throw new Error("search_code aborted");
801
- const code = (err as NodeJS.ErrnoException | undefined)?.code;
802
- if (code !== "EACCES" && code !== "EPERM" && code !== "ENOENT") throw err;
803
- } finally {
804
- lines.close();
805
- input.destroy();
806
- }
807
- };
808
-
809
- const visit = async (currentPath: string): Promise<void> => {
810
- ensureActive();
811
- if (truncated) return;
812
- let info;
813
- try {
814
- info = currentPath === searchPath ? rootInfo : await stat(currentPath);
815
- } catch (err) {
816
- const code = (err as NodeJS.ErrnoException | undefined)?.code;
817
- if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
818
- throw err;
819
- }
820
- if (info.isFile()) {
821
- await searchFile(currentPath);
822
- return;
823
- }
824
- if (!info.isDirectory()) return;
825
-
826
- let entries;
827
- try {
828
- entries = await readdir(currentPath, { withFileTypes: true });
829
- } catch (err) {
830
- const code = (err as NodeJS.ErrnoException | undefined)?.code;
831
- if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
832
- throw err;
833
- }
834
- entries.sort((left, right) => left.name.localeCompare(right.name));
835
- for (const entry of entries) {
836
- if (truncated) break;
837
- if (entry.name.startsWith(".")) continue;
838
- if (entry.isDirectory() && FALLBACK_SKIPPED_DIRECTORIES.has(entry.name)) continue;
839
- if (entry.isSymbolicLink()) continue;
840
- await visit(resolve(currentPath, entry.name));
841
- }
842
- };
843
-
844
- await visit(searchPath);
845
- return { matches, truncated };
846
- }
847
-
848
- export async function searchCodeForTool(
849
- cwd: string,
850
- input: SearchCodeInput,
851
- signal?: AbortSignal,
852
- runtimeOptions: SearchCodeRuntimeOptions = {},
853
- ): Promise<SearchCodeOutput> {
854
- const query = input.query?.trim();
855
- if (!query) throw new Error("query is required");
856
- if (signal?.aborted) throw new Error("search_code aborted");
857
-
858
- const searchPath = resolveToolPath(cwd, input.path);
859
- const maxResults = Math.min(toPositiveInt(input.maxResults) ?? 50, MAX_SEARCH_RESULTS);
860
- const args = [
861
- "--line-number",
862
- "--column",
863
- "--no-heading",
864
- "--color",
865
- "never",
866
- "--max-count",
867
- String(maxResults),
868
- ];
869
- if (input.glob?.trim()) {
870
- args.push("--glob", input.glob.trim());
871
- }
872
- args.push("--", query, searchPath);
873
-
874
- const commands = runtimeOptions.ripgrepCommands ?? defaultRipgrepCommands();
875
- let output: RipgrepOutput | undefined;
876
- for (const command of commands) {
877
- try {
878
- output = await runRipgrep(command, args, cwd, signal);
879
- break;
880
- } catch (err) {
881
- if (!isUnavailableExecutableError(err)) throw err;
882
- }
883
- }
884
-
885
- const fallback = output
886
- ? undefined
887
- : await searchCodeWithNode(query, searchPath, input.glob?.trim(), maxResults, signal);
888
- const matches = output
889
- ? output.stdout
890
- .split(/\r?\n/)
891
- .filter(Boolean)
892
- .map(parseRgLine)
893
- .filter((match): match is SearchCodeMatch => !!match)
894
- .slice(0, maxResults)
895
- : fallback!.matches;
896
-
897
- return {
898
- query,
899
- path: searchPath,
900
- ...(input.glob?.trim() ? { glob: input.glob.trim() } : {}),
901
- matches,
902
- truncated: output
903
- ? output.truncated || matches.length >= maxResults
904
- : fallback!.truncated,
905
- };
906
- }
907
-
908
- export async function runCommandForTool(
909
- cwd: string,
910
- input: RunCommandInput,
911
- abortSignal?: AbortSignal,
912
- ): Promise<RunCommandOutput> {
913
- const command = input.command?.trim();
914
- if (!command) throw new Error("command is required");
915
-
916
- const commandCwd = resolveToolPath(cwd, input.cwd);
917
- const cwdInfo = await stat(commandCwd);
918
- if (!cwdInfo.isDirectory()) {
919
- throw new Error(`cwd is not a directory: ${commandCwd}`);
920
- }
921
-
922
- const timeoutMs = normalizeCommandTimeoutMs(input.timeoutMs);
923
- const startedAt = Date.now();
924
- const stdout = { chunks: [] as string[], bytes: 0, truncated: false };
925
- const stderr = { chunks: [] as string[], bytes: 0, truncated: false };
926
-
927
- return new Promise<RunCommandOutput>((resolvePromise, reject) => {
928
- let settled = false;
929
- let timedOut = false;
930
- let timeout: NodeJS.Timeout;
931
- let fallbackTimer: NodeJS.Timeout | undefined;
932
-
933
- const child = spawn(command, {
934
- cwd: commandCwd,
935
- shell: true,
936
- windowsHide: true,
937
- stdio: ["ignore", "pipe", "pipe"],
938
- detached: process.platform !== "win32",
939
- });
940
-
941
- const cleanup = () => {
942
- clearTimeout(timeout);
943
- if (fallbackTimer) clearTimeout(fallbackTimer);
944
- abortSignal?.removeEventListener("abort", abort);
945
- };
946
-
947
- const finish = (exitCode: number | null, signal: NodeJS.Signals | string | null) => {
948
- if (settled) return;
949
- settled = true;
950
- cleanup();
951
- resolvePromise({
952
- command,
953
- cwd: commandCwd,
954
- exitCode,
955
- signal,
956
- stdout: stdout.chunks.join(""),
957
- stderr: stderr.chunks.join(""),
958
- timedOut,
959
- truncated: stdout.truncated || stderr.truncated,
960
- durationMs: Date.now() - startedAt,
961
- });
962
- };
963
-
964
- const requestKill = (reason: NodeJS.Signals | "timeout" | "abort") => {
965
- void killProcessTree(child.pid);
966
- fallbackTimer = setTimeout(() => {
967
- child.stdout?.destroy();
968
- child.stderr?.destroy();
969
- finish(null, reason === "timeout" ? "SIGTERM" : reason);
970
- }, 5_000);
971
- fallbackTimer.unref?.();
972
- };
973
-
974
- timeout = setTimeout(() => {
975
- timedOut = true;
976
- requestKill("timeout");
977
- }, timeoutMs);
978
-
979
- const abort = () => {
980
- requestKill("abort");
981
- };
982
- abortSignal?.addEventListener("abort", abort, { once: true });
983
-
984
- child.stdout?.on("data", (chunk: Buffer) => {
985
- appendLimitedOutput(stdout, chunk);
986
- });
987
- child.stderr?.on("data", (chunk: Buffer) => {
988
- appendLimitedOutput(stderr, chunk);
989
- });
990
- child.once("error", (err) => {
991
- if (settled) return;
992
- settled = true;
993
- cleanup();
994
- reject(err);
995
- });
996
- child.once("close", (code, signal) => {
997
- finish(code, signal);
998
- });
999
- });
1000
- }
1001
-
1002
- export async function editFileForTool(cwd: string, input: EditFileInput): Promise<EditFileOutput> {
1003
- if (!Array.isArray(input.edits) || input.edits.length === 0) {
1004
- throw new Error("edits must contain at least one replacement");
1005
- }
1006
-
1007
- const filePath = resolveToolPath(cwd, input.path);
1008
- const before = await readEditableTextFile(filePath);
1009
- assertExpectedSha256(filePath, before.sha, input.expectedSha256);
1010
-
1011
- // Normalize line endings before matching so that LF-based oldText/newText
1012
- // (which is what models typically emit) works against CRLF files checked
1013
- // out on Windows. The file's dominant EOL style is restored on write.
1014
- const eol = detectEol(before.text);
1015
- let text = eol === "\r\n" ? before.text.replace(/\r\n/g, "\n") : before.text;
1016
- let editsApplied = 0;
1017
- for (const [index, edit] of input.edits.entries()) {
1018
- if (!edit.oldText) {
1019
- throw new Error(`edit ${index + 1} oldText must not be empty`);
1020
- }
1021
- const oldText = edit.oldText.replace(/\r\n/g, "\n");
1022
- const newText = edit.newText.replace(/\r\n/g, "\n");
1023
- const count = countOccurrences(text, oldText);
1024
- if (count === 0) {
1025
- throw new Error(`edit ${index + 1} oldText was not found in ${filePath}`);
1026
- }
1027
- if (count > 1 && !edit.replaceAll) {
1028
- throw new Error(`edit ${index + 1} oldText matched ${count} times in ${filePath}; set replaceAll=true or provide more context`);
1029
- }
1030
- text = edit.replaceAll
1031
- ? replaceAllLiteral(text, oldText, newText)
1032
- : text.replace(oldText, newText);
1033
- editsApplied += edit.replaceAll ? count : 1;
1034
- }
1035
- if (eol === "\r\n") {
1036
- // After normalization above the buffer contains only \n, so this is safe.
1037
- text = text.replace(/\n/g, "\r\n");
1038
- }
1039
-
1040
- assertTextSize(filePath, text, MAX_EDIT_BYTES);
1041
- const afterSha = sha256(text);
1042
- const changed = afterSha !== before.sha;
1043
- if (changed) {
1044
- await atomicWriteTextFile(filePath, text);
1045
- }
1046
-
1047
- return {
1048
- path: filePath,
1049
- beforeSha256: before.sha,
1050
- afterSha256: afterSha,
1051
- bytesWritten: changed ? Buffer.byteLength(text, "utf8") : 0,
1052
- changed,
1053
- editsApplied,
1054
- };
1055
- }
1056
-
1057
- export async function createFileForTool(cwd: string, input: CreateFileInput): Promise<FileWriteOutput> {
1058
- const filePath = resolveToolPath(cwd, input.path);
1059
- assertTextSize(filePath, input.content, MAX_CREATE_BYTES);
1060
-
1061
- let beforeSha: string | undefined;
1062
- if (await pathExists(filePath)) {
1063
- const existing = await readEditableTextFile(filePath);
1064
- beforeSha = existing.sha;
1065
- assertExpectedSha256(filePath, existing.sha, input.expectedSha256);
1066
- if (!input.overwrite) {
1067
- throw new Error(`File already exists: ${filePath}`);
1068
- }
1069
- } else if (input.expectedSha256) {
1070
- throw new Error(`Cannot check expectedSha256 because file does not exist: ${filePath}`);
1071
- }
1072
-
1073
- const afterSha = sha256(input.content);
1074
- const changed = beforeSha !== afterSha;
1075
- if (changed) {
1076
- await atomicWriteTextFile(filePath, input.content);
1077
- }
1078
-
1079
- return {
1080
- path: filePath,
1081
- ...(beforeSha ? { beforeSha256: beforeSha } : {}),
1082
- afterSha256: afterSha,
1083
- bytesWritten: changed ? Buffer.byteLength(input.content, "utf8") : 0,
1084
- changed,
1085
- };
1086
- }
1087
-
1088
- export async function deleteFileForTool(cwd: string, input: DeleteFileInput): Promise<DeleteFileOutput> {
1089
- const filePath = resolveToolPath(cwd, input.path);
1090
- const before = await readEditableTextFile(filePath);
1091
- assertExpectedSha256(filePath, before.sha, input.expectedSha256);
1092
- await unlink(filePath);
1093
- return {
1094
- path: filePath,
1095
- beforeSha256: before.sha,
1096
- deleted: true,
1097
- };
1098
- }
1099
-
1100
- export async function moveFileForTool(cwd: string, input: MoveFileInput): Promise<MoveFileOutput> {
1101
- const sourcePath = resolveToolPath(cwd, input.sourcePath);
1102
- const destinationPath = resolveToolPath(cwd, input.destinationPath);
1103
- const source = await readEditableTextFile(sourcePath);
1104
- assertExpectedSha256(sourcePath, source.sha, input.expectedSourceSha256);
1105
-
1106
- let destinationSha: string | undefined;
1107
- if (await pathExists(destinationPath)) {
1108
- const destination = await readEditableTextFile(destinationPath);
1109
- destinationSha = destination.sha;
1110
- assertExpectedSha256(destinationPath, destination.sha, input.expectedDestinationSha256);
1111
- if (!input.overwrite) {
1112
- throw new Error(`Destination already exists: ${destinationPath}`);
1113
- }
1114
- } else if (input.expectedDestinationSha256) {
1115
- throw new Error(`Cannot check expectedDestinationSha256 because destination does not exist: ${destinationPath}`);
1116
- }
1117
-
1118
- await mkdir(dirname(destinationPath), { recursive: true });
1119
- try {
1120
- await rename(sourcePath, destinationPath);
1121
- } catch (err) {
1122
- if ((err as NodeJS.ErrnoException).code !== "EXDEV") throw err;
1123
- await copyFile(sourcePath, destinationPath);
1124
- await unlink(sourcePath);
1125
- }
1126
-
1127
- return {
1128
- sourcePath,
1129
- destinationPath,
1130
- sourceSha256: source.sha,
1131
- ...(destinationSha ? { overwrittenDestinationSha256: destinationSha } : {}),
1132
- moved: true,
1133
- };
1134
- }
1135
-
1136
- function expectedPatchHash(
1137
- input: ApplyPatchInput,
1138
- absolutePath: string,
1139
- patchPath: string,
1140
- ): string | undefined {
1141
- return input.expectedSha256ByPath?.[absolutePath] ?? input.expectedSha256ByPath?.[patchPath];
1142
- }
1143
-
1144
- export async function applyPatchForTool(cwd: string, input: ApplyPatchInput): Promise<ApplyPatchOutput> {
1145
- const files = parseUnifiedPatch(input.patch);
1146
- const changedFiles: ApplyPatchFileChange[] = [];
1147
-
1148
- for (const file of files) {
1149
- const patchPath = file.newPath ?? file.oldPath;
1150
- if (!patchPath) throw new Error("Patch file is missing both old and new paths");
1151
- const targetPath = resolveToolPath(cwd, patchPath);
1152
- const action: ApplyPatchFileChange["action"] =
1153
- file.oldPath === null ? "create" :
1154
- file.newPath === null ? "delete" :
1155
- "edit";
1156
-
1157
- if (action === "create") {
1158
- if (await pathExists(targetPath)) {
1159
- throw new Error(`Patch target already exists: ${targetPath}`);
1160
- }
1161
- const text = applyParsedHunks(targetPath, "", file.hunks);
1162
- assertTextSize(targetPath, text, MAX_CREATE_BYTES);
1163
- await atomicWriteTextFile(targetPath, text);
1164
- changedFiles.push({
1165
- path: targetPath,
1166
- action,
1167
- afterSha256: sha256(text),
1168
- bytesWritten: Buffer.byteLength(text, "utf8"),
1169
- });
1170
- continue;
1171
- }
1172
-
1173
- const before = await readEditableTextFile(targetPath);
1174
- assertExpectedSha256(targetPath, before.sha, expectedPatchHash(input, targetPath, patchPath));
1175
- const text = applyParsedHunks(targetPath, before.text, file.hunks);
1176
-
1177
- if (action === "delete") {
1178
- await unlink(targetPath);
1179
- changedFiles.push({
1180
- path: targetPath,
1181
- action,
1182
- beforeSha256: before.sha,
1183
- });
1184
- continue;
1185
- }
1186
-
1187
- assertTextSize(targetPath, text, MAX_EDIT_BYTES);
1188
- const afterSha = sha256(text);
1189
- if (afterSha !== before.sha) {
1190
- await atomicWriteTextFile(targetPath, text);
1191
- }
1192
- changedFiles.push({
1193
- path: targetPath,
1194
- action,
1195
- beforeSha256: before.sha,
1196
- afterSha256: afterSha,
1197
- bytesWritten: afterSha !== before.sha ? Buffer.byteLength(text, "utf8") : 0,
1198
- });
1199
- }
1200
-
1201
- return { changedFiles };
1202
- }
1203
-
1204
- export interface BuiltinFileToolsOptions {
1205
- /** 权限门控:副作用工具(run_command/文件写操作)执行前会先经过 gate.check */
1206
- permissionGate?: PermissionGate;
1207
- /** session_search 工具的会话/原始日志目录(默认 ~/.deepccc/sessions 与 raw-stream-logs;测试可注入) */
1208
- sessionSearch?: {
1209
- contextDir?: string;
1210
- rawLogsDir?: string;
1211
- };
1212
- }
1213
-
1214
- export function createBuiltinFileTools(
1215
- cwd: string,
1216
- options: BuiltinFileToolsOptions = {},
1217
- ): ToolSet {
1218
- const gate = options.permissionGate;
1219
-
1220
- /** 文件路径的候选匹配键:原始路径 + 绝对路径 + 相对 cwd 路径(正/反斜杠双版本,规则可任选其一) */
1221
- const pathKeys = (p: string): string[] => {
1222
- const abs = resolveToolPath(cwd, p);
1223
- const rel = relative(cwd, abs);
1224
- return [
1225
- p,
1226
- abs,
1227
- rel,
1228
- rel.split(sep).join("/"),
1229
- abs.split(sep).join("/"),
1230
- ];
1231
- };
1232
-
1233
- /** 副作用工具执行前的权限守卫:gate 拒绝时抛错,工具不会真正执行 */
1234
- const guard = async (request: PermissionRequest): Promise<void> => {
1235
- if (!gate) return;
1236
- const decision = await gate.check(request);
1237
- if (decision === "deny") {
1238
- throw new Error(
1239
- `权限拒绝:${request.detail}(已按规则或用户选择拦截;如需放行请调整 ~/.deepccc/allow.json,或使用 --dangerously-bypass-permissions)`,
1240
- );
1241
- }
1242
- };
1243
-
1244
- return {
1245
- read_file: tool<ReadFileInput, ReadFileOutput>({
1246
- description: "从本地文件系统读取 UTF-8 文本文件。大文件请使用行范围。",
1247
- inputSchema: jsonSchema<ReadFileInput>({
1248
- type: "object",
1249
- additionalProperties: false,
1250
- properties: {
1251
- path: { type: "string", description: "绝对路径或相对于会话工作目录的路径。" },
1252
- startLine: { type: "number", description: "可选,从第几行开始返回(从 1 开始)。" },
1253
- endLine: { type: "number", description: "可选,返回到第几行(从 1 开始)。" },
1254
- },
1255
- required: ["path"],
1256
- }),
1257
- execute: (input) => readFileForTool(cwd, input),
1258
- }),
1259
- list_dir: tool<ListDirInput, ListDirOutput>({
1260
- description: "列出本地目录中的文件。",
1261
- inputSchema: jsonSchema<ListDirInput>({
1262
- type: "object",
1263
- additionalProperties: false,
1264
- properties: {
1265
- path: { type: "string", description: "目录路径。默认为会话工作目录。" },
1266
- },
1267
- }),
1268
- execute: (input) => listDirForTool(cwd, input),
1269
- }),
1270
- search_code: tool<SearchCodeInput, SearchCodeOutput>({
1271
- description: "用 ripgrep 搜索本地文件,无需调用 shell。",
1272
- inputSchema: jsonSchema<SearchCodeInput>({
1273
- type: "object",
1274
- additionalProperties: false,
1275
- properties: {
1276
- query: { type: "string", description: "传递给 ripgrep 的文本或正则查询。" },
1277
- path: { type: "string", description: "要搜索的文件或目录。默认为会话工作目录。" },
1278
- glob: { type: "string", description: "可选的 ripgrep glob 过滤器,例如 **/*.ts。" },
1279
- maxResults: { type: "number", description: "最大结果行数,内部设有上限。" },
1280
- },
1281
- required: ["query"],
1282
- }),
1283
- execute: (input, options) => searchCodeForTool(cwd, input, options.abortSignal),
1284
- }),
1285
- run_command: tool<RunCommandInput, RunCommandOutput>({
1286
- description: "在本地工作区运行非交互式 shell 命令。用于测试、git 和包脚本。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。",
1287
- inputSchema: jsonSchema<RunCommandInput>({
1288
- type: "object",
1289
- additionalProperties: false,
1290
- properties: {
1291
- command: { type: "string", description: "要在平台 shell 中运行的命令行。" },
1292
- cwd: { type: "string", description: "可选的工作目录。默认为会话工作目录。" },
1293
- timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
1294
- },
1295
- required: ["command"],
1296
- }),
1297
- execute: async (input, options) => {
1298
- await guard({
1299
- tool: "run_command",
1300
- action: input.command,
1301
- reason: isDangerousCommand(input.command) ? "high-risk" : "rule",
1302
- detail: `运行命令: ${input.command}`,
1303
- });
1304
- return runCommandForTool(cwd, input, options.abortSignal);
1305
- },
1306
- }),
1307
- edit_file: tool<EditFileInput, EditFileOutput>({
1308
- description: "通过精确的 oldText -> newText 替换编辑现有 UTF-8 文本文件。可行时使用 SHA-256 前置条件以避免覆盖并发编辑。",
1309
- inputSchema: jsonSchema<EditFileInput>({
1310
- type: "object",
1311
- additionalProperties: false,
1312
- properties: {
1313
- path: { type: "string", description: "绝对路径或相对于会话工作目录的路径。" },
1314
- expectedSha256: { type: "string", description: "当前文件内容的可选 SHA-256 哈希。" },
1315
- edits: {
1316
- type: "array",
1317
- minItems: 1,
1318
- items: {
1319
- type: "object",
1320
- additionalProperties: false,
1321
- properties: {
1322
- oldText: { type: "string", description: "要替换的精确文本。包含足够的上下文使其唯一。" },
1323
- newText: { type: "string", description: "替换文本。" },
1324
- replaceAll: { type: "boolean", description: "当 oldText 出现多次时替换所有出现。" },
1325
- },
1326
- required: ["oldText", "newText"],
1327
- },
1328
- },
1329
- },
1330
- required: ["path", "edits"],
1331
- }),
1332
- execute: async (input) => {
1333
- await guard({
1334
- tool: "edit_file",
1335
- action: input.path,
1336
- altKeys: pathKeys(input.path),
1337
- reason: "rule",
1338
- detail: `编辑文件: ${input.path}`,
1339
- });
1340
- return editFileForTool(cwd, input);
1341
- },
1342
- }),
1343
- create_file: tool<CreateFileInput, FileWriteOutput>({
1344
- description: "创建 UTF-8 文本文件,或在 overwrite=true 时覆盖现有文件。",
1345
- inputSchema: jsonSchema<CreateFileInput>({
1346
- type: "object",
1347
- additionalProperties: false,
1348
- properties: {
1349
- path: { type: "string", description: "绝对路径或相对于会话工作目录的路径。" },
1350
- content: { type: "string", description: "要写入的完整文件内容。" },
1351
- overwrite: { type: "boolean", description: "允许替换现有文件。" },
1352
- expectedSha256: { type: "string", description: "覆盖现有文件时需要的可选 SHA-256 哈希。" },
1353
- },
1354
- required: ["path", "content"],
1355
- }),
1356
- execute: async (input) => {
1357
- await guard({
1358
- tool: "create_file",
1359
- action: input.path,
1360
- altKeys: pathKeys(input.path),
1361
- reason: "rule",
1362
- detail: `创建/覆盖文件: ${input.path}`,
1363
- });
1364
- return createFileForTool(cwd, input);
1365
- },
1366
- }),
1367
- delete_file: tool<DeleteFileInput, DeleteFileOutput>({
1368
- description: "删除现有的文本文件。使用 expectedSha256 避免删除读取后已更改的文件。",
1369
- inputSchema: jsonSchema<DeleteFileInput>({
1370
- type: "object",
1371
- additionalProperties: false,
1372
- properties: {
1373
- path: { type: "string", description: "绝对路径或相对于会话工作目录的路径。" },
1374
- expectedSha256: { type: "string", description: "必须删除的文件的可选 SHA-256 哈希。" },
1375
- },
1376
- required: ["path"],
1377
- }),
1378
- execute: async (input) => {
1379
- await guard({
1380
- tool: "delete_file",
1381
- action: input.path,
1382
- altKeys: pathKeys(input.path),
1383
- reason: "rule",
1384
- detail: `删除文件: ${input.path}`,
1385
- });
1386
- return deleteFileForTool(cwd, input);
1387
- },
1388
- }),
1389
- move_file: tool<MoveFileInput, MoveFileOutput>({
1390
- description: "移动或重命名现有文本文件。仅在 overwrite=true 时可覆盖现有目标。",
1391
- inputSchema: jsonSchema<MoveFileInput>({
1392
- type: "object",
1393
- additionalProperties: false,
1394
- properties: {
1395
- sourcePath: { type: "string", description: "现有源文件路径。" },
1396
- destinationPath: { type: "string", description: "目标文件路径。" },
1397
- overwrite: { type: "boolean", description: "允许覆盖现有目标文件。" },
1398
- expectedSourceSha256: { type: "string", description: "源文件的可选 SHA-256 哈希。" },
1399
- expectedDestinationSha256: { type: "string", description: "覆盖目标时目标文件的可选 SHA-256 哈希。" },
1400
- },
1401
- required: ["sourcePath", "destinationPath"],
1402
- }),
1403
- execute: async (input) => {
1404
- await guard({
1405
- tool: "move_file",
1406
- action: input.sourcePath,
1407
- altKeys: pathKeys(input.sourcePath),
1408
- reason: "rule",
1409
- detail: `移动/重命名文件: ${input.sourcePath} → ${input.destinationPath}`,
1410
- });
1411
- return moveFileForTool(cwd, input);
1412
- },
1413
- }),
1414
- apply_patch: tool<ApplyPatchInput, ApplyPatchOutput>({
1415
- description: "将统一差异补丁应用于一个或多个 UTF-8 文本文件。小范围精确编辑优先使用 edit_file。",
1416
- inputSchema: jsonSchema<ApplyPatchInput>({
1417
- type: "object",
1418
- additionalProperties: false,
1419
- properties: {
1420
- patch: { type: "string", description: "统一差异补丁文本。" },
1421
- expectedSha256ByPath: {
1422
- type: "object",
1423
- description: "补丁路径或绝对路径到预期 SHA-256 的可选映射。",
1424
- additionalProperties: { type: "string" },
1425
- },
1426
- },
1427
- required: ["patch"],
1428
- }),
1429
- execute: async (input) => {
1430
- await guard({
1431
- tool: "apply_patch",
1432
- action: "patch",
1433
- reason: "rule",
1434
- detail: "应用 unified diff patch",
1435
- });
1436
- return applyPatchForTool(cwd, input);
1437
- },
1438
- }),
1439
- // 联网工具:只读外部网络操作,不触碰本地文件系统,无需权限询问
1440
- websearch: tool<WebSearchInput, WebSearchOutput>({
1441
- description:
1442
- "Search the web (DuckDuckGo, no API key) and return matching titles, URLs, and snippets. Use when you need current or external information not available locally, e.g. latest docs, news, or package versions.",
1443
- inputSchema: jsonSchema<WebSearchInput>({
1444
- type: "object",
1445
- additionalProperties: false,
1446
- properties: {
1447
- query: { type: "string", description: "Search query." },
1448
- maxResults: { type: "number", description: "Optional result count, default 5, capped at 10." },
1449
- },
1450
- required: ["query"],
1451
- }),
1452
- execute: (input, options) => webSearchForTool(input, { abortSignal: options.abortSignal }),
1453
- }),
1454
- webfetch: tool<WebFetchInput, WebFetchOutput>({
1455
- description:
1456
- "Fetch a URL and return its readable text content (HTML stripped, truncated). Use for documentation pages, articles, or API docs. Only http/https URLs are allowed.",
1457
- inputSchema: jsonSchema<WebFetchInput>({
1458
- type: "object",
1459
- additionalProperties: false,
1460
- properties: {
1461
- url: { type: "string", description: "http/https URL to fetch." },
1462
- maxChars: { type: "number", description: "Optional text length cap, default 10000, capped at 100000." },
1463
- },
1464
- required: ["url"],
1465
- }),
1466
- execute: (input, options) => webFetchForTool(input, { abortSignal: options.abortSignal }),
1467
- }),
1468
- // 会话历史检索:只读 ~/.deepccc 存档,无需权限询问
1469
- session_search: tool<SessionSearchInput, SessionSearchOutput>({
1470
- description:
1471
- "Search DeepCCC session archives by keyword and return matching snippets. Use when you need to recall old user messages, assistant replies, or tool calls from previous sessions. Multiple terms must all appear in the same message (AND, case-insensitive). Set include_raw_logs to also scan gzipped raw stream logs (slower).",
1472
- inputSchema: jsonSchema<SessionSearchInput>({
1473
- type: "object",
1474
- additionalProperties: false,
1475
- properties: {
1476
- query: { type: "string", description: "Keywords to search for (space-separated, all must match)." },
1477
- session_id: { type: "string", description: "Optional session id to restrict the search to." },
1478
- include_raw_logs: { type: "boolean", description: "Also scan gzipped raw stream logs under ~/.deepccc/raw-stream-logs. Default false." },
1479
- max_results: { type: "number", description: "Maximum number of matches, capped at 50." },
1480
- },
1481
- required: ["query"],
1482
- }),
1483
- execute: (input) =>
1484
- searchBuiltinSessions(input.query, {
1485
- contextDir: options.sessionSearch?.contextDir,
1486
- rawLogsDir: options.sessionSearch?.rawLogsDir,
1487
- includeRawLogs: input.include_raw_logs ?? false,
1488
- sessionId: input.session_id,
1489
- maxResults: input.max_results,
1490
- }),
1491
- }),
1492
- };
1493
- }