@oculisecurity/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +67 -0
  3. package/dist/cli.d.ts +18 -0
  4. package/dist/cli.js +565 -0
  5. package/dist/commands/init.d.ts +14 -0
  6. package/dist/commands/init.js +135 -0
  7. package/dist/commands/report.d.ts +33 -0
  8. package/dist/commands/report.js +145 -0
  9. package/dist/commands/serve.d.ts +27 -0
  10. package/dist/commands/serve.js +163 -0
  11. package/dist/commands/tail.d.ts +7 -0
  12. package/dist/commands/tail.js +211 -0
  13. package/dist/commands/uninstall.d.ts +13 -0
  14. package/dist/commands/uninstall.js +111 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.js +90 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +35 -0
  19. package/dist/init.d.ts +9 -0
  20. package/dist/init.js +50 -0
  21. package/dist/install/claude-code.d.ts +13 -0
  22. package/dist/install/claude-code.js +118 -0
  23. package/dist/install/cursor.d.ts +13 -0
  24. package/dist/install/cursor.js +119 -0
  25. package/dist/install/detect.d.ts +5 -0
  26. package/dist/install/detect.js +64 -0
  27. package/dist/middleware/auth.d.ts +15 -0
  28. package/dist/middleware/auth.js +116 -0
  29. package/dist/routes/adapters/claude-code.d.ts +38 -0
  30. package/dist/routes/adapters/claude-code.js +125 -0
  31. package/dist/routes/adapters/cursor.d.ts +21 -0
  32. package/dist/routes/adapters/cursor.js +139 -0
  33. package/dist/routes/adapters/index.d.ts +16 -0
  34. package/dist/routes/adapters/index.js +56 -0
  35. package/dist/routes/adapters/router.d.ts +31 -0
  36. package/dist/routes/adapters/router.js +97 -0
  37. package/dist/routes/adapters/schema.d.ts +141 -0
  38. package/dist/routes/adapters/schema.js +83 -0
  39. package/dist/routes/adapters/windsurf.d.ts +6 -0
  40. package/dist/routes/adapters/windsurf.js +48 -0
  41. package/dist/routes/admin.d.ts +15 -0
  42. package/dist/routes/admin.js +399 -0
  43. package/dist/routes/call.d.ts +13 -0
  44. package/dist/routes/call.js +68 -0
  45. package/dist/routes/events.d.ts +7 -0
  46. package/dist/routes/events.js +125 -0
  47. package/dist/routes/health.d.ts +2 -0
  48. package/dist/routes/health.js +12 -0
  49. package/dist/routes/hooks.d.ts +11 -0
  50. package/dist/routes/hooks.js +166 -0
  51. package/dist/routes/mcp.d.ts +10 -0
  52. package/dist/routes/mcp.js +170 -0
  53. package/dist/routes/openai-tools.d.ts +9 -0
  54. package/dist/routes/openai-tools.js +121 -0
  55. package/dist/server.d.ts +11 -0
  56. package/dist/server.js +118 -0
  57. package/dist/services/audit.d.ts +92 -0
  58. package/dist/services/audit.js +388 -0
  59. package/dist/services/data-dir.d.ts +7 -0
  60. package/dist/services/data-dir.js +61 -0
  61. package/dist/services/local-policy-templates.d.ts +9 -0
  62. package/dist/services/local-policy-templates.js +47 -0
  63. package/dist/services/local-policy.d.ts +39 -0
  64. package/dist/services/local-policy.js +172 -0
  65. package/dist/services/policy-store.d.ts +82 -0
  66. package/dist/services/policy-store.js +331 -0
  67. package/dist/services/policy.d.ts +8 -0
  68. package/dist/services/policy.js +126 -0
  69. package/dist/services/ratelimit.d.ts +26 -0
  70. package/dist/services/ratelimit.js +60 -0
  71. package/dist/services/sanitizer.d.ts +9 -0
  72. package/dist/services/sanitizer.js +73 -0
  73. package/dist/services/sqlite-loader.d.ts +4 -0
  74. package/dist/services/sqlite-loader.js +16 -0
  75. package/dist/services/telemetry-log.d.ts +76 -0
  76. package/dist/services/telemetry-log.js +260 -0
  77. package/dist/services/tool-executor.d.ts +46 -0
  78. package/dist/services/tool-executor.js +167 -0
  79. package/dist/services/upstream.d.ts +18 -0
  80. package/dist/services/upstream.js +72 -0
  81. package/dist/types.d.ts +112 -0
  82. package/dist/types.js +3 -0
  83. package/package.json +72 -0
  84. package/public/favicon.svg +4 -0
  85. package/public/index.html +3893 -0
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.formatEntry = formatEntry;
37
+ exports.runTail = runTail;
38
+ const fs = __importStar(require("fs"));
39
+ const telemetry_log_1 = require("../services/telemetry-log");
40
+ // ---------------------------------------------------------------------------
41
+ // ANSI colors
42
+ // ---------------------------------------------------------------------------
43
+ function useColor() {
44
+ if (process.env.NO_COLOR)
45
+ return false;
46
+ return process.stdout.isTTY === true;
47
+ }
48
+ const color = {
49
+ reset: '\x1b[0m',
50
+ dim: '\x1b[2m',
51
+ green: '\x1b[32m',
52
+ yellow: '\x1b[33m',
53
+ red: '\x1b[31m',
54
+ cyan: '\x1b[36m',
55
+ white: '\x1b[37m',
56
+ };
57
+ function c(code, text, enabled) {
58
+ return enabled ? `${code}${text}${color.reset}` : text;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Formatting
62
+ // ---------------------------------------------------------------------------
63
+ function padRight(s, len) {
64
+ return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
65
+ }
66
+ function truncate(s, max) {
67
+ return s.length <= max ? s : s.slice(0, max - 1) + '…';
68
+ }
69
+ function formatEntry(entry, colorEnabled) {
70
+ // Time: HH:MM:SS
71
+ const ts = new Date(entry.timestamp);
72
+ const time = [
73
+ String(ts.getHours()).padStart(2, '0'),
74
+ String(ts.getMinutes()).padStart(2, '0'),
75
+ String(ts.getSeconds()).padStart(2, '0'),
76
+ ].join(':');
77
+ // Phase
78
+ const phaseMap = { pre: 'PRE ', post: 'POST', complete: 'DONE' };
79
+ const phase = phaseMap[entry.phase] ?? entry.phase.toUpperCase().slice(0, 4);
80
+ // Tool
81
+ const tool = padRight(entry.tool ?? '—', 10);
82
+ // Detail: command for shell, file_path for file ops, mcp_server for mcp
83
+ let detail = '—';
84
+ if (entry.action?.command) {
85
+ detail = entry.action.command;
86
+ }
87
+ else if (entry.file_path) {
88
+ detail = entry.file_path;
89
+ }
90
+ else if (entry.shell_command) {
91
+ detail = entry.shell_command;
92
+ }
93
+ else if (entry.mcp_server) {
94
+ detail = entry.mcp_server;
95
+ }
96
+ else if (entry.tool_args) {
97
+ // Fallback: show first string arg value
98
+ for (const val of Object.values(entry.tool_args)) {
99
+ if (typeof val === 'string' && val.length > 0) {
100
+ detail = val;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ detail = truncate(detail, 40);
106
+ // Decision
107
+ let decisionStr;
108
+ const ruleIds = entry.policy_rule_ids?.length ? ` (${entry.policy_rule_ids.join(', ')})` : '';
109
+ switch (entry.policy_decision) {
110
+ case 'allow':
111
+ decisionStr = c(color.green, `✅ allow${ruleIds}`, colorEnabled);
112
+ break;
113
+ case 'warn':
114
+ decisionStr = c(color.yellow, `⚠️ warn${ruleIds}`, colorEnabled);
115
+ break;
116
+ case 'deny':
117
+ decisionStr = c(color.red, `🚫 deny${ruleIds}`, colorEnabled);
118
+ break;
119
+ default:
120
+ decisionStr = entry.policy_decision;
121
+ }
122
+ const timeStr = c(color.dim, `[${time}]`, colorEnabled);
123
+ const phaseStr = c(color.cyan, phase, colorEnabled);
124
+ const toolStr = c(color.white, tool, colorEnabled);
125
+ return `${timeStr} ${phaseStr} | ${toolStr} | ${padRight(detail, 40)} | ${decisionStr}`;
126
+ }
127
+ function runTail(opts) {
128
+ const logPath = (0, telemetry_log_1.findTelemetryLog)();
129
+ if (!logPath) {
130
+ process.stderr.write('oculi: no telemetry log found. Run some commands first.\n');
131
+ process.exit(1);
132
+ }
133
+ const colorEnabled = !opts.noColor && useColor();
134
+ // Print existing lines from near the end
135
+ let fileSize = 0;
136
+ try {
137
+ fileSize = fs.statSync(logPath).size;
138
+ }
139
+ catch {
140
+ // File might not exist yet
141
+ }
142
+ // Read last ~64KB for initial display
143
+ const TAIL_BYTES = 64 * 1024;
144
+ const startPos = Math.max(0, fileSize - TAIL_BYTES);
145
+ if (fileSize > 0) {
146
+ const fd = fs.openSync(logPath, 'r');
147
+ const buf = Buffer.alloc(Math.min(fileSize, TAIL_BYTES));
148
+ fs.readSync(fd, buf, 0, buf.length, startPos);
149
+ fs.closeSync(fd);
150
+ const text = buf.toString('utf8');
151
+ const lines = text.split('\n');
152
+ // If we started mid-line, skip the first partial line
153
+ const start = startPos > 0 ? 1 : 0;
154
+ for (let i = start; i < lines.length; i++) {
155
+ const line = lines[i].trim();
156
+ if (!line)
157
+ continue;
158
+ try {
159
+ const entry = JSON.parse(line);
160
+ if (opts.filter && entry.policy_decision !== opts.filter)
161
+ continue;
162
+ process.stdout.write(formatEntry(entry, colorEnabled) + '\n');
163
+ }
164
+ catch {
165
+ // Skip malformed
166
+ }
167
+ }
168
+ }
169
+ // Watch for new lines
170
+ let currentSize = fileSize;
171
+ const watcher = fs.watch(logPath, () => {
172
+ let newSize;
173
+ try {
174
+ newSize = fs.statSync(logPath).size;
175
+ }
176
+ catch {
177
+ return;
178
+ }
179
+ if (newSize <= currentSize) {
180
+ // File was rotated — reset
181
+ currentSize = 0;
182
+ return;
183
+ }
184
+ const fd = fs.openSync(logPath, 'r');
185
+ const bytesToRead = newSize - currentSize;
186
+ const buf = Buffer.alloc(bytesToRead);
187
+ fs.readSync(fd, buf, 0, bytesToRead, currentSize);
188
+ fs.closeSync(fd);
189
+ currentSize = newSize;
190
+ const text = buf.toString('utf8');
191
+ for (const line of text.split('\n')) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed)
194
+ continue;
195
+ try {
196
+ const entry = JSON.parse(trimmed);
197
+ if (opts.filter && entry.policy_decision !== opts.filter)
198
+ continue;
199
+ process.stdout.write(formatEntry(entry, colorEnabled) + '\n');
200
+ }
201
+ catch {
202
+ // Skip
203
+ }
204
+ }
205
+ });
206
+ // Clean exit on SIGINT
207
+ process.on('SIGINT', () => {
208
+ watcher.close();
209
+ process.exit(0);
210
+ });
211
+ }
@@ -0,0 +1,13 @@
1
+ import { Agent } from '../install/detect';
2
+ export interface UninstallOpts {
3
+ agent?: Agent;
4
+ /** Override the home directory (tests only). Production callers should omit this. */
5
+ home?: string;
6
+ }
7
+ export interface UninstallResult {
8
+ exitCode: 0;
9
+ removed: Partial<Record<Agent, string[]>>;
10
+ policyRemoved: boolean;
11
+ remainingAgents: Agent[];
12
+ }
13
+ export declare function runUninstall(opts?: UninstallOpts): UninstallResult;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runUninstall = runUninstall;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const detect_1 = require("../install/detect");
41
+ const claude_code_1 = require("../install/claude-code");
42
+ const cursor_1 = require("../install/cursor");
43
+ const AGENT_LABEL = {
44
+ 'claude-code': { configPath: '~/.claude/settings.json' },
45
+ cursor: { configPath: '~/.cursor/hooks.json' },
46
+ };
47
+ /** Render an absolute path with the user's home dir collapsed to ~. */
48
+ function tilde(absPath, home) {
49
+ return absPath.startsWith(home) ? '~' + absPath.slice(home.length) : absPath;
50
+ }
51
+ function agentHasOculi(agent, home) {
52
+ return agent === 'claude-code' ? (0, claude_code_1.hasOculiHooks)(home) : (0, cursor_1.hasOculiHooks)(home);
53
+ }
54
+ function runUninstall(opts = {}) {
55
+ const home = opts.home ?? os.homedir();
56
+ const explicit = opts.agent !== undefined;
57
+ const targets = opts.agent ? [opts.agent] : [...detect_1.AGENTS];
58
+ const policyPath = path.join(home, '.oculi', 'policy.yaml');
59
+ const policyExistedBefore = fs.existsSync(policyPath);
60
+ // Remove hooks for each target. Existing per-agent uninstallers preserve
61
+ // non-oculi entries — we never write JSON files directly here.
62
+ const removed = {};
63
+ for (const a of targets) {
64
+ const r = a === 'claude-code' ? (0, claude_code_1.uninstallClaudeCode)({ home }) : (0, cursor_1.uninstallCursor)({ home });
65
+ removed[a] = r.removed;
66
+ }
67
+ // After per-agent uninstall, recompute which agents still have oculi markers
68
+ // on disk. We scan BOTH agents regardless of which one we just touched.
69
+ const remainingAgents = detect_1.AGENTS.filter((a) => agentHasOculi(a, home));
70
+ // Policy removal rule: delete ~/.oculi/policy.yaml iff no agent has an oculi marker.
71
+ let policyRemoved = false;
72
+ if (remainingAgents.length === 0 && policyExistedBefore) {
73
+ try {
74
+ fs.rmSync(policyPath);
75
+ policyRemoved = true;
76
+ }
77
+ catch {
78
+ // best effort
79
+ }
80
+ // NOTE: do NOT rmdir ~/.oculi/ — the dashboard's oculi.db and telemetry.jsonl
81
+ // live there. Only the policy file is owned by us.
82
+ }
83
+ // Build stdout.
84
+ const changes = [];
85
+ const notices = [];
86
+ for (const a of targets) {
87
+ const ev = removed[a] ?? [];
88
+ const cfg = AGENT_LABEL[a].configPath;
89
+ if (ev.length > 0) {
90
+ changes.push(`Removed ${a} hooks ← ${cfg}`);
91
+ for (const e of ev)
92
+ changes.push(` - ${e}`);
93
+ }
94
+ else if (explicit) {
95
+ notices.push(`No oculi hooks found for ${a} in ${cfg}.`);
96
+ }
97
+ }
98
+ if (policyRemoved) {
99
+ changes.push(`Removed ${tilde(policyPath, home)}`);
100
+ }
101
+ else if (policyExistedBefore && remainingAgents.length > 0) {
102
+ notices.push(`Leaving ${tilde(policyPath, home)} in place (${remainingAgents.join(', ')} is still wired).`);
103
+ }
104
+ if (changes.length === 0 && notices.length === 0) {
105
+ process.stdout.write(`Nothing to remove — no oculi hooks or policy found.\n`);
106
+ }
107
+ else {
108
+ process.stdout.write([...changes, ...notices].join('\n') + '\n');
109
+ }
110
+ return { exitCode: 0, removed, policyRemoved, remainingAgents };
111
+ }
@@ -0,0 +1,17 @@
1
+ import { UpstreamConfig } from './types';
2
+ export interface AppConfig {
3
+ port: number;
4
+ jwtSecret: string;
5
+ opaUrl: string;
6
+ opaEnabled: boolean;
7
+ dbPath: string;
8
+ storeFullArgs: boolean;
9
+ upstreams: UpstreamConfig[];
10
+ rateLimitCapacity: number;
11
+ rateLimitRefillPerSecond: number;
12
+ logLevel: string;
13
+ }
14
+ export declare class ConfigValidationError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export declare function loadConfig(): AppConfig;
package/dist/config.js ADDED
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigValidationError = void 0;
7
+ exports.loadConfig = loadConfig;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ /** Resolve ${ENV_VAR:-default} placeholders in strings */
11
+ function resolveEnvVars(str) {
12
+ return str.replace(/\$\{(\w+)(?::-(.*?))?\}/g, (_, key, fallback) => {
13
+ return process.env[key] ?? fallback ?? '';
14
+ });
15
+ }
16
+ function loadUpstreams() {
17
+ const configPath = path_1.default.join(process.cwd(), 'config', 'upstreams.yaml');
18
+ if (!fs_1.default.existsSync(configPath)) {
19
+ // Hardcoded defaults for when config file is absent (tests, Docker, etc.)
20
+ return [
21
+ {
22
+ id: 'fs-server',
23
+ name: 'Demo Filesystem Server',
24
+ baseUrl: process.env.FS_SERVER_URL ?? 'http://localhost:3001',
25
+ timeout: 5000,
26
+ tools: ['readFile', 'listDir', 'writeFile'],
27
+ },
28
+ {
29
+ id: 'http-server',
30
+ name: 'Demo HTTP Fetch Server',
31
+ baseUrl: process.env.HTTP_SERVER_URL ?? 'http://localhost:3002',
32
+ timeout: 10000,
33
+ tools: ['fetchUrl', 'postUrl'],
34
+ },
35
+ ];
36
+ }
37
+ // Lazy-require yaml to keep startup fast when file doesn't exist
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
+ const YAML = require('yaml');
40
+ const raw = fs_1.default.readFileSync(configPath, 'utf8');
41
+ const parsed = YAML.parse(raw);
42
+ return parsed.upstreams.map((u) => {
43
+ if (u.id.includes('__')) {
44
+ throw new Error(`Upstream id '${u.id}' must not contain '__' (reserved as tool name separator)`);
45
+ }
46
+ return {
47
+ ...u,
48
+ baseUrl: resolveEnvVars(u.baseUrl),
49
+ tools: Array.isArray(u.tools) ? u.tools : [],
50
+ };
51
+ });
52
+ }
53
+ class ConfigValidationError extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = 'ConfigValidationError';
57
+ }
58
+ }
59
+ exports.ConfigValidationError = ConfigValidationError;
60
+ const JWT_SECRET_REQUIRED = 'JWT_SECRET environment variable is required for the gateway server.\n' +
61
+ '\n' +
62
+ 'The gateway uses JWT_SECRET to verify tokens issued by your upstream\n' +
63
+ 'authentication provider. It must match the secret your issuer uses to\n' +
64
+ 'sign tokens, and must be at least 32 characters of high-entropy data.\n' +
65
+ '\n' +
66
+ 'Generate a new secret with:\n' +
67
+ ' openssl rand -hex 32\n' +
68
+ '\n' +
69
+ 'See https://docs.oculisecurity.com/gateway/configuration for details.';
70
+ function requireJwtSecret() {
71
+ const v = process.env.JWT_SECRET;
72
+ if (!v || v.length < 32) {
73
+ throw new ConfigValidationError(JWT_SECRET_REQUIRED);
74
+ }
75
+ return v;
76
+ }
77
+ function loadConfig() {
78
+ return {
79
+ port: parseInt(process.env.PORT ?? '3000', 10),
80
+ jwtSecret: requireJwtSecret(),
81
+ opaUrl: process.env.OPA_URL ?? 'http://localhost:8181',
82
+ opaEnabled: process.env.OPA_ENABLED !== 'false',
83
+ dbPath: process.env.DB_PATH ?? path_1.default.join(process.cwd(), 'data', 'audit.db'),
84
+ storeFullArgs: process.env.STORE_FULL_ARGS !== 'false',
85
+ upstreams: loadUpstreams(),
86
+ rateLimitCapacity: parseInt(process.env.RATE_LIMIT_CAPACITY ?? '10', 10),
87
+ rateLimitRefillPerSecond: parseFloat(process.env.RATE_LIMIT_REFILL ?? '2'),
88
+ logLevel: process.env.LOG_LEVEL ?? 'info',
89
+ };
90
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("./config");
4
+ const server_1 = require("./server");
5
+ async function main() {
6
+ const config = (0, config_1.loadConfig)();
7
+ const { app, close } = await (0, server_1.buildServer)(config);
8
+ try {
9
+ await app.listen({ port: config.port, host: '0.0.0.0' });
10
+ console.log(`[gateway] Oculi Security Gateway listening on :${config.port}`);
11
+ console.log(`[gateway] OPA: ${config.opaEnabled ? config.opaUrl : 'disabled (built-in policy)'}`);
12
+ console.log(`[gateway] Upstreams: ${config.upstreams.map((u) => u.id).join(', ')}`);
13
+ console.log(`[gateway] Admin UI: http://localhost:${config.port}/admin`);
14
+ }
15
+ catch (err) {
16
+ app.log.error(err);
17
+ process.exit(1);
18
+ }
19
+ // Graceful shutdown
20
+ for (const signal of ['SIGINT', 'SIGTERM']) {
21
+ process.on(signal, async () => {
22
+ console.log(`\n[gateway] Received ${signal}, shutting down...`);
23
+ await close();
24
+ process.exit(0);
25
+ });
26
+ }
27
+ }
28
+ main().catch((err) => {
29
+ if (err instanceof config_1.ConfigValidationError) {
30
+ console.error(err.message);
31
+ process.exit(1);
32
+ }
33
+ console.error('[gateway] Fatal error:', err);
34
+ process.exit(1);
35
+ });
package/dist/init.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface InitOpts {
2
+ /** Override the home directory (tests only). Production callers should omit this. */
3
+ home?: string;
4
+ }
5
+ export interface InitResult {
6
+ path: string;
7
+ created: boolean;
8
+ }
9
+ export declare function runInit(opts?: InitOpts): InitResult;
package/dist/init.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runInit = runInit;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const local_policy_templates_1 = require("./services/local-policy-templates");
41
+ function runInit(opts = {}) {
42
+ const base = opts.home ?? os.homedir();
43
+ const policyPath = path.join(base, '.oculi', 'policy.yaml');
44
+ if (fs.existsSync(policyPath)) {
45
+ return { path: policyPath, created: false };
46
+ }
47
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true });
48
+ fs.writeFileSync(policyPath, local_policy_templates_1.templates.standard, 'utf8');
49
+ return { path: policyPath, created: true };
50
+ }
@@ -0,0 +1,13 @@
1
+ export interface InstallOpts {
2
+ /** Override the home directory (tests only). Production callers should omit this. */
3
+ home?: string;
4
+ }
5
+ export interface InstallResult {
6
+ path: string;
7
+ added: string[];
8
+ removed: string[];
9
+ }
10
+ export declare function installClaudeCode(opts?: InstallOpts): InstallResult;
11
+ export declare function uninstallClaudeCode(opts?: InstallOpts): InstallResult;
12
+ /** Does ~/.claude/settings.json still have any oculi-marked hook? */
13
+ export declare function hasOculiHooks(home?: string): boolean;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installClaudeCode = installClaudeCode;
37
+ exports.uninstallClaudeCode = uninstallClaudeCode;
38
+ exports.hasOculiHooks = hasOculiHooks;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const os = __importStar(require("os"));
42
+ /** Claude Code hook events managed by Oculi. */
43
+ const OCULI_EVENTS = ['PreToolUse', 'PostToolUse', 'Stop'];
44
+ const OCULI_MARKER = 'oculi emit claude-code';
45
+ function resolveConfigPath(opts) {
46
+ return path.join(opts.home ?? os.homedir(), '.claude', 'settings.json');
47
+ }
48
+ function readSettings(filePath) {
49
+ if (!fs.existsSync(filePath)) {
50
+ return {};
51
+ }
52
+ const raw = fs.readFileSync(filePath, 'utf8');
53
+ return JSON.parse(raw);
54
+ }
55
+ function groupHasOculi(group) {
56
+ return group.hooks.some((h) => h.command?.includes(OCULI_MARKER));
57
+ }
58
+ function installClaudeCode(opts = {}) {
59
+ const configPath = resolveConfigPath(opts);
60
+ const settings = readSettings(configPath);
61
+ const hooksMap = settings.hooks ?? {};
62
+ const added = [];
63
+ for (const event of OCULI_EVENTS) {
64
+ const groups = hooksMap[event] ?? [];
65
+ const alreadyInstalled = groups.some(groupHasOculi);
66
+ if (!alreadyInstalled) {
67
+ hooksMap[event] = [
68
+ ...groups,
69
+ {
70
+ matcher: '',
71
+ hooks: [{ type: 'command', command: `oculi emit claude-code ${event}` }],
72
+ },
73
+ ];
74
+ added.push(event);
75
+ }
76
+ }
77
+ if (added.length > 0) {
78
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
79
+ const updated = { ...settings, hooks: hooksMap };
80
+ fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
81
+ }
82
+ return { path: configPath, added, removed: [] };
83
+ }
84
+ function uninstallClaudeCode(opts = {}) {
85
+ const configPath = resolveConfigPath(opts);
86
+ const settings = readSettings(configPath);
87
+ const hooksMap = settings.hooks ?? {};
88
+ const removed = [];
89
+ for (const event of Object.keys(hooksMap)) {
90
+ const before = hooksMap[event];
91
+ const after = before.filter((g) => !groupHasOculi(g));
92
+ if (after.length < before.length) {
93
+ removed.push(...before.filter(groupHasOculi).map(() => event));
94
+ if (after.length === 0) {
95
+ delete hooksMap[event];
96
+ }
97
+ else {
98
+ hooksMap[event] = after;
99
+ }
100
+ }
101
+ }
102
+ if (removed.length > 0) {
103
+ const updated = { ...settings, hooks: hooksMap };
104
+ fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
105
+ }
106
+ return { path: configPath, added: [], removed };
107
+ }
108
+ /** Does ~/.claude/settings.json still have any oculi-marked hook? */
109
+ function hasOculiHooks(home) {
110
+ const configPath = resolveConfigPath({ home });
111
+ const settings = readSettings(configPath);
112
+ const hooksMap = settings.hooks ?? {};
113
+ for (const groups of Object.values(hooksMap)) {
114
+ if (groups.some(groupHasOculi))
115
+ return true;
116
+ }
117
+ return false;
118
+ }