codebot-ai 1.0.2 → 1.1.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.
@@ -0,0 +1,101 @@
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.loadPlugins = loadPlugins;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Plugin system for CodeBot.
41
+ *
42
+ * Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
43
+ * Each plugin exports a default function or object that implements the Tool interface:
44
+ *
45
+ * module.exports = {
46
+ * name: 'my_tool',
47
+ * description: 'Does something useful',
48
+ * permission: 'prompt',
49
+ * parameters: { type: 'object', properties: { ... }, required: [...] },
50
+ * execute: async (args) => { return 'result'; }
51
+ * };
52
+ */
53
+ function loadPlugins(projectRoot) {
54
+ const plugins = [];
55
+ const os = require('os');
56
+ const dirs = [
57
+ path.join(os.homedir(), '.codebot', 'plugins'),
58
+ ];
59
+ if (projectRoot) {
60
+ dirs.push(path.join(projectRoot, '.codebot', 'plugins'));
61
+ }
62
+ for (const dir of dirs) {
63
+ if (!fs.existsSync(dir))
64
+ continue;
65
+ let entries;
66
+ try {
67
+ entries = fs.readdirSync(dir, { withFileTypes: true });
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ for (const entry of entries) {
73
+ if (!entry.isFile() || !entry.name.endsWith('.js'))
74
+ continue;
75
+ try {
76
+ const pluginPath = path.join(dir, entry.name);
77
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
78
+ const mod = require(pluginPath);
79
+ const plugin = mod.default || mod;
80
+ if (isValidTool(plugin)) {
81
+ plugins.push(plugin);
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error(`Plugin load error (${entry.name}): ${err instanceof Error ? err.message : String(err)}`);
86
+ }
87
+ }
88
+ }
89
+ return plugins;
90
+ }
91
+ function isValidTool(obj) {
92
+ if (!obj || typeof obj !== 'object')
93
+ return false;
94
+ const t = obj;
95
+ return (typeof t.name === 'string' &&
96
+ typeof t.description === 'string' &&
97
+ typeof t.execute === 'function' &&
98
+ typeof t.parameters === 'object' &&
99
+ typeof t.permission === 'string');
100
+ }
101
+ //# sourceMappingURL=plugins.js.map
@@ -20,6 +20,13 @@ class OpenAIProvider {
20
20
  if (tools?.length && this.supportsTools) {
21
21
  body.tools = tools;
22
22
  }
23
+ // Ollama/local provider optimizations: set context window and keep model loaded
24
+ const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
25
+ if (isLocal) {
26
+ const modelInfo = (0, registry_1.getModelInfo)(this.config.model);
27
+ body.options = { num_ctx: modelInfo.contextWindow };
28
+ body.keep_alive = '30m';
29
+ }
23
30
  const headers = {
24
31
  'Content-Type': 'application/json',
25
32
  };
package/dist/setup.js CHANGED
@@ -59,9 +59,9 @@ function loadConfig() {
59
59
  /** Save config to ~/.codebot/config.json */
60
60
  function saveConfig(config) {
61
61
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
62
- // Never persist API keys to disk — use env vars
63
62
  const safe = { ...config };
64
- delete safe.apiKey;
63
+ // Persist API key if user entered it during setup (convenience over env vars)
64
+ // The key is stored in the user's home directory with default permissions
65
65
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(safe, null, 2) + '\n');
66
66
  }
67
67
  /** Check if this is the first run (no config, no env keys) */
@@ -113,6 +113,16 @@ function detectApiKeys() {
113
113
  set: !!process.env[defaults.envKey],
114
114
  }));
115
115
  }
116
+ /** Cloud provider display info */
117
+ const CLOUD_PROVIDERS = [
118
+ { provider: 'openai', name: 'OpenAI', defaultModel: 'gpt-4o', description: 'GPT-4o, GPT-4.1, o3/o4' },
119
+ { provider: 'anthropic', name: 'Anthropic', defaultModel: 'claude-sonnet-4-6', description: 'Claude Opus/Sonnet/Haiku' },
120
+ { provider: 'gemini', name: 'Google Gemini', defaultModel: 'gemini-2.5-flash', description: 'Gemini 2.5 Pro/Flash' },
121
+ { provider: 'deepseek', name: 'DeepSeek', defaultModel: 'deepseek-chat', description: 'DeepSeek Chat/Reasoner' },
122
+ { provider: 'groq', name: 'Groq', defaultModel: 'llama-3.3-70b-versatile', description: 'Fast Llama/Mixtral inference' },
123
+ { provider: 'mistral', name: 'Mistral', defaultModel: 'mistral-large-latest', description: 'Mistral Large, Codestral' },
124
+ { provider: 'xai', name: 'xAI', defaultModel: 'grok-3', description: 'Grok-3' },
125
+ ];
116
126
  const C = {
117
127
  reset: '\x1b[0m',
118
128
  bold: '\x1b[1m',
@@ -155,46 +165,58 @@ async function runSetup() {
155
165
  console.log(fmt(` ✓ ${key.provider} API key found (${key.envVar})`, 'green'));
156
166
  }
157
167
  }
158
- const missingKeys = apiKeys.filter(k => !k.set);
159
- if (missingKeys.length > 0 && localServers.length === 0) {
160
- console.log(fmt('\n No API keys found. Set one to use cloud models:', 'yellow'));
161
- for (const key of missingKeys) {
162
- console.log(fmt(` export ${key.envVar}="your-key-here"`, 'dim'));
163
- }
164
- }
165
- // Step 3: Choose provider
168
+ // Step 3: Choose provider — show ALL options (local + cloud)
166
169
  console.log(fmt('\nChoose your setup:', 'bold'));
167
170
  const options = [];
168
171
  let idx = 1;
169
172
  // Local options first
170
173
  for (const server of localServers) {
171
174
  const defaultModel = server.models[0] || 'qwen2.5-coder:32b';
172
- options.push({ label: `${server.name} (local, free)`, provider: 'openai', model: defaultModel, baseUrl: server.url });
175
+ options.push({
176
+ label: `${server.name} (local, free)`,
177
+ provider: 'openai',
178
+ model: defaultModel,
179
+ baseUrl: server.url,
180
+ needsKey: false,
181
+ });
173
182
  console.log(` ${fmt(`${idx}`, 'cyan')} ${server.name} — ${defaultModel} ${fmt('(local, free, private)', 'green')}`);
174
183
  idx++;
175
184
  }
176
- // Cloud options
177
- for (const key of availableKeys) {
178
- const models = Object.entries(registry_1.MODEL_REGISTRY)
179
- .filter(([, info]) => info.provider === key.provider)
180
- .map(([name]) => name);
181
- const defaultModel = models[0] || key.provider;
182
- const defaults = registry_1.PROVIDER_DEFAULTS[key.provider];
183
- options.push({ label: key.provider, provider: key.provider, model: defaultModel, baseUrl: defaults.baseUrl });
184
- console.log(` ${fmt(`${idx}`, 'cyan')} ${key.provider} — ${defaultModel} ${fmt('(cloud)', 'dim')}`);
185
+ // Cloud options — ALWAYS show all providers
186
+ for (const cloud of CLOUD_PROVIDERS) {
187
+ const keyInfo = apiKeys.find(k => k.provider === cloud.provider);
188
+ const hasKey = keyInfo?.set || false;
189
+ const defaults = registry_1.PROVIDER_DEFAULTS[cloud.provider];
190
+ const keyStatus = hasKey ? fmt('✓ key set', 'green') : fmt('enter key during setup', 'yellow');
191
+ options.push({
192
+ label: cloud.name,
193
+ provider: cloud.provider,
194
+ model: cloud.defaultModel,
195
+ baseUrl: defaults.baseUrl,
196
+ needsKey: !hasKey,
197
+ envVar: defaults.envKey,
198
+ });
199
+ console.log(` ${fmt(`${idx}`, 'cyan')} ${cloud.name} — ${cloud.description} ${fmt(`(${keyStatus})`, 'dim')}`);
185
200
  idx++;
186
201
  }
187
- if (options.length === 0) {
188
- console.log(fmt('\n No providers available. Either:', 'yellow'));
189
- console.log(fmt(' 1. Install Ollama: https://ollama.ai', 'dim'));
190
- console.log(fmt(' 2. Set an API key: export ANTHROPIC_API_KEY="..."', 'dim'));
191
- rl.close();
192
- return {};
193
- }
194
202
  const choice = await ask(rl, fmt(`\nSelect [1-${options.length}]: `, 'cyan'));
195
203
  const selected = options[parseInt(choice, 10) - 1] || options[0];
196
- // Step 4: Show available models for chosen provider
197
- // For local servers, use the actual installed models instead of the hardcoded registry
204
+ // Step 4: If cloud provider needs API key, prompt for it
205
+ let apiKey = '';
206
+ if (selected.needsKey && selected.envVar) {
207
+ console.log(fmt(`\n ${selected.label} requires an API key.`, 'yellow'));
208
+ console.log(fmt(` Get one at: ${getKeyUrl(selected.provider)}`, 'dim'));
209
+ apiKey = await ask(rl, fmt(`\n Enter your ${selected.label} API key: `, 'cyan'));
210
+ if (!apiKey) {
211
+ console.log(fmt(`\n No key entered. You can set it later:`, 'yellow'));
212
+ console.log(fmt(` export ${selected.envVar}="your-key-here"`, 'dim'));
213
+ }
214
+ }
215
+ else if (selected.envVar) {
216
+ // Use existing env var
217
+ apiKey = process.env[selected.envVar] || '';
218
+ }
219
+ // Step 5: Show available models for chosen provider
198
220
  const matchedServer = localServers.find(s => s.url === selected.baseUrl);
199
221
  const providerModels = matchedServer && matchedServer.models.length > 0
200
222
  ? matchedServer.models
@@ -219,7 +241,7 @@ async function runSetup() {
219
241
  }
220
242
  }
221
243
  }
222
- // Step 5: Auto mode?
244
+ // Step 6: Auto mode?
223
245
  const autoChoice = await ask(rl, fmt('\nEnable autonomous mode? (skip permission prompts) [y/N]: ', 'cyan'));
224
246
  const autoApprove = autoChoice.toLowerCase().startsWith('y');
225
247
  rl.close();
@@ -230,14 +252,34 @@ async function runSetup() {
230
252
  baseUrl: selected.baseUrl,
231
253
  autoApprove,
232
254
  };
255
+ // Save API key if user entered one
256
+ if (apiKey) {
257
+ config.apiKey = apiKey;
258
+ }
233
259
  saveConfig(config);
234
260
  console.log(fmt('\n✓ Config saved to ~/.codebot/config.json', 'green'));
235
261
  console.log(fmt(` Model: ${config.model}`, 'dim'));
236
262
  console.log(fmt(` Provider: ${config.provider}`, 'dim'));
263
+ if (apiKey) {
264
+ console.log(fmt(` API Key: ${'*'.repeat(Math.min(apiKey.length, 20))}`, 'dim'));
265
+ }
237
266
  if (autoApprove) {
238
267
  console.log(fmt(` Mode: AUTONOMOUS`, 'yellow'));
239
268
  }
240
269
  console.log(fmt(`\nRun ${fmt('codebot', 'bold')} to start. Run ${fmt('codebot --setup', 'bold')} to reconfigure.\n`, 'dim'));
241
270
  return config;
242
271
  }
272
+ /** Get the URL where users can get API keys for each provider */
273
+ function getKeyUrl(provider) {
274
+ switch (provider) {
275
+ case 'openai': return 'https://platform.openai.com/api-keys';
276
+ case 'anthropic': return 'https://console.anthropic.com/settings/keys';
277
+ case 'gemini': return 'https://aistudio.google.com/app/apikey';
278
+ case 'deepseek': return 'https://platform.deepseek.com/api_keys';
279
+ case 'groq': return 'https://console.groq.com/keys';
280
+ case 'mistral': return 'https://console.mistral.ai/api-keys';
281
+ case 'xai': return 'https://console.x.ai/';
282
+ default: return 'Check provider documentation';
283
+ }
284
+ }
243
285
  //# sourceMappingURL=setup.js.map
@@ -0,0 +1,36 @@
1
+ import { Tool } from '../types';
2
+ export declare class BatchEditTool implements Tool {
3
+ name: string;
4
+ description: string;
5
+ permission: Tool['permission'];
6
+ parameters: {
7
+ type: string;
8
+ properties: {
9
+ edits: {
10
+ type: string;
11
+ description: string;
12
+ items: {
13
+ type: string;
14
+ properties: {
15
+ path: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ old_string: {
20
+ type: string;
21
+ description: string;
22
+ };
23
+ new_string: {
24
+ type: string;
25
+ description: string;
26
+ };
27
+ };
28
+ required: string[];
29
+ };
30
+ };
31
+ };
32
+ required: string[];
33
+ };
34
+ execute(args: Record<string, unknown>): Promise<string>;
35
+ }
36
+ //# sourceMappingURL=batch-edit.d.ts.map
@@ -0,0 +1,122 @@
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.BatchEditTool = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ class BatchEditTool {
40
+ name = 'batch_edit';
41
+ description = 'Apply multiple find-and-replace edits across one or more files atomically. All edits are validated before any are applied. Useful for renaming, refactoring, and coordinated multi-file changes.';
42
+ permission = 'prompt';
43
+ parameters = {
44
+ type: 'object',
45
+ properties: {
46
+ edits: {
47
+ type: 'array',
48
+ description: 'Array of edit operations: [{path, old_string, new_string}, ...]',
49
+ items: {
50
+ type: 'object',
51
+ properties: {
52
+ path: { type: 'string', description: 'File path' },
53
+ old_string: { type: 'string', description: 'Exact string to find' },
54
+ new_string: { type: 'string', description: 'Replacement string' },
55
+ },
56
+ required: ['path', 'old_string', 'new_string'],
57
+ },
58
+ },
59
+ },
60
+ required: ['edits'],
61
+ };
62
+ async execute(args) {
63
+ const edits = args.edits;
64
+ if (!edits || !Array.isArray(edits) || edits.length === 0) {
65
+ return 'Error: edits array is required and must not be empty';
66
+ }
67
+ // Phase 1: Validate all edits before applying any
68
+ const errors = [];
69
+ const validated = [];
70
+ // Group edits by file so we can chain them
71
+ const byFile = new Map();
72
+ for (const edit of edits) {
73
+ if (!edit.path || !edit.old_string === undefined) {
74
+ errors.push(`Invalid edit: missing path or old_string`);
75
+ continue;
76
+ }
77
+ const filePath = path.resolve(edit.path);
78
+ if (!byFile.has(filePath))
79
+ byFile.set(filePath, []);
80
+ byFile.get(filePath).push(edit);
81
+ }
82
+ for (const [filePath, fileEdits] of byFile) {
83
+ if (!fs.existsSync(filePath)) {
84
+ errors.push(`File not found: ${filePath}`);
85
+ continue;
86
+ }
87
+ let content = fs.readFileSync(filePath, 'utf-8');
88
+ const originalContent = content;
89
+ for (const edit of fileEdits) {
90
+ const oldStr = String(edit.old_string);
91
+ const newStr = String(edit.new_string);
92
+ const count = content.split(oldStr).length - 1;
93
+ if (count === 0) {
94
+ errors.push(`String not found in ${filePath}: "${oldStr.substring(0, 60)}${oldStr.length > 60 ? '...' : ''}"`);
95
+ continue;
96
+ }
97
+ if (count > 1) {
98
+ errors.push(`String found ${count} times in ${filePath} (must be unique): "${oldStr.substring(0, 60)}${oldStr.length > 60 ? '...' : ''}"`);
99
+ continue;
100
+ }
101
+ content = content.replace(oldStr, newStr);
102
+ }
103
+ if (content !== originalContent) {
104
+ validated.push({ filePath, content: originalContent, updated: content, edit: fileEdits[0] });
105
+ }
106
+ }
107
+ if (errors.length > 0) {
108
+ return `Validation failed (no changes made):\n${errors.map(e => ` - ${e}`).join('\n')}`;
109
+ }
110
+ // Phase 2: Apply all edits atomically
111
+ const results = [];
112
+ for (const { filePath, updated } of validated) {
113
+ fs.writeFileSync(filePath, updated, 'utf-8');
114
+ results.push(filePath);
115
+ }
116
+ const fileCount = validated.length;
117
+ const editCount = edits.length;
118
+ return `Applied ${editCount} edit${editCount > 1 ? 's' : ''} across ${fileCount} file${fileCount > 1 ? 's' : ''}:\n${results.map(f => ` ✓ ${f}`).join('\n')}`;
119
+ }
120
+ }
121
+ exports.BatchEditTool = BatchEditTool;
122
+ //# sourceMappingURL=batch-edit.js.map
@@ -1,15 +1,71 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.BrowserTool = void 0;
4
37
  const cdp_1 = require("../browser/cdp");
5
38
  const child_process_1 = require("child_process");
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ const fs = __importStar(require("fs"));
6
42
  // Shared browser instance across tool calls
7
43
  let client = null;
8
44
  let debugPort = 9222;
45
+ const CHROME_DATA_DIR = path.join(os.homedir(), '.codebot', 'chrome-profile');
46
+ /** Kill any Chrome using our debug port or data dir */
47
+ function killExistingChrome() {
48
+ const { execSync } = require('child_process');
49
+ try {
50
+ if (process.platform === 'win32') {
51
+ execSync(`for /f "tokens=5" %a in ('netstat -aon ^| findstr :${debugPort}') do taskkill /F /PID %a`, { stdio: 'ignore' });
52
+ }
53
+ else {
54
+ // Kill any process listening on our debug port
55
+ execSync(`lsof -ti:${debugPort} | xargs kill -9 2>/dev/null || true`, { stdio: 'ignore' });
56
+ // Also kill any Chrome using our data dir
57
+ execSync(`pkill -f "${CHROME_DATA_DIR}" 2>/dev/null || true`, { stdio: 'ignore' });
58
+ }
59
+ }
60
+ catch {
61
+ // ignore — nothing to kill
62
+ }
63
+ // Give OS time to release the port
64
+ }
9
65
  async function ensureConnected() {
10
66
  if (client?.isConnected())
11
67
  return client;
12
- // Try connecting to existing Chrome
68
+ // Try connecting to existing Chrome with debug port
13
69
  try {
14
70
  const wsUrl = await (0, cdp_1.getDebuggerUrl)(debugPort);
15
71
  client = new cdp_1.CDPClient();
@@ -28,7 +84,9 @@ async function ensureConnected() {
28
84
  return client;
29
85
  }
30
86
  catch {
31
- // Chrome not running with debugging try to launch it
87
+ // Can't connect kill stale processes and launch fresh
88
+ killExistingChrome();
89
+ await new Promise(r => setTimeout(r, 500));
32
90
  }
33
91
  // Launch Chrome with debugging
34
92
  const chromePaths = process.platform === 'win32'
@@ -55,14 +113,49 @@ async function ensureConnected() {
55
113
  'chromium',
56
114
  ];
57
115
  let launched = false;
58
- for (const chromePath of chromePaths) {
59
- try {
60
- (0, child_process_1.execSync)(`"${chromePath}" --remote-debugging-port=${debugPort} --no-first-run --no-default-browser-check about:blank &`, { stdio: 'ignore', timeout: 5000 });
61
- launched = true;
62
- break;
116
+ // Create isolated Chrome profile dir so it doesn't conflict with user's running Chrome
117
+ fs.mkdirSync(CHROME_DATA_DIR, { recursive: true });
118
+ const chromeArgs = [
119
+ `--remote-debugging-port=${debugPort}`,
120
+ `--user-data-dir=${CHROME_DATA_DIR}`,
121
+ '--no-first-run',
122
+ '--no-default-browser-check',
123
+ '--disable-background-timer-throttling',
124
+ '--disable-backgrounding-occluded-windows',
125
+ 'about:blank',
126
+ ];
127
+ // On macOS, launch directly (not via 'open -a' which reuses existing instance)
128
+ if (process.platform === 'darwin') {
129
+ const macPaths = [
130
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
131
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
132
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
133
+ ];
134
+ for (const chromePath of macPaths) {
135
+ try {
136
+ if (fs.existsSync(chromePath)) {
137
+ const child = (0, child_process_1.spawn)(chromePath, chromeArgs, { stdio: 'ignore', detached: true });
138
+ child.unref();
139
+ launched = true;
140
+ break;
141
+ }
142
+ }
143
+ catch {
144
+ continue;
145
+ }
63
146
  }
64
- catch {
65
- continue;
147
+ }
148
+ if (!launched) {
149
+ for (const chromePath of chromePaths) {
150
+ try {
151
+ const child = (0, child_process_1.spawn)(chromePath, chromeArgs, { stdio: 'ignore', detached: true });
152
+ child.unref();
153
+ launched = true;
154
+ break;
155
+ }
156
+ catch {
157
+ continue;
158
+ }
66
159
  }
67
160
  }
68
161
  if (!launched) {
@@ -151,16 +244,30 @@ class BrowserTool {
151
244
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
152
245
  url = 'https://' + url;
153
246
  }
247
+ // Set up load event listener BEFORE navigating
248
+ const loadPromise = cdp.waitForEvent('Page.loadEventFired', 15000);
154
249
  await cdp.send('Page.navigate', { url });
155
- // Wait for page load
156
- await new Promise(r => setTimeout(r, 2000));
157
- // Get page title
250
+ // Wait for actual page load event (up to 15s)
251
+ await loadPromise;
252
+ // Extra delay for SPA hydration (React, Next.js, etc.)
253
+ await new Promise(r => setTimeout(r, 1500));
254
+ // Get final URL (after redirects) and page title
158
255
  const result = await cdp.send('Runtime.evaluate', {
159
- expression: 'document.title',
256
+ expression: 'JSON.stringify({ title: document.title, url: window.location.href })',
160
257
  returnByValue: true,
161
258
  });
162
- const title = result.result?.value || 'untitled';
163
- return `Navigated to: ${url}\nTitle: ${title}`;
259
+ const val = result.result?.value;
260
+ let title = 'untitled';
261
+ let finalUrl = url;
262
+ try {
263
+ const parsed = JSON.parse(val);
264
+ title = parsed.title || 'untitled';
265
+ finalUrl = parsed.url || url;
266
+ }
267
+ catch {
268
+ // fallback
269
+ }
270
+ return `Navigated to: ${finalUrl}\nTitle: ${title}`;
164
271
  }
165
272
  async getContent() {
166
273
  const cdp = await ensureConnected();
@@ -22,5 +22,11 @@ export declare class EditFileTool implements Tool {
22
22
  required: string[];
23
23
  };
24
24
  execute(args: Record<string, unknown>): Promise<string>;
25
+ private generateDiff;
26
+ /** Save a snapshot for undo */
27
+ private saveSnapshot;
28
+ private loadManifest;
29
+ /** Undo the last edit to a file. Returns result message. */
30
+ static undo(filePath?: string): string;
25
31
  }
26
32
  //# sourceMappingURL=edit.d.ts.map