lonny-agent 0.2.3 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -326
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/project.d.ts +26 -0
- package/dist/agent/project.d.ts.map +1 -0
- package/dist/agent/project.js +303 -0
- package/dist/agent/project.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +22 -13
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/anthropic.d.ts.map +1 -1
- package/dist/agent/providers/anthropic.js +20 -2
- package/dist/agent/providers/anthropic.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/ollama.d.ts.map +1 -1
- package/dist/agent/providers/ollama.js +2 -1
- package/dist/agent/providers/ollama.js.map +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +63 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +8 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +250 -153
- package/dist/agent/session.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +1 -1
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +875 -371
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +32 -0
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +83 -10
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +58 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +366 -149
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +54 -30
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/read.d.ts +6 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +42 -15
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts +0 -6
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +2 -33
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- package/dist/tools/write_plan.d.ts +1 -1
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +14 -4
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -4
- package/dist/tui/index.js.map +1 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +30 -7
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +395 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +996 -949
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +3 -7
- package/dist/web/session-bridge.js.map +1 -1
- package/package.json +55 -54
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/index.ts +25 -25
- package/src/agent/llm.ts +4 -0
- package/src/agent/project.ts +366 -0
- package/src/agent/prompt-builder.ts +23 -13
- package/src/agent/providers/anthropic.ts +179 -172
- package/src/agent/providers/google.ts +210 -211
- package/src/agent/providers/ollama.ts +186 -182
- package/src/agent/providers/openai.ts +73 -5
- package/src/agent/session.ts +275 -153
- package/src/config/index.ts +342 -335
- package/src/tools/__tests__/bash.test.ts +1 -1
- package/src/tools/__tests__/edit.test.ts +981 -385
- package/src/tools/__tests__/grep.test.ts +35 -0
- package/src/tools/bash.ts +97 -11
- package/src/tools/edit.ts +414 -154
- package/src/tools/git.ts +76 -76
- package/src/tools/grep.ts +57 -30
- package/src/tools/read.ts +61 -25
- package/src/tools/registry.ts +231 -265
- package/src/tools/write_plan.ts +21 -5
- package/src/tui/index.ts +786 -785
- package/src/web/index.ts +29 -7
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +395 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +996 -949
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +193 -194
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/src/tools/exec.ts +0 -348
package/src/tools/registry.ts
CHANGED
|
@@ -1,265 +1,231 @@
|
|
|
1
|
-
import type { FileReadTracker } from '../diff/apply.js'
|
|
2
|
-
import { bashTool } from './bash.js'
|
|
3
|
-
import { createEditTool } from './edit.js'
|
|
4
|
-
import { fmtErr } from './errors.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
toolName === '
|
|
41
|
-
toolName === '
|
|
42
|
-
toolName === '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
getDefinitions(): ToolDefinition[] {
|
|
233
|
-
return Array.from(this.tools.values()).map(t => t.definition)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async dispatch(call: ToolCall): Promise<ToolResult> {
|
|
237
|
-
const tool = this.tools.get(call.name)
|
|
238
|
-
if (!tool) {
|
|
239
|
-
return {
|
|
240
|
-
success: false,
|
|
241
|
-
output: '',
|
|
242
|
-
error: `Unknown tool: "${call.name}". Available: ${Array.from(this.tools.keys()).join(', ')}`,
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ── Universal input normalization ──────────────────────────────────
|
|
247
|
-
// Auto-correct common LLM call misuses before they reach tool execute()
|
|
248
|
-
let input = call.input
|
|
249
|
-
try {
|
|
250
|
-
input = normalizeToolInput(call.name, tool.definition, input)
|
|
251
|
-
} catch {
|
|
252
|
-
// If normalization throws, let the tool handle it (or fail naturally)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
return await tool.execute(input)
|
|
257
|
-
} catch (err) {
|
|
258
|
-
return {
|
|
259
|
-
success: false,
|
|
260
|
-
output: '',
|
|
261
|
-
error: `Tool "${call.name}" threw: ${fmtErr(err)}`,
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
1
|
+
import type { FileReadTracker } from '../diff/apply.js'
|
|
2
|
+
import { bashTool } from './bash.js'
|
|
3
|
+
import { createEditTool } from './edit.js'
|
|
4
|
+
import { fmtErr } from './errors.js'
|
|
5
|
+
import { fetchTool } from './fetch.js'
|
|
6
|
+
import { createFindTool } from './find.js'
|
|
7
|
+
import { createGitTool } from './git.js'
|
|
8
|
+
import { globTool } from './glob.js'
|
|
9
|
+
import { createGrepTool } from './grep.js'
|
|
10
|
+
import { createInstallSkillTool } from './install_skill.js'
|
|
11
|
+
import { createLsTool } from './ls.js'
|
|
12
|
+
import { createReadTool } from './read.js'
|
|
13
|
+
import { searchTool } from './search.js'
|
|
14
|
+
import type { Tool, ToolCall, ToolDefinition, ToolResult } from './types.js'
|
|
15
|
+
import { createWritePlanTool } from './write_plan.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize tool input to prevent common LLM call misuses.
|
|
19
|
+
* The LLM sometimes passes parameters in the wrong format — this function
|
|
20
|
+
* auto-corrects those patterns so tools work reliably.
|
|
21
|
+
*
|
|
22
|
+
* Common misuses handled:
|
|
23
|
+
* 1. Passed as a string instead of { ... } object wrapper
|
|
24
|
+
* 2. Passed top-level params directly instead of nested in the expected key
|
|
25
|
+
* 3. Array passed directly (for tools that expect { items: [...] })
|
|
26
|
+
*/
|
|
27
|
+
function normalizeToolInput(
|
|
28
|
+
toolName: string,
|
|
29
|
+
_definition: ToolDefinition,
|
|
30
|
+
input: Record<string, unknown>,
|
|
31
|
+
): Record<string, unknown> {
|
|
32
|
+
// If input is a string (e.g. install_skill("package-name") or bash("ls -la"))
|
|
33
|
+
// wrap it into the appropriate key
|
|
34
|
+
if (typeof input === 'string') {
|
|
35
|
+
if (toolName === 'bash' || toolName === 'git') {
|
|
36
|
+
return { command: input }
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
toolName === 'install_skill' ||
|
|
40
|
+
toolName === 'find' ||
|
|
41
|
+
toolName === 'glob' ||
|
|
42
|
+
toolName === 'grep'
|
|
43
|
+
) {
|
|
44
|
+
return { pattern: input }
|
|
45
|
+
}
|
|
46
|
+
if (toolName === 'search' || toolName === 'fetch') {
|
|
47
|
+
return { query: input, url: input }
|
|
48
|
+
}
|
|
49
|
+
if (toolName === 'write_plan') {
|
|
50
|
+
return { filename: input, content: input }
|
|
51
|
+
}
|
|
52
|
+
// For tools that take a single string param, guess from the definition
|
|
53
|
+
const params = Object.keys(_definition.parameters)
|
|
54
|
+
if (params.length === 1) {
|
|
55
|
+
return { [params[0]]: input }
|
|
56
|
+
}
|
|
57
|
+
return input
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If input is an array (e.g. read called with ["file.ts"] directly)
|
|
61
|
+
if (Array.isArray(input)) {
|
|
62
|
+
if (toolName === 'read' || toolName === 'ls') {
|
|
63
|
+
return { paths: input, path: (input as string[])[0] }
|
|
64
|
+
}
|
|
65
|
+
if (toolName === 'edit') {
|
|
66
|
+
return { edits: input }
|
|
67
|
+
}
|
|
68
|
+
return input
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return input
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ToolContext {
|
|
75
|
+
cwd: string
|
|
76
|
+
autoApprove: boolean
|
|
77
|
+
applier: FileReadTracker
|
|
78
|
+
mode: 'code' | 'plan' | 'ask'
|
|
79
|
+
onPlanWritten?: (display: string) => void
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ToolPlugin {
|
|
83
|
+
name: string
|
|
84
|
+
description: string
|
|
85
|
+
create: (context: ToolContext) => Tool
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extensible ToolRegistry with plugin support.
|
|
90
|
+
* Inspired by pi's extension system for dynamic tool registration.
|
|
91
|
+
*/
|
|
92
|
+
export class ToolRegistry {
|
|
93
|
+
private tools: Map<string, Tool> = new Map()
|
|
94
|
+
private context: ToolContext
|
|
95
|
+
private plugins: Map<string, ToolPlugin> = new Map()
|
|
96
|
+
|
|
97
|
+
constructor(context: ToolContext) {
|
|
98
|
+
this.context = context
|
|
99
|
+
this.registerBuiltins()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Register all built-in tools */
|
|
103
|
+
private registerBuiltins(): void {
|
|
104
|
+
// ask mode: only fetch and search
|
|
105
|
+
if (this.context.mode === 'ask') {
|
|
106
|
+
this.register(fetchTool)
|
|
107
|
+
this.register(searchTool)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.register(createReadTool(this.context.applier, this.context.cwd))
|
|
112
|
+
this.register(globTool)
|
|
113
|
+
this.register(createGrepTool(this.context.cwd))
|
|
114
|
+
this.register(createLsTool(this.context.cwd))
|
|
115
|
+
this.register(createFindTool(this.context.cwd))
|
|
116
|
+
this.register(fetchTool)
|
|
117
|
+
this.register(searchTool)
|
|
118
|
+
|
|
119
|
+
if (this.context.mode === 'code') {
|
|
120
|
+
// Code mode: full toolset including write operations
|
|
121
|
+
this.register(bashTool)
|
|
122
|
+
this.register(createGitTool(this.context.cwd))
|
|
123
|
+
this.register(createInstallSkillTool(this.context.cwd))
|
|
124
|
+
this.register(createEditTool(this.context.applier, this.context.cwd))
|
|
125
|
+
} else {
|
|
126
|
+
// Plan mode: read-only investigation + write_plan
|
|
127
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
132
|
+
// Update context.mode FIRST so registerBuiltins() and mode-based logic
|
|
133
|
+
// use the NEW mode, not the stale one (fixes bug when switching from
|
|
134
|
+
// ask→code or plan→code where all tools would be missing)
|
|
135
|
+
this.context.mode = mode
|
|
136
|
+
|
|
137
|
+
if (mode === 'code') {
|
|
138
|
+
// Re-register all built-in tools for code mode
|
|
139
|
+
this.tools.clear()
|
|
140
|
+
this.registerBuiltins()
|
|
141
|
+
} else if (mode === 'plan') {
|
|
142
|
+
// Keep existing tools but swap edit/write_plan
|
|
143
|
+
if (!this.tools.has('write_plan')) {
|
|
144
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
145
|
+
}
|
|
146
|
+
this.tools.delete('edit')
|
|
147
|
+
} else if (mode === 'ask') {
|
|
148
|
+
// Ask mode: only fetch and search
|
|
149
|
+
this.tools.clear()
|
|
150
|
+
this.register(fetchTool)
|
|
151
|
+
this.register(searchTool)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Register a single tool */
|
|
156
|
+
register(tool: Tool): void {
|
|
157
|
+
this.tools.set(tool.definition.name, tool)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Register a tool plugin (lazy creation pattern) */
|
|
161
|
+
registerPlugin(plugin: ToolPlugin): void {
|
|
162
|
+
this.plugins.set(plugin.name, plugin)
|
|
163
|
+
// Activate immediately
|
|
164
|
+
try {
|
|
165
|
+
const tool = plugin.create(this.context)
|
|
166
|
+
this.register(tool)
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Unregister a tool by name */
|
|
173
|
+
unregister(name: string): boolean {
|
|
174
|
+
return this.tools.delete(name)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Check if a tool is registered */
|
|
178
|
+
has(name: string): boolean {
|
|
179
|
+
return this.tools.has(name)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Re-register write_plan tool with an updated callback */
|
|
183
|
+
reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
|
|
184
|
+
this.tools.delete('write_plan')
|
|
185
|
+
this.tools.set('write_plan', createWritePlanTool(cwd, cb))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Partially update the context (used by session.onPlanWritten setter) */
|
|
189
|
+
updateContext(partial: Partial<ToolContext>): void {
|
|
190
|
+
Object.assign(this.context, partial)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** List all registered tool names */
|
|
194
|
+
listTools(): string[] {
|
|
195
|
+
return Array.from(this.tools.keys())
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getDefinitions(): ToolDefinition[] {
|
|
199
|
+
return Array.from(this.tools.values()).map(t => t.definition)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async dispatch(call: ToolCall): Promise<ToolResult> {
|
|
203
|
+
const tool = this.tools.get(call.name)
|
|
204
|
+
if (!tool) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
output: '',
|
|
208
|
+
error: `Unknown tool: "${call.name}". Available: ${Array.from(this.tools.keys()).join(', ')}`,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Universal input normalization ──────────────────────────────────
|
|
213
|
+
// Auto-correct common LLM call misuses before they reach tool execute()
|
|
214
|
+
let input = call.input
|
|
215
|
+
try {
|
|
216
|
+
input = normalizeToolInput(call.name, tool.definition, input)
|
|
217
|
+
} catch {
|
|
218
|
+
// If normalization throws, let the tool handle it (or fail naturally)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
return await tool.execute(input)
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
output: '',
|
|
227
|
+
error: `Tool "${call.name}" threw: ${fmtErr(err)}`,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/tools/write_plan.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from 'node:fs'
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
import { fmtErr } from './errors.js'
|
|
4
4
|
import type { Tool, ToolResult } from './types.js'
|
|
@@ -6,6 +6,9 @@ import type { Tool, ToolResult } from './types.js'
|
|
|
6
6
|
/** Directory where plans are stored, relative to project root */
|
|
7
7
|
export const PLAN_DIR = '.lonny'
|
|
8
8
|
|
|
9
|
+
/** Maximum plan content size (1MB) */
|
|
10
|
+
const MAX_CONTENT_SIZE = 1_000_000
|
|
11
|
+
|
|
9
12
|
function sanitizeFilename(name: string): string {
|
|
10
13
|
const trimmed = name.trim()
|
|
11
14
|
if (!trimmed) return ''
|
|
@@ -22,7 +25,10 @@ function sanitizeFilename(name: string): string {
|
|
|
22
25
|
return normalized
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function createWritePlanTool(
|
|
28
|
+
export function createWritePlanTool(
|
|
29
|
+
cwd: string,
|
|
30
|
+
onPlanWritten?: (display: string) => void | Promise<void>,
|
|
31
|
+
): Tool {
|
|
26
32
|
return {
|
|
27
33
|
definition: {
|
|
28
34
|
name: 'write_plan',
|
|
@@ -49,9 +55,19 @@ export function createWritePlanTool(cwd: string, onPlanWritten?: (display: strin
|
|
|
49
55
|
if (typeof input.content !== 'string') {
|
|
50
56
|
return { success: false, output: '', error: 'content is required (string)' }
|
|
51
57
|
}
|
|
58
|
+
|
|
52
59
|
const filename = input.filename
|
|
53
60
|
const content = input.content
|
|
54
61
|
|
|
62
|
+
// Check content size
|
|
63
|
+
if (content.length > MAX_CONTENT_SIZE) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
output: '',
|
|
67
|
+
error: `Content too large (max ${MAX_CONTENT_SIZE} bytes)`,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
const safeName = sanitizeFilename(filename)
|
|
56
72
|
if (!safeName) {
|
|
57
73
|
return {
|
|
@@ -69,10 +85,10 @@ export function createWritePlanTool(cwd: string, onPlanWritten?: (display: strin
|
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
try {
|
|
72
|
-
fs.
|
|
73
|
-
fs.
|
|
88
|
+
await fs.mkdir(path.dirname(target), { recursive: true })
|
|
89
|
+
await fs.writeFile(target, content, 'utf8')
|
|
74
90
|
const display = path.relative(cwd, target).replace(/\\/g, '/')
|
|
75
|
-
onPlanWritten?.(display)
|
|
91
|
+
await Promise.resolve(onPlanWritten?.(display))
|
|
76
92
|
return {
|
|
77
93
|
success: true,
|
|
78
94
|
output: `Wrote plan to ${display} (${Buffer.byteLength(content, 'utf8')} bytes)`,
|