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/git.ts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import type { Tool, ToolDefinition } from './types.js'
|
|
3
|
-
|
|
4
|
-
export const createGitTool = (cwd: string): Tool => {
|
|
5
|
-
const definition: ToolDefinition = {
|
|
6
|
-
name: 'git',
|
|
7
|
-
description:
|
|
8
|
-
'Run git commands for repository operations. Supports status, diff, log, show, branch, and other read-only git operations.',
|
|
9
|
-
parameters: {
|
|
10
|
-
command: {
|
|
11
|
-
type: 'string',
|
|
12
|
-
description: 'Git subcommand and arguments (e.g. "status", "diff", "log --oneline -5")',
|
|
13
|
-
required: true,
|
|
14
|
-
},
|
|
15
|
-
path: { type: 'string', description: 'Repository path (default: cwd)', required: false },
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const execute = async (
|
|
20
|
-
input: Record<string, unknown>,
|
|
21
|
-
): Promise<{ success: boolean; output: string; error?: string }> => {
|
|
22
|
-
const gitCmd = String(input.command || '').trim()
|
|
23
|
-
const repoPath = String(input.path || cwd)
|
|
24
|
-
|
|
25
|
-
if (!gitCmd) {
|
|
26
|
-
return { success: false, output: '', error: 'command is required' }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Safety: block destructive commands
|
|
30
|
-
const destructivePatterns = [
|
|
31
|
-
/^reset\s+(--hard|--soft|--mixed|--merge|--keep)/i,
|
|
32
|
-
/^push\s+.*(--force|-f)/i,
|
|
33
|
-
/^(rebase|merge|cherry-pick|revert)\b/i,
|
|
34
|
-
/^branch\s+-[dD]/i,
|
|
35
|
-
/^tag\s+-[dD]/i,
|
|
36
|
-
/^(clean|gc|prune)\b/i,
|
|
37
|
-
/^update-ref\b/i,
|
|
38
|
-
/^rm\b/i,
|
|
39
|
-
/^commit\s+--amend/i,
|
|
40
|
-
/^checkout\s+.*-B/i,
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
for (const pattern of destructivePatterns) {
|
|
44
|
-
if (pattern.test(gitCmd)) {
|
|
45
|
-
return {
|
|
46
|
-
success: false,
|
|
47
|
-
output: '',
|
|
48
|
-
error: `Destructive git command not allowed: "${gitCmd}". Only read-only operations are permitted.`,
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = execSync(`git ${gitCmd}`, {
|
|
55
|
-
cwd: repoPath,
|
|
56
|
-
encoding: 'utf-8',
|
|
57
|
-
timeout: 30000,
|
|
58
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
return { success: true, output: result.trim() || '(empty output)' }
|
|
62
|
-
} catch (err) {
|
|
63
|
-
const error = err as { stderr?: string; stdout?: string; message?: string }
|
|
64
|
-
const stderr = error.stderr || ''
|
|
65
|
-
const stdout = error.stdout || ''
|
|
66
|
-
const msg = error.message || String(err)
|
|
67
|
-
return {
|
|
68
|
-
success: false,
|
|
69
|
-
output: stdout.trim(),
|
|
70
|
-
error: stderr.trim() || msg,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { definition, execute }
|
|
76
|
-
}
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import type { Tool, ToolDefinition } from './types.js'
|
|
3
|
+
|
|
4
|
+
export const createGitTool = (cwd: string): Tool => {
|
|
5
|
+
const definition: ToolDefinition = {
|
|
6
|
+
name: 'git',
|
|
7
|
+
description:
|
|
8
|
+
'Run git commands for repository operations. Supports status, diff, log, show, branch, and other read-only git operations.',
|
|
9
|
+
parameters: {
|
|
10
|
+
command: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Git subcommand and arguments (e.g. "status", "diff", "log --oneline -5")',
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
path: { type: 'string', description: 'Repository path (default: cwd)', required: false },
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const execute = async (
|
|
20
|
+
input: Record<string, unknown>,
|
|
21
|
+
): Promise<{ success: boolean; output: string; error?: string }> => {
|
|
22
|
+
const gitCmd = String(input.command || '').trim()
|
|
23
|
+
const repoPath = String(input.path || cwd)
|
|
24
|
+
|
|
25
|
+
if (!gitCmd) {
|
|
26
|
+
return { success: false, output: '', error: 'command is required' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Safety: block destructive commands
|
|
30
|
+
const destructivePatterns = [
|
|
31
|
+
/^reset\s+(--hard|--soft|--mixed|--merge|--keep)/i,
|
|
32
|
+
/^push\s+.*(--force|-f)/i,
|
|
33
|
+
/^(rebase|merge|cherry-pick|revert)\b/i,
|
|
34
|
+
/^branch\s+-[dD]/i,
|
|
35
|
+
/^tag\s+-[dD]/i,
|
|
36
|
+
/^(clean|gc|prune)\b/i,
|
|
37
|
+
/^update-ref\b/i,
|
|
38
|
+
/^rm\b/i,
|
|
39
|
+
/^commit\s+--amend/i,
|
|
40
|
+
/^checkout\s+.*-B/i,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
for (const pattern of destructivePatterns) {
|
|
44
|
+
if (pattern.test(gitCmd)) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
output: '',
|
|
48
|
+
error: `Destructive git command not allowed: "${gitCmd}". Only read-only operations are permitted.`,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = execSync(`git ${gitCmd}`, {
|
|
55
|
+
cwd: repoPath,
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
timeout: 30000,
|
|
58
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return { success: true, output: result.trim() || '(empty output)' }
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const error = err as { stderr?: string; stdout?: string; message?: string }
|
|
64
|
+
const stderr = error.stderr || ''
|
|
65
|
+
const stdout = error.stdout || ''
|
|
66
|
+
const msg = error.message || String(err)
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
output: stdout.trim(),
|
|
70
|
+
error: stderr.trim() || msg,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { definition, execute }
|
|
76
|
+
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import * as fs from 'node:fs/promises'
|
|
3
3
|
import * as path from 'node:path'
|
|
4
4
|
import { fmtErr } from './errors.js'
|
|
5
5
|
import type { Tool, ToolResult } from './types.js'
|
|
6
6
|
|
|
7
|
+
let _hasRg: boolean | undefined
|
|
8
|
+
|
|
7
9
|
function hasRg(): boolean {
|
|
10
|
+
if (_hasRg !== undefined) return _hasRg
|
|
8
11
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
execFileSync('rg', ['--version'], { stdio: 'pipe' })
|
|
13
|
+
_hasRg = true
|
|
11
14
|
} catch {
|
|
12
|
-
|
|
15
|
+
_hasRg = false
|
|
13
16
|
}
|
|
17
|
+
return _hasRg
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/** Simple glob-to-regex for the `include` parameter (e.g. "*.ts", "*.{ts,tsx}"). */
|
|
@@ -25,9 +29,11 @@ function includeRe(include: string): RegExp {
|
|
|
25
29
|
continue
|
|
26
30
|
}
|
|
27
31
|
if (ch === '*') {
|
|
32
|
+
// Handle ** (match across dirs) and * (match within a single dir segment)
|
|
28
33
|
if (include[i + 1] === '*') {
|
|
29
34
|
re += '.*'
|
|
30
|
-
|
|
35
|
+
// Skip all consecutive asterisks so `***` doesn't leave a trailing literal `*`
|
|
36
|
+
while (i < include.length && include[i] === '*') i++
|
|
31
37
|
continue
|
|
32
38
|
}
|
|
33
39
|
re += '[^/]*'
|
|
@@ -48,6 +54,12 @@ function includeRe(include: string): RegExp {
|
|
|
48
54
|
i++
|
|
49
55
|
continue
|
|
50
56
|
}
|
|
57
|
+
// Escape special regex characters (except those we handle above)
|
|
58
|
+
if (/[.+^${}()|[\]\\]/.test(ch)) {
|
|
59
|
+
re += '\\' + ch
|
|
60
|
+
i++
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
51
63
|
re += ch
|
|
52
64
|
i++
|
|
53
65
|
}
|
|
@@ -64,32 +76,38 @@ async function nodeGrep(dir: string, re: RegExp, incRe: RegExp | null): Promise<
|
|
|
64
76
|
const results: NodeGrepMatch[] = []
|
|
65
77
|
try {
|
|
66
78
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
67
|
-
const
|
|
79
|
+
const promises: Promise<void>[] = []
|
|
80
|
+
|
|
68
81
|
for (const e of entries) {
|
|
69
82
|
const full = path.join(dir, e.name)
|
|
70
83
|
if (e.isDirectory()) {
|
|
71
84
|
if (e.name === '.git' || e.name === 'node_modules') continue
|
|
72
|
-
|
|
85
|
+
promises.push(
|
|
86
|
+
nodeGrep(full, re, incRe).then(sub => {
|
|
87
|
+
for (const m of sub) results.push(m)
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
73
90
|
} else if (e.isFile()) {
|
|
74
91
|
if (incRe && !incRe.test(e.name)) continue
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
promises.push(
|
|
93
|
+
(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(full, 'utf-8')
|
|
96
|
+
const lines = content.split('\n')
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
if (re.test(lines[i])) {
|
|
99
|
+
results.push({ file: full, line: i + 1, text: lines[i] })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
/* permission denied etc */
|
|
81
104
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* permission denied etc */
|
|
85
|
-
}
|
|
105
|
+
})(),
|
|
106
|
+
)
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const sm of subMatches) {
|
|
91
|
-
results.push(...sm)
|
|
92
|
-
}
|
|
109
|
+
|
|
110
|
+
await Promise.all(promises)
|
|
93
111
|
} catch {
|
|
94
112
|
/* permission denied etc */
|
|
95
113
|
}
|
|
@@ -120,11 +138,23 @@ export function createGrepTool(cwd: string): Tool {
|
|
|
120
138
|
|
|
121
139
|
try {
|
|
122
140
|
if (useRg) {
|
|
123
|
-
|
|
124
|
-
if (include)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
const args = ['-n', '--no-heading', pattern]
|
|
142
|
+
if (include) args.push('-g', include)
|
|
143
|
+
args.push(searchPath)
|
|
144
|
+
try {
|
|
145
|
+
const output = execFileSync('rg', args, {
|
|
146
|
+
encoding: 'utf-8',
|
|
147
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
148
|
+
})
|
|
149
|
+
return { success: true, output: output || 'No matches found.' }
|
|
150
|
+
} catch (rgErr: unknown) {
|
|
151
|
+
const rgMsg = fmtErr(rgErr)
|
|
152
|
+
// ripgrep exit code 1 = no matches, exit code 2 = error
|
|
153
|
+
if (rgMsg.includes('exit code 1')) {
|
|
154
|
+
return { success: true, output: 'No matches found.' }
|
|
155
|
+
}
|
|
156
|
+
throw rgErr
|
|
157
|
+
}
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
const re = new RegExp(pattern)
|
|
@@ -141,9 +171,6 @@ export function createGrepTool(cwd: string): Tool {
|
|
|
141
171
|
return { success: true, output }
|
|
142
172
|
} catch (err) {
|
|
143
173
|
const msg = fmtErr(err)
|
|
144
|
-
if (useRg && msg.includes('exit code 1')) {
|
|
145
|
-
return { success: true, output: 'No matches found.' }
|
|
146
|
-
}
|
|
147
174
|
return { success: false, output: '', error: `Grep failed: ${msg}` }
|
|
148
175
|
}
|
|
149
176
|
},
|
package/src/tools/read.ts
CHANGED
|
@@ -4,14 +4,23 @@ import type { FileReadTracker } from '../diff/apply.js'
|
|
|
4
4
|
import { fmtErr } from './errors.js'
|
|
5
5
|
import type { Tool, ToolResult } from './types.js'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// ── Configuration ─────────────────────────────────────────────────────────
|
|
8
|
+
const MAX_FILE_SIZE = 1_000_000 // 1MB
|
|
9
|
+
const DEFAULT_MAX_LINES = 500 // Default pagination limit
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format content with line numbers.
|
|
13
|
+
* Handles trailing newline correctly.
|
|
14
|
+
* Exported for testing and reuse.
|
|
15
|
+
*/
|
|
16
|
+
export function formatWithLineNumbers(content: string, startLine = 1): string {
|
|
8
17
|
const lines = content.split('\n')
|
|
9
18
|
// Drop trailing empty element produced by a final \n so the displayed
|
|
10
19
|
// line count matches the actual line count of the file.
|
|
11
20
|
const hasTrailingNewline = lines.length > 0 && lines[lines.length - 1] === ''
|
|
12
21
|
const body = hasTrailingNewline ? lines.slice(0, -1) : lines
|
|
13
|
-
const pad = String(body.length).length
|
|
14
|
-
return body.map((l, i) => `${String(
|
|
22
|
+
const pad = String(startLine + body.length - 1).length
|
|
23
|
+
return body.map((l, i) => `${String(startLine + i).padStart(pad, ' ')}: ${l}`).join('\n')
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export function createReadTool(applier: FileReadTracker, cwd: string): Tool {
|
|
@@ -19,9 +28,15 @@ export function createReadTool(applier: FileReadTracker, cwd: string): Tool {
|
|
|
19
28
|
definition: {
|
|
20
29
|
name: 'read',
|
|
21
30
|
description:
|
|
22
|
-
'Read the contents of one or more files. Always read a file before editing it. Each line is prefixed with "<lineNumber>: " for accurate line references; the prefix is a display aid only — do NOT include it in batch_edit patch content.',
|
|
31
|
+
'Read the contents of one or more files. Always read a file before editing it. Each line is prefixed with "<lineNumber>: " for accurate line references; the prefix is a display aid only — do NOT include it in batch_edit patch content. Supports pagination via startLine and maxLines for large files.',
|
|
23
32
|
parameters: {
|
|
24
33
|
paths: { type: 'array', description: 'File paths to read', required: true },
|
|
34
|
+
startLine: { type: 'number', description: 'Start line number (1-based)', required: false },
|
|
35
|
+
maxLines: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'Maximum number of lines to read',
|
|
38
|
+
required: false,
|
|
39
|
+
},
|
|
25
40
|
},
|
|
26
41
|
},
|
|
27
42
|
async execute(input): Promise<ToolResult> {
|
|
@@ -30,28 +45,49 @@ export function createReadTool(applier: FileReadTracker, cwd: string): Tool {
|
|
|
30
45
|
return { success: false, output: '', error: 'paths must be a non-empty array' }
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`=== ${filePath} ===\n(error:
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const startLine = typeof input.startLine === 'number' ? Math.max(1, input.startLine) : 1
|
|
49
|
+
const maxLines =
|
|
50
|
+
typeof input.maxLines === 'number' ? Math.max(1, input.maxLines) : DEFAULT_MAX_LINES
|
|
51
|
+
|
|
52
|
+
// Read files in parallel
|
|
53
|
+
const results = await Promise.all(
|
|
54
|
+
paths.map(async (filePath): Promise<string> => {
|
|
55
|
+
try {
|
|
56
|
+
const resolved = path.resolve(cwd, filePath)
|
|
57
|
+
const stat = await fs.stat(resolved)
|
|
58
|
+
if (!stat.isFile()) {
|
|
59
|
+
return `=== ${filePath} ===\n(error: not a file)`
|
|
60
|
+
}
|
|
61
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
62
|
+
return `=== ${filePath} ===\n(error: file too large (>1MB), use bash to read it selectively)`
|
|
63
|
+
}
|
|
64
|
+
let content = await fs.readFile(resolved, 'utf-8')
|
|
65
|
+
|
|
66
|
+
// Handle pagination
|
|
67
|
+
const allLines = content.split('\n')
|
|
68
|
+
const hasTrailingNewline = allLines.length > 0 && allLines[allLines.length - 1] === ''
|
|
69
|
+
const totalLines = hasTrailingNewline ? allLines.length - 1 : allLines.length
|
|
70
|
+
|
|
71
|
+
if (startLine > totalLines) {
|
|
72
|
+
return `=== ${filePath} ===\n(error: startLine ${startLine} exceeds file length ${totalLines})`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const endLine = Math.min(startLine + maxLines - 1, totalLines)
|
|
76
|
+
const selectedLines = allLines.slice(startLine - 1, endLine)
|
|
77
|
+
content = selectedLines.join('\n') + (hasTrailingNewline ? '\n' : '')
|
|
78
|
+
|
|
79
|
+
const displayStart = startLine
|
|
80
|
+
const displayEnd = endLine
|
|
81
|
+
const truncated =
|
|
82
|
+
endLine < totalLines ? ` (lines ${displayStart}-${displayEnd} of ${totalLines})` : ''
|
|
83
|
+
|
|
84
|
+
applier.markRead(resolved)
|
|
85
|
+
return `=== ${filePath} ===${truncated}\n${formatWithLineNumbers(content, startLine)}`
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return `=== ${filePath} ===\n(error: ${fmtErr(err)})`
|
|
47
88
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
results.push(`=== ${filePath} ===\n${formatWithLineNumbers(content)}`)
|
|
51
|
-
} catch (err) {
|
|
52
|
-
results.push(`=== ${filePath} ===\n(error: ${fmtErr(err)})`)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
55
91
|
|
|
56
92
|
return { success: true, output: results.join('\n\n') }
|
|
57
93
|
},
|