codebot-ai 1.4.3 → 1.6.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 +3 -0
- package/dist/agent.js +165 -29
- package/dist/audit.d.ts +39 -0
- package/dist/audit.js +157 -0
- 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/mcp.js +54 -3
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +71 -7
- package/dist/plugins.d.ts +0 -14
- package/dist/plugins.js +27 -14
- package/dist/rate-limiter.d.ts +23 -0
- package/dist/rate-limiter.js +52 -0
- package/dist/secrets.d.ts +26 -0
- package/dist/secrets.js +86 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.js +167 -0
- package/dist/tools/batch-edit.js +20 -1
- 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/edit.js +29 -5
- package/dist/tools/execute.js +53 -1
- 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/package-manager.js +36 -0
- package/dist/tools/read.d.ts +1 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
package/dist/agent.js
CHANGED
|
@@ -43,6 +43,52 @@ 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
|
+
const audit_1 = require("./audit");
|
|
49
|
+
/** Lightweight schema validation — returns error string or null if valid */
|
|
50
|
+
function validateToolArgs(args, schema) {
|
|
51
|
+
const props = schema.properties;
|
|
52
|
+
const required = schema.required;
|
|
53
|
+
if (!props)
|
|
54
|
+
return null;
|
|
55
|
+
// Check required fields exist
|
|
56
|
+
if (required) {
|
|
57
|
+
for (const field of required) {
|
|
58
|
+
if (args[field] === undefined || args[field] === null) {
|
|
59
|
+
return `missing required field '${field}'`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Check types match for provided fields
|
|
64
|
+
for (const [key, value] of Object.entries(args)) {
|
|
65
|
+
const propSchema = props[key];
|
|
66
|
+
if (!propSchema)
|
|
67
|
+
continue; // extra fields are OK
|
|
68
|
+
const expectedType = propSchema.type;
|
|
69
|
+
if (!expectedType)
|
|
70
|
+
continue;
|
|
71
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
72
|
+
if (expectedType === 'string' && actualType !== 'string') {
|
|
73
|
+
return `field '${key}' expected string, got ${actualType}`;
|
|
74
|
+
}
|
|
75
|
+
if (expectedType === 'number' && actualType !== 'number') {
|
|
76
|
+
return `field '${key}' expected number, got ${actualType}`;
|
|
77
|
+
}
|
|
78
|
+
if (expectedType === 'boolean' && actualType !== 'boolean') {
|
|
79
|
+
return `field '${key}' expected boolean, got ${actualType}`;
|
|
80
|
+
}
|
|
81
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
82
|
+
return `field '${key}' expected array, got ${actualType}`;
|
|
83
|
+
}
|
|
84
|
+
if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value))) {
|
|
85
|
+
return `field '${key}' expected object, got ${actualType}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** Tools that use shared global state and must not run concurrently */
|
|
91
|
+
const SEQUENTIAL_TOOLS = new Set(['browser']);
|
|
46
92
|
class Agent {
|
|
47
93
|
provider;
|
|
48
94
|
tools;
|
|
@@ -51,6 +97,9 @@ class Agent {
|
|
|
51
97
|
maxIterations;
|
|
52
98
|
autoApprove;
|
|
53
99
|
model;
|
|
100
|
+
cache;
|
|
101
|
+
rateLimiter;
|
|
102
|
+
auditLogger;
|
|
54
103
|
askPermission;
|
|
55
104
|
onMessage;
|
|
56
105
|
constructor(opts) {
|
|
@@ -62,6 +111,9 @@ class Agent {
|
|
|
62
111
|
this.autoApprove = opts.autoApprove || false;
|
|
63
112
|
this.askPermission = opts.askPermission || defaultAskPermission;
|
|
64
113
|
this.onMessage = opts.onMessage;
|
|
114
|
+
this.cache = new cache_1.ToolCache();
|
|
115
|
+
this.rateLimiter = new rate_limiter_1.RateLimiter();
|
|
116
|
+
this.auditLogger = new audit_1.AuditLogger();
|
|
65
117
|
// Load plugins
|
|
66
118
|
try {
|
|
67
119
|
const plugins = (0, plugins_1.loadPlugins)(process.cwd());
|
|
@@ -179,16 +231,12 @@ class Agent {
|
|
|
179
231
|
yield { type: 'done' };
|
|
180
232
|
return;
|
|
181
233
|
}
|
|
182
|
-
|
|
234
|
+
const prepared = [];
|
|
183
235
|
for (const tc of toolCalls) {
|
|
184
236
|
const toolName = tc.function.name;
|
|
185
237
|
const tool = this.tools.get(toolName);
|
|
186
238
|
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 } };
|
|
239
|
+
prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
|
|
192
240
|
continue;
|
|
193
241
|
}
|
|
194
242
|
let args;
|
|
@@ -196,41 +244,120 @@ class Agent {
|
|
|
196
244
|
args = JSON.parse(tc.function.arguments);
|
|
197
245
|
}
|
|
198
246
|
catch {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
247
|
+
prepared.push({ tc, tool, args: {}, denied: false, error: `Error: Invalid JSON arguments for ${toolName}` });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// Arg validation against schema
|
|
251
|
+
const validationError = validateToolArgs(args, tool.parameters);
|
|
252
|
+
if (validationError) {
|
|
253
|
+
prepared.push({ tc, tool, args, denied: false, error: `Error: ${validationError} for ${toolName}` });
|
|
204
254
|
continue;
|
|
205
255
|
}
|
|
206
256
|
yield { type: 'tool_call', toolCall: { name: toolName, args } };
|
|
207
|
-
// Permission check
|
|
257
|
+
// Permission check (sequential — needs user interaction)
|
|
208
258
|
const needsPermission = tool.permission === 'always-ask' ||
|
|
209
259
|
(tool.permission === 'prompt' && !this.autoApprove);
|
|
260
|
+
let denied = false;
|
|
210
261
|
if (needsPermission) {
|
|
211
262
|
const approved = await this.askPermission(toolName, args);
|
|
212
263
|
if (!approved) {
|
|
213
|
-
|
|
214
|
-
this.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
264
|
+
denied = true;
|
|
265
|
+
this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
prepared.push({ tc, tool, args, denied });
|
|
269
|
+
}
|
|
270
|
+
const results = new Array(prepared.length);
|
|
271
|
+
// Immediately resolve errors and denials
|
|
272
|
+
const toExecute = [];
|
|
273
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
274
|
+
const prep = prepared[idx];
|
|
275
|
+
if (prep.error) {
|
|
276
|
+
results[idx] = { content: prep.error, is_error: true };
|
|
277
|
+
}
|
|
278
|
+
else if (prep.denied) {
|
|
279
|
+
results[idx] = { content: 'Permission denied by user.' };
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
toExecute.push({ index: idx, prep });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Split into parallel-safe and sequential (browser) groups
|
|
286
|
+
const parallelBatch = [];
|
|
287
|
+
const sequentialBatch = [];
|
|
288
|
+
for (const item of toExecute) {
|
|
289
|
+
if (SEQUENTIAL_TOOLS.has(item.prep.tc.function.name)) {
|
|
290
|
+
sequentialBatch.push(item);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
parallelBatch.push(item);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Helper to execute a single tool with cache + rate limiting
|
|
297
|
+
const executeTool = async (prep) => {
|
|
298
|
+
const toolName = prep.tc.function.name;
|
|
299
|
+
// Check cache first
|
|
300
|
+
if (prep.tool.cacheable) {
|
|
301
|
+
const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
|
|
302
|
+
const cached = this.cache.get(cacheKey);
|
|
303
|
+
if (cached !== null) {
|
|
304
|
+
return { content: cached };
|
|
218
305
|
}
|
|
219
306
|
}
|
|
220
|
-
//
|
|
307
|
+
// Rate limit
|
|
308
|
+
await this.rateLimiter.throttle(toolName);
|
|
221
309
|
try {
|
|
222
|
-
const output = await tool.execute(args);
|
|
223
|
-
|
|
224
|
-
this.
|
|
225
|
-
|
|
226
|
-
|
|
310
|
+
const output = await prep.tool.execute(prep.args);
|
|
311
|
+
// Audit log: successful execution
|
|
312
|
+
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
313
|
+
// Store in cache for cacheable tools
|
|
314
|
+
if (prep.tool.cacheable) {
|
|
315
|
+
const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
|
|
316
|
+
this.cache.set(cache_1.ToolCache.key(toolName, prep.args), output, ttl);
|
|
317
|
+
}
|
|
318
|
+
// Invalidate cache on write operations
|
|
319
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
|
|
320
|
+
const filePath = prep.args.path;
|
|
321
|
+
if (filePath)
|
|
322
|
+
this.cache.invalidate(filePath);
|
|
323
|
+
}
|
|
324
|
+
// Audit log: check if tool returned a security block
|
|
325
|
+
if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
|
|
326
|
+
this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
|
|
327
|
+
}
|
|
328
|
+
return { content: output };
|
|
227
329
|
}
|
|
228
330
|
catch (err) {
|
|
229
331
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
332
|
+
// Audit log: error
|
|
333
|
+
this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
|
|
334
|
+
return { content: `Error: ${errMsg}`, is_error: true };
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
// Execute parallel batch concurrently
|
|
338
|
+
if (parallelBatch.length > 0) {
|
|
339
|
+
const promises = parallelBatch.map(async ({ index, prep }) => {
|
|
340
|
+
results[index] = await executeTool(prep);
|
|
341
|
+
});
|
|
342
|
+
await Promise.allSettled(promises);
|
|
343
|
+
}
|
|
344
|
+
// Execute sequential batch one at a time
|
|
345
|
+
for (const { index, prep } of sequentialBatch) {
|
|
346
|
+
results[index] = await executeTool(prep);
|
|
347
|
+
}
|
|
348
|
+
// ── Phase 3: Push results in original order + yield events ──
|
|
349
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
350
|
+
const prep = prepared[idx];
|
|
351
|
+
const output = results[idx] || { content: 'Error: execution failed', is_error: true };
|
|
352
|
+
const toolName = prep.tc.function.name;
|
|
353
|
+
const toolMsg = { role: 'tool', content: output.content, tool_call_id: prep.tc.id };
|
|
354
|
+
this.messages.push(toolMsg);
|
|
355
|
+
this.onMessage?.(toolMsg);
|
|
356
|
+
if (prep.denied) {
|
|
357
|
+
yield { type: 'tool_result', toolResult: { name: toolName, result: 'Permission denied.' } };
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
yield { type: 'tool_result', toolResult: { name: toolName, result: output.content, is_error: output.is_error } };
|
|
234
361
|
}
|
|
235
362
|
}
|
|
236
363
|
// Compact after tool results if needed
|
|
@@ -391,6 +518,7 @@ ${this.tools.all().map(t => `- ${t.name}: ${t.description}`).join('\n')}`;
|
|
|
391
518
|
}
|
|
392
519
|
}
|
|
393
520
|
exports.Agent = Agent;
|
|
521
|
+
const PERMISSION_TIMEOUT_MS = 30_000;
|
|
394
522
|
async function defaultAskPermission(tool, args) {
|
|
395
523
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
396
524
|
const summary = Object.entries(args)
|
|
@@ -399,11 +527,19 @@ async function defaultAskPermission(tool, args) {
|
|
|
399
527
|
return ` ${k}: ${val}`;
|
|
400
528
|
})
|
|
401
529
|
.join('\n');
|
|
402
|
-
|
|
403
|
-
rl.question(`\n⚡ ${tool}\n${summary}\nAllow? [y/N] `, answer => {
|
|
530
|
+
const userResponse = new Promise(resolve => {
|
|
531
|
+
rl.question(`\n⚡ ${tool}\n${summary}\nAllow? [y/N] (${PERMISSION_TIMEOUT_MS / 1000}s timeout) `, answer => {
|
|
404
532
|
rl.close();
|
|
405
533
|
resolve(answer.toLowerCase().startsWith('y'));
|
|
406
534
|
});
|
|
407
535
|
});
|
|
536
|
+
const timeout = new Promise(resolve => {
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
rl.close();
|
|
539
|
+
process.stdout.write('\n⏱ Permission timed out — denied by default.\n');
|
|
540
|
+
resolve(false);
|
|
541
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
542
|
+
});
|
|
543
|
+
return Promise.race([userResponse, timeout]);
|
|
408
544
|
}
|
|
409
545
|
//# sourceMappingURL=agent.js.map
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit logger for CodeBot.
|
|
3
|
+
*
|
|
4
|
+
* Provides append-only JSONL logging of all security-relevant actions.
|
|
5
|
+
* Logs are stored at ~/.codebot/audit/audit-YYYY-MM-DD.jsonl
|
|
6
|
+
* Masks secrets in args before writing.
|
|
7
|
+
* NEVER throws — audit failures must not crash the agent.
|
|
8
|
+
*/
|
|
9
|
+
export interface AuditEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
tool: string;
|
|
13
|
+
action: 'execute' | 'deny' | 'error' | 'security_block';
|
|
14
|
+
args: Record<string, unknown>;
|
|
15
|
+
result?: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class AuditLogger {
|
|
19
|
+
private logDir;
|
|
20
|
+
private sessionId;
|
|
21
|
+
constructor(logDir?: string);
|
|
22
|
+
/** Get the current session ID */
|
|
23
|
+
getSessionId(): string;
|
|
24
|
+
/** Append an audit entry to the log file */
|
|
25
|
+
log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId'>): void;
|
|
26
|
+
/** Read log entries, optionally filtered */
|
|
27
|
+
query(filter?: {
|
|
28
|
+
tool?: string;
|
|
29
|
+
action?: string;
|
|
30
|
+
since?: string;
|
|
31
|
+
}): AuditEntry[];
|
|
32
|
+
/** Get the path to today's log file */
|
|
33
|
+
private getLogFilePath;
|
|
34
|
+
/** Rotate log file if it exceeds MAX_LOG_SIZE */
|
|
35
|
+
private rotateIfNeeded;
|
|
36
|
+
/** Sanitize args for logging: mask secrets and truncate long values */
|
|
37
|
+
private sanitizeArgs;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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.AuditLogger = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const secrets_1 = require("./secrets");
|
|
41
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB before rotation
|
|
42
|
+
const MAX_ARG_LENGTH = 500; // Truncate long arg values for logging
|
|
43
|
+
class AuditLogger {
|
|
44
|
+
logDir;
|
|
45
|
+
sessionId;
|
|
46
|
+
constructor(logDir) {
|
|
47
|
+
this.logDir = logDir || path.join(os.homedir(), '.codebot', 'audit');
|
|
48
|
+
this.sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Can't create dir — logging will be disabled
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Get the current session ID */
|
|
57
|
+
getSessionId() {
|
|
58
|
+
return this.sessionId;
|
|
59
|
+
}
|
|
60
|
+
/** Append an audit entry to the log file */
|
|
61
|
+
log(entry) {
|
|
62
|
+
try {
|
|
63
|
+
const fullEntry = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
sessionId: this.sessionId,
|
|
66
|
+
...entry,
|
|
67
|
+
args: this.sanitizeArgs(entry.args),
|
|
68
|
+
};
|
|
69
|
+
const logFile = this.getLogFilePath();
|
|
70
|
+
const line = JSON.stringify(fullEntry) + '\n';
|
|
71
|
+
// Check if rotation is needed
|
|
72
|
+
this.rotateIfNeeded(logFile);
|
|
73
|
+
fs.appendFileSync(logFile, line, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Audit failures must NEVER crash the agent
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Read log entries, optionally filtered */
|
|
80
|
+
query(filter) {
|
|
81
|
+
const entries = [];
|
|
82
|
+
try {
|
|
83
|
+
const files = fs.readdirSync(this.logDir)
|
|
84
|
+
.filter(f => f.startsWith('audit-') && f.endsWith('.jsonl'))
|
|
85
|
+
.sort();
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const content = fs.readFileSync(path.join(this.logDir, file), 'utf-8');
|
|
88
|
+
for (const line of content.split('\n')) {
|
|
89
|
+
if (!line.trim())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const entry = JSON.parse(line);
|
|
93
|
+
if (filter?.tool && entry.tool !== filter.tool)
|
|
94
|
+
continue;
|
|
95
|
+
if (filter?.action && entry.action !== filter.action)
|
|
96
|
+
continue;
|
|
97
|
+
if (filter?.since && entry.timestamp < filter.since)
|
|
98
|
+
continue;
|
|
99
|
+
entries.push(entry);
|
|
100
|
+
}
|
|
101
|
+
catch { /* skip malformed */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Can't read logs
|
|
107
|
+
}
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
/** Get the path to today's log file */
|
|
111
|
+
getLogFilePath() {
|
|
112
|
+
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
113
|
+
return path.join(this.logDir, `audit-${date}.jsonl`);
|
|
114
|
+
}
|
|
115
|
+
/** Rotate log file if it exceeds MAX_LOG_SIZE */
|
|
116
|
+
rotateIfNeeded(logFile) {
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(logFile))
|
|
119
|
+
return;
|
|
120
|
+
const stat = fs.statSync(logFile);
|
|
121
|
+
if (stat.size >= MAX_LOG_SIZE) {
|
|
122
|
+
const rotated = logFile.replace('.jsonl', `-${Date.now()}.jsonl`);
|
|
123
|
+
fs.renameSync(logFile, rotated);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Rotation failure is non-fatal
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Sanitize args for logging: mask secrets and truncate long values */
|
|
131
|
+
sanitizeArgs(args) {
|
|
132
|
+
const sanitized = {};
|
|
133
|
+
for (const [key, value] of Object.entries(args)) {
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
let masked = (0, secrets_1.maskSecretsInString)(value);
|
|
136
|
+
if (masked.length > MAX_ARG_LENGTH) {
|
|
137
|
+
masked = masked.substring(0, MAX_ARG_LENGTH) + `... (${value.length} chars)`;
|
|
138
|
+
}
|
|
139
|
+
sanitized[key] = masked;
|
|
140
|
+
}
|
|
141
|
+
else if (typeof value === 'object' && value !== null) {
|
|
142
|
+
// For objects/arrays, stringify and mask
|
|
143
|
+
const str = JSON.stringify(value);
|
|
144
|
+
const masked = (0, secrets_1.maskSecretsInString)(str);
|
|
145
|
+
sanitized[key] = masked.length > MAX_ARG_LENGTH
|
|
146
|
+
? masked.substring(0, MAX_ARG_LENGTH) + '...'
|
|
147
|
+
: masked;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
sanitized[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return sanitized;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.AuditLogger = AuditLogger;
|
|
157
|
+
//# sourceMappingURL=audit.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.6.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[];
|