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 CHANGED
@@ -7,6 +7,9 @@ export declare class Agent {
7
7
  private maxIterations;
8
8
  private autoApprove;
9
9
  private model;
10
+ private cache;
11
+ private rateLimiter;
12
+ private auditLogger;
10
13
  private askPermission;
11
14
  private onMessage?;
12
15
  constructor(opts: {
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
- // Execute each tool call
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
- const errResult = `Error: Unknown tool "${toolName}"`;
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
- const errResult = `Error: Invalid JSON arguments for ${toolName}`;
200
- const toolMsg = { role: 'tool', content: errResult, tool_call_id: tc.id };
201
- this.messages.push(toolMsg);
202
- this.onMessage?.(toolMsg);
203
- yield { type: 'tool_result', toolResult: { name: toolName, result: errResult, is_error: true } };
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
- const toolMsg = { role: 'tool', content: 'Permission denied by user.', tool_call_id: tc.id };
214
- this.messages.push(toolMsg);
215
- this.onMessage?.(toolMsg);
216
- yield { type: 'tool_result', toolResult: { name: toolName, result: 'Permission denied.' } };
217
- continue;
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
- // Execute
307
+ // Rate limit
308
+ await this.rateLimiter.throttle(toolName);
221
309
  try {
222
- const output = await tool.execute(args);
223
- const toolMsg = { role: 'tool', content: output, tool_call_id: tc.id };
224
- this.messages.push(toolMsg);
225
- this.onMessage?.(toolMsg);
226
- yield { type: 'tool_result', toolResult: { name: toolName, result: output } };
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
- const toolMsg = { role: 'tool', content: `Error: ${errMsg}`, tool_call_id: tc.id };
231
- this.messages.push(toolMsg);
232
- this.onMessage?.(toolMsg);
233
- yield { type: 'tool_result', toolResult: { name: toolName, result: errMsg, is_error: true } };
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
- return new Promise(resolve => {
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
@@ -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
@@ -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.4.3';
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[];