@lupaflow/tools 0.1.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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@lupaflow/tools",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "require": "./dist/index.js",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch"
22
+ },
23
+ "dependencies": {
24
+ "@lupaflow/types": "workspace:*",
25
+ "@lupaflow/core": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "tsup": "^8.3.0",
29
+ "typescript": "^5.6.0"
30
+ }
31
+ }
@@ -0,0 +1,26 @@
1
+ import { BaseTool } from "../interface"
2
+
3
+ export class CalculatorTool extends BaseTool {
4
+ definition = {
5
+ name: "calculator",
6
+ description: "Evaluate mathematical expressions safely",
7
+ parameters: {
8
+ expression: {
9
+ type: "string" as const,
10
+ description: "Mathematical expression to evaluate (e.g., '2 + 2 * 3')",
11
+ },
12
+ },
13
+ required: ["expression"],
14
+ }
15
+
16
+ async execute(args: Record<string, unknown>): Promise<unknown> {
17
+ const { expression } = args as { expression: string }
18
+ const sanitized = expression.replace(/[^0-9+\-*/.()^% ]/g, "")
19
+ try {
20
+ const result = Function(`"use strict"; return (${sanitized})`)()
21
+ return { result, expression }
22
+ } catch (err: any) {
23
+ return { error: err.message, expression }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,33 @@
1
+ import { unlink } from "fs/promises"
2
+ import { relative, resolve, isAbsolute } from "path"
3
+ import { BaseTool } from "../interface"
4
+
5
+ export class DeleteFileTool extends BaseTool {
6
+ definition = {
7
+ name: "deleteFile",
8
+ description: "Delete a file",
9
+ parameters: {
10
+ path: {
11
+ type: "string" as const,
12
+ description: "Relative path to the file to delete",
13
+ },
14
+ },
15
+ required: ["path"],
16
+ }
17
+
18
+ async execute(args: Record<string, unknown>): Promise<unknown> {
19
+ const { path } = args as { path: string }
20
+ const cwd = process.cwd()
21
+ const resolved = resolve(cwd, path)
22
+ const rel = relative(cwd, resolved)
23
+ if (rel.startsWith("..") || isAbsolute(rel)) {
24
+ return { error: "Path is outside the project directory" }
25
+ }
26
+ try {
27
+ await unlink(resolved)
28
+ return { success: true, path: rel }
29
+ } catch (err: any) {
30
+ return { error: err.message }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,101 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises"
2
+ import { dirname, relative, resolve, isAbsolute } from "path"
3
+ import { BaseTool } from "../interface"
4
+
5
+ export class EditFileTool extends BaseTool {
6
+ definition = {
7
+ name: "editFile",
8
+ description: "Replace an exact string match in a file with new content",
9
+ parameters: {
10
+ path: {
11
+ type: "string" as const,
12
+ description: "Relative path to edit",
13
+ },
14
+ oldString: {
15
+ type: "string" as const,
16
+ description: "Exact text to replace; must be unique in the file",
17
+ },
18
+ newString: {
19
+ type: "string" as const,
20
+ description: "Replacement text",
21
+ },
22
+ },
23
+ required: ["path", "oldString", "newString"],
24
+ }
25
+
26
+ async execute(args: Record<string, unknown>): Promise<unknown> {
27
+ const { path, oldString, newString } = args as { path: string; oldString: string; newString: string }
28
+ const cwd = process.cwd()
29
+ const resolved = resolve(cwd, path)
30
+ const rel = relative(cwd, resolved)
31
+ if (rel.startsWith("..") || isAbsolute(rel)) {
32
+ return { error: "Path is outside the project directory" }
33
+ }
34
+ try {
35
+ const content = await readFile(resolved, "utf-8")
36
+ const occurrences = content.split(oldString).length - 1
37
+
38
+ if (occurrences === 0) {
39
+ return { error: "oldString not found in file" }
40
+ }
41
+ if (occurrences > 1) {
42
+ return { error: `oldString is ambiguous; found ${occurrences} matches` }
43
+ }
44
+
45
+ const backupDir = resolve(cwd, ".lupaflow/backups")
46
+ const backupPath = resolve(backupDir, rel)
47
+ await mkdir(dirname(backupPath), { recursive: true })
48
+ await writeFile(backupPath, content, "utf-8")
49
+
50
+ const newContent = content.replace(oldString, newString)
51
+ await writeFile(resolved, newContent, "utf-8")
52
+
53
+ return {
54
+ success: true,
55
+ path: rel,
56
+ diff: computeDiff(content, newContent),
57
+ }
58
+ } catch (err: any) {
59
+ return { error: err.message }
60
+ }
61
+ }
62
+ }
63
+
64
+ function computeDiff(oldStr: string, newStr: string): string {
65
+ const oldLines = oldStr.split("\n")
66
+ const newLines = newStr.split("\n")
67
+ const lines: string[] = []
68
+ let i = 0, j = 0
69
+
70
+ while (i < oldLines.length || j < newLines.length) {
71
+ if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
72
+ lines.push(` ${oldLines[i]}`)
73
+ i++
74
+ j++
75
+ } else {
76
+ let found = false
77
+ for (let k = 1; k <= 3; k++) {
78
+ if (i + k < oldLines.length && oldLines[i + k] === newLines[j]) {
79
+ for (let l = 0; l < k; l++) lines.push(`-${oldLines[i + l]}`)
80
+ i += k
81
+ found = true
82
+ break
83
+ }
84
+ if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
85
+ for (let l = 0; l < k; l++) lines.push(`+${newLines[j + l]}`)
86
+ j += k
87
+ found = true
88
+ break
89
+ }
90
+ }
91
+ if (!found) {
92
+ if (i < oldLines.length) lines.push(`-${oldLines[i]}`)
93
+ if (j < newLines.length) lines.push(`+${newLines[j]}`)
94
+ i++
95
+ j++
96
+ }
97
+ }
98
+ }
99
+
100
+ return lines.join("\n")
101
+ }
@@ -0,0 +1,53 @@
1
+ import { relative, resolve, isAbsolute } from "path"
2
+ import { BaseTool } from "../interface"
3
+
4
+ const MAX_RESULTS = 200
5
+
6
+ export class GlobTool extends BaseTool {
7
+ definition = {
8
+ name: "glob",
9
+ description: "Find files matching a glob pattern",
10
+ parameters: {
11
+ pattern: {
12
+ type: "string" as const,
13
+ description: "Glob pattern to match files",
14
+ },
15
+ path: {
16
+ type: "string" as const,
17
+ description: "Directory to search from (default: .)",
18
+ },
19
+ },
20
+ required: ["pattern"],
21
+ }
22
+
23
+ async execute(args: Record<string, unknown>): Promise<unknown> {
24
+ const { pattern, path: dir = "." } = args as { pattern: string; path?: string }
25
+ const cwd = process.cwd()
26
+ const resolved = resolve(cwd, dir)
27
+ const rel = relative(cwd, resolved)
28
+ if (rel.startsWith("..") || isAbsolute(rel)) {
29
+ return { error: "Path is outside the project directory" }
30
+ }
31
+ try {
32
+ const glob = new Bun.Glob(pattern)
33
+ const files: string[] = []
34
+ let truncated = false
35
+
36
+ for await (const match of glob.scan({ cwd: resolved, dot: false, onlyFiles: true })) {
37
+ if (match.includes("node_modules")) continue
38
+ if (files.length >= MAX_RESULTS) {
39
+ truncated = true
40
+ break
41
+ }
42
+ files.push(relative(cwd, resolve(resolved, match)))
43
+ }
44
+
45
+ files.sort()
46
+ const result: Record<string, unknown> = { files }
47
+ if (truncated) result.truncated = true
48
+ return result
49
+ } catch (err: any) {
50
+ return { error: err.message }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,100 @@
1
+ import { relative, resolve, isAbsolute } from "path"
2
+ import { BaseTool } from "../interface"
3
+
4
+ const MAX_MATCHES = 50
5
+
6
+ export class GrepTool extends BaseTool {
7
+ definition = {
8
+ name: "grep",
9
+ description: "Search file contents using a regex pattern",
10
+ parameters: {
11
+ pattern: {
12
+ type: "string" as const,
13
+ description: "Regex pattern to search for",
14
+ },
15
+ path: {
16
+ type: "string" as const,
17
+ description: "Directory to search from (default: .)",
18
+ },
19
+ include: {
20
+ type: "string" as const,
21
+ description: "Optional glob for files to include (e.g. *.ts)",
22
+ },
23
+ },
24
+ required: ["pattern"],
25
+ }
26
+
27
+ async execute(args: Record<string, unknown>): Promise<unknown> {
28
+ const { pattern, path: dir = ".", include } = args as { pattern: string; path?: string; include?: string }
29
+ const cwd = process.cwd()
30
+ const resolved = resolve(cwd, dir)
31
+ const rel = relative(cwd, resolved)
32
+ if (rel.startsWith("..") || isAbsolute(rel)) {
33
+ return { error: "Path is outside the project directory" }
34
+ }
35
+ try {
36
+ const grepArgs = [
37
+ "-rn", "--color=never",
38
+ "--exclude-dir=node_modules",
39
+ "--exclude-dir=.git",
40
+ "-E",
41
+ ]
42
+ if (include) grepArgs.push(`--include=${include}`)
43
+ grepArgs.push(pattern, resolved)
44
+
45
+ const proc = Bun.spawn(["grep", ...grepArgs], {
46
+ cwd,
47
+ stdout: "pipe",
48
+ stderr: "pipe",
49
+ })
50
+
51
+ const [stdout, stderr] = await Promise.all([
52
+ new Response(proc.stdout).text(),
53
+ new Response(proc.stderr).text(),
54
+ ])
55
+
56
+ const exitCode = await proc.exited
57
+ if (exitCode !== 0 && exitCode !== 1) {
58
+ const msg = stderr.trim() || `grep exited with code ${exitCode}`
59
+ return { error: msg }
60
+ }
61
+
62
+ const trimmed = stderr.trim()
63
+ if (trimmed) {
64
+ return { error: trimmed }
65
+ }
66
+
67
+ const lines = stdout.trim().split("\n")
68
+ if (!lines[0]) {
69
+ return { matches: [], message: "No matches found" }
70
+ }
71
+
72
+ const matches: { file: string; line: number; content: string }[] = []
73
+ let truncated = false
74
+
75
+ for (const line of lines) {
76
+ if (matches.length >= MAX_MATCHES) {
77
+ truncated = true
78
+ break
79
+ }
80
+ const match = line.match(/^(.+?):(\d+):(.*)$/)
81
+ if (match) {
82
+ matches.push({
83
+ file: relative(cwd, match[1]!),
84
+ line: Number(match[2]),
85
+ content: match[3]!,
86
+ })
87
+ }
88
+ }
89
+
90
+ const result: Record<string, unknown> = { matches }
91
+ if (truncated) {
92
+ result.truncated = true
93
+ result.totalMatches = lines.length
94
+ }
95
+ return result
96
+ } catch (err: any) {
97
+ return { error: err.message || String(err) }
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,55 @@
1
+ import { BaseTool } from "../interface"
2
+
3
+ export class HTTPTool extends BaseTool {
4
+ definition = {
5
+ name: "http",
6
+ description: "Make HTTP requests (GET, POST, PUT, DELETE)",
7
+ parameters: {
8
+ method: {
9
+ type: "string" as const,
10
+ description: "HTTP method: GET, POST, PUT, DELETE",
11
+ },
12
+ url: {
13
+ type: "string" as const,
14
+ description: "Request URL",
15
+ },
16
+ headers: {
17
+ type: "object" as const,
18
+ description: "Request headers as key-value object",
19
+ },
20
+ body: {
21
+ type: "string" as const,
22
+ description: "Request body (stringified JSON for POST/PUT)",
23
+ },
24
+ },
25
+ required: ["method", "url"],
26
+ }
27
+
28
+ async execute(args: Record<string, unknown>): Promise<unknown> {
29
+ const { method, url, headers, body } = args as {
30
+ method: string
31
+ url: string
32
+ headers?: Record<string, string>
33
+ body?: string
34
+ }
35
+
36
+ const response = await fetch(url, {
37
+ method: method.toUpperCase(),
38
+ headers: { "Content-Type": "application/json", ...headers },
39
+ body: body || undefined,
40
+ })
41
+
42
+ const text = await response.text()
43
+ let data: unknown = text
44
+ try {
45
+ data = JSON.parse(text)
46
+ } catch { /* ignore */ }
47
+
48
+ return {
49
+ status: response.status,
50
+ statusText: response.statusText,
51
+ headers: Object.fromEntries(response.headers.entries()),
52
+ data,
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,52 @@
1
+ import { readdir, stat } from "fs/promises"
2
+ import { join, relative, resolve, isAbsolute } from "path"
3
+ import { BaseTool } from "../interface"
4
+
5
+ export class ListDirectoryTool extends BaseTool {
6
+ definition = {
7
+ name: "listDirectory",
8
+ description: "List entries in a directory",
9
+ parameters: {
10
+ path: {
11
+ type: "string" as const,
12
+ description: "Relative directory path to list",
13
+ },
14
+ },
15
+ required: ["path"],
16
+ }
17
+
18
+ async execute(args: Record<string, unknown>): Promise<unknown> {
19
+ const { path } = args as { path: string }
20
+ const cwd = process.cwd()
21
+ const resolved = resolve(cwd, path)
22
+ const rel = relative(cwd, resolved)
23
+ if (rel.startsWith("..") || isAbsolute(rel)) {
24
+ return { error: "Path is outside the project directory" }
25
+ }
26
+ try {
27
+ const entries = await readdir(resolved)
28
+ const results: { name: string; type: "file" | "directory" }[] = []
29
+
30
+ for (const entry of entries) {
31
+ if (entry.startsWith(".") || entry === "node_modules") continue
32
+ try {
33
+ const info = await stat(join(resolved, entry))
34
+ results.push({
35
+ name: entry,
36
+ type: info.isDirectory() ? "directory" : "file",
37
+ })
38
+ } catch { /* ignore */ }
39
+ }
40
+
41
+ results.sort((a, b) =>
42
+ a.type !== b.type
43
+ ? a.type === "directory" ? -1 : 1
44
+ : a.name.localeCompare(b.name)
45
+ )
46
+
47
+ return { path: rel || ".", entries: results }
48
+ } catch (err: any) {
49
+ return { error: err.message }
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile } from "fs/promises"
2
+ import { relative, resolve, isAbsolute } from "path"
3
+ import { BaseTool } from "../interface"
4
+
5
+ const MAX_FILE_SIZE = 10_000
6
+
7
+ export class ReadFileTool extends BaseTool {
8
+ definition = {
9
+ name: "readFile",
10
+ description: "Read the contents of a file",
11
+ parameters: {
12
+ path: {
13
+ type: "string" as const,
14
+ description: "Relative path to the file to read",
15
+ },
16
+ },
17
+ required: ["path"],
18
+ }
19
+
20
+ async execute(args: Record<string, unknown>): Promise<unknown> {
21
+ const { path } = args as { path: string }
22
+ const cwd = process.cwd()
23
+ const resolved = resolve(cwd, path)
24
+ const rel = relative(cwd, resolved)
25
+ if (rel.startsWith("..") || isAbsolute(rel)) {
26
+ return { error: "Path is outside the project directory" }
27
+ }
28
+ try {
29
+ const content = await readFile(resolved, "utf-8")
30
+ if (content.length > MAX_FILE_SIZE) {
31
+ return {
32
+ content: content.slice(0, MAX_FILE_SIZE),
33
+ truncated: true,
34
+ totalLength: content.length,
35
+ }
36
+ }
37
+ return { content }
38
+ } catch (err: any) {
39
+ const msg = err.message?.replace(/^ENOENT:\s*/, "") || String(err)
40
+ return { error: msg }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,93 @@
1
+ import { BaseTool } from "../interface"
2
+
3
+ export class SearchTool extends BaseTool {
4
+ definition = {
5
+ name: "search",
6
+ description: "Search the web for information",
7
+ parameters: {
8
+ query: {
9
+ type: "string" as const,
10
+ description: "Search query",
11
+ },
12
+ maxResults: {
13
+ type: "number" as const,
14
+ description: "Maximum number of results (default: 5)",
15
+ },
16
+ },
17
+ required: ["query"],
18
+ }
19
+
20
+ async execute(args: Record<string, unknown>): Promise<unknown> {
21
+ const { query, maxResults } = args as { query: string; maxResults?: number }
22
+
23
+ const tavilyKey = process.env.TAVILY_API_KEY
24
+ if (tavilyKey) {
25
+ return this.searchTavily(query, maxResults || 5, tavilyKey)
26
+ }
27
+
28
+ return this.searchDuckDuckGo(query, maxResults || 5)
29
+ }
30
+
31
+ private async searchTavily(query: string, maxResults: number, apiKey: string): Promise<unknown> {
32
+ try {
33
+ const res = await fetch("https://api.tavily.com/search", {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({
37
+ api_key: apiKey,
38
+ query,
39
+ search_depth: "advanced",
40
+ include_answer: true,
41
+ max_results: maxResults,
42
+ }),
43
+ })
44
+ if (!res.ok) {
45
+ throw new Error(`Tavily API error: ${res.status} ${res.statusText}`)
46
+ }
47
+ const data = await res.json() as {
48
+ results: { title: string; url: string; content: string }[]
49
+ answer?: string
50
+ }
51
+ return {
52
+ query,
53
+ results: data.results.map((r) => ({
54
+ title: r.title,
55
+ url: r.url,
56
+ snippet: r.content,
57
+ })),
58
+ answer: data.answer,
59
+ source: "tavily",
60
+ }
61
+ } catch (err: any) {
62
+ return { error: err.message, query, source: "tavily" }
63
+ }
64
+ }
65
+
66
+ private async searchDuckDuckGo(query: string, maxResults: number): Promise<unknown> {
67
+ try {
68
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
69
+ const response = await fetch(url)
70
+ const html = await response.text()
71
+ const results: Array<{ title: string; link: string; snippet: string }> = []
72
+
73
+ const linkRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g
74
+ const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g
75
+
76
+ let count = 0
77
+ let linkMatch
78
+ while ((linkMatch = linkRegex.exec(html)) !== null && count < maxResults) {
79
+ const snippetMatch = snippetRegex.exec(html)
80
+ results.push({
81
+ link: linkMatch[1],
82
+ title: linkMatch[2].replace(/<[^>]*>/g, ""),
83
+ snippet: snippetMatch ? snippetMatch[1].replace(/<[^>]*>/g, "") : "",
84
+ })
85
+ count++
86
+ }
87
+
88
+ return { query, results, source: "duckduckgo" }
89
+ } catch (err: any) {
90
+ return { error: err.message, query, source: "duckduckgo" }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,76 @@
1
+ import { BaseTool } from "../interface"
2
+
3
+ const MAX_OUTPUT = 20_000
4
+ const DEFAULT_TIMEOUT = 30_000
5
+
6
+ export class ShellTool extends BaseTool {
7
+ definition = {
8
+ name: "shell",
9
+ description: "Execute a shell command",
10
+ parameters: {
11
+ command: {
12
+ type: "string" as const,
13
+ description: "Shell command to run",
14
+ },
15
+ description: {
16
+ type: "string" as const,
17
+ description: "Short description of what the command does",
18
+ },
19
+ timeout: {
20
+ type: "number" as const,
21
+ description: "Timeout in milliseconds (default: 30000)",
22
+ },
23
+ },
24
+ required: ["command"],
25
+ }
26
+
27
+ async execute(args: Record<string, unknown>): Promise<unknown> {
28
+ const { command, timeout = DEFAULT_TIMEOUT } = args as {
29
+ command: string
30
+ timeout?: number
31
+ }
32
+ try {
33
+ const isWindows = process.platform === "win32"
34
+ const shell = isWindows ? "cmd.exe" : "bash"
35
+ const shellFlag = isWindows ? "/c" : "-c"
36
+ const env = isWindows
37
+ ? { ...process.env }
38
+ : { ...process.env, TERM: "dumb" }
39
+
40
+ const proc = Bun.spawn([shell, shellFlag, command], {
41
+ cwd: process.cwd(),
42
+ stdout: "pipe",
43
+ stderr: "pipe",
44
+ env,
45
+ })
46
+
47
+ const timer = setTimeout(() => proc.kill(), timeout as number)
48
+
49
+ const [stdout, stderr] = await Promise.all([
50
+ new Response(proc.stdout).text(),
51
+ new Response(proc.stderr).text(),
52
+ ])
53
+
54
+ const exitCode = await proc.exited
55
+ clearTimeout(timer)
56
+
57
+ return {
58
+ stdout: truncate(stdout, MAX_OUTPUT),
59
+ stderr: truncate(stderr, MAX_OUTPUT),
60
+ exitCode,
61
+ }
62
+ } catch (err: any) {
63
+ return {
64
+ stdout: "",
65
+ stderr: err.message || String(err),
66
+ exitCode: 1,
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ function truncate(value: string, limit: number): string {
73
+ return value.length > limit
74
+ ? `${value.slice(0, limit)}\n... (truncated, ${value.length} total chars)`
75
+ : value
76
+ }
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises"
2
+ import { dirname, relative, resolve, isAbsolute } from "path"
3
+ import { BaseTool } from "../interface"
4
+
5
+ export class WriteFileTool extends BaseTool {
6
+ definition = {
7
+ name: "writeFile",
8
+ description: "Create a new file or overwrite an existing one",
9
+ parameters: {
10
+ path: {
11
+ type: "string" as const,
12
+ description: "Relative path to write",
13
+ },
14
+ content: {
15
+ type: "string" as const,
16
+ description: "File contents",
17
+ },
18
+ },
19
+ required: ["path", "content"],
20
+ }
21
+
22
+ async execute(args: Record<string, unknown>): Promise<unknown> {
23
+ const { path, content } = args as { path: string; content: string }
24
+ const cwd = process.cwd()
25
+ const resolved = resolve(cwd, path)
26
+ const rel = relative(cwd, resolved)
27
+ if (rel.startsWith("..") || isAbsolute(rel)) {
28
+ return { error: "Path is outside the project directory" }
29
+ }
30
+ try {
31
+ let oldContent: string | null = null
32
+ try {
33
+ oldContent = await readFile(resolved, "utf-8")
34
+ } catch { /* ignore */ }
35
+
36
+ const backupDir = resolve(cwd, ".lupaflow/backups")
37
+ if (oldContent !== null) {
38
+ const backupPath = resolve(backupDir, rel)
39
+ await mkdir(dirname(backupPath), { recursive: true })
40
+ await writeFile(backupPath, oldContent, "utf-8")
41
+ }
42
+
43
+ await mkdir(dirname(resolved), { recursive: true })
44
+ await writeFile(resolved, content, "utf-8")
45
+
46
+ const result: Record<string, unknown> = {
47
+ success: true,
48
+ path: rel,
49
+ }
50
+ if (oldContent !== null) {
51
+ result.diff = computeDiff(oldContent, content)
52
+ }
53
+ return result
54
+ } catch (err: any) {
55
+ return { error: err.message }
56
+ }
57
+ }
58
+ }
59
+
60
+ function computeDiff(oldStr: string, newStr: string): string {
61
+ const oldLines = oldStr.split("\n")
62
+ const newLines = newStr.split("\n")
63
+ const lines: string[] = []
64
+ let i = 0, j = 0
65
+
66
+ while (i < oldLines.length || j < newLines.length) {
67
+ if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
68
+ lines.push(` ${oldLines[i]}`)
69
+ i++
70
+ j++
71
+ } else {
72
+ let found = false
73
+ for (let k = 1; k <= 3; k++) {
74
+ if (i + k < oldLines.length && oldLines[i + k] === newLines[j]) {
75
+ for (let l = 0; l < k; l++) lines.push(`-${oldLines[i + l]}`)
76
+ i += k
77
+ found = true
78
+ break
79
+ }
80
+ if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
81
+ for (let l = 0; l < k; l++) lines.push(`+${newLines[j + l]}`)
82
+ j += k
83
+ found = true
84
+ break
85
+ }
86
+ }
87
+ if (!found) {
88
+ if (i < oldLines.length) lines.push(`-${oldLines[i]}`)
89
+ if (j < newLines.length) lines.push(`+${newLines[j]}`)
90
+ i++
91
+ j++
92
+ }
93
+ }
94
+ }
95
+
96
+ return lines.join("\n")
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { toolRegistry } from "./registry"
2
+ import { CalculatorTool } from "./builtins/calculator"
3
+ import { HTTPTool } from "./builtins/http"
4
+ import { SearchTool } from "./builtins/search"
5
+ import { ReadFileTool } from "./builtins/readFile"
6
+ import { WriteFileTool } from "./builtins/writeFile"
7
+ import { EditFileTool } from "./builtins/editFile"
8
+ import { DeleteFileTool } from "./builtins/deleteFile"
9
+ import { ListDirectoryTool } from "./builtins/listDirectory"
10
+ import { GlobTool } from "./builtins/glob"
11
+ import { GrepTool } from "./builtins/grep"
12
+ import { ShellTool } from "./builtins/shell"
13
+
14
+ toolRegistry.register(new CalculatorTool())
15
+ toolRegistry.register(new HTTPTool())
16
+ toolRegistry.register(new SearchTool())
17
+ toolRegistry.register(new ReadFileTool())
18
+ toolRegistry.register(new WriteFileTool())
19
+ toolRegistry.register(new EditFileTool())
20
+ toolRegistry.register(new DeleteFileTool())
21
+ toolRegistry.register(new ListDirectoryTool())
22
+ toolRegistry.register(new GlobTool())
23
+ toolRegistry.register(new GrepTool())
24
+ toolRegistry.register(new ShellTool())
25
+
26
+ export { toolRegistry }
27
+ export { BaseTool } from "./interface"
28
+ export { CalculatorTool } from "./builtins/calculator"
29
+ export { HTTPTool } from "./builtins/http"
30
+ export { SearchTool } from "./builtins/search"
31
+ export { ReadFileTool } from "./builtins/readFile"
32
+ export { WriteFileTool } from "./builtins/writeFile"
33
+ export { EditFileTool } from "./builtins/editFile"
34
+ export { DeleteFileTool } from "./builtins/deleteFile"
35
+ export { ListDirectoryTool } from "./builtins/listDirectory"
36
+ export { GlobTool } from "./builtins/glob"
37
+ export { GrepTool } from "./builtins/grep"
38
+ export { ShellTool } from "./builtins/shell"
@@ -0,0 +1,11 @@
1
+ import type { Tool, ToolDefinition } from "@lupaflow/types"
2
+
3
+ export abstract class BaseTool implements Tool {
4
+ abstract definition: ToolDefinition
5
+
6
+ abstract execute(args: Record<string, unknown>): Promise<unknown>
7
+
8
+ toJSON(): ToolDefinition {
9
+ return this.definition
10
+ }
11
+ }
@@ -0,0 +1,36 @@
1
+ import type { Tool } from "@lupaflow/types"
2
+ import { ToolError } from "@lupaflow/core"
3
+
4
+ class ToolRegistry {
5
+ private tools: Map<string, Tool> = new Map()
6
+
7
+ register(tool: Tool): void {
8
+ this.tools.set(tool.definition.name, tool)
9
+ }
10
+
11
+ get(name: string): Tool {
12
+ const tool = this.tools.get(name)
13
+ if (!tool) {
14
+ throw new ToolError(name, `Tool not found: "${name}"`)
15
+ }
16
+ return tool
17
+ }
18
+
19
+ has(name: string): boolean {
20
+ return this.tools.has(name)
21
+ }
22
+
23
+ list(): Tool[] {
24
+ return Array.from(this.tools.values())
25
+ }
26
+
27
+ remove(name: string): void {
28
+ this.tools.delete(name)
29
+ }
30
+
31
+ clear(): void {
32
+ this.tools.clear()
33
+ }
34
+ }
35
+
36
+ export const toolRegistry = new ToolRegistry()
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup"
2
+ export default defineConfig({
3
+ entry: { index: "src/index.ts" },
4
+ format: ["cjs", "esm"],
5
+ dts: true,
6
+ sourcemap: true,
7
+ clean: true,
8
+ external: [/node_modules/],
9
+ })