novacode 0.5.5 → 0.7.0
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 +16 -10
- package/dist/app-CbJSUNmf.mjs +22 -0
- package/dist/app-CbJSUNmf.mjs.map +1 -0
- package/dist/main.mjs +51 -61
- package/dist/main.mjs.map +1 -1
- package/package.json +3 -5
- package/dist/app-QfQR2FN9.mjs +0 -21
- package/dist/app-QfQR2FN9.mjs.map +0 -1
- package/src/agent/agent.ts +0 -87
- package/src/agent/loop.ts +0 -237
- package/src/agent/prompt.ts +0 -50
- package/src/commands/compact.ts +0 -28
- package/src/commands/index.ts +0 -86
- package/src/commands/models.ts +0 -85
- package/src/commands/providers.ts +0 -213
- package/src/commands/session.ts +0 -40
- package/src/config/providers.ts +0 -207
- package/src/config/store.ts +0 -66
- package/src/main.ts +0 -175
- package/src/onboarding/wizard.ts +0 -54
- package/src/provider/gemini.ts +0 -261
- package/src/provider/openai.ts +0 -215
- package/src/provider/stream.ts +0 -138
- package/src/session/compact.ts +0 -126
- package/src/session/store.ts +0 -229
- package/src/tools/fs.ts +0 -189
- package/src/tools/git.ts +0 -99
- package/src/tools/index.ts +0 -33
- package/src/tools/search.ts +0 -274
- package/src/tools/shell.ts +0 -90
- package/src/tools/web.ts +0 -239
- package/src/tui/app.tsx +0 -364
- package/src/tui/components/liveArea.tsx +0 -73
- package/src/tui/components/message.tsx +0 -113
- package/src/tui/components/statusBar.tsx +0 -58
- package/src/tui/markdown.ts +0 -62
- package/src/tui/prompts.tsx +0 -205
- package/src/types.ts +0 -248
- package/src/update.ts +0 -89
- package/src/util.ts +0 -61
package/src/tools/git.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git tools for executing safe repository operations programmatically.
|
|
3
|
-
*/
|
|
4
|
-
import { spawn } from "node:child_process"
|
|
5
|
-
import type { Tool, ToolResult } from "../types.ts"
|
|
6
|
-
import { textPart } from "../util.ts"
|
|
7
|
-
|
|
8
|
-
export function gitTool(cwd: string): Tool {
|
|
9
|
-
return {
|
|
10
|
-
def: {
|
|
11
|
-
name: "git",
|
|
12
|
-
description:
|
|
13
|
-
"Execute safe, non-interactive git commands (status, diff, log, add, commit) in the repository.",
|
|
14
|
-
parameters: {
|
|
15
|
-
type: "object",
|
|
16
|
-
properties: {
|
|
17
|
-
action: {
|
|
18
|
-
type: "string",
|
|
19
|
-
enum: ["status", "diff", "log", "add", "commit"],
|
|
20
|
-
description: "The git action to execute",
|
|
21
|
-
},
|
|
22
|
-
args: {
|
|
23
|
-
type: "array",
|
|
24
|
-
description: "Optional additional arguments or file paths for the git action",
|
|
25
|
-
items: { type: "string" },
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
required: ["action"],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
async execute(args, signal): Promise<ToolResult> {
|
|
32
|
-
const action = args.action as string
|
|
33
|
-
const extraArgs = (args.args as string[]) || []
|
|
34
|
-
|
|
35
|
-
const allowed = new Set(["status", "diff", "log", "add", "commit"])
|
|
36
|
-
if (!allowed.has(action)) {
|
|
37
|
-
return {
|
|
38
|
-
content: [textPart(`Error: Git action '${action}' is not supported.`)],
|
|
39
|
-
isError: true,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const cmd = ["git", action, ...extraArgs]
|
|
45
|
-
const proc = spawn(cmd[0]!, cmd.slice(1), {
|
|
46
|
-
cwd,
|
|
47
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
-
env: { ...process.env, PAGER: "cat" },
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
let stdout = ""
|
|
52
|
-
let stderr = ""
|
|
53
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
54
|
-
stdout += chunk.toString()
|
|
55
|
-
})
|
|
56
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
57
|
-
stderr += chunk.toString()
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const onAbort = () => {
|
|
61
|
-
proc.kill("SIGKILL")
|
|
62
|
-
proc.stdout.destroy()
|
|
63
|
-
proc.stderr.destroy()
|
|
64
|
-
}
|
|
65
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
66
|
-
|
|
67
|
-
let exitCode: number
|
|
68
|
-
try {
|
|
69
|
-
exitCode = await new Promise<number>((resolve, reject) => {
|
|
70
|
-
proc.on("error", reject)
|
|
71
|
-
proc.on("close", (code) => resolve(code ?? -1))
|
|
72
|
-
})
|
|
73
|
-
} finally {
|
|
74
|
-
signal?.removeEventListener("abort", onAbort)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Prevent context window blowout by truncating very large outputs
|
|
78
|
-
const MAX = 50_000
|
|
79
|
-
let out = ""
|
|
80
|
-
if (stdout) out += stdout.slice(0, MAX)
|
|
81
|
-
if (stderr) {
|
|
82
|
-
if (out) out += "\n"
|
|
83
|
-
out += stderr.slice(0, MAX - out.length)
|
|
84
|
-
}
|
|
85
|
-
if (out.length >= MAX) out += "\n…truncated"
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
content: [textPart(out || "(no output)")],
|
|
89
|
-
isError: exitCode !== 0,
|
|
90
|
-
}
|
|
91
|
-
} catch (e) {
|
|
92
|
-
return {
|
|
93
|
-
content: [textPart(`Error running git: ${(e as Error).message}`)],
|
|
94
|
-
isError: true,
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/tools/index.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Tool } from "../types.ts"
|
|
2
|
-
import { editTool, readTool, writeTool } from "./fs.ts"
|
|
3
|
-
import { gitTool } from "./git.ts"
|
|
4
|
-
import { globTool, grepTool, lsTool, treeTool } from "./search.ts"
|
|
5
|
-
import { bashTool } from "./shell.ts"
|
|
6
|
-
import { webFetchTool, webSearchTool } from "./web.ts"
|
|
7
|
-
|
|
8
|
-
export function getAllTools(cwd: string): Tool[] {
|
|
9
|
-
return [
|
|
10
|
-
readTool(cwd),
|
|
11
|
-
writeTool(cwd),
|
|
12
|
-
editTool(cwd),
|
|
13
|
-
bashTool(cwd),
|
|
14
|
-
globTool(cwd),
|
|
15
|
-
grepTool(cwd),
|
|
16
|
-
lsTool(cwd),
|
|
17
|
-
treeTool(cwd),
|
|
18
|
-
gitTool(cwd),
|
|
19
|
-
webSearchTool(),
|
|
20
|
-
webFetchTool(),
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getDefaultTools(cwd: string): Tool[] {
|
|
25
|
-
return [
|
|
26
|
-
readTool(cwd),
|
|
27
|
-
writeTool(cwd),
|
|
28
|
-
editTool(cwd),
|
|
29
|
-
bashTool(cwd),
|
|
30
|
-
webSearchTool(),
|
|
31
|
-
webFetchTool(),
|
|
32
|
-
]
|
|
33
|
-
}
|
package/src/tools/search.ts
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search tools for finding files and content.
|
|
3
|
-
* Uses 'rg' (ripgrep) if available, falling back to a pure JS implementation.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawn } from "node:child_process"
|
|
7
|
-
import { readdir, readFile } from "node:fs/promises"
|
|
8
|
-
import { relative, resolve } from "node:path"
|
|
9
|
-
import { glob } from "glob"
|
|
10
|
-
import type { Tool, ToolResult } from "../types.ts"
|
|
11
|
-
|
|
12
|
-
import { textPart } from "../util.ts"
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Tool for finding files by glob pattern.
|
|
16
|
-
*/
|
|
17
|
-
export function globTool(cwd: string): Tool {
|
|
18
|
-
return {
|
|
19
|
-
def: {
|
|
20
|
-
name: "glob",
|
|
21
|
-
description: "Find files by glob pattern (e.g. **/*.ts, src/**/*.test.ts).",
|
|
22
|
-
parameters: {
|
|
23
|
-
type: "object",
|
|
24
|
-
properties: {
|
|
25
|
-
pattern: { type: "string", description: "Glob pattern (e.g. **/*.ts)" },
|
|
26
|
-
path: { type: "string", description: "Directory to search in (default .)" },
|
|
27
|
-
nocase: { type: "boolean", description: "Case-insensitive search (default false)" },
|
|
28
|
-
},
|
|
29
|
-
required: ["pattern"],
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
async execute(args): Promise<ToolResult> {
|
|
33
|
-
try {
|
|
34
|
-
const rawPath = (args.path as string) || "."
|
|
35
|
-
const dir = resolve(cwd, rawPath)
|
|
36
|
-
if (dir !== cwd && !dir.startsWith(`${cwd}/`)) {
|
|
37
|
-
throw new Error(`Path outside project: ${rawPath}`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const pattern = args.pattern as string
|
|
41
|
-
const nocase = !!args.nocase
|
|
42
|
-
const files = await glob(pattern, { cwd: dir, nocase })
|
|
43
|
-
const sliced = files.slice(0, 500)
|
|
44
|
-
const relSearchPath = relative(cwd, dir)
|
|
45
|
-
const prefix = relSearchPath ? `${relSearchPath}/` : ""
|
|
46
|
-
const relFiles = sliced.map((f) => prefix + f)
|
|
47
|
-
const out = relFiles.length > 0 ? relFiles.join("\n") : "No files found"
|
|
48
|
-
return { content: [textPart(out)], isError: false }
|
|
49
|
-
} catch (e) {
|
|
50
|
-
return {
|
|
51
|
-
content: [textPart(`Error: ${(e as Error).message}`)],
|
|
52
|
-
isError: true,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Tool for searching file contents using regex.
|
|
61
|
-
*/
|
|
62
|
-
export function grepTool(cwd: string): Tool {
|
|
63
|
-
return {
|
|
64
|
-
def: {
|
|
65
|
-
name: "grep",
|
|
66
|
-
description:
|
|
67
|
-
"Search file contents with a regex pattern. Returns matching lines with file paths and line numbers.",
|
|
68
|
-
parameters: {
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
pattern: { type: "string", description: "Regex pattern to search for" },
|
|
72
|
-
path: { type: "string", description: "Directory or file to search in (default .)" },
|
|
73
|
-
glob: { type: "string", description: "File filter glob (e.g. *.ts)" },
|
|
74
|
-
},
|
|
75
|
-
required: ["pattern"],
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
async execute(args, signal): Promise<ToolResult> {
|
|
79
|
-
try {
|
|
80
|
-
const rawPath = (args.path as string) || "."
|
|
81
|
-
const dir = resolve(cwd, rawPath)
|
|
82
|
-
if (dir !== cwd && !dir.startsWith(`${cwd}/`)) {
|
|
83
|
-
throw new Error(`Path outside project: ${rawPath}`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const pattern = args.pattern as string
|
|
87
|
-
const globFilter = args.glob as string | undefined
|
|
88
|
-
const relSearchPath = relative(cwd, dir) || "."
|
|
89
|
-
|
|
90
|
-
// rg is 10-100x faster than our JS fallback, but isn't always installed
|
|
91
|
-
try {
|
|
92
|
-
const cmd = ["rg", "--line-number", "--max-count", "200"]
|
|
93
|
-
if (globFilter) cmd.push(`--glob=${globFilter}`)
|
|
94
|
-
cmd.push("--", pattern, relSearchPath)
|
|
95
|
-
|
|
96
|
-
const proc = spawn(cmd[0]!, cmd.slice(1), {
|
|
97
|
-
cwd,
|
|
98
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const onAbort = () => {
|
|
102
|
-
proc.kill()
|
|
103
|
-
proc.stdout.destroy()
|
|
104
|
-
proc.stderr.destroy()
|
|
105
|
-
}
|
|
106
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
107
|
-
|
|
108
|
-
let stdout = ""
|
|
109
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
110
|
-
stdout += chunk.toString()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
let exitCode: number
|
|
114
|
-
try {
|
|
115
|
-
exitCode = await new Promise<number>((resolve, reject) => {
|
|
116
|
-
proc.on("error", reject)
|
|
117
|
-
proc.on("close", (code) => resolve(code ?? -1))
|
|
118
|
-
})
|
|
119
|
-
} finally {
|
|
120
|
-
signal?.removeEventListener("abort", onAbort)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (exitCode === 0) {
|
|
124
|
-
const lines = stdout.split("\n").slice(0, 200).join("\n")
|
|
125
|
-
return { content: [textPart(lines || "No matches")], isError: false }
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// rg not available, fall through
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Pure JS fallback when rg is not available
|
|
132
|
-
const files = await glob(globFilter || "**/*", {
|
|
133
|
-
cwd: dir,
|
|
134
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
135
|
-
})
|
|
136
|
-
const prefix = relSearchPath === "." ? "" : `${relSearchPath}/`
|
|
137
|
-
const re = new RegExp(pattern, "i")
|
|
138
|
-
const matches: string[] = []
|
|
139
|
-
for (const file of files.slice(0, 500)) {
|
|
140
|
-
if (signal?.aborted) break
|
|
141
|
-
try {
|
|
142
|
-
const content = await readFile(resolve(dir, file), "utf-8")
|
|
143
|
-
const lines = content.split("\n")
|
|
144
|
-
for (let i = 0; i < lines.length && matches.length < 200; i++) {
|
|
145
|
-
const line = lines[i]
|
|
146
|
-
if (line && re.test(line)) matches.push(`${prefix}${file}:${i + 1}:${line}`)
|
|
147
|
-
}
|
|
148
|
-
} catch {
|
|
149
|
-
// Skip binary/unreadable files silently
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
content: [textPart(matches.join("\n") || "No matches")],
|
|
154
|
-
isError: false,
|
|
155
|
-
}
|
|
156
|
-
} catch (e) {
|
|
157
|
-
return {
|
|
158
|
-
content: [textPart(`Error: ${(e as Error).message}`)],
|
|
159
|
-
isError: true,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Tool for listing directory entries.
|
|
168
|
-
*/
|
|
169
|
-
export function lsTool(cwd: string): Tool {
|
|
170
|
-
return {
|
|
171
|
-
def: {
|
|
172
|
-
name: "ls",
|
|
173
|
-
description: "List directory contents.",
|
|
174
|
-
parameters: {
|
|
175
|
-
type: "object",
|
|
176
|
-
properties: {
|
|
177
|
-
path: { type: "string", description: "Directory to list (default .)" },
|
|
178
|
-
},
|
|
179
|
-
required: [],
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
async execute(args): Promise<ToolResult> {
|
|
183
|
-
try {
|
|
184
|
-
const dir = resolve(cwd, (args.path as string) || ".")
|
|
185
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
186
|
-
const lines = entries.map((e) => {
|
|
187
|
-
const suffix = e.isDirectory() ? "/" : e.isSymbolicLink() ? "@" : ""
|
|
188
|
-
return `${e.name}${suffix}`
|
|
189
|
-
})
|
|
190
|
-
return { content: [textPart(lines.join("\n") || "(empty)")], isError: false }
|
|
191
|
-
} catch (e) {
|
|
192
|
-
return {
|
|
193
|
-
content: [textPart(`Error: ${(e as Error).message}`)],
|
|
194
|
-
isError: true,
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Tool for visualizing a truncated directory tree.
|
|
203
|
-
*/
|
|
204
|
-
export function treeTool(cwd: string): Tool {
|
|
205
|
-
return {
|
|
206
|
-
def: {
|
|
207
|
-
name: "tree",
|
|
208
|
-
description:
|
|
209
|
-
"Print a visual directory tree structure, ignoring common ignored folders like node_modules and .git.",
|
|
210
|
-
parameters: {
|
|
211
|
-
type: "object",
|
|
212
|
-
properties: {
|
|
213
|
-
path: { type: "string", description: "Directory to start tree from (default .)" },
|
|
214
|
-
depth: { type: "number", description: "Maximum depth to traverse (default 3)" },
|
|
215
|
-
},
|
|
216
|
-
required: [],
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
async execute(args): Promise<ToolResult> {
|
|
220
|
-
try {
|
|
221
|
-
const startDir = resolve(cwd, (args.path as string) || ".")
|
|
222
|
-
const maxDepth = Number(args.depth ?? 3) || 3
|
|
223
|
-
|
|
224
|
-
const ignoreList = new Set([
|
|
225
|
-
".git",
|
|
226
|
-
"node_modules",
|
|
227
|
-
"dist",
|
|
228
|
-
"build",
|
|
229
|
-
".svelte-kit",
|
|
230
|
-
".next",
|
|
231
|
-
"out",
|
|
232
|
-
".scannerwork",
|
|
233
|
-
"coverage",
|
|
234
|
-
])
|
|
235
|
-
|
|
236
|
-
async function walk(dir: string, currentDepth: number, prefix: string): Promise<string> {
|
|
237
|
-
if (currentDepth > maxDepth) return ""
|
|
238
|
-
let result = ""
|
|
239
|
-
|
|
240
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
241
|
-
const sorted = entries
|
|
242
|
-
.filter((e) => !ignoreList.has(e.name))
|
|
243
|
-
.sort((a, b) => {
|
|
244
|
-
if (a.isDirectory() && !b.isDirectory()) return -1
|
|
245
|
-
if (!a.isDirectory() && b.isDirectory()) return 1
|
|
246
|
-
return a.name.localeCompare(b.name)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
250
|
-
const entry = sorted[i]!
|
|
251
|
-
const isLast = i === sorted.length - 1
|
|
252
|
-
const connector = isLast ? "└── " : "├── "
|
|
253
|
-
const childPrefix = prefix + (isLast ? " " : "│ ")
|
|
254
|
-
|
|
255
|
-
result += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}\n`
|
|
256
|
-
|
|
257
|
-
if (entry.isDirectory()) {
|
|
258
|
-
result += await walk(resolve(dir, entry.name), currentDepth + 1, childPrefix)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return result
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const treeText = await walk(startDir, 1, "")
|
|
265
|
-
return { content: [textPart(treeText || "(empty)")], isError: false }
|
|
266
|
-
} catch (e) {
|
|
267
|
-
return {
|
|
268
|
-
content: [textPart(`Error: ${(e as Error).message}`)],
|
|
269
|
-
isError: true,
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
}
|
|
274
|
-
}
|
package/src/tools/shell.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool for executing shell commands within the project root.
|
|
3
|
-
* Supports timeouts and output truncation to protect the context window.
|
|
4
|
-
*/
|
|
5
|
-
import { spawn } from "node:child_process"
|
|
6
|
-
import type { Tool, ToolResult } from "../types.ts"
|
|
7
|
-
|
|
8
|
-
import { textPart } from "../util.ts"
|
|
9
|
-
|
|
10
|
-
export function bashTool(cwd: string): Tool {
|
|
11
|
-
return {
|
|
12
|
-
def: {
|
|
13
|
-
name: "bash",
|
|
14
|
-
description:
|
|
15
|
-
"Execute a shell command. Returns stdout and stderr. Timeout after N seconds (default 120).",
|
|
16
|
-
parameters: {
|
|
17
|
-
type: "object",
|
|
18
|
-
properties: {
|
|
19
|
-
command: { type: "string", description: "Shell command to run" },
|
|
20
|
-
timeout: { type: "number", description: "Timeout in seconds (default 120)" },
|
|
21
|
-
},
|
|
22
|
-
required: ["command"],
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
async execute(args, signal): Promise<ToolResult> {
|
|
26
|
-
const command = args.command as string
|
|
27
|
-
const timeoutMs = (Number(args.timeout) || 120) * 1000
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const proc = spawn("sh", ["-c", command], { cwd, stdio: ["ignore", "pipe", "pipe"] })
|
|
31
|
-
|
|
32
|
-
let stdout = ""
|
|
33
|
-
let stderr = ""
|
|
34
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
35
|
-
stdout += chunk.toString()
|
|
36
|
-
})
|
|
37
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
38
|
-
stderr += chunk.toString()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
let killed = false
|
|
42
|
-
const timer = setTimeout(() => {
|
|
43
|
-
killed = true
|
|
44
|
-
proc.kill("SIGKILL")
|
|
45
|
-
proc.stdout.destroy()
|
|
46
|
-
proc.stderr.destroy()
|
|
47
|
-
}, timeoutMs)
|
|
48
|
-
|
|
49
|
-
const onAbort = () => {
|
|
50
|
-
killed = true
|
|
51
|
-
proc.kill("SIGKILL")
|
|
52
|
-
proc.stdout.destroy()
|
|
53
|
-
proc.stderr.destroy()
|
|
54
|
-
}
|
|
55
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
56
|
-
|
|
57
|
-
let exitCode: number
|
|
58
|
-
try {
|
|
59
|
-
exitCode = await new Promise<number>((resolve, reject) => {
|
|
60
|
-
proc.on("error", reject)
|
|
61
|
-
proc.on("close", (code) => resolve(code ?? -1))
|
|
62
|
-
})
|
|
63
|
-
} finally {
|
|
64
|
-
clearTimeout(timer)
|
|
65
|
-
signal?.removeEventListener("abort", onAbort)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Prevent context-window blowout from noisy commands
|
|
69
|
-
const MAX = 50_000
|
|
70
|
-
let out = ""
|
|
71
|
-
if (stdout) out += stdout.slice(0, MAX)
|
|
72
|
-
if (stderr) {
|
|
73
|
-
if (out) out += "\n"
|
|
74
|
-
out += stderr.slice(0, MAX - out.length)
|
|
75
|
-
}
|
|
76
|
-
if (out.length >= MAX) out += `\n…truncated`
|
|
77
|
-
|
|
78
|
-
if (killed) out += `\n[timeout after ${timeoutMs / 1000}s]`
|
|
79
|
-
out += `\n[exit ${exitCode}]`
|
|
80
|
-
|
|
81
|
-
return { content: [textPart(out)], isError: exitCode !== 0 || killed }
|
|
82
|
-
} catch (e) {
|
|
83
|
-
return {
|
|
84
|
-
content: [textPart(`Error: ${(e as Error).message}`)],
|
|
85
|
-
isError: true,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
}
|