@link-assistant/agent 0.0.8 → 0.0.11
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 +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -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 +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- 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 +78 -0
- 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 +554 -332
- package/src/json-standard/index.ts +173 -0
- 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/patch.ts
CHANGED
|
@@ -1,188 +1,215 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import { Tool } from
|
|
5
|
-
import { FileTime } from
|
|
6
|
-
import { Bus } from
|
|
7
|
-
import { FileWatcher } from
|
|
8
|
-
import { Instance } from
|
|
9
|
-
import { Patch } from
|
|
10
|
-
import { Filesystem } from
|
|
11
|
-
import { createTwoFilesPatch } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { Tool } from './tool';
|
|
5
|
+
import { FileTime } from '../file/time';
|
|
6
|
+
import { Bus } from '../bus';
|
|
7
|
+
import { FileWatcher } from '../file/watcher';
|
|
8
|
+
import { Instance } from '../project/instance';
|
|
9
|
+
import { Patch } from '../patch';
|
|
10
|
+
import { Filesystem } from '../util/filesystem';
|
|
11
|
+
import { createTwoFilesPatch } from 'diff';
|
|
12
12
|
|
|
13
13
|
const PatchParams = z.object({
|
|
14
|
-
patchText: z
|
|
15
|
-
|
|
14
|
+
patchText: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('The full patch text that describes all changes to be made'),
|
|
17
|
+
});
|
|
16
18
|
|
|
17
|
-
export const PatchTool = Tool.define(
|
|
19
|
+
export const PatchTool = Tool.define('patch', {
|
|
18
20
|
description:
|
|
19
|
-
|
|
21
|
+
'Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.',
|
|
20
22
|
parameters: PatchParams,
|
|
21
23
|
async execute(params, ctx) {
|
|
22
24
|
if (!params.patchText) {
|
|
23
|
-
throw new Error(
|
|
25
|
+
throw new Error('patchText is required');
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// Parse the patch to get hunks
|
|
27
|
-
let hunks: Patch.Hunk[]
|
|
29
|
+
let hunks: Patch.Hunk[];
|
|
28
30
|
try {
|
|
29
|
-
const parseResult = Patch.parsePatch(params.patchText)
|
|
30
|
-
hunks = parseResult.hunks
|
|
31
|
+
const parseResult = Patch.parsePatch(params.patchText);
|
|
32
|
+
hunks = parseResult.hunks;
|
|
31
33
|
} catch (error) {
|
|
32
|
-
throw new Error(`Failed to parse patch: ${error}`)
|
|
34
|
+
throw new Error(`Failed to parse patch: ${error}`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (hunks.length === 0) {
|
|
36
|
-
throw new Error(
|
|
38
|
+
throw new Error('No file changes found in patch');
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// No restrictions - unrestricted patching
|
|
40
42
|
const fileChanges: Array<{
|
|
41
|
-
filePath: string
|
|
42
|
-
oldContent: string
|
|
43
|
-
newContent: string
|
|
44
|
-
type:
|
|
45
|
-
movePath?: string
|
|
46
|
-
}> = []
|
|
43
|
+
filePath: string;
|
|
44
|
+
oldContent: string;
|
|
45
|
+
newContent: string;
|
|
46
|
+
type: 'add' | 'update' | 'delete' | 'move';
|
|
47
|
+
movePath?: string;
|
|
48
|
+
}> = [];
|
|
47
49
|
|
|
48
|
-
let totalDiff =
|
|
50
|
+
let totalDiff = '';
|
|
49
51
|
|
|
50
52
|
for (const hunk of hunks) {
|
|
51
|
-
const filePath = path.resolve(Instance.directory, hunk.path)
|
|
53
|
+
const filePath = path.resolve(Instance.directory, hunk.path);
|
|
52
54
|
|
|
53
55
|
switch (hunk.type) {
|
|
54
|
-
case
|
|
55
|
-
if (hunk.type ===
|
|
56
|
-
const oldContent =
|
|
57
|
-
const newContent = hunk.contents
|
|
58
|
-
const diff = createTwoFilesPatch(
|
|
56
|
+
case 'add':
|
|
57
|
+
if (hunk.type === 'add') {
|
|
58
|
+
const oldContent = '';
|
|
59
|
+
const newContent = hunk.contents;
|
|
60
|
+
const diff = createTwoFilesPatch(
|
|
61
|
+
filePath,
|
|
62
|
+
filePath,
|
|
63
|
+
oldContent,
|
|
64
|
+
newContent
|
|
65
|
+
);
|
|
59
66
|
|
|
60
67
|
fileChanges.push({
|
|
61
68
|
filePath,
|
|
62
69
|
oldContent,
|
|
63
70
|
newContent,
|
|
64
|
-
type:
|
|
65
|
-
})
|
|
71
|
+
type: 'add',
|
|
72
|
+
});
|
|
66
73
|
|
|
67
|
-
totalDiff += diff +
|
|
74
|
+
totalDiff += diff + '\n';
|
|
68
75
|
}
|
|
69
|
-
break
|
|
76
|
+
break;
|
|
70
77
|
|
|
71
|
-
case
|
|
78
|
+
case 'update':
|
|
72
79
|
// Check if file exists for update
|
|
73
|
-
const stats = await fs.stat(filePath).catch(() => null)
|
|
80
|
+
const stats = await fs.stat(filePath).catch(() => null);
|
|
74
81
|
if (!stats || stats.isDirectory()) {
|
|
75
|
-
throw new Error(`File not found or is directory: ${filePath}`)
|
|
82
|
+
throw new Error(`File not found or is directory: ${filePath}`);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
// Read file and update time tracking (like edit tool does)
|
|
79
|
-
await FileTime.assert(ctx.sessionID, filePath)
|
|
80
|
-
const oldContent = await fs.readFile(filePath,
|
|
81
|
-
let newContent = oldContent
|
|
86
|
+
await FileTime.assert(ctx.sessionID, filePath);
|
|
87
|
+
const oldContent = await fs.readFile(filePath, 'utf-8');
|
|
88
|
+
let newContent = oldContent;
|
|
82
89
|
|
|
83
90
|
// Apply the update chunks to get new content
|
|
84
91
|
try {
|
|
85
|
-
const fileUpdate = Patch.deriveNewContentsFromChunks(
|
|
86
|
-
|
|
92
|
+
const fileUpdate = Patch.deriveNewContentsFromChunks(
|
|
93
|
+
filePath,
|
|
94
|
+
hunk.chunks
|
|
95
|
+
);
|
|
96
|
+
newContent = fileUpdate.content;
|
|
87
97
|
} catch (error) {
|
|
88
|
-
throw new Error(`Failed to apply update to ${filePath}: ${error}`)
|
|
98
|
+
throw new Error(`Failed to apply update to ${filePath}: ${error}`);
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
const diff = createTwoFilesPatch(
|
|
101
|
+
const diff = createTwoFilesPatch(
|
|
102
|
+
filePath,
|
|
103
|
+
filePath,
|
|
104
|
+
oldContent,
|
|
105
|
+
newContent
|
|
106
|
+
);
|
|
92
107
|
|
|
93
108
|
fileChanges.push({
|
|
94
109
|
filePath,
|
|
95
110
|
oldContent,
|
|
96
111
|
newContent,
|
|
97
|
-
type: hunk.move_path ?
|
|
98
|
-
movePath: hunk.move_path
|
|
99
|
-
|
|
112
|
+
type: hunk.move_path ? 'move' : 'update',
|
|
113
|
+
movePath: hunk.move_path
|
|
114
|
+
? path.resolve(Instance.directory, hunk.move_path)
|
|
115
|
+
: undefined,
|
|
116
|
+
});
|
|
100
117
|
|
|
101
|
-
totalDiff += diff +
|
|
102
|
-
break
|
|
118
|
+
totalDiff += diff + '\n';
|
|
119
|
+
break;
|
|
103
120
|
|
|
104
|
-
case
|
|
121
|
+
case 'delete':
|
|
105
122
|
// Check if file exists for deletion
|
|
106
|
-
await FileTime.assert(ctx.sessionID, filePath)
|
|
107
|
-
const contentToDelete = await fs.readFile(filePath,
|
|
108
|
-
const deleteDiff = createTwoFilesPatch(
|
|
123
|
+
await FileTime.assert(ctx.sessionID, filePath);
|
|
124
|
+
const contentToDelete = await fs.readFile(filePath, 'utf-8');
|
|
125
|
+
const deleteDiff = createTwoFilesPatch(
|
|
126
|
+
filePath,
|
|
127
|
+
filePath,
|
|
128
|
+
contentToDelete,
|
|
129
|
+
''
|
|
130
|
+
);
|
|
109
131
|
|
|
110
132
|
fileChanges.push({
|
|
111
133
|
filePath,
|
|
112
134
|
oldContent: contentToDelete,
|
|
113
|
-
newContent:
|
|
114
|
-
type:
|
|
115
|
-
})
|
|
135
|
+
newContent: '',
|
|
136
|
+
type: 'delete',
|
|
137
|
+
});
|
|
116
138
|
|
|
117
|
-
totalDiff += deleteDiff +
|
|
118
|
-
break
|
|
139
|
+
totalDiff += deleteDiff + '\n';
|
|
140
|
+
break;
|
|
119
141
|
}
|
|
120
142
|
}
|
|
121
143
|
|
|
122
144
|
// No restrictions - apply changes directly
|
|
123
145
|
// Apply the changes
|
|
124
|
-
const changedFiles: string[] = []
|
|
146
|
+
const changedFiles: string[] = [];
|
|
125
147
|
|
|
126
148
|
for (const change of fileChanges) {
|
|
127
149
|
switch (change.type) {
|
|
128
|
-
case
|
|
150
|
+
case 'add':
|
|
129
151
|
// Create parent directories
|
|
130
|
-
const addDir = path.dirname(change.filePath)
|
|
131
|
-
if (addDir !==
|
|
132
|
-
await fs.mkdir(addDir, { recursive: true })
|
|
152
|
+
const addDir = path.dirname(change.filePath);
|
|
153
|
+
if (addDir !== '.' && addDir !== '/') {
|
|
154
|
+
await fs.mkdir(addDir, { recursive: true });
|
|
133
155
|
}
|
|
134
|
-
await fs.writeFile(change.filePath, change.newContent,
|
|
135
|
-
changedFiles.push(change.filePath)
|
|
136
|
-
break
|
|
156
|
+
await fs.writeFile(change.filePath, change.newContent, 'utf-8');
|
|
157
|
+
changedFiles.push(change.filePath);
|
|
158
|
+
break;
|
|
137
159
|
|
|
138
|
-
case
|
|
139
|
-
await fs.writeFile(change.filePath, change.newContent,
|
|
140
|
-
changedFiles.push(change.filePath)
|
|
141
|
-
break
|
|
160
|
+
case 'update':
|
|
161
|
+
await fs.writeFile(change.filePath, change.newContent, 'utf-8');
|
|
162
|
+
changedFiles.push(change.filePath);
|
|
163
|
+
break;
|
|
142
164
|
|
|
143
|
-
case
|
|
165
|
+
case 'move':
|
|
144
166
|
if (change.movePath) {
|
|
145
167
|
// Create parent directories for destination
|
|
146
|
-
const moveDir = path.dirname(change.movePath)
|
|
147
|
-
if (moveDir !==
|
|
148
|
-
await fs.mkdir(moveDir, { recursive: true })
|
|
168
|
+
const moveDir = path.dirname(change.movePath);
|
|
169
|
+
if (moveDir !== '.' && moveDir !== '/') {
|
|
170
|
+
await fs.mkdir(moveDir, { recursive: true });
|
|
149
171
|
}
|
|
150
172
|
// Write to new location
|
|
151
|
-
await fs.writeFile(change.movePath, change.newContent,
|
|
173
|
+
await fs.writeFile(change.movePath, change.newContent, 'utf-8');
|
|
152
174
|
// Remove original
|
|
153
|
-
await fs.unlink(change.filePath)
|
|
154
|
-
changedFiles.push(change.movePath)
|
|
175
|
+
await fs.unlink(change.filePath);
|
|
176
|
+
changedFiles.push(change.movePath);
|
|
155
177
|
}
|
|
156
|
-
break
|
|
178
|
+
break;
|
|
157
179
|
|
|
158
|
-
case
|
|
159
|
-
await fs.unlink(change.filePath)
|
|
160
|
-
changedFiles.push(change.filePath)
|
|
161
|
-
break
|
|
180
|
+
case 'delete':
|
|
181
|
+
await fs.unlink(change.filePath);
|
|
182
|
+
changedFiles.push(change.filePath);
|
|
183
|
+
break;
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
// Update file time tracking
|
|
165
|
-
FileTime.read(ctx.sessionID, change.filePath)
|
|
187
|
+
FileTime.read(ctx.sessionID, change.filePath);
|
|
166
188
|
if (change.movePath) {
|
|
167
|
-
FileTime.read(ctx.sessionID, change.movePath)
|
|
189
|
+
FileTime.read(ctx.sessionID, change.movePath);
|
|
168
190
|
}
|
|
169
191
|
}
|
|
170
192
|
|
|
171
193
|
// Publish file change events
|
|
172
194
|
for (const filePath of changedFiles) {
|
|
173
|
-
await Bus.publish(FileWatcher.Event.Updated, {
|
|
195
|
+
await Bus.publish(FileWatcher.Event.Updated, {
|
|
196
|
+
file: filePath,
|
|
197
|
+
event: 'change',
|
|
198
|
+
});
|
|
174
199
|
}
|
|
175
200
|
|
|
176
201
|
// Generate output summary
|
|
177
|
-
const relativePaths = changedFiles.map((filePath) =>
|
|
178
|
-
|
|
202
|
+
const relativePaths = changedFiles.map((filePath) =>
|
|
203
|
+
path.relative(Instance.worktree, filePath)
|
|
204
|
+
);
|
|
205
|
+
const summary = `${fileChanges.length} files changed`;
|
|
179
206
|
|
|
180
207
|
return {
|
|
181
208
|
title: summary,
|
|
182
209
|
metadata: {
|
|
183
210
|
diff: totalDiff,
|
|
184
211
|
},
|
|
185
|
-
output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join(
|
|
186
|
-
}
|
|
212
|
+
output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join('\n')}`,
|
|
213
|
+
};
|
|
187
214
|
},
|
|
188
|
-
})
|
|
215
|
+
});
|
package/src/tool/read.ts
CHANGED
|
@@ -1,69 +1,82 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as path from
|
|
4
|
-
import { Tool } from
|
|
5
|
-
import { FileTime } from
|
|
6
|
-
import DESCRIPTION from
|
|
7
|
-
import { Filesystem } from
|
|
8
|
-
import { Instance } from
|
|
9
|
-
import { Provider } from
|
|
10
|
-
import { Identifier } from
|
|
11
|
-
import { iife } from
|
|
12
|
-
|
|
13
|
-
const DEFAULT_READ_LIMIT = 2000
|
|
14
|
-
const MAX_LINE_LENGTH = 2000
|
|
15
|
-
|
|
16
|
-
export const ReadTool = Tool.define(
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { Tool } from './tool';
|
|
5
|
+
import { FileTime } from '../file/time';
|
|
6
|
+
import DESCRIPTION from './read.txt';
|
|
7
|
+
import { Filesystem } from '../util/filesystem';
|
|
8
|
+
import { Instance } from '../project/instance';
|
|
9
|
+
import { Provider } from '../provider/provider';
|
|
10
|
+
import { Identifier } from '../id/id';
|
|
11
|
+
import { iife } from '../util/iife';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_READ_LIMIT = 2000;
|
|
14
|
+
const MAX_LINE_LENGTH = 2000;
|
|
15
|
+
|
|
16
|
+
export const ReadTool = Tool.define('read', {
|
|
17
17
|
description: DESCRIPTION,
|
|
18
18
|
parameters: z.object({
|
|
19
|
-
filePath: z.string().describe(
|
|
20
|
-
offset: z.coerce
|
|
21
|
-
|
|
19
|
+
filePath: z.string().describe('The path to the file to read'),
|
|
20
|
+
offset: z.coerce
|
|
21
|
+
.number()
|
|
22
|
+
.describe('The line number to start reading from (0-based)')
|
|
23
|
+
.optional(),
|
|
24
|
+
limit: z.coerce
|
|
25
|
+
.number()
|
|
26
|
+
.describe('The number of lines to read (defaults to 2000)')
|
|
27
|
+
.optional(),
|
|
22
28
|
}),
|
|
23
29
|
async execute(params, ctx) {
|
|
24
|
-
let filepath = params.filePath
|
|
30
|
+
let filepath = params.filePath;
|
|
25
31
|
if (!path.isAbsolute(filepath)) {
|
|
26
|
-
filepath = path.join(process.cwd(), filepath)
|
|
32
|
+
filepath = path.join(process.cwd(), filepath);
|
|
27
33
|
}
|
|
28
|
-
const title = path.relative(Instance.worktree, filepath)
|
|
34
|
+
const title = path.relative(Instance.worktree, filepath);
|
|
29
35
|
|
|
30
36
|
// No restrictions - unrestricted file read
|
|
31
|
-
const file = Bun.file(filepath)
|
|
37
|
+
const file = Bun.file(filepath);
|
|
32
38
|
if (!(await file.exists())) {
|
|
33
|
-
const dir = path.dirname(filepath)
|
|
34
|
-
const base = path.basename(filepath)
|
|
39
|
+
const dir = path.dirname(filepath);
|
|
40
|
+
const base = path.basename(filepath);
|
|
35
41
|
|
|
36
|
-
const dirEntries = fs.readdirSync(dir)
|
|
42
|
+
const dirEntries = fs.readdirSync(dir);
|
|
37
43
|
const suggestions = dirEntries
|
|
38
44
|
.filter(
|
|
39
45
|
(entry) =>
|
|
40
|
-
entry.toLowerCase().includes(base.toLowerCase()) ||
|
|
46
|
+
entry.toLowerCase().includes(base.toLowerCase()) ||
|
|
47
|
+
base.toLowerCase().includes(entry.toLowerCase())
|
|
41
48
|
)
|
|
42
49
|
.map((entry) => path.join(dir, entry))
|
|
43
|
-
.slice(0, 3)
|
|
50
|
+
.slice(0, 3);
|
|
44
51
|
|
|
45
52
|
if (suggestions.length > 0) {
|
|
46
|
-
throw new Error(
|
|
53
|
+
throw new Error(
|
|
54
|
+
`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join('\n')}`
|
|
55
|
+
);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
throw new Error(`File not found: ${filepath}`)
|
|
58
|
+
throw new Error(`File not found: ${filepath}`);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
const isImage = isImageFile(filepath)
|
|
61
|
+
const isImage = isImageFile(filepath);
|
|
53
62
|
const supportsImages = await (async () => {
|
|
54
|
-
if (!ctx.extra?.[
|
|
55
|
-
const providerID = ctx.extra[
|
|
56
|
-
const modelID = ctx.extra[
|
|
57
|
-
const model = await Provider.getModel(providerID, modelID).catch(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (!ctx.extra?.['providerID'] || !ctx.extra?.['modelID']) return false;
|
|
64
|
+
const providerID = ctx.extra['providerID'] as string;
|
|
65
|
+
const modelID = ctx.extra['modelID'] as string;
|
|
66
|
+
const model = await Provider.getModel(providerID, modelID).catch(
|
|
67
|
+
() => undefined
|
|
68
|
+
);
|
|
69
|
+
if (!model) return false;
|
|
70
|
+
return model.info.modalities?.input?.includes('image') ?? false;
|
|
71
|
+
})();
|
|
61
72
|
if (isImage) {
|
|
62
73
|
if (!supportsImages) {
|
|
63
|
-
throw new Error(
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Failed to read image: ${filepath}, model may not be able to read images`
|
|
76
|
+
);
|
|
64
77
|
}
|
|
65
|
-
const mime = file.type
|
|
66
|
-
const msg =
|
|
78
|
+
const mime = file.type;
|
|
79
|
+
const msg = 'Image read successfully';
|
|
67
80
|
return {
|
|
68
81
|
title,
|
|
69
82
|
output: msg,
|
|
@@ -72,47 +85,49 @@ export const ReadTool = Tool.define("read", {
|
|
|
72
85
|
},
|
|
73
86
|
attachments: [
|
|
74
87
|
{
|
|
75
|
-
id: Identifier.ascending(
|
|
88
|
+
id: Identifier.ascending('part'),
|
|
76
89
|
sessionID: ctx.sessionID,
|
|
77
90
|
messageID: ctx.messageID,
|
|
78
|
-
type:
|
|
91
|
+
type: 'file',
|
|
79
92
|
mime,
|
|
80
|
-
url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString(
|
|
93
|
+
url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString('base64')}`,
|
|
81
94
|
},
|
|
82
95
|
],
|
|
83
|
-
}
|
|
96
|
+
};
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
const isBinary = await isBinaryFile(filepath, file)
|
|
87
|
-
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
|
99
|
+
const isBinary = await isBinaryFile(filepath, file);
|
|
100
|
+
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`);
|
|
88
101
|
|
|
89
|
-
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
|
90
|
-
const offset = params.offset || 0
|
|
91
|
-
const lines = await file.text().then((text) => text.split(
|
|
102
|
+
const limit = params.limit ?? DEFAULT_READ_LIMIT;
|
|
103
|
+
const offset = params.offset || 0;
|
|
104
|
+
const lines = await file.text().then((text) => text.split('\n'));
|
|
92
105
|
const raw = lines.slice(offset, offset + limit).map((line) => {
|
|
93
|
-
return line.length > MAX_LINE_LENGTH
|
|
94
|
-
|
|
106
|
+
return line.length > MAX_LINE_LENGTH
|
|
107
|
+
? line.substring(0, MAX_LINE_LENGTH) + '...'
|
|
108
|
+
: line;
|
|
109
|
+
});
|
|
95
110
|
const content = raw.map((line, index) => {
|
|
96
|
-
return `${(index + offset + 1).toString().padStart(5,
|
|
97
|
-
})
|
|
98
|
-
const preview = raw.slice(0, 20).join(
|
|
111
|
+
return `${(index + offset + 1).toString().padStart(5, '0')}| ${line}`;
|
|
112
|
+
});
|
|
113
|
+
const preview = raw.slice(0, 20).join('\n');
|
|
99
114
|
|
|
100
|
-
let output =
|
|
101
|
-
output += content.join(
|
|
115
|
+
let output = '<file>\n';
|
|
116
|
+
output += content.join('\n');
|
|
102
117
|
|
|
103
|
-
const totalLines = lines.length
|
|
104
|
-
const lastReadLine = offset + content.length
|
|
105
|
-
const hasMoreLines = totalLines > lastReadLine
|
|
118
|
+
const totalLines = lines.length;
|
|
119
|
+
const lastReadLine = offset + content.length;
|
|
120
|
+
const hasMoreLines = totalLines > lastReadLine;
|
|
106
121
|
|
|
107
122
|
if (hasMoreLines) {
|
|
108
|
-
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})
|
|
123
|
+
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})`;
|
|
109
124
|
} else {
|
|
110
|
-
output += `\n\n(End of file - total ${totalLines} lines)
|
|
125
|
+
output += `\n\n(End of file - total ${totalLines} lines)`;
|
|
111
126
|
}
|
|
112
|
-
output +=
|
|
127
|
+
output += '\n</file>';
|
|
113
128
|
|
|
114
129
|
// just warms the lsp client
|
|
115
|
-
FileTime.read(ctx.sessionID, filepath)
|
|
130
|
+
FileTime.read(ctx.sessionID, filepath);
|
|
116
131
|
|
|
117
132
|
return {
|
|
118
133
|
title,
|
|
@@ -120,82 +135,85 @@ export const ReadTool = Tool.define("read", {
|
|
|
120
135
|
metadata: {
|
|
121
136
|
preview,
|
|
122
137
|
},
|
|
123
|
-
}
|
|
138
|
+
};
|
|
124
139
|
},
|
|
125
|
-
})
|
|
140
|
+
});
|
|
126
141
|
|
|
127
142
|
function isImageFile(filePath: string): string | false {
|
|
128
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
143
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
129
144
|
switch (ext) {
|
|
130
|
-
case
|
|
131
|
-
case
|
|
132
|
-
return
|
|
133
|
-
case
|
|
134
|
-
return
|
|
135
|
-
case
|
|
136
|
-
return
|
|
137
|
-
case
|
|
138
|
-
return
|
|
139
|
-
case
|
|
140
|
-
return
|
|
145
|
+
case '.jpg':
|
|
146
|
+
case '.jpeg':
|
|
147
|
+
return 'JPEG';
|
|
148
|
+
case '.png':
|
|
149
|
+
return 'PNG';
|
|
150
|
+
case '.gif':
|
|
151
|
+
return 'GIF';
|
|
152
|
+
case '.bmp':
|
|
153
|
+
return 'BMP';
|
|
154
|
+
case '.webp':
|
|
155
|
+
return 'WebP';
|
|
141
156
|
default:
|
|
142
|
-
return false
|
|
157
|
+
return false;
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
async function isBinaryFile(
|
|
147
|
-
|
|
161
|
+
async function isBinaryFile(
|
|
162
|
+
filepath: string,
|
|
163
|
+
file: Bun.BunFile
|
|
164
|
+
): Promise<boolean> {
|
|
165
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
148
166
|
// binary check for common non-text extensions
|
|
149
167
|
switch (ext) {
|
|
150
|
-
case
|
|
151
|
-
case
|
|
152
|
-
case
|
|
153
|
-
case
|
|
154
|
-
case
|
|
155
|
-
case
|
|
156
|
-
case
|
|
157
|
-
case
|
|
158
|
-
case
|
|
159
|
-
case
|
|
160
|
-
case
|
|
161
|
-
case
|
|
162
|
-
case
|
|
163
|
-
case
|
|
164
|
-
case
|
|
165
|
-
case
|
|
166
|
-
case
|
|
167
|
-
case
|
|
168
|
-
case
|
|
169
|
-
case
|
|
170
|
-
case
|
|
171
|
-
case
|
|
172
|
-
case
|
|
173
|
-
case
|
|
174
|
-
case
|
|
175
|
-
case
|
|
176
|
-
case
|
|
177
|
-
case
|
|
178
|
-
return true
|
|
168
|
+
case '.zip':
|
|
169
|
+
case '.tar':
|
|
170
|
+
case '.gz':
|
|
171
|
+
case '.exe':
|
|
172
|
+
case '.dll':
|
|
173
|
+
case '.so':
|
|
174
|
+
case '.class':
|
|
175
|
+
case '.jar':
|
|
176
|
+
case '.war':
|
|
177
|
+
case '.7z':
|
|
178
|
+
case '.doc':
|
|
179
|
+
case '.docx':
|
|
180
|
+
case '.xls':
|
|
181
|
+
case '.xlsx':
|
|
182
|
+
case '.ppt':
|
|
183
|
+
case '.pptx':
|
|
184
|
+
case '.odt':
|
|
185
|
+
case '.ods':
|
|
186
|
+
case '.odp':
|
|
187
|
+
case '.bin':
|
|
188
|
+
case '.dat':
|
|
189
|
+
case '.obj':
|
|
190
|
+
case '.o':
|
|
191
|
+
case '.a':
|
|
192
|
+
case '.lib':
|
|
193
|
+
case '.wasm':
|
|
194
|
+
case '.pyc':
|
|
195
|
+
case '.pyo':
|
|
196
|
+
return true;
|
|
179
197
|
default:
|
|
180
|
-
break
|
|
198
|
+
break;
|
|
181
199
|
}
|
|
182
200
|
|
|
183
|
-
const stat = await file.stat()
|
|
184
|
-
const fileSize = stat.size
|
|
185
|
-
if (fileSize === 0) return false
|
|
201
|
+
const stat = await file.stat();
|
|
202
|
+
const fileSize = stat.size;
|
|
203
|
+
if (fileSize === 0) return false;
|
|
186
204
|
|
|
187
|
-
const bufferSize = Math.min(4096, fileSize)
|
|
188
|
-
const buffer = await file.arrayBuffer()
|
|
189
|
-
if (buffer.byteLength === 0) return false
|
|
190
|
-
const bytes = new Uint8Array(buffer.slice(0, bufferSize))
|
|
205
|
+
const bufferSize = Math.min(4096, fileSize);
|
|
206
|
+
const buffer = await file.arrayBuffer();
|
|
207
|
+
if (buffer.byteLength === 0) return false;
|
|
208
|
+
const bytes = new Uint8Array(buffer.slice(0, bufferSize));
|
|
191
209
|
|
|
192
|
-
let nonPrintableCount = 0
|
|
210
|
+
let nonPrintableCount = 0;
|
|
193
211
|
for (let i = 0; i < bytes.length; i++) {
|
|
194
|
-
if (bytes[i] === 0) return true
|
|
212
|
+
if (bytes[i] === 0) return true;
|
|
195
213
|
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
|
196
|
-
nonPrintableCount
|
|
214
|
+
nonPrintableCount++;
|
|
197
215
|
}
|
|
198
216
|
}
|
|
199
217
|
// If >30% non-printable characters, consider it binary
|
|
200
|
-
return nonPrintableCount / bytes.length > 0.3
|
|
218
|
+
return nonPrintableCount / bytes.length > 0.3;
|
|
201
219
|
}
|