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.
- package/README.md +10 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/config.sample.json +2 -1
- package/deepccc-agent/README.md +40 -35
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +11 -11
- package/deepccc-agent/package.json +65 -65
- package/dist/deepccc-agent/src/cli.js +634 -0
- package/dist/deepccc-agent/src/config.js +81 -0
- package/dist/deepccc-agent/src/context.js +348 -0
- package/dist/deepccc-agent/src/file-log.js +34 -0
- package/dist/deepccc-agent/src/file-tools.js +1208 -0
- package/dist/deepccc-agent/src/index.js +601 -0
- package/dist/deepccc-agent/src/permissions.js +170 -0
- package/dist/deepccc-agent/src/privacy.js +124 -0
- package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
- package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
- package/dist/deepccc-agent/src/progress/reducer.js +102 -0
- package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
- package/dist/deepccc-agent/src/progress/view.js +30 -0
- package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
- package/dist/deepccc-agent/src/session-search.js +276 -0
- package/dist/deepccc-agent/src/session-select.js +23 -0
- package/dist/deepccc-agent/src/sigint.js +26 -0
- package/dist/deepccc-agent/src/skills.js +178 -0
- package/dist/deepccc-agent/src/web-tools.js +246 -0
- package/dist/src/adapters/adapter-interface.js +19 -0
- package/dist/src/adapters/ccc-adapter.js +112 -0
- package/dist/src/adapters/claude-adapter.js +497 -0
- package/dist/src/adapters/claude-session-meta-store.js +92 -0
- package/dist/src/adapters/codex-adapter.js +279 -0
- package/dist/src/adapters/codex-session-meta-store.js +94 -0
- package/dist/src/adapters/cursor-adapter.js +491 -0
- package/dist/src/adapters/cursor-session-meta-store.js +116 -0
- package/dist/src/adapters/jsonl-stream.js +104 -0
- package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
- package/dist/src/adapters/raw-stream-log.js +106 -0
- package/dist/src/adapters/resource-monitor.js +113 -0
- package/dist/src/agent-activity.js +133 -0
- package/dist/src/agent-delegate-task-rpc.js +129 -0
- package/dist/src/agent-delegate-task.js +48 -0
- package/dist/src/agent-file-rpc.js +152 -0
- package/dist/src/agent-image-rpc.js +148 -0
- package/dist/src/agent-platform-routing.js +13 -0
- package/dist/src/agent-reload-config-rpc.js +23 -0
- package/dist/src/agent-rpc-body.js +87 -0
- package/dist/src/agent-stop-stuck.js +110 -0
- package/dist/src/agent-team/application/board-service.js +142 -0
- package/dist/src/agent-team/application/main-agent-service.js +151 -0
- package/dist/src/agent-team/domain/board.js +69 -0
- package/dist/src/agent-team/http/board-routes.js +213 -0
- package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
- package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
- package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
- package/dist/src/agent-team/repositories/board-repository.js +10 -0
- package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
- package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
- package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
- package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
- package/dist/src/agent-team/web/agent-team-page.js +219 -0
- package/dist/src/agent-tool.js +10 -0
- package/dist/src/card-action-routing.js +7 -0
- package/dist/src/card-plain-text.js +101 -0
- package/dist/src/cardkit.js +158 -0
- package/dist/src/cards.js +573 -0
- package/dist/src/chatgpt-subscription-rpc.js +18 -0
- package/dist/src/chatgpt-subscription.js +199 -0
- package/dist/src/chrome-devtools-guard.js +238 -0
- package/dist/src/claude-sdk-installer.js +249 -0
- package/dist/src/codex-reset-actions.js +143 -0
- package/dist/src/config-utils.js +149 -0
- package/dist/src/config.js +808 -0
- package/dist/src/cursor-usage.js +77 -0
- package/dist/src/exit-banner.js +28 -0
- package/dist/src/feishu-api.js +1404 -0
- package/dist/src/feishu-message-ingress.js +137 -0
- package/dist/src/feishu-platform.js +97 -0
- package/dist/src/format-message.js +252 -0
- package/dist/src/git-command.js +155 -0
- package/dist/src/im-skills.js +121 -0
- package/dist/src/index.js +835 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2096 -0
- package/dist/src/package-root.js +26 -0
- package/dist/src/platform-adapter.js +7 -0
- package/dist/src/platform-startup.js +6 -0
- package/dist/src/privacy.js +100 -0
- package/dist/src/progress/reducer.js +102 -0
- package/dist/src/progress/terminal-renderer.js +264 -0
- package/dist/src/progress/view.js +30 -0
- package/dist/src/response-stall.js +14 -0
- package/dist/src/runtime-entry.js +13 -0
- package/dist/src/runtime-reload.js +19 -0
- package/dist/src/session-chat-binding.js +183 -0
- package/dist/src/session-name.js +7 -0
- package/dist/src/session.js +2147 -0
- package/dist/src/shared-prefix.js +16 -0
- package/dist/src/shared.js +493 -0
- package/dist/src/sim-agent.js +105 -0
- package/dist/src/sim-platform.js +142 -0
- package/dist/src/sim-store.js +231 -0
- package/dist/src/simplify.js +99 -0
- package/dist/src/startup-lifecycle.js +209 -0
- package/dist/src/stream-state.js +141 -0
- package/dist/src/terminal-error.js +100 -0
- package/dist/src/trace.js +50 -0
- package/dist/src/turn-cards.js +92 -0
- package/dist/src/update-command-guard.js +114 -0
- package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
- package/dist/src/wechat-platform.js +545 -0
- package/package.json +76 -74
- package/deepccc-agent/LICENSE +0 -201
- package/deepccc-agent/bin/deepccc.mjs +0 -26
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +0 -2027
- package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
- package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
- package/deepccc-agent/src/__tests__/config.test.ts +0 -34
- package/deepccc-agent/src/__tests__/context.test.ts +0 -341
- package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
- package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
- package/deepccc-agent/src/cli.ts +0 -682
- package/deepccc-agent/src/config.ts +0 -101
- package/deepccc-agent/src/context.ts +0 -465
- package/deepccc-agent/src/file-log.ts +0 -38
- package/deepccc-agent/src/file-tools.ts +0 -1493
- package/deepccc-agent/src/index.ts +0 -710
- package/deepccc-agent/src/permissions.ts +0 -226
- package/deepccc-agent/src/privacy.ts +0 -141
- package/deepccc-agent/src/proc-tree-kill.ts +0 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
- package/deepccc-agent/src/progress/reducer.ts +0 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
- package/deepccc-agent/src/progress/view.ts +0 -77
- package/deepccc-agent/src/raw-stream-log.ts +0 -124
- package/deepccc-agent/src/session-search.ts +0 -370
- package/deepccc-agent/src/session-select.ts +0 -48
- package/deepccc-agent/src/sigint.ts +0 -50
- package/deepccc-agent/src/skills.ts +0 -205
- package/deepccc-agent/src/web-tools.ts +0 -313
- package/deepccc-agent/tsconfig.build.json +0 -13
- package/deepccc-agent/tsconfig.json +0 -13
- package/deepccc-agent/vitest.config.ts +0 -7
- package/src/__tests__/adapter-interface.test.ts +0 -152
- package/src/__tests__/agent-activity.test.ts +0 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
- package/src/__tests__/agent-image-rpc.test.ts +0 -34
- package/src/__tests__/agent-platform-routing.test.ts +0 -26
- package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
- package/src/__tests__/agent-rpc-body.test.ts +0 -42
- package/src/__tests__/builtin-chat-session.test.ts +0 -532
- package/src/__tests__/builtin-cli-json.test.ts +0 -39
- package/src/__tests__/builtin-config.test.ts +0 -26
- package/src/__tests__/builtin-context.test.ts +0 -319
- package/src/__tests__/builtin-file-tools.test.ts +0 -240
- package/src/__tests__/builtin-permissions.test.ts +0 -219
- package/src/__tests__/builtin-session-search.test.ts +0 -262
- package/src/__tests__/builtin-session-select.test.ts +0 -116
- package/src/__tests__/builtin-sigint.test.ts +0 -56
- package/src/__tests__/builtin-skills.test.ts +0 -284
- package/src/__tests__/builtin-web-tools.test.ts +0 -220
- package/src/__tests__/card-action-routing.test.ts +0 -18
- package/src/__tests__/card-plain-text.test.ts +0 -45
- package/src/__tests__/cardkit.test.ts +0 -60
- package/src/__tests__/cards.test.ts +0 -607
- package/src/__tests__/ccc-adapter.test.ts +0 -194
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
- package/src/__tests__/chatgpt-subscription.test.ts +0 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
- package/src/__tests__/claude-adapter.test.ts +0 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
- package/src/__tests__/claude-sdk-installer.test.ts +0 -285
- package/src/__tests__/codex-adapter.test.ts +0 -331
- package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
- package/src/__tests__/codex-reset-actions.test.ts +0 -146
- package/src/__tests__/config-reload.test.ts +0 -284
- package/src/__tests__/config-sample.test.ts +0 -97
- package/src/__tests__/config-utils.test.ts +0 -40
- package/src/__tests__/config.test.ts +0 -395
- package/src/__tests__/crash-logging.test.ts +0 -360
- package/src/__tests__/cursor-adapter.test.ts +0 -890
- package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
- package/src/__tests__/feishu-api.test.ts +0 -60
- package/src/__tests__/feishu-avatar.test.ts +0 -504
- package/src/__tests__/feishu-message-ingress.test.ts +0 -138
- package/src/__tests__/feishu-platform.test.ts +0 -75
- package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
- package/src/__tests__/format-message.test.ts +0 -316
- package/src/__tests__/git-command.test.ts +0 -288
- package/src/__tests__/im-skills.test.ts +0 -125
- package/src/__tests__/jsonl-stream.test.ts +0 -79
- package/src/__tests__/orchestrator.test.ts +0 -1268
- package/src/__tests__/package-files.test.ts +0 -24
- package/src/__tests__/platform-startup.test.ts +0 -19
- package/src/__tests__/privacy.test.ts +0 -198
- package/src/__tests__/proc-tree-kill.test.ts +0 -108
- package/src/__tests__/progress-reducer.test.ts +0 -121
- package/src/__tests__/raw-stream-log.test.ts +0 -106
- package/src/__tests__/response-stall.test.ts +0 -49
- package/src/__tests__/restart.test.ts +0 -232
- package/src/__tests__/session-ccc-config.test.ts +0 -66
- package/src/__tests__/session.test.ts +0 -3004
- package/src/__tests__/shared-prefix.test.ts +0 -36
- package/src/__tests__/sim-agent.test.ts +0 -174
- package/src/__tests__/sim-platform.test.ts +0 -93
- package/src/__tests__/sim-store.test.ts +0 -214
- package/src/__tests__/simplify.test.ts +0 -283
- package/src/__tests__/startup-lifecycle.test.ts +0 -231
- package/src/__tests__/stop-session.test.ts +0 -162
- package/src/__tests__/stream-state.test.ts +0 -164
- package/src/__tests__/terminal-error.test.ts +0 -54
- package/src/__tests__/terminal-renderer.test.ts +0 -247
- package/src/__tests__/update-command-guard.test.ts +0 -144
- package/src/__tests__/web-ui.test.ts +0 -438
- package/src/__tests__/wechat-platform.test.ts +0 -111
- package/src/adapters/adapter-interface.ts +0 -217
- package/src/adapters/ccc-adapter.ts +0 -150
- package/src/adapters/claude-adapter.ts +0 -673
- package/src/adapters/claude-session-meta-store.ts +0 -120
- package/src/adapters/codex-adapter.ts +0 -426
- package/src/adapters/codex-session-meta-store.ts +0 -131
- package/src/adapters/cursor-adapter.ts +0 -681
- package/src/adapters/cursor-session-meta-store.ts +0 -154
- package/src/adapters/jsonl-stream.ts +0 -157
- package/src/adapters/raw-stream-log.ts +0 -124
- package/src/adapters/resource-monitor.ts +0 -141
- package/src/agent-activity.ts +0 -175
- package/src/agent-delegate-task-rpc.ts +0 -153
- package/src/agent-delegate-task.ts +0 -91
- package/src/agent-file-rpc.ts +0 -172
- package/src/agent-image-rpc.ts +0 -168
- package/src/agent-platform-routing.ts +0 -28
- package/src/agent-reload-config-rpc.ts +0 -34
- package/src/agent-rpc-body.ts +0 -92
- package/src/agent-stop-stuck.ts +0 -129
- package/src/card-action-routing.ts +0 -14
- package/src/card-plain-text.ts +0 -108
- package/src/cardkit.ts +0 -179
- package/src/cards.ts +0 -684
- package/src/chatgpt-subscription-rpc.ts +0 -27
- package/src/chatgpt-subscription.ts +0 -299
- package/src/chrome-devtools-guard.ts +0 -318
- package/src/claude-sdk-installer.ts +0 -324
- package/src/codex-reset-actions.ts +0 -184
- package/src/config-utils.ts +0 -211
- package/src/config.ts +0 -1063
- package/src/cursor-usage.ts +0 -128
- package/src/exit-banner.ts +0 -33
- package/src/feishu-api.ts +0 -1616
- package/src/feishu-message-ingress.ts +0 -195
- package/src/feishu-platform.ts +0 -159
- package/src/format-message.ts +0 -293
- package/src/git-command.ts +0 -202
- package/src/im-skills.ts +0 -149
- package/src/index.ts +0 -1089
- package/src/litellm-proxy.ts +0 -374
- package/src/orchestrator.ts +0 -2543
- package/src/platform-adapter.ts +0 -70
- package/src/platform-startup.ts +0 -16
- package/src/privacy.ts +0 -118
- package/src/progress/reducer.ts +0 -113
- package/src/progress/terminal-renderer.ts +0 -294
- package/src/progress/view.ts +0 -77
- package/src/response-stall.ts +0 -28
- package/src/runtime-reload.ts +0 -34
- package/src/session-chat-binding.ts +0 -292
- package/src/session-name.ts +0 -8
- package/src/session.ts +0 -2659
- package/src/shared-prefix.ts +0 -29
- package/src/shared.ts +0 -552
- package/src/sim-agent.ts +0 -167
- package/src/sim-platform.ts +0 -177
- package/src/sim-store.ts +0 -317
- package/src/simplify.ts +0 -120
- package/src/startup-lifecycle.ts +0 -250
- package/src/stream-state.ts +0 -177
- package/src/terminal-error.ts +0 -129
- package/src/trace.ts +0 -51
- package/src/turn-cards.ts +0 -118
- package/src/update-command-guard.ts +0 -165
- 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
|
-
}
|