@llmist/cli 10.1.1 → 10.3.1
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/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +56 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/package.json +15 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as llmist from 'llmist';
|
|
2
|
+
|
|
3
|
+
declare const editFile: llmist.AbstractGadget;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ListDirectory gadget - Lists files and directories with full metadata.
|
|
7
|
+
* All directory paths are validated to be within the current working directory.
|
|
8
|
+
*/
|
|
9
|
+
declare const listDirectory: llmist.AbstractGadget;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ReadFile gadget - Reads the entire content of a file and returns it as text.
|
|
13
|
+
* All file paths are validated to be within the current working directory.
|
|
14
|
+
*/
|
|
15
|
+
declare const readFile: llmist.AbstractGadget;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WriteFile gadget - Writes content to a file.
|
|
19
|
+
* Creates parent directories if needed. Overwrites existing files.
|
|
20
|
+
* All file paths are validated to be within the current working directory.
|
|
21
|
+
*/
|
|
22
|
+
declare const writeFile: llmist.AbstractGadget;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* RunCommand gadget - Executes a command with arguments and returns its output.
|
|
26
|
+
*
|
|
27
|
+
* Uses argv array to bypass shell interpretation entirely - arguments are
|
|
28
|
+
* passed directly to the process without any escaping or shell expansion.
|
|
29
|
+
* This allows special characters (quotes, backticks, newlines) to work correctly.
|
|
30
|
+
*
|
|
31
|
+
* Safety should be added externally via the hook system (see example 10).
|
|
32
|
+
*
|
|
33
|
+
* Output format follows the established pattern: `status=N\n\n<output>`
|
|
34
|
+
*/
|
|
35
|
+
declare const runCommand: llmist.AbstractGadget;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exception thrown when a path validation fails due to sandbox constraints.
|
|
39
|
+
* This ensures all file operations are restricted to the current working directory.
|
|
40
|
+
*/
|
|
41
|
+
declare class PathSandboxException extends Error {
|
|
42
|
+
constructor(inputPath: string, reason: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validates that a given path is within the current working directory.
|
|
46
|
+
* This prevents directory traversal attacks and ensures all file operations
|
|
47
|
+
* are sandboxed to the CWD and its subdirectories.
|
|
48
|
+
*
|
|
49
|
+
* @param inputPath - Path to validate (can be relative or absolute)
|
|
50
|
+
* @returns The validated absolute path
|
|
51
|
+
* @throws PathSandboxException if the path is outside the CWD
|
|
52
|
+
* @throws Error for other file system errors
|
|
53
|
+
*/
|
|
54
|
+
declare function validatePathIsWithinCwd(inputPath: string): string;
|
|
55
|
+
|
|
56
|
+
export { editFile as EditFile, listDirectory as ListDirectory, PathSandboxException, readFile as ReadFile, runCommand as RunCommand, writeFile as WriteFile, editFile, listDirectory, readFile, runCommand, validatePathIsWithinCwd, writeFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// src/builtins/filesystem/edit-file.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGadget } from "llmist";
|
|
4
|
+
|
|
5
|
+
// src/spawn.ts
|
|
6
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
7
|
+
var isBun = typeof Bun !== "undefined";
|
|
8
|
+
function adaptBunProcess(proc) {
|
|
9
|
+
return {
|
|
10
|
+
exited: proc.exited,
|
|
11
|
+
stdout: proc.stdout ?? null,
|
|
12
|
+
stderr: proc.stderr ?? null,
|
|
13
|
+
stdin: proc.stdin ? {
|
|
14
|
+
write(data) {
|
|
15
|
+
proc.stdin?.write(data);
|
|
16
|
+
},
|
|
17
|
+
end() {
|
|
18
|
+
proc.stdin?.end();
|
|
19
|
+
}
|
|
20
|
+
} : null,
|
|
21
|
+
kill: () => proc.kill()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function nodeStreamToReadableStream(nodeStream) {
|
|
25
|
+
if (!nodeStream) return null;
|
|
26
|
+
return new ReadableStream({
|
|
27
|
+
start(controller) {
|
|
28
|
+
nodeStream.on("data", (chunk) => {
|
|
29
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
30
|
+
});
|
|
31
|
+
nodeStream.on("end", () => {
|
|
32
|
+
controller.close();
|
|
33
|
+
});
|
|
34
|
+
nodeStream.on("error", (err) => {
|
|
35
|
+
controller.error(err);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
cancel() {
|
|
39
|
+
nodeStream.destroy();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function spawn(argv, options = {}) {
|
|
44
|
+
if (isBun) {
|
|
45
|
+
return adaptBunProcess(Bun.spawn(argv, options));
|
|
46
|
+
}
|
|
47
|
+
const [command, ...args] = argv;
|
|
48
|
+
const proc = nodeSpawn(command, args, {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
stdio: [
|
|
51
|
+
options.stdin === "pipe" ? "pipe" : options.stdin ?? "ignore",
|
|
52
|
+
options.stdout === "pipe" ? "pipe" : options.stdout ?? "ignore",
|
|
53
|
+
options.stderr === "pipe" ? "pipe" : options.stderr ?? "ignore"
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
const exited = new Promise((resolve, reject) => {
|
|
57
|
+
proc.on("exit", (code) => {
|
|
58
|
+
resolve(code ?? 1);
|
|
59
|
+
});
|
|
60
|
+
proc.on("error", (err) => {
|
|
61
|
+
reject(err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const stdin = proc.stdin ? {
|
|
65
|
+
write(data) {
|
|
66
|
+
proc.stdin?.write(data);
|
|
67
|
+
},
|
|
68
|
+
end() {
|
|
69
|
+
proc.stdin?.end();
|
|
70
|
+
}
|
|
71
|
+
} : null;
|
|
72
|
+
return {
|
|
73
|
+
exited,
|
|
74
|
+
stdout: nodeStreamToReadableStream(proc.stdout),
|
|
75
|
+
stderr: nodeStreamToReadableStream(proc.stderr),
|
|
76
|
+
stdin,
|
|
77
|
+
kill() {
|
|
78
|
+
proc.kill();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/builtins/filesystem/utils.ts
|
|
84
|
+
import fs from "fs";
|
|
85
|
+
import path from "path";
|
|
86
|
+
var PathSandboxException = class extends Error {
|
|
87
|
+
constructor(inputPath, reason) {
|
|
88
|
+
super(`Path access denied: ${inputPath}. ${reason}`);
|
|
89
|
+
this.name = "PathSandboxException";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function validatePathIsWithinCwd(inputPath) {
|
|
93
|
+
const cwd = process.cwd();
|
|
94
|
+
const resolvedPath = path.resolve(cwd, inputPath);
|
|
95
|
+
let finalPath;
|
|
96
|
+
try {
|
|
97
|
+
finalPath = fs.realpathSync(resolvedPath);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const nodeError = error;
|
|
100
|
+
if (nodeError.code === "ENOENT") {
|
|
101
|
+
finalPath = resolvedPath;
|
|
102
|
+
} else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const cwdWithSep = cwd + path.sep;
|
|
107
|
+
if (!finalPath.startsWith(cwdWithSep) && finalPath !== cwd) {
|
|
108
|
+
throw new PathSandboxException(inputPath, "Path is outside the current working directory");
|
|
109
|
+
}
|
|
110
|
+
return finalPath;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/builtins/filesystem/edit-file.ts
|
|
114
|
+
function filterDangerousCommands(commands) {
|
|
115
|
+
return commands.split("\n").filter((line) => !line.trimStart().startsWith("!")).join("\n");
|
|
116
|
+
}
|
|
117
|
+
var editFile = createGadget({
|
|
118
|
+
name: "EditFile",
|
|
119
|
+
description: "Edit a file using ed commands. Ed is a line-oriented text editor - pipe commands to it for precise file modifications. Commands are executed in sequence. Remember to end with 'w' (write) and 'q' (quit). Shell escape commands (!) are filtered for security.",
|
|
120
|
+
schema: z.object({
|
|
121
|
+
filePath: z.string().describe("Path to the file to edit (relative or absolute)"),
|
|
122
|
+
commands: z.string().describe("Ed commands to execute, one per line")
|
|
123
|
+
}),
|
|
124
|
+
examples: [
|
|
125
|
+
{
|
|
126
|
+
params: {
|
|
127
|
+
filePath: "config.txt",
|
|
128
|
+
commands: `1,$p
|
|
129
|
+
q`
|
|
130
|
+
},
|
|
131
|
+
output: "path=config.txt\n\n32\nkey=value\noption=true",
|
|
132
|
+
comment: "Print entire file contents (ed shows byte count, then content)"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
params: {
|
|
136
|
+
filePath: "data.txt",
|
|
137
|
+
commands: `1,$s/foo/bar/g
|
|
138
|
+
w
|
|
139
|
+
q`
|
|
140
|
+
},
|
|
141
|
+
output: "path=data.txt\n\n42\n42",
|
|
142
|
+
comment: "Replace all 'foo' with 'bar' (ed shows bytes read, then bytes written)"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
params: {
|
|
146
|
+
filePath: "list.txt",
|
|
147
|
+
commands: `3d
|
|
148
|
+
w
|
|
149
|
+
q`
|
|
150
|
+
},
|
|
151
|
+
output: "path=list.txt\n\n45\n28",
|
|
152
|
+
comment: "Delete line 3, save and quit"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
params: {
|
|
156
|
+
filePath: "readme.txt",
|
|
157
|
+
commands: `$a
|
|
158
|
+
New last line
|
|
159
|
+
.
|
|
160
|
+
w
|
|
161
|
+
q`
|
|
162
|
+
},
|
|
163
|
+
output: "path=readme.txt\n\n40\n56",
|
|
164
|
+
comment: "Append text after last line ($ = last line, . = end input mode)"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
timeoutMs: 3e4,
|
|
168
|
+
execute: async ({ filePath, commands }) => {
|
|
169
|
+
const validatedPath = validatePathIsWithinCwd(filePath);
|
|
170
|
+
const safeCommands = filterDangerousCommands(commands);
|
|
171
|
+
try {
|
|
172
|
+
const proc = spawn(["ed", validatedPath], {
|
|
173
|
+
stdin: "pipe",
|
|
174
|
+
stdout: "pipe",
|
|
175
|
+
stderr: "pipe"
|
|
176
|
+
});
|
|
177
|
+
if (!proc.stdin) {
|
|
178
|
+
return `path=${filePath}
|
|
179
|
+
|
|
180
|
+
error: Failed to open stdin for ed process`;
|
|
181
|
+
}
|
|
182
|
+
proc.stdin.write(`${safeCommands}
|
|
183
|
+
`);
|
|
184
|
+
proc.stdin.end();
|
|
185
|
+
let timeoutId;
|
|
186
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
187
|
+
timeoutId = setTimeout(() => {
|
|
188
|
+
proc.kill();
|
|
189
|
+
reject(new Error("ed command timed out after 30000ms"));
|
|
190
|
+
}, 3e4);
|
|
191
|
+
});
|
|
192
|
+
const [exitCode, stdout, stderr] = await Promise.race([
|
|
193
|
+
Promise.all([
|
|
194
|
+
proc.exited,
|
|
195
|
+
new Response(proc.stdout).text(),
|
|
196
|
+
new Response(proc.stderr).text()
|
|
197
|
+
]),
|
|
198
|
+
timeoutPromise
|
|
199
|
+
]);
|
|
200
|
+
if (timeoutId) {
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
}
|
|
203
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
204
|
+
if (exitCode !== 0) {
|
|
205
|
+
return `path=${filePath}
|
|
206
|
+
|
|
207
|
+
${output || "ed exited with non-zero status"}`;
|
|
208
|
+
}
|
|
209
|
+
return `path=${filePath}
|
|
210
|
+
|
|
211
|
+
${output || "(no output)"}`;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
return `path=${filePath}
|
|
215
|
+
|
|
216
|
+
error: ${message}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/builtins/filesystem/list-directory.ts
|
|
222
|
+
import fs2 from "fs";
|
|
223
|
+
import path2 from "path";
|
|
224
|
+
import { z as z2 } from "zod";
|
|
225
|
+
import { createGadget as createGadget2 } from "llmist";
|
|
226
|
+
function listFiles(dirPath, basePath = dirPath, maxDepth = 1, currentDepth = 1) {
|
|
227
|
+
const entries = [];
|
|
228
|
+
try {
|
|
229
|
+
const items = fs2.readdirSync(dirPath);
|
|
230
|
+
for (const item of items) {
|
|
231
|
+
const fullPath = path2.join(dirPath, item);
|
|
232
|
+
const relativePath = path2.relative(basePath, fullPath);
|
|
233
|
+
try {
|
|
234
|
+
const stats = fs2.lstatSync(fullPath);
|
|
235
|
+
let type;
|
|
236
|
+
let size;
|
|
237
|
+
if (stats.isSymbolicLink()) {
|
|
238
|
+
type = "symlink";
|
|
239
|
+
size = 0;
|
|
240
|
+
} else if (stats.isDirectory()) {
|
|
241
|
+
type = "directory";
|
|
242
|
+
size = 0;
|
|
243
|
+
} else {
|
|
244
|
+
type = "file";
|
|
245
|
+
size = stats.size;
|
|
246
|
+
}
|
|
247
|
+
entries.push({
|
|
248
|
+
name: item,
|
|
249
|
+
relativePath,
|
|
250
|
+
type,
|
|
251
|
+
size,
|
|
252
|
+
modified: Math.floor(stats.mtime.getTime() / 1e3)
|
|
253
|
+
});
|
|
254
|
+
if (type === "directory" && currentDepth < maxDepth) {
|
|
255
|
+
try {
|
|
256
|
+
validatePathIsWithinCwd(fullPath);
|
|
257
|
+
const subEntries = listFiles(fullPath, basePath, maxDepth, currentDepth + 1);
|
|
258
|
+
entries.push(...subEntries);
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
return entries;
|
|
269
|
+
}
|
|
270
|
+
function formatAge(epochSeconds) {
|
|
271
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
272
|
+
const seconds = now - epochSeconds;
|
|
273
|
+
if (seconds < 60) return `${seconds}s`;
|
|
274
|
+
const minutes = Math.floor(seconds / 60);
|
|
275
|
+
if (minutes < 60) return `${minutes}m`;
|
|
276
|
+
const hours = Math.floor(minutes / 60);
|
|
277
|
+
if (hours < 24) return `${hours}h`;
|
|
278
|
+
const days = Math.floor(hours / 24);
|
|
279
|
+
if (days < 7) return `${days}d`;
|
|
280
|
+
const weeks = Math.floor(days / 7);
|
|
281
|
+
if (weeks < 4) return `${weeks}w`;
|
|
282
|
+
const months = Math.floor(days / 30);
|
|
283
|
+
if (months < 12) return `${months}mo`;
|
|
284
|
+
const years = Math.floor(days / 365);
|
|
285
|
+
return `${years}y`;
|
|
286
|
+
}
|
|
287
|
+
function formatEntriesAsString(entries) {
|
|
288
|
+
if (entries.length === 0) {
|
|
289
|
+
return "#empty";
|
|
290
|
+
}
|
|
291
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
292
|
+
const typeOrder = { directory: 0, file: 1, symlink: 2 };
|
|
293
|
+
const typeCompare = typeOrder[a.type] - typeOrder[b.type];
|
|
294
|
+
if (typeCompare !== 0) return typeCompare;
|
|
295
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
296
|
+
});
|
|
297
|
+
const typeCode = {
|
|
298
|
+
directory: "D",
|
|
299
|
+
file: "F",
|
|
300
|
+
symlink: "L"
|
|
301
|
+
};
|
|
302
|
+
const encodeName = (name) => name.replace(/\|/g, "%7C").replace(/\n/g, "%0A");
|
|
303
|
+
const header = "#T|N|S|A";
|
|
304
|
+
const rows = sortedEntries.map(
|
|
305
|
+
(e) => `${typeCode[e.type]}|${encodeName(e.relativePath)}|${e.size}|${formatAge(e.modified)}`
|
|
306
|
+
);
|
|
307
|
+
return [header, ...rows].join("\n");
|
|
308
|
+
}
|
|
309
|
+
var listDirectory = createGadget2({
|
|
310
|
+
name: "ListDirectory",
|
|
311
|
+
description: "List files and directories in a directory with full details (names, types, sizes, modification dates). Use maxDepth to explore subdirectories recursively. The directory path must be within the current working directory or its subdirectories.",
|
|
312
|
+
schema: z2.object({
|
|
313
|
+
directoryPath: z2.string().default(".").describe("Path to the directory to list"),
|
|
314
|
+
maxDepth: z2.number().int().min(1).max(10).default(1).describe(
|
|
315
|
+
"Maximum depth to recurse (1 = immediate children only, 2 = include grandchildren, etc.)"
|
|
316
|
+
)
|
|
317
|
+
}),
|
|
318
|
+
examples: [
|
|
319
|
+
{
|
|
320
|
+
params: { directoryPath: ".", maxDepth: 1 },
|
|
321
|
+
output: "path=. maxDepth=1\n\n#T|N|S|A\nD|src|0|2h\nD|tests|0|1d\nF|package.json|2841|3h",
|
|
322
|
+
comment: "List current directory"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
params: { directoryPath: "src", maxDepth: 2 },
|
|
326
|
+
output: "path=src maxDepth=2\n\n#T|N|S|A\nD|components|0|1d\nD|utils|0|2d\nF|index.ts|512|1h\nF|components/Button.tsx|1024|3h",
|
|
327
|
+
comment: "List src directory recursively"
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
execute: ({ directoryPath, maxDepth }) => {
|
|
331
|
+
const validatedPath = validatePathIsWithinCwd(directoryPath);
|
|
332
|
+
const stats = fs2.statSync(validatedPath);
|
|
333
|
+
if (!stats.isDirectory()) {
|
|
334
|
+
throw new Error(`Path is not a directory: ${directoryPath}`);
|
|
335
|
+
}
|
|
336
|
+
const entries = listFiles(validatedPath, validatedPath, maxDepth);
|
|
337
|
+
const formattedList = formatEntriesAsString(entries);
|
|
338
|
+
return `path=${directoryPath} maxDepth=${maxDepth}
|
|
339
|
+
|
|
340
|
+
${formattedList}`;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// src/builtins/filesystem/read-file.ts
|
|
345
|
+
import fs3 from "fs";
|
|
346
|
+
import { z as z3 } from "zod";
|
|
347
|
+
import { createGadget as createGadget3 } from "llmist";
|
|
348
|
+
var readFile = createGadget3({
|
|
349
|
+
name: "ReadFile",
|
|
350
|
+
description: "Read the entire content of a file and return it as text. The file path must be within the current working directory or its subdirectories.",
|
|
351
|
+
schema: z3.object({
|
|
352
|
+
filePath: z3.string().describe("Path to the file to read (relative or absolute)")
|
|
353
|
+
}),
|
|
354
|
+
examples: [
|
|
355
|
+
{
|
|
356
|
+
params: { filePath: "package.json" },
|
|
357
|
+
output: 'path=package.json\n\n{\n "name": "my-project",\n "version": "1.0.0"\n ...\n}',
|
|
358
|
+
comment: "Read a JSON config file"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
params: { filePath: "src/index.ts" },
|
|
362
|
+
output: "path=src/index.ts\n\nexport function main() { ... }",
|
|
363
|
+
comment: "Read a source file"
|
|
364
|
+
}
|
|
365
|
+
],
|
|
366
|
+
execute: ({ filePath }) => {
|
|
367
|
+
const validatedPath = validatePathIsWithinCwd(filePath);
|
|
368
|
+
const content = fs3.readFileSync(validatedPath, "utf-8");
|
|
369
|
+
return `path=${filePath}
|
|
370
|
+
|
|
371
|
+
${content}`;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// src/builtins/filesystem/write-file.ts
|
|
376
|
+
import fs4 from "fs";
|
|
377
|
+
import path3 from "path";
|
|
378
|
+
import { z as z4 } from "zod";
|
|
379
|
+
import { createGadget as createGadget4 } from "llmist";
|
|
380
|
+
var writeFile = createGadget4({
|
|
381
|
+
name: "WriteFile",
|
|
382
|
+
description: "Write content to a file. Creates parent directories if needed. Overwrites existing files. The file path must be within the current working directory or its subdirectories.",
|
|
383
|
+
schema: z4.object({
|
|
384
|
+
filePath: z4.string().describe("Path to the file to write (relative or absolute)"),
|
|
385
|
+
content: z4.string().describe("Content to write to the file")
|
|
386
|
+
}),
|
|
387
|
+
examples: [
|
|
388
|
+
{
|
|
389
|
+
params: { filePath: "output.txt", content: "Hello, World!" },
|
|
390
|
+
output: "path=output.txt\n\nWrote 13 bytes",
|
|
391
|
+
comment: "Write a simple text file"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
params: {
|
|
395
|
+
filePath: "src/server.ts",
|
|
396
|
+
content: `import { serve } from "bun";
|
|
397
|
+
|
|
398
|
+
const port = 3000;
|
|
399
|
+
|
|
400
|
+
serve({
|
|
401
|
+
port,
|
|
402
|
+
fetch: (req) => new Response(\`Hello from \${req.url}\`),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
console.log(\`Server running on http://localhost:\${port}\`);`
|
|
406
|
+
},
|
|
407
|
+
output: "path=src/server.ts\n\nWrote 198 bytes (created directory: src)",
|
|
408
|
+
comment: "Write code with template literals - NO escaping needed inside heredoc (use <<<EOF...EOF)"
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
execute: ({ filePath, content }) => {
|
|
412
|
+
const validatedPath = validatePathIsWithinCwd(filePath);
|
|
413
|
+
const parentDir = path3.dirname(validatedPath);
|
|
414
|
+
let createdDir = false;
|
|
415
|
+
if (!fs4.existsSync(parentDir)) {
|
|
416
|
+
validatePathIsWithinCwd(parentDir);
|
|
417
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
418
|
+
createdDir = true;
|
|
419
|
+
}
|
|
420
|
+
fs4.writeFileSync(validatedPath, content, "utf-8");
|
|
421
|
+
const bytesWritten = Buffer.byteLength(content, "utf-8");
|
|
422
|
+
const dirNote = createdDir ? ` (created directory: ${path3.dirname(filePath)})` : "";
|
|
423
|
+
return `path=${filePath}
|
|
424
|
+
|
|
425
|
+
Wrote ${bytesWritten} bytes${dirNote}`;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// src/builtins/run-command.ts
|
|
430
|
+
import { z as z5 } from "zod";
|
|
431
|
+
import { createGadget as createGadget5 } from "llmist";
|
|
432
|
+
var runCommand = createGadget5({
|
|
433
|
+
name: "RunCommand",
|
|
434
|
+
description: "Execute a command with arguments and return its output. Uses argv array to bypass shell - arguments are passed directly without interpretation. Returns stdout/stderr combined with exit status.",
|
|
435
|
+
schema: z5.object({
|
|
436
|
+
argv: z5.array(z5.string()).describe("Command and arguments as array (e.g., ['git', 'commit', '-m', 'message'])"),
|
|
437
|
+
cwd: z5.string().optional().describe("Working directory for the command (default: current directory)"),
|
|
438
|
+
timeout: z5.number().default(3e4).describe("Timeout in milliseconds (default: 30000)")
|
|
439
|
+
}),
|
|
440
|
+
examples: [
|
|
441
|
+
{
|
|
442
|
+
params: { argv: ["ls", "-la"], timeout: 3e4 },
|
|
443
|
+
output: "status=0\n\ntotal 24\ndrwxr-xr-x 5 user staff 160 Nov 27 10:00 .\ndrwxr-xr-x 3 user staff 96 Nov 27 09:00 ..\n-rw-r--r-- 1 user staff 1024 Nov 27 10:00 package.json",
|
|
444
|
+
comment: "List directory contents with details"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
params: { argv: ["echo", "Hello World"], timeout: 3e4 },
|
|
448
|
+
output: "status=0\n\nHello World",
|
|
449
|
+
comment: "Echo without shell - argument passed directly"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
params: { argv: ["cat", "nonexistent.txt"], timeout: 3e4 },
|
|
453
|
+
output: "status=1\n\ncat: nonexistent.txt: No such file or directory",
|
|
454
|
+
comment: "Command that fails returns non-zero status"
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
params: { argv: ["pwd"], cwd: "/tmp", timeout: 3e4 },
|
|
458
|
+
output: "status=0\n\n/tmp",
|
|
459
|
+
comment: "Execute command in a specific directory"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
params: {
|
|
463
|
+
argv: [
|
|
464
|
+
"gh",
|
|
465
|
+
"pr",
|
|
466
|
+
"review",
|
|
467
|
+
"123",
|
|
468
|
+
"--comment",
|
|
469
|
+
"--body",
|
|
470
|
+
"Review with `backticks` and 'quotes'"
|
|
471
|
+
],
|
|
472
|
+
timeout: 3e4
|
|
473
|
+
},
|
|
474
|
+
output: "status=0\n\n(no output)",
|
|
475
|
+
comment: "Complex arguments with special characters - no escaping needed"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
params: {
|
|
479
|
+
argv: [
|
|
480
|
+
"gh",
|
|
481
|
+
"pr",
|
|
482
|
+
"review",
|
|
483
|
+
"123",
|
|
484
|
+
"--approve",
|
|
485
|
+
"--body",
|
|
486
|
+
"## Review Summary\n\n**Looks good!**\n\n- Clean code\n- Tests pass"
|
|
487
|
+
],
|
|
488
|
+
timeout: 3e4
|
|
489
|
+
},
|
|
490
|
+
output: "status=0\n\nApproving pull request #123",
|
|
491
|
+
comment: "Multiline body: --body flag and content must be SEPARATE array elements"
|
|
492
|
+
}
|
|
493
|
+
],
|
|
494
|
+
execute: async ({ argv, cwd, timeout }) => {
|
|
495
|
+
const workingDir = cwd ?? process.cwd();
|
|
496
|
+
if (argv.length === 0) {
|
|
497
|
+
return "status=1\n\nerror: argv array cannot be empty";
|
|
498
|
+
}
|
|
499
|
+
let timeoutId;
|
|
500
|
+
try {
|
|
501
|
+
const proc = spawn(argv, {
|
|
502
|
+
cwd: workingDir,
|
|
503
|
+
stdout: "pipe",
|
|
504
|
+
stderr: "pipe"
|
|
505
|
+
});
|
|
506
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
507
|
+
timeoutId = setTimeout(() => {
|
|
508
|
+
proc.kill();
|
|
509
|
+
reject(new Error(`Command timed out after ${timeout}ms`));
|
|
510
|
+
}, timeout);
|
|
511
|
+
});
|
|
512
|
+
const [exitCode, stdout, stderr] = await Promise.race([
|
|
513
|
+
Promise.all([
|
|
514
|
+
proc.exited,
|
|
515
|
+
new Response(proc.stdout).text(),
|
|
516
|
+
new Response(proc.stderr).text()
|
|
517
|
+
]),
|
|
518
|
+
timeoutPromise
|
|
519
|
+
]);
|
|
520
|
+
if (timeoutId) {
|
|
521
|
+
clearTimeout(timeoutId);
|
|
522
|
+
}
|
|
523
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
524
|
+
return `status=${exitCode}
|
|
525
|
+
|
|
526
|
+
${output || "(no output)"}`;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (timeoutId) {
|
|
529
|
+
clearTimeout(timeoutId);
|
|
530
|
+
}
|
|
531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
532
|
+
return `status=1
|
|
533
|
+
|
|
534
|
+
error: ${message}`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
export {
|
|
539
|
+
editFile as EditFile,
|
|
540
|
+
listDirectory as ListDirectory,
|
|
541
|
+
PathSandboxException,
|
|
542
|
+
readFile as ReadFile,
|
|
543
|
+
runCommand as RunCommand,
|
|
544
|
+
writeFile as WriteFile,
|
|
545
|
+
editFile,
|
|
546
|
+
listDirectory,
|
|
547
|
+
readFile,
|
|
548
|
+
runCommand,
|
|
549
|
+
validatePathIsWithinCwd,
|
|
550
|
+
writeFile
|
|
551
|
+
};
|
|
552
|
+
//# sourceMappingURL=index.js.map
|