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.
package/dist/cli.js CHANGED
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.main = main;
37
37
  const readline = __importStar(require("readline"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
38
40
  const agent_1 = require("./agent");
39
41
  const openai_1 = require("./providers/openai");
40
42
  const anthropic_1 = require("./providers/anthropic");
@@ -44,9 +46,10 @@ const setup_1 = require("./setup");
44
46
  const banner_1 = require("./banner");
45
47
  const tools_1 = require("./tools");
46
48
  const scheduler_1 = require("./scheduler");
47
- const VERSION = '1.6.0';
48
- // Session-wide token tracking
49
- let sessionTokens = { input: 0, output: 0, total: 0 };
49
+ const audit_1 = require("./audit");
50
+ const policy_1 = require("./policy");
51
+ const sandbox_1 = require("./sandbox");
52
+ const VERSION = '1.7.0';
50
53
  const C = {
51
54
  reset: '\x1b[0m',
52
55
  bold: '\x1b[1m',
@@ -86,12 +89,89 @@ async function main() {
86
89
  await (0, setup_1.runSetup)();
87
90
  return;
88
91
  }
92
+ // ── v1.7.0: New standalone commands ──
93
+ // --init-policy: Generate default policy file
94
+ if (args['init-policy']) {
95
+ const policyPath = path.join(process.cwd(), '.codebot', 'policy.json');
96
+ const policyDir = path.dirname(policyPath);
97
+ if (!fs.existsSync(policyDir))
98
+ fs.mkdirSync(policyDir, { recursive: true });
99
+ if (fs.existsSync(policyPath)) {
100
+ console.log(c(`Policy file already exists at ${policyPath}`, 'yellow'));
101
+ console.log(c('Delete it first if you want to regenerate.', 'dim'));
102
+ }
103
+ else {
104
+ fs.writeFileSync(policyPath, (0, policy_1.generateDefaultPolicyFile)(), 'utf-8');
105
+ console.log(c(`Created default policy at ${policyPath}`, 'green'));
106
+ }
107
+ return;
108
+ }
109
+ // --verify-audit: Verify audit chain integrity
110
+ if (args['verify-audit']) {
111
+ const logger = new audit_1.AuditLogger();
112
+ const sessionId = typeof args['verify-audit'] === 'string' ? args['verify-audit'] : undefined;
113
+ if (sessionId) {
114
+ const entries = logger.query({ sessionId });
115
+ if (entries.length === 0) {
116
+ console.log(c(`No audit entries found for session ${sessionId}`, 'yellow'));
117
+ return;
118
+ }
119
+ const result = audit_1.AuditLogger.verify(entries);
120
+ if (result.valid) {
121
+ console.log(c(`Audit chain valid (${result.entriesChecked} entries checked)`, 'green'));
122
+ }
123
+ else {
124
+ console.log(c(`Audit chain INVALID at sequence ${result.firstInvalidAt}`, 'red'));
125
+ console.log(c(`Reason: ${result.reason}`, 'red'));
126
+ }
127
+ }
128
+ else {
129
+ // Verify all entries from today's log
130
+ const entries = logger.query();
131
+ if (entries.length === 0) {
132
+ console.log(c('No audit entries found.', 'yellow'));
133
+ return;
134
+ }
135
+ // Group by session and verify each
136
+ const sessions = new Map();
137
+ for (const e of entries) {
138
+ if (!sessions.has(e.sessionId))
139
+ sessions.set(e.sessionId, []);
140
+ sessions.get(e.sessionId).push(e);
141
+ }
142
+ let allValid = true;
143
+ for (const [sid, sessionEntries] of sessions) {
144
+ const result = audit_1.AuditLogger.verify(sessionEntries);
145
+ const shortId = sid.substring(0, 12);
146
+ if (result.valid) {
147
+ console.log(c(` ${shortId} ${result.entriesChecked} entries valid`, 'green'));
148
+ }
149
+ else {
150
+ console.log(c(` ${shortId} INVALID at seq ${result.firstInvalidAt}: ${result.reason}`, 'red'));
151
+ allValid = false;
152
+ }
153
+ }
154
+ console.log(allValid
155
+ ? c(`\nAll ${sessions.size} session chains verified.`, 'green')
156
+ : c(`\nSome chains are invalid — possible tampering detected.`, 'red'));
157
+ }
158
+ return;
159
+ }
160
+ // --sandbox-info: Show sandbox status
161
+ if (args['sandbox-info']) {
162
+ const info = (0, sandbox_1.getSandboxInfo)();
163
+ console.log(c('Sandbox Status:', 'bold'));
164
+ console.log(` Docker: ${info.available ? c('available', 'green') : c('not available', 'yellow')}`);
165
+ console.log(` Image: ${info.image}`);
166
+ console.log(` CPU: ${info.defaults.cpus} cores max`);
167
+ console.log(` Memory: ${info.defaults.memoryMb}MB max`);
168
+ console.log(` Network: ${info.defaults.network ? 'enabled' : 'disabled'} by default`);
169
+ return;
170
+ }
89
171
  // First run: auto-launch setup if nothing is configured
90
172
  if ((0, setup_1.isFirstRun)() && process.stdin.isTTY && !args.message) {
91
173
  console.log(c('Welcome! No configuration found — launching setup...', 'cyan'));
92
174
  await (0, setup_1.runSetup)();
93
- // If setup saved a config, continue to main flow
94
- // Otherwise exit
95
175
  if ((0, setup_1.isFirstRun)())
96
176
  return;
97
177
  }
@@ -118,6 +198,7 @@ async function main() {
118
198
  const agent = new agent_1.Agent({
119
199
  provider,
120
200
  model: config.model,
201
+ providerName: config.provider,
121
202
  maxIterations: config.maxIterations,
122
203
  autoApprove: config.autoApprove,
123
204
  onMessage: (msg) => session.save(msg),
@@ -133,6 +214,7 @@ async function main() {
133
214
  // Non-interactive: single message from CLI args
134
215
  if (typeof args.message === 'string') {
135
216
  await runOnce(agent, args.message);
217
+ printSessionSummary(agent);
136
218
  return;
137
219
  }
138
220
  // Non-interactive: piped stdin
@@ -140,6 +222,7 @@ async function main() {
140
222
  const input = await readStdin();
141
223
  if (input.trim()) {
142
224
  await runOnce(agent, input.trim());
225
+ printSessionSummary(agent);
143
226
  }
144
227
  return;
145
228
  }
@@ -151,6 +234,22 @@ async function main() {
151
234
  // Cleanup scheduler on exit
152
235
  scheduler.stop();
153
236
  }
237
+ /** Print session summary with tokens, cost, tool calls, files modified */
238
+ function printSessionSummary(agent) {
239
+ const tracker = agent.getTokenTracker();
240
+ tracker.saveUsage();
241
+ const summary = tracker.getSummary();
242
+ const duration = (new Date(summary.endTime).getTime() - new Date(summary.startTime).getTime()) / 1000;
243
+ const mins = Math.floor(duration / 60);
244
+ const secs = Math.floor(duration % 60);
245
+ console.log(c('\n── Session Summary ──', 'dim'));
246
+ console.log(` Duration: ${mins}m ${secs}s`);
247
+ console.log(` Model: ${summary.model} via ${summary.provider}`);
248
+ console.log(` Tokens: ${summary.totalInputTokens.toLocaleString()} in / ${summary.totalOutputTokens.toLocaleString()} out (${tracker.formatCost()})`);
249
+ console.log(` Requests: ${summary.requestCount}`);
250
+ console.log(` Tools: ${summary.toolCalls} calls`);
251
+ console.log(` Files: ${summary.filesModified} modified`);
252
+ }
154
253
  function createProvider(config) {
155
254
  if (config.provider === 'anthropic') {
156
255
  return new anthropic_1.AnthropicProvider({
@@ -159,7 +258,6 @@ function createProvider(config) {
159
258
  model: config.model,
160
259
  });
161
260
  }
162
- // All other providers use OpenAI-compatible format
163
261
  return new openai_1.OpenAIProvider({
164
262
  baseUrl: config.baseUrl,
165
263
  apiKey: config.apiKey,
@@ -186,7 +284,7 @@ async function repl(agent, config, session) {
186
284
  }
187
285
  try {
188
286
  for await (const event of agent.run(input)) {
189
- renderEvent(event);
287
+ renderEvent(event, agent);
190
288
  }
191
289
  }
192
290
  catch (err) {
@@ -197,18 +295,19 @@ async function repl(agent, config, session) {
197
295
  rl.prompt();
198
296
  });
199
297
  rl.on('close', () => {
298
+ printSessionSummary(agent);
200
299
  console.log(c('\nBye!', 'dim'));
201
300
  process.exit(0);
202
301
  });
203
302
  }
204
303
  async function runOnce(agent, message) {
205
304
  for await (const event of agent.run(message)) {
206
- renderEvent(event);
305
+ renderEvent(event, agent);
207
306
  }
208
307
  console.log();
209
308
  }
210
309
  let isThinking = false;
211
- function renderEvent(event) {
310
+ function renderEvent(event, agent) {
212
311
  switch (event.type) {
213
312
  case 'thinking':
214
313
  if (!isThinking) {
@@ -248,20 +347,15 @@ function renderEvent(event) {
248
347
  }
249
348
  break;
250
349
  case 'usage':
251
- if (event.usage) {
252
- if (event.usage.inputTokens)
253
- sessionTokens.input += event.usage.inputTokens;
254
- if (event.usage.outputTokens)
255
- sessionTokens.output += event.usage.outputTokens;
256
- if (event.usage.totalTokens)
257
- sessionTokens.total += event.usage.totalTokens;
350
+ if (event.usage && agent) {
351
+ const tracker = agent.getTokenTracker();
258
352
  const parts = [];
259
353
  if (event.usage.inputTokens)
260
354
  parts.push(`in: ${event.usage.inputTokens}`);
261
355
  if (event.usage.outputTokens)
262
356
  parts.push(`out: ${event.usage.outputTokens}`);
263
357
  if (parts.length > 0) {
264
- console.log(c(` [${parts.join(', ')} tokens]`, 'dim'));
358
+ console.log(c(` [${parts.join(', ')} tokens | ${tracker.formatCost()}]`, 'dim'));
265
359
  }
266
360
  }
267
361
  break;
@@ -306,7 +400,10 @@ function handleSlashCommand(input, agent, config) {
306
400
  /auto Toggle autonomous mode
307
401
  /routines List scheduled routines
308
402
  /undo Undo last file edit (/undo [path])
309
- /usage Show token usage for this session
403
+ /usage Show token usage & cost for this session
404
+ /cost Show running cost
405
+ /policy Show current security policy
406
+ /audit Verify audit chain for this session
310
407
  /config Show current config
311
408
  /quit Exit`);
312
409
  break;
@@ -365,10 +462,41 @@ function handleSlashCommand(input, agent, config) {
365
462
  break;
366
463
  }
367
464
  case '/usage': {
368
- console.log(c('\nToken Usage (this session):', 'bold'));
369
- console.log(` Input: ${sessionTokens.input.toLocaleString()} tokens`);
370
- console.log(` Output: ${sessionTokens.output.toLocaleString()} tokens`);
371
- console.log(` Total: ${(sessionTokens.input + sessionTokens.output).toLocaleString()} tokens`);
465
+ const tracker = agent.getTokenTracker();
466
+ const summary = tracker.getSummary();
467
+ console.log(c('\nSession Usage:', 'bold'));
468
+ console.log(` Input: ${summary.totalInputTokens.toLocaleString()} tokens`);
469
+ console.log(` Output: ${summary.totalOutputTokens.toLocaleString()} tokens`);
470
+ console.log(` Cost: ${tracker.formatCost()}`);
471
+ console.log(` Requests: ${summary.requestCount}`);
472
+ console.log(` Tools: ${summary.toolCalls} calls`);
473
+ console.log(` Files: ${summary.filesModified} modified`);
474
+ break;
475
+ }
476
+ case '/cost': {
477
+ const tracker = agent.getTokenTracker();
478
+ console.log(c(` ${tracker.formatStatusLine()}`, 'dim'));
479
+ break;
480
+ }
481
+ case '/policy': {
482
+ const policy = agent.getPolicyEnforcer().getPolicy();
483
+ console.log(c('\nCurrent Policy:', 'bold'));
484
+ console.log(JSON.stringify(policy, null, 2));
485
+ break;
486
+ }
487
+ case '/audit': {
488
+ const auditLogger = agent.getAuditLogger();
489
+ const result = auditLogger.verifySession();
490
+ if (result.entriesChecked === 0) {
491
+ console.log(c('No audit entries yet.', 'dim'));
492
+ }
493
+ else if (result.valid) {
494
+ console.log(c(`Audit chain valid (${result.entriesChecked} entries)`, 'green'));
495
+ }
496
+ else {
497
+ console.log(c(`Audit chain INVALID at sequence ${result.firstInvalidAt}`, 'red'));
498
+ console.log(c(` ${result.reason}`, 'red'));
499
+ }
372
500
  break;
373
501
  }
374
502
  case '/routines': {
@@ -409,7 +537,6 @@ function showModels() {
409
537
  }
410
538
  }
411
539
  async function resolveConfig(args) {
412
- // Load saved config (CLI args override saved config)
413
540
  const saved = (0, setup_1.loadConfig)();
414
541
  const model = args.model || process.env.CODEBOT_MODEL || saved.model || 'qwen2.5-coder:32b';
415
542
  const detected = (0, registry_1.detectProvider)(model);
@@ -421,7 +548,6 @@ async function resolveConfig(args) {
421
548
  maxIterations: parseInt(args['max-iterations'] || String(saved.maxIterations || 50), 10),
422
549
  autoApprove: !!args['auto-approve'] || !!args.autonomous || !!args.auto || !!saved.autoApprove,
423
550
  };
424
- // Auto-resolve base URL and API key from provider
425
551
  if (!config.baseUrl || !config.apiKey) {
426
552
  const defaults = registry_1.PROVIDER_DEFAULTS[config.provider];
427
553
  if (defaults) {
@@ -431,14 +557,12 @@ async function resolveConfig(args) {
431
557
  config.apiKey = process.env[defaults.envKey] || process.env.CODEBOT_API_KEY || '';
432
558
  }
433
559
  }
434
- // Fallback: try saved config API key, then generic env vars
435
560
  if (!config.apiKey && saved.apiKey) {
436
561
  config.apiKey = saved.apiKey;
437
562
  }
438
563
  if (!config.apiKey) {
439
564
  config.apiKey = process.env.CODEBOT_API_KEY || process.env.OPENAI_API_KEY || '';
440
565
  }
441
- // If still no base URL, auto-detect local provider
442
566
  if (!config.baseUrl) {
443
567
  config.baseUrl = await autoDetectProvider();
444
568
  }
@@ -494,6 +618,25 @@ function parseArgs(argv) {
494
618
  result.setup = true;
495
619
  continue;
496
620
  }
621
+ if (arg === '--init-policy') {
622
+ result['init-policy'] = true;
623
+ continue;
624
+ }
625
+ if (arg === '--sandbox-info') {
626
+ result['sandbox-info'] = true;
627
+ continue;
628
+ }
629
+ if (arg === '--verify-audit') {
630
+ const next = argv[i + 1];
631
+ if (next && !next.startsWith('--')) {
632
+ result['verify-audit'] = next;
633
+ i++;
634
+ }
635
+ else {
636
+ result['verify-audit'] = true;
637
+ }
638
+ continue;
639
+ }
497
640
  if (arg.startsWith('--')) {
498
641
  const key = arg.slice(2);
499
642
  const next = argv[i + 1];
@@ -540,9 +683,15 @@ ${c('Options:', 'bold')}
540
683
  --resume <id> Resume a previous session by ID
541
684
  --continue, -c Resume the most recent session
542
685
  --max-iterations <n> Max agent loop iterations (default: 50)
686
+ --sandbox <mode> Execution sandbox: docker, host, auto (default: auto)
543
687
  -h, --help Show this help
544
688
  -v, --version Show version
545
689
 
690
+ ${c('Security & Policy:', 'bold')}
691
+ --init-policy Generate default .codebot/policy.json
692
+ --verify-audit [id] Verify audit log hash chain integrity
693
+ --sandbox-info Show Docker sandbox status
694
+
546
695
  ${c('Supported Providers:', 'bold')}
547
696
  Local: Ollama, LM Studio, vLLM (auto-detected)
548
697
  Anthropic: Claude Opus/Sonnet/Haiku (ANTHROPIC_API_KEY)
@@ -560,6 +709,8 @@ ${c('Examples:', 'bold')}
560
709
  codebot --model deepseek-chat Uses DeepSeek API
561
710
  codebot --model qwen2.5-coder:32b Uses local Ollama
562
711
  codebot --autonomous "refactor src/" Full auto, no prompts
712
+ codebot --init-policy Create security policy
713
+ codebot --verify-audit Check audit integrity
563
714
 
564
715
  ${c('Interactive Commands:', 'bold')}
565
716
  /help Show commands
@@ -569,6 +720,10 @@ ${c('Interactive Commands:', 'bold')}
569
720
  /auto Toggle autonomous mode
570
721
  /clear Clear conversation
571
722
  /compact Force context compaction
723
+ /usage Show token usage & cost
724
+ /cost Show running cost
725
+ /policy Show security policy
726
+ /audit Verify session audit chain
572
727
  /config Show configuration
573
728
  /quit Exit`);
574
729
  }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Policy Engine for CodeBot v1.7.0
3
+ *
4
+ * Loads, validates, and enforces declarative security policies.
5
+ * Policy files: .codebot/policy.json (project) + ~/.codebot/policy.json (global)
6
+ * Project policy overrides global policy where specified.
7
+ */
8
+ export interface PolicyExecution {
9
+ sandbox?: 'docker' | 'host' | 'auto';
10
+ network?: boolean;
11
+ timeout_seconds?: number;
12
+ max_memory_mb?: number;
13
+ }
14
+ export interface PolicyFilesystem {
15
+ writable_paths?: string[];
16
+ read_only_paths?: string[];
17
+ denied_paths?: string[];
18
+ allow_outside_project?: boolean;
19
+ }
20
+ export interface PolicyToolPermission {
21
+ [toolName: string]: 'auto' | 'prompt' | 'always-ask';
22
+ }
23
+ export interface PolicyTools {
24
+ enabled?: string[];
25
+ disabled?: string[];
26
+ permissions?: PolicyToolPermission;
27
+ }
28
+ export interface PolicySecrets {
29
+ block_on_detect?: boolean;
30
+ scan_on_write?: boolean;
31
+ allowed_patterns?: string[];
32
+ }
33
+ export interface PolicyGit {
34
+ always_branch?: boolean;
35
+ branch_prefix?: string;
36
+ require_tests_before_commit?: boolean;
37
+ never_push_main?: boolean;
38
+ }
39
+ export interface PolicyMcp {
40
+ allowed_servers?: string[];
41
+ blocked_servers?: string[];
42
+ }
43
+ export interface PolicyLimits {
44
+ max_iterations?: number;
45
+ max_file_size_kb?: number;
46
+ max_files_per_operation?: number;
47
+ cost_limit_usd?: number;
48
+ }
49
+ export interface Policy {
50
+ version?: string;
51
+ execution?: PolicyExecution;
52
+ filesystem?: PolicyFilesystem;
53
+ tools?: PolicyTools;
54
+ secrets?: PolicySecrets;
55
+ git?: PolicyGit;
56
+ mcp?: PolicyMcp;
57
+ limits?: PolicyLimits;
58
+ }
59
+ export declare const DEFAULT_POLICY: Required<Policy>;
60
+ /**
61
+ * Load and merge policies from project + global locations.
62
+ * Project policy overrides global where specified.
63
+ */
64
+ export declare function loadPolicy(projectRoot?: string): Policy;
65
+ export declare class PolicyEnforcer {
66
+ private policy;
67
+ private projectRoot;
68
+ constructor(policy?: Policy, projectRoot?: string);
69
+ getPolicy(): Policy;
70
+ /** Check if a tool is enabled by policy. Returns { allowed, reason }. */
71
+ isToolAllowed(toolName: string): {
72
+ allowed: boolean;
73
+ reason?: string;
74
+ };
75
+ /** Get the permission level for a tool (policy override or null for default). */
76
+ getToolPermission(toolName: string): 'auto' | 'prompt' | 'always-ask' | null;
77
+ /** Check if a path is writable according to policy. */
78
+ isPathWritable(filePath: string): {
79
+ allowed: boolean;
80
+ reason?: string;
81
+ };
82
+ /** Get sandbox mode. */
83
+ getSandboxMode(): 'docker' | 'host' | 'auto';
84
+ /** Check if network is allowed for executed commands. */
85
+ isNetworkAllowed(): boolean;
86
+ /** Get execution timeout in milliseconds. */
87
+ getTimeoutMs(): number;
88
+ /** Get max memory in MB for sandbox. */
89
+ getMaxMemoryMb(): number;
90
+ /** Check if agent should always work on a branch. */
91
+ shouldAlwaysBranch(): boolean;
92
+ /** Get branch prefix for auto-created branches. */
93
+ getBranchPrefix(): string;
94
+ /** Check if pushing to main/master is blocked. */
95
+ isMainPushBlocked(): boolean;
96
+ /** Should secrets block writes (vs just warn)? */
97
+ shouldBlockSecrets(): boolean;
98
+ /** Should scan for secrets on write? */
99
+ shouldScanSecrets(): boolean;
100
+ /** Check if an MCP server is allowed. */
101
+ isMcpServerAllowed(serverName: string): {
102
+ allowed: boolean;
103
+ reason?: string;
104
+ };
105
+ /** Get max iterations for the agent loop. */
106
+ getMaxIterations(): number;
107
+ /** Get max file size in bytes for write operations. */
108
+ getMaxFileSizeBytes(): number;
109
+ /** Get cost limit in USD (0 = no limit). */
110
+ getCostLimitUsd(): number;
111
+ /**
112
+ * Simple glob-like pattern matching:
113
+ * - `*` matches any single path component
114
+ * - `**` matches any number of path components
115
+ * - `.env` matches exact filename
116
+ */
117
+ private matchesPattern;
118
+ }
119
+ /**
120
+ * Generate a default policy file content for `codebot --init-policy`.
121
+ */
122
+ export declare function generateDefaultPolicyFile(): string;
123
+ //# sourceMappingURL=policy.d.ts.map