codex-blocker 0.0.3

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.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # codex-blocker
2
+
3
+ CLI tool and server for Codex Blocker — block distracting websites unless Codex is actively running.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g codex-blocker
9
+ # or
10
+ npx codex-blocker
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Optional setup info
17
+ npx codex-blocker --setup
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ # Start server (default port 8765)
24
+ npx codex-blocker
25
+
26
+ # Show setup info
27
+ npx codex-blocker --setup
28
+
29
+ # Custom port
30
+ npx codex-blocker --port 9000
31
+
32
+ # Remove setup (no-op)
33
+ npx codex-blocker --remove
34
+
35
+ # Show help
36
+ npx codex-blocker --help
37
+ ```
38
+
39
+ ## How It Works
40
+
41
+ 1. **Codex sessions** — The server tails Codex session logs under `~/.codex/sessions`
42
+ to detect activity.
43
+
44
+ 2. **Server** — Runs on localhost and:
45
+ - Tracks active Codex sessions
46
+ - Marks sessions "working" when new log lines arrive
47
+ - Broadcasts state via WebSocket to the Chrome extension
48
+
49
+ 3. **Extension** — Connects to the server and:
50
+ - Blocks configured sites when no sessions are working
51
+ - Shows a modal overlay (soft block, not network block)
52
+ - Updates in real-time without page refresh
53
+
54
+ ## API
55
+
56
+ ### HTTP Endpoints
57
+
58
+ | Endpoint | Method | Description |
59
+ |----------|--------|-------------|
60
+ | `/status` | GET | Returns current state (sessions, blocked status) |
61
+
62
+ ### WebSocket
63
+
64
+ Connect to `ws://localhost:8765/ws` to receive real-time state updates:
65
+
66
+ ```json
67
+ {
68
+ "type": "state",
69
+ "blocked": true,
70
+ "sessions": 1,
71
+ "working": 0
72
+ }
73
+ ```
74
+
75
+ ## Programmatic Usage
76
+
77
+ ```typescript
78
+ import { startServer } from 'codex-blocker';
79
+
80
+ // Start on default port (8765)
81
+ startServer();
82
+
83
+ // Or custom port
84
+ startServer(9000);
85
+ ```
86
+
87
+ ## Requirements
88
+
89
+ - Node.js 18+
90
+ - Codex CLI
91
+
92
+ ## License
93
+
94
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ startServer
4
+ } from "./chunk-7TKQB72O.js";
5
+
6
+ // src/bin.ts
7
+ import { createInterface } from "readline";
8
+
9
+ // src/setup.ts
10
+ import { existsSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var CODEX_HOME = process.env.CODEX_HOME ?? join(homedir(), ".codex");
14
+ var CODEX_SESSIONS_DIR = join(CODEX_HOME, "sessions");
15
+ function setupCodex() {
16
+ console.log(`
17
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
18
+ \u2502 \u2502
19
+ \u2502 Codex Blocker Setup \u2502
20
+ \u2502 \u2502
21
+ \u2502 No hooks needed. The server reads Codex \u2502
22
+ \u2502 session logs from: \u2502
23
+ \u2502 ${CODEX_SESSIONS_DIR}
24
+ \u2502 \u2502
25
+ \u2502 Tip: run Codex once to create the sessions \u2502
26
+ \u2502 directory if it doesn't exist yet. \u2502
27
+ \u2502 \u2502
28
+ \u2502 Next: Run 'npx codex-blocker' to start \u2502
29
+ \u2502 \u2502
30
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
31
+ `);
32
+ }
33
+ function isCodexAvailable() {
34
+ return existsSync(CODEX_SESSIONS_DIR);
35
+ }
36
+ function removeCodexSetup() {
37
+ console.log("No Codex hooks to remove.");
38
+ }
39
+
40
+ // src/bin.ts
41
+ import { DEFAULT_PORT } from "@claude-blocker/shared";
42
+ var args = process.argv.slice(2);
43
+ function prompt(question) {
44
+ const rl = createInterface({
45
+ input: process.stdin,
46
+ output: process.stdout
47
+ });
48
+ return new Promise((resolve) => {
49
+ rl.question(question, (answer) => {
50
+ rl.close();
51
+ resolve(answer);
52
+ });
53
+ });
54
+ }
55
+ function printHelp() {
56
+ console.log(`
57
+ Codex Blocker - Block distracting sites when Codex isn't working
58
+
59
+ Usage:
60
+ npx codex-blocker [options]
61
+
62
+ Options:
63
+ --setup Show Codex setup info
64
+ --remove Remove Codex setup (no-op)
65
+ --port Server port (default: ${DEFAULT_PORT})
66
+ --help Show this help message
67
+
68
+ Examples:
69
+ npx codex-blocker # Start the server
70
+ npx codex-blocker --port 9000
71
+ `);
72
+ }
73
+ async function main() {
74
+ if (args.includes("--help") || args.includes("-h")) {
75
+ printHelp();
76
+ process.exit(0);
77
+ }
78
+ if (args.includes("--setup")) {
79
+ setupCodex();
80
+ process.exit(0);
81
+ }
82
+ if (args.includes("--remove")) {
83
+ removeCodexSetup();
84
+ process.exit(0);
85
+ }
86
+ let port = DEFAULT_PORT;
87
+ const portIndex = args.indexOf("--port");
88
+ if (portIndex !== -1 && args[portIndex + 1]) {
89
+ const parsed = parseInt(args[portIndex + 1], 10);
90
+ if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
91
+ port = parsed;
92
+ } else {
93
+ console.error("Invalid port number");
94
+ process.exit(1);
95
+ }
96
+ }
97
+ if (!isCodexAvailable()) {
98
+ console.log("Codex sessions directory not found yet.");
99
+ const answer = await prompt("Run Codex once to create it, then press enter to continue. ");
100
+ if (answer !== void 0) {
101
+ console.log("");
102
+ }
103
+ }
104
+ startServer(port);
105
+ }
106
+ main();
@@ -0,0 +1,509 @@
1
+ // src/server.ts
2
+ import { createServer } from "http";
3
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir as homedir2 } from "os";
5
+ import { join as join2 } from "path";
6
+ import { WebSocketServer, WebSocket } from "ws";
7
+
8
+ // src/types.ts
9
+ var DEFAULT_PORT = 8765;
10
+ var SESSION_TIMEOUT_MS = 5 * 60 * 1e3;
11
+ var CODEX_ACTIVITY_IDLE_TIMEOUT_MS = 60 * 1e3;
12
+ var CODEX_SESSIONS_SCAN_INTERVAL_MS = 2e3;
13
+
14
+ // src/state.ts
15
+ var SessionState = class {
16
+ sessions = /* @__PURE__ */ new Map();
17
+ listeners = /* @__PURE__ */ new Set();
18
+ cleanupInterval = null;
19
+ constructor() {
20
+ this.cleanupInterval = setInterval(() => {
21
+ this.cleanupStaleSessions();
22
+ }, 3e4);
23
+ }
24
+ subscribe(callback) {
25
+ this.listeners.add(callback);
26
+ callback(this.getStateMessage());
27
+ return () => this.listeners.delete(callback);
28
+ }
29
+ broadcast() {
30
+ const message = this.getStateMessage();
31
+ for (const listener of this.listeners) {
32
+ listener(message);
33
+ }
34
+ }
35
+ getStateMessage() {
36
+ const sessions = Array.from(this.sessions.values());
37
+ const working = sessions.filter((s) => s.status === "working").length;
38
+ const waitingForInput = sessions.filter(
39
+ (s) => s.status === "waiting_for_input"
40
+ ).length;
41
+ return {
42
+ type: "state",
43
+ blocked: working === 0,
44
+ sessions: sessions.length,
45
+ working,
46
+ waitingForInput
47
+ };
48
+ }
49
+ handleCodexActivity(activity) {
50
+ this.ensureSession(activity.sessionId, activity.cwd);
51
+ const session = this.sessions.get(activity.sessionId);
52
+ session.status = "working";
53
+ session.waitingForInputSince = void 0;
54
+ session.lastActivity = /* @__PURE__ */ new Date();
55
+ session.lastSeen = /* @__PURE__ */ new Date();
56
+ session.idleTimeoutMs = activity.idleTimeoutMs;
57
+ this.broadcast();
58
+ }
59
+ setCodexIdle(sessionId, cwd) {
60
+ this.ensureSession(sessionId, cwd);
61
+ const session = this.sessions.get(sessionId);
62
+ if (session.status !== "idle") {
63
+ session.status = "idle";
64
+ session.waitingForInputSince = void 0;
65
+ session.lastActivity = /* @__PURE__ */ new Date();
66
+ this.broadcast();
67
+ }
68
+ }
69
+ markCodexSessionSeen(sessionId, cwd) {
70
+ const created = this.ensureSession(sessionId, cwd);
71
+ const session = this.sessions.get(sessionId);
72
+ session.lastSeen = /* @__PURE__ */ new Date();
73
+ if (created) {
74
+ this.broadcast();
75
+ }
76
+ }
77
+ removeSession(sessionId) {
78
+ if (this.sessions.delete(sessionId)) {
79
+ this.broadcast();
80
+ }
81
+ }
82
+ ensureSession(sessionId, cwd) {
83
+ if (!this.sessions.has(sessionId)) {
84
+ this.sessions.set(sessionId, {
85
+ id: sessionId,
86
+ status: "idle",
87
+ lastActivity: /* @__PURE__ */ new Date(),
88
+ lastSeen: /* @__PURE__ */ new Date(),
89
+ cwd
90
+ });
91
+ console.log("Codex session connected");
92
+ return true;
93
+ } else if (cwd) {
94
+ const session = this.sessions.get(sessionId);
95
+ session.cwd = cwd;
96
+ }
97
+ return false;
98
+ }
99
+ cleanupStaleSessions() {
100
+ const now = Date.now();
101
+ let removed = 0;
102
+ let changed = 0;
103
+ for (const [id, session] of this.sessions) {
104
+ if (now - session.lastSeen.getTime() > SESSION_TIMEOUT_MS) {
105
+ this.sessions.delete(id);
106
+ removed++;
107
+ continue;
108
+ }
109
+ if (session.status === "working" && session.idleTimeoutMs && now - session.lastActivity.getTime() > session.idleTimeoutMs) {
110
+ session.status = "idle";
111
+ session.waitingForInputSince = void 0;
112
+ changed++;
113
+ }
114
+ }
115
+ if (removed > 0 || changed > 0) {
116
+ this.broadcast();
117
+ }
118
+ }
119
+ getStatus() {
120
+ const sessions = Array.from(this.sessions.values());
121
+ const working = sessions.filter((s) => s.status === "working").length;
122
+ const waitingForInput = sessions.filter(
123
+ (s) => s.status === "waiting_for_input"
124
+ ).length;
125
+ return {
126
+ blocked: working === 0,
127
+ sessions: sessions.length,
128
+ working,
129
+ waitingForInput
130
+ };
131
+ }
132
+ destroy() {
133
+ if (this.cleanupInterval) {
134
+ clearInterval(this.cleanupInterval);
135
+ }
136
+ this.sessions.clear();
137
+ this.listeners.clear();
138
+ }
139
+ };
140
+ var state = new SessionState();
141
+
142
+ // src/codex.ts
143
+ import { existsSync, createReadStream, promises as fs } from "fs";
144
+ import { homedir } from "os";
145
+ import { basename, dirname, join } from "path";
146
+ var CODEX_HOME = process.env.CODEX_HOME ?? join(homedir(), ".codex");
147
+ var CODEX_SESSIONS_DIR = join(CODEX_HOME, "sessions");
148
+ function isRolloutFile(filePath) {
149
+ const name = basename(filePath);
150
+ return name === "rollout.jsonl" || /^rollout-.+\.jsonl$/.test(name);
151
+ }
152
+ function sessionIdFromPath(filePath) {
153
+ const name = basename(filePath);
154
+ const match = name.match(/^rollout-(.+)\.jsonl$/);
155
+ if (match) return match[1];
156
+ if (name === "rollout.jsonl") {
157
+ const parent = basename(dirname(filePath));
158
+ if (parent !== "sessions") return parent;
159
+ }
160
+ return filePath;
161
+ }
162
+ function findFirstStringValue(obj, keys, maxDepth = 6) {
163
+ if (!obj || typeof obj !== "object") return void 0;
164
+ const queue = [{ value: obj, depth: 0 }];
165
+ while (queue.length) {
166
+ const current = queue.shift();
167
+ if (!current) break;
168
+ const { value, depth } = current;
169
+ if (!value || typeof value !== "object") continue;
170
+ const record = value;
171
+ for (const key of keys) {
172
+ const candidate = record[key];
173
+ if (typeof candidate === "string" && candidate.length > 0) {
174
+ return candidate;
175
+ }
176
+ }
177
+ if (depth >= maxDepth) continue;
178
+ for (const child of Object.values(record)) {
179
+ if (child && typeof child === "object") {
180
+ queue.push({ value: child, depth: depth + 1 });
181
+ }
182
+ }
183
+ }
184
+ return void 0;
185
+ }
186
+ async function listRolloutFiles(root) {
187
+ const files = [];
188
+ const entries = await fs.readdir(root, { withFileTypes: true });
189
+ for (const entry of entries) {
190
+ const fullPath = join(root, entry.name);
191
+ if (entry.isDirectory()) {
192
+ files.push(...await listRolloutFiles(fullPath));
193
+ } else if (entry.isFile() && isRolloutFile(fullPath)) {
194
+ files.push(fullPath);
195
+ }
196
+ }
197
+ return files;
198
+ }
199
+ async function readNewLines(filePath, fileState) {
200
+ const stat = await fs.stat(filePath);
201
+ if (stat.size < fileState.position) {
202
+ fileState.position = 0;
203
+ fileState.remainder = "";
204
+ }
205
+ if (stat.size === fileState.position) return [];
206
+ const start = fileState.position;
207
+ const end = Math.max(stat.size - 1, start);
208
+ const chunks = [];
209
+ await new Promise((resolve, reject) => {
210
+ const stream = createReadStream(filePath, { start, end });
211
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
212
+ stream.on("error", reject);
213
+ stream.on("end", resolve);
214
+ });
215
+ fileState.position = stat.size;
216
+ const content = fileState.remainder + Buffer.concat(chunks).toString("utf-8");
217
+ const lines = content.split("\n");
218
+ fileState.remainder = content.endsWith("\n") ? "" : lines.pop() ?? "";
219
+ return lines.filter((line) => line.trim().length > 0);
220
+ }
221
+ function handleLine(line, fileState) {
222
+ let sessionId = fileState.sessionId;
223
+ let cwd;
224
+ let shouldMarkWorking = false;
225
+ let shouldMarkIdle = false;
226
+ try {
227
+ const payload = JSON.parse(line);
228
+ const entryType = typeof payload.type === "string" ? payload.type : void 0;
229
+ const innerPayload = payload.payload;
230
+ const innerType = innerPayload && typeof innerPayload === "object" ? innerPayload.type : void 0;
231
+ if (entryType === "session_meta") {
232
+ const metaId = innerPayload && typeof innerPayload === "object" ? innerPayload.id : void 0;
233
+ if (typeof metaId === "string" && metaId.length > 0 && metaId !== sessionId) {
234
+ const previousId = sessionId;
235
+ fileState.sessionId = metaId;
236
+ sessionId = metaId;
237
+ state.removeSession(previousId);
238
+ }
239
+ }
240
+ cwd = findFirstStringValue(innerPayload, ["cwd"]) ?? findFirstStringValue(payload, ["cwd"]);
241
+ const innerTypeString = typeof innerType === "string" ? innerType : void 0;
242
+ if (entryType === "event_msg" && innerTypeString === "user_message") {
243
+ shouldMarkWorking = true;
244
+ }
245
+ if (entryType === "event_msg" && (innerTypeString === "agent_message" || innerTypeString === "turn_aborted")) {
246
+ shouldMarkIdle = true;
247
+ }
248
+ if (entryType === "response_item" && innerTypeString === "message") {
249
+ const role = innerPayload && typeof innerPayload === "object" ? innerPayload.role : void 0;
250
+ if (role === "assistant") {
251
+ shouldMarkIdle = true;
252
+ }
253
+ }
254
+ } catch {
255
+ }
256
+ state.markCodexSessionSeen(sessionId, cwd);
257
+ if (shouldMarkWorking) {
258
+ state.handleCodexActivity({
259
+ sessionId,
260
+ cwd,
261
+ idleTimeoutMs: CODEX_ACTIVITY_IDLE_TIMEOUT_MS
262
+ });
263
+ }
264
+ if (shouldMarkIdle) {
265
+ state.setCodexIdle(sessionId, cwd);
266
+ }
267
+ }
268
+ var CodexSessionWatcher = class {
269
+ fileStates = /* @__PURE__ */ new Map();
270
+ scanTimer = null;
271
+ warnedMissing = false;
272
+ start() {
273
+ this.scan();
274
+ this.scanTimer = setInterval(() => {
275
+ this.scan();
276
+ }, CODEX_SESSIONS_SCAN_INTERVAL_MS);
277
+ }
278
+ stop() {
279
+ if (this.scanTimer) {
280
+ clearInterval(this.scanTimer);
281
+ this.scanTimer = null;
282
+ }
283
+ }
284
+ async scan() {
285
+ if (!existsSync(CODEX_SESSIONS_DIR)) {
286
+ if (!this.warnedMissing) {
287
+ console.log(`Waiting for Codex sessions at ${CODEX_SESSIONS_DIR}`);
288
+ this.warnedMissing = true;
289
+ }
290
+ return;
291
+ }
292
+ this.warnedMissing = false;
293
+ let files = [];
294
+ try {
295
+ files = await listRolloutFiles(CODEX_SESSIONS_DIR);
296
+ } catch {
297
+ return;
298
+ }
299
+ for (const filePath of files) {
300
+ const fileState = this.fileStates.get(filePath) ?? {
301
+ position: 0,
302
+ remainder: "",
303
+ sessionId: sessionIdFromPath(filePath)
304
+ };
305
+ if (!this.fileStates.has(filePath)) {
306
+ this.fileStates.set(filePath, fileState);
307
+ try {
308
+ const stat = await fs.stat(filePath);
309
+ fileState.position = stat.size;
310
+ } catch {
311
+ continue;
312
+ }
313
+ continue;
314
+ }
315
+ let newLines = [];
316
+ try {
317
+ newLines = await readNewLines(filePath, fileState);
318
+ } catch {
319
+ continue;
320
+ }
321
+ if (newLines.length === 0) continue;
322
+ for (const line of newLines) {
323
+ handleLine(line, fileState);
324
+ }
325
+ }
326
+ }
327
+ };
328
+
329
+ // src/server.ts
330
+ var TOKEN_DIR = join2(homedir2(), ".codex-blocker");
331
+ var TOKEN_PATH = join2(TOKEN_DIR, "token");
332
+ var RATE_WINDOW_MS = 6e4;
333
+ var RATE_LIMIT = 60;
334
+ var MAX_WS_CONNECTIONS_PER_IP = 3;
335
+ var rateByIp = /* @__PURE__ */ new Map();
336
+ var wsConnectionsByIp = /* @__PURE__ */ new Map();
337
+ function loadToken() {
338
+ if (!existsSync2(TOKEN_PATH)) return null;
339
+ try {
340
+ return readFileSync(TOKEN_PATH, "utf-8").trim() || null;
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+ function saveToken(token) {
346
+ if (!existsSync2(TOKEN_DIR)) {
347
+ mkdirSync(TOKEN_DIR, { recursive: true });
348
+ }
349
+ writeFileSync(TOKEN_PATH, token, "utf-8");
350
+ }
351
+ function isChromeExtensionOrigin(origin) {
352
+ return Boolean(origin && origin.startsWith("chrome-extension://"));
353
+ }
354
+ function getClientIp(req) {
355
+ return req.socket.remoteAddress ?? "unknown";
356
+ }
357
+ function checkRateLimit(ip) {
358
+ const now = Date.now();
359
+ const state2 = rateByIp.get(ip);
360
+ if (!state2 || state2.resetAt <= now) {
361
+ rateByIp.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
362
+ return true;
363
+ }
364
+ if (state2.count >= RATE_LIMIT) return false;
365
+ state2.count += 1;
366
+ return true;
367
+ }
368
+ function readAuthToken(req, url) {
369
+ const header = req.headers.authorization;
370
+ if (header && header.startsWith("Bearer ")) {
371
+ return header.slice("Bearer ".length).trim();
372
+ }
373
+ const query = url.searchParams.get("token");
374
+ if (query) return query;
375
+ const alt = req.headers["x-codex-blocker-token"];
376
+ if (typeof alt === "string" && alt.length > 0) return alt;
377
+ return null;
378
+ }
379
+ function sendJson(res, data, status = 200) {
380
+ res.writeHead(status, { "Content-Type": "application/json" });
381
+ res.end(JSON.stringify(data));
382
+ }
383
+ function startServer(port = DEFAULT_PORT) {
384
+ let authToken = loadToken();
385
+ const server = createServer(async (req, res) => {
386
+ const clientIp = getClientIp(req);
387
+ if (!checkRateLimit(clientIp)) {
388
+ sendJson(res, { error: "Too Many Requests" }, 429);
389
+ return;
390
+ }
391
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
392
+ const origin = req.headers.origin;
393
+ const allowOrigin = isChromeExtensionOrigin(origin);
394
+ if (allowOrigin && origin) {
395
+ res.setHeader("Access-Control-Allow-Origin", origin);
396
+ res.setHeader("Vary", "Origin");
397
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
398
+ res.setHeader(
399
+ "Access-Control-Allow-Headers",
400
+ "Content-Type, Authorization, X-Codex-Blocker-Token"
401
+ );
402
+ }
403
+ if (req.method === "OPTIONS") {
404
+ res.writeHead(allowOrigin ? 204 : 403);
405
+ res.end();
406
+ return;
407
+ }
408
+ const providedToken = readAuthToken(req, url);
409
+ if (authToken) {
410
+ if (!providedToken || providedToken !== authToken) {
411
+ sendJson(res, { error: "Unauthorized" }, 401);
412
+ return;
413
+ }
414
+ } else if (providedToken && allowOrigin) {
415
+ authToken = providedToken;
416
+ saveToken(providedToken);
417
+ } else {
418
+ sendJson(res, { error: "Unauthorized" }, 401);
419
+ return;
420
+ }
421
+ if (req.method === "GET" && url.pathname === "/status") {
422
+ sendJson(res, state.getStatus());
423
+ return;
424
+ }
425
+ sendJson(res, { error: "Not found" }, 404);
426
+ });
427
+ const wss = new WebSocketServer({ server, path: "/ws" });
428
+ wss.on("connection", (ws, req) => {
429
+ const wsUrl = new URL(req.url || "", `http://localhost:${port}`);
430
+ const providedToken = wsUrl.searchParams.get("token");
431
+ const origin = req.headers.origin;
432
+ const allowOrigin = isChromeExtensionOrigin(origin);
433
+ const clientIp = getClientIp(req);
434
+ const currentConnections = wsConnectionsByIp.get(clientIp) ?? 0;
435
+ if (currentConnections >= MAX_WS_CONNECTIONS_PER_IP) {
436
+ ws.close(1013, "Too many connections");
437
+ return;
438
+ }
439
+ if (authToken) {
440
+ if (!providedToken || providedToken !== authToken) {
441
+ ws.close(1008, "Unauthorized");
442
+ return;
443
+ }
444
+ } else if (providedToken && allowOrigin) {
445
+ authToken = providedToken;
446
+ saveToken(providedToken);
447
+ } else {
448
+ ws.close(1008, "Unauthorized");
449
+ return;
450
+ }
451
+ wsConnectionsByIp.set(clientIp, currentConnections + 1);
452
+ const unsubscribe = state.subscribe((message) => {
453
+ if (ws.readyState === WebSocket.OPEN) {
454
+ ws.send(JSON.stringify(message));
455
+ }
456
+ });
457
+ ws.on("message", (data) => {
458
+ try {
459
+ const message = JSON.parse(data.toString());
460
+ if (message.type === "ping") {
461
+ ws.send(JSON.stringify({ type: "pong" }));
462
+ }
463
+ } catch {
464
+ }
465
+ });
466
+ ws.on("close", () => {
467
+ unsubscribe();
468
+ wsConnectionsByIp.set(
469
+ clientIp,
470
+ Math.max(0, (wsConnectionsByIp.get(clientIp) ?? 1) - 1)
471
+ );
472
+ });
473
+ ws.on("error", () => {
474
+ unsubscribe();
475
+ wsConnectionsByIp.set(
476
+ clientIp,
477
+ Math.max(0, (wsConnectionsByIp.get(clientIp) ?? 1) - 1)
478
+ );
479
+ });
480
+ });
481
+ const codexWatcher = new CodexSessionWatcher();
482
+ codexWatcher.start();
483
+ server.listen(port, "127.0.0.1", () => {
484
+ console.log(`
485
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
486
+ \u2502 \u2502
487
+ \u2502 Codex Blocker Server \u2502
488
+ \u2502 \u2502
489
+ \u2502 HTTP: http://localhost:${port} \u2502
490
+ \u2502 WebSocket: ws://localhost:${port}/ws \u2502
491
+ \u2502 \u2502
492
+ \u2502 Watching Codex sessions... \u2502
493
+ \u2502 \u2502
494
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
495
+ `);
496
+ });
497
+ process.once("SIGINT", () => {
498
+ console.log("\nShutting down...");
499
+ state.destroy();
500
+ codexWatcher.stop();
501
+ wss.close();
502
+ server.close();
503
+ process.exit(0);
504
+ });
505
+ }
506
+
507
+ export {
508
+ startServer
509
+ };
@@ -0,0 +1,3 @@
1
+ declare function startServer(port?: number): void;
2
+
3
+ export { startServer };
package/dist/server.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ startServer
3
+ } from "./chunk-7TKQB72O.js";
4
+ export {
5
+ startServer
6
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "codex-blocker",
3
+ "version": "0.0.3",
4
+ "description": "Block distracting websites unless Codex is actively running inference. Forked from Theo Browne's (T3) Claude Blocker",
5
+ "author": "Adam Blumoff ",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/adamblumoff/codex-blocker.git"
9
+ },
10
+ "homepage": "https://github.com/adamblumoff/codex-blocker",
11
+ "bugs": {
12
+ "url": "https://github.com/adamblumoff/codex-blocker/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "codex-blocker": "dist/bin.js"
17
+ },
18
+ "main": "./dist/server.js",
19
+ "types": "./dist/server.d.ts",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup src/bin.ts src/server.ts --format esm --dts --clean",
25
+ "dev": "tsx src/bin.ts",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@claude-blocker/shared": "workspace:*",
30
+ "ws": "^8.18.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.10.2",
34
+ "@types/ws": "^8.5.13",
35
+ "tsup": "^8.3.5",
36
+ "tsx": "^4.19.2",
37
+ "typescript": "^5.7.2"
38
+ },
39
+ "keywords": [
40
+ "codex",
41
+ "codex-cli",
42
+ "productivity",
43
+ "blocker",
44
+ "focus",
45
+ "distraction"
46
+ ],
47
+ "license": "MIT",
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ }
51
+ }