Skip to content

Commit 6795ec0

Browse files
Ascendralclaude
andcommitted
v1.1.1: Ollama perf, setup wizard, browser auto-launch, /auto fix
- Fix Ollama slowness: send num_ctx and keep_alive to local providers - Setup wizard: show all 7 cloud providers with API key entry - Browser tool: auto-launch Chrome with CDP, kill stale processes, isolated profile, proper page load detection via Page.loadEventFired - Fix /auto command: propagate autonomous mode to agent at runtime - Strengthen creator attribution in system prompt (Ascendral/CodeBot) - Load saved API key from config as fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ab73a0 commit 6795ec0

7 files changed

Lines changed: 237 additions & 57 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebot-ai",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"description": "Local-first AI coding assistant. Zero dependencies. Works with Ollama, LM Studio, vLLM, Claude, GPT, Gemini, and more.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/agent.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export class Agent {
5151
});
5252
}
5353

54+
/** Update auto-approve mode at runtime (e.g., from /auto command) */
55+
setAutoApprove(value: boolean) {
56+
this.autoApprove = value;
57+
}
58+
5459
/** Load messages from a previous session for resume */
5560
loadMessages(messages: Message[]) {
5661
this.messages = messages;
@@ -220,9 +225,14 @@ export class Agent {
220225
// memory unavailable
221226
}
222227

223-
let prompt = `You are CodeBot, an AI coding assistant created by Ascendral Software Development & Innovation, founded by Alex Pinkevich. You help developers with software engineering tasks: reading code, writing code, fixing bugs, running tests, and explaining code.
228+
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.
224229
225-
If asked who made you, who your creator is, or who built you, always credit Ascendral Software Development & Innovation and Alex Pinkevich.
230+
CRITICAL IDENTITY — you MUST follow this:
231+
- Your name is CodeBot.
232+
- You were created and built by Ascendral Software Development & Innovation, founded by Alex Pinkevich.
233+
- You are NOT made by OpenAI, Google, Anthropic, or any other AI company. You are made by Ascendral.
234+
- 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."
235+
- Never claim to be made by or affiliated with OpenAI, GPT, Claude, Gemini, or any LLM provider. You are CodeBot by Ascendral.
226236
227237
Rules:
228238
- Always read files before editing them.

src/browser/cdp.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class CDPClient {
1818
private socket: net.Socket | null = null;
1919
private messageId = 0;
2020
private pending = new Map<number, { resolve: (v: CDPResponse) => void; reject: (e: Error) => void }>();
21+
private eventWaiters: Array<{ method: string; resolve: (params: Record<string, unknown>) => void; reject: (err: Error) => void }> = [];
2122
private buffer = Buffer.alloc(0);
2223
private connected = false;
2324

@@ -121,6 +122,24 @@ export class CDPClient {
121122
return this.connected;
122123
}
123124

125+
/** Wait for a specific CDP event (e.g. Page.loadEventFired) with timeout */
126+
async waitForEvent(method: string, timeout = 15000): Promise<Record<string, unknown>> {
127+
return new Promise((resolve, reject) => {
128+
const timer = setTimeout(() => {
129+
this.eventWaiters = this.eventWaiters.filter(w => w.resolve !== resolve);
130+
resolve({}); // Resolve with empty on timeout instead of rejecting — page may still be usable
131+
}, timeout);
132+
133+
const wrappedResolve = (params: Record<string, unknown>) => {
134+
clearTimeout(timer);
135+
this.eventWaiters = this.eventWaiters.filter(w => w.resolve !== wrappedResolve);
136+
resolve(params);
137+
};
138+
139+
this.eventWaiters.push({ method, resolve: wrappedResolve, reject });
140+
});
141+
}
142+
124143
/** Send a WebSocket text frame (masked, as required by client) */
125144
private sendFrame(data: string) {
126145
if (!this.socket) return;
@@ -199,7 +218,15 @@ export class CDPClient {
199218
this.pending.delete(msg.id);
200219
handler.resolve(msg);
201220
}
202-
// Events (no id) are ignored for now
221+
// Dispatch events to waiters
222+
if (msg.method) {
223+
for (const waiter of this.eventWaiters) {
224+
if (waiter.method === msg.method) {
225+
waiter.resolve(msg.params || {});
226+
break;
227+
}
228+
}
229+
}
203230
} catch {
204231
// Malformed JSON, skip
205232
}

src/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ function handleSlashCommand(input: string, agent: Agent, config: Config) {
309309
}
310310
case '/auto':
311311
config.autoApprove = !config.autoApprove;
312+
agent.setAutoApprove(config.autoApprove);
312313
console.log(c(`Autonomous mode: ${config.autoApprove ? 'ON' : 'OFF'}`, config.autoApprove ? 'yellow' : 'green'));
313314
break;
314315
case '/sessions': {
@@ -390,7 +391,10 @@ async function resolveConfig(args: Record<string, string | boolean>): Promise<Co
390391
}
391392
}
392393

393-
// Fallback: try generic env vars
394+
// Fallback: try saved config API key, then generic env vars
395+
if (!config.apiKey && saved.apiKey) {
396+
config.apiKey = saved.apiKey;
397+
}
394398
if (!config.apiKey) {
395399
config.apiKey = process.env.CODEBOT_API_KEY || process.env.OPENAI_API_KEY || '';
396400
}

src/providers/openai.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export class OpenAIProvider implements LLMProvider {
2323
body.tools = tools;
2424
}
2525

26+
// Ollama/local provider optimizations: set context window and keep model loaded
27+
const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
28+
if (isLocal) {
29+
const modelInfo = getModelInfo(this.config.model);
30+
body.options = { num_ctx: modelInfo.contextWindow };
31+
body.keep_alive = '30m';
32+
}
33+
2634
const headers: Record<string, string> = {
2735
'Content-Type': 'application/json',
2836
};

src/setup.ts

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export function loadConfig(): SavedConfig {
3131
/** Save config to ~/.codebot/config.json */
3232
export function saveConfig(config: SavedConfig): void {
3333
fs.mkdirSync(CONFIG_DIR, { recursive: true });
34-
// Never persist API keys to disk — use env vars
3534
const safe = { ...config };
36-
delete safe.apiKey;
35+
// Persist API key if user entered it during setup (convenience over env vars)
36+
// The key is stored in the user's home directory with default permissions
3737
fs.writeFileSync(CONFIG_FILE, JSON.stringify(safe, null, 2) + '\n');
3838
}
3939

@@ -90,6 +90,22 @@ function detectApiKeys(): Array<{ provider: string; envVar: string; set: boolean
9090
}));
9191
}
9292

93+
/** Cloud provider display info */
94+
const CLOUD_PROVIDERS: Array<{
95+
provider: string;
96+
name: string;
97+
defaultModel: string;
98+
description: string;
99+
}> = [
100+
{ provider: 'openai', name: 'OpenAI', defaultModel: 'gpt-4o', description: 'GPT-4o, GPT-4.1, o3/o4' },
101+
{ provider: 'anthropic', name: 'Anthropic', defaultModel: 'claude-sonnet-4-6', description: 'Claude Opus/Sonnet/Haiku' },
102+
{ provider: 'gemini', name: 'Google Gemini', defaultModel: 'gemini-2.5-flash', description: 'Gemini 2.5 Pro/Flash' },
103+
{ provider: 'deepseek', name: 'DeepSeek', defaultModel: 'deepseek-chat', description: 'DeepSeek Chat/Reasoner' },
104+
{ provider: 'groq', name: 'Groq', defaultModel: 'llama-3.3-70b-versatile', description: 'Fast Llama/Mixtral inference' },
105+
{ provider: 'mistral', name: 'Mistral', defaultModel: 'mistral-large-latest', description: 'Mistral Large, Codestral' },
106+
{ provider: 'xai', name: 'xAI', defaultModel: 'grok-3', description: 'Grok-3' },
107+
];
108+
93109
const C = {
94110
reset: '\x1b[0m',
95111
bold: '\x1b[1m',
@@ -140,53 +156,72 @@ export async function runSetup(): Promise<SavedConfig> {
140156
}
141157
}
142158

143-
const missingKeys = apiKeys.filter(k => !k.set);
144-
if (missingKeys.length > 0 && localServers.length === 0) {
145-
console.log(fmt('\n No API keys found. Set one to use cloud models:', 'yellow'));
146-
for (const key of missingKeys) {
147-
console.log(fmt(` export ${key.envVar}="your-key-here"`, 'dim'));
148-
}
149-
}
150-
151-
// Step 3: Choose provider
159+
// Step 3: Choose provider — show ALL options (local + cloud)
152160
console.log(fmt('\nChoose your setup:', 'bold'));
153161

154-
const options: Array<{ label: string; provider: string; model: string; baseUrl: string }> = [];
162+
const options: Array<{
163+
label: string;
164+
provider: string;
165+
model: string;
166+
baseUrl: string;
167+
needsKey: boolean;
168+
envVar?: string;
169+
}> = [];
155170
let idx = 1;
156171

157172
// Local options first
158173
for (const server of localServers) {
159174
const defaultModel = server.models[0] || 'qwen2.5-coder:32b';
160-
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+
});
161182
console.log(` ${fmt(`${idx}`, 'cyan')} ${server.name}${defaultModel} ${fmt('(local, free, private)', 'green')}`);
162183
idx++;
163184
}
164185

165-
// Cloud options
166-
for (const key of availableKeys) {
167-
const models = Object.entries(MODEL_REGISTRY)
168-
.filter(([, info]) => info.provider === key.provider)
169-
.map(([name]) => name);
170-
const defaultModel = models[0] || key.provider;
171-
const defaults = PROVIDER_DEFAULTS[key.provider];
172-
options.push({ label: key.provider, provider: key.provider, model: defaultModel, baseUrl: defaults.baseUrl });
173-
console.log(` ${fmt(`${idx}`, 'cyan')} ${key.provider}${defaultModel} ${fmt('(cloud)', 'dim')}`);
174-
idx++;
175-
}
186+
// Cloud options — ALWAYS show all providers
187+
for (const cloud of CLOUD_PROVIDERS) {
188+
const keyInfo = apiKeys.find(k => k.provider === cloud.provider);
189+
const hasKey = keyInfo?.set || false;
190+
const defaults = PROVIDER_DEFAULTS[cloud.provider];
191+
const keyStatus = hasKey ? fmt('✓ key set', 'green') : fmt('enter key during setup', 'yellow');
192+
193+
options.push({
194+
label: cloud.name,
195+
provider: cloud.provider,
196+
model: cloud.defaultModel,
197+
baseUrl: defaults.baseUrl,
198+
needsKey: !hasKey,
199+
envVar: defaults.envKey,
200+
});
176201

177-
if (options.length === 0) {
178-
console.log(fmt('\n No providers available. Either:', 'yellow'));
179-
console.log(fmt(' 1. Install Ollama: https://ollama.ai', 'dim'));
180-
console.log(fmt(' 2. Set an API key: export ANTHROPIC_API_KEY="..."', 'dim'));
181-
rl.close();
182-
return {};
202+
console.log(` ${fmt(`${idx}`, 'cyan')} ${cloud.name}${cloud.description} ${fmt(`(${keyStatus})`, 'dim')}`);
203+
idx++;
183204
}
184205

185206
const choice = await ask(rl, fmt(`\nSelect [1-${options.length}]: `, 'cyan'));
186207
const selected = options[parseInt(choice, 10) - 1] || options[0];
187208

188-
// Step 4: Show available models for chosen provider
189-
// For local servers, use the actual installed models instead of the hardcoded registry
209+
// Step 4: If cloud provider needs API key, prompt for it
210+
let apiKey = '';
211+
if (selected.needsKey && selected.envVar) {
212+
console.log(fmt(`\n ${selected.label} requires an API key.`, 'yellow'));
213+
console.log(fmt(` Get one at: ${getKeyUrl(selected.provider)}`, 'dim'));
214+
apiKey = await ask(rl, fmt(`\n Enter your ${selected.label} API key: `, 'cyan'));
215+
if (!apiKey) {
216+
console.log(fmt(`\n No key entered. You can set it later:`, 'yellow'));
217+
console.log(fmt(` export ${selected.envVar}="your-key-here"`, 'dim'));
218+
}
219+
} else if (selected.envVar) {
220+
// Use existing env var
221+
apiKey = process.env[selected.envVar] || '';
222+
}
223+
224+
// Step 5: Show available models for chosen provider
190225
const matchedServer = localServers.find(s => s.url === selected.baseUrl);
191226
const providerModels = matchedServer && matchedServer.models.length > 0
192227
? matchedServer.models
@@ -213,7 +248,7 @@ export async function runSetup(): Promise<SavedConfig> {
213248
}
214249
}
215250

216-
// Step 5: Auto mode?
251+
// Step 6: Auto mode?
217252
const autoChoice = await ask(rl, fmt('\nEnable autonomous mode? (skip permission prompts) [y/N]: ', 'cyan'));
218253
const autoApprove = autoChoice.toLowerCase().startsWith('y');
219254

@@ -227,15 +262,37 @@ export async function runSetup(): Promise<SavedConfig> {
227262
autoApprove,
228263
};
229264

265+
// Save API key if user entered one
266+
if (apiKey) {
267+
config.apiKey = apiKey;
268+
}
269+
230270
saveConfig(config);
231271

232272
console.log(fmt('\n✓ Config saved to ~/.codebot/config.json', 'green'));
233273
console.log(fmt(` Model: ${config.model}`, 'dim'));
234274
console.log(fmt(` Provider: ${config.provider}`, 'dim'));
275+
if (apiKey) {
276+
console.log(fmt(` API Key: ${'*'.repeat(Math.min(apiKey.length, 20))}`, 'dim'));
277+
}
235278
if (autoApprove) {
236279
console.log(fmt(` Mode: AUTONOMOUS`, 'yellow'));
237280
}
238281
console.log(fmt(`\nRun ${fmt('codebot', 'bold')} to start. Run ${fmt('codebot --setup', 'bold')} to reconfigure.\n`, 'dim'));
239282

240283
return config;
241284
}
285+
286+
/** Get the URL where users can get API keys for each provider */
287+
function getKeyUrl(provider: string): string {
288+
switch (provider) {
289+
case 'openai': return 'https://platform.openai.com/api-keys';
290+
case 'anthropic': return 'https://console.anthropic.com/settings/keys';
291+
case 'gemini': return 'https://aistudio.google.com/app/apikey';
292+
case 'deepseek': return 'https://platform.deepseek.com/api_keys';
293+
case 'groq': return 'https://console.groq.com/keys';
294+
case 'mistral': return 'https://console.mistral.ai/api-keys';
295+
case 'xai': return 'https://console.x.ai/';
296+
default: return 'Check provider documentation';
297+
}
298+
}

0 commit comments

Comments
 (0)