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,17 @@
|
|
|
1
|
+
import type { SSEStreamingApi } from 'hono/streaming';
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
eventType: string;
|
|
5
|
+
data: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function addToHistory(eventType: string, data: string): void;
|
|
9
|
+
export declare function getHistory(): ChatMessage[];
|
|
10
|
+
export declare function clearHistory(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Helper to write an SSE event with a typed payload
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeSSEEvent(stream: SSEStreamingApi, event: string, payload: {
|
|
15
|
+
type: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}): Promise<void>;
|
package/dist/cli/sse.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// In-memory chat history (persists for the lifetime of the CLI process)
|
|
2
|
+
const chatHistory = [];
|
|
3
|
+
const MAX_HISTORY = 500; // Limit to prevent memory bloat
|
|
4
|
+
// Event types that should not be persisted in history
|
|
5
|
+
const TRANSIENT_EVENTS = new Set(['status', 'done']);
|
|
6
|
+
export function addToHistory(eventType, data) {
|
|
7
|
+
if (TRANSIENT_EVENTS.has(eventType))
|
|
8
|
+
return;
|
|
9
|
+
// Merge consecutive text events into a single history entry
|
|
10
|
+
if (eventType === 'text') {
|
|
11
|
+
const last = chatHistory[chatHistory.length - 1];
|
|
12
|
+
if (last && last.eventType === 'text') {
|
|
13
|
+
try {
|
|
14
|
+
const lastParsed = JSON.parse(last.data);
|
|
15
|
+
const newParsed = JSON.parse(data);
|
|
16
|
+
if (lastParsed.type === 'text' && newParsed.type === 'text') {
|
|
17
|
+
lastParsed.text = (lastParsed.text || '') + (newParsed.text || '');
|
|
18
|
+
last.data = JSON.stringify(lastParsed);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fall through to push as new entry
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
chatHistory.push({
|
|
28
|
+
id: crypto.randomUUID(),
|
|
29
|
+
eventType,
|
|
30
|
+
data,
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
});
|
|
33
|
+
// Trim old messages if we exceed the limit
|
|
34
|
+
if (chatHistory.length > MAX_HISTORY) {
|
|
35
|
+
chatHistory.splice(0, chatHistory.length - MAX_HISTORY);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function getHistory() {
|
|
39
|
+
return [...chatHistory];
|
|
40
|
+
}
|
|
41
|
+
export function clearHistory() {
|
|
42
|
+
chatHistory.length = 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Helper to write an SSE event with a typed payload
|
|
46
|
+
*/
|
|
47
|
+
export async function writeSSEEvent(stream, event, payload) {
|
|
48
|
+
const data = JSON.stringify(payload);
|
|
49
|
+
addToHistory(event, data);
|
|
50
|
+
await stream.writeSSE({ event, data });
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess manager singleton for the user's dev server.
|
|
3
|
+
* Handles spawning, restarting, health checks, and auto-restart on crash.
|
|
4
|
+
*/
|
|
5
|
+
export type DevServerStatus = 'stopped' | 'starting' | 'running' | 'restarting' | 'crashed';
|
|
6
|
+
/**
|
|
7
|
+
* Spawn the dev server. Called once from CLI entry point.
|
|
8
|
+
*/
|
|
9
|
+
export declare function spawnDevServer(opts: {
|
|
10
|
+
port: number;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Restart the dev server. Sends SIGTERM, waits for exit, then spawns a new one.
|
|
15
|
+
*/
|
|
16
|
+
export declare function restartDevServer(): Promise<{
|
|
17
|
+
success: boolean;
|
|
18
|
+
message: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Get the current status of the dev server.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getDevServerStatus(): {
|
|
24
|
+
status: DevServerStatus;
|
|
25
|
+
port: number;
|
|
26
|
+
startedAt: number | null;
|
|
27
|
+
restartCount: number;
|
|
28
|
+
lastError: string | null;
|
|
29
|
+
pid: number | undefined;
|
|
30
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess manager singleton for the user's dev server.
|
|
3
|
+
* Handles spawning, restarting, health checks, and auto-restart on crash.
|
|
4
|
+
*/
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { awel, pipeChildOutput } from './logger.js';
|
|
7
|
+
const state = {
|
|
8
|
+
process: null,
|
|
9
|
+
status: 'stopped',
|
|
10
|
+
port: 3000,
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
startedAt: null,
|
|
13
|
+
restartCount: 0,
|
|
14
|
+
lastError: null,
|
|
15
|
+
};
|
|
16
|
+
let autoRestartEnabled = true;
|
|
17
|
+
/**
|
|
18
|
+
* Wait for the dev server to respond on its port.
|
|
19
|
+
* Polls with 500ms intervals up to the timeout.
|
|
20
|
+
*/
|
|
21
|
+
async function waitForServer(port, timeoutMs = 30_000) {
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
while (Date.now() - start < timeoutMs) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`http://localhost:${port}`, {
|
|
26
|
+
signal: AbortSignal.timeout(2000),
|
|
27
|
+
});
|
|
28
|
+
// Any response (even 500) means the server is up
|
|
29
|
+
if (res.status)
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not ready yet
|
|
34
|
+
}
|
|
35
|
+
await new Promise(r => setTimeout(r, 500));
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Spawn the dev server process and pipe its output.
|
|
41
|
+
*/
|
|
42
|
+
function spawn(port, cwd) {
|
|
43
|
+
const child = execa('npm', ['run', 'dev'], {
|
|
44
|
+
stdin: 'inherit',
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
PORT: String(port),
|
|
48
|
+
},
|
|
49
|
+
cwd,
|
|
50
|
+
});
|
|
51
|
+
pipeChildOutput(child);
|
|
52
|
+
return child;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compute exponential backoff delay for auto-restart.
|
|
56
|
+
*/
|
|
57
|
+
function getBackoffDelay(restartCount) {
|
|
58
|
+
const delay = Math.min(1000 * Math.pow(2, restartCount - 1), 10_000);
|
|
59
|
+
return delay;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Handle process exit — auto-restart with exponential backoff if enabled.
|
|
63
|
+
*/
|
|
64
|
+
function attachExitHandler(child) {
|
|
65
|
+
child.catch(async (error) => {
|
|
66
|
+
// Don't auto-restart if we're intentionally stopping or restarting
|
|
67
|
+
if (state.status === 'restarting' || state.status === 'stopped')
|
|
68
|
+
return;
|
|
69
|
+
state.status = 'crashed';
|
|
70
|
+
state.lastError = error instanceof Error ? error.message : String(error);
|
|
71
|
+
awel.error(`Dev server crashed: ${state.lastError}`);
|
|
72
|
+
if (!autoRestartEnabled)
|
|
73
|
+
return;
|
|
74
|
+
state.restartCount++;
|
|
75
|
+
const delay = getBackoffDelay(state.restartCount);
|
|
76
|
+
awel.log(`Auto-restarting dev server in ${delay}ms (attempt ${state.restartCount})...`);
|
|
77
|
+
await new Promise(r => setTimeout(r, delay));
|
|
78
|
+
// Check we're still in crashed state (user may have manually restarted)
|
|
79
|
+
if (state.status !== 'crashed')
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
await doSpawn();
|
|
83
|
+
awel.log('Dev server auto-restarted successfully.');
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
awel.error(`Auto-restart failed: ${msg}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Internal spawn + health-check routine.
|
|
93
|
+
*/
|
|
94
|
+
async function doSpawn() {
|
|
95
|
+
state.status = 'starting';
|
|
96
|
+
const child = spawn(state.port, state.cwd);
|
|
97
|
+
state.process = child;
|
|
98
|
+
attachExitHandler(child);
|
|
99
|
+
const ready = await waitForServer(state.port);
|
|
100
|
+
if (ready) {
|
|
101
|
+
state.status = 'running';
|
|
102
|
+
state.startedAt = Date.now();
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
awel.error('Dev server did not respond within 30s — it may still be starting.');
|
|
106
|
+
// Keep it as 'starting'; the exit handler will catch a crash if it happens
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Spawn the dev server. Called once from CLI entry point.
|
|
111
|
+
*/
|
|
112
|
+
export async function spawnDevServer(opts) {
|
|
113
|
+
state.port = opts.port;
|
|
114
|
+
state.cwd = opts.cwd;
|
|
115
|
+
state.restartCount = 0;
|
|
116
|
+
awel.log('Starting your Next.js app...');
|
|
117
|
+
await doSpawn();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Restart the dev server. Sends SIGTERM, waits for exit, then spawns a new one.
|
|
121
|
+
*/
|
|
122
|
+
export async function restartDevServer() {
|
|
123
|
+
if (!state.process) {
|
|
124
|
+
return { success: false, message: 'No dev server process to restart.' };
|
|
125
|
+
}
|
|
126
|
+
state.status = 'restarting';
|
|
127
|
+
awel.log('Restarting dev server...');
|
|
128
|
+
// Kill current process
|
|
129
|
+
try {
|
|
130
|
+
state.process.kill('SIGTERM');
|
|
131
|
+
// Wait up to 5s for graceful shutdown
|
|
132
|
+
await Promise.race([
|
|
133
|
+
state.process.catch(() => { }),
|
|
134
|
+
new Promise(r => setTimeout(r, 5000)),
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Process may already be dead
|
|
139
|
+
}
|
|
140
|
+
state.process = null;
|
|
141
|
+
state.restartCount = 0;
|
|
142
|
+
try {
|
|
143
|
+
await doSpawn();
|
|
144
|
+
return { success: true, message: 'Dev server restarted successfully.' };
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
return { success: false, message: `Restart failed: ${msg}` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get the current status of the dev server.
|
|
153
|
+
*/
|
|
154
|
+
export function getDevServerStatus() {
|
|
155
|
+
return {
|
|
156
|
+
status: state.status,
|
|
157
|
+
port: state.port,
|
|
158
|
+
startedAt: state.startedAt,
|
|
159
|
+
restartCount: state.restartCount,
|
|
160
|
+
lastError: state.lastError,
|
|
161
|
+
pid: state.process?.pid,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export function createAskUserTool() {
|
|
4
|
+
return tool({
|
|
5
|
+
description: 'Ask the user clarifying questions before proceeding with a task. ' +
|
|
6
|
+
'Use this when you need to gather preferences, clarify ambiguous requirements, ' +
|
|
7
|
+
'or let the user choose between approaches. Present 1-4 questions, each with 2-4 options. ' +
|
|
8
|
+
'Set multiSelect to true when choices are not mutually exclusive. ' +
|
|
9
|
+
'After asking, STOP and wait for the user to respond before continuing. ' +
|
|
10
|
+
'IMPORTANT: All string fields must be plain text — no markdown, no bullet points, no bold/italic, no code blocks. ' +
|
|
11
|
+
'The UI renders these strings directly in a structured card layout.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
questions: z.array(z.object({
|
|
14
|
+
question: z.string().describe('The full question text. Plain text only, no markdown.'),
|
|
15
|
+
header: z.string().describe('Short tab label (max 12 chars). Plain text only, e.g. "Framework" or "Auth method".'),
|
|
16
|
+
multiSelect: z.boolean().describe('Whether the user can select multiple options'),
|
|
17
|
+
options: z.array(z.object({
|
|
18
|
+
label: z.string().describe('Short option name (1-5 words). Plain text only, no markdown.'),
|
|
19
|
+
description: z.string().describe('One sentence explaining this option. Plain text only, no markdown.'),
|
|
20
|
+
})).min(2).max(4),
|
|
21
|
+
})).min(1).max(4),
|
|
22
|
+
}),
|
|
23
|
+
execute: async (input) => {
|
|
24
|
+
// Actual interception happens in the streaming loop (vercel.ts).
|
|
25
|
+
return JSON.stringify(input);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
export function createBashTool(cwd) {
|
|
5
|
+
return tool({
|
|
6
|
+
description: 'Execute a shell command and return stdout/stderr. Runs in the project directory.',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
command: z.string().describe('The shell command to execute'),
|
|
9
|
+
timeout: z.number().optional().default(30000).describe('Timeout in milliseconds (default 30s)'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ command, timeout }) => {
|
|
12
|
+
try {
|
|
13
|
+
const output = execSync(command, {
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: 'utf-8',
|
|
16
|
+
timeout,
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
19
|
+
});
|
|
20
|
+
return output || '(no output)';
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const execErr = err;
|
|
24
|
+
const stderr = execErr.stderr || '';
|
|
25
|
+
const stdout = execErr.stdout || '';
|
|
26
|
+
return `Error: ${execErr.message || 'Command failed'}\n${stderr}\n${stdout}`.trim();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
TIMEOUT_MS: 30_000,
|
|
7
|
+
DEFAULT_TOKENS: 5_000,
|
|
8
|
+
};
|
|
9
|
+
export function createCodeSearchTool() {
|
|
10
|
+
return tool({
|
|
11
|
+
description: 'Search the web for code examples, API documentation, and SDK references. ' +
|
|
12
|
+
'Use this when you need to look up how to use a library, find code patterns, ' +
|
|
13
|
+
'or check API usage. Returns code-focused results from documentation and repositories.',
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
query: z.string().describe('Search query for code examples or documentation (e.g. "Next.js App Router middleware example", "zod union type validation")'),
|
|
16
|
+
tokensNum: z.number().optional().default(5000)
|
|
17
|
+
.describe('Response length control: 1000-50000 tokens (default: 5000). Use higher values for detailed API docs.'),
|
|
18
|
+
}),
|
|
19
|
+
execute: async ({ query, tokensNum }) => {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT_MS);
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'accept': 'application/json, text/event-stream',
|
|
27
|
+
'content-type': 'application/json',
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
jsonrpc: '2.0',
|
|
31
|
+
id: 1,
|
|
32
|
+
method: 'tools/call',
|
|
33
|
+
params: {
|
|
34
|
+
name: 'codedocs',
|
|
35
|
+
arguments: {
|
|
36
|
+
query,
|
|
37
|
+
tokensNum: Math.min(Math.max(tokensNum || API_CONFIG.DEFAULT_TOKENS, 1000), 50000),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const errorText = await response.text();
|
|
46
|
+
return `Search error (${response.status}): ${errorText}`;
|
|
47
|
+
}
|
|
48
|
+
const responseText = await response.text();
|
|
49
|
+
// Parse SSE response
|
|
50
|
+
const lines = responseText.split('\n');
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (line.startsWith('data: ')) {
|
|
53
|
+
const data = JSON.parse(line.substring(6));
|
|
54
|
+
if (data.result?.content?.length > 0) {
|
|
55
|
+
return data.result.content[0].text;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 'No code examples found. Try a more specific query.';
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
64
|
+
return 'Error: Code search request timed out after 30 seconds.';
|
|
65
|
+
}
|
|
66
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 createEditTool(cwd) {
|
|
7
|
+
return tool({
|
|
8
|
+
description: 'Perform a find-and-replace edit on a file. Replaces the first occurrence of old_string with new_string. Use replace_all to replace every occurrence.',
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
file_path: z.string().describe('The path to the file to edit (absolute or relative to project root)'),
|
|
11
|
+
old_string: z.string().describe('The exact string to find in the file'),
|
|
12
|
+
new_string: z.string().describe('The replacement string'),
|
|
13
|
+
replace_all: z.boolean().optional().default(false).describe('Replace all occurrences instead of just the first'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async ({ file_path, old_string, new_string, replace_all }) => {
|
|
16
|
+
const fullPath = file_path.startsWith('/') ? file_path : resolve(cwd, file_path);
|
|
17
|
+
if (!existsSync(fullPath)) {
|
|
18
|
+
return `Error: File not found: ${fullPath}`;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
pushSnapshot(fullPath);
|
|
22
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
23
|
+
if (!content.includes(old_string)) {
|
|
24
|
+
return `Error: old_string not found in ${file_path}`;
|
|
25
|
+
}
|
|
26
|
+
const updated = replace_all
|
|
27
|
+
? content.replaceAll(old_string, new_string)
|
|
28
|
+
: content.replace(old_string, new_string);
|
|
29
|
+
writeFileSync(fullPath, updated, 'utf-8');
|
|
30
|
+
return `Successfully edited ${file_path}`;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return `Error editing file: ${err instanceof Error ? err.message : String(err)}`;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
export function createGlobTool(cwd) {
|
|
5
|
+
return tool({
|
|
6
|
+
description: 'Find files matching a glob pattern. Returns a list of matching file paths relative to the project root.',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
pattern: z.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.tsx")'),
|
|
9
|
+
path: z.string().optional().describe('Directory to search in (default: project root)'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ pattern, path }) => {
|
|
12
|
+
try {
|
|
13
|
+
const searchDir = path || cwd;
|
|
14
|
+
const files = await fg(pattern, {
|
|
15
|
+
cwd: searchDir,
|
|
16
|
+
dot: false,
|
|
17
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
18
|
+
});
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
return 'No files matched the pattern.';
|
|
21
|
+
}
|
|
22
|
+
return files.join('\n');
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve, relative } from 'path';
|
|
6
|
+
import fg from 'fast-glob';
|
|
7
|
+
const MAX_RESULTS = 100;
|
|
8
|
+
const MAX_LINE_LENGTH = 2000;
|
|
9
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
10
|
+
/** Check once at startup whether ripgrep is available. */
|
|
11
|
+
let rgAvailable = null;
|
|
12
|
+
function hasRipgrep() {
|
|
13
|
+
if (rgAvailable === null) {
|
|
14
|
+
try {
|
|
15
|
+
execSync('rg --version', { stdio: 'pipe', timeout: 3000 });
|
|
16
|
+
rgAvailable = true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
rgAvailable = false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return rgAvailable;
|
|
23
|
+
}
|
|
24
|
+
export function createGrepTool(cwd) {
|
|
25
|
+
return tool({
|
|
26
|
+
description: 'Search file contents for a regex pattern. ' +
|
|
27
|
+
'Returns matching lines with file paths and line numbers. ' +
|
|
28
|
+
'Use this to find where functions are defined, where variables are used, ' +
|
|
29
|
+
'or to locate specific strings across the codebase.',
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
pattern: z.string().describe('Regex pattern to search for (e.g. "function\\s+handleSubmit", "TODO", "import.*from")'),
|
|
32
|
+
path: z.string().optional().describe('Directory or file to search in (default: project root)'),
|
|
33
|
+
include: z.string().optional().describe('File glob filter (e.g. "*.ts", "*.{js,jsx,ts,tsx}")'),
|
|
34
|
+
}),
|
|
35
|
+
execute: async ({ pattern, path, include }) => {
|
|
36
|
+
const searchPath = path
|
|
37
|
+
? (path.startsWith('/') ? path : resolve(cwd, path))
|
|
38
|
+
: cwd;
|
|
39
|
+
if (hasRipgrep()) {
|
|
40
|
+
return searchWithRipgrep(cwd, pattern, searchPath, include);
|
|
41
|
+
}
|
|
42
|
+
return searchWithNode(cwd, pattern, searchPath, include);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function searchWithRipgrep(cwd, pattern, searchPath, include) {
|
|
47
|
+
const args = [
|
|
48
|
+
'--no-heading',
|
|
49
|
+
'--line-number',
|
|
50
|
+
'--color=never',
|
|
51
|
+
'--max-count=5',
|
|
52
|
+
'--hidden',
|
|
53
|
+
'--follow',
|
|
54
|
+
...IGNORE_DIRS.map(d => `--glob=!${d}`),
|
|
55
|
+
];
|
|
56
|
+
if (include) {
|
|
57
|
+
args.push(`--glob=${include}`);
|
|
58
|
+
}
|
|
59
|
+
args.push('--', pattern, searchPath);
|
|
60
|
+
try {
|
|
61
|
+
const output = execSync(`rg ${args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
timeout: 15_000,
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
67
|
+
});
|
|
68
|
+
return formatOutput(output);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const execErr = err;
|
|
72
|
+
if (execErr.status === 1) {
|
|
73
|
+
return 'No matches found.';
|
|
74
|
+
}
|
|
75
|
+
return `Error: ${execErr.stderr || execErr.message || 'Search failed'}`.trim();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function searchWithNode(cwd, pattern, searchPath, include) {
|
|
79
|
+
let regex;
|
|
80
|
+
try {
|
|
81
|
+
regex = new RegExp(pattern);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return `Error: Invalid regex pattern: ${pattern}`;
|
|
85
|
+
}
|
|
86
|
+
const globPattern = include || '**/*';
|
|
87
|
+
const ignore = IGNORE_DIRS.map(d => `**/${d}/**`);
|
|
88
|
+
let files;
|
|
89
|
+
try {
|
|
90
|
+
files = await fg(globPattern, {
|
|
91
|
+
cwd: searchPath,
|
|
92
|
+
dot: true,
|
|
93
|
+
ignore,
|
|
94
|
+
followSymbolicLinks: true,
|
|
95
|
+
onlyFiles: true,
|
|
96
|
+
absolute: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return 'Error: Failed to list files for search.';
|
|
101
|
+
}
|
|
102
|
+
const matches = [];
|
|
103
|
+
let matchCount = 0;
|
|
104
|
+
for (const filePath of files) {
|
|
105
|
+
if (matchCount >= MAX_RESULTS)
|
|
106
|
+
break;
|
|
107
|
+
let content;
|
|
108
|
+
try {
|
|
109
|
+
content = readFileSync(filePath, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue; // skip binary / unreadable files
|
|
113
|
+
}
|
|
114
|
+
// Quick binary check: skip files with null bytes
|
|
115
|
+
if (content.includes('\0'))
|
|
116
|
+
continue;
|
|
117
|
+
const lines = content.split('\n');
|
|
118
|
+
let fileMatches = 0;
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
if (fileMatches >= 5 || matchCount >= MAX_RESULTS)
|
|
121
|
+
break;
|
|
122
|
+
if (regex.test(lines[i])) {
|
|
123
|
+
const rel = relative(cwd, filePath);
|
|
124
|
+
const line = lines[i].length > MAX_LINE_LENGTH
|
|
125
|
+
? lines[i].slice(0, MAX_LINE_LENGTH) + '...'
|
|
126
|
+
: lines[i];
|
|
127
|
+
matches.push(`${rel}:${i + 1}:${line}`);
|
|
128
|
+
fileMatches++;
|
|
129
|
+
matchCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (matches.length === 0) {
|
|
134
|
+
return 'No matches found.';
|
|
135
|
+
}
|
|
136
|
+
return matches.join('\n');
|
|
137
|
+
}
|
|
138
|
+
function formatOutput(output) {
|
|
139
|
+
const lines = output.split('\n').filter(Boolean);
|
|
140
|
+
const truncated = lines.slice(0, MAX_RESULTS).map(line => line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + '...' : line);
|
|
141
|
+
const result = truncated.join('\n');
|
|
142
|
+
const suffix = lines.length > MAX_RESULTS
|
|
143
|
+
? `\n\n(showing ${MAX_RESULTS} of ${lines.length} matches)`
|
|
144
|
+
: '';
|
|
145
|
+
return result + suffix || 'No matches found.';
|
|
146
|
+
}
|