lonny-agent 0.2.2 → 0.2.4
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/.claude/settings.local.json +14 -0
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.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 +10 -5
- 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.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 +23 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +4 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +206 -147
- package/dist/agent/session.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +14 -14
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/edit.d.ts +17 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +81 -34
- package/dist/tools/edit.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.map +1 -1
- package/dist/tools/registry.js +0 -4
- package/dist/tools/registry.js.map +1 -1
- 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.js +5 -5
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/index.ts +1 -1
- package/src/agent/project.ts +366 -0
- package/src/agent/prompt-builder.ts +11 -5
- package/src/agent/providers/anthropic.ts +21 -2
- package/src/agent/providers/google.ts +1 -1
- package/src/agent/providers/ollama.ts +5 -1
- package/src/agent/providers/openai.ts +27 -3
- package/src/agent/session.ts +216 -150
- package/src/tools/__tests__/edit.test.ts +14 -14
- package/src/tools/edit.ts +103 -36
- package/src/tools/read.ts +61 -25
- package/src/tools/registry.ts +260 -265
- package/src/tools/write_plan.ts +21 -5
- package/src/tui/index.ts +5 -4
- package/src/web/index.ts +5 -5
package/src/tools/registry.ts
CHANGED
|
@@ -1,265 +1,260 @@
|
|
|
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 { createExecTool, updateExecToolDefinition } from './exec.js'
|
|
6
|
-
import { fetchTool } from './fetch.js'
|
|
7
|
-
import { createFindTool } from './find.js'
|
|
8
|
-
import { createGitTool } from './git.js'
|
|
9
|
-
import { globTool } from './glob.js'
|
|
10
|
-
import { createGrepTool } from './grep.js'
|
|
11
|
-
import { createInstallSkillTool } from './install_skill.js'
|
|
12
|
-
import { createLsTool } from './ls.js'
|
|
13
|
-
import { createReadTool } from './read.js'
|
|
14
|
-
import { searchTool } from './search.js'
|
|
15
|
-
import type { Tool, ToolCall, ToolDefinition, ToolResult } from './types.js'
|
|
16
|
-
import { createWritePlanTool } from './write_plan.js'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Normalize tool input to prevent common LLM call misuses.
|
|
20
|
-
* The LLM sometimes passes parameters in the wrong format — this function
|
|
21
|
-
* auto-corrects those patterns so tools work reliably.
|
|
22
|
-
*
|
|
23
|
-
* Common misuses handled:
|
|
24
|
-
* 1. Passed as a string instead of { ... } object wrapper
|
|
25
|
-
* 2. Passed top-level params directly instead of nested in the expected key
|
|
26
|
-
* 3. Array passed directly (for tools that expect { items: [...] })
|
|
27
|
-
*/
|
|
28
|
-
function normalizeToolInput(
|
|
29
|
-
toolName: string,
|
|
30
|
-
_definition: ToolDefinition,
|
|
31
|
-
input: Record<string, unknown>,
|
|
32
|
-
): Record<string, unknown> {
|
|
33
|
-
// If input is a string (e.g. install_skill("package-name") or bash("ls -la"))
|
|
34
|
-
// wrap it into the appropriate key
|
|
35
|
-
if (typeof input === 'string') {
|
|
36
|
-
if (toolName === 'bash' || toolName === 'git') {
|
|
37
|
-
return { command: input }
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
toolName === 'install_skill' ||
|
|
41
|
-
toolName === 'find' ||
|
|
42
|
-
toolName === 'glob' ||
|
|
43
|
-
toolName === 'grep'
|
|
44
|
-
) {
|
|
45
|
-
return { pattern: input }
|
|
46
|
-
}
|
|
47
|
-
if (toolName === 'search' || toolName === 'fetch') {
|
|
48
|
-
return { query: input, url: input }
|
|
49
|
-
}
|
|
50
|
-
if (toolName === 'write_plan') {
|
|
51
|
-
return { filename: input, content: input }
|
|
52
|
-
}
|
|
53
|
-
if (toolName === 'exec') {
|
|
54
|
-
return { code: input }
|
|
55
|
-
}
|
|
56
|
-
// For tools that take a single string param, guess from the definition
|
|
57
|
-
const params = Object.keys(_definition.parameters)
|
|
58
|
-
if (params.length === 1) {
|
|
59
|
-
return { [params[0]]: input }
|
|
60
|
-
}
|
|
61
|
-
return input
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// If input is an array (e.g. read called with ["file.ts"] directly)
|
|
65
|
-
if (Array.isArray(input)) {
|
|
66
|
-
if (toolName === 'read' || toolName === 'ls') {
|
|
67
|
-
return { paths: input, path: (input as string[])[0] }
|
|
68
|
-
}
|
|
69
|
-
if (toolName === 'edit') {
|
|
70
|
-
return { edits: input }
|
|
71
|
-
}
|
|
72
|
-
return input
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.register(
|
|
123
|
-
this.register(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.register(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (mode === '
|
|
164
|
-
//
|
|
165
|
-
this.tools.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.tools.
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
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
|
-
return this.tools.
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 { createExecTool, updateExecToolDefinition } from './exec.js'
|
|
6
|
+
import { fetchTool } from './fetch.js'
|
|
7
|
+
import { createFindTool } from './find.js'
|
|
8
|
+
import { createGitTool } from './git.js'
|
|
9
|
+
import { globTool } from './glob.js'
|
|
10
|
+
import { createGrepTool } from './grep.js'
|
|
11
|
+
import { createInstallSkillTool } from './install_skill.js'
|
|
12
|
+
import { createLsTool } from './ls.js'
|
|
13
|
+
import { createReadTool } from './read.js'
|
|
14
|
+
import { searchTool } from './search.js'
|
|
15
|
+
import type { Tool, ToolCall, ToolDefinition, ToolResult } from './types.js'
|
|
16
|
+
import { createWritePlanTool } from './write_plan.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize tool input to prevent common LLM call misuses.
|
|
20
|
+
* The LLM sometimes passes parameters in the wrong format — this function
|
|
21
|
+
* auto-corrects those patterns so tools work reliably.
|
|
22
|
+
*
|
|
23
|
+
* Common misuses handled:
|
|
24
|
+
* 1. Passed as a string instead of { ... } object wrapper
|
|
25
|
+
* 2. Passed top-level params directly instead of nested in the expected key
|
|
26
|
+
* 3. Array passed directly (for tools that expect { items: [...] })
|
|
27
|
+
*/
|
|
28
|
+
function normalizeToolInput(
|
|
29
|
+
toolName: string,
|
|
30
|
+
_definition: ToolDefinition,
|
|
31
|
+
input: Record<string, unknown>,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
// If input is a string (e.g. install_skill("package-name") or bash("ls -la"))
|
|
34
|
+
// wrap it into the appropriate key
|
|
35
|
+
if (typeof input === 'string') {
|
|
36
|
+
if (toolName === 'bash' || toolName === 'git') {
|
|
37
|
+
return { command: input }
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
toolName === 'install_skill' ||
|
|
41
|
+
toolName === 'find' ||
|
|
42
|
+
toolName === 'glob' ||
|
|
43
|
+
toolName === 'grep'
|
|
44
|
+
) {
|
|
45
|
+
return { pattern: input }
|
|
46
|
+
}
|
|
47
|
+
if (toolName === 'search' || toolName === 'fetch') {
|
|
48
|
+
return { query: input, url: input }
|
|
49
|
+
}
|
|
50
|
+
if (toolName === 'write_plan') {
|
|
51
|
+
return { filename: input, content: input }
|
|
52
|
+
}
|
|
53
|
+
if (toolName === 'exec') {
|
|
54
|
+
return { code: input }
|
|
55
|
+
}
|
|
56
|
+
// For tools that take a single string param, guess from the definition
|
|
57
|
+
const params = Object.keys(_definition.parameters)
|
|
58
|
+
if (params.length === 1) {
|
|
59
|
+
return { [params[0]]: input }
|
|
60
|
+
}
|
|
61
|
+
return input
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If input is an array (e.g. read called with ["file.ts"] directly)
|
|
65
|
+
if (Array.isArray(input)) {
|
|
66
|
+
if (toolName === 'read' || toolName === 'ls') {
|
|
67
|
+
return { paths: input, path: (input as string[])[0] }
|
|
68
|
+
}
|
|
69
|
+
if (toolName === 'edit') {
|
|
70
|
+
return { edits: input }
|
|
71
|
+
}
|
|
72
|
+
return input
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return input
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ToolContext {
|
|
79
|
+
cwd: string
|
|
80
|
+
autoApprove: boolean
|
|
81
|
+
applier: FileReadTracker
|
|
82
|
+
mode: 'code' | 'plan' | 'ask'
|
|
83
|
+
onPlanWritten?: (display: string) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ToolPlugin {
|
|
87
|
+
name: string
|
|
88
|
+
description: string
|
|
89
|
+
create: (context: ToolContext) => Tool
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extensible ToolRegistry with plugin support.
|
|
94
|
+
* Inspired by pi's extension system for dynamic tool registration.
|
|
95
|
+
*/
|
|
96
|
+
export class ToolRegistry {
|
|
97
|
+
private tools: Map<string, Tool> = new Map()
|
|
98
|
+
private context: ToolContext
|
|
99
|
+
private plugins: Map<string, ToolPlugin> = new Map()
|
|
100
|
+
/** Reference to the exec tool for dynamic description updates */
|
|
101
|
+
private execTool: Tool | null = null
|
|
102
|
+
|
|
103
|
+
constructor(context: ToolContext) {
|
|
104
|
+
this.context = context
|
|
105
|
+
this.registerBuiltins()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Register all built-in tools */
|
|
109
|
+
private registerBuiltins(): void {
|
|
110
|
+
// ask mode: only fetch and search + exec (exec works with any toolset)
|
|
111
|
+
if (this.context.mode === 'ask') {
|
|
112
|
+
this.register(fetchTool)
|
|
113
|
+
this.register(searchTool)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.register(createReadTool(this.context.applier, this.context.cwd))
|
|
118
|
+
this.register(globTool)
|
|
119
|
+
this.register(createGrepTool(this.context.cwd))
|
|
120
|
+
this.register(createLsTool(this.context.cwd))
|
|
121
|
+
this.register(createFindTool(this.context.cwd))
|
|
122
|
+
this.register(fetchTool)
|
|
123
|
+
this.register(searchTool)
|
|
124
|
+
|
|
125
|
+
if (this.context.mode === 'code') {
|
|
126
|
+
// Code mode: full toolset including write operations
|
|
127
|
+
this.register(bashTool)
|
|
128
|
+
this.register(createGitTool(this.context.cwd))
|
|
129
|
+
this.register(createInstallSkillTool(this.context.cwd))
|
|
130
|
+
this.register(createEditTool(this.context.applier, this.context.cwd))
|
|
131
|
+
this.registerExecTool()
|
|
132
|
+
} else {
|
|
133
|
+
// Plan mode: read-only investigation + write_plan
|
|
134
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Register the exec tool with dynamic type declarations for all registered tools */
|
|
139
|
+
private registerExecTool(): void {
|
|
140
|
+
const execTool = createExecTool(() => Array.from(this.tools.values()))
|
|
141
|
+
this.execTool = execTool
|
|
142
|
+
this.register(execTool)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Refresh the exec tool's description to include current tool type declarations */
|
|
146
|
+
private refreshExecDescription(): void {
|
|
147
|
+
if (this.execTool) {
|
|
148
|
+
updateExecToolDefinition(this.execTool, this.getDefinitions())
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
153
|
+
// Update context.mode FIRST so registerBuiltins() and mode-based logic
|
|
154
|
+
// use the NEW mode, not the stale one (fixes bug when switching from
|
|
155
|
+
// ask→code or plan→code where all tools would be missing)
|
|
156
|
+
this.context.mode = mode
|
|
157
|
+
|
|
158
|
+
if (mode === 'code') {
|
|
159
|
+
// Re-register all built-in tools for code mode
|
|
160
|
+
this.tools.clear()
|
|
161
|
+
this.execTool = null
|
|
162
|
+
this.registerBuiltins()
|
|
163
|
+
} else if (mode === 'plan') {
|
|
164
|
+
// Keep existing tools but swap edit/write_plan, remove exec
|
|
165
|
+
if (!this.tools.has('write_plan')) {
|
|
166
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
167
|
+
}
|
|
168
|
+
this.tools.delete('edit')
|
|
169
|
+
this.tools.delete('exec')
|
|
170
|
+
this.execTool = null
|
|
171
|
+
} else if (mode === 'ask') {
|
|
172
|
+
// Ask mode: only fetch and search
|
|
173
|
+
this.tools.clear()
|
|
174
|
+
this.execTool = null
|
|
175
|
+
this.register(fetchTool)
|
|
176
|
+
this.register(searchTool)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Register a single tool */
|
|
181
|
+
register(tool: Tool): void {
|
|
182
|
+
this.tools.set(tool.definition.name, tool)
|
|
183
|
+
// Refresh exec tool description to include the new tool's type declarations
|
|
184
|
+
if (this.execTool && tool.definition.name !== 'exec') {
|
|
185
|
+
this.refreshExecDescription()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Register a tool plugin (lazy creation pattern) */
|
|
190
|
+
registerPlugin(plugin: ToolPlugin): void {
|
|
191
|
+
this.plugins.set(plugin.name, plugin)
|
|
192
|
+
// Activate immediately
|
|
193
|
+
try {
|
|
194
|
+
const tool = plugin.create(this.context)
|
|
195
|
+
this.register(tool)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Unregister a tool by name */
|
|
202
|
+
unregister(name: string): boolean {
|
|
203
|
+
return this.tools.delete(name)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Check if a tool is registered */
|
|
207
|
+
has(name: string): boolean {
|
|
208
|
+
return this.tools.has(name)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Re-register write_plan tool with an updated callback */
|
|
212
|
+
reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
|
|
213
|
+
this.tools.delete('write_plan')
|
|
214
|
+
this.tools.set('write_plan', createWritePlanTool(cwd, cb))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Partially update the context (used by session.onPlanWritten setter) */
|
|
218
|
+
updateContext(partial: Partial<ToolContext>): void {
|
|
219
|
+
Object.assign(this.context, partial)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** List all registered tool names */
|
|
223
|
+
listTools(): string[] {
|
|
224
|
+
return Array.from(this.tools.keys())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getDefinitions(): ToolDefinition[] {
|
|
228
|
+
return Array.from(this.tools.values()).map(t => t.definition)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async dispatch(call: ToolCall): Promise<ToolResult> {
|
|
232
|
+
const tool = this.tools.get(call.name)
|
|
233
|
+
if (!tool) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
output: '',
|
|
237
|
+
error: `Unknown tool: "${call.name}". Available: ${Array.from(this.tools.keys()).join(', ')}`,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Universal input normalization ──────────────────────────────────
|
|
242
|
+
// Auto-correct common LLM call misuses before they reach tool execute()
|
|
243
|
+
let input = call.input
|
|
244
|
+
try {
|
|
245
|
+
input = normalizeToolInput(call.name, tool.definition, input)
|
|
246
|
+
} catch {
|
|
247
|
+
// If normalization throws, let the tool handle it (or fail naturally)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
return await tool.execute(input)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
output: '',
|
|
256
|
+
error: `Tool "${call.name}" threw: ${fmtErr(err)}`,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
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)`,
|
package/src/tui/index.ts
CHANGED
|
@@ -225,7 +225,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
225
225
|
|
|
226
226
|
// Try to restore a saved session for this directory (MUST be before landing screen setup)
|
|
227
227
|
let restored = false
|
|
228
|
-
const restoredSession = Session.load(config, output)
|
|
228
|
+
const restoredSession = await Session.load(config, output)
|
|
229
229
|
if (restoredSession) {
|
|
230
230
|
restored = true
|
|
231
231
|
session = restoredSession
|
|
@@ -484,7 +484,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
// ── Input handling ──────────────────────────────────────────────────────
|
|
487
|
-
function sendMessage(text: string): void {
|
|
487
|
+
async function sendMessage(text: string): Promise<void> {
|
|
488
488
|
const trimmed = text.trim()
|
|
489
489
|
if (!trimmed) return
|
|
490
490
|
editor.setText('')
|
|
@@ -532,7 +532,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
532
532
|
|
|
533
533
|
if (cmd === 'mode') {
|
|
534
534
|
if (arg === 'code' || arg === 'plan' || arg === 'ask') {
|
|
535
|
-
session.setMode(arg)
|
|
535
|
+
await session.setMode(arg)
|
|
536
536
|
chatContent += `\n${colors.warn('\u21E8')} Switched to ${arg === 'ask' ? colors.success(arg) : colors.warn(arg)} mode\n`
|
|
537
537
|
chatMarkdown.setText(chatContent)
|
|
538
538
|
updateFooter()
|
|
@@ -547,7 +547,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
547
547
|
if (arg) {
|
|
548
548
|
session.config.model = arg
|
|
549
549
|
// Rebuild system prompt with new model context
|
|
550
|
-
session.setMode(session.config.mode) // triggers rebuild
|
|
550
|
+
await session.setMode(session.config.mode) // triggers rebuild
|
|
551
551
|
chatContent += `\n${colors.warn('\u21E8')} Model switched to ${colors.warn(arg)}\n`
|
|
552
552
|
chatMarkdown.setText(chatContent)
|
|
553
553
|
updateFooter()
|
|
@@ -625,6 +625,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
625
625
|
chatContent += `\n${colors.warn('\u23F9')} Stopping agent...\n`
|
|
626
626
|
chatMarkdown.setText(chatContent)
|
|
627
627
|
isRunning = false
|
|
628
|
+
loader.stop()
|
|
628
629
|
loader.setMessage('')
|
|
629
630
|
tui.setShowHardwareCursor(true)
|
|
630
631
|
updateFooter()
|