codebot-ai 1.7.0 → 1.9.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,60 @@
1
+ /**
2
+ * MetricsCollector for CodeBot v1.9.0
3
+ *
4
+ * Structured telemetry: counters + histograms.
5
+ * Persists to ~/.codebot/telemetry/metrics-YYYY-MM-DD.jsonl
6
+ * Optional OTLP HTTP export when OTEL_EXPORTER_OTLP_ENDPOINT is set.
7
+ *
8
+ * Pattern: fail-safe, session-scoped, never throws.
9
+ * Follows TokenTracker conventions from src/telemetry.ts.
10
+ */
11
+ export interface CounterValue {
12
+ name: string;
13
+ labels: Record<string, string>;
14
+ value: number;
15
+ }
16
+ export interface HistogramValue {
17
+ name: string;
18
+ labels: Record<string, string>;
19
+ count: number;
20
+ sum: number;
21
+ min: number;
22
+ max: number;
23
+ buckets: number[];
24
+ }
25
+ export interface MetricsSnapshot {
26
+ sessionId: string;
27
+ timestamp: string;
28
+ counters: CounterValue[];
29
+ histograms: HistogramValue[];
30
+ }
31
+ export declare class MetricsCollector {
32
+ private sessionId;
33
+ private counters;
34
+ private histograms;
35
+ constructor(sessionId?: string);
36
+ getSessionId(): string;
37
+ /** Increment a counter by delta (default 1) */
38
+ increment(name: string, labels?: Record<string, string>, delta?: number): void;
39
+ /** Record a histogram observation */
40
+ observe(name: string, value: number, labels?: Record<string, string>): void;
41
+ /** Read a counter value */
42
+ getCounter(name: string, labels?: Record<string, string>): number;
43
+ /** Read a histogram summary */
44
+ getHistogram(name: string, labels?: Record<string, string>): HistogramValue | null;
45
+ /** Full session snapshot */
46
+ snapshot(): MetricsSnapshot;
47
+ /** Persist snapshot to ~/.codebot/telemetry/metrics-YYYY-MM-DD.jsonl */
48
+ save(sessionId?: string): void;
49
+ /** Human-readable per-tool breakdown */
50
+ formatSummary(): string;
51
+ /**
52
+ * Export snapshot in OTLP JSON format via HTTP POST.
53
+ * Only fires when OTEL_EXPORTER_OTLP_ENDPOINT is set.
54
+ * Fails silently — never blocks or crashes.
55
+ */
56
+ exportOtel(snap?: MetricsSnapshot): void;
57
+ /** Build OTLP-compatible JSON payload */
58
+ private buildOtlpPayload;
59
+ }
60
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ /**
3
+ * MetricsCollector for CodeBot v1.9.0
4
+ *
5
+ * Structured telemetry: counters + histograms.
6
+ * Persists to ~/.codebot/telemetry/metrics-YYYY-MM-DD.jsonl
7
+ * Optional OTLP HTTP export when OTEL_EXPORTER_OTLP_ENDPOINT is set.
8
+ *
9
+ * Pattern: fail-safe, session-scoped, never throws.
10
+ * Follows TokenTracker conventions from src/telemetry.ts.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.MetricsCollector = void 0;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const os = __importStar(require("os"));
50
+ const http = __importStar(require("http"));
51
+ const https = __importStar(require("https"));
52
+ // ── Helpers ──
53
+ /** Encode a metric key: name|label1=val1|label2=val2 (sorted labels) */
54
+ function encodeKey(name, labels) {
55
+ if (!labels || Object.keys(labels).length === 0)
56
+ return name;
57
+ const sorted = Object.entries(labels)
58
+ .sort(([a], [b]) => a.localeCompare(b))
59
+ .map(([k, v]) => `${k}=${v}`)
60
+ .join('|');
61
+ return `${name}|${sorted}`;
62
+ }
63
+ /** Decode a metric key back to name + labels */
64
+ function decodeKey(key) {
65
+ const parts = key.split('|');
66
+ const name = parts[0];
67
+ const labels = {};
68
+ for (let i = 1; i < parts.length; i++) {
69
+ const eq = parts[i].indexOf('=');
70
+ if (eq > 0) {
71
+ labels[parts[i].substring(0, eq)] = parts[i].substring(eq + 1);
72
+ }
73
+ }
74
+ return { name, labels };
75
+ }
76
+ /** Compute percentile from sorted array */
77
+ function percentile(sorted, p) {
78
+ if (sorted.length === 0)
79
+ return 0;
80
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
81
+ return sorted[Math.max(0, idx)];
82
+ }
83
+ // ── Collector ──
84
+ const MAX_BUCKETS = 1000; // cap stored observations to prevent memory bloat
85
+ class MetricsCollector {
86
+ sessionId;
87
+ counters = new Map();
88
+ histograms = new Map();
89
+ constructor(sessionId) {
90
+ this.sessionId = sessionId || `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
91
+ }
92
+ getSessionId() {
93
+ return this.sessionId;
94
+ }
95
+ /** Increment a counter by delta (default 1) */
96
+ increment(name, labels, delta = 1) {
97
+ const key = encodeKey(name, labels);
98
+ this.counters.set(key, (this.counters.get(key) || 0) + delta);
99
+ }
100
+ /** Record a histogram observation */
101
+ observe(name, value, labels) {
102
+ const key = encodeKey(name, labels);
103
+ const existing = this.histograms.get(key);
104
+ if (existing) {
105
+ existing.count++;
106
+ existing.sum += value;
107
+ existing.min = Math.min(existing.min, value);
108
+ existing.max = Math.max(existing.max, value);
109
+ if (existing.buckets.length < MAX_BUCKETS) {
110
+ existing.buckets.push(value);
111
+ }
112
+ }
113
+ else {
114
+ this.histograms.set(key, {
115
+ count: 1,
116
+ sum: value,
117
+ min: value,
118
+ max: value,
119
+ buckets: [value],
120
+ });
121
+ }
122
+ }
123
+ /** Read a counter value */
124
+ getCounter(name, labels) {
125
+ return this.counters.get(encodeKey(name, labels)) || 0;
126
+ }
127
+ /** Read a histogram summary */
128
+ getHistogram(name, labels) {
129
+ const key = encodeKey(name, labels);
130
+ const h = this.histograms.get(key);
131
+ if (!h)
132
+ return null;
133
+ const { name: n, labels: l } = decodeKey(key);
134
+ return { name: n, labels: l, ...h };
135
+ }
136
+ /** Full session snapshot */
137
+ snapshot() {
138
+ const counters = [];
139
+ for (const [key, value] of this.counters) {
140
+ const { name, labels } = decodeKey(key);
141
+ counters.push({ name, labels, value });
142
+ }
143
+ const histograms = [];
144
+ for (const [key, h] of this.histograms) {
145
+ const { name, labels } = decodeKey(key);
146
+ histograms.push({ name, labels, ...h });
147
+ }
148
+ return {
149
+ sessionId: this.sessionId,
150
+ timestamp: new Date().toISOString(),
151
+ counters,
152
+ histograms,
153
+ };
154
+ }
155
+ /** Persist snapshot to ~/.codebot/telemetry/metrics-YYYY-MM-DD.jsonl */
156
+ save(sessionId) {
157
+ try {
158
+ const telemetryDir = path.join(os.homedir(), '.codebot', 'telemetry');
159
+ fs.mkdirSync(telemetryDir, { recursive: true });
160
+ const snap = this.snapshot();
161
+ if (sessionId)
162
+ snap.sessionId = sessionId;
163
+ const date = new Date().toISOString().split('T')[0];
164
+ const filePath = path.join(telemetryDir, `metrics-${date}.jsonl`);
165
+ fs.appendFileSync(filePath, JSON.stringify(snap) + '\n', 'utf-8');
166
+ }
167
+ catch {
168
+ // Telemetry failures are non-fatal
169
+ }
170
+ }
171
+ /** Human-readable per-tool breakdown */
172
+ formatSummary() {
173
+ const lines = ['Metrics Summary', '─'.repeat(50)];
174
+ // Counters
175
+ const counterEntries = Array.from(this.counters.entries())
176
+ .sort(([a], [b]) => a.localeCompare(b));
177
+ if (counterEntries.length > 0) {
178
+ lines.push('Counters:');
179
+ for (const [key, value] of counterEntries) {
180
+ const { name, labels } = decodeKey(key);
181
+ const labelStr = Object.keys(labels).length > 0
182
+ ? ` {${Object.entries(labels).map(([k, v]) => `${k}="${v}"`).join(', ')}}`
183
+ : '';
184
+ lines.push(` ${name}${labelStr}: ${value}`);
185
+ }
186
+ }
187
+ // Histograms — per-tool breakdown
188
+ const histEntries = Array.from(this.histograms.entries())
189
+ .sort(([a], [b]) => a.localeCompare(b));
190
+ if (histEntries.length > 0) {
191
+ lines.push('Histograms:');
192
+ for (const [key, h] of histEntries) {
193
+ const { name, labels } = decodeKey(key);
194
+ const labelStr = Object.keys(labels).length > 0
195
+ ? ` {${Object.entries(labels).map(([k, v]) => `${k}="${v}"`).join(', ')}}`
196
+ : '';
197
+ const sorted = [...h.buckets].sort((a, b) => a - b);
198
+ const avg = h.count > 0 ? (h.sum / h.count).toFixed(3) : '0';
199
+ const p50 = percentile(sorted, 50).toFixed(3);
200
+ const p95 = percentile(sorted, 95).toFixed(3);
201
+ const p99 = percentile(sorted, 99).toFixed(3);
202
+ lines.push(` ${name}${labelStr}: count=${h.count} avg=${avg} p50=${p50} p95=${p95} p99=${p99} min=${h.min.toFixed(3)} max=${h.max.toFixed(3)}`);
203
+ }
204
+ }
205
+ if (counterEntries.length === 0 && histEntries.length === 0) {
206
+ lines.push(' (no metrics recorded)');
207
+ }
208
+ return lines.join('\n');
209
+ }
210
+ /**
211
+ * Export snapshot in OTLP JSON format via HTTP POST.
212
+ * Only fires when OTEL_EXPORTER_OTLP_ENDPOINT is set.
213
+ * Fails silently — never blocks or crashes.
214
+ */
215
+ exportOtel(snap) {
216
+ try {
217
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
218
+ if (!endpoint)
219
+ return;
220
+ const data = snap || this.snapshot();
221
+ const payload = this.buildOtlpPayload(data);
222
+ const url = new URL('/v1/metrics', endpoint);
223
+ const body = JSON.stringify(payload);
224
+ const mod = url.protocol === 'https:' ? https : http;
225
+ const req = mod.request(url, {
226
+ method: 'POST',
227
+ headers: {
228
+ 'Content-Type': 'application/json',
229
+ 'Content-Length': Buffer.byteLength(body),
230
+ },
231
+ timeout: 5000,
232
+ });
233
+ req.on('error', () => { });
234
+ req.on('timeout', () => req.destroy());
235
+ req.write(body);
236
+ req.end();
237
+ }
238
+ catch {
239
+ // OTLP export failures are non-fatal
240
+ }
241
+ }
242
+ /** Build OTLP-compatible JSON payload */
243
+ buildOtlpPayload(snap) {
244
+ const metrics = [];
245
+ for (const counter of snap.counters) {
246
+ metrics.push({
247
+ name: counter.name,
248
+ sum: {
249
+ dataPoints: [{
250
+ asInt: counter.value,
251
+ attributes: Object.entries(counter.labels).map(([k, v]) => ({
252
+ key: k, value: { stringValue: v },
253
+ })),
254
+ timeUnixNano: Date.now() * 1_000_000,
255
+ }],
256
+ isMonotonic: true,
257
+ aggregationTemporality: 2, // CUMULATIVE
258
+ },
259
+ });
260
+ }
261
+ for (const hist of snap.histograms) {
262
+ metrics.push({
263
+ name: hist.name,
264
+ histogram: {
265
+ dataPoints: [{
266
+ count: hist.count,
267
+ sum: hist.sum,
268
+ min: hist.min,
269
+ max: hist.max,
270
+ attributes: Object.entries(hist.labels).map(([k, v]) => ({
271
+ key: k, value: { stringValue: v },
272
+ })),
273
+ timeUnixNano: Date.now() * 1_000_000,
274
+ }],
275
+ aggregationTemporality: 2,
276
+ },
277
+ });
278
+ }
279
+ return {
280
+ resourceMetrics: [{
281
+ resource: {
282
+ attributes: [
283
+ { key: 'service.name', value: { stringValue: 'codebot' } },
284
+ { key: 'session.id', value: { stringValue: snap.sessionId } },
285
+ ],
286
+ },
287
+ scopeMetrics: [{
288
+ scope: { name: 'codebot-metrics', version: '1.9.0' },
289
+ metrics,
290
+ }],
291
+ }],
292
+ };
293
+ }
294
+ }
295
+ exports.MetricsCollector = MetricsCollector;
296
+ //# sourceMappingURL=metrics.js.map
package/dist/policy.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * Policy files: .codebot/policy.json (project) + ~/.codebot/policy.json (global)
6
6
  * Project policy overrides global policy where specified.
7
7
  */
8
+ import { ToolCapabilities } from './capabilities';
8
9
  export interface PolicyExecution {
9
10
  sandbox?: 'docker' | 'host' | 'auto';
10
11
  network?: boolean;
@@ -24,6 +25,7 @@ export interface PolicyTools {
24
25
  enabled?: string[];
25
26
  disabled?: string[];
26
27
  permissions?: PolicyToolPermission;
28
+ capabilities?: Record<string, ToolCapabilities>;
27
29
  }
28
30
  export interface PolicySecrets {
29
31
  block_on_detect?: boolean;
@@ -108,6 +110,13 @@ export declare class PolicyEnforcer {
108
110
  getMaxFileSizeBytes(): number;
109
111
  /** Get cost limit in USD (0 = no limit). */
110
112
  getCostLimitUsd(): number;
113
+ /** Get capability restrictions for a tool. undefined = unrestricted. */
114
+ getToolCapabilities(toolName: string): ToolCapabilities | undefined;
115
+ /** Check a specific capability for a tool. Returns { allowed, reason }. */
116
+ checkCapability(toolName: string, capabilityType: keyof ToolCapabilities, value: string | number): {
117
+ allowed: boolean;
118
+ reason?: string;
119
+ };
111
120
  /**
112
121
  * Simple glob-like pattern matching:
113
122
  * - `*` matches any single path component
package/dist/policy.js CHANGED
@@ -46,12 +46,13 @@ exports.generateDefaultPolicyFile = generateDefaultPolicyFile;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const os = __importStar(require("os"));
49
+ const capabilities_1 = require("./capabilities");
49
50
  // ── Default Policy ──
50
51
  exports.DEFAULT_POLICY = {
51
52
  version: '1.0',
52
53
  execution: {
53
54
  sandbox: 'auto',
54
- network: true,
55
+ network: false, // safe default: no network in sandbox
55
56
  timeout_seconds: 120,
56
57
  max_memory_mb: 512,
57
58
  },
@@ -67,12 +68,12 @@ exports.DEFAULT_POLICY = {
67
68
  permissions: {},
68
69
  },
69
70
  secrets: {
70
- block_on_detect: false,
71
+ block_on_detect: true, // safe default: block writes containing secrets
71
72
  scan_on_write: true,
72
73
  allowed_patterns: [],
73
74
  },
74
75
  git: {
75
- always_branch: false,
76
+ always_branch: true, // safe default: auto-branch on first write
76
77
  branch_prefix: 'codebot/',
77
78
  require_tests_before_commit: false,
78
79
  never_push_main: true,
@@ -327,6 +328,19 @@ class PolicyEnforcer {
327
328
  getCostLimitUsd() {
328
329
  return this.policy.limits?.cost_limit_usd || 0;
329
330
  }
331
+ // ── Capabilities (v1.8.0) ──
332
+ /** Get capability restrictions for a tool. undefined = unrestricted. */
333
+ getToolCapabilities(toolName) {
334
+ return this.policy.tools?.capabilities?.[toolName];
335
+ }
336
+ /** Check a specific capability for a tool. Returns { allowed, reason }. */
337
+ checkCapability(toolName, capabilityType, value) {
338
+ const caps = this.policy.tools?.capabilities;
339
+ if (!caps)
340
+ return { allowed: true };
341
+ const checker = new capabilities_1.CapabilityChecker(caps, this.projectRoot);
342
+ return checker.checkCapability(toolName, capabilityType, value);
343
+ }
330
344
  // ── Helpers ──
331
345
  /**
332
346
  * Simple glob-like pattern matching:
@@ -374,7 +388,7 @@ function generateDefaultPolicyFile() {
374
388
  version: '1.0',
375
389
  execution: {
376
390
  sandbox: 'auto',
377
- network: true,
391
+ network: false,
378
392
  timeout_seconds: 120,
379
393
  max_memory_mb: 512,
380
394
  },
@@ -392,13 +406,25 @@ function generateDefaultPolicyFile() {
392
406
  write_file: 'prompt',
393
407
  edit_file: 'prompt',
394
408
  },
409
+ capabilities: {
410
+ execute: {
411
+ shell_commands: [
412
+ 'npm', 'npx', 'node', 'git', 'tsc', 'eslint', 'prettier',
413
+ 'jest', 'vitest', 'pytest', 'make', 'cargo', 'go', 'python',
414
+ 'python3', 'ruby', 'php', 'java', 'javac', 'mvn', 'gradle',
415
+ 'docker', 'ls', 'cat', 'head', 'tail', 'wc', 'sort', 'uniq',
416
+ 'find', 'which', 'echo', 'pwd', 'env', 'date',
417
+ ],
418
+ max_output_kb: 500,
419
+ },
420
+ },
395
421
  },
396
422
  secrets: {
397
- block_on_detect: false,
423
+ block_on_detect: true,
398
424
  scan_on_write: true,
399
425
  },
400
426
  git: {
401
- always_branch: false,
427
+ always_branch: true,
402
428
  branch_prefix: 'codebot/',
403
429
  require_tests_before_commit: false,
404
430
  never_push_main: true,
@@ -1,6 +1,7 @@
1
1
  import { LLMProvider, Message, ToolSchema, StreamEvent, ProviderConfig } from '../types';
2
2
  export declare class AnthropicProvider implements LLMProvider {
3
3
  name: string;
4
+ temperature?: number;
4
5
  private config;
5
6
  constructor(config: ProviderConfig);
6
7
  chat(messages: Message[], tools?: ToolSchema[]): AsyncGenerator<StreamEvent>;
@@ -4,6 +4,7 @@ exports.AnthropicProvider = void 0;
4
4
  const retry_1 = require("../retry");
5
5
  class AnthropicProvider {
6
6
  name;
7
+ temperature;
7
8
  config;
8
9
  constructor(config) {
9
10
  this.config = config;
@@ -22,6 +23,9 @@ class AnthropicProvider {
22
23
  max_tokens: 8192,
23
24
  stream: true,
24
25
  };
26
+ if (this.temperature !== undefined) {
27
+ body.temperature = this.temperature;
28
+ }
25
29
  if (systemPrompt) {
26
30
  body.system = systemPrompt;
27
31
  }
@@ -1,6 +1,7 @@
1
1
  import { LLMProvider, Message, ToolSchema, StreamEvent, ProviderConfig } from '../types';
2
2
  export declare class OpenAIProvider implements LLMProvider {
3
3
  name: string;
4
+ temperature?: number;
4
5
  private config;
5
6
  private supportsTools;
6
7
  constructor(config: ProviderConfig);
@@ -5,6 +5,7 @@ const registry_1 = require("./registry");
5
5
  const retry_1 = require("../retry");
6
6
  class OpenAIProvider {
7
7
  name;
8
+ temperature;
8
9
  config;
9
10
  supportsTools;
10
11
  constructor(config) {
@@ -25,6 +26,9 @@ class OpenAIProvider {
25
26
  messages: messages.map(m => this.formatMessage(m)),
26
27
  stream: true,
27
28
  };
29
+ if (this.temperature !== undefined) {
30
+ body.temperature = this.temperature;
31
+ }
28
32
  if (tools?.length && this.supportsTools) {
29
33
  body.tools = tools;
30
34
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Session Replay Engine for CodeBot v1.8.0
3
+ *
4
+ * Replays saved sessions by feeding recorded assistant responses
5
+ * instead of calling the LLM. Tool calls are re-executed and outputs
6
+ * compared against recorded results to detect environment divergences.
7
+ *
8
+ * Usage:
9
+ * codebot --replay <session-id>
10
+ * codebot --replay (replays latest session)
11
+ */
12
+ import { Message, LLMProvider, ToolSchema, StreamEvent } from './types';
13
+ /**
14
+ * Mock LLM provider that feeds recorded assistant messages.
15
+ * Used during replay to bypass actual LLM calls.
16
+ */
17
+ export declare class ReplayProvider implements LLMProvider {
18
+ name: string;
19
+ private assistantMessages;
20
+ private callIndex;
21
+ constructor(assistantMessages: Message[]);
22
+ chat(_messages: Message[], _tools?: ToolSchema[]): AsyncGenerator<StreamEvent>;
23
+ }
24
+ export interface SessionReplayData {
25
+ messages: Message[];
26
+ assistantMessages: Message[];
27
+ userMessages: Message[];
28
+ toolResults: Map<string, string>;
29
+ }
30
+ /**
31
+ * Load a session from disk and prepare it for replay.
32
+ * Returns null if session doesn't exist or is empty.
33
+ */
34
+ export declare function loadSessionForReplay(sessionId: string): SessionReplayData | null;
35
+ export interface ReplayDivergence {
36
+ toolCallId: string;
37
+ toolName: string;
38
+ type: 'output_mismatch';
39
+ diff: string;
40
+ }
41
+ /**
42
+ * Compare recorded vs actual tool output.
43
+ * Returns null if identical, or a diff description.
44
+ */
45
+ export declare function compareOutputs(recorded: string, actual: string): string | null;
46
+ /**
47
+ * List sessions available for replay.
48
+ */
49
+ export declare function listReplayableSessions(limit?: number): Array<{
50
+ id: string;
51
+ preview: string;
52
+ messageCount: number;
53
+ date: string;
54
+ }>;
55
+ //# sourceMappingURL=replay.d.ts.map