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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// ── Project type detection ─────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export type ProjectType = 'node' | 'python' | 'rust' | 'go' | 'java' | 'cpp' | 'unknown'
|
|
7
|
+
|
|
8
|
+
export interface ProjectInfo {
|
|
9
|
+
type: ProjectType
|
|
10
|
+
entryPoints: string[]
|
|
11
|
+
dependencies: Record<string, string>
|
|
12
|
+
devDependencies: Record<string, string>
|
|
13
|
+
testFiles: string[]
|
|
14
|
+
configFiles: string[]
|
|
15
|
+
srcDirs: string[]
|
|
16
|
+
hasTests: boolean
|
|
17
|
+
packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun' | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PackageJson {
|
|
21
|
+
name?: string
|
|
22
|
+
version?: string
|
|
23
|
+
main?: string
|
|
24
|
+
module?: string
|
|
25
|
+
exports?: string | Record<string, string>
|
|
26
|
+
dependencies?: Record<string, string>
|
|
27
|
+
devDependencies?: Record<string, string>
|
|
28
|
+
scripts?: Record<string, string>
|
|
29
|
+
type?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Common entry point patterns
|
|
33
|
+
const ENTRY_PATTERNS: Record<ProjectType, string[]> = {
|
|
34
|
+
node: [
|
|
35
|
+
'src/index.ts',
|
|
36
|
+
'src/main.ts',
|
|
37
|
+
'src/app.ts',
|
|
38
|
+
'src/server.ts',
|
|
39
|
+
'index.ts',
|
|
40
|
+
'app.ts',
|
|
41
|
+
'server.ts',
|
|
42
|
+
'main.ts',
|
|
43
|
+
],
|
|
44
|
+
python: ['main.py', 'app.py', 'src/main.py', 'src/app.py'],
|
|
45
|
+
rust: ['src/main.rs', 'src/lib.rs'],
|
|
46
|
+
go: ['main.go', 'cmd/main.go'],
|
|
47
|
+
java: ['src/main/java/Main.java'],
|
|
48
|
+
cpp: ['src/main.cpp', 'main.cpp'],
|
|
49
|
+
unknown: [],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TEST_PATTERNS: Record<ProjectType, string[]> = {
|
|
53
|
+
node: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**', 'tests/**/*.ts'],
|
|
54
|
+
python: ['test_*.py', '*_test.py', 'tests/**/*.py'],
|
|
55
|
+
rust: ['tests/**/*.rs', 'src/**/*.rs'],
|
|
56
|
+
go: ['*_test.go'],
|
|
57
|
+
java: ['**/*Test.java', '**/*Tests.java'],
|
|
58
|
+
cpp: ['**/*test*.cpp', 'tests/**/*.cpp'],
|
|
59
|
+
unknown: [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CONFIG_FILES: Record<ProjectType, string[]> = {
|
|
63
|
+
node: [
|
|
64
|
+
'package.json',
|
|
65
|
+
'tsconfig.json',
|
|
66
|
+
'biome.json',
|
|
67
|
+
'.biomerc',
|
|
68
|
+
'jest.config.*',
|
|
69
|
+
'vitest.config.*',
|
|
70
|
+
'.eslintrc*',
|
|
71
|
+
'.prettierrc*',
|
|
72
|
+
'.npmrc',
|
|
73
|
+
],
|
|
74
|
+
python: [
|
|
75
|
+
'pyproject.toml',
|
|
76
|
+
'setup.py',
|
|
77
|
+
'setup.cfg',
|
|
78
|
+
'requirements.txt',
|
|
79
|
+
'Pipfile',
|
|
80
|
+
'poetry.lock',
|
|
81
|
+
'pytest.ini',
|
|
82
|
+
'pyproject.toml',
|
|
83
|
+
],
|
|
84
|
+
rust: ['Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml'],
|
|
85
|
+
go: ['go.mod', 'go.sum'],
|
|
86
|
+
java: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
87
|
+
cpp: ['CMakeLists.txt', 'Makefile', '*.cmake'],
|
|
88
|
+
unknown: [],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SRC_DIRS: Record<ProjectType, string[]> = {
|
|
92
|
+
node: ['src', 'lib', 'app', 'packages'],
|
|
93
|
+
python: ['src', 'lib', 'app'],
|
|
94
|
+
rust: ['src'],
|
|
95
|
+
go: ['cmd', 'internal', 'pkg'],
|
|
96
|
+
java: ['src/main/java', 'src'],
|
|
97
|
+
cpp: ['src', 'lib', 'include'],
|
|
98
|
+
unknown: [],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Detection functions ───────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** Detect project type from root directory */
|
|
104
|
+
async function detectProjectType(cwd: string): Promise<ProjectType> {
|
|
105
|
+
const files = await fs.readdir(cwd, { withFileTypes: true })
|
|
106
|
+
const fileNames = files.map(f => f.name.toLowerCase())
|
|
107
|
+
|
|
108
|
+
// Priority order matters
|
|
109
|
+
if (fileNames.includes('cargo.toml')) return 'rust'
|
|
110
|
+
if (fileNames.includes('go.mod')) return 'go'
|
|
111
|
+
if (fileNames.includes('pom.xml') || fileNames.includes('build.gradle')) return 'java'
|
|
112
|
+
if (fileNames.includes('package.json')) return 'node'
|
|
113
|
+
if (
|
|
114
|
+
fileNames.includes('pyproject.toml') ||
|
|
115
|
+
fileNames.includes('setup.py') ||
|
|
116
|
+
fileNames.includes('requirements.txt')
|
|
117
|
+
)
|
|
118
|
+
return 'python'
|
|
119
|
+
if (fileNames.includes('cmakelists.txt')) return 'cpp'
|
|
120
|
+
|
|
121
|
+
return 'unknown'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Find entry points for the project */
|
|
125
|
+
async function findEntryPoints(cwd: string, type: ProjectType): Promise<string[]> {
|
|
126
|
+
if (type === 'unknown') return []
|
|
127
|
+
|
|
128
|
+
const entryPoints: string[] = []
|
|
129
|
+
const patterns = ENTRY_PATTERNS[type]
|
|
130
|
+
|
|
131
|
+
for (const pattern of patterns) {
|
|
132
|
+
const fullPath = path.join(cwd, pattern)
|
|
133
|
+
try {
|
|
134
|
+
const stat = await fs.stat(fullPath)
|
|
135
|
+
if (stat.isFile()) {
|
|
136
|
+
entryPoints.push(pattern)
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// File doesn't exist, skip
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// For Node.js, also check package.json main/module
|
|
144
|
+
if (type === 'node') {
|
|
145
|
+
try {
|
|
146
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
147
|
+
const content = await fs.readFile(pkgPath, 'utf-8')
|
|
148
|
+
const pkg: PackageJson = JSON.parse(content)
|
|
149
|
+
if (pkg.main && !entryPoints.includes(pkg.main)) {
|
|
150
|
+
entryPoints.push(pkg.main)
|
|
151
|
+
}
|
|
152
|
+
if (pkg.module && !entryPoints.includes(pkg.module)) {
|
|
153
|
+
entryPoints.push(pkg.module)
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// No package.json
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return entryPoints
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Find test files */
|
|
164
|
+
async function findTestFiles(cwd: string, type: ProjectType): Promise<string[]> {
|
|
165
|
+
if (type === 'unknown') return []
|
|
166
|
+
|
|
167
|
+
const testFiles: string[] = []
|
|
168
|
+
const patterns = TEST_PATTERNS[type]
|
|
169
|
+
|
|
170
|
+
// Simple glob simulation - just check common locations
|
|
171
|
+
const dirs =
|
|
172
|
+
type === 'node'
|
|
173
|
+
? ['src', 'tests', '__tests__', '']
|
|
174
|
+
: type === 'python'
|
|
175
|
+
? ['tests', '']
|
|
176
|
+
: type === 'rust'
|
|
177
|
+
? ['tests', 'src']
|
|
178
|
+
: ['']
|
|
179
|
+
|
|
180
|
+
for (const dir of dirs) {
|
|
181
|
+
const searchDir = dir ? path.join(cwd, dir) : cwd
|
|
182
|
+
try {
|
|
183
|
+
const files = await fs.readdir(searchDir, { withFileTypes: true })
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
if (file.isFile()) {
|
|
186
|
+
const name = file.name.toLowerCase()
|
|
187
|
+
if (name.includes('test') || name.includes('spec')) {
|
|
188
|
+
const relPath = dir ? `${dir}/${file.name}` : file.name
|
|
189
|
+
testFiles.push(relPath)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Directory doesn't exist
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return testFiles.slice(0, 10) // Limit to 10
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Find config files */
|
|
202
|
+
async function findConfigFiles(cwd: string, type: ProjectType): Promise<string[]> {
|
|
203
|
+
const configs: string[] = []
|
|
204
|
+
const patterns = CONFIG_FILES[type] || []
|
|
205
|
+
|
|
206
|
+
for (const pattern of patterns) {
|
|
207
|
+
const fullPath = path.join(cwd, pattern)
|
|
208
|
+
try {
|
|
209
|
+
const stat = await fs.stat(fullPath)
|
|
210
|
+
if (stat.isFile()) {
|
|
211
|
+
configs.push(pattern)
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// File doesn't exist
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return configs
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Find source directories */
|
|
222
|
+
async function findSrcDirs(cwd: string, type: ProjectType): Promise<string[]> {
|
|
223
|
+
const srcDirs: string[] = []
|
|
224
|
+
const patterns = SRC_DIRS[type] || []
|
|
225
|
+
|
|
226
|
+
for (const pattern of patterns) {
|
|
227
|
+
const fullPath = path.join(cwd, pattern)
|
|
228
|
+
try {
|
|
229
|
+
const stat = await fs.stat(fullPath)
|
|
230
|
+
if (stat.isDirectory()) {
|
|
231
|
+
srcDirs.push(pattern)
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// Directory doesn't exist
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return srcDirs
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Detect package manager */
|
|
242
|
+
async function detectPackageManager(cwd: string): Promise<ProjectInfo['packageManager']> {
|
|
243
|
+
const files = await fs.readdir(cwd, { withFileTypes: true })
|
|
244
|
+
const fileNames = files.map(f => f.name)
|
|
245
|
+
|
|
246
|
+
if (fileNames.includes('pnpm-lock.yaml')) return 'pnpm'
|
|
247
|
+
if (fileNames.includes('yarn.lock')) return 'yarn'
|
|
248
|
+
if (fileNames.includes('bun.lockb')) return 'bun'
|
|
249
|
+
if (fileNames.includes('package-lock.json')) return 'npm'
|
|
250
|
+
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Load dependencies from package.json */
|
|
255
|
+
async function loadDependencies(
|
|
256
|
+
cwd: string,
|
|
257
|
+
): Promise<{ deps: Record<string, string>; devDeps: Record<string, string> }> {
|
|
258
|
+
try {
|
|
259
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
260
|
+
const content = await fs.readFile(pkgPath, 'utf-8')
|
|
261
|
+
const pkg: PackageJson = JSON.parse(content)
|
|
262
|
+
return {
|
|
263
|
+
deps: pkg.dependencies || {},
|
|
264
|
+
devDeps: pkg.devDependencies || {},
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
return { deps: {}, devDeps: {} }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Main discovery function ───────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
// Cache for project info
|
|
274
|
+
const projectCache = new Map<string, { info: ProjectInfo; timestamp: number }>()
|
|
275
|
+
const CACHE_TTL = 60_000 // 1 minute
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Discover project information for a given directory.
|
|
279
|
+
* Results are cached for 1 minute to avoid repeated disk I/O.
|
|
280
|
+
*/
|
|
281
|
+
export async function discoverProject(cwd: string): Promise<ProjectInfo> {
|
|
282
|
+
// Check cache first
|
|
283
|
+
const cached = projectCache.get(cwd)
|
|
284
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
285
|
+
return cached.info
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const type = await detectProjectType(cwd)
|
|
289
|
+
const [entryPoints, testFiles, configFiles, srcDirs, packageManager, deps] = await Promise.all([
|
|
290
|
+
findEntryPoints(cwd, type),
|
|
291
|
+
findTestFiles(cwd, type),
|
|
292
|
+
findConfigFiles(cwd, type),
|
|
293
|
+
findSrcDirs(cwd, type),
|
|
294
|
+
detectPackageManager(cwd),
|
|
295
|
+
type === 'node' ? loadDependencies(cwd) : Promise.resolve({ deps: {}, devDeps: {} }),
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
const info: ProjectInfo = {
|
|
299
|
+
type,
|
|
300
|
+
entryPoints,
|
|
301
|
+
dependencies: deps.deps,
|
|
302
|
+
devDependencies: deps.devDeps,
|
|
303
|
+
testFiles,
|
|
304
|
+
configFiles,
|
|
305
|
+
srcDirs,
|
|
306
|
+
hasTests: testFiles.length > 0,
|
|
307
|
+
packageManager,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Cache the result
|
|
311
|
+
projectCache.set(cwd, { info, timestamp: Date.now() })
|
|
312
|
+
|
|
313
|
+
return info
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Clear project cache */
|
|
317
|
+
export function clearProjectCache(): void {
|
|
318
|
+
projectCache.clear()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Invalidate cache for a specific directory */
|
|
322
|
+
export function invalidateProjectCache(cwd: string): void {
|
|
323
|
+
projectCache.delete(cwd)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Format project info as a string for inclusion in prompts.
|
|
328
|
+
*/
|
|
329
|
+
export function formatProjectContext(info: ProjectInfo): string {
|
|
330
|
+
const lines: string[] = ['## Project Context']
|
|
331
|
+
|
|
332
|
+
if (info.type === 'unknown') {
|
|
333
|
+
lines.push('- Project type: Unknown')
|
|
334
|
+
return lines.join('\n')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lines.push(`- Project type: ${info.type}`)
|
|
338
|
+
lines.push(`- Package manager: ${info.packageManager || 'unknown'}`)
|
|
339
|
+
|
|
340
|
+
if (info.entryPoints.length > 0) {
|
|
341
|
+
lines.push(`- Entry point(s): ${info.entryPoints.join(', ')}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (info.srcDirs.length > 0) {
|
|
345
|
+
lines.push(`- Source directories: ${info.srcDirs.join(', ')}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (info.configFiles.length > 0) {
|
|
349
|
+
lines.push(`- Config files: ${info.configFiles.join(', ')}`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (info.hasTests) {
|
|
353
|
+
const testCount = info.testFiles.length
|
|
354
|
+
lines.push(`- Test files: ${testCount} found`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Show main dependencies
|
|
358
|
+
const mainDeps = Object.keys(info.dependencies).slice(0, 10)
|
|
359
|
+
if (mainDeps.length > 0) {
|
|
360
|
+
lines.push(
|
|
361
|
+
`- Dependencies: ${mainDeps.join(', ')}${Object.keys(info.dependencies).length > 10 ? '...' : ''}`,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return lines.join('\n')
|
|
366
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as os from 'node:os'
|
|
2
2
|
import type { Config } from '../config/index.js'
|
|
3
|
+
import { discoverProject, formatProjectContext } from './project.js'
|
|
3
4
|
import { formatSkillsForPrompt, loadSkills } from './skills.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Build the system prompt for the current configuration.
|
|
7
8
|
* Extracted from session.ts to keep module size manageable (<500 LoC target).
|
|
8
9
|
*/
|
|
9
|
-
export function buildSystemPrompt(config: Config): string {
|
|
10
|
+
export async function buildSystemPrompt(config: Config): Promise<string> {
|
|
10
11
|
const platform = os.platform()
|
|
11
12
|
const release = os.release()
|
|
12
13
|
const shell = process.env.SHELL || process.env.ComSpec || 'unknown'
|
|
@@ -18,6 +19,10 @@ export function buildSystemPrompt(config: Config): string {
|
|
|
18
19
|
const skills = loadSkills(cwd)
|
|
19
20
|
const skillsSection = formatSkillsForPrompt(skills)
|
|
20
21
|
|
|
22
|
+
// ── Load project context ─────────────────────────────────────────────────
|
|
23
|
+
const projectInfo = await discoverProject(cwd)
|
|
24
|
+
const projectSection = formatProjectContext(projectInfo)
|
|
25
|
+
|
|
21
26
|
// ── Mode-specific tool list ───────────────────────────────────────────
|
|
22
27
|
function getToolListForMode(mode: string): string {
|
|
23
28
|
if (mode === 'ask') {
|
|
@@ -36,7 +41,7 @@ export function buildSystemPrompt(config: Config): string {
|
|
|
36
41
|
- \`find\`: Find files by name pattern (pattern: string, path?: string, maxResults?: number)
|
|
37
42
|
- \`git\`: Run read-only git commands (command: string)
|
|
38
43
|
- \`search\`: Search the web using Tavily (query: string, search_depth?: string, include_answer?: boolean, max_results?: number, topic?: string, days?: number)
|
|
39
|
-
- \`write_plan\`: Save plan/todo markdown into .lonny/ folder
|
|
44
|
+
- \`write_plan\`: Save plan/todo markdown into .lonny/ folder (use descriptive names like backend-api.md, frontend-ui.md if splitting into multiple files)
|
|
40
45
|
`
|
|
41
46
|
}
|
|
42
47
|
return `Available tools:
|
|
@@ -45,7 +50,7 @@ export function buildSystemPrompt(config: Config): string {
|
|
|
45
50
|
- \`grep\`: Search file content by regex (pattern: string, include?: string, path?: string)
|
|
46
51
|
- \`ls\`: List directory (path?: string)
|
|
47
52
|
- \`bash\`: Execute a shell command
|
|
48
|
-
- \`edit\`: Replace text in files — call with {
|
|
53
|
+
- \`edit\`: Replace text in files — call with {edits: [{file_path: string, old_string: string, new_string: string}]} (array required)
|
|
49
54
|
- \`install_skill\`: Install an npm package as a skill — fetches package info from npm, runs npm install, and creates a .lonny/skills/ file with usage instructions for the AI
|
|
50
55
|
- \`find\`: Find files by name pattern (pattern: string, path?: string, maxResults?: number)
|
|
51
56
|
- \`git\`: Run read-only git commands (command: string)
|
|
@@ -78,7 +83,8 @@ RULES (plan-specific):
|
|
|
78
83
|
3. Use \`bash\` for investigation only — NEVER to modify files, install packages, or run write operations.
|
|
79
84
|
4. Your ONLY output is a plan file saved via \`write_plan\`. You CANNOT modify the codebase directly.
|
|
80
85
|
5. You MUST persist the final plan AND todo list to a file in \`.lonny/\` using \`write_plan\`. The \`write_plan\` content MUST include both ## Plan and ## Todo List sections.
|
|
81
|
-
6.
|
|
86
|
+
6. If the plan is very long (or the todo list has many items), split into multiple files with descriptive names like \`backend-api.md\`, \`frontend-ui.md\`, \`database.md\`, etc.
|
|
87
|
+
7. You MUST also include the todo list in your text response to the user (not just in the file).
|
|
82
88
|
7. If the user asks you to modify files, run write commands, or install packages — refuse and explain they need to switch to code mode (\`/mode code\`).
|
|
83
89
|
|
|
84
90
|
OUTPUT FORMAT (you MUST include both in write_plan AND in your response text):
|
|
@@ -171,5 +177,5 @@ ${isWindows ? ' - Use `type` instead of `cat`, `dir` instead of `ls`, `where` i
|
|
|
171
177
|
${sharedRules}`
|
|
172
178
|
return `${modeInstructions}
|
|
173
179
|
|
|
174
|
-
${envSection}${rulesSection}${methodologySection}${skillsSection}`
|
|
180
|
+
${envSection}${rulesSection}${methodologySection}${projectSection}${skillsSection}`
|
|
175
181
|
}
|
|
@@ -116,12 +116,23 @@ export class AnthropicProvider implements LLMProvider {
|
|
|
116
116
|
}
|
|
117
117
|
} else if (event.type === 'content_block_stop') {
|
|
118
118
|
if (currentToolUse) {
|
|
119
|
+
let input: Record<string, unknown>
|
|
120
|
+
const rawInput = currentToolUse.input || ''
|
|
121
|
+
try {
|
|
122
|
+
input = JSON.parse(rawInput || '{}')
|
|
123
|
+
} catch {
|
|
124
|
+
console.error(
|
|
125
|
+
'[anthropic] Failed to parse tool_use input (content_block_stop):',
|
|
126
|
+
rawInput,
|
|
127
|
+
)
|
|
128
|
+
input = {}
|
|
129
|
+
}
|
|
119
130
|
yield {
|
|
120
131
|
type: 'tool_use',
|
|
121
132
|
tool_call: {
|
|
122
133
|
id: currentToolUse.id,
|
|
123
134
|
name: currentToolUse.name,
|
|
124
|
-
input
|
|
135
|
+
input,
|
|
125
136
|
},
|
|
126
137
|
}
|
|
127
138
|
currentToolUse = null
|
|
@@ -138,12 +149,20 @@ export class AnthropicProvider implements LLMProvider {
|
|
|
138
149
|
}
|
|
139
150
|
if (event.delta.stop_reason === 'end_turn' || event.delta.stop_reason === 'stop_sequence') {
|
|
140
151
|
if (currentToolUse) {
|
|
152
|
+
let input: Record<string, unknown>
|
|
153
|
+
const rawInput = currentToolUse.input || ''
|
|
154
|
+
try {
|
|
155
|
+
input = JSON.parse(rawInput || '{}')
|
|
156
|
+
} catch {
|
|
157
|
+
console.error('[anthropic] Failed to parse tool_use input (message_delta):', rawInput)
|
|
158
|
+
input = {}
|
|
159
|
+
}
|
|
141
160
|
yield {
|
|
142
161
|
type: 'tool_use',
|
|
143
162
|
tool_call: {
|
|
144
163
|
id: currentToolUse.id,
|
|
145
164
|
name: currentToolUse.name,
|
|
146
|
-
input
|
|
165
|
+
input,
|
|
147
166
|
},
|
|
148
167
|
}
|
|
149
168
|
currentToolUse = null
|
|
@@ -150,6 +150,10 @@ export class OllamaProvider implements LLMProvider {
|
|
|
150
150
|
try {
|
|
151
151
|
input = JSON.parse(tc.function.arguments || '{}')
|
|
152
152
|
} catch {
|
|
153
|
+
console.error(
|
|
154
|
+
'[ollama] Failed to parse tool_call arguments:',
|
|
155
|
+
tc.function.arguments,
|
|
156
|
+
)
|
|
153
157
|
input = {}
|
|
154
158
|
}
|
|
155
159
|
yield {
|
|
@@ -174,7 +178,7 @@ export class OllamaProvider implements LLMProvider {
|
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
} catch {
|
|
177
|
-
|
|
181
|
+
console.error('[ollama] Failed to parse stream line:', line)
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
}
|
|
@@ -200,12 +200,23 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
200
200
|
for (const tc of delta.tool_calls) {
|
|
201
201
|
if (tc.id) {
|
|
202
202
|
if (currentToolCall) {
|
|
203
|
+
let input: Record<string, unknown>
|
|
204
|
+
const rawArgs = currentToolCall.arguments || ''
|
|
205
|
+
try {
|
|
206
|
+
input = JSON.parse(rawArgs || '{}')
|
|
207
|
+
} catch {
|
|
208
|
+
console.error(
|
|
209
|
+
'[openai] Failed to parse tool_call arguments (flush on new tool):',
|
|
210
|
+
rawArgs,
|
|
211
|
+
)
|
|
212
|
+
input = {}
|
|
213
|
+
}
|
|
203
214
|
yield {
|
|
204
215
|
type: 'tool_use',
|
|
205
216
|
tool_call: {
|
|
206
217
|
id: currentToolCall.id,
|
|
207
218
|
name: currentToolCall.name,
|
|
208
|
-
input
|
|
219
|
+
input,
|
|
209
220
|
},
|
|
210
221
|
reasoning_content: reasoningContent,
|
|
211
222
|
}
|
|
@@ -224,17 +235,22 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
224
235
|
|
|
225
236
|
if (chunk.choices?.[0]?.finish_reason) {
|
|
226
237
|
if (currentToolCall) {
|
|
238
|
+
const finalArgs = currentToolCall.arguments || ''
|
|
227
239
|
try {
|
|
228
240
|
yield {
|
|
229
241
|
type: 'tool_use',
|
|
230
242
|
tool_call: {
|
|
231
243
|
id: currentToolCall.id,
|
|
232
244
|
name: currentToolCall.name,
|
|
233
|
-
input: JSON.parse(
|
|
245
|
+
input: JSON.parse(finalArgs || '{}'),
|
|
234
246
|
},
|
|
235
247
|
reasoning_content: reasoningContent,
|
|
236
248
|
}
|
|
237
249
|
} catch {
|
|
250
|
+
console.error(
|
|
251
|
+
'[openai] Failed to parse tool_call arguments (finish_reason):',
|
|
252
|
+
finalArgs,
|
|
253
|
+
)
|
|
238
254
|
yield {
|
|
239
255
|
type: 'tool_use',
|
|
240
256
|
tool_call: {
|
|
@@ -296,12 +312,20 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
296
312
|
output_tokens: lastUsage.completion_tokens ?? 0,
|
|
297
313
|
}
|
|
298
314
|
: undefined
|
|
315
|
+
let input: Record<string, unknown>
|
|
316
|
+
const rawFinalArgs = currentToolCall.arguments || ''
|
|
317
|
+
try {
|
|
318
|
+
input = JSON.parse(rawFinalArgs || '{}')
|
|
319
|
+
} catch {
|
|
320
|
+
console.error('[openai] Failed to parse tool_call arguments (final flush):', rawFinalArgs)
|
|
321
|
+
input = {}
|
|
322
|
+
}
|
|
299
323
|
yield {
|
|
300
324
|
type: 'tool_use',
|
|
301
325
|
tool_call: {
|
|
302
326
|
id: currentToolCall.id,
|
|
303
327
|
name: currentToolCall.name,
|
|
304
|
-
input
|
|
328
|
+
input,
|
|
305
329
|
},
|
|
306
330
|
reasoning_content: reasoningContent,
|
|
307
331
|
usage,
|