codex-blocker 0.0.6 → 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-FXDFFIXM.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,51 +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") {
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
260
  var CodexSessionWatcher = class {
263
261
  fileStates = /* @__PURE__ */ new Map();
264
262
  scanTimer = null;
265
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
+ }
266
271
  start() {
267
272
  this.scan();
268
273
  this.scanTimer = setInterval(() => {
@@ -276,9 +281,9 @@ var CodexSessionWatcher = class {
276
281
  }
277
282
  }
278
283
  async scan() {
279
- if (!existsSync(CODEX_SESSIONS_DIR)) {
284
+ if (!existsSync(this.sessionsDir)) {
280
285
  if (!this.warnedMissing) {
281
- console.log(`Waiting for Codex sessions at ${CODEX_SESSIONS_DIR}`);
286
+ console.log(`Waiting for Codex sessions at ${this.sessionsDir}`);
282
287
  this.warnedMissing = true;
283
288
  }
284
289
  return;
@@ -286,7 +291,7 @@ var CodexSessionWatcher = class {
286
291
  this.warnedMissing = false;
287
292
  let files = [];
288
293
  try {
289
- files = await listRolloutFiles(CODEX_SESSIONS_DIR);
294
+ files = await listRolloutFiles(this.sessionsDir);
290
295
  } catch {
291
296
  return;
292
297
  }
@@ -314,33 +319,48 @@ var CodexSessionWatcher = class {
314
319
  }
315
320
  if (newLines.length === 0) continue;
316
321
  for (const line of newLines) {
317
- 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
+ }
318
337
  }
319
338
  }
320
339
  }
321
340
  };
322
341
 
323
342
  // src/server.ts
324
- var TOKEN_DIR = join2(homedir2(), ".codex-blocker");
325
- 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");
326
345
  var RATE_WINDOW_MS = 6e4;
327
346
  var RATE_LIMIT = 60;
328
347
  var MAX_WS_CONNECTIONS_PER_IP = 3;
329
348
  var rateByIp = /* @__PURE__ */ new Map();
330
349
  var wsConnectionsByIp = /* @__PURE__ */ new Map();
331
- function loadToken() {
332
- if (!existsSync2(TOKEN_PATH)) return null;
350
+ function loadToken(tokenPath) {
351
+ if (!existsSync2(tokenPath)) return null;
333
352
  try {
334
- return readFileSync(TOKEN_PATH, "utf-8").trim() || null;
353
+ return readFileSync(tokenPath, "utf-8").trim() || null;
335
354
  } catch {
336
355
  return null;
337
356
  }
338
357
  }
339
- function saveToken(token) {
340
- if (!existsSync2(TOKEN_DIR)) {
341
- mkdirSync(TOKEN_DIR, { recursive: true });
358
+ function saveToken(tokenPath, token) {
359
+ const tokenDir = dirname2(tokenPath);
360
+ if (!existsSync2(tokenDir)) {
361
+ mkdirSync(tokenDir, { recursive: true });
342
362
  }
343
- writeFileSync(TOKEN_PATH, token, "utf-8");
363
+ writeFileSync(tokenPath, token, "utf-8");
344
364
  }
345
365
  function isChromeExtensionOrigin(origin) {
346
366
  return Boolean(origin && origin.startsWith("chrome-extension://"));
@@ -374,8 +394,12 @@ function sendJson(res, data, status = 200) {
374
394
  res.writeHead(status, { "Content-Type": "application/json" });
375
395
  res.end(JSON.stringify(data));
376
396
  }
377
- function startServer(port = DEFAULT_PORT) {
378
- 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);
379
403
  const server = createServer(async (req, res) => {
380
404
  const clientIp = getClientIp(req);
381
405
  if (!checkRateLimit(clientIp)) {
@@ -407,13 +431,13 @@ function startServer(port = DEFAULT_PORT) {
407
431
  }
408
432
  } else if (providedToken && allowOrigin) {
409
433
  authToken = providedToken;
410
- saveToken(providedToken);
434
+ saveToken(tokenPath, providedToken);
411
435
  } else {
412
436
  sendJson(res, { error: "Unauthorized" }, 401);
413
437
  return;
414
438
  }
415
439
  if (req.method === "GET" && url.pathname === "/status") {
416
- sendJson(res, state.getStatus());
440
+ sendJson(res, stateInstance.getStatus());
417
441
  return;
418
442
  }
419
443
  sendJson(res, { error: "Not found" }, 404);
@@ -437,13 +461,13 @@ function startServer(port = DEFAULT_PORT) {
437
461
  }
438
462
  } else if (providedToken && allowOrigin) {
439
463
  authToken = providedToken;
440
- saveToken(providedToken);
464
+ saveToken(tokenPath, providedToken);
441
465
  } else {
442
466
  ws.close(1008, "Unauthorized");
443
467
  return;
444
468
  }
445
469
  wsConnectionsByIp.set(clientIp, currentConnections + 1);
446
- const unsubscribe = state.subscribe((message) => {
470
+ const unsubscribe = stateInstance.subscribe((message) => {
447
471
  if (ws.readyState === WebSocket.OPEN) {
448
472
  ws.send(JSON.stringify(message));
449
473
  }
@@ -472,16 +496,40 @@ function startServer(port = DEFAULT_PORT) {
472
496
  );
473
497
  });
474
498
  });
475
- const codexWatcher = new CodexSessionWatcher();
476
- 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
+ };
477
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;
478
526
  console.log(`
479
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
480
528
  \u2502 \u2502
481
529
  \u2502 Codex Blocker Server \u2502
482
530
  \u2502 \u2502
483
- \u2502 HTTP: http://localhost:${port} \u2502
484
- \u2502 WebSocket: ws://localhost:${port}/ws \u2502
531
+ \u2502 HTTP: http://localhost:${actualPort} \u2502
532
+ \u2502 WebSocket: ws://localhost:${actualPort}/ws \u2502
485
533
  \u2502 \u2502
486
534
  \u2502 Watching Codex sessions... \u2502
487
535
  \u2502 \u2502
@@ -489,13 +537,12 @@ function startServer(port = DEFAULT_PORT) {
489
537
  `);
490
538
  });
491
539
  process.once("SIGINT", () => {
492
- console.log("\nShutting down...");
493
- state.destroy();
494
- codexWatcher.stop();
495
- wss.close();
496
- server.close();
497
- process.exit(0);
540
+ if (logBanner) {
541
+ console.log("\nShutting down...");
542
+ }
543
+ void handle.close().then(() => process.exit(0));
498
544
  });
545
+ return handle;
499
546
  }
500
547
 
501
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-FXDFFIXM.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.6",
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": {