consensus-cli 0.1.0

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.
@@ -0,0 +1,332 @@
1
+ import fsp from "fs/promises";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { redactText } from "./redact.js";
5
+ const SESSION_WINDOW_MS = 30 * 60 * 1000;
6
+ const SESSION_SCAN_INTERVAL_MS = 5000;
7
+ const MAX_READ_BYTES = 512 * 1024;
8
+ const MAX_EVENTS = 50;
9
+ let cachedSessions = [];
10
+ let lastSessionScan = 0;
11
+ const tailStates = new Map();
12
+ export function resolveCodexHome(env = process.env) {
13
+ const override = env.CONSENSUS_CODEX_HOME || env.CODEX_HOME;
14
+ return override ? path.resolve(override) : path.join(os.homedir(), ".codex");
15
+ }
16
+ async function walk(dir, out, windowMs) {
17
+ let entries;
18
+ try {
19
+ entries = await fsp.readdir(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ const now = Date.now();
25
+ await Promise.all(entries.map(async (entry) => {
26
+ const fullPath = path.join(dir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ await walk(fullPath, out, windowMs);
29
+ return;
30
+ }
31
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
32
+ return;
33
+ try {
34
+ const stat = await fsp.stat(fullPath);
35
+ if (now - stat.mtimeMs <= windowMs) {
36
+ out.push({ path: fullPath, mtimeMs: stat.mtimeMs });
37
+ }
38
+ }
39
+ catch {
40
+ return;
41
+ }
42
+ }));
43
+ }
44
+ export async function listRecentSessions(codexHome, windowMs = SESSION_WINDOW_MS) {
45
+ const now = Date.now();
46
+ if (now - lastSessionScan < SESSION_SCAN_INTERVAL_MS) {
47
+ return cachedSessions.filter((item) => now - item.mtimeMs <= windowMs);
48
+ }
49
+ lastSessionScan = now;
50
+ const sessionsDir = path.join(codexHome, "sessions");
51
+ const results = [];
52
+ await walk(sessionsDir, results, windowMs);
53
+ results.sort((a, b) => b.mtimeMs - a.mtimeMs);
54
+ cachedSessions = results;
55
+ return results;
56
+ }
57
+ export function pickSessionForProcess(sessions, startTimeMs) {
58
+ if (sessions.length === 0)
59
+ return undefined;
60
+ if (!startTimeMs)
61
+ return sessions[0];
62
+ let best = sessions[0];
63
+ let bestDelta = Math.abs(best.mtimeMs - startTimeMs);
64
+ for (const session of sessions) {
65
+ const delta = Math.abs(session.mtimeMs - startTimeMs);
66
+ if (delta < bestDelta) {
67
+ best = session;
68
+ bestDelta = delta;
69
+ }
70
+ }
71
+ return best;
72
+ }
73
+ function getEventTimestamp(ev) {
74
+ const ts = ev.ts || ev.timestamp || ev.time || ev.created_at || ev.createdAt;
75
+ if (typeof ts === "number") {
76
+ if (ts < 100000000000) {
77
+ return ts * 1000;
78
+ }
79
+ return ts;
80
+ }
81
+ if (typeof ts === "string") {
82
+ const parsed = Date.parse(ts);
83
+ if (!Number.isNaN(parsed))
84
+ return parsed;
85
+ }
86
+ return Date.now();
87
+ }
88
+ function extractModel(ev) {
89
+ const model = ev.model || ev?.metadata?.model || ev?.data?.model || ev?.item?.model;
90
+ return typeof model === "string" ? model : undefined;
91
+ }
92
+ function extractText(value) {
93
+ if (typeof value === "string")
94
+ return value;
95
+ if (Array.isArray(value))
96
+ return value.map(extractText).filter(Boolean).join(" ");
97
+ if (value && typeof value === "object") {
98
+ if (typeof value.content === "string")
99
+ return value.content;
100
+ if (typeof value.text === "string")
101
+ return value.text;
102
+ if (typeof value.message === "string")
103
+ return value.message;
104
+ if (value.message && typeof value.message.content === "string")
105
+ return value.message.content;
106
+ }
107
+ return undefined;
108
+ }
109
+ function summarizeEvent(ev) {
110
+ const item = ev?.item || ev?.data?.item || ev?.delta?.item || ev?.message?.item;
111
+ const rawType = ev?.type || ev?.event?.type || "event";
112
+ const rawItemType = item?.type || item?.item_type || item?.itemType;
113
+ const type = typeof rawType === "string"
114
+ ? rawType
115
+ : typeof rawItemType === "string"
116
+ ? rawItemType
117
+ : "event";
118
+ const status = item?.status || ev?.status || ev?.error?.status;
119
+ const isError = !!ev?.error ||
120
+ type.includes("error") ||
121
+ status === "error" ||
122
+ status === "failed" ||
123
+ status === "failure";
124
+ const model = extractModel(ev);
125
+ const itemType = typeof rawItemType === "string" ? rawItemType : undefined;
126
+ const cmd = item?.command ||
127
+ item?.cmd ||
128
+ item?.input?.command ||
129
+ item?.input?.cmd ||
130
+ (Array.isArray(item?.args) ? item.args.join(" ") : undefined) ||
131
+ ev?.command ||
132
+ ev?.cmd;
133
+ if (typeof cmd === "string" && cmd.trim()) {
134
+ const summary = redactText(`cmd: ${cmd.trim()}`) || `cmd: ${cmd.trim()}`;
135
+ return { summary, kind: "command", isError, model, type };
136
+ }
137
+ const pathHint = item?.path || item?.file || item?.filename || item?.target || ev?.path;
138
+ const editTypes = new Set(["file_change", "file_edit", "file_write"]);
139
+ if (typeof pathHint === "string" &&
140
+ pathHint.trim() &&
141
+ (editTypes.has(itemType || "") || /file_change|file_edit|file_write/i.test(type))) {
142
+ const summary = redactText(`edit: ${pathHint.trim()}`) || `edit: ${pathHint.trim()}`;
143
+ return { summary, kind: "edit", isError, model, type };
144
+ }
145
+ const toolName = item?.tool_name ||
146
+ item?.tool?.name ||
147
+ item?.tool ||
148
+ ev?.tool_name ||
149
+ ev?.tool?.name ||
150
+ ev?.tool;
151
+ const toolTypes = new Set(["tool_call", "mcp_tool_call", "tool", "tool_execution"]);
152
+ if (typeof toolName === "string" &&
153
+ toolName.trim() &&
154
+ (toolTypes.has(itemType || "") || /tool/i.test(type))) {
155
+ const summary = redactText(`tool: ${toolName.trim()}`) || `tool: ${toolName.trim()}`;
156
+ return { summary, kind: "tool", isError, model, type };
157
+ }
158
+ const promptText = extractText(item?.input) ||
159
+ extractText(item?.prompt) ||
160
+ extractText(item?.instruction) ||
161
+ extractText(ev?.input) ||
162
+ extractText(ev?.prompt) ||
163
+ extractText(ev?.instruction) ||
164
+ extractText(ev?.data?.input) ||
165
+ extractText(ev?.data?.prompt);
166
+ if (promptText &&
167
+ (itemType === "prompt" || /thread\.started|turn\.started|prompt/i.test(String(type)))) {
168
+ const trimmed = promptText.replace(/\s+/g, " ").trim();
169
+ if (trimmed) {
170
+ const snippet = trimmed.slice(0, 120);
171
+ const summary = redactText(`prompt: ${snippet}`) || `prompt: ${snippet}`;
172
+ return { summary, kind: "prompt", isError, model, type };
173
+ }
174
+ }
175
+ const messageText = extractText(item?.content) ||
176
+ extractText(item?.message) ||
177
+ extractText(ev?.message) ||
178
+ extractText(item?.text) ||
179
+ extractText(ev?.text);
180
+ if (messageText && itemType !== "reasoning") {
181
+ const trimmed = messageText.replace(/\s+/g, " ").trim();
182
+ if (trimmed) {
183
+ const snippet = trimmed.slice(0, 80);
184
+ const summary = redactText(snippet) || snippet;
185
+ return { summary, kind: "message", isError, model, type };
186
+ }
187
+ }
188
+ const fallbackBits = [
189
+ typeof type === "string" && type !== "event" ? type : undefined,
190
+ itemType,
191
+ ].filter(Boolean);
192
+ if (fallbackBits.length) {
193
+ const summary = redactText(`event: ${fallbackBits.join(" ")}`) || `event: ${fallbackBits.join(" ")}`;
194
+ return { summary, kind: "other", isError, model, type };
195
+ }
196
+ return { kind: "other", isError, model, type };
197
+ }
198
+ export async function updateTail(sessionPath) {
199
+ let stat;
200
+ try {
201
+ stat = await fsp.stat(sessionPath);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ const prev = tailStates.get(sessionPath);
207
+ const state = prev ||
208
+ {
209
+ path: sessionPath,
210
+ offset: 0,
211
+ partial: "",
212
+ events: [],
213
+ };
214
+ if (stat.size < state.offset) {
215
+ state.offset = 0;
216
+ state.partial = "";
217
+ state.events = [];
218
+ state.lastEventAt = undefined;
219
+ state.lastCommand = undefined;
220
+ state.lastEdit = undefined;
221
+ state.lastMessage = undefined;
222
+ state.lastTool = undefined;
223
+ state.lastError = undefined;
224
+ state.model = undefined;
225
+ }
226
+ const prevOffset = state.offset;
227
+ let readStart = prevOffset;
228
+ let trimmed = false;
229
+ const delta = stat.size - prevOffset;
230
+ if (delta > MAX_READ_BYTES) {
231
+ readStart = Math.max(0, stat.size - MAX_READ_BYTES);
232
+ trimmed = true;
233
+ state.partial = "";
234
+ }
235
+ if (stat.size <= readStart) {
236
+ tailStates.set(sessionPath, state);
237
+ return state;
238
+ }
239
+ const readLength = stat.size - readStart;
240
+ const buffer = Buffer.alloc(readLength);
241
+ try {
242
+ const handle = await fsp.open(sessionPath, "r");
243
+ await handle.read(buffer, 0, readLength, readStart);
244
+ await handle.close();
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ let text = buffer.toString("utf8");
250
+ if (trimmed) {
251
+ const firstNewline = text.indexOf("\n");
252
+ if (firstNewline !== -1) {
253
+ text = text.slice(firstNewline + 1);
254
+ }
255
+ }
256
+ const combined = state.partial + text;
257
+ const lines = combined.split(/\r?\n/);
258
+ state.partial = lines.pop() || "";
259
+ for (const line of lines) {
260
+ if (!line.trim())
261
+ continue;
262
+ let ev;
263
+ try {
264
+ ev = JSON.parse(line);
265
+ }
266
+ catch {
267
+ continue;
268
+ }
269
+ const ts = getEventTimestamp(ev);
270
+ const { summary, kind, isError, model, type } = summarizeEvent(ev);
271
+ if (model)
272
+ state.model = model;
273
+ if (summary) {
274
+ const entry = {
275
+ ts,
276
+ type: typeof type === "string" ? type : "event",
277
+ summary,
278
+ isError,
279
+ };
280
+ state.events.push(entry);
281
+ state.lastEventAt = Math.max(state.lastEventAt || 0, ts);
282
+ if (kind === "command")
283
+ state.lastCommand = entry;
284
+ if (kind === "edit")
285
+ state.lastEdit = entry;
286
+ if (kind === "message")
287
+ state.lastMessage = entry;
288
+ if (kind === "tool")
289
+ state.lastTool = entry;
290
+ if (kind === "prompt")
291
+ state.lastPrompt = entry;
292
+ if (isError)
293
+ state.lastError = entry;
294
+ if (state.events.length > MAX_EVENTS) {
295
+ state.events = state.events.slice(-MAX_EVENTS);
296
+ }
297
+ }
298
+ else {
299
+ state.lastEventAt = Math.max(state.lastEventAt || 0, ts);
300
+ if (isError) {
301
+ state.lastError = {
302
+ ts,
303
+ type: typeof type === "string" ? type : "event",
304
+ summary: "error",
305
+ isError,
306
+ };
307
+ }
308
+ }
309
+ }
310
+ state.offset = stat.size;
311
+ tailStates.set(sessionPath, state);
312
+ return state;
313
+ }
314
+ export function summarizeTail(state) {
315
+ const title = state.lastPrompt?.summary;
316
+ const doing = state.lastCommand?.summary ||
317
+ state.lastEdit?.summary ||
318
+ state.lastMessage?.summary ||
319
+ state.events[state.events.length - 1]?.summary;
320
+ const events = state.events.slice(-20);
321
+ const hasError = !!state.lastError || events.some((event) => event.isError);
322
+ const lastEventAt = state.lastEventAt || events[events.length - 1]?.ts;
323
+ const summary = {
324
+ current: doing,
325
+ lastCommand: state.lastCommand?.summary,
326
+ lastEdit: state.lastEdit?.summary,
327
+ lastMessage: state.lastMessage?.summary,
328
+ lastTool: state.lastTool?.summary,
329
+ lastPrompt: state.lastPrompt?.summary,
330
+ };
331
+ return { doing, title, events, model: state.model, hasError, summary, lastEventAt };
332
+ }
package/dist/redact.js ADDED
@@ -0,0 +1,20 @@
1
+ const REDACT_ENABLED = process.env.CONSENSUS_REDACT_PII !== "0";
2
+ const HOME_PATTERNS = [
3
+ /\/Users\/[^/\s]+/g,
4
+ /\/home\/[^/\s]+/g,
5
+ /C:\\Users\\[^\\\s]+/gi,
6
+ ];
7
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
8
+ export function redactText(value) {
9
+ if (!value || !REDACT_ENABLED)
10
+ return value;
11
+ let output = value;
12
+ for (const pattern of HOME_PATTERNS) {
13
+ output = output.replace(pattern, "~");
14
+ }
15
+ output = output.replace(EMAIL_PATTERN, "<redacted-email>");
16
+ return output;
17
+ }
18
+ export function isRedactionEnabled() {
19
+ return REDACT_ENABLED;
20
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,259 @@
1
+ import psList from "ps-list";
2
+ import pidusage from "pidusage";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { execFile } from "child_process";
6
+ import { promisify } from "util";
7
+ import { deriveState } from "./activity.js";
8
+ import { listRecentSessions, pickSessionForProcess, resolveCodexHome, summarizeTail, updateTail, } from "./codexLogs.js";
9
+ import { redactText } from "./redact.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const repoCache = new Map();
12
+ function isCodexProcess(cmd, name, matchRe) {
13
+ if (!cmd && !name)
14
+ return false;
15
+ if (matchRe) {
16
+ return matchRe.test(cmd || "") || matchRe.test(name || "");
17
+ }
18
+ const cmdLine = cmd || "";
19
+ if (name === "codex")
20
+ return true;
21
+ if (cmdLine === "codex" || cmdLine.startsWith("codex "))
22
+ return true;
23
+ if (cmdLine.includes("/codex") || cmdLine.includes(" codex "))
24
+ return true;
25
+ return false;
26
+ }
27
+ function inferKind(cmd) {
28
+ if (cmd.includes(" app-server"))
29
+ return "app-server";
30
+ if (cmd.includes(" exec"))
31
+ return "exec";
32
+ if (cmd.includes(" codex") || cmd.startsWith("codex"))
33
+ return "tui";
34
+ return "unknown";
35
+ }
36
+ function shortenCmd(cmd, max = 120) {
37
+ const clean = cmd.replace(/\s+/g, " ").trim();
38
+ if (clean.length <= max)
39
+ return clean;
40
+ return `${clean.slice(0, max - 3)}...`;
41
+ }
42
+ function parseDoingFromCmd(cmd) {
43
+ const parts = cmd.split(/\s+/g);
44
+ const execIndex = parts.indexOf("exec");
45
+ if (execIndex !== -1) {
46
+ for (let i = execIndex + 1; i < parts.length; i += 1) {
47
+ const part = parts[i];
48
+ if (part === "--") {
49
+ const next = parts[i + 1];
50
+ return next ? `exec: ${next}` : "exec";
51
+ }
52
+ if (!part.startsWith("-")) {
53
+ return `exec: ${part}`;
54
+ }
55
+ }
56
+ return "exec";
57
+ }
58
+ const resumeIndex = parts.indexOf("resume");
59
+ if (resumeIndex !== -1) {
60
+ const token = parts[resumeIndex + 1];
61
+ return token ? `resume: ${token}` : "resume";
62
+ }
63
+ const monitorIndex = parts.indexOf("monitor");
64
+ if (monitorIndex !== -1) {
65
+ return "monitor";
66
+ }
67
+ if (cmd.includes("app-server"))
68
+ return "app-server";
69
+ if (cmd.startsWith("codex"))
70
+ return "codex";
71
+ return undefined;
72
+ }
73
+ function normalizeTitle(value) {
74
+ if (!value)
75
+ return undefined;
76
+ return value.replace(/^prompt:\s*/i, "").trim();
77
+ }
78
+ function deriveTitle(doing, repo, pid) {
79
+ if (doing) {
80
+ const trimmed = doing.trim();
81
+ if (trimmed.startsWith("cmd:"))
82
+ return `Run ${trimmed.slice(4).trim()}`;
83
+ if (trimmed.startsWith("edit:"))
84
+ return `Editing ${trimmed.slice(5).trim()}`;
85
+ if (trimmed.startsWith("tool:"))
86
+ return `Tool ${trimmed.slice(5).trim()}`;
87
+ if (trimmed.startsWith("exec:"))
88
+ return `Exec ${trimmed.slice(5).trim()}`;
89
+ if (trimmed.startsWith("resume:"))
90
+ return `Resume ${trimmed.slice(7).trim()}`;
91
+ }
92
+ if (repo)
93
+ return repo;
94
+ return `codex#${pid}`;
95
+ }
96
+ function sanitizeSummary(summary) {
97
+ if (!summary)
98
+ return undefined;
99
+ const cleaned = {};
100
+ for (const [key, value] of Object.entries(summary)) {
101
+ if (typeof value !== "string")
102
+ continue;
103
+ cleaned[key] = redactText(value) || value;
104
+ }
105
+ return cleaned;
106
+ }
107
+ async function getCwdsForPids(pids) {
108
+ const result = new Map();
109
+ if (pids.length === 0)
110
+ return result;
111
+ if (process.platform === "win32")
112
+ return result;
113
+ try {
114
+ const { stdout } = await execFileAsync("ps", [
115
+ "-o",
116
+ "pid=,cwd=",
117
+ "-p",
118
+ pids.join(","),
119
+ ]);
120
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
121
+ for (const line of lines) {
122
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
123
+ if (!match)
124
+ continue;
125
+ const pid = Number(match[1]);
126
+ const cwd = match[2].trim();
127
+ if (cwd)
128
+ result.set(pid, cwd);
129
+ }
130
+ }
131
+ catch {
132
+ return result;
133
+ }
134
+ return result;
135
+ }
136
+ function findRepoRoot(cwd) {
137
+ if (repoCache.has(cwd))
138
+ return repoCache.get(cwd) || null;
139
+ let current = cwd;
140
+ while (true) {
141
+ const gitPath = path.join(current, ".git");
142
+ if (fs.existsSync(gitPath)) {
143
+ repoCache.set(cwd, current);
144
+ return current;
145
+ }
146
+ const parent = path.dirname(current);
147
+ if (parent === current)
148
+ break;
149
+ current = parent;
150
+ }
151
+ repoCache.set(cwd, null);
152
+ return null;
153
+ }
154
+ export async function scanCodexProcesses() {
155
+ const matchEnv = process.env.CONSENSUS_PROCESS_MATCH;
156
+ let matchRe;
157
+ if (matchEnv) {
158
+ try {
159
+ matchRe = new RegExp(matchEnv);
160
+ }
161
+ catch {
162
+ matchRe = undefined;
163
+ }
164
+ }
165
+ const processes = await psList();
166
+ const codexProcs = processes.filter((proc) => isCodexProcess(proc.cmd, proc.name, matchRe));
167
+ const pids = codexProcs.map((proc) => proc.pid);
168
+ let usage = {};
169
+ try {
170
+ usage = (await pidusage(pids));
171
+ }
172
+ catch {
173
+ usage = {};
174
+ }
175
+ const cwds = await getCwdsForPids(pids);
176
+ const codexHome = resolveCodexHome();
177
+ const sessions = await listRecentSessions(codexHome);
178
+ const agents = [];
179
+ for (const proc of codexProcs) {
180
+ const stats = usage[proc.pid] || {};
181
+ const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
182
+ const mem = typeof stats.memory === "number" ? stats.memory : 0;
183
+ const elapsed = stats.elapsed;
184
+ const startMs = typeof elapsed === "number" ? Date.now() - elapsed : undefined;
185
+ const session = pickSessionForProcess(sessions, startMs);
186
+ let doing;
187
+ let events;
188
+ let model;
189
+ let hasError = false;
190
+ let title;
191
+ let summary;
192
+ let lastEventAt;
193
+ if (session) {
194
+ const tail = await updateTail(session.path);
195
+ if (tail) {
196
+ const tailSummary = summarizeTail(tail);
197
+ doing = tailSummary.doing;
198
+ events = tailSummary.events;
199
+ model = tailSummary.model;
200
+ hasError = tailSummary.hasError;
201
+ title = normalizeTitle(tailSummary.title);
202
+ summary = tailSummary.summary;
203
+ lastEventAt = tailSummary.lastEventAt;
204
+ }
205
+ }
206
+ if (!doing) {
207
+ doing =
208
+ parseDoingFromCmd(proc.cmd || "") || shortenCmd(proc.cmd || proc.name || "");
209
+ }
210
+ if (!summary && doing) {
211
+ summary = { current: doing };
212
+ }
213
+ const cwdRaw = cwds.get(proc.pid);
214
+ const cwd = redactText(cwdRaw) || cwdRaw;
215
+ const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
216
+ const repoName = repoRoot ? path.basename(repoRoot) : undefined;
217
+ const state = deriveState({ cpu, hasError, lastEventAt });
218
+ const cmdRaw = proc.cmd || proc.name || "";
219
+ const cmd = redactText(cmdRaw) || cmdRaw;
220
+ const cmdShort = shortenCmd(cmd);
221
+ const kind = inferKind(cmd);
222
+ const id = `${proc.pid}`;
223
+ const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
224
+ const computedTitle = title || deriveTitle(doing, repoName, proc.pid);
225
+ const safeSummary = sanitizeSummary(summary);
226
+ agents.push({
227
+ id,
228
+ pid: proc.pid,
229
+ startedAt,
230
+ lastEventAt,
231
+ title: redactText(computedTitle) || computedTitle,
232
+ cmd,
233
+ cmdShort,
234
+ kind,
235
+ cpu,
236
+ mem,
237
+ state,
238
+ doing: redactText(doing) || doing,
239
+ sessionPath: redactText(session?.path) || session?.path,
240
+ repo: repoName,
241
+ cwd,
242
+ model,
243
+ summary: safeSummary,
244
+ events,
245
+ });
246
+ }
247
+ return { ts: Date.now(), agents };
248
+ }
249
+ const isDirectRun = process.argv[1] && process.argv[1].endsWith("scan.js");
250
+ if (isDirectRun) {
251
+ scanCodexProcesses()
252
+ .then((snapshot) => {
253
+ process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
254
+ })
255
+ .catch((err) => {
256
+ console.error(err);
257
+ process.exit(1);
258
+ });
259
+ }
package/dist/server.js ADDED
@@ -0,0 +1,61 @@
1
+ import express from "express";
2
+ import http from "http";
3
+ import path from "path";
4
+ import { WebSocketServer, WebSocket } from "ws";
5
+ import { fileURLToPath } from "url";
6
+ import { scanCodexProcesses } from "./scan.js";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const app = express();
10
+ const server = http.createServer(app);
11
+ const wss = new WebSocketServer({ server });
12
+ const port = Number(process.env.CONSENSUS_PORT || 8787);
13
+ const host = process.env.CONSENSUS_HOST || "127.0.0.1";
14
+ const pollMs = Math.max(250, Number(process.env.CONSENSUS_POLL_MS || 1000));
15
+ const publicDir = path.join(__dirname, "..", "public");
16
+ app.use(express.static(publicDir));
17
+ app.get("/api/snapshot", async (_req, res) => {
18
+ try {
19
+ const snapshot = await scanCodexProcesses();
20
+ res.json(snapshot);
21
+ }
22
+ catch (err) {
23
+ res.status(500).json({ error: "scan_failed" });
24
+ }
25
+ });
26
+ app.get("/health", (_req, res) => {
27
+ res.json({ ok: true });
28
+ });
29
+ let lastSnapshot = { ts: Date.now(), agents: [] };
30
+ let scanning = false;
31
+ async function tick() {
32
+ if (scanning)
33
+ return;
34
+ scanning = true;
35
+ try {
36
+ lastSnapshot = await scanCodexProcesses();
37
+ const payload = JSON.stringify(lastSnapshot);
38
+ for (const client of wss.clients) {
39
+ if (client.readyState === WebSocket.OPEN) {
40
+ client.send(payload);
41
+ }
42
+ }
43
+ }
44
+ catch (err) {
45
+ // Keep server alive on scan errors.
46
+ }
47
+ finally {
48
+ scanning = false;
49
+ }
50
+ }
51
+ wss.on("connection", (socket) => {
52
+ socket.send(JSON.stringify(lastSnapshot));
53
+ });
54
+ setInterval(tick, pollMs);
55
+ server.listen(port, host, () => {
56
+ const url = `http://${host}:${port}`;
57
+ process.stdout.write(`consensus dev server running on ${url}\n`);
58
+ });
59
+ tick().catch(() => {
60
+ // initial scan failure is non-fatal
61
+ });
package/dist/tail.js ADDED
@@ -0,0 +1,27 @@
1
+ import { summarizeTail, updateTail } from "./codexLogs.js";
2
+ const target = process.argv[2];
3
+ if (!target) {
4
+ console.error("usage: npm run tail -- <session.jsonl>");
5
+ process.exit(1);
6
+ }
7
+ let running = false;
8
+ async function tick() {
9
+ if (running)
10
+ return;
11
+ running = true;
12
+ try {
13
+ const state = await updateTail(target);
14
+ if (state) {
15
+ const summary = summarizeTail(state);
16
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
17
+ }
18
+ }
19
+ finally {
20
+ running = false;
21
+ }
22
+ }
23
+ setInterval(tick, 1000);
24
+ tick().catch((err) => {
25
+ console.error(err);
26
+ process.exit(1);
27
+ });
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};