koneck 1.0.0
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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/astValidator.d.ts +6 -0
- package/dist/astValidator.d.ts.map +1 -0
- package/dist/astValidator.js +57 -0
- package/dist/astValidator.js.map +1 -0
- package/dist/chat.d.ts +3 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +205 -0
- package/dist/chat.js.map +1 -0
- package/dist/completion.d.ts +2 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +137 -0
- package/dist/completion.js.map +1 -0
- package/dist/compressor.d.ts +9 -0
- package/dist/compressor.d.ts.map +1 -0
- package/dist/compressor.js +71 -0
- package/dist/compressor.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +40 -0
- package/dist/config.js.map +1 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +99 -0
- package/dist/doctor.js.map +1 -0
- package/dist/engine.d.ts +11 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +510 -0
- package/dist/engine.js.map +1 -0
- package/dist/generate-koneck-md.d.ts +3 -0
- package/dist/generate-koneck-md.d.ts.map +1 -0
- package/dist/generate-koneck-md.js +69 -0
- package/dist/generate-koneck-md.js.map +1 -0
- package/dist/git-tools.d.ts +4 -0
- package/dist/git-tools.d.ts.map +1 -0
- package/dist/git-tools.js +206 -0
- package/dist/git-tools.js.map +1 -0
- package/dist/highlight.d.ts +3 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +139 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +448 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +93 -0
- package/dist/init.js.map +1 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +75 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +3 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +18 -0
- package/dist/memory.js.map +1 -0
- package/dist/plugins.d.ts +9 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +60 -0
- package/dist/plugins.js.map +1 -0
- package/dist/pricing.d.ts +9 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +84 -0
- package/dist/pricing.js.map +1 -0
- package/dist/project.d.ts +14 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +151 -0
- package/dist/project.js.map +1 -0
- package/dist/providers.d.ts +13 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +130 -0
- package/dist/providers.js.map +1 -0
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +71 -0
- package/dist/session.js.map +1 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +63 -0
- package/dist/setup.js.map +1 -0
- package/dist/stats.d.ts +2 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +81 -0
- package/dist/stats.js.map +1 -0
- package/dist/tools.d.ts +17 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +713 -0
- package/dist/tools.js.map +1 -0
- package/dist/tui.d.ts +38 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +364 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +68 -0
- package/dist/watch.js.map +1 -0
- package/dist/worktree.d.ts +8 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +94 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +67 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { globby } from 'globby';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import * as diffLib from 'diff';
|
|
6
|
+
import { validateCode } from './astValidator.js';
|
|
7
|
+
import { tui } from './tui.js';
|
|
8
|
+
import { appendMemory } from './memory.js';
|
|
9
|
+
import { GIT_TOOL_DEFINITIONS, dispatchGitTool } from './git-tools.js';
|
|
10
|
+
export const TOOL_DEFINITIONS = [
|
|
11
|
+
{
|
|
12
|
+
name: 'read_files',
|
|
13
|
+
description: 'Read one or more files concurrently. Returns content with line numbers. Use for any file inspection.',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
paths: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: { type: 'string' },
|
|
20
|
+
description: 'File paths to read (absolute or relative to cwd)',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['paths'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'write_file',
|
|
28
|
+
description: 'Write full content to a file. For TypeScript/JavaScript files, syntax is validated before writing. Creates parent directories as needed.',
|
|
29
|
+
parameters: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
path: { type: 'string', description: 'Target file path' },
|
|
33
|
+
content: { type: 'string', description: 'Complete file content to write' },
|
|
34
|
+
},
|
|
35
|
+
required: ['path', 'content'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'apply_diff',
|
|
40
|
+
description: 'Apply a unified diff patch to a file for surgical line-level modifications. Preferred over write_file for partial changes.',
|
|
41
|
+
parameters: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
path: { type: 'string', description: 'File path to patch' },
|
|
45
|
+
diff: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Unified diff content in --- a/file +++ b/file @@ ... format',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['path', 'diff'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'execute_command',
|
|
55
|
+
description: 'Run a shell command with configurable timeout. Captures stdout+stderr. Use for builds, tests, installs, git ops.',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
command: { type: 'string', description: 'Bash command to execute' },
|
|
60
|
+
timeout: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
description: 'Timeout in milliseconds (default 30000)',
|
|
63
|
+
},
|
|
64
|
+
cwd: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Working directory override (relative to project cwd)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['command'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'list_dir',
|
|
74
|
+
description: 'List directory contents. Set recursive=true for full tree view.',
|
|
75
|
+
parameters: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
path: { type: 'string', description: 'Directory path to list' },
|
|
79
|
+
recursive: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: 'List recursively (default false)',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ['path'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'glob_search',
|
|
89
|
+
description: 'Search for files matching a glob pattern. Respects .gitignore. Returns matching paths.',
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
pattern: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Glob pattern e.g. "src/**/*.ts", "**/*.json"',
|
|
96
|
+
},
|
|
97
|
+
cwd: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Base directory for search (default: project cwd)',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['pattern'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'grep_search',
|
|
107
|
+
description: 'Search file contents for a regex or literal pattern. Uses ripgrep when available, falls back to grep. Returns file:line:match results.',
|
|
108
|
+
parameters: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
pattern: { type: 'string', description: 'Search pattern (regex or literal string)' },
|
|
112
|
+
path: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Directory or file to search (default: project cwd)',
|
|
115
|
+
},
|
|
116
|
+
file_pattern: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Glob to restrict file types e.g. "*.ts"',
|
|
119
|
+
},
|
|
120
|
+
case_sensitive: {
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
description: 'Case-sensitive match (default: false)',
|
|
123
|
+
},
|
|
124
|
+
max_results: {
|
|
125
|
+
type: 'number',
|
|
126
|
+
description: 'Maximum lines to return (default: 50)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['pattern'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'move_file',
|
|
134
|
+
description: 'Move or rename a file or directory. Creates destination parent dirs as needed.',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
source: { type: 'string', description: 'Source path' },
|
|
139
|
+
destination: { type: 'string', description: 'Destination path' },
|
|
140
|
+
},
|
|
141
|
+
required: ['source', 'destination'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'delete_file',
|
|
146
|
+
description: 'Delete a file or directory. Requires recursive=true to delete directories.',
|
|
147
|
+
parameters: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
path: { type: 'string', description: 'Path to delete' },
|
|
151
|
+
recursive: {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
description: 'Delete directory and all contents (default: false)',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ['path'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'search_replace',
|
|
161
|
+
description: 'Find an exact string in a file and replace it. More reliable than apply_diff for targeted edits. ' +
|
|
162
|
+
'The search string must match exactly — whitespace and indentation included. ' +
|
|
163
|
+
'Returns an error if the search string is not found.',
|
|
164
|
+
parameters: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
path: { type: 'string', description: 'File to edit' },
|
|
168
|
+
search: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'Exact string to find (must match character-for-character)',
|
|
171
|
+
},
|
|
172
|
+
replace: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'String to substitute in place of the search string',
|
|
175
|
+
},
|
|
176
|
+
replace_all: {
|
|
177
|
+
type: 'boolean',
|
|
178
|
+
description: 'Replace every occurrence, not just the first (default: false)',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['path', 'search', 'replace'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'fetch_url',
|
|
186
|
+
description: 'Fetch the content of a URL. HTML is stripped to readable text. ' +
|
|
187
|
+
'Use to look up documentation, GitHub issues, changelogs, or any web resource.',
|
|
188
|
+
parameters: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
url: { type: 'string', description: 'URL to fetch (http or https)' },
|
|
192
|
+
max_chars: {
|
|
193
|
+
type: 'number',
|
|
194
|
+
description: 'Maximum characters to return (default: 20000)',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ['url'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'remember',
|
|
202
|
+
description: 'Persist a fact or decision to .koneck/MEMORY.md so it is available in future sessions. ' +
|
|
203
|
+
'Use for architecture decisions, recurring commands, project conventions, known bugs, or anything worth retaining across sessions.',
|
|
204
|
+
parameters: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
content: { type: 'string', description: 'The information to remember' },
|
|
208
|
+
category: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: 'Category tag: architecture | commands | conventions | bugs | notes (default: notes)',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
required: ['content'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'web_search',
|
|
218
|
+
description: 'Search the web for current information: docs, package changelogs, error messages, RFCs. ' +
|
|
219
|
+
'Requires BRAVE_API_KEY or SERPER_API_KEY environment variable.',
|
|
220
|
+
parameters: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
query: { type: 'string', description: 'Search query' },
|
|
224
|
+
num_results: {
|
|
225
|
+
type: 'number',
|
|
226
|
+
description: 'Number of results to return (default: 5, max: 10)',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ['query'],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'spawn_agents',
|
|
234
|
+
description: 'Fan out up to 8 parallel sub-agents, each working on an independent task in its own git worktree. ' +
|
|
235
|
+
'Use for large refactors that can be split into independent pieces running concurrently. ' +
|
|
236
|
+
'Each agent result is returned as a summary. Cannot be called from within a sub-agent.',
|
|
237
|
+
parameters: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
tasks: {
|
|
241
|
+
type: 'array',
|
|
242
|
+
items: { type: 'string' },
|
|
243
|
+
description: 'List of task descriptions — one per agent (max 8)',
|
|
244
|
+
},
|
|
245
|
+
use_worktrees: {
|
|
246
|
+
type: 'boolean',
|
|
247
|
+
description: 'Create an isolated git worktree per agent (default: true, requires git repo)',
|
|
248
|
+
},
|
|
249
|
+
max_turns: {
|
|
250
|
+
type: 'number',
|
|
251
|
+
description: 'Max turns per sub-agent (default: inherits parent)',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['tasks'],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
...GIT_TOOL_DEFINITIONS,
|
|
258
|
+
];
|
|
259
|
+
// ─── Tool handlers ────────────────────────────────────────────────────────────
|
|
260
|
+
export async function readFiles(args, cwd) {
|
|
261
|
+
const results = await Promise.allSettled(args.paths.map(async (filePath) => {
|
|
262
|
+
const abs = path.resolve(cwd, filePath);
|
|
263
|
+
const content = await fs.readFile(abs, 'utf-8');
|
|
264
|
+
const numbered = content
|
|
265
|
+
.split('\n')
|
|
266
|
+
.map((line, i) => `${String(i + 1).padStart(5)} │ ${line}`)
|
|
267
|
+
.join('\n');
|
|
268
|
+
return `=== ${filePath} (${content.split('\n').length} lines) ===\n${numbered}`;
|
|
269
|
+
}));
|
|
270
|
+
return results
|
|
271
|
+
.map((r, i) => r.status === 'fulfilled'
|
|
272
|
+
? r.value
|
|
273
|
+
: `=== ${args.paths[i]} ===\nERROR: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`)
|
|
274
|
+
.join('\n\n');
|
|
275
|
+
}
|
|
276
|
+
export async function writeFile(args, cwd, requireApproval) {
|
|
277
|
+
const abs = path.resolve(cwd, args.path);
|
|
278
|
+
const validation = validateCode(args.content, abs);
|
|
279
|
+
if (!validation.valid) {
|
|
280
|
+
return [
|
|
281
|
+
`ERROR: AST validation failed — ${args.path} was NOT written.`,
|
|
282
|
+
...validation.errors.map(e => ` • ${e}`),
|
|
283
|
+
].join('\n');
|
|
284
|
+
}
|
|
285
|
+
if (requireApproval) {
|
|
286
|
+
const approved = await tui.confirmWrite(args.path, args.content);
|
|
287
|
+
if (!approved)
|
|
288
|
+
return `Write to ${args.path} cancelled by user.`;
|
|
289
|
+
}
|
|
290
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
291
|
+
await fs.writeFile(abs, args.content, 'utf-8');
|
|
292
|
+
const lines = args.content.split('\n').length;
|
|
293
|
+
return `Wrote ${lines} lines to ${args.path}`;
|
|
294
|
+
}
|
|
295
|
+
export async function applyDiff(args, cwd, requireApproval) {
|
|
296
|
+
const abs = path.resolve(cwd, args.path);
|
|
297
|
+
let original = '';
|
|
298
|
+
try {
|
|
299
|
+
original = await fs.readFile(abs, 'utf-8');
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// new file — empty original is valid for a diff that adds everything
|
|
303
|
+
}
|
|
304
|
+
const patched = diffLib.applyPatch(original, args.diff);
|
|
305
|
+
if (patched === false) {
|
|
306
|
+
return (`ERROR: Patch failed to apply cleanly to ${args.path}. ` +
|
|
307
|
+
`The file may have changed since the diff was generated. ` +
|
|
308
|
+
`Use read_files to get the current content and generate a fresh diff.`);
|
|
309
|
+
}
|
|
310
|
+
const validation = validateCode(patched, abs);
|
|
311
|
+
if (!validation.valid) {
|
|
312
|
+
return [
|
|
313
|
+
`ERROR: Patched result has syntax errors in ${args.path} — diff NOT applied.`,
|
|
314
|
+
...validation.errors.map(e => ` • ${e}`),
|
|
315
|
+
].join('\n');
|
|
316
|
+
}
|
|
317
|
+
if (requireApproval) {
|
|
318
|
+
tui.renderDiff(original, patched, args.path);
|
|
319
|
+
const approved = await tui.confirmDiff(args.path);
|
|
320
|
+
if (!approved)
|
|
321
|
+
return `Diff to ${args.path} cancelled by user.`;
|
|
322
|
+
}
|
|
323
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
324
|
+
await fs.writeFile(abs, patched, 'utf-8');
|
|
325
|
+
const changes = diffLib.diffLines(original, patched);
|
|
326
|
+
const added = changes.filter(c => c.added).reduce((n, c) => n + (c.count ?? 0), 0);
|
|
327
|
+
const removed = changes.filter(c => c.removed).reduce((n, c) => n + (c.count ?? 0), 0);
|
|
328
|
+
return `Applied diff to ${args.path}: +${added} -${removed} lines`;
|
|
329
|
+
}
|
|
330
|
+
export async function executeCommand(args, cwd, requireApproval) {
|
|
331
|
+
if (requireApproval) {
|
|
332
|
+
const approved = await tui.confirmCommand(args.command);
|
|
333
|
+
if (!approved)
|
|
334
|
+
return `Command cancelled by user: ${args.command}`;
|
|
335
|
+
}
|
|
336
|
+
const execCwd = args.cwd ? path.resolve(cwd, args.cwd) : cwd;
|
|
337
|
+
try {
|
|
338
|
+
const result = await execa('bash', ['-c', args.command], {
|
|
339
|
+
cwd: execCwd,
|
|
340
|
+
timeout: args.timeout ?? 30_000,
|
|
341
|
+
all: true,
|
|
342
|
+
reject: false,
|
|
343
|
+
});
|
|
344
|
+
const output = (result.all ?? '').trim();
|
|
345
|
+
const code = result.exitCode;
|
|
346
|
+
if (code !== 0) {
|
|
347
|
+
return `Exit code ${code}:\n${output || '(no output)'}`;
|
|
348
|
+
}
|
|
349
|
+
return output || '(command succeeded with no output)';
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
if (err instanceof Error && err.message.includes('timed out')) {
|
|
353
|
+
return `ERROR: Command timed out after ${args.timeout ?? 30_000}ms`;
|
|
354
|
+
}
|
|
355
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
export async function listDir(args, cwd) {
|
|
359
|
+
const abs = path.resolve(cwd, args.path);
|
|
360
|
+
try {
|
|
361
|
+
if (args.recursive) {
|
|
362
|
+
const entries = await globby('**', {
|
|
363
|
+
cwd: abs,
|
|
364
|
+
dot: true,
|
|
365
|
+
gitignore: true,
|
|
366
|
+
onlyFiles: false,
|
|
367
|
+
markDirectories: true,
|
|
368
|
+
});
|
|
369
|
+
return entries.length > 0
|
|
370
|
+
? entries.join('\n')
|
|
371
|
+
: '(empty directory)';
|
|
372
|
+
}
|
|
373
|
+
const entries = await fs.readdir(abs, { withFileTypes: true });
|
|
374
|
+
if (entries.length === 0)
|
|
375
|
+
return '(empty directory)';
|
|
376
|
+
const lines = await Promise.all(entries.map(async (e) => {
|
|
377
|
+
const type = e.isDirectory() ? 'd' : e.isSymbolicLink() ? 'l' : '-';
|
|
378
|
+
let size = '';
|
|
379
|
+
if (e.isFile()) {
|
|
380
|
+
try {
|
|
381
|
+
const stat = await fs.stat(path.join(abs, e.name));
|
|
382
|
+
size = ` (${formatBytes(stat.size)})`;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
/* best-effort */
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return `${type} ${e.name}${e.isDirectory() ? '/' : ''}${size}`;
|
|
389
|
+
}));
|
|
390
|
+
return lines.join('\n');
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export async function globSearch(args, cwd) {
|
|
397
|
+
const base = args.cwd ? path.resolve(cwd, args.cwd) : cwd;
|
|
398
|
+
try {
|
|
399
|
+
const files = await globby(args.pattern, {
|
|
400
|
+
cwd: base,
|
|
401
|
+
dot: true,
|
|
402
|
+
gitignore: true,
|
|
403
|
+
});
|
|
404
|
+
if (files.length === 0) {
|
|
405
|
+
return `No files match: ${args.pattern}`;
|
|
406
|
+
}
|
|
407
|
+
return `${files.length} match(es):\n${files.join('\n')}`;
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function formatBytes(bytes) {
|
|
414
|
+
if (bytes < 1024)
|
|
415
|
+
return `${bytes}B`;
|
|
416
|
+
if (bytes < 1024 * 1024)
|
|
417
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
418
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
419
|
+
}
|
|
420
|
+
function isENOENT(err) {
|
|
421
|
+
return (err instanceof Error &&
|
|
422
|
+
'code' in err &&
|
|
423
|
+
err.code === 'ENOENT');
|
|
424
|
+
}
|
|
425
|
+
export async function grepSearch(args, cwd) {
|
|
426
|
+
const searchPath = args.path ? path.resolve(cwd, args.path) : cwd;
|
|
427
|
+
const maxResults = args.max_results ?? 50;
|
|
428
|
+
// Try ripgrep first — much faster on large repos
|
|
429
|
+
try {
|
|
430
|
+
const rgArgs = ['--line-number', '--no-heading', '--color=never'];
|
|
431
|
+
if (!args.case_sensitive)
|
|
432
|
+
rgArgs.push('--ignore-case');
|
|
433
|
+
if (args.file_pattern)
|
|
434
|
+
rgArgs.push('--glob', args.file_pattern);
|
|
435
|
+
rgArgs.push(args.pattern, searchPath);
|
|
436
|
+
const result = await execa('rg', rgArgs, { reject: false, all: true });
|
|
437
|
+
const lines = (result.all ?? '').split('\n').filter(Boolean);
|
|
438
|
+
if (result.exitCode === 1 && lines.length === 0)
|
|
439
|
+
return 'No matches found.';
|
|
440
|
+
if (result.exitCode === 0 || lines.length > 0) {
|
|
441
|
+
const truncated = lines.slice(0, maxResults);
|
|
442
|
+
const suffix = lines.length > maxResults ? `\n(showing ${maxResults} of ${lines.length} matches)` : '';
|
|
443
|
+
return truncated.join('\n') + suffix;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
if (!isENOENT(err)) {
|
|
448
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
449
|
+
}
|
|
450
|
+
// ripgrep not installed — fall through to grep
|
|
451
|
+
}
|
|
452
|
+
// Fallback: POSIX grep
|
|
453
|
+
try {
|
|
454
|
+
const grepArgs = ['-rn', '--color=never'];
|
|
455
|
+
if (!args.case_sensitive)
|
|
456
|
+
grepArgs.push('-i');
|
|
457
|
+
if (args.file_pattern)
|
|
458
|
+
grepArgs.push(`--include=${args.file_pattern}`);
|
|
459
|
+
grepArgs.push(args.pattern, searchPath);
|
|
460
|
+
const result = await execa('grep', grepArgs, { reject: false, all: true });
|
|
461
|
+
const lines = (result.all ?? '').split('\n').filter(Boolean);
|
|
462
|
+
if (result.exitCode === 1 && lines.length === 0)
|
|
463
|
+
return 'No matches found.';
|
|
464
|
+
const truncated = lines.slice(0, maxResults);
|
|
465
|
+
const suffix = lines.length > maxResults ? `\n(showing ${maxResults} of ${lines.length} matches)` : '';
|
|
466
|
+
return truncated.join('\n') + suffix;
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
export async function moveFile(args, cwd) {
|
|
473
|
+
const src = path.resolve(cwd, args.source);
|
|
474
|
+
const dst = path.resolve(cwd, args.destination);
|
|
475
|
+
try {
|
|
476
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
477
|
+
await fs.rename(src, dst);
|
|
478
|
+
return `Moved ${args.source} → ${args.destination}`;
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
if (err instanceof Error &&
|
|
482
|
+
'code' in err &&
|
|
483
|
+
err.code === 'EXDEV') {
|
|
484
|
+
// Cross-device: copy then delete
|
|
485
|
+
await fs.copyFile(src, dst);
|
|
486
|
+
await fs.unlink(src);
|
|
487
|
+
return `Moved ${args.source} → ${args.destination}`;
|
|
488
|
+
}
|
|
489
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
export async function deleteFile(args, cwd, requireApproval) {
|
|
493
|
+
const abs = path.resolve(cwd, args.path);
|
|
494
|
+
try {
|
|
495
|
+
const stat = await fs.stat(abs);
|
|
496
|
+
if (stat.isDirectory() && !args.recursive) {
|
|
497
|
+
return `ERROR: ${args.path} is a directory. Pass recursive=true to delete it.`;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return `ERROR: ${args.path} does not exist.`;
|
|
502
|
+
}
|
|
503
|
+
if (requireApproval) {
|
|
504
|
+
const approved = await tui.confirmDelete(args.path, args.recursive ?? false);
|
|
505
|
+
if (!approved)
|
|
506
|
+
return `Delete of ${args.path} cancelled by user.`;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const stat = await fs.stat(abs);
|
|
510
|
+
if (stat.isDirectory()) {
|
|
511
|
+
await fs.rm(abs, { recursive: true, force: true });
|
|
512
|
+
return `Deleted directory ${args.path}`;
|
|
513
|
+
}
|
|
514
|
+
await fs.unlink(abs);
|
|
515
|
+
return `Deleted ${args.path}`;
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function countOccurrences(text, search) {
|
|
522
|
+
let count = 0;
|
|
523
|
+
let pos = 0;
|
|
524
|
+
while (true) {
|
|
525
|
+
pos = text.indexOf(search, pos);
|
|
526
|
+
if (pos === -1)
|
|
527
|
+
break;
|
|
528
|
+
count++;
|
|
529
|
+
pos += search.length;
|
|
530
|
+
}
|
|
531
|
+
return count;
|
|
532
|
+
}
|
|
533
|
+
export async function searchReplace(args, cwd, requireApproval) {
|
|
534
|
+
const abs = path.resolve(cwd, args.path);
|
|
535
|
+
let original;
|
|
536
|
+
try {
|
|
537
|
+
original = await fs.readFile(abs, 'utf-8');
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return `ERROR: Cannot read ${args.path} — file does not exist.`;
|
|
541
|
+
}
|
|
542
|
+
const occurrences = countOccurrences(original, args.search);
|
|
543
|
+
if (occurrences === 0) {
|
|
544
|
+
return (`ERROR: Search string not found in ${args.path}.\n` +
|
|
545
|
+
`Tip: use read_files to get the exact current content, then retry with the precise text.`);
|
|
546
|
+
}
|
|
547
|
+
const patched = args.replace_all
|
|
548
|
+
? original.split(args.search).join(args.replace)
|
|
549
|
+
: original.replace(args.search, args.replace);
|
|
550
|
+
const validation = validateCode(patched, abs);
|
|
551
|
+
if (!validation.valid) {
|
|
552
|
+
return [
|
|
553
|
+
`ERROR: Result has syntax errors in ${args.path} — not written.`,
|
|
554
|
+
...validation.errors.map(e => ` • ${e}`),
|
|
555
|
+
].join('\n');
|
|
556
|
+
}
|
|
557
|
+
if (requireApproval) {
|
|
558
|
+
tui.renderDiff(original, patched, args.path);
|
|
559
|
+
const approved = await tui.confirmDiff(args.path);
|
|
560
|
+
if (!approved)
|
|
561
|
+
return `Search-replace in ${args.path} cancelled by user.`;
|
|
562
|
+
}
|
|
563
|
+
await fs.writeFile(abs, patched, 'utf-8');
|
|
564
|
+
const replaced = args.replace_all ? occurrences : 1;
|
|
565
|
+
const skipped = args.replace_all ? 0 : occurrences - 1;
|
|
566
|
+
const note = skipped > 0 ? ` (${skipped} additional occurrence(s) left unchanged — pass replace_all:true to replace all)` : '';
|
|
567
|
+
return `Replaced ${replaced} occurrence(s) in ${args.path}${note}`;
|
|
568
|
+
}
|
|
569
|
+
// ─── fetch_url ────────────────────────────────────────────────────────────────
|
|
570
|
+
function stripHtml(html) {
|
|
571
|
+
return html
|
|
572
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
573
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
574
|
+
.replace(/<[^>]+>/g, ' ')
|
|
575
|
+
.replace(/ /g, ' ')
|
|
576
|
+
.replace(/&/g, '&')
|
|
577
|
+
.replace(/</g, '<')
|
|
578
|
+
.replace(/>/g, '>')
|
|
579
|
+
.replace(/"/g, '"')
|
|
580
|
+
.replace(/'/g, "'")
|
|
581
|
+
.replace(/\s+/g, ' ')
|
|
582
|
+
.trim();
|
|
583
|
+
}
|
|
584
|
+
export async function fetchUrl(args) {
|
|
585
|
+
const maxChars = args.max_chars ?? 20_000;
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetch(args.url, {
|
|
588
|
+
headers: { 'User-Agent': 'KONECK/1.0 (AI coding agent)' },
|
|
589
|
+
signal: AbortSignal.timeout(15_000),
|
|
590
|
+
});
|
|
591
|
+
if (!response.ok) {
|
|
592
|
+
return `ERROR: HTTP ${response.status} ${response.statusText} fetching ${args.url}`;
|
|
593
|
+
}
|
|
594
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
595
|
+
const raw = await response.text();
|
|
596
|
+
let text = contentType.includes('text/html') ? stripHtml(raw) : raw;
|
|
597
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
598
|
+
if (text.length > maxChars) {
|
|
599
|
+
return text.slice(0, maxChars) + `\n\n[truncated — ${text.length} chars total, showing first ${maxChars}]`;
|
|
600
|
+
}
|
|
601
|
+
return text || '(empty response)';
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// ─── web_search ───────────────────────────────────────────────────────────────
|
|
608
|
+
async function braveSearch(query, n, key) {
|
|
609
|
+
const url = 'https://api.search.brave.com/res/v1/web/search?q=' +
|
|
610
|
+
encodeURIComponent(query) + '&count=' + String(n);
|
|
611
|
+
const res = await fetch(url, {
|
|
612
|
+
headers: { Accept: 'application/json', 'X-Subscription-Token': key },
|
|
613
|
+
signal: AbortSignal.timeout(10_000),
|
|
614
|
+
});
|
|
615
|
+
if (!res.ok)
|
|
616
|
+
return `ERROR: Brave Search returned HTTP ${res.status}`;
|
|
617
|
+
const data = await res.json();
|
|
618
|
+
const results = data.web?.results ?? [];
|
|
619
|
+
if (results.length === 0)
|
|
620
|
+
return 'No results found.';
|
|
621
|
+
return results
|
|
622
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.description ?? ''}`)
|
|
623
|
+
.join('\n\n');
|
|
624
|
+
}
|
|
625
|
+
async function serperSearch(query, n, key) {
|
|
626
|
+
const res = await fetch('https://google.serper.dev/search', {
|
|
627
|
+
method: 'POST',
|
|
628
|
+
headers: { 'Content-Type': 'application/json', 'X-API-KEY': key },
|
|
629
|
+
body: JSON.stringify({ q: query, num: n }),
|
|
630
|
+
signal: AbortSignal.timeout(10_000),
|
|
631
|
+
});
|
|
632
|
+
if (!res.ok)
|
|
633
|
+
return `ERROR: Serper returned HTTP ${res.status}`;
|
|
634
|
+
const data = await res.json();
|
|
635
|
+
const results = data.organic ?? [];
|
|
636
|
+
if (results.length === 0)
|
|
637
|
+
return 'No results found.';
|
|
638
|
+
return results
|
|
639
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.link}\n ${r.snippet ?? ''}`)
|
|
640
|
+
.join('\n\n');
|
|
641
|
+
}
|
|
642
|
+
export async function webSearch(args) {
|
|
643
|
+
const n = Math.min(args.num_results ?? 5, 10);
|
|
644
|
+
const braveKey = process.env['BRAVE_API_KEY'];
|
|
645
|
+
if (braveKey) {
|
|
646
|
+
try {
|
|
647
|
+
return await braveSearch(args.query, n, braveKey);
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const serperKey = process.env['SERPER_API_KEY'];
|
|
654
|
+
if (serperKey) {
|
|
655
|
+
try {
|
|
656
|
+
return await serperSearch(args.query, n, serperKey);
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
return `ERROR: ${err instanceof Error ? err.message : String(err)}`;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return ('ERROR: No search API configured. ' +
|
|
663
|
+
'Set BRAVE_API_KEY (https://api.search.brave.com) or ' +
|
|
664
|
+
'SERPER_API_KEY (https://serper.dev) to enable web_search.');
|
|
665
|
+
}
|
|
666
|
+
// ─── remember ─────────────────────────────────────────────────────────────────
|
|
667
|
+
export async function remember(args, cwd) {
|
|
668
|
+
await appendMemory(cwd, args.content, args.category ?? 'notes');
|
|
669
|
+
return `Remembered (${args.category ?? 'notes'}): ${args.content.slice(0, 100)}`;
|
|
670
|
+
}
|
|
671
|
+
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
672
|
+
export async function dispatchTool(name, argsJson, cwd, requireApproval) {
|
|
673
|
+
let parsed;
|
|
674
|
+
try {
|
|
675
|
+
parsed = JSON.parse(argsJson);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return `ERROR: Malformed JSON arguments for tool "${name}": ${argsJson.slice(0, 200)}`;
|
|
679
|
+
}
|
|
680
|
+
switch (name) {
|
|
681
|
+
case 'read_files':
|
|
682
|
+
return readFiles(parsed, cwd);
|
|
683
|
+
case 'write_file':
|
|
684
|
+
return writeFile(parsed, cwd, requireApproval);
|
|
685
|
+
case 'apply_diff':
|
|
686
|
+
return applyDiff(parsed, cwd, requireApproval);
|
|
687
|
+
case 'execute_command':
|
|
688
|
+
return executeCommand(parsed, cwd, requireApproval);
|
|
689
|
+
case 'list_dir':
|
|
690
|
+
return listDir(parsed, cwd);
|
|
691
|
+
case 'glob_search':
|
|
692
|
+
return globSearch(parsed, cwd);
|
|
693
|
+
case 'grep_search':
|
|
694
|
+
return grepSearch(parsed, cwd);
|
|
695
|
+
case 'move_file':
|
|
696
|
+
return moveFile(parsed, cwd);
|
|
697
|
+
case 'delete_file':
|
|
698
|
+
return deleteFile(parsed, cwd, requireApproval);
|
|
699
|
+
case 'search_replace':
|
|
700
|
+
return searchReplace(parsed, cwd, requireApproval);
|
|
701
|
+
case 'fetch_url':
|
|
702
|
+
return fetchUrl(parsed);
|
|
703
|
+
case 'remember':
|
|
704
|
+
return remember(parsed, cwd);
|
|
705
|
+
case 'web_search':
|
|
706
|
+
return webSearch(parsed);
|
|
707
|
+
default:
|
|
708
|
+
if (name.startsWith('git_'))
|
|
709
|
+
return dispatchGitTool(name, argsJson, cwd);
|
|
710
|
+
return `ERROR: Unknown tool "${name}"`;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
//# sourceMappingURL=tools.js.map
|