pulse-coder-engine 0.0.1-alpha.1
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/new-engine-usage.ts +52 -0
- package/package.json +54 -0
- package/src/Engine.ts +150 -0
- package/src/ai/index.ts +116 -0
- package/src/built-in/index.ts +27 -0
- package/src/built-in/mcp-plugin/index.ts +104 -0
- package/src/built-in/skills-plugin/index.ts +223 -0
- package/src/built-in/sub-agent-plugin/index.ts +203 -0
- package/src/config/index.ts +35 -0
- package/src/context/index.ts +134 -0
- package/src/core/loop.ts +147 -0
- package/src/index.ts +17 -0
- package/src/plugin/EnginePlugin.ts +60 -0
- package/src/plugin/PluginManager.ts +426 -0
- package/src/plugin/UserConfigPlugin.ts +183 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/system.ts +126 -0
- package/src/shared/types.ts +50 -0
- package/src/tools/bash.ts +59 -0
- package/src/tools/clarify.ts +74 -0
- package/src/tools/edit.ts +79 -0
- package/src/tools/grep.ts +148 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/ls.ts +20 -0
- package/src/tools/read.ts +69 -0
- package/src/tools/tavily.ts +55 -0
- package/src/tools/utils.ts +16 -0
- package/src/tools/write.ts +42 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import type { Tool } from "../shared/types";
|
|
5
|
+
import { truncateOutput } from "./utils";
|
|
6
|
+
|
|
7
|
+
export const GrepTool: Tool<
|
|
8
|
+
{
|
|
9
|
+
pattern: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
glob?: string;
|
|
12
|
+
type?: string;
|
|
13
|
+
outputMode?: 'content' | 'files_with_matches' | 'count';
|
|
14
|
+
context?: number;
|
|
15
|
+
caseInsensitive?: boolean;
|
|
16
|
+
headLimit?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
multiline?: boolean;
|
|
19
|
+
},
|
|
20
|
+
{ output: string; matches?: number }
|
|
21
|
+
> = {
|
|
22
|
+
name: 'grep',
|
|
23
|
+
description: 'A powerful search tool built on ripgrep. Supports regex patterns, file filtering, and multiple output modes.',
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
pattern: z.string().describe('The regular expression pattern to search for in file contents'),
|
|
26
|
+
path: z.string().optional().describe('File or directory to search in. Defaults to current working directory.'),
|
|
27
|
+
glob: z.string().optional().describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
|
|
28
|
+
type: z.string().optional().describe('File type to search (e.g., js, py, rust, go, java, ts, tsx, json, md)'),
|
|
29
|
+
outputMode: z.enum(['content', 'files_with_matches', 'count']).optional().default('files_with_matches')
|
|
30
|
+
.describe('Output mode: "content" shows matching lines, "files_with_matches" shows file paths, "count" shows match counts'),
|
|
31
|
+
context: z.number().optional().describe('Number of lines to show before and after each match (only with output_mode: "content")'),
|
|
32
|
+
caseInsensitive: z.boolean().optional().default(false).describe('Case insensitive search'),
|
|
33
|
+
headLimit: z.number().optional().default(0).describe('Limit output to first N lines/entries. 0 means unlimited.'),
|
|
34
|
+
offset: z.number().optional().default(0).describe('Skip first N lines/entries before applying head_limit'),
|
|
35
|
+
multiline: z.boolean().optional().default(false).describe('Enable multiline mode where patterns can span lines'),
|
|
36
|
+
}),
|
|
37
|
+
execute: async ({
|
|
38
|
+
pattern,
|
|
39
|
+
path = '.',
|
|
40
|
+
glob,
|
|
41
|
+
type,
|
|
42
|
+
outputMode = 'files_with_matches',
|
|
43
|
+
context,
|
|
44
|
+
caseInsensitive = false,
|
|
45
|
+
headLimit = 0,
|
|
46
|
+
offset = 0,
|
|
47
|
+
multiline = false,
|
|
48
|
+
}) => {
|
|
49
|
+
// Build ripgrep command
|
|
50
|
+
const args: string[] = ['rg'];
|
|
51
|
+
|
|
52
|
+
// Pattern
|
|
53
|
+
args.push(pattern);
|
|
54
|
+
|
|
55
|
+
// Case sensitivity
|
|
56
|
+
if (caseInsensitive) {
|
|
57
|
+
args.push('-i');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Output mode
|
|
61
|
+
if (outputMode === 'files_with_matches') {
|
|
62
|
+
args.push('-l'); // --files-with-matches
|
|
63
|
+
} else if (outputMode === 'count') {
|
|
64
|
+
args.push('-c'); // --count
|
|
65
|
+
} else if (outputMode === 'content') {
|
|
66
|
+
args.push('-n'); // Show line numbers
|
|
67
|
+
if (context !== undefined) {
|
|
68
|
+
args.push(`-C${context}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Multiline mode
|
|
73
|
+
if (multiline) {
|
|
74
|
+
args.push('-U'); // --multiline
|
|
75
|
+
args.push('--multiline-dotall');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// File filtering
|
|
79
|
+
if (glob) {
|
|
80
|
+
args.push('--glob', glob);
|
|
81
|
+
}
|
|
82
|
+
if (type) {
|
|
83
|
+
args.push('--type', type);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Path
|
|
87
|
+
if (path && path !== '.') {
|
|
88
|
+
// Verify path exists
|
|
89
|
+
if (!existsSync(path)) {
|
|
90
|
+
throw new Error(`Path does not exist: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
args.push(path);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build the command string
|
|
96
|
+
let command = args.map(arg => {
|
|
97
|
+
// Quote arguments that contain spaces or special characters
|
|
98
|
+
if (arg.includes(' ') || arg.includes('$') || arg.includes('*')) {
|
|
99
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
100
|
+
}
|
|
101
|
+
return arg;
|
|
102
|
+
}).join(' ');
|
|
103
|
+
|
|
104
|
+
// Add tail/head for offset/limit
|
|
105
|
+
if (offset > 0) {
|
|
106
|
+
command += ` | tail -n +${offset + 1}`;
|
|
107
|
+
}
|
|
108
|
+
if (headLimit > 0) {
|
|
109
|
+
command += ` | head -n ${headLimit}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const output = execSync(command, {
|
|
114
|
+
encoding: 'utf-8',
|
|
115
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB
|
|
116
|
+
shell: '/bin/bash',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Count matches for count mode
|
|
120
|
+
let matches: number | undefined;
|
|
121
|
+
if (outputMode === 'count') {
|
|
122
|
+
matches = output.split('\n').filter(line => line.trim()).length;
|
|
123
|
+
} else if (outputMode === 'files_with_matches') {
|
|
124
|
+
matches = output.split('\n').filter(line => line.trim()).length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
output: truncateOutput(output || '(no matches found)'),
|
|
129
|
+
matches,
|
|
130
|
+
};
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
// Exit code 1 means no matches found (not an error)
|
|
133
|
+
if (error.status === 1) {
|
|
134
|
+
return {
|
|
135
|
+
output: '(no matches found)',
|
|
136
|
+
matches: 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Other errors
|
|
141
|
+
throw new Error(
|
|
142
|
+
`grep failed: ${error.stderr || error.message}\nCommand: ${command}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default GrepTool;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ReadTool } from './read';
|
|
2
|
+
import { WriteTool } from './write';
|
|
3
|
+
import { EditTool } from './edit';
|
|
4
|
+
import { GrepTool } from './grep';
|
|
5
|
+
import { LsTool } from './ls';
|
|
6
|
+
import { BashTool } from './bash';
|
|
7
|
+
import { TavilyTool } from './tavily';
|
|
8
|
+
import { ClarifyTool } from './clarify';
|
|
9
|
+
import { Tool } from 'ai';
|
|
10
|
+
// import { SkillTool } from './skill;
|
|
11
|
+
|
|
12
|
+
export const BuiltinTools = [
|
|
13
|
+
ReadTool,
|
|
14
|
+
WriteTool,
|
|
15
|
+
EditTool,
|
|
16
|
+
GrepTool,
|
|
17
|
+
LsTool,
|
|
18
|
+
BashTool,
|
|
19
|
+
TavilyTool,
|
|
20
|
+
ClarifyTool,
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export const BuiltinToolsMap = BuiltinTools.reduce((acc, toolInstance) => {
|
|
24
|
+
acc[toolInstance.name] = toolInstance;
|
|
25
|
+
return acc;
|
|
26
|
+
}, {} as Record<string, any>);
|
|
27
|
+
|
|
28
|
+
export const getFinalToolsMap = (customTools?: Record<string, Tool>) => {
|
|
29
|
+
if (!customTools) {
|
|
30
|
+
return BuiltinToolsMap;
|
|
31
|
+
}
|
|
32
|
+
return { ...BuiltinToolsMap, ...customTools };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
ReadTool,
|
|
37
|
+
WriteTool,
|
|
38
|
+
EditTool,
|
|
39
|
+
GrepTool,
|
|
40
|
+
LsTool,
|
|
41
|
+
BashTool,
|
|
42
|
+
TavilyTool,
|
|
43
|
+
ClarifyTool,
|
|
44
|
+
};
|
package/src/tools/ls.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { readdirSync } from "fs";
|
|
3
|
+
import type { Tool } from "../shared/types";
|
|
4
|
+
|
|
5
|
+
export const LsTool: Tool<
|
|
6
|
+
{ path?: string },
|
|
7
|
+
{ files: string[] }
|
|
8
|
+
> = {
|
|
9
|
+
name: 'ls',
|
|
10
|
+
description: 'List files and directories in a given path',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
path: z.string().optional().describe('The path to list files from (defaults to current directory)'),
|
|
13
|
+
}),
|
|
14
|
+
execute: async ({ path = '.' }) => {
|
|
15
|
+
const files = readdirSync(path);
|
|
16
|
+
return { files };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default LsTool;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
3
|
+
import type { Tool } from "../shared/types";
|
|
4
|
+
import { truncateOutput } from "./utils";
|
|
5
|
+
|
|
6
|
+
export const ReadTool: Tool<
|
|
7
|
+
{ filePath: string; offset?: number; limit?: number },
|
|
8
|
+
{ content: string; totalLines?: number }
|
|
9
|
+
> = {
|
|
10
|
+
name: 'read',
|
|
11
|
+
description: 'Read the contents of a file. Supports reading specific line ranges with offset and limit.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
filePath: z.string().describe('The absolute path to the file to read'),
|
|
14
|
+
offset: z.number().optional().describe('The line number to start reading from (0-based). Only provide if the file is too large to read at once.'),
|
|
15
|
+
limit: z.number().optional().describe('The number of lines to read. Only provide if the file is too large to read at once.'),
|
|
16
|
+
}),
|
|
17
|
+
execute: async ({ filePath, offset, limit }) => {
|
|
18
|
+
// Check if file exists
|
|
19
|
+
if (!existsSync(filePath)) {
|
|
20
|
+
throw new Error(`File does not exist: ${filePath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if it's a directory
|
|
24
|
+
const stats = statSync(filePath);
|
|
25
|
+
if (stats.isDirectory()) {
|
|
26
|
+
throw new Error(`Cannot read directory: ${filePath}. Use 'ls' tool to list directory contents.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read the entire file
|
|
30
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
31
|
+
const lines = content.split('\n');
|
|
32
|
+
const totalLines = lines.length;
|
|
33
|
+
|
|
34
|
+
// If no offset/limit specified, return the entire file
|
|
35
|
+
if (offset === undefined && limit === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
content: truncateOutput(content),
|
|
38
|
+
totalLines,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Apply offset and limit
|
|
43
|
+
const startLine = offset || 0;
|
|
44
|
+
const endLine = limit ? startLine + limit : lines.length;
|
|
45
|
+
|
|
46
|
+
// Validate range
|
|
47
|
+
if (startLine < 0 || startLine >= totalLines) {
|
|
48
|
+
throw new Error(`Invalid offset: ${startLine}. File has ${totalLines} lines.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Extract the requested lines
|
|
52
|
+
const selectedLines = lines.slice(startLine, endLine);
|
|
53
|
+
|
|
54
|
+
// Format with line numbers (1-based for display)
|
|
55
|
+
const numberedContent = selectedLines
|
|
56
|
+
.map((line, idx) => {
|
|
57
|
+
const lineNum = startLine + idx + 1;
|
|
58
|
+
return `${String(lineNum).padStart(6, ' ')}→${line}`;
|
|
59
|
+
})
|
|
60
|
+
.join('\n');
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
content: truncateOutput(numberedContent),
|
|
64
|
+
totalLines,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default ReadTool;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type { Tool } from "../shared/types";
|
|
3
|
+
import { truncateOutput } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const TavilyTool: Tool<
|
|
6
|
+
{ query: string; maxResults?: number },
|
|
7
|
+
{ results: Array<{ title: string; url: string; content: string; score?: number }> }
|
|
8
|
+
> = {
|
|
9
|
+
name: 'tavily',
|
|
10
|
+
description: 'Search the web using Tavily API',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
query: z.string().describe('The search query'),
|
|
13
|
+
maxResults: z.number().optional().default(5).describe('Maximum number of results to return'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async ({ query, maxResults = 5 }) => {
|
|
16
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
17
|
+
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
throw new Error('TAVILY_API_KEY environment variable is not set');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const response = await fetch('https://api.tavily.com/search', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
api_key: apiKey,
|
|
29
|
+
query,
|
|
30
|
+
max_results: maxResults,
|
|
31
|
+
search_depth: 'basic',
|
|
32
|
+
include_answer: false,
|
|
33
|
+
include_raw_content: false,
|
|
34
|
+
include_images: false,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Tavily API error: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
results: data.results?.map((result: any) => ({
|
|
46
|
+
title: result.title || '',
|
|
47
|
+
url: result.url || '',
|
|
48
|
+
content: truncateOutput(result.content || ''),
|
|
49
|
+
score: result.score || 0,
|
|
50
|
+
})) || [],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default TavilyTool;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MAX_TOOL_OUTPUT_LENGTH } from "../config";
|
|
2
|
+
|
|
3
|
+
export const truncateOutput = (output: string): string => {
|
|
4
|
+
if (output.length <= MAX_TOOL_OUTPUT_LENGTH) {
|
|
5
|
+
return output;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const half = Math.floor(MAX_TOOL_OUTPUT_LENGTH / 2);
|
|
9
|
+
const removed = output.length - MAX_TOOL_OUTPUT_LENGTH;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
output.slice(0, half) +
|
|
13
|
+
`\n\n... [truncated ${removed} characters] ...\n\n` +
|
|
14
|
+
output.slice(-half)
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import type { Tool } from "../shared/types";
|
|
5
|
+
|
|
6
|
+
export const WriteTool: Tool<
|
|
7
|
+
{ filePath: string; content: string },
|
|
8
|
+
{ success: boolean; created: boolean; bytes: number }
|
|
9
|
+
> = {
|
|
10
|
+
name: 'write',
|
|
11
|
+
description: 'Write contents to a file. Automatically creates parent directories if they do not exist. Will overwrite existing files.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
filePath: z.string().describe('The absolute path to the file to write (must be absolute, not relative)'),
|
|
14
|
+
content: z.string().describe('The content to write to the file'),
|
|
15
|
+
}),
|
|
16
|
+
execute: async ({ filePath, content }) => {
|
|
17
|
+
// Check if file already exists
|
|
18
|
+
const fileExists = existsSync(filePath);
|
|
19
|
+
|
|
20
|
+
// Get the directory path
|
|
21
|
+
const dir = dirname(filePath);
|
|
22
|
+
|
|
23
|
+
// Create parent directories if they don't exist
|
|
24
|
+
if (!existsSync(dir)) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Write the file
|
|
29
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
30
|
+
|
|
31
|
+
// Calculate bytes written
|
|
32
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
created: !fileExists,
|
|
37
|
+
bytes,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default WriteTool;
|
package/tsconfig.json
ADDED