@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,135 @@
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 os = __importStar(require("os"));
38
+ const detect_1 = require("../install/detect");
39
+ const claude_code_1 = require("../install/claude-code");
40
+ const cursor_1 = require("../install/cursor");
41
+ const init_1 = require("../init");
42
+ const AGENT_LABEL = {
43
+ 'claude-code': { configPath: '~/.claude/settings.json' },
44
+ cursor: { configPath: '~/.cursor/hooks.json' },
45
+ };
46
+ /** Render an absolute path with the user's home dir collapsed to ~. */
47
+ function tilde(absPath, home) {
48
+ return absPath.startsWith(home) ? '~' + absPath.slice(home.length) : absPath;
49
+ }
50
+ function runInit(opts = {}) {
51
+ const home = opts.home ?? os.homedir();
52
+ // 1. Resolve target agent list.
53
+ let targets;
54
+ let explicit = false;
55
+ if (opts.agent) {
56
+ explicit = true;
57
+ if (!(0, detect_1.isAgentInstalled)(opts.agent)) {
58
+ const bin = detect_1.BINARIES[opts.agent];
59
+ process.stderr.write(`oculi: '${opts.agent}' not found on PATH (looked for '${bin}').\n` +
60
+ `If you have ${opts.agent === 'claude-code' ? 'Claude Code' : 'Cursor'} installed, make sure \`${bin}\` is on your PATH.\n` +
61
+ `Otherwise run \`oculi init\` (no --agent) to write the policy and skip hook wiring.\n`);
62
+ return { exitCode: 1, policyPath: null, policyCreated: false, wired: [], added: {} };
63
+ }
64
+ targets = [opts.agent];
65
+ }
66
+ else {
67
+ targets = (0, detect_1.detectAgents)();
68
+ }
69
+ // 2. Write ~/.oculi/policy.yaml if missing (always — even when no agents are detected).
70
+ const policy = (0, init_1.runInit)({ home });
71
+ // 3. No agents detected → warn, don't wire, exit 0.
72
+ if (targets.length === 0) {
73
+ process.stdout.write((policy.created
74
+ ? `Created ${tilde(policy.path, home)}\n`
75
+ : `policy already exists, leaving alone (${tilde(policy.path, home)})\n`) +
76
+ `\n` +
77
+ `No supported agents detected on PATH.\n` +
78
+ ` - For Claude Code: install it from https://claude.com/claude-code\n` +
79
+ ` and ensure \`claude\` is on your PATH.\n` +
80
+ ` - For Cursor: in Cursor, run "Shell Command: Install 'cursor' command\n` +
81
+ ` in PATH" from the command palette.\n` +
82
+ `\n` +
83
+ `Once an agent is on PATH, re-run \`oculi init\` to wire hooks.\n`);
84
+ return {
85
+ exitCode: 0,
86
+ policyPath: policy.path,
87
+ policyCreated: policy.created,
88
+ wired: [],
89
+ added: {},
90
+ };
91
+ }
92
+ // 4. Wire each target agent's hooks (installers are idempotent).
93
+ const added = {};
94
+ for (const a of targets) {
95
+ const r = a === 'claude-code' ? (0, claude_code_1.installClaudeCode)({ home }) : (0, cursor_1.installCursor)({ home });
96
+ added[a] = r.added;
97
+ }
98
+ // 5. Print stdout summary.
99
+ let out = '';
100
+ if (!explicit) {
101
+ out += `Detected agents: ${targets.join(', ')}\n`;
102
+ }
103
+ out += policy.created
104
+ ? `Created ${tilde(policy.path, home)}\n`
105
+ : `policy already exists, leaving alone (${tilde(policy.path, home)})\n`;
106
+ for (const a of targets) {
107
+ const events = added[a] ?? [];
108
+ const cfg = AGENT_LABEL[a].configPath;
109
+ if (events.length === 0) {
110
+ out += `Wired ${a} hooks → ${cfg} (already up to date)\n`;
111
+ }
112
+ else {
113
+ out += `Wired ${a} hooks → ${cfg}\n`;
114
+ for (const e of events) {
115
+ out += ` + ${e}\n`;
116
+ }
117
+ }
118
+ }
119
+ out += `\nRestart your IDE for the hooks to take effect.\n`;
120
+ out += `Then run \`oculi serve\` to view events in the dashboard.\n`;
121
+ out += `\n`;
122
+ out += `To remove later, run BOTH (in order):\n`;
123
+ out += ` oculi uninstall # removes hooks + policy\n`;
124
+ out += ` npm uninstall -g @oculisecurity/cli # removes the binary\n`;
125
+ out += `(npm uninstall alone won't clean the hooks — npm has no way to know\n`;
126
+ out += ` about files we wrote outside our own package directory.)\n`;
127
+ process.stdout.write(out);
128
+ return {
129
+ exitCode: 0,
130
+ policyPath: policy.path,
131
+ policyCreated: policy.created,
132
+ wired: targets,
133
+ added,
134
+ };
135
+ }
@@ -0,0 +1,33 @@
1
+ import { TelemetryLogEntry } from '../services/telemetry-log';
2
+ export interface ReportData {
3
+ period: {
4
+ from: string;
5
+ to: string;
6
+ hours: number;
7
+ };
8
+ total: number;
9
+ by_phase: Record<string, number>;
10
+ by_tool: Array<{
11
+ tool: string;
12
+ count: number;
13
+ }>;
14
+ by_ide: Array<{
15
+ ide: string;
16
+ count: number;
17
+ percent: number;
18
+ }>;
19
+ violations: Array<{
20
+ timestamp: string;
21
+ decision: string;
22
+ tool: string;
23
+ detail: string;
24
+ rule_ids: string[];
25
+ }>;
26
+ }
27
+ export declare function buildReport(entries: TelemetryLogEntry[], hours: number): ReportData;
28
+ export declare function formatReport(report: ReportData): string;
29
+ export interface ReportOptions {
30
+ json: boolean;
31
+ hours: number;
32
+ }
33
+ export declare function runReport(opts: ReportOptions): void;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildReport = buildReport;
4
+ exports.formatReport = formatReport;
5
+ exports.runReport = runReport;
6
+ const telemetry_log_1 = require("../services/telemetry-log");
7
+ // ---------------------------------------------------------------------------
8
+ // Aggregation
9
+ // ---------------------------------------------------------------------------
10
+ function buildReport(entries, hours) {
11
+ const now = new Date();
12
+ const since = new Date(now.getTime() - hours * 3600_000);
13
+ const byPhase = {};
14
+ const toolCounts = {};
15
+ const ideCounts = {};
16
+ const violations = [];
17
+ for (const e of entries) {
18
+ // Phase
19
+ byPhase[e.phase] = (byPhase[e.phase] ?? 0) + 1;
20
+ // Tool
21
+ const tool = e.tool ?? '(none)';
22
+ toolCounts[tool] = (toolCounts[tool] ?? 0) + 1;
23
+ // IDE
24
+ ideCounts[e.ide_source] = (ideCounts[e.ide_source] ?? 0) + 1;
25
+ // Violations
26
+ if (e.policy_decision === 'deny' || e.policy_decision === 'warn') {
27
+ let detail = '—';
28
+ if (e.action?.command)
29
+ detail = e.action.command;
30
+ else if (e.file_path)
31
+ detail = e.file_path;
32
+ else if (e.shell_command)
33
+ detail = e.shell_command;
34
+ else if (e.mcp_server)
35
+ detail = e.mcp_server;
36
+ violations.push({
37
+ timestamp: e.timestamp,
38
+ decision: e.policy_decision,
39
+ tool,
40
+ detail,
41
+ rule_ids: e.policy_rule_ids ?? [],
42
+ });
43
+ }
44
+ }
45
+ const total = entries.length;
46
+ const byTool = Object.entries(toolCounts)
47
+ .sort((a, b) => b[1] - a[1])
48
+ .map(([tool, count]) => ({ tool, count }));
49
+ const byIde = Object.entries(ideCounts)
50
+ .sort((a, b) => b[1] - a[1])
51
+ .map(([ide, count]) => ({
52
+ ide,
53
+ count,
54
+ percent: total > 0 ? Math.round((count / total) * 100) : 0,
55
+ }));
56
+ return {
57
+ period: { from: since.toISOString(), to: now.toISOString(), hours },
58
+ total,
59
+ by_phase: byPhase,
60
+ by_tool: byTool,
61
+ by_ide: byIde,
62
+ violations,
63
+ };
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Human-readable formatting
67
+ // ---------------------------------------------------------------------------
68
+ function bar(count, max, width = 20) {
69
+ if (max === 0)
70
+ return '';
71
+ const filled = Math.round((count / max) * width);
72
+ return '█'.repeat(filled);
73
+ }
74
+ function padRight(s, len) {
75
+ return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
76
+ }
77
+ function formatTimestamp(iso) {
78
+ const d = new Date(iso);
79
+ return d.toLocaleString('sv-SE', { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }).replace('T', ' ');
80
+ }
81
+ function formatReport(report) {
82
+ const lines = [];
83
+ const from = formatTimestamp(report.period.from);
84
+ const to = formatTimestamp(report.period.to);
85
+ lines.push(`Oculi Report — last ${report.period.hours} hours (${from} → ${to})`);
86
+ lines.push('');
87
+ // Event counts by phase
88
+ const phaseStr = Object.entries(report.by_phase)
89
+ .map(([phase, count]) => `${count} ${phase}`)
90
+ .join(', ');
91
+ lines.push(`Events: ${report.total} total${phaseStr ? ` (${phaseStr})` : ''}`);
92
+ lines.push('');
93
+ // By tool
94
+ if (report.by_tool.length > 0) {
95
+ lines.push('By tool:');
96
+ const maxCount = report.by_tool[0]?.count ?? 0;
97
+ for (const { tool, count } of report.by_tool) {
98
+ lines.push(` ${padRight(tool, 14)} ${String(count).padStart(4)} ${bar(count, maxCount)}`);
99
+ }
100
+ lines.push('');
101
+ }
102
+ // By IDE
103
+ if (report.by_ide.length > 0) {
104
+ lines.push('By IDE:');
105
+ for (const { ide, count, percent } of report.by_ide) {
106
+ lines.push(` ${padRight(ide, 14)} ${String(count).padStart(4)} (${percent}%)`);
107
+ }
108
+ lines.push('');
109
+ }
110
+ // Violations
111
+ if (report.violations.length > 0) {
112
+ lines.push(`Policy violations (${report.violations.length}):`);
113
+ for (const v of report.violations) {
114
+ const ts = formatTimestamp(v.timestamp);
115
+ const ruleStr = v.rule_ids.length > 0 ? v.rule_ids.join(', ') : '—';
116
+ lines.push(` ${ts} ${padRight(v.decision, 5)} ${padRight(v.tool, 12)} ${padRight(v.detail, 25)} ${ruleStr}`);
117
+ }
118
+ }
119
+ else {
120
+ lines.push('Policy violations: none');
121
+ }
122
+ lines.push('');
123
+ return lines.join('\n');
124
+ }
125
+ function runReport(opts) {
126
+ const logPath = (0, telemetry_log_1.findTelemetryLog)();
127
+ if (!logPath) {
128
+ if (opts.json) {
129
+ process.stdout.write(JSON.stringify({ error: 'no telemetry log found' }) + '\n');
130
+ }
131
+ else {
132
+ process.stderr.write('oculi: no telemetry log found. Run some commands first.\n');
133
+ }
134
+ process.exit(1);
135
+ }
136
+ const since = new Date(Date.now() - opts.hours * 3600_000);
137
+ const entries = (0, telemetry_log_1.readTelemetryLines)(logPath, { since });
138
+ const report = buildReport(entries, opts.hours);
139
+ if (opts.json) {
140
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
141
+ }
142
+ else {
143
+ process.stdout.write(formatReport(report));
144
+ }
145
+ }
@@ -0,0 +1,27 @@
1
+ import { AppConfig } from '../config';
2
+ export interface ServeOpts {
3
+ port: number;
4
+ bind: string;
5
+ dataDir: string;
6
+ /** When undefined, no-auth mode (synthetic admin actor). */
7
+ auth?: string;
8
+ /** When undefined, OPA is disabled and the built-in fallback policy runs. */
9
+ opaUrl?: string;
10
+ verbose?: boolean;
11
+ }
12
+ /**
13
+ * Build an AppConfig from serve opts + sensible defaults.
14
+ *
15
+ * Zero-config localhost: no auth, no OPA, no rate limit, no upstreams.
16
+ * Non-localhost bind: same defaults except rate limit and auth are required.
17
+ */
18
+ export declare function buildServeConfig(opts: ServeOpts): AppConfig;
19
+ /**
20
+ * Validate serve opts. Throws an Error with a CLI-ready message on misuse.
21
+ * Caller is responsible for printing the message to stderr and exiting.
22
+ */
23
+ export declare function validateServeOpts(opts: ServeOpts): void;
24
+ /**
25
+ * Start the gateway. Foreground only — blocks on app.listen and SIGINT/SIGTERM.
26
+ */
27
+ export declare function runServe(opts: ServeOpts): Promise<void>;
@@ -0,0 +1,163 @@
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.buildServeConfig = buildServeConfig;
37
+ exports.validateServeOpts = validateServeOpts;
38
+ exports.runServe = runServe;
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ const server_1 = require("../server");
42
+ const data_dir_1 = require("../services/data-dir");
43
+ const telemetry_log_1 = require("../services/telemetry-log");
44
+ const LOCALHOST = new Set(['127.0.0.1', 'localhost', '::1']);
45
+ function isLocalhost(addr) {
46
+ return LOCALHOST.has(addr);
47
+ }
48
+ /**
49
+ * Build an AppConfig from serve opts + sensible defaults.
50
+ *
51
+ * Zero-config localhost: no auth, no OPA, no rate limit, no upstreams.
52
+ * Non-localhost bind: same defaults except rate limit and auth are required.
53
+ */
54
+ function buildServeConfig(opts) {
55
+ const local = isLocalhost(opts.bind);
56
+ return {
57
+ port: opts.port,
58
+ jwtSecret: opts.auth ?? '',
59
+ opaUrl: opts.opaUrl ?? 'http://localhost:8181',
60
+ opaEnabled: !!opts.opaUrl,
61
+ dbPath: path.join(opts.dataDir, 'oculi.db'),
62
+ storeFullArgs: true,
63
+ upstreams: [],
64
+ rateLimitCapacity: local ? 0 : 10,
65
+ rateLimitRefillPerSecond: local ? 0 : 2,
66
+ logLevel: opts.verbose ? 'info' : 'warn',
67
+ };
68
+ }
69
+ /**
70
+ * Validate serve opts. Throws an Error with a CLI-ready message on misuse.
71
+ * Caller is responsible for printing the message to stderr and exiting.
72
+ */
73
+ function validateServeOpts(opts) {
74
+ const local = isLocalhost(opts.bind);
75
+ const envAuth = process.env.OCULI_GATEWAY_TOKEN;
76
+ const effectiveAuth = opts.auth ?? envAuth;
77
+ if (!local) {
78
+ if (!effectiveAuth) {
79
+ throw new Error(`Binding to ${opts.bind} requires authentication.\n` +
80
+ 'Set --auth <secret> (≥32 chars) or the OCULI_GATEWAY_TOKEN env var.\n' +
81
+ 'Refusing to start an unauthenticated gateway on a non-localhost interface.');
82
+ }
83
+ if (effectiveAuth.length < 32) {
84
+ throw new Error('--auth secret must be at least 32 characters of high-entropy data.\n' +
85
+ 'Generate one with: openssl rand -hex 32');
86
+ }
87
+ }
88
+ // If the user supplied --auth on localhost, honor it but don't require length —
89
+ // we treat it as advanced usage.
90
+ }
91
+ /**
92
+ * Start the gateway. Foreground only — blocks on app.listen and SIGINT/SIGTERM.
93
+ */
94
+ async function runServe(opts) {
95
+ try {
96
+ validateServeOpts(opts);
97
+ }
98
+ catch (err) {
99
+ process.stderr.write(`oculi: ${err.message}\n`);
100
+ process.exit(1);
101
+ }
102
+ (0, data_dir_1.ensureDataDir)(opts.dataDir);
103
+ // OCULI_GATEWAY_TOKEN env var is an alternative way to provide --auth.
104
+ // Merge here so the rest of the code doesn't have to know about the env var.
105
+ const auth = opts.auth ?? process.env.OCULI_GATEWAY_TOKEN;
106
+ const config = buildServeConfig({ ...opts, auth });
107
+ const { app, audit, close } = await (0, server_1.buildServer)(config);
108
+ // Backfill the dashboard's sqlite from the CLI's telemetry.jsonl. Without
109
+ // this, events generated while no gateway was running are invisible at
110
+ // /admin even though `oculi tail` shows them. Checkpointed so re-runs of
111
+ // serve are no-ops.
112
+ const telemetryPath = (0, telemetry_log_1.findTelemetryLog)();
113
+ if (telemetryPath) {
114
+ try {
115
+ const r = audit.replayFromTelemetry(telemetryPath);
116
+ if (r.inserted > 0 || r.rotated) {
117
+ const tildePath = telemetryPath.startsWith(os.homedir())
118
+ ? '~' + telemetryPath.slice(os.homedir().length)
119
+ : telemetryPath;
120
+ const suffix = r.skipped > 0 ? ` (${r.skipped} malformed lines skipped)` : '';
121
+ const rotateNote = r.rotated ? ' [file rotated — replayed from start]' : '';
122
+ process.stdout.write(`Replayed ${r.inserted} events from ${tildePath}${rotateNote}${suffix}\n`);
123
+ }
124
+ }
125
+ catch (err) {
126
+ process.stderr.write(`oculi: telemetry replay failed (${err.message}) — continuing\n`);
127
+ }
128
+ }
129
+ try {
130
+ // listenTextResolver suppresses Fastify's default "Server listening at …"
131
+ // info-level log so our own banner is the only thing the user sees.
132
+ await app.listen({
133
+ port: config.port,
134
+ host: opts.bind,
135
+ listenTextResolver: () => '',
136
+ });
137
+ }
138
+ catch (err) {
139
+ const e = err;
140
+ if (e.code === 'EADDRINUSE') {
141
+ process.stderr.write(`oculi: port ${config.port} is already in use.\n` +
142
+ `Try \`oculi serve --port <other-port>\` or stop the process bound to ${config.port}.\n`);
143
+ await close();
144
+ process.exit(2);
145
+ }
146
+ process.stderr.write(`oculi: failed to start gateway: ${e.message}\n`);
147
+ await close();
148
+ process.exit(1);
149
+ }
150
+ const url = `http://${opts.bind === '0.0.0.0' ? 'localhost' : opts.bind}:${config.port}`;
151
+ process.stdout.write(`Oculi gateway listening on ${opts.bind}:${config.port}\n`);
152
+ process.stdout.write(` Dashboard: ${url}/admin/\n`);
153
+ process.stdout.write(` Data dir: ${opts.dataDir}\n`);
154
+ process.stdout.write(` Auth: ${auth ? 'enabled (JWT)' : 'disabled (local-dev)'}\n`);
155
+ process.stdout.write(` Rate limit: ${config.rateLimitCapacity === 0 ? 'off' : `${config.rateLimitCapacity} cap / ${config.rateLimitRefillPerSecond}/s`}\n`);
156
+ for (const signal of ['SIGINT', 'SIGTERM']) {
157
+ process.on(signal, async () => {
158
+ process.stdout.write(`\noculi: received ${signal}, shutting down...\n`);
159
+ await close();
160
+ process.exit(0);
161
+ });
162
+ }
163
+ }
@@ -0,0 +1,7 @@
1
+ import { TelemetryLogEntry } from '../services/telemetry-log';
2
+ export declare function formatEntry(entry: TelemetryLogEntry, colorEnabled: boolean): string;
3
+ export interface TailOptions {
4
+ filter?: 'allow' | 'warn' | 'deny';
5
+ noColor?: boolean;
6
+ }
7
+ export declare function runTail(opts: TailOptions): void;