@llmtune/cli 0.1.0 → 0.1.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.
Files changed (196) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/conversation.d.ts +42 -0
  3. package/dist/agent/conversation.js +105 -0
  4. package/dist/agent/loop.d.ts +19 -0
  5. package/dist/agent/loop.js +185 -0
  6. package/dist/agent/planner.d.ts +8 -0
  7. package/dist/agent/planner.js +43 -0
  8. package/dist/auth/client.d.ts +4 -0
  9. package/dist/auth/client.js +24 -0
  10. package/dist/auth/config.d.ts +21 -0
  11. package/dist/auth/config.js +83 -0
  12. package/dist/commands/chat.d.ts +5 -0
  13. package/dist/commands/chat.js +27 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +37 -0
  16. package/dist/commands/login.d.ts +2 -0
  17. package/dist/commands/login.js +93 -0
  18. package/dist/commands/marketplace.d.ts +6 -0
  19. package/dist/commands/marketplace.js +213 -0
  20. package/dist/commands/models.d.ts +2 -0
  21. package/dist/commands/models.js +53 -0
  22. package/dist/compact/history-store.d.ts +29 -0
  23. package/dist/compact/history-store.js +110 -0
  24. package/dist/compact/microcompact.d.ts +10 -0
  25. package/dist/compact/microcompact.js +43 -0
  26. package/dist/compact/service.d.ts +13 -0
  27. package/dist/compact/service.js +156 -0
  28. package/dist/context/analyzer.d.ts +26 -0
  29. package/dist/context/analyzer.js +99 -0
  30. package/dist/context/builder.d.ts +13 -0
  31. package/dist/context/builder.js +144 -0
  32. package/dist/context/cache.d.ts +6 -0
  33. package/dist/context/cache.js +8 -0
  34. package/dist/context/git-context.d.ts +9 -0
  35. package/dist/context/git-context.js +49 -0
  36. package/dist/context/llmtune-md.d.ts +6 -0
  37. package/dist/context/llmtune-md.js +73 -0
  38. package/dist/context/workspace.d.ts +11 -0
  39. package/dist/context/workspace.js +115 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.js +1 -1
  42. package/dist/marketplace/client.d.ts +52 -0
  43. package/dist/marketplace/client.js +86 -0
  44. package/dist/memory/files.d.ts +14 -0
  45. package/dist/memory/files.js +116 -0
  46. package/dist/memory/service.d.ts +22 -0
  47. package/dist/memory/service.js +146 -0
  48. package/dist/repl/repl.d.ts +8 -0
  49. package/dist/repl/repl.js +374 -0
  50. package/dist/skills/args.d.ts +10 -0
  51. package/dist/skills/args.js +37 -0
  52. package/dist/skills/frontmatter.d.ts +6 -0
  53. package/dist/skills/frontmatter.js +44 -0
  54. package/dist/skills/loader.d.ts +5 -0
  55. package/dist/skills/loader.js +59 -0
  56. package/dist/skills/registry.d.ts +27 -0
  57. package/dist/skills/registry.js +162 -0
  58. package/dist/skills/signing/signer.d.ts +19 -0
  59. package/dist/skills/signing/signer.js +110 -0
  60. package/dist/skills/trust.d.ts +11 -0
  61. package/dist/skills/trust.js +42 -0
  62. package/dist/telemetry/logger.d.ts +51 -0
  63. package/dist/telemetry/logger.js +135 -0
  64. package/dist/tools/permissions.d.ts +20 -0
  65. package/dist/tools/permissions.js +58 -0
  66. package/dist/tools/protocol.d.ts +22 -0
  67. package/dist/tools/protocol.js +3 -0
  68. package/dist/tools/registry.d.ts +20 -0
  69. package/dist/tools/registry.js +77 -0
  70. package/dist/tools/sandbox/docker.d.ts +16 -0
  71. package/dist/tools/sandbox/docker.js +240 -0
  72. package/dist/tools/sandbox/index.d.ts +18 -0
  73. package/dist/tools/sandbox/index.js +80 -0
  74. package/dist/tools/tools/ask-user.d.ts +3 -0
  75. package/dist/tools/tools/ask-user.js +56 -0
  76. package/dist/tools/tools/bash.d.ts +3 -0
  77. package/dist/tools/tools/bash.js +85 -0
  78. package/dist/tools/tools/edit.d.ts +3 -0
  79. package/dist/tools/tools/edit.js +138 -0
  80. package/dist/tools/tools/glob.d.ts +3 -0
  81. package/dist/tools/tools/glob.js +63 -0
  82. package/dist/tools/tools/grep.d.ts +3 -0
  83. package/dist/tools/tools/grep.js +148 -0
  84. package/dist/tools/tools/read.d.ts +3 -0
  85. package/dist/tools/tools/read.js +85 -0
  86. package/dist/tools/tools/web-fetch.d.ts +3 -0
  87. package/dist/tools/tools/web-fetch.js +142 -0
  88. package/dist/tools/tools/write.d.ts +3 -0
  89. package/dist/tools/tools/write.js +84 -0
  90. package/dist/tools/validation.d.ts +13 -0
  91. package/dist/tools/validation.js +142 -0
  92. package/dist/utils/markdown.d.ts +9 -0
  93. package/dist/utils/markdown.js +89 -0
  94. package/dist/utils/streaming.d.ts +10 -0
  95. package/dist/utils/streaming.js +63 -0
  96. package/dist/utils/tokens.d.ts +12 -0
  97. package/dist/utils/tokens.js +44 -0
  98. package/package.json +2 -2
  99. package/dist/agent/conversation.d.ts.map +0 -1
  100. package/dist/agent/loop.d.ts.map +0 -1
  101. package/dist/agent/planner.d.ts.map +0 -1
  102. package/dist/auth/client.d.ts.map +0 -1
  103. package/dist/auth/config.d.ts.map +0 -1
  104. package/dist/commands/chat.d.ts.map +0 -1
  105. package/dist/commands/config.d.ts.map +0 -1
  106. package/dist/commands/login.d.ts.map +0 -1
  107. package/dist/commands/marketplace.d.ts.map +0 -1
  108. package/dist/commands/models.d.ts.map +0 -1
  109. package/dist/compact/history-store.d.ts.map +0 -1
  110. package/dist/compact/microcompact.d.ts.map +0 -1
  111. package/dist/compact/service.d.ts.map +0 -1
  112. package/dist/context/analyzer.d.ts.map +0 -1
  113. package/dist/context/builder.d.ts.map +0 -1
  114. package/dist/context/cache.d.ts.map +0 -1
  115. package/dist/context/git-context.d.ts.map +0 -1
  116. package/dist/context/llmtune-md.d.ts.map +0 -1
  117. package/dist/context/workspace.d.ts.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/marketplace/client.d.ts.map +0 -1
  120. package/dist/memory/files.d.ts.map +0 -1
  121. package/dist/memory/service.d.ts.map +0 -1
  122. package/dist/repl/repl.d.ts.map +0 -1
  123. package/dist/skills/args.d.ts.map +0 -1
  124. package/dist/skills/frontmatter.d.ts.map +0 -1
  125. package/dist/skills/loader.d.ts.map +0 -1
  126. package/dist/skills/registry.d.ts.map +0 -1
  127. package/dist/skills/signing/signer.d.ts.map +0 -1
  128. package/dist/skills/trust.d.ts.map +0 -1
  129. package/dist/telemetry/logger.d.ts.map +0 -1
  130. package/dist/tools/permissions.d.ts.map +0 -1
  131. package/dist/tools/protocol.d.ts.map +0 -1
  132. package/dist/tools/registry.d.ts.map +0 -1
  133. package/dist/tools/sandbox/docker.d.ts.map +0 -1
  134. package/dist/tools/sandbox/index.d.ts.map +0 -1
  135. package/dist/tools/tools/ask-user.d.ts.map +0 -1
  136. package/dist/tools/tools/bash.d.ts.map +0 -1
  137. package/dist/tools/tools/edit.d.ts.map +0 -1
  138. package/dist/tools/tools/glob.d.ts.map +0 -1
  139. package/dist/tools/tools/grep.d.ts.map +0 -1
  140. package/dist/tools/tools/read.d.ts.map +0 -1
  141. package/dist/tools/tools/web-fetch.d.ts.map +0 -1
  142. package/dist/tools/tools/write.d.ts.map +0 -1
  143. package/dist/tools/validation.d.ts.map +0 -1
  144. package/dist/utils/markdown.d.ts.map +0 -1
  145. package/dist/utils/streaming.d.ts.map +0 -1
  146. package/dist/utils/tokens.d.ts.map +0 -1
  147. package/src/agent/conversation.ts +0 -140
  148. package/src/agent/loop.ts +0 -215
  149. package/src/agent/planner.ts +0 -55
  150. package/src/auth/client.ts +0 -19
  151. package/src/auth/config.ts +0 -89
  152. package/src/commands/chat.ts +0 -28
  153. package/src/commands/config.ts +0 -36
  154. package/src/commands/login.ts +0 -63
  155. package/src/commands/marketplace.ts +0 -190
  156. package/src/commands/models.ts +0 -74
  157. package/src/compact/history-store.ts +0 -101
  158. package/src/compact/microcompact.ts +0 -49
  159. package/src/compact/service.ts +0 -154
  160. package/src/context/analyzer.ts +0 -127
  161. package/src/context/builder.ts +0 -123
  162. package/src/context/cache.ts +0 -11
  163. package/src/context/git-context.ts +0 -58
  164. package/src/context/llmtune-md.ts +0 -48
  165. package/src/context/workspace.ts +0 -139
  166. package/src/index.ts +0 -100
  167. package/src/marketplace/client.ts +0 -118
  168. package/src/memory/files.ts +0 -81
  169. package/src/memory/service.ts +0 -124
  170. package/src/repl/repl.ts +0 -400
  171. package/src/skills/args.ts +0 -35
  172. package/src/skills/builtin/explain-code/SKILL.md +0 -30
  173. package/src/skills/frontmatter.ts +0 -47
  174. package/src/skills/loader.ts +0 -25
  175. package/src/skills/registry.ts +0 -155
  176. package/src/skills/signing/signer.ts +0 -101
  177. package/src/skills/trust.ts +0 -50
  178. package/src/telemetry/logger.ts +0 -108
  179. package/src/tools/permissions.ts +0 -83
  180. package/src/tools/protocol.ts +0 -24
  181. package/src/tools/registry.ts +0 -93
  182. package/src/tools/sandbox/docker.ts +0 -225
  183. package/src/tools/sandbox/index.ts +0 -91
  184. package/src/tools/tools/ask-user.ts +0 -60
  185. package/src/tools/tools/bash.ts +0 -97
  186. package/src/tools/tools/edit.ts +0 -111
  187. package/src/tools/tools/glob.ts +0 -68
  188. package/src/tools/tools/grep.ts +0 -121
  189. package/src/tools/tools/read.ts +0 -57
  190. package/src/tools/tools/web-fetch.ts +0 -158
  191. package/src/tools/tools/write.ts +0 -52
  192. package/src/tools/validation.ts +0 -164
  193. package/src/utils/markdown.ts +0 -96
  194. package/src/utils/streaming.ts +0 -63
  195. package/src/utils/tokens.ts +0 -41
  196. package/tsconfig.json +0 -20
@@ -1,225 +0,0 @@
1
- /**
2
- * Docker-based tool sandbox for enterprise deployments.
3
- * Isolates bash and file write operations inside a Docker container
4
- * to prevent accidental or malicious damage to the host system.
5
- */
6
- import { execFile } from "child_process"
7
- import { promisify } from "util"
8
- import * as path from "path"
9
- import * as fs from "fs"
10
- import * as os from "os"
11
- import type { ToolResult } from "../protocol"
12
-
13
- const execFileAsync = promisify(execFile)
14
-
15
- const SANDBOX_IMAGE = "llmtune-sandbox:latest"
16
- const SANDBOX_WORKDIR = "/workspace"
17
- const EXECUTION_TIMEOUT_MS = 60_000
18
- const MAX_OUTPUT_BYTES = 100_000
19
-
20
- export interface SandboxConfig {
21
- enabled: boolean
22
- image: string
23
- timeoutMs: number
24
- readOnlyPaths: string[]
25
- maxMemoryMB: number
26
- }
27
-
28
- export const DEFAULT_SANDBOX_CONFIG: SandboxConfig = {
29
- enabled: false,
30
- image: SANDBOX_IMAGE,
31
- timeoutMs: EXECUTION_TIMEOUT_MS,
32
- readOnlyPaths: [],
33
- maxMemoryMB: 512,
34
- }
35
-
36
- let sandboxConfig: SandboxConfig = { ...DEFAULT_SANDBOX_CONFIG }
37
-
38
- export function configureSandbox(config: Partial<SandboxConfig>): void {
39
- sandboxConfig = { ...sandboxConfig, ...config }
40
- }
41
-
42
- export function isSandboxEnabled(): boolean {
43
- return sandboxConfig.enabled
44
- }
45
-
46
- export async function isDockerAvailable(): Promise<boolean> {
47
- try {
48
- const { stdout } = await execFileAsync("docker", ["--version"], {
49
- timeout: 5000,
50
- })
51
- return /docker/i.test(stdout)
52
- } catch {
53
- return false
54
- }
55
- }
56
-
57
- export async function ensureSandboxImage(): Promise<boolean> {
58
- try {
59
- const { stdout } = await execFileAsync("docker", ["images", "-q", sandboxConfig.image], {
60
- timeout: 10000,
61
- })
62
- if (stdout.trim()) return true
63
-
64
- // Pull the image if not found
65
- console.log(`Pulling sandbox image: ${sandboxConfig.image}...`)
66
- await execFileAsync("docker", ["pull", sandboxConfig.image], {
67
- timeout: 120_000,
68
- })
69
- return true
70
- } catch {
71
- return false
72
- }
73
- }
74
-
75
- export async function runInSandbox(command: string, cwd: string): Promise<ToolResult> {
76
- if (!sandboxConfig.enabled) {
77
- return {
78
- name: "sandbox",
79
- output: { error: "Sandbox is not enabled" },
80
- isError: true,
81
- }
82
- }
83
-
84
- const dockerAvailable = await isDockerAvailable()
85
- if (!dockerAvailable) {
86
- return {
87
- name: "sandbox",
88
- output: { error: "Docker is not available. Install Docker to use sandbox mode." },
89
- isError: true,
90
- }
91
- }
92
-
93
- const workspaceMount = `${path.resolve(cwd)}:${SANDBOX_WORKDIR}`
94
- const memoryLimit = `${sandboxConfig.maxMemoryMB}m`
95
- const timeout = Math.min(sandboxConfig.timeoutMs, 120_000)
96
-
97
- const args = [
98
- "run",
99
- "--rm",
100
- "--network", "none",
101
- "--memory", memoryLimit,
102
- "--cpus", "1",
103
- "--timeout", String(Math.ceil(timeout / 1000)),
104
- "-v", `${workspaceMount}:ro`,
105
- "--workdir", SANDBOX_WORKDIR,
106
- sandboxConfig.image,
107
- "sh", "-c", command,
108
- ]
109
-
110
- try {
111
- const { stdout, stderr } = await execFileAsync("docker", args, {
112
- timeout: timeout + 10_000,
113
- maxBuffer: MAX_OUTPUT_BYTES,
114
- })
115
-
116
- let output = ""
117
- if (stdout) output += truncateOutput(stdout)
118
- if (stderr) {
119
- output += "\n--- stderr ---\n"
120
- output += truncateOutput(stderr)
121
- }
122
-
123
- return {
124
- name: "sandbox",
125
- output: {
126
- stdout: output.trim(),
127
- exit_code: 0,
128
- sandboxed: true,
129
- },
130
- isError: false,
131
- }
132
- } catch (err: unknown) {
133
- const execErr = err as { stdout?: string; stderr?: string; killed?: boolean; code?: number }
134
- let output = ""
135
- if (execErr.stdout) output += truncateOutput(execErr.stdout)
136
- if (execErr.stderr) {
137
- output += "\n--- stderr ---\n"
138
- output += truncateOutput(execErr.stderr)
139
- }
140
- if (execErr.killed) {
141
- output += `\n--- Process killed (timeout or memory limit) ---`
142
- }
143
-
144
- return {
145
- name: "sandbox",
146
- output: {
147
- stdout: output.trim(),
148
- exit_code: typeof execErr.code === "number" ? execErr.code : 1,
149
- sandboxed: true,
150
- timed_out: execErr.killed ?? false,
151
- },
152
- isError: true,
153
- }
154
- }
155
- }
156
-
157
- export async function writeFileInSandbox(
158
- filePath: string,
159
- content: string,
160
- cwd: string,
161
- ): Promise<ToolResult> {
162
- if (!sandboxConfig.enabled) {
163
- return {
164
- name: "sandbox",
165
- output: { error: "Sandbox is not enabled" },
166
- isError: true,
167
- }
168
- }
169
-
170
- // Write to a temp file on host, then copy into container
171
- const tmpDir = path.join(os.tmpdir(), "llmtune-sandbox")
172
- fs.mkdirSync(tmpDir, { recursive: true })
173
- const tmpFile = path.join(tmpDir, `write-${Date.now()}.tmp`)
174
- fs.writeFileSync(tmpFile, content, "utf-8")
175
-
176
- try {
177
- const absTarget = path.resolve(cwd, filePath)
178
- const containerPath = `${SANDBOX_WORKDIR}/${path.relative(cwd, absTarget).replace(/\\/g, "/")}`
179
- const workspaceMount = `${path.resolve(cwd)}:${SANDBOX_WORKDIR}`
180
-
181
- const dockerArgs = [
182
- "run",
183
- "--rm",
184
- "--network", "none",
185
- "--memory", `${sandboxConfig.maxMemoryMB}m`,
186
- "-v", `${workspaceMount}`,
187
- "-v", `${tmpFile}:/tmp/write-content.tmp:ro`,
188
- "--workdir", SANDBOX_WORKDIR,
189
- sandboxConfig.image,
190
- "sh", "-c", `mkdir -p "$(dirname '${containerPath}')" && cp /tmp/write-content.tmp '${containerPath}'`,
191
- ]
192
-
193
- await execFileAsync("docker", dockerArgs, { timeout: 30_000 })
194
-
195
- return {
196
- name: "sandbox",
197
- output: {
198
- type: "text",
199
- filePath,
200
- bytesWritten: Buffer.byteLength(content, "utf-8"),
201
- sandboxed: true,
202
- },
203
- isError: false,
204
- }
205
- } catch (err: unknown) {
206
- return {
207
- name: "sandbox",
208
- output: { error: `Sandbox write failed: ${(err as Error).message}` },
209
- isError: true,
210
- }
211
- } finally {
212
- try {
213
- fs.unlinkSync(tmpFile)
214
- } catch {
215
- // cleanup failure is non-critical
216
- }
217
- }
218
- }
219
-
220
- function truncateOutput(output: string): string {
221
- if (output.length > 50_000) {
222
- return output.slice(0, 50_000) + "\n... (truncated)"
223
- }
224
- return output
225
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Sandbox manager: decides when to use Docker isolation vs local execution.
3
- *
4
- * Modes:
5
- * - "off" — all tools run locally (default)
6
- * - "available" — use Docker when available, fall back to local
7
- * - "required" — require Docker for destructive tools (enterprise mode)
8
- */
9
- import type { ToolResult } from "../protocol"
10
- import { runInSandbox } from "./docker"
11
-
12
- export type SandboxMode = "off" | "available" | "required"
13
-
14
- let sandboxMode: SandboxMode = "off"
15
- let dockerAvailable: boolean | null = null
16
-
17
- export function getSandboxMode(): SandboxMode {
18
- return sandboxMode
19
- }
20
-
21
- export function setSandboxMode(mode: SandboxMode): void {
22
- sandboxMode = mode
23
- dockerAvailable = null // re-check on next use
24
- }
25
-
26
- export function isDockerAvailable(): boolean {
27
- if (dockerAvailable !== null) return dockerAvailable
28
- // Quick check: can we run `docker info`?
29
- try {
30
- const { execSync } = require("child_process") as typeof import("child_process")
31
- execSync("docker info", { timeout: 3000, stdio: "pipe" })
32
- dockerAvailable = true
33
- } catch {
34
- dockerAvailable = false
35
- }
36
- return dockerAvailable
37
- }
38
-
39
- /**
40
- * Execute a bash command, optionally in a Docker sandbox.
41
- */
42
- export async function sandboxedExec(
43
- command: string,
44
- cwd: string,
45
- timeout: number,
46
- ): Promise<ToolResult> {
47
- if (sandboxMode === "off") {
48
- return localExec(command, cwd, timeout)
49
- }
50
-
51
- if (sandboxMode === "available" && !isDockerAvailable()) {
52
- return localExec(command, cwd, timeout)
53
- }
54
-
55
- if (sandboxMode === "required" && !isDockerAvailable()) {
56
- return {
57
- name: "bash",
58
- output: {
59
- error: "Enterprise mode requires Docker but Docker is not available. Install Docker or disable enterprise mode.",
60
- },
61
- isError: true,
62
- }
63
- }
64
-
65
- return runInSandbox(command, cwd)
66
- }
67
-
68
- function localExec(command: string, cwd: string, timeout: number): ToolResult {
69
- const { execSync } = require("child_process") as typeof import("child_process")
70
- try {
71
- const stdout = execSync(command, {
72
- cwd,
73
- timeout,
74
- maxBuffer: 1024 * 1024 * 10,
75
- encoding: "utf-8",
76
- shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash",
77
- })
78
- return { name: "bash", output: { stdout: stdout || "(no output)", exitCode: 0 }, isError: false }
79
- } catch (err: unknown) {
80
- const e = err as { stdout?: string; stderr?: string; status?: number; killed?: boolean }
81
- let output = ""
82
- if (e.stdout) output += String(e.stdout).slice(0, 50_000)
83
- if (e.stderr) output += "\n--- stderr ---\n" + String(e.stderr).slice(0, 50_000)
84
- if (e.killed) output += `\n--- Process timed out after ${timeout}ms ---`
85
- return {
86
- name: "bash",
87
- output: { stdout: output.trim(), exitCode: e.status ?? 1 },
88
- isError: true,
89
- }
90
- }
91
- }
@@ -1,60 +0,0 @@
1
- import { createInterface } from "readline"
2
- import type { Tool, ToolSpec, ToolResult, ToolContext } from "../protocol"
3
-
4
- export const askUserTool: Tool = {
5
- spec(): ToolSpec {
6
- return {
7
- name: "ask_user",
8
- description:
9
- "Ask the user a question and wait for their response. Use when you need clarification, confirmation, or additional information before proceeding.",
10
- inputSchema: {
11
- type: "object",
12
- properties: {
13
- question: {
14
- type: "string",
15
- description: "The question to ask the user",
16
- },
17
- options: {
18
- type: "array",
19
- items: { type: "string" },
20
- description: "Optional list of choices for the user to pick from",
21
- },
22
- },
23
- required: ["question"],
24
- },
25
- isReadOnly: true,
26
- }
27
- },
28
-
29
- async run(input: Record<string, unknown>, _ctx: ToolContext): Promise<ToolResult> {
30
- const question = String(input.question ?? "")
31
- if (!question) {
32
- return { name: "ask_user", output: { error: "question is required" }, isError: true }
33
- }
34
-
35
- const options = Array.isArray(input.options) ? input.options.map(String) : []
36
-
37
- if (options.length > 0) {
38
- console.log(`\n ${question}`)
39
- options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`))
40
- console.log()
41
- } else {
42
- console.log(`\n ${question}\n`)
43
- }
44
-
45
- const rl = createInterface({ input: process.stdin, output: process.stdout })
46
-
47
- const answer = await new Promise<string>((resolve) => {
48
- rl.question(" > ", (ans) => {
49
- rl.close()
50
- resolve(ans.trim())
51
- })
52
- })
53
-
54
- return {
55
- name: "ask_user",
56
- output: { question, answer, selectedOption: options.length > 0 ? parseInt(answer) || 0 : undefined },
57
- isError: false,
58
- }
59
- },
60
- }
@@ -1,97 +0,0 @@
1
- import { exec } from "child_process"
2
- import { promisify } from "util"
3
- import type { Tool, ToolSpec, ToolResult, ToolContext } from "../protocol"
4
-
5
- const execAsync = promisify(exec)
6
-
7
- const MAX_OUTPUT = 50_000
8
- const TIMEOUT_MS = 120_000
9
-
10
- export const bashTool: Tool = {
11
- spec(): ToolSpec {
12
- return {
13
- name: "bash",
14
- description:
15
- "Execute a shell command. Returns stdout, stderr, and exit code. Use for running builds, tests, git commands, and other shell operations.",
16
- inputSchema: {
17
- type: "object",
18
- properties: {
19
- command: {
20
- type: "string",
21
- description: "The command to execute",
22
- },
23
- timeout: {
24
- type: "number",
25
- description: "Optional timeout in ms (default 120000, max 300000)",
26
- },
27
- },
28
- required: ["command"],
29
- },
30
- isReadOnly: false,
31
- isDestructive: true,
32
- }
33
- },
34
-
35
- async run(
36
- input: Record<string, unknown>,
37
- _ctx: ToolContext
38
- ): Promise<ToolResult> {
39
- const command = String(input.command ?? "")
40
- const timeout = Math.min(
41
- typeof input.timeout === "number" ? input.timeout : TIMEOUT_MS,
42
- 300_000
43
- )
44
-
45
- if (!command.trim()) {
46
- return { name: "bash", output: { error: "command is required" }, isError: true }
47
- }
48
-
49
- try {
50
- const { stdout, stderr } = await execAsync(command, {
51
- maxBuffer: 1024 * 1024 * 10,
52
- timeout,
53
- shell: process.platform === "win32" ? "cmd" : "/bin/bash",
54
- })
55
-
56
- let output = ""
57
- if (stdout) {
58
- output += stdout.length > MAX_OUTPUT
59
- ? stdout.slice(0, MAX_OUTPUT) + "\n... (truncated)"
60
- : stdout
61
- }
62
- if (stderr) {
63
- output += "\n--- stderr ---\n"
64
- output += stderr.length > MAX_OUTPUT
65
- ? stderr.slice(0, MAX_OUTPUT) + "\n... (truncated)"
66
- : stderr
67
- }
68
-
69
- return {
70
- name: "bash",
71
- output: { stdout: output.trim(), exit_code: 0 },
72
- isError: false,
73
- }
74
- } catch (err: unknown) {
75
- const e = err as { stdout?: string; stderr?: string; code?: number | string; killed?: boolean }
76
- const stdout = e.stdout || ""
77
- const stderr = e.stderr || ""
78
- const exitCode = typeof e.code === "number" ? e.code : 1
79
-
80
- let output = ""
81
- if (stdout) output += stdout.slice(0, MAX_OUTPUT)
82
- if (stderr) {
83
- output += "\n--- stderr ---\n"
84
- output += stderr.slice(0, MAX_OUTPUT)
85
- }
86
- if (e.killed) {
87
- output += `\n--- Process timed out after ${timeout}ms ---`
88
- }
89
-
90
- return {
91
- name: "bash",
92
- output: { stdout: output.trim(), exit_code: exitCode },
93
- isError: true,
94
- }
95
- }
96
- },
97
- }
@@ -1,111 +0,0 @@
1
- import * as fs from "fs/promises"
2
- import * as path from "path"
3
- import type { Tool, ToolResult, ToolContext } from "../protocol"
4
-
5
- export const editTool: Tool = {
6
- spec() {
7
- return {
8
- name: "edit",
9
- description:
10
- "Edit a file by replacing old text with new text. Supports replacing all occurrences.",
11
- inputSchema: {
12
- type: "object",
13
- properties: {
14
- file_path: {
15
- type: "string",
16
- description: "Path to the file to edit",
17
- },
18
- old_string: {
19
- type: "string",
20
- description: "The text to find and replace",
21
- },
22
- new_string: {
23
- type: "string",
24
- description: "The replacement text",
25
- },
26
- replace_all: {
27
- type: "boolean",
28
- description: "Replace all occurrences (default: false)",
29
- },
30
- },
31
- required: ["file_path", "old_string", "new_string"],
32
- },
33
- isReadOnly: false,
34
- isDestructive: true,
35
- }
36
- },
37
-
38
- async run(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
39
- const filePath = String(input.file_path ?? "")
40
- const oldString = String(input.old_string ?? "")
41
- const newString = String(input.new_string ?? "")
42
- const replaceAll = Boolean(input.replace_all)
43
-
44
- if (!filePath) {
45
- return { name: "edit", output: { error: "file_path is required" }, isError: true }
46
- }
47
- if (!oldString) {
48
- return { name: "edit", output: { error: "old_string is required" }, isError: true }
49
- }
50
-
51
- const resolved = path.resolve(ctx.cwd, filePath)
52
-
53
- let content: string
54
- try {
55
- content = await fs.readFile(resolved, "utf-8")
56
- } catch (err: unknown) {
57
- return {
58
- name: "edit",
59
- output: { error: `Failed to read file: ${(err as Error).message}` },
60
- isError: true,
61
- }
62
- }
63
-
64
- if (!content.includes(oldString)) {
65
- return {
66
- name: "edit",
67
- output: {
68
- error: `old_string not found in ${filePath}`,
69
- hint: "Make sure the old_string matches exactly, including whitespace.",
70
- },
71
- isError: true,
72
- }
73
- }
74
-
75
- const occurrences = content.split(oldString).length - 1
76
- if (occurrences > 1 && !replaceAll) {
77
- return {
78
- name: "edit",
79
- output: {
80
- error: `Found ${occurrences} occurrences. Use replace_all: true or provide more specific old_string.`,
81
- occurrences,
82
- },
83
- isError: true,
84
- }
85
- }
86
-
87
- const newContent = replaceAll
88
- ? content.split(oldString).join(newString)
89
- : content.replace(oldString, newString)
90
-
91
- try {
92
- await fs.writeFile(resolved, newContent, "utf-8")
93
- } catch (err: unknown) {
94
- return {
95
- name: "edit",
96
- output: { error: `Failed to write file: ${(err as Error).message}` },
97
- isError: true,
98
- }
99
- }
100
-
101
- return {
102
- name: "edit",
103
- output: {
104
- file_path: filePath,
105
- replacements: replaceAll ? occurrences : 1,
106
- status: "edited",
107
- },
108
- isError: false,
109
- }
110
- },
111
- }
@@ -1,68 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import { glob } from "glob"
4
- import type { Tool, ToolSpec, ToolResult, ToolContext } from "../protocol"
5
-
6
- export const globTool: Tool = {
7
- spec(): ToolSpec {
8
- return {
9
- name: "glob",
10
- description:
11
- "Search for files matching a glob pattern. Returns matching file paths relative to the workspace root.",
12
- inputSchema: {
13
- type: "object",
14
- properties: {
15
- pattern: {
16
- type: "string",
17
- description: "Glob pattern (e.g. '**/*.ts', 'src/**/*.py')",
18
- },
19
- path: {
20
- type: "string",
21
- description: "Base directory for the search (default: workspace root)",
22
- },
23
- },
24
- required: ["pattern"],
25
- },
26
- isReadOnly: true,
27
- }
28
- },
29
-
30
- async run(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
31
- const pattern = String(input.pattern ?? "")
32
- const baseDir = String(input.path ?? ctx.workspaceRoot)
33
-
34
- if (!pattern) {
35
- return { name: "glob", output: { error: "pattern is required" }, isError: true }
36
- }
37
-
38
- try {
39
- const matches = await glob(pattern, {
40
- cwd: baseDir,
41
- nodir: true,
42
- absolute: false,
43
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.next/**"],
44
- })
45
-
46
- const truncated = matches.length > 500
47
- const limited = truncated ? matches.slice(0, 500) : matches
48
-
49
- return {
50
- name: "glob",
51
- output: {
52
- pattern,
53
- baseDir,
54
- matches: limited,
55
- numFiles: matches.length,
56
- truncated,
57
- },
58
- isError: false,
59
- }
60
- } catch (err: unknown) {
61
- return {
62
- name: "glob",
63
- output: { error: String((err as Error).message ?? err) },
64
- isError: true,
65
- }
66
- }
67
- },
68
- }