codex-blocker 0.0.6 → 0.0.8

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-FXDFFIXM.js";
5
+ } from "./chunk-2Q6Z6NQH.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,47 @@ 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");
225
+ var TAIL_MAX_BYTES = 64 * 1024;
226
+ var TAIL_MAX_LINES = 200;
186
227
  async function listRolloutFiles(root) {
187
228
  const files = [];
188
229
  const entries = await fs.readdir(root, { withFileTypes: true });
@@ -196,6 +237,30 @@ async function listRolloutFiles(root) {
196
237
  }
197
238
  return files;
198
239
  }
240
+ async function readTailLines(filePath, fileSize, maxBytes, maxLines) {
241
+ if (fileSize === 0) return [];
242
+ const start = Math.max(0, fileSize - maxBytes);
243
+ const end = Math.max(fileSize - 1, start);
244
+ const chunks = [];
245
+ await new Promise((resolve, reject) => {
246
+ const stream = createReadStream(filePath, { start, end });
247
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
248
+ stream.on("error", reject);
249
+ stream.on("end", resolve);
250
+ });
251
+ let content = Buffer.concat(chunks).toString("utf-8");
252
+ let lines = content.split("\n");
253
+ if (start > 0 && content[0] !== "\n") {
254
+ lines = lines.slice(1);
255
+ }
256
+ if (lines.length > 0 && lines[lines.length - 1]?.trim() === "") {
257
+ lines.pop();
258
+ }
259
+ if (lines.length > maxLines) {
260
+ lines = lines.slice(-maxLines);
261
+ }
262
+ return lines.filter((line) => line.trim().length > 0);
263
+ }
199
264
  async function readNewLines(filePath, fileState) {
200
265
  const stat = await fs.stat(filePath);
201
266
  if (stat.size < fileState.position) {
@@ -218,51 +283,17 @@ async function readNewLines(filePath, fileState) {
218
283
  fileState.remainder = content.endsWith("\n") ? "" : lines.pop() ?? "";
219
284
  return lines.filter((line) => line.trim().length > 0);
220
285
  }
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") {
246
- shouldMarkIdle = true;
247
- }
248
- } catch {
249
- }
250
- state.markCodexSessionSeen(sessionId, cwd);
251
- if (shouldMarkWorking) {
252
- state.handleCodexActivity({
253
- sessionId,
254
- cwd,
255
- idleTimeoutMs: CODEX_ACTIVITY_IDLE_TIMEOUT_MS
256
- });
257
- }
258
- if (shouldMarkIdle) {
259
- state.setCodexIdle(sessionId, cwd);
260
- }
261
- }
262
286
  var CodexSessionWatcher = class {
263
287
  fileStates = /* @__PURE__ */ new Map();
264
288
  scanTimer = null;
265
289
  warnedMissing = false;
290
+ sessionsDir;
291
+ state;
292
+ constructor(state2, options) {
293
+ this.state = state2;
294
+ const base = process.env.CODEX_HOME ?? DEFAULT_CODEX_HOME;
295
+ this.sessionsDir = options?.sessionsDir ?? join(base, "sessions");
296
+ }
266
297
  start() {
267
298
  this.scan();
268
299
  this.scanTimer = setInterval(() => {
@@ -276,9 +307,9 @@ var CodexSessionWatcher = class {
276
307
  }
277
308
  }
278
309
  async scan() {
279
- if (!existsSync(CODEX_SESSIONS_DIR)) {
310
+ if (!existsSync(this.sessionsDir)) {
280
311
  if (!this.warnedMissing) {
281
- console.log(`Waiting for Codex sessions at ${CODEX_SESSIONS_DIR}`);
312
+ console.log(`Waiting for Codex sessions at ${this.sessionsDir}`);
282
313
  this.warnedMissing = true;
283
314
  }
284
315
  return;
@@ -286,7 +317,7 @@ var CodexSessionWatcher = class {
286
317
  this.warnedMissing = false;
287
318
  let files = [];
288
319
  try {
289
- files = await listRolloutFiles(CODEX_SESSIONS_DIR);
320
+ files = await listRolloutFiles(this.sessionsDir);
290
321
  } catch {
291
322
  return;
292
323
  }
@@ -301,6 +332,15 @@ var CodexSessionWatcher = class {
301
332
  try {
302
333
  const stat = await fs.stat(filePath);
303
334
  fileState.position = stat.size;
335
+ const tailLines = await readTailLines(
336
+ filePath,
337
+ stat.size,
338
+ TAIL_MAX_BYTES,
339
+ TAIL_MAX_LINES
340
+ );
341
+ if (tailLines.length > 0) {
342
+ this.processLines(tailLines, fileState);
343
+ }
304
344
  } catch {
305
345
  continue;
306
346
  }
@@ -313,34 +353,52 @@ var CodexSessionWatcher = class {
313
353
  continue;
314
354
  }
315
355
  if (newLines.length === 0) continue;
316
- for (const line of newLines) {
317
- handleLine(line, fileState);
356
+ this.processLines(newLines, fileState);
357
+ }
358
+ }
359
+ processLines(lines, fileState) {
360
+ for (const line of lines) {
361
+ const parsed = parseCodexLine(line, fileState.sessionId);
362
+ fileState.sessionId = parsed.sessionId;
363
+ if (parsed.previousSessionId) {
364
+ this.state.removeSession(parsed.previousSessionId);
365
+ }
366
+ this.state.markCodexSessionSeen(parsed.sessionId, parsed.cwd);
367
+ if (parsed.markWorking) {
368
+ this.state.handleCodexActivity({
369
+ sessionId: parsed.sessionId,
370
+ cwd: parsed.cwd
371
+ });
372
+ }
373
+ if (parsed.markIdle) {
374
+ this.state.setCodexIdle(parsed.sessionId, parsed.cwd);
318
375
  }
319
376
  }
320
377
  }
321
378
  };
322
379
 
323
380
  // src/server.ts
324
- var TOKEN_DIR = join2(homedir2(), ".codex-blocker");
325
- var TOKEN_PATH = join2(TOKEN_DIR, "token");
381
+ var DEFAULT_TOKEN_DIR = join2(homedir2(), ".codex-blocker");
382
+ var DEFAULT_TOKEN_PATH = join2(DEFAULT_TOKEN_DIR, "token");
326
383
  var RATE_WINDOW_MS = 6e4;
327
384
  var RATE_LIMIT = 60;
328
385
  var MAX_WS_CONNECTIONS_PER_IP = 3;
329
386
  var rateByIp = /* @__PURE__ */ new Map();
330
387
  var wsConnectionsByIp = /* @__PURE__ */ new Map();
331
- function loadToken() {
332
- if (!existsSync2(TOKEN_PATH)) return null;
388
+ function loadToken(tokenPath) {
389
+ if (!existsSync2(tokenPath)) return null;
333
390
  try {
334
- return readFileSync(TOKEN_PATH, "utf-8").trim() || null;
391
+ return readFileSync(tokenPath, "utf-8").trim() || null;
335
392
  } catch {
336
393
  return null;
337
394
  }
338
395
  }
339
- function saveToken(token) {
340
- if (!existsSync2(TOKEN_DIR)) {
341
- mkdirSync(TOKEN_DIR, { recursive: true });
396
+ function saveToken(tokenPath, token) {
397
+ const tokenDir = dirname2(tokenPath);
398
+ if (!existsSync2(tokenDir)) {
399
+ mkdirSync(tokenDir, { recursive: true });
342
400
  }
343
- writeFileSync(TOKEN_PATH, token, "utf-8");
401
+ writeFileSync(tokenPath, token, "utf-8");
344
402
  }
345
403
  function isChromeExtensionOrigin(origin) {
346
404
  return Boolean(origin && origin.startsWith("chrome-extension://"));
@@ -374,8 +432,12 @@ function sendJson(res, data, status = 200) {
374
432
  res.writeHead(status, { "Content-Type": "application/json" });
375
433
  res.end(JSON.stringify(data));
376
434
  }
377
- function startServer(port = DEFAULT_PORT) {
378
- let authToken = loadToken();
435
+ function startServer(port = DEFAULT_PORT, options) {
436
+ const stateInstance = options?.state ?? state;
437
+ const tokenPath = options?.tokenPath ?? DEFAULT_TOKEN_PATH;
438
+ const startWatcher = options?.startWatcher ?? true;
439
+ const logBanner = options?.log ?? true;
440
+ let authToken = loadToken(tokenPath);
379
441
  const server = createServer(async (req, res) => {
380
442
  const clientIp = getClientIp(req);
381
443
  if (!checkRateLimit(clientIp)) {
@@ -407,13 +469,13 @@ function startServer(port = DEFAULT_PORT) {
407
469
  }
408
470
  } else if (providedToken && allowOrigin) {
409
471
  authToken = providedToken;
410
- saveToken(providedToken);
472
+ saveToken(tokenPath, providedToken);
411
473
  } else {
412
474
  sendJson(res, { error: "Unauthorized" }, 401);
413
475
  return;
414
476
  }
415
477
  if (req.method === "GET" && url.pathname === "/status") {
416
- sendJson(res, state.getStatus());
478
+ sendJson(res, stateInstance.getStatus());
417
479
  return;
418
480
  }
419
481
  sendJson(res, { error: "Not found" }, 404);
@@ -437,13 +499,13 @@ function startServer(port = DEFAULT_PORT) {
437
499
  }
438
500
  } else if (providedToken && allowOrigin) {
439
501
  authToken = providedToken;
440
- saveToken(providedToken);
502
+ saveToken(tokenPath, providedToken);
441
503
  } else {
442
504
  ws.close(1008, "Unauthorized");
443
505
  return;
444
506
  }
445
507
  wsConnectionsByIp.set(clientIp, currentConnections + 1);
446
- const unsubscribe = state.subscribe((message) => {
508
+ const unsubscribe = stateInstance.subscribe((message) => {
447
509
  if (ws.readyState === WebSocket.OPEN) {
448
510
  ws.send(JSON.stringify(message));
449
511
  }
@@ -472,16 +534,40 @@ function startServer(port = DEFAULT_PORT) {
472
534
  );
473
535
  });
474
536
  });
475
- const codexWatcher = new CodexSessionWatcher();
476
- codexWatcher.start();
537
+ const codexWatcher = new CodexSessionWatcher(stateInstance, {
538
+ sessionsDir: options?.sessionsDir
539
+ });
540
+ if (startWatcher) {
541
+ codexWatcher.start();
542
+ }
543
+ let resolveReady = () => {
544
+ };
545
+ const ready = new Promise((resolve) => {
546
+ resolveReady = resolve;
547
+ });
548
+ const handle = {
549
+ port,
550
+ ready,
551
+ close: async () => {
552
+ stateInstance.destroy();
553
+ codexWatcher.stop();
554
+ await new Promise((resolve) => wss.close(() => resolve()));
555
+ await new Promise((resolve) => server.close(() => resolve()));
556
+ }
557
+ };
477
558
  server.listen(port, "127.0.0.1", () => {
559
+ const address = server.address();
560
+ const actualPort = typeof address === "object" && address ? address.port : port;
561
+ handle.port = actualPort;
562
+ resolveReady(actualPort);
563
+ if (!logBanner) return;
478
564
  console.log(`
479
565
  \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
480
566
  \u2502 \u2502
481
567
  \u2502 Codex Blocker Server \u2502
482
568
  \u2502 \u2502
483
- \u2502 HTTP: http://localhost:${port} \u2502
484
- \u2502 WebSocket: ws://localhost:${port}/ws \u2502
569
+ \u2502 HTTP: http://localhost:${actualPort} \u2502
570
+ \u2502 WebSocket: ws://localhost:${actualPort}/ws \u2502
485
571
  \u2502 \u2502
486
572
  \u2502 Watching Codex sessions... \u2502
487
573
  \u2502 \u2502
@@ -489,13 +575,12 @@ function startServer(port = DEFAULT_PORT) {
489
575
  `);
490
576
  });
491
577
  process.once("SIGINT", () => {
492
- console.log("\nShutting down...");
493
- state.destroy();
494
- codexWatcher.stop();
495
- wss.close();
496
- server.close();
497
- process.exit(0);
578
+ if (logBanner) {
579
+ console.log("\nShutting down...");
580
+ }
581
+ void handle.close().then(() => process.exit(0));
498
582
  });
583
+ return handle;
499
584
  }
500
585
 
501
586
  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-FXDFFIXM.js";
3
+ } from "./chunk-2Q6Z6NQH.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.6",
3
+ "version": "0.0.8",
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": {