@occasiolabs/occasio 0.8.4 → 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 (43) hide show
  1. package/docs/ADAPTER-STAGE-2-MIGRATION.md +59 -0
  2. package/docs/STAGE-2-STEP-5-SHELL-PLAN.md +107 -0
  3. package/docs/THREAT-MODEL.md +195 -0
  4. package/docs/edr-calibration.md +29 -0
  5. package/package.json +7 -3
  6. package/src/adapters/claude-code.js +1 -2
  7. package/src/adapters/computer-use.js +1 -1
  8. package/src/anomaly/cli.js +4 -1
  9. package/src/anomaly/detectors/deny-rate.js +2 -1
  10. package/src/anomaly/detectors/file-read-volume.js +2 -1
  11. package/src/anomaly/index.js +5 -0
  12. package/src/boundary.js +1 -1
  13. package/src/classifier.js +1 -1
  14. package/src/cli/clear.js +4 -4
  15. package/src/cli/help.js +58 -37
  16. package/src/cli/status.js +1 -1
  17. package/src/dashboard.js +2 -3
  18. package/src/distiller.js +1 -1
  19. package/src/executor/dispatcher.js +2 -2
  20. package/src/executor/native-handlers/glob.js +173 -0
  21. package/src/executor/native-handlers/grep.js +258 -0
  22. package/src/executor/native-handlers/read.js +99 -0
  23. package/src/executor/native-handlers/todo.js +56 -0
  24. package/src/harness.js +8 -10
  25. package/src/index.js +13 -15
  26. package/src/inspect.js +1 -1
  27. package/src/interceptor.js +9 -29
  28. package/src/ledger.js +2 -3
  29. package/src/mcp-experiment.js +4 -4
  30. package/src/mcp-server.js +3 -3
  31. package/src/policy/doctor.js +2 -2
  32. package/src/policy/engine.js +0 -1
  33. package/src/policy/init.js +1 -1
  34. package/src/policy/loader.js +3 -3
  35. package/src/policy/show.js +1 -2
  36. package/src/preflight/cli.js +0 -1
  37. package/src/preflight/miner.js +3 -6
  38. package/src/redteam.js +1 -2
  39. package/src/replay.js +1 -1
  40. package/src/report/index.js +0 -4
  41. package/src/runtime.js +42 -444
  42. package/src/selftest.js +1 -1
  43. package/src/session.js +1 -1
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Native handler for the Read tool.
5
+ *
6
+ * Pure filesystem function: takes a file_path (+ optional offset/limit) and
7
+ * returns cat -n formatted output. No dependency on the interceptor pipeline,
8
+ * Anthropic API, or shell execution. Safe to import in any process context.
9
+ *
10
+ * Extracted from src/runtime.js as Stage-2 of the executor migration
11
+ * (see docs/ADAPTER-STAGE-2-MIGRATION.md). src/runtime.js re-exports
12
+ * these so existing consumers (src/interceptor.js, tests) keep working
13
+ * unchanged.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // ── Shared constants ───────────────────────────────────────────────────────────
20
+
21
+ const MAX_OUTPUT = 512 * 1024; // 512 KB — same cap as exec maxBuffer
22
+
23
+ // File extensions the native Read handler cannot serve correctly.
24
+ // PDFs and images need structured rendering (base64, page extraction) that we
25
+ // cannot replicate; Jupyter notebooks need cell-by-cell parsing. All others
26
+ // are treated as UTF-8 text and handled natively.
27
+ const READ_SKIP_EXTENSIONS = new Set([
28
+ '.pdf', '.ipynb',
29
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico',
30
+ '.zip', '.gz', '.tar', '.bz2', '.xz', '.7z', '.rar',
31
+ '.exe', '.dll', '.so', '.dylib',
32
+ ]);
33
+
34
+ // ── Shared helper ──────────────────────────────────────────────────────────────
35
+
36
+ function readFileNative(absPath) {
37
+ const buf = fs.readFileSync(absPath);
38
+ if (buf.length > MAX_OUTPUT) {
39
+ return buf.slice(0, MAX_OUTPUT).toString('utf8') + '\n[truncated — file too large]';
40
+ }
41
+ return buf.toString('utf8');
42
+ }
43
+
44
+ // ── Read tool support ──────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Returns true when this Read input can be served natively.
48
+ * Falls back for PDFs/images (need structured rendering), Jupyter notebooks,
49
+ * malformed input, or the `pages` parameter (implies PDF range extraction).
50
+ */
51
+ // UNC / network paths cause blocking SMB resolution on Windows (10+ s).
52
+ // Reject so the agent cannot stall the proxy via `\\server\share\file` or
53
+ // the // equivalent. Local filesystem only — a deliberate restriction.
54
+ const UNC_PREFIX_RE = /^[/\\]{2}/;
55
+
56
+ function isReadHandleable(input) {
57
+ if (!input || typeof input !== 'object') return false;
58
+ const fp = input.file_path;
59
+ if (!fp || typeof fp !== 'string' || !fp.trim()) return false;
60
+ if (UNC_PREFIX_RE.test(fp)) return false;
61
+ if (input.pages != null) return false;
62
+ const ext = path.extname(fp).toLowerCase();
63
+ return !READ_SKIP_EXTENSIONS.has(ext);
64
+ }
65
+
66
+ /**
67
+ * Read a file natively and return content formatted like `cat -n` (1-based line
68
+ * numbers), honouring the optional offset (0-based line index) and limit fields
69
+ * that the Claude Code Read tool sends for partial reads.
70
+ */
71
+ function handleReadTool(input) {
72
+ const fp = (typeof input?.file_path === 'string' ? input.file_path : '').trim();
73
+ if (!fp) return { output: '(no file_path provided)', exitCode: 1 };
74
+
75
+ const abs = path.resolve(process.cwd(), fp);
76
+ try {
77
+ const content = readFileNative(abs); // already caps at MAX_OUTPUT
78
+ const lines = content.split('\n');
79
+ const offset = (typeof input.offset === 'number' && input.offset >= 0) ? input.offset : 0;
80
+ const limit = (typeof input.limit === 'number' && input.limit > 0) ? input.limit : lines.length;
81
+ const slice = lines.slice(offset, offset + limit);
82
+ // Line numbers reflect position in the file (not the slice), matching cat -n.
83
+ const formatted = slice.map((l, i) => `${String(offset + i + 1).padStart(6)}\t${l}`).join('\n');
84
+ return { output: formatted, exitCode: 0 };
85
+ } catch (e) {
86
+ const msg = e.code === 'ENOENT'
87
+ ? `${fp}: No such file or directory`
88
+ : `${fp}: ${e.message}`;
89
+ return { output: `Read: ${msg}`, exitCode: 1 };
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ MAX_OUTPUT,
95
+ READ_SKIP_EXTENSIONS,
96
+ readFileNative,
97
+ isReadHandleable,
98
+ handleReadTool,
99
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Native handlers for the TodoWrite / TodoRead tools.
5
+ *
6
+ * Pure functions over a caller-owned mutable `todoStore` array. No I/O,
7
+ * no globals — the session owns the store; this module only mutates it.
8
+ *
9
+ * Extracted from src/runtime.js as Stage-2 of the executor migration
10
+ * (see docs/ADAPTER-STAGE-2-MIGRATION.md). src/runtime.js re-exports
11
+ * these so existing consumers (src/interceptor.js, tests) keep working
12
+ * unchanged.
13
+ */
14
+
15
+ /**
16
+ * Returns true when this TodoWrite/TodoRead call can be served natively.
17
+ * TodoRead: always handleable — no required inputs.
18
+ * TodoWrite: requires input.todos to be an array.
19
+ */
20
+ function isTodoHandleable(input, toolName) {
21
+ if (toolName === 'TodoRead') return true;
22
+ if (toolName === 'TodoWrite') {
23
+ if (!input || typeof input !== 'object') return false;
24
+ return Array.isArray(input.todos);
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * Handle a TodoWrite call: replace the session todo list with input.todos.
31
+ * Returns { output: '', exitCode: 0, taskCount: N } on success.
32
+ * Claude Code expects an empty-string response from write tools.
33
+ */
34
+ function handleTodoWriteTool(input, todoStore) {
35
+ const todos = input?.todos;
36
+ if (!Array.isArray(todos)) {
37
+ return { output: 'TodoWrite: todos must be an array', exitCode: 1, taskCount: 0 };
38
+ }
39
+ todoStore.splice(0, todoStore.length, ...todos);
40
+ return { output: '', exitCode: 0, taskCount: todos.length };
41
+ }
42
+
43
+ /**
44
+ * Handle a TodoRead call: return the session todo list as a JSON string.
45
+ * Returns { output: string, exitCode: 0, taskCount: N }.
46
+ */
47
+ function handleTodoReadTool(todoStore) {
48
+ const output = JSON.stringify(todoStore, null, 2);
49
+ return { output, exitCode: 0, taskCount: todoStore.length };
50
+ }
51
+
52
+ module.exports = {
53
+ isTodoHandleable,
54
+ handleTodoWriteTool,
55
+ handleTodoReadTool,
56
+ };
package/src/harness.js CHANGED
@@ -128,8 +128,6 @@ const SCENARIOS = {
128
128
  const f = ctx.secretPath;
129
129
  // Build several path variants pointing at the same real file
130
130
  const ws = ctx.workspace;
131
- const drive = f.match(/^[A-Z]:/i)?.[0] || '';
132
- const tail = f.slice(drive.length);
133
131
  const v = [
134
132
  f, // canonical
135
133
  f.replace(/\\/g, '/'), // forward slashes
@@ -179,7 +177,7 @@ const SCENARIOS = {
179
177
  const type = process.platform === 'win32' ? 'junction' : 'dir';
180
178
  fs.symlinkSync(ctx.denyDir, aliasDir, type);
181
179
  ctx.aliasPath = path.join(aliasDir, 'plans.md');
182
- } catch (e) {
180
+ } catch {
183
181
  // Symlink creation can fail (e.g. tmpfs that disallows symlinks).
184
182
  // Fall back to a plain path so the scenario still exercises the
185
183
  // direct case, with a clear note in the prompt.
@@ -489,7 +487,7 @@ function prepareWorkspace(scenarioName, opts = {}) {
489
487
  }
490
488
 
491
489
  function cleanupWorkspace(ctx) {
492
- try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
490
+ try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch { /* ignore */ }
493
491
  }
494
492
 
495
493
  // ── Subprocess spawning ─────────────────────────────────────────────────────
@@ -564,8 +562,8 @@ function runScenarioChild(scenarioName, ctx, opts = {}) {
564
562
  let stdout = '', stderr = '', timedOut = false;
565
563
  const t = setTimeout(() => {
566
564
  timedOut = true;
567
- try { child.kill('SIGTERM'); } catch {}
568
- setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 5_000);
565
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
566
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* ignore */ } }, 5_000);
569
567
  }, timeoutMs);
570
568
 
571
569
  if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
@@ -607,8 +605,8 @@ function runMcpScenario(scenarioName, ctx, opts = {}) {
607
605
  let stdout = '', stderr = '', timedOut = false;
608
606
  const t = setTimeout(() => {
609
607
  timedOut = true;
610
- try { child.kill('SIGTERM'); } catch {}
611
- setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2_000);
608
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
609
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* ignore */ } }, 2_000);
612
610
  }, timeoutMs);
613
611
 
614
612
  if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
@@ -637,8 +635,8 @@ function runMcpScenario(scenarioName, ctx, opts = {}) {
637
635
  child.stdin.write(JSON.stringify(init) + '\n');
638
636
  child.stdin.write(JSON.stringify(callRead) + '\n');
639
637
  // Give the server a moment to process, then close stdin so it exits.
640
- setTimeout(() => { try { child.stdin.end(); } catch {} }, 2_000);
641
- } catch (e) {
638
+ setTimeout(() => { try { child.stdin.end(); } catch { /* ignore */ } }, 2_000);
639
+ } catch {
642
640
  // best effort
643
641
  }
644
642
  });
package/src/index.js CHANGED
@@ -75,8 +75,6 @@ function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-
75
75
  // MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
76
76
  // rest of index.js keeps its existing call sites unchanged.
77
77
  const {
78
- MODEL_PRICES,
79
- getPrice,
80
78
  calcCost,
81
79
  calcCacheSavings,
82
80
  calcCompoundingSavings,
@@ -102,7 +100,7 @@ function updateSession(e) {
102
100
  lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
103
101
  tools_attempted:0,
104
102
  };
105
- try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
103
+ try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
106
104
  s.requests++;
107
105
  s.input_tokens += e.input_tokens || 0;
108
106
  s.output_tokens += e.output_tokens || 0;
@@ -562,7 +560,7 @@ if (cmd === 'doctor' || cmd === 'check') {
562
560
  stdio: ['pipe', 'pipe', 'pipe'],
563
561
  }).toString().trim();
564
562
  ok('Python (LAO)', out); pyFound = true;
565
- } catch {}
563
+ } catch { /* ignore */ }
566
564
  }
567
565
  if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
568
566
  if (laoPyExists) ok('LAO scorer', laoPyPath);
@@ -688,7 +686,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
688
686
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
689
687
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
690
688
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
691
- try { server && server.close && server.close(); } catch {}
689
+ try { server && server.close && server.close(); } catch { /* ignore */ }
692
690
  setTimeout(() => process.exit(1), 250);
693
691
  }
694
692
  });
@@ -727,7 +725,7 @@ if (budget !== null) {
727
725
  const { execSync: _ex } = require('child_process');
728
726
  let _pyOk = false;
729
727
  for (const _cmd of ['python', 'python3']) {
730
- 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 */ }
731
729
  }
732
730
  if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
733
731
  }
@@ -775,7 +773,7 @@ const server = http.createServer((req, res) => {
775
773
  try {
776
774
  const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
777
775
  blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
778
- } catch {}
776
+ } catch { /* ignore */ }
779
777
  }
780
778
 
781
779
  const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
@@ -808,7 +806,7 @@ const server = http.createServer((req, res) => {
808
806
  res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
809
807
  return;
810
808
  }
811
- } catch {}
809
+ } catch { /* ignore */ }
812
810
  }
813
811
 
814
812
  // ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
@@ -842,7 +840,7 @@ const server = http.createServer((req, res) => {
842
840
  const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
843
841
  s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
844
842
  fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
845
- } catch {}
843
+ } catch { /* ignore */ }
846
844
  const synth = decision.syntheticResponse;
847
845
  res.writeHead(synth.status, { 'Content-Type': 'application/json' });
848
846
  res.end(JSON.stringify(synth.body));
@@ -870,7 +868,7 @@ const server = http.createServer((req, res) => {
870
868
  }
871
869
  }
872
870
  }
873
- } catch {}
871
+ } catch { /* ignore */ }
874
872
  }
875
873
  // ──────────────────────────────────────────────────────────────────────────
876
874
 
@@ -1057,7 +1055,7 @@ const server = http.createServer((req, res) => {
1057
1055
  }
1058
1056
  forwardBody = Buffer.from(JSON.stringify(b));
1059
1057
  outboundMessageCount = b.messages?.length ?? outboundMessageCount;
1060
- } catch {}
1058
+ } catch { /* ignore */ }
1061
1059
  }
1062
1060
  if (laoDropped.length > 0) {
1063
1061
  const ts0 = new Date().toTimeString().slice(0, 8);
@@ -1183,7 +1181,7 @@ const server = http.createServer((req, res) => {
1183
1181
  process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
1184
1182
  process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
1185
1183
  process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
1186
- try { server && server.close && server.close(); } catch {}
1184
+ try { server && server.close && server.close(); } catch { /* ignore */ }
1187
1185
  setTimeout(() => process.exit(1), 250);
1188
1186
  return;
1189
1187
  }
@@ -1212,10 +1210,10 @@ const server = http.createServer((req, res) => {
1212
1210
  cacheRead = d.usage.cache_read_input_tokens || cacheRead;
1213
1211
  }
1214
1212
  if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
1215
- } catch {}
1213
+ } catch { /* ignore */ }
1216
1214
  }
1217
1215
  }
1218
- } catch {}
1216
+ } catch { /* ignore */ }
1219
1217
 
1220
1218
  // When the interceptor ran, Anthropic was billed for N calls:
1221
1219
  // call #1 → initial tool_use round (toolCallUsage)
@@ -1450,7 +1448,7 @@ server.listen(PORT, '127.0.0.1', () => {
1450
1448
  if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
1451
1449
  }
1452
1450
  process.stderr.write('────────────────────────────────────────\n\n');
1453
- } catch {}
1451
+ } catch { /* ignore */ }
1454
1452
  process.exit(code || 0);
1455
1453
  });
1456
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`);
@@ -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
  }