@metabob/minibob 0.1.2
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/ARCHITECTURE.md +255 -0
- package/CHANGELOG.md +112 -0
- package/README.md +380 -0
- package/bin/minibob.js +36 -0
- package/dist/acp-gossip.d.ts +72 -0
- package/dist/acp-gossip.d.ts.map +1 -0
- package/dist/acp-gossip.js +156 -0
- package/dist/acp-gossip.js.map +1 -0
- package/dist/acp.d.ts +62 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +292 -0
- package/dist/acp.js.map +1 -0
- package/dist/activity.d.ts +157 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +518 -0
- package/dist/activity.js.map +1 -0
- package/dist/agent-runtime.d.ts +104 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/boredom.d.ts +125 -0
- package/dist/boredom.d.ts.map +1 -0
- package/dist/boredom.js +244 -0
- package/dist/boredom.js.map +1 -0
- package/dist/cli/acp-server.d.ts +23 -0
- package/dist/cli/acp-server.d.ts.map +1 -0
- package/dist/cli/burrow.d.ts +26 -0
- package/dist/cli/burrow.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/goal.d.ts +22 -0
- package/dist/cli/goal.d.ts.map +1 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/instance-registry.d.ts +78 -0
- package/dist/cli/instance-registry.d.ts.map +1 -0
- package/dist/cli/observe.d.ts +35 -0
- package/dist/cli/observe.d.ts.map +1 -0
- package/dist/cli/vessel.d.ts +14 -0
- package/dist/cli/vessel.d.ts.map +1 -0
- package/dist/composition-observer.d.ts +96 -0
- package/dist/composition-observer.d.ts.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +128 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/Dockerfile +35 -0
- package/dist/environment.d.ts +72 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +142 -0
- package/dist/environment.js.map +1 -0
- package/dist/goal-processor.d.ts +165 -0
- package/dist/goal-processor.d.ts.map +1 -0
- package/dist/helm/minibob-cluster/Chart.yaml +13 -0
- package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
- package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
- package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
- package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
- package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
- package/dist/helm/minibob-cluster/values-local.yaml +41 -0
- package/dist/helm/minibob-cluster/values-production.yaml +57 -0
- package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
- package/dist/helm/minibob-cluster/values.yaml +127 -0
- package/dist/improviser.d.ts +74 -0
- package/dist/improviser.d.ts.map +1 -0
- package/dist/impulse-filter.d.ts +74 -0
- package/dist/impulse-filter.d.ts.map +1 -0
- package/dist/impulse.d.ts +92 -0
- package/dist/impulse.d.ts.map +1 -0
- package/dist/impulse.js +234 -0
- package/dist/impulse.js.map +1 -0
- package/dist/lib.d.ts +29 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +18561 -0
- package/dist/lib.js.map +98 -0
- package/dist/lifecycle-hooks.d.ts +99 -0
- package/dist/lifecycle-hooks.d.ts.map +1 -0
- package/dist/lifecycle-hooks.js +135 -0
- package/dist/lifecycle-hooks.js.map +1 -0
- package/dist/llm.d.ts +31 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +349 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp-activity-bridge.d.ts +66 -0
- package/dist/mcp-activity-bridge.d.ts.map +1 -0
- package/dist/mcp-activity-bridge.js +126 -0
- package/dist/mcp-activity-bridge.js.map +1 -0
- package/dist/mcp.d.ts +216 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +292 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory-agent.d.ts +92 -0
- package/dist/memory-agent.d.ts.map +1 -0
- package/dist/memory-agent.js +277 -0
- package/dist/memory-agent.js.map +1 -0
- package/dist/runtime-mapping.d.ts +97 -0
- package/dist/runtime-mapping.d.ts.map +1 -0
- package/dist/search-first-executor.d.ts +113 -0
- package/dist/search-first-executor.d.ts.map +1 -0
- package/dist/session.d.ts +48 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/template-extractor.d.ts +9 -0
- package/dist/template-extractor.d.ts.map +1 -0
- package/dist/template-generator.d.ts +12 -0
- package/dist/template-generator.d.ts.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +771 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +503 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/understanding/analyzer.d.ts +55 -0
- package/dist/understanding/analyzer.d.ts.map +1 -0
- package/dist/understanding/explorer.d.ts +73 -0
- package/dist/understanding/explorer.d.ts.map +1 -0
- package/dist/understanding/index.d.ts +7 -0
- package/dist/understanding/index.d.ts.map +1 -0
- package/dist/understanding/types.d.ts +136 -0
- package/dist/understanding/types.d.ts.map +1 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +106 -0
- package/dist/validation.js.map +1 -0
- package/dist/vessel-bootstrap.d.ts +190 -0
- package/dist/vessel-bootstrap.d.ts.map +1 -0
- package/dist/vessel-registry.d.ts +229 -0
- package/dist/vessel-registry.d.ts.map +1 -0
- package/index.ts +1329 -0
- package/package.json +54 -0
- package/src/acp-gossip.ts +193 -0
- package/src/acp.ts +362 -0
- package/src/activity.ts +1464 -0
- package/src/agent-runtime.ts +365 -0
- package/src/boredom.ts +423 -0
- package/src/cli/acp-server.ts +377 -0
- package/src/cli/burrow.ts +896 -0
- package/src/cli/doctor.ts +526 -0
- package/src/cli/goal.ts +224 -0
- package/src/cli/index.ts +147 -0
- package/src/cli/instance-registry.ts +271 -0
- package/src/cli/observe.ts +682 -0
- package/src/cli/vessel.ts +287 -0
- package/src/components/SystemOverview.tsx +331 -0
- package/src/composition-observer.ts +449 -0
- package/src/config.ts +172 -0
- package/src/environment.ts +167 -0
- package/src/goal-processor.ts +654 -0
- package/src/improviser.ts +591 -0
- package/src/impulse-filter.ts +273 -0
- package/src/impulse.ts +311 -0
- package/src/lib.ts +147 -0
- package/src/lifecycle-hooks.ts +181 -0
- package/src/llm.ts +434 -0
- package/src/mcp-activity-bridge.ts +158 -0
- package/src/mcp.ts +747 -0
- package/src/memory-agent.ts +316 -0
- package/src/runtime-mapping.ts +527 -0
- package/src/search-first-executor.ts +666 -0
- package/src/session.ts +141 -0
- package/src/template-extractor.ts +256 -0
- package/src/template-generator.ts +130 -0
- package/src/tools.ts +924 -0
- package/src/types.ts +497 -0
- package/src/understanding/analyzer.ts +354 -0
- package/src/understanding/explorer.ts +488 -0
- package/src/understanding/index.ts +27 -0
- package/src/understanding/types.ts +153 -0
- package/src/validation.ts +125 -0
- package/src/vessel-bootstrap.ts +440 -0
- package/src/vessel-registry.ts +621 -0
- package/templates/core/edit-file.json +85 -0
- package/templates/understanding/diagnose-problem.json +32 -0
- package/templates/understanding/explore-codebase-v2.json +57 -0
- package/templates/understanding/explore-codebase.json +37 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob Built-in Tools
|
|
3
|
+
*
|
|
4
|
+
* Minimal set of tools for activity execution and self-development.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY: Path validation, command whitelisting, and input sanitization
|
|
7
|
+
* enforced to prevent command injection and path traversal attacks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ToolDefinition, ToolHandler, ToolResult } from "./types"
|
|
11
|
+
|
|
12
|
+
// Re-export ToolDefinition for consumers that import from tools.ts
|
|
13
|
+
export type { ToolDefinition } from "./types"
|
|
14
|
+
import * as path from "node:path"
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// SECURITY UTILITIES
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Allowed bash commands (whitelist approach)
|
|
22
|
+
* Only these commands can be executed via the bash tool
|
|
23
|
+
*/
|
|
24
|
+
const ALLOWED_BASH_COMMANDS = new Set([
|
|
25
|
+
// Version control
|
|
26
|
+
"git",
|
|
27
|
+
// Package managers
|
|
28
|
+
"npm", "pnpm", "yarn", "bun",
|
|
29
|
+
// Build tools
|
|
30
|
+
"make", "cmake", "cargo", "go",
|
|
31
|
+
// File operations (read-only)
|
|
32
|
+
"ls", "cat", "head", "tail", "find", "grep", "rg", "fd",
|
|
33
|
+
// Text processing
|
|
34
|
+
"sed", "awk", "cut", "sort", "uniq", "wc",
|
|
35
|
+
// Directory operations (safe)
|
|
36
|
+
"pwd", "cd", "mkdir", "rmdir",
|
|
37
|
+
// Testing
|
|
38
|
+
"pytest", "jest", "vitest", "cargo test",
|
|
39
|
+
// Linting/formatting
|
|
40
|
+
"eslint", "prettier", "black", "rustfmt",
|
|
41
|
+
// Type checking
|
|
42
|
+
"tsc", "mypy", "cargo check",
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dangerous command patterns that are always blocked
|
|
47
|
+
*/
|
|
48
|
+
const BLOCKED_COMMAND_PATTERNS = [
|
|
49
|
+
/rm\s+-rf\s+\//, // rm -rf /
|
|
50
|
+
/:\(\)\{.*\}/, // Fork bombs
|
|
51
|
+
/>\s*\/dev\/sd[a-z]/, // Writing to raw disk
|
|
52
|
+
/mkfs/, // Filesystem formatting
|
|
53
|
+
/dd\s+if=.*of=\/dev/, // Direct disk write
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate and sanitize a bash command
|
|
58
|
+
* @throws Error if command is not allowed
|
|
59
|
+
*/
|
|
60
|
+
function validateBashCommand(command: string): void {
|
|
61
|
+
// Check blocked patterns first
|
|
62
|
+
for (const pattern of BLOCKED_COMMAND_PATTERNS) {
|
|
63
|
+
if (pattern.test(command)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Blocked dangerous command pattern: ${pattern.source}`
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract the first command (before pipes, semicolons, etc.)
|
|
71
|
+
const firstCommand = command
|
|
72
|
+
.split(/[|;&]/)
|
|
73
|
+
[0]?.trim()
|
|
74
|
+
.split(/\s+/)
|
|
75
|
+
[0]
|
|
76
|
+
|
|
77
|
+
if (!firstCommand) {
|
|
78
|
+
throw new Error("Empty command")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if command is in whitelist
|
|
82
|
+
if (!ALLOWED_BASH_COMMANDS.has(firstCommand)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Command '${firstCommand}' not in whitelist. Allowed: ${Array.from(ALLOWED_BASH_COMMANDS).join(", ")}`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate a file path is within the working directory
|
|
91
|
+
* Prevents path traversal attacks (../../etc/passwd)
|
|
92
|
+
* @returns Canonicalized absolute path
|
|
93
|
+
* @throws Error if path escapes working directory
|
|
94
|
+
*/
|
|
95
|
+
function validatePath(filePath: string, workingDirectory: string): string {
|
|
96
|
+
// Resolve to absolute path
|
|
97
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
98
|
+
? path.resolve(filePath)
|
|
99
|
+
: path.resolve(workingDirectory, filePath)
|
|
100
|
+
|
|
101
|
+
// Canonicalize (removes .., ., symlinks)
|
|
102
|
+
const canonicalPath = path.normalize(absolutePath)
|
|
103
|
+
const canonicalWorkDir = path.normalize(path.resolve(workingDirectory))
|
|
104
|
+
|
|
105
|
+
// Check if path is within working directory
|
|
106
|
+
if (!canonicalPath.startsWith(canonicalWorkDir)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Path traversal blocked: ${filePath} resolves outside working directory (${workingDirectory})`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return canonicalPath
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// TOOL DEFINITIONS
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
export const toolDefinitions: Record<string, ToolDefinition> = {
|
|
120
|
+
bash: {
|
|
121
|
+
name: "bash",
|
|
122
|
+
description: "Execute a shell command. Returns stdout and stderr.",
|
|
123
|
+
parameters: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
command: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "The shell command to execute",
|
|
129
|
+
},
|
|
130
|
+
cwd: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Working directory for the command (optional)",
|
|
133
|
+
},
|
|
134
|
+
timeout: {
|
|
135
|
+
type: "number",
|
|
136
|
+
description: "Timeout in milliseconds (default: 60000)",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ["command"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
read: {
|
|
144
|
+
name: "read",
|
|
145
|
+
description: "Read a file from the filesystem. Returns file content with line numbers.",
|
|
146
|
+
parameters: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
path: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "Path to the file to read",
|
|
152
|
+
},
|
|
153
|
+
offset: {
|
|
154
|
+
type: "number",
|
|
155
|
+
description: "Line number to start reading from (0-based)",
|
|
156
|
+
},
|
|
157
|
+
limit: {
|
|
158
|
+
type: "number",
|
|
159
|
+
description: "Maximum number of lines to read (default: 2000)",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ["path"],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
write: {
|
|
167
|
+
name: "write",
|
|
168
|
+
description: "Write content to a file. Creates directories if needed.",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
path: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "Path to the file to write",
|
|
175
|
+
},
|
|
176
|
+
content: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "Content to write to the file",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ["path", "content"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
edit: {
|
|
186
|
+
name: "edit",
|
|
187
|
+
description: "Edit a file by replacing exact text. The oldString must match exactly.",
|
|
188
|
+
parameters: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
path: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Path to the file to edit",
|
|
194
|
+
},
|
|
195
|
+
oldString: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Exact text to find and replace",
|
|
198
|
+
},
|
|
199
|
+
newString: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Text to replace with",
|
|
202
|
+
},
|
|
203
|
+
replaceAll: {
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "Replace all occurrences (default: false)",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ["path", "oldString", "newString"],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
glob: {
|
|
213
|
+
name: "glob",
|
|
214
|
+
description: "Find files matching a glob pattern.",
|
|
215
|
+
parameters: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
pattern: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "Glob pattern to match (e.g., '**/*.ts')",
|
|
221
|
+
},
|
|
222
|
+
cwd: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "Directory to search from (optional)",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["pattern"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
grep: {
|
|
232
|
+
name: "grep",
|
|
233
|
+
description: "Search file contents using a regex pattern.",
|
|
234
|
+
parameters: {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
pattern: {
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Regex pattern to search for",
|
|
240
|
+
},
|
|
241
|
+
include: {
|
|
242
|
+
type: "string",
|
|
243
|
+
description: "File pattern to include (e.g., '*.ts')",
|
|
244
|
+
},
|
|
245
|
+
cwd: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Directory to search in (optional)",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ["pattern"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
list: {
|
|
255
|
+
name: "list",
|
|
256
|
+
description: "List files and directories in a path.",
|
|
257
|
+
parameters: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
path: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Directory path to list",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
required: ["path"],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
git: {
|
|
270
|
+
name: "git",
|
|
271
|
+
description: "Execute git commands for version control operations.",
|
|
272
|
+
parameters: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
command: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "Git subcommand (e.g., 'status', 'add', 'commit', 'diff')",
|
|
278
|
+
enum: ["status", "add", "commit", "diff", "log", "branch", "checkout", "push", "pull"],
|
|
279
|
+
},
|
|
280
|
+
args: {
|
|
281
|
+
type: "array",
|
|
282
|
+
description: "Arguments for the git command",
|
|
283
|
+
},
|
|
284
|
+
cwd: {
|
|
285
|
+
type: "string",
|
|
286
|
+
description: "Working directory (optional)",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
required: ["command"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
activity: {
|
|
294
|
+
name: "activity",
|
|
295
|
+
description: "Execute an activity template (enables nested activity execution). Template can be loaded from MCP backend or local file.",
|
|
296
|
+
parameters: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
templateId: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "Activity template ID (from MCP) or path (local file)",
|
|
302
|
+
},
|
|
303
|
+
variables: {
|
|
304
|
+
type: "object",
|
|
305
|
+
description: "Variables to pass to the activity",
|
|
306
|
+
},
|
|
307
|
+
reason: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Reason for executing this activity",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
required: ["templateId"],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
search_activities: {
|
|
317
|
+
name: "search_activities",
|
|
318
|
+
description: "Search for existing activity templates by category or keyword. Use this BEFORE creating new activities to avoid duplication.",
|
|
319
|
+
parameters: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
category: {
|
|
323
|
+
type: "string",
|
|
324
|
+
description: "Filter by category (feature, bugfix, refactor, tool, infrastructure)",
|
|
325
|
+
enum: ["feature", "bugfix", "refactor", "tool", "infrastructure"],
|
|
326
|
+
},
|
|
327
|
+
verbose: {
|
|
328
|
+
type: "boolean",
|
|
329
|
+
description: "Show full details (default: false, returns compact list)",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
required: [],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
create_activity_goal_seeking: {
|
|
337
|
+
name: "create_activity_goal_seeking",
|
|
338
|
+
description: "Create a new activity template for a non-trivial task. The agent will autonomously generate tasks and learn from execution. Use this when no existing activity matches your need.",
|
|
339
|
+
parameters: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
goalDescription: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "High-level goal description (e.g., 'Deploy app to production with health checks')",
|
|
345
|
+
},
|
|
346
|
+
templateName: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Human-readable template name (e.g., 'Deploy Production Application')",
|
|
349
|
+
},
|
|
350
|
+
category: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "Template category",
|
|
353
|
+
enum: ["feature", "bugfix", "refactor", "tool", "infrastructure"],
|
|
354
|
+
},
|
|
355
|
+
variables: {
|
|
356
|
+
type: "object",
|
|
357
|
+
description: "Context variables for goal decomposition",
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
required: ["goalDescription", "templateName", "category"],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
impulse_create: {
|
|
365
|
+
name: "impulse_create",
|
|
366
|
+
description: "Create a new impulse for context management.",
|
|
367
|
+
parameters: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
id: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Unique impulse ID",
|
|
373
|
+
},
|
|
374
|
+
type: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Impulse pointer type",
|
|
377
|
+
enum: ["memo", "file", "activityOutput", "custom"],
|
|
378
|
+
},
|
|
379
|
+
content: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: "Content (for memo type) or path (for file type)",
|
|
382
|
+
},
|
|
383
|
+
budget: {
|
|
384
|
+
type: "number",
|
|
385
|
+
description: "Token budget for this impulse",
|
|
386
|
+
},
|
|
387
|
+
priority: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Priority level",
|
|
390
|
+
enum: ["critical", "high", "medium", "low"],
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
required: ["id", "type", "budget"],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// =============================================================================
|
|
399
|
+
// TOOL HANDLERS
|
|
400
|
+
// =============================================================================
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Options for creating tool handlers
|
|
404
|
+
*/
|
|
405
|
+
export interface ToolHandlerOptions {
|
|
406
|
+
workingDirectory: string
|
|
407
|
+
onActivityExecute?: (templateId: string, variables: Record<string, unknown>, reason?: string) => Promise<unknown>
|
|
408
|
+
onSearchActivities?: (category?: string, verbose?: boolean) => Promise<{ count: number; activities: unknown[] }>
|
|
409
|
+
onCreateActivity?: (params: {
|
|
410
|
+
goalDescription: string
|
|
411
|
+
templateName: string
|
|
412
|
+
category: string
|
|
413
|
+
variables: Record<string, unknown>
|
|
414
|
+
}) => Promise<{ templateId: string }>
|
|
415
|
+
onImpulseCreate?: (impulse: { id: string; type: string; content?: string; budget: number; priority: string }) => void
|
|
416
|
+
/**
|
|
417
|
+
* Custom domain-specific tools injected by the host application.
|
|
418
|
+
* Each entry provides a tool definition (for LLM function calling) and a handler.
|
|
419
|
+
* Custom tools are merged with built-in tools; custom names must not collide with
|
|
420
|
+
* built-in names (bash, read, write, edit, glob, grep, list, git, activity,
|
|
421
|
+
* search_activities, create_activity_goal_seeking, impulse_create).
|
|
422
|
+
*/
|
|
423
|
+
customTools?: Record<string, { definition: ToolDefinition; handler: ToolHandler }>
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create tool handlers with working directory context
|
|
428
|
+
*/
|
|
429
|
+
export function createToolHandlers(options: string | ToolHandlerOptions): Record<string, ToolHandler> {
|
|
430
|
+
// Support backward compatibility - if string passed, convert to options
|
|
431
|
+
const opts: ToolHandlerOptions = typeof options === "string"
|
|
432
|
+
? { workingDirectory: options }
|
|
433
|
+
: options
|
|
434
|
+
|
|
435
|
+
const workingDirectory = opts.workingDirectory
|
|
436
|
+
const resolvePath = (path: string): string => {
|
|
437
|
+
if (path.startsWith("/")) return path
|
|
438
|
+
return `${workingDirectory}/${path}`
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
bash: async (params): Promise<ToolResult> => {
|
|
443
|
+
const command = params.command as string
|
|
444
|
+
const cwd = (params.cwd as string) ?? workingDirectory
|
|
445
|
+
const timeout = (params.timeout as number) ?? 60000
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
// SECURITY: Validate command against whitelist
|
|
449
|
+
validateBashCommand(command)
|
|
450
|
+
|
|
451
|
+
const proc = Bun.spawn(["sh", "-c", command], {
|
|
452
|
+
cwd,
|
|
453
|
+
stdout: "pipe",
|
|
454
|
+
stderr: "pipe",
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// Handle timeout
|
|
458
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
proc.kill()
|
|
461
|
+
reject(new Error(`Command timed out after ${timeout}ms`))
|
|
462
|
+
}, timeout)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const exitCode = await Promise.race([proc.exited, timeoutPromise])
|
|
466
|
+
const stdout = await new Response(proc.stdout).text()
|
|
467
|
+
const stderr = await new Response(proc.stderr).text()
|
|
468
|
+
|
|
469
|
+
if (exitCode !== 0) {
|
|
470
|
+
return {
|
|
471
|
+
success: false,
|
|
472
|
+
error: `Command failed with exit code ${exitCode}\nStderr: ${stderr}`,
|
|
473
|
+
output: stdout,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
output: stdout + (stderr ? `\nStderr: ${stderr}` : ""),
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: error instanceof Error ? error.message : String(error),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
read: async (params): Promise<ToolResult> => {
|
|
490
|
+
// Validate path parameter exists and is a string
|
|
491
|
+
if (!params.path || typeof params.path !== 'string') {
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
error: `Invalid path parameter: expected string, got ${typeof params.path}. Params: ${JSON.stringify(params)}`,
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const filePath = params.path as string
|
|
499
|
+
const offset = (params.offset as number) ?? 0
|
|
500
|
+
const limit = (params.limit as number) ?? 2000
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
// SECURITY: Validate path is within working directory
|
|
504
|
+
const validatedPath = validatePath(filePath, workingDirectory)
|
|
505
|
+
|
|
506
|
+
// Debug logging
|
|
507
|
+
console.log(`[read] Attempting to read file:`, {
|
|
508
|
+
originalPath: filePath,
|
|
509
|
+
validatedPath,
|
|
510
|
+
validatedPathType: typeof validatedPath,
|
|
511
|
+
workingDirectory
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
let file
|
|
515
|
+
try {
|
|
516
|
+
file = Bun.file(validatedPath)
|
|
517
|
+
} catch (bunError) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: `Bun.file() failed for '${validatedPath}': ${bunError instanceof Error ? bunError.message : String(bunError)}`,
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!(await file.exists())) {
|
|
525
|
+
return {
|
|
526
|
+
success: false,
|
|
527
|
+
error: `File not found: ${validatedPath}`,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const content = await file.text()
|
|
532
|
+
const lines = content.split("\n")
|
|
533
|
+
const selectedLines = lines.slice(offset, offset + limit)
|
|
534
|
+
|
|
535
|
+
// Format with line numbers
|
|
536
|
+
const formatted = selectedLines
|
|
537
|
+
.map((line, i) => {
|
|
538
|
+
const lineNum = (offset + i + 1).toString().padStart(5, "0")
|
|
539
|
+
return `${lineNum}| ${line}`
|
|
540
|
+
})
|
|
541
|
+
.join("\n")
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
output: `<file>\n${formatted}\n</file>`,
|
|
546
|
+
metadata: {
|
|
547
|
+
totalLines: lines.length,
|
|
548
|
+
readLines: selectedLines.length,
|
|
549
|
+
offset,
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
// Provide detailed error information for debugging
|
|
554
|
+
const errorMsg = error instanceof Error ? error.message : String(error)
|
|
555
|
+
return {
|
|
556
|
+
success: false,
|
|
557
|
+
error: `Failed to read file '${filePath}': ${errorMsg}`,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
write: async (params): Promise<ToolResult> => {
|
|
563
|
+
const filePath = params.path as string
|
|
564
|
+
const content = params.content as string
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
// SECURITY: Validate path is within working directory
|
|
568
|
+
const validatedPath = validatePath(filePath, workingDirectory)
|
|
569
|
+
|
|
570
|
+
// Create directory if needed
|
|
571
|
+
const dir = validatedPath.substring(0, validatedPath.lastIndexOf("/"))
|
|
572
|
+
if (dir) {
|
|
573
|
+
await Bun.$`mkdir -p ${dir}`.quiet()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
await Bun.write(validatedPath, content)
|
|
577
|
+
return {
|
|
578
|
+
success: true,
|
|
579
|
+
output: `Wrote ${content.length} bytes to ${filePath}`,
|
|
580
|
+
}
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: error instanceof Error ? error.message : String(error),
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
edit: async (params): Promise<ToolResult> => {
|
|
590
|
+
const filePath = params.path as string
|
|
591
|
+
const oldString = params.oldString as string
|
|
592
|
+
const newString = params.newString as string
|
|
593
|
+
const replaceAll = (params.replaceAll as boolean) ?? false
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
// SECURITY: Validate path is within working directory
|
|
597
|
+
const validatedPath = validatePath(filePath, workingDirectory)
|
|
598
|
+
const file = Bun.file(validatedPath)
|
|
599
|
+
if (!(await file.exists())) {
|
|
600
|
+
return {
|
|
601
|
+
success: false,
|
|
602
|
+
error: `File not found: ${filePath}`,
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const content = await file.text()
|
|
607
|
+
|
|
608
|
+
if (!content.includes(oldString)) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: `oldString not found in file: "${oldString.substring(0, 50)}..."`,
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check for multiple matches if not replaceAll
|
|
616
|
+
if (!replaceAll) {
|
|
617
|
+
const matches = content.split(oldString).length - 1
|
|
618
|
+
if (matches > 1) {
|
|
619
|
+
return {
|
|
620
|
+
success: false,
|
|
621
|
+
error: `oldString found ${matches} times. Use replaceAll: true or provide more context.`,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const newContent = replaceAll
|
|
627
|
+
? content.split(oldString).join(newString)
|
|
628
|
+
: content.replace(oldString, newString)
|
|
629
|
+
|
|
630
|
+
await Bun.write(validatedPath, newContent)
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
success: true,
|
|
634
|
+
output: `Edited ${validatedPath}`,
|
|
635
|
+
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
return {
|
|
638
|
+
success: false,
|
|
639
|
+
error: error instanceof Error ? error.message : String(error),
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
glob: async (params): Promise<ToolResult> => {
|
|
645
|
+
const pattern = params.pattern as string
|
|
646
|
+
const cwd = (params.cwd as string) ?? workingDirectory
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const glob = new Bun.Glob(pattern)
|
|
650
|
+
const files: string[] = []
|
|
651
|
+
for await (const file of glob.scan({ cwd })) {
|
|
652
|
+
files.push(file)
|
|
653
|
+
if (files.length >= 100) break // Limit results
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
success: true,
|
|
658
|
+
output: files.length > 0 ? files.join("\n") : "No files found",
|
|
659
|
+
metadata: { count: files.length },
|
|
660
|
+
}
|
|
661
|
+
} catch (error) {
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
error: error instanceof Error ? error.message : String(error),
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
grep: async (params): Promise<ToolResult> => {
|
|
670
|
+
const pattern = params.pattern as string
|
|
671
|
+
const include = params.include as string | undefined
|
|
672
|
+
const cwd = (params.cwd as string) ?? workingDirectory
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
// Use ripgrep if available, fall back to grep
|
|
676
|
+
const rgArgs = ["rg", "-l", pattern]
|
|
677
|
+
if (include) {
|
|
678
|
+
rgArgs.push("-g", include)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const proc = Bun.spawn(rgArgs, {
|
|
682
|
+
cwd,
|
|
683
|
+
stdout: "pipe",
|
|
684
|
+
stderr: "pipe",
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
const exitCode = await proc.exited
|
|
688
|
+
const stdout = await new Response(proc.stdout).text()
|
|
689
|
+
|
|
690
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
691
|
+
// Exit code 1 means no matches, which is OK
|
|
692
|
+
return {
|
|
693
|
+
success: false,
|
|
694
|
+
error: `grep failed with exit code ${exitCode}`,
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const files = stdout.trim().split("\n").filter(Boolean)
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
output: files.length > 0 ? `Found ${files.length} files:\n${files.join("\n")}` : "No matches found",
|
|
703
|
+
metadata: { count: files.length },
|
|
704
|
+
}
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
error: error instanceof Error ? error.message : String(error),
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
list: async (params): Promise<ToolResult> => {
|
|
714
|
+
const filePath = params.path as string
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
// SECURITY: Validate path is within working directory
|
|
718
|
+
const validatedPath = validatePath(filePath, workingDirectory)
|
|
719
|
+
const dir = await Bun.$`ls -la ${validatedPath}`.text()
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
success: true,
|
|
723
|
+
output: dir,
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
return {
|
|
727
|
+
success: false,
|
|
728
|
+
error: error instanceof Error ? error.message : String(error),
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
git: async (params): Promise<ToolResult> => {
|
|
734
|
+
const command = params.command as string
|
|
735
|
+
const args = (params.args as string[]) ?? []
|
|
736
|
+
|
|
737
|
+
// Default to /repos if REPOS_PATH is set and no cwd provided
|
|
738
|
+
const defaultCwd = process.env.REPOS_PATH
|
|
739
|
+
? process.env.REPOS_PATH
|
|
740
|
+
: workingDirectory
|
|
741
|
+
const cwd = (params.cwd as string) ?? defaultCwd
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
const gitArgs = ["git", command, ...args]
|
|
745
|
+
const proc = Bun.spawn(gitArgs, {
|
|
746
|
+
cwd,
|
|
747
|
+
stdout: "pipe",
|
|
748
|
+
stderr: "pipe",
|
|
749
|
+
env: {
|
|
750
|
+
...process.env,
|
|
751
|
+
// Ensure git uses credentials from mounted config
|
|
752
|
+
GIT_CONFIG_GLOBAL: "/root/.gitconfig",
|
|
753
|
+
},
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
const exitCode = await proc.exited
|
|
757
|
+
const stdout = await new Response(proc.stdout).text()
|
|
758
|
+
const stderr = await new Response(proc.stderr).text()
|
|
759
|
+
|
|
760
|
+
if (exitCode !== 0) {
|
|
761
|
+
return {
|
|
762
|
+
success: false,
|
|
763
|
+
error: `git ${command} failed: ${stderr}`,
|
|
764
|
+
output: stdout,
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
success: true,
|
|
770
|
+
output: stdout || `git ${command} completed successfully`,
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
return {
|
|
774
|
+
success: false,
|
|
775
|
+
error: error instanceof Error ? error.message : String(error),
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
activity: async (params): Promise<ToolResult> => {
|
|
781
|
+
if (!opts.onActivityExecute) {
|
|
782
|
+
return {
|
|
783
|
+
success: false,
|
|
784
|
+
error: "Activity execution not configured (onActivityExecute callback required)",
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
const templateId = params.templateId as string
|
|
790
|
+
const variables = (params.variables as Record<string, unknown>) ?? {}
|
|
791
|
+
const reason = params.reason as string | undefined
|
|
792
|
+
|
|
793
|
+
console.log(`[Tool:activity] Executing nested activity: ${templateId}`)
|
|
794
|
+
const result = await opts.onActivityExecute(templateId, variables, reason)
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
success: true,
|
|
798
|
+
output: `Activity "${templateId}" executed successfully\n\nResult:\n${JSON.stringify(result, null, 2)}`,
|
|
799
|
+
metadata: { result },
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
return {
|
|
803
|
+
success: false,
|
|
804
|
+
error: error instanceof Error ? error.message : String(error),
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
search_activities: async (params): Promise<ToolResult> => {
|
|
810
|
+
if (!opts.onSearchActivities) {
|
|
811
|
+
return {
|
|
812
|
+
success: false,
|
|
813
|
+
error: "Activity search not configured (onSearchActivities callback required)",
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const category = params.category as string | undefined
|
|
819
|
+
const verbose = (params.verbose as boolean) ?? false
|
|
820
|
+
|
|
821
|
+
console.log(`[Tool:search_activities] Searching activities:`, { category, verbose })
|
|
822
|
+
const result = await opts.onSearchActivities(category, verbose)
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
success: true,
|
|
826
|
+
output: `Found ${result.count} activities:\n\n${JSON.stringify(result.activities, null, 2)}`,
|
|
827
|
+
metadata: { result },
|
|
828
|
+
}
|
|
829
|
+
} catch (error) {
|
|
830
|
+
return {
|
|
831
|
+
success: false,
|
|
832
|
+
error: error instanceof Error ? error.message : String(error),
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
create_activity_goal_seeking: async (params): Promise<ToolResult> => {
|
|
838
|
+
if (!opts.onCreateActivity) {
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
error: "Activity creation not configured (onCreateActivity callback required)",
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const goalDescription = params.goalDescription as string
|
|
847
|
+
const templateName = params.templateName as string
|
|
848
|
+
const category = params.category as string
|
|
849
|
+
const variables = (params.variables as Record<string, unknown>) ?? {}
|
|
850
|
+
|
|
851
|
+
console.log(`[Tool:create_activity_goal_seeking] Creating activity:`, {
|
|
852
|
+
templateName,
|
|
853
|
+
category,
|
|
854
|
+
goalDescription: goalDescription.substring(0, 100) + "...",
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const result = await opts.onCreateActivity({
|
|
858
|
+
goalDescription,
|
|
859
|
+
templateName,
|
|
860
|
+
category,
|
|
861
|
+
variables,
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
success: true,
|
|
866
|
+
output: `Activity template "${templateName}" created successfully\n\nTemplate ID: ${result.templateId}\n\nYou can now execute it with: activity(templateId: "${result.templateId}")`,
|
|
867
|
+
metadata: { result },
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return {
|
|
871
|
+
success: false,
|
|
872
|
+
error: error instanceof Error ? error.message : String(error),
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
impulse_create: async (params): Promise<ToolResult> => {
|
|
878
|
+
if (!opts.onImpulseCreate) {
|
|
879
|
+
return {
|
|
880
|
+
success: false,
|
|
881
|
+
error: "Impulse creation not configured (onImpulseCreate callback required)",
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const id = params.id as string
|
|
887
|
+
const type = params.type as string
|
|
888
|
+
const content = params.content as string | undefined
|
|
889
|
+
const budget = params.budget as number
|
|
890
|
+
const priority = params.priority as string
|
|
891
|
+
|
|
892
|
+
const impulse = { id, type, content, budget, priority }
|
|
893
|
+
opts.onImpulseCreate(impulse)
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
success: true,
|
|
897
|
+
output: `Impulse "${id}" created with type ${type} and budget ${budget} tokens`,
|
|
898
|
+
}
|
|
899
|
+
} catch (error) {
|
|
900
|
+
return {
|
|
901
|
+
success: false,
|
|
902
|
+
error: error instanceof Error ? error.message : String(error),
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
// Merge in any custom tools provided by the host application
|
|
908
|
+
...Object.fromEntries(
|
|
909
|
+
Object.entries(opts.customTools ?? {}).map(([name, { handler }]) => [name, handler])
|
|
910
|
+
),
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Get all tool definitions as an array.
|
|
916
|
+
* @param extraDefinitions Additional tool definitions from custom tools (e.g. Perspective domain tools).
|
|
917
|
+
*/
|
|
918
|
+
export function getAllToolDefinitions(extraDefinitions?: ToolDefinition[]): ToolDefinition[] {
|
|
919
|
+
const base = Object.values(toolDefinitions)
|
|
920
|
+
if (extraDefinitions && extraDefinitions.length > 0) {
|
|
921
|
+
return [...base, ...extraDefinitions]
|
|
922
|
+
}
|
|
923
|
+
return base
|
|
924
|
+
}
|