@shawnstack/quickforge 1.3.18 → 1.3.20
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 -10
- package/bin/quickforge.mjs +258 -49
- package/dist/assets/anthropic-Bj3HAZgj.js +39 -0
- package/dist/assets/azure-openai-responses-IdZZrSrI.js +1 -0
- package/dist/assets/github-copilot-headers-CMb2BbzT.js +1 -0
- package/dist/assets/google-Brt_lS1J.js +1 -0
- package/dist/assets/{google-shared-XhYUKiGZ.js → google-shared-CLc4ziON.js} +3 -3
- package/dist/assets/google-vertex-B6HsoZ34.js +1 -0
- package/dist/assets/{index-Dm7aEWvT.js → index-D0CVLdX_.js} +525 -489
- package/dist/assets/index-D0W9hAl_.css +3 -0
- package/dist/assets/{mistral-DxhS4Wkn.js → mistral-CenXqwPz.js} +3 -3
- package/dist/assets/openai-codex-responses-D9ffGwbj.js +7 -0
- package/dist/assets/openai-completions-eWdeSGBG.js +5 -0
- package/dist/assets/openai-responses-Cavpmjeu.js +1 -0
- package/dist/assets/{openai-responses-shared-f_P3e1nz.js → openai-responses-shared-DF3ZGaUx.js} +5 -3
- package/dist/assets/transform-messages-CmnxG9RB.js +1 -0
- package/dist/index.html +2 -2
- package/node_modules/@anthropic-ai/sdk/CHANGELOG.md +34 -0
- package/node_modules/@anthropic-ai/sdk/bin/migration-config.json +185 -0
- package/node_modules/@anthropic-ai/sdk/package.json +1 -1
- package/node_modules/@anthropic-ai/sdk/resources/beta/beta.js +4 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/beta.mjs +4 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/files.js +5 -5
- package/node_modules/@anthropic-ai/sdk/resources/beta/files.mjs +5 -5
- package/node_modules/@anthropic-ai/sdk/resources/beta/index.js +11 -9
- package/node_modules/@anthropic-ai/sdk/resources/beta/index.mjs +1 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.js +11 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.mjs +5 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.js +130 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.mjs +126 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.js +145 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.mjs +140 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.js +81 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.mjs +77 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.js +6 -0
- package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.mjs +3 -0
- package/node_modules/@anthropic-ai/sdk/tools/memory/node.js +12 -5
- package/node_modules/@anthropic-ai/sdk/tools/memory/node.mjs +12 -5
- package/node_modules/@anthropic-ai/sdk/version.js +1 -1
- package/node_modules/@anthropic-ai/sdk/version.mjs +1 -1
- package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +5 -5
- package/node_modules/@aws-sdk/core/package.json +2 -2
- package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
- package/node_modules/@aws-sdk/credential-provider-http/dist-cjs/fromHttp/fromHttp.js +12 -6
- package/node_modules/@aws-sdk/credential-provider-http/dist-es/fromHttp/fromHttp.js +12 -6
- package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -2
- package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
- package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
- package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
- package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
- package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
- package/node_modules/@aws-sdk/middleware-websocket/package.json +2 -2
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
- package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +1 -2
- package/node_modules/@aws-sdk/token-providers/package.json +3 -3
- package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
- package/node_modules/@mariozechner/pi-agent-core/README.md +14 -0
- package/node_modules/@mariozechner/pi-agent-core/dist/agent-loop.js +9 -0
- package/node_modules/@mariozechner/pi-agent-core/dist/agent.js +1 -1
- package/node_modules/@mariozechner/pi-agent-core/package.json +2 -2
- package/node_modules/@mariozechner/pi-ai/README.md +20 -31
- package/node_modules/@mariozechner/pi-ai/dist/env-api-keys.js +7 -0
- package/node_modules/@mariozechner/pi-ai/dist/index.js +2 -0
- package/node_modules/@mariozechner/pi-ai/dist/models.generated.js +2420 -1213
- package/node_modules/@mariozechner/pi-ai/dist/models.js +28 -20
- package/node_modules/@mariozechner/pi-ai/dist/providers/amazon-bedrock.js +11 -11
- package/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js +43 -26
- package/node_modules/@mariozechner/pi-ai/dist/providers/azure-openai-responses.js +12 -6
- package/node_modules/@mariozechner/pi-ai/dist/providers/cloudflare.js +10 -3
- package/node_modules/@mariozechner/pi-ai/dist/providers/google-shared.js +4 -13
- package/node_modules/@mariozechner/pi-ai/dist/providers/google-vertex.js +4 -3
- package/node_modules/@mariozechner/pi-ai/dist/providers/google.js +4 -3
- package/node_modules/@mariozechner/pi-ai/dist/providers/mistral.js +8 -7
- package/node_modules/@mariozechner/pi-ai/dist/providers/openai-codex-responses.js +296 -41
- package/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js +169 -153
- package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses-shared.js +14 -1
- package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses.js +22 -8
- package/node_modules/@mariozechner/pi-ai/dist/providers/register-builtins.js +0 -18
- package/node_modules/@mariozechner/pi-ai/dist/providers/simple-options.js +1 -0
- package/node_modules/@mariozechner/pi-ai/dist/session-resources.js +22 -0
- package/node_modules/@mariozechner/pi-ai/dist/utils/diagnostics.js +25 -0
- package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/index.js +0 -10
- package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/openai-codex.js +25 -14
- package/node_modules/@mariozechner/pi-ai/dist/utils/overflow.js +14 -0
- package/node_modules/@mariozechner/pi-ai/package.json +2 -6
- package/package.json +3 -3
- package/server/agent-manager.mjs +279 -12
- package/server/auto-compaction.mjs +1 -2
- package/server/conversation-compaction.mjs +0 -5
- package/server/index.mjs +1 -0
- package/server/routes/static.mjs +1 -0
- package/server/routes/tools.mjs +3 -1
- package/server/session-utils.mjs +6 -1
- package/server/share-store.mjs +27 -4
- package/server/subagents.mjs +101 -0
- package/server/system-prompt.mjs +30 -1
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +1013 -911
- package/dist/assets/anthropic-Ck2DxOfr.js +0 -39
- package/dist/assets/azure-openai-responses-DIoz5q4Z.js +0 -1
- package/dist/assets/github-copilot-headers-CrI0CIJ7.js +0 -1
- package/dist/assets/google-Dau-4ve_.js +0 -1
- package/dist/assets/google-gemini-cli-DttMmbGb.js +0 -2
- package/dist/assets/google-vertex-BeukMl44.js +0 -1
- package/dist/assets/index-DgJVElbv.css +0 -3
- package/dist/assets/openai-codex-responses-X3sTzNAa.js +0 -7
- package/dist/assets/openai-completions-CRB9Vm0w.js +0 -5
- package/dist/assets/openai-responses-DXluu3oi.js +0 -1
- package/dist/assets/transform-messages-CV4kCtBB.js +0 -1
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/LICENSE +0 -201
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/README.md +0 -62
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +0 -156
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/constants.js +0 -2
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromEnvSigningName.js +0 -16
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +0 -80
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromStatic.js +0 -8
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getNewSsoOidcToken.js +0 -11
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getSsoOidcClient.js +0 -10
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/index.js +0 -4
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/nodeProvider.js +0 -5
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenExpiry.js +0 -7
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenKey.js +0 -7
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/writeSSOTokenToFile.js +0 -8
- package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +0 -69
- package/node_modules/@mariozechner/pi-ai/dist/providers/google-gemini-cli.js +0 -779
- package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-antigravity.js +0 -377
- package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-gemini-cli.js +0 -482
package/server/tools/index.mjs
CHANGED
|
@@ -1,911 +1,1013 @@
|
|
|
1
|
-
import { createWriteStream, promises as fs } from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import { resolveWorkspacePath, toWorkspaceRelative, assertSafeWorkspacePath, truncateText, splitLines, walkFiles } from '../utils/workspace.mjs'
|
|
6
|
-
import { logsDir } from '../storage.mjs'
|
|
7
|
-
import { createTextDiff } from '../utils/text-diff.mjs'
|
|
8
|
-
import {
|
|
9
|
-
formatSkillActivation,
|
|
10
|
-
loadSelectedGlobalSkills,
|
|
11
|
-
loadSelectedProjectSkills,
|
|
12
|
-
mergeSkills,
|
|
13
|
-
readSkillResource,
|
|
14
|
-
} from '../skills.mjs'
|
|
15
|
-
import { getToolWorkspaceRoot } from '../utils/workspace.mjs'
|
|
16
|
-
|
|
17
|
-
const require = createRequire(import.meta.url)
|
|
18
|
-
|
|
19
|
-
// --- read_file ---
|
|
20
|
-
export async function toolReadFile(params, context) {
|
|
21
|
-
const file = resolveWorkspacePath(params?.path, context)
|
|
22
|
-
await assertSafeWorkspacePath(file, context)
|
|
23
|
-
|
|
24
|
-
const text = await fs.readFile(file, 'utf8')
|
|
25
|
-
const lines = splitLines(text)
|
|
26
|
-
const offset = Math.max(1, Number(params?.offset || 1))
|
|
27
|
-
const limit = Math.min(2000, Math.max(1, Number(params?.limit || 200)))
|
|
28
|
-
const selected = lines.slice(offset - 1, offset - 1 + limit)
|
|
29
|
-
const content = selected.map((line, index) => `${offset + index}: ${line}`).join('\n')
|
|
30
|
-
const suffix = offset - 1 + limit < lines.length ? `\n\n[showing ${selected.length} of ${lines.length} lines]` : ''
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
content: truncateText(`${content}${suffix}`),
|
|
34
|
-
details: { path: toWorkspaceRelative(file, context), project: context?.project, totalLines: lines.length, offset, limit },
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- grep_files ---
|
|
39
|
-
|
|
40
|
-
const RIPGREP_MAX_FILESIZE = '1M'
|
|
41
|
-
const RIPGREP_TIMEOUT_MS = 60 * 1000
|
|
42
|
-
const DEFAULT_EXCLUDE_GLOBS = [
|
|
43
|
-
'!.git/**',
|
|
44
|
-
'!node_modules/**',
|
|
45
|
-
'!dist/**',
|
|
46
|
-
'!dist-ssr/**',
|
|
47
|
-
'!.vite/**',
|
|
48
|
-
'!**/*.png',
|
|
49
|
-
'!**/*.jpg',
|
|
50
|
-
'!**/*.jpeg',
|
|
51
|
-
'!**/*.gif',
|
|
52
|
-
'!**/*.webp',
|
|
53
|
-
'!**/*.ico',
|
|
54
|
-
'!**/*.pdf',
|
|
55
|
-
'!**/*.zip',
|
|
56
|
-
'!**/*.gz',
|
|
57
|
-
'!**/*.7z',
|
|
58
|
-
'!**/*.exe',
|
|
59
|
-
'!**/*.dll',
|
|
60
|
-
'!**/*.woff',
|
|
61
|
-
'!**/*.woff2',
|
|
62
|
-
'!**/*.ttf',
|
|
63
|
-
]
|
|
64
|
-
const SENSITIVE_EXCLUDE_GLOBS = [
|
|
65
|
-
'!.env',
|
|
66
|
-
'!**/.env',
|
|
67
|
-
'!.env.*',
|
|
68
|
-
'!**/.env.*',
|
|
69
|
-
'!**/*.pem',
|
|
70
|
-
'!**/*.key',
|
|
71
|
-
'!**/*.p12',
|
|
72
|
-
'!**/*.pfx',
|
|
73
|
-
'!**/*.crt',
|
|
74
|
-
'!**/*.cer',
|
|
75
|
-
'!**/*.token',
|
|
76
|
-
'!credentials.json',
|
|
77
|
-
'!**/credentials.json',
|
|
78
|
-
'!secrets.json',
|
|
79
|
-
'!**/secrets.json',
|
|
80
|
-
'!id_rsa',
|
|
81
|
-
'!**/id_rsa',
|
|
82
|
-
'!id_ed25519',
|
|
83
|
-
'!**/id_ed25519',
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
let cachedRipgrepExecutable
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Process items with bounded concurrency. Returns results in input order.
|
|
90
|
-
* @template T, R
|
|
91
|
-
* @param {T[]} items
|
|
92
|
-
* @param {(item: T, index: number) => Promise<R>} fn
|
|
93
|
-
* @param {number} concurrency
|
|
94
|
-
* @returns {Promise<R[]>}
|
|
95
|
-
*/
|
|
96
|
-
async function poolMap(items, fn, concurrency = 20) {
|
|
97
|
-
const results = new Array(items.length)
|
|
98
|
-
let cursor = 0
|
|
99
|
-
|
|
100
|
-
async function worker() {
|
|
101
|
-
while (cursor < items.length) {
|
|
102
|
-
const index = cursor++
|
|
103
|
-
results[index] = await fn(items[index], index)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
|
|
108
|
-
await Promise.all(workers)
|
|
109
|
-
return results
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function clampNumber(value, defaultValue, min, max) {
|
|
113
|
-
const number = Number(value)
|
|
114
|
-
if (!Number.isFinite(number)) return defaultValue
|
|
115
|
-
return Math.min(max, Math.max(min, Math.trunc(number)))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function escapeRegExp(value) {
|
|
119
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function normalizeGlobList(value) {
|
|
123
|
-
const values = Array.isArray(value) ? value : typeof value === 'string' ? [value] : []
|
|
124
|
-
return values
|
|
125
|
-
.map((item) => String(item || '').trim())
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.slice(0, 50)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeGrepParams(params, context) {
|
|
131
|
-
const root = resolveWorkspacePath(params?.path || '.', context)
|
|
132
|
-
const query = String(params?.query || '')
|
|
133
|
-
if (!query) {
|
|
134
|
-
const error = new Error('query is required')
|
|
135
|
-
error.statusCode = 400
|
|
136
|
-
throw error
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const flags = params?.caseSensitive ? 'g' : 'gi'
|
|
140
|
-
try {
|
|
141
|
-
params?.regex
|
|
142
|
-
? new RegExp(query, flags)
|
|
143
|
-
: new RegExp(escapeRegExp(query), flags)
|
|
144
|
-
} catch {
|
|
145
|
-
const error = new Error('Invalid regular expression')
|
|
146
|
-
error.statusCode = 400
|
|
147
|
-
throw error
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
root,
|
|
152
|
-
query,
|
|
153
|
-
regex: Boolean(params?.regex),
|
|
154
|
-
caseSensitive: Boolean(params?.caseSensitive),
|
|
155
|
-
limit: clampNumber(params?.limit, 200, 1, 1000),
|
|
156
|
-
glob: normalizeGlobList(params?.glob),
|
|
157
|
-
context: clampNumber(params?.context, 0, 0, 20),
|
|
158
|
-
beforeContext: clampNumber(params?.beforeContext, 0, 0, 20),
|
|
159
|
-
afterContext: clampNumber(params?.afterContext, 0, 0, 20),
|
|
160
|
-
filesWithMatches: Boolean(params?.filesWithMatches),
|
|
161
|
-
respectGitIgnore: Boolean(params?.respectGitIgnore),
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isRegexLikelyRipgrepCompatible(query) {
|
|
166
|
-
return !(/\(\?[=!<]/.test(query) || /\\[1-9]/.test(query))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function ripgrepCandidatePath() {
|
|
170
|
-
try {
|
|
171
|
-
return require('@vscode/ripgrep').rgPath || null
|
|
172
|
-
} catch {
|
|
173
|
-
return null
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function verifyRipgrepExecutable(command) {
|
|
178
|
-
return new Promise((resolve) => {
|
|
179
|
-
const child = spawn(command, ['--version'], { shell: false, windowsHide: true })
|
|
180
|
-
child.once('error', () => resolve(false))
|
|
181
|
-
child.once('close', (code) => resolve(code === 0))
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function resolveRipgrepExecutable() {
|
|
186
|
-
if (cachedRipgrepExecutable !== undefined) return cachedRipgrepExecutable
|
|
187
|
-
|
|
188
|
-
const bundled = ripgrepCandidatePath()
|
|
189
|
-
if (bundled && await verifyRipgrepExecutable(bundled)) {
|
|
190
|
-
cachedRipgrepExecutable = { command: bundled, source: 'bundled' }
|
|
191
|
-
return cachedRipgrepExecutable
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (await verifyRipgrepExecutable('rg')) {
|
|
195
|
-
cachedRipgrepExecutable = { command: 'rg', source: 'system' }
|
|
196
|
-
return cachedRipgrepExecutable
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
cachedRipgrepExecutable = null
|
|
200
|
-
return cachedRipgrepExecutable
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function buildRipgrepArgs(options, context) {
|
|
204
|
-
const args = [
|
|
205
|
-
'--line-number',
|
|
206
|
-
'--color=never',
|
|
207
|
-
'--max-filesize',
|
|
208
|
-
RIPGREP_MAX_FILESIZE,
|
|
209
|
-
]
|
|
210
|
-
|
|
211
|
-
if (options.filesWithMatches) {
|
|
212
|
-
args.push('--files-with-matches')
|
|
213
|
-
} else {
|
|
214
|
-
args.push('--json')
|
|
215
|
-
}
|
|
216
|
-
if (!options.regex) args.push('--fixed-strings')
|
|
217
|
-
if (!options.caseSensitive) args.push('--ignore-case')
|
|
218
|
-
if (!options.respectGitIgnore) args.push('--hidden', '--no-ignore')
|
|
219
|
-
if (options.context > 0) args.push('-C', String(options.context))
|
|
220
|
-
if (options.beforeContext > 0) args.push('-B', String(options.beforeContext))
|
|
221
|
-
if (options.afterContext > 0) args.push('-A', String(options.afterContext))
|
|
222
|
-
|
|
223
|
-
for (const pattern of options.glob) args.push('--glob', pattern)
|
|
224
|
-
for (const pattern of DEFAULT_EXCLUDE_GLOBS) args.push('--glob', pattern)
|
|
225
|
-
for (const pattern of SENSITIVE_EXCLUDE_GLOBS) args.push('--glob', pattern)
|
|
226
|
-
|
|
227
|
-
args.push('--', options.query, toWorkspaceRelative(options.root, context) || '.')
|
|
228
|
-
return args
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function cleanRipgrepLine(value) {
|
|
232
|
-
return String(value || '').replace(/[\r\n]+$/, '')
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function ripgrepRelativePath(value) {
|
|
236
|
-
return String(value || '').replace(/\\/g, '/')
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
matchCount
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
?
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
})
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1
|
+
import { createWriteStream, promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { resolveWorkspacePath, toWorkspaceRelative, assertSafeWorkspacePath, truncateText, splitLines, walkFiles } from '../utils/workspace.mjs'
|
|
6
|
+
import { logsDir } from '../storage.mjs'
|
|
7
|
+
import { createTextDiff } from '../utils/text-diff.mjs'
|
|
8
|
+
import {
|
|
9
|
+
formatSkillActivation,
|
|
10
|
+
loadSelectedGlobalSkills,
|
|
11
|
+
loadSelectedProjectSkills,
|
|
12
|
+
mergeSkills,
|
|
13
|
+
readSkillResource,
|
|
14
|
+
} from '../skills.mjs'
|
|
15
|
+
import { getToolWorkspaceRoot } from '../utils/workspace.mjs'
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
|
|
19
|
+
// --- read_file ---
|
|
20
|
+
export async function toolReadFile(params, context) {
|
|
21
|
+
const file = resolveWorkspacePath(params?.path, context)
|
|
22
|
+
await assertSafeWorkspacePath(file, context)
|
|
23
|
+
|
|
24
|
+
const text = await fs.readFile(file, 'utf8')
|
|
25
|
+
const lines = splitLines(text)
|
|
26
|
+
const offset = Math.max(1, Number(params?.offset || 1))
|
|
27
|
+
const limit = Math.min(2000, Math.max(1, Number(params?.limit || 200)))
|
|
28
|
+
const selected = lines.slice(offset - 1, offset - 1 + limit)
|
|
29
|
+
const content = selected.map((line, index) => `${offset + index}: ${line}`).join('\n')
|
|
30
|
+
const suffix = offset - 1 + limit < lines.length ? `\n\n[showing ${selected.length} of ${lines.length} lines]` : ''
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
content: truncateText(`${content}${suffix}`),
|
|
34
|
+
details: { path: toWorkspaceRelative(file, context), project: context?.project, totalLines: lines.length, offset, limit },
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- grep_files ---
|
|
39
|
+
|
|
40
|
+
const RIPGREP_MAX_FILESIZE = '1M'
|
|
41
|
+
const RIPGREP_TIMEOUT_MS = 60 * 1000
|
|
42
|
+
const DEFAULT_EXCLUDE_GLOBS = [
|
|
43
|
+
'!.git/**',
|
|
44
|
+
'!node_modules/**',
|
|
45
|
+
'!dist/**',
|
|
46
|
+
'!dist-ssr/**',
|
|
47
|
+
'!.vite/**',
|
|
48
|
+
'!**/*.png',
|
|
49
|
+
'!**/*.jpg',
|
|
50
|
+
'!**/*.jpeg',
|
|
51
|
+
'!**/*.gif',
|
|
52
|
+
'!**/*.webp',
|
|
53
|
+
'!**/*.ico',
|
|
54
|
+
'!**/*.pdf',
|
|
55
|
+
'!**/*.zip',
|
|
56
|
+
'!**/*.gz',
|
|
57
|
+
'!**/*.7z',
|
|
58
|
+
'!**/*.exe',
|
|
59
|
+
'!**/*.dll',
|
|
60
|
+
'!**/*.woff',
|
|
61
|
+
'!**/*.woff2',
|
|
62
|
+
'!**/*.ttf',
|
|
63
|
+
]
|
|
64
|
+
const SENSITIVE_EXCLUDE_GLOBS = [
|
|
65
|
+
'!.env',
|
|
66
|
+
'!**/.env',
|
|
67
|
+
'!.env.*',
|
|
68
|
+
'!**/.env.*',
|
|
69
|
+
'!**/*.pem',
|
|
70
|
+
'!**/*.key',
|
|
71
|
+
'!**/*.p12',
|
|
72
|
+
'!**/*.pfx',
|
|
73
|
+
'!**/*.crt',
|
|
74
|
+
'!**/*.cer',
|
|
75
|
+
'!**/*.token',
|
|
76
|
+
'!credentials.json',
|
|
77
|
+
'!**/credentials.json',
|
|
78
|
+
'!secrets.json',
|
|
79
|
+
'!**/secrets.json',
|
|
80
|
+
'!id_rsa',
|
|
81
|
+
'!**/id_rsa',
|
|
82
|
+
'!id_ed25519',
|
|
83
|
+
'!**/id_ed25519',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
let cachedRipgrepExecutable
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process items with bounded concurrency. Returns results in input order.
|
|
90
|
+
* @template T, R
|
|
91
|
+
* @param {T[]} items
|
|
92
|
+
* @param {(item: T, index: number) => Promise<R>} fn
|
|
93
|
+
* @param {number} concurrency
|
|
94
|
+
* @returns {Promise<R[]>}
|
|
95
|
+
*/
|
|
96
|
+
async function poolMap(items, fn, concurrency = 20) {
|
|
97
|
+
const results = new Array(items.length)
|
|
98
|
+
let cursor = 0
|
|
99
|
+
|
|
100
|
+
async function worker() {
|
|
101
|
+
while (cursor < items.length) {
|
|
102
|
+
const index = cursor++
|
|
103
|
+
results[index] = await fn(items[index], index)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
|
|
108
|
+
await Promise.all(workers)
|
|
109
|
+
return results
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clampNumber(value, defaultValue, min, max) {
|
|
113
|
+
const number = Number(value)
|
|
114
|
+
if (!Number.isFinite(number)) return defaultValue
|
|
115
|
+
return Math.min(max, Math.max(min, Math.trunc(number)))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeRegExp(value) {
|
|
119
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeGlobList(value) {
|
|
123
|
+
const values = Array.isArray(value) ? value : typeof value === 'string' ? [value] : []
|
|
124
|
+
return values
|
|
125
|
+
.map((item) => String(item || '').trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.slice(0, 50)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeGrepParams(params, context) {
|
|
131
|
+
const root = resolveWorkspacePath(params?.path || '.', context)
|
|
132
|
+
const query = String(params?.query || '')
|
|
133
|
+
if (!query) {
|
|
134
|
+
const error = new Error('query is required')
|
|
135
|
+
error.statusCode = 400
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const flags = params?.caseSensitive ? 'g' : 'gi'
|
|
140
|
+
try {
|
|
141
|
+
params?.regex
|
|
142
|
+
? new RegExp(query, flags)
|
|
143
|
+
: new RegExp(escapeRegExp(query), flags)
|
|
144
|
+
} catch {
|
|
145
|
+
const error = new Error('Invalid regular expression')
|
|
146
|
+
error.statusCode = 400
|
|
147
|
+
throw error
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
root,
|
|
152
|
+
query,
|
|
153
|
+
regex: Boolean(params?.regex),
|
|
154
|
+
caseSensitive: Boolean(params?.caseSensitive),
|
|
155
|
+
limit: clampNumber(params?.limit, 200, 1, 1000),
|
|
156
|
+
glob: normalizeGlobList(params?.glob),
|
|
157
|
+
context: clampNumber(params?.context, 0, 0, 20),
|
|
158
|
+
beforeContext: clampNumber(params?.beforeContext, 0, 0, 20),
|
|
159
|
+
afterContext: clampNumber(params?.afterContext, 0, 0, 20),
|
|
160
|
+
filesWithMatches: Boolean(params?.filesWithMatches),
|
|
161
|
+
respectGitIgnore: Boolean(params?.respectGitIgnore),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRegexLikelyRipgrepCompatible(query) {
|
|
166
|
+
return !(/\(\?[=!<]/.test(query) || /\\[1-9]/.test(query))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ripgrepCandidatePath() {
|
|
170
|
+
try {
|
|
171
|
+
return require('@vscode/ripgrep').rgPath || null
|
|
172
|
+
} catch {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function verifyRipgrepExecutable(command) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const child = spawn(command, ['--version'], { shell: false, windowsHide: true })
|
|
180
|
+
child.once('error', () => resolve(false))
|
|
181
|
+
child.once('close', (code) => resolve(code === 0))
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function resolveRipgrepExecutable() {
|
|
186
|
+
if (cachedRipgrepExecutable !== undefined) return cachedRipgrepExecutable
|
|
187
|
+
|
|
188
|
+
const bundled = ripgrepCandidatePath()
|
|
189
|
+
if (bundled && await verifyRipgrepExecutable(bundled)) {
|
|
190
|
+
cachedRipgrepExecutable = { command: bundled, source: 'bundled' }
|
|
191
|
+
return cachedRipgrepExecutable
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (await verifyRipgrepExecutable('rg')) {
|
|
195
|
+
cachedRipgrepExecutable = { command: 'rg', source: 'system' }
|
|
196
|
+
return cachedRipgrepExecutable
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cachedRipgrepExecutable = null
|
|
200
|
+
return cachedRipgrepExecutable
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildRipgrepArgs(options, context) {
|
|
204
|
+
const args = [
|
|
205
|
+
'--line-number',
|
|
206
|
+
'--color=never',
|
|
207
|
+
'--max-filesize',
|
|
208
|
+
RIPGREP_MAX_FILESIZE,
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if (options.filesWithMatches) {
|
|
212
|
+
args.push('--files-with-matches')
|
|
213
|
+
} else {
|
|
214
|
+
args.push('--json')
|
|
215
|
+
}
|
|
216
|
+
if (!options.regex) args.push('--fixed-strings')
|
|
217
|
+
if (!options.caseSensitive) args.push('--ignore-case')
|
|
218
|
+
if (!options.respectGitIgnore) args.push('--hidden', '--no-ignore')
|
|
219
|
+
if (options.context > 0) args.push('-C', String(options.context))
|
|
220
|
+
if (options.beforeContext > 0) args.push('-B', String(options.beforeContext))
|
|
221
|
+
if (options.afterContext > 0) args.push('-A', String(options.afterContext))
|
|
222
|
+
|
|
223
|
+
for (const pattern of options.glob) args.push('--glob', pattern)
|
|
224
|
+
for (const pattern of DEFAULT_EXCLUDE_GLOBS) args.push('--glob', pattern)
|
|
225
|
+
for (const pattern of SENSITIVE_EXCLUDE_GLOBS) args.push('--glob', pattern)
|
|
226
|
+
|
|
227
|
+
args.push('--', options.query, toWorkspaceRelative(options.root, context) || '.')
|
|
228
|
+
return args
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function cleanRipgrepLine(value) {
|
|
232
|
+
return String(value || '').replace(/[\r\n]+$/, '')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function ripgrepRelativePath(value) {
|
|
236
|
+
return String(value || '').replace(/\\/g, '/')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatCommandArg(value) {
|
|
240
|
+
const text = String(value ?? '')
|
|
241
|
+
return /^[A-Za-z0-9_@%+=:,./\\-]+$/.test(text) ? text : JSON.stringify(text)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function formatSpawnCommand(command, args) {
|
|
245
|
+
return [command, ...args].map(formatCommandArg).join(' ')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function grepSearchOptionsDetails(options) {
|
|
249
|
+
return {
|
|
250
|
+
regex: options.regex,
|
|
251
|
+
caseSensitive: options.caseSensitive,
|
|
252
|
+
glob: options.glob,
|
|
253
|
+
context: options.context,
|
|
254
|
+
beforeContext: options.beforeContext,
|
|
255
|
+
afterContext: options.afterContext,
|
|
256
|
+
filesWithMatches: options.filesWithMatches,
|
|
257
|
+
respectGitIgnore: options.respectGitIgnore,
|
|
258
|
+
maxFileSize: RIPGREP_MAX_FILESIZE,
|
|
259
|
+
defaultExcludeGlobs: DEFAULT_EXCLUDE_GLOBS,
|
|
260
|
+
sensitiveExcludeGlobs: SENSITIVE_EXCLUDE_GLOBS,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatRipgrepJsonEvent(event) {
|
|
265
|
+
if (event?.type !== 'match' && event?.type !== 'context') return null
|
|
266
|
+
const data = event.data || {}
|
|
267
|
+
const file = ripgrepRelativePath(data.path?.text)
|
|
268
|
+
const lineNumber = data.line_number
|
|
269
|
+
const line = cleanRipgrepLine(data.lines?.text)
|
|
270
|
+
if (!file || !lineNumber) return null
|
|
271
|
+
const separator = event.type === 'match' ? ':' : '-'
|
|
272
|
+
return `${file}:${lineNumber}${separator} ${line}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fallbackDetails(extra = {}) {
|
|
276
|
+
return Object.fromEntries(Object.entries(extra).filter(([, value]) => value !== undefined && value !== null && value !== ''))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function grepFilesWithRipgrep(executable, options, context, runtime = {}) {
|
|
280
|
+
if (options.regex && !isRegexLikelyRipgrepCompatible(options.query)) {
|
|
281
|
+
throw new Error('regex uses JavaScript-only features that ripgrep does not support')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const cwd = getToolWorkspaceRoot(context)
|
|
285
|
+
const args = buildRipgrepArgs(options, context)
|
|
286
|
+
const matches = []
|
|
287
|
+
let stderr = ''
|
|
288
|
+
let buffer = ''
|
|
289
|
+
let killedForLimit = false
|
|
290
|
+
let settled = false
|
|
291
|
+
|
|
292
|
+
await new Promise((resolve, reject) => {
|
|
293
|
+
if (runtime.signal?.aborted) {
|
|
294
|
+
reject(new Error('Search aborted'))
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const child = spawn(executable.command, args, {
|
|
299
|
+
cwd,
|
|
300
|
+
shell: false,
|
|
301
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
302
|
+
windowsHide: true,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const cleanup = () => {
|
|
306
|
+
clearTimeout(timer)
|
|
307
|
+
runtime.signal?.removeEventListener?.('abort', onAbort)
|
|
308
|
+
}
|
|
309
|
+
const finish = (error) => {
|
|
310
|
+
if (settled) return
|
|
311
|
+
settled = true
|
|
312
|
+
cleanup()
|
|
313
|
+
if (error) reject(error)
|
|
314
|
+
else resolve()
|
|
315
|
+
}
|
|
316
|
+
const stopForLimit = () => {
|
|
317
|
+
if (killedForLimit) return
|
|
318
|
+
killedForLimit = true
|
|
319
|
+
killProcessTree(child, 'SIGTERM')
|
|
320
|
+
}
|
|
321
|
+
const processLine = (line) => {
|
|
322
|
+
if (!line || matches.length >= options.limit) return
|
|
323
|
+
if (options.filesWithMatches) {
|
|
324
|
+
matches.push(ripgrepRelativePath(line))
|
|
325
|
+
} else {
|
|
326
|
+
try {
|
|
327
|
+
const formatted = formatRipgrepJsonEvent(JSON.parse(line))
|
|
328
|
+
if (formatted) matches.push(formatted)
|
|
329
|
+
} catch {
|
|
330
|
+
// Ignore malformed partial output and let process exit handling decide fallback.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (matches.length >= options.limit) stopForLimit()
|
|
334
|
+
}
|
|
335
|
+
const flushLines = (chunk) => {
|
|
336
|
+
buffer += chunk.toString()
|
|
337
|
+
const lines = buffer.split(/\r?\n/)
|
|
338
|
+
buffer = lines.pop() || ''
|
|
339
|
+
for (const line of lines) processLine(line)
|
|
340
|
+
}
|
|
341
|
+
function onAbort() {
|
|
342
|
+
killProcessTree(child, 'SIGTERM')
|
|
343
|
+
finish(new Error('Search aborted'))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const timer = setTimeout(() => {
|
|
347
|
+
killProcessTree(child, 'SIGTERM')
|
|
348
|
+
finish(new Error('ripgrep search timed out'))
|
|
349
|
+
}, RIPGREP_TIMEOUT_MS)
|
|
350
|
+
|
|
351
|
+
runtime.signal?.addEventListener?.('abort', onAbort, { once: true })
|
|
352
|
+
child.stdout.on('data', flushLines)
|
|
353
|
+
child.stderr.on('data', (chunk) => {
|
|
354
|
+
stderr = truncateText(stderr + chunk.toString(), 2000)
|
|
355
|
+
})
|
|
356
|
+
child.once('error', finish)
|
|
357
|
+
child.once('close', (code) => {
|
|
358
|
+
if (buffer) processLine(buffer)
|
|
359
|
+
if (killedForLimit || code === 0 || code === 1) {
|
|
360
|
+
finish()
|
|
361
|
+
} else {
|
|
362
|
+
finish(new Error(stderr.trim() || `ripgrep exited with code ${code}`))
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
content: matches.length ? truncateText(matches.slice(0, options.limit).join('\n')) : 'No matches found.',
|
|
369
|
+
details: {
|
|
370
|
+
path: toWorkspaceRelative(options.root, context),
|
|
371
|
+
project: context?.project,
|
|
372
|
+
query: options.query,
|
|
373
|
+
count: matches.length,
|
|
374
|
+
limit: options.limit,
|
|
375
|
+
backend: 'ripgrep',
|
|
376
|
+
ripgrepSource: executable.source,
|
|
377
|
+
cwd,
|
|
378
|
+
command: formatSpawnCommand(executable.command, args),
|
|
379
|
+
executable: executable.command,
|
|
380
|
+
args,
|
|
381
|
+
searchOptions: grepSearchOptionsDetails(options),
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function grepFilesWithNode(options, context, extraDetails = {}) {
|
|
387
|
+
const flags = options.caseSensitive ? 'g' : 'gi'
|
|
388
|
+
const matcher = options.regex
|
|
389
|
+
? new RegExp(options.query, flags)
|
|
390
|
+
: new RegExp(escapeRegExp(options.query), flags)
|
|
391
|
+
|
|
392
|
+
const files = await walkFiles(options.root, [], context)
|
|
393
|
+
const matches = []
|
|
394
|
+
|
|
395
|
+
// Stat and filter files in parallel, then grep in parallel batches.
|
|
396
|
+
const candidateResults = await poolMap(files, async (file) => {
|
|
397
|
+
try {
|
|
398
|
+
const stat = await fs.stat(file)
|
|
399
|
+
if (stat.size > 1024 * 1024) return { file, skip: true }
|
|
400
|
+
return { file, skip: false }
|
|
401
|
+
} catch {
|
|
402
|
+
return { file, skip: true }
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const candidates = candidateResults.filter((r) => !r.skip).map((r) => r.file)
|
|
407
|
+
|
|
408
|
+
// Grep with bounded concurrency — short-circuit when limit reached.
|
|
409
|
+
let matchCount = 0
|
|
410
|
+
const filesWithMatches = new Set()
|
|
411
|
+
for (let batchStart = 0; batchStart < candidates.length && matchCount < options.limit; batchStart += 20) {
|
|
412
|
+
const batch = candidates.slice(batchStart, batchStart + 20)
|
|
413
|
+
const batchMatches = await Promise.all(
|
|
414
|
+
batch.map(async (file) => {
|
|
415
|
+
if (matchCount >= options.limit) return []
|
|
416
|
+
try {
|
|
417
|
+
const text = await fs.readFile(file, 'utf8')
|
|
418
|
+
const lines = splitLines(text)
|
|
419
|
+
const fileMatches = []
|
|
420
|
+
for (let index = 0; index < lines.length && (matchCount + fileMatches.length) < options.limit; index++) {
|
|
421
|
+
matcher.lastIndex = 0
|
|
422
|
+
if (matcher.test(lines[index])) {
|
|
423
|
+
const relative = toWorkspaceRelative(file, context)
|
|
424
|
+
if (options.filesWithMatches) {
|
|
425
|
+
if (!filesWithMatches.has(relative)) {
|
|
426
|
+
filesWithMatches.add(relative)
|
|
427
|
+
fileMatches.push(relative)
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
fileMatches.push(`${relative}:${index + 1}: ${lines[index]}`)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return fileMatches
|
|
435
|
+
} catch {
|
|
436
|
+
return []
|
|
437
|
+
}
|
|
438
|
+
}),
|
|
439
|
+
)
|
|
440
|
+
for (const fm of batchMatches) {
|
|
441
|
+
if (matchCount >= options.limit) break
|
|
442
|
+
for (const m of fm) {
|
|
443
|
+
if (matchCount >= options.limit) break
|
|
444
|
+
matches.push(m)
|
|
445
|
+
matchCount++
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
content: matches.length ? truncateText(matches.join('\n')) : 'No matches found.',
|
|
452
|
+
details: {
|
|
453
|
+
path: toWorkspaceRelative(options.root, context),
|
|
454
|
+
project: context?.project,
|
|
455
|
+
query: options.query,
|
|
456
|
+
count: matches.length,
|
|
457
|
+
limit: options.limit,
|
|
458
|
+
backend: 'node',
|
|
459
|
+
searchOptions: grepSearchOptionsDetails(options),
|
|
460
|
+
...fallbackDetails(extraDetails),
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function toolGrepFiles(params, context, runtime = {}) {
|
|
466
|
+
const options = normalizeGrepParams(params, context)
|
|
467
|
+
await assertSafeWorkspacePath(options.root, context)
|
|
468
|
+
|
|
469
|
+
const executable = await resolveRipgrepExecutable()
|
|
470
|
+
if (executable) {
|
|
471
|
+
try {
|
|
472
|
+
return await grepFilesWithRipgrep(executable, options, context, runtime)
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (runtime.signal?.aborted) throw error
|
|
475
|
+
return grepFilesWithNode(options, context, {
|
|
476
|
+
fallbackFrom: 'ripgrep',
|
|
477
|
+
fallbackReason: error?.message || 'ripgrep unavailable',
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return grepFilesWithNode(options, context, { fallbackReason: 'ripgrep unavailable' })
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// --- write_file ---
|
|
486
|
+
export async function toolWriteFile(params, context) {
|
|
487
|
+
const file = resolveWorkspacePath(params?.path, context)
|
|
488
|
+
await assertSafeWorkspacePath(file, context, { forWrite: true })
|
|
489
|
+
|
|
490
|
+
const content = String(params?.content ?? '')
|
|
491
|
+
const relativePath = toWorkspaceRelative(file, context)
|
|
492
|
+
let oldText = ''
|
|
493
|
+
let existed = true
|
|
494
|
+
try {
|
|
495
|
+
oldText = await fs.readFile(file, 'utf8')
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error?.code !== 'ENOENT') throw error
|
|
498
|
+
existed = false
|
|
499
|
+
}
|
|
500
|
+
const diff = createTextDiff(oldText, content, relativePath, { oldExists: existed })
|
|
501
|
+
|
|
502
|
+
await fs.mkdir(path.dirname(file), { recursive: true })
|
|
503
|
+
await fs.writeFile(file, content, 'utf8')
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
content: `${existed ? 'Wrote' : 'Created'} ${relativePath} (+${diff.addedLines} -${diff.removedLines})`,
|
|
507
|
+
details: { path: relativePath, project: context?.project, bytes: Buffer.byteLength(content, 'utf8'), created: !existed, diff },
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// --- edit_file ---
|
|
512
|
+
function countOccurrences(text, needle) {
|
|
513
|
+
if (!needle) return 0
|
|
514
|
+
let count = 0
|
|
515
|
+
let index = 0
|
|
516
|
+
while ((index = text.indexOf(needle, index)) !== -1) {
|
|
517
|
+
count++
|
|
518
|
+
index += needle.length
|
|
519
|
+
}
|
|
520
|
+
return count
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function detectLineEnding(text) {
|
|
524
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function normalizeLineEndings(text) {
|
|
528
|
+
return text.replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function convertToLineEnding(text, ending) {
|
|
532
|
+
const normalized = normalizeLineEndings(text)
|
|
533
|
+
return ending === '\r\n' ? normalized.replaceAll('\n', '\r\n') : normalized
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function toolEditFile(params, context) {
|
|
537
|
+
const file = resolveWorkspacePath(params?.path, context)
|
|
538
|
+
await assertSafeWorkspacePath(file, context)
|
|
539
|
+
|
|
540
|
+
const rawOldText = String(params?.oldText ?? '')
|
|
541
|
+
const rawNewText = String(params?.newText ?? '')
|
|
542
|
+
const text = await fs.readFile(file, 'utf8')
|
|
543
|
+
const lineEnding = detectLineEnding(text)
|
|
544
|
+
const oldText = convertToLineEnding(rawOldText, lineEnding)
|
|
545
|
+
const newText = convertToLineEnding(rawNewText, lineEnding)
|
|
546
|
+
const count = countOccurrences(text, oldText)
|
|
547
|
+
|
|
548
|
+
if (count !== 1) {
|
|
549
|
+
const rawCount = oldText === rawOldText ? count : countOccurrences(text, rawOldText)
|
|
550
|
+
const suffix = rawCount !== count ? ` after normalizing line endings; raw match count was ${rawCount}` : ''
|
|
551
|
+
const error = new Error(`oldText must match exactly once; found ${count} matches${suffix}`)
|
|
552
|
+
error.statusCode = 400
|
|
553
|
+
throw error
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const nextText = text.replace(oldText, newText)
|
|
557
|
+
const relativePath = toWorkspaceRelative(file, context)
|
|
558
|
+
const diff = createTextDiff(text, nextText, relativePath)
|
|
559
|
+
|
|
560
|
+
await fs.writeFile(file, nextText, 'utf8')
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
content: `Edited ${relativePath} (+${diff.addedLines} -${diff.removedLines})`,
|
|
564
|
+
details: { path: relativePath, project: context?.project, replaced: count, diff },
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// --- run_command ---
|
|
569
|
+
function activeSkillsForContext(context) {
|
|
570
|
+
return mergeSkills(context?.globalSkills, context?.projectSkills)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function activeSkillByName(context, name) {
|
|
574
|
+
const skillName = String(name || '')
|
|
575
|
+
return activeSkillsForContext(context).find((skill) => skill.name === skillName)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export async function loadSkillToolContext(config = {}) {
|
|
579
|
+
const globalSkills = await loadSelectedGlobalSkills(config.globalSkillNames)
|
|
580
|
+
const projectSkills = config.workspaceRoot
|
|
581
|
+
? await loadSelectedProjectSkills(config.projectSkillNames, config.workspaceRoot)
|
|
582
|
+
: []
|
|
583
|
+
return { globalSkills, projectSkills }
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// --- activate_skill ---
|
|
587
|
+
export async function toolActivateSkill(params, context) {
|
|
588
|
+
const skill = activeSkillByName(context, params?.name)
|
|
589
|
+
if (!skill) {
|
|
590
|
+
const error = new Error(`Unknown or disabled skill: ${params?.name || ''}`)
|
|
591
|
+
error.statusCode = 404
|
|
592
|
+
throw error
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
content: truncateText(await formatSkillActivation(skill)),
|
|
597
|
+
details: {
|
|
598
|
+
skill: skill.name,
|
|
599
|
+
source: skill.source,
|
|
600
|
+
directory: skill.rootDir,
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// --- read_skill_resource ---
|
|
606
|
+
export async function toolReadSkillResource(params, context) {
|
|
607
|
+
const skill = activeSkillByName(context, params?.skill)
|
|
608
|
+
if (!skill) {
|
|
609
|
+
const error = new Error(`Unknown or disabled skill: ${params?.skill || ''}`)
|
|
610
|
+
error.statusCode = 404
|
|
611
|
+
throw error
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const result = await readSkillResource(skill, params?.path, params)
|
|
615
|
+
return {
|
|
616
|
+
content: truncateText(result.content),
|
|
617
|
+
details: result.details,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// --- run_command ---
|
|
622
|
+
const DEFAULT_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
623
|
+
const MIN_RUN_COMMAND_TIMEOUT_MS = 1000
|
|
624
|
+
const MAX_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
625
|
+
const COMMAND_PREVIEW_LINES = 200
|
|
626
|
+
const COMMAND_PREVIEW_TOTAL_CHARS = 10000
|
|
627
|
+
|
|
628
|
+
function formatDurationMs(durationMs = 0) {
|
|
629
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000))
|
|
630
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
631
|
+
const seconds = totalSeconds % 60
|
|
632
|
+
if (minutes > 0) return `${minutes}m ${seconds}s (${durationMs}ms)`
|
|
633
|
+
return `${seconds}s (${durationMs}ms)`
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function tailText(current, chunk, maxLines = COMMAND_PREVIEW_LINES, maxChars = COMMAND_PREVIEW_TOTAL_CHARS) {
|
|
637
|
+
const lines = (current + chunk).split(/\r?\n/)
|
|
638
|
+
const truncatedByLines = lines.length > maxLines
|
|
639
|
+
const lineLimitedText = truncatedByLines ? lines.slice(lines.length - maxLines).join('\n') : lines.join('\n')
|
|
640
|
+
const charLimitedText = tailChars(lineLimitedText, maxChars)
|
|
641
|
+
return {
|
|
642
|
+
text: charLimitedText.text,
|
|
643
|
+
truncated: truncatedByLines || charLimitedText.truncated,
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function tailChars(text, maxChars) {
|
|
648
|
+
if (text.length <= maxChars) return { text, truncated: false }
|
|
649
|
+
return { text: text.slice(text.length - maxChars), truncated: true }
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function balanceCommandPreviews(stdout, stderr, maxChars = COMMAND_PREVIEW_TOTAL_CHARS) {
|
|
653
|
+
if (stdout.length + stderr.length <= maxChars) {
|
|
654
|
+
return { stdout, stderr, stdoutTruncated: false, stderrTruncated: false }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const half = Math.floor(maxChars / 2)
|
|
658
|
+
let stdoutBudget = Math.min(stdout.length, half)
|
|
659
|
+
let stderrBudget = Math.min(stderr.length, half)
|
|
660
|
+
const remaining = maxChars - stdoutBudget - stderrBudget
|
|
661
|
+
|
|
662
|
+
if (remaining > 0) {
|
|
663
|
+
const stdoutNeed = Math.max(0, stdout.length - stdoutBudget)
|
|
664
|
+
const stderrNeed = Math.max(0, stderr.length - stderrBudget)
|
|
665
|
+
if (stderrNeed >= stdoutNeed) stderrBudget += Math.min(remaining, stderrNeed)
|
|
666
|
+
else stdoutBudget += Math.min(remaining, stdoutNeed)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const nextStdout = tailChars(stdout, stdoutBudget)
|
|
670
|
+
const nextStderr = tailChars(stderr, stderrBudget)
|
|
671
|
+
return {
|
|
672
|
+
stdout: nextStdout.text,
|
|
673
|
+
stderr: nextStderr.text,
|
|
674
|
+
stdoutTruncated: nextStdout.truncated,
|
|
675
|
+
stderrTruncated: nextStderr.truncated,
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function tailLabel(name, truncated) {
|
|
680
|
+
return truncated ? `${name} preview (last ${COMMAND_PREVIEW_LINES} lines, shared ${COMMAND_PREVIEW_TOTAL_CHARS} chars):` : `${name} preview:`
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function commandStatus(meta = {}) {
|
|
684
|
+
if (meta.running) return 'Status: running'
|
|
685
|
+
const flags = [
|
|
686
|
+
meta.timedOut ? 'timed out' : null,
|
|
687
|
+
meta.aborted ? 'aborted' : null,
|
|
688
|
+
].filter(Boolean)
|
|
689
|
+
const suffix = flags.length ? ` (${flags.join(', ')})` : ''
|
|
690
|
+
return `Exit code: ${meta.code ?? 'unknown'}${meta.signal ? `, signal: ${meta.signal}` : ''}${suffix}`
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function formatCommandOutput(command, stdout, stderr, meta = {}) {
|
|
694
|
+
const lines = [
|
|
695
|
+
`Command: ${command}`,
|
|
696
|
+
]
|
|
697
|
+
if (meta.description) lines.push(`Description: ${meta.description}`)
|
|
698
|
+
lines.push(commandStatus(meta))
|
|
699
|
+
if (typeof meta.durationMs === 'number') lines.push(`Duration: ${formatDurationMs(meta.durationMs)}`)
|
|
700
|
+
if (typeof meta.timeoutMs === 'number') lines.push(`Timeout: ${formatDurationMs(meta.timeoutMs)}`)
|
|
701
|
+
if (meta.cwd) lines.push(`CWD: ${meta.cwd}`)
|
|
702
|
+
if (meta.outputFile) lines.push(`Full output: ${meta.outputFile}`)
|
|
703
|
+
if (meta.truncated) lines.push(`Output mode: showing stdout/stderr previews; each stream is limited to the last ${COMMAND_PREVIEW_LINES} lines and both streams share ${COMMAND_PREVIEW_TOTAL_CHARS} characters. Full output is saved to the log file.`)
|
|
704
|
+
if (meta.logError) lines.push(`Log warning: ${meta.logError}`)
|
|
705
|
+
lines.push('', tailLabel('STDOUT', meta.stdoutTruncated), stdout || '(empty)', '', tailLabel('STDERR', meta.stderrTruncated), stderr || '(empty)')
|
|
706
|
+
return lines.join('\n')
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function safeLogFilePart(value, fallback) {
|
|
710
|
+
const text = String(value || '').replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 80)
|
|
711
|
+
return text || fallback
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function createCommandLogStream(command, { cwd, description, toolCallId } = {}) {
|
|
715
|
+
const dir = path.join(logsDir, 'commands')
|
|
716
|
+
await fs.mkdir(dir, { recursive: true })
|
|
717
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
718
|
+
const fileName = `${timestamp}_${safeLogFilePart(toolCallId, 'command')}.log`
|
|
719
|
+
const outputFile = path.join(dir, fileName)
|
|
720
|
+
const stream = createWriteStream(outputFile, { flags: 'wx' })
|
|
721
|
+
stream.write([
|
|
722
|
+
`Command: ${command}`,
|
|
723
|
+
description ? `Description: ${description}` : null,
|
|
724
|
+
`CWD: ${cwd}`,
|
|
725
|
+
`Started at: ${new Date().toISOString()}`,
|
|
726
|
+
'',
|
|
727
|
+
].filter(Boolean).join('\n'))
|
|
728
|
+
return { stream, outputFile }
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function writeCommandLog(stream, source, chunk) {
|
|
732
|
+
stream.write(`\n[${source} ${new Date().toISOString()}]\n`)
|
|
733
|
+
stream.write(chunk)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function killProcessTree(child, signal = 'SIGTERM') {
|
|
737
|
+
if (!child?.pid) return
|
|
738
|
+
|
|
739
|
+
if (process.platform === 'win32') {
|
|
740
|
+
const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
741
|
+
stdio: 'ignore',
|
|
742
|
+
windowsHide: true,
|
|
743
|
+
})
|
|
744
|
+
killer.on('error', () => {
|
|
745
|
+
try { child.kill(signal) } catch { /* ignore */ }
|
|
746
|
+
})
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
process.kill(-child.pid, signal)
|
|
752
|
+
} catch {
|
|
753
|
+
try { child.kill(signal) } catch { /* ignore */ }
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const runningCommands = new Map()
|
|
758
|
+
|
|
759
|
+
export function abortRunningCommand(toolCallId) {
|
|
760
|
+
if (!toolCallId) return false
|
|
761
|
+
const stop = runningCommands.get(toolCallId)
|
|
762
|
+
if (!stop) return false
|
|
763
|
+
stop('abort')
|
|
764
|
+
return true
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export async function toolRunCommand(params, context, runtime = {}) {
|
|
768
|
+
const command = String(params?.command || '')
|
|
769
|
+
if (!command.trim()) {
|
|
770
|
+
const error = new Error('command is required')
|
|
771
|
+
error.statusCode = 400
|
|
772
|
+
throw error
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const description = String(params?.description || '').trim().slice(0, 500)
|
|
776
|
+
const timeoutMs = clampNumber(params?.timeoutMs, DEFAULT_RUN_COMMAND_TIMEOUT_MS, MIN_RUN_COMMAND_TIMEOUT_MS, MAX_RUN_COMMAND_TIMEOUT_MS)
|
|
777
|
+
const cwd = getToolWorkspaceRoot(context)
|
|
778
|
+
const startedAt = Date.now()
|
|
779
|
+
|
|
780
|
+
if (runtime.signal?.aborted) {
|
|
781
|
+
const details = {
|
|
782
|
+
command,
|
|
783
|
+
description,
|
|
784
|
+
project: context?.project,
|
|
785
|
+
cwd,
|
|
786
|
+
timeoutMs,
|
|
787
|
+
outputFile: null,
|
|
788
|
+
stdout: '',
|
|
789
|
+
stderr: 'Command aborted before start.',
|
|
790
|
+
stdout_preview: '',
|
|
791
|
+
stderr_preview: 'Command aborted before start.',
|
|
792
|
+
stdoutTruncated: false,
|
|
793
|
+
stderrTruncated: false,
|
|
794
|
+
stdout_truncated: false,
|
|
795
|
+
stderr_truncated: false,
|
|
796
|
+
outputTruncated: false,
|
|
797
|
+
truncated: false,
|
|
798
|
+
previewLineLimit: COMMAND_PREVIEW_LINES,
|
|
799
|
+
previewCharLimit: COMMAND_PREVIEW_TOTAL_CHARS,
|
|
800
|
+
previewMode: 'tail',
|
|
801
|
+
durationMs: 0,
|
|
802
|
+
aborted: true,
|
|
803
|
+
}
|
|
804
|
+
const content = formatCommandOutput(command, details.stdout, details.stderr, details)
|
|
805
|
+
return { content: truncateText(content), details }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let logStream = null
|
|
809
|
+
let outputFile = null
|
|
810
|
+
let logError = null
|
|
811
|
+
try {
|
|
812
|
+
const log = await createCommandLogStream(command, { cwd, description, toolCallId: runtime.toolCallId })
|
|
813
|
+
logStream = log.stream
|
|
814
|
+
outputFile = log.outputFile
|
|
815
|
+
logStream.on('error', (error) => {
|
|
816
|
+
logError = error?.message || 'Failed to write command log.'
|
|
817
|
+
})
|
|
818
|
+
} catch (error) {
|
|
819
|
+
logError = error?.message || 'Failed to create command log.'
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return new Promise((resolve) => {
|
|
823
|
+
const child = spawn(command, {
|
|
824
|
+
cwd,
|
|
825
|
+
shell: true,
|
|
826
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
827
|
+
windowsHide: true,
|
|
828
|
+
detached: process.platform !== 'win32',
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
let stdout = ''
|
|
832
|
+
let stderr = ''
|
|
833
|
+
let stdoutTruncated = false
|
|
834
|
+
let stderrTruncated = false
|
|
835
|
+
let timedOut = false
|
|
836
|
+
let aborted = false
|
|
837
|
+
let settled = false
|
|
838
|
+
let updateTimer = null
|
|
839
|
+
let updatePending = false
|
|
840
|
+
let forceKillTimer = null
|
|
841
|
+
|
|
842
|
+
const cleanup = () => {
|
|
843
|
+
clearTimeout(timer)
|
|
844
|
+
if (forceKillTimer) clearTimeout(forceKillTimer)
|
|
845
|
+
if (updateTimer) clearTimeout(updateTimer)
|
|
846
|
+
if (runtime.toolCallId) runningCommands.delete(runtime.toolCallId)
|
|
847
|
+
runtime.signal?.removeEventListener?.('abort', onAbort)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const commonDetails = (extra = {}) => {
|
|
851
|
+
const now = Date.now()
|
|
852
|
+
const previews = balanceCommandPreviews(stdout, stderr)
|
|
853
|
+
const stdoutPreview = previews.stdout
|
|
854
|
+
const stderrPreview = previews.stderr
|
|
855
|
+
const stdoutPreviewTruncated = stdoutTruncated || previews.stdoutTruncated
|
|
856
|
+
const stderrPreviewTruncated = stderrTruncated || previews.stderrTruncated
|
|
857
|
+
const truncated = stdoutPreviewTruncated || stderrPreviewTruncated
|
|
858
|
+
return {
|
|
859
|
+
command,
|
|
860
|
+
description,
|
|
861
|
+
project: context?.project,
|
|
862
|
+
cwd,
|
|
863
|
+
timeoutMs,
|
|
864
|
+
outputFile,
|
|
865
|
+
stdout: stdoutPreview,
|
|
866
|
+
stderr: stderrPreview,
|
|
867
|
+
stdout_preview: stdoutPreview,
|
|
868
|
+
stderr_preview: stderrPreview,
|
|
869
|
+
stdoutTruncated: stdoutPreviewTruncated,
|
|
870
|
+
stderrTruncated: stderrPreviewTruncated,
|
|
871
|
+
stdout_truncated: stdoutPreviewTruncated,
|
|
872
|
+
stderr_truncated: stderrPreviewTruncated,
|
|
873
|
+
outputTruncated: truncated,
|
|
874
|
+
truncated,
|
|
875
|
+
previewLineLimit: COMMAND_PREVIEW_LINES,
|
|
876
|
+
previewCharLimit: COMMAND_PREVIEW_TOTAL_CHARS,
|
|
877
|
+
previewMode: 'tail',
|
|
878
|
+
durationMs: now - startedAt,
|
|
879
|
+
toolCallId: runtime.toolCallId,
|
|
880
|
+
logError,
|
|
881
|
+
...extra,
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const resolveAfterLogClose = (result) => {
|
|
886
|
+
if (!logStream) {
|
|
887
|
+
resolve(result)
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
const details = result.details || {}
|
|
891
|
+
logStream.write([
|
|
892
|
+
'',
|
|
893
|
+
'',
|
|
894
|
+
`[quickforge ${new Date().toISOString()}]`,
|
|
895
|
+
`Exit code: ${details.code ?? 'unknown'}${details.signal ? `, signal: ${details.signal}` : ''}`,
|
|
896
|
+
`Duration: ${formatDurationMs(details.durationMs)}`,
|
|
897
|
+
`Timed out: ${Boolean(details.timedOut)}`,
|
|
898
|
+
`Aborted: ${Boolean(details.aborted)}`,
|
|
899
|
+
'Command finished.',
|
|
900
|
+
'',
|
|
901
|
+
].join('\n'))
|
|
902
|
+
logStream.end(() => resolve(result))
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const finish = ({ code = null, signal = null, error = null } = {}) => {
|
|
906
|
+
if (settled) return
|
|
907
|
+
flushUpdate()
|
|
908
|
+
settled = true
|
|
909
|
+
cleanup()
|
|
910
|
+
const durationMs = Date.now() - startedAt
|
|
911
|
+
if (error) {
|
|
912
|
+
const details = commonDetails({ error: error.message, aborted, timedOut, durationMs })
|
|
913
|
+
resolveAfterLogClose({
|
|
914
|
+
isError: true,
|
|
915
|
+
content: truncateText(formatCommandOutput(command, details.stdout, `Error running command: ${error.message}\n${details.stderr}`.trim(), details)),
|
|
916
|
+
details,
|
|
917
|
+
})
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
const details = commonDetails({ code, signal, timedOut, aborted, durationMs })
|
|
921
|
+
const content = formatCommandOutput(command, details.stdout, details.stderr, details)
|
|
922
|
+
resolveAfterLogClose({ content: truncateText(content), details })
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const stopChild = (reason) => {
|
|
926
|
+
if (reason === 'timeout') timedOut = true
|
|
927
|
+
if (reason === 'abort') aborted = true
|
|
928
|
+
killProcessTree(child, 'SIGTERM')
|
|
929
|
+
forceKillTimer = setTimeout(() => {
|
|
930
|
+
killProcessTree(child, 'SIGKILL')
|
|
931
|
+
}, 1500)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (runtime.toolCallId) runningCommands.set(runtime.toolCallId, stopChild)
|
|
935
|
+
|
|
936
|
+
function onAbort() {
|
|
937
|
+
stopChild('abort')
|
|
938
|
+
finish({ signal: 'SIGTERM' })
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const runningDetails = () => commonDetails({ running: true })
|
|
942
|
+
|
|
943
|
+
const emitUpdate = () => {
|
|
944
|
+
updateTimer = null
|
|
945
|
+
if (settled || !updatePending) return
|
|
946
|
+
updatePending = false
|
|
947
|
+
const details = runningDetails()
|
|
948
|
+
runtime.onUpdate?.({
|
|
949
|
+
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, details.stdout, details.stderr, details)) }],
|
|
950
|
+
details,
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
const flushUpdate = () => {
|
|
954
|
+
if (updateTimer) {
|
|
955
|
+
clearTimeout(updateTimer)
|
|
956
|
+
updateTimer = null
|
|
957
|
+
}
|
|
958
|
+
if (!updatePending) return
|
|
959
|
+
updatePending = false
|
|
960
|
+
const details = runningDetails()
|
|
961
|
+
runtime.onUpdate?.({
|
|
962
|
+
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, details.stdout, details.stderr, details)) }],
|
|
963
|
+
details,
|
|
964
|
+
})
|
|
965
|
+
}
|
|
966
|
+
const scheduleUpdate = () => {
|
|
967
|
+
if (settled) return
|
|
968
|
+
updatePending = true
|
|
969
|
+
if (!updateTimer) updateTimer = setTimeout(emitUpdate, 150)
|
|
970
|
+
}
|
|
971
|
+
const timer = setTimeout(() => {
|
|
972
|
+
stopChild('timeout')
|
|
973
|
+
finish({ signal: 'SIGTERM' })
|
|
974
|
+
}, timeoutMs)
|
|
975
|
+
|
|
976
|
+
runtime.signal?.addEventListener?.('abort', onAbort, { once: true })
|
|
977
|
+
|
|
978
|
+
child.stdout.on('data', (chunk) => {
|
|
979
|
+
if (settled) return
|
|
980
|
+
const text = chunk.toString()
|
|
981
|
+
const nextStdout = tailText(stdout, text)
|
|
982
|
+
stdoutTruncated = stdoutTruncated || nextStdout.truncated
|
|
983
|
+
stdout = nextStdout.text
|
|
984
|
+
if (logStream && !logError) writeCommandLog(logStream, 'stdout', text)
|
|
985
|
+
scheduleUpdate()
|
|
986
|
+
})
|
|
987
|
+
child.stderr.on('data', (chunk) => {
|
|
988
|
+
if (settled) return
|
|
989
|
+
const text = chunk.toString()
|
|
990
|
+
const nextStderr = tailText(stderr, text)
|
|
991
|
+
stderrTruncated = stderrTruncated || nextStderr.truncated
|
|
992
|
+
stderr = nextStderr.text
|
|
993
|
+
if (logStream && !logError) writeCommandLog(logStream, 'stderr', text)
|
|
994
|
+
scheduleUpdate()
|
|
995
|
+
})
|
|
996
|
+
child.on('close', (code, signal) => {
|
|
997
|
+
finish({ code, signal })
|
|
998
|
+
})
|
|
999
|
+
child.on('error', (err) => {
|
|
1000
|
+
finish({ error: err })
|
|
1001
|
+
})
|
|
1002
|
+
})
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export const toolHandlers = {
|
|
1006
|
+
read_file: toolReadFile,
|
|
1007
|
+
grep_files: toolGrepFiles,
|
|
1008
|
+
write_file: toolWriteFile,
|
|
1009
|
+
edit_file: toolEditFile,
|
|
1010
|
+
run_command: toolRunCommand,
|
|
1011
|
+
activate_skill: toolActivateSkill,
|
|
1012
|
+
read_skill_resource: toolReadSkillResource,
|
|
1013
|
+
}
|