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 +2 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-7IRFKJUB.js → chunk-APDAIY47.js} +122 -81
- package/dist/server.d.ts +53 -2
- package/dist/server.js +1 -1
- package/package.json +1 -1
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,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 {
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
284
|
+
if (!existsSync(this.sessionsDir)) {
|
|
286
285
|
if (!this.warnedMissing) {
|
|
287
|
-
console.log(`Waiting for Codex sessions at ${
|
|
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(
|
|
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
|
-
|
|
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
|
|
331
|
-
var
|
|
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(
|
|
350
|
+
function loadToken(tokenPath) {
|
|
351
|
+
if (!existsSync2(tokenPath)) return null;
|
|
339
352
|
try {
|
|
340
|
-
return readFileSync(
|
|
353
|
+
return readFileSync(tokenPath, "utf-8").trim() || null;
|
|
341
354
|
} catch {
|
|
342
355
|
return null;
|
|
343
356
|
}
|
|
344
357
|
}
|
|
345
|
-
function saveToken(token) {
|
|
346
|
-
|
|
347
|
-
|
|
358
|
+
function saveToken(tokenPath, token) {
|
|
359
|
+
const tokenDir = dirname2(tokenPath);
|
|
360
|
+
if (!existsSync2(tokenDir)) {
|
|
361
|
+
mkdirSync(tokenDir, { recursive: true });
|
|
348
362
|
}
|
|
349
|
-
writeFileSync(
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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:${
|
|
490
|
-
\u2502 WebSocket: ws://localhost:${
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED