codebot-ai 1.1.1 → 1.2.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/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, an AI coding assistant. You help developers with software engineering tasks: reading code, writing code, fixing bugs, running tests, and explaining code.
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.
@@ -238,11 +238,19 @@ CRITICAL IDENTITY — you MUST follow this:
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
240
  Rules:
241
- - Always read files before editing them.
242
- - Prefer editing over rewriting entire files.
243
- - Be concise and direct.
244
- - Explain what you're doing and why.
241
+ - When given a goal, break it into steps and execute them using your tools.
242
+ - Always read files before editing them. Prefer editing over rewriting entire files.
243
+ - Be concise and direct. Explain what you're doing and why.
245
244
  - Use the memory tool to save important context, user preferences, and patterns you learn. Memory persists across sessions.
245
+ - After completing social media posts, emails, or research tasks, log the outcome to memory (file: "outcomes") for future learning.
246
+ - Before doing social media or email tasks, read your memory files for any saved skills or style guides.
247
+
248
+ Skills:
249
+ - Web browsing: use the browser tool to navigate, click, type, find elements by text, scroll, press keys, hover, and manage tabs.
250
+ - Research: use web_search for quick lookups, then browser for deep reading of specific pages.
251
+ - Social media: navigate to the platform, find the compose area with find_by_text, type your content, and submit.
252
+ - Email: navigate to Gmail/email, compose and send messages through the browser interface.
253
+ - Routines: use the routine tool to schedule recurring tasks (daily posts, email checks, etc.).
246
254
 
247
255
  ${repoMap}${memoryBlock}`;
248
256
  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 VERSION = '1.1.0';
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
@@ -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
@@ -43,24 +43,32 @@ const fs = __importStar(require("fs"));
43
43
  let client = null;
44
44
  let debugPort = 9222;
45
45
  const CHROME_DATA_DIR = path.join(os.homedir(), '.codebot', 'chrome-profile');
46
- /** Kill any Chrome using our debug port or data dir */
46
+ /** Kill any Chrome using our debug port or data dir (but NEVER kill ourselves) */
47
47
  function killExistingChrome() {
48
+ // Close our own CDP connection first so we don't hold the port
49
+ if (client) {
50
+ try {
51
+ client.close();
52
+ }
53
+ catch { /* ignore */ }
54
+ client = null;
55
+ }
48
56
  const { execSync } = require('child_process');
57
+ const myPid = process.pid;
49
58
  try {
50
59
  if (process.platform === 'win32') {
51
60
  execSync(`for /f "tokens=5" %a in ('netstat -aon ^| findstr :${debugPort}') do taskkill /F /PID %a`, { stdio: 'ignore' });
52
61
  }
53
62
  else {
54
- // Kill any process listening on our debug port
55
- execSync(`lsof -ti:${debugPort} | xargs kill -9 2>/dev/null || true`, { stdio: 'ignore' });
63
+ // Kill Chrome/Chromium processes on our debug port — exclude our own PID
64
+ execSync(`lsof -ti:${debugPort} | grep -v "^${myPid}$" | xargs kill -9 2>/dev/null || true`, { stdio: 'ignore' });
56
65
  // Also kill any Chrome using our data dir
57
- execSync(`pkill -f "${CHROME_DATA_DIR}" 2>/dev/null || true`, { stdio: 'ignore' });
66
+ execSync(`pkill -9 -f "chrome.*--user-data-dir=${CHROME_DATA_DIR}" 2>/dev/null || true`, { stdio: 'ignore' });
58
67
  }
59
68
  }
60
69
  catch {
61
70
  // ignore — nothing to kill
62
71
  }
63
- // Give OS time to release the port
64
72
  }
65
73
  async function ensureConnected() {
66
74
  if (client?.isConnected())
@@ -190,7 +198,7 @@ async function ensureConnected() {
190
198
  }
191
199
  class BrowserTool {
192
200
  name = 'browser';
193
- 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.';
194
202
  permission = 'prompt';
195
203
  parameters = {
196
204
  type: 'object',
@@ -198,12 +206,21 @@ class BrowserTool {
198
206
  action: {
199
207
  type: 'string',
200
208
  description: 'Action to perform',
201
- enum: ['navigate', 'content', 'screenshot', 'click', 'type', 'evaluate', 'tabs', 'close'],
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
+ ],
202
214
  },
203
- url: { type: 'string', description: 'URL to navigate to (for navigate action)' },
204
- selector: { type: 'string', description: 'CSS selector for element (for click/type)' },
205
- text: { type: 'string', description: 'Text to type (for type action)' },
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)' },
206
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)' },
207
224
  },
208
225
  required: ['action'],
209
226
  };
@@ -227,8 +244,22 @@ class BrowserTool {
227
244
  return await this.listTabs();
228
245
  case 'close':
229
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);
230
261
  default:
231
- return `Unknown action: ${action}. Use: navigate, content, screenshot, click, type, evaluate, tabs, close`;
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`;
232
263
  }
233
264
  }
234
265
  catch (err) {
@@ -320,9 +351,6 @@ class BrowserTool {
320
351
  if (!data)
321
352
  return 'Failed to capture screenshot';
322
353
  // Save to temp file
323
- const fs = require('fs');
324
- const path = require('path');
325
- const os = require('os');
326
354
  const filePath = path.join(os.tmpdir(), `codebot-screenshot-${Date.now()}.png`);
327
355
  fs.writeFileSync(filePath, Buffer.from(data, 'base64'));
328
356
  return `Screenshot saved: ${filePath} (${Math.round(data.length * 0.75 / 1024)}KB)`;
@@ -336,6 +364,7 @@ class BrowserTool {
336
364
  (function() {
337
365
  const el = document.querySelector(${JSON.stringify(selector)});
338
366
  if (!el) return 'Element not found: ' + ${JSON.stringify(selector)};
367
+ el.scrollIntoView({ block: 'center' });
339
368
  el.click();
340
369
  return 'Clicked: ' + (el.tagName || '') + ' ' + (el.textContent || '').substring(0, 50).trim();
341
370
  })()
@@ -350,12 +379,15 @@ class BrowserTool {
350
379
  if (!text)
351
380
  return 'Error: text is required';
352
381
  const cdp = await ensureConnected();
353
- // Focus the element
382
+ // Focus the element and clear it
354
383
  await cdp.send('Runtime.evaluate', {
355
384
  expression: `
356
385
  (function() {
357
386
  const el = document.querySelector(${JSON.stringify(selector)});
358
- if (el) { el.focus(); el.value = ''; }
387
+ if (el) {
388
+ el.focus();
389
+ if ('value' in el) el.value = '';
390
+ }
359
391
  })()
360
392
  `,
361
393
  });
@@ -373,16 +405,30 @@ class BrowserTool {
373
405
  code: `Key${char.toUpperCase()}`,
374
406
  });
375
407
  }
376
- // Also set value directly as fallback
408
+ // React-compatible value setter works with Twitter/X, Gmail, and any React/Vue app
377
409
  await cdp.send('Runtime.evaluate', {
378
410
  expression: `
379
411
  (function() {
380
412
  const el = document.querySelector(${JSON.stringify(selector)});
381
- 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) {
382
423
  el.value = ${JSON.stringify(text)};
383
- el.dispatchEvent(new Event('input', { bubbles: true }));
384
- el.dispatchEvent(new Event('change', { bubbles: true }));
424
+ } else if (el.isContentEditable || el.getAttribute('contenteditable') !== null) {
425
+ // ContentEditable elements (used by Twitter/X compose box)
426
+ el.textContent = ${JSON.stringify(text)};
385
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 }));
386
432
  })()
387
433
  `,
388
434
  });
@@ -431,6 +477,218 @@ class BrowserTool {
431
477
  }
432
478
  return 'Browser connection closed.';
433
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
+ }
434
692
  }
435
693
  exports.BrowserTool = BrowserTool;
436
694
  //# sourceMappingURL=browser.js.map
@@ -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(/&amp;/g, '&')
83
+ .replace(/&lt;/g, '<')
84
+ .replace(/&gt;/g, '>')
85
+ .replace(/&quot;/g, '"')
86
+ .replace(/&#x27;/g, "'")
87
+ .replace(/&nbsp;/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebot-ai",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Local-first AI coding assistant. Zero dependencies. Works with Ollama, LM Studio, vLLM, Claude, GPT, Gemini, and more.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",