codebot-ai 1.8.0 → 2.0.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/README.md +42 -3
- package/dist/agent.d.ts +10 -0
- package/dist/agent.js +55 -11
- package/dist/cli.js +83 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -1
- package/dist/metrics.d.ts +60 -0
- package/dist/metrics.js +296 -0
- package/dist/risk.d.ts +52 -0
- package/dist/risk.js +367 -0
- package/dist/sarif.d.ts +82 -0
- package/dist/sarif.js +176 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/zanderone1980/codebot-ai/blob/main/LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
**Zero-dependency autonomous AI agent.** Works with any LLM — local or cloud. Code, browse the web, run commands, search, automate routines, and more.
|
|
7
|
+
**Zero-dependency autonomous AI coding agent with enterprise security.** Works with any LLM — local or cloud. Code, browse the web, run commands, search, automate routines, and more. Includes VS Code extension, GitHub Action, policy engine, risk scoring, and hash-chained audit trail.
|
|
8
8
|
|
|
9
9
|
Built by [Ascendral Software Development & Innovation](https://github.com/AscendralSoftware).
|
|
10
10
|
|
|
@@ -22,6 +22,27 @@ That's it. The setup wizard launches on first run — pick your model, paste an
|
|
|
22
22
|
npx codebot-ai
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
### VS Code Extension
|
|
26
|
+
|
|
27
|
+
Install from the VS Code Marketplace: search for **CodeBot AI**, or:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
code --install-extension codebot-ai-vscode-2.0.0.vsix
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Features: sidebar chat panel, inline diff preview, status bar (tokens, cost, risk level), and full theme integration.
|
|
34
|
+
|
|
35
|
+
### GitHub Action
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
- uses: zanderone1980/codebot-ai/actions/codebot@v2
|
|
39
|
+
with:
|
|
40
|
+
task: review # or: fix, scan
|
|
41
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Tasks: `review` (PR code review), `fix` (auto-fix CI failures), `scan` (security scan with SARIF upload).
|
|
45
|
+
|
|
25
46
|
## What Can It Do?
|
|
26
47
|
|
|
27
48
|
- **Write & edit code** — reads your codebase, makes targeted edits, runs tests
|
|
@@ -101,6 +122,8 @@ echo "explain this error" | codebot # Pipe mode
|
|
|
101
122
|
/usage Show token usage for this session
|
|
102
123
|
/clear Clear conversation
|
|
103
124
|
/compact Force context compaction
|
|
125
|
+
/metrics Show session metrics (token counts, latency, costs)
|
|
126
|
+
/risk Show risk assessment history
|
|
104
127
|
/config Show configuration
|
|
105
128
|
/quit Exit
|
|
106
129
|
```
|
|
@@ -217,16 +240,31 @@ Connect external tool servers via [Model Context Protocol](https://modelcontextp
|
|
|
217
240
|
|
|
218
241
|
MCP tools appear automatically with the `mcp_<server>_<tool>` prefix.
|
|
219
242
|
|
|
243
|
+
## Security
|
|
244
|
+
|
|
245
|
+
CodeBot v2.0.0 is built with security as a core architectural principle:
|
|
246
|
+
|
|
247
|
+
- **Policy engine** — declarative JSON policies control tool access, filesystem scope, and execution limits
|
|
248
|
+
- **Risk scoring** — every tool call receives a 0-100 risk score based on 6 weighted factors
|
|
249
|
+
- **Secret detection** — scans for AWS keys, GitHub tokens, JWTs, private keys before writing
|
|
250
|
+
- **Sandbox execution** — Docker-based sandboxing with network, CPU, and memory limits
|
|
251
|
+
- **Audit trail** — hash-chained JSONL log with `--verify-audit` integrity check
|
|
252
|
+
- **SARIF export** — `--export-audit sarif` for GitHub Code Scanning integration
|
|
253
|
+
- **SSRF protection** — blocks localhost, private IPs, cloud metadata endpoints
|
|
254
|
+
- **Path safety** — blocks writes to system directories, detects path traversal
|
|
255
|
+
|
|
256
|
+
See [SECURITY.md](SECURITY.md) and [docs/HARDENING.md](docs/HARDENING.md) for the full security model.
|
|
257
|
+
|
|
220
258
|
## Stability
|
|
221
259
|
|
|
222
|
-
CodeBot
|
|
260
|
+
CodeBot is hardened for continuous operation:
|
|
223
261
|
|
|
224
262
|
- **Automatic retry** — network errors, rate limits (429), and server errors (5xx) retry with exponential backoff
|
|
225
263
|
- **Stream recovery** — if the LLM connection drops mid-response, the agent loop retries on the next iteration
|
|
226
264
|
- **Context compaction** — when the conversation exceeds the model's context window, messages are intelligently summarized
|
|
227
265
|
- **Process resilience** — unhandled exceptions and rejections are caught, logged, and the REPL keeps running
|
|
228
266
|
- **Routine timeouts** — scheduled tasks are capped at 5 minutes to prevent the scheduler from hanging
|
|
229
|
-
- **
|
|
267
|
+
- **483 tests** — comprehensive suite covering core agent, security, extension, and action
|
|
230
268
|
|
|
231
269
|
## Programmatic API
|
|
232
270
|
|
|
@@ -245,6 +283,7 @@ const agent = new Agent({
|
|
|
245
283
|
provider,
|
|
246
284
|
model: 'claude-sonnet-4-6',
|
|
247
285
|
autoApprove: true,
|
|
286
|
+
projectRoot: '/path/to/project', // optional, defaults to cwd
|
|
248
287
|
});
|
|
249
288
|
|
|
250
289
|
for await (const event of agent.run('list all TypeScript files')) {
|
package/dist/agent.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Message, AgentEvent, LLMProvider } from './types';
|
|
|
2
2
|
import { AuditLogger } from './audit';
|
|
3
3
|
import { PolicyEnforcer } from './policy';
|
|
4
4
|
import { TokenTracker } from './telemetry';
|
|
5
|
+
import { MetricsCollector } from './metrics';
|
|
6
|
+
import { RiskScorer } from './risk';
|
|
5
7
|
export declare class Agent {
|
|
6
8
|
private provider;
|
|
7
9
|
private tools;
|
|
@@ -15,6 +17,9 @@ export declare class Agent {
|
|
|
15
17
|
private auditLogger;
|
|
16
18
|
private policyEnforcer;
|
|
17
19
|
private tokenTracker;
|
|
20
|
+
private metricsCollector;
|
|
21
|
+
private riskScorer;
|
|
22
|
+
private projectRoot;
|
|
18
23
|
private branchCreated;
|
|
19
24
|
private askPermission;
|
|
20
25
|
private onMessage?;
|
|
@@ -24,6 +29,7 @@ export declare class Agent {
|
|
|
24
29
|
providerName?: string;
|
|
25
30
|
maxIterations?: number;
|
|
26
31
|
autoApprove?: boolean;
|
|
32
|
+
projectRoot?: string;
|
|
27
33
|
askPermission?: (tool: string, args: Record<string, unknown>) => Promise<boolean>;
|
|
28
34
|
onMessage?: (message: Message) => void;
|
|
29
35
|
});
|
|
@@ -44,6 +50,10 @@ export declare class Agent {
|
|
|
44
50
|
getPolicyEnforcer(): PolicyEnforcer;
|
|
45
51
|
/** Get the audit logger for verification */
|
|
46
52
|
getAuditLogger(): AuditLogger;
|
|
53
|
+
/** Get the metrics collector for session metrics */
|
|
54
|
+
getMetrics(): MetricsCollector;
|
|
55
|
+
/** Get the risk scorer for risk assessment history */
|
|
56
|
+
getRiskScorer(): RiskScorer;
|
|
47
57
|
/**
|
|
48
58
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
49
59
|
* Handles three types of corruption:
|
package/dist/agent.js
CHANGED
|
@@ -48,6 +48,8 @@ const rate_limiter_1 = require("./rate-limiter");
|
|
|
48
48
|
const audit_1 = require("./audit");
|
|
49
49
|
const policy_1 = require("./policy");
|
|
50
50
|
const telemetry_1 = require("./telemetry");
|
|
51
|
+
const metrics_1 = require("./metrics");
|
|
52
|
+
const risk_1 = require("./risk");
|
|
51
53
|
/** Lightweight schema validation — returns error string or null if valid */
|
|
52
54
|
function validateToolArgs(args, schema) {
|
|
53
55
|
const props = schema.properties;
|
|
@@ -104,15 +106,19 @@ class Agent {
|
|
|
104
106
|
auditLogger;
|
|
105
107
|
policyEnforcer;
|
|
106
108
|
tokenTracker;
|
|
109
|
+
metricsCollector;
|
|
110
|
+
riskScorer;
|
|
111
|
+
projectRoot;
|
|
107
112
|
branchCreated = false;
|
|
108
113
|
askPermission;
|
|
109
114
|
onMessage;
|
|
110
115
|
constructor(opts) {
|
|
111
116
|
this.provider = opts.provider;
|
|
112
117
|
this.model = opts.model;
|
|
118
|
+
this.projectRoot = opts.projectRoot || process.cwd();
|
|
113
119
|
// Load policy FIRST — tools need it for filesystem/git enforcement
|
|
114
|
-
this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(
|
|
115
|
-
this.tools = new tools_1.ToolRegistry(
|
|
120
|
+
this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(this.projectRoot), this.projectRoot);
|
|
121
|
+
this.tools = new tools_1.ToolRegistry(this.projectRoot, this.policyEnforcer);
|
|
116
122
|
this.context = new manager_1.ContextManager(opts.model, opts.provider);
|
|
117
123
|
// Use policy-defined max iterations as default, CLI overrides
|
|
118
124
|
this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
|
|
@@ -124,12 +130,14 @@ class Agent {
|
|
|
124
130
|
this.auditLogger = new audit_1.AuditLogger();
|
|
125
131
|
// Token & cost tracking
|
|
126
132
|
this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
|
|
133
|
+
this.metricsCollector = new metrics_1.MetricsCollector();
|
|
134
|
+
this.riskScorer = new risk_1.RiskScorer();
|
|
127
135
|
const costLimit = this.policyEnforcer.getCostLimitUsd();
|
|
128
136
|
if (costLimit > 0)
|
|
129
137
|
this.tokenTracker.setCostLimit(costLimit);
|
|
130
138
|
// Load plugins
|
|
131
139
|
try {
|
|
132
|
-
const plugins = (0, plugins_1.loadPlugins)(
|
|
140
|
+
const plugins = (0, plugins_1.loadPlugins)(this.projectRoot);
|
|
133
141
|
for (const plugin of plugins) {
|
|
134
142
|
this.tools.register(plugin);
|
|
135
143
|
}
|
|
@@ -196,6 +204,9 @@ class Agent {
|
|
|
196
204
|
// Track tokens and cost
|
|
197
205
|
if (event.usage) {
|
|
198
206
|
this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
|
|
207
|
+
this.metricsCollector.increment('llm_requests_total');
|
|
208
|
+
this.metricsCollector.increment('llm_tokens_total', { direction: 'input' }, event.usage.inputTokens || 0);
|
|
209
|
+
this.metricsCollector.increment('llm_tokens_total', { direction: 'output' }, event.usage.outputTokens || 0);
|
|
199
210
|
}
|
|
200
211
|
yield { type: 'usage', usage: event.usage };
|
|
201
212
|
break;
|
|
@@ -282,10 +293,17 @@ class Agent {
|
|
|
282
293
|
prepared.push({ tc, tool, args, denied: false, error: `Error: ${validationError} for ${toolName}` });
|
|
283
294
|
continue;
|
|
284
295
|
}
|
|
285
|
-
|
|
286
|
-
// Permission check: policy override > tool default
|
|
296
|
+
// Compute risk score before execution
|
|
287
297
|
const policyPermission = this.policyEnforcer.getToolPermission(toolName);
|
|
288
298
|
const effectivePermission = policyPermission || tool.permission;
|
|
299
|
+
const riskAssessment = this.riskScorer.assess(toolName, args, effectivePermission);
|
|
300
|
+
yield { type: 'tool_call', toolCall: { name: toolName, args }, risk: { score: riskAssessment.score, level: riskAssessment.level } };
|
|
301
|
+
// Log risk breakdown for high-risk calls
|
|
302
|
+
if (riskAssessment.score > 50) {
|
|
303
|
+
const breakdown = riskAssessment.factors.map(f => `${f.name}=${f.rawScore}`).join(', ');
|
|
304
|
+
this.auditLogger.log({ tool: toolName, action: 'execute', args, result: `risk:${riskAssessment.score}`, reason: breakdown });
|
|
305
|
+
}
|
|
306
|
+
// Permission check: policy override > tool default
|
|
289
307
|
const needsPermission = effectivePermission === 'always-ask' ||
|
|
290
308
|
(effectivePermission === 'prompt' && !this.autoApprove);
|
|
291
309
|
let denied = false;
|
|
@@ -294,6 +312,7 @@ class Agent {
|
|
|
294
312
|
if (!approved) {
|
|
295
313
|
denied = true;
|
|
296
314
|
this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
|
|
315
|
+
this.metricsCollector.increment('permission_denials_total', { tool: toolName });
|
|
297
316
|
}
|
|
298
317
|
}
|
|
299
318
|
prepared.push({ tc, tool, args, denied });
|
|
@@ -324,9 +343,10 @@ class Agent {
|
|
|
324
343
|
parallelBatch.push(item);
|
|
325
344
|
}
|
|
326
345
|
}
|
|
327
|
-
// Helper to execute a single tool with cache + rate limiting
|
|
346
|
+
// Helper to execute a single tool with cache + rate limiting + metrics
|
|
328
347
|
const executeTool = async (prep) => {
|
|
329
348
|
const toolName = prep.tc.function.name;
|
|
349
|
+
const toolStartTime = Date.now();
|
|
330
350
|
// Auto-branch on first write/edit when always_branch is enabled (v1.8.0)
|
|
331
351
|
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
|
|
332
352
|
const branchName = await this.ensureBranch();
|
|
@@ -338,6 +358,7 @@ class Agent {
|
|
|
338
358
|
const capBlock = this.checkToolCapabilities(toolName, prep.args);
|
|
339
359
|
if (capBlock) {
|
|
340
360
|
this.auditLogger.log({ tool: toolName, action: 'capability_block', args: prep.args, reason: capBlock });
|
|
361
|
+
this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'capability' });
|
|
341
362
|
return { content: `Error: ${capBlock}`, is_error: true };
|
|
342
363
|
}
|
|
343
364
|
// Check cache first
|
|
@@ -345,19 +366,30 @@ class Agent {
|
|
|
345
366
|
const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
|
|
346
367
|
const cached = this.cache.get(cacheKey);
|
|
347
368
|
if (cached !== null) {
|
|
369
|
+
this.metricsCollector.increment('cache_hits_total', { tool: toolName });
|
|
348
370
|
return { content: cached };
|
|
349
371
|
}
|
|
372
|
+
this.metricsCollector.increment('cache_misses_total', { tool: toolName });
|
|
350
373
|
}
|
|
351
374
|
// Rate limit
|
|
352
375
|
await this.rateLimiter.throttle(toolName);
|
|
353
376
|
try {
|
|
354
377
|
const output = await prep.tool.execute(prep.args);
|
|
378
|
+
// Record tool latency
|
|
379
|
+
const latencyMs = Date.now() - toolStartTime;
|
|
380
|
+
this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
|
|
381
|
+
this.metricsCollector.increment('tool_calls_total', { tool: toolName });
|
|
355
382
|
// Audit log: successful execution
|
|
356
383
|
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
357
384
|
// Telemetry: track tool calls and file modifications
|
|
358
385
|
this.tokenTracker.recordToolCall();
|
|
359
386
|
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
|
|
360
387
|
this.tokenTracker.recordFileModified(prep.args.path);
|
|
388
|
+
this.metricsCollector.increment('files_written_total', { tool: toolName });
|
|
389
|
+
}
|
|
390
|
+
// Track commands executed
|
|
391
|
+
if (toolName === 'execute') {
|
|
392
|
+
this.metricsCollector.increment('commands_executed_total');
|
|
361
393
|
}
|
|
362
394
|
// Store in cache for cacheable tools
|
|
363
395
|
if (prep.tool.cacheable) {
|
|
@@ -373,11 +405,16 @@ class Agent {
|
|
|
373
405
|
// Audit log: check if tool returned a security block
|
|
374
406
|
if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
|
|
375
407
|
this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
|
|
408
|
+
this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'security' });
|
|
376
409
|
}
|
|
377
410
|
return { content: output };
|
|
378
411
|
}
|
|
379
412
|
catch (err) {
|
|
380
413
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
// Record latency even on error
|
|
415
|
+
const latencyMs = Date.now() - toolStartTime;
|
|
416
|
+
this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
|
|
417
|
+
this.metricsCollector.increment('errors_total', { tool: toolName });
|
|
381
418
|
// Audit log: error
|
|
382
419
|
this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
|
|
383
420
|
return { content: `Error: ${errMsg}`, is_error: true };
|
|
@@ -448,6 +485,14 @@ class Agent {
|
|
|
448
485
|
getAuditLogger() {
|
|
449
486
|
return this.auditLogger;
|
|
450
487
|
}
|
|
488
|
+
/** Get the metrics collector for session metrics */
|
|
489
|
+
getMetrics() {
|
|
490
|
+
return this.metricsCollector;
|
|
491
|
+
}
|
|
492
|
+
/** Get the risk scorer for risk assessment history */
|
|
493
|
+
getRiskScorer() {
|
|
494
|
+
return this.riskScorer;
|
|
495
|
+
}
|
|
451
496
|
/**
|
|
452
497
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
453
498
|
* Handles three types of corruption:
|
|
@@ -526,9 +571,8 @@ class Agent {
|
|
|
526
571
|
return null;
|
|
527
572
|
try {
|
|
528
573
|
const { execSync } = require('child_process');
|
|
529
|
-
const cwd = process.cwd();
|
|
530
574
|
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
531
|
-
cwd, encoding: 'utf-8', timeout: 5000,
|
|
575
|
+
cwd: this.projectRoot, encoding: 'utf-8', timeout: 5000,
|
|
532
576
|
}).trim();
|
|
533
577
|
if (currentBranch !== 'main' && currentBranch !== 'master') {
|
|
534
578
|
this.branchCreated = true;
|
|
@@ -541,7 +585,7 @@ class Agent {
|
|
|
541
585
|
const slug = this.sanitizeSlug(firstUserMsg?.content || 'task');
|
|
542
586
|
const branchName = `${prefix}${timestamp}-${slug}`;
|
|
543
587
|
execSync(`git checkout -b "${branchName}"`, {
|
|
544
|
-
cwd, encoding: 'utf-8', timeout: 10000,
|
|
588
|
+
cwd: this.projectRoot, encoding: 'utf-8', timeout: 10000,
|
|
545
589
|
});
|
|
546
590
|
this.branchCreated = true;
|
|
547
591
|
return branchName;
|
|
@@ -590,7 +634,7 @@ class Agent {
|
|
|
590
634
|
buildSystemPrompt(supportsTools) {
|
|
591
635
|
let repoMap = '';
|
|
592
636
|
try {
|
|
593
|
-
repoMap = (0, repo_map_1.buildRepoMap)(
|
|
637
|
+
repoMap = (0, repo_map_1.buildRepoMap)(this.projectRoot);
|
|
594
638
|
}
|
|
595
639
|
catch {
|
|
596
640
|
repoMap = 'Project structure: (unable to scan)';
|
|
@@ -598,7 +642,7 @@ class Agent {
|
|
|
598
642
|
// Load persistent memory
|
|
599
643
|
let memoryBlock = '';
|
|
600
644
|
try {
|
|
601
|
-
const memory = new memory_1.MemoryManager(
|
|
645
|
+
const memory = new memory_1.MemoryManager(this.projectRoot);
|
|
602
646
|
memoryBlock = memory.getContextBlock();
|
|
603
647
|
}
|
|
604
648
|
catch {
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,9 @@ const audit_1 = require("./audit");
|
|
|
50
50
|
const policy_1 = require("./policy");
|
|
51
51
|
const sandbox_1 = require("./sandbox");
|
|
52
52
|
const replay_1 = require("./replay");
|
|
53
|
-
const
|
|
53
|
+
const risk_1 = require("./risk");
|
|
54
|
+
const sarif_1 = require("./sarif");
|
|
55
|
+
const VERSION = '2.0.0';
|
|
54
56
|
const C = {
|
|
55
57
|
reset: '\x1b[0m',
|
|
56
58
|
bold: '\x1b[1m',
|
|
@@ -228,6 +230,19 @@ async function main() {
|
|
|
228
230
|
}
|
|
229
231
|
return;
|
|
230
232
|
}
|
|
233
|
+
// --export-audit sarif: Export audit log as SARIF 2.1.0
|
|
234
|
+
if (args['export-audit'] === 'sarif' || args['export-audit'] === true) {
|
|
235
|
+
const logger = new audit_1.AuditLogger();
|
|
236
|
+
const sessionId = typeof args['session'] === 'string' ? args['session'] : undefined;
|
|
237
|
+
const entries = sessionId ? logger.query({ sessionId }) : logger.query();
|
|
238
|
+
if (entries.length === 0) {
|
|
239
|
+
console.error(c('No audit entries found.', 'yellow'));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const sarif = (0, sarif_1.exportSarif)(entries, { version: VERSION, sessionId });
|
|
243
|
+
process.stdout.write((0, sarif_1.sarifToString)(sarif) + '\n');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
231
246
|
// First run: auto-launch setup if nothing is configured
|
|
232
247
|
if ((0, setup_1.isFirstRun)() && process.stdin.isTTY && !args.message) {
|
|
233
248
|
console.log(c('Welcome! No configuration found — launching setup...', 'cyan'));
|
|
@@ -299,7 +314,7 @@ async function main() {
|
|
|
299
314
|
// Cleanup scheduler on exit
|
|
300
315
|
scheduler.stop();
|
|
301
316
|
}
|
|
302
|
-
/** Print session summary with tokens, cost, tool calls, files modified */
|
|
317
|
+
/** Print session summary with tokens, cost, tool calls, files modified, metrics */
|
|
303
318
|
function printSessionSummary(agent) {
|
|
304
319
|
const tracker = agent.getTokenTracker();
|
|
305
320
|
tracker.saveUsage();
|
|
@@ -314,6 +329,27 @@ function printSessionSummary(agent) {
|
|
|
314
329
|
console.log(` Requests: ${summary.requestCount}`);
|
|
315
330
|
console.log(` Tools: ${summary.toolCalls} calls`);
|
|
316
331
|
console.log(` Files: ${summary.filesModified} modified`);
|
|
332
|
+
// v1.9.0: Per-tool breakdown from MetricsCollector
|
|
333
|
+
const metrics = agent.getMetrics();
|
|
334
|
+
const snap = metrics.snapshot();
|
|
335
|
+
const toolCounters = snap.counters.filter(c => c.name === 'tool_calls_total');
|
|
336
|
+
if (toolCounters.length > 0) {
|
|
337
|
+
console.log(c(' Per-tool:', 'dim'));
|
|
338
|
+
for (const tc of toolCounters.sort((a, b) => b.value - a.value)) {
|
|
339
|
+
const hist = snap.histograms.find(h => h.name === 'tool_latency_seconds' && h.labels.tool === tc.labels.tool);
|
|
340
|
+
const avg = hist && hist.count > 0 ? (hist.sum / hist.count * 1000).toFixed(0) : '?';
|
|
341
|
+
console.log(c(` ${tc.labels.tool}: ${tc.value} calls (avg ${avg}ms)`, 'dim'));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Risk summary
|
|
345
|
+
const riskScorer = agent.getRiskScorer();
|
|
346
|
+
const riskAvg = riskScorer.getSessionAverage();
|
|
347
|
+
if (riskScorer.getHistory().length > 0) {
|
|
348
|
+
console.log(` Risk: avg ${riskAvg}/100`);
|
|
349
|
+
}
|
|
350
|
+
// Save metrics
|
|
351
|
+
metrics.save();
|
|
352
|
+
metrics.exportOtel();
|
|
317
353
|
}
|
|
318
354
|
function createProvider(config) {
|
|
319
355
|
if (config.provider === 'anthropic') {
|
|
@@ -393,8 +429,14 @@ function renderEvent(event, agent) {
|
|
|
393
429
|
process.stdout.write('\n');
|
|
394
430
|
isThinking = false;
|
|
395
431
|
}
|
|
396
|
-
|
|
397
|
-
|
|
432
|
+
{
|
|
433
|
+
const riskStr = event.risk
|
|
434
|
+
? ' ' + risk_1.RiskScorer.formatIndicator({ score: event.risk.score, level: event.risk.level, factors: [] })
|
|
435
|
+
: '';
|
|
436
|
+
console.log(c(`\n⚡ ${event.toolCall?.name}`, 'yellow') +
|
|
437
|
+
c(`(${formatArgs(event.toolCall?.args)})`, 'dim') +
|
|
438
|
+
riskStr);
|
|
439
|
+
}
|
|
398
440
|
break;
|
|
399
441
|
case 'tool_result':
|
|
400
442
|
if (event.toolResult?.is_error) {
|
|
@@ -467,6 +509,8 @@ function handleSlashCommand(input, agent, config) {
|
|
|
467
509
|
/undo Undo last file edit (/undo [path])
|
|
468
510
|
/usage Show token usage & cost for this session
|
|
469
511
|
/cost Show running cost
|
|
512
|
+
/metrics Show session metrics (counters + histograms)
|
|
513
|
+
/risk Show risk assessment summary
|
|
470
514
|
/policy Show current security policy
|
|
471
515
|
/audit Verify audit chain for this session
|
|
472
516
|
/config Show current config
|
|
@@ -543,6 +587,26 @@ function handleSlashCommand(input, agent, config) {
|
|
|
543
587
|
console.log(c(` ${tracker.formatStatusLine()}`, 'dim'));
|
|
544
588
|
break;
|
|
545
589
|
}
|
|
590
|
+
case '/metrics': {
|
|
591
|
+
const metricsOutput = agent.getMetrics().formatSummary();
|
|
592
|
+
console.log('\n' + metricsOutput);
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case '/risk': {
|
|
596
|
+
const riskHistory = agent.getRiskScorer().getHistory();
|
|
597
|
+
if (riskHistory.length === 0) {
|
|
598
|
+
console.log(c('No risk assessments yet.', 'dim'));
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const avg = agent.getRiskScorer().getSessionAverage();
|
|
602
|
+
console.log(c(`\nRisk Summary: ${riskHistory.length} assessments, avg ${avg}/100`, 'bold'));
|
|
603
|
+
const last5 = riskHistory.slice(-5);
|
|
604
|
+
for (const a of last5) {
|
|
605
|
+
console.log(` ${risk_1.RiskScorer.formatIndicator(a)}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
546
610
|
case '/policy': {
|
|
547
611
|
const policy = agent.getPolicyEnforcer().getPolicy();
|
|
548
612
|
console.log(c('\nCurrent Policy:', 'bold'));
|
|
@@ -702,6 +766,17 @@ function parseArgs(argv) {
|
|
|
702
766
|
}
|
|
703
767
|
continue;
|
|
704
768
|
}
|
|
769
|
+
if (arg === '--export-audit') {
|
|
770
|
+
const next = argv[i + 1];
|
|
771
|
+
if (next && !next.startsWith('--')) {
|
|
772
|
+
result['export-audit'] = next;
|
|
773
|
+
i++;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
result['export-audit'] = true;
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
705
780
|
if (arg === '--replay') {
|
|
706
781
|
const next = argv[i + 1];
|
|
707
782
|
if (next && !next.startsWith('--')) {
|
|
@@ -770,6 +845,7 @@ ${c('Options:', 'bold')}
|
|
|
770
845
|
${c('Security & Policy:', 'bold')}
|
|
771
846
|
--init-policy Generate default .codebot/policy.json
|
|
772
847
|
--verify-audit [id] Verify audit log hash chain integrity
|
|
848
|
+
--export-audit sarif Export audit log as SARIF 2.1.0 JSON
|
|
773
849
|
--sandbox-info Show Docker sandbox status
|
|
774
850
|
|
|
775
851
|
${c('Debugging & Replay:', 'bold')}
|
|
@@ -795,6 +871,7 @@ ${c('Examples:', 'bold')}
|
|
|
795
871
|
codebot --autonomous "refactor src/" Full auto, no prompts
|
|
796
872
|
codebot --init-policy Create security policy
|
|
797
873
|
codebot --verify-audit Check audit integrity
|
|
874
|
+
codebot --export-audit sarif > r.sarif Export SARIF report
|
|
798
875
|
|
|
799
876
|
${c('Interactive Commands:', 'bold')}
|
|
800
877
|
/help Show commands
|
|
@@ -806,6 +883,8 @@ ${c('Interactive Commands:', 'bold')}
|
|
|
806
883
|
/compact Force context compaction
|
|
807
884
|
/usage Show token usage & cost
|
|
808
885
|
/cost Show running cost
|
|
886
|
+
/metrics Show session metrics
|
|
887
|
+
/risk Show risk assessment summary
|
|
809
888
|
/policy Show security policy
|
|
810
889
|
/audit Verify session audit chain
|
|
811
890
|
/config Show configuration
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const VERSION = "2.0.0";
|
|
1
2
|
export { Agent } from './agent';
|
|
2
3
|
export { OpenAIProvider } from './providers/openai';
|
|
3
4
|
export { AnthropicProvider } from './providers/anthropic';
|
|
@@ -17,5 +18,11 @@ export { deriveSessionKey, signMessage, verifyMessage, verifyMessages } from './
|
|
|
17
18
|
export type { IntegrityResult } from './integrity';
|
|
18
19
|
export { ReplayProvider, loadSessionForReplay, compareOutputs, listReplayableSessions } from './replay';
|
|
19
20
|
export type { SessionReplayData, ReplayDivergence } from './replay';
|
|
21
|
+
export { MetricsCollector } from './metrics';
|
|
22
|
+
export type { MetricsSnapshot, CounterValue, HistogramValue } from './metrics';
|
|
23
|
+
export { RiskScorer } from './risk';
|
|
24
|
+
export type { RiskAssessment, RiskFactor } from './risk';
|
|
25
|
+
export { exportSarif, sarifToString } from './sarif';
|
|
26
|
+
export type { SarifLog, SarifResult, SarifRule } from './sarif';
|
|
20
27
|
export * from './types';
|
|
21
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.listReplayableSessions = exports.compareOutputs = exports.loadSessionForReplay = exports.ReplayProvider = exports.verifyMessages = exports.verifyMessage = exports.signMessage = exports.deriveSessionKey = exports.CapabilityChecker = exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = void 0;
|
|
17
|
+
exports.sarifToString = exports.exportSarif = exports.RiskScorer = exports.MetricsCollector = exports.listReplayableSessions = exports.compareOutputs = exports.loadSessionForReplay = exports.ReplayProvider = exports.verifyMessages = exports.verifyMessage = exports.signMessage = exports.deriveSessionKey = exports.CapabilityChecker = exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = exports.VERSION = void 0;
|
|
18
|
+
exports.VERSION = '2.0.0';
|
|
18
19
|
var agent_1 = require("./agent");
|
|
19
20
|
Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return agent_1.Agent; } });
|
|
20
21
|
var openai_1 = require("./providers/openai");
|
|
@@ -54,5 +55,12 @@ Object.defineProperty(exports, "ReplayProvider", { enumerable: true, get: functi
|
|
|
54
55
|
Object.defineProperty(exports, "loadSessionForReplay", { enumerable: true, get: function () { return replay_1.loadSessionForReplay; } });
|
|
55
56
|
Object.defineProperty(exports, "compareOutputs", { enumerable: true, get: function () { return replay_1.compareOutputs; } });
|
|
56
57
|
Object.defineProperty(exports, "listReplayableSessions", { enumerable: true, get: function () { return replay_1.listReplayableSessions; } });
|
|
58
|
+
var metrics_1 = require("./metrics");
|
|
59
|
+
Object.defineProperty(exports, "MetricsCollector", { enumerable: true, get: function () { return metrics_1.MetricsCollector; } });
|
|
60
|
+
var risk_1 = require("./risk");
|
|
61
|
+
Object.defineProperty(exports, "RiskScorer", { enumerable: true, get: function () { return risk_1.RiskScorer; } });
|
|
62
|
+
var sarif_1 = require("./sarif");
|
|
63
|
+
Object.defineProperty(exports, "exportSarif", { enumerable: true, get: function () { return sarif_1.exportSarif; } });
|
|
64
|
+
Object.defineProperty(exports, "sarifToString", { enumerable: true, get: function () { return sarif_1.sarifToString; } });
|
|
57
65
|
__exportStar(require("./types"), exports);
|
|
58
66
|
//# sourceMappingURL=index.js.map
|
|
@@ -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
|