codebot-ai 1.6.0 → 1.7.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Token & Cost Tracking for CodeBot v1.7.0
3
+ *
4
+ * Tracks per-request and per-session token usage and estimated costs.
5
+ * Supports cost limits and historical usage queries.
6
+ */
7
+ export interface UsageRecord {
8
+ timestamp: string;
9
+ model: string;
10
+ provider: string;
11
+ inputTokens: number;
12
+ outputTokens: number;
13
+ costUsd: number;
14
+ }
15
+ export interface SessionSummary {
16
+ sessionId: string;
17
+ model: string;
18
+ provider: string;
19
+ startTime: string;
20
+ endTime: string;
21
+ totalInputTokens: number;
22
+ totalOutputTokens: number;
23
+ totalCostUsd: number;
24
+ requestCount: number;
25
+ toolCalls: number;
26
+ filesModified: number;
27
+ }
28
+ export declare class TokenTracker {
29
+ private model;
30
+ private provider;
31
+ private sessionId;
32
+ private records;
33
+ private toolCallCount;
34
+ private filesModifiedSet;
35
+ private startTime;
36
+ private costLimitUsd;
37
+ constructor(model: string, provider: string, sessionId?: string);
38
+ /** Set cost limit in USD. 0 = no limit. */
39
+ setCostLimit(usd: number): void;
40
+ /** Record token usage from an LLM request */
41
+ recordUsage(inputTokens: number, outputTokens: number): UsageRecord;
42
+ /** Record a tool call (for summary) */
43
+ recordToolCall(): void;
44
+ /** Record a file modification (for summary) */
45
+ recordFileModified(filePath: string): void;
46
+ /** Check if cost limit has been exceeded */
47
+ isOverBudget(): boolean;
48
+ /** Get remaining budget in USD (Infinity if no limit) */
49
+ getRemainingBudget(): number;
50
+ getTotalInputTokens(): number;
51
+ getTotalOutputTokens(): number;
52
+ getTotalCost(): number;
53
+ getRequestCount(): number;
54
+ /** Generate a session summary */
55
+ getSummary(): SessionSummary;
56
+ /** Format cost for display */
57
+ formatCost(): string;
58
+ /** Format a compact status line for CLI */
59
+ formatStatusLine(): string;
60
+ /** Save session usage to ~/.codebot/usage/ for historical tracking */
61
+ saveUsage(): void;
62
+ /**
63
+ * Load historical usage from ~/.codebot/usage/
64
+ */
65
+ static loadHistory(days?: number): SessionSummary[];
66
+ /**
67
+ * Format a historical usage report.
68
+ */
69
+ static formatUsageReport(days?: number): string;
70
+ private getPricing;
71
+ private isLocalModel;
72
+ }
73
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ /**
3
+ * Token & Cost Tracking for CodeBot v1.7.0
4
+ *
5
+ * Tracks per-request and per-session token usage and estimated costs.
6
+ * Supports cost limits and historical usage queries.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.TokenTracker = void 0;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const PRICING = {
47
+ // Anthropic
48
+ 'claude-opus-4-6': { input: 15.0, output: 75.0 },
49
+ 'claude-sonnet-4-20250514': { input: 3.0, output: 15.0 },
50
+ 'claude-haiku-3-5': { input: 0.80, output: 4.0 },
51
+ // OpenAI
52
+ 'gpt-4o': { input: 2.50, output: 10.0 },
53
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
54
+ 'gpt-4.1': { input: 2.0, output: 8.0 },
55
+ 'gpt-4.1-mini': { input: 0.40, output: 1.60 },
56
+ 'gpt-4.1-nano': { input: 0.10, output: 0.40 },
57
+ 'o1': { input: 15.0, output: 60.0 },
58
+ 'o3': { input: 10.0, output: 40.0 },
59
+ 'o3-mini': { input: 1.10, output: 4.40 },
60
+ 'o4-mini': { input: 1.10, output: 4.40 },
61
+ // Google
62
+ 'gemini-2.5-pro': { input: 1.25, output: 10.0 },
63
+ 'gemini-2.5-flash': { input: 0.15, output: 0.60 },
64
+ 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
65
+ // DeepSeek
66
+ 'deepseek-chat': { input: 0.14, output: 0.28 },
67
+ 'deepseek-reasoner': { input: 0.55, output: 2.19 },
68
+ // Groq (free tier pricing)
69
+ 'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
70
+ // Mistral
71
+ 'mistral-large-latest': { input: 2.0, output: 6.0 },
72
+ 'codestral-latest': { input: 0.30, output: 0.90 },
73
+ // xAI
74
+ 'grok-3': { input: 3.0, output: 15.0 },
75
+ 'grok-3-mini': { input: 0.30, output: 0.50 },
76
+ };
77
+ // Default pricing for unknown models (conservative estimate)
78
+ const DEFAULT_PRICING = { input: 2.0, output: 8.0 };
79
+ // Free pricing for local models
80
+ const LOCAL_PRICING = { input: 0, output: 0 };
81
+ class TokenTracker {
82
+ model;
83
+ provider;
84
+ sessionId;
85
+ records = [];
86
+ toolCallCount = 0;
87
+ filesModifiedSet = new Set();
88
+ startTime;
89
+ costLimitUsd = 0;
90
+ constructor(model, provider, sessionId) {
91
+ this.model = model;
92
+ this.provider = provider;
93
+ this.sessionId = sessionId || `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
94
+ this.startTime = new Date().toISOString();
95
+ }
96
+ /** Set cost limit in USD. 0 = no limit. */
97
+ setCostLimit(usd) {
98
+ this.costLimitUsd = usd;
99
+ }
100
+ /** Record token usage from an LLM request */
101
+ recordUsage(inputTokens, outputTokens) {
102
+ const pricing = this.getPricing();
103
+ const costUsd = (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
104
+ const record = {
105
+ timestamp: new Date().toISOString(),
106
+ model: this.model,
107
+ provider: this.provider,
108
+ inputTokens,
109
+ outputTokens,
110
+ costUsd,
111
+ };
112
+ this.records.push(record);
113
+ return record;
114
+ }
115
+ /** Record a tool call (for summary) */
116
+ recordToolCall() {
117
+ this.toolCallCount++;
118
+ }
119
+ /** Record a file modification (for summary) */
120
+ recordFileModified(filePath) {
121
+ this.filesModifiedSet.add(filePath);
122
+ }
123
+ /** Check if cost limit has been exceeded */
124
+ isOverBudget() {
125
+ if (this.costLimitUsd <= 0)
126
+ return false;
127
+ return this.getTotalCost() >= this.costLimitUsd;
128
+ }
129
+ /** Get remaining budget in USD (Infinity if no limit) */
130
+ getRemainingBudget() {
131
+ if (this.costLimitUsd <= 0)
132
+ return Infinity;
133
+ return Math.max(0, this.costLimitUsd - this.getTotalCost());
134
+ }
135
+ // ── Aggregates ──
136
+ getTotalInputTokens() {
137
+ return this.records.reduce((sum, r) => sum + r.inputTokens, 0);
138
+ }
139
+ getTotalOutputTokens() {
140
+ return this.records.reduce((sum, r) => sum + r.outputTokens, 0);
141
+ }
142
+ getTotalCost() {
143
+ return this.records.reduce((sum, r) => sum + r.costUsd, 0);
144
+ }
145
+ getRequestCount() {
146
+ return this.records.length;
147
+ }
148
+ /** Generate a session summary */
149
+ getSummary() {
150
+ return {
151
+ sessionId: this.sessionId,
152
+ model: this.model,
153
+ provider: this.provider,
154
+ startTime: this.startTime,
155
+ endTime: new Date().toISOString(),
156
+ totalInputTokens: this.getTotalInputTokens(),
157
+ totalOutputTokens: this.getTotalOutputTokens(),
158
+ totalCostUsd: this.getTotalCost(),
159
+ requestCount: this.getRequestCount(),
160
+ toolCalls: this.toolCallCount,
161
+ filesModified: this.filesModifiedSet.size,
162
+ };
163
+ }
164
+ /** Format cost for display */
165
+ formatCost() {
166
+ const cost = this.getTotalCost();
167
+ if (cost === 0)
168
+ return 'free (local model)';
169
+ if (cost < 0.01)
170
+ return `< $0.01`;
171
+ return `$${cost.toFixed(4)}`;
172
+ }
173
+ /** Format a compact status line for CLI */
174
+ formatStatusLine() {
175
+ const inTk = this.getTotalInputTokens();
176
+ const outTk = this.getTotalOutputTokens();
177
+ const cost = this.formatCost();
178
+ return `${inTk.toLocaleString()} in / ${outTk.toLocaleString()} out | ${cost}`;
179
+ }
180
+ /** Save session usage to ~/.codebot/usage/ for historical tracking */
181
+ saveUsage() {
182
+ try {
183
+ const usageDir = path.join(os.homedir(), '.codebot', 'usage');
184
+ fs.mkdirSync(usageDir, { recursive: true });
185
+ const summary = this.getSummary();
186
+ const fileName = `usage-${summary.startTime.split('T')[0]}.jsonl`;
187
+ const filePath = path.join(usageDir, fileName);
188
+ fs.appendFileSync(filePath, JSON.stringify(summary) + '\n', 'utf-8');
189
+ }
190
+ catch {
191
+ // Usage tracking failures are non-fatal
192
+ }
193
+ }
194
+ /**
195
+ * Load historical usage from ~/.codebot/usage/
196
+ */
197
+ static loadHistory(days) {
198
+ const summaries = [];
199
+ try {
200
+ const usageDir = path.join(os.homedir(), '.codebot', 'usage');
201
+ if (!fs.existsSync(usageDir))
202
+ return [];
203
+ const files = fs.readdirSync(usageDir)
204
+ .filter(f => f.startsWith('usage-') && f.endsWith('.jsonl'))
205
+ .sort();
206
+ // Filter by date range if specified
207
+ const cutoff = days
208
+ ? new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
209
+ : undefined;
210
+ for (const file of files) {
211
+ const content = fs.readFileSync(path.join(usageDir, file), 'utf-8');
212
+ for (const line of content.split('\n')) {
213
+ if (!line.trim())
214
+ continue;
215
+ try {
216
+ const summary = JSON.parse(line);
217
+ if (cutoff && summary.startTime < cutoff)
218
+ continue;
219
+ summaries.push(summary);
220
+ }
221
+ catch { /* skip malformed */ }
222
+ }
223
+ }
224
+ }
225
+ catch {
226
+ // Can't read usage
227
+ }
228
+ return summaries;
229
+ }
230
+ /**
231
+ * Format a historical usage report.
232
+ */
233
+ static formatUsageReport(days = 30) {
234
+ const history = TokenTracker.loadHistory(days);
235
+ if (history.length === 0)
236
+ return 'No usage data found.';
237
+ let totalInput = 0;
238
+ let totalOutput = 0;
239
+ let totalCost = 0;
240
+ let totalRequests = 0;
241
+ let totalTools = 0;
242
+ for (const s of history) {
243
+ totalInput += s.totalInputTokens;
244
+ totalOutput += s.totalOutputTokens;
245
+ totalCost += s.totalCostUsd;
246
+ totalRequests += s.requestCount;
247
+ totalTools += s.toolCalls;
248
+ }
249
+ const lines = [
250
+ `Usage Report (last ${days} days)`,
251
+ '─'.repeat(40),
252
+ `Sessions: ${history.length}`,
253
+ `LLM Requests: ${totalRequests.toLocaleString()}`,
254
+ `Tool Calls: ${totalTools.toLocaleString()}`,
255
+ `Input Tokens: ${totalInput.toLocaleString()}`,
256
+ `Output Tokens: ${totalOutput.toLocaleString()}`,
257
+ `Total Cost: $${totalCost.toFixed(4)}`,
258
+ ];
259
+ return lines.join('\n');
260
+ }
261
+ // ── Helpers ──
262
+ getPricing() {
263
+ // Local models are free
264
+ if (this.isLocalModel())
265
+ return LOCAL_PRICING;
266
+ // Exact match
267
+ if (PRICING[this.model])
268
+ return PRICING[this.model];
269
+ // Prefix match (for versioned models like claude-sonnet-4-20250514)
270
+ for (const [key, pricing] of Object.entries(PRICING)) {
271
+ if (this.model.startsWith(key))
272
+ return pricing;
273
+ }
274
+ return DEFAULT_PRICING;
275
+ }
276
+ isLocalModel() {
277
+ // Models running on Ollama, LM Studio, vLLM are free
278
+ return !this.provider ||
279
+ this.provider === 'ollama' ||
280
+ this.provider === 'lmstudio' ||
281
+ this.provider === 'vllm' ||
282
+ this.provider === 'local';
283
+ }
284
+ }
285
+ exports.TokenTracker = TokenTracker;
286
+ //# sourceMappingURL=telemetry.js.map
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExecuteTool = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const security_1 = require("../security");
6
+ const sandbox_1 = require("../sandbox");
7
+ const policy_1 = require("../policy");
6
8
  const BLOCKED_PATTERNS = [
7
9
  // Destructive filesystem operations
8
10
  /rm\s+-rf\s+\//,
@@ -109,25 +111,48 @@ class ExecuteTool {
109
111
  if (!cwdSafety.safe) {
110
112
  return `Error: ${cwdSafety.reason}`;
111
113
  }
112
- // Security: filter sensitive env vars
114
+ const timeout = args.timeout || 30000;
115
+ // ── v1.7.0: Sandbox routing ──
116
+ const policy = (0, policy_1.loadPolicy)(projectRoot);
117
+ const sandboxMode = policy.execution?.sandbox || 'auto';
118
+ const useSandbox = sandboxMode === 'docker' ||
119
+ (sandboxMode === 'auto' && (0, sandbox_1.isDockerAvailable)());
120
+ if (useSandbox) {
121
+ const result = (0, sandbox_1.sandboxExec)(cmd, projectRoot, {
122
+ network: policy.execution?.network !== false,
123
+ memoryMb: policy.execution?.max_memory_mb || 512,
124
+ timeoutMs: timeout,
125
+ });
126
+ if (result.sandboxed) {
127
+ const output = result.stdout || result.stderr || '(no output)';
128
+ const tag = '[sandboxed]';
129
+ if (result.exitCode !== 0) {
130
+ return `${tag} Exit code ${result.exitCode}\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`;
131
+ }
132
+ return `${tag} ${output}`;
133
+ }
134
+ // Fallthrough: sandboxExec returned sandboxed=false (Docker wasn't available after all)
135
+ }
136
+ // ── Host execution (existing path) ──
113
137
  const safeEnv = { ...process.env };
114
138
  for (const key of FILTERED_ENV_VARS) {
115
139
  delete safeEnv[key];
116
140
  }
141
+ const tag = useSandbox ? '[host-fallback]' : '[host]';
117
142
  try {
118
143
  const output = (0, child_process_1.execSync)(cmd, {
119
144
  cwd,
120
- timeout: args.timeout || 30000,
145
+ timeout,
121
146
  maxBuffer: 1024 * 1024,
122
147
  encoding: 'utf-8',
123
148
  stdio: ['pipe', 'pipe', 'pipe'],
124
149
  env: safeEnv,
125
150
  });
126
- return output || '(no output)';
151
+ return `${tag} ${output || '(no output)'}`;
127
152
  }
128
153
  catch (err) {
129
154
  const e = err;
130
- return `Exit code ${e.status || 1}\nSTDOUT:\n${e.stdout || ''}\nSTDERR:\n${e.stderr || ''}`;
155
+ return `${tag} Exit code ${e.status || 1}\nSTDOUT:\n${e.stdout || ''}\nSTDERR:\n${e.stderr || ''}`;
131
156
  }
132
157
  }
133
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebot-ai",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",