lonny-agent 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.kilo/plans/1780293725888-quick-wizard.md +62 -0
- package/.lonny/plan-web-cwd-status.md +38 -0
- package/dist/agent/compaction.d.ts +5 -0
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +5 -0
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +39 -4
- package/dist/agent/prompt-builder.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/openai.js +1 -1
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +18 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/config/__tests__/context-window.test.d.ts +2 -0
- package/dist/config/__tests__/context-window.test.d.ts.map +1 -0
- package/dist/config/__tests__/context-window.test.js +80 -0
- package/dist/config/__tests__/context-window.test.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +211 -0
- package/dist/config/index.js.map +1 -1
- package/dist/pi-tui/tui.d.ts.map +1 -1
- package/dist/pi-tui/tui.js +7 -16
- package/dist/pi-tui/tui.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +139 -0
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/fetch.test.js +11 -9
- package/dist/tools/__tests__/fetch.test.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +185 -32
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/install_superpowers.d.ts +3 -0
- package/dist/tools/install_superpowers.d.ts.map +1 -0
- package/dist/tools/install_superpowers.js +207 -0
- package/dist/tools/install_superpowers.js.map +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +25 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +56 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +8 -0
- package/dist/web/public/index.html +6 -0
- package/dist/web/public/style.css +11 -0
- package/m.role+' +0 -0
- package/package.json +1 -1
- package/src/agent/compaction.ts +6 -0
- package/src/agent/prompt-builder.ts +40 -4
- package/src/agent/providers/google.ts +1 -1
- package/src/agent/providers/openai.ts +1 -1
- package/src/agent/session.ts +19 -2
- package/src/config/__tests__/context-window.test.ts +94 -0
- package/src/config/index.ts +242 -0
- package/src/pi-tui/tui.ts +8 -14
- package/src/tools/__tests__/edit.test.ts +155 -0
- package/src/tools/__tests__/fetch.test.ts +12 -10
- package/src/tools/edit.ts +202 -33
- package/src/tools/registry.ts +265 -265
- package/src/tui/index.ts +23 -1
- package/src/web/index.ts +71 -0
- package/src/web/public/app.js +9 -0
- package/src/web/public/index.html +6 -0
- package/src/web/public/style.css +11 -0
- package/.lonny/fix-new-command-session-cleanup.md +0 -65
- package/.lonny/tui-autocomplete-above.md +0 -62
package/src/tools/registry.ts
CHANGED
|
@@ -1,265 +1,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
|
-
// If edit tool gets an empty object, some models hallucinate the call
|
|
76
|
-
if (toolName === 'edit' && Object.keys(input).length === 0) {
|
|
77
|
-
return { edits: [] }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return input
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface ToolContext {
|
|
84
|
-
cwd: string
|
|
85
|
-
autoApprove: boolean
|
|
86
|
-
applier: FileReadTracker
|
|
87
|
-
mode: 'code' | 'plan' | 'ask'
|
|
88
|
-
onPlanWritten?: (display: string) => void
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface ToolPlugin {
|
|
92
|
-
name: string
|
|
93
|
-
description: string
|
|
94
|
-
create: (context: ToolContext) => Tool
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Extensible ToolRegistry with plugin support.
|
|
99
|
-
* Inspired by pi's extension system for dynamic tool registration.
|
|
100
|
-
*/
|
|
101
|
-
export class ToolRegistry {
|
|
102
|
-
private tools: Map<string, Tool> = new Map()
|
|
103
|
-
private context: ToolContext
|
|
104
|
-
private plugins: Map<string, ToolPlugin> = new Map()
|
|
105
|
-
/** Reference to the exec tool for dynamic description updates */
|
|
106
|
-
private execTool: Tool | null = null
|
|
107
|
-
|
|
108
|
-
constructor(context: ToolContext) {
|
|
109
|
-
this.context = context
|
|
110
|
-
this.registerBuiltins()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Register all built-in tools */
|
|
114
|
-
private registerBuiltins(): void {
|
|
115
|
-
// ask mode: only fetch and search + exec (exec works with any toolset)
|
|
116
|
-
if (this.context.mode === 'ask') {
|
|
117
|
-
this.register(fetchTool)
|
|
118
|
-
this.register(searchTool)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.register(createReadTool(this.context.applier, this.context.cwd))
|
|
123
|
-
this.register(globTool)
|
|
124
|
-
this.register(createGrepTool(this.context.cwd))
|
|
125
|
-
this.register(createLsTool(this.context.cwd))
|
|
126
|
-
this.register(createFindTool(this.context.cwd))
|
|
127
|
-
this.register(fetchTool)
|
|
128
|
-
this.register(searchTool)
|
|
129
|
-
|
|
130
|
-
if (this.context.mode === 'code') {
|
|
131
|
-
// Code mode: full toolset including write operations
|
|
132
|
-
this.register(bashTool)
|
|
133
|
-
this.register(createGitTool(this.context.cwd))
|
|
134
|
-
this.register(createInstallSkillTool(this.context.cwd))
|
|
135
|
-
this.register(createEditTool(this.context.applier, this.context.cwd))
|
|
136
|
-
this.registerExecTool()
|
|
137
|
-
} else {
|
|
138
|
-
// Plan mode: read-only investigation + write_plan
|
|
139
|
-
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Register the exec tool with dynamic type declarations for all registered tools */
|
|
144
|
-
private registerExecTool(): void {
|
|
145
|
-
const execTool = createExecTool(() => Array.from(this.tools.values()))
|
|
146
|
-
this.execTool = execTool
|
|
147
|
-
this.register(execTool)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Refresh the exec tool's description to include current tool type declarations */
|
|
151
|
-
private refreshExecDescription(): void {
|
|
152
|
-
if (this.execTool) {
|
|
153
|
-
updateExecToolDefinition(this.execTool, this.getDefinitions())
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
158
|
-
// Update context.mode FIRST so registerBuiltins() and mode-based logic
|
|
159
|
-
// use the NEW mode, not the stale one (fixes bug when switching from
|
|
160
|
-
// ask→code or plan→code where all tools would be missing)
|
|
161
|
-
this.context.mode = mode
|
|
162
|
-
|
|
163
|
-
if (mode === 'code') {
|
|
164
|
-
// Re-register all built-in tools for code mode
|
|
165
|
-
this.tools.clear()
|
|
166
|
-
this.execTool = null
|
|
167
|
-
this.registerBuiltins()
|
|
168
|
-
} else if (mode === 'plan') {
|
|
169
|
-
// Keep existing tools but swap edit/write_plan, remove exec
|
|
170
|
-
if (!this.tools.has('write_plan')) {
|
|
171
|
-
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
172
|
-
}
|
|
173
|
-
this.tools.delete('edit')
|
|
174
|
-
this.tools.delete('exec')
|
|
175
|
-
this.execTool = null
|
|
176
|
-
} else if (mode === 'ask') {
|
|
177
|
-
// Ask mode: only fetch and search
|
|
178
|
-
this.tools.clear()
|
|
179
|
-
this.execTool = null
|
|
180
|
-
this.register(fetchTool)
|
|
181
|
-
this.register(searchTool)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Register a single tool */
|
|
186
|
-
register(tool: Tool): void {
|
|
187
|
-
this.tools.set(tool.definition.name, tool)
|
|
188
|
-
// Refresh exec tool description to include the new tool's type declarations
|
|
189
|
-
if (this.execTool && tool.definition.name !== 'exec') {
|
|
190
|
-
this.refreshExecDescription()
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Register a tool plugin (lazy creation pattern) */
|
|
195
|
-
registerPlugin(plugin: ToolPlugin): void {
|
|
196
|
-
this.plugins.set(plugin.name, plugin)
|
|
197
|
-
// Activate immediately
|
|
198
|
-
try {
|
|
199
|
-
const tool = plugin.create(this.context)
|
|
200
|
-
this.register(tool)
|
|
201
|
-
} catch (err) {
|
|
202
|
-
console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Unregister a tool by name */
|
|
207
|
-
unregister(name: string): boolean {
|
|
208
|
-
return this.tools.delete(name)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Check if a tool is registered */
|
|
212
|
-
has(name: string): boolean {
|
|
213
|
-
return this.tools.has(name)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Re-register write_plan tool with an updated callback */
|
|
217
|
-
reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
|
|
218
|
-
this.tools.delete('write_plan')
|
|
219
|
-
this.tools.set('write_plan', createWritePlanTool(cwd, cb))
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Partially update the context (used by session.onPlanWritten setter) */
|
|
223
|
-
updateContext(partial: Partial<ToolContext>): void {
|
|
224
|
-
Object.assign(this.context, partial)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** List all registered tool names */
|
|
228
|
-
listTools(): string[] {
|
|
229
|
-
return Array.from(this.tools.keys())
|
|
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 { 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
|
+
// If edit tool gets an empty object, some models hallucinate the call
|
|
76
|
+
if (toolName === 'edit' && Object.keys(input).length === 0) {
|
|
77
|
+
return { edits: [] }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return input
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ToolContext {
|
|
84
|
+
cwd: string
|
|
85
|
+
autoApprove: boolean
|
|
86
|
+
applier: FileReadTracker
|
|
87
|
+
mode: 'code' | 'plan' | 'ask'
|
|
88
|
+
onPlanWritten?: (display: string) => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ToolPlugin {
|
|
92
|
+
name: string
|
|
93
|
+
description: string
|
|
94
|
+
create: (context: ToolContext) => Tool
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extensible ToolRegistry with plugin support.
|
|
99
|
+
* Inspired by pi's extension system for dynamic tool registration.
|
|
100
|
+
*/
|
|
101
|
+
export class ToolRegistry {
|
|
102
|
+
private tools: Map<string, Tool> = new Map()
|
|
103
|
+
private context: ToolContext
|
|
104
|
+
private plugins: Map<string, ToolPlugin> = new Map()
|
|
105
|
+
/** Reference to the exec tool for dynamic description updates */
|
|
106
|
+
private execTool: Tool | null = null
|
|
107
|
+
|
|
108
|
+
constructor(context: ToolContext) {
|
|
109
|
+
this.context = context
|
|
110
|
+
this.registerBuiltins()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Register all built-in tools */
|
|
114
|
+
private registerBuiltins(): void {
|
|
115
|
+
// ask mode: only fetch and search + exec (exec works with any toolset)
|
|
116
|
+
if (this.context.mode === 'ask') {
|
|
117
|
+
this.register(fetchTool)
|
|
118
|
+
this.register(searchTool)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.register(createReadTool(this.context.applier, this.context.cwd))
|
|
123
|
+
this.register(globTool)
|
|
124
|
+
this.register(createGrepTool(this.context.cwd))
|
|
125
|
+
this.register(createLsTool(this.context.cwd))
|
|
126
|
+
this.register(createFindTool(this.context.cwd))
|
|
127
|
+
this.register(fetchTool)
|
|
128
|
+
this.register(searchTool)
|
|
129
|
+
|
|
130
|
+
if (this.context.mode === 'code') {
|
|
131
|
+
// Code mode: full toolset including write operations
|
|
132
|
+
this.register(bashTool)
|
|
133
|
+
this.register(createGitTool(this.context.cwd))
|
|
134
|
+
this.register(createInstallSkillTool(this.context.cwd))
|
|
135
|
+
this.register(createEditTool(this.context.applier, this.context.cwd))
|
|
136
|
+
this.registerExecTool()
|
|
137
|
+
} else {
|
|
138
|
+
// Plan mode: read-only investigation + write_plan
|
|
139
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Register the exec tool with dynamic type declarations for all registered tools */
|
|
144
|
+
private registerExecTool(): void {
|
|
145
|
+
const execTool = createExecTool(() => Array.from(this.tools.values()))
|
|
146
|
+
this.execTool = execTool
|
|
147
|
+
this.register(execTool)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Refresh the exec tool's description to include current tool type declarations */
|
|
151
|
+
private refreshExecDescription(): void {
|
|
152
|
+
if (this.execTool) {
|
|
153
|
+
updateExecToolDefinition(this.execTool, this.getDefinitions())
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
158
|
+
// Update context.mode FIRST so registerBuiltins() and mode-based logic
|
|
159
|
+
// use the NEW mode, not the stale one (fixes bug when switching from
|
|
160
|
+
// ask→code or plan→code where all tools would be missing)
|
|
161
|
+
this.context.mode = mode
|
|
162
|
+
|
|
163
|
+
if (mode === 'code') {
|
|
164
|
+
// Re-register all built-in tools for code mode
|
|
165
|
+
this.tools.clear()
|
|
166
|
+
this.execTool = null
|
|
167
|
+
this.registerBuiltins()
|
|
168
|
+
} else if (mode === 'plan') {
|
|
169
|
+
// Keep existing tools but swap edit/write_plan, remove exec
|
|
170
|
+
if (!this.tools.has('write_plan')) {
|
|
171
|
+
this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
|
|
172
|
+
}
|
|
173
|
+
this.tools.delete('edit')
|
|
174
|
+
this.tools.delete('exec')
|
|
175
|
+
this.execTool = null
|
|
176
|
+
} else if (mode === 'ask') {
|
|
177
|
+
// Ask mode: only fetch and search
|
|
178
|
+
this.tools.clear()
|
|
179
|
+
this.execTool = null
|
|
180
|
+
this.register(fetchTool)
|
|
181
|
+
this.register(searchTool)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Register a single tool */
|
|
186
|
+
register(tool: Tool): void {
|
|
187
|
+
this.tools.set(tool.definition.name, tool)
|
|
188
|
+
// Refresh exec tool description to include the new tool's type declarations
|
|
189
|
+
if (this.execTool && tool.definition.name !== 'exec') {
|
|
190
|
+
this.refreshExecDescription()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Register a tool plugin (lazy creation pattern) */
|
|
195
|
+
registerPlugin(plugin: ToolPlugin): void {
|
|
196
|
+
this.plugins.set(plugin.name, plugin)
|
|
197
|
+
// Activate immediately
|
|
198
|
+
try {
|
|
199
|
+
const tool = plugin.create(this.context)
|
|
200
|
+
this.register(tool)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Unregister a tool by name */
|
|
207
|
+
unregister(name: string): boolean {
|
|
208
|
+
return this.tools.delete(name)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Check if a tool is registered */
|
|
212
|
+
has(name: string): boolean {
|
|
213
|
+
return this.tools.has(name)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Re-register write_plan tool with an updated callback */
|
|
217
|
+
reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
|
|
218
|
+
this.tools.delete('write_plan')
|
|
219
|
+
this.tools.set('write_plan', createWritePlanTool(cwd, cb))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Partially update the context (used by session.onPlanWritten setter) */
|
|
223
|
+
updateContext(partial: Partial<ToolContext>): void {
|
|
224
|
+
Object.assign(this.context, partial)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** List all registered tool names */
|
|
228
|
+
listTools(): string[] {
|
|
229
|
+
return Array.from(this.tools.keys())
|
|
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
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -39,6 +39,19 @@ import {
|
|
|
39
39
|
} from './components.js'
|
|
40
40
|
import { highlightLine } from './highlight.js'
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Invisible spacer that renders N empty lines.
|
|
44
|
+
* Used to reserve space at the bottom of the chat area so the editor
|
|
45
|
+
* overlay doesn't cover the last lines of command output.
|
|
46
|
+
*/
|
|
47
|
+
class Spacer {
|
|
48
|
+
constructor(private height: number) {}
|
|
49
|
+
render(_width: number): string[] {
|
|
50
|
+
return Array.from({ length: this.height }, () => '')
|
|
51
|
+
}
|
|
52
|
+
invalidate(): void {}
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export async function startTui(config: Config): Promise<void> {
|
|
43
56
|
let chatContent = ''
|
|
44
57
|
let isRunning = false
|
|
@@ -95,7 +108,13 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
95
108
|
const filtered = data.replace(/\x1b\[\?1049[hl]/g, '')
|
|
96
109
|
if (filtered) origWrite(filtered)
|
|
97
110
|
}
|
|
98
|
-
|
|
111
|
+
// Show hardware cursor by default so IME (Chinese input method) can
|
|
112
|
+
// position its candidate window at the correct cursor location.
|
|
113
|
+
// The cursor is hidden during agent execution via setShowHardwareCursor(false).
|
|
114
|
+
// Note: on some terminals (Windows Terminal), showing the hardware cursor
|
|
115
|
+
// can interfere with editor rendering layout. If you see editor border gap,
|
|
116
|
+
// set this to false and use PI_HARDWARE_CURSOR=1 env var to enable.
|
|
117
|
+
const tui = new TUI(terminal, true)
|
|
99
118
|
tui.setClearOnShrink(true)
|
|
100
119
|
terminal.setTitle(`lonny ${config.model} ${config.provider}`)
|
|
101
120
|
|
|
@@ -106,6 +125,9 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
106
125
|
const chatMarkdown = new Markdown('', 1, 0, markdownTheme)
|
|
107
126
|
const chatBox = new Box(1, 0)
|
|
108
127
|
chatBox.addChild(chatMarkdown)
|
|
128
|
+
// Reserve space at the bottom so the editor overlay doesn't cover
|
|
129
|
+
// the last lines of command output. 13 = maxHeight(12) + offsetY(1).
|
|
130
|
+
chatBox.addChild(new Spacer(13))
|
|
109
131
|
|
|
110
132
|
// Chat input — Editor with multi-line support, history, and autocomplete
|
|
111
133
|
const slashCommands: SlashCommand[] = [
|
package/src/web/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ import * as path from 'node:path'
|
|
|
4
4
|
import * as url from 'node:url'
|
|
5
5
|
import { type WebSocket, WebSocketServer } from 'ws'
|
|
6
6
|
import { resetGlobalEventBus } from '../agent/event-bus.js'
|
|
7
|
+
import { ensurePromptsDir, loadPromptTemplates } from '../agent/prompt-templates.js'
|
|
7
8
|
import { Session, type SessionOutput } from '../agent/session.js'
|
|
9
|
+
import { ensureSkillsDir, loadSkills } from '../agent/skills.js'
|
|
8
10
|
import type { Config } from '../config/index.js'
|
|
9
11
|
import { fmtErr } from '../tools/errors.js'
|
|
10
12
|
import { fetchDeepSeekBalance, isDeepSeekOfficial } from '../tui/balance.js'
|
|
@@ -255,6 +257,70 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
|
|
|
255
257
|
return
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
if (cmd === 'stop') {
|
|
261
|
+
sessionWithOutput.stop()
|
|
262
|
+
ws.send(JSON.stringify({ type: 'chunk', text: '\nStopped.\n' }))
|
|
263
|
+
ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (cmd === 'skills') {
|
|
268
|
+
const skills = loadSkills(config.cwd)
|
|
269
|
+
if (skills.length === 0) {
|
|
270
|
+
ws.send(
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
type: 'chunk',
|
|
273
|
+
text: 'No skills loaded. Create .md files in .lonny/skills/ or run install_superpowers.',
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
} else {
|
|
277
|
+
let msg = `Active Skills (${skills.length}):\n`
|
|
278
|
+
for (const s of skills) {
|
|
279
|
+
msg += ` \u2022 ${s.name}`
|
|
280
|
+
if (s.description) msg += ` - ${s.description}`
|
|
281
|
+
msg += '\n'
|
|
282
|
+
}
|
|
283
|
+
ws.send(JSON.stringify({ type: 'chunk', text: msg }))
|
|
284
|
+
}
|
|
285
|
+
ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (cmd === 'prompts') {
|
|
290
|
+
const templates = loadPromptTemplates(config.cwd)
|
|
291
|
+
if (templates.length === 0) {
|
|
292
|
+
ws.send(
|
|
293
|
+
JSON.stringify({
|
|
294
|
+
type: 'chunk',
|
|
295
|
+
text: 'No prompt templates found. Create .md files in .lonny/prompts/.',
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
298
|
+
} else {
|
|
299
|
+
let msg = `Prompt Templates (${templates.length}):\n`
|
|
300
|
+
for (const t of templates) {
|
|
301
|
+
msg += ` \u2022 ${t.name}`
|
|
302
|
+
if (t.description) msg += ` - ${t.description}`
|
|
303
|
+
msg += '\n'
|
|
304
|
+
}
|
|
305
|
+
ws.send(JSON.stringify({ type: 'chunk', text: msg }))
|
|
306
|
+
}
|
|
307
|
+
ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (cmd === 'init') {
|
|
312
|
+
ensureSkillsDir(config.cwd)
|
|
313
|
+
ensurePromptsDir(config.cwd)
|
|
314
|
+
ws.send(
|
|
315
|
+
JSON.stringify({
|
|
316
|
+
type: 'chunk',
|
|
317
|
+
text: 'Initialized .lonny/skills/ and .lonny/prompts/.',
|
|
318
|
+
}),
|
|
319
|
+
)
|
|
320
|
+
ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
258
324
|
if (cmd === 'help') {
|
|
259
325
|
ws.send(
|
|
260
326
|
JSON.stringify({
|
|
@@ -263,6 +329,10 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
|
|
|
263
329
|
'/mode code|plan|ask - Switch mode',
|
|
264
330
|
'/model <name> - Switch model',
|
|
265
331
|
'/new - Start a new session',
|
|
332
|
+
'/stop - Stop the running agent',
|
|
333
|
+
'/skills - List active skills',
|
|
334
|
+
'/prompts - List prompt templates',
|
|
335
|
+
'/init - Create .lonny/skills/ & prompts/',
|
|
266
336
|
'/help - Show this help',
|
|
267
337
|
],
|
|
268
338
|
}),
|
|
@@ -344,6 +414,7 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
|
|
|
344
414
|
JSON.stringify({
|
|
345
415
|
type: 'hello',
|
|
346
416
|
version: 1,
|
|
417
|
+
cwd: config.cwd,
|
|
347
418
|
mode: config.mode,
|
|
348
419
|
model: config.model,
|
|
349
420
|
provider: config.provider,
|