codebot-ai 1.4.3 → 1.5.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.d.ts +2 -0
- package/dist/agent.js +153 -29
- package/dist/cache.d.ts +36 -0
- package/dist/cache.js +106 -0
- package/dist/cli.js +1 -1
- package/dist/history.d.ts +2 -2
- package/dist/history.js +43 -17
- package/dist/rate-limiter.d.ts +23 -0
- package/dist/rate-limiter.js +52 -0
- package/dist/tools/browser.js +18 -3
- package/dist/tools/code-analysis.d.ts +1 -0
- package/dist/tools/code-analysis.js +1 -0
- package/dist/tools/code-review.d.ts +1 -0
- package/dist/tools/code-review.js +1 -0
- package/dist/tools/glob.d.ts +1 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.d.ts +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/image-info.d.ts +1 -0
- package/dist/tools/image-info.js +1 -0
- package/dist/tools/read.d.ts +1 -0
- package/dist/tools/read.js +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
package/dist/agent.js
CHANGED
|
@@ -43,6 +43,51 @@ const repo_map_1 = require("./context/repo-map");
|
|
|
43
43
|
const memory_1 = require("./memory");
|
|
44
44
|
const registry_1 = require("./providers/registry");
|
|
45
45
|
const plugins_1 = require("./plugins");
|
|
46
|
+
const cache_1 = require("./cache");
|
|
47
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
48
|
+
/** Lightweight schema validation — returns error string or null if valid */
|
|
49
|
+
function validateToolArgs(args, schema) {
|
|
50
|
+
const props = schema.properties;
|
|
51
|
+
const required = schema.required;
|
|
52
|
+
if (!props)
|
|
53
|
+
return null;
|
|
54
|
+
// Check required fields exist
|
|
55
|
+
if (required) {
|
|
56
|
+
for (const field of required) {
|
|
57
|
+
if (args[field] === undefined || args[field] === null) {
|
|
58
|
+
return `missing required field '${field}'`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check types match for provided fields
|
|
63
|
+
for (const [key, value] of Object.entries(args)) {
|
|
64
|
+
const propSchema = props[key];
|
|
65
|
+
if (!propSchema)
|
|
66
|
+
continue; // extra fields are OK
|
|
67
|
+
const expectedType = propSchema.type;
|
|
68
|
+
if (!expectedType)
|
|
69
|
+
continue;
|
|
70
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
71
|
+
if (expectedType === 'string' && actualType !== 'string') {
|
|
72
|
+
return `field '${key}' expected string, got ${actualType}`;
|
|
73
|
+
}
|
|
74
|
+
if (expectedType === 'number' && actualType !== 'number') {
|
|
75
|
+
return `field '${key}' expected number, got ${actualType}`;
|
|
76
|
+
}
|
|
77
|
+
if (expectedType === 'boolean' && actualType !== 'boolean') {
|
|
78
|
+
return `field '${key}' expected boolean, got ${actualType}`;
|
|
79
|
+
}
|
|
80
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
81
|
+
return `field '${key}' expected array, got ${actualType}`;
|
|
82
|
+
}
|
|
83
|
+
if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value))) {
|
|
84
|
+
return `field '${key}' expected object, got ${actualType}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/** Tools that use shared global state and must not run concurrently */
|
|
90
|
+
const SEQUENTIAL_TOOLS = new Set(['browser']);
|
|
46
91
|
class Agent {
|
|
47
92
|
provider;
|
|
48
93
|
tools;
|
|
@@ -51,6 +96,8 @@ class Agent {
|
|
|
51
96
|
maxIterations;
|
|
52
97
|
autoApprove;
|
|
53
98
|
model;
|
|
99
|
+
cache;
|
|
100
|
+
rateLimiter;
|
|
54
101
|
askPermission;
|
|
55
102
|
onMessage;
|
|
56
103
|
constructor(opts) {
|
|
@@ -62,6 +109,8 @@ class Agent {
|
|
|
62
109
|
this.autoApprove = opts.autoApprove || false;
|
|
63
110
|
this.askPermission = opts.askPermission || defaultAskPermission;
|
|
64
111
|
this.onMessage = opts.onMessage;
|
|
112
|
+
this.cache = new cache_1.ToolCache();
|
|
113
|
+
this.rateLimiter = new rate_limiter_1.RateLimiter();
|
|
65
114
|
// Load plugins
|
|
66
115
|
try {
|
|
67
116
|
const plugins = (0, plugins_1.loadPlugins)(process.cwd());
|
|
@@ -179,16 +228,12 @@ class Agent {
|
|
|
179
228
|
yield { type: 'done' };
|
|
180
229
|
return;
|
|
181
230
|
}
|
|
182
|
-
|
|
231
|
+
const prepared = [];
|
|
183
232
|
for (const tc of toolCalls) {
|
|
184
233
|
const toolName = tc.function.name;
|
|
185
234
|
const tool = this.tools.get(toolName);
|
|
186
235
|
if (!tool) {
|
|
187
|
-
|
|
188
|
-
const toolMsg = { role: 'tool', content: errResult, tool_call_id: tc.id };
|
|
189
|
-
this.messages.push(toolMsg);
|
|
190
|
-
this.onMessage?.(toolMsg);
|
|
191
|
-
yield { type: 'tool_result', toolResult: { name: toolName, result: errResult, is_error: true } };
|
|
236
|
+
prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
|
|
192
237
|
continue;
|
|
193
238
|
}
|
|
194
239
|
let args;
|
|
@@ -196,41 +241,111 @@ class Agent {
|
|
|
196
241
|
args = JSON.parse(tc.function.arguments);
|
|
197
242
|
}
|
|
198
243
|
catch {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
244
|
+
prepared.push({ tc, tool, args: {}, denied: false, error: `Error: Invalid JSON arguments for ${toolName}` });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Arg validation against schema
|
|
248
|
+
const validationError = validateToolArgs(args, tool.parameters);
|
|
249
|
+
if (validationError) {
|
|
250
|
+
prepared.push({ tc, tool, args, denied: false, error: `Error: ${validationError} for ${toolName}` });
|
|
204
251
|
continue;
|
|
205
252
|
}
|
|
206
253
|
yield { type: 'tool_call', toolCall: { name: toolName, args } };
|
|
207
|
-
// Permission check
|
|
254
|
+
// Permission check (sequential — needs user interaction)
|
|
208
255
|
const needsPermission = tool.permission === 'always-ask' ||
|
|
209
256
|
(tool.permission === 'prompt' && !this.autoApprove);
|
|
257
|
+
let denied = false;
|
|
210
258
|
if (needsPermission) {
|
|
211
259
|
const approved = await this.askPermission(toolName, args);
|
|
212
260
|
if (!approved) {
|
|
213
|
-
|
|
214
|
-
this.messages.push(toolMsg);
|
|
215
|
-
this.onMessage?.(toolMsg);
|
|
216
|
-
yield { type: 'tool_result', toolResult: { name: toolName, result: 'Permission denied.' } };
|
|
217
|
-
continue;
|
|
261
|
+
denied = true;
|
|
218
262
|
}
|
|
219
263
|
}
|
|
220
|
-
|
|
264
|
+
prepared.push({ tc, tool, args, denied });
|
|
265
|
+
}
|
|
266
|
+
const results = new Array(prepared.length);
|
|
267
|
+
// Immediately resolve errors and denials
|
|
268
|
+
const toExecute = [];
|
|
269
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
270
|
+
const prep = prepared[idx];
|
|
271
|
+
if (prep.error) {
|
|
272
|
+
results[idx] = { content: prep.error, is_error: true };
|
|
273
|
+
}
|
|
274
|
+
else if (prep.denied) {
|
|
275
|
+
results[idx] = { content: 'Permission denied by user.' };
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
toExecute.push({ index: idx, prep });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Split into parallel-safe and sequential (browser) groups
|
|
282
|
+
const parallelBatch = [];
|
|
283
|
+
const sequentialBatch = [];
|
|
284
|
+
for (const item of toExecute) {
|
|
285
|
+
if (SEQUENTIAL_TOOLS.has(item.prep.tc.function.name)) {
|
|
286
|
+
sequentialBatch.push(item);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
parallelBatch.push(item);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Helper to execute a single tool with cache + rate limiting
|
|
293
|
+
const executeTool = async (prep) => {
|
|
294
|
+
const toolName = prep.tc.function.name;
|
|
295
|
+
// Check cache first
|
|
296
|
+
if (prep.tool.cacheable) {
|
|
297
|
+
const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
|
|
298
|
+
const cached = this.cache.get(cacheKey);
|
|
299
|
+
if (cached !== null) {
|
|
300
|
+
return { content: cached };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Rate limit
|
|
304
|
+
await this.rateLimiter.throttle(toolName);
|
|
221
305
|
try {
|
|
222
|
-
const output = await tool.execute(args);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
306
|
+
const output = await prep.tool.execute(prep.args);
|
|
307
|
+
// Store in cache for cacheable tools
|
|
308
|
+
if (prep.tool.cacheable) {
|
|
309
|
+
const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
|
|
310
|
+
this.cache.set(cache_1.ToolCache.key(toolName, prep.args), output, ttl);
|
|
311
|
+
}
|
|
312
|
+
// Invalidate cache on write operations
|
|
313
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
|
|
314
|
+
const filePath = prep.args.path;
|
|
315
|
+
if (filePath)
|
|
316
|
+
this.cache.invalidate(filePath);
|
|
317
|
+
}
|
|
318
|
+
return { content: output };
|
|
227
319
|
}
|
|
228
320
|
catch (err) {
|
|
229
321
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
322
|
+
return { content: `Error: ${errMsg}`, is_error: true };
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
// Execute parallel batch concurrently
|
|
326
|
+
if (parallelBatch.length > 0) {
|
|
327
|
+
const promises = parallelBatch.map(async ({ index, prep }) => {
|
|
328
|
+
results[index] = await executeTool(prep);
|
|
329
|
+
});
|
|
330
|
+
await Promise.allSettled(promises);
|
|
331
|
+
}
|
|
332
|
+
// Execute sequential batch one at a time
|
|
333
|
+
for (const { index, prep } of sequentialBatch) {
|
|
334
|
+
results[index] = await executeTool(prep);
|
|
335
|
+
}
|
|
336
|
+
// ── Phase 3: Push results in original order + yield events ──
|
|
337
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
338
|
+
const prep = prepared[idx];
|
|
339
|
+
const output = results[idx] || { content: 'Error: execution failed', is_error: true };
|
|
340
|
+
const toolName = prep.tc.function.name;
|
|
341
|
+
const toolMsg = { role: 'tool', content: output.content, tool_call_id: prep.tc.id };
|
|
342
|
+
this.messages.push(toolMsg);
|
|
343
|
+
this.onMessage?.(toolMsg);
|
|
344
|
+
if (prep.denied) {
|
|
345
|
+
yield { type: 'tool_result', toolResult: { name: toolName, result: 'Permission denied.' } };
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
yield { type: 'tool_result', toolResult: { name: toolName, result: output.content, is_error: output.is_error } };
|
|
234
349
|
}
|
|
235
350
|
}
|
|
236
351
|
// Compact after tool results if needed
|
|
@@ -391,6 +506,7 @@ ${this.tools.all().map(t => `- ${t.name}: ${t.description}`).join('\n')}`;
|
|
|
391
506
|
}
|
|
392
507
|
}
|
|
393
508
|
exports.Agent = Agent;
|
|
509
|
+
const PERMISSION_TIMEOUT_MS = 30_000;
|
|
394
510
|
async function defaultAskPermission(tool, args) {
|
|
395
511
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
396
512
|
const summary = Object.entries(args)
|
|
@@ -399,11 +515,19 @@ async function defaultAskPermission(tool, args) {
|
|
|
399
515
|
return ` ${k}: ${val}`;
|
|
400
516
|
})
|
|
401
517
|
.join('\n');
|
|
402
|
-
|
|
403
|
-
rl.question(`\n⚡ ${tool}\n${summary}\nAllow? [y/N] `, answer => {
|
|
518
|
+
const userResponse = new Promise(resolve => {
|
|
519
|
+
rl.question(`\n⚡ ${tool}\n${summary}\nAllow? [y/N] (${PERMISSION_TIMEOUT_MS / 1000}s timeout) `, answer => {
|
|
404
520
|
rl.close();
|
|
405
521
|
resolve(answer.toLowerCase().startsWith('y'));
|
|
406
522
|
});
|
|
407
523
|
});
|
|
524
|
+
const timeout = new Promise(resolve => {
|
|
525
|
+
setTimeout(() => {
|
|
526
|
+
rl.close();
|
|
527
|
+
process.stdout.write('\n⏱ Permission timed out — denied by default.\n');
|
|
528
|
+
resolve(false);
|
|
529
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
530
|
+
});
|
|
531
|
+
return Promise.race([userResponse, timeout]);
|
|
408
532
|
}
|
|
409
533
|
//# sourceMappingURL=agent.js.map
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU Tool Result Cache with TTL
|
|
3
|
+
*
|
|
4
|
+
* Caches results from read-only tools (read_file, grep, glob, etc.)
|
|
5
|
+
* to avoid redundant I/O. Invalidates on writes to affected paths.
|
|
6
|
+
*
|
|
7
|
+
* @since v1.5.0
|
|
8
|
+
*/
|
|
9
|
+
export declare class ToolCache {
|
|
10
|
+
private cache;
|
|
11
|
+
private maxSize;
|
|
12
|
+
private currentSize;
|
|
13
|
+
/** Default TTLs per tool (ms) */
|
|
14
|
+
static readonly TTL: Record<string, number>;
|
|
15
|
+
constructor(maxSizeBytes?: number);
|
|
16
|
+
/** Build a deterministic cache key from tool name + sorted args */
|
|
17
|
+
static key(toolName: string, args: Record<string, unknown>): string;
|
|
18
|
+
/** Get a cached value, or null if expired/missing */
|
|
19
|
+
get(key: string): string | null;
|
|
20
|
+
/** Store a value with TTL */
|
|
21
|
+
set(key: string, value: string, ttlMs: number): void;
|
|
22
|
+
/** Remove a specific key */
|
|
23
|
+
private delete;
|
|
24
|
+
/**
|
|
25
|
+
* Invalidate all cache entries whose key contains the given substring.
|
|
26
|
+
* Used when a file is written/edited — invalidate any cached reads for that path.
|
|
27
|
+
*/
|
|
28
|
+
invalidate(pattern: string): void;
|
|
29
|
+
/** Clear entire cache */
|
|
30
|
+
clear(): void;
|
|
31
|
+
/** Number of entries currently cached */
|
|
32
|
+
get size(): number;
|
|
33
|
+
/** Total bytes currently cached */
|
|
34
|
+
get bytes(): number;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=cache.d.ts.map
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LRU Tool Result Cache with TTL
|
|
4
|
+
*
|
|
5
|
+
* Caches results from read-only tools (read_file, grep, glob, etc.)
|
|
6
|
+
* to avoid redundant I/O. Invalidates on writes to affected paths.
|
|
7
|
+
*
|
|
8
|
+
* @since v1.5.0
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ToolCache = void 0;
|
|
12
|
+
class ToolCache {
|
|
13
|
+
cache = new Map();
|
|
14
|
+
maxSize;
|
|
15
|
+
currentSize = 0;
|
|
16
|
+
/** Default TTLs per tool (ms) */
|
|
17
|
+
static TTL = {
|
|
18
|
+
read_file: 30_000,
|
|
19
|
+
grep: 30_000,
|
|
20
|
+
glob: 30_000,
|
|
21
|
+
code_analysis: 60_000,
|
|
22
|
+
code_review: 60_000,
|
|
23
|
+
image_info: 60_000,
|
|
24
|
+
};
|
|
25
|
+
constructor(maxSizeBytes = 50 * 1024 * 1024) {
|
|
26
|
+
this.maxSize = maxSizeBytes;
|
|
27
|
+
}
|
|
28
|
+
/** Build a deterministic cache key from tool name + sorted args */
|
|
29
|
+
static key(toolName, args) {
|
|
30
|
+
const sorted = Object.keys(args)
|
|
31
|
+
.sort()
|
|
32
|
+
.map(k => `${k}=${JSON.stringify(args[k])}`)
|
|
33
|
+
.join('&');
|
|
34
|
+
return `${toolName}:${sorted}`;
|
|
35
|
+
}
|
|
36
|
+
/** Get a cached value, or null if expired/missing */
|
|
37
|
+
get(key) {
|
|
38
|
+
const entry = this.cache.get(key);
|
|
39
|
+
if (!entry)
|
|
40
|
+
return null;
|
|
41
|
+
if (Date.now() > entry.expires) {
|
|
42
|
+
this.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Move to end (most recently used) — Map preserves insertion order
|
|
46
|
+
this.cache.delete(key);
|
|
47
|
+
this.cache.set(key, entry);
|
|
48
|
+
return entry.value;
|
|
49
|
+
}
|
|
50
|
+
/** Store a value with TTL */
|
|
51
|
+
set(key, value, ttlMs) {
|
|
52
|
+
// Delete existing entry first (update size tracking)
|
|
53
|
+
this.delete(key);
|
|
54
|
+
const size = key.length + value.length;
|
|
55
|
+
// Don't cache if single entry exceeds 10% of max
|
|
56
|
+
if (size > this.maxSize * 0.1)
|
|
57
|
+
return;
|
|
58
|
+
// Evict LRU entries until we have room
|
|
59
|
+
while (this.currentSize + size > this.maxSize && this.cache.size > 0) {
|
|
60
|
+
const oldest = this.cache.keys().next().value;
|
|
61
|
+
if (oldest !== undefined) {
|
|
62
|
+
this.delete(oldest);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.cache.set(key, {
|
|
66
|
+
value,
|
|
67
|
+
expires: Date.now() + ttlMs,
|
|
68
|
+
size,
|
|
69
|
+
});
|
|
70
|
+
this.currentSize += size;
|
|
71
|
+
}
|
|
72
|
+
/** Remove a specific key */
|
|
73
|
+
delete(key) {
|
|
74
|
+
const entry = this.cache.get(key);
|
|
75
|
+
if (entry) {
|
|
76
|
+
this.currentSize -= entry.size;
|
|
77
|
+
this.cache.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Invalidate all cache entries whose key contains the given substring.
|
|
82
|
+
* Used when a file is written/edited — invalidate any cached reads for that path.
|
|
83
|
+
*/
|
|
84
|
+
invalidate(pattern) {
|
|
85
|
+
for (const key of [...this.cache.keys()]) {
|
|
86
|
+
if (key.includes(pattern)) {
|
|
87
|
+
this.delete(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Clear entire cache */
|
|
92
|
+
clear() {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
this.currentSize = 0;
|
|
95
|
+
}
|
|
96
|
+
/** Number of entries currently cached */
|
|
97
|
+
get size() {
|
|
98
|
+
return this.cache.size;
|
|
99
|
+
}
|
|
100
|
+
/** Total bytes currently cached */
|
|
101
|
+
get bytes() {
|
|
102
|
+
return this.currentSize;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.ToolCache = ToolCache;
|
|
106
|
+
//# sourceMappingURL=cache.js.map
|
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ const setup_1 = require("./setup");
|
|
|
44
44
|
const banner_1 = require("./banner");
|
|
45
45
|
const tools_1 = require("./tools");
|
|
46
46
|
const scheduler_1 = require("./scheduler");
|
|
47
|
-
const VERSION = '1.
|
|
47
|
+
const VERSION = '1.5.0';
|
|
48
48
|
// Session-wide token tracking
|
|
49
49
|
let sessionTokens = { input: 0, output: 0, total: 0 };
|
|
50
50
|
const C = {
|
package/dist/history.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export declare class SessionManager {
|
|
|
15
15
|
getId(): string;
|
|
16
16
|
/** Append a message to the session file */
|
|
17
17
|
save(message: Message): void;
|
|
18
|
-
/** Save all messages (overwrite) */
|
|
18
|
+
/** Save all messages (atomic overwrite via temp file + rename) */
|
|
19
19
|
saveAll(messages: Message[]): void;
|
|
20
|
-
/** Load messages from a session file */
|
|
20
|
+
/** Load messages from a session file (skips malformed lines) */
|
|
21
21
|
load(): Message[];
|
|
22
22
|
/** List recent sessions */
|
|
23
23
|
static list(limit?: number): SessionMeta[];
|
package/dist/history.js
CHANGED
|
@@ -54,31 +54,57 @@ class SessionManager {
|
|
|
54
54
|
}
|
|
55
55
|
/** Append a message to the session file */
|
|
56
56
|
save(message) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
try {
|
|
58
|
+
const line = JSON.stringify({
|
|
59
|
+
...message,
|
|
60
|
+
_ts: new Date().toISOString(),
|
|
61
|
+
_model: this.model,
|
|
62
|
+
});
|
|
63
|
+
fs.appendFileSync(this.filePath, line + '\n');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Don't crash on write failure — session persistence is best-effort
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
|
-
/** Save all messages (overwrite) */
|
|
69
|
+
/** Save all messages (atomic overwrite via temp file + rename) */
|
|
65
70
|
saveAll(messages) {
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
try {
|
|
72
|
+
const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
|
|
73
|
+
const tmpPath = this.filePath + '.tmp';
|
|
74
|
+
fs.writeFileSync(tmpPath, lines.join('\n') + '\n');
|
|
75
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Don't crash — session persistence is best-effort
|
|
79
|
+
}
|
|
68
80
|
}
|
|
69
|
-
/** Load messages from a session file */
|
|
81
|
+
/** Load messages from a session file (skips malformed lines) */
|
|
70
82
|
load() {
|
|
71
83
|
if (!fs.existsSync(this.filePath))
|
|
72
84
|
return [];
|
|
73
|
-
|
|
85
|
+
let content;
|
|
86
|
+
try {
|
|
87
|
+
content = fs.readFileSync(this.filePath, 'utf-8').trim();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
74
92
|
if (!content)
|
|
75
93
|
return [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
const messages = [];
|
|
95
|
+
for (const line of content.split('\n')) {
|
|
96
|
+
try {
|
|
97
|
+
const obj = JSON.parse(line);
|
|
98
|
+
delete obj._ts;
|
|
99
|
+
delete obj._model;
|
|
100
|
+
messages.push(obj);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Skip malformed line — don't crash the whole load
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return messages;
|
|
82
108
|
}
|
|
83
109
|
/** List recent sessions */
|
|
84
110
|
static list(limit = 10) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Simple sliding-window throttle that enforces minimum intervals between
|
|
5
|
+
* calls to the same tool. Prevents hammering web services and self-DOS.
|
|
6
|
+
*
|
|
7
|
+
* @since v1.5.0
|
|
8
|
+
*/
|
|
9
|
+
export declare class RateLimiter {
|
|
10
|
+
private lastCall;
|
|
11
|
+
/** Minimum interval (ms) between calls per tool */
|
|
12
|
+
private limits;
|
|
13
|
+
constructor(overrides?: Record<string, number>);
|
|
14
|
+
/** Wait if needed to respect the tool's rate limit */
|
|
15
|
+
throttle(toolName: string): Promise<void>;
|
|
16
|
+
/** Get the configured limit for a tool (0 = no limit) */
|
|
17
|
+
getLimit(toolName: string): number;
|
|
18
|
+
/** Update a tool's rate limit at runtime */
|
|
19
|
+
setLimit(toolName: string, intervalMs: number): void;
|
|
20
|
+
/** Reset all tracking (useful for tests) */
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-tool Rate Limiter
|
|
4
|
+
*
|
|
5
|
+
* Simple sliding-window throttle that enforces minimum intervals between
|
|
6
|
+
* calls to the same tool. Prevents hammering web services and self-DOS.
|
|
7
|
+
*
|
|
8
|
+
* @since v1.5.0
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.RateLimiter = void 0;
|
|
12
|
+
class RateLimiter {
|
|
13
|
+
lastCall = new Map();
|
|
14
|
+
/** Minimum interval (ms) between calls per tool */
|
|
15
|
+
limits = {
|
|
16
|
+
browser: 200,
|
|
17
|
+
web_fetch: 500,
|
|
18
|
+
web_search: 1000,
|
|
19
|
+
execute: 100,
|
|
20
|
+
};
|
|
21
|
+
constructor(overrides) {
|
|
22
|
+
if (overrides) {
|
|
23
|
+
Object.assign(this.limits, overrides);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Wait if needed to respect the tool's rate limit */
|
|
27
|
+
async throttle(toolName) {
|
|
28
|
+
const limit = this.limits[toolName];
|
|
29
|
+
if (!limit)
|
|
30
|
+
return; // no limit for this tool
|
|
31
|
+
const last = this.lastCall.get(toolName) || 0;
|
|
32
|
+
const elapsed = Date.now() - last;
|
|
33
|
+
if (elapsed < limit) {
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, limit - elapsed));
|
|
35
|
+
}
|
|
36
|
+
this.lastCall.set(toolName, Date.now());
|
|
37
|
+
}
|
|
38
|
+
/** Get the configured limit for a tool (0 = no limit) */
|
|
39
|
+
getLimit(toolName) {
|
|
40
|
+
return this.limits[toolName] || 0;
|
|
41
|
+
}
|
|
42
|
+
/** Update a tool's rate limit at runtime */
|
|
43
|
+
setLimit(toolName, intervalMs) {
|
|
44
|
+
this.limits[toolName] = intervalMs;
|
|
45
|
+
}
|
|
46
|
+
/** Reset all tracking (useful for tests) */
|
|
47
|
+
reset() {
|
|
48
|
+
this.lastCall.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.RateLimiter = RateLimiter;
|
|
52
|
+
//# sourceMappingURL=rate-limiter.js.map
|
package/dist/tools/browser.js
CHANGED
|
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
// Shared browser instance across tool calls
|
|
43
43
|
let client = null;
|
|
44
44
|
let debugPort = 9222;
|
|
45
|
+
let connectingPromise = null;
|
|
45
46
|
const CHROME_DATA_DIR = path.join(os.homedir(), '.codebot', 'chrome-profile');
|
|
46
47
|
/** Kill any Chrome using our debug port or data dir (but NEVER kill ourselves) */
|
|
47
48
|
function killExistingChrome() {
|
|
@@ -71,8 +72,21 @@ function killExistingChrome() {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
async function ensureConnected() {
|
|
75
|
+
// Fast path: already connected
|
|
74
76
|
if (client?.isConnected())
|
|
75
77
|
return client;
|
|
78
|
+
// Mutex: if another call is already connecting, reuse that promise
|
|
79
|
+
if (connectingPromise)
|
|
80
|
+
return connectingPromise;
|
|
81
|
+
connectingPromise = doConnect();
|
|
82
|
+
try {
|
|
83
|
+
return await connectingPromise;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
connectingPromise = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function doConnect() {
|
|
76
90
|
// Try connecting to existing Chrome with debug port
|
|
77
91
|
try {
|
|
78
92
|
const wsUrl = await (0, cdp_1.getDebuggerUrl)(debugPort);
|
|
@@ -172,10 +186,11 @@ async function ensureConnected() {
|
|
|
172
186
|
'Or on macOS:\n' +
|
|
173
187
|
` /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=${debugPort}`);
|
|
174
188
|
}
|
|
175
|
-
// Wait for Chrome to start
|
|
176
|
-
|
|
189
|
+
// Wait for Chrome to start — exponential backoff: 500ms, 1s, 2s, 4s
|
|
190
|
+
const backoffDelays = [500, 1000, 2000, 4000, 4000, 4000];
|
|
191
|
+
for (let i = 0; i < backoffDelays.length; i++) {
|
|
177
192
|
try {
|
|
178
|
-
await new Promise(r => setTimeout(r,
|
|
193
|
+
await new Promise(r => setTimeout(r, backoffDelays[i]));
|
|
179
194
|
const wsUrl = await (0, cdp_1.getDebuggerUrl)(debugPort);
|
|
180
195
|
client = new cdp_1.CDPClient();
|
|
181
196
|
await client.connect(wsUrl);
|
|
@@ -40,6 +40,7 @@ class CodeAnalysisTool {
|
|
|
40
40
|
name = 'code_analysis';
|
|
41
41
|
description = 'Analyze code structure. Actions: symbols (list classes/functions/exports), imports (list imports), outline (file structure), references (find where a symbol is used).';
|
|
42
42
|
permission = 'auto';
|
|
43
|
+
cacheable = true;
|
|
43
44
|
parameters = {
|
|
44
45
|
type: 'object',
|
|
45
46
|
properties: {
|
|
@@ -52,6 +52,7 @@ class CodeReviewTool {
|
|
|
52
52
|
name = 'code_review';
|
|
53
53
|
description = 'Review code for security issues, complexity, and code smells. Actions: security, complexity, review (full).';
|
|
54
54
|
permission = 'auto';
|
|
55
|
+
cacheable = true;
|
|
55
56
|
parameters = {
|
|
56
57
|
type: 'object',
|
|
57
58
|
properties: {
|
package/dist/tools/glob.d.ts
CHANGED
package/dist/tools/glob.js
CHANGED
package/dist/tools/grep.d.ts
CHANGED
package/dist/tools/grep.js
CHANGED
package/dist/tools/image-info.js
CHANGED
package/dist/tools/read.d.ts
CHANGED
package/dist/tools/read.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface Tool {
|
|
|
23
23
|
description: string;
|
|
24
24
|
parameters: Record<string, unknown>;
|
|
25
25
|
permission: 'auto' | 'prompt' | 'always-ask';
|
|
26
|
+
cacheable?: boolean;
|
|
26
27
|
execute(args: Record<string, unknown>): Promise<string>;
|
|
27
28
|
}
|
|
28
29
|
export interface ProviderConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebot-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Zero-dependency autonomous AI agent. Code, browse, search, automate. Works with any LLM — Ollama, Claude, GPT, Gemini, DeepSeek, Groq, Mistral, Grok.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|