awel 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/LICENSE +200 -0
- package/README.md +98 -0
- package/babel-plugin-awel-source.cjs +79 -0
- package/bin/awel.js +2 -0
- package/dist/cli/agent.d.ts +6 -0
- package/dist/cli/agent.js +266 -0
- package/dist/cli/babel-setup.d.ts +1 -0
- package/dist/cli/babel-setup.js +180 -0
- package/dist/cli/comment-popup.d.ts +2 -0
- package/dist/cli/comment-popup.js +206 -0
- package/dist/cli/config.d.ts +14 -0
- package/dist/cli/config.js +29 -0
- package/dist/cli/devserver.d.ts +17 -0
- package/dist/cli/devserver.js +43 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/inspector.d.ts +2 -0
- package/dist/cli/inspector.js +117 -0
- package/dist/cli/logger.d.ts +10 -0
- package/dist/cli/logger.js +40 -0
- package/dist/cli/plan-store.d.ts +14 -0
- package/dist/cli/plan-store.js +18 -0
- package/dist/cli/providers/registry.d.ts +17 -0
- package/dist/cli/providers/registry.js +112 -0
- package/dist/cli/providers/types.d.ts +17 -0
- package/dist/cli/providers/types.js +1 -0
- package/dist/cli/providers/vercel.d.ts +4 -0
- package/dist/cli/providers/vercel.js +483 -0
- package/dist/cli/proxy.d.ts +5 -0
- package/dist/cli/proxy.js +72 -0
- package/dist/cli/server.d.ts +7 -0
- package/dist/cli/server.js +104 -0
- package/dist/cli/session.d.ts +32 -0
- package/dist/cli/session.js +77 -0
- package/dist/cli/skills/react-best-practices.md +2934 -0
- package/dist/cli/skills/skills/react-best-practices.md +2934 -0
- package/dist/cli/sse.d.ts +17 -0
- package/dist/cli/sse.js +51 -0
- package/dist/cli/subprocess.d.ts +30 -0
- package/dist/cli/subprocess.js +163 -0
- package/dist/cli/tools/ask-user.d.ts +11 -0
- package/dist/cli/tools/ask-user.js +28 -0
- package/dist/cli/tools/bash.d.ts +4 -0
- package/dist/cli/tools/bash.js +30 -0
- package/dist/cli/tools/code-search.d.ts +4 -0
- package/dist/cli/tools/code-search.js +70 -0
- package/dist/cli/tools/edit.d.ts +6 -0
- package/dist/cli/tools/edit.js +37 -0
- package/dist/cli/tools/glob.d.ts +4 -0
- package/dist/cli/tools/glob.js +29 -0
- package/dist/cli/tools/grep.d.ts +5 -0
- package/dist/cli/tools/grep.js +146 -0
- package/dist/cli/tools/index.d.ts +86 -0
- package/dist/cli/tools/index.js +41 -0
- package/dist/cli/tools/ls.d.ts +3 -0
- package/dist/cli/tools/ls.js +31 -0
- package/dist/cli/tools/multi-edit.d.ts +8 -0
- package/dist/cli/tools/multi-edit.js +53 -0
- package/dist/cli/tools/propose-plan.d.ts +4 -0
- package/dist/cli/tools/propose-plan.js +21 -0
- package/dist/cli/tools/react-best-practices.d.ts +3 -0
- package/dist/cli/tools/react-best-practices.js +55 -0
- package/dist/cli/tools/read.d.ts +3 -0
- package/dist/cli/tools/read.js +24 -0
- package/dist/cli/tools/restart-dev-server.d.ts +3 -0
- package/dist/cli/tools/restart-dev-server.js +18 -0
- package/dist/cli/tools/todo.d.ts +8 -0
- package/dist/cli/tools/todo.js +59 -0
- package/dist/cli/tools/web-fetch.d.ts +5 -0
- package/dist/cli/tools/web-fetch.js +116 -0
- package/dist/cli/tools/web-search.d.ts +5 -0
- package/dist/cli/tools/web-search.js +74 -0
- package/dist/cli/tools/write.d.ts +4 -0
- package/dist/cli/tools/write.js +26 -0
- package/dist/cli/types.d.ts +16 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/undo.d.ts +49 -0
- package/dist/cli/undo.js +212 -0
- package/dist/cli/verbose.d.ts +7 -0
- package/dist/cli/verbose.js +60 -0
- package/dist/dashboard/assets/index-Bk--q3wu.js +313 -0
- package/dist/dashboard/assets/index-DkWV03So.css +1 -0
- package/dist/dashboard/index.html +16 -0
- package/dist/host/host.js +274 -0
- package/package.json +67 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns all agentic tools configured for the given project directory.
|
|
3
|
+
* Tool names use PascalCase to match Claude Code tool names for consistent SSE events.
|
|
4
|
+
*/
|
|
5
|
+
export declare function awelTools(cwd: string): {
|
|
6
|
+
Read: import("ai").Tool<{
|
|
7
|
+
file_path: string;
|
|
8
|
+
}, string>;
|
|
9
|
+
Write: import("ai").Tool<{
|
|
10
|
+
file_path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}, string>;
|
|
13
|
+
Edit: import("ai").Tool<{
|
|
14
|
+
file_path: string;
|
|
15
|
+
old_string: string;
|
|
16
|
+
new_string: string;
|
|
17
|
+
replace_all: boolean;
|
|
18
|
+
}, string>;
|
|
19
|
+
MultiEdit: import("ai").Tool<{
|
|
20
|
+
file_path: string;
|
|
21
|
+
edits: {
|
|
22
|
+
old_string: string;
|
|
23
|
+
new_string: string;
|
|
24
|
+
replace_all: boolean;
|
|
25
|
+
}[];
|
|
26
|
+
}, string>;
|
|
27
|
+
Bash: import("ai").Tool<{
|
|
28
|
+
command: string;
|
|
29
|
+
timeout: number;
|
|
30
|
+
}, string>;
|
|
31
|
+
Glob: import("ai").Tool<{
|
|
32
|
+
pattern: string;
|
|
33
|
+
path?: string | undefined;
|
|
34
|
+
}, string>;
|
|
35
|
+
Grep: import("ai").Tool<{
|
|
36
|
+
pattern: string;
|
|
37
|
+
path?: string | undefined;
|
|
38
|
+
include?: string | undefined;
|
|
39
|
+
}, string>;
|
|
40
|
+
Ls: import("ai").Tool<{
|
|
41
|
+
path: string;
|
|
42
|
+
}, string>;
|
|
43
|
+
ProposePlan: import("ai").Tool<{
|
|
44
|
+
title: string;
|
|
45
|
+
content: string;
|
|
46
|
+
}, string>;
|
|
47
|
+
AskUser: import("ai").Tool<{
|
|
48
|
+
questions: {
|
|
49
|
+
question: string;
|
|
50
|
+
header: string;
|
|
51
|
+
multiSelect: boolean;
|
|
52
|
+
options: {
|
|
53
|
+
label: string;
|
|
54
|
+
description: string;
|
|
55
|
+
}[];
|
|
56
|
+
}[];
|
|
57
|
+
}, string>;
|
|
58
|
+
ReactBestPractices: import("ai").Tool<{
|
|
59
|
+
section: "waterfalls" | "bundle" | "server" | "client" | "rerender" | "rendering" | "javascript" | "advanced" | "all";
|
|
60
|
+
}, string>;
|
|
61
|
+
WebSearch: import("ai").Tool<{
|
|
62
|
+
query: string;
|
|
63
|
+
numResults: number;
|
|
64
|
+
type: "auto" | "fast" | "deep";
|
|
65
|
+
}, string>;
|
|
66
|
+
WebFetch: import("ai").Tool<{
|
|
67
|
+
url: string;
|
|
68
|
+
format: "text" | "markdown" | "html";
|
|
69
|
+
timeout?: number | undefined;
|
|
70
|
+
}, string>;
|
|
71
|
+
CodeSearch: import("ai").Tool<{
|
|
72
|
+
query: string;
|
|
73
|
+
tokensNum: number;
|
|
74
|
+
}, string>;
|
|
75
|
+
TodoRead: import("ai").Tool<Record<string, never>, string>;
|
|
76
|
+
TodoWrite: import("ai").Tool<{
|
|
77
|
+
todos: {
|
|
78
|
+
content: string;
|
|
79
|
+
status: "pending" | "in_progress" | "completed";
|
|
80
|
+
id?: number | undefined;
|
|
81
|
+
}[];
|
|
82
|
+
}, string>;
|
|
83
|
+
RestartDevServer: import("ai").Tool<{
|
|
84
|
+
reason?: string | undefined;
|
|
85
|
+
}, string>;
|
|
86
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createReadTool } from './read.js';
|
|
2
|
+
import { createWriteTool } from './write.js';
|
|
3
|
+
import { createEditTool } from './edit.js';
|
|
4
|
+
import { createBashTool } from './bash.js';
|
|
5
|
+
import { createGlobTool } from './glob.js';
|
|
6
|
+
import { createLsTool } from './ls.js';
|
|
7
|
+
import { createProposePlanTool } from './propose-plan.js';
|
|
8
|
+
import { createAskUserTool } from './ask-user.js';
|
|
9
|
+
import { createReactBestPracticesTool } from './react-best-practices.js';
|
|
10
|
+
import { createWebSearchTool } from './web-search.js';
|
|
11
|
+
import { createWebFetchTool } from './web-fetch.js';
|
|
12
|
+
import { createGrepTool } from './grep.js';
|
|
13
|
+
import { createCodeSearchTool } from './code-search.js';
|
|
14
|
+
import { createMultiEditTool } from './multi-edit.js';
|
|
15
|
+
import { createTodoReadTool, createTodoWriteTool } from './todo.js';
|
|
16
|
+
import { createRestartDevServerTool } from './restart-dev-server.js';
|
|
17
|
+
/**
|
|
18
|
+
* Returns all agentic tools configured for the given project directory.
|
|
19
|
+
* Tool names use PascalCase to match Claude Code tool names for consistent SSE events.
|
|
20
|
+
*/
|
|
21
|
+
export function awelTools(cwd) {
|
|
22
|
+
return {
|
|
23
|
+
Read: createReadTool(cwd),
|
|
24
|
+
Write: createWriteTool(cwd),
|
|
25
|
+
Edit: createEditTool(cwd),
|
|
26
|
+
MultiEdit: createMultiEditTool(cwd),
|
|
27
|
+
Bash: createBashTool(cwd),
|
|
28
|
+
Glob: createGlobTool(cwd),
|
|
29
|
+
Grep: createGrepTool(cwd),
|
|
30
|
+
Ls: createLsTool(cwd),
|
|
31
|
+
ProposePlan: createProposePlanTool(),
|
|
32
|
+
AskUser: createAskUserTool(),
|
|
33
|
+
ReactBestPractices: createReactBestPracticesTool(),
|
|
34
|
+
WebSearch: createWebSearchTool(),
|
|
35
|
+
WebFetch: createWebFetchTool(),
|
|
36
|
+
CodeSearch: createCodeSearchTool(),
|
|
37
|
+
TodoRead: createTodoReadTool(),
|
|
38
|
+
TodoWrite: createTodoWriteTool(),
|
|
39
|
+
RestartDevServer: createRestartDevServerTool(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readdirSync, statSync } from 'fs';
|
|
4
|
+
import { resolve, join } from 'path';
|
|
5
|
+
export function createLsTool(cwd) {
|
|
6
|
+
return tool({
|
|
7
|
+
description: 'List the contents of a directory. Returns file and directory names with type indicators.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
path: z.string().optional().default('.').describe('Directory path to list (default: project root)'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ path }) => {
|
|
12
|
+
const fullPath = path.startsWith('/') ? path : resolve(cwd, path);
|
|
13
|
+
try {
|
|
14
|
+
const entries = readdirSync(fullPath);
|
|
15
|
+
const results = entries.map(name => {
|
|
16
|
+
try {
|
|
17
|
+
const stat = statSync(join(fullPath, name));
|
|
18
|
+
return stat.isDirectory() ? `${name}/` : name;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return results.join('\n') || '(empty directory)';
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { pushSnapshot } from '../undo.js';
|
|
6
|
+
export function createMultiEditTool(cwd) {
|
|
7
|
+
return tool({
|
|
8
|
+
description: 'Perform multiple find-and-replace edits on a single file in one operation. ' +
|
|
9
|
+
'Edits are applied sequentially — each edit sees the result of previous ones. ' +
|
|
10
|
+
'Use this instead of multiple Edit calls when making several changes to the same file.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
file_path: z.string().describe('Path to the file to edit (absolute or relative to project root)'),
|
|
13
|
+
edits: z.array(z.object({
|
|
14
|
+
old_string: z.string().describe('The exact string to find'),
|
|
15
|
+
new_string: z.string().describe('The replacement string'),
|
|
16
|
+
replace_all: z.boolean().optional().default(false).describe('Replace all occurrences'),
|
|
17
|
+
})).min(1).describe('List of edits to apply sequentially'),
|
|
18
|
+
}),
|
|
19
|
+
execute: async ({ file_path, edits }) => {
|
|
20
|
+
const fullPath = file_path.startsWith('/') ? file_path : resolve(cwd, file_path);
|
|
21
|
+
if (!existsSync(fullPath)) {
|
|
22
|
+
return `Error: File not found: ${fullPath}`;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
pushSnapshot(fullPath);
|
|
26
|
+
let content = readFileSync(fullPath, 'utf-8');
|
|
27
|
+
const results = [];
|
|
28
|
+
for (let i = 0; i < edits.length; i++) {
|
|
29
|
+
const { old_string, new_string, replace_all } = edits[i];
|
|
30
|
+
if (!content.includes(old_string)) {
|
|
31
|
+
results.push(`Edit ${i + 1}: old_string not found`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
content = replace_all
|
|
35
|
+
? content.replaceAll(old_string, new_string)
|
|
36
|
+
: content.replace(old_string, new_string);
|
|
37
|
+
results.push(`Edit ${i + 1}: applied`);
|
|
38
|
+
}
|
|
39
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
40
|
+
const applied = results.filter(r => r.endsWith('applied')).length;
|
|
41
|
+
const failed = results.filter(r => r.endsWith('not found')).length;
|
|
42
|
+
let summary = `Applied ${applied}/${edits.length} edits to ${file_path}`;
|
|
43
|
+
if (failed > 0) {
|
|
44
|
+
summary += `\n${results.filter(r => r.endsWith('not found')).join('\n')}`;
|
|
45
|
+
}
|
|
46
|
+
return summary;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export function createProposePlanTool() {
|
|
4
|
+
return tool({
|
|
5
|
+
description: 'Propose a structured implementation plan before executing a complex task. ' +
|
|
6
|
+
'Use this tool when the user\'s request involves changes to 2 or more files, ' +
|
|
7
|
+
'or any non-trivial multi-step work. The content should be a detailed markdown plan ' +
|
|
8
|
+
'(up to 600-700 words for complex projects) covering an overview, step-by-step ' +
|
|
9
|
+
'implementation details, files to modify/create, and critical considerations. ' +
|
|
10
|
+
'After proposing a plan, STOP and wait for the user to approve or provide feedback.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
title: z.string().describe('A concise title for the plan'),
|
|
13
|
+
content: z.string().describe('The full markdown plan content'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async (input) => {
|
|
16
|
+
// The actual interception happens in the streaming loop (vercel.ts).
|
|
17
|
+
// This execute just returns the input so the tool call completes.
|
|
18
|
+
return JSON.stringify(input);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { resolve, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const skillPath = resolve(__dirname, '..', 'skills', 'react-best-practices.md');
|
|
8
|
+
let cached = null;
|
|
9
|
+
function loadSkill() {
|
|
10
|
+
if (!cached) {
|
|
11
|
+
cached = readFileSync(skillPath, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
const SECTIONS = {
|
|
16
|
+
waterfalls: '## 1. Eliminating Waterfalls',
|
|
17
|
+
bundle: '## 2. Bundle Size Optimization',
|
|
18
|
+
server: '## 3. Server-Side Performance',
|
|
19
|
+
client: '## 4. Client-Side Data Fetching',
|
|
20
|
+
rerender: '## 5. Re-render Optimization',
|
|
21
|
+
rendering: '## 6. Rendering Performance',
|
|
22
|
+
javascript: '## 7. JavaScript Performance',
|
|
23
|
+
advanced: '## 8. Advanced Patterns',
|
|
24
|
+
};
|
|
25
|
+
export function createReactBestPracticesTool() {
|
|
26
|
+
return tool({
|
|
27
|
+
description: 'Get React and Next.js performance best practices (40+ rules across 8 categories). ' +
|
|
28
|
+
'Call with no section to get the full guide, or specify a section to get just that category. ' +
|
|
29
|
+
'Sections: waterfalls, bundle, server, client, rerender, rendering, javascript, advanced.',
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
section: z
|
|
32
|
+
.enum(['all', 'waterfalls', 'bundle', 'server', 'client', 'rerender', 'rendering', 'javascript', 'advanced'])
|
|
33
|
+
.optional()
|
|
34
|
+
.default('all')
|
|
35
|
+
.describe('Which section to return. Defaults to "all" for the full guide.'),
|
|
36
|
+
}),
|
|
37
|
+
execute: async ({ section }) => {
|
|
38
|
+
const content = loadSkill();
|
|
39
|
+
if (section === 'all') {
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
const heading = SECTIONS[section];
|
|
43
|
+
if (!heading) {
|
|
44
|
+
return `Unknown section: ${section}. Available: ${Object.keys(SECTIONS).join(', ')}`;
|
|
45
|
+
}
|
|
46
|
+
const start = content.indexOf(heading);
|
|
47
|
+
if (start === -1) {
|
|
48
|
+
return `Section "${section}" not found in the guide.`;
|
|
49
|
+
}
|
|
50
|
+
// Find the next section heading (## N.) or end of file
|
|
51
|
+
const nextSection = content.indexOf('\n## ', start + heading.length);
|
|
52
|
+
return nextSection === -1 ? content.slice(start) : content.slice(start, nextSection);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
export function createReadTool(cwd) {
|
|
6
|
+
return tool({
|
|
7
|
+
description: 'Read the contents of a file at the given path. Returns the file content as a string.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
file_path: z.string().describe('The path to the file to read (absolute or relative to project root)'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ file_path }) => {
|
|
12
|
+
const fullPath = file_path.startsWith('/') ? file_path : resolve(cwd, file_path);
|
|
13
|
+
if (!existsSync(fullPath)) {
|
|
14
|
+
return `Error: File not found: ${fullPath}`;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(fullPath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { restartDevServer } from '../subprocess.js';
|
|
4
|
+
export function createRestartDevServerTool() {
|
|
5
|
+
return tool({
|
|
6
|
+
description: 'Restart the user\'s dev server (Next.js). Use when the dev server has crashed, is unresponsive, or needs a restart after config changes.',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
reason: z.string().optional().describe('Why the restart is needed (for logging)'),
|
|
9
|
+
}),
|
|
10
|
+
execute: async ({ reason }) => {
|
|
11
|
+
if (reason) {
|
|
12
|
+
console.error(`[awel] RestartDevServer: ${reason}`);
|
|
13
|
+
}
|
|
14
|
+
const result = await restartDevServer();
|
|
15
|
+
return result.message;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function createTodoReadTool(): import("ai").Tool<Record<string, never>, string>;
|
|
2
|
+
export declare function createTodoWriteTool(): import("ai").Tool<{
|
|
3
|
+
todos: {
|
|
4
|
+
content: string;
|
|
5
|
+
status: "pending" | "in_progress" | "completed";
|
|
6
|
+
id?: number | undefined;
|
|
7
|
+
}[];
|
|
8
|
+
}, string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
let todos = [];
|
|
4
|
+
let nextId = 1;
|
|
5
|
+
export function createTodoReadTool() {
|
|
6
|
+
return tool({
|
|
7
|
+
description: 'Read the current task/todo list. Use this to check progress, ' +
|
|
8
|
+
'review remaining work, or decide what to do next during complex multi-step tasks.',
|
|
9
|
+
inputSchema: z.object({}),
|
|
10
|
+
execute: async () => {
|
|
11
|
+
if (todos.length === 0) {
|
|
12
|
+
return 'No tasks in the list.';
|
|
13
|
+
}
|
|
14
|
+
const pending = todos.filter(t => t.status === 'pending').length;
|
|
15
|
+
const inProgress = todos.filter(t => t.status === 'in_progress').length;
|
|
16
|
+
const completed = todos.filter(t => t.status === 'completed').length;
|
|
17
|
+
const lines = todos.map(t => {
|
|
18
|
+
const icon = t.status === 'completed' ? '[x]'
|
|
19
|
+
: t.status === 'in_progress' ? '[~]'
|
|
20
|
+
: '[ ]';
|
|
21
|
+
return `${icon} #${t.id}: ${t.content}`;
|
|
22
|
+
});
|
|
23
|
+
return [
|
|
24
|
+
`Tasks: ${completed}/${todos.length} done (${pending} pending, ${inProgress} in progress)`,
|
|
25
|
+
'',
|
|
26
|
+
...lines,
|
|
27
|
+
].join('\n');
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function createTodoWriteTool() {
|
|
32
|
+
return tool({
|
|
33
|
+
description: 'Create or update the task/todo list. Pass the full list of tasks — this replaces the current list. ' +
|
|
34
|
+
'Use this to plan work, track progress on multi-step tasks, and mark items as completed.',
|
|
35
|
+
inputSchema: z.object({
|
|
36
|
+
todos: z.array(z.object({
|
|
37
|
+
id: z.number().optional().describe('Task ID (omit for new tasks, include to update existing)'),
|
|
38
|
+
content: z.string().describe('Task description'),
|
|
39
|
+
status: z.enum(['pending', 'in_progress', 'completed']).describe('Task status'),
|
|
40
|
+
})).min(1).describe('The full task list (replaces current list)'),
|
|
41
|
+
}),
|
|
42
|
+
execute: async ({ todos: newTodos }) => {
|
|
43
|
+
todos = newTodos.map(t => ({
|
|
44
|
+
id: t.id ?? nextId++,
|
|
45
|
+
content: t.content,
|
|
46
|
+
status: t.status,
|
|
47
|
+
}));
|
|
48
|
+
// Ensure nextId stays ahead of all existing IDs
|
|
49
|
+
const maxId = Math.max(0, ...todos.map(t => t.id));
|
|
50
|
+
if (nextId <= maxId) {
|
|
51
|
+
nextId = maxId + 1;
|
|
52
|
+
}
|
|
53
|
+
const pending = todos.filter(t => t.status === 'pending').length;
|
|
54
|
+
const inProgress = todos.filter(t => t.status === 'in_progress').length;
|
|
55
|
+
const completed = todos.filter(t => t.status === 'completed').length;
|
|
56
|
+
return `Updated task list: ${todos.length} tasks (${completed} done, ${inProgress} in progress, ${pending} pending)`;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import TurndownService from 'turndown';
|
|
4
|
+
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
7
|
+
export function createWebFetchTool() {
|
|
8
|
+
return tool({
|
|
9
|
+
description: 'Fetch content from a URL and return it as markdown, plain text, or raw HTML. ' +
|
|
10
|
+
'Use this to read documentation pages, API references, blog posts, or any web content. ' +
|
|
11
|
+
'Defaults to markdown format which works best for reading HTML pages.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
url: z.string().describe('The URL to fetch (must start with http:// or https://)'),
|
|
14
|
+
format: z.enum(['markdown', 'text', 'html']).optional().default('markdown')
|
|
15
|
+
.describe('Output format: "markdown" (default, best for HTML pages), "text" (stripped), or "html" (raw)'),
|
|
16
|
+
timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 120)'),
|
|
17
|
+
}),
|
|
18
|
+
execute: async ({ url, format, timeout }) => {
|
|
19
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
20
|
+
return 'Error: URL must start with http:// or https://';
|
|
21
|
+
}
|
|
22
|
+
const timeoutMs = Math.min((timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000, MAX_TIMEOUT_MS);
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
25
|
+
// Build Accept header based on requested format
|
|
26
|
+
let acceptHeader;
|
|
27
|
+
switch (format) {
|
|
28
|
+
case 'text':
|
|
29
|
+
acceptHeader = 'text/plain;q=1.0, text/html;q=0.8, */*;q=0.1';
|
|
30
|
+
break;
|
|
31
|
+
case 'html':
|
|
32
|
+
acceptHeader = 'text/html;q=1.0, application/xhtml+xml;q=0.9, */*;q=0.1';
|
|
33
|
+
break;
|
|
34
|
+
case 'markdown':
|
|
35
|
+
default:
|
|
36
|
+
acceptHeader = 'text/html;q=1.0, text/plain;q=0.8, */*;q=0.1';
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const headers = {
|
|
41
|
+
'User-Agent': 'Mozilla/5.0 (compatible; Awel/1.0)',
|
|
42
|
+
'Accept': acceptHeader,
|
|
43
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
44
|
+
};
|
|
45
|
+
let response = await fetch(url, { signal: controller.signal, headers });
|
|
46
|
+
// Retry with honest UA if Cloudflare blocks the request
|
|
47
|
+
if (response.status === 403 && response.headers.get('cf-mitigated') === 'challenge') {
|
|
48
|
+
response = await fetch(url, {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
headers: { ...headers, 'User-Agent': 'Awel' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
clearTimeout(timeoutId);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return `Error: Request failed with status ${response.status}`;
|
|
56
|
+
}
|
|
57
|
+
const contentLength = response.headers.get('content-length');
|
|
58
|
+
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
|
|
59
|
+
return 'Error: Response too large (exceeds 5MB limit)';
|
|
60
|
+
}
|
|
61
|
+
const buffer = await response.arrayBuffer();
|
|
62
|
+
if (buffer.byteLength > MAX_RESPONSE_SIZE) {
|
|
63
|
+
return 'Error: Response too large (exceeds 5MB limit)';
|
|
64
|
+
}
|
|
65
|
+
const content = new TextDecoder().decode(buffer);
|
|
66
|
+
const contentType = response.headers.get('content-type') || '';
|
|
67
|
+
const isHTML = contentType.includes('text/html');
|
|
68
|
+
switch (format) {
|
|
69
|
+
case 'markdown':
|
|
70
|
+
return isHTML ? convertHTMLToMarkdown(content) : content;
|
|
71
|
+
case 'text':
|
|
72
|
+
return isHTML ? stripHTMLTags(content) : content;
|
|
73
|
+
case 'html':
|
|
74
|
+
return content;
|
|
75
|
+
default:
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
82
|
+
return `Error: Request timed out after ${Math.round(timeoutMs / 1000)} seconds.`;
|
|
83
|
+
}
|
|
84
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function convertHTMLToMarkdown(html) {
|
|
90
|
+
const turndown = new TurndownService({
|
|
91
|
+
headingStyle: 'atx',
|
|
92
|
+
hr: '---',
|
|
93
|
+
bulletListMarker: '-',
|
|
94
|
+
codeBlockStyle: 'fenced',
|
|
95
|
+
emDelimiter: '*',
|
|
96
|
+
});
|
|
97
|
+
turndown.remove(['script', 'style', 'meta', 'link', 'noscript']);
|
|
98
|
+
return turndown.turndown(html);
|
|
99
|
+
}
|
|
100
|
+
function stripHTMLTags(html) {
|
|
101
|
+
// Remove script, style, and noscript blocks entirely
|
|
102
|
+
let text = html.replace(/<(script|style|noscript)[^>]*>[\s\S]*?<\/\1>/gi, '');
|
|
103
|
+
// Remove all remaining HTML tags
|
|
104
|
+
text = text.replace(/<[^>]+>/g, ' ');
|
|
105
|
+
// Decode common HTML entities
|
|
106
|
+
text = text
|
|
107
|
+
.replace(/&/g, '&')
|
|
108
|
+
.replace(/</g, '<')
|
|
109
|
+
.replace(/>/g, '>')
|
|
110
|
+
.replace(/"/g, '"')
|
|
111
|
+
.replace(/'/g, "'")
|
|
112
|
+
.replace(/ /g, ' ');
|
|
113
|
+
// Collapse whitespace
|
|
114
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const API_CONFIG = {
|
|
4
|
+
BASE_URL: 'https://mcp.exa.ai',
|
|
5
|
+
ENDPOINT: '/mcp',
|
|
6
|
+
DEFAULT_NUM_RESULTS: 8,
|
|
7
|
+
TIMEOUT_MS: 25_000,
|
|
8
|
+
};
|
|
9
|
+
export function createWebSearchTool() {
|
|
10
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
11
|
+
return tool({
|
|
12
|
+
description: `Search the web for real-time information. Use this to find current documentation, ` +
|
|
13
|
+
`look up error messages, find API references, or research libraries and tools. ` +
|
|
14
|
+
`Today's date is ${today}. Returns search results with page content.`,
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
query: z.string().describe('The search query'),
|
|
17
|
+
numResults: z.number().optional().default(8).describe('Number of results to return (default: 8)'),
|
|
18
|
+
type: z.enum(['auto', 'fast', 'deep']).optional().default('auto')
|
|
19
|
+
.describe('Search type: "auto" (balanced), "fast" (quick), or "deep" (comprehensive)'),
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ query, numResults, type }) => {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT_MS);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'accept': 'application/json, text/event-stream',
|
|
29
|
+
'content-type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
jsonrpc: '2.0',
|
|
33
|
+
id: 1,
|
|
34
|
+
method: 'tools/call',
|
|
35
|
+
params: {
|
|
36
|
+
name: 'web_search_exa',
|
|
37
|
+
arguments: {
|
|
38
|
+
query,
|
|
39
|
+
type: type || 'auto',
|
|
40
|
+
numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
|
|
41
|
+
livecrawl: 'fallback',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const errorText = await response.text();
|
|
50
|
+
return `Search error (${response.status}): ${errorText}`;
|
|
51
|
+
}
|
|
52
|
+
const responseText = await response.text();
|
|
53
|
+
// Parse SSE response — Exa returns data as Server-Sent Events
|
|
54
|
+
const lines = responseText.split('\n');
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
if (line.startsWith('data: ')) {
|
|
57
|
+
const data = JSON.parse(line.substring(6));
|
|
58
|
+
if (data.result?.content?.length > 0) {
|
|
59
|
+
return data.result.content[0].text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return 'No search results found. Try a different query.';
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
68
|
+
return 'Error: Search request timed out after 25 seconds.';
|
|
69
|
+
}
|
|
70
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|