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 +2 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-FXDFFIXM.js → chunk-APDAIY47.js} +122 -75
- 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,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(
|
|
284
|
+
if (!existsSync(this.sessionsDir)) {
|
|
280
285
|
if (!this.warnedMissing) {
|
|
281
|
-
console.log(`Waiting for Codex sessions at ${
|
|
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(
|
|
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
|
-
|
|
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
|
|
325
|
-
var
|
|
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(
|
|
350
|
+
function loadToken(tokenPath) {
|
|
351
|
+
if (!existsSync2(tokenPath)) return null;
|
|
333
352
|
try {
|
|
334
|
-
return readFileSync(
|
|
353
|
+
return readFileSync(tokenPath, "utf-8").trim() || null;
|
|
335
354
|
} catch {
|
|
336
355
|
return null;
|
|
337
356
|
}
|
|
338
357
|
}
|
|
339
|
-
function saveToken(token) {
|
|
340
|
-
|
|
341
|
-
|
|
358
|
+
function saveToken(tokenPath, token) {
|
|
359
|
+
const tokenDir = dirname2(tokenPath);
|
|
360
|
+
if (!existsSync2(tokenDir)) {
|
|
361
|
+
mkdirSync(tokenDir, { recursive: true });
|
|
342
362
|
}
|
|
343
|
-
writeFileSync(
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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:${
|
|
484
|
-
\u2502 WebSocket: ws://localhost:${
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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