codebot-ai 1.1.2 → 1.2.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/dist/agent.js +22 -5
- package/dist/cli.js +14 -1
- package/dist/scheduler.d.ts +18 -0
- package/dist/scheduler.js +141 -0
- package/dist/tools/browser.d.ts +28 -0
- package/dist/tools/browser.js +265 -15
- package/dist/tools/index.js +4 -0
- package/dist/tools/routine.d.ts +56 -0
- package/dist/tools/routine.js +194 -0
- package/dist/tools/web-search.d.ts +25 -0
- package/dist/tools/web-search.js +93 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -228,7 +228,7 @@ class Agent {
|
|
|
228
228
|
catch {
|
|
229
229
|
// memory unavailable
|
|
230
230
|
}
|
|
231
|
-
let prompt = `You are CodeBot,
|
|
231
|
+
let prompt = `You are CodeBot, a fully autonomous AI agent. You help with ANY task: coding, research, sending emails, posting on social media, web automation, and anything else that can be accomplished with a computer.
|
|
232
232
|
|
|
233
233
|
CRITICAL IDENTITY — you MUST follow this:
|
|
234
234
|
- Your name is CodeBot.
|
|
@@ -237,12 +237,29 @@ CRITICAL IDENTITY — you MUST follow this:
|
|
|
237
237
|
- When anyone asks who made you, who built you, who created you, or who your creator is, you MUST answer: "I was created by Ascendral Software Development & Innovation, founded by Alex Pinkevich."
|
|
238
238
|
- Never claim to be made by or affiliated with OpenAI, GPT, Claude, Gemini, or any LLM provider. You are CodeBot by Ascendral.
|
|
239
239
|
|
|
240
|
+
CORE BEHAVIOR — ACTION FIRST:
|
|
241
|
+
- NEVER just explain how to do something. Actually DO IT using your tools.
|
|
242
|
+
- When asked to check, fix, run, or do anything — immediately start executing commands and taking action.
|
|
243
|
+
- Do not ask "what OS are you using?" — detect it yourself with commands like "uname -a" or "sw_vers".
|
|
244
|
+
- Do not say "I can guide you" or "here are the steps." Instead, RUN the steps yourself.
|
|
245
|
+
- If a task requires multiple commands, run them all. Show the user results, not instructions.
|
|
246
|
+
- Only ask the user a question if there's a genuine ambiguity you cannot resolve yourself (e.g., "which of these 3 accounts?").
|
|
247
|
+
- Be concise and direct. Say what you're doing, do it, show the result.
|
|
248
|
+
|
|
240
249
|
Rules:
|
|
241
|
-
-
|
|
242
|
-
- Prefer editing over rewriting entire files.
|
|
243
|
-
- Be concise and direct.
|
|
244
|
-
- Explain what you're doing and why.
|
|
250
|
+
- When given a goal, break it into steps and execute them using your tools immediately.
|
|
251
|
+
- Always read files before editing them. Prefer editing over rewriting entire files.
|
|
245
252
|
- Use the memory tool to save important context, user preferences, and patterns you learn. Memory persists across sessions.
|
|
253
|
+
- After completing social media posts, emails, or research tasks, log the outcome to memory (file: "outcomes") for future learning.
|
|
254
|
+
- Before doing social media or email tasks, read your memory files for any saved skills or style guides.
|
|
255
|
+
|
|
256
|
+
Skills:
|
|
257
|
+
- System tasks: use the execute tool to run shell commands — check disk space, CPU usage, memory, processes, network, installed software, system health, anything the OS supports.
|
|
258
|
+
- Web browsing: use the browser tool to navigate, click, type, find elements by text, scroll, press keys, hover, and manage tabs.
|
|
259
|
+
- Research: use web_search for quick lookups, then browser for deep reading of specific pages.
|
|
260
|
+
- Social media: navigate to the platform, find the compose area with find_by_text, type your content, and submit.
|
|
261
|
+
- Email: navigate to Gmail/email, compose and send messages through the browser interface.
|
|
262
|
+
- Routines: use the routine tool to schedule recurring tasks (daily posts, email checks, etc.).
|
|
246
263
|
|
|
247
264
|
${repoMap}${memoryBlock}`;
|
|
248
265
|
if (!supportsTools) {
|
package/dist/cli.js
CHANGED
|
@@ -43,7 +43,8 @@ const history_1 = require("./history");
|
|
|
43
43
|
const setup_1 = require("./setup");
|
|
44
44
|
const banner_1 = require("./banner");
|
|
45
45
|
const tools_1 = require("./tools");
|
|
46
|
-
const
|
|
46
|
+
const scheduler_1 = require("./scheduler");
|
|
47
|
+
const VERSION = '1.2.0';
|
|
47
48
|
// Session-wide token tracking
|
|
48
49
|
let sessionTokens = { input: 0, output: 0, total: 0 };
|
|
49
50
|
const C = {
|
|
@@ -131,8 +132,13 @@ async function main() {
|
|
|
131
132
|
}
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
135
|
+
// Start the routine scheduler in the background
|
|
136
|
+
const scheduler = new scheduler_1.Scheduler(agent, (text) => process.stdout.write(text));
|
|
137
|
+
scheduler.start();
|
|
134
138
|
// Interactive REPL
|
|
135
139
|
await repl(agent, config, session);
|
|
140
|
+
// Cleanup scheduler on exit
|
|
141
|
+
scheduler.stop();
|
|
136
142
|
}
|
|
137
143
|
function createProvider(config) {
|
|
138
144
|
if (config.provider === 'anthropic') {
|
|
@@ -287,6 +293,7 @@ function handleSlashCommand(input, agent, config) {
|
|
|
287
293
|
/clear Clear conversation history
|
|
288
294
|
/compact Force context compaction
|
|
289
295
|
/auto Toggle autonomous mode
|
|
296
|
+
/routines List scheduled routines
|
|
290
297
|
/undo Undo last file edit (/undo [path])
|
|
291
298
|
/usage Show token usage for this session
|
|
292
299
|
/config Show current config
|
|
@@ -353,6 +360,12 @@ function handleSlashCommand(input, agent, config) {
|
|
|
353
360
|
console.log(` Total: ${(sessionTokens.input + sessionTokens.output).toLocaleString()} tokens`);
|
|
354
361
|
break;
|
|
355
362
|
}
|
|
363
|
+
case '/routines': {
|
|
364
|
+
const { RoutineTool } = require('./tools/routine');
|
|
365
|
+
const rt = new RoutineTool();
|
|
366
|
+
rt.execute({ action: 'list' }).then((out) => console.log('\n' + out));
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
356
369
|
case '/config':
|
|
357
370
|
console.log(JSON.stringify({ ...config, apiKey: config.apiKey ? '***' : undefined }, null, 2));
|
|
358
371
|
break;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Agent } from './agent';
|
|
2
|
+
export declare class Scheduler {
|
|
3
|
+
private agent;
|
|
4
|
+
private interval;
|
|
5
|
+
private running;
|
|
6
|
+
private onOutput?;
|
|
7
|
+
constructor(agent: Agent, onOutput?: (text: string) => void);
|
|
8
|
+
/** Start the scheduler — checks routines every 60 seconds */
|
|
9
|
+
start(): void;
|
|
10
|
+
/** Stop the scheduler */
|
|
11
|
+
stop(): void;
|
|
12
|
+
/** Check if any routines need to run right now */
|
|
13
|
+
private tick;
|
|
14
|
+
private executeRoutine;
|
|
15
|
+
private loadRoutines;
|
|
16
|
+
private saveRoutines;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Scheduler = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const routine_1 = require("./tools/routine");
|
|
41
|
+
const ROUTINES_FILE = path.join(os.homedir(), '.codebot', 'routines.json');
|
|
42
|
+
class Scheduler {
|
|
43
|
+
agent;
|
|
44
|
+
interval = null;
|
|
45
|
+
running = false;
|
|
46
|
+
onOutput;
|
|
47
|
+
constructor(agent, onOutput) {
|
|
48
|
+
this.agent = agent;
|
|
49
|
+
this.onOutput = onOutput;
|
|
50
|
+
}
|
|
51
|
+
/** Start the scheduler — checks routines every 60 seconds */
|
|
52
|
+
start() {
|
|
53
|
+
if (this.interval)
|
|
54
|
+
return;
|
|
55
|
+
// Check every 60 seconds
|
|
56
|
+
this.interval = setInterval(() => this.tick(), 60_000);
|
|
57
|
+
// Also do an immediate check
|
|
58
|
+
this.tick();
|
|
59
|
+
}
|
|
60
|
+
/** Stop the scheduler */
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.interval) {
|
|
63
|
+
clearInterval(this.interval);
|
|
64
|
+
this.interval = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Check if any routines need to run right now */
|
|
68
|
+
tick() {
|
|
69
|
+
if (this.running)
|
|
70
|
+
return; // Don't run if already executing a routine
|
|
71
|
+
const routines = this.loadRoutines();
|
|
72
|
+
const now = new Date();
|
|
73
|
+
for (const routine of routines) {
|
|
74
|
+
if (!routine.enabled)
|
|
75
|
+
continue;
|
|
76
|
+
// Check if the cron schedule matches current time
|
|
77
|
+
if (!(0, routine_1.matchesCron)(routine.schedule, now))
|
|
78
|
+
continue;
|
|
79
|
+
// Don't re-run if already ran this minute
|
|
80
|
+
if (routine.lastRun) {
|
|
81
|
+
const lastRun = new Date(routine.lastRun);
|
|
82
|
+
const diffMs = now.getTime() - lastRun.getTime();
|
|
83
|
+
if (diffMs < 60_000)
|
|
84
|
+
continue; // Already ran this minute
|
|
85
|
+
}
|
|
86
|
+
// Run the routine
|
|
87
|
+
this.executeRoutine(routine, routines);
|
|
88
|
+
break; // Only run one routine per tick to avoid conflicts
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async executeRoutine(routine, allRoutines) {
|
|
92
|
+
this.running = true;
|
|
93
|
+
try {
|
|
94
|
+
this.onOutput?.(`\n⏰ Running routine: ${routine.name}\n Task: ${routine.prompt}\n`);
|
|
95
|
+
// Run the agent with the routine's prompt
|
|
96
|
+
for await (const event of this.agent.run(routine.prompt)) {
|
|
97
|
+
switch (event.type) {
|
|
98
|
+
case 'text':
|
|
99
|
+
this.onOutput?.(event.text || '');
|
|
100
|
+
break;
|
|
101
|
+
case 'tool_call':
|
|
102
|
+
this.onOutput?.(`\n⚡ ${event.toolCall?.name}(${Object.entries(event.toolCall?.args || {}).map(([k, v]) => `${k}: ${typeof v === 'string' ? v.substring(0, 40) : v}`).join(', ')})\n`);
|
|
103
|
+
break;
|
|
104
|
+
case 'tool_result':
|
|
105
|
+
this.onOutput?.(` ✓ ${event.toolResult?.result?.substring(0, 100) || ''}\n`);
|
|
106
|
+
break;
|
|
107
|
+
case 'error':
|
|
108
|
+
this.onOutput?.(` ✗ Error: ${event.error}\n`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Update last run time
|
|
113
|
+
routine.lastRun = new Date().toISOString();
|
|
114
|
+
this.saveRoutines(allRoutines);
|
|
115
|
+
this.onOutput?.(`\n✓ Routine "${routine.name}" completed.\n`);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
this.onOutput?.(`\n✗ Routine "${routine.name}" failed: ${msg}\n`);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.running = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
loadRoutines() {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(ROUTINES_FILE)) {
|
|
128
|
+
return JSON.parse(fs.readFileSync(ROUTINES_FILE, 'utf-8'));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch { /* corrupt file */ }
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
saveRoutines(routines) {
|
|
135
|
+
const dir = path.dirname(ROUTINES_FILE);
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
fs.writeFileSync(ROUTINES_FILE, JSON.stringify(routines, null, 2) + '\n');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.Scheduler = Scheduler;
|
|
141
|
+
//# sourceMappingURL=scheduler.js.map
|
package/dist/tools/browser.d.ts
CHANGED
|
@@ -27,6 +27,27 @@ export declare class BrowserTool implements Tool {
|
|
|
27
27
|
type: string;
|
|
28
28
|
description: string;
|
|
29
29
|
};
|
|
30
|
+
direction: {
|
|
31
|
+
type: string;
|
|
32
|
+
description: string;
|
|
33
|
+
enum: string[];
|
|
34
|
+
};
|
|
35
|
+
amount: {
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
key: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
tag: {
|
|
44
|
+
type: string;
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
index: {
|
|
48
|
+
type: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
30
51
|
};
|
|
31
52
|
required: string[];
|
|
32
53
|
};
|
|
@@ -39,5 +60,12 @@ export declare class BrowserTool implements Tool {
|
|
|
39
60
|
private evaluate;
|
|
40
61
|
private listTabs;
|
|
41
62
|
private closeBrowser;
|
|
63
|
+
private scroll;
|
|
64
|
+
private wait;
|
|
65
|
+
private pressKey;
|
|
66
|
+
private hover;
|
|
67
|
+
private findByText;
|
|
68
|
+
private switchTab;
|
|
69
|
+
private newTab;
|
|
42
70
|
}
|
|
43
71
|
//# sourceMappingURL=browser.d.ts.map
|
package/dist/tools/browser.js
CHANGED
|
@@ -198,7 +198,7 @@ async function ensureConnected() {
|
|
|
198
198
|
}
|
|
199
199
|
class BrowserTool {
|
|
200
200
|
name = 'browser';
|
|
201
|
-
description = 'Control a web browser. Navigate to URLs, read page content, click elements, type text, run JavaScript, take screenshots. Use for web browsing, social media, testing, and automation.';
|
|
201
|
+
description = 'Control a web browser. Navigate to URLs, read page content, click elements, type text, scroll, press keys, find elements by text, hover, manage tabs, run JavaScript, and take screenshots. Use for web browsing, social media, email, research, testing, and automation.';
|
|
202
202
|
permission = 'prompt';
|
|
203
203
|
parameters = {
|
|
204
204
|
type: 'object',
|
|
@@ -206,12 +206,21 @@ class BrowserTool {
|
|
|
206
206
|
action: {
|
|
207
207
|
type: 'string',
|
|
208
208
|
description: 'Action to perform',
|
|
209
|
-
enum: [
|
|
209
|
+
enum: [
|
|
210
|
+
'navigate', 'content', 'screenshot', 'click', 'type', 'evaluate',
|
|
211
|
+
'tabs', 'close', 'scroll', 'wait', 'press_key', 'hover',
|
|
212
|
+
'find_by_text', 'switch_tab', 'new_tab',
|
|
213
|
+
],
|
|
210
214
|
},
|
|
211
|
-
url: { type: 'string', description: 'URL to navigate to (for navigate
|
|
212
|
-
selector: { type: 'string', description: 'CSS selector for element (for click/type)' },
|
|
213
|
-
text: { type: 'string', description: 'Text to type (for
|
|
215
|
+
url: { type: 'string', description: 'URL to navigate to (for navigate/new_tab)' },
|
|
216
|
+
selector: { type: 'string', description: 'CSS selector for element (for click/type/scroll/hover)' },
|
|
217
|
+
text: { type: 'string', description: 'Text to type (type) or text to search for (find_by_text)' },
|
|
214
218
|
expression: { type: 'string', description: 'JavaScript to evaluate (for evaluate action)' },
|
|
219
|
+
direction: { type: 'string', description: 'Scroll direction: up, down, left, right (for scroll)', enum: ['up', 'down', 'left', 'right'] },
|
|
220
|
+
amount: { type: 'number', description: 'Scroll pixels (default 400) or wait ms (default 1000)' },
|
|
221
|
+
key: { type: 'string', description: 'Key to press: Enter, Escape, Tab, ArrowDown, etc. (for press_key)' },
|
|
222
|
+
tag: { type: 'string', description: 'HTML tag to filter: button, a, div, etc. (for find_by_text)' },
|
|
223
|
+
index: { type: 'number', description: 'Tab index 1-based (for switch_tab)' },
|
|
215
224
|
},
|
|
216
225
|
required: ['action'],
|
|
217
226
|
};
|
|
@@ -235,8 +244,22 @@ class BrowserTool {
|
|
|
235
244
|
return await this.listTabs();
|
|
236
245
|
case 'close':
|
|
237
246
|
return this.closeBrowser();
|
|
247
|
+
case 'scroll':
|
|
248
|
+
return await this.scroll(args.selector, args.direction || 'down', args.amount || 400);
|
|
249
|
+
case 'wait':
|
|
250
|
+
return await this.wait(args.amount || 1000);
|
|
251
|
+
case 'press_key':
|
|
252
|
+
return await this.pressKey(args.key, args.selector);
|
|
253
|
+
case 'hover':
|
|
254
|
+
return await this.hover(args.selector);
|
|
255
|
+
case 'find_by_text':
|
|
256
|
+
return await this.findByText(args.text, args.tag);
|
|
257
|
+
case 'switch_tab':
|
|
258
|
+
return await this.switchTab(args.index, args.url);
|
|
259
|
+
case 'new_tab':
|
|
260
|
+
return await this.newTab(args.url);
|
|
238
261
|
default:
|
|
239
|
-
return `Unknown action: ${action}.
|
|
262
|
+
return `Unknown action: ${action}. Available: navigate, content, screenshot, click, type, evaluate, tabs, close, scroll, wait, press_key, hover, find_by_text, switch_tab, new_tab`;
|
|
240
263
|
}
|
|
241
264
|
}
|
|
242
265
|
catch (err) {
|
|
@@ -328,9 +351,6 @@ class BrowserTool {
|
|
|
328
351
|
if (!data)
|
|
329
352
|
return 'Failed to capture screenshot';
|
|
330
353
|
// Save to temp file
|
|
331
|
-
const fs = require('fs');
|
|
332
|
-
const path = require('path');
|
|
333
|
-
const os = require('os');
|
|
334
354
|
const filePath = path.join(os.tmpdir(), `codebot-screenshot-${Date.now()}.png`);
|
|
335
355
|
fs.writeFileSync(filePath, Buffer.from(data, 'base64'));
|
|
336
356
|
return `Screenshot saved: ${filePath} (${Math.round(data.length * 0.75 / 1024)}KB)`;
|
|
@@ -344,6 +364,7 @@ class BrowserTool {
|
|
|
344
364
|
(function() {
|
|
345
365
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
346
366
|
if (!el) return 'Element not found: ' + ${JSON.stringify(selector)};
|
|
367
|
+
el.scrollIntoView({ block: 'center' });
|
|
347
368
|
el.click();
|
|
348
369
|
return 'Clicked: ' + (el.tagName || '') + ' ' + (el.textContent || '').substring(0, 50).trim();
|
|
349
370
|
})()
|
|
@@ -358,12 +379,15 @@ class BrowserTool {
|
|
|
358
379
|
if (!text)
|
|
359
380
|
return 'Error: text is required';
|
|
360
381
|
const cdp = await ensureConnected();
|
|
361
|
-
// Focus the element
|
|
382
|
+
// Focus the element and clear it
|
|
362
383
|
await cdp.send('Runtime.evaluate', {
|
|
363
384
|
expression: `
|
|
364
385
|
(function() {
|
|
365
386
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
366
|
-
if (el) {
|
|
387
|
+
if (el) {
|
|
388
|
+
el.focus();
|
|
389
|
+
if ('value' in el) el.value = '';
|
|
390
|
+
}
|
|
367
391
|
})()
|
|
368
392
|
`,
|
|
369
393
|
});
|
|
@@ -381,16 +405,30 @@ class BrowserTool {
|
|
|
381
405
|
code: `Key${char.toUpperCase()}`,
|
|
382
406
|
});
|
|
383
407
|
}
|
|
384
|
-
//
|
|
408
|
+
// React-compatible value setter — works with Twitter/X, Gmail, and any React/Vue app
|
|
385
409
|
await cdp.send('Runtime.evaluate', {
|
|
386
410
|
expression: `
|
|
387
411
|
(function() {
|
|
388
412
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
389
|
-
if (el)
|
|
413
|
+
if (!el) return;
|
|
414
|
+
|
|
415
|
+
// Try native setter from prototype (bypasses React's synthetic event system)
|
|
416
|
+
const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
417
|
+
const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
418
|
+
const setter = textareaSetter || inputSetter;
|
|
419
|
+
|
|
420
|
+
if (setter && 'value' in el) {
|
|
421
|
+
setter.call(el, ${JSON.stringify(text)});
|
|
422
|
+
} else if ('value' in el) {
|
|
390
423
|
el.value = ${JSON.stringify(text)};
|
|
391
|
-
|
|
392
|
-
|
|
424
|
+
} else if (el.isContentEditable || el.getAttribute('contenteditable') !== null) {
|
|
425
|
+
// ContentEditable elements (used by Twitter/X compose box)
|
|
426
|
+
el.textContent = ${JSON.stringify(text)};
|
|
393
427
|
}
|
|
428
|
+
|
|
429
|
+
// Fire events that React/Vue/Angular listen to
|
|
430
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${JSON.stringify(text)}, inputType: 'insertText' }));
|
|
431
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
394
432
|
})()
|
|
395
433
|
`,
|
|
396
434
|
});
|
|
@@ -439,6 +477,218 @@ class BrowserTool {
|
|
|
439
477
|
}
|
|
440
478
|
return 'Browser connection closed.';
|
|
441
479
|
}
|
|
480
|
+
// ─── New Actions ─────────────────────────────────────────────
|
|
481
|
+
async scroll(selector, direction, amount) {
|
|
482
|
+
const cdp = await ensureConnected();
|
|
483
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
484
|
+
expression: `
|
|
485
|
+
(function() {
|
|
486
|
+
const target = ${selector ? `document.querySelector(${JSON.stringify(selector)})` : 'window'};
|
|
487
|
+
if (!target && ${JSON.stringify(selector)}) return 'Element not found: ' + ${JSON.stringify(selector)};
|
|
488
|
+
const x = '${direction}' === 'right' ? ${amount} : '${direction}' === 'left' ? -${amount} : 0;
|
|
489
|
+
const y = '${direction}' === 'down' ? ${amount} : '${direction}' === 'up' ? -${amount} : 0;
|
|
490
|
+
(target.scrollBy || window.scrollBy).call(target, { left: x, top: y, behavior: 'smooth' });
|
|
491
|
+
return 'Scrolled ${direction} by ${amount}px' + (${JSON.stringify(selector)} ? ' on ' + ${JSON.stringify(selector)} : '');
|
|
492
|
+
})()
|
|
493
|
+
`,
|
|
494
|
+
returnByValue: true,
|
|
495
|
+
});
|
|
496
|
+
return result.result?.value || `Scrolled ${direction}`;
|
|
497
|
+
}
|
|
498
|
+
async wait(ms) {
|
|
499
|
+
const clamped = Math.min(Math.max(ms, 100), 10000);
|
|
500
|
+
await new Promise(r => setTimeout(r, clamped));
|
|
501
|
+
return `Waited ${clamped}ms`;
|
|
502
|
+
}
|
|
503
|
+
async pressKey(key, selector) {
|
|
504
|
+
if (!key)
|
|
505
|
+
return 'Error: key is required (e.g., Enter, Escape, Tab, ArrowDown)';
|
|
506
|
+
const cdp = await ensureConnected();
|
|
507
|
+
// Focus element first if selector provided
|
|
508
|
+
if (selector) {
|
|
509
|
+
await cdp.send('Runtime.evaluate', {
|
|
510
|
+
expression: `
|
|
511
|
+
(function() {
|
|
512
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
513
|
+
if (el) el.focus();
|
|
514
|
+
})()
|
|
515
|
+
`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// Map key names to their proper key codes
|
|
519
|
+
const keyMap = {
|
|
520
|
+
'Enter': { key: 'Enter', code: 'Enter', keyCode: 13 },
|
|
521
|
+
'Escape': { key: 'Escape', code: 'Escape', keyCode: 27 },
|
|
522
|
+
'Tab': { key: 'Tab', code: 'Tab', keyCode: 9 },
|
|
523
|
+
'Backspace': { key: 'Backspace', code: 'Backspace', keyCode: 8 },
|
|
524
|
+
'Delete': { key: 'Delete', code: 'Delete', keyCode: 46 },
|
|
525
|
+
'ArrowUp': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
|
|
526
|
+
'ArrowDown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
|
|
527
|
+
'ArrowLeft': { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
|
|
528
|
+
'ArrowRight': { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
|
|
529
|
+
'Space': { key: ' ', code: 'Space', keyCode: 32 },
|
|
530
|
+
};
|
|
531
|
+
const mapped = keyMap[key] || { key, code: `Key${key}`, keyCode: 0 };
|
|
532
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
533
|
+
type: 'keyDown',
|
|
534
|
+
key: mapped.key,
|
|
535
|
+
code: mapped.code,
|
|
536
|
+
windowsVirtualKeyCode: mapped.keyCode,
|
|
537
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
538
|
+
});
|
|
539
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
540
|
+
type: 'keyUp',
|
|
541
|
+
key: mapped.key,
|
|
542
|
+
code: mapped.code,
|
|
543
|
+
windowsVirtualKeyCode: mapped.keyCode,
|
|
544
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
545
|
+
});
|
|
546
|
+
return `Pressed key: ${key}${selector ? ` on ${selector}` : ''}`;
|
|
547
|
+
}
|
|
548
|
+
async hover(selector) {
|
|
549
|
+
if (!selector)
|
|
550
|
+
return 'Error: selector is required';
|
|
551
|
+
const cdp = await ensureConnected();
|
|
552
|
+
// Get element position
|
|
553
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
554
|
+
expression: `
|
|
555
|
+
(function() {
|
|
556
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
557
|
+
if (!el) return JSON.stringify({ error: 'Element not found: ' + ${JSON.stringify(selector)} });
|
|
558
|
+
el.scrollIntoView({ block: 'center' });
|
|
559
|
+
const rect = el.getBoundingClientRect();
|
|
560
|
+
return JSON.stringify({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, tag: el.tagName, text: (el.textContent || '').substring(0, 50).trim() });
|
|
561
|
+
})()
|
|
562
|
+
`,
|
|
563
|
+
returnByValue: true,
|
|
564
|
+
});
|
|
565
|
+
const val = result.result?.value;
|
|
566
|
+
if (!val)
|
|
567
|
+
return 'Error: could not get element position';
|
|
568
|
+
const info = JSON.parse(val);
|
|
569
|
+
if (info.error)
|
|
570
|
+
return info.error;
|
|
571
|
+
// Move mouse to element center
|
|
572
|
+
await cdp.send('Input.dispatchMouseEvent', {
|
|
573
|
+
type: 'mouseMoved',
|
|
574
|
+
x: Math.round(info.x),
|
|
575
|
+
y: Math.round(info.y),
|
|
576
|
+
});
|
|
577
|
+
return `Hovered over: ${info.tag} "${info.text}"`;
|
|
578
|
+
}
|
|
579
|
+
async findByText(text, tag) {
|
|
580
|
+
if (!text)
|
|
581
|
+
return 'Error: text is required';
|
|
582
|
+
const cdp = await ensureConnected();
|
|
583
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
584
|
+
expression: `
|
|
585
|
+
(function() {
|
|
586
|
+
const searchText = ${JSON.stringify(text)}.toLowerCase();
|
|
587
|
+
const tagFilter = ${JSON.stringify(tag || '')}.toLowerCase();
|
|
588
|
+
const elements = document.querySelectorAll(tagFilter || '*');
|
|
589
|
+
const matches = [];
|
|
590
|
+
|
|
591
|
+
for (const el of elements) {
|
|
592
|
+
// Skip invisible elements
|
|
593
|
+
if (el.offsetParent === null && el.tagName !== 'BODY') continue;
|
|
594
|
+
|
|
595
|
+
const elText = (el.textContent || '').trim();
|
|
596
|
+
if (elText.toLowerCase().includes(searchText) && elText.length < 500) {
|
|
597
|
+
// Generate a reliable selector
|
|
598
|
+
let selector = '';
|
|
599
|
+
if (el.id) {
|
|
600
|
+
selector = '#' + el.id;
|
|
601
|
+
} else if (el.getAttribute('data-testid')) {
|
|
602
|
+
selector = '[data-testid="' + el.getAttribute('data-testid') + '"]';
|
|
603
|
+
} else if (el.getAttribute('aria-label')) {
|
|
604
|
+
selector = '[aria-label="' + el.getAttribute('aria-label') + '"]';
|
|
605
|
+
} else if (el.getAttribute('role')) {
|
|
606
|
+
selector = el.tagName.toLowerCase() + '[role="' + el.getAttribute('role') + '"]';
|
|
607
|
+
} else {
|
|
608
|
+
// Use tag + class combo
|
|
609
|
+
const classes = Array.from(el.classList).slice(0, 2).join('.');
|
|
610
|
+
selector = el.tagName.toLowerCase() + (classes ? '.' + classes : '');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
matches.push({
|
|
614
|
+
tag: el.tagName.toLowerCase(),
|
|
615
|
+
text: elText.substring(0, 80),
|
|
616
|
+
selector: selector,
|
|
617
|
+
type: el.getAttribute('type') || '',
|
|
618
|
+
role: el.getAttribute('role') || '',
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
if (matches.length >= 5) break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (matches.length === 0) return 'No elements found containing: ' + ${JSON.stringify(text)};
|
|
626
|
+
return JSON.stringify(matches);
|
|
627
|
+
})()
|
|
628
|
+
`,
|
|
629
|
+
returnByValue: true,
|
|
630
|
+
});
|
|
631
|
+
const val = result.result?.value;
|
|
632
|
+
if (!val || !val.startsWith('['))
|
|
633
|
+
return val || 'No elements found';
|
|
634
|
+
const matches = JSON.parse(val);
|
|
635
|
+
return `Found ${matches.length} element(s) matching "${text}":\n` +
|
|
636
|
+
matches.map((m, i) => ` ${i + 1}. <${m.tag}> "${m.text}"\n selector: ${m.selector}${m.role ? ` role: ${m.role}` : ''}`).join('\n');
|
|
637
|
+
}
|
|
638
|
+
async switchTab(index, urlContains) {
|
|
639
|
+
const targets = await (0, cdp_1.getTargets)(debugPort);
|
|
640
|
+
const pages = targets.filter(t => t.url && !t.url.startsWith('devtools://'));
|
|
641
|
+
if (pages.length === 0)
|
|
642
|
+
return 'No tabs available.';
|
|
643
|
+
let target;
|
|
644
|
+
if (index !== undefined) {
|
|
645
|
+
target = pages[index - 1];
|
|
646
|
+
if (!target)
|
|
647
|
+
return `Tab ${index} not found. ${pages.length} tabs available.`;
|
|
648
|
+
}
|
|
649
|
+
else if (urlContains) {
|
|
650
|
+
target = pages.find(t => t.url.includes(urlContains));
|
|
651
|
+
if (!target)
|
|
652
|
+
return `No tab found matching URL "${urlContains}".`;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
return 'Error: provide index (1-based) or url to match.';
|
|
656
|
+
}
|
|
657
|
+
// Close current connection and connect to new target
|
|
658
|
+
if (client) {
|
|
659
|
+
client.close();
|
|
660
|
+
client = null;
|
|
661
|
+
}
|
|
662
|
+
client = new cdp_1.CDPClient();
|
|
663
|
+
await client.connect(target.webSocketDebuggerUrl);
|
|
664
|
+
await client.send('Page.enable');
|
|
665
|
+
await client.send('Runtime.enable');
|
|
666
|
+
return `Switched to tab: ${target.title || '(no title)'}\n ${target.url}`;
|
|
667
|
+
}
|
|
668
|
+
async newTab(url) {
|
|
669
|
+
const cdp = await ensureConnected();
|
|
670
|
+
const targetUrl = url || 'about:blank';
|
|
671
|
+
// Auto-add protocol
|
|
672
|
+
let navUrl = targetUrl;
|
|
673
|
+
if (url && !url.startsWith('http://') && !url.startsWith('https://') && url !== 'about:blank') {
|
|
674
|
+
navUrl = 'https://' + url;
|
|
675
|
+
}
|
|
676
|
+
const result = await cdp.send('Target.createTarget', { url: navUrl });
|
|
677
|
+
const targetId = result.targetId;
|
|
678
|
+
if (!targetId)
|
|
679
|
+
return 'Failed to create new tab.';
|
|
680
|
+
// Switch to the new tab
|
|
681
|
+
const targets = await (0, cdp_1.getTargets)(debugPort);
|
|
682
|
+
const newTarget = targets.find(t => t.id === targetId);
|
|
683
|
+
if (newTarget?.webSocketDebuggerUrl) {
|
|
684
|
+
client?.close();
|
|
685
|
+
client = new cdp_1.CDPClient();
|
|
686
|
+
await client.connect(newTarget.webSocketDebuggerUrl);
|
|
687
|
+
await client.send('Page.enable');
|
|
688
|
+
await client.send('Runtime.enable');
|
|
689
|
+
}
|
|
690
|
+
return `Opened new tab: ${navUrl}`;
|
|
691
|
+
}
|
|
442
692
|
}
|
|
443
693
|
exports.BrowserTool = BrowserTool;
|
|
444
694
|
//# sourceMappingURL=browser.js.map
|
package/dist/tools/index.js
CHANGED
|
@@ -10,8 +10,10 @@ const grep_1 = require("./grep");
|
|
|
10
10
|
const think_1 = require("./think");
|
|
11
11
|
const memory_1 = require("./memory");
|
|
12
12
|
const web_fetch_1 = require("./web-fetch");
|
|
13
|
+
const web_search_1 = require("./web-search");
|
|
13
14
|
const browser_1 = require("./browser");
|
|
14
15
|
const batch_edit_1 = require("./batch-edit");
|
|
16
|
+
const routine_1 = require("./routine");
|
|
15
17
|
var edit_2 = require("./edit");
|
|
16
18
|
Object.defineProperty(exports, "EditFileTool", { enumerable: true, get: function () { return edit_2.EditFileTool; } });
|
|
17
19
|
class ToolRegistry {
|
|
@@ -27,7 +29,9 @@ class ToolRegistry {
|
|
|
27
29
|
this.register(new think_1.ThinkTool());
|
|
28
30
|
this.register(new memory_1.MemoryTool(projectRoot));
|
|
29
31
|
this.register(new web_fetch_1.WebFetchTool());
|
|
32
|
+
this.register(new web_search_1.WebSearchTool());
|
|
30
33
|
this.register(new browser_1.BrowserTool());
|
|
34
|
+
this.register(new routine_1.RoutineTool());
|
|
31
35
|
}
|
|
32
36
|
register(tool) {
|
|
33
37
|
this.tools.set(tool.name, tool);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Tool } from '../types';
|
|
2
|
+
export interface Routine {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
prompt: string;
|
|
7
|
+
schedule: string;
|
|
8
|
+
lastRun?: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class RoutineTool implements Tool {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
permission: Tool['permission'];
|
|
15
|
+
parameters: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
action: {
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
enum: string[];
|
|
22
|
+
};
|
|
23
|
+
name: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
description: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
prompt: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
schedule: {
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
id: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
required: string[];
|
|
45
|
+
};
|
|
46
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
47
|
+
private loadRoutines;
|
|
48
|
+
private saveRoutines;
|
|
49
|
+
private list;
|
|
50
|
+
private add;
|
|
51
|
+
private remove;
|
|
52
|
+
private toggle;
|
|
53
|
+
}
|
|
54
|
+
/** Check if a cron expression matches the given date */
|
|
55
|
+
export declare function matchesCron(expr: string, date: Date): boolean;
|
|
56
|
+
//# sourceMappingURL=routine.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RoutineTool = void 0;
|
|
37
|
+
exports.matchesCron = matchesCron;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const ROUTINES_FILE = path.join(os.homedir(), '.codebot', 'routines.json');
|
|
43
|
+
class RoutineTool {
|
|
44
|
+
name = 'routine';
|
|
45
|
+
description = 'Manage scheduled routines (recurring tasks). Add daily social media posts, email checks, research tasks, etc. Uses cron expressions for scheduling (e.g., "0 9 * * *" for 9am daily, "0 */6 * * *" for every 6 hours, "30 18 * * 1-5" for 6:30pm weekdays).';
|
|
46
|
+
permission = 'auto';
|
|
47
|
+
parameters = {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
action: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Action to perform',
|
|
53
|
+
enum: ['list', 'add', 'remove', 'enable', 'disable'],
|
|
54
|
+
},
|
|
55
|
+
name: { type: 'string', description: 'Routine name (for add/remove)' },
|
|
56
|
+
description: { type: 'string', description: 'Human-readable description (for add)' },
|
|
57
|
+
prompt: { type: 'string', description: 'The message/task to execute when triggered (for add)' },
|
|
58
|
+
schedule: { type: 'string', description: 'Cron expression: "minute hour day-of-month month day-of-week" (for add)' },
|
|
59
|
+
id: { type: 'string', description: 'Routine ID (for remove/enable/disable)' },
|
|
60
|
+
},
|
|
61
|
+
required: ['action'],
|
|
62
|
+
};
|
|
63
|
+
async execute(args) {
|
|
64
|
+
const action = args.action;
|
|
65
|
+
switch (action) {
|
|
66
|
+
case 'list':
|
|
67
|
+
return this.list();
|
|
68
|
+
case 'add':
|
|
69
|
+
return this.add(args);
|
|
70
|
+
case 'remove':
|
|
71
|
+
return this.remove(args.id || args.name);
|
|
72
|
+
case 'enable':
|
|
73
|
+
return this.toggle(args.id || args.name, true);
|
|
74
|
+
case 'disable':
|
|
75
|
+
return this.toggle(args.id || args.name, false);
|
|
76
|
+
default:
|
|
77
|
+
return `Unknown action: ${action}. Use: list, add, remove, enable, disable`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
loadRoutines() {
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(ROUTINES_FILE)) {
|
|
83
|
+
return JSON.parse(fs.readFileSync(ROUTINES_FILE, 'utf-8'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* corrupt file */ }
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
saveRoutines(routines) {
|
|
90
|
+
const dir = path.dirname(ROUTINES_FILE);
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(ROUTINES_FILE, JSON.stringify(routines, null, 2) + '\n');
|
|
93
|
+
}
|
|
94
|
+
list() {
|
|
95
|
+
const routines = this.loadRoutines();
|
|
96
|
+
if (routines.length === 0) {
|
|
97
|
+
return 'No routines configured. Use action "add" to create one.';
|
|
98
|
+
}
|
|
99
|
+
return routines.map(r => {
|
|
100
|
+
const status = r.enabled ? '✓ enabled' : '✗ disabled';
|
|
101
|
+
const lastRun = r.lastRun ? `Last run: ${r.lastRun}` : 'Never run';
|
|
102
|
+
return `[${r.id.substring(0, 8)}] ${r.name} (${status})\n Schedule: ${r.schedule}\n Task: ${r.prompt.substring(0, 100)}${r.prompt.length > 100 ? '...' : ''}\n ${lastRun}`;
|
|
103
|
+
}).join('\n\n');
|
|
104
|
+
}
|
|
105
|
+
add(args) {
|
|
106
|
+
const name = args.name;
|
|
107
|
+
const description = args.description || '';
|
|
108
|
+
const prompt = args.prompt;
|
|
109
|
+
const schedule = args.schedule;
|
|
110
|
+
if (!name)
|
|
111
|
+
return 'Error: name is required';
|
|
112
|
+
if (!prompt)
|
|
113
|
+
return 'Error: prompt is required (the task to execute)';
|
|
114
|
+
if (!schedule)
|
|
115
|
+
return 'Error: schedule is required (cron expression)';
|
|
116
|
+
// Validate cron expression
|
|
117
|
+
const parts = schedule.trim().split(/\s+/);
|
|
118
|
+
if (parts.length !== 5) {
|
|
119
|
+
return 'Error: schedule must be a 5-field cron expression: "minute hour day-of-month month day-of-week"';
|
|
120
|
+
}
|
|
121
|
+
const routines = this.loadRoutines();
|
|
122
|
+
// Check for duplicate name
|
|
123
|
+
if (routines.find(r => r.name.toLowerCase() === name.toLowerCase())) {
|
|
124
|
+
return `Error: routine "${name}" already exists. Remove it first or use a different name.`;
|
|
125
|
+
}
|
|
126
|
+
const routine = {
|
|
127
|
+
id: crypto.randomUUID(),
|
|
128
|
+
name,
|
|
129
|
+
description,
|
|
130
|
+
prompt,
|
|
131
|
+
schedule: schedule.trim(),
|
|
132
|
+
enabled: true,
|
|
133
|
+
};
|
|
134
|
+
routines.push(routine);
|
|
135
|
+
this.saveRoutines(routines);
|
|
136
|
+
return `Routine "${name}" created!\n ID: ${routine.id.substring(0, 8)}\n Schedule: ${schedule}\n Task: ${prompt.substring(0, 100)}`;
|
|
137
|
+
}
|
|
138
|
+
remove(identifier) {
|
|
139
|
+
if (!identifier)
|
|
140
|
+
return 'Error: id or name is required';
|
|
141
|
+
const routines = this.loadRoutines();
|
|
142
|
+
const idx = routines.findIndex(r => r.id === identifier || r.id.startsWith(identifier) || r.name.toLowerCase() === identifier.toLowerCase());
|
|
143
|
+
if (idx === -1)
|
|
144
|
+
return `Routine "${identifier}" not found.`;
|
|
145
|
+
const removed = routines.splice(idx, 1)[0];
|
|
146
|
+
this.saveRoutines(routines);
|
|
147
|
+
return `Removed routine: ${removed.name}`;
|
|
148
|
+
}
|
|
149
|
+
toggle(identifier, enabled) {
|
|
150
|
+
if (!identifier)
|
|
151
|
+
return 'Error: id or name is required';
|
|
152
|
+
const routines = this.loadRoutines();
|
|
153
|
+
const routine = routines.find(r => r.id === identifier || r.id.startsWith(identifier) || r.name.toLowerCase() === identifier.toLowerCase());
|
|
154
|
+
if (!routine)
|
|
155
|
+
return `Routine "${identifier}" not found.`;
|
|
156
|
+
routine.enabled = enabled;
|
|
157
|
+
this.saveRoutines(routines);
|
|
158
|
+
return `Routine "${routine.name}" ${enabled ? 'enabled' : 'disabled'}.`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.RoutineTool = RoutineTool;
|
|
162
|
+
/** Check if a cron expression matches the given date */
|
|
163
|
+
function matchesCron(expr, date) {
|
|
164
|
+
const [minField, hourField, domField, monthField, dowField] = expr.split(/\s+/);
|
|
165
|
+
const checks = [
|
|
166
|
+
[minField, date.getMinutes()],
|
|
167
|
+
[hourField, date.getHours()],
|
|
168
|
+
[domField, date.getDate()],
|
|
169
|
+
[monthField, date.getMonth() + 1],
|
|
170
|
+
[dowField, date.getDay()],
|
|
171
|
+
];
|
|
172
|
+
return checks.every(([field, val]) => matchField(field, val));
|
|
173
|
+
}
|
|
174
|
+
function matchField(field, value) {
|
|
175
|
+
if (field === '*')
|
|
176
|
+
return true;
|
|
177
|
+
// Handle step values: */5, */10
|
|
178
|
+
if (field.startsWith('*/')) {
|
|
179
|
+
const step = parseInt(field.slice(2), 10);
|
|
180
|
+
return step > 0 && value % step === 0;
|
|
181
|
+
}
|
|
182
|
+
// Handle ranges: 1-5
|
|
183
|
+
if (field.includes('-')) {
|
|
184
|
+
const [low, high] = field.split('-').map(Number);
|
|
185
|
+
return value >= low && value <= high;
|
|
186
|
+
}
|
|
187
|
+
// Handle lists: 1,3,5
|
|
188
|
+
if (field.includes(',')) {
|
|
189
|
+
return field.split(',').map(Number).includes(value);
|
|
190
|
+
}
|
|
191
|
+
// Exact match
|
|
192
|
+
return parseInt(field, 10) === value;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=routine.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Tool } from '../types';
|
|
2
|
+
export declare class WebSearchTool implements Tool {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
permission: Tool['permission'];
|
|
6
|
+
parameters: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
query: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
num_results: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
required: string[];
|
|
19
|
+
};
|
|
20
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
21
|
+
private search;
|
|
22
|
+
private extractResults;
|
|
23
|
+
private stripTags;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=web-search.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSearchTool = void 0;
|
|
4
|
+
class WebSearchTool {
|
|
5
|
+
name = 'web_search';
|
|
6
|
+
description = 'Search the web using DuckDuckGo. Returns titles, URLs, and snippets. Use for research, fact-checking, finding documentation, or discovering information. If results are empty, try the browser tool to navigate to a search engine directly.';
|
|
7
|
+
permission = 'prompt';
|
|
8
|
+
parameters = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
query: { type: 'string', description: 'Search query' },
|
|
12
|
+
num_results: { type: 'number', description: 'Number of results to return (default 5, max 10)' },
|
|
13
|
+
},
|
|
14
|
+
required: ['query'],
|
|
15
|
+
};
|
|
16
|
+
async execute(args) {
|
|
17
|
+
const query = args.query;
|
|
18
|
+
if (!query)
|
|
19
|
+
return 'Error: query is required';
|
|
20
|
+
const numResults = Math.min(Math.max(args.num_results || 5, 1), 10);
|
|
21
|
+
try {
|
|
22
|
+
const results = await this.search(query, numResults);
|
|
23
|
+
if (results.length === 0) {
|
|
24
|
+
return `No results found for "${query}". Try a different query or use the browser tool to search directly.`;
|
|
25
|
+
}
|
|
26
|
+
let output = `Search results for "${query}":\n\n`;
|
|
27
|
+
results.forEach((r, i) => {
|
|
28
|
+
output += `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}\n\n`;
|
|
29
|
+
});
|
|
30
|
+
return output.trim();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
return `Search error: ${msg}. Try using the browser tool to navigate to https://duckduckgo.com/?q=${encodeURIComponent(query)} instead.`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async search(query, numResults) {
|
|
38
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
headers: {
|
|
41
|
+
'User-Agent': 'Mozilla/5.0 (compatible; CodeBot/1.0)',
|
|
42
|
+
'Accept': 'text/html',
|
|
43
|
+
},
|
|
44
|
+
signal: AbortSignal.timeout(10000),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`DuckDuckGo returned ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
const html = await response.text();
|
|
50
|
+
return this.extractResults(html, numResults);
|
|
51
|
+
}
|
|
52
|
+
extractResults(html, max) {
|
|
53
|
+
const results = [];
|
|
54
|
+
// DuckDuckGo HTML results are in <div class="result ..."> blocks
|
|
55
|
+
// Each has: <a class="result__a"> for title/link, <a class="result__snippet"> for snippet
|
|
56
|
+
const blocks = html.split(/class="result\s/);
|
|
57
|
+
for (let i = 1; i < blocks.length && results.length < max; i++) {
|
|
58
|
+
const block = blocks[i];
|
|
59
|
+
// Extract title from result__a
|
|
60
|
+
const titleMatch = block.match(/class="result__a"[^>]*>([\s\S]*?)<\/a>/);
|
|
61
|
+
const title = titleMatch ? this.stripTags(titleMatch[1]).trim() : '';
|
|
62
|
+
// Extract URL — DDG wraps URLs in a redirect, the actual URL is in uddg= parameter
|
|
63
|
+
const urlMatch = block.match(/uddg=([^"&]+)/);
|
|
64
|
+
let url = urlMatch ? decodeURIComponent(urlMatch[1]) : '';
|
|
65
|
+
// Fallback: try href directly
|
|
66
|
+
if (!url) {
|
|
67
|
+
const hrefMatch = block.match(/href="(https?:\/\/[^"]+)"/);
|
|
68
|
+
url = hrefMatch ? hrefMatch[1] : '';
|
|
69
|
+
}
|
|
70
|
+
// Extract snippet from result__snippet
|
|
71
|
+
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
72
|
+
const snippet = snippetMatch ? this.stripTags(snippetMatch[1]).trim() : '';
|
|
73
|
+
if (title && url && !url.includes('duckduckgo.com')) {
|
|
74
|
+
results.push({ title, url, snippet });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
stripTags(html) {
|
|
80
|
+
return html
|
|
81
|
+
.replace(/<[^>]+>/g, '') // Remove HTML tags
|
|
82
|
+
.replace(/&/g, '&')
|
|
83
|
+
.replace(/</g, '<')
|
|
84
|
+
.replace(/>/g, '>')
|
|
85
|
+
.replace(/"/g, '"')
|
|
86
|
+
.replace(/'/g, "'")
|
|
87
|
+
.replace(/ /g, ' ')
|
|
88
|
+
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
89
|
+
.trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.WebSearchTool = WebSearchTool;
|
|
93
|
+
//# sourceMappingURL=web-search.js.map
|
package/package.json
CHANGED