@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.
Files changed (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
package/src/tool/patch.ts CHANGED
@@ -1,188 +1,215 @@
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"
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.string().describe("The full patch text that describes all changes to be made"),
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("patch", {
19
+ export const PatchTool = Tool.define('patch', {
18
20
  description:
19
- "Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.",
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("patchText is required")
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("No file changes found in patch")
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: "add" | "update" | "delete" | "move"
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 "add":
55
- if (hunk.type === "add") {
56
- const oldContent = ""
57
- const newContent = hunk.contents
58
- const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
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: "add",
65
- })
71
+ type: 'add',
72
+ });
66
73
 
67
- totalDiff += diff + "\n"
74
+ totalDiff += diff + '\n';
68
75
  }
69
- break
76
+ break;
70
77
 
71
- case "update":
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, "utf-8")
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(filePath, hunk.chunks)
86
- newContent = fileUpdate.content
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(filePath, filePath, oldContent, newContent)
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 ? "move" : "update",
98
- movePath: hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined,
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 + "\n"
102
- break
118
+ totalDiff += diff + '\n';
119
+ break;
103
120
 
104
- case "delete":
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, "utf-8")
108
- const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "")
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: "delete",
115
- })
135
+ newContent: '',
136
+ type: 'delete',
137
+ });
116
138
 
117
- totalDiff += deleteDiff + "\n"
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 "add":
150
+ case 'add':
129
151
  // Create parent directories
130
- const addDir = path.dirname(change.filePath)
131
- if (addDir !== "." && 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, "utf-8")
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 "update":
139
- await fs.writeFile(change.filePath, change.newContent, "utf-8")
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 "move":
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 !== "." && 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, "utf-8")
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 "delete":
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, { file: filePath, event: "change" })
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) => path.relative(Instance.worktree, filePath))
178
- const summary = `${fileChanges.length} files changed`
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("\n")}`,
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 "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", {
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("The path to the file to read"),
20
- offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
21
- limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
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()) || base.toLowerCase().includes(entry.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(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
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?.["providerID"] || !ctx.extra?.["modelID"]) return false
55
- const providerID = ctx.extra["providerID"] as string
56
- const modelID = ctx.extra["modelID"] as string
57
- const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
58
- if (!model) return false
59
- return model.info.modalities?.input?.includes("image") ?? false
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(`Failed to read image: ${filepath}, model may not be able to read images`)
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 = "Image read successfully"
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("part"),
88
+ id: Identifier.ascending('part'),
76
89
  sessionID: ctx.sessionID,
77
90
  messageID: ctx.messageID,
78
- type: "file",
91
+ type: 'file',
79
92
  mime,
80
- url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,
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("\n"))
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 ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
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, "0")}| ${line}`
97
- })
98
- const preview = raw.slice(0, 20).join("\n")
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 = "<file>\n"
101
- output += content.join("\n")
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 += "\n</file>"
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 ".jpg":
131
- case ".jpeg":
132
- return "JPEG"
133
- case ".png":
134
- return "PNG"
135
- case ".gif":
136
- return "GIF"
137
- case ".bmp":
138
- return "BMP"
139
- case ".webp":
140
- return "WebP"
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(filepath: string, file: Bun.BunFile): Promise<boolean> {
147
- const ext = path.extname(filepath).toLowerCase()
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 ".zip":
151
- case ".tar":
152
- case ".gz":
153
- case ".exe":
154
- case ".dll":
155
- case ".so":
156
- case ".class":
157
- case ".jar":
158
- case ".war":
159
- case ".7z":
160
- case ".doc":
161
- case ".docx":
162
- case ".xls":
163
- case ".xlsx":
164
- case ".ppt":
165
- case ".pptx":
166
- case ".odt":
167
- case ".ods":
168
- case ".odp":
169
- case ".bin":
170
- case ".dat":
171
- case ".obj":
172
- case ".o":
173
- case ".a":
174
- case ".lib":
175
- case ".wasm":
176
- case ".pyc":
177
- case ".pyo":
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
  }