codex-blocker 0.0.3
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 +94 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +106 -0
- package/dist/chunk-7TKQB72O.js +509 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +6 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# codex-blocker
|
|
2
|
+
|
|
3
|
+
CLI tool and server for Codex Blocker — block distracting websites unless Codex is actively running.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codex-blocker
|
|
9
|
+
# or
|
|
10
|
+
npx codex-blocker
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Optional setup info
|
|
17
|
+
npx codex-blocker --setup
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Start server (default port 8765)
|
|
24
|
+
npx codex-blocker
|
|
25
|
+
|
|
26
|
+
# Show setup info
|
|
27
|
+
npx codex-blocker --setup
|
|
28
|
+
|
|
29
|
+
# Custom port
|
|
30
|
+
npx codex-blocker --port 9000
|
|
31
|
+
|
|
32
|
+
# Remove setup (no-op)
|
|
33
|
+
npx codex-blocker --remove
|
|
34
|
+
|
|
35
|
+
# Show help
|
|
36
|
+
npx codex-blocker --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How It Works
|
|
40
|
+
|
|
41
|
+
1. **Codex sessions** — The server tails Codex session logs under `~/.codex/sessions`
|
|
42
|
+
to detect activity.
|
|
43
|
+
|
|
44
|
+
2. **Server** — Runs on localhost and:
|
|
45
|
+
- Tracks active Codex sessions
|
|
46
|
+
- Marks sessions "working" when new log lines arrive
|
|
47
|
+
- Broadcasts state via WebSocket to the Chrome extension
|
|
48
|
+
|
|
49
|
+
3. **Extension** — Connects to the server and:
|
|
50
|
+
- Blocks configured sites when no sessions are working
|
|
51
|
+
- Shows a modal overlay (soft block, not network block)
|
|
52
|
+
- Updates in real-time without page refresh
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
### HTTP Endpoints
|
|
57
|
+
|
|
58
|
+
| Endpoint | Method | Description |
|
|
59
|
+
|----------|--------|-------------|
|
|
60
|
+
| `/status` | GET | Returns current state (sessions, blocked status) |
|
|
61
|
+
|
|
62
|
+
### WebSocket
|
|
63
|
+
|
|
64
|
+
Connect to `ws://localhost:8765/ws` to receive real-time state updates:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"type": "state",
|
|
69
|
+
"blocked": true,
|
|
70
|
+
"sessions": 1,
|
|
71
|
+
"working": 0
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Programmatic Usage
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { startServer } from 'codex-blocker';
|
|
79
|
+
|
|
80
|
+
// Start on default port (8765)
|
|
81
|
+
startServer();
|
|
82
|
+
|
|
83
|
+
// Or custom port
|
|
84
|
+
startServer(9000);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Node.js 18+
|
|
90
|
+
- Codex CLI
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
startServer
|
|
4
|
+
} from "./chunk-7TKQB72O.js";
|
|
5
|
+
|
|
6
|
+
// src/bin.ts
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
|
|
9
|
+
// src/setup.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var CODEX_HOME = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
14
|
+
var CODEX_SESSIONS_DIR = join(CODEX_HOME, "sessions");
|
|
15
|
+
function setupCodex() {
|
|
16
|
+
console.log(`
|
|
17
|
+
\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
18
|
+
\u2502 \u2502
|
|
19
|
+
\u2502 Codex Blocker Setup \u2502
|
|
20
|
+
\u2502 \u2502
|
|
21
|
+
\u2502 No hooks needed. The server reads Codex \u2502
|
|
22
|
+
\u2502 session logs from: \u2502
|
|
23
|
+
\u2502 ${CODEX_SESSIONS_DIR}
|
|
24
|
+
\u2502 \u2502
|
|
25
|
+
\u2502 Tip: run Codex once to create the sessions \u2502
|
|
26
|
+
\u2502 directory if it doesn't exist yet. \u2502
|
|
27
|
+
\u2502 \u2502
|
|
28
|
+
\u2502 Next: Run 'npx codex-blocker' to start \u2502
|
|
29
|
+
\u2502 \u2502
|
|
30
|
+
\u2514\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
function isCodexAvailable() {
|
|
34
|
+
return existsSync(CODEX_SESSIONS_DIR);
|
|
35
|
+
}
|
|
36
|
+
function removeCodexSetup() {
|
|
37
|
+
console.log("No Codex hooks to remove.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/bin.ts
|
|
41
|
+
import { DEFAULT_PORT } from "@claude-blocker/shared";
|
|
42
|
+
var args = process.argv.slice(2);
|
|
43
|
+
function prompt(question) {
|
|
44
|
+
const rl = createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout
|
|
47
|
+
});
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
rl.question(question, (answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolve(answer);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function printHelp() {
|
|
56
|
+
console.log(`
|
|
57
|
+
Codex Blocker - Block distracting sites when Codex isn't working
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
npx codex-blocker [options]
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--setup Show Codex setup info
|
|
64
|
+
--remove Remove Codex setup (no-op)
|
|
65
|
+
--port Server port (default: ${DEFAULT_PORT})
|
|
66
|
+
--help Show this help message
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
npx codex-blocker # Start the server
|
|
70
|
+
npx codex-blocker --port 9000
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
async function main() {
|
|
74
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
75
|
+
printHelp();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
if (args.includes("--setup")) {
|
|
79
|
+
setupCodex();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
if (args.includes("--remove")) {
|
|
83
|
+
removeCodexSetup();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
let port = DEFAULT_PORT;
|
|
87
|
+
const portIndex = args.indexOf("--port");
|
|
88
|
+
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
89
|
+
const parsed = parseInt(args[portIndex + 1], 10);
|
|
90
|
+
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
91
|
+
port = parsed;
|
|
92
|
+
} else {
|
|
93
|
+
console.error("Invalid port number");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!isCodexAvailable()) {
|
|
98
|
+
console.log("Codex sessions directory not found yet.");
|
|
99
|
+
const answer = await prompt("Run Codex once to create it, then press enter to continue. ");
|
|
100
|
+
if (answer !== void 0) {
|
|
101
|
+
console.log("");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
startServer(port);
|
|
105
|
+
}
|
|
106
|
+
main();
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir as homedir2 } from "os";
|
|
5
|
+
import { join as join2 } from "path";
|
|
6
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
7
|
+
|
|
8
|
+
// src/types.ts
|
|
9
|
+
var DEFAULT_PORT = 8765;
|
|
10
|
+
var SESSION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
11
|
+
var CODEX_ACTIVITY_IDLE_TIMEOUT_MS = 60 * 1e3;
|
|
12
|
+
var CODEX_SESSIONS_SCAN_INTERVAL_MS = 2e3;
|
|
13
|
+
|
|
14
|
+
// src/state.ts
|
|
15
|
+
var SessionState = class {
|
|
16
|
+
sessions = /* @__PURE__ */ new Map();
|
|
17
|
+
listeners = /* @__PURE__ */ new Set();
|
|
18
|
+
cleanupInterval = null;
|
|
19
|
+
constructor() {
|
|
20
|
+
this.cleanupInterval = setInterval(() => {
|
|
21
|
+
this.cleanupStaleSessions();
|
|
22
|
+
}, 3e4);
|
|
23
|
+
}
|
|
24
|
+
subscribe(callback) {
|
|
25
|
+
this.listeners.add(callback);
|
|
26
|
+
callback(this.getStateMessage());
|
|
27
|
+
return () => this.listeners.delete(callback);
|
|
28
|
+
}
|
|
29
|
+
broadcast() {
|
|
30
|
+
const message = this.getStateMessage();
|
|
31
|
+
for (const listener of this.listeners) {
|
|
32
|
+
listener(message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
getStateMessage() {
|
|
36
|
+
const sessions = Array.from(this.sessions.values());
|
|
37
|
+
const working = sessions.filter((s) => s.status === "working").length;
|
|
38
|
+
const waitingForInput = sessions.filter(
|
|
39
|
+
(s) => s.status === "waiting_for_input"
|
|
40
|
+
).length;
|
|
41
|
+
return {
|
|
42
|
+
type: "state",
|
|
43
|
+
blocked: working === 0,
|
|
44
|
+
sessions: sessions.length,
|
|
45
|
+
working,
|
|
46
|
+
waitingForInput
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
handleCodexActivity(activity) {
|
|
50
|
+
this.ensureSession(activity.sessionId, activity.cwd);
|
|
51
|
+
const session = this.sessions.get(activity.sessionId);
|
|
52
|
+
session.status = "working";
|
|
53
|
+
session.waitingForInputSince = void 0;
|
|
54
|
+
session.lastActivity = /* @__PURE__ */ new Date();
|
|
55
|
+
session.lastSeen = /* @__PURE__ */ new Date();
|
|
56
|
+
session.idleTimeoutMs = activity.idleTimeoutMs;
|
|
57
|
+
this.broadcast();
|
|
58
|
+
}
|
|
59
|
+
setCodexIdle(sessionId, cwd) {
|
|
60
|
+
this.ensureSession(sessionId, cwd);
|
|
61
|
+
const session = this.sessions.get(sessionId);
|
|
62
|
+
if (session.status !== "idle") {
|
|
63
|
+
session.status = "idle";
|
|
64
|
+
session.waitingForInputSince = void 0;
|
|
65
|
+
session.lastActivity = /* @__PURE__ */ new Date();
|
|
66
|
+
this.broadcast();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
markCodexSessionSeen(sessionId, cwd) {
|
|
70
|
+
const created = this.ensureSession(sessionId, cwd);
|
|
71
|
+
const session = this.sessions.get(sessionId);
|
|
72
|
+
session.lastSeen = /* @__PURE__ */ new Date();
|
|
73
|
+
if (created) {
|
|
74
|
+
this.broadcast();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
removeSession(sessionId) {
|
|
78
|
+
if (this.sessions.delete(sessionId)) {
|
|
79
|
+
this.broadcast();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ensureSession(sessionId, cwd) {
|
|
83
|
+
if (!this.sessions.has(sessionId)) {
|
|
84
|
+
this.sessions.set(sessionId, {
|
|
85
|
+
id: sessionId,
|
|
86
|
+
status: "idle",
|
|
87
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
88
|
+
lastSeen: /* @__PURE__ */ new Date(),
|
|
89
|
+
cwd
|
|
90
|
+
});
|
|
91
|
+
console.log("Codex session connected");
|
|
92
|
+
return true;
|
|
93
|
+
} else if (cwd) {
|
|
94
|
+
const session = this.sessions.get(sessionId);
|
|
95
|
+
session.cwd = cwd;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
cleanupStaleSessions() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
let removed = 0;
|
|
102
|
+
let changed = 0;
|
|
103
|
+
for (const [id, session] of this.sessions) {
|
|
104
|
+
if (now - session.lastSeen.getTime() > SESSION_TIMEOUT_MS) {
|
|
105
|
+
this.sessions.delete(id);
|
|
106
|
+
removed++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (session.status === "working" && session.idleTimeoutMs && now - session.lastActivity.getTime() > session.idleTimeoutMs) {
|
|
110
|
+
session.status = "idle";
|
|
111
|
+
session.waitingForInputSince = void 0;
|
|
112
|
+
changed++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (removed > 0 || changed > 0) {
|
|
116
|
+
this.broadcast();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
getStatus() {
|
|
120
|
+
const sessions = Array.from(this.sessions.values());
|
|
121
|
+
const working = sessions.filter((s) => s.status === "working").length;
|
|
122
|
+
const waitingForInput = sessions.filter(
|
|
123
|
+
(s) => s.status === "waiting_for_input"
|
|
124
|
+
).length;
|
|
125
|
+
return {
|
|
126
|
+
blocked: working === 0,
|
|
127
|
+
sessions: sessions.length,
|
|
128
|
+
working,
|
|
129
|
+
waitingForInput
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
destroy() {
|
|
133
|
+
if (this.cleanupInterval) {
|
|
134
|
+
clearInterval(this.cleanupInterval);
|
|
135
|
+
}
|
|
136
|
+
this.sessions.clear();
|
|
137
|
+
this.listeners.clear();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var state = new SessionState();
|
|
141
|
+
|
|
142
|
+
// src/codex.ts
|
|
143
|
+
import { existsSync, createReadStream, promises as fs } from "fs";
|
|
144
|
+
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");
|
|
148
|
+
function isRolloutFile(filePath) {
|
|
149
|
+
const name = basename(filePath);
|
|
150
|
+
return name === "rollout.jsonl" || /^rollout-.+\.jsonl$/.test(name);
|
|
151
|
+
}
|
|
152
|
+
function sessionIdFromPath(filePath) {
|
|
153
|
+
const name = basename(filePath);
|
|
154
|
+
const match = name.match(/^rollout-(.+)\.jsonl$/);
|
|
155
|
+
if (match) return match[1];
|
|
156
|
+
if (name === "rollout.jsonl") {
|
|
157
|
+
const parent = basename(dirname(filePath));
|
|
158
|
+
if (parent !== "sessions") return parent;
|
|
159
|
+
}
|
|
160
|
+
return filePath;
|
|
161
|
+
}
|
|
162
|
+
function findFirstStringValue(obj, keys, maxDepth = 6) {
|
|
163
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
164
|
+
const queue = [{ value: obj, depth: 0 }];
|
|
165
|
+
while (queue.length) {
|
|
166
|
+
const current = queue.shift();
|
|
167
|
+
if (!current) break;
|
|
168
|
+
const { value, depth } = current;
|
|
169
|
+
if (!value || typeof value !== "object") continue;
|
|
170
|
+
const record = value;
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
const candidate = record[key];
|
|
173
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
174
|
+
return candidate;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (depth >= maxDepth) continue;
|
|
178
|
+
for (const child of Object.values(record)) {
|
|
179
|
+
if (child && typeof child === "object") {
|
|
180
|
+
queue.push({ value: child, depth: depth + 1 });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
async function listRolloutFiles(root) {
|
|
187
|
+
const files = [];
|
|
188
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const fullPath = join(root, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
files.push(...await listRolloutFiles(fullPath));
|
|
193
|
+
} else if (entry.isFile() && isRolloutFile(fullPath)) {
|
|
194
|
+
files.push(fullPath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return files;
|
|
198
|
+
}
|
|
199
|
+
async function readNewLines(filePath, fileState) {
|
|
200
|
+
const stat = await fs.stat(filePath);
|
|
201
|
+
if (stat.size < fileState.position) {
|
|
202
|
+
fileState.position = 0;
|
|
203
|
+
fileState.remainder = "";
|
|
204
|
+
}
|
|
205
|
+
if (stat.size === fileState.position) return [];
|
|
206
|
+
const start = fileState.position;
|
|
207
|
+
const end = Math.max(stat.size - 1, start);
|
|
208
|
+
const chunks = [];
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
const stream = createReadStream(filePath, { start, end });
|
|
211
|
+
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
212
|
+
stream.on("error", reject);
|
|
213
|
+
stream.on("end", resolve);
|
|
214
|
+
});
|
|
215
|
+
fileState.position = stat.size;
|
|
216
|
+
const content = fileState.remainder + Buffer.concat(chunks).toString("utf-8");
|
|
217
|
+
const lines = content.split("\n");
|
|
218
|
+
fileState.remainder = content.endsWith("\n") ? "" : lines.pop() ?? "";
|
|
219
|
+
return lines.filter((line) => line.trim().length > 0);
|
|
220
|
+
}
|
|
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
|
+
var CodexSessionWatcher = class {
|
|
269
|
+
fileStates = /* @__PURE__ */ new Map();
|
|
270
|
+
scanTimer = null;
|
|
271
|
+
warnedMissing = false;
|
|
272
|
+
start() {
|
|
273
|
+
this.scan();
|
|
274
|
+
this.scanTimer = setInterval(() => {
|
|
275
|
+
this.scan();
|
|
276
|
+
}, CODEX_SESSIONS_SCAN_INTERVAL_MS);
|
|
277
|
+
}
|
|
278
|
+
stop() {
|
|
279
|
+
if (this.scanTimer) {
|
|
280
|
+
clearInterval(this.scanTimer);
|
|
281
|
+
this.scanTimer = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async scan() {
|
|
285
|
+
if (!existsSync(CODEX_SESSIONS_DIR)) {
|
|
286
|
+
if (!this.warnedMissing) {
|
|
287
|
+
console.log(`Waiting for Codex sessions at ${CODEX_SESSIONS_DIR}`);
|
|
288
|
+
this.warnedMissing = true;
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.warnedMissing = false;
|
|
293
|
+
let files = [];
|
|
294
|
+
try {
|
|
295
|
+
files = await listRolloutFiles(CODEX_SESSIONS_DIR);
|
|
296
|
+
} catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const filePath of files) {
|
|
300
|
+
const fileState = this.fileStates.get(filePath) ?? {
|
|
301
|
+
position: 0,
|
|
302
|
+
remainder: "",
|
|
303
|
+
sessionId: sessionIdFromPath(filePath)
|
|
304
|
+
};
|
|
305
|
+
if (!this.fileStates.has(filePath)) {
|
|
306
|
+
this.fileStates.set(filePath, fileState);
|
|
307
|
+
try {
|
|
308
|
+
const stat = await fs.stat(filePath);
|
|
309
|
+
fileState.position = stat.size;
|
|
310
|
+
} catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
let newLines = [];
|
|
316
|
+
try {
|
|
317
|
+
newLines = await readNewLines(filePath, fileState);
|
|
318
|
+
} catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (newLines.length === 0) continue;
|
|
322
|
+
for (const line of newLines) {
|
|
323
|
+
handleLine(line, fileState);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/server.ts
|
|
330
|
+
var TOKEN_DIR = join2(homedir2(), ".codex-blocker");
|
|
331
|
+
var TOKEN_PATH = join2(TOKEN_DIR, "token");
|
|
332
|
+
var RATE_WINDOW_MS = 6e4;
|
|
333
|
+
var RATE_LIMIT = 60;
|
|
334
|
+
var MAX_WS_CONNECTIONS_PER_IP = 3;
|
|
335
|
+
var rateByIp = /* @__PURE__ */ new Map();
|
|
336
|
+
var wsConnectionsByIp = /* @__PURE__ */ new Map();
|
|
337
|
+
function loadToken() {
|
|
338
|
+
if (!existsSync2(TOKEN_PATH)) return null;
|
|
339
|
+
try {
|
|
340
|
+
return readFileSync(TOKEN_PATH, "utf-8").trim() || null;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function saveToken(token) {
|
|
346
|
+
if (!existsSync2(TOKEN_DIR)) {
|
|
347
|
+
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
writeFileSync(TOKEN_PATH, token, "utf-8");
|
|
350
|
+
}
|
|
351
|
+
function isChromeExtensionOrigin(origin) {
|
|
352
|
+
return Boolean(origin && origin.startsWith("chrome-extension://"));
|
|
353
|
+
}
|
|
354
|
+
function getClientIp(req) {
|
|
355
|
+
return req.socket.remoteAddress ?? "unknown";
|
|
356
|
+
}
|
|
357
|
+
function checkRateLimit(ip) {
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
const state2 = rateByIp.get(ip);
|
|
360
|
+
if (!state2 || state2.resetAt <= now) {
|
|
361
|
+
rateByIp.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (state2.count >= RATE_LIMIT) return false;
|
|
365
|
+
state2.count += 1;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
function readAuthToken(req, url) {
|
|
369
|
+
const header = req.headers.authorization;
|
|
370
|
+
if (header && header.startsWith("Bearer ")) {
|
|
371
|
+
return header.slice("Bearer ".length).trim();
|
|
372
|
+
}
|
|
373
|
+
const query = url.searchParams.get("token");
|
|
374
|
+
if (query) return query;
|
|
375
|
+
const alt = req.headers["x-codex-blocker-token"];
|
|
376
|
+
if (typeof alt === "string" && alt.length > 0) return alt;
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
function sendJson(res, data, status = 200) {
|
|
380
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify(data));
|
|
382
|
+
}
|
|
383
|
+
function startServer(port = DEFAULT_PORT) {
|
|
384
|
+
let authToken = loadToken();
|
|
385
|
+
const server = createServer(async (req, res) => {
|
|
386
|
+
const clientIp = getClientIp(req);
|
|
387
|
+
if (!checkRateLimit(clientIp)) {
|
|
388
|
+
sendJson(res, { error: "Too Many Requests" }, 429);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
392
|
+
const origin = req.headers.origin;
|
|
393
|
+
const allowOrigin = isChromeExtensionOrigin(origin);
|
|
394
|
+
if (allowOrigin && origin) {
|
|
395
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
396
|
+
res.setHeader("Vary", "Origin");
|
|
397
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
398
|
+
res.setHeader(
|
|
399
|
+
"Access-Control-Allow-Headers",
|
|
400
|
+
"Content-Type, Authorization, X-Codex-Blocker-Token"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (req.method === "OPTIONS") {
|
|
404
|
+
res.writeHead(allowOrigin ? 204 : 403);
|
|
405
|
+
res.end();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const providedToken = readAuthToken(req, url);
|
|
409
|
+
if (authToken) {
|
|
410
|
+
if (!providedToken || providedToken !== authToken) {
|
|
411
|
+
sendJson(res, { error: "Unauthorized" }, 401);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
} else if (providedToken && allowOrigin) {
|
|
415
|
+
authToken = providedToken;
|
|
416
|
+
saveToken(providedToken);
|
|
417
|
+
} else {
|
|
418
|
+
sendJson(res, { error: "Unauthorized" }, 401);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (req.method === "GET" && url.pathname === "/status") {
|
|
422
|
+
sendJson(res, state.getStatus());
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
sendJson(res, { error: "Not found" }, 404);
|
|
426
|
+
});
|
|
427
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
428
|
+
wss.on("connection", (ws, req) => {
|
|
429
|
+
const wsUrl = new URL(req.url || "", `http://localhost:${port}`);
|
|
430
|
+
const providedToken = wsUrl.searchParams.get("token");
|
|
431
|
+
const origin = req.headers.origin;
|
|
432
|
+
const allowOrigin = isChromeExtensionOrigin(origin);
|
|
433
|
+
const clientIp = getClientIp(req);
|
|
434
|
+
const currentConnections = wsConnectionsByIp.get(clientIp) ?? 0;
|
|
435
|
+
if (currentConnections >= MAX_WS_CONNECTIONS_PER_IP) {
|
|
436
|
+
ws.close(1013, "Too many connections");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (authToken) {
|
|
440
|
+
if (!providedToken || providedToken !== authToken) {
|
|
441
|
+
ws.close(1008, "Unauthorized");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
} else if (providedToken && allowOrigin) {
|
|
445
|
+
authToken = providedToken;
|
|
446
|
+
saveToken(providedToken);
|
|
447
|
+
} else {
|
|
448
|
+
ws.close(1008, "Unauthorized");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
wsConnectionsByIp.set(clientIp, currentConnections + 1);
|
|
452
|
+
const unsubscribe = state.subscribe((message) => {
|
|
453
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
454
|
+
ws.send(JSON.stringify(message));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
ws.on("message", (data) => {
|
|
458
|
+
try {
|
|
459
|
+
const message = JSON.parse(data.toString());
|
|
460
|
+
if (message.type === "ping") {
|
|
461
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
ws.on("close", () => {
|
|
467
|
+
unsubscribe();
|
|
468
|
+
wsConnectionsByIp.set(
|
|
469
|
+
clientIp,
|
|
470
|
+
Math.max(0, (wsConnectionsByIp.get(clientIp) ?? 1) - 1)
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
ws.on("error", () => {
|
|
474
|
+
unsubscribe();
|
|
475
|
+
wsConnectionsByIp.set(
|
|
476
|
+
clientIp,
|
|
477
|
+
Math.max(0, (wsConnectionsByIp.get(clientIp) ?? 1) - 1)
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
const codexWatcher = new CodexSessionWatcher();
|
|
482
|
+
codexWatcher.start();
|
|
483
|
+
server.listen(port, "127.0.0.1", () => {
|
|
484
|
+
console.log(`
|
|
485
|
+
\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
|
+
\u2502 \u2502
|
|
487
|
+
\u2502 Codex Blocker Server \u2502
|
|
488
|
+
\u2502 \u2502
|
|
489
|
+
\u2502 HTTP: http://localhost:${port} \u2502
|
|
490
|
+
\u2502 WebSocket: ws://localhost:${port}/ws \u2502
|
|
491
|
+
\u2502 \u2502
|
|
492
|
+
\u2502 Watching Codex sessions... \u2502
|
|
493
|
+
\u2502 \u2502
|
|
494
|
+
\u2514\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\u2518
|
|
495
|
+
`);
|
|
496
|
+
});
|
|
497
|
+
process.once("SIGINT", () => {
|
|
498
|
+
console.log("\nShutting down...");
|
|
499
|
+
state.destroy();
|
|
500
|
+
codexWatcher.stop();
|
|
501
|
+
wss.close();
|
|
502
|
+
server.close();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export {
|
|
508
|
+
startServer
|
|
509
|
+
};
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-blocker",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Block distracting websites unless Codex is actively running inference. Forked from Theo Browne's (T3) Claude Blocker",
|
|
5
|
+
"author": "Adam Blumoff ",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/adamblumoff/codex-blocker.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/adamblumoff/codex-blocker",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/adamblumoff/codex-blocker/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"codex-blocker": "dist/bin.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/server.js",
|
|
19
|
+
"types": "./dist/server.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/bin.ts src/server.ts --format esm --dts --clean",
|
|
25
|
+
"dev": "tsx src/bin.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@claude-blocker/shared": "workspace:*",
|
|
30
|
+
"ws": "^8.18.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.10.2",
|
|
34
|
+
"@types/ws": "^8.5.13",
|
|
35
|
+
"tsup": "^8.3.5",
|
|
36
|
+
"tsx": "^4.19.2",
|
|
37
|
+
"typescript": "^5.7.2"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"codex",
|
|
41
|
+
"codex-cli",
|
|
42
|
+
"productivity",
|
|
43
|
+
"blocker",
|
|
44
|
+
"focus",
|
|
45
|
+
"distraction"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|