@occasiolabs/occasio 0.8.4 → 0.8.6

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 (47) hide show
  1. package/README.md +4 -3
  2. package/docs/ADAPTER-STAGE-2-MIGRATION.md +59 -0
  3. package/docs/STAGE-2-STEP-5-SHELL-PLAN.md +107 -0
  4. package/docs/THREAT-MODEL.md +195 -0
  5. package/docs/edr-calibration.md +29 -0
  6. package/package.json +8 -3
  7. package/src/adapters/claude-code.js +1 -2
  8. package/src/adapters/computer-use.js +1 -1
  9. package/src/anomaly/cli.js +4 -1
  10. package/src/anomaly/detectors/deny-rate.js +2 -1
  11. package/src/anomaly/detectors/file-read-volume.js +2 -1
  12. package/src/anomaly/index.js +5 -0
  13. package/src/boundary.js +1 -1
  14. package/src/classifier.js +1 -1
  15. package/src/cli/clear.js +4 -4
  16. package/src/cli/conversation.js +121 -0
  17. package/src/cli/help.js +62 -38
  18. package/src/cli/recap.js +367 -0
  19. package/src/cli/status.js +1 -1
  20. package/src/dashboard.js +2 -3
  21. package/src/demo/audit-demo.js +330 -0
  22. package/src/distiller.js +1 -1
  23. package/src/executor/dispatcher.js +2 -2
  24. package/src/executor/native-handlers/glob.js +173 -0
  25. package/src/executor/native-handlers/grep.js +258 -0
  26. package/src/executor/native-handlers/read.js +99 -0
  27. package/src/executor/native-handlers/todo.js +56 -0
  28. package/src/harness.js +8 -10
  29. package/src/index.js +118 -30
  30. package/src/inspect.js +1 -1
  31. package/src/interceptor.js +9 -29
  32. package/src/ledger.js +2 -3
  33. package/src/mcp-experiment.js +4 -4
  34. package/src/mcp-server.js +3 -3
  35. package/src/policy/doctor.js +2 -2
  36. package/src/policy/engine.js +0 -1
  37. package/src/policy/init.js +1 -1
  38. package/src/policy/loader.js +3 -3
  39. package/src/policy/show.js +1 -2
  40. package/src/preflight/cli.js +0 -1
  41. package/src/preflight/miner.js +3 -6
  42. package/src/redteam.js +1 -2
  43. package/src/replay.js +1 -1
  44. package/src/report/index.js +0 -4
  45. package/src/runtime.js +42 -444
  46. package/src/selftest.js +1 -1
  47. package/src/session.js +1 -1
package/src/index.js CHANGED
@@ -56,10 +56,11 @@ const VERSION = (() => {
56
56
  catch { return '0.0.0-unknown'; }
57
57
  })();
58
58
  const LOG_SCHEMA_VERSION = 2;
59
- // Port override via env var (used by `occasio harness` and redteam to
60
- // run isolated proxies against scratch audit chains on free ports). Default
61
- // is 8081 to preserve existing user-facing behaviour.
62
- let PORT = parseInt(process.env.OCCASIO_PORT, 10) || 8081;
59
+ // Port: defaults to 0 (auto-assigned by the OS) so multiple `occasio claude`
60
+ // sessions can run in parallel. Explicit overrides via OCCASIO_PORT env or
61
+ // --port flag still pin a fixed port (and surface EADDRINUSE if taken).
62
+ let PORT = parseInt(process.env.OCCASIO_PORT, 10) || 0;
63
+ let PORT_EXPLICIT = PORT !== 0;
63
64
  const ANTHROPIC_REAL = 'api.anthropic.com';
64
65
  const LOG_DIR = path.join(os.homedir(), '.occasio');
65
66
  const SESSION_FILE = path.join(LOG_DIR, 'session.json');
@@ -75,8 +76,6 @@ function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-
75
76
  // MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
76
77
  // rest of index.js keeps its existing call sites unchanged.
77
78
  const {
78
- MODEL_PRICES,
79
- getPrice,
80
79
  calcCost,
81
80
  calcCacheSavings,
82
81
  calcCompoundingSavings,
@@ -102,7 +101,7 @@ function updateSession(e) {
102
101
  lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
103
102
  tools_attempted:0,
104
103
  };
105
- try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
104
+ try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
106
105
  s.requests++;
107
106
  s.input_tokens += e.input_tokens || 0;
108
107
  s.output_tokens += e.output_tokens || 0;
@@ -133,6 +132,60 @@ const col = {
133
132
  d: s => `\x1b[2m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`,
134
133
  };
135
134
 
135
+ // Print a compact recap banner just before claude starts — gives the user
136
+ // (and the agent, via on-screen context) a memory anchor for the previous
137
+ // session in this cwd. Silent if no prior data exists.
138
+ function printSessionRecapBanner() {
139
+ let conv = null;
140
+ try { conv = require('./cli/conversation').readLastConversation(process.cwd()); }
141
+ catch { /* module load issue → skip */ }
142
+
143
+ // Last run summary from the audit chain.
144
+ let runLine = null;
145
+ try {
146
+ const chainFile = path.join(LOG_DIR, 'pipeline-events.jsonl');
147
+ if (fs.existsSync(chainFile)) {
148
+ const text = fs.readFileSync(chainFile, 'utf8');
149
+ const rows = [];
150
+ for (const line of text.split('\n')) {
151
+ if (!line) continue;
152
+ try { rows.push(JSON.parse(line)); } catch { /* skip */ }
153
+ }
154
+ const byRun = new Map();
155
+ for (const r of rows) {
156
+ const id = r.run_id || 'legacy';
157
+ if (!byRun.has(id)) byRun.set(id, []);
158
+ byRun.get(id).push(r);
159
+ }
160
+ const newest = [...byRun.entries()].map(([id, rs]) => ({
161
+ id, rs, end: rs[rs.length - 1]?.ts || '',
162
+ })).sort((a, b) => a.end < b.end ? 1 : -1)[0];
163
+ if (newest) {
164
+ const tc = newest.rs.filter(r => r.kind === 'tool_call').length;
165
+ const blocked = newest.rs.filter(r => r.action === 'BLOCK').length;
166
+ const date = newest.end ? new Date(newest.end).toISOString().slice(0, 16).replace('T', ' ') : 'unknown';
167
+ runLine = `${tc} tool calls, ${blocked} blocked · ${date}`;
168
+ }
169
+ }
170
+ } catch { /* skip */ }
171
+
172
+ if (!conv && !runLine) return;
173
+
174
+ const oneLine = (s, max = 160) => {
175
+ const flat = String(s).replace(/\s+/g, ' ').trim();
176
+ return flat.length > max ? flat.slice(0, max - 1) + '…' : flat;
177
+ };
178
+ process.stderr.write('\n' + col.b('── previous session in this directory ──') + '\n');
179
+ if (runLine) process.stderr.write(' ' + col.d(runLine) + '\n');
180
+ if (conv && conv.lastUser) {
181
+ process.stderr.write(' ' + col.y('You: ') + oneLine(conv.lastUser) + '\n');
182
+ }
183
+ if (conv && conv.lastAssistant) {
184
+ process.stderr.write(' ' + col.g('Agent: ') + oneLine(conv.lastAssistant) + '\n');
185
+ }
186
+ process.stderr.write(col.d(' (enabled via --recap; suppress next time by omitting it)') + '\n\n');
187
+ }
188
+
136
189
  // ── Pre-send manifest ─────────────────────────────────────────────────────────
137
190
 
138
191
  function fmtTok(n) {
@@ -286,6 +339,10 @@ if (cmd === 'replay') {
286
339
  process.exit(0);
287
340
  }
288
341
 
342
+ if (cmd === 'recap') {
343
+ process.exit(require('./cli/recap').run(args.slice(1)));
344
+ }
345
+
289
346
  if (cmd === 'distill') {
290
347
  runDistillCli(args.slice(1));
291
348
  process.exit(0);
@@ -406,6 +463,13 @@ if (cmd === 'demo' && (args[1] === 'anomalies' || args[1] === 'anomaly')) {
406
463
  process.exit(runAnomaliesDemoCli(args.slice(2)));
407
464
  }
408
465
 
466
+ if (cmd === 'demo' && (args[1] === 'audit' || args[1] === 'auditor')) {
467
+ const { runAuditDemoCli } = require('./demo/audit-demo');
468
+ runAuditDemoCli(args.slice(2)).then(code => process.exit(code))
469
+ .catch(e => { process.stderr.write(`[demo audit] ${e.message}\n`); process.exit(1); });
470
+ return;
471
+ }
472
+
409
473
  if (cmd === 'demo') {
410
474
  const { scanSecrets } = require('../src/analyzer');
411
475
  // Realistic-looking but synthetic credentials. Each hits exactly one pattern.
@@ -539,16 +603,22 @@ if (cmd === 'doctor' || cmd === 'check') {
539
603
  ok('log dir', LOG_DIR);
540
604
  } catch (e) { bad('log dir', e.message); }
541
605
 
542
- // 4. Port availability (async)
543
- await new Promise(resolve => {
544
- const srv = net.createServer();
545
- srv.once('error', () => {
546
- bad(`port ${PORT}`, `already in use — kill it: netstat -ano | findstr :${PORT}`);
547
- resolve();
606
+ // 4. Port availability (async) — only probe when an explicit port is pinned.
607
+ // With auto-assignment (default), the OS picks a free port at listen-time
608
+ // so there's nothing meaningful to probe in advance.
609
+ if (PORT_EXPLICIT) {
610
+ await new Promise(resolve => {
611
+ const srv = net.createServer();
612
+ srv.once('error', () => {
613
+ bad(`port ${PORT}`, `already in use — kill it: netstat -ano | findstr :${PORT}`);
614
+ resolve();
615
+ });
616
+ srv.once('listening', () => { srv.close(); ok(`port ${PORT}`, 'available'); resolve(); });
617
+ srv.listen(PORT, '127.0.0.1');
548
618
  });
549
- srv.once('listening', () => { srv.close(); ok(`port ${PORT}`, 'available'); resolve(); });
550
- srv.listen(PORT, '127.0.0.1');
551
- });
619
+ } else {
620
+ ok('port', 'auto-assigned at runtime');
621
+ }
552
622
 
553
623
  // 5. Python + LAO scorer script
554
624
  const laoPyPath = path.join(__dirname, 'lao_prep.py');
@@ -562,7 +632,7 @@ if (cmd === 'doctor' || cmd === 'check') {
562
632
  stdio: ['pipe', 'pipe', 'pipe'],
563
633
  }).toString().trim();
564
634
  ok('Python (LAO)', out); pyFound = true;
565
- } catch {}
635
+ } catch { /* ignore */ }
566
636
  }
567
637
  if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
568
638
  if (laoPyExists) ok('LAO scorer', laoPyPath);
@@ -634,8 +704,15 @@ if (prIdx >= 0) {
634
704
  const di = claudeArgs.indexOf('--dashboard');
635
705
  const useDashboard = di >= 0;
636
706
  if (di >= 0) claudeArgs.splice(di, 1);
707
+ const recapIdx = claudeArgs.indexOf('--recap');
708
+ const showRecap = recapIdx >= 0;
709
+ if (recapIdx >= 0) claudeArgs.splice(recapIdx, 1);
637
710
  const pi = claudeArgs.indexOf('--port');
638
- if (pi >= 0) { PORT = parseInt(claudeArgs[pi+1], 10) || PORT; claudeArgs.splice(pi, 2); }
711
+ if (pi >= 0) {
712
+ const parsed = parseInt(claudeArgs[pi+1], 10);
713
+ if (parsed > 0) { PORT = parsed; PORT_EXPLICIT = true; }
714
+ claudeArgs.splice(pi, 2);
715
+ }
639
716
 
640
717
  // Budget flag: --budget <N> sets a session dollar limit.
641
718
  const bgtIdx = claudeArgs.indexOf('--budget');
@@ -688,7 +765,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
688
765
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
689
766
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
690
767
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
691
- try { server && server.close && server.close(); } catch {}
768
+ try { server && server.close && server.close(); } catch { /* ignore */ }
692
769
  setTimeout(() => process.exit(1), 250);
693
770
  }
694
771
  });
@@ -727,7 +804,7 @@ if (budget !== null) {
727
804
  const { execSync: _ex } = require('child_process');
728
805
  let _pyOk = false;
729
806
  for (const _cmd of ['python', 'python3']) {
730
- try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch {}
807
+ try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch { /* ignore */ }
731
808
  }
732
809
  if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
733
810
  }
@@ -775,7 +852,7 @@ const server = http.createServer((req, res) => {
775
852
  try {
776
853
  const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
777
854
  blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
778
- } catch {}
855
+ } catch { /* ignore */ }
779
856
  }
780
857
 
781
858
  const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
@@ -808,7 +885,7 @@ const server = http.createServer((req, res) => {
808
885
  res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
809
886
  return;
810
887
  }
811
- } catch {}
888
+ } catch { /* ignore */ }
812
889
  }
813
890
 
814
891
  // ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
@@ -842,7 +919,7 @@ const server = http.createServer((req, res) => {
842
919
  const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
843
920
  s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
844
921
  fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
845
- } catch {}
922
+ } catch { /* ignore */ }
846
923
  const synth = decision.syntheticResponse;
847
924
  res.writeHead(synth.status, { 'Content-Type': 'application/json' });
848
925
  res.end(JSON.stringify(synth.body));
@@ -870,7 +947,7 @@ const server = http.createServer((req, res) => {
870
947
  }
871
948
  }
872
949
  }
873
- } catch {}
950
+ } catch { /* ignore */ }
874
951
  }
875
952
  // ──────────────────────────────────────────────────────────────────────────
876
953
 
@@ -1057,7 +1134,7 @@ const server = http.createServer((req, res) => {
1057
1134
  }
1058
1135
  forwardBody = Buffer.from(JSON.stringify(b));
1059
1136
  outboundMessageCount = b.messages?.length ?? outboundMessageCount;
1060
- } catch {}
1137
+ } catch { /* ignore */ }
1061
1138
  }
1062
1139
  if (laoDropped.length > 0) {
1063
1140
  const ts0 = new Date().toTimeString().slice(0, 8);
@@ -1183,7 +1260,7 @@ const server = http.createServer((req, res) => {
1183
1260
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
1184
1261
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
1185
1262
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
1186
- try { server && server.close && server.close(); } catch {}
1263
+ try { server && server.close && server.close(); } catch { /* ignore */ }
1187
1264
  setTimeout(() => process.exit(1), 250);
1188
1265
  return;
1189
1266
  }
@@ -1212,10 +1289,10 @@ const server = http.createServer((req, res) => {
1212
1289
  cacheRead = d.usage.cache_read_input_tokens || cacheRead;
1213
1290
  }
1214
1291
  if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
1215
- } catch {}
1292
+ } catch { /* ignore */ }
1216
1293
  }
1217
1294
  }
1218
- } catch {}
1295
+ } catch { /* ignore */ }
1219
1296
 
1220
1297
  // When the interceptor ran, Anthropic was billed for N calls:
1221
1298
  // call #1 → initial tool_use round (toolCallUsage)
@@ -1355,7 +1432,7 @@ const server = http.createServer((req, res) => {
1355
1432
 
1356
1433
  server.on('error', e => {
1357
1434
  if (e.code === 'EADDRINUSE') {
1358
- process.stderr.write(col.r(`\n❌ Port ${PORT} already in use. Kill the old process first:\n`));
1435
+ process.stderr.write(col.r(`\n❌ Port ${PORT} already in use. Kill the old process first or omit --port / OCCASIO_PORT to let the OS auto-assign:\n`));
1359
1436
  process.stderr.write(col.d(` netstat -ano | findstr :${PORT} → then: taskkill /PID <pid> /F\n\n`));
1360
1437
  } else {
1361
1438
  process.stderr.write(col.r(`\n❌ ${e.message}\n`));
@@ -1364,6 +1441,17 @@ server.on('error', e => {
1364
1441
  });
1365
1442
 
1366
1443
  server.listen(PORT, '127.0.0.1', () => {
1444
+ PORT = server.address().port;
1445
+ process.stderr.write(col.d(`occasio proxy listening on 127.0.0.1:${PORT}\n`));
1446
+
1447
+ // Best-effort: print a 3-line recap of the previous session so the user
1448
+ // (and via screen-context, the agent itself) has a memory anchor before
1449
+ // typing the next prompt. Silent if no prior session in this cwd.
1450
+ // Disable with --no-recap.
1451
+ if (showRecap) {
1452
+ try { printSessionRecapBanner(); } catch { /* never block startup */ }
1453
+ }
1454
+
1367
1455
  const env = { ...process.env, ANTHROPIC_BASE_URL: `http://localhost:${PORT}` };
1368
1456
  // On Windows, npm installs binaries as .cmd wrappers (claude.cmd).
1369
1457
  // spawn() without shell:true calls CreateProcess directly, which won't
@@ -1450,7 +1538,7 @@ server.listen(PORT, '127.0.0.1', () => {
1450
1538
  if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
1451
1539
  }
1452
1540
  process.stderr.write('────────────────────────────────────────\n\n');
1453
- } catch {}
1541
+ } catch { /* ignore */ }
1454
1542
  process.exit(code || 0);
1455
1543
  });
1456
1544
 
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`);
@@ -264,7 +264,7 @@ function normalize(parsed) {
264
264
  try {
265
265
  const regex = new RegExp(rawPattern);
266
266
  patterns.push(Object.freeze({ label, regex }));
267
- } catch (e) {
267
+ } catch {
268
268
  process.stderr.write(`[Occasio] policy.yml: deny_patterns.${label} — invalid RegExp "${rawPattern}", entry skipped\n`);
269
269
  }
270
270
  }
@@ -276,7 +276,7 @@ function normalize(parsed) {
276
276
  // module load (loader.js is imported by other code paths that don't
277
277
  // need the registry).
278
278
  let toolNames;
279
- try { toolNames = require('../core/tool-names'); } catch {}
279
+ try { toolNames = require('../core/tool-names'); } catch { /* ignore */ }
280
280
  const tools = {};
281
281
  for (const name of Object.keys(parsed.tools)) {
282
282
  const entry = normalizeToolEntry(parsed.tools[name]);
@@ -351,7 +351,7 @@ function _firePolicyChange(filePath, policy, hash, fileWasPresent) {
351
351
  });
352
352
  } catch (e) {
353
353
  // Listener crash must not break the proxy — surface to stderr only.
354
- try { process.stderr.write(`[occasio] policy-change listener threw: ${e.message}\n`); } catch {}
354
+ try { process.stderr.write(`[occasio] policy-change listener threw: ${e.message}\n`); } catch { /* ignore */ }
355
355
  }
356
356
  }
357
357
  }
@@ -12,7 +12,6 @@
12
12
  */
13
13
 
14
14
  const fs = require('fs');
15
- const path = require('path');
16
15
 
17
16
  // Transforms currently implemented in the dispatcher.
18
17
  const KNOWN_TRANSFORMS = new Set(['redact-secrets', 'distill-output']);
@@ -88,7 +87,7 @@ function runPolicyCli(args, opts = {}) {
88
87
  const text = fs.readFileSync(filePath, 'utf8');
89
88
  fileExists = true;
90
89
  userParsed = loader.parse(text);
91
- } catch {}
90
+ } catch { /* ignore */ }
92
91
 
93
92
  const active = loader.load();
94
93
  const defaults = loader.DEFAULT_POLICY;
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  const os = require('os');
14
- const path = require('path');
15
14
  const {
16
15
  mine,
17
16
  getGitRoot,
@@ -122,10 +122,10 @@ function readRecentEntries(days, logsDir) {
122
122
  for (const line of raw.split('\n')) {
123
123
  const trimmed = line.trim();
124
124
  if (!trimmed) continue;
125
- try { entries.push(JSON.parse(trimmed)); } catch {}
125
+ try { entries.push(JSON.parse(trimmed)); } catch { /* ignore */ }
126
126
  }
127
127
  }
128
- } catch {}
128
+ } catch { /* ignore */ }
129
129
  return entries;
130
130
  }
131
131
 
@@ -231,14 +231,11 @@ function mine(opts) {
231
231
 
232
232
  // project_root → { projectRoot, sessions: Set, toolCounts: Map, legacySessions: number }
233
233
  const projects = new Map();
234
- let totalLegacy = 0;
235
234
 
236
235
  for (const [, runEntries] of runsMap) {
237
236
  const cwd = runCwd(runEntries);
238
237
  if (!cwd) {
239
- // Pre-schema run; count it if it falls under the filter project
240
- // but we can't know which project it belongs to — just count globally.
241
- totalLegacy++;
238
+ // Pre-schema run with no cwd can't attribute to a project, skip.
242
239
  continue;
243
240
  }
244
241
 
package/src/redteam.js CHANGED
@@ -33,7 +33,6 @@
33
33
  */
34
34
 
35
35
  const fs = require('fs');
36
- const path = require('path');
37
36
  const harness = require('./harness');
38
37
 
39
38
  const C = (() => {
@@ -411,7 +410,7 @@ async function runRedteamCli(args = []) {
411
410
  return result;
412
411
  } finally {
413
412
  if (!keepScratch && !process.env.OCC_REDTEAM_KEEP) {
414
- try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
413
+ try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch { /* ignore */ }
415
414
  }
416
415
  }
417
416
  }
package/src/replay.js CHANGED
@@ -386,7 +386,7 @@ function runReplayCli(args) {
386
386
  for (const raw of lines) {
387
387
  const line = raw.trim();
388
388
  if (!line) continue;
389
- try { entries.push(JSON.parse(line)); } catch {}
389
+ try { entries.push(JSON.parse(line)); } catch { /* ignore */ }
390
390
  }
391
391
  }
392
392
 
@@ -25,10 +25,6 @@ const LOGS_DIR = path.join(LOG_DIR, 'logs');
25
25
 
26
26
  // ── Helpers ───────────────────────────────────────────────────────────────────
27
27
 
28
- function todayStr(d) {
29
- return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
30
- }
31
-
32
28
  function cutoffMs(days) {
33
29
  const d = new Date();
34
30
  d.setDate(d.getDate() - days);