@link-assistant/agent 0.0.9 → 0.0.12
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/EXAMPLES.md +36 -0
- package/MODELS.md +72 -24
- package/README.md +59 -2
- package/TOOLS.md +20 -0
- package/package.json +35 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +469 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +144 -119
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +39 -24
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +346 -199
- package/src/json-standard/index.ts +67 -51
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/tool/glob.ts
CHANGED
|
@@ -1,55 +1,59 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import path from
|
|
3
|
-
import { Tool } from
|
|
4
|
-
import DESCRIPTION from
|
|
5
|
-
import { Ripgrep } from
|
|
6
|
-
import { Instance } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Tool } from './tool';
|
|
4
|
+
import DESCRIPTION from './glob.txt';
|
|
5
|
+
import { Ripgrep } from '../file/ripgrep';
|
|
6
|
+
import { Instance } from '../project/instance';
|
|
7
7
|
|
|
8
|
-
export const GlobTool = Tool.define(
|
|
8
|
+
export const GlobTool = Tool.define('glob', {
|
|
9
9
|
description: DESCRIPTION,
|
|
10
10
|
parameters: z.object({
|
|
11
|
-
pattern: z.string().describe(
|
|
11
|
+
pattern: z.string().describe('The glob pattern to match files against'),
|
|
12
12
|
path: z
|
|
13
13
|
.string()
|
|
14
14
|
.optional()
|
|
15
15
|
.describe(
|
|
16
|
-
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided
|
|
16
|
+
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`
|
|
17
17
|
),
|
|
18
18
|
}),
|
|
19
19
|
async execute(params) {
|
|
20
|
-
let search = params.path ?? Instance.directory
|
|
21
|
-
search = path.isAbsolute(search)
|
|
20
|
+
let search = params.path ?? Instance.directory;
|
|
21
|
+
search = path.isAbsolute(search)
|
|
22
|
+
? search
|
|
23
|
+
: path.resolve(Instance.directory, search);
|
|
22
24
|
|
|
23
|
-
const limit = 100
|
|
24
|
-
const files = []
|
|
25
|
-
let truncated = false
|
|
25
|
+
const limit = 100;
|
|
26
|
+
const files = [];
|
|
27
|
+
let truncated = false;
|
|
26
28
|
for await (const file of Ripgrep.files({
|
|
27
29
|
cwd: search,
|
|
28
30
|
glob: [params.pattern],
|
|
29
31
|
})) {
|
|
30
32
|
if (files.length >= limit) {
|
|
31
|
-
truncated = true
|
|
32
|
-
break
|
|
33
|
+
truncated = true;
|
|
34
|
+
break;
|
|
33
35
|
}
|
|
34
|
-
const full = path.resolve(search, file)
|
|
36
|
+
const full = path.resolve(search, file);
|
|
35
37
|
const stats = await Bun.file(full)
|
|
36
38
|
.stat()
|
|
37
39
|
.then((x) => x.mtime.getTime())
|
|
38
|
-
.catch(() => 0)
|
|
40
|
+
.catch(() => 0);
|
|
39
41
|
files.push({
|
|
40
42
|
path: full,
|
|
41
43
|
mtime: stats,
|
|
42
|
-
})
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
|
-
files.sort((a, b) => b.mtime - a.mtime)
|
|
46
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
45
47
|
|
|
46
|
-
const output = []
|
|
47
|
-
if (files.length === 0) output.push(
|
|
48
|
+
const output = [];
|
|
49
|
+
if (files.length === 0) output.push('No files found');
|
|
48
50
|
if (files.length > 0) {
|
|
49
|
-
output.push(...files.map((f) => f.path))
|
|
51
|
+
output.push(...files.map((f) => f.path));
|
|
50
52
|
if (truncated) {
|
|
51
|
-
output.push(
|
|
52
|
-
output.push(
|
|
53
|
+
output.push('');
|
|
54
|
+
output.push(
|
|
55
|
+
'(Results are truncated. Consider using a more specific path or pattern.)'
|
|
56
|
+
);
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -59,7 +63,7 @@ export const GlobTool = Tool.define("glob", {
|
|
|
59
63
|
count: files.length,
|
|
60
64
|
truncated,
|
|
61
65
|
},
|
|
62
|
-
output: output.join(
|
|
63
|
-
}
|
|
66
|
+
output: output.join('\n'),
|
|
67
|
+
};
|
|
64
68
|
},
|
|
65
|
-
})
|
|
69
|
+
});
|
package/src/tool/grep.ts
CHANGED
|
@@ -1,107 +1,126 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Tool } from
|
|
3
|
-
import { Ripgrep } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Tool } from './tool';
|
|
3
|
+
import { Ripgrep } from '../file/ripgrep';
|
|
4
4
|
|
|
5
|
-
import DESCRIPTION from
|
|
6
|
-
import { Instance } from
|
|
5
|
+
import DESCRIPTION from './grep.txt';
|
|
6
|
+
import { Instance } from '../project/instance';
|
|
7
7
|
|
|
8
|
-
export const GrepTool = Tool.define(
|
|
8
|
+
export const GrepTool = Tool.define('grep', {
|
|
9
9
|
description: DESCRIPTION,
|
|
10
10
|
parameters: z.object({
|
|
11
|
-
pattern: z
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
pattern: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('The regex pattern to search for in file contents'),
|
|
14
|
+
path: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
'The directory to search in. Defaults to the current working directory.'
|
|
19
|
+
),
|
|
20
|
+
include: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(
|
|
24
|
+
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'
|
|
25
|
+
),
|
|
14
26
|
}),
|
|
15
27
|
async execute(params) {
|
|
16
28
|
if (!params.pattern) {
|
|
17
|
-
throw new Error(
|
|
29
|
+
throw new Error('pattern is required');
|
|
18
30
|
}
|
|
19
31
|
|
|
20
|
-
const searchPath = params.path || Instance.directory
|
|
32
|
+
const searchPath = params.path || Instance.directory;
|
|
21
33
|
|
|
22
|
-
const rgPath = await Ripgrep.filepath()
|
|
23
|
-
const args = [
|
|
34
|
+
const rgPath = await Ripgrep.filepath();
|
|
35
|
+
const args = [
|
|
36
|
+
'-nH',
|
|
37
|
+
'--field-match-separator=|',
|
|
38
|
+
'--regexp',
|
|
39
|
+
params.pattern,
|
|
40
|
+
];
|
|
24
41
|
if (params.include) {
|
|
25
|
-
args.push(
|
|
42
|
+
args.push('--glob', params.include);
|
|
26
43
|
}
|
|
27
|
-
args.push(searchPath)
|
|
44
|
+
args.push(searchPath);
|
|
28
45
|
|
|
29
46
|
const proc = Bun.spawn([rgPath, ...args], {
|
|
30
|
-
stdout:
|
|
31
|
-
stderr:
|
|
32
|
-
})
|
|
47
|
+
stdout: 'pipe',
|
|
48
|
+
stderr: 'pipe',
|
|
49
|
+
});
|
|
33
50
|
|
|
34
|
-
const output = await new Response(proc.stdout).text()
|
|
35
|
-
const errorOutput = await new Response(proc.stderr).text()
|
|
36
|
-
const exitCode = await proc.exited
|
|
51
|
+
const output = await new Response(proc.stdout).text();
|
|
52
|
+
const errorOutput = await new Response(proc.stderr).text();
|
|
53
|
+
const exitCode = await proc.exited;
|
|
37
54
|
|
|
38
55
|
if (exitCode === 1) {
|
|
39
56
|
return {
|
|
40
57
|
title: params.pattern,
|
|
41
58
|
metadata: { matches: 0, truncated: false },
|
|
42
|
-
output:
|
|
43
|
-
}
|
|
59
|
+
output: 'No files found',
|
|
60
|
+
};
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
if (exitCode !== 0) {
|
|
47
|
-
throw new Error(`ripgrep failed: ${errorOutput}`)
|
|
64
|
+
throw new Error(`ripgrep failed: ${errorOutput}`);
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
const lines = output.trim().split(
|
|
51
|
-
const matches = []
|
|
67
|
+
const lines = output.trim().split('\n');
|
|
68
|
+
const matches = [];
|
|
52
69
|
|
|
53
70
|
for (const line of lines) {
|
|
54
|
-
if (!line) continue
|
|
71
|
+
if (!line) continue;
|
|
55
72
|
|
|
56
|
-
const [filePath, lineNumStr, ...lineTextParts] = line.split(
|
|
57
|
-
if (!filePath || !lineNumStr || lineTextParts.length === 0) continue
|
|
73
|
+
const [filePath, lineNumStr, ...lineTextParts] = line.split('|');
|
|
74
|
+
if (!filePath || !lineNumStr || lineTextParts.length === 0) continue;
|
|
58
75
|
|
|
59
|
-
const lineNum = parseInt(lineNumStr, 10)
|
|
60
|
-
const lineText = lineTextParts.join(
|
|
76
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
77
|
+
const lineText = lineTextParts.join('|');
|
|
61
78
|
|
|
62
|
-
const file = Bun.file(filePath)
|
|
63
|
-
const stats = await file.stat().catch(() => null)
|
|
64
|
-
if (!stats) continue
|
|
79
|
+
const file = Bun.file(filePath);
|
|
80
|
+
const stats = await file.stat().catch(() => null);
|
|
81
|
+
if (!stats) continue;
|
|
65
82
|
|
|
66
83
|
matches.push({
|
|
67
84
|
path: filePath,
|
|
68
85
|
modTime: stats.mtime.getTime(),
|
|
69
86
|
lineNum,
|
|
70
87
|
lineText,
|
|
71
|
-
})
|
|
88
|
+
});
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
matches.sort((a, b) => b.modTime - a.modTime)
|
|
91
|
+
matches.sort((a, b) => b.modTime - a.modTime);
|
|
75
92
|
|
|
76
|
-
const limit = 100
|
|
77
|
-
const truncated = matches.length > limit
|
|
78
|
-
const finalMatches = truncated ? matches.slice(0, limit) : matches
|
|
93
|
+
const limit = 100;
|
|
94
|
+
const truncated = matches.length > limit;
|
|
95
|
+
const finalMatches = truncated ? matches.slice(0, limit) : matches;
|
|
79
96
|
|
|
80
97
|
if (finalMatches.length === 0) {
|
|
81
98
|
return {
|
|
82
99
|
title: params.pattern,
|
|
83
100
|
metadata: { matches: 0, truncated: false },
|
|
84
|
-
output:
|
|
85
|
-
}
|
|
101
|
+
output: 'No files found',
|
|
102
|
+
};
|
|
86
103
|
}
|
|
87
104
|
|
|
88
|
-
const outputLines = [`Found ${finalMatches.length} matches`]
|
|
105
|
+
const outputLines = [`Found ${finalMatches.length} matches`];
|
|
89
106
|
|
|
90
|
-
let currentFile =
|
|
107
|
+
let currentFile = '';
|
|
91
108
|
for (const match of finalMatches) {
|
|
92
109
|
if (currentFile !== match.path) {
|
|
93
|
-
if (currentFile !==
|
|
94
|
-
outputLines.push(
|
|
110
|
+
if (currentFile !== '') {
|
|
111
|
+
outputLines.push('');
|
|
95
112
|
}
|
|
96
|
-
currentFile = match.path
|
|
97
|
-
outputLines.push(`${match.path}:`)
|
|
113
|
+
currentFile = match.path;
|
|
114
|
+
outputLines.push(`${match.path}:`);
|
|
98
115
|
}
|
|
99
|
-
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`)
|
|
116
|
+
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`);
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
if (truncated) {
|
|
103
|
-
outputLines.push(
|
|
104
|
-
outputLines.push(
|
|
120
|
+
outputLines.push('');
|
|
121
|
+
outputLines.push(
|
|
122
|
+
'(Results are truncated. Consider using a more specific path or pattern.)'
|
|
123
|
+
);
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
return {
|
|
@@ -110,7 +129,7 @@ export const GrepTool = Tool.define("grep", {
|
|
|
110
129
|
matches: finalMatches.length,
|
|
111
130
|
truncated,
|
|
112
131
|
},
|
|
113
|
-
output: outputLines.join(
|
|
114
|
-
}
|
|
132
|
+
output: outputLines.join('\n'),
|
|
133
|
+
};
|
|
115
134
|
},
|
|
116
|
-
})
|
|
135
|
+
});
|
package/src/tool/invalid.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Tool } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Tool } from './tool';
|
|
3
3
|
|
|
4
|
-
export const InvalidTool = Tool.define(
|
|
5
|
-
description:
|
|
4
|
+
export const InvalidTool = Tool.define('invalid', {
|
|
5
|
+
description: 'Do not use',
|
|
6
6
|
parameters: z.object({
|
|
7
7
|
tool: z.string(),
|
|
8
8
|
error: z.string(),
|
|
9
9
|
}),
|
|
10
10
|
async execute(params) {
|
|
11
11
|
return {
|
|
12
|
-
title:
|
|
12
|
+
title: 'Invalid Tool',
|
|
13
13
|
output: `The arguments provided to the tool are invalid: ${params.error}`,
|
|
14
14
|
metadata: {},
|
|
15
|
-
}
|
|
15
|
+
};
|
|
16
16
|
},
|
|
17
|
-
})
|
|
17
|
+
});
|
package/src/tool/ls.ts
CHANGED
|
@@ -1,102 +1,115 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Tool } from
|
|
3
|
-
import * as path from
|
|
4
|
-
import DESCRIPTION from
|
|
5
|
-
import { Instance } from
|
|
6
|
-
import { Ripgrep } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Tool } from './tool';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import DESCRIPTION from './ls.txt';
|
|
5
|
+
import { Instance } from '../project/instance';
|
|
6
|
+
import { Ripgrep } from '../file/ripgrep';
|
|
7
7
|
|
|
8
8
|
export const IGNORE_PATTERNS = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
const LIMIT = 100
|
|
36
|
-
|
|
37
|
-
export const ListTool = Tool.define(
|
|
9
|
+
'node_modules/',
|
|
10
|
+
'__pycache__/',
|
|
11
|
+
'.git/',
|
|
12
|
+
'dist/',
|
|
13
|
+
'build/',
|
|
14
|
+
'target/',
|
|
15
|
+
'vendor/',
|
|
16
|
+
'bin/',
|
|
17
|
+
'obj/',
|
|
18
|
+
'.idea/',
|
|
19
|
+
'.vscode/',
|
|
20
|
+
'.zig-cache/',
|
|
21
|
+
'zig-out',
|
|
22
|
+
'.coverage',
|
|
23
|
+
'coverage/',
|
|
24
|
+
'vendor/',
|
|
25
|
+
'tmp/',
|
|
26
|
+
'temp/',
|
|
27
|
+
'.cache/',
|
|
28
|
+
'cache/',
|
|
29
|
+
'logs/',
|
|
30
|
+
'.venv/',
|
|
31
|
+
'venv/',
|
|
32
|
+
'env/',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const LIMIT = 100;
|
|
36
|
+
|
|
37
|
+
export const ListTool = Tool.define('list', {
|
|
38
38
|
description: DESCRIPTION,
|
|
39
39
|
parameters: z.object({
|
|
40
|
-
path: z
|
|
41
|
-
|
|
40
|
+
path: z
|
|
41
|
+
.string()
|
|
42
|
+
.describe(
|
|
43
|
+
'The absolute path to the directory to list (must be absolute, not relative)'
|
|
44
|
+
)
|
|
45
|
+
.optional(),
|
|
46
|
+
ignore: z
|
|
47
|
+
.array(z.string())
|
|
48
|
+
.describe('List of glob patterns to ignore')
|
|
49
|
+
.optional(),
|
|
42
50
|
}),
|
|
43
51
|
async execute(params) {
|
|
44
|
-
const searchPath = path.resolve(Instance.directory, params.path ||
|
|
45
|
-
|
|
46
|
-
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const searchPath = path.resolve(Instance.directory, params.path || '.');
|
|
53
|
+
|
|
54
|
+
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(
|
|
55
|
+
params.ignore?.map((p) => `!${p}`) || []
|
|
56
|
+
);
|
|
57
|
+
const files = [];
|
|
58
|
+
for await (const file of Ripgrep.files({
|
|
59
|
+
cwd: searchPath,
|
|
60
|
+
glob: ignoreGlobs,
|
|
61
|
+
})) {
|
|
62
|
+
files.push(file);
|
|
63
|
+
if (files.length >= LIMIT) break;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
// Build directory structure
|
|
54
|
-
const dirs = new Set<string>()
|
|
55
|
-
const filesByDir = new Map<string, string[]>()
|
|
67
|
+
const dirs = new Set<string>();
|
|
68
|
+
const filesByDir = new Map<string, string[]>();
|
|
56
69
|
|
|
57
70
|
for (const file of files) {
|
|
58
|
-
const dir = path.dirname(file)
|
|
59
|
-
const parts = dir ===
|
|
71
|
+
const dir = path.dirname(file);
|
|
72
|
+
const parts = dir === '.' ? [] : dir.split('/');
|
|
60
73
|
|
|
61
74
|
// Add all parent directories
|
|
62
75
|
for (let i = 0; i <= parts.length; i++) {
|
|
63
|
-
const dirPath = i === 0 ?
|
|
64
|
-
dirs.add(dirPath)
|
|
76
|
+
const dirPath = i === 0 ? '.' : parts.slice(0, i).join('/');
|
|
77
|
+
dirs.add(dirPath);
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
// Add file to its directory
|
|
68
|
-
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
|
|
69
|
-
filesByDir.get(dir)!.push(path.basename(file))
|
|
81
|
+
if (!filesByDir.has(dir)) filesByDir.set(dir, []);
|
|
82
|
+
filesByDir.get(dir)!.push(path.basename(file));
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
function renderDir(dirPath: string, depth: number): string {
|
|
73
|
-
const indent =
|
|
74
|
-
let output =
|
|
86
|
+
const indent = ' '.repeat(depth);
|
|
87
|
+
let output = '';
|
|
75
88
|
|
|
76
89
|
if (depth > 0) {
|
|
77
|
-
output += `${indent}${path.basename(dirPath)}/\n
|
|
90
|
+
output += `${indent}${path.basename(dirPath)}/\n`;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
const childIndent =
|
|
93
|
+
const childIndent = ' '.repeat(depth + 1);
|
|
81
94
|
const children = Array.from(dirs)
|
|
82
95
|
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
|
83
|
-
.sort()
|
|
96
|
+
.sort();
|
|
84
97
|
|
|
85
98
|
// Render subdirectories first
|
|
86
99
|
for (const child of children) {
|
|
87
|
-
output += renderDir(child, depth + 1)
|
|
100
|
+
output += renderDir(child, depth + 1);
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
// Render files
|
|
91
|
-
const files = filesByDir.get(dirPath) || []
|
|
104
|
+
const files = filesByDir.get(dirPath) || [];
|
|
92
105
|
for (const file of files.sort()) {
|
|
93
|
-
output += `${childIndent}${file}\n
|
|
106
|
+
output += `${childIndent}${file}\n`;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
return output
|
|
109
|
+
return output;
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
const output = `${searchPath}/\n` + renderDir(
|
|
112
|
+
const output = `${searchPath}/\n` + renderDir('.', 0);
|
|
100
113
|
|
|
101
114
|
return {
|
|
102
115
|
title: path.relative(Instance.worktree, searchPath),
|
|
@@ -105,6 +118,6 @@ export const ListTool = Tool.define("list", {
|
|
|
105
118
|
truncated: files.length >= LIMIT,
|
|
106
119
|
},
|
|
107
120
|
output,
|
|
108
|
-
}
|
|
121
|
+
};
|
|
109
122
|
},
|
|
110
|
-
})
|
|
123
|
+
});
|
package/src/tool/multiedit.ts
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Tool } from
|
|
3
|
-
import { EditTool } from
|
|
4
|
-
import DESCRIPTION from
|
|
5
|
-
import path from
|
|
6
|
-
import { Instance } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Tool } from './tool';
|
|
3
|
+
import { EditTool } from './edit';
|
|
4
|
+
import DESCRIPTION from './multiedit.txt';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { Instance } from '../project/instance';
|
|
7
7
|
|
|
8
|
-
export const MultiEditTool = Tool.define(
|
|
8
|
+
export const MultiEditTool = Tool.define('multiedit', {
|
|
9
9
|
description: DESCRIPTION,
|
|
10
10
|
parameters: z.object({
|
|
11
|
-
filePath: z.string().describe(
|
|
11
|
+
filePath: z.string().describe('The absolute path to the file to modify'),
|
|
12
12
|
edits: z
|
|
13
13
|
.array(
|
|
14
14
|
z.object({
|
|
15
|
-
filePath: z
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
filePath: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('The absolute path to the file to modify'),
|
|
18
|
+
oldString: z.string().describe('The text to replace'),
|
|
19
|
+
newString: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe(
|
|
22
|
+
'The text to replace it with (must be different from oldString)'
|
|
23
|
+
),
|
|
24
|
+
replaceAll: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Replace all occurrences of oldString (default false)'),
|
|
28
|
+
})
|
|
20
29
|
)
|
|
21
|
-
.describe(
|
|
30
|
+
.describe('Array of edit operations to perform sequentially on the file'),
|
|
22
31
|
}),
|
|
23
32
|
async execute(params, ctx) {
|
|
24
|
-
const tool = await EditTool.init()
|
|
25
|
-
const results = []
|
|
33
|
+
const tool = await EditTool.init();
|
|
34
|
+
const results = [];
|
|
26
35
|
for (const [, edit] of params.edits.entries()) {
|
|
27
36
|
const result = await tool.execute(
|
|
28
37
|
{
|
|
@@ -31,9 +40,9 @@ export const MultiEditTool = Tool.define("multiedit", {
|
|
|
31
40
|
newString: edit.newString,
|
|
32
41
|
replaceAll: edit.replaceAll,
|
|
33
42
|
},
|
|
34
|
-
ctx
|
|
35
|
-
)
|
|
36
|
-
results.push(result)
|
|
43
|
+
ctx
|
|
44
|
+
);
|
|
45
|
+
results.push(result);
|
|
37
46
|
}
|
|
38
47
|
return {
|
|
39
48
|
title: path.relative(Instance.worktree, params.filePath),
|
|
@@ -41,6 +50,6 @@ export const MultiEditTool = Tool.define("multiedit", {
|
|
|
41
50
|
results: results.map((r) => r.metadata),
|
|
42
51
|
},
|
|
43
52
|
output: results.at(-1)!.output,
|
|
44
|
-
}
|
|
53
|
+
};
|
|
45
54
|
},
|
|
46
|
-
})
|
|
55
|
+
});
|