codex-blocker 0.0.4 → 0.0.7

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 CHANGED
@@ -39,7 +39,8 @@ npx codex-blocker --help
39
39
  ## How It Works
40
40
 
41
41
  1. **Codex sessions** — The server tails Codex session logs under `~/.codex/sessions`
42
- to detect activity.
42
+ to detect activity. It marks a session “working” on your prompt and “idle” on the
43
+ final assistant reply (tool calls don’t count as idle).
43
44
 
44
45
  2. **Server** — Runs on localhost and:
45
46
  - Tracks active Codex sessions
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  DEFAULT_PORT,
4
4
  startServer
5
- } from "./chunk-7IRFKJUB.js";
5
+ } from "./chunk-APDAIY47.js";
6
6
 
7
7
  // src/bin.ts
8
8
  import { createInterface } from "readline";
@@ -2,13 +2,12 @@
2
2
  import { createServer } from "http";
3
3
  import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
4
4
  import { homedir as homedir2 } from "os";
5
- import { join as join2 } from "path";
5
+ import { dirname as dirname2, join as join2 } from "path";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
 
8
8
  // src/types.ts
9
9
  var DEFAULT_PORT = 8765;
10
10
  var SESSION_TIMEOUT_MS = 5 * 60 * 1e3;
11
- var CODEX_ACTIVITY_IDLE_TIMEOUT_MS = 60 * 1e3;
12
11
  var CODEX_SESSIONS_SCAN_INTERVAL_MS = 2e3;
13
12
 
14
13
  // src/state.ts
@@ -142,9 +141,10 @@ var state = new SessionState();
142
141
  // src/codex.ts
143
142
  import { existsSync, createReadStream, promises as fs } from "fs";
144
143
  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");
144
+ import { join } from "path";
145
+
146
+ // src/codex-parse.ts
147
+ import { basename, dirname } from "path";
148
148
  function isRolloutFile(filePath) {
149
149
  const name = basename(filePath);
150
150
  return name === "rollout.jsonl" || /^rollout-.+\.jsonl$/.test(name);
@@ -183,6 +183,45 @@ function findFirstStringValue(obj, keys, maxDepth = 6) {
183
183
  }
184
184
  return void 0;
185
185
  }
186
+ function parseCodexLine(line, sessionId) {
187
+ let currentSessionId = sessionId;
188
+ let previousSessionId;
189
+ let cwd;
190
+ let markWorking = false;
191
+ let markIdle = false;
192
+ try {
193
+ const payload = JSON.parse(line);
194
+ const entryType = typeof payload.type === "string" ? payload.type : void 0;
195
+ const innerPayload = payload.payload;
196
+ const innerType = innerPayload && typeof innerPayload === "object" ? innerPayload.type : void 0;
197
+ if (entryType === "session_meta") {
198
+ const metaId = innerPayload && typeof innerPayload === "object" ? innerPayload.id : void 0;
199
+ if (typeof metaId === "string" && metaId.length > 0 && metaId !== currentSessionId) {
200
+ previousSessionId = currentSessionId;
201
+ currentSessionId = metaId;
202
+ }
203
+ }
204
+ cwd = findFirstStringValue(innerPayload, ["cwd"]) ?? findFirstStringValue(payload, ["cwd"]);
205
+ const innerTypeString = typeof innerType === "string" ? innerType : void 0;
206
+ if (entryType === "event_msg" && innerTypeString === "user_message") {
207
+ markWorking = true;
208
+ }
209
+ if (entryType === "event_msg" && innerTypeString === "agent_message") {
210
+ markIdle = true;
211
+ }
212
+ } catch {
213
+ }
214
+ return {
215
+ sessionId: currentSessionId,
216
+ previousSessionId,
217
+ cwd,
218
+ markWorking,
219
+ markIdle
220
+ };
221
+ }
222
+
223
+ // src/codex.ts
224
+ var DEFAULT_CODEX_HOME = join(homedir(), ".codex");
186
225
  async function listRolloutFiles(root) {
187
226
  const files = [];
188
227
  const entries = await fs.readdir(root, { withFileTypes: true });
@@ -218,57 +257,17 @@ async function readNewLines(filePath, fileState) {
218
257
  fileState.remainder = content.endsWith("\n") ? "" : lines.pop() ?? "";
219
258
  return lines.filter((line) => line.trim().length > 0);
220
259
  }
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
260
  var CodexSessionWatcher = class {
269
261
  fileStates = /* @__PURE__ */ new Map();
270
262
  scanTimer = null;
271
263
  warnedMissing = false;
264
+ sessionsDir;
265
+ state;
266
+ constructor(state2, options) {
267
+ this.state = state2;
268
+ const base = process.env.CODEX_HOME ?? DEFAULT_CODEX_HOME;
269
+ this.sessionsDir = options?.sessionsDir ?? join(base, "sessions");
270
+ }
272
271
  start() {
273
272
  this.scan();
274
273
  this.scanTimer = setInterval(() => {
@@ -282,9 +281,9 @@ var CodexSessionWatcher = class {
282
281
  }
283
282
  }
284
283
  async scan() {
285
- if (!existsSync(CODEX_SESSIONS_DIR)) {
284
+ if (!existsSync(this.sessionsDir)) {
286
285
  if (!this.warnedMissing) {
287
- console.log(`Waiting for Codex sessions at ${CODEX_SESSIONS_DIR}`);
286
+ console.log(`Waiting for Codex sessions at ${this.sessionsDir}`);
288
287
  this.warnedMissing = true;
289
288
  }
290
289
  return;
@@ -292,7 +291,7 @@ var CodexSessionWatcher = class {
292
291
  this.warnedMissing = false;
293
292
  let files = [];
294
293
  try {
295
- files = await listRolloutFiles(CODEX_SESSIONS_DIR);
294
+ files = await listRolloutFiles(this.sessionsDir);
296
295
  } catch {
297
296
  return;
298
297
  }
@@ -320,33 +319,48 @@ var CodexSessionWatcher = class {
320
319
  }
321
320
  if (newLines.length === 0) continue;
322
321
  for (const line of newLines) {
323
- handleLine(line, fileState);
322
+ const parsed = parseCodexLine(line, fileState.sessionId);
323
+ fileState.sessionId = parsed.sessionId;
324
+ if (parsed.previousSessionId) {
325
+ this.state.removeSession(parsed.previousSessionId);
326
+ }
327
+ this.state.markCodexSessionSeen(parsed.sessionId, parsed.cwd);
328
+ if (parsed.markWorking) {
329
+ this.state.handleCodexActivity({
330
+ sessionId: parsed.sessionId,
331
+ cwd: parsed.cwd
332
+ });
333
+ }
334
+ if (parsed.markIdle) {
335
+ this.state.setCodexIdle(parsed.sessionId, parsed.cwd);
336
+ }
324
337
  }
325
338
  }
326
339
  }
327
340
  };
328
341
 
329
342
  // src/server.ts
330
- var TOKEN_DIR = join2(homedir2(), ".codex-blocker");
331
- var TOKEN_PATH = join2(TOKEN_DIR, "token");
343
+ var DEFAULT_TOKEN_DIR = join2(homedir2(), ".codex-blocker");
344
+ var DEFAULT_TOKEN_PATH = join2(DEFAULT_TOKEN_DIR, "token");
332
345
  var RATE_WINDOW_MS = 6e4;
333
346
  var RATE_LIMIT = 60;
334
347
  var MAX_WS_CONNECTIONS_PER_IP = 3;
335
348
  var rateByIp = /* @__PURE__ */ new Map();
336
349
  var wsConnectionsByIp = /* @__PURE__ */ new Map();
337
- function loadToken() {
338
- if (!existsSync2(TOKEN_PATH)) return null;
350
+ function loadToken(tokenPath) {
351
+ if (!existsSync2(tokenPath)) return null;
339
352
  try {
340
- return readFileSync(TOKEN_PATH, "utf-8").trim() || null;
353
+ return readFileSync(tokenPath, "utf-8").trim() || null;
341
354
  } catch {
342
355
  return null;
343
356
  }
344
357
  }
345
- function saveToken(token) {
346
- if (!existsSync2(TOKEN_DIR)) {
347
- mkdirSync(TOKEN_DIR, { recursive: true });
358
+ function saveToken(tokenPath, token) {
359
+ const tokenDir = dirname2(tokenPath);
360
+ if (!existsSync2(tokenDir)) {
361
+ mkdirSync(tokenDir, { recursive: true });
348
362
  }
349
- writeFileSync(TOKEN_PATH, token, "utf-8");
363
+ writeFileSync(tokenPath, token, "utf-8");
350
364
  }
351
365
  function isChromeExtensionOrigin(origin) {
352
366
  return Boolean(origin && origin.startsWith("chrome-extension://"));
@@ -380,8 +394,12 @@ function sendJson(res, data, status = 200) {
380
394
  res.writeHead(status, { "Content-Type": "application/json" });
381
395
  res.end(JSON.stringify(data));
382
396
  }
383
- function startServer(port = DEFAULT_PORT) {
384
- let authToken = loadToken();
397
+ function startServer(port = DEFAULT_PORT, options) {
398
+ const stateInstance = options?.state ?? state;
399
+ const tokenPath = options?.tokenPath ?? DEFAULT_TOKEN_PATH;
400
+ const startWatcher = options?.startWatcher ?? true;
401
+ const logBanner = options?.log ?? true;
402
+ let authToken = loadToken(tokenPath);
385
403
  const server = createServer(async (req, res) => {
386
404
  const clientIp = getClientIp(req);
387
405
  if (!checkRateLimit(clientIp)) {
@@ -413,13 +431,13 @@ function startServer(port = DEFAULT_PORT) {
413
431
  }
414
432
  } else if (providedToken && allowOrigin) {
415
433
  authToken = providedToken;
416
- saveToken(providedToken);
434
+ saveToken(tokenPath, providedToken);
417
435
  } else {
418
436
  sendJson(res, { error: "Unauthorized" }, 401);
419
437
  return;
420
438
  }
421
439
  if (req.method === "GET" && url.pathname === "/status") {
422
- sendJson(res, state.getStatus());
440
+ sendJson(res, stateInstance.getStatus());
423
441
  return;
424
442
  }
425
443
  sendJson(res, { error: "Not found" }, 404);
@@ -443,13 +461,13 @@ function startServer(port = DEFAULT_PORT) {
443
461
  }
444
462
  } else if (providedToken && allowOrigin) {
445
463
  authToken = providedToken;
446
- saveToken(providedToken);
464
+ saveToken(tokenPath, providedToken);
447
465
  } else {
448
466
  ws.close(1008, "Unauthorized");
449
467
  return;
450
468
  }
451
469
  wsConnectionsByIp.set(clientIp, currentConnections + 1);
452
- const unsubscribe = state.subscribe((message) => {
470
+ const unsubscribe = stateInstance.subscribe((message) => {
453
471
  if (ws.readyState === WebSocket.OPEN) {
454
472
  ws.send(JSON.stringify(message));
455
473
  }
@@ -478,16 +496,40 @@ function startServer(port = DEFAULT_PORT) {
478
496
  );
479
497
  });
480
498
  });
481
- const codexWatcher = new CodexSessionWatcher();
482
- codexWatcher.start();
499
+ const codexWatcher = new CodexSessionWatcher(stateInstance, {
500
+ sessionsDir: options?.sessionsDir
501
+ });
502
+ if (startWatcher) {
503
+ codexWatcher.start();
504
+ }
505
+ let resolveReady = () => {
506
+ };
507
+ const ready = new Promise((resolve) => {
508
+ resolveReady = resolve;
509
+ });
510
+ const handle = {
511
+ port,
512
+ ready,
513
+ close: async () => {
514
+ stateInstance.destroy();
515
+ codexWatcher.stop();
516
+ await new Promise((resolve) => wss.close(() => resolve()));
517
+ await new Promise((resolve) => server.close(() => resolve()));
518
+ }
519
+ };
483
520
  server.listen(port, "127.0.0.1", () => {
521
+ const address = server.address();
522
+ const actualPort = typeof address === "object" && address ? address.port : port;
523
+ handle.port = actualPort;
524
+ resolveReady(actualPort);
525
+ if (!logBanner) return;
484
526
  console.log(`
485
527
  \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
528
  \u2502 \u2502
487
529
  \u2502 Codex Blocker Server \u2502
488
530
  \u2502 \u2502
489
- \u2502 HTTP: http://localhost:${port} \u2502
490
- \u2502 WebSocket: ws://localhost:${port}/ws \u2502
531
+ \u2502 HTTP: http://localhost:${actualPort} \u2502
532
+ \u2502 WebSocket: ws://localhost:${actualPort}/ws \u2502
491
533
  \u2502 \u2502
492
534
  \u2502 Watching Codex sessions... \u2502
493
535
  \u2502 \u2502
@@ -495,13 +537,12 @@ function startServer(port = DEFAULT_PORT) {
495
537
  `);
496
538
  });
497
539
  process.once("SIGINT", () => {
498
- console.log("\nShutting down...");
499
- state.destroy();
500
- codexWatcher.stop();
501
- wss.close();
502
- server.close();
503
- process.exit(0);
540
+ if (logBanner) {
541
+ console.log("\nShutting down...");
542
+ }
543
+ void handle.close().then(() => process.exit(0));
504
544
  });
545
+ return handle;
505
546
  }
506
547
 
507
548
  export {
package/dist/server.d.ts CHANGED
@@ -1,3 +1,54 @@
1
- declare function startServer(port?: number): void;
1
+ interface CodexActivity {
2
+ sessionId: string;
3
+ cwd?: string;
4
+ idleTimeoutMs?: number;
5
+ }
6
+ type ServerMessage = {
7
+ type: "state";
8
+ blocked: boolean;
9
+ sessions: number;
10
+ working: number;
11
+ waitingForInput: number;
12
+ } | {
13
+ type: "pong";
14
+ };
2
15
 
3
- export { startServer };
16
+ type StateChangeCallback = (message: ServerMessage) => void;
17
+ declare class SessionState {
18
+ private sessions;
19
+ private listeners;
20
+ private cleanupInterval;
21
+ constructor();
22
+ subscribe(callback: StateChangeCallback): () => void;
23
+ private broadcast;
24
+ private getStateMessage;
25
+ handleCodexActivity(activity: CodexActivity): void;
26
+ setCodexIdle(sessionId: string, cwd?: string): void;
27
+ markCodexSessionSeen(sessionId: string, cwd?: string): void;
28
+ removeSession(sessionId: string): void;
29
+ private ensureSession;
30
+ private cleanupStaleSessions;
31
+ getStatus(): {
32
+ blocked: boolean;
33
+ sessions: number;
34
+ working: number;
35
+ waitingForInput: number;
36
+ };
37
+ destroy(): void;
38
+ }
39
+
40
+ type ServerOptions = {
41
+ sessionsDir?: string;
42
+ startWatcher?: boolean;
43
+ tokenPath?: string;
44
+ state?: SessionState;
45
+ log?: boolean;
46
+ };
47
+ type ServerHandle = {
48
+ port: number;
49
+ ready: Promise<number>;
50
+ close: () => Promise<void>;
51
+ };
52
+ declare function startServer(port?: number, options?: ServerOptions): ServerHandle;
53
+
54
+ export { type ServerHandle, type ServerOptions, startServer };
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startServer
3
- } from "./chunk-7IRFKJUB.js";
3
+ } from "./chunk-APDAIY47.js";
4
4
  export {
5
5
  startServer
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-blocker",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
4
4
  "description": "Block distracting websites unless Codex is actively running inference. Forked from Theo Browne's (T3) Claude Blocker",
5
5
  "author": "Adam Blumoff ",
6
6
  "repository": {