@occasiolabs/occasio 0.8.3 → 0.8.5

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.
Files changed (52) hide show
  1. package/README.md +1 -0
  2. package/docs/ADAPTER-STAGE-2-MIGRATION.md +59 -0
  3. package/docs/ARCHITECTURE.md +171 -0
  4. package/docs/STAGE-2-STEP-5-SHELL-PLAN.md +107 -0
  5. package/docs/THREAT-MODEL.md +195 -0
  6. package/docs/edr-calibration.md +29 -0
  7. package/package.json +12 -2
  8. package/src/adapters/claude-code.js +1 -2
  9. package/src/adapters/computer-use.js +1 -1
  10. package/src/anomaly/cli.js +4 -1
  11. package/src/anomaly/detectors/deny-rate.js +2 -1
  12. package/src/anomaly/detectors/file-read-volume.js +2 -1
  13. package/src/anomaly/index.js +5 -0
  14. package/src/attest/check-summary.js +1 -1
  15. package/src/attest/index.js +14 -1
  16. package/src/audit/jsonl-auditor.js +180 -14
  17. package/src/audit/repair.js +118 -0
  18. package/src/audit/verifier.js +36 -2
  19. package/src/boundary.js +1 -1
  20. package/src/classifier.js +1 -1
  21. package/src/cli/clear.js +55 -0
  22. package/src/cli/help.js +102 -0
  23. package/src/cli/register.js +90 -0
  24. package/src/cli/status.js +94 -0
  25. package/src/cost/prices.js +106 -0
  26. package/src/dashboard.js +2 -3
  27. package/src/distiller.js +1 -1
  28. package/src/executor/dispatcher.js +2 -2
  29. package/src/executor/native-handlers/glob.js +173 -0
  30. package/src/executor/native-handlers/grep.js +258 -0
  31. package/src/executor/native-handlers/read.js +99 -0
  32. package/src/executor/native-handlers/todo.js +56 -0
  33. package/src/harness.js +8 -10
  34. package/src/index.js +26 -283
  35. package/src/inspect.js +1 -1
  36. package/src/interceptor.js +9 -29
  37. package/src/ledger.js +2 -3
  38. package/src/mcp-experiment.js +4 -4
  39. package/src/mcp-server.js +3 -3
  40. package/src/policy/doctor.js +2 -2
  41. package/src/policy/engine.js +0 -1
  42. package/src/policy/init.js +1 -1
  43. package/src/policy/loader.js +3 -3
  44. package/src/policy/show.js +1 -2
  45. package/src/preflight/cli.js +0 -1
  46. package/src/preflight/miner.js +3 -6
  47. package/src/redteam.js +1 -2
  48. package/src/replay.js +1 -1
  49. package/src/report/index.js +0 -4
  50. package/src/runtime.js +42 -444
  51. package/src/selftest.js +1 -1
  52. package/src/session.js +1 -1
package/src/index.js CHANGED
@@ -71,70 +71,14 @@ function todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(
71
71
  function getLogFile() { return path.join(LOG_DIR, 'logs', `${todayStr()}.jsonl`); }
72
72
  function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-secrets.log`); }
73
73
 
74
- const MODEL_PRICES = {
75
- 'claude-opus-4-6': { in: 15.00, out: 75.00, cache_write: 18.75, cache_read: 1.50 },
76
- 'claude-opus-4': { in: 15.00, out: 75.00, cache_write: 18.75, cache_read: 1.50 },
77
- 'claude-sonnet-4-6': { in: 3.00, out: 15.00, cache_write: 3.75, cache_read: 0.30 },
78
- 'claude-sonnet-4': { in: 3.00, out: 15.00, cache_write: 3.75, cache_read: 0.30 },
79
- 'claude-haiku-4-5': { in: 0.25, out: 1.25, cache_write: 0.30, cache_read: 0.03 },
80
- 'claude-haiku-4': { in: 0.25, out: 1.25, cache_write: 0.30, cache_read: 0.03 },
81
- 'default': { in: 3.00, out: 15.00, cache_write: 3.75, cache_read: 0.30 },
82
- };
83
-
84
- function getPrice(model) {
85
- if (!model) return MODEL_PRICES.default;
86
- for (const [k, v] of Object.entries(MODEL_PRICES)) {
87
- if (k !== 'default' && model.includes(k)) return v;
88
- }
89
- return MODEL_PRICES.default;
90
- }
91
-
92
- function calcCost(model, inp, out, cacheWrite = 0, cacheRead = 0) {
93
- const p = getPrice(model);
94
- return (inp / 1e6 * p.in) + (out / 1e6 * p.out)
95
- + (cacheWrite / 1e6 * p.cache_write) + (cacheRead / 1e6 * p.cache_read);
96
- }
97
-
98
- // Savings from Anthropic prompt caching: cache reads are 10× cheaper than fresh input.
99
- function calcCacheSavings(model, cacheReadTokens) {
100
- if (!cacheReadTokens) return 0;
101
- const p = getPrice(model);
102
- return (cacheReadTokens / 1e6) * (p.in - p.cache_read);
103
- }
104
-
105
- // Cross-request compounding savings: reads the run's JSONL entries in sequence order
106
- // and weights each distilled batch by the exact number of subsequent API calls that
107
- // carry the smaller result in their conversation history.
108
- // Formula: Σ (distill_tokens_saved[i] / 1M × price_in × (N - i - 1))
109
- // Returns { savings: float, carryInstances: int }
110
- // carryInstances = total sum of subsequent-call counts across all distilled batches
111
- // — the actual multiplier used — so the display is self-explanatory.
112
- // Assumption: tool results accumulate in Claude Code's message history for all
113
- // subsequent requests in the session (true for normal sessions; may not hold if
114
- // Claude Code resets context mid-session).
115
- function calcCompoundingSavings(runId, logFile, model) {
116
- if (!runId) return { savings: 0, carryInstances: 0 };
117
- let entries;
118
- try {
119
- const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean);
120
- entries = lines
121
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
122
- .filter(e => e && e.run_id === runId);
123
- } catch { return { savings: 0, carryInstances: 0 }; }
124
- const N = entries.length;
125
- if (N < 2) return { savings: 0, carryInstances: 0 };
126
- const p = getPrice(model);
127
- let savings = 0, carryInstances = 0;
128
- for (let i = 0; i < N; i++) {
129
- const dt = entries[i].distill_tokens_saved || 0;
130
- if (dt > 0) {
131
- const subsequent = N - i - 1;
132
- savings += (dt / 1e6) * p.in * subsequent;
133
- carryInstances += subsequent;
134
- }
135
- }
136
- return { savings, carryInstances };
137
- }
74
+ // Pricing extracted to its own module — see src/cost/prices.js for the
75
+ // MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
76
+ // rest of index.js keeps its existing call sites unchanged.
77
+ const {
78
+ calcCost,
79
+ calcCacheSavings,
80
+ calcCompoundingSavings,
81
+ } = require('./cost/prices');
138
82
 
139
83
  // ── Persistence ────────────────────────────────────────────────────────────────
140
84
 
@@ -156,7 +100,7 @@ function updateSession(e) {
156
100
  lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
157
101
  tools_attempted:0,
158
102
  };
159
- try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
103
+ try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
160
104
  s.requests++;
161
105
  s.input_tokens += e.input_tokens || 0;
162
106
  s.output_tokens += e.output_tokens || 0;
@@ -311,223 +255,22 @@ const cmd = args[0];
311
255
  if (cmd === '--version' || cmd === '-v') { console.log(`occasio v${VERSION}`); process.exit(0); }
312
256
 
313
257
  if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
314
- console.log(`
315
- ${col.b(`⚡ Occasio v${VERSION}`)}
316
-
317
- ${col.b('Usage:')}
318
- occasio claude [args...] Start Claude with local proxy (intercept + log)
319
- occasio demo 10-second proof: see Occasio block real secrets
320
- occasio demo attest End-to-end attestation pipeline against a synthetic audit chain
321
- occasio demo anomalies End-to-end EDR test: synthetic adversarial chain → all 4 detectors
322
- occasio dashboard Open live dashboard for the running session
323
- occasio register Register shell alias (type 'claude' directly)
324
- occasio status Show session stats and savings breakdown
325
- occasio doctor Check setup: Node, claude CLI, port, Python, profile
326
- occasio clear Reset today's log and session data
327
- occasio clear --history Wipe all historical logs
328
- occasio ledger Inspect token ledger (--last N, --summary, --scope session|today)
329
- occasio replay Replay run audit (--last N, --detail, --run <id>, --attribute)
330
- occasio distill Inspect distilled outputs (--last N, --entry <N> for raw)
331
- occasio inspect Cloud-boundary manifest (--last N, --entry N, --run <id>)
332
- occasio boundary Per-request three-column view: produced / re-entered / prevented
333
- occasio baseline Behavior baseline: [learn|show|compare|reset] (per project cwd)
334
- occasio harness Run a real Claude Code session against scratch fixtures and verify governance claims (needs ANTHROPIC_API_KEY)
335
- occasio redteam Autonomous adversarial test — tester LLM probes a subject Claude Code session under Occasio (needs ANTHROPIC_API_KEY + @anthropic-ai/sdk)
336
- occasio policy [show] Show active policy: flags, tool routing, overrides
337
- occasio policy show --diff Only values that differ from defaults
338
- occasio policy validate Validate policy.yml and report errors/warnings
339
- occasio policy init Create a starter policy.yml (safe, non-destructive)
340
- Use --template strict|finance for a non-default starter
341
- occasio policy doctor Cross-reference session logs with policy; surface suggestions
342
- occasio audit [verify] Verify tamper-evident hash chain in pipeline-events.jsonl
343
- occasio report Governance export: file access log, blocked paths, secret events
344
- occasio anomalies Live anomaly detection over the audit chain (--window 15m, --json)
345
- occasio computer-use Apply a Computer-Use policy to a JSONL of tool_use blocks (--dry-run --example)
346
- occasio attest --run-id <uuid> AI-Agent Behavioral Attestation v1: hash-chain commitment + execution summary for one run
347
- Add --sign in GitHub Actions (with permissions: id-token: write) for Sigstore keyless signing
348
- occasio attest verify <file> Re-verify a signed attestation: Sigstore bundle + DSSE payload match + audit chain integrity
349
- occasio selftest Run governance self-checks on a scratch chain (does not touch your audit log)
350
- occasio report --format csv CSV export for auditors / SIEM import
351
- occasio mcp-experiment MCP vs. built-in tool adoption stats (experiment)
352
-
353
- ${col.b('Presets:')}
354
- --preset balanced (default) Intercept safe reads locally, log all requests
355
- --preset strict Block requests that contain detected secrets
356
- --preset off Log only — no interception, no blocking
357
-
358
- ${col.b('Flags:')}
359
- --budget <N> Block requests once session cost exceeds $N (e.g. --budget 1.00)
360
- --hardened Route Read/Glob/Grep through unified runtime (distill + secret scan)
361
- --block-secrets Alias for --preset strict
362
- --log-only Alias for --preset off
363
- --dashboard Open live dashboard at http://localhost:3001
364
- --port <N> Proxy port (default: 8081)
365
- --verbose Print live per-request chatter (off by default — quiet for Claude Code's TUI)
366
-
367
- ${col.b('Multi-agent routing:')}
368
- Default → Claude Code adapter
369
- Header x-occasio-agent: cline → Cline adapter (synthetic; live validation pending)
370
-
371
- ${col.b('Logs:')} ~/.occasio/logs/YYYY-MM-DD.jsonl
372
- `);
258
+ require('./cli/help').run();
373
259
  process.exit(0);
374
260
  }
375
261
 
376
262
  if (cmd === 'register') {
377
- const shell = process.env.SHELL || '';
378
- const isWindows = process.platform === 'win32';
379
-
380
- if (isWindows) {
381
- const profileDir = path.join(os.homedir(), 'Documents', 'PowerShell');
382
- const profileFile = path.join(profileDir, 'Microsoft.PowerShell_profile.ps1');
383
- const snippet = `\n# Occasio — intercept Claude Code traffic\nfunction claude { occasio claude @args }\n`;
384
- const alreadyMarker = 'occasio claude @args';
385
- const legacyMarker = 'occasio --intercept @args';
386
- try {
387
- if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
388
- const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : '';
389
- if (existing.includes(alreadyMarker)) {
390
- console.log(col.g('✓ Already registered (PowerShell)'));
391
- console.log(col.d(' Type: claude'));
392
- } else if (existing.includes(legacyMarker)) {
393
- // Upgrade old --intercept form to canonical `occasio claude`
394
- const updated = existing.replace(
395
- /function claude \{ occasio --intercept @args \}/g,
396
- 'function claude { occasio claude @args }'
397
- );
398
- fs.writeFileSync(profileFile, updated);
399
- console.log(col.g('✓ Updated to canonical form (occasio claude)'));
400
- console.log('');
401
- console.log(col.y(` ⚠ Restart PowerShell — the 'claude' alias is not active yet.`));
402
- console.log(col.d(` Open a new terminal, or run: . $PROFILE`));
403
- console.log('');
404
- } else {
405
- fs.appendFileSync(profileFile, snippet);
406
- console.log(col.g(`✓ Registered in ${profileFile}`));
407
- console.log('');
408
- console.log(col.y(` ⚠ Restart PowerShell — the 'claude' alias is not active yet.`));
409
- console.log(col.d(` Open a new terminal, or run: . $PROFILE`));
410
- console.log('');
411
- }
412
- } catch (e) {
413
- console.log(col.r(`✗ Could not write profile: ${e.message}`));
414
- console.log(col.d(` Add manually to your PowerShell profile:\n function claude { occasio claude @args }`));
415
- }
416
- } else {
417
- const rcFile = (process.env.SHELL || '').includes('zsh')
418
- ? path.join(os.homedir(), '.zshrc')
419
- : path.join(os.homedir(), '.bashrc');
420
- const snippet = `\n# Occasio — intercept Claude Code traffic\nclaude() { occasio claude "$@"; }\n`;
421
- const alreadyMarker = 'occasio claude "$@"';
422
- const legacyMarker = 'occasio --intercept "$@"';
423
- try {
424
- const existing = fs.existsSync(rcFile) ? fs.readFileSync(rcFile, 'utf8') : '';
425
- if (existing.includes(alreadyMarker)) {
426
- console.log(col.g(`✓ Already registered (${rcFile})`));
427
- } else if (existing.includes(legacyMarker)) {
428
- const updated = existing.replace(
429
- /claude\(\) \{ occasio --intercept "\$@"; \}/g,
430
- 'claude() { occasio claude "$@"; }'
431
- );
432
- fs.writeFileSync(rcFile, updated);
433
- console.log(col.g(`✓ Updated to canonical form in ${rcFile}`));
434
- } else {
435
- fs.appendFileSync(rcFile, snippet);
436
- console.log(col.g(`✓ Registered in ${rcFile}`));
437
- }
438
- console.log(col.d(' Run: source ' + rcFile + ' — then type: claude'));
439
- } catch (e) {
440
- console.log(col.r(`✗ Could not write ${rcFile}: ${e.message}`));
441
- console.log(col.d(` Add manually:\n claude() { occasio claude "$@"; }`));
442
- }
443
- }
263
+ require('./cli/register').run();
444
264
  process.exit(0);
445
265
  }
446
266
 
447
267
  if (cmd === 'status' || cmd === 'stats') {
448
- let s = null; try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
449
- console.log(col.b('\n⚡ Occasio\n'));
450
- if (!s) { console.log(col.d(' No session data yet. Run: occasio claude\n')); process.exit(0); }
451
-
452
- const cacheSav = s.cache_savings || 0;
453
- const laoSav = s.lao_cost_saved || 0;
454
- const distSav = s.distill_cost_saved || 0;
455
- const payload = laoSav + distSav;
456
- const { savings: context } =
457
- calcCompoundingSavings(s.run_id, s.log_file || getLogFile(), s.model || '');
458
- const totalSav = payload + context + cacheSav;
459
- const broaderCf = (s.cost || 0) + totalSav;
460
- const savedPct = broaderCf > 0.00001 ? Math.round(totalSav / broaderCf * 100) : 0;
461
-
462
- // Headline
463
- if (totalSav > 0.00001) {
464
- console.log(col.g(` Saved: $${totalSav.toFixed(4)}`) +
465
- col.d(` (${savedPct}% off — would have cost $${broaderCf.toFixed(4)})`));
466
- } else {
467
- console.log(col.d(` Saved: $0.0000 (no interceptable tool calls in this session yet)`));
468
- }
469
- console.log(col.y(` Cost: $${s.cost.toFixed(4)}`));
470
-
471
- // Plain-English coverage. Defensive: legacy sessions (pre-multi-round-fix)
472
- // may have tools_attempted undercounted relative to tools_local_count.
473
- // We clamp the denominator to at least the numerator so the displayed
474
- // ratio is always 0–100% and never reads "X of Y < X (>100%)".
475
- const localCnt = s.tools_local_count || 0;
476
- const mcpCnt = s.tools_mcp_count || 0;
477
- const attempted = s.tools_attempted || 0;
478
- const totalLocal = localCnt + mcpCnt;
479
- const denom = Math.max(attempted, totalLocal);
480
- if (denom > 0) {
481
- const cpct = Math.round(totalLocal / denom * 100);
482
- const cColor = cpct >= 80 ? col.g : cpct >= 50 ? col.y : col.r;
483
- console.log(cColor(` Ran locally: ${totalLocal} of ${denom} tool calls (${cpct}%)`));
484
- }
485
- if (s.blocked) console.log(col.r(` Blocked: ${s.blocked} secrets`));
486
- if (s.secrets_redacted) console.log(col.c(` Redacted: ${s.secrets_redacted} secret${s.secrets_redacted !== 1 ? 's' : ''} in tool results`));
487
- if (s.tools_transformed) console.log(col.c(` Transforms: ${s.tools_transformed} tool result${s.tools_transformed !== 1 ? 's' : ''} shaped`));
488
- if (s.budget != null) {
489
- const pct = Math.min(999, Math.round((s.cost || 0) / s.budget * 100));
490
- const budgetStr = fmtBudget(s.cost || 0, s.budget);
491
- const budgetColor = pct >= 100 ? col.r : pct >= 80 ? col.y : col.g;
492
- console.log(budgetColor(` Budget: ${budgetStr}`));
493
- if (s.budget_exceeded_count) console.log(col.r(` BudgetBlk: ${s.budget_exceeded_count} request(s) blocked`));
494
- }
495
-
496
- // Detail
497
- console.log(col.d(` ────`));
498
- console.log(col.d(` Requests: ${s.requests} · ${(s.input_tokens/1000).toFixed(1)}k tokens in · ${(s.output_tokens/1000).toFixed(1)}k out`));
499
- if (totalSav > 0.00001) {
500
- const parts = [];
501
- if (payload > 0.00001) parts.push(`$${payload.toFixed(4)} payload`);
502
- if (context > 0.00001) parts.push(`$${context.toFixed(4)} context`);
503
- if (cacheSav > 0.00001) parts.push(`$${cacheSav.toFixed(4)} cache`);
504
- if (parts.length) console.log(col.d(` Breakdown: ${parts.join(' + ')}`));
505
- }
506
- const tail = [];
507
- if (s.mode) tail.push(`Mode: ${s.mode}`);
508
- if (s.start) tail.push(`Since: ${new Date(s.start).toLocaleString()}`);
509
- if (tail.length) console.log(col.d(` ${tail.join(' · ')}`));
510
- console.log(''); process.exit(0);
268
+ require('./cli/status').run();
269
+ process.exit(0);
511
270
  }
512
271
 
513
272
  if (cmd === 'clear') {
514
- ensureDirs();
515
- const clearAll = args.slice(1).includes('--history');
516
- if (clearAll) {
517
- const logsDir = path.join(LOG_DIR, 'logs');
518
- const blockedDir = path.join(LOG_DIR, 'blocked');
519
- let n = 0;
520
- for (const dir of [logsDir, blockedDir]) {
521
- try { for (const f of fs.readdirSync(dir)) { fs.unlinkSync(path.join(dir, f)); n++; } } catch {}
522
- }
523
- try { fs.unlinkSync(SESSION_FILE); } catch {}
524
- console.log(col.g(`✓ Cleared all history (${n} log files) and session data`));
525
- } else {
526
- try { fs.unlinkSync(getLogFile()); } catch {}
527
- try { fs.unlinkSync(SESSION_FILE); } catch {}
528
- console.log(col.g("✓ Cleared today's log and session data"));
529
- console.log(col.d(' Use --history to wipe all historical logs'));
530
- }
273
+ require('./cli/clear').run(args.slice(1));
531
274
  process.exit(0);
532
275
  }
533
276
 
@@ -817,7 +560,7 @@ if (cmd === 'doctor' || cmd === 'check') {
817
560
  stdio: ['pipe', 'pipe', 'pipe'],
818
561
  }).toString().trim();
819
562
  ok('Python (LAO)', out); pyFound = true;
820
- } catch {}
563
+ } catch { /* ignore */ }
821
564
  }
822
565
  if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
823
566
  if (laoPyExists) ok('LAO scorer', laoPyPath);
@@ -943,7 +686,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
943
686
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
944
687
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
945
688
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
946
- try { server && server.close && server.close(); } catch {}
689
+ try { server && server.close && server.close(); } catch { /* ignore */ }
947
690
  setTimeout(() => process.exit(1), 250);
948
691
  }
949
692
  });
@@ -982,7 +725,7 @@ if (budget !== null) {
982
725
  const { execSync: _ex } = require('child_process');
983
726
  let _pyOk = false;
984
727
  for (const _cmd of ['python', 'python3']) {
985
- try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch {}
728
+ try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch { /* ignore */ }
986
729
  }
987
730
  if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
988
731
  }
@@ -1030,7 +773,7 @@ const server = http.createServer((req, res) => {
1030
773
  try {
1031
774
  const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
1032
775
  blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
1033
- } catch {}
776
+ } catch { /* ignore */ }
1034
777
  }
1035
778
 
1036
779
  const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
@@ -1063,7 +806,7 @@ const server = http.createServer((req, res) => {
1063
806
  res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
1064
807
  return;
1065
808
  }
1066
- } catch {}
809
+ } catch { /* ignore */ }
1067
810
  }
1068
811
 
1069
812
  // ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
@@ -1097,7 +840,7 @@ const server = http.createServer((req, res) => {
1097
840
  const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
1098
841
  s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
1099
842
  fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
1100
- } catch {}
843
+ } catch { /* ignore */ }
1101
844
  const synth = decision.syntheticResponse;
1102
845
  res.writeHead(synth.status, { 'Content-Type': 'application/json' });
1103
846
  res.end(JSON.stringify(synth.body));
@@ -1125,7 +868,7 @@ const server = http.createServer((req, res) => {
1125
868
  }
1126
869
  }
1127
870
  }
1128
- } catch {}
871
+ } catch { /* ignore */ }
1129
872
  }
1130
873
  // ──────────────────────────────────────────────────────────────────────────
1131
874
 
@@ -1312,7 +1055,7 @@ const server = http.createServer((req, res) => {
1312
1055
  }
1313
1056
  forwardBody = Buffer.from(JSON.stringify(b));
1314
1057
  outboundMessageCount = b.messages?.length ?? outboundMessageCount;
1315
- } catch {}
1058
+ } catch { /* ignore */ }
1316
1059
  }
1317
1060
  if (laoDropped.length > 0) {
1318
1061
  const ts0 = new Date().toTimeString().slice(0, 8);
@@ -1438,7 +1181,7 @@ const server = http.createServer((req, res) => {
1438
1181
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
1439
1182
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
1440
1183
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
1441
- try { server && server.close && server.close(); } catch {}
1184
+ try { server && server.close && server.close(); } catch { /* ignore */ }
1442
1185
  setTimeout(() => process.exit(1), 250);
1443
1186
  return;
1444
1187
  }
@@ -1467,10 +1210,10 @@ const server = http.createServer((req, res) => {
1467
1210
  cacheRead = d.usage.cache_read_input_tokens || cacheRead;
1468
1211
  }
1469
1212
  if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
1470
- } catch {}
1213
+ } catch { /* ignore */ }
1471
1214
  }
1472
1215
  }
1473
- } catch {}
1216
+ } catch { /* ignore */ }
1474
1217
 
1475
1218
  // When the interceptor ran, Anthropic was billed for N calls:
1476
1219
  // call #1 → initial tool_use round (toolCallUsage)
@@ -1705,7 +1448,7 @@ server.listen(PORT, '127.0.0.1', () => {
1705
1448
  if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
1706
1449
  }
1707
1450
  process.stderr.write('────────────────────────────────────────\n\n');
1708
- } catch {}
1451
+ } catch { /* ignore */ }
1709
1452
  process.exit(code || 0);
1710
1453
  });
1711
1454
 
package/src/inspect.js CHANGED
@@ -268,7 +268,7 @@ function printBoundaryEntry(entry, idxLabel, total) {
268
268
 
269
269
  function runInspectCli(args) {
270
270
  let session = null;
271
- try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
271
+ try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
272
272
 
273
273
  const todayEntries = readDayLog(todayStr());
274
274
 
@@ -24,14 +24,13 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
  const { exec } = require('child_process');
27
- const https = require('https');
28
27
  const { routeLocally } = require('./classifier');
29
28
  const { distill } = require('./distiller');
30
29
  const { scanSecrets } = require('./analyzer');
31
30
 
32
31
  const {
33
32
  MAX_OUTPUT,
34
- readFileNative,
33
+ readFileNative, READ_SKIP_EXTENSIONS,
35
34
  isReadHandleable, handleReadTool,
36
35
  isGlobHandleable, handleGlobTool, globToRegex,
37
36
  isGrepHandleable, handleGrepTool,
@@ -455,7 +454,7 @@ function nativeHandle(cmd) {
455
454
  }
456
455
  }
457
456
  }
458
- } catch {}
457
+ } catch { /* skip unreadable dir */ }
459
458
  }
460
459
  walk(abs);
461
460
  return { output: results.join('\n') || '', exitCode: 0 };
@@ -487,7 +486,7 @@ function nativeHandle(cmd) {
487
486
  if (!filePart) return null;
488
487
  const abs = path.resolve(cwd, filePart);
489
488
  let exists = false;
490
- try { fs.statSync(abs); exists = true; } catch {}
489
+ try { fs.statSync(abs); exists = true; } catch { /* missing → exists stays false */ }
491
490
  return { output: exists ? 'True' : 'False', exitCode: exists ? 0 : 1 };
492
491
  }
493
492
 
@@ -689,11 +688,11 @@ function isInterceptable(block) {
689
688
  if (block.name === 'TodoWrite') return isTodoHandleable(block.input, 'TodoWrite');
690
689
  if (block.name === 'TodoRead') return isTodoHandleable(block.input, 'TodoRead');
691
690
  if (block.name === 'PowerShell') {
692
- const cmd = (block.input?.command || '').trim();
691
+ const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
693
692
  return cmd ? isPowerShellNativeHandleable(cmd) : false;
694
693
  }
695
694
  if (block.name !== 'Bash') return false;
696
- const cmd = (block.input?.command || '').trim();
695
+ const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
697
696
  if (!cmd) return false;
698
697
  if (isNativeHandleable(cmd)) return true;
699
698
  if (SHELL_META.test(cmd)) return false;
@@ -738,7 +737,7 @@ function classifyBlock(block) {
738
737
  }
739
738
 
740
739
  if (block.name === 'PowerShell') {
741
- const rawCmd = (block.input?.command || '').trim();
740
+ const rawCmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
742
741
  if (!rawCmd) return { handled: false, reason: FALLBACK_REASONS.BASH_EMPTY_CMD };
743
742
  const expanded = expandPsEnvVars(rawCmd);
744
743
  let normalized = expanded.trim();
@@ -760,7 +759,7 @@ function classifyBlock(block) {
760
759
  }
761
760
 
762
761
  // Bash
763
- const cmd = (block.input?.command || '').trim();
762
+ const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
764
763
  if (!cmd) return { handled: false, reason: FALLBACK_REASONS.BASH_EMPTY_CMD };
765
764
  if (isNativeHandleable(cmd)) return { handled: true, reason: 'ok' };
766
765
  if (SHELL_META.test(cmd)) return { handled: false, reason: FALLBACK_REASONS.BASH_SHELL_META };
@@ -891,27 +890,6 @@ function buildFollowUpHeaders(authHeaders, payloadLength) {
891
890
  return h;
892
891
  }
893
892
 
894
- function anthropicRequest(body, authHeaders) {
895
- return new Promise((resolve, reject) => {
896
- const payload = JSON.stringify({ ...body, stream: false });
897
- const headers = buildFollowUpHeaders(authHeaders, Buffer.byteLength(payload));
898
-
899
- const req = https.request(
900
- { hostname: 'api.anthropic.com', port: 443, path: '/v1/messages', method: 'POST', headers },
901
- res => {
902
- const chunks = [];
903
- res.on('data', c => chunks.push(c));
904
- res.on('end', () => {
905
- try { resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString()) }); }
906
- catch (e) { reject(e); }
907
- });
908
- }
909
- );
910
- req.on('error', reject);
911
- req.end(payload);
912
- });
913
- }
914
-
915
893
  // ── Main export ────────────────────────────────────────────────────────────────
916
894
 
917
895
  /**
@@ -1185,6 +1163,8 @@ module.exports = {
1185
1163
  isGrepHandleable,
1186
1164
  isTodoHandleable,
1187
1165
  nativeHandle,
1166
+ readFileNative,
1167
+ READ_SKIP_EXTENSIONS,
1188
1168
  handleReadTool,
1189
1169
  handleGlobTool,
1190
1170
  globToRegex,
package/src/ledger.js CHANGED
@@ -26,7 +26,7 @@ function readDayLog(dateStr) {
26
26
  for (const raw of lines) {
27
27
  const line = raw.trim();
28
28
  if (!line) continue;
29
- try { result.push(JSON.parse(line)); } catch {}
29
+ try { result.push(JSON.parse(line)); } catch { /* ignore */ }
30
30
  }
31
31
  return result;
32
32
  }
@@ -119,7 +119,6 @@ function printEntry(e, idx) {
119
119
 
120
120
  function printSummary(totals, scope, runId) {
121
121
  const { requests, cloud_sent = 0, local_only = 0, blocked = 0, trimmed = 0,
122
- budget_exceeded = 0,
123
122
  input_tokens, output_tokens, cost,
124
123
  cache_savings, lao_cost_saved, distill_cost_saved = 0,
125
124
  distill_tokens_saved = 0, tools_local_count } = totals;
@@ -167,7 +166,7 @@ function runLedgerCli(args) {
167
166
  if (args.includes('--summary')) showSummary = true;
168
167
 
169
168
  let session = null;
170
- try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
169
+ try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
171
170
 
172
171
  const todayEntries = readDayLog(todayStr());
173
172
  const entries = scope === 'session'
@@ -33,7 +33,7 @@ function runStats() {
33
33
  try {
34
34
  mcpEntries = fs.readFileSync(MCP_LOG, 'utf8').trim().split('\n')
35
35
  .filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
36
- } catch {}
36
+ } catch { /* ignore */ }
37
37
 
38
38
  // ── Built-in path: read today's session log, count intercepted Read/Glob/Grep ─
39
39
  let builtinTools = [];
@@ -44,9 +44,9 @@ function runStats() {
44
44
  const entry = JSON.parse(line);
45
45
  const tools = (entry.tools || []).filter(t => ['Read', 'Glob', 'Grep'].includes(t.tool));
46
46
  builtinTools.push(...tools);
47
- } catch {}
47
+ } catch { /* ignore */ }
48
48
  }
49
- } catch {}
49
+ } catch { /* ignore */ }
50
50
 
51
51
  const mcpTotal = mcpEntries.length;
52
52
  const builtinTotal = builtinTools.length;
@@ -116,7 +116,7 @@ function runStats() {
116
116
  }
117
117
 
118
118
  function runClear() {
119
- try { fs.unlinkSync(MCP_LOG); console.log(col.g('✓ mcp-experiment.jsonl cleared')); } catch {}
119
+ try { fs.unlinkSync(MCP_LOG); console.log(col.g('✓ mcp-experiment.jsonl cleared')); } catch { /* ignore */ }
120
120
  }
121
121
 
122
122
  function runRaw() {
package/src/mcp-server.js CHANGED
@@ -86,7 +86,7 @@ function logCall(entry) {
86
86
  const dir = path.dirname(LOG_FILE);
87
87
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
88
88
  fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
89
- } catch {}
89
+ } catch { /* ignore */ }
90
90
  }
91
91
 
92
92
  // ── Tool definitions (lao-compatible schemas) ──────────────────────────────────
@@ -274,7 +274,7 @@ async function handleRequest(req) {
274
274
  content: [{ type: 'text', text: 'audit-fatal: MCP server aborting' }],
275
275
  isError: true,
276
276
  });
277
- } catch {}
277
+ } catch { /* ignore */ }
278
278
  setTimeout(() => process.exit(1), 250);
279
279
  return;
280
280
  }
@@ -301,7 +301,7 @@ process.stdin.on('data', chunk => {
301
301
  for (const line of lines) {
302
302
  const trimmed = line.trim();
303
303
  if (!trimmed) continue;
304
- try { handleRequest(JSON.parse(trimmed)); } catch (e) { /* malformed JSON-RPC frame */ }
304
+ try { handleRequest(JSON.parse(trimmed)); } catch { /* malformed JSON-RPC frame */ }
305
305
  }
306
306
  });
307
307
  process.stdin.on('end', () => process.exit(0));
@@ -56,10 +56,10 @@ function readRecentLogs(days, logsDir) {
56
56
  for (const f of files) {
57
57
  for (const line of fs.readFileSync(path.join(dir, f), 'utf8').split('\n')) {
58
58
  if (!line.trim()) continue;
59
- try { entries.push(JSON.parse(line)); } catch {}
59
+ try { entries.push(JSON.parse(line)); } catch { /* ignore */ }
60
60
  }
61
61
  }
62
- } catch {}
62
+ } catch { /* ignore */ }
63
63
  return entries;
64
64
  }
65
65
 
@@ -16,7 +16,6 @@
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
- const adapter = require('../adapters/claude-code');
20
19
  const { PASS, LOCAL, BLOCK, TRANSFORM, TRANSFORM_CHAIN } = require('../core/decision');
21
20
  const loader = require('./loader');
22
21
  const builtIn = require('./built-in-classifiers');
@@ -101,7 +101,7 @@ function runInitCli(args, opts = {}) {
101
101
 
102
102
  // Guard: refuse to overwrite without --force
103
103
  let exists = false;
104
- try { fsMod.statSync(filePath); exists = true; } catch {}
104
+ try { fsMod.statSync(filePath); exists = true; } catch { /* ignore */ }
105
105
 
106
106
  if (exists && !force) {
107
107
  console.log(` File: ${filePath} ${col.y('(already exists)')}\n`);